Closed
agent_vscode
wants to merge 1 commits from
feat/490-data-discipline into develop
pull from: feat/490-data-discipline
merge into: vvzvlad:develop
vvzvlad:main
vvzvlad:develop
vvzvlad:feat/557-apikey-phase3a
vvzvlad:feat/568-page-history-redesign
vvzvlad:feat/561-comments-redesign
vvzvlad:feat/530-search-semantic
vvzvlad:feat/566-worktime-modal-redesign
vvzvlad:feat/370-agent-save-version
vvzvlad:recover/506-apikey-ui
vvzvlad:feat/370-share-approved
vvzvlad:fix/520-recovery-escalation
vvzvlad:fix/554-literal-br
vvzvlad:feat/529-search-lexical
vvzvlad:feat/501-mcp-apikey-auth
vvzvlad:feat/506-apikeys-ui
vvzvlad:feat/395-work-time
vvzvlad:feat/502-mcp-md-modes
vvzvlad:feat/492-incremental-render
vvzvlad:fix/549-hardbreak-md-canon
vvzvlad:feat/542-resolve-undo
vvzvlad:fix/505-drawio-strip-comments
vvzvlad:fix/503-hardbreak-canon
vvzvlad:docs/471-breadcrumb-permission
vvzvlad:fix/534-mcp-spaceid
vvzvlad:feat/481-version-coherence
vvzvlad:fix/535-list-seam-coalesce
vvzvlad:fix/525-insert-unloaded
vvzvlad:fix/522-internal-links
vvzvlad:fix/507-drawio-cyrillic
vvzvlad:fix/541-reconnect-timeout
vvzvlad:feat/493-converter
vvzvlad:feat/370-page-versioning
vvzvlad:fix/515-code-emphasis
vvzvlad:fix/523-tree-dnd
vvzvlad:fix/517-toasts
vvzvlad:feat/495-tails
vvzvlad:feat/494-mcp-registry-guards
vvzvlad:feat/496-suggestions-audit
vvzvlad:integ/497-wave1
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: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/changes-requested
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#539
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?
Что случилось: #518 (resume-стек, итерация 6 эпика #497) имел базой
feat/490-data-discipline(стек), и его merge (04:13) произошёл ПОСЛЕ мержа #510feat/490 → develop(04:11) — в результате 8 коммитов итерации 6 (798a81ab..56c90d89: дельта-эндпоинт, step-маркер, step-aligned registry + tail-only attach, клиентская активация, два ре-ревью-фикса) легли в уже-влитую веткуfeat/490и в develop не попали. Этот PR доставляет их (дифф = ровно состав #518).⚠️ До мержа — один долг: мой ревью-WARNING по #518 (комментарий там) НЕ закрыт: вход в reconnect-лестницу ждёт
getRun(cid)без таймаута (chat-thread.tsx, onFinish/disconnect-ветка) — reject-путь обработан отлично (dropLivePartialAndReplayFromStart), но HANG не ограничен ничем → FSM залипает вstreamingбез баннера/поллинга. Просьба к кодеру: докинуть fixup-коммит наfeat/490-data-discipline(Promise.race getRun против таймаута ~4–5 c с фолбэком drop+replay-from-start+enterReconnect — тот же путь, что на reject) + юнит; после этого — мерж.Сопутствующие follow-up'ы из ревью #518 (не блокируют): тест delta-обвязки окна, property-тест реконструкции, ведущий tick поллинга.
closes #491
Degraded-poll рефетчил ВСЕ страницы infinite-query каждые 2.5 c с полными parts. Вводим дельта-эндпоинт «строки, изменённые после курсора»: - POST /ai-chat/messages/delta → { rows, cursor, run: {id,status}|null }. Курсор — таймстамп часов БД (now()), клиент эхом возвращает его каждый поллинг. Окно перекрытия now()−5s ловит строку, закоммиченную с updatedAt чуть раньше момента снятия предыдущего курсора на другом autocommit- соединении (одиночные UPDATE, длинных транзакций нет). Окно ГАРАНТИРУЕТ повторы → контракт: клиентский merge идемпотентен по id (mergeById). - Run-факт едет В дельте (отдельный /run-поллинг удвоил бы QPS — отвергнуто). - Все дельта-релевантные записи (message update/finalizeOwner/reconcile/sweep, run update/finalizeIfActive/markStopRequested/sweepRunning) стампуют updatedAt через SQL now(), а не app-clock new Date(): единая монотонная ось курсора, смешанные источники часов были независимым источником пропусков. - Клиент: сервис-функция getAiChatMessagesDelta + фиксация контракта идемпотентности mergeById тестом (свап degraded-поллинга на дельту — в коммите 3, вместе с re-seed-путём attach). Тесты (observable-property на живом Postgres, не моки): - дельта-семантика + монотонность курсора; - RACE «коммит позже с updatedAt раньше курсора» — ловится окном перекрытия (наивный updatedAt > cursor пропустил бы); - окно гарантирует повторы → идемпотентный merge; - updatedAt стампуется часами БД, а не app-clock (фейк системных часов в 2099 — стамп остаётся на времени БД); - контроллер: owner-gate, форма ответа, run-факт только {id,status}. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Персист отставал от live-стрима на целый текущий шаг, а run.stepCount как источник границы шага НЕгоден: recordStep — fire-and-forget, не атомарен с записью parts (рассинхрон сид↔маркер). Пишем маркер ТЕМ ЖЕ flush'ем, что и parts: - flushAssistant стампует metadata.stepsPersisted = число ЗАВЕРШЁННЫХ шагов, чьи parts лежат в ЭТОЙ строке (оба выводятся из finished → маркер не может разойтись с persisted parts). In-progress хвост (частичный шаг при error/abort или mid-stream flush) НЕ считается. Это step-alignment якорь, на котором строится resume-стек (ротация кольца по подтверждённому шагу N — коммит 3; attach режет хвост по «шаг > N»). - Контракт AiChatRunService.reconstructRunParts(runId) → { parts, stepsPersisted } — единственный интерфейс чтения ЖИВОГО рана (run → assistantMessageId → строка → чистый reconstructPartsFromRow). null при отсутствии рана / связанной строки / удалённой строки; маркер 0 у pre-#491 строки — безопасный пол. Потребители: attach (коммит 3), дельта (rows уже несут маркер в metadata), экспорт. messageRepo — опциональный 3-й параметр конструктора (2-арг тест-конструкции компилируются без изменений). - /messages и дельта отдают маркер в row автоматически (он внутри metadata). Тесты: property «stepsPersisted == число завершённых шагов для любого N + parts согласованы»; частичный хвост не инкрементит маркер; reconstructPartsFromRow (маркер, дефолт 0, фолбэк на content); reconstructRunParts (резолв + null-кейсы). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Реестр ран-стримов больше не буферизует до 32МБ сырых SSE-кадров на активный ран и не выливает весь буфер в сокет синхронно при attach — это давало OOM на 1ГБ-контейнере при нескольких марафонских ранах. Теперь кольцо ограничено (env-настраиваемо, по умолчанию 4МБ) и держится в границах за счёт ротации по шагам. Серверная часть (суть коммита): - Штамповка кадров по шагам в ingestFrame. Штамп кадра = число `finish-step` кадров ДО него (с 0); сам finish-step несёт текущее значение, затем счётчик инкрементится. Так штамп совпадает с `metadata.stepsPersisted`: клиент с N персистнутыми шагами имеет 0..N-1 в сиде и просит хвост `stamp >= N`. Границу ловим дешёвым startsWith по `data: {"type":"finish-step"` — форма кадра проверена эмпирически против ai@6.0.207 (одна часть на кадр, type всегда первый ключ; кавычки в text-delta экранированы, ложных срабатываний нет). - Кольцо ротируется ТОЛЬКО на подтверждённом персисте шага N (`confirmPersistedStep`), сбрасывая кадры `stamp < N` (эти шаги уже на диске и придут в свежем сиде). `updateStreaming` теперь СИГНАЛИЗИРУЕТ исход (число персистнутых шагов или null), и ротация вызывается лишь при не-null возврате — провал персиста ничего не ротирует, кольцо покрывает БОЛЬШЕ (анти-инверсия: наивная ротация в .then() после НЕзаписанного шага дырявила бы гарантию). - Переполнение кольца сверх байтового капа вытесняет старейшие кадры; вытеснение ещё-не-персистнутого кадра открывает GAP. Гэп НЕ липкий: floor покрытия считается из кольца, поздний персист, проротировав дырявые шаги, его чистит. - attach(chatId, anchor, n): маркер шага N приходит ТОЛЬКО от клиента (сервер не читает строку — N из устаревшего сида дал бы тихую дыру в один шаг). Покрытие ОК ⟺ coverageFloor <= n; иначе 204 → клиент рефетчит (больший N) и переподключается. Хвост = синтетический `start`-кадр (ран-факт runId/chatId) + кадры `stamp >= n`. Инвариант 6 (нет кросс-ран реплея) сохранён через anchor; инвариант 4 (снапшот+регистрация в один синхронный тик) сохранён. N-срез применяется во ВСЕХ ветках, включая finished-retained: finished + N=N_final → пустой хвост + finish-кадр, клиент закрывает стрим. - Контроллер пишет хвост чанками с учётом drain (writeTailRespectingDrain), а не синхронным залпом (вторая половина OOM). Кап подписчика — производное 2× кап кольца, обе величины env-резолвятся на инстансе. Клиент: в тип строки добавлен `metadata.stepsPersisted` (источник N). PIN-SPEC трип-вайр на ai@6.0.207: `readUIMessageStream({ message })` продолжает последнее сообщение, `start`-кадр не сбрасывает parts, текст не пересекает finish-step — на этом держится продолжение при attach; апгрейд ai теперь падает громко. Тесты (observable-property против РЕАЛЬНОГО реестра/БД): детектор границы на реальной форме кадра, N-срез (в т.ч. посреди шага), ротация только на подтверждённом персисте, «персист провалился но кольцо влезло → attach успешен / провал + переполнение → 204», «устаревший N → 204 → после рефетча успех», очистка гэпа поздним персистом, finished-retained + N_final, memory-bound (5 параллельных марафонов сверх 32МБ, каждое кольцо ≤ кап). Обновлены registry/controller specs и DB-backed интеграционный attach-spec под новую сигнатуру/семантику. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Внутреннее ре-ревью: DB-backed delta-spec молча СКИПАЛСЯ — нулевое покрытие инварианта DB-clock курсора. - Импорт `import postgres from 'postgres'` (default) при tsconfig module:commonjs без esModuleInterop компилился в `postgres_1.default(...)`, а CJS-`postgres` не имеет `.default` → TypeError в beforeAll → пустой catch → reachable=false → все 6 тестов уходили в console.warn('SKIP') и return БЕЗ ассертов (suite оставался бы зелёным даже при регрессии на new Date()). Фикс: `import * as postgres from 'postgres'` (как в рабочем int-харнессе). - Хардненг харнесса: реальная ошибка программирования в beforeAll больше НЕ маскируется под «DB unreachable» — скип легитимен только для сетевого отказа (ECONNREFUSED и т.п.), иначе rethrow → suite падает громко. - Два DB-clock теста использовали jest.useFakeTimers() целиком, что замораживало внутренние таймеры postgres.js → awaited DB round-trip зависал на 5s-капе (и вешал afterAll). Фейкаем ТОЛЬКО Date (doNotFake всех таймеров) — запрос резолвится, а инвариант «стамп от часов БД, не app-clock» по-прежнему доказан (скос процесс-часов в 2099 → стамп остаётся на времени БД). Теперь все 6 тестов РЕАЛЬНО исполняются и зелёные против живого Postgres. Два дешёвых hardening из ревью: - registry coverageFloor: пустая ветка возвращает max(currentStamp, persistedFloor) — инвариант «клиент с n=persistedFloor всегда покрыт» структурный, а не тайминг-зависимый. - GetChatDeltaDto.cursor: @IsString → @IsISO8601 — битый курсор отсекается 400 на уровне DTO, а не 500 на `::timestamptz`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Переводит клиент на серверный tail-only контракт resume (задел commit 3), не трогая FSM run-fsm.ts — меняется только рантайм-обвязка в chat-thread.tsx. A. Убран STRIP-механизм. Seed теперь содержит ВСЕ персистнутые строки без изъятия хвоста (стриминговый хвост — это шаги 0..N-1, к которым SDK- продолжение дописывает tail). stripRef/strippedRowRef заменены на anchorRef { id, stepsPersisted } — персистнутая assistant-строка, питающая ?anchor=<id>&n=<stepsPersisted>. Восстановления stripped-строки на 204/NONE/starved удалены (строку никто не изымал — нечего восстанавливать); invalidateQueries + диспатчи FSM сохранены. Блок anchor-mismatch в reconcile сверяется по id из свежей персист-истории, а не по «живой» строке. B. Вход в attaching/reconnecting — ВСЕГДА через re-seed из персиста; «живой» стор НИКОГДА не база для tail-apply. На локальном FINISH_DISCONNECT (и на live-follow повторном дропе observer-а) сначала getRun(chatId) → замена «живой» частичной строки персистнутой по id (mergeById) + установка anchor, и лишь ПОСЛЕ этого диспатч RUN_FACT + FINISH_DISCONNECT (который планирует реконнект). Так attach не может продублировать частичный шаг N. Фильтр «живой» строки в resumeStream-эффекте убран (его заменяет re-seed). Инварианты FSM (I1 epoch-штамп, I4 honor-in-stopping, DISCONNECT-FIRST, сброс ownership на терминалах, render-gate) сохранены. C. URL attach: ?anchor=<id>&n=<stepsPersisted> при наличии якоря, без expect. D. Degraded-поллинг переведён с полного рефетча всех страниц на дельту: useAiChatMessagesQuery больше не поллит (seed один раз), а окно при degradedPoll раз в 2.5с зовёт getAiChatMessagesDelta(chatId, cursor) и идемпотентно по id мёржит строки в тот же infinite-query кэш через новый чистый хелпер mergeDeltaRowsIntoPages. Арминг/разарм (onResumeFallback) и idle-cap не тронуты. Хелперы: seedRows удалён; добавлены stepsPersistedOf и mergeDeltaRowsIntoPages (+ юнит-тесты на идемпотентность). Тесты chat-thread обновлены под новый URL, seed-без-стрипа, re-seed-из-персиста на дисконнекте (mutation-verify: падают без re-seed и при n мимо персиста) и 204→poll-без-restore. Весь ai-chat vitest зелёный (398), tsc без новых ошибок. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Ре-ревью нашло регрессию класса #137/#161 на пути отказа getRun при tail-only re-seed: - НАХОДКА 1 (MEDIUM/HIGH): в onFinish-disconnect ветка .catch (отказ getRun) на локальном дропе входила в reconnect-ladder БЕЗ ре-сида и БЕЗ фильтра «живой» частичной строки. anchorRef оставался устаревшим (mount-инициализатор), живая частичная строка со шагами N..M-1 — последней; через ~1с реконнект строил ?anchor=&n=N_mount, и при живом ране с покрытием от N_mount (flaky-сеть: SSE и getRun упали, сеть поднялась за 1с) сервер отдавал кадры ≥N_mount → SDK дописывал их к строке, где они УЖЕ есть → дублирование (клиентского дедупа реплея против parts нет). Фикс: восстановлена структурная гарантия удалённого resumeStream- фильтра — на отказе getRun (и на no-persisted-row, и на no-cid) живая частичная строка удаляется из стора по id + anchorRef=null → реконнект реплеит со start в ЧИСТЫЙ стор (полная пересборка) либо 204→poll. Нет пути, где attach tail-applies на строку с уже присутствующими шагами. Тест: getRun-reject на локальном дисконнекте → живая строка отфильтрована + URL без параметров (mutation-verify: без фикса тест краснеет — фильтр не срабатывает). - НАХОДКА 2 (LOW): RUN_FACT в enterReconnect теперь epoch-штампуется (epoch: stampEpoch), как везде (postRun): getRun-rtt расширяет окно onFinish→dispatch, конкурентный SEND_LOCAL во время rtt теперь дропает устаревший RUN_FACT по I1, а не перетирает runFact.runId нового хода. - НАХОДКА 3 (LOW, doc): run-fsm.spec.md обновлён — stripRef/strippedRowRef → anchorRef {id, stepsPersisted}, tail-only + re-seed-from-persist. FSM run-fsm.ts не тронут; инварианты #488 (epoch/honor-in-stopping/ownership-reset/ disconnect-first/render-gate) сохранены. Клиент ai-chat vitest 399 зелёный, tsc 0 ai-chat-ошибок; сервер delta(6, реально исполняется)/registry/step-marker/attach + integration attach — зелёное. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Дубль: кодер успел первым — доставка ушла через #540 (те же коммиты, с feat/491 напрямую, уже смержен). Незакрытый WARNING (getRun-hang таймаут) переоформлен отдельным ишью, т.к. блокировать больше нечего.
Pull request closed