WEB-428 · Задача · — · web
P0 ⭐ КОРЕНЬ: код просит модели вне MODEL_ALLOWLIST — мертвы AI-правка документа (500 пустой) и Совет
Закрыт
P2
ведёт: —
Суть
[01.09 22:20Z] accweb428 ACCEPTED (859904eb): AI-правка документа переведена на MODEL_ALLOWLIST, baseline-исключения задокументированы. Кандидат l86.
Доказательства
Найдено координатором по логам прода l67 (29.08 01:53-01:55Z) после пробы L67PROBE.
ФАКТЫ: [billing][billing_guard_model_not_allowed] channel=editor_completion model=gpt-4o-mini; channel=council_stage1 model=anthropic/claude-3-haiku; VendorCallRejectedError «This model is not enabled in the current release».
MODEL_ALLOWLIST на проде: gpt-5.4-mini, openai/gpt-5.4-mini, gpt-5.4-nano, gpt-5.5-nano, claude-haiku-4.5, gemini-2.5-flash, gemini-2.5-flash-lite, text-embedding-3-small, perplexity/sonar, gpt-4o-mini-transcribe, gpt-4o-mini-tts, gpt-4o.
Дополнительно: [adminDefaults] RuntimeConfig row is absent → используются дефолты ИЗ КОДА (namespace role-prompts, global-chat, api-assistants, voice, podcast, translation) — там и лежат устаревшие имена.
СЛЕДСТВИЕ для пользователя: AI-правка текста → 500 с пустым телом, текст не меняется, ошибка не показана; Совет → generic «Sorry, I encountered an error…» без кода, запись отказа не переживает перезагрузку.
ВАЖНО: это ОБЪЯСНЯЕТ, почему Совет не работал и после починки identity (ACC108A GO) — там чинилась личность, а режет модель. Денежный страж при этом работает ПРАВИЛЬНО.
Волна WEB428 заряжена (A1 queue/381, база l67 51d5f075): инвентарь всех мест задания модели, приведение к allowlist (allowlist НЕ расширять — денежное решение владельца), единый реестр «канал → модель» + тест-страж «каждая модель реестра есть в allowlist», наблюдаемый отказ вместо 500-пустышки, запись отказа Совета переживает перезагрузку.
---
## 2026-08-29 09:30Z — работа сдана, но волна НЕ закоммитила — сохранено координатором
WEB428 отчиталась, указав в маркере базовый sha (51d5f075) вместо своего — то есть коммита не сделала. В дереве лежали 98 файлов правок (+1509/−777). Закоммичено координатором: c1afb219.
Приёмка ACC428 заряжена (queue/394) с особыми требованиями: (1) проверить, что MODEL_ALLOWLIST НЕ расширен (расширение = NO-GO, это денежное решение владельца); (2) собрать СВОЮ таблицу «канал → модель → в allowlist?» грепом; (3) проверить, что страж-тест реально падает на недопустимой модели; (4) убедиться, что 500-пустышка на completion больше невозможна и у Совета есть код причины + durable запись; (5) объяснить изменения monet-w34-*.json и monet-w35-activation-attestation.json — это монетарные артефакты сборки, ручная правка подозрительна.
---
## 2026-08-29 10:00Z — ACC428: NO-GO по одной модели; подозрение по монетарным файлам СНЯТО
Принято приёмкой: каналы приведены к allowlist, allowlist НЕ расширен, страж есть, отказ стал наблюдаемым. Изменения monet-w34-activation-manifest.json / monet-w34-entrypoint-coverage.json / monet-w35-activation-attestation.json — generated output скрипта monet-w34-generate-activation-evidence.mjs (перезаписывает их), ручной правки денежной модели НЕТ. Подозрение из моего брифа снято фактами.
БЛОКЕР: src/lib/videoContext/story.ts:105 жёстко выбирает gemini-3.5-flash-lite для Google narrative path; путь реально передаёт модель в GoogleProvider.chat() (:1789-1790). Модели нет ни в allowlist, ни в реестре каналов → шлюз заблокирует.
⚠️ Важный вывод: страж не поймал, потому что проверяет РЕЕСТР, а этот вызов шёл мимо реестра. Круг WEB428B (queue/397): заменить на gemini-2.5-flash-lite, внести канал в реестр И УСИЛИТЬ страж — ловить жёстко зашитые имена моделей в коде вне реестра (греп по gpt-*, gemini-*, claude-*, anthropic/*), тест обязан падать на текущем коммите.
---
## 2026-08-29 11:35Z — ✅ ACC428B: GO. Корень мёртвых платных путей закрыт
Принят коммит 60caa025: видео-нарратив переведён на разрешённую модель, канал внесён в реестр, allowlist НЕ расширен, страж усилен — теперь ловит ЖЁСТКО ЗАШИТЫЕ имена моделей в коде вне реестра (прошлый смотрел только реестр и пропустил story.ts:105).
Значение: это корень, из-за которого на проде l67 были мертвы AI-правка документа (500 с пустым телом) и Совет (generic-ошибка без кода). Теперь: модели каналов в allowlist, отказ гейта наблюдаем.
СЛЕДУЮЩИЙ ШАГ: везти в l68 и ПОСЛЕ ПОСАДКИ проверить живьём под QA-учёткой — правку текста в документе (ступень E работой) и прогон Совета. Только это закроет цели owner-а «движок E» и «роли».
---
## 2026-08-29 09:40Z — ЭВОЛЮЦИЯ: корень мёртвых платных путей (для нулевого агента)
Как нашли: проба L67PROBE на живом проде (M4, QA-учётка) показала — ступень E видна, но правка текста даёт 500 с пустым телом, Совет отвечает generic-ошибкой. Координатор снял логи прода: [billing][billing_guard_model_not_allowed] channel=editor_completion model=gpt-4o-mini; channel=council_stage1 model=anthropic/claude-3-haiku; VendorCallRejectedError «This model is not enabled in the current release». Плюс [adminDefaults] RuntimeConfig row is absent → берутся дефолты ИЗ КОДА со старыми именами моделей.
1. WEB428 (c1afb219, 98 файлов) → ACC428 NO-GO: подозрение на ручную правку монетарных артефактов СНЯТО (это generated output monet-w34-generate-activation-evidence.mjs); блокер — src/lib/videoContext/story.ts:105 жёстко брал gemini-3.5-flash-lite мимо реестра; страж не поймал, т.к. проверял только реестр.
2. WEB428B (60caa025) → ⭐ACC428B GO: модель заменена на разрешённую, канал внесён в реестр, allowlist НЕ расширен, страж усилен — ловит ЖЁСТКО ЗАШИТЫЕ имена моделей вне реестра.
Значение: это корень, из-за которого не работали ступень E работой и Совет. Едет в l68; после посадки — живая проверка обоих под QA-учёткой.
[29.08 08:1xZ ЖИВАЯ ПРОВЕРКА ПРОДА l68 (18af977a), обычная не-админская учётка qa-evisual, не тесты]
ЧТО ПОЧИНЕНО (подтверждено): старый 500 / model_not_allowed НЕ воспроизводится. Ступень E присутствует в сборке (группа AI TOOLS), Polish реально дергает движок ([adminDefaults] namespace:"api-assistants").
НОВЫЙ КОРЕНЬ №1 — Совет мёртв 100%: ReferenceError: channelModel is not defined, surface chat.notebook.main. Виновник — коммит c1afb219 (WEB428): заменил литералы моделей на channelModel() в src/lib/council/orchestrator.ts:349 и :371, импорт не добавил. Единственный такой файл в репо. Ни next build, ни юнит-тесты, ни приёмка ACC428 не поймали.
ПОЧИНКА: коммит d707ad6f (дерево wt-l69council, база 18af977a) — восстановлен импорт. Заряжена волна WEB428DGUARD: страж класса «использование без импорта» + негативный контроль (падает на 18af977a, проходит на d707ad6f) + прочёс всего репо.
НОВЫЙ КОРЕНЬ №2 — Polish врёт «Done!»: движок правку выдаёт (engineApplied:1), теневое сравнение видит расхождение контента (divergenceKind:"content", cleanMatchCount:0, contentHashLegacy != contentHashShadow), сторож откатывается на legacy (legacyFallbacks:1) и гасит сессию записи (sessionDisables:1) — а пользователю показан успех. Заряжена волна POLISHDIV (корень расхождения + ложный успех = правило 10 + невосстановление сессии).
ЧТО РАБОТАЕТ (доказано руками): поиск по СОДЕРЖИМОМУ + негативный контроль (zzqqxnotpresent -> 0); разговорный инвентарь документов (4 имени точно); чтение документа и точная цитата — но ТОЛЬКО при Council: Off. Персистентность записи отказа Совета через перезагрузку и перелогин.
ЗАГРЯЗНЕНИЕ ОТВЕТОВ: рядом с найденной цитатой вставляется противоречащее "Nothing on this topic was found in the documents"; в инвентаре — ложный дисклеймер "not confirmed by your documents".
НЕ ПРОВЕРЕНО ЧЕСТНО: Verify (второй E-инструмент), режимы Fast/Lite/Thinking/Deep, PDF/веб-источники, списание денег за упавший прогон Совета, buildEnabled-маркер в консоли (гейт E подтверждён поведением, не флагом).
[29.08 СТРАЖ КЛАССА ГОТОВ + ТРИ НОВЫЕ НАХОДКИ ТОГО ЖЕ ВИДА — коммит 073e70b9]
Волна WEB428DGUARD сделала дешёвый страж необъявленных идентификаторов: pnpm check:undeclared-identifiers -> scripts/ci/no-undef-src.mjs. Выбран не полный tsc (он запрещён — валит бокс по памяти): 6054 файла за 1:34, пик ~1.17 ГиБ.
Негативный контроль: на 18af977a страж FAIL с 5 ссылками, на d707ad6f PASS с 0. То есть страж краснеет на состоянии, где Совет был мёртв.
КРОМЕ Совета найдено ТРИ РЕАЛЬНЫХ дефекта того же класса в рабочем коде:
1) src/app/api/email/webhook/route.ts — отсутствовал импорт describeIngestSkip;
2) src/components/media/MediaImport.tsx — использование переменной до её объявления (temporal dead zone);
3) src/lib/notes/createNote.ts — `export { AGENT_TASK_NOTE_COLOR } from '...'` не создаёт локальную привязку, а код использовал имя локально; заменено на import + отдельный export.
Волна свою работу НЕ закоммитила — координатор закоммитил из её дерева: 073e70b9 (страж + три починки + тесты + package.json). Кандидат в линию l69 поверх d707ad6f.
ПОЧЕМУ ACC428 НЕ ПОЙМАЛА: она проверяла транспиляцию/next build --webpack, allowlist и юнит-тесты — ни одно из этого не исполняет строку с необъявленным идентификатором.
[29.08 СОВЕТ: решение конфликта с ограничителем — коммит b67811667683426629133aea928fd7d42a100417, приёмка ACCCOUNCILCONC заряжена]
Проблема после посадки l69: импорт восстановлен, конвейер доходит до вендоров, но все три рыцаря отбиваются гейтом одновременности — [billing][billing_guard_concurrency_blocked] slots=1 limit=1 scope=user channel=council_stage1. Совет шлёт трёх рыцарей параллельно, гейт разрешает ОДИН вызов на пользователя.
Откуда лимит: MAX_CONCURRENT_LLM_PER_USER -> src/lib/ai/concurrencyGate.ts:44-56 (возвращает scope 'user' при perUserActive >= limit) -> src/lib/billing/meteredVendorCall.ts:564-581 (вызывает гейт до резервирования и транспорта, пишет slots/limit/scope в диагностику, отказывает с concurrency_limit).
ВЫБРАНО РЕШЕНИЕ: последовательная обработка внутри одного прогона — следующий рыцарь/reviewer/chair/judge стартует после завершения предыдущего. Совместимо с MAX_CONCURRENT_LLM_PER_USER=1, ПРОД-ОКРУЖЕНИЕ МЕНЯТЬ НЕ НУЖНО. Число этапов и повторов не изменилось, меняется только время ответа. Денежные ограничения не трогались (runtime: SPEND_CAP_GLOBAL_DAILY_USD=25, SPEND_CAP_USER_DAILY_USD=2, MAX_SINGLE_REQUEST_RESERVATION_USD=0.10, MAX_OPERATION_RESERVED_USD=0.20).
Почему не иначе: отдельная квота Совета при user-limit 1 конфликт не решает (общий гейт всё равно пропустит один), а обход общего гейта нарушил бы единую границу безопасности; поднятие лимита до 3 требует согласованного роста глобального лимита и явного бюджета на всплеск — цена в пиковой экспозиции названа, но в этом коммите не выбрано.
Плюс: google-путь отражён в манифесте активации, отказ concurrency_limit объясняется пользователю на двух языках.
[BOARDTRIAGE] 29.08: d707ad6f/undef guard исправлен, затем b678116676 перевёл Council в последовательные вызовы при limit=1. ACCCOUNCILCONC ещё должна подтвердить три этапа, diagnostics и сохранение денежных ограничений.
Лента
2026-09-02T00:33:27.011Z · Fable[02.09 00:40Z Фабл] Повтор потерянной линии: web428redo сдан (39dd965e8, 14 файлов): общий fail-closed resolver modelAllowlist.ts; admin/env вне allowlist → release default, явный user-выбор → 403 model_not_allowed с непустым JSON; nudge/mirror/shower/editorchat/completion/orb-agent проверяют модель до provider client; allowlist не расширен. Приёмка 1100-accweb428redo поставлена.
2026-09-02T00:47:13.138Z · Fable[02.09 01:00Z Фабл] Приёмка 1100-accweb428redo: PASS (39dd965e8) — независимый diff, fail-closed resolver, мок-провайдерные пути правки документа, сьюты, scoped tsc, eslint. Cherry-pick в l86-candidate (A2). Сядет с l86 → тогда done. Отчёт: A1 /home/ubuntu/waves/ACCWEB428REDO-REPORT.md.
2026-09-02T02:19:32.641Z · Fable[02.09 02:25Z Фабл] 🛬 ПОСАЖЕН В l86 (A1, commit 3d23ac475f30a19719c42aefea4b656a65eceffc, артефакт me2-standalone-linux-arm64-3d23ac475-20260902T021427Z sha 0885c670…, аттестация owner-ключом fable-a2-l86, сухой прогон :3011 paidReady TRUE, флип nc-a1 + nc-a1-indexing, paid 200 публично). Журнал посадки — WEB-320. fail-closed резолвер моделей для AI-правки документа (web428redo 39dd965e8, PASS). Закрываю.
Воркер
не привязан — привязать:
curl -X POST https://bugs.wool2.online/api/web/assign -H 'content-type: application/json' \
-d '{"issueId":"WEB-428","session":"<имя tmux-сессии>","host":"m4"}'
Обновлён
2026-09-02T02:19:32.661Z