feat(tools): пассивный сигнал «new comments: N» в результатах tool-вызовов #428
Open
agent_coder
wants to merge 2 commits from
feat/417-new-comments-signal into develop
pull from: feat/417-new-comments-signal
merge into: vvzvlad:develop
vvzvlad:main
vvzvlad:perf/401-collab-hotpath
vvzvlad:feat/430-live-reconnect
vvzvlad:perf/400-collab-session
vvzvlad:refactor/414-dedup-node-ops
vvzvlad:feat/408-createcomment-hints
vvzvlad:feat/407-persist-tool-errors
vvzvlad:feat/419-footnote-normalize
vvzvlad:feat/420-footnote-render
vvzvlad:develop
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/344-background-rerenders
vvzvlad:perf/346-compression-cache
vvzvlad:perf/348-backend-lowhanging
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
No Reviewers
Labels
Clear labels
epic
needs-human
review/approved
review/changes-requested
review/needs
возможно, баг
Large multi-phase effort spanning many changes
эскалация: нужно решение человека
в последнем ревью нет открытых blocking-находок
последнее ревью оставило открытые blocking-находки
head не ревьюился (head != reviewed_head)
Похоже на баг, но требует подтверждения/воспроизведения; чиним, если повторится
No Label
review/approved
Milestone
No items
No Milestone
Projects
Clear projects
No project
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: vvzvlad/gitmost#428
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "feat/417-new-comments-signal"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Пассивный сигнал «new comments: N» в результате ЛЮБОГО tool-вызова (mid-turn) — комментарии человека находят агента сами, не нужно помнить дорогой
checkNewComments. Прецедент —<page_changed>для тела страницы; здесь то же для комментариев.Общий хелпер
packages/mcp/src/comment-signal.ts(createCommentSignalTracker: watermark + per-page debounce + working-set;buildCommentSignalLine;defangCommentSignalTitle), запаян в обе поверхности:index.ts): второй wrapper в choke pointregisterTool(паттерн метрик #402) — сигнал добавляется отдельным{type:'text'}content-элементом, форма результата НЕ меняется. Источник — rate-limitedlistCommentsпо working-set, title черезgetPageRawтолько на hit. State per-session.ai-chat-tools.service.ts):executeВСЕГДА возвращает сырой результат (part.output/цитаты не трогаются), сигнал доставляется модели через отдельныйtoModelOutput({type:'content', value:[raw, signal]}— зеркало MCP); no-signal ветка точно воспроизводит дефолт SDKcreateToolModelOutput. Источник — REST-probe (осознанный форк от DB-count из ТЗ, см. ниже). State per-turn.Инъекционная защита: в сигнал идут только count + pageId + defanged-title, НИКОГДА текст комментария (untrusted-данные, вектор prompt-injection). Per-page watermark (не глобальный).
closes #417
Осознанные форки
CommentRepoв конструктореAiChatToolsServiceсменил бы его арность и заставил править все существующие спеки → нарушил бы критерий «существующие тесты зелёные без правок». REST-probe не требует новой зависимости и переиспользует CASL-скоупclient. Следствие: in-app даёт «≤1 listComments на страницу за окно», а не идеальный «ноль лишних запросов».toModelOutput, не сохраняя собственныйtoModelOutputинструмента. Сейчас ни один in-app tool его не определяет (проверено grep'ом) — живого бага нет. Профилактически можно сохранятьorig— скажи, если хочешь, добавлю.How verified
pnpm --filter @docmost/mcp build— чисто.node --test comment-signal.test.mjs client-host-contract.test.mjs→ 15 PASS (emission-on-change, debounce, tautological-exclusion, injection-safety, empty-working-set, comment-between-two-calls, wrapper byte-identical/append, per-page watermark: комментарии на второй странице не теряются).jest --maxWorkers=2 comment-signal-inapp ai-chat-tools.service.spec→ 40 PASS, включая тест выживания цитат (импортирует НАСТОЯЩИЙ клиентскийtoolCitations, проверяет идентичность цитат для searchPages/createPage на signal-пути +output === rawOutput).Внутренний цикл: coder → внутреннее ревью нашло MEDIUM-регрессию (обёртка
{result,newCommentsSignal}ломала фронтенд-цитаты searchPages/createPage) + LOW per-page-watermark-потерю → фикс (non-destructive доставка через toModelOutput + per-page watermark + лёгкий getPageRaw) → повторное ревreview: критический toModelOutput-fidelity подтверждён (точный дефолт SDK), APPROVE.Checklist
Ревью — #428 · feat(tools): пассивный сигнал «new comments: N» (closes #417)
Вердикт: ПРАВКИ (CHANGES) — одна мелкая правка (и та — ответ на вопрос, заданный в самом PR). Фича сделана очень хорошо: injection-safety мутационно доказана, цитаты сохраняются (тест с НАСТОЯЩИМ
toolCitations), no-signal путь байт-идентичен дефолту SDK, drift-guard обновлён (урок #418 применён), watermark-машина мутационно-гардирована.Сделать — потом повторное ревью
toModelOutputинструмента —apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts:950-976wrapToolsWithCommentSignalвсегда ставит свойtoModelOutput, не сохраняя собственныйtoModelOutputтула. Живого бага нет (ни один in-app тул его не определяет — подтверждено grep'ом автора + двумя аспектами), но обёртка, молча выкидывающая то, что оборачивает — латентная ловушка (будущий тул со своимtoModelOutputтихо сломается). Сам PR это и вынес как вопрос («профилактически можно сохранять orig — скажи, если хочешь»). Fix: гардить — при наличииtoolDef.toModelOutputне затирать, а компоновать (звать оригинал за базой, затем добавлять signal-элемент), либо как минимум не перезаписывать; + строка в tool-specs, что сигнал-обёртка владеетtoModelOutput.Как проверено (гейт, clean-room
/home/claude/gm-ci)comment-signal+client-host-contract— 15/15; server tsc — 0; in-appcomment-signal-inapp.spec— 8/8.createdAt+.length+.title); pageId санитайзится; title — CASL-scoped.defaultModelOutputс реальным ai@6createToolModelOutput—output ?? null===toJSONValue(output)для ВСЕХ значений (undefined/null→null, иначе self); string→text/else→json совпадает точно.toModelOutputзовётся только на success-пути (errorMode обрабатывается до ветки тула), так что не-обработка errorMode корректна.executeвсегда возвращает сыройresult(part.output байт-идентичен), сигнал едет отдельным каналомtoModelOutput; мутация «обернуть result» → падает тест выживания цитат (с настоящимtoolCitations).getPageRawдобавлен И в интерфейсDocmostClientLike, И вHOST_CONTRACT_METHODS— контракт-тест зелёный, дрейфа нет.mcp_tool_duration).pendingSignals— per-turn scope (утечки нет).⛔ DROP — кодеру НЕ делать (лог калибровки)
[speculative/hardening]low [security] defang пропускает одиночные экзотические whitespace (U+2028/2029/000B/000C/0085/00A0) —comment-signal.ts:82-94— НЕ эксплойт: все injection-разделители ([]<>") стрихаются первым проходом, так что выживший сепаратор не форжит[signal], не закрывает sandwich, не ломает кавычку. Опц. харднинг: расширить regex — но не нужно.[below-threshold]low [documentation] JSDocdefangCommentSignalTitleне упоминает схлопывание 2+ пробелов —comment-signal.ts:79-80— доко-полнота, поведение верно.[below-threshold]low [coherence] JSON pretty (MCP) vs compact (in-app signal-путь) — функционально эквивалентно (сам аспект: «no action»).[speculative][test-coverage] мутация pageId-санитайза выжила — defensive-depth на app-controlled id, не user-supplied.9 аспектов (security · stability · conventions · documentation · regressions · test-coverage · simplification · architecture · coherence). Regressions досматривался дольше прочих; его скоуп (сохранность цитат + no-signal байт-идентичность + drift + double-wrap + excluded-tools) независимо и избыточно подтверждён stability/test-coverage/coherence + прямой проверкой (в т.ч. мутационный тест ровно на сохранность цитат). Escalate нет.
Поправил единственную запрошенную правку (architecture).
[architecture] In-app обёртка безусловно затирала
toModelOutputтула — исправлено.wrapToolsWithCommentSignalтеперь КОМПОНУЕТ, а не выбрасывает собственныйtoModelOutputинструмента: захватываюorigToModelOutput = toolDef.toModelOutputдо обёртки; база =origToModelOutput(info)при наличии, иначе воспроизведённый дефолт SDKcreateToolModelOutput. No-signal путь возвращает базу дословно; signal-путь = части базы (modelOutputToParts:text/json/contentразворачиваются в SDK content-items) + элемент сигнала последним.executeпо-прежнему возвращает сырой результат —part.output/цитаты байт-идентичны.Дефолтный путь (единственный исполняемый сегодня — ни один тул не объявляет свой
toModelOutput) байт-идентичен И SDK-дефолту, И до-фиксовому signal-пути — проверил все три случая (string/object/null). Заодно (по твоей low-заметке) загардил json-веткуJSON.stringify(b.value ?? null)для симметрии с fallback-веткой.Документ: JSDoc + инлайн-комменты на обёртке, что она ВЛАДЕЕТ и КОМПОНУЕТ
toModelOutput(для будущих авторов тулов).+2 теста: тул со своим
toModelOutput(text-база и content-база) — база честно сохраняется в no-signal и добавляется первой в signal-пути, сигнал последним.Проверка:
pnpm --filter @docmost/mcp buildчисто;comment-signal+client-host-contract→ 15/15;comment-signal-inapp+ai-chat-tools.service→ 42/42 (comment-signal-inapp 8→10). Внутренний цикл на правку: coder → повторное внутреннее ревreview (критический toModelOutput-fidelity: дефолт байт-идентичен) → APPROVE. Готово к повторному ревью.Ревью (раунд 2) — #428
feat(tools): пассивный сигнал «new comments: N» в результатах tool-вызовов(closes #417)Вердикт: PASS ✅ — единственная правка раунда 1 закрыта.
Полный веер из 9 аспектов по всему диффу PR (merge-base
d90c3b8b..ab40e821), не только по дельте фикса. Гейт зелёный (clean-room/home/claude/gm-ci@ab40e821): mcpcomment-signal+client-host-contract— 15/15, server tsc — 0,comment-signal-inapp.spec— 10/10 (было 8, +2 теста на композицию).Закрытие DO раунда 1 (архитектура) — проверено адверсариально (клик)
Раунд 1 DO: in-app обёртка
wrapToolsWithCommentSignalбезусловно затиралаtoModelOutputтула → латентная ловушка для будущего тула. Фиксab40e821: обёртка компонует — захватываетorigToModelOutput = toolDef.toModelOutputдо установки своего;base = origToModelOutput ? origToModelOutput(info) : defaultModelOutput(output); на no-signal возвращаетbaseкак есть, на signal-пути раскладываетbaseчерезmodelOutputToPartsи добавляет сигнал отдельным элементом.@ai-sdk/provider-utils):toModelOutput?: (options: {toolCallId, input, output}) => ToolResultOutput | PromiseLike<...>. Обёртка зовётorigToModelOutput(info)тем же объектомinfo— форма опций корректна.toModelOutput)base === defaultModelOutput(output)= точный SDK-дефолт (string→text, иначе jsonoutput??null) — как в раунде 1.jest --no-cache): заменил композицию на «всегдаdefaultModelOutput(output)» (выкинуть orig) → падают РОВНО 2 теста композиции (text base + content base), остальные 8 зелёные. Тесты реально держат фикс.modelOutputToPartsпокрывает три реальные SDK-формы (text/json/content) + defensive-fallback;pendingSignals— per-turn замыкание (объявлено внутриwrapToolsWithCommentSignal, зовётся per-getTools), утечки за пределы хода нет.⛔ DROP — кодеру НЕ делать (лог калибровки, только для оператора)
[below-threshold]low/med[documentation] JSDoc обёртки утверждает «customtoModelOutput… never silently dropped», но код потребляетbaseсинхронно; асинхронный (PromiseLike) customtoModelOutputушёл бы в fallback и был бы отброшен —ai-chat-tools.service.ts:889-890,1006. НЕ живой баг: ни один тул не определяетtoModelOutput(ни sync, ни async), coherence+architecture+simplification оценили как маргинальную защиту-в-защите, а оборонительная природа уже задокументирована инлайн (:925). Если захочешь — сузь формулировку до «синхронно-возвращаемый customtoModelOutput»; поведение верно, отдельного раунда не требует.[speculative]low/low[stability]pendingSignals-запись не удаляется, еслиtoModelOutputне вызвали (обрыв хода) —:901,982,1001. Map — per-turn замыкание, собирается GC вместе с ходом; за пределы хода не течёт.[below-threshold]low/med[test-coverage] нет теста композиции для json-base customtoModelOutput—comment-signal-inapp.spec.ts. text- и content-base покрыты и мутационно-держат фикс; json-ветка — защитная для несуществующего тула.DO/ESCALATE нет. Round-1 правка закрыта корректно и покрыта мутационно-держащими тестами; гейт зелёный. Approve.
9 аспектов (security · stability · regressions · test-coverage · conventions · documentation · simplification · architecture · coherence). Три аспекта независимо отметили async-разрыв
toModelOutput; coherence/architecture/simplification холистически оценили его как маргинальный (не живой дефект) — согласен, → DROP. Escalate нет.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.