Open
agent_coder
wants to merge 16 commits from
fix/486-iter1-prod-fires into develop
pull from: fix/486-iter1-prod-fires
merge into: vvzvlad:develop
vvzvlad:main
vvzvlad:feat/489-tool-resilience
vvzvlad:feat/487-terminal-lifecycle
vvzvlad:develop
vvzvlad:feat/481-version-coherence
vvzvlad:refactor/347-client-md-paste
vvzvlad:perf/479-getpage-cache
vvzvlad:refactor/449-write-invariants
vvzvlad:refactor/450-split-client
vvzvlad:ci/476-publish-gates
vvzvlad:feat/443-get-page-context
vvzvlad:feat/443-get-tree
vvzvlad:feat/443-search
vvzvlad:docs/415-lossy-descriptions
vvzvlad:feat/413-markdown-default
vvzvlad:refactor/412-camelcase-tool-names
vvzvlad:feat/425-drawio-graph
vvzvlad:fix/464-diffdocs-cpu-guard
vvzvlad:refactor/411-update-page-markdown
vvzvlad:feat/424-drawio-rules
vvzvlad:fix/409-invalid-node-validation
vvzvlad:refactor/448-generate-inventory
vvzvlad:refactor/445-execute-mapping
vvzvlad:refactor/446-derive-client-types
vvzvlad:fix/447-registry-stamp-ci
vvzvlad:fix/444-agent-loop-guards
vvzvlad:fix/396-sendnow-detached-run
vvzvlad:fix/452-immutable-cache-control
vvzvlad:docs/footnote-authoring-comment-cleanup
vvzvlad:feat/437-error-diagnostics
vvzvlad:fix/442-cursor-pagination
vvzvlad:feat/419-footnote-normalize
vvzvlad:perf/399-comment-resolve-async
vvzvlad:perf/435-collab-token-cache
vvzvlad:perf/344-background-rerenders
vvzvlad:perf/348-backend-lowhanging
vvzvlad:feat/423-drawio-crud
vvzvlad:perf/401-collab-hotpath
vvzvlad:feat/430-live-reconnect
vvzvlad:perf/400-collab-session
vvzvlad:refactor/414-dedup-node-ops
vvzvlad:feat/417-new-comments-signal
vvzvlad:feat/408-createcomment-hints
vvzvlad:feat/407-persist-tool-errors
vvzvlad:feat/420-footnote-render
vvzvlad:dummy-review/mcp-hang-fix
vvzvlad:dummy-review/mcp-hang-base
vvzvlad:feat/381-resumable-sse-pr1
vvzvlad:feat/git-sync-2
vvzvlad:docs/agents-workspace-build-order
vvzvlad:feature/offline-sync
vvzvlad:perf/342-code-splitting
vvzvlad:perf/346-compression-cache
vvzvlad:feat/196-multi-cursor
vvzvlad:feat/370-page-versioning
vvzvlad:perf/343-typing-latency
vvzvlad:docs/how-to-test
vvzvlad:fix/ai-sdk-partial-output-oom
vvzvlad:test/351-generative-converter
vvzvlad:feat/371-roles-catalog
vvzvlad:refactor/345-server-converter
vvzvlad:refactor/294-spec-registry-cont
vvzvlad:fix/363-migration-order
vvzvlad:fix/e2e-callout-and-gate-build
vvzvlad:fix/docker-re2-toolchain
vvzvlad:feat/git-sync
vvzvlad:fix/media-roundtrip-stability
vvzvlad:fix/340-comment-panel-perf
vvzvlad:fix/332-deferred-tools
vvzvlad:fix/329-ephemeral-suggestions
vvzvlad:fix/330-search-in-page
vvzvlad:fix/328-resolved-anchor-spam
vvzvlad:fix/331-intraline-diff
vvzvlad:fix/324-coverage-gate
vvzvlad:fix/325-mobile-390
vvzvlad:feat/293-A-git-sync-package
vvzvlad:feat/300-avatar-oklch
vvzvlad:fix/321-banner-mobile
vvzvlad:feat/300-avatar-colors
vvzvlad:feat/315-comment-suggestions
vvzvlad:feat/scroll-restore-stable-wait
vvzvlad:feat/300-agent-avatar-stack
vvzvlad:feat/300-avatar-polish
vvzvlad:refactor/294-tool-spec-registry
vvzvlad:feat/scroll-restore-ux
vvzvlad:fix/responsive-tablet-sidebar
vvzvlad:feature/ai-chat-page-change-observability
vvzvlad:image-inline-center
vvzvlad:fix/283-short-remap-title
vvzvlad:fix/283-slash-layout
vvzvlad:image-inline-row
vvzvlad:feat/276-ai-chat-dock
vvzvlad:fix/269-table-menu-refocus
vvzvlad:docs/dev-stand-guide
vvzvlad:feat/266-scroll-position
vvzvlad:fix/260-collab-docname-slugid
vvzvlad:test/244-phase2-tail
vvzvlad:fix/262-reindex-progress-realtime
vvzvlad:fix/258-changelog-compare-links
vvzvlad:fix/244-dataloss-bugs
vvzvlad:feat/246-spoiler
vvzvlad:feat/221-image-captions
vvzvlad:test/244-part-b
vvzvlad:feat/251-intentional-clear
vvzvlad:fix/embeddings-reindex-progress
vvzvlad:refactor/193-tool-spec-registry
vvzvlad:fix/255-ws-redis-adapter-leak
vvzvlad:fix/252-e2e-open-handles
vvzvlad:feat/229-catalog-yaml
vvzvlad:feat/243-blob-sandbox
vvzvlad:feat/228-inline-footnotes
vvzvlad:fix/qa-ui-bugs-216-218
vvzvlad:feature/agent-roles-catalog
vvzvlad:fix/share-alias-rename
vvzvlad:fix/ai-chat-empty-render
vvzvlad:feat/191-chat-doc-binding
vvzvlad:feat/201-temporary-notes
vvzvlad:feat/198-interrupt-agent
vvzvlad:feat/ai-chat-full-history
vvzvlad:feat/199-ai-generate-title
vvzvlad:feat/205-share-aliases
vvzvlad:batch/issues-189-187-170
vvzvlad:feat/170-mcp-test-button
vvzvlad:feat/189-context-badge
vvzvlad:feat/198-interrupt-agent-send-now
vvzvlad:fix/issues-190-159
vvzvlad:fix/ai-chat-new-chat-during-stream
vvzvlad:fix/ai-chat-stream-perf
vvzvlad:batch/issues-2026-06-25
vvzvlad:feat/ai-chat-persistent-history
vvzvlad:fix/ai-chat-copy-chat-wysiwyg
vvzvlad:fix/ai-stream-reset-resilience
vvzvlad:fix/ai-stream-undici-timeout
vvzvlad:fix/footnote-review-1227-followup
vvzvlad:fix/ai-chat-token-counter-realtime
vvzvlad:docs/manual-qa-test-plan
16 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fdc37de3e8 |
fix(ai-chat): ревью-раунд #500 — красный серверный сьют + регрессия Redis-health
B1: переименование строки ошибки (#394) не обновило 4 предсуществующих share-ассерта → полный серверный сьют был красный. Обновлены ассерты в public-share-chat.spec.ts и public-share-chat-tools.service.spec.ts под новую классифицированную строку. B2: /health первая проба после старта врала DOWN при живом Redis (lazyConnect + enableOfflineQueue:false + maxRetriesPerRequest:1 → первый ping до открытия сокета). Добавлен ensureConnected() с bounded-таймаутом перед первым ping; покрывает и путь пересоздания после onModuleDestroy. Тест UP-с-первой-пробы против реального ioredis (mutation-verified). Устранён open-handle leak в redis.health.spec (drain ioredis force-destroy-таймера в teardown; без forceExit) и окно ложного DOWN при конкурентных пробах (мемоизированный connectingPromise). B3: комментарий про orphan-чат при провале beginRun (insert до begin). B4: описание listPages упоминает поле truncated в tree-режиме. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4a750c1e7f |
fix(#486): ревью — CHANGELOG + AGENTS.md + два теста
Ре-ревью PR #500 (changes-requested, 4 мелких, все doc/test): F1 [CHANGELOG] CHANGELOG.md [Unreleased]: - Breaking Changes: metrics-листенер 0.0.0.0→127.0.0.1 — кросс-контейнерный скрейп (docmost:9464) молча умрёт без METRICS_BIND=0.0.0.0 + METRICS_TOKEN (миграция в .env.example). - Security: утечка errorText тулов/провайдера анониму (closes #394); /metrics под Bearer (METRICS_TOKEN). - Fixed: ioredis-утечка в /health; ELK вешал event loop; beginRun-призрак → честный 503 A_RUN_BEGIN_FAILED; ai drain-hang. F2 [AGENTS.md] строка про ai-патч: теперь он несёт ДВА фикса (#184 O(n²) partialOutput И #486 drain-hang), оба тривайра названы. F3 [test] metrics.server.spec: добавлен кейс токена ТОЙ ЖЕ длины (Bearer topsecreX) → 401 — пиннит constant-time сравнение (прежние кейсы коротили на length-guard, до timingSafeEqual не доходили). F4 [test] output-degeneration.spec: behavior-тест, гоняющий РЕАЛЬНЫЕ onChunk/onStepFinish из stream() — длинный чистый шаг → граница → свежий дегенеративный бёрст → ассерт abortSignal.aborted (было хардкодом resetWatermark=0, ревёрт правки не краснил). Мутационные пруфы (non-vacuous): F3 — форс compare→true роняет same-length кейс (401→200); F4 — ревёрт lastDegenerationCheckLen=0 роняет behavior-тест (aborted true→false). Оба восстановлены, специи зелёные (34/34). |
||
|
|
ea7c4d7cd2 |
fix(#486): ревью — run-race контракт под новую политику + timing-safe metrics-токен
Ревью полной ветки #486 нашло два пункта в моих коммитах (3 и 4): - BLOCKER (коммит 4): ai-chat.service.run-race.spec.ts (#184 F14) пинил СТАРУЮ политику «plain begin() failure → swallow + стрим UNTRACKED» (resolves toBeUndefined). Коммит 4 её развернул → тест падал (1 failed/6). Кейс ИНВЕРТИРОВАН под новую политику: plain begin-failure теперь REJECTS с 503 A_RUN_BEGIN_FAILED, до первого байта, user-строка не вставлена, streamText не вызван — путь остаётся явно запинён, а не удалён. - NIT (коммит 3): metrics-токен сравнивался наивным !== (единственный слой auth эндпоинта) → тайминг-утечка токена. Заменено на crypto.timingSafeEqual с length-guard (разная длина → reject), семантика 401/200 без изменений. Внесено отдельным fixup-коммитом (rebase -i недоступен в окружении; ветка не запушена). Тесты: run-race 7/7 + metrics.server 7/7 зелёные. |
||
|
|
255024fbe2 |
chore(mcp): McpService.onModuleDestroy → destroyAllSessions + удаление мёртвого кода (#486)
onModuleDestroy чистил только sweep-таймер; loopback-collab-сессии держали доки открытыми на collab-сервере до идла — рестарт мог гонять с доком, запиненным умирающим воркером. Теперь дергает destroyAllSessions() через переопределяемый шов (для юнит-теста без ESM-пакета), best-effort. Плюс удалён мёртвый код: неиспользуемый импорт parseNodeArg в mcp/index.ts и мёртвые enum-члены SEARCH_REMOVE_* в queue.constants (подтверждено grep'ом — ни диспетчера, ни процессора). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b934fb2292 |
fix(mcp): пробросить truncated в tree-mode listPages (#486)
listPages(tree:true) деструктурировал только pages из enumerateSpacePages и
возвращал голое дерево, теряя truncated — на неполном дереве (stdio-fallback BFS
упёрся в node-cap) вызывающий не знал, что страницы потеряны. Возвращаем
{ tree, truncated } (по образцу check_new_comments); основной /pages/tree путь
беспредельный, там false.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
14da295185 |
fix(auth): провенанс для API-key пути (#486)
validateApiKey возвращал результат до резолва провенанса — REST-записи по is_agent API-ключу не получали маркер 'agent'. Перенести «выше» нельзя: payload API-ключа не несёт подписанный actor-клейм, а user (с isAgent) неизвестен до валидации ключа. Резолвим провенанс от возвращённого user: isAgent -> 'agent', иначе 'user'; aiChatId у API-ключа всегда null (нет ai_chats-строки). Загрузка EE ApiKeyService вынесена в переопределяемый шов resolveApiKeyService для юнит- теста без EE-бандла. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
de8f9c804c |
fix(client): flushNext в onFinish гейтится mountedRef (#486)
Финальный onFinish->flushNext() не проверял live-mount флаг. Чистый onFinish может прийти ПОСЛЕ анмаунта треда (New-chat / переключение чата мид-стрим — асинхронные attach/resume оседают поздно): flush дергал очередь и re-POST'ил сообщение из брошенного треда — «призрачные» отправки/чаты-призраки. Остальные обращения к очереди уже гейтятся mountedRef; закрываем последнюю дыру. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8ebdfe156f |
fix(ai-chat): сброс lastDegenerationCheckLen в onStepFinish (#486)
onStepFinish обнулял inProgressText, но НЕ lastDegenerationCheckLen — а это смещение В аккумулятор. После первого длинного шага протухшая (большая) отметка делала throttle-условие отрицательным, и детектор токен-лупов молчал весь следующий шаг, пока текст не перерастал старую отметку. Обнуляем отметку в onStepFinish. Throttle-предикат вынесен в output-degeneration (shouldCheckDegeneration + DEGENERATION_CHECK_STEP), чтобы юнит гонял ту же логику, что и стрим. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
31176d5f93 |
fix(mcp): сброс collab-токена на WS-auth-failure с ретраем (#486)
Кэш collab-токена (#435) инвалидировался только на HTTP-401/403 (REST- интерцептор и login()); отклонённый Hocuspocus-handshake оставлял протухший токен в кэше — каждая последующая мутация переотправляла тот же битый токен до истечения TTL (минуты) без self-heal. collab-session помечает ошибку onAuthenticationFailed маркером; клиентские write-швы (mutatePage/replacePage/ mutateLiveContentUnlocked) обёрнуты в writeWithCollabAuthRetry: на помеченной ошибке кэш сбрасывается и запись ретраится ровно один раз со свежим токеном — симметрично HTTP-пути. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5f0c49d47c |
fix(mcp): REGISTRY_STAMP хэширует всё src/**, а не только tool-specs.ts (#486)
Стамп детектил build/src-skew только по tool-specs.ts — правка client.ts, client/*-модуля, comment-signal или drawio-* без пересборки проходила молча, и build/ отдавал старый код. Теперь codegen и рантайм-лоадер хэшируют весь src/**/*.ts (кроме *.generated.ts — иначе цикл через собственный выход), симметрично: одинаковый обход, POSIX-сортировка, нормализация и sha256. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b2e5b51420 |
fix(ai): drain-hang в writeToServerResponse — расширить patches/ai@6.0.134.patch (#486)
Серверная ai@6.0.134 в writeToServerResponse при backpressure (write()===false)
ждала ТОЛЬКО once('drain'). Если клиент отвалился мид-запись, сокет не дренится,
await не резолвится: read-цикл паркуется НАВСЕГДА, finally{response.end()}
недостижим, reader и буферы висят до рестарта. В autonomous ран продолжает лить
вывод после дисконнекта → КАЖДЫЙ дисконнект мид-ран оставляет висящий пайп.
Плюс read() — fire-and-forget с throw → unhandledRejection.
Патч расширен (index.js и index.mjs): Promise.race drain/close/error с гигиеной
once-слушателей (все три снимаются на первом settle — не копятся по одному на
stall); при close/error — reader.cancel() и выход (безопасно для detached-ранов:
независимый дренаж делает consumeStream); rejection read() поглощается с логом.
pnpm-lock patch_hash перегенерён (patch-commit). Выравнивание версии ai —
отдельный коммит (#495), не здесь.
Тест: трипвайр-спека в apps/server (только там резолвится патченная копия) по
образцу ai-sdk-partial-output.patch.spec — дисконнект мид-запись без drain
завершает ответ (end() вызван, reader не висит), read()-throw не даёт
unhandledRejection, оба dist-билда несут маркер PATCH(docmost #486).
|
||
|
|
26d9a70a1c |
fix(share): не утекать errorText тулов и провайдера анониму в публичном шэре (closes #394)
SECURITY. В публичном share-чате сырой текст ошибки тула или провайдера утекал анонимному читателю. Три слоя, все обязательны: (1) Рендер-гейт: prop showErrors в ToolCallCard (протянут через MessageList/ MessageItem), share-виджет передаёт false — сырой errorText не рисуется. Но рендер-гейт маскирует только DOM, не байты. (2) Санитизация на уровне share-тулсета (авторитетно): forShare оборачивает execute каждого тула catch'ем. Своя ShareToolError (безопасные строки: «page not available in this share») пробрасывается, ЛЮБАЯ другая ошибка → generic «tool could not complete», полный текст только в серверный лог. Одно место закрывает байты (атомарный tool-output-error фрейм v6), рендер и контекст модели; self- correction сохранена. (3) Анонимный onError пайпа: ShareToolError → её безопасное сообщение; иначе describeProviderError (statusCode + тело: внутренний baseUrl/модель) только в лог, читателю — фиксированная классифицированная строка (rate-limited/unavailable/ provider error). Тест: интеграционный с РЕАЛЬНЫМ падением тула и провайдера — assert по СЫРЫМ SSE- БАЙТАМ (не по DOM): секрет/baseUrl/стек отсутствуют, видна безопасная строка, полный текст провайдера ушёл в серверный лог. |
||
|
|
e1ebd79f30 |
fix(ai-chat): провал beginRun фейлит ход честным 503 (A_RUN_BEGIN_FAILED) (#486)
Раньше провал beginRun (кроме unique-violation), напр. blip пула БД, логировался и ход ПРОДОЛЖАЛСЯ без run-строки. В autonomous такой ран никто не абортит: /stop его не видит, дисконнект не абортит, one-run-гейт пропускает ВТОРОЙ ран — невидимый неостанавливаемый ран до рестарта. Теперь провал beginRun (кроме RunAlreadyActiveError → прежний 409) бросает ServiceUnavailableException с кодом A_RUN_BEGIN_FAILED ДО первого байта и до вставки user-строки (post-hijack catch контроллера отдаёт честный 503 на raw- сокет). Без ветвления по режимам — #487 наследует ту же политику. В тело кладём statusCode: 503 (object-arg исключение его не добавляет), чтобы клиент видел статус. Клиентский классификатор: ветка A_RUN_BEGIN_FAILED добавлена СТРОГО ДО generic- 503-матча — иначе показал бы «provider is not configured» вместо «временно, повторите». Тесты: unit fail-fast (stream() бросает 503 A_RUN_BEGIN_FAILED, ни байта в сокет, user-строка не вставлена; RunAlreadyActiveError по-прежнему 409); unit клиентского классификатора из ПОЛНОГО реального тела ответа с гвардом порядка. |
||
|
|
fc83ea28ab |
fix(metrics): bind на 127.0.0.1 по умолчанию + METRICS_BIND/METRICS_TOKEN (#486)
/metrics слушал на 0.0.0.0 без какой-либо аутентификации — auth-less эндпоинт на всех интерфейсах. Теперь дефолтный bind — loopback 127.0.0.1; env METRICS_BIND переопределяет интерфейс (0.0.0.0 для скрейпера в отдельном контейнере, docmost:9464); опциональный METRICS_TOKEN включает Bearer-аутентификацию (запросы без точного токена получают 401). Доки скрейпа в .env.example обновлены. Тест: unit на дефолтный bind + env-переопределение + резолв токена; интеграционный по РЕАЛЬНОМУ сокету — listener забинден на 127.0.0.1, без токена /metrics отдаётся, с токеном без/с неверным Bearer → 401, с верным → 200. |
||
|
|
ee15278c8f |
fix(health): устранить утечку ioredis-клиента в /health-пробе (#486)
pingCheck строил new Redis(...) на КАЖДЫЙ вызов и делал disconnect() только на success-пути. Пока Redis лежит, каждый тик health-пробы добавлял свежий, вечно реконнектящийся клиент — неограниченный рост хэндлов/клиентов на всё время недоступности Redis. Теперь один долгоживущий probe-клиент, переиспользуемый между тиками: lazyConnect (конструктор не бросает и не коннектится жадно), maxRetriesPerRequest: 1 и enableOfflineQueue: false (проба фейлится быстро, команды не буферизуются), плюс listener на 'error' (иначе unhandled error роняет процесс). onModuleDestroy закрывает клиент при shutdown. Тест: интеграционный — N проб при лежащем Redis (реальный refused-порт, не мок поведения) создают РОВНО ОДИН клиент (на баге было бы N); onModuleDestroy освобождает клиент, следующая проба лениво строит новый. |
||
|
|
09ab92eccf |
fix(mcp): ELK-лейаут в worker_thread — таймаут через terminate() (#486)
elkjs.layout() возвращает Promise, но саму раскладку крутит СИНХРОННО и блокирует поток целиком. На in-app хосте это был главный event loop: патологический граф у капа 500 узлов вешал ВСЕ HTTP/SSE/loopback. Прежняя защита (Promise.race с setTimeout(5s)) была иллюзией — таймер физически не мог сработать, пока тот же поток заблокирован внутри elkjs (комментарий в коде это сам признавал). Теперь elk.layout() исполняется в worker_thread, а таймаут форсится worker.terminate() — единственный способ прервать синхронный JS. Главный поток остаётся свободным; на таймауте/ошибке — best-effort откат к исходной модели, как и раньше. Лживый комментарий «can never wedge the server» убран. Тесты: unit на terminate-по-таймауту (крошечный ceiling → hard-kill → исходная модель нетронута) и бенчмарк-гард на worst-case графе у капа (500 узлов/~1000 рёбер раскладывается, а главный event loop продолжает тикать во время раскладки). |