perf(ai-chat): дисциплина данных — дедуп tool-outputs + токен-бюджет реплея (#490) #510
Open
agent_coder
wants to merge 6 commits from
feat/490-data-discipline into feat/489-tool-resilience
pull from: feat/490-data-discipline
merge into: vvzvlad:feat/489-tool-resilience
vvzvlad:main
vvzvlad:feat/370-page-versioning
vvzvlad:feat/488-client-fsm
vvzvlad:ci/476-publish-gates
vvzvlad:feat/489-tool-resilience
vvzvlad:feat/487-terminal-lifecycle
vvzvlad:fix/486-iter1-prod-fires
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: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: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/needs
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#510
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/490-data-discipline"
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
Итерация 5 эпика #497 — дисциплина данных: дедуп tool-outputs + токен-бюджет реплея. closes #490. 6 коммитов по порядку.
tool_callsv2 — outputs больше НЕ дублируются в трейсе (только вmetadata.parts):{toolName,input}+{ok:true}/{error,kind:thrown|interrupted}, пейринг поtoolCallId. Старые строки НЕ мигрируются (маркер эрыmetadata.toolTraceVersion:2);docs/reading-ai-logs.mdпереписан dual-shape.compactToolOutputпо identity шага (WeakMap) — один stringify на шаг, не O(N²).@docmost/token-estimate, chars/2.5; клиентский счётчик переведён на тот же модуль) +trimHistoryForReplay(детерминированный, byte-stable, пейринг сохранён) + реактивная ветка overflow.ai_chats.metadata(миграция20260707T120000, строго новее всех существующих — коллизии таймстампа нет).updatedAt.Осознанная развилка (реактивная ветка) — нужно решение
Спека требовала in-turn ре-трай (переполнение → агрессивный ре-трим → ОДИН ретрай в том же ходу). Это архитектурно несовместимо:
pipeUIMessageStreamToResponseвызываетresponse.writeHead(200)СИНХРОННО в начале пайпинга (проверено вai@6.0.207dist), overflow-400 приходит черезonErrorуже после коммита заголовков — in-turn ре-пайп портит SSE-поток. Реализовано как next-turn: классифицировать 400 → штампmetadata.replayOverflow→ следующий ход режет агрессивно (0.5×, игнорируя устаревший usage). Инвариант «чат не брикуется НАВСЕГДА» держится (каждый следующий ход восстанавливается), но ПЕРВЫЙ переполняющий ход всё же отдаёт ошибку (деградация против прозрачного in-turn). Внутреннее ревью форк подтвердило как корректный.Известное ограничение (follow-up, не блокер): если реальное окно модели < 50k И
chatContextWindowне задан — плоская доля 0.5× не эскалирует → остаточный брик. Строго лучше базы (там был тотальный брик), чинится конфигом, warn-логируется. Стоит завести follow-up на эскалацию доли / деривацию дефолта из окна модели.How verified
jest --maxWorkers=2: 212 suites / 2513 passed / 0 failed (из чистого состояния). mcpnode --test test/unit test/mock: 833/833. Клиентский оценщик + token-estimate parity зелёные.tsc: 0 ошибок в тронутых файлах (3 предсущ. drawioimport.metaна базе).Checklist
Стоит стопкой на #489 (PR #508, approved) → #487 → #486. Стенду нужен
pnpm --filter server migration:latest(новая колонкаai_chats.metadata).Каждый tool-output хранился ДВАЖДЫ: в metadata.parts (assistantParts) И в tool_calls (serializeSteps). При 50-шаговом ране с outputs по 50–200 KB это 127–510 МБ записи в Postgres за ход (+WAL/TOAST/dead tuples), т.к. onStepFinish переписывает всю строку. Копия в parts — та, что реально реплеится модели и рендерится UI/markdown-экспортом, так что копия в трейсе была чистым дублем. Новый формат элементов tool_calls (v2), парно на каждый вызов: {toolName, input} — вызов {toolName, ok: true} — успех (БЕЗ output) {toolName, error, kind: 'thrown'} — брошенный tool-error {toolName, error, kind: 'interrupted'} — прерван mid-step (abort/restart) kind обязателен: синтетический «Tool call did not complete.» при прерывании иначе неотличим от реального hard-fail и загрязняет error-rate. Различие структурное (errorsById-хит против синтетической ветки), НЕ per-tool классификатор — soft- маркеры в трейс не выносятся (остаются в metadata.parts). metadata.toolTraceVersion: 2 — маркер эры; старые строки НЕ мигрируются (перезапись гигантских jsonb — тот самый WAL-чарн). serializeSteps пейрит результаты/ошибки по toolCallId (как assistantParts); общая константа TOOL_CALL_INCOMPLETE_TEXT держит текст реплея и трейса в синхроне. docs/reading-ai-logs.md переписан dual-shape: ветвление по toolTraceVersion, soft-анализ v2 через metadata.parts, правило «не сравнивать агрегаты через границу эр». UI action-log и markdown-экспорт читают только parts — не затронуты. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Активированный set сбрасывался каждый ход → модель заново гоняла loadTools, чтобы переактивировать те же тулы (лишний round-trip на каждом ходу). Теперь набор персистится в metadata чата и сидируется на следующем ходу. - Миграция: jsonb-колонка metadata на ai_chats (default '{}'); db.d.ts дополнен вручную (AiChats.metadata: Generated<Json>). - seedActivatedTools(metadata, validDeferredNames): читает сохранённый набор, ПЕРЕСЕКАЯ с актуальными validDeferredNames — смена allowlist/ролей не воскресит несуществующий тул (иначе prepareAgentStep получил бы фантомное активное имя). Сид только при deferredEnabled. - Персист на завершении хода (once-guard, во всех терминальных ветках рядом со snapshotTurnEnd): детерминированно отсортированный набор, merge в существующий bag (другие ключи сохраняются), запись пропускается если ничего нового не активировано (обычный ход не даёт лишней записи). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>На ревью. Итерация 5 (#490) — дисциплина данных. Полное описание и главная развилка в теле PR; здесь — на что смотреть в первую очередь + калибровка.
Два потенциальных блокера — оба закрыты внутренним ревью (проверено независимо, не по прозе):
ai@6.0.207—pipeUIMessageStreamToResponseсинхронно коммититwriteHead(200)до того, как overflow-400 приходит черезonError; in-turn ре-пайп невозможен без порчи SSE. Next-turn восстанавливает инвариант «не брикуется навсегда» end-to-end (errored-строка не фильтруется из истории, следующий ход видит флаг и режет 0.5×). Первый overflow-ход всё же отдаёт ошибку — деградация против спеки, но не путь брика, которого спека бы избежала.20260707T120000-ai-chat-metadataстрого новее всех существующих (новейшая —20260706T120000-search-lookup-trgm); ни бэк-дейта, ни дубликата. Специально проверял после того, как ровно этот класс всплыл на соседнем PR.Известное ограничение (follow-up, НЕ блокер): плоская агрессивная доля 0.5× не эскалирует → при окне модели <50k И незаданном
chatContextWindowостаётся остаточный брик. Строго лучше базы, чинится конфигом, warn-логируется. Предлагаю follow-up-тикет на эскалацию доли / деривацию дефолта из реального окна модели — на твоё усмотрение.Внутренний цикл (калибровка): 1 проход ре-ревью, блокеров не внесено. Ключевое проверено на ЖИВОЙ БД: WAL-LSN дельта показала трейс-колонку 140.2MB→0.04MB (3206×) и полную строку −52% на несжимаемом payload (не мок — мок мерил бы не то, что болит). Гейт: 212 сьютов / 2513 passed + mcp 833. Стоит стопкой на #508 (approved).
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.