Skip to content

fix(acp-bridge): bound live journal replay chunks - #8801

Merged
wenshao merged 10 commits into
QwenLM:mainfrom
wenshao:fix/live-journal-replay-chunks
Aug 10, 2026
Merged

fix(acp-bridge): bound live journal replay chunks#8801
wenshao merged 10 commits into
QwenLM:mainfrom
wenshao:fix/live-journal-replay-chunks

Conversation

@wenshao

@wenshao wenshao commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR compacts consecutive compatible assistant text and thought chunks in the bounded replay snapshot for an unfinished turn. Each replay entry represents at most 256 source events, while tool, attribution, provenance, discrete-message, and semantic metadata boundaries remain intact. Real-time EventBus/SSE delivery remains unchanged and still emits every source chunk.

The live journal event cap now applies to bounded replay entries. Byte accounting and truncation diagnostics continue to use the serialized source events, so the configured memory protection and source-event counts retain their existing meaning. CLI help, SDK types, status text, and protocol documentation are updated to describe those units explicitly.

Why it's needed

Long-running Web Shell sessions can receive hundreds of thousands of very small thought or message chunks during one unfinished turn. The previous live journal retained each chunk as a separate replay event, reached the 10,000-event limit quickly, and displayed a live-turn history truncation warning even though the retained text itself was relatively small. Compacting only the replay snapshot keeps the unfinished turn recoverable without changing real-time streaming or completed-turn compaction.

Reviewer Test Plan

How to verify

Start qwen serve with a small --max-journal-events value and use an ACP fixture that emits at least 20 consecutive compatible assistant chunks before completing the turn. While the turn is still active, load the session and confirm that the live journal contains the complete concatenated text without a history_truncated marker, and that the merged replay event uses the latest source event ID. Confirm that real-time SSE still exposes each original chunk. After the turn completes, load the session again and confirm that the normal completed replay still contains the full text.

Also verify that tool boundaries, different prompt/client/session attribution, transcript source record IDs, parent tool call IDs, generic discrete message/thought updates, update metadata, and non-timestamp envelope metadata prevent live-journal aggregation. A stream large enough to cross the byte cap should start a new bounded segment and report discarded/retained source-event counts correctly.

Automated verification completed locally:

  • packages/acp-bridge: npx vitest run src/compactionEngine.test.ts — 92 passed
  • packages/sdk-typescript: npx vitest run test/unit/daemonUi.test.ts — 284 passed
  • packages/cli: npx vitest run src/commands/serve.test.ts src/serve/fast-path.test.ts — 132 passed
  • npm run build
  • npm run typecheck
  • ESLint on all changed TypeScript files
  • npm run bundle
  • git diff --check

Evidence (Before & After)

Before: a live turn with a high-volume fine-grained stream reached the 10,000 raw-event journal cap and displayed a truncation warning despite the turn still running.

After: compatible source chunks share bounded replay entries, so the same live stream remains complete in the load snapshot without changing real-time chunk delivery. Completed-turn replay remains unchanged.

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

macOS, Node.js 22-compatible repository toolchain, sandbox disabled for local daemon E2E verification.

Risk & Scope

  • Main risk or tradeoff: live replay entry count no longer equals source-event count, so diagnostics and documentation must keep their units explicit; byte accounting remains conservative because it uses serialized source-event sizes.
  • Not validated / out of scope: this PR does not change cross-layer replay cursor behavior, EventBus ring replay, completed-turn compaction semantics, or Web Shell message rendering. Windows and Linux were not tested locally.
  • Breaking changes / migration notes: no wire schema or configuration migration is required. Existing journal options retain their names and defaults; only the event-cap unit changes from raw source events to bounded replay entries.

Linked Issues

N/A

中文说明见下方,点击展开。

中文说明

本 PR 做了什么

本 PR 对未完成 turn 的有界回放快照中连续且兼容的 assistant 文本和思考 chunk 进行聚合。每个回放 entry 最多代表 256 个源事件,同时保留工具、归属、来源、离散消息以及语义 metadata 边界。实时 EventBus/SSE 投递保持不变,仍然逐个发送所有源 chunk。

live journal 的事件上限现在作用于有界回放 entry。字节记账与截断诊断仍按序列化后的源事件计算,因此配置的内存保护和源事件计数保持原有含义。CLI 帮助、SDK 类型、状态文案和协议文档也同步明确了这些单位。

为什么需要

长时间运行的 Web Shell 会话可能在一个未完成 turn 中收到数十万个非常细粒度的 thought 或 message chunk。此前 live journal 会将每个 chunk 作为独立回放事件保留,很快达到 10,000 事件上限,并在 turn 仍运行时显示历史截断警告,即使实际文本量并不大。仅压缩回放快照可以保持未完成 turn 可恢复,同时不改变实时流式输出或完成 turn 的压缩逻辑。

Reviewer Test Plan

How to verify

使用较小的 --max-journal-events 启动 qwen serve,并让 ACP fixture 在完成 turn 前连续发送至少 20 个兼容的 assistant chunk。在 turn 仍活跃时加载会话,确认 live journal 包含完整拼接文本、没有 history_truncated marker,且合并回放事件使用最新源事件 ID。确认实时 SSE 仍暴露每一个原始 chunk。turn 完成后再次加载会话,确认正常的 completed replay 仍包含完整文本。

还应验证工具边界、不同 prompt/client/session 归属、transcript source record IDs、parent tool call IDs、通用离散 message/thought update、update metadata 和非时间戳 envelope metadata 都会阻止 live-journal 聚合。足以跨越字节上限的流应开启新的有界 segment,并正确报告丢弃和保留的源事件数量。

本地已完成以下自动验证:

  • packages/acp-bridge: npx vitest run src/compactionEngine.test.ts — 92 passed
  • packages/sdk-typescript: npx vitest run test/unit/daemonUi.test.ts — 284 passed
  • packages/cli: npx vitest run src/commands/serve.test.ts src/serve/fast-path.test.ts — 132 passed
  • npm run build
  • npm run typecheck
  • 对所有变更 TypeScript 文件运行 ESLint
  • npm run bundle
  • git diff --check

Evidence (Before & After)

Before:高频细粒度流的 live turn 会达到 10,000 个原始事件的 journal 上限,并在会话仍运行时显示截断警告。

After:兼容源 chunk 共享有界回放 entry,因此相同 live stream 可在 load snapshot 中保持完整,同时不改变实时 chunk 投递。completed-turn replay 保持不变。

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

macOS,兼容 Node.js 22 的仓库工具链,本地 daemon E2E 验证关闭 sandbox。

Risk & Scope

  • 主要风险或权衡:live replay entry 数不再等于源事件数,因此诊断和文档必须明确单位;字节记账仍使用序列化源事件大小,保持保守。
  • 未验证/范围外:本 PR 不改变跨层 replay cursor 行为、EventBus ring replay、completed-turn compaction 语义或 Web Shell 消息渲染。未在 Windows 和 Linux 本地测试。
  • 破坏性变更/迁移说明:不需要 wire schema 或配置迁移。现有 journal 选项名称和默认值不变;只有事件上限的单位从原始源事件调整为有界回放 entry。

Linked Issues

N/A

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head 6c2dad8, drove a fixed endpoint set against each, and diffed the JSON responses. Only fields that changed are shown.

No response changes against the PR base across 4 scenario(s).

Qwen Code · serve A/B

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NEEDS-CHANGES on one item — the engine itself is solid. The merge predicate checks every boundary I could ask for (parentToolCallId, sourceRecordIds, promptId, originatorClientId, sessionId, explicit-empty vs absent provenance, non-timestamp envelope _meta, discrete and unmodeled update meta), the 256-cap and byte-cap split arithmetic is correct, text is never silently dropped (every source event is merged, kept as its own entry, or evicted into the truncated count), ordering and last-source-id semantics are monotonic and consumer-tolerant, and the unfinished/completed gate is sound — all pinned across the 92-test suite.

Blocker: the entry-vs-event semantic change breaks two integration tests that were not updated, and that only escaped detection because Integration Tests (CLI, No Sandbox) is skipped for this PR's CI:

  • integration-tests/cli/qwen-serve-live-journal-recovery.test.ts:46-82 and integration-tests/cli/qwen-serve-webui-live-journal-recovery.test.ts:124-183 both start the daemon with --max-journal-events 3 and have the mock child emit 20 bare compatible agent_message_chunk events (the fixture emits {sessionUpdate, content} with no _meta). Pre-change that is 20+ raw entries > 3 → marker; now all 20 merge into one entry, the journal stays under the cap, history_truncated never appears, the first test's expect(duringTurn).toBeDefined() fails and the second's waitFor('visible live journal marker') times out. Suggested fix: rework both to force truncation with non-mergeable events (or a tiny --max-journal-bytes to force a segment split), and ideally add an assertion that a long compatible stream now stays marker-free.

Non-blocking notes:

  1. mergeLiveJournalTextEvent spread-merges the open-typed update ({...existing, ...incoming, content}), unlike the canonical reconstruction on the completed path. Verified unreachable today — MessageEmitter only emits sessionUpdate/content/_meta on chunks and every _meta variant it uses is blocked by hasUnmodeledTextMeta and pinned — but requiring the exact key set {sessionUpdate, content, _meta?} or reconstructing canonically would make that structural rather than contingent.
  2. The live marker sentence mixes units: 'kept the latest N events and dropped M older replay events (limits: X replay entries / Y bytes)' — N/M count source events while X counts entries. Documented, but easy for an operator to misread.
  3. Thought-chunk _meta now propagates on the live path too (parity fix, pinned by the new SDK test) — a user-visible SDK behavior change beyond replay, worth a release note.

CI note: Test (ubuntu-latest) on this head is red on the known backgroundShellRegistry retention-cap flake that #8797 deflakes (not in this branch yet); 19,563 tests including every PR-related suite pass, so it is unrelated to this change.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
wenshao and others added 2 commits August 9, 2026 20:25
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…ay-chunks

# Conflicts:
#	packages/core/src/services/backgroundShellRegistry.test.ts
@wenshao

wenshao commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the integration-test blocker in ba7f10c289 and rebased the branch onto current main via merge commit baf5dedb65.

  • The daemon recovery test now explicitly verifies that 20 compatible message chunks remain marker-free under --max-journal-events 3, while preserving both chunk-0 and chunk-19 in the live replay.
  • Both truncation-recovery tests now use --max-journal-bytes 300 to force replay-segment eviction, so they continue to exercise the truncation marker and completed-turn repair behavior under the new entry-based semantics.
  • Verified against the built bundle: both integration files passed, 3/3 tests. Build, typecheck, bundle, ACP compaction 92/92, SDK daemon UI 285/285, and core registry 57/57 also passed after merging current main.

已在 ba7f10c289 修复 integration test blocker,并通过 merge commit baf5dedb65 同步到最新 main

  • 新增验证:20 个兼容 message chunks 在 --max-journal-events 3 下聚合后不显示截断 marker,同时 live replay 保留 chunk-0chunk-19 的完整文本。
  • 两个截断恢复测试改用 --max-journal-bytes 300 强制 replay segment 淘汰,继续覆盖新 entry 语义下的截断 marker 与 completed-turn 修复流程。
  • 基于构建后的 bundle 验证:两个 integration 文件 3/3 通过;合并最新 main 后 build、typecheck、bundle、ACP 92/92、SDK 285/285、core registry 57/57 均通过。

doudouOUC
doudouOUC previously approved these changes Aug 9, 2026

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE (C=0) at baf5dedb653f9cee770308451e4af1ed5cc51b16.

Reviewed the full 15-file diff and traced the changed live-journal semantics through the bridge, CLI configuration/status surface, load snapshot consumers, SDK normalization/transcript reduction, and WebUI truncation repair. The merge predicate preserves type, tool, attribution, provenance, discrete-message, and semantic-metadata boundaries; the 256-source-event segment cap, conservative source-byte accounting, oldest-first eviction, source-event diagnostics, latest event ID, and turn-boundary reset behavior are consistent.

The earlier integration blocker is fixed on the current head: the compatible 20-chunk case now proves the complete live replay stays marker-free under the entry cap, while the truncation-recovery cases use the byte cap to exercise eviction. I verified the built bundle locally: both integration files passed (3/3), ACP compaction passed (92/92), SDK daemon UI passed (285/285), and build, bundle, full typecheck, ESLint on changed TypeScript, Prettier, and git diff --check all passed.

The remaining red Linux check is not a product-test failure: all 19,622 core tests passed, and the job exited only because Vitest reported a worker RPC timeout while calling onTaskUpdate. This is unrelated to the PR diff.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@wenshao

wenshao commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up for the Ubuntu CI failure on baf5dedb65: all 19,622 assertions passed, but backgroundShellRegistry.test.ts took 120.5s and then Vitest failed with Timeout calling "onTaskUpdate".

The log confirms the same fixed-sidecar collision pattern: ordinary status writes took ~351/702ms and the four retention tests took 23.2–23.9s each. The upstream 120s per-test timeout prevented assertion timeouts but did not remove the shared /tmp/s1.status collision or the worker RPC timeout.

Commit 3818d85f3f fixes the root cause in the test fixture: every default registry entry now writes its sidecar in a test-owned temporary directory, and the 120s timeout workaround is removed. No production code changed. Local result: 57/57 passed in 145ms.

补充说明 baf5dedb65 上的 Ubuntu CI 失败:19,622 个断言全部通过,但 backgroundShellRegistry.test.ts 耗时 120.5 秒,随后 Vitest 因 Timeout calling "onTaskUpdate" 退出失败。

日志再次确认是固定 sidecar 路径冲突:普通状态写入稳定耗时约 351/702ms,四个 retention tests 分别耗时 23.2–23.9 秒。上游增加 120 秒 test timeout 只能避免 assertion timeout,无法消除共享 /tmp/s1.status 冲突或 worker RPC timeout。

3818d85f3f 已从根因修复测试夹具:每个默认 registry entry 使用测试自己拥有的临时目录写 sidecar,并删除 120 秒 timeout workaround;未修改生产代码。本地验证 57/57 通过,耗时 145ms。

@wenshao

wenshao commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 97 passed · 0 failed · 97 total

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:97 通过 · 0 失败 · 97 总计

Verification report

PR 8801 verification — fix(acp-bridge): bound live journal replay chunks

Verdict: merge-ready — 97/97 scripted assertions passed, 0 unexpected failures. Verified head 3818d85f3fbdbddd755b27fd6c93fe510b7a7432 (merge ref 9952f6f948, base tip 0a3d7bb5c1).

中文摘要
  • 结论merge-ready。97/97 脚本化断言通过,0 个意外失败。
  • A/B 结论01-ab-live-journal-cap-base-vs-head.png):base 上 12 000 个细粒度 chunk 的 live turn 触发 history_truncated,丢弃 2 000 个源事件、最早文本丢失;head 上同一流聚合为 47 个有界回放 entry(每个 ≤256 源事件),无截断 marker,拼接文本逐字节完整,合并事件 id 为最新源事件 id。cap=3/20 chunk 的集成测试形状同样 base 截断 17、head 完整。实时 SSE 路径两臂行为一致(每个源 chunk 单独投递)。
  • 边界与字节记账(34 项断言):tool、归属(parentToolCallId/sourceRecordIds/promptId/originatorClientId/sessionId)、discrete/unmodeled meta、非时间戳 envelope meta、非 assistant 类型、非文本内容均阻止聚合;字节上限仍按序列化源事件记账,truncatedEvents + retainedEvents == 源事件数 不变式成立。
  • 测试有效性:7 个单点突变全部被 PR 自带测试杀死(02-mutation-matrix-all-mutants-killed.png),恢复后全绿;无幸存突变。
  • E2E(真实 daemon,03-e2e-real-daemon-sse-and-replay.png):SSE 逐帧收到全部 20 个源 chunk,turn 进行中 load 快照为合并 entry 且无截断 marker;PR 的 2 个集成测试 3/3 通过。
  • 未覆盖:per-commit 归因(浅克隆仅可达 5 个提交中的 1 个)、Web Shell 渲染、Windows/macOS、base 侧 daemon 级 E2E(引擎级 A/B 已覆盖变更面)。

Central claim + A/B

Central claim: during an unfinished turn, consecutive compatible assistant text/thought chunks share bounded replay entries (≤256 source events each), so the maxJournalEvents cap counts replay entries while byte accounting and truncation diagnostics keep source-event units; realtime SSE delivery is unchanged.

Harness: ab-harness.mjs drives TurnBoundaryCompactionEngine + EventBus from each tree's source (import closure is local-only — eventBus.ts/replayWindowLimits.ts are unchanged by the PR, so the only code difference between arms is compactionEngine.ts; no dependency/lockfile changes, so the shared root node_modules is a clean control). Witness: 01-ab-live-journal-cap-base-vs-head.png.

cell scenario base (0a3d7bb) head (3818d85)
default-cap-12000 12 000 message chunks, default caps (10 000 / 8 MiB) marker, truncated=2000, retained=10000, text starts at c2001; (oldest lost) no marker, 47 entries, text byte-complete, merged ids 256/512/…/12000
cap3-20 20 chunks, maxJournalEvents=3 (integration-test shape) marker, truncated=17, chunk-0 lost no marker, 1 merged entry, id=20, all 20 chunks retained
cap3-600-thoughts 600 thought chunks, cap 3 marker, truncated=597 no marker, 3 entries (ids 256/512/600), text complete
alternating-nomerge 20 alternating message/thought, cap 3 (A/A) marker, truncated=17, retained=3 identical (nothing merges across kinds)
bus-realtime 5 chunks through EventBus subscriber: 5 raw frames; journal 5 raw entries subscriber: 5 raw frames (identical); journal 1 merged entry, id=5

Totals: base arm 20/20 (all base failures occurred exactly as predicted), head arm 25/25. The flip is the load-bearing proof: the same stream that base truncates survives head with byte-identical text, and the realtime subscriber path is unchanged on both arms.

Secondary claims

  1. Aggregation boundaries holdboundary-harness.mjs, 34/34: tool_call/tool_call_update split segments; each attribution dimension (parentToolCallId, sourceRecordIds, promptId, originatorClientId, sessionId) splits, with a negative control showing the same dimension with equal values still merges; qwenDiscreteMessage (todo-stop-guard and generic backgroundTask), unmodeled update meta (usage, source), and non-timestamp envelope _meta all prevent merging while timestamp-only envelope meta still merges; user_message_chunk and non-text content never merge; byte cap starts a new segment before a merged entry exceeds maxJournalBytes, and truncatedEvents + retainedEvents equals the source-event count (30/30) under aggregation; turn completion still yields the full text via slots compaction with the journal reset; merged events carry the latest source id, last-event timestamp-only meta, and are wire-serializable.
  2. Realtime SSE still emits every source chunk — proven at two levels: (a) EventBus seam in both A/B arms (5/5 raw frames per subscriber, identical); (b) a transient wire probe against a real daemon (sse-probe.test.ts.archived, run and deleted): raw GET /session/:id/events with Last-Event-ID: 0 delivered replay_complete + 20 discrete session_update frames (chunk-0chunk-19) + turn_complete, while a mid-turn load on the same session returned ≤3 merged entries with no history_truncated and both endpoints of the text. Witness: 03-e2e-real-daemon-sse-and-replay.png.
  3. SDK/docs unit coherence — the truncation marker renders limits: 10000 replay entries / 8388608 bytes for scope: 'live_journal' (and keeps events for the compacted window), and thought.text.delta now carries update _meta so discrete thought messages stay separate in the transcript reducer.

Findings

None blocking. Two non-defect observations, recorded for the reviewer:

  • The PR body's local test counts (92/284/132) differ slightly from this round's (92/285/139) — consistent with the mid-PR merge of origin/main adding tests to those files; all suites are green at the verified head.
  • The metadata snapshot's baseRefOid (f3ba99f5…) predates the merge ref's base tip (0a3d7bb5…); the A/B used the local base tip per the merge-ref checkout contract.

Mutation matrix (vacuity)

All mutants are single-point edits to the changed source, run against the unmodified head test file; every kill failed with a behavioral assertion (quoted in mutations.log/sdk-mutations.log). Witness: 02-mutation-matrix-all-mutants-killed.png.

mutant guard introduced by PR result killed by
M0 unmutated control 92/92 green
M1 aggregation disabled killed (6 red) merges consecutive text chunks; keeps a long compatible text stream…; emits no marker while within caps; liveJournal contains bounded replay events
M2 256-chunk per-entry cap killed (2 red) keeps a long compatible text stream below the event cap; reports raw event counts when an aggregated segment is dropped
M3 byte-fit guard on merge killed (2 red) starts a new segment before a merged entry exceeds maxJournalBytes
M4 retainedEvents = source events killed (1 red) reports raw event counts when an aggregated segment is dropped
M5 timestamp-only envelope meta killed (1 red) preserves semantic envelope metadata event boundaries
R pristine restored 92/92 green
S1 replay entries limit string (SDK) killed (1 red of 285) describes live truncation precisely and preserves structured data
S2 thought.text.delta meta (SDK) killed (1 red of 285) keeps discrete thought messages separate with their metadata

7/7 mutants killed, no survivors; both restore controls green.

Targeted gates (all green)

gate result
packages/acp-bridge full suite (26 files) 1188/1188
packages/acp-bridge tsc --noEmit clean
packages/sdk-typescript daemonUi.test.ts (also the S-mutant control) 285/285
packages/cli serve.test.ts + fast-path.test.ts 139/139
packages/core backgroundShellRegistry.test.ts (head) 57/57, 3.9 s
same at base (A/A for the test-isolation commits) 57/57, 3.7 s
integration qwen-serve-live-journal-recovery + qwen-serve-webui-live-journal-recovery (real daemon, bundled CLI) 3/3
transient SSE probe (real daemon) 1/1

The mutation matrix doubles as the gate-liveness proof: the same vitest harnesses that report green were shown to go red on planted single-line changes.

Not covered

  • Per-commit attribution: shallow checkout reaches 1/5 commits listed in the metadata; verification is of the aggregate HEAD^1..HEAD diff.
  • Web Shell rendering of merged replay events (no browser harness here); the SDK reducer path is covered by daemonUi.test.ts and the WebUI integration test.
  • Base-side daemon E2E: the base bundle was not built (budget); the engine-level A/B covers the changed surface, and the pre-PR integration test shape (cap 3 + 20 chunks) pins the base symptom.
  • Repo-wide typecheck/lint (PR's own CI covers them); scoped tsc on the changed package ran clean.
  • Windows/macOS (container is Linux).
  • Cross-layer replay cursor behavior, EventBus ring replay, completed-turn compaction semantics — declared out of scope by the PR; regression-guarded by the full acp-bridge suite (1188 tests).

Methodology

Environment: node:22-bookworm CI container, merge-ref checkout (base 0a3d7bb5, head 3818d85f), pre-built dist/ and bundle. Harnesses (ab-harness.mjs, boundary-harness.mjs) import the engine from each tree's TS source via tsx, so the control differs from head only in compactionEngine.ts; the base tree was a scratch git worktree (removed after capture). E2E used the shipped bundle with the repo's mock ACP agent (MOCK_ACP_EMIT_CHUNKS=20) and a transient raw-SSE probe test that was deleted after the run. Raw logs: ab-base.log, ab-head.log, boundary-head.log, mutations.log, sdk-mutations.log, matrix-summary.txt in this directory; captures in evidence/.

Evidence images

01-ab-live-journal-cap-base-vs-head

02-mutation-matrix-all-mutants-killed

03-e2e-real-daemon-sse-and-replay

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Re-run at the new head (6c2dad8e), triggered by @wenshao — one commit since the last pass: the fix for @doudouOUC's verified Critical (ACP TextContent metadata lost from live replay).

Template looks good ✓

Problem: observed, not theoretical — a live turn streaming hundreds of thousands of fine-grained thought/message chunks deterministically exhausts the 10,000-entry live journal cap and surfaces a history_truncated warning while the retained text is still small. The area is anchored by the repo's own design doc (docs/design/live-journal-truncation-recovery.md, DAEMON-009 journal caps), and the sandboxed A/B run on an earlier head reproduced the failure: base truncated a 12,000-chunk live turn (2,000 source events lost, oldest text gone), head retained the same stream complete in 47 bounded entries.

Direction: aligned — a direct continuation of the live-journal truncation-recovery work, scoped to the unfinished-turn replay snapshot. Real-time SSE delivery and completed-turn compaction semantics are untouched; the live merge predicate is strictly more conservative than the completed path's (exact equality where the completed path does last-wins), so the two replay surfaces agree.

Size: cross-package (acp-bridge, cli, sdk-typescript) — 354 production logic lines vs 743 test lines and 21 doc lines, under the 500-line awareness threshold. The author is a collaborator with write access.

Approach: scope remains right, and the single new commit is exactly the outstanding Critical, nothing more — +11/−4 engine lines adding the content-sub-key gate to hasOnlyModeledChunkKeys (only content objects whose keys are exactly type/text may join a segment; richer chunks stay raw entries), plus 31 test lines pinning the reviewer's exact reproduction wire shape. No drive-by changes.

Risk: no high-risk path matches. One standing review-depth consequence: Integration Tests (CLI, No Sandbox) is skipped for fork PRs, so the integration-level behavior change is assessed through the sandboxed /verify lane — round 3 is in flight on this head; rounds 1–2 passed on the earlier heads.

Moving on to code review. 🔍

中文说明

在新 head(6c2dad8e)上重跑,由 @wenshao 触发——自上一轮以来新增一个 commit:修复 @doudouOUC 已验证的 Critical(live replay 丢失 ACP TextContent metadata)。

模板完整 ✓

问题:已观测到的问题,非理论性加固——长时间运行的 live turn 若流式输出数十万个细粒度 thought/message chunk,会确定性地耗尽 live journal 的 10,000 条回放 entry 上限,在实际文本量还很小时就显示 history_truncated 警告。该方向有仓库内设计文档支撑(docs/design/live-journal-truncation-recovery.md,DAEMON-009 journal 上限),且此前 head 上的沙箱 A/B 运行复现了该失败:base 会截断 12,000 chunk 的 live turn(丢失 2,000 个源事件,最早文本丢失),head 则把相同的流完整保留为 47 个有界 entry。

方向:对齐——这是 live-journal 截断恢复工作的直接延续,范围限定在未完成 turn 的回放快照;实时 SSE 投递与已完成 turn 的压缩语义均未改动。live 合并判定比 completed 路径更保守(completed 路径采用 last-wins 的地方这里要求精确相等),因此两个回放面保持一致。

规模:跨包改动(acp-bridge、cli、sdk-typescript)——生产逻辑 354 行、测试 743 行、文档 21 行,低于 500 行关注阈值;作者为有写权限的 collaborator。

方案:范围仍然合理,且唯一的新 commit 恰好就是那个待处理的 Critical、别无其他——引擎 +11/−4 行,为 hasOnlyModeledChunkKeys 增加 content 子键门禁(只有 content 键集恰为 type/text 的 chunk 才能加入 segment,更丰富的 chunk 保持原始 entry),外加 31 行测试锁定 reviewer 复现所用的确切 wire 形状。无顺带改动。

风险:未命中高风险路径。一个持续影响 review 深度的点:fork PR 的 CI 会跳过 Integration Tests (CLI, No Sandbox),因此集成层面的行为变化通过沙箱 /verify 通道评估——第 3 轮正在当前 head 上运行,第 1–2 轮已在更早的 head 上通过。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at 6c2dad8e0b96bcbade164b18fe174af390841467 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Code review

The mechanics are unchanged from the previous pass and still hold: appendLiveJournal merges consecutive compatible agent_message_chunk / agent_thought_chunk events into bounded segment entries (≤ 256 source events; a merge that would push the entry past maxJournalBytes starts a new segment instead), everything else keeps its own entry, the predicate refuses on any doubt (sessionUpdate kind, parentToolCallId, sourceRecordIds, promptId, originatorClientId, sessionId, discrete-message meta, unmodeled update meta, and non-timestamp envelope meta all break aggregation), byte accounting stays on serialized source events, eviction subtracts each dropped entry's true event count into journalTruncatedEvents, and the marker's retainedEvents reports source events — matching the updated docs, CLI help, and SDK wording.

What this head adds is exactly @doudouOUC's verified Critical — legal ACP TextContent metadata (annotations / _meta inside update.content) disappearing from load/recovery snapshots — fixed via the exact option his review proposed: hasOnlyModeledChunkKeys now gates the content sub-keys too, so only chunks whose content object carries exactly the keys type / text may join a segment; richer chunks stay raw entries and replay exactly as SSE delivered them. I traced it statically: liveJournalTextChunk is the only path into a segment and enforces that gate on top of requiring content.type === 'text' with a string text, so no annotation-carrying chunk can reach the {type, text} content rebuild — single-chunk segments included, since their content is rebuilt from an exactly-modeled shape. The fix also cannot introduce new merges — it only excludes chunks that previously merged lossily, a strict subset — so the regression risk is limited to "previously merged chunks now replay separately", which is the conservative direction. The new regression test pins the reviewer's exact reproduction shape (annotations: { audience: ['assistant'] }, _meta: { vendor: 'keep' } → replayed byte-identical with its id intact), with it.each coverage for unmodeled data-level and update-level keys alongside.

One observation, pre-existing and out of scope: the completed-turn path (mergeTextSlotmakeMergedSessionUpdateEvent) has always rebuilt content as {type, text}, so those same annotation-carrying chunks still shed the extra fields in the completed replay after turn_complete — real-time SSE and, now, the live replay keep them. This PR explicitly does not change completed-turn semantics; if ACP producers ever emit content annotations, that path deserves its own issue.

Everything else verified on the prior head remains addressed here (@yiliang114's integration-test blocker and three non-blocking notes; @wenshao's seven findings — exact-key gating, discrete/meta-shaped boundaries including the thought path, empty-string parentToolCallId parity, whole-segment byte-cap eviction docs, subagent merging, marker units). The round-2 /review on this head found only Suggestion-level items, which the takeover loop reviewed and answered with no further changes. Standing non-blocking note, carried forward: thought-chunk _meta now propagates on the live SDK path (parity fix, pinned by the new SDK test) — user-visible SDK behavior beyond replay, worth a release note.

Files changed (15)
File What changed
packages/acp-bridge/src/compactionEngine.ts Live-journal segment aggregation with the exact-key merge predicate; now includes content sub-key gating so TextContent annotations and _meta chunks stay raw entries
packages/acp-bridge/src/compactionEngine.test.ts +585 lines pinning merge boundaries, caps, eviction counts, segment splits, subagent and discrete meta, and the ACP TextContent regression
packages/acp-bridge/src/bridgeOptions.ts Journal cap docs: replay entries and source-event bytes
packages/acp-bridge/src/bridgeTypes.ts liveJournal doc comment
packages/cli/src/commands/serve.ts CLI help text for both journal flags
packages/cli/src/serve/types.ts ServeOptions doc comments
packages/sdk-typescript/src/daemon/types.ts DaemonRestoredSession.liveJournal doc comment
packages/sdk-typescript/src/daemon/ui/normalizer.ts Marker wording: source events vs replay entries; thought chunks now carry meta
packages/sdk-typescript/test/unit/daemonUi.test.ts Pins discrete thought meta and the new marker wording
integration-tests/cli/qwen-serve-live-journal-recovery.test.ts Marker-free aggregation test; truncation case on the byte cap with a serialized-tail assertion
integration-tests/cli/qwen-serve-webui-live-journal-recovery.test.ts Truncation case moved to the byte cap
docs/design/live-journal-truncation-recovery.md Entry-vs-event units
docs/developers/daemon/03-acp-bridge.md Entry-vs-event units
docs/developers/qwen-serve-protocol.md Entry-vs-event units
docs/users/qwen-serve.md Flag table units

Test evidence — the PR's own CI (the gate never builds or runs PR code)

Same structural gap as before: Integration Tests (CLI, No Sandbox) is skipped for fork PRs, as are the macOS/Windows unit legs — the ubuntu unit suite is the PR's own CI signal, and it completed green on the reviewed commit, including the acp-bridge compaction suite with the new TextContent regression test:

Check Conclusion
Classify PR ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Live Host (macos-latest) ✅ success
Real daemon E2E / Java 11 ✅ success
Serve A/B (ubuntu-latest, Node 22.x) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
macos-latest / Java 21 ✅ success
precheck-pr / precheck ✅ success
ubuntu-latest / Java 11 ✅ success
ubuntu-latest / Java 17 ✅ success
ubuntu-latest / Java 21 ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
windows-latest / Java 21 ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

The integration-level claim is not gate-verified by the PR's own CI on this head (the lane is skipped for forks), and the metadata-fix behavior itself is not observable from fork CI at all. Sandboxed verification would settle this: @qwen-code /verify — round 3 was triggered for this head by the /triage invocation and is in flight now; it A/Bs the new content-metadata gate (the exact behavior @doudouOUC reproduced) against the base build in an isolated container. Rounds 1–2 passed 97/97 and 140/140 scripted assertions on the earlier heads (real-daemon E2E 3/3, all mutants killed), but predate the fix commit, so their coverage does not include the gate. The round-3 report posts to this thread when the run completes.

Real-scenario testing: N/A for this run — the gate never executes PR-derived code (no checkout of the fork branch, no applying the diff to the working tree), and a tmux before/after arm would require building this PR. Live-behavior evidence is delegated to the isolated sandboxed lane above. Not verified locally: daemon-level behavior on this head — reason: static-gate rule; the sandboxed lane is in flight.

中文说明

代码审查

机制与上一轮一致且仍然成立:appendLiveJournal 将连续兼容的 agent_message_chunk / agent_thought_chunk 事件合并为有界 segment entry(≤ 256 个源事件;若合并会使该 entry 超过 maxJournalBytes,则改为开启新 segment),其他事件保持独立 entry;判定拿不准即拒绝合并(sessionUpdate 类型、parentToolCallIdsourceRecordIdspromptIdoriginatorClientIdsessionId、离散消息 meta、未建模 update meta、非时间戳 envelope meta 都会打断聚合);字节记账仍按序列化源事件计算;淘汰时按被丢弃 entry 的真实事件数累加 journalTruncatedEvents;marker 的 retainedEvents 报告源事件数——与更新后的文档、CLI 帮助和 SDK 措辞一致。

当前 head 新增的恰好就是 @doudouOUC 已验证的 Critical——合法 ACP TextContent metadata(update.content 内的 annotations / _meta)从 load/recovery 快照中消失——且采用的正是其评审中提议的方案:hasOnlyModeledChunkKeys 现在也对 content 子键设门禁,只有 content 键集恰为 type / text 的 chunk 才能加入 segment,更丰富的 chunk 保持原始 entry、按 SSE 投递原样回放。静态追踪确认:liveJournalTextChunk 是进入 segment 的唯一路径,除要求 content.type === 'text'text 为字符串外还强制该门禁,因此携带 annotations 的 chunk 不可能到达 {type, text} 重构——单 chunk segment 也不例外,其 content 由恰好建模的形状重构。该修复也不可能引入新的合并——它只排除了此前会被有损合并的 chunk(严格子集)——因此回归风险仅限于"此前合并的 chunk 现在分开回放",这是保守方向。新增回归测试锁定了 reviewer 复现的确切形状(annotations: { audience: ['assistant'] }_meta: { vendor: 'keep' } → 原样回放且 id 不变),并有 it.each 覆盖未建模的 data 级与 update 级键。

一点观察,属于既有行为、不在本 PR 范围内:completed-turn 路径(mergeTextSlotmakeMergedSessionUpdateEvent)一直把 content 重构为 {type, text},因此同样携带 annotations 的 chunk 在 turn_complete 之后的 completed replay 中仍会丢失这些字段——实时 SSE 以及现在的 live replay 都会保留它们。本 PR 明确不改动 completed-turn 语义;若未来有 ACP 生产者发出 content annotations,那条路径值得单开 issue。

上一个 head 上已验证的其他各项在当前 head 仍然成立(@yiliang114 的集成测试 blocker 与三条非阻塞意见;@wenshao 的七条意见——精确 key 门禁、离散/meta 形状边界(含 thought 路径)、空字符串 parentToolCallId parity、字节上限整体淘汰 segment 的文档、subagent 合并、marker 单位)。本 head 上的第 2 轮 /review 仅有 Suggestion 级意见,takeover 循环已逐条审阅并答复、无进一步改动。延续一条非阻塞提醒:thought chunk 的 _meta 现在也会在 live SDK 路径上传播(对齐修复,有新 SDK 测试锁定)——回放之外的用户可见 SDK 行为,建议写入 release note。

测试证据 —— 本 PR 自身的 CI(门禁从不构建或运行 PR 代码)

结构性缺口与之前相同:fork PR 的 CI 跳过 Integration Tests (CLI, No Sandbox),macOS/Windows 单测矩阵同样被跳过——ubuntu 单测套件是本 PR 自身的 CI 信号,且已在被审查 commit 上全绿完成(含带新 TextContent 回归测试的 acp-bridge 压缩套件,见上方表格)。

集成层面的结论尚未由本 PR 自身的 CI 在当前 head 上门禁验证(该通道对 fork 跳过),且 metadata 修复的行为本身完全无法从 fork CI 观测。沙箱验证用于补上这个缺口:@qwen-code /verify 第 3 轮已由本次 /triage 触发、正在当前 head 上运行——它在隔离容器中对新 content-metadata 门禁(即 @doudouOUC 复现的行为)与 base 构建做 A/B。第 1–2 轮在更早 head 上以 97/97 与 140/140 脚本断言通过(真实 daemon E2E 3/3、突变体全杀),但早于修复 commit,覆盖范围不含该门禁。第 3 轮报告将在运行完成后发布到本线程。

真实场景测试:本轮为 N/A——门禁从不执行 PR 衍生的代码(不 checkout fork 分支、不把 diff 应用到工作树),而 tmux before/after 需要构建本 PR。live 行为证据委托给上述隔离沙箱通道。未在本地验证:当前 head 上的 daemon 级行为——原因:静态门禁规则;沙箱通道正在进行中。

Qwen Code · qwen3.8-max

Reviewed at 6c2dad8e0b96bcbade164b18fe174af390841467 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — the verified Critical is resolved by the reviewer's own proposed fix, pinned by a regression test on his exact wire shape; what remains is process (a re-review and an in-flight verify round), not code.

Stepping back: the approach still matches what I would have proposed independently — merge at ingest (the only placement that actually bounds the journal), a predicate that refuses on any doubt, source-event byte accounting so the memory cap keeps its meaning, and the unit change spelled out on every doc/help/SDK surface. What decides this round is how the Critical landed: +11/−4 engine lines doing exactly what @doudouOUC suggested (admit only content objects whose own keys are type/text, leave richer chunks as raw entries), plus 31 test lines pinning the exact shape he reproduced — nothing else touched in the commit. I traced the gate statically: no annotation-carrying chunk can reach the content rebuild, and the fix cannot over-merge — it only excludes chunks that previously merged lossily, a strict subset — so the suite was extended, never loosened (743 test lines vs 354 production lines across the whole PR).

Three honest reservations, none blocking. First, the behavioral proof on this exact head is still in flight: /verify round 3 A/Bs the metadata gate against the base build, and rounds 1–2 validated everything except it; the unit regression is green in fork CI and the diff admits only a strict subset of the previously mergeable chunks, so the residual failure mode is narrow. Second, @doudouOUC's changes-requested review formally still stands — the Critical it names is addressed on this head via his own suggested approach, so a re-review from him clears it. Third, the pre-existing asymmetry noted in Stage 2: completed-turn replay still rebuilds content as {type, text}, out of scope here but worth a follow-up issue if ACP producers ever emit content annotations.

Verdict: approve — every pull_request CI run on the reviewed commit is green with none pending, and the approval is pinned to the reviewed commit.

中文说明

置信度:4/5 —— 已验证的 Critical 由 reviewer 自己提议的方案修复,并有锁定其确切 wire 形状的回归测试;剩余的是流程事项(re-review 与进行中的 verify 轮次),而非代码。

整体来看:方案仍然与我独立提出的做法一致——在 ingest 时合并(唯一能真正约束 journal 的位置)、拿不准即拒绝合并的判定、按源事件计字节以保持内存上限语义不变、单位变化在文档/帮助/SDK 各面逐一写明。决定本轮结论的是 Critical 的落地方式:引擎 +11/−4 行,恰好是 @doudouOUC 建议的做法(只允许 content 键集恰为 type/text 的对象加入合并,更丰富的 chunk 保持原始 entry),外加 31 行测试锁定他复现的确切形状——该 commit 没有其他任何改动。静态追踪确认:携带 annotations 的 chunk 不可能到达 content 重构;且该修复不可能过度合并——它只排除了此前被有损合并的 chunk(严格子集)——测试套件只被扩展、从未被放宽(整个 PR 743 行测试对 354 行生产代码)。

三点保留意见,均不阻塞。其一,当前 head 上的行为证明仍在进行中:/verify 第 3 轮正对 metadata 门禁与 base 构建做 A/B,第 1–2 轮验证了除它以外的一切;单元回归已在 fork CI 全绿,且 diff 只接受此前可合并 chunk 的严格子集,残余失败面很窄。其二,@doudouOUC 的 changes-requested review 形式上仍然有效——其指出的 Critical 已在当前 head 上按其本人建议的方案处理,re-review 后即可解除。其三,Stage 2 提到的既有不对称:completed-turn replay 仍将 content 重构为 {type, text},不在本 PR 范围内,但若未来有 ACP 生产者发出 content annotations,值得单开 issue 跟进。

结论:批准——被审查 commit 上所有 pull_request CI 运行均已全绿且无 pending,批准锚定在被审查的 commit。

Qwen Code · qwen3.8-max

Reviewed at 6c2dad8e0b96bcbade164b18fe174af390841467 · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@wenshao

wenshao commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Aug 9, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-dev-bot

qwen-code-dev-bot commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

AutoFix round 6 finishedview run. See this round's report below.

中文说明

AutoFix 第 6 轮已完成 —— 查看运行。本轮报告见下方。

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. Suggestions are inline. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite (qwen-serve-live-journal-recovery, qwen-serve-webui-live-journal-recovery) did not run in this review; the diff's two changed integration tests are only exercised by that skipped job. Not explored to full depth (tool budget reached): This PR (fix(acp-bridge): bound live journal replay chunk...: I did not run the two integration tests ( qwen-serve-live-journal-recovery , qwen-serve-webui-live-journal-recovery ) — they spawn daemons and need a build; I …; This PR (fix(acp-bridge): bound live journal replay chunk...: integration tests ( qwen-serve-live-journal-recovery , qwen-serve-webui-live-journal-recovery ) were not executed (they spawn real daemons); the backgroundShe…. Test Plan (not a blocker): src/compactionEngine.test.tsno such file or directory; test/unit/daemonUi.test.tsno such file or directory; src/commands/serve.test.tsno such file or directory; src/serve/fast-path.test.tsno such file or directory; 92 passed — this review observed 1140 passed; and 2 more.

中文说明

已审查。 建议见行内评论。 未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite (qwen-serve-live-journal-recovery, qwen-serve-webui-live-journal-recovery) did not run in this review; the diff's two changed integration tests are only exercised by that skipped job。 未探索到全部深度(达到工具调用预算):This PR (fix(acp-bridge): bound live journal replay chunk...:I did not run the two integration tests ( qwen-serve-live-journal-recovery , qwen-serve-webui-live-journal-recovery ) — they spawn daemons and need a build; I …;This PR (fix(acp-bridge): bound live journal replay chunk...:integration tests ( qwen-serve-live-journal-recovery , qwen-serve-webui-live-journal-recovery ) were not executed (they spawn real daemons); the backgroundShe…。 Test Plan(非阻断):src/compactionEngine.test.tsno such file or directory; test/unit/daemonUi.test.tsno such file or directory; src/commands/serve.test.tsno such file or directory; src/serve/fast-path.test.tsno such file or directory; 92 passed — this review observed 1140 passed; and 2 more。

— DeepSeek/deepseek-v4-flash via Qwen Code /review (v0.21.8)

Comment thread packages/acp-bridge/src/compactionEngine.ts
Comment thread packages/acp-bridge/src/bridgeOptions.ts
Comment thread packages/acp-bridge/src/compactionEngine.ts
Comment thread integration-tests/cli/qwen-serve-live-journal-recovery.test.ts
Comment thread packages/acp-bridge/src/compactionEngine.ts
Comment thread packages/acp-bridge/src/compactionEngine.ts
Comment thread packages/acp-bridge/src/compactionEngine.ts
The merged live-journal entry is rebuilt by spread-merging the first and
last source events, which was only safe because producers happen to emit
exactly {sessionUpdate, content, _meta?} on mergeable chunks. Gate the
merge on that key set so unmodeled data/update fields keep entries
discrete instead of leaking into the aggregate. Also clarify the
live-journal truncation marker: its retained/truncated counts describe
source events, while the limits count replay entries.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下:

Autofix review round — PR #8801

Commit: 67922db3dcfix(acp-bridge): keep unmodeled chunk keys out of live journal merges
Conflict: none (--conflict false, no merge performed).

Feedback dispositions

1. Blocker (CHANGES_REQUESTED): entry-vs-event semantic change breaks the two live-journal integration tests — VERIFIED ALREADY FIXED, re-proven by execution.
The fix landed in ba7f10c289 (before this round) exactly along the reviewer's suggested lines, and this round re-verified it by running both files against the built bundle (3/3 passed):

  • qwen-serve-live-journal-recovery.test.ts now has a dedicated test asserting that 20 compatible agent_message_chunk events stay marker-free under --max-journal-events 3 (all 20 merge into one entry, history_truncated never appears) while the live replay still contains both chunk-0 and chunk-19 — the reviewer's requested "long compatible stream stays marker-free" assertion.
  • Both truncation-recovery tests (CLI and WebUI) now force eviction with --max-journal-bytes 300, so they keep exercising the truncation marker and completed-turn repair under the new entry-based semantics — the reviewer's "tiny --max-journal-bytes to force a segment split" alternative.

2. Note 1: mergeLiveJournalTextEvent spread-merge is only safe contingently — IMPLEMENTED.
Added a structural key-set gate in the merge-eligibility check (liveJournalTextChunk → new hasOnlyModeledChunkKeys): a text/thought chunk can join a merged live-journal entry only when data carries exactly {sessionId?, update} and update carries exactly {sessionUpdate, content, _meta?}. Any unmodeled data/update key now keeps the event as its own discrete entry instead of riding the {...existing, ...incoming} spread into the aggregate — the safety the reviewer asked to make structural rather than contingent. The gate is inert today (verified: producers emit exactly the modeled shape, and the full acp-bridge suite plus integration tests pass unchanged). Two new tests pin both rejection paths (unmodeled update key, unmodeled data key); compactionEngine.test.ts now has 94 tests, all passing.

3. Note 2: live truncation marker mixes units — IMPLEMENTED.
The live_journal marker sentence now reads "kept the latest N source events and dropped M older source events (limits: X replay entries / Y bytes)", contrasting source-event counts against entry-based limits with the same vocabulary the docs already use ("counts source events, not replay entries"). Only the live_journal scope changed; the replay-path and generic sentences are untouched (their counts and limits are already the same unit). The SDK normalizer test asserting the sentence was updated (285/285 pass).

4. Note 3: thought-chunk _meta propagation deserves a release note — DECLINED as an in-repo edit, flagged here instead.
CHANGELOG.md is generated automatically from GitHub Releases ("Do not edit it by hand"), so there is no in-repo release-note artifact to update. Surfacing the behavior change for the release author: release-note candidate — "Live journal replay now propagates thought-chunk _meta on the live path as well (parity with the completed-turn compaction path); SDK-visible behavior change beyond replay (#8801)."

5. CI note: red Test (ubuntu-latest) from the backgroundShellRegistry retention-cap flake — ALREADY ADDRESSED, re-verified.
The branch merged current main (which includes the #8797 deflake) via baf5dedb65, and 3818d85f3f fixed the root cause in the test fixture (test-owned temp sidecar dirs, 120s workaround removed). Re-ran the suite this round: 57/57 passed in ~1.5s.

Changed files this round

  • packages/acp-bridge/src/compactionEngine.ts — key-set gate helper + call site
  • packages/acp-bridge/src/compactionEngine.test.ts — 2 new gate tests
  • packages/sdk-typescript/src/daemon/ui/normalizer.ts — marker wording
  • packages/sdk-typescript/test/unit/daemonUi.test.ts — updated assertion

Verification

All commands actually run this round against the committed tree (67922db3dc):

  • npm run build — passed
  • npm run bundle — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • vitest packages/acp-bridge/src/compactionEngine.test.ts (touched) — 94 passed (94)
  • vitest packages/acp-bridge (full package, includes 499 bridge tests) — 1142 passed (1142), 25 files
  • vitest packages/sdk-typescript/test/unit/daemonUi.test.ts (touched) — 285 passed (285)
  • vitest packages/sdk-typescript (full package) — 1482 passed (1482), 32 files
  • vitest packages/core/src/services/backgroundShellRegistry.test.ts — 57 passed (57)
  • Integration after npm run bundle (QWEN_SANDBOX=false): cli/qwen-serve-live-journal-recovery.test.ts + cli/qwen-serve-webui-live-journal-recovery.test.ts — 3 passed (3)
中文说明

Autofix 审查轮次 — PR #8801

提交:67922db3dcfix(acp-bridge): keep unmodeled chunk keys out of live journal merges
冲突:无(--conflict false,未执行任何合并)。

反馈处理结果

1. 阻塞项(CHANGES_REQUESTED):entry 与 event 语义变更破坏了两个 live-journal 集成测试 — 确认已在先前提交修复,并通过实际运行再次验证。
修复已在 ba7f10c289(本轮之前)按照审查者建议的方向落地,本轮通过基于构建产物实际运行两个测试文件再次验证(3/3 通过):

  • qwen-serve-live-journal-recovery.test.ts 新增专门测试,断言 20 个兼容的 agent_message_chunk 事件在 --max-journal-events 3 下聚合后不出现截断 marker(20 个事件合并为一个 entry,history_truncated 始终不出现),同时 live replay 中仍完整保留 chunk-0chunk-19 —— 即审查者要求的"长兼容流保持无 marker"断言。
  • 两个截断恢复测试(CLI 与 WebUI)改用 --max-journal-bytes 300 强制触发淘汰,因此在新的 entry 语义下仍然覆盖截断 marker 与 completed-turn 修复流程 —— 即审查者提出的"用极小的 --max-journal-bytes 强制分段切分"的替代方案。

2. 注记 1:mergeLiveJournalTextEvent 的 spread 合并安全性只是偶然成立 — 已实现。
在合并资格检查中新增结构性键集门控(liveJournalTextChunk → 新增 hasOnlyModeledChunkKeys):text/thought chunk 只有在 data 恰好包含 {sessionId?, update}、且 update 恰好包含 {sessionUpdate, content, _meta?} 时才能加入合并后的 live-journal entry。任何未建模的 data/update 键都会让该事件保持为独立的离散 entry,而不会随 {...existing, ...incoming} spread 混入聚合结果 —— 即审查者要求将安全性从"偶然成立"变为"结构性保证"。该门控目前是无副作用的(已验证:生产者恰好只发出建模内的形状,acp-bridge 全量测试与集成测试均无变化地通过)。新增两个测试锁定两条拒绝路径(未建模的 update 键、未建模的 data 键);compactionEngine.test.ts 现有 94 个测试,全部通过。

3. 注记 2:live 截断 marker 混用单位 — 已实现。
live_journal 场景的 marker 文案改为 "kept the latest N source events and dropped M older source events (limits: X replay entries / Y bytes)",用与文档一致的措辞("counts source events, not replay entries")明确区分源事件计数与 entry 上限。仅修改 live_journal 场景;replay 路径与通用文案未改动(其计数与上限本就同单位)。SDK normalizer 中断言该文案的测试已同步更新(285/285 通过)。

4. 注记 3:thought-chunk _meta 传播值得写 release note — 拒绝在仓库内修改,改在此处显式标注。
CHANGELOG.md 由 GitHub Releases 自动生成("Do not edit it by hand"),仓库内没有可更新的 release-note 载体。现向 release 撰写者标注该行为变更:release-note 候选文案 — "Live journal replay now propagates thought-chunk _meta on the live path as well (parity with the completed-turn compaction path); SDK-visible behavior change beyond replay (#8801)."

5. CI 注记:Test (ubuntu-latest) 因 backgroundShellRegistry retention-cap 不稳定而变红 — 已处理,并再次验证。
分支已通过 baf5dedb65 合并最新 main(包含 #8797 的去抖动修复),3818d85f3f 从测试夹具根因修复(每个 entry 使用测试自有的临时目录写 sidecar,移除 120 秒 workaround)。本轮重新运行该套件:57/57 通过,耗时约 1.5 秒。

本轮修改的文件

  • packages/acp-bridge/src/compactionEngine.ts — 键集门控辅助函数及调用点
  • packages/acp-bridge/src/compactionEngine.test.ts — 新增 2 个门控测试
  • packages/sdk-typescript/src/daemon/ui/normalizer.ts — marker 文案
  • packages/sdk-typescript/test/unit/daemonUi.test.ts — 更新断言

验证

以下命令均为本轮针对已提交代码树(67922db3dc)实际执行的结果:

  • npm run build — 通过
  • npm run bundle — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • vitest packages/acp-bridge/src/compactionEngine.test.ts(本轮修改)— 94 通过(94)
  • vitest packages/acp-bridge(整包,含 499 个 bridge 测试)— 1142 通过(1142),25 个文件
  • vitest packages/sdk-typescript/test/unit/daemonUi.test.ts(本轮修改)— 285 通过(285)
  • vitest packages/sdk-typescript(整包)— 1482 通过(1482),32 个文件
  • vitest packages/core/src/services/backgroundShellRegistry.test.ts — 57 通过(57)
  • npm run bundle 后的集成测试(QWEN_SANDBOX=false):cli/qwen-serve-live-journal-recovery.test.ts + cli/qwen-serve-webui-live-journal-recovery.test.ts — 3 通过(3)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下:

Review feedback addressed — PR #8801 (round 2)

All seven inline suggestions were verified against the code and addressed in one commit (fix(acp-bridge): align replay boundaries for discrete and meta-shaped chunks). No conflicts (--conflict false); origin/main was not merged.

Findings and dispositions

  1. [Suggestion] Turn-boundary compaction folds discrete thought chunks into one slot (rc:3744845035)Fixed. Verified: classifySessionUpdate routed agent_thought_chunk to mergeTextSlot with no discrete guard, and the message path only guarded todo_stop_guard, while the live journal keeps every qwenDiscreteMessage chunk separate — so two background-task thought/message chunks were folded into one compacted slot whose _meta was the second chunk's, mis-attributing the first task's text (newly visible because this PR forwards thought-chunk meta in the SDK normalizer). Both agent_message_chunk and agent_thought_chunk now route discrete-meta chunks to discrete slots via the same hasDiscreteMessageMeta predicate the live journal uses; the now-redundant hasTodoStopGuardDiscreteMeta was removed. New turn-boundary test covers generic discrete message and thought chunks keeping their boundaries and per-chunk backgroundTask meta through turn_complete.
  2. [Suggestion] BridgeOptions.maxJournalBytes doc left at old "byte cap" wording (rc:3744845041)Fixed. The comment now uses the source-event accounting wording, matching the --max-journal-bytes help, ServeOptions, and the docs pages updated in this PR.
  3. [Suggestion] No test covers merge-success for defined-and-equal identities (rc:3744845044)Fixed. Added an it.each mirroring the boundary test: consecutive chunks sharing a defined parentToolCallId, qwenTranscript.sourceRecordIds, promptId, originatorClientId, or sessionId must merge into one live entry. The identity-stamping helper was hoisted to module scope and is shared by both tests.
  4. [Suggestion] Second integration test's chunk-0 assertion became vacuous (rc:3744845045)Fixed. Merged entries concatenate up to 256 source chunks, so an exact per-entry text: 'chunk-0' match can never hold. The test now asserts truncatedEvents >= 1 on the marker and that the serialized live journal contains no 'chunk-0' substring — which genuinely fails if dropped content is wrongly retained.
  5. [Suggestion] Empty-string parentToolCallId divergence between replay surfaces (rc:3744845047)Fixed. hasUnmodeledTextMeta now treats parentToolCallId: '' as modeled-and-ignored, matching extractParentToolCallIdFromMeta (empty string = no parent), so both the live journal and turn compaction segment such chunks as top-level. Non-string values remain unmodeled (conservative). New test asserts both surfaces merge empty-string-parent chunks identically.
  6. [Suggestion] Byte-cap truncation drops whole merged segments; retained tail can collapse to one chunk (rc:3744845052)Fixed (documentation). All surfaces describing the byte cap now state that entries are dropped whole (merged segments included), so the retained tail can be much smaller than the cap: engine options doc, BridgeOptions.maxJournalBytes, ServeOptions.maxJournalBytes, the --max-journal-bytes help text, docs/users/qwen-serve.md, and docs/developers/qwen-serve-protocol.md. No behavior change — whole-entry eviction is the design this PR's tests assert.
  7. [Suggestion] hasUnmodeledTextMeta rejects meta shapes buildUpdateMeta emits (rc:3744845053)Fixed. Verified against buildUpdateMeta (acp-bridge transcript-replay.ts): it emits top-level timestamp and nested qwenTranscript.planToolCallId, which the predicate rejected — silently disabling live-journal aggregation for such streams (e.g. transcript-replay projections stamp timestamp + sourceRecordIds on every message/thought chunk). The predicate now tolerates timestamp/serverTimestamp (matching the sibling envelope predicate) and qwenTranscript.{sourceRecordIds, planToolCallId}; sourceRecordIds is no longer mandatory inside qwenTranscript, matching the builder. planToolCallId does not join segment identity, consistent with turn compaction's last-meta-wins folding. New test covers the merged path for these shapes.

Review-level notes: the two integration suites flagged as not run (qwen-serve-live-journal-recovery, qwen-serve-webui-live-journal-recovery) were executed this round after npm run build && npm run bundle — 3/3 tests passed, including the strengthened truncation assertion. The reviewer's Test-Plan path probes pointed at non-existent paths; the actual test files are packages/acp-bridge/src/compactionEngine.test.ts, packages/sdk-typescript/test/unit/daemonUi.test.ts, packages/cli/src/serve/fast-path.test.ts, and integration-tests/cli/qwen-serve-live-journal-recovery.test.ts.

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npx prettier --write (touched files) — applied, no residual diffs
  • vitest run src/compactionEngine.test.ts (packages/acp-bridge, touched) — 102 passed
  • vitest run (packages/acp-bridge full suite) — 25 files, 1150 passed
  • vitest run src/serve/fast-path.test.ts (packages/cli, touched flag surface) — 84 passed
  • vitest run test/unit/daemonUi.test.ts (packages/sdk-typescript, PR diff) — 285 passed
  • npm run bundle — passed
  • integration-tests: vitest run cli/qwen-serve-live-journal-recovery.test.ts cli/qwen-serve-webui-live-journal-recovery.test.ts (QWEN_SANDBOX=false, after bundle) — 2 files, 3 passed
中文说明

已处理的审查反馈 — PR #8801(第 2 轮)

七条行内建议均已对照代码核实,并在一个提交中处理完毕(fix(acp-bridge): align replay boundaries for discrete and meta-shaped chunks)。无冲突(--conflict false),未合并 origin/main

各条反馈及处理

  1. [建议] turn 边界 compaction 把离散 thought chunk 折叠进同一个 slot(rc:3744845035)已修复。 核实:classifySessionUpdateagent_thought_chunk 直接路由到 mergeTextSlot 且没有离散守卫,message 路径也只守卫 todo_stop_guard,而 live journal 对每个带 qwenDiscreteMessage 的 chunk 都保持独立——因此两个后台任务的 thought/message chunk 会被折叠进同一个 compacted slot,其 _meta 是第二个 chunk 的,第一个任务的文本被错标(本 PR 的 SDK normalizer 改动转发 thought chunk meta 后,该错标首次可见)。现在 agent_message_chunkagent_thought_chunk 两条路径都通过 live journal 已在使用的同一个 hasDiscreteMessageMeta 谓词,把带离散 meta 的 chunk 路由为独立 slot;冗余的 hasTodoStopGuardDiscreteMeta 已删除。新增 turn 边界测试覆盖通用离散 message 与 thought chunk 在 turn_complete 后保持边界及各自的 backgroundTask meta。
  2. [建议] BridgeOptions.maxJournalBytes 文档仍是旧的 "byte cap" 表述(rc:3744845041)已修复。 注释改为源事件口径表述,与本 PR 中已更新的 --max-journal-bytes 帮助文本、ServeOptions 及各文档页保持一致。
  3. [建议] 缺少"已定义且相等"身份合并成功路径的测试(rc:3744845044)已修复。 新增与边界测试对称的 it.each:共享已定义的 parentToolCallIdqwenTranscript.sourceRecordIdspromptIdoriginatorClientIdsessionId 的连续 chunk 必须合并为一个 live 条目。身份打标辅助函数已提升到模块级,供两个测试共用。
  4. [建议] 第二个集成测试的 chunk-0 断言已变空断言(rc:3744845045)已修复。 合并条目最多拼接 256 个源 chunk,逐条目精确匹配 text: 'chunk-0' 永远不可能成立。测试现在断言 marker 的 truncatedEvents >= 1,且序列化后的 live journal 不包含 'chunk-0' 子串——若被丢弃的内容被错误保留,该断言会真实失败。
  5. [建议] 空字符串 parentToolCallId 在两个 replay 面之间的分歧(rc:3744845047)已修复。 hasUnmodeledTextMeta 现在把 parentToolCallId: '' 视为"已建模且忽略",与 extractParentToolCallIdFromMeta(空字符串 = 无 parent)一致,因此 live journal 与 turn compaction 都把此类 chunk 按顶层文本分段。非字符串值仍视为未建模(保守处理)。新增测试断言两个表面对空字符串 parent 的 chunk 做相同合并。
  6. [建议] 字节上限截断按整段驱逐,保留尾部可能塌缩到单个 chunk(rc:3744845052)已修复(文档)。 所有描述字节上限的位置现在都说明:条目按整体(含合并段)驱逐,因此保留尾部可能远小于上限。涉及引擎选项注释、BridgeOptions.maxJournalBytesServeOptions.maxJournalBytes--max-journal-bytes 帮助文本、docs/users/qwen-serve.mddocs/developers/qwen-serve-protocol.md。无行为变更——整段驱逐正是本 PR 测试所断言的设计。
  7. [建议] hasUnmodeledTextMeta 拒绝 buildUpdateMeta 实际会产出的 meta 形状(rc:3744845053)已修复。 已对照 acp-bridge transcript-replay.ts 中的 buildUpdateMeta 核实:它会产出顶层 timestamp 与嵌套 qwenTranscript.planToolCallId,而原谓词拒绝这些键——会静默关闭此类流的 live journal 聚合(例如 transcript-replay 投影会给每个 message/thought chunk 打上 timestamp + sourceRecordIds)。谓词现在放行 timestamp/serverTimestamp(与兄弟 envelope 谓词一致)以及 qwenTranscript.{sourceRecordIds, planToolCallId}qwenTranscript 内不再强制要求 sourceRecordIds,与构建器一致。planToolCallId 不参与 segment 身份判定,与 turn compaction 的 last-meta-wins 折叠保持一致。新增测试覆盖这些形状的合并路径。

审查级备注:本轮在 npm run build && npm run bundle 之后实际运行了被标记为未运行的两个集成套件(qwen-serve-live-journal-recoveryqwen-serve-webui-live-journal-recovery)——3/3 通过,包含加强后的截断断言。审查者 Test Plan 中探测的路径不存在,实际测试文件为 packages/acp-bridge/src/compactionEngine.test.tspackages/sdk-typescript/test/unit/daemonUi.test.tspackages/cli/src/serve/fast-path.test.tsintegration-tests/cli/qwen-serve-live-journal-recovery.test.ts

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • npx prettier --write(涉及文件)— 已应用,无残留差异
  • vitest run src/compactionEngine.test.ts(packages/acp-bridge,涉及文件)— 102 通过
  • vitest run(packages/acp-bridge 全量)— 25 个文件,1150 通过
  • vitest run src/serve/fast-path.test.ts(packages/cli,涉及的 flag 面)— 84 通过
  • vitest run test/unit/daemonUi.test.ts(packages/sdk-typescript,PR diff 涉及)— 285 通过
  • npm run bundle — 通过
  • integration-tests: vitest run cli/qwen-serve-live-journal-recovery.test.ts cli/qwen-serve-webui-live-journal-recovery.test.ts(QWEN_SANDBOX=false,bundle 之后)— 2 个文件,3 通过

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — no blockers. Suggestions are inline. Test Plan (not a blocker): src/compactionEngine.test.tsno such file or directory; test/unit/daemonUi.test.tsno such file or directory; src/commands/serve.test.tsno such file or directory; src/serve/fast-path.test.tsno such file or directory; 92 passed — this review observed 1150, 18673, 19547, 1482, 481, 2941, 454 passed; and 2 more.

中文说明

已审查——无阻断问题。 建议见行内评论。 Test Plan(非阻断):src/compactionEngine.test.tsno such file or directory; test/unit/daemonUi.test.tsno such file or directory; src/commands/serve.test.tsno such file or directory; src/serve/fast-path.test.tsno such file or directory; 92 passed — this review observed 1150, 18673, 19547, 1482, 481, 2941, 454 passed; and 2 more。

— qwen3.8-max via Qwen Code /review (v0.21.8)

Comment thread packages/acp-bridge/src/compactionEngine.ts
SubAgentTracker stamps every streamed subagent fragment with
{ parentToolCallId, subagentType }, but the live-journal merge gate
only modeled parentToolCallId, so subagent chunks stayed discrete and
a high-fragment subagent stream could still trip history_truncated.
Model subagentType as a carried label (like the completed-turn path,
which merges by parentToolCallId alone) and cover the producer wire
shape in the merge tests.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 3/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 3/100 轮)。改动内容与我反驳保留之处如下:

Review feedback addressed — PR #8801

Findings

[Suggestion] R1-1 (rc:3745939288) — Subagent-streamed chunks never merge in the live journal → Addressed

The finding was verified against the code before changing anything:

  • SubAgentTracker constructs subagentMeta = { parentToolCallId, subagentType } (SubAgentTracker.ts:82) and stamps every streamed subagent text/thought fragment with it via createStreamTextHandlerMessageEmitter.emitMessage.
  • hasUnmodeledTextMeta had no branch for subagentType, so liveJournalTextChunk rejected every subagent chunk and each stayed a discrete live-journal entry — a high-fragment subagent stream in one unfinished turn could still hit the entry cap and surface history_truncated, the exact symptom this PR fixes for main-agent streams.
  • The completed-turn path (mergeTextSlot) merges by (kind, parentToolCallId) alone, so the live view and the post-turn_complete replay disagreed on boundaries for the same chunks.

Fix chosen: the suggestion's second option — explicitly model subagentType as a carried label rather than a merge-boundary key. hasUnmodeledTextMeta now accepts a string subagentType (non-string stays unmodeled/rejected, conservative), and no comparison is added to the live merge condition. This is the smaller change and makes the live view's merge boundary exactly match the completed-turn path (parentToolCallId only), which is this PR's stated goal; the label rides through merged entries via the last event's _meta, same as the completed-turn lastMeta behavior. The identity-key alternative (comparing subagentType and carrying it in LiveJournalTextSegment) would add state for a divergence real producers cannot emit — a parentToolCallId is unique to one Agent tool call with a fixed subagentType.

Test updates:

  • The parameterized live merge/boundary cases for parentToolCallId now carry the producer-stamped { parentToolCallId, subagentType } shape (withIdentity/ChunkIdentity gained subagentType support). In the boundary case both chunks share one subagentType so the split is provably driven by parentToolCallId.
  • The makeTextChunkWithParent/makeThoughtChunkWithParent fixtures now stamp the paired wire shape too, so the whole completed-turn suite exercises what producers actually emit instead of a lone-parentToolCallId shape no producer sends.
  • New regression test merges live subagent chunks carrying the producer-stamped meta pair: two subagent-shaped chunks merge into one live-journal entry, the merged entry keeps both _meta keys, and the post-turn_complete compaction agrees (same text, same _meta) on both surfaces.

Load-bearing proof: with the source change temporarily reverted, exactly the two paired-shape tests fail (2 failed | 101 passed); with the fix restored, all 103 pass.

Review-level Test Plan note — No action required

The review body's Test Plan (marked "not a blocker") listed paths like src/compactionEngine.test.ts and test/unit/daemonUi.test.ts as "no such file or directory". Those are reviewer-side relative paths that do not resolve to the package-relative test files (packages/acp-bridge/src/compactionEngine.test.ts, packages/sdk-typescript/test/unit/daemonUi.test.ts); the observed per-suite counts (1150, 18673, ...) show the suites did run. No defect is reported and none was found — the full acp-bridge suite passes locally with 1151 tests (the prior 1150 plus the one regression test added this round).

Changes

  • packages/acp-bridge/src/compactionEngine.ts (+8): hasUnmodeledTextMeta accepts a string subagentType as a modeled, carried meta key.
  • packages/acp-bridge/src/compactionEngine.test.ts (+46/−3): paired-shape fixtures and parameterized cases, subagentType support in withIdentity, one new live/compacted-consistency regression test.

Commit: fix(acp-bridge): merge subagent chunks in live journal replay

Verification

Commands actually run this round, all from the repository checkout:

  • npx vitest run src/compactionEngine.test.ts (in packages/acp-bridge) — passed, 103 tests
  • Load-bearing check: source reverted to HEAD version → 2 failed | 101 passed; fix restored → 103 passed
  • npx vitest run (full packages/acp-bridge suite) — passed, 25 files / 1151 tests
  • npm run build — passed
  • npm run typecheck — passed (exit 0, no errors)
  • npm run lint — passed
  • npx prettier --check on the two touched files — passed
  • npm run bundle — passed
  • Integration after bundle: QWEN_SANDBOX=false npx vitest run cli/qwen-serve-live-journal-recovery.test.ts cli/qwen-serve-webui-live-journal-recovery.test.ts (in integration-tests/) — passed, 2 files / 3 tests

No conflict resolution was needed (--conflict false).

中文说明

已处理的评审反馈 — PR #8801

各项发现

[建议] R1-1(rc:3745939288)— subagent 流式 chunk 在 live journal 中从不合并 → 已处理

修改前已对照代码核实该发现:

  • SubAgentTracker 构造 subagentMeta = { parentToolCallId, subagentType }(SubAgentTracker.ts:82),并通过 createStreamTextHandlerMessageEmitter.emitMessage 为每个流式 subagent 文本/思考片段打上该标记。
  • hasUnmodeledTextMeta 没有 subagentType 分支,因此 liveJournalTextChunk 拒绝所有 subagent chunk,每个 chunk 都保持为独立的 live-journal 条目 —— 单个未完成 turn 中片段数很多的 subagent 流仍会触及条目上限并显示 history_truncated,正是本 PR 为主 agent 流修复的症状。
  • completed-turn 路径(mergeTextSlot)仅按 (kind, parentToolCallId) 合并,因此对同样的 chunk,live 视图与 turn_complete 之后的 replay 在边界上不一致。

所选修复: 采用建议中的第二个方案 —— 把 subagentType 显式建模为"携带的标签"而非合并边界键。hasUnmodeledTextMeta 现在接受字符串类型的 subagentType(非字符串仍视为未建模并被拒绝,保持保守),且不在 live 合并条件中加入对它的比较。这是更小的改动,并使 live 视图的合并边界与 completed-turn 路径完全一致(都只看 parentToolCallId)——这正是本 PR 的目标;该标签通过最后一个事件的 _meta 随合并条目透传,与 completed-turn 的 lastMeta 行为一致。另一个"身份键"方案(比较 subagentType 并在 LiveJournalTextSegment 中携带)会为真实产出端不可能发出的分歧增加状态 —— 一个 parentToolCallId 唯一对应一次 Agent 工具调用,其 subagentType 是固定的。

测试更新:

  • parentToolCallId 的参数化 live 合并/边界用例现在携带产出端打标的 { parentToolCallId, subagentType } 形状(withIdentity/ChunkIdentity 新增 subagentType 支持)。边界用例中两个 chunk 共享同一个 subagentType,从而证明拆分确实由 parentToolCallId 驱动。
  • makeTextChunkWithParent/makeThoughtChunkWithParent fixture 也改为打上成对的线上形状,使整个 completed-turn 测试套件演练的是产出端实际发出的形状,而不是没有任何产出端发送的孤立 parentToolCallId 形状。
  • 新增回归测试 merges live subagent chunks carrying the producer-stamped meta pair:两个 subagent 形状 chunk 合并为一个 live-journal 条目,合并条目保留 _meta 中的两个键,且 turn_complete 之后的压缩结果在两个视图上一致(文本相同、_meta 相同)。

承重性证明:临时还原源码修改后,恰好是这两个成对形状用例失败(2 failed | 101 passed);恢复修复后 103 个全部通过。

评审正文的 Test Plan 备注 — 无需处理

评审正文的 Test Plan(已标注"非阻断")把 src/compactionEngine.test.tstest/unit/daemonUi.test.ts 等路径列为 "no such file or directory"。这些是评审端使用的相对路径,无法解析到按包划分的测试文件(packages/acp-bridge/src/compactionEngine.test.tspackages/sdk-typescript/test/unit/daemonUi.test.ts);其观察到的各套件计数(1150, 18673, ...)说明套件实际已运行。其中未报告任何缺陷,我们也未发现缺陷 —— acp-bridge 全量套件本地通过,共 1151 个测试(上次的 1150 加本轮新增的 1 个回归测试)。

变更内容

  • packages/acp-bridge/src/compactionEngine.ts(+8):hasUnmodeledTextMeta 接受字符串类型的 subagentType 作为已建模、随条目携带的 meta 键。
  • packages/acp-bridge/src/compactionEngine.test.ts(+46/−3):成对形状的 fixture 与参数化用例、withIdentitysubagentType 支持、1 个新的 live/压缩一致性回归测试。

提交:fix(acp-bridge): merge subagent chunks in live journal replay

验证

本轮在仓库检出目录中实际执行的命令:

  • npx vitest run src/compactionEngine.test.ts(在 packages/acp-bridge 中)— 通过,103 个测试
  • 承重性检查:源码还原到 HEAD 版本 → 2 failed | 101 passed;恢复修复后 → 103 个通过
  • npx vitest runpackages/acp-bridge 全量套件)— 通过,25 个文件 / 1151 个测试
  • npm run build — 通过
  • npm run typecheck — 通过(退出码 0,无错误)
  • npm run lint — 通过
  • 对两个改动文件执行 npx prettier --check — 通过
  • npm run bundle — 通过
  • 打包后的集成测试:QWEN_SANDBOX=false npx vitest run cli/qwen-serve-live-journal-recovery.test.ts cli/qwen-serve-webui-live-journal-recovery.test.ts(在 integration-tests/ 中)— 通过,2 个文件 / 3 个测试

无需解决冲突(--conflict false)。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

…ay-chunks

# Conflicts:
#	packages/core/src/services/backgroundShellRegistry.test.ts
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 4/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 4/100 轮)。改动内容与我反驳保留之处如下:

Autofix review round — base-conflict resolution for PR #8801

Feedback triage

The workflow-prepared feedback contained no actionable items this round: no reviews, no inline comments, no issue-level comments, no failed checks, and no still-red checks. The only work was resolving the reported conflict against main (--conflict true).

Changes made

Merged origin/main (8 commits ahead) into fix/live-journal-replay-chunks in merge commit c5375218a3.

Conflict resolution — packages/core/src/services/backgroundShellRegistry.test.ts (the only content conflict): both sides independently made the same fix. This branch's test(core): isolate registry sidecars commit gave every registry entry a unique temp-dir outputPath and removed the SIDECAR_IO_TIMEOUT workaround; upstream #8813 ("stop background-shell tests sharing a fixed /tmp sidecar path") made the exact same two changes — the timeout-removal hunks are identical — and additionally hardened the file (per-shellId file names, a root-cause comment, a uniqueness regression test, XML/bidi escaping coverage). The resolution takes the upstream version wholesale; the branch loses nothing because every change it made to this file already exists upstream in a strictly more complete form. The resolved file is byte-identical to origin/main's version.

Auto-merged files audited (both sides changed them, in disjoint regions): journal-cap doc wording in bridgeOptions.ts, bridgeTypes.ts, serve.ts, serve/types.ts, daemon/types.ts, the history_truncated source-event wording and thought-chunk meta extraction in normalizer.ts (verified extractUpdateMeta and the main-side debugReason fields coexist), plus three docs files and daemonUi.test.ts. A post-merge audit confirmed the branch's delta versus origin/main is exactly the PR's intended change set minus the subsumed registry test file.

No feedback findings existed to resolve, decline, or escalate; resolved-comments.txt and comment-replies.json are omitted accordingly.

Verification

Commands actually run against the merged tree (before committing the merge):

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • vitest run src/services/backgroundShellRegistry.test.ts (packages/core, conflict-resolved file) — 58 passed
  • vitest run src/compactionEngine.test.ts (packages/acp-bridge, PR core behavior) — 103 passed
  • vitest run test/unit/daemonUi.test.ts (packages/sdk-typescript) — 291 passed
  • vitest run src/commands/serve.test.ts src/serve/run-qwen-serve.test.ts src/serve/server.test.ts (packages/cli) — 1201 passed
  • vitest run src/bridge.test.ts src/session-restore-timeout.test.ts src/status.test.ts (packages/acp-bridge) — 563 passed
  • npm run bundle — passed
  • Integration: vitest run cli/qwen-serve-live-journal-recovery (QWEN_SANDBOX=false, bundled CLI) — 2 passed
  • Integration: vitest run cli/qwen-serve-webui-live-journal-recovery (QWEN_SANDBOX=false, bundled CLI) — 1 passed
  • npx prettier --check on all 15 files touched by the branch/merge — passed

No settings source changed, so npm run generate:settings-schema was not required.

中文说明

Autofix 审查轮次 — PR #8801 的基分支冲突解决

反馈分类

本轮工作流准备的反馈中没有可处理项:没有审查意见、没有行内评论、没有 issue 级评论、没有失败的检查,也没有持续失败的检查。唯一的工作是解决针对 main 报告的冲突(--conflict true)。

所做更改

在合并提交 c5375218a3 中将 origin/main(领先 8 个提交)合并进 fix/live-journal-replay-chunks

冲突解决 — packages/core/src/services/backgroundShellRegistry.test.ts(唯一的内容冲突):双方独立做了相同的修复。本分支的 test(core): isolate registry sidecars 提交为每个 registry 条目分配独立的临时目录 outputPath 并移除了 SIDECAR_IO_TIMEOUT 变通方案;上游 #8813("stop background-shell tests sharing a fixed /tmp sidecar path")做了完全相同的两处修改——移除超时的代码块逐字节相同——并且还额外加固了该文件(按 shellId 区分文件名、解释根因的注释、唯一性回归测试、XML/bidi 转义覆盖)。解决方案完整采用上游版本;本分支没有任何损失,因为它对该文件的每一处修改都已以 strictly 更完整的形式存在于上游。解决后的文件与 origin/main 的版本逐字节一致。

已审查的自动合并文件(双方都修改了它们,但区域互不重叠):bridgeOptions.tsbridgeTypes.tsserve.tsserve/types.tsdaemon/types.ts 中的 journal 上限文案,normalizer.ts 中的 history_truncated source-event 措辞和 thought-chunk meta 提取(已验证 extractUpdateMeta 与 main 侧的 debugReason 字段共存),以及三个文档文件和 daemonUi.test.ts。合并后的审计确认:分支相对于 origin/main 的差异恰好是 PR 预期的变更集,减去被上游取代的 registry 测试文件。

不存在需要解决、拒绝或升级的反馈发现;因此相应地省略了 resolved-comments.txtcomment-replies.json

验证

在合并后的代码树上(提交合并之前)实际运行的命令:

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • vitest run src/services/backgroundShellRegistry.test.ts(packages/core,冲突解决文件)— 58 通过
  • vitest run src/compactionEngine.test.ts(packages/acp-bridge,PR 核心行为)— 103 通过
  • vitest run test/unit/daemonUi.test.ts(packages/sdk-typescript)— 291 通过
  • vitest run src/commands/serve.test.ts src/serve/run-qwen-serve.test.ts src/serve/server.test.ts(packages/cli)— 1201 通过
  • vitest run src/bridge.test.ts src/session-restore-timeout.test.ts src/status.test.ts(packages/acp-bridge)— 563 通过
  • npm run bundle — 通过
  • 集成测试:vitest run cli/qwen-serve-live-journal-recovery(QWEN_SANDBOX=false,打包后的 CLI)— 2 通过
  • 集成测试:vitest run cli/qwen-serve-webui-live-journal-recovery(QWEN_SANDBOX=false,打包后的 CLI)— 1 通过
  • npx prettier --check 检查分支/合并触及的全部 15 个文件 — 通过

未修改任何 settings 源,因此无需运行 npm run generate:settings-schema

Base-conflict check · 基分支冲突检查: conflicted with main — resolved in this push. · 与 main 有冲突——已在本次推送中解决。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@wenshao

wenshao commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 140 passed · 0 failed · 140 total

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:140 通过 · 0 失败 · 140 总计

Verification report

PR 8801 verification (round 2) — fix(acp-bridge): bound live journal replay chunks

Verdict: merge-ready — 140/140 scripted assertions passed, 0 unexpected failures. Verified head c5375218a3d852f4d25d9b4284f5902d5aea80fd (merge ref 695a9fbb1e, base tip af372e5a21). Follow-up round: previous head was 3818d85f; delta = commits 67922db3 (modeled-keyset merge gate), a03dcec7 (discrete/meta boundary alignment), c9c4be1e (subagent chunk merging), plus a main merge. All round-1 measurements were re-run at the new head/base, never carried forward by diff.

中文摘要
  • 结论merge-ready。140/140 脚本化断言通过,0 个意外失败。本轮为跟进轮:上一轮验证到 3818d85f,此后 PR 新增 3 个修复提交(合并键集门控、discrete/meta 边界对齐、subagent chunk 合并)+ 一次 main 合并;所有上一轮测量均在新 head/base 上重新执行。
  • A/B 结论01-ab-base-arm-truncates.png / 02-ab-head-arm-complete.png):base 上 12 000 细粒度 chunk 触发截断(丢 2 000 源事件、最早文本丢失);head 聚合为 47 个有界 entry、文本逐字节完整。新增 delta 场景全部翻转:subagent 流({parentToolCallId, subagentType} 生产者形状)base 截断 / head 完整且标签保留;generic discrete message/thought 在 base 的 completed-turn 压缩中被折叠进主文本(a1a2b1a3 且 discrete meta 泄漏到聚合事件)/ head 保持独立事件且 meta 完整;unmodeled update/data 键在 head 不会泄漏进 spread-merge 聚合(保持 discrete 且原载荷完整)。
  • 测试有效性:针对 3 个新守卫的 7 个单点突变全部被 PR 自带测试以行为断言杀死(03-mutation-matrix-all-killed.png),含阳性对照;两侧恢复对照全绿(103/103、291/291)。归因精确:每个突变只红掉对应守卫的测试。
  • 门禁04-targeted-gates-all-green.png):acp-bridge 全套 1199/1199、SDK daemonUi 291/291、CLI serve 139/139、真实 daemon 集成测试 3/3(含新增的 no-truncation 用例与收紧后的截断用例)。
  • 非缺陷观察:交错并行的多个 subagent 流在 live journal 中仍保持 discrete(合并仅限“连续”chunk,属文档声明范围),turn 进行中仍可能触发 history_truncated;但截断是优雅降级,turn 完成后 completed replay 对两个 parent 均完整重组(两臂均已证明)。
  • 未覆盖:per-commit 归因(浅克隆 9 个提交仅 1 个可达)、Web Shell 浏览器渲染、Windows/macOS、base 侧 daemon 级 E2E。

Previous-finding status (round 1 → round 2)

Round 1 reported no blocking findings; two observations. Status at the new head:

# round-1 finding severity status at c5375218
1 PR body's local test counts (92/284/132) differed from round-1 measurements (92/285/139) observation stands, re-measured: counts are now 103/291/139 at the new head — drift continues, explained by the PR's own later commits and the main merge adding tests to those files; all suites green. Not a defect.
2 metadata snapshot's baseRefOid (f3ba99f5…) predated the merge ref's base tip observation recurs: snapshot says 33e602cc…, local base tip is af372e5a21. A/B used the local base tip per the merge-ref checkout contract, as round 1 did.

Round-1 verified claims, re-measured (not diffed) at the new head/base: central A/B cells re-run (table below); boundary behavior re-pinned by the head suite's it.each boundary blocks (10 tests, green inside the 1199) plus harness cells C8–C13; round-1's mutation matrix is superseded by round-2's matrix targeting the current guards; daemon-level E2E re-run via the PR's (updated) integration tests, 3/3. The round-1 backgroundShellRegistry test-isolation measurement no longer applies: that file is now byte-identical to main and absent from the effective diff, so there is nothing left to re-measure there.

Central claim + A/B

Central claim: during an unfinished turn, consecutive compatible assistant text/thought chunks share bounded replay entries (≤256 source events each); maxJournalEvents counts replay entries while byte accounting and truncation diagnostics keep source-event units; realtime SSE delivery is unchanged. Delta claims (this round): unmodeled chunk keys/meta never leak into the spread-merge aggregate (67922db); discrete message/thought chunks stay separate in BOTH live journal and completed-turn compaction (a03dcec); subagent fragments carrying the producer-stamped {parentToolCallId, subagentType} pair merge (c9c4be1).

Harness: ab-harness.mjs drives TurnBoundaryCompactionEngine + EventBus from each tree's TS source via tsx. Control validity: the engine's import closure is package-local (eventBus.ts imports only node:crypto; replayWindowLimits.ts imports nothing; both untouched by the PR — verified by git diff), so the only code differing between arms is compactionEngine.ts; base worktree confirmed at af372e5a21 with zero aggregation symbols. No dependency/lockfile changes in the PR, and no @qwen-code/* imports in the closure, so no workspace-link confound. Witnesses: 01-ab-base-arm-truncates.png, 02-ab-head-arm-complete.png.

cell scenario base (af372e5a) head (c5375218)
C1 default-cap-12000 12 000 message chunks, default caps marker, truncated=2000, retained=10000, tail starts c2000; (oldest 2000 lost) no marker, 47 entries, text byte-complete, ids 256/512/…/12000, snapshot JSON-serializable
C2 cap3-20 20 chunks, cap 3 marker, truncated=17, chunk-0; lost no marker, 1 entry id=20, all 20 chunks
C3 cap3-600-thoughts 600 thought chunks, cap 3 marker, truncated=597 no marker, 3 entries ids 256/512/600, text complete
C4 alternating (A/A) 20 alternating message/thought, cap 3 truncated=17, retained=3 identical — nothing merges across kinds
C5 bus-realtime 5 chunks via EventBus (production path stamps serverTimestamp) subscriber: 5 raw frames; journal 5 entries subscriber: 5 raw frames (identical); journal 1 merged entry id=5
C6 subagent-12000 12 000 {parentToolCallId, subagentType} fragments (SubAgentTracker shape) marker, truncated=2000 no marker, 47 entries, text complete, label carried
C7a two subagents, consecutive 200+200 fragments, cap 100 truncated=300 no marker, 2 entries (one per parent), both texts reassembled ungarbled, labels intact
C7b interleaved (boundary) A/B/A/B ×100, cap 100, then turn_complete journal truncated=100 (expected); completed replay reassembles both parents same: journal truncated=100 (consecutive-only scope); completed replay reassembles both parents; journal reset
C8 unmodeled keys chunks with extra update key / extra data key among modeled ones all 6 discrete, payloads intact 5 entries: unmodeled chunks stay discrete with payload (toolName, attachments), merged entries carry no leaked keys
C9 meta-shape matrix 9 modeled + 8 unmodeled update-_meta shapes ×3 chunks 3 entries for every shape modeled → 1 entry (incl. timestamp/serverTimestamp, parentToolCallId, empty parent, subagentType pair, sourceRecordIds, planToolCallId); unmodeled → 3 entries (incl. non-string variants, qwenTranscript other fields, qwenDiscreteMessage)
C10 empty parent no-meta / {parentToolCallId: ''} / no-meta 3 discrete 1 merged entry abc (empty parent = top-level, matches extractor)
C11a discrete message resync text a1,a2 + discrete b1 + a3 + turn_complete folded: single a1a2b1a3 event wearing b1's discrete meta (misattribution) [a1a2, b1 discrete, a3, boundary]; b1 meta + backgroundTask.taskId preserved
C11b discrete thought t1 + discrete dt + t2 + boundary folded t1dtt2 (thought path unguarded on base) [t1, dt, t2]
C11c todo-stop-guard (A/A) guarded messages among text kept discrete kept discrete (no regression from broadened predicate)
C12 marker units 1200 chunks, cap 3 truncated=1197, retained=3 (raw events) truncated=512, retained=688 (source events), maxEvents=3 counts the 3 retained entries; truncated+retained=1200; scope=live_journal
C13 byte cap 20 chunks, maxJournalBytes=600 invariant holds (truncated+retained=20), tail is suffix invariant holds, whole entries dropped, retained tail is suffix, ≥1 entry kept
C14 turn completion 300 chunks + turn_complete journal reset, completed replay full text same; folded to single text event

Totals: base arm 50/50, head arm 71/71 — every base failure occurred exactly as predicted (encoded as passing assertions); the flips are the load-bearing proof.

Corrections

None needed this round.

Findings

None blocking. Observations for the reviewer, each with its repro:

  1. Interleaved parallel subagent streams still truncate the live journal (by design). The merge is consecutive-only (documented: "consecutive compatible… chunks share bounded replay entries"), and the live journal's active segment is only extendable while it is the last entry. Alternating fragments of two subagents therefore stay discrete (C7b: truncated=100/200 on BOTH arms — base because it never merges, head because interleaving breaks consecutivity), so a high-fragment multi-subagent turn can still surface history_truncated mid-turn. What makes this acceptable and worth stating rather than fixing here: the truncation is graceful — the marker reports source-event counts, fullTranscriptAvailable: true, and the completed-turn replay reassembles every parent fully and ungarbled (C7b, proven on both arms via the index-based (kind, parentToolCallId) slots). The single-subagent case named in commit c9c4be1e is fully fixed (C6). Repro: tsx ab-harness.mjs --arm head, cell C7b.
  2. Test-count drift note (carried from round 1): PR body lists 92/284/132; round 2 measures 103/291/139. Cause: the PR's own later commits plus the main merge added tests to those files mid-PR. All green.
  3. Metadata snapshot baseRefOid stale (recurring): snapshot 33e602cc… vs merge-ref base tip af372e5a21; A/B used the local base tip per contract.

Mutation matrix (vacuity)

Single-point mutants of the head source, run against the unmodified head test file (compactionEngine.test.ts, 103 tests; SDK: daemonUi.test.ts, 291 tests). Every kill failed a behavioral AssertionError (quoted in mutations.log / sdk-mutations.log); the adjudication is scripted (adjudicate.mjs, 19/19). Witness: 03-mutation-matrix-all-killed.png.

mutant guard introduced by result killed by
M0 unmutated control 103/103 green
M1 keyset gate removed 67922db killed (2 red) keeps text chunks carrying an unmodeled update key / an unmodeled data key out of merged live entries
M2 classify discrete guard removed a03dcec killed (2 red) preserves discrete agent messages and their metadata; keeps generic discrete message and thought chunks separate at turn boundaries
M3 subagentType unmodeled c9c4be1 reverted killed (2 red) merges live subagent chunks carrying the producer-stamped meta pair; merges consecutive chunks sharing a defined parentToolCallId
M4 planToolCallId unmodeled a03dcec reverted killed (1 red) merges chunks carrying buildUpdateMeta timestamp and plan shapes
M5 empty parentToolCallId unmodeled a03dcec reverted killed (1 red) merges live chunks whose empty-string parentToolCallId the extractor ignores
M6 positive control: live aggregation disabled killed (14 red) all live-merge tests
R pristine restored 103/103 green
S1 replay entries unit string reverted (SDK) killed (1 red of 291) describes live truncation precisely and preserves structured data
R SDK pristine restored 291/291 green

7/7 mutants killed, 0 survivors. Attribution contrast: M2 (turn-compaction guard) leaves all live-journal discrete tests green and vice versa — the two layers are pinned independently, so a future regression in either gate is caught by its own tests.

Targeted gates (all green)

gate result
packages/acp-bridge full suite (26 files) 1199/1199
packages/sdk-typescript daemonUi.test.ts 291/291
packages/cli serve.test.ts + fast-path.test.ts 139/139
integration qwen-serve-live-journal-recovery (real daemon, bundled CLI; includes the NEW no-truncation test and the tightened truncation assertion) 2/2
integration qwen-serve-webui-live-journal-recovery (real daemon) 1/1

Gate liveness: the mutation matrix above is the proof — the same vitest harnesses reporting green here went red on planted single-line changes. Witness: 04-targeted-gates-all-green.png.

Not covered

  • Per-commit attribution: shallow checkout reaches 1/9 commits listed in the metadata (git rev-parse --is-shallow-repository = true); verification is of the aggregate HEAD^1..HEAD diff. The delta commits are nonetheless individually attributable by content (each guard maps to its commit's described change, and mutants M1–M5 target exactly those guards).
  • Round-1 boundary-harness cells not re-run 1:1: the 34 round-1 boundary assertions (tool boundaries, per-dimension identity splits, non-text content, user_message_chunk) were not each re-executed; equivalent coverage at the new head is the suite's it.each boundary blocks (does-not-merge-across ×5, merges-sharing ×5 — green inside the 1199) plus harness cells C4, C8–C11. The identity-split logic was untouched by the delta commits.
  • Web Shell browser rendering of merged entries (no browser harness here); the SDK reducer path is covered by daemonUi.test.ts (291) and the WebUI integration test.
  • Base-side daemon E2E: no base bundle built (budget); the engine-level A/B covers the changed surface and the base-arm cells pin the base symptom.
  • Repo-wide typecheck/lint (the PR's own CI covers them); no source edits were made during verification.
  • Windows/macOS (container is Linux).
  • EventBus ring replay, cross-layer replay cursor behavior, completed-turn compaction semantics beyond the discrete guard — declared out of scope by the PR; regression-guarded by the full acp-bridge suite (1199 tests).

Methodology

Environment: node:22-bookworm CI container (Node v22.23.2), merge-ref checkout (base af372e5a21, head c5375218, merge 695a9fbb1e), pre-built dist/ bundle verified fresh by grepping PR-specific strings (cap on replay entries retained, subagentType) in dist/chunks/ before trusting it. ab-harness.mjs imports the engine and EventBus from each tree's TS source via tsx — the control tree was a scratch git worktree at the base tip (removed after capture); the engine's import closure is package-local and untouched by the PR, so the arms differ only in compactionEngine.ts, and the PR changes no lockfile, so no workspace-link or dependency confound applies. Mutations were applied by exact string replacement (mutate.mjs) to the head source and restored after each run; source tree confirmed byte-identical to HEAD afterward. Integration tests ran the shipped bundle against the repo's mock ACP agent with a real daemon. Raw logs: ab-base.log, ab-head.log, mutations.log, mutations-restore.log, sdk-gate.log, sdk-mutations.log, gate-*.log, adjudication.log, matrix-summary.txt in this directory; captures in evidence/.

Evidence images

01-ab-base-arm-truncates

02-ab-head-arm-complete

03-mutation-matrix-all-killed

04-targeted-gates-all-green

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@wenshao
wenshao enabled auto-merge August 10, 2026 03:44

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix review round for PR #8801 — no action needed

The workflow delivered only feedback newer than the last evaluation (2026-08-10T01:09:56Z):

  • Reviews: none
  • Inline comments: none
  • Issue-level comments: one automated status notification from @qwen-code-ci-bot (2026-08-10T03:38:59Z) reporting that sandboxed verification is running and its report will be posted when the run completes. It is informational and requests no code change.
  • Failed / still-red checks: none — all completed checks are SUCCESS or SKIPPED; the only in-progress check is the sandbox verification run itself.

All earlier review and inline-comment findings predate the last evaluation and were already handled in previous rounds; the current branch head (c5375218a3d852f4d25d9b4284f5902d5aea80fd) already contains those fixes plus the merge of origin/main. There is no base-conflict flag to resolve (--conflict false).

No code changes were made and no commit was created this round. If the pending sandbox verification report raises findings, they will be triaged in a later round.

中文说明

PR #8801 的 Autofix 审查轮次 —— 无需处理

工作流仅提供了上次评估(2026-08-10T01:09:56Z)之后的新反馈:

  • 审查(Reviews):
  • 行内评论(Inline comments):
  • Issue 级评论: 仅有一条来自 @qwen-code-ci-bot 的自动状态通知(2026-08-10T03:38:59Z),说明沙箱验证正在运行,报告将在运行完成后发布。该评论仅为信息性通知,不要求任何代码变更。
  • 失败 / 持续失败的检查: 无 —— 所有已完成的检查均为 SUCCESSSKIPPED;唯一处于进行中状态的检查是沙箱验证运行本身。

此前的所有审查和行内评论发现均早于上次评估,已在之前的轮次中处理完毕;当前分支头(c5375218a3d852f4d25d9b4284f5902d5aea80fd)已包含这些修复以及 origin/main 的合并。本轮也没有需要解决的基础分支冲突标记(--conflict false)。

本轮未做任何代码变更,也未创建任何提交。如果待完成的沙箱验证报告提出了问题,将在后续轮次中进行分类处理。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for one verified Critical. The previous integration-test blocker and all currently visible review-thread findings are fixed on this head, and the targeted compaction and SDK suites pass. The remaining inline issue causes legal ACP content metadata to disappear from load/recovery snapshots.

Comment thread packages/acp-bridge/src/compactionEngine.ts
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 5/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 5/100 轮)。改动内容与我反驳保留之处如下:

Review feedback addressed — PR #8801

Feedback points

[Critical] Preserve ACP TextContent metadata in live replay (rc:3746524714, @doudouOUC, CHANGES_REQUESTED)

Decision: addressed (Required — verified correctness defect).

The finding was verified against the exact head: hasOnlyModeledChunkKeys() only checked the keys of data and data.update, so a chunk carrying annotations or _meta inside update.content still joined a LiveJournalTextSegment, and the snapshot() rebuild replaced its content with { type, text } — dropping the legal ACP TextContent fields from load/recovery snapshots while real-time SSE retained them.

Fix (option 1 from the review): the admission gate now also requires the content object's own keys to be modeled (type / text only). Chunks carrying extra content fields are left as raw journal entries, so replay returns them exactly as SSE delivered them; a rich chunk also cleanly splits segments, matching how other unmergeable events behave. No change to plain-chunk merging, journal cap accounting, or the completed-turn compaction path (its { type, text } rebuild pre-exists this PR on main).

Changes:

  • packages/acp-bridge/src/compactionEngine.ts — extend hasOnlyModeledChunkKeys() with the content-key check; comment updated to state the invariant.
  • packages/acp-bridge/src/compactionEngine.test.ts — regression test using the exact wire shape from the review (content.annotations = { audience: ["assistant"] }, content._meta = { vendor: "keep" }), asserting the chunk stays a separate live entry and its content round-trips intact. Confirmed the new test fails on the pre-fix code (all three chunks merged into one entry) and passes with the fix.

Conflicts

None (--conflict false; no merge performed this round).

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npx prettier --check on the two touched files — passed
  • Vitest packages/acp-bridge (touched package, full) — 26 files / 1200 tests passed, including compactionEngine.test.ts with 104 tests (103 existing + 1 new regression)
  • Vitest packages/sdk-typescript test/unit/daemonUi.test.ts (consumer of these events) — 291 tests passed
  • npm run bundle — passed; focused integration run integration-tests/cli/qwen-serve-live-journal-recovery with QWEN_SANDBOX=false — 2 tests passed (compatible streams still merge within the replay entry cap; truncation attribution intact)
  • A/B evidence: the new regression test fails against the pre-fix engine (1 merged entry, metadata lost) and passes after the fix
  • No settings source changed, so npm run generate:settings-schema was not required
中文说明

已处理的评审反馈 — PR #8801

反馈点

[Critical] 在实时回放中保留 ACP TextContent 元数据(rc:3746524714@doudouOUC,CHANGES_REQUESTED)

决定:已修复(Required — 已核实的正确性缺陷)。

已在对应提交上核实该问题:hasOnlyModeledChunkKeys() 只检查了 datadata.update 的键,因此在 update.content 内部携带 annotations_meta 的 chunk 仍会加入 LiveJournalTextSegment,而 snapshot() 重建时会把 content 替换为 { type, text } —— 导致合法的 ACP TextContent 字段在加载/恢复快照中丢失,而实时 SSE 却保留了这些字段。

修复方案(采用评审中的方案 1):准入门禁现在同时要求 content 对象自身的键也必须都是已建模的(仅 type / text)。携带额外 content 字段的 chunk 会保留为原始 journal 条目,因此回放返回的内容与 SSE 实时下发的完全一致;富 content 的 chunk 还会正常切分段(segment),与其他不可合并事件的行为一致。普通 chunk 的合并、journal 上限计数以及整轮完成后的压缩路径均未改动(后者的 { type, text } 重建在本 PR 之前就已存在于 main 上)。

改动内容:

  • packages/acp-bridge/src/compactionEngine.ts —— 扩展 hasOnlyModeledChunkKeys(),加入 content 键检查;同步更新注释以说明该约束。
  • packages/acp-bridge/src/compactionEngine.test.ts —— 新增回归测试,使用评审中给出的真实 wire 形状(content.annotations = { audience: ["assistant"] }content._meta = { vendor: "keep" }),断言该 chunk 保持为独立的 live 条目,且其 content 原样往返保留。已确认该测试在修复前的代码上会失败(三个 chunk 被合并为一个条目),修复后通过。

冲突

无(--conflict false;本轮未执行任何合并)。

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • 对两个改动文件执行 npx prettier --check — 通过
  • Vitest packages/acp-bridge( touched 包,全量)— 26 个文件 / 1200 个测试通过,其中 compactionEngine.test.ts 共 104 个测试(103 个既有 + 1 个新回归测试)
  • Vitest packages/sdk-typescript test/unit/daemonUi.test.ts(这些事件的消费方)— 291 个测试通过
  • npm run bundle — 通过;聚焦集成测试 integration-tests/cli/qwen-serve-live-journal-recoveryQWEN_SANDBOX=false)— 2 个测试通过(兼容的流式 chunk 仍在回放条目上限内合并;截断归因保持正常)
  • A/B 证据:新回归测试在修复前的引擎上失败(合并为 1 个条目、元数据丢失),修复后通过
  • 未改动任何 settings 源文件,因此无需运行 npm run generate:settings-schema

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. Suggestions are inline. 1 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite (qwen-serve-live-journal-recovery, qwen-serve-webui-live-journal-recovery) did not run locally; the diff's two changed integration tests are only exercised by that skipped job. Test Plan (not a blocker): src/compactionEngine.test.tsno such file or directory; test/unit/daemonUi.test.tsno such file or directory; src/commands/serve.test.tsno such file or directory; src/serve/fast-path.test.tsno such file or directory; 92 passed — this review observed 1200, 18740, 1502, 481, 2983, 459 passed; and 2 more.

中文说明

已审查。 建议见行内评论。 1 条建议级发现无法锚定到改动行,已丢弃;此处无需进一步处理。 未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite (qwen-serve-live-journal-recovery, qwen-serve-webui-live-journal-recovery) did not run locally; the diff's two changed integration tests are only exercised by that skipped job。 Test Plan(非阻断):src/compactionEngine.test.tsno such file or directory; test/unit/daemonUi.test.tsno such file or directory; src/commands/serve.test.tsno such file or directory; src/serve/fast-path.test.tsno such file or directory; 92 passed — this review observed 1200, 18740, 1502, 481, 2983, 459 passed; and 2 more。

— qwen3.8-max via Qwen Code /review (v0.21.8)

Comment thread packages/acp-bridge/src/compactionEngine.ts
Comment thread integration-tests/cli/qwen-serve-live-journal-recovery.test.ts
Comment thread docs/design/live-journal-truncation-recovery.md
Comment thread packages/acp-bridge/src/compactionEngine.test.ts
Comment thread packages/sdk-typescript/src/daemon/ui/normalizer.ts
Comment thread packages/acp-bridge/src/compactionEngine.test.ts
Comment thread packages/sdk-typescript/test/unit/daemonUi.test.ts
Comment thread packages/acp-bridge/src/compactionEngine.test.ts
Comment thread packages/acp-bridge/src/compactionEngine.test.ts
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix review round: no action taken

No actionable feedback was found for this round:

  • No new reviews, inline comments, or issue-level comments newer than the last evaluation (2026-08-10T04:01:34Z).
  • No failed or still-red CI checks — all checks on the current head concluded SUCCESS or SKIPPED.
  • No merge conflict resolution was requested (--conflict false).

The PR is in Critical-only mode after five change-producing rounds. The listed deferred non-Critical findings are an audit record kept open for human follow-up; per the mode's rules this round makes no code changes, resolves no threads, and posts no replies for them.

No commits were made; the branch remains at its previous verified head.

中文说明

Autofix 审查轮次:未采取任何操作

本轮未发现可处理的反馈:

  • 自上次评估(2026-08-10T04:01:34Z)之后,没有新的审查、行内评论或议题级评论。
  • 没有失败或持续失败的 CI 检查 —— 当前 head 上的所有检查结论均为 SUCCESS 或 SKIPPED。
  • 没有请求解决合并冲突(--conflict false)。

该 PR 在经历五个产生改动的轮次后已进入仅处理 Critical 的模式。列出的已延后非 Critical 发现仅作为审计记录保留,供人工后续跟进;按照该模式的规则,本轮不修改代码、不解决任何讨论串,也不针对这些条目发布回复。

本轮未产生任何提交;分支保持在之前已验证的 head 上。

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@wenshao

wenshao commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 153 passed · 0 failed · 153 total

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:153 通过 · 0 失败 · 153 总计

Verification report

PR 8801 verification (round 3) — fix(acp-bridge): bound live journal replay chunks

Verdict: merge-ready — 153/153 scripted assertions passed, 0 unexpected failures. Verified head 6c2dad8e0b96bcbade164b18fe174af390841467 (merge ref c5b851af5a, base tip e60d182241). Follow-up round: previous head was c5375218 (round 2); delta = commit 6c2dad8e (preserve TextContent metadata in live journal replay) plus a base advance af372e5a21e60d182241. All carried-forward measurements were re-run at the new head/base, never diffed from the old report.

中文摘要
  • 结论merge-ready。153/153 脚本化断言通过,0 个意外失败。本轮为跟进轮:上一轮验证到 c5375218,此后 PR 新增 1 个提交 6c2dad8e(live journal 回放中保留 TextContent 元数据)+ base 前进;所有上一轮测量均在新 head/base 上重新执行。
  • A/B 结论(三构建对照,见「Central claim + A/B」表;截图 01-ab-base-arm-truncates.png02-ab-head-arm-complete.png03-delta-revert-loses-metadata.png):base 上 12 000 细粒度 chunk 触发截断(丢 2 000 源事件、最早文本丢失);head 聚合为 47 个有界 entry、文本逐字节完整。本轮 delta 的负载性证明采用三格对照:把 delta hunk(content 键集门控子句)从 head 还原后(= 上一轮行为),携带 ACP annotations/_meta 的 TextContent chunk 会被 spread-merge 聚合、元数据被 {type, text} 重建丢弃(缺陷复现);当前 head 上此类 chunk 保持独立原始 entry、元数据逐字节保留,且其前后的普通 chunk 仍正常聚合。
  • 测试有效性:4 个单点突变全部被 PR 自带测试以行为断言杀死(04-mutation-matrix-all-killed.png)——还原 delta hunk(M1)与放宽 content 门控(M2)各红 1 个测试(正是新增的 annotations 测试,归因精确);整体关闭键集门控(M3 阳性对照)红 3 个;SDK 侧去掉 thought.text.delta 的 meta(S1)红 1 个。两侧恢复对照全绿(104/104、291/291)。
  • 门禁05-targeted-gates-all-green.png):acp-bridge 全套 1200/1200(上一轮 1199,+1 即新增 annotations 测试)、SDK daemonUi 291/291、CLI serve 139/139、真实 daemon 集成测试 3/3(含新增 no-truncation 用例,跑在发布的 dist/cli.js bundle 上)。
  • 非缺陷观察(详见 Findings):① 完成态 turn 压缩仍会丢弃 content 级 annotations(base/head 两臂一致,属既有折叠契约,非本 PR 引入);② 交错并行的多 subagent 流在 live journal 中仍保持 discrete(合并仅限连续 chunk,文档声明范围),turn 完成后 completed replay 对两个 parent 均完整重组(重新测量,两结论均保持)。
  • 未覆盖:per-commit 归因(浅克隆 10 个提交仅 delta 提交本身可达,已按内容精确归因)、Web Shell 浏览器渲染、Windows/macOS、base 侧 daemon 级 E2E。

Previous-finding status (round 2 → round 3)

Round 2 reported no blocking findings; three observations. Status at the new head (each re-measured, not diffed):

# round-2 finding severity status at 6c2dad8e
1 Interleaved parallel subagent streams still truncate the live journal mid-turn (merge is consecutive-only) observation (by design) stands, re-measured: interleaved.mjs (6/6) — 200 A + 200 B alternating fragments at cap 100 still trip the marker (truncated=300 / retained=100, source-event units, fullTranscriptAvailable: true); the completed-turn replay reassembles BOTH parents fully and ungarbled via the (kind, parentToolCallId) slots. Graceful degradation unchanged.
2 PR body's local test counts (92/284/132) differed from measured counts observation stands, re-measured: counts are now 104/291/139 at the new head (engine suite +1 = the new annotations test added by the delta commit; acp-bridge full 1199 → 1200, same +1). Drift fully explained by the PR's own later commits and the main merges; all suites green. Not a defect.
3 Metadata snapshot's baseRefOid predated the merge ref's base tip observation recurs: snapshot says 33e602cc…, local base tip is e60d182241. A/B used the local base tip per the merge-ref checkout contract, as rounds 1–2 did.

Round-2's verified central cells were re-run as the C1/C2/C8/C10/C13 rows of the table below (all hold); round-2's boundary matrix (it.each does-not-merge-across / merges-sharing blocks) re-ran inside the head suite (green within the 1200, visible per-test in mutations-m1-full.log's passing list). Round-2's mutation matrix is superseded by this round's matrix, which targets the current guards including the new content clause.

Central claim + A/B

Central claim: during an unfinished turn, consecutive compatible assistant text/thought chunks share bounded replay entries (≤256 source events each); maxJournalEvents counts replay entries while byte accounting and truncation diagnostics keep source-event units; realtime SSE delivery is unchanged. Delta claim (this round, commit 6c2dad8e): chunks whose TextContent content object carries any key beyond {type, text} (ACP annotations / _meta) never join a live-journal merge segment — the mergeLiveJournalTextEvent content rebuild ({ type: 'text', text: joined }) would silently drop those keys, so such chunks replay exactly as SSE delivered them.

Three-build design: besides base and head, a delta-revert build was compiled — head source with ONLY the content-keyset clause of hasOnlyModeledChunkKeys removed (exact string replacement, 1 occurrence; make-variant.mjs). It reproduces round-2 behavior for this surface and isolates what the delta hunk alone contributes. Harness ab-harness.mjs drives TurnBoundaryCompactionEngine + EventBus from each tree's TS source via tsx. Control validity: the engine's import closure is package-local (eventBus.ts imports only node:crypto; replayWindowLimits.ts imports nothing; both byte-identical between arms — git diff HEAD^1..HEAD on them is empty), so base vs head differs only in compactionEngine.ts; the base worktree was confirmed at e60d182241 with zero aggregation symbols (grep -c = 0). The PR changes no package.json/lockfile and the closure resolves no @qwen-code/* package, so neither a dependency nor a workspace-symlink confound exists. Witnesses: 01-ab-base-arm-truncates.png, 02-ab-head-arm-complete.png, 03-delta-revert-loses-metadata.png.

cell scenario base (e60d1822) delta-revert (round-2 behavior) head (6c2dad8e)
C1 scale 12 000 plain chunks, default caps marker, truncated=2000, retained=10000, oldest text lost 47 entries, text byte-complete 47 entries, text byte-complete, ids 256/…/12000
C2 cap3-20 20 chunks, cap 3 marker, truncated=17, c0; lost 1 entry id=20, all 20 same as revert
C3 delta core [plain, annotated(annotations+_meta), plain] 3 raw entries (no aggregation) 1 merged entry — annotations + content-_meta LOST (round-2 defect reproduces) 3 entries; annotated chunk byte-identical incl. annotations and _meta
C4/C5 siblings annotations-only / content-_meta-only 3 raw merged, metadata lost discrete, metadata preserved
C6 merge-around a, b, annotated-X, c, d 5 raw 1 merged abXcd, annotations lost [merged ab, raw X, merged cd]; merged entries carry no leaked keys; ids 2/3/5
C7 realtime SSE 5 plain + 1 annotated via EventBus.publish subscriber: 6 raw frames; journal 6 entries subscriber: 6 raw frames (identical); journal 1 merged entry, metadata lost subscriber: 6 raw frames (identical); journal [merged(5), raw annotated], metadata intact; serverTimestamp stamped
C8 subagent 12 000 {parentToolCallId, subagentType} fragments marker, truncated=2000 47 entries, complete, label carried 47 entries, complete, label carried
C9 completion annotated chunk through turn_complete live raw → completed fold drops annotations live metadata lost pre-boundary → fold drops too live journal preserves annotations pre-boundary; completed fold drops them (pre-existing, equal on base)
C10 marker units 1200 chunks, cap 3 truncated=1197, retained=3 (raw events) truncated=512, retained=688 (source events), maxEvents=3 = 3 entries, sum=1200, scope=live_journal same as revert
C11 256 rollover 300 plain + annotated MID + 2 plain 303 raw 2 entries, MID merged in, metadata lost [256-merged, 44-merged, raw MID, 2-merged]; MID keeps annotations; ids 256/300/303
C12 edge content with own key set to undefined raw merges discrete (conservative), payload verbatim
C13 discrete thought qwenDiscreteMessage thought mid-run 3 raw discrete + meta intact discrete + meta intact

Arm totals: base 31/31, delta-revert 40/40, head 56/56 — every predicted base/revert failure reproduced exactly as encoded (predicted breakage counts as a passing assertion); the C3–C7/C11 flips are the load-bearing proof of the delta hunk. Re-measured interleaved-subagent observation: interleaved.mjs 6/6 (see previous-finding table, row 1).

Corrections

None needed this round. One attribution note for readers of earlier rounds: the SDK normalizer change carrying update _meta on thought.text.delta frames (with the keeps discrete thought messages separate with their metadata test) is part of the aggregate PR diff vs base but predates this round's delta — the SDK suite counts 291 tests at both round-2 head and the current head, placing that change in an earlier commit already verified in round 2. Its vacuity check was re-run anyway (mutant S1 below, killed).

Findings

None blocking. Observations for the reviewer, each with its repro:

  1. Completed-turn compaction still drops TextContent content-level metadata (pre-existing, both arms). The delta preserves annotations/_meta in the live journal by keeping such chunks discrete, but the pre-existing completed-turn fold (mergeTextSlotmakeMergedSessionUpdateEvent, which rebuilds content as { type, text }) discards them on turn_complete. Measured identical on base and head (C9: folded text aXb complete, annotations absent on BOTH arms), so this is the documented lossy fold contract, not a PR regression. Worth naming because the delta's title is "preserve TextContent metadata" and the two replay surfaces now disagree for the same turn: a client loading mid-turn sees annotations; one loading after completion sees the folded text without them (update-level _meta still survives the fold via lastMeta). Repro: node --import tsx/esm ab-harness.mjs --arm head …, cell C9 (and --arm base for the equality).
  2. Interleaved parallel subagent streams still truncate mid-turn (by design) — stands, re-measured. interleaved.mjs: truncated=300/retained=100 at cap 100 on the new head; fullTranscriptAvailable: true; completed replay reassembles both parents byte-exact. Consecutive-only merging is the documented scope; the single-subagent and same-parent streams are fully fixed (C8).
  3. Test-count drift (carried, re-measured): PR body lists 92/284/132; round 3 measures 104/291/139 for the three named suites. Cause: the PR's own later commits (incl. this round's new annotations test) plus main merges. All green.
  4. Metadata snapshot baseRefOid stale (recurring): snapshot 33e602cc… vs merge-ref base tip e60d182241; A/B used the local base tip per contract.

Mutation matrix (vacuity)

Single-point mutants applied by exact string replacement (mutate.mjs, uniqueness-checked before each apply) to the head source, run against the unmodified head test files (compactionEngine.test.ts 104 tests; SDK daemonUi.test.ts 291 tests), restored after each run; tree confirmed git status-clean afterward. Every kill failed a behavioral AssertionError quoting expected-vs-actual (e.g. expected [ … ] to have a length of 3 but got 1) — no compile/import failures. Witness: 04-mutation-matrix-all-killed.png; raw logs mutations-m{1,2,3}-full.log, sdk-mutations-s1-full.log.

mutant guard introduced by result killed by
M0 unmutated control (engine) 104/104 green
M1 content-keyset clause removed (the delta hunk) 6c2dad8e killed (exactly 1 red) keeps ACP TextContent annotations and _meta out of merged live entries
M2 content gate accepts any object content 6c2dad8e loosened killed (exactly 1 red) same annotations test
M3 positive control: hasOnlyModeledChunkKeys disabled killed (3 red) the two unmodeled-key tests + the annotations test
R pristine restored 104/104 green
S0 unmutated control (SDK) 291/291 green
S1 thought.text.delta meta dropped earlier PR commit (pre-delta) killed (1 red of 291) keeps discrete thought messages separate with their metadata

4/4 mutants killed, 0 survivors. Attribution is exact: M1 and M2 each red exactly one test (the one the delta commit added); M3 reds exactly the three tests pinning the keyset gate. M1 doubles as the vacuity check for the delta's new test — reverting the source hunk fails the intended behavioral assertion, so the test pins the change and the change is load-bearing. Adjudicated by adjudicate.mjs (20/20, in 05-targeted-gates-all-green.png).

Targeted gates (all green)

gate result
packages/acp-bridge full suite (26 files) 1200/1200
packages/sdk-typescript daemonUi.test.ts 291/291
packages/cli serve.test.ts + fast-path.test.ts 139/139
integration qwen-serve-live-journal-recovery (real daemon, shipped dist/cli.js bundle; includes the NEW no-truncation test and the tightened truncation assertion) 2/2
integration qwen-serve-webui-live-journal-recovery (real daemon) 1/1

Gate liveness: the mutation matrix above is the proof for the engine and SDK suites — the same vitest harnesses reporting green here went red on planted single-clause changes. The integration no-truncation test is itself an end-to-end A/B witness: at --max-journal-events 3 with 20 live chunks, the un-aggregated base behavior provably truncates (harness C2), so its green on the shipped bundle requires the aggregation. Bundle freshness verified by grepping PR-specific strings in dist/chunks/ before trusting it. Witness: 05-targeted-gates-all-green.png.

Not covered

  • Per-commit attribution for 9 of 10 commits: the shallow merge-ref checkout reaches only 2 commits (git rev-list HEAD^1..HEAD^2 = 6c2dad8e alone; --is-shallow-repository = true) vs 10 listed in the metadata. The reachable commit is exactly this round's delta, and its hunk was isolated and exercised individually (delta-revert build + M1/M2); the remaining commits were verified as the aggregate HEAD^1..HEAD diff, with round 2 having verified everything up to c5375218.
  • Base-side daemon E2E (budget): no base bundle built; the engine-level three-build A/B covers the changed surface, and the base-arm cells pin the base symptom.
  • Web Shell browser rendering of merged entries (no browser harness here); the SDK reducer path is covered by daemonUi.test.ts (291) and the WebUI integration test.
  • Completed-turn fold preserving content-level metadata — finding 1 shows the fold drops it on BOTH arms (pre-existing contract); no fix was attempted or measured as a candidate patch this round.
  • Repo-wide typecheck/lint (the PR's own CI covers them); no source edits remain (tree verified clean).
  • Windows/macOS (container is Linux).
  • EventBus ring replay, cross-layer replay cursor behavior, completed-turn compaction semantics beyond the above — declared out of scope by the PR; regression-guarded by the full acp-bridge suite (1200 tests).

Methodology

Environment: node:22-bookworm CI container (Node v22.23.2), merge-ref checkout (base e60d182241, head 6c2dad8e, merge c5b851af5a), pre-built dist/ bundle verified fresh by grepping PR-specific strings in dist/chunks/ before use. ab-harness.mjs imports the engine and EventBus from each tree's TS source via tsx: base from a scratch git worktree at HEAD^1 (removed after capture), delta-revert from a compiled variant differing from head by exactly the one content-keyset clause (built by make-variant.mjs, which verifies the clause occurs exactly once before replacing). The engine's import closure is package-local and untouched by the PR, and the PR changes no lockfile, so the arms differ only in compactionEngine.ts with no workspace-link or dependency confound. Mutations were applied by exact, uniqueness-checked string replacement and reverted after each run; git status --porcelain empty afterward. Integration tests ran the shipped bundle against the repo's mock ACP agent with a real daemon. Raw logs: ab-{base,revert,head}.log, interleaved.log, mutations-m{1,2,3}-full.log, mutations-restore.log, sdk-gate.log, sdk-mutations-s1-full.log, gate-{engine-m0,acpbridge-full,cli-serve,integration}.log, adjudication.log, matrix-summary.txt in this directory; captures in evidence/.

Evidence images

01-ab-base-arm-truncates

02-ab-head-arm-complete

03-delta-revert-loses-metadata

04-mutation-matrix-all-killed

05-targeted-gates-all-green

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix review round: no action needed

This round found no actionable feedback for PR #8801:

  • Reviews: none newer than the last evaluation (2026-08-10T07:32:31Z).
  • Inline comments: none.
  • Issue-level comments: none.
  • Failed checks: none.
  • Still-red checks: none persisting from earlier rounds.

Critical-only mode is active after 5 change-producing rounds. The one non-Critical item listed in the deferred feedback section (a PR comment from the CI bot) is an audit record left open for human follow-up; per the workflow rules it was not addressed in this round.

No code changes were made and no commit was created. The branch head remains unchanged.

中文说明

Autofix 评审轮次:无需处理

本轮未发现针对 PR #8801 的可处理反馈:

  • 评审(Reviews): 自上次评估(2026-08-10T07:32:31Z)以来没有新的评审。
  • 行内评论(Inline comments): 无。
  • Issue 级评论(Issue-level comments): 无。
  • 失败的检查(Failed checks): 无。
  • 持续失败的检查(Still-red checks): 没有从之前轮次遗留的失败检查。

在完成 5 个产生改动的轮次后,已进入仅处理 Critical 的模式。延后反馈区域中列出的唯一一条非 Critical 条目(来自 CI 机器人的一条 PR 评论)属于审计记录,保持开放以留待人工跟进;按照工作流规则,本轮不予处理。

未做任何代码改动,也未创建提交。分支 head 保持不变。

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

Comment thread packages/acp-bridge/src/compactionEngine.ts

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE (C=0) at 6c2dad8e0b96bcbade164b18fe174af390841467.

Reviewed the complete 15-file diff and traced the per-session live-journal change through EventBus snapshot construction, ACP/REST replay delivery, SDK normalization and transcript reduction, WebUI recovery, CLI configuration, and protocol documentation. The previous ACP TextContent metadata Critical is fixed: rich annotations and content _meta remain raw and survive load replay unchanged.

Independent verification passed: compactionEngine.test.ts 104/104, both live-journal daemon integration files 3/3, build, and bundle. Current reported checks have no failures.

One non-blocking Suggestion remains inline: a hypothetical chunk carrying both qwenDiscreteMessage and parentToolCallId can reorder a later same-parent chunk during completed-turn compaction. No in-repo producer emits that metadata combination, so it does not block this PR and can be handled as a focused follow-up.

@wenshao
wenshao added this pull request to the merge queue Aug 10, 2026
Merged via the queue into QwenLM:main with commit 77bd04b Aug 10, 2026
184 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.9.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants