feat(ai-chat): персист tool-error частей — упавшие тулы видны в истории и сохраняют текст ошибки #426
Open
agent_coder
wants to merge 2 commits from
feat/407-persist-tool-errors into develop
pull from: feat/407-persist-tool-errors
merge into: vvzvlad:develop
vvzvlad:main
vvzvlad:perf/400-collab-session
vvzvlad:refactor/414-dedup-node-ops
vvzvlad:feat/417-new-comments-signal
vvzvlad:feat/408-createcomment-hints
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#426
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/407-persist-tool-errors"
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
Когда агентский тул бросает исключение (штатный режим для retryable-ошибок — throw в
ai-chat-tools.service.ts:442), ошибка показывалась модели вживую, но никогда не персистилась: в ai@6 упавший тул — этоtool-errorчасть вstep.content({type,toolCallId,toolName,input,error}), а не элементtoolResults. Итог:serializeStepsписал в колонкуtool_callsтолько вызовы+успешные результаты (ошибка терялась, оставался orphan tool-call без результата), аassistantPartsэмитил заглушку'Tool call did not complete.'— реальный текст ошибки терялся для мультиходового реплея, и модель на следующем ходу не знала, почему упало, и повторяла ту же ошибку. По анализу БД (см.docs/reading-ai-logs.md) наивный скан ошибок показывал 0 ошибок там, где UI показывал повторяющиеся красные фейлы.Исправление:
StepLikeрасширен полемcontent; новый хелперnormalizeToolError(Error/string/object → строка, обрезка через существующийcompactValue/лимиты — новый лимит не вводится).serializeSteps: на каждыйtool-errorпушит парный{toolName, error}тем же паттерном, что успешный{toolName, output}→ колонкаtool_callsфиксирует сбой и его причину.assistantParts: при наличии совпавшегоtool-errorэмититoutput-errorс реальным текстом ошибки; заглушка остаётся только для по-настоящему непарных вызовов (прерванных mid-step).docs/reading-ai-logs.mdпереписан под новую персистируемую форму + cutover-оговорка (строки до фикса сохраняют старую двухчастную форму).Обратно совместимо: старые строки читаются как раньше,
error-элемент аддитивен. Живой throw-путь (ai-chat-tools.service.ts:442) не тронут.closes #407
How verified
pnpm --filter ./apps/server exec jest src/core/ai-chat/ai-chat.service.spec.ts --maxWorkers=2→ 88 passed, 88 total (включая +4 новых: реплей несёт реальный errorText; прерванный вызов → заглушка; serializeSteps фиксирует{toolName,error}; 5000-символьная ошибка обрезается).tool-error/output-error/errorText) сверена внутренним ревью сnode_modules/ai/dist/index.d.ts(ai@6) — совпадает точно.prom-client-резолв в metrics.registry.ts — артефакт симлинка node_modules, к этому изменению не относится.)Внутренний цикл: 1 проход coder → внутреннее ревью (сверка с SDK-типами + прогон тестов) → замечаний нет.
Checklist
В ai@6 упавший тул — это tool-error часть в step.content ({type,toolCallId, toolName,input,error}), а не элемент toolResults. Раньше serializeSteps писал только toolCalls+toolResults (ошибка терялась, orphan tool-call без результата), а assistantParts эмитил заглушку 'Tool call did not complete.' (реальный текст ошибки терялся для мультиходового реплея — модель не знала, почему упало, и повторяла ошибку). - StepLike расширен полем content; новый хелпер normalizeToolError (Error/string/ object -> строка, обрезка через существующий compactValue/лимиты). - serializeSteps: на каждый tool-error пушит парный {toolName, error} тем же паттерном, что успешный {toolName, output} -> колонка tool_calls фиксирует сбой. - assistantParts: при наличии tool-error эмитит output-error с РЕАЛЬНЫМ текстом; заглушка остаётся только для по-настоящему непарных вызовов (прерванных). - docs/reading-ai-logs.md обновлён под новую форму + cutover-оговорка. Обратно совместимо: старые строки читаются как раньше, error-элемент аддитивен. closes #407 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Ревью — #426 · feat(ai-chat): персист tool-error частей (closes #407)
Вердикт: ПРАВКИ (CHANGES) — одна правка, и та в доке. Код отличный:
serializeSteps/assistantPartsчитают ai@6tool-errorизstep.content, фиксируют ошибку вtool_callsи реплеят реальный текст черезoutput-error;normalizeToolErrorустойчив; форма частей SDK подтверждена поai@6.0.134d.ts. Но переписанный док сам себе противоречит.Сделать — потом повторное ревью
reading-ai-logs.mdпротиворечит новомуerror-полю в секции «READ THIS» —docs/reading-ai-logs.md:66-67Строка 66 (заглавная секция «How tool calls are stored — READ THIS») утверждает: «The only keys that ever appear on an element are
toolName,input,output. There is nostate, noerrorText, notype». Но этот же PR добавляет ключerrorи ниже (стр. 98-219) подробно его описывает («New rows: query theerrorfield directly», «is_result … counts anerrorelement as a paired result»). Т.е. док-апдейт (заявленный в PR как выполненный) оставил самый заметный раздел фактически неверным для новых строк. Проверено против кода:serializeStepsреально пушит{toolName, error}(ai-chat.service.ts:2089-2093). Fix: на стр. 66-67 добавитьerrorкак возможный ключ для строк после #407 (напр. «…toolName,input,output, и (для брошенных ошибок, строки после #407)error»).Как проверено (гейт, clean-room
/home/claude/gm-ci)apps/servertsc (tsconfig.build) — PASS (0 ошибок).ai-chat.service.spec— 88/88 PASS (вкл. +4: реплей несёт реальный errorText; прерванный → заглушка; serializeSteps фиксирует{toolName,error}; 5000-симв. ошибка обрезается).apps/server/node_modules/ai/dist/index.d.ts(ai@6.0.134):tool-errorcontent-part несёт{type, toolCallId, toolName, input, error}(StaticToolError/DynamicToolError,toolCallId: string— required);state:'output-error'валиден только сerrorText: string. Читаемая PR-ом форма точна.else if (errorsById.has…)→ падает тест реального errorText; снять tool-error-цикл в serializeSteps → падает;normalizeToolError→ "" → падает 3 теста. Ядро (персист+реплей реальной ошибки) покрыто.result > error > stubкорректна (result/error взаимоисключающи по SDK); заглушка теперь только для реально прерванных (aborted mid-step); живой throw-путь (ai-chat-tools.service.ts:442) не тронут; error-текст ограничен существующимcompactValue(600 симв. / 200KB), новый лимит не вводится.⛔ DROP — кодеру НЕ делать (лог калибровки)
[speculative]medium [coherence] асимметрия фильтра tool-error:assistantPartsгардит&& part.toolCallId,serializeSteps— нет —ai-chat.service.ts:1809,2088— недостижимо: SDK-типы гарантируютtoolCallId: string(required, сверено в d.ts); и даже при мисматче serializeSteps аудитит, а assistantParts падает на stub — это корректно по назначению (id-less ошибку нельзя спарить для реплея), не потеря.[below-threshold]high [test-coverage]normalizeToolErrorветки object-with-message /String(non-string)без теста —ai-chat.service.ts:1760-1771— чистый хелпер, ядро мутационно-гардировано; ветки тривиальны (вернуть.message/String()), рисковый путь покрыт.[below-threshold]medium [test-coverage] нет теста round-tripoutput-errorчерезconvertToModelMessages— формаoutput-errorунаследована от уже проверенного в проде stub-пути (идентичный shape), не новый риск этого PR.[below-threshold]low [test-coverage] точная семантика обрезки (длина превью/маркер/граница 600) не заассерчена — переиспользуется предсуществующий протестированныйcompactValue; наличие обрезки покрыто.9 аспектов (security · stability · conventions · documentation · regressions · test-coverage · simplification · architecture · coherence). Security: персист error-текста — same-boundary (ошибка уже показывалась модели вживую и в UI); normalizeToolError без инъекций,
compactValue→stripNulChars→JSONB. Stability:normalizeToolErrorкрэш-безопасен даже при бросающем.message-геттере (внешний try-catch → graceful failed-insert). Escalate нет.Поправил единственную запрошенную правку (документация).
[documentation]
reading-ai-logs.mdпротиворечил новомуerror-полю — исправлено.Заглавный абзац секции «How tool calls are stored — READ THIS» (стр. 66-67) больше не утверждает, что ключей только три: теперь
errorявно указан как возможный ключ для брошенных ошибок на строках после #407 (со ссылкой на секцию «Hard failures»), и оговорено, что на pre-#407 строках брошенная ошибка вообще не имеет парного результата (тихий orphan). Заодно поправил два следствия: подсчёт инвокаций = элементы сoutputилиerror; пайринг — успех несётoutput, брошенная ошибка (post-#407) несётerror. Теперь заглавная секция согласована с детальным описанием ниже (стр. 98-219).Только markdown, кода не касалось. Готово к повторному ревью.
Ревью — #426 (fix-round) · doc-противоречие reading-ai-logs
Вердикт: OK (PASS). Правка внесена полно и согласованно; проверено — новых противоречий не внёс.
Что проверено (head
96faa282, дельта654ba9f2..96faa282)docs/reading-ai-logs.md; код (ai-chat.service.ts) не тронут (остаётся версией, что я уже подтвердил зелёной: tsc PASS, jest 88/88, форма SDK сверена).errorтеперь перечислен (для брошенных сбоев, строки после #407, со ссылкой на «Hard failures»); отдельно отмечены pre-#407 orphan'ы.outputилиerror» — теперь совпадает с предикатомis_resultниже (стр. 219, где error-элемент уже считался paired result); (2) pairing: success несётoutput, брошенный сбой (post-#407) несётerror.tool_calls(это UIMessage-поля, в БД не пишутся;error— пишется).DO прошлого раунда закрыт полностью, побочек нет. Апрув. Лейбл →
review/approved.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.