fix(acp-bridge): keep event attribution through turn compaction - #7499
fix(acp-bridge): keep event attribution through turn compaction#7499he-yufeng wants to merge 2 commits into
Conversation
makeMergedSessionUpdateEvent and mergeToolCallEvent rebuilt the event envelope from scratch and dropped top-level promptId and originatorClientId, even though the source chunks carried them. Live subscribers see those fields on every chunk, but a client resuming from a compacted replay lost them: prompt correlation broke and echo suppression (event.originatorClientId === clientId) silently stopped applying to merged events. Track the latest promptId / originatorClientId on text and thought slots (mirroring lastMeta / lastEnvelopeMeta) and copy them onto merged events; folded tool calls take them from the incoming event with the existing event as fallback, matching the id merge. Events whose sources carried no attribution still emit none. Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
|
Thanks for the PR! Template looks good ✓ — the What / Why / Reviewer Test Plan sections are all there and detailed. Minor housekeeping note: the formal Problem: real, not theoretical. This is DAEMON-007 from the open audit issue #7457, with a concrete mechanism: Direction: aligned. This is a correctness fix in the daemon ( Size: not applicable — Approach: the scope feels right and the discipline is good — the PR deliberately addresses only DAEMON-007's envelope attribution, leaving DAEMON-001 to #7458 and DAEMON-008's documented never-throws contract alone. The change mirrors the existing Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ —— What / Why / Reviewer Test Plan 三部分齐全且详尽。一个小的格式提醒:模板里的 问题:真实存在,非理论性。这是开放审计 issue #7457 中的 DAEMON-007,机制具体: 方向:对齐。这是 daemon( 规模:不适用 —— 方案:范围合理,纪律良好——PR 只处理 DAEMON-007 的 envelope 归属,把 DAEMON-001 留给 #7458,DAEMON-008 的"永不抛错"契约保持不动。改动完全沿用了现有 进入代码审查 🔍 — Qwen Code · qwen3.8-max-preview Reviewed at |
Code reviewI formed my independent take first: to keep attribution through the merge, track the latest The change is minimal and idiomatic. It threads the two fields through all four ingest sites (subagent merge/new, top-level merge/new), the snapshot emit, and both merge helpers, mirroring the surrounding bookkeeping so it reads as a natural extension. The spread is present-only-if-set, so attribution-less sources still emit nothing (one of the new tests guards exactly this). Type-correct against On the TestingThis is a non-UI change in the daemon ( Before (base commit, fix reverted): the two new "keeps …" tests fail, demonstrating the bug — merged events come back with no attribution: After (this PR): the targeted file passes in full: Full package regression run (tmux capture-pane): 898 passed across 19 files, matching the PR's claim. No regressions. 中文说明代码审查我先独立形成判断:要在合并中保留归属信息,就在 text/thought slot 上追踪最新的 改动最小且符合惯例。它在四个 ingest 位点(subagent 合并/新建、top-level 合并/新建)、snapshot 输出以及两个 merge helper 中贯通了这两个字段,沿用了周围的记账模式,读起来是自然扩展。展开是"有值才贴",因此无归属的源仍然不输出任何东西(其中一个新单测正是守护这一点)。对 关于 Stage 1 提到的 测试这是 daemon( Before(base 提交,回退修复): 两个新的 "keeps …" 测试失败,证明 bug 存在——合并事件返回时没有归属信息( After(本 PR): 目标文件全部通过(60 passed)。 整包回归(tmux capture-pane):19 个文件 898 个测试全部通过,与 PR 声明一致,无回归。 — Qwen Code · qwen3.8-max-preview Reviewed at |
|
Confidence: 4/5 — a clean, minimal, well-tested correctness fix; the only open item is a non-blocking confirmation about Stepping back: this is exactly the kind of PR the gate should let through. The problem is real and verified — I confirmed in code that the merge helpers rebuilt the envelope from scratch and dropped The implementation matches the simplest path I'd have proposed independently — thread the latest values through the existing slot bookkeeping and spread them present-only-if-set — and it mirrors the surrounding The one reservation, carried over from Stage 1 and non-blocking: #7457 also names If I had to maintain this in six months I'd thank the author. Approving. 中文说明置信度:4/5 —— 一个干净、最小、测试充分的正确性修复;唯一未决项是关于 退一步看:这正是 gate 应当放行的那类 PR。问题真实且已验证——我在代码中确认 merge helper 从头重建 envelope 并丢掉了 实现与我独立提出的最简路径一致——把最新值贯通到现有 slot 记账并以"有值才展开"的方式贴上——并且与周围 唯一保留意见(承自 Stage 1,非阻塞):#7457 还把 如果半年后由我来维护,我会感谢作者。批准。 — Qwen Code · qwen3.8-max-preview Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅
— qwen3.7-max via Qwen Code /review
|
@qwen-code /resolve |
PR QwenLM#7458 on main adopted a more structured approach to the same problem PR QwenLM#7499 solves (preserving event attribution through turn compaction). Resolved in favour of main's structured lastTurn/lastSessionId fields plus captureTurnFields/captureSessionId helpers, removing the duplicate spread operators both sides independently added to makeMergedSessionUpdateEvent and mergeToolCallEvent.
|
Qwen Code resolved the merge conflicts and pushed the branch update. Merge Conflict Resolution — PR #7499Root causePR #7458 ( Textual or semanticSemantic conflict. Both sides modified the same logic (attribution preservation) with incompatible representations:
Main's approach is a superset — it captures everything PR #7499 does plus The auto-merge also produced duplicate spread operators in two functions: // makeMergedSessionUpdateEvent had both turn?.promptId AND bare promptId spreads
// mergeToolCallEvent had promptId/originatorClientId spreads twice (before and after _meta)Both were cleaned up — keeping only main's versions. What is load-bearing
What I could not verify
中文说明合并冲突解决 — PR #7499根因PR #7458( 文本冲突还是语义冲突语义冲突。 双方修改了相同逻辑,但表示方式不兼容:
main 的方案是超集,额外捕获了 自动合并还在 关键约束
未能验证的部分
|
Review:
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
— qwen3.7-max via Qwen Code /review
| const textEvent = engine.snapshot().compactedTurns[0]!; | ||
| expect(textEvent.promptId).toBeUndefined(); | ||
| expect(textEvent.originatorClientId).toBeUndefined(); |
There was a problem hiding this comment.
[Suggestion] Missing test for data.sessionId preservation — Issue #7457 DAEMON-007 names three dropped stamps: promptId, originatorClientId, and data.sessionId. The production code (via #7458) correctly preserves all three, but this PR only tests two.
Failure scenario: a future refactor removing lastSessionId from the slot type or the sessionId parameter from makeMergedSessionUpdateEvent would regress silently — no test here would catch it.
Concrete cost: data.sessionId is the one stamp whose preservation this PR leaves unverified.
— qwen3.7-max via Qwen Code /review
| engine.ingest({ | ||
| ...makeTextChunk(1, 'Hello'), | ||
| promptId: 'prompt-1', | ||
| originatorClientId: 'client-a', | ||
| }); |
There was a problem hiding this comment.
[Suggestion] No test exercises the per-field ?? fallback with mixed attribution — Both captureTurnFields (text path) and mergeToolCallEvent (tool path) use event.promptId ?? previous?.promptId, but all three tests provide uniform attribution across every chunk.
Failure scenario: if the ?? fallback were replaced with a direct assignment, all three tests still pass — but in production, streaming commonly delivers attribution on the first chunk only. The merged event silently loses promptId.
Concrete cost: the core invariant documented in JSDoc ("Each field falls back independently") is untested.
— qwen3.7-max via Qwen Code /review
|
Closing this out. #7458 landed the same attribution preservation and goes further: merged text/thought events and folded tool calls now keep promptId, originatorClientId and data.sessionId on main, with tests covering both paths. Nothing left here worth rebasing. |
What this PR does
Turn-boundary compaction now keeps
promptIdandoriginatorClientIdon merged events. Refs #7457 (DAEMON-007).Why it's needed
session_updatechunks carry those two fields at the envelope level, and replay consumers use them: the bridge client groups transcript text by originator, and echo suppression comparesevent.originatorClientIdwith the subscriber's own client id. Both merge helpers rebuild the envelope from scratch and drop the fields, so after compaction a resuming client loses prompt correlation and can see its own messages echoed back, while the live stream for the same turn was fine.Text/thought slots now track the latest
promptId/originatorClientIdalongside the existinglastMeta/lastEnvelopeMetaand copy them onto the merged event. Folded tool calls take them from the incoming event, falling back to the existing one, same as the id merge. Events whose sources carried no attribution still emit none, and pass-through paths are untouched.Scope note: #7457 lists three defects. DAEMON-001 is covered by #7458, and DAEMON-008's bare
catch {}is the documented never-throws contract for the best-effort engine. This PR doesn't touch either.Reviewer Test Plan
How to verify
npx vitest run src/compactionEngine.test.tsinpackages/acp-bridge: 60 tests pass, three of them new (merged text keeps both fields, folded tool calls keep them with incoming winning, sources without attribution still emit none). Full package: 898 passed across 19 files.Evidence (Before & After)
N/A (non-UI change; coverage is the new unit tests)
Tested on