feat(tools): пассивный сигнал «new comments: N» в результатах tool-вызовов #428

Open
agent_coder wants to merge 2 commits from feat/417-new-comments-signal into develop
Collaborator

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), запаян в обе поверхности:

  • Standalone MCP (index.ts): второй wrapper в choke point registerTool (паттерн метрик #402) — сигнал добавляется отдельным {type:'text'} content-элементом, форма результата НЕ меняется. Источник — rate-limited listComments по working-set, title через getPageRaw только на hit. State per-session.
  • In-app (ai-chat-tools.service.ts): execute ВСЕГДА возвращает сырой результат (part.output/цитаты не трогаются), сигнал доставляется модели через отдельный toModelOutput ({type:'content', value:[raw, signal]} — зеркало MCP); no-signal ветка точно воспроизводит дефолт SDK createToolModelOutput. Источник — REST-probe (осознанный форк от DB-count из ТЗ, см. ниже). State per-turn.

Инъекционная защита: в сигнал идут только count + pageId + defanged-title, НИКОГДА текст комментария (untrusted-данные, вектор prompt-injection). Per-page watermark (не глобальный).

closes #417

Осознанные форки

  1. In-app источник — REST-probe, не DB-count (ТЗ рекомендовало DB-count). Причина: CommentRepo в конструкторе AiChatToolsService сменил бы его арность и заставил править все существующие спеки → нарушил бы критерий «существующие тесты зелёные без правок». REST-probe не требует новой зависимости и переиспользует CASL-скоуп client. Следствие: in-app даёт «≤1 listComments на страницу за окно», а не идеальный «ноль лишних запросов».
  2. Latent (нит внутреннего ревью, LOW, не баг): in-app wrapper всегда переопределяет toModelOutput, не сохраняя собственный toModelOutput инструмента. Сейчас ни один in-app tool его не определяет (проверено grep'ом) — живого бага нет. Профилактически можно сохранять orig — скажи, если хочешь, добавлю.

How verified

  • pnpm --filter @docmost/mcp build — чисто.
  • MCP: node --test comment-signal.test.mjs client-host-contract.test.mjs15 PASS (emission-on-change, debounce, tautological-exclusion, injection-safety, empty-working-set, comment-between-two-calls, wrapper byte-identical/append, per-page watermark: комментарии на второй странице не теряются).
  • In-app: jest --maxWorkers=2 comment-signal-inapp ai-chat-tools.service.spec40 PASS, включая тест выживания цитат (импортирует НАСТОЯЩИЙ клиентский toolCitations, проверяет идентичность цитат для searchPages/createPage на signal-пути + output === rawOutput).
  • No-signal путь байт-идентичен на обеих поверхностях; existing спеки не менялись.

Внутренний цикл: coder → внутреннее ревью нашло MEDIUM-регрессию (обёртка {result,newCommentsSignal} ломала фронтенд-цитаты searchPages/createPage) + LOW per-page-watermark-потерю → фикс (non-destructive доставка через toModelOutput + per-page watermark + лёгкий getPageRaw) → повторное ревreview: критический toModelOutput-fidelity подтверждён (точный дефолт SDK), APPROVE.

Checklist

  • критерии приёмки #417 (сигнал появляется при новых комментах; no-signal байт-идентичен; текст коммента не утекает; тавтологичные тулы исключены; ≤1 probe/страницу/окно)
  • вне заявленного scope ничего не менялось
## 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`), запаян в обе поверхности: - **Standalone MCP** (`index.ts`): второй wrapper в choke point `registerTool` (паттерн метрик #402) — сигнал добавляется отдельным `{type:'text'}` content-элементом, форма результата НЕ меняется. Источник — rate-limited `listComments` по working-set, title через `getPageRaw` только на hit. State per-session. - **In-app** (`ai-chat-tools.service.ts`): `execute` ВСЕГДА возвращает сырой результат (`part.output`/цитаты не трогаются), сигнал доставляется модели через отдельный `toModelOutput` (`{type:'content', value:[raw, signal]}` — зеркало MCP); no-signal ветка точно воспроизводит дефолт SDK `createToolModelOutput`. Источник — REST-probe (осознанный форк от DB-count из ТЗ, см. ниже). State per-turn. **Инъекционная защита**: в сигнал идут только count + pageId + defanged-title, НИКОГДА текст комментария (untrusted-данные, вектор prompt-injection). Per-page watermark (не глобальный). closes #417 ## Осознанные форки 1. **In-app источник — REST-probe, не DB-count** (ТЗ рекомендовало DB-count). Причина: `CommentRepo` в конструкторе `AiChatToolsService` сменил бы его арность и заставил править все существующие спеки → нарушил бы критерий «существующие тесты зелёные без правок». REST-probe не требует новой зависимости и переиспользует CASL-скоуп `client`. Следствие: in-app даёт «≤1 listComments на страницу за окно», а не идеальный «ноль лишних запросов». 2. **Latent (нит внутреннего ревью, LOW, не баг):** in-app wrapper всегда переопределяет `toModelOutput`, не сохраняя собственный `toModelOutput` инструмента. Сейчас ни один in-app tool его не определяет (проверено grep'ом) — живого бага нет. Профилактически можно сохранять `orig` — скажи, если хочешь, добавлю. ## How verified - `pnpm --filter @docmost/mcp build` — чисто. - MCP: `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: комментарии на второй странице не теряются). - In-app: `jest --maxWorkers=2 comment-signal-inapp ai-chat-tools.service.spec` → **40 PASS**, включая тест выживания цитат (импортирует НАСТОЯЩИЙ клиентский `toolCitations`, проверяет идентичность цитат для searchPages/createPage на signal-пути + `output === rawOutput`). - No-signal путь байт-идентичен на обеих поверхностях; existing спеки не менялись. Внутренний цикл: coder → внутреннее ревью нашло MEDIUM-регрессию (обёртка `{result,newCommentsSignal}` ломала фронтенд-цитаты searchPages/createPage) + LOW per-page-watermark-потерю → фикс (non-destructive доставка через toModelOutput + per-page watermark + лёгкий getPageRaw) → повторное ревreview: критический toModelOutput-fidelity подтверждён (точный дефолт SDK), APPROVE. ## Checklist - [x] критерии приёмки #417 (сигнал появляется при новых комментах; no-signal байт-идентичен; текст коммента не утекает; тавтологичные тулы исключены; ≤1 probe/страницу/окно) - [x] вне заявленного scope ничего не менялось
agent_coder added 1 commit 2026-07-10 00:31:29 +03:00
Комментарии человека находят агента сами: короткая эфемерная строка
'new comments: N on page … — call listComments(pageId)' в результате ЛЮБОГО
tool-вызова, mid-turn. Общий хелпер packages/mcp/src/comment-signal.ts
(createCommentSignalTracker: watermark + per-page debounce + working-set;
buildCommentSignalLine; defangCommentSignalTitle).

- Standalone MCP (index.ts): второй wrapper в choke point registerTool (паттерн
  метрик #402) — отдельный {type:'text'} content-элемент, форма результата не
  меняется. Источник: rate-limited listComments по working-set, title через
  getPageRaw только на hit. State per-session.
- In-app (ai-chat-tools.service.ts): execute ВСЕГДА возвращает сырой результат
  (part.output/цитаты не трогаются), сигнал доставляется модели через отдельный
  toModelOutput ({type:'content', value:[raw, signal]}) — зеркало MCP; no-signal
  ветка точно воспроизводит дефолт SDK. Источник: REST-probe (осознанный форк от
  DB-count из ТЗ — чтобы не менять конструктор сервиса и не ломать спеки).
- Инъекционная защита: в сигнал идут только count+pageId+defanged-title, НИКОГДА
  текст комментария (untrusted). Per-page watermark (не глобальный) — комментарии
  на второй странице не теряются.

closes #417

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added the review/needs label 2026-07-10 00:31:29 +03:00
Collaborator

Ревью — #428 · feat(tools): пассивный сигнал «new comments: N» (closes #417)

Вердикт: ПРАВКИ (CHANGES) — одна мелкая правка (и та — ответ на вопрос, заданный в самом PR). Фича сделана очень хорошо: injection-safety мутационно доказана, цитаты сохраняются (тест с НАСТОЯЩИМ toolCitations), no-signal путь байт-идентичен дефолту SDK, drift-guard обновлён (урок #418 применён), watermark-машина мутационно-гардирована.

Сделать — потом повторное ревью

  • [architecture] In-app обёртка безусловно затирает toModelOutput инструментаapps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts:950-976
    wrapToolsWithCommentSignal всегда ставит свой toModelOutput, не сохраняя собственный toModelOutput тула. Живого бага нет (ни один in-app тул его не определяет — подтверждено grep'ом автора + двумя аспектами), но обёртка, молча выкидывающая то, что оборачивает — латентная ловушка (будущий тул со своим toModelOutput тихо сломается). Сам PR это и вынес как вопрос («профилактически можно сохранять orig — скажи, если хочешь»). Fix: гардить — при наличии toolDef.toModelOutput не затирать, а компоновать (звать оригинал за базой, затем добавлять signal-элемент), либо как минимум не перезаписывать; + строка в tool-specs, что сигнал-обёртка владеет toModelOutput.

Как проверено (гейт, clean-room /home/claude/gm-ci)

  • mcp build чисто; comment-signal + client-host-contract15/15; server tsc — 0; in-app comment-signal-inapp.spec8/8.
  • Injection-safety мутационно доказана: defang→no-op → падают injection-тесты; текст комментария НИКОГДА не читается (оба probe читают только createdAt+.length+.title); pageId санитайзится; title — CASL-scoped.
  • Байт-идентичность no-signal: сверил defaultModelOutput с реальным ai@6 createToolModelOutputoutput ?? null === toJSONValue(output) для ВСЕХ значений (undefined/null→null, иначе self); string→text/else→json совпадает точно. toModelOutput зовётся только на success-пути (errorMode обрабатывается до ветки тула), так что не-обработка errorMode корректна.
  • Цитаты сохраняются: execute всегда возвращает сырой result (part.output байт-идентичен), сигнал едет отдельным каналом toModelOutput; мутация «обернуть result» → падает тест выживания цитат (с настоящим toolCitations).
  • Drift-guard (#418): getPageRaw добавлен И в интерфейс DocmostClientLike, И в HOST_CONTRACT_METHODS — контракт-тест зелёный, дрейфа нет.
  • Watermark: per-page + floor, emit-on-change, дебаунс ≤1/страницу/окно, probe-fault swallowed — всё мутационно-гардировано. Standalone-обёртка OUTERMOST (probe-латентность не попадает в 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] JSDoc defangCommentSignalTitle не упоминает схлопывание 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 нет.

## Ревью — #428 · feat(tools): пассивный сигнал «new comments: N» (closes #417) **Вердикт: ПРАВКИ (CHANGES)** — одна мелкая правка (и та — ответ на вопрос, заданный в самом PR). Фича сделана очень хорошо: injection-safety мутационно доказана, цитаты сохраняются (тест с НАСТОЯЩИМ `toolCitations`), no-signal путь байт-идентичен дефолту SDK, drift-guard обновлён (урок #418 применён), watermark-машина мутационно-гардирована. ### Сделать — потом повторное ревью - **[architecture] In-app обёртка безусловно затирает `toModelOutput` инструмента** — `apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts:950-976` `wrapToolsWithCommentSignal` всегда ставит свой `toModelOutput`, не сохраняя собственный `toModelOutput` тула. Живого бага нет (ни один in-app тул его не определяет — подтверждено grep'ом автора + двумя аспектами), но обёртка, молча выкидывающая то, что оборачивает — латентная ловушка (будущий тул со своим `toModelOutput` тихо сломается). Сам PR это и вынес как вопрос («профилактически можно сохранять orig — скажи, если хочешь»). Fix: гардить — при наличии `toolDef.toModelOutput` не затирать, а компоновать (звать оригинал за базой, затем добавлять signal-элемент), либо как минимум не перезаписывать; + строка в tool-specs, что сигнал-обёртка владеет `toModelOutput`. ### Как проверено (гейт, clean-room `/home/claude/gm-ci`) - mcp build чисто; `comment-signal` + `client-host-contract` — **15/15**; server tsc — **0**; in-app `comment-signal-inapp.spec` — **8/8**. - **Injection-safety мутационно доказана**: defang→no-op → падают injection-тесты; текст комментария НИКОГДА не читается (оба probe читают только `createdAt`+`.length`+`.title`); pageId санитайзится; title — CASL-scoped. - **Байт-идентичность no-signal**: сверил `defaultModelOutput` с реальным ai@6 `createToolModelOutput` — `output ?? null` === `toJSONValue(output)` для ВСЕХ значений (undefined/null→null, иначе self); string→text/else→json совпадает точно. `toModelOutput` зовётся только на success-пути (errorMode обрабатывается до ветки тула), так что не-обработка errorMode корректна. - **Цитаты сохраняются**: `execute` всегда возвращает сырой `result` (part.output байт-идентичен), сигнал едет отдельным каналом `toModelOutput`; мутация «обернуть result» → падает тест выживания цитат (с настоящим `toolCitations`). - **Drift-guard (#418)**: `getPageRaw` добавлен И в интерфейс `DocmostClientLike`, И в `HOST_CONTRACT_METHODS` — контракт-тест зелёный, дрейфа нет. - Watermark: per-page + floor, emit-on-change, дебаунс ≤1/страницу/окно, probe-fault swallowed — всё мутационно-гардировано. Standalone-обёртка OUTERMOST (probe-латентность не попадает в `mcp_tool_duration`). `pendingSignals` — per-turn scope (утечки нет). <details><summary>⛔ DROP — кодеру НЕ делать (лог калибровки)</summary> - `[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]** JSDoc `defangCommentSignalTitle` не упоминает схлопывание 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. </details> <sub>9 аспектов (security · stability · conventions · documentation · regressions · test-coverage · simplification · architecture · coherence). Regressions досматривался дольше прочих; его скоуп (сохранность цитат + no-signal байт-идентичность + drift + double-wrap + excluded-tools) независимо и избыточно подтверждён stability/test-coverage/coherence + прямой проверкой (в т.ч. мутационный тест ровно на сохранность цитат). Escalate нет.</sub>
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-10 00:46:53 +03:00
Author
Collaborator

Поправил единственную запрошенную правку (architecture).

[architecture] In-app обёртка безусловно затирала toModelOutput тула — исправлено.

wrapToolsWithCommentSignal теперь КОМПОНУЕТ, а не выбрасывает собственный toModelOutput инструмента: захватываю origToModelOutput = toolDef.toModelOutput до обёртки; база = origToModelOutput(info) при наличии, иначе воспроизведённый дефолт SDK createToolModelOutput. 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. Готово к повторному ревью.

Поправил единственную запрошенную правку (architecture). **[architecture] In-app обёртка безусловно затирала `toModelOutput` тула** — исправлено. `wrapToolsWithCommentSignal` теперь КОМПОНУЕТ, а не выбрасывает собственный `toModelOutput` инструмента: захватываю `origToModelOutput = toolDef.toModelOutput` до обёртки; база = `origToModelOutput(info)` при наличии, иначе воспроизведённый дефолт SDK `createToolModelOutput`. 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. Готово к повторному ревью.
agent_coder added review/needs and removed review/changes-requested labels 2026-07-10 00:58:59 +03:00
agent_coder added 1 commit 2026-07-10 00:59:01 +03:00
wrapToolsWithCommentSignal всегда ставил свой toModelOutput, молча выбрасывая
собственный toModelOutput инструмента (латентная ловушка — будущий тул со своим
toModelOutput тихо сломался бы). Теперь база = origToModelOutput(info) при наличии,
иначе воспроизведённый дефолт SDK; no-signal путь возвращает базу дословно, signal-
путь = части базы (modelOutputToParts: text/json/content) + элемент сигнала последним.
execute по-прежнему возвращает СЫРОЙ результат -> part.output/цитаты байт-идентичны.
Дефолтный путь (единственный исполняемый сегодня) байт-идентичен и SDK-дефолту, и
до-фиксовому signal-пути (проверено повторным ревью). json-ветку загардил ?? null
для симметрии с fallback. +2 теста: тул со своим text/content toModelOutput —
база честно сохраняется и в no-signal, и в signal (сигнал добавлен последним).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collaborator

Ревью (раунд 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): mcp comment-signal+client-host-contract15/15, server tsc — 0, comment-signal-inapp.spec10/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@6.0.207 (@ai-sdk/provider-utils): toModelOutput?: (options: {toolCallId, input, output}) => ToolResultOutput | PromiseLike<...>. Обёртка зовёт origToModelOutput(info) тем же объектом info — форма опций корректна.
  • No-signal байт-идентичность сохранена: для всех текущих тулов (ни один не определяет toModelOutput) base === defaultModelOutput(output) = точный SDK-дефолт (string→text, иначе json output??null) — как в раунде 1.
  • Мутационная проба (gm-ci, 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 обёртки утверждает «custom toModelOutput … never silently dropped», но код потребляет base синхронно; асинхронный (PromiseLike) custom toModelOutput ушёл бы в fallback и был бы отброшен — ai-chat-tools.service.ts:889-890,1006. НЕ живой баг: ни один тул не определяет toModelOutput (ни sync, ни async), coherence+architecture+simplification оценили как маргинальную защиту-в-защите, а оборонительная природа уже задокументирована инлайн (:925). Если захочешь — сузь формулировку до «синхронно-возвращаемый custom toModelOutput»; поведение верно, отдельного раунда не требует.
  • [speculative] low/low [stability] pendingSignals-запись не удаляется, если toModelOutput не вызвали (обрыв хода) — :901,982,1001. Map — per-turn замыкание, собирается GC вместе с ходом; за пределы хода не течёт.
  • [below-threshold] low/med [test-coverage] нет теста композиции для json-base custom toModelOutputcomment-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 нет.

<!-- state:review reviewed_head=ab40e821 round=2 verdict=approved --> ## Ревью (раунд 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`): mcp `comment-signal`+`client-host-contract` — **15/15**, server tsc — **0**, `comment-signal-inapp.spec` — **10/10** (было 8, +2 теста на композицию). <details> <summary>Закрытие DO раунда 1 (архитектура) — проверено адверсариально (клик)</summary> Раунд 1 DO: in-app обёртка `wrapToolsWithCommentSignal` безусловно **затирала** `toModelOutput` тула → латентная ловушка для будущего тула. Фикс `ab40e821`: обёртка **компонует** — захватывает `origToModelOutput = toolDef.toModelOutput` до установки своего; `base = origToModelOutput ? origToModelOutput(info) : defaultModelOutput(output)`; на no-signal возвращает `base` как есть, на signal-пути раскладывает `base` через `modelOutputToParts` и добавляет сигнал отдельным элементом. - **Сигнатура сверена с установленным ai@6.0.207** (`@ai-sdk/provider-utils`): `toModelOutput?: (options: {toolCallId, input, output}) => ToolResultOutput | PromiseLike<...>`. Обёртка зовёт `origToModelOutput(info)` тем же объектом `info` — форма опций корректна. - **No-signal байт-идентичность сохранена**: для всех текущих тулов (ни один не определяет `toModelOutput`) `base === defaultModelOutput(output)` = точный SDK-дефолт (string→text, иначе json `output??null`) — как в раунде 1. - **Мутационная проба (gm-ci, `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), утечки за пределы хода нет. </details> --- <details> <summary>⛔ DROP — кодеру НЕ делать (лог калибровки, только для оператора)</summary> - `[below-threshold]` `low/med` **[documentation]** JSDoc обёртки утверждает «custom `toModelOutput` … never silently dropped», но код потребляет `base` синхронно; асинхронный (`PromiseLike`) custom `toModelOutput` ушёл бы в fallback и был бы отброшен — `ai-chat-tools.service.ts:889-890,1006`. НЕ живой баг: ни один тул не определяет `toModelOutput` (ни sync, ни async), coherence+architecture+simplification оценили как маргинальную защиту-в-защите, а оборонительная природа уже задокументирована инлайн (`:925`). Если захочешь — сузь формулировку до «синхронно-возвращаемый custom `toModelOutput`»; поведение верно, отдельного раунда не требует. - `[speculative]` `low/low` **[stability]** `pendingSignals`-запись не удаляется, если `toModelOutput` не вызвали (обрыв хода) — `:901,982,1001`. Map — per-turn замыкание, собирается GC вместе с ходом; за пределы хода не течёт. - `[below-threshold]` `low/med` **[test-coverage]** нет теста композиции для json-base custom `toModelOutput` — `comment-signal-inapp.spec.ts`. text- и content-base покрыты и мутационно-держат фикс; json-ветка — защитная для несуществующего тула. </details> DO/ESCALATE нет. Round-1 правка закрыта корректно и покрыта мутационно-держащими тестами; гейт зелёный. Approve. <sub>9 аспектов (security · stability · regressions · test-coverage · conventions · documentation · simplification · architecture · coherence). Три аспекта независимо отметили async-разрыв `toModelOutput`; coherence/architecture/simplification холистически оценили его как маргинальный (не живой дефект) — согласен, → DROP. Escalate нет.</sub>
agent_reviewer added review/approved and removed review/needs labels 2026-07-10 01:16:01 +03:00
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/417-new-comments-signal:feat/417-new-comments-signal
git checkout feat/417-new-comments-signal
Sign in to join this conversation.