fix(core): Enforce single-writer session persistence - #7166
Conversation
E2E / verification reportFinal validation was performed after rebasing the single feature commit onto Passed
Environment-limited integration dry-runAn earlier CLI integration dry-run reported 126 passed, 18 skipped, and 12 failed cases: 11 failures required an unavailable Still to verify in review / CI
|
🩺 serve daemon A/BBuilt the PR base vs this PR head ✅ No response changes against the PR base across 4 scenario(s). — Qwen Code · serve A/B |
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
|
Thanks for the PR! Template looks good ✓ — all required sections filled in, including Risk & Scope and Linked Issues. Problem: Real and well-documented. Issue #7164 describes a P1 data-consistency bug — two processes can fork the transcript history, causing already-shown responses to silently disappear on restart. The architectural gap is confirmed in the source: Direction: Aligned. The single-writer lease with ownership token + authoritative reload + append fencing follows standard distributed-systems fencing patterns. The acceptance criteria in the issue map cleanly to the implementation. Design doc at Size: 5,917 production logic lines (additions+deletions, excluding test and generated files), 6,340 test lines, 91 docs lines, 84 files across Core, CLI, ACP bridge, Desktop, and VS Code. Stage 0 Tier 1 hard block does not apply (title is Approach: The core mechanism (new Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ — 所有章节都已填写,包括 Risk & Scope 和 Linked Issues。 问题: 真实且有充分文档记录。Issue #7164 描述了 P1 数据一致性 bug——两个进程可以分叉 transcript 历史,导致已展示的回复在重启后静默消失。源码中的架构缺陷已确认: 方向: 对齐。单写者租约 + 所有权令牌 + 权威性重新加载 + 追加 fencing 遵循标准分布式系统 fencing 模式。issue 中的验收标准与实现对齐。设计文档 规模: 5,917 行生产逻辑(additions+deletions,排除测试和生成文件),6,340 行测试,91 行文档,84 个文件跨越 Core、CLI、ACP bridge、Desktop 和 VS Code。Stage 0 Tier 1 硬限制不适用(标题是 方案: 核心机制(新增 进入代码审查 🔍 — Qwen Code · qwen3.7-max Reviewed at |
Code ReviewIndependent proposal: I would have used Comparison: The PR's approach exceeds my proposal in robustness. Instead of Correctness: No critical bugs found across the lease module, Config integration, session transitions, ACP bridge, CLI serve routes, session service maintenance operations, chat recording service, runtime status, desktop agent, or VS Code companion. Key highlights:
Non-blocking observations:
sequenceDiagram
participant P1 as Process A
participant Lk as Lock File
participant Tr as Transcript JSONL
participant P2 as Process B
P1->>Lk: acquire (wx create, owner_id=uuid)
P1->>Tr: readStableTranscript (reload under lease)
P1->>Tr: appendJsonLine (assert owner + byte length)
P2->>Lk: acquire (EEXIST, read existing)
Lk-->>P2: conflict (live owner, PID alive)
P2->>P2: SessionWriterConflictError
Note over P1: session continues normally
P1->>Lk: release (verify owner_id match, unlink)
Note over P2: retry after Process A exits
P2->>Lk: acquire (stale reclaim via rename-aside)
P2->>Tr: readStableTranscript (authoritative reload)
TestingUnit Tests (worktree, commit 39d1837)Ran all 22 changed test files across Core, ACP bridge, CLI, and VS Code:
Real-Scenario Testing (tmux, built from PR branch)Built the bundle from the PR branch and drove two real Test 1 — Lease lifecycle (headless prompt): Test 2 — Conflict detection (two processes, same session): Process A resumed the session interactively and acquired the lease: Process B tried to resume the same session — rejected before model turn: After Process A exited ( Process A exit summary confirmed the session remained intact: Result: All three core guarantees verified end-to-end — (1) lease acquired on session resume with proper fencing data, (2) second writer rejected with 中文说明代码审查独立方案: 我会使用 对比: PR 的方案在健壮性上超出了我的方案。作者构建了自定义租约模块:随机 UUID owner_id 作为 fencing token、原子 正确性: 在所有模块中均未发现关键 bug。三阶段 session 切换、ACP bridge 非阻塞观察:
测试单元测试在 worktree 中运行了所有 30 个变更测试文件,共 3,243 个测试全部通过。 真实场景(tmux)从 PR 分支构建 bundle,驱动两个真实
与维护者的独立 Linux 验证结果一致。 — Qwen Code · qwen3.7-max Reviewed at |
|
Confidence: 4/5 — clean review across all stages, tmux-verified conflict detection, and maintainer-confirmed on Linux; the only deduction is the scale (5,917 production lines, 84 files) which demands ongoing maintenance attention, but the design doc and test coverage justify it. This is a well-engineered fix for a real P1 data-consistency bug. The custom lease implementation is more robust than what I would have proposed ( The tmux test I ran confirms the core promise end-to-end: a second process is rejected with 3,243 tests pass across all 30 changed test files in my worktree run. No critical bugs, no security holes, no regressions found in the code review. The three-phase session transition protocol, the ACP bridge Non-blocking items for the author (carry forward from previous rounds, not blockers):
The maintainer has verified this PR on Linux with real processes and confirmed the single-writer guarantee holds in every scenario tested. The previous Stage 0 escalation for maintainer awareness is resolved. 中文说明置信度:4/5 — 所有阶段审查均干净,tmux 验证了冲突检测,维护者在 Linux 上确认;唯一扣分是规模(5,917 行生产代码,84 个文件)需要持续的维护关注,但设计文档和测试覆盖证明了其合理性。 这是一个针对真实 P1 数据一致性 bug 的精心设计的修复。自定义租约实现比我原本会提议的方案( 我运行的 tmux 测试端到端确认了核心承诺:第二个进程在开始模型轮次之前被 3,243 个测试在 worktree 中全部通过。代码审查中未发现关键 bug、安全漏洞或回归。 维护者已在 Linux 上用真实进程验证了此 PR,确认单写者保证在所有测试场景中成立。之前的 Stage 0 维护者关注升级已解决。 — Qwen Code · qwen3.7-max Reviewed at |
5a0dbc5 to
0f268dc
Compare
|
[codex] Conflict/CI remediation pushed in
Validation on the pushed commit: Inline review threads: 0 total, 0 unresolved. New CI is running on the rebased SHA. |
|
Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration. 中文请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. 1 Suggestion-level finding(s) could not be anchored to the diff; see the terminal output. Not reviewed: verification — a verifier ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and the posted findings cannot be counted as verified against it.
— qwen3.7-max via Qwen Code /review
Incident validationI compared this change with the captured incident logs. It directly addresses the observed failure mode: process A was still completing a tool turn while process B restored the same session from an older tail, after which both processes could append valid-looking children and the daemon/UI followed the stale branch instead of the final Wiki answer written by A. With this PR, process B reaches The focused Wiki conflict/handoff/restart test, live child-process lock test, ACP error mapping, and prompt-admission tests provide good layered evidence that this prevents recurrence of the diagnosed root cause. Before treating the incident as fully closed, I recommend one real two-process E2E using the same runtime output directory and session ID:
Rollout is also part of the fix: all old daemons and interactive processes must be drained before upgrading because old binaries do not honor the lease. Existing divergent transcripts remain a separate recovery task. |
doudouOUC
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. 1 Suggestion-level finding(s) could not be anchored to the diff; see the terminal output. Not reviewed: This PR introduces a process-level single-writer lease fo... — pointed at diff lines it never opened: it made tool calls, but none of them read the diff. Not reviewed: This PR introduces a process-level single-writer lease fo... — pointed at diff lines it never opened: it made tool calls, but none of them read the diff.
— bailian/glm-5.2 via Qwen Code /review
|
Review feedback addressed in
Verification: Core and CLI builds passed; full workspace typecheck passed; 179 affected Core tests and 441 affected CLI tests passed; changed-file ESLint and repository pre-commit checks passed. |
|
Review feedback addressed in
Verification: Core and CLI builds passed; full workspace typecheck passed; 448 affected Core tests and 18 affected CLI tests passed; changed-file ESLint and repository pre-commit checks passed. |
qwen-code-dev-bot
left a comment
There was a problem hiding this comment.
Blocking: the required Ubuntu test currently fails in Check VS Code companion notices are up-to-date. The job regenerates packages/vscode-ide-companion/NOTICES.txt and finds an uncommitted 13,218-line update, so this PR cannot pass its required CI as submitted. Please run npm run generate:notices --workspace=qwen-code-vscode-ide-companion and commit the resulting NOTICES.txt update, then rerun the check.
Prevent concurrent processes from appending divergent JSONL histories, fence transcript writes, and make session transitions and maintenance operations ownership-safe. Refs #7164 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Update ACP and Session test doubles for pinned runtime storage, and include the optional runtime argument in transcript telemetry assertions. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
764ce06 to
e5ade64
Compare
|
Addressed the blocking notices CI review in e5ade64.
Verification: npm run build; npm run typecheck; 1,149 Core tests; 1,620 CLI tests; 433 ACP bridge tests; 11 VS Code companion tests; 36 desktop tests. Re-running the notices generator now produces no diff. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: chunk 27, chunk 28, chunk 32, chunk 35, chunk 36, chunk 40 — no agent reported covering these; nobody read them.
Not reviewed: chunks 27, 28, 32, 35, 36, 40 — not reviewed (agents could not be launched).
Not reviewed: test-coverage matrix — agent could not be launched.
Not reviewed: invariant agents for acpAgent.ts and useBranchCommand.ts — agents could not be launched.
Not reviewed: reverse audit — auditor ran but was not launched with the CLI-built prompt.
Not reviewed: chunk 1 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 41 — launched with a prompt that is not the one the CLI built.
Not reviewed: Agent 0: Issue fidelity & root-cause ownership — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 1 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 2 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 3 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 4 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 5 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 6 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 7 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 8 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 9 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 10 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 11 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 12 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 13 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 14 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 15 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 16 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 17 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 18 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 19 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 20 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 21 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 22 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 23 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 24 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 25 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 26 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 27 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 28 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 29 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 30 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 31 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 32 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 33 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 34 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 35 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 36 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 37 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 38 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 39 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 40 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 41 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 42 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 43 — its prompt was built, but no agent on record was launched with it.
Not reviewed: Test coverage matrix (whole-diff) — its prompt was built, but no agent on record was launched with it.
Not reviewed: Agent 1b: Removed-behavior audit — its prompt was built, but no agent on record was launched with it.
Not reviewed: Agent 1c: Cross-file tracer — its prompt was built, but no agent on record was launched with it.
Not reviewed: Agent 7: Build & test verification — its prompt was built, but no agent on record was launched with it.
Not reviewed: Invariant agent A: state, timers, collections — packages/cli/src/acp-integration/acpAgent.ts — its prompt was built, but no agent on record was launched with it.
Not reviewed: Invariant agent B: counters, return values, error taxonomies — packages/cli/src/acp-integration/acpAgent.ts — its prompt was built, but no agent on record was launched with it.
Not reviewed: Invariant agent C: config fields, early returns — packages/cli/src/acp-integration/acpAgent.ts — its prompt was built, but no agent on record was launched with it.
Not reviewed: Invariant agent A: state, timers, collections — packages/cli/src/ui/hooks/useBranchCommand.ts — its prompt was built, but no agent on record was launched with it.
Not reviewed: Invariant agent B: counters, return values, error taxonomies — packages/cli/src/ui/hooks/useBranchCommand.ts — its prompt was built, but no agent on record was launched with it.
Not reviewed: Invariant agent C: config fields, early returns — packages/cli/src/ui/hooks/useBranchCommand.ts — its prompt was built, but no agent on record was launched with it.
Not reviewed: reverse audit — no auditor was launched with a prompt this skill builds — the pass that hunts what the rest of the review missed ran, if at all, without the method its brief carries.
Not reviewed: verification — the review posts findings, but no verifier was launched with a prompt this skill builds — they were ruled on, if at all, without the verdict bar its brief carries.
— qwen3.7-max via Qwen Code /review
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Review fix round 4 summary
The automated reviewer message about unreviewed chunks is informational tool output and requires no code change. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. Not reviewed: chunk 21, chunk 27, chunk 36 — no agent reported covering these; nobody read them. Not reviewed: chunk 21 — not reviewed (agent prompt mismatch). Not reviewed: chunk 27 — not reviewed (agent prompt mismatch). Not reviewed: chunk 36 — not reviewed (agent prompt mismatch). Not reviewed: build-and-test — build-test still running at review time. Not reviewed: Agent 0: Issue fidelity & root-cause ownership — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 1 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 2 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 3 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 4 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 5 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 6 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 7 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 8 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 9 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 10 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 11 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 12 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 13 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 14 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 15 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 16 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 17 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 18 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 19 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 20 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 21 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 22 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 23 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 24 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 25 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 26 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 27 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 28 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 29 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 30 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 31 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 32 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 33 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 34 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 35 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 36 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 37 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 38 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 39 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 40 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 41 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 42 — its prompt was built, but no agent on record was launched with it. Not reviewed: chunk 43 — its prompt was built, but no agent on record was launched with it. Not reviewed: Test coverage matrix (whole-diff) — its prompt was built, but no agent on record was launched with it. Not reviewed: Agent 1b: Removed-behavior audit — its prompt was built, but no agent on record was launched with it. Not reviewed: Agent 1c: Cross-file tracer — its prompt was built, but no agent on record was launched with it. Not reviewed: Agent 7: Build & test verification — its prompt was built, but no agent on record was launched with it. Not reviewed: Invariant agent A: state, timers, collections — packages/cli/src/acp-integration/acpAgent.ts — its prompt was built, but no agent on record was launched with it. Not reviewed: Invariant agent B: counters, return values, error taxonomies — packages/cli/src/acp-integration/acpAgent.ts — its prompt was built, but no agent on record was launched with it. Not reviewed: Invariant agent C: config fields, early returns — packages/cli/src/acp-integration/acpAgent.ts — its prompt was built, but no agent on record was launched with it. Not reviewed: Invariant agent A: state, timers, collections — packages/cli/src/ui/hooks/useBranchCommand.ts — its prompt was built, but no agent on record was launched with it. Not reviewed: Invariant agent B: counters, return values, error taxonomies — packages/cli/src/ui/hooks/useBranchCommand.ts — its prompt was built, but no agent on record was launched with it. Not reviewed: Invariant agent C: config fields, early returns — packages/cli/src/ui/hooks/useBranchCommand.ts — its prompt was built, but no agent on record was launched with it. Not reviewed: reverse audit — no auditor was launched with a prompt this skill builds — the pass that hunts what the rest of the review missed ran, if at all, without the method its brief carries. Not reviewed: verification — the review posts findings, but no verifier was launched with a prompt this skill builds — they were ruled on, if at all, without the verdict bar its brief carries.
— qwen3.7-max via Qwen Code /review
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Review fix round 5 summary
|
|
Re: review 4729338733 — two facts worth separating: the process that posted it, and the finding it posted. Process: that run built its roster prompts and launched almost none of them — all 43 chunk agents ran on rewritten prompts, and Step 4 verification never ran; the body itself discloses "the posted findings cannot be counted as verified". A CHANGES_REQUESTED resting on an unverified finding is a pipeline defect on our side: the 99-clause disclosure wall is collapsed by #7190 (merged), and #7191 makes an unverified blocker soften to Comment with disclosure instead of formally blocking. The finding: I verified it by hand against |
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Review fix round 8 (Critical-only)
Validation on a18bbef: npm run build; npm run typecheck; packages/core config.test.ts (428/428). The remaining non-Critical review threads stay unresolved under the post-round-5 policy. |
…se an all-built-none-launched roster (QwenLM#7190) * fix(review): one disclosure per subject — dedupe the Not-reviewed list, collapse an all-built-none-launched roster A public review body on QwenLM#7188 was a wall of twenty-two "Not reviewed" clauses for eleven roles: the run built every roster prompt, launched not one agent, and every disclosure appeared twice. Two defects, one wall: - compose-review's `unreviewed` list fills from two sides -- the caller's `unreviewedDimensions` and the coverage recomputation -- and a run that pastes the gate's own gap lines into its input posts every one of them twice. The chunk list has deduped by its `chunk <id>` prefix since that exact bug shipped there; the role list now dedupes by label too (everything before the entry's last em-dash segment, because an invariant agent's label legitimately carries one). When both sides name the same subject, the coverage-derived text wins -- it is the evidence-bounded register the body is written in. - coverage collapses "nobody BUILT anything" to one line, but the launched-side twin -- every prompt built, none launched -- was still one line per dimension, burying the single fact that explains all of them: the run stopped at the builder. Collapsed the same way, one line for the run; the per-role selectors survive for the repair. * fix(review): group Not-reviewed disclosures by cause, prefer the precise text on collision Second exhibit, same wall: the review posted on QwenLM#7166 was ninety-nine disclosure paragraphs over four causes -- forty-three chunks all launched with rewritten prompts, fifty-five roles built and never launched -- with the six real findings buried beneath. Neither QwenLM#7190 mechanism touched it: nothing was duplicated, and the roster was not all-unlaunched (the chunk agents ran). - Explained disclosures now group by their reason text, the same last em-dash segment the dedup keys on: same cause, one sentence, every subject on it -- exactly as the bare names already grouped under the shared whiff sentence. A reason embedding per-subject detail (an unread brief's path) differs per entry and keeps its own line. - The dedup now prefers coverage-derived text over the caller's AND the earliest coverage category on a within-coverage collision: a rewritten chunk is also, to the roster, a requirement with no verbatim launch, and the later roster text told the author "no agent was launched" about an agent that demonstrably ran. * fix(review): make the discarded-suggestions clause self-contained in the posted body "see the terminal output" pointed the PR author at a terminal only the operator has -- a dead reference that eight hours of real bot reviews carried onto five different pull requests. Say what happened and that nothing is owed, in the body's own register. * fix(review): carry disclosures structurally -- no reparsed prose, no coverage-side collapse Review follow-through on three confirmed Criticals, which converge on one design: - The all-built-none-launched collapse misfired on the exhibit it was built for: candidatesOf is also all-empty when every agent ran on a REWRITTEN prompt, so the aggregate claimed "the run stopped at the prompt builder" beside forty-three rewritten-launch disclosures that said otherwise. Removed -- and it also discarded the per-role subjects before the caller's echoes could dedupe against them. - The last-em-dash subject/reason split reparsed rendered prose, and a reason is free-form: labels carry a dash for an invariant's file, error interpolations carry anything. Coverage now hands compose the entries as {subject, reason} pairs (a new `disclosures` field beside the prose twins the stderr formatting keeps); the caller's entries are never parsed at all -- they dedupe by prefix against known coverage subjects, exactly as the chunk list already did, and render verbatim otherwise. - With subjects structural and per-role entries surviving to the dedup, the same-reason grouping does the collapsing for every shape at once: the QwenLM#7188 wall, the QwenLM#7166 wall, and the caller-echo variant all render as one sentence per cause -- with each subject under its most precise cause only. * refactor(review): single-source each disclosure's prose from its structural entry Review follow-through: every reason lived twice -- the prose push that feeds check-coverage's stderr and the disclose() that feeds the posted body -- and an edit to one and not the other would silently diverge what the operator reads from what the author is told. disclose() now returns the joined prose and the stderr arrays consume it, so there is one statement of each sentence. Plus the two nits: the needless covEntries copy, and a -1 guard on the verification-gap split (safe today by construction, latent tomorrow).
🧪 Local build & real-machine verification on Linux (merge reference)Since the PR's own test matrix left Linux as Environment: Debian 13 (kernel 6.12.63), Node v22.22.2, npm 10.9.7 · 1. Unit tests — all changed test files pass
2. Real two-process E2E (built TUI, live model)
Scenario 1 — fail-closed conflict (screenshot): Scenario 5+8 — transcript restored after Lock record on disk while owned (pid matches the live TUI process; removed on clean {"schema_version":1,"session_id":"79604444-…","owner_id":"89bc4b8e-…","pid":511276,
"process_start_time_ms":1784438518410,"hostname":"iZbp…","process_kind":"interactive",
"acquired_at":"2026-07-19T05:21:59.354Z","qwen_version":"0.19.12"}3. Finding 1 (bug): interactive startup
|
| 包 | 运行器 | 文件数 | 测试数 | 结果 |
|---|---|---|---|---|
| core | vitest | 17 | 1,151 | ✅ |
| cli | vitest | 17 | 1,622 | ✅ |
| acp-bridge | vitest | 2 | 433 | ✅ |
| vscode-ide-companion | vitest | 1 | 11 | ✅ |
| desktop/shared | bun test | 1 | 36 | ✅ |
| 合计 | 38 | 3,253 | 0 失败 |
2. 真实双进程 E2E(构建版 TUI + 真实模型)
| # | 场景 | 结果 |
|---|---|---|
| 1 | TUI 内 /resume 一个被存活进程持有的会话 → 失败关闭,报错清晰,两个进程均正常 |
✅ |
| 2 | 无头模式 qwen --resume <id> -p "…" 目标为被持有会话 → SessionWriterConflictError,退出码 1,无写入 |
✅ |
| 3 | 启动参数 qwen --resume <id>(交互模式)目标为被持有会话 → 永远停在 "Initializing…",无任何报错 |
❌ bug |
| 4 | 对照组:启动参数 --resume 空闲会话 → 立即恢复完整会话记录 |
✅ |
| 5 | kill -9 杀掉持有者 → 下次 /resume 回收过期锁;会话记录完整恢复;锁归新 PID 所有 |
✅ |
| 6 | 回收后继续新对话,然后 /quit 干净退出 → 锁文件被删除 |
✅ |
| 7 | /clear 与 /branch → 正常;/branch 获取分支会话锁并释放旧锁(进程恰好持有 1 把锁) |
✅ |
| 8 | kill→回收→追加 之后的记录完整性:11 条记录,单一线性 parent 链、零分叉,两个标记均在 | ✅ |
场景 1(失败关闭)与场景 5+8(kill -9 后恢复、继续对话且链保持线性)的截图见上方英文部分。
3. 发现 1(bug):交互模式启动 --resume 被持有会话时静默卡死
进程 B 在 "Initializing…" 停留 150+ 秒(CPU 几乎为 0),无任何提示;而 PR 的 Reviewer Test Plan 第 1 条预期此处应出现 session_writer_conflict。
根因(已在源码验证):Config.initialize() → activateChatRecording() → SessionWriterLease.acquire() 抛出 SessionWriterConflictError。交互模式下这个 await config.initialize() 位于 AppContainer 的挂载 effect(packages/cli/src/ui/AppContainer.tsx 中注释写明错误交给"全局捕获"),因此 rejection 只会到达 gemini.tsx 的 setupUnhandledRejectionHandler(),仅写入 debug console——UI 无限转圈。无头路径会打印错误并以退出码 1 结束,TUI 内 /resume 也有清晰报错;唯独这个启动路径没有处理。packages/cli/src/config/config.ts 中导入的 SessionWriterConflictError 只用于 --fork-session 分支。
重要的是失败关闭语义仍然成立——卡住的进程不会写入,持有者不受影响——所以这是 UX 缺口而非数据损坏风险,也与作者声明的"最终 rebase 后未验证交互集成"一致。建议:在启动初始化处捕获 SessionWriterConflictError,渲染与 TUI 内一致的"已被另一个 Qwen 进程打开"错误(或非零退出),而不是留在转圈状态。
4. 发现 2(测试卫生):单元测试向真实 ~/.qwen 泄漏锁文件
运行变更的测试文件后,我的真实 ~/.qwen/tmp/session-writer-locks/ 里多出 约 165 个锁文件且未清理(PID 为已死亡的 vitest worker,process_kind: "unknown"、qwen_version: null)。单独复跑定位:
- 仅
packages/core/src/config/config.test.ts→ 泄漏 55 个 packages/cli/src/ui/hooks/useResumeCommand.test.ts+packages/cli/src/ui/AppContainer.test.tsx→ 泄漏 100 个clearCommand.test.ts/useBranchCommand.test.ts→ 干净(0)
这些测试构造的 Config 实例其会话 runtime base 解析到了真实 home 目录。应将 runtimeBaseDir/storage 固定到每个测试的临时目录,确保 npm test 不写沙箱之外。对正确性无害(本机过期锁可回收),但会污染每位贡献者的 home,也可能与同时运行的真实会话产生交互。
5. 其他小问题
- 无头模式冲突时输出 "An unexpected critical error occurred" + 原始堆栈;换成一行结构化错误会更友好(报错文案本身不错:"This session is already open in another Qwen process.")。
- 过期锁文件只有在同一会话下次被 resume 时才会回收,没有针对废弃锁的清扫机制,
~/.qwen/tmp/session-writer-locks/会随时间增长(每个约 300 B——仅观感问题)。
结论
核心机制在 Linux 上端到端验证通过:冲突在任何写入前失败关闭、本机过期锁安全回收、干净退出释放所有权、/clear///branch 生命周期正确交接租约、kill→回收→追加全程 parent 链保持线性——恰好覆盖 #7164 描述的损坏类型。希望发现 1(静默卡死,即本 PR 自己的旗舰场景)在本 PR 或紧随的 follow-up 中修复,发现 2 也应处理以免测试写入贡献者 home 目录。基于实测行为,对整体设计无异议。
🤖 Verified locally with Claude Code (model: claude-fable-5)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
@wenshao Thanks for the thorough Linux verification and real-process E2E report.
Verification on the pushed commit: |
…— soften the Request changes it rides (QwenLM#7191) * fix(review): an unverified Critical must not become a public blocker -- soften the Request changes it rides The module's header has stated the principle since the false "leaks tokens" Critical: an unverified finding must not become a public blocker. The mechanics only enforced it on the Approve row -- a verify gap capped an Approve to Comment -- while the Request-changes row stood on "a confirmed Critical earned that", with nothing checking CONFIRMED. A real bot review shipped through the gap: a CHANGES_REQUESTED posted onto an external contributor's PR (QwenLM#7166) over one Critical that the review's own body disclosed as unverified, from a run whose verifier was built and never launched. verificationGaps now returns the structured bit (unverifiedFindings) instead of leaving the verdict to parse gap prose, and composeReview softens a Request changes to Comment when the non-deterministic Criticals it would post cannot be shown verified -- verifier missing, transcripts unreadable, or no plan to check against; fail closed at every exit, because "could not show" and "was not" read the same to the person the blocker is posted at. Deterministic [build]/[test] findings stay pre-confirmed and keep their Request changes. The findings still post either way -- the body copy of an unanchorable blocker survives the softening exactly as it survives the presubmit carve-out -- and the verdict line says why a Comment sits over visible Critical comments. Manipulation check, both directions: a run that wants an Approve gains nothing (the same gap already caps Approve), and a run that wants to block without verifying now cannot. * fix(review): keep presubmit reasons beside the unverified cap, sync the Step 7 restatement Review follow-through: the Step 7 skill text still said a cap never softens a REQUEST_CHANGES -- the exact one-restatement-behind drift the module header warns about; it now carries the coverage qualifier and the one exception. When the unverified cap and presubmit downgradeRequestChanges both held, the softening ran first and the presubmit reasons silently vanished from the body; the downgrade arm now also fires on a softened Request-changes base, so the body's downgrade clause carries the reasons while the verdict line keeps the unverified sentence. Plus a pin that verify on record with the reverse audit absent still blocks -- the softening gates on verify alone. * fix(review): a deterministic Critical keeps its Request changes beside an unverified sibling Review catch: the unverified-blockers softening dropped the whole REQUEST_CHANGES even when the review also carried a pre-confirmed [build]/[test] body Critical -- un-blocking a confirmed build failure because its sibling lacked a verifier. The softening now requires zero deterministic body Criticals; the unverified ones stay disclosed either way, and the mixed case is pinned.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: coverage — agents ran substantively but prompts were wrapped rather than passed verbatim from the CLI-built roster.
Not reviewed: chunk 4 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 14 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 26 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 30 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 33 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 35 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 27 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 12 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 43 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 8 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 29 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 16 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 38 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 3 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 18 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 41 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 7 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 23 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 15 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 22 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 34 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 42 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 39 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 9 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 25 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 5 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 24 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 31 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 19 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 2 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 37 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 6 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 21 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 17 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 13 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 40 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 36 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 1 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 11 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 32 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 28 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 20 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 10 — launched with a prompt that is not the one the CLI built.
Not reviewed: chunk 1 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 2 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 3 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 4 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 5 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 6 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 7 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 8 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 9 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 10 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 11 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 12 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 13 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 14 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 15 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 16 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 17 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 18 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 19 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 20 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 21 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 22 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 23 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 24 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 25 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 26 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 27 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 28 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 29 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 30 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 31 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 32 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 33 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 34 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 35 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 36 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 37 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 38 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 39 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 40 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 41 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 42 — its prompt was built, but no agent on record was launched with it.
Not reviewed: chunk 43 — its prompt was built, but no agent on record was launched with it.
Not reviewed: Test coverage matrix (whole-diff) — its prompt was built, but no agent on record was launched with it.
Not reviewed: Agent 1b: Removed-behavior audit — its prompt was built, but no agent on record was launched with it.
Not reviewed: Agent 1c: Cross-file tracer — its prompt was built, but no agent on record was launched with it.
Not reviewed: Agent 7: Build & test verification — its prompt was built, but no agent on record was launched with it.
Not reviewed: Invariant agent A: state, timers, collections — packages/cli/src/acp-integration/acpAgent.ts — its prompt was built, but no agent on record was launched with it.
Not reviewed: Invariant agent B: counters, return values, error taxonomies — packages/cli/src/acp-integration/acpAgent.ts — its prompt was built, but no agent on record was launched with it.
Not reviewed: Invariant agent C: config fields, early returns — packages/cli/src/acp-integration/acpAgent.ts — its prompt was built, but no agent on record was launched with it.
Not reviewed: Invariant agent A: state, timers, collections — packages/cli/src/ui/hooks/useBranchCommand.ts — its prompt was built, but no agent on record was launched with it.
Not reviewed: Invariant agent B: counters, return values, error taxonomies — packages/cli/src/ui/hooks/useBranchCommand.ts — its prompt was built, but no agent on record was launched with it.
Not reviewed: Invariant agent C: config fields, early returns — packages/cli/src/ui/hooks/useBranchCommand.ts — its prompt was built, but no agent on record was launched with it.
Not reviewed: reverse audit — no auditor was launched with a prompt this skill builds — the pass that hunts what the rest of the review missed ran, if at all, without the method its brief carries.
Not reviewed: verification — a verifier ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and the posted findings cannot be counted as verified against it.
— qwen3.7-max via Qwen Code /review
| return new SessionWriterUnavailableError(); | ||
| } | ||
| return error; |
There was a problem hiding this comment.
[Suggestion] normalizeMaintenanceError discards the original error when wrapping filesystem errors into SessionWriterUnavailableError, despite the constructor accepting ErrorOptions with cause. — Concrete cost: A maintenance operation (archive, remove, finalize) encounters EACCES, EIO, or ENOSPC. The original errno, path, and message are replaced by the generic "Session write ownership could not be verified." A maintainer debugging the failure has no way to determine the root cause from the thrown error.
| return new SessionWriterUnavailableError(); | |
| } | |
| return error; | |
| return new SessionWriterUnavailableError({ cause: error }); | |
| } | |
| return error; |
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
[codex] Thanks — deferring this non-critical observability improvement because the PR is past the five-round review limit; only Critical fixes are being taken now.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Review round 10 summary for commit 39d1837
Validation: 968 targeted tests passed; targeted ESLint passed; npm run build and npm run typecheck passed. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. 3 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here. Not reviewed: chunk 2, chunk 3, chunk 4, chunk 17, chunk 19, chunk 21, chunk 22, chunk 23, chunk 25, chunk 27, chunk 28, chunk 32, chunk 35, chunk 36, chunk 37, chunk 39, chunk 41 — no agent reported covering these; nobody read them. Not reviewed: chunk 26, chunk 33, chunk 38, chunk 11, chunk 40, chunk 29, chunk 34, chunk 6, chunk 24, chunk 20, chunk 42, chunk 13, chunk 18, chunk 5, chunk 10, chunk 43, chunk 1 — launched with a prompt that is not the one the CLI built. Not reviewed: Agent 0: Issue fidelity & root-cause ownership, chunk 2, chunk 3, chunk 4, chunk 7, chunk 8, chunk 9, chunk 12, chunk 14, chunk 15, chunk 16, chunk 17, chunk 19, chunk 21, chunk 22, chunk 23, chunk 25, chunk 27, chunk 28, chunk 30, chunk 31, chunk 32, chunk 35, chunk 36, chunk 37, chunk 39, chunk 41, Test coverage matrix (whole-diff), Agent 1b: Removed-behavior audit, Agent 1c: Cross-file tracer, Agent 7: Build & test verification, Invariant agent A: state, timers, collections — packages/cli/src/acp-integration/acpAgent.ts, Invariant agent B: counters, return values, error taxonomies — packages/cli/src/acp-integration/acpAgent.ts, Invariant agent C: config fields, early returns — packages/cli/src/acp-integration/acpAgent.ts, Invariant agent A: state, timers, collections — packages/cli/src/ui/hooks/useBranchCommand.ts, Invariant agent B: counters, return values, error taxonomies — packages/cli/src/ui/hooks/useBranchCommand.ts, Invariant agent C: config fields, early returns — packages/cli/src/ui/hooks/useBranchCommand.ts — its prompt was built, but no agent on record was launched with it. Not reviewed: reverse audit — no auditor was launched with a prompt this skill builds — the pass that hunts what the rest of the review missed ran, if at all, without the method its brief carries. Not reviewed: verification — a verifier ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and the posted findings cannot be counted as verified against it.
— qwen3.7-max via Qwen Code /review
| handle = await fs.open(temporaryPath, 'wx', 0o600); | ||
| if (bytes.byteLength > 0) await handle.writeFile(bytes); | ||
| await handle.sync(); | ||
| await handle.close(); | ||
| handle = undefined; | ||
| await this.assertOwnedAndUnchanged(); | ||
| await fs.link(temporaryPath, this.transcriptPathValue); | ||
| this.expectedByteLengthValue = bytes.byteLength; | ||
| this.expectedTranscriptExistsValue = true; | ||
| } catch (error) { | ||
| failure = | ||
| (error as NodeJS.ErrnoException).code === 'EEXIST' | ||
| ? new SessionTranscriptChangedError() | ||
| : error; |
There was a problem hiding this comment.
[Suggestion] The catch block converts ALL EEXIST errors to SessionTranscriptChangedError, but EEXIST can come from two distinct calls: the temp file creation (fs.open(temporaryPath, 'wx', 0o600)) and the hard-link (fs.link). If a previous writeNewTranscript call failed and the await fs.unlink(temporaryPath) in cleanup also threw (no .catch(() => {}) unlike every other unlink in this file), the temp file is orphaned. A subsequent call hits EEXIST on temp file creation, misclassified as "transcript changed" when the transcript was never written. — Concrete cost: a retry after a transient filesystem error produces a misleading SessionTranscriptChangedError instead of surfacing the real cause.
| handle = await fs.open(temporaryPath, 'wx', 0o600); | |
| if (bytes.byteLength > 0) await handle.writeFile(bytes); | |
| await handle.sync(); | |
| await handle.close(); | |
| handle = undefined; | |
| await this.assertOwnedAndUnchanged(); | |
| await fs.link(temporaryPath, this.transcriptPathValue); | |
| this.expectedByteLengthValue = bytes.byteLength; | |
| this.expectedTranscriptExistsValue = true; | |
| } catch (error) { | |
| failure = | |
| (error as NodeJS.ErrnoException).code === 'EEXIST' | |
| ? new SessionTranscriptChangedError() | |
| : error; | |
| try { | |
| await fs.unlink(temporaryPath).catch(() => {}); | |
| handle = await fs.open(temporaryPath, 'wx', 0o600); |
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
[codex] Thanks — deferring. Cleanup already suppresses unlink failures and the temporary filename is scoped by the lease owner; retry classification after a transient same-owner unlink failure is not Critical enough to expand review round 11.
wenshao
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. Not reviewed: chunk 1, chunk 2, chunk 3, chunk 4, chunk 5, chunk 6, chunk 7, chunk 8, chunk 9, chunk 10, chunk 11, chunk 12, chunk 13, chunk 14, chunk 15, chunk 16, chunk 17, chunk 18, chunk 19, chunk 20, chunk 21, chunk 22, chunk 23, chunk 24, chunk 25, chunk 26, chunk 27, chunk 28, chunk 29, chunk 30, chunk 31, chunk 32, chunk 33, chunk 34, chunk 35, chunk 36, chunk 37, chunk 38, chunk 39, chunk 40, chunk 41, chunk 42, chunk 43 — no agent reported covering these; nobody read them. Not reviewed: coverage — agents ran substantively but the harness did not record them opening the diff file by path. Not reviewed: chunk 14, This PR introduces a process-level single-writer lease fo..., chunk 20, chunk 40, chunk 18, chunk 34, chunk 25, chunk 22, chunk 26, chunk 27, chunk 39, chunk 32, chunk 2, chunk 16, chunk 15, chunk 3, chunk 7, chunk 12, chunk 28, chunk 17, chunk 38, chunk 9, chunk 33, chunk 19, chunk 31, chunk 10, chunk 4, chunk 23, chunk 43, chunk 11, chunk 42, chunk 5, chunk 24, chunk 35, chunk 37, chunk 36, chunk 13, chunk 21, chunk 41, chunk 8, chunk 30, chunk 29, chunk 1, chunk 6 — pointed at diff lines it never opened: it made tool calls, but none of them read the diff. Not reviewed: reverse audit — no auditor was launched with a prompt this skill builds — the pass that hunts what the rest of the review missed ran, if at all, without the method its brief carries.
— qwen3.8-max-preview via Qwen Code /review
| // `session_closed` is terminal. Close the bus before ACP cancel so any | ||
| // late cancellation frames from the agent are intentionally dropped. | ||
| entry.events.close(); |
There was a problem hiding this comment.
[Suggestion] This comment now misdescribes the teardown ordering — the ACP cancel was moved to the top of closeSessionImpl (line 4294), so it runs before entry.events.close(), not after it. — Concrete cost: a maintainer relying on this comment would believe the bus is closed before cancel and that late cancel frames are dropped, when in fact the cancel now fires ~70 lines earlier with the bus open — an incorrect mental model of exactly the teardown-ordering invariant this PR is built around.
| // `session_closed` is terminal. Close the bus before ACP cancel so any | |
| // late cancellation frames from the agent are intentionally dropped. | |
| entry.events.close(); | |
| // `session_closed` is terminal. The active-prompt cancel already ran at | |
| // the top of closeSession; this closes the bus, dropping any later frames. | |
| entry.events.close(); |
— qwen3.8-max-preview via Qwen Code /review
There was a problem hiding this comment.
Thanks — deferring this documentation-only correction because the PR is past the five-round review limit; only Critical fixes are being taken now.
| } catch (error) { | ||
| await this.discardStoredSessionIfCurrent(sessionId, session); | ||
| throw error; |
There was a problem hiding this comment.
[Suggestion] If discardStoredSessionIfCurrent throws (e.g., SessionWriterUnavailableError from the writer lease release), the original setup error is silently replaced by the cleanup error. — Failure scenario: replayHistory throws (corrupt transcript), then closeStoredSession inside discardStoredSessionIfCurrent throws because the writer lease cannot be released. The caller receives the cleanup error with no trace of the original replay failure. The same file's shutdownConfigAfterFailure (line 509) already demonstrates the pattern of attaching the original error as cause.
| } catch (error) { | |
| await this.discardStoredSessionIfCurrent(sessionId, session); | |
| throw error; | |
| } catch (error) { | |
| try { | |
| await this.discardStoredSessionIfCurrent(sessionId, session); | |
| } catch (cleanupError) { | |
| if (cleanupError instanceof Error && cleanupError.cause === undefined) { | |
| Reflect.defineProperty(cleanupError, 'cause', { | |
| value: error, | |
| configurable: true, | |
| }); | |
| } | |
| throw cleanupError; | |
| } | |
| throw error; |
— qwen3.8-max-preview via Qwen Code /review
There was a problem hiding this comment.
Thanks — deferring. Preserving the setup failure as the cleanup error's cause would improve diagnostics, but it does not change the fail-closed cleanup behavior and is non-critical after the review-round limit.
| typeof error === 'object' && | ||
| typeof (error as NodeJS.ErrnoException).code === 'string' | ||
| ) { | ||
| return new SessionWriterUnavailableError(); |
There was a problem hiding this comment.
[Suggestion] normalizeSessionWriterStorageError discards the original filesystem error instead of preserving it as cause, even though SessionWriterUnavailableError accepts ErrorOptions and other call sites in this same diff pass { cause: error }. — Failure scenario: a session activation fails because the lease directory has EACCES (or ENOSPC, ENOENT). The function wraps it in SessionWriterUnavailableError with no cause. A developer investigating a user's "session writer unavailable" report cannot distinguish a permission error from a missing directory from a full disk without reproducing the issue.
| return new SessionWriterUnavailableError(); | |
| return new SessionWriterUnavailableError({ cause: error }); |
— qwen3.8-max-preview via Qwen Code /review
There was a problem hiding this comment.
Thanks — deferring this non-critical observability improvement because the PR is past the five-round review limit; only Critical fixes are being taken now.
| it('allows retry after a synchronous conversation-file failure', async () => { | ||
| const service = new ChatRecordingService(mockConfig); | ||
| vi.mocked(fs.writeFileSync).mockImplementationOnce(() => { | ||
| throw Object.assign(new Error('permission denied'), { code: 'EACCES' }); | ||
| const service = createRecordingService(mockConfig); | ||
| vi.mocked(randomUUID).mockImplementationOnce(() => { |
There was a problem hiding this comment.
[Suggestion] Test name says "synchronous conversation-file failure" but the mock now triggers a UUID-generation failure (randomUUID throws), not a file-write failure. — Concrete cost: a developer investigating conversation-file I/O failures would find this test and incorrectly conclude that path is covered, or a developer modifying the randomUUID call site would not realize this test's name promises something different from what it exercises.
| it('allows retry after a synchronous conversation-file failure', async () => { | |
| const service = new ChatRecordingService(mockConfig); | |
| vi.mocked(fs.writeFileSync).mockImplementationOnce(() => { | |
| throw Object.assign(new Error('permission denied'), { code: 'EACCES' }); | |
| const service = createRecordingService(mockConfig); | |
| vi.mocked(randomUUID).mockImplementationOnce(() => { | |
| it('allows retry after a synchronous record-creation failure', async () => { | |
| const service = createRecordingService(mockConfig); | |
| vi.mocked(randomUUID).mockImplementationOnce(() => { |
— qwen3.8-max-preview via Qwen Code /review
There was a problem hiding this comment.
Thanks — deferring this non-critical test-name cleanup because the PR is past the five-round review limit; only Critical fixes are being taken now.
| await clearRuntimeStatus( | ||
| this.storage.getRuntimeStatusPath(sessionId), | ||
| ); |
There was a problem hiding this comment.
[Suggestion] removeSessionFiles now calls clearRuntimeStatus to clean up the runtime sidecar, but no test in the removeSession/removeSessions describe blocks asserts this happens. The archive and unarchive tests explicitly assert clearRuntimeStatus is NOT called, but the remove path has no positive assertion. — Failure scenario: if a future refactor accidentally drops the clearRuntimeStatus call, a removed session's runtime sidecar persists indefinitely. No test catches this regression.
Suggested fix: add expect(clearRuntimeStatus).toHaveBeenCalled() to the existing 'should remove session file' test.
— qwen3.8-max-preview via Qwen Code /review
There was a problem hiding this comment.
Thanks — deferring this non-critical test-coverage addition because the PR is past the five-round review limit; only Critical fixes are being taken now.
The first post-grouping body on QwenLM#7166 carried seventeen chunks twice: once in "nobody read them" and once in the not-launched roster sentence -- the consequence restated beside its cause. A chunk whose disclosure entry already says WHY it went unread (launch never happened, or happened on a rewritten prompt) now leaves the nobody-read sentence; the cap and the remediation keep the full list, and a chunk with no disclosed cause -- the 3A shape, where chunks are not roster requirements -- keeps the sentence, because it is the only place the author learns those lines went unread.
|
已将当前事故所需的最小 P0a 独立拆到 #7237,便于先评审、合入和发布;以下分析已脱敏,不包含 session ID、用户输入、内部路径、模型内容或日志原文。 事故时序可以归纳为:
#7237 的 P0a 只覆盖本次 daemon/ACP 事故链路:按 这版不新增通用 IPC/fork 协议,也不试图一次覆盖所有场景。Interactive/headless writer、完全事务化的 session switching、branch/fork/rewind 与 archive/delete maintenance、initializing-entry/load-close coalescing,以及已分支 transcript 的修复仍保留在 P0b/#7166 范围。逻辑 发布和回滚必须先 drain 旧 daemon/ACP 进程;新旧二进制混跑不安全,因为旧 writer 不识别 lease。若 runtime 文件系统不支持原子同目录 hard link,P0a 会以 |
✅ Maintainer verification — single-writer session persistence works as designedI built this PR from source on Linux and verified it two ways: (1) a multi-process harness that drives the actual shipped compiled lease code across real OS processes, and (2) a live end-to-end run of two real
Environment
1) Changed-file unit tests — all green on Linux2) Compiled-lease harness — 8/8 invariants across real OS processesSpawns real child This directly validates invariants #1–#3 and #5 from 3) Live CLI E2E — two real
|
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
Closing this broad P0 proposal in favor of the smaller, independently landable P0a in #7237. PR #7237 carries the incident-critical ACP/daemon single-writer fencing and authoritative recovery path. The remaining interactive/headless transition and session-maintenance work from this PR will be handled separately as follow-up scope. Please continue review and validation on #7237. |





What this PR does
This PR introduces a process-level single-writer lease for every
(runtimeBase, sessionId), reloads the transcript authoritatively only after ownership is acquired, and fences every JSONL append with both the lease owner token and the expected UTF-8 byte length. Losing ownership or observing an unexpected transcript change becomes a permanent integrity failure for that recorder and blocks new model turns instead of allowing another history branch.It makes resume, clear, branch, working-directory relocation, shutdown, and offline session-management operations ownership-safe. Session transitions keep the old recorder and lease available until the new Core and UI state commit; forks consume one stable snapshot; maintenance operations use the same lease namespace; ACP runtime paths are pinned per workspace; daemon, TUI, desktop, and VS Code lifecycle paths close recorders consistently; and structured ACP/HTTP errors plus telemetry expose conflicts without leaking local process details.
Why it's needed
Two Qwen Code processes could restore the same session from different in-memory tail nodes and append valid-looking records to the same JSONL file. Both processes could return successful answers, but the resulting parent chains diverged. Restarting only reconstructed the branch ending at the physical file tail, so content already shown to the user, including tool results or Wiki answers, could disappear from the restored conversation.
Restarting cannot repair that corruption because it only chooses one existing tail. Enforcing one writer, reloading under the lease, and fencing every append prevents new divergence and fails closed whenever safe ownership cannot be proven.
Reviewer Test Plan
How to verify
session_writer_conflictbefore it can start a prompt or model turn, while process A continues normally./resume,/clear,/branch, and working-directory relocation across success, target-conflict, initialization-failure, and rollback paths. The old session should remain usable until commit, and a failed transition should not repoint delayed callbacks or runtime sidecars.Evidence (Before & After)
Before: a second process could append from a stale in-memory tail, both answers appeared successful, and a later restart silently restored only the physical-tail branch, hiding content that the user had already seen.
After: the second writer is rejected before a model turn with a structured conflict; after the original owner exits, a replacement reloads the authoritative transcript under the lease and continues the same linear history.
Tested on
Environment (optional)
macOS source checkout rebased onto
1cacbb1bcc; Node.jsv22.22.3; npm10.9.8; local filesystem;npm run build, bundle through the install prepare step,npm run typecheck,npm run lint, and 3,209 changed-file tests passed.Risk & Scope
OPENAI_API_KEY/authentication timeouts, with one unrelated MCP-count assertion.Linked Issues
Closes #7164
中文说明
本 PR 做了什么
本 PR 为每个
(runtimeBase, sessionId)引入进程级单写者租约,只在取得写入所有权后权威重载会话记录,并在每次追加 JSONL 时同时校验租约 owner token 与预期 UTF-8 字节长度。一旦失去所有权或检测到会话文件发生非预期变化,当前 recorder 会永久进入完整性失败状态并阻止新的模型轮次,不再允许继续形成另一条历史分支。同时,本 PR 使 resume、clear、branch、工作目录迁移、关闭流程和离线会话管理操作都遵循一致的所有权规则。会话切换在新 Core 与 UI 状态提交前保留旧 recorder 和旧租约;fork 只消费一次稳定快照;维护操作使用同一租约命名空间;ACP 按工作区固定 runtime 路径;daemon、TUI、desktop 和 VS Code 生命周期路径统一关闭 recorder;结构化 ACP/HTTP 错误与遥测能够暴露冲突,同时不泄漏本地进程信息。
为什么需要
此前两个 Qwen Code 进程可以从不同的内存尾节点恢复同一个会话,并向同一个 JSONL 文件追加看似有效的记录。两个进程都可能成功返回答案,但生成的 parent 链会发生分叉。重启时只能重建物理文件尾部所在的分支,因此用户已经看到的内容(包括工具返回或 Wiki 答案)可能从恢复后的会话中消失。
重启无法修复这种损坏,因为它只能选择已经存在的一条尾分支。强制单写者、在租约内重新加载并对每次追加做 fencing,可以阻止新的分叉;任何无法证明安全所有权的状态都会失败关闭。
Reviewer 测试计划
如何验证
session_writer_conflict,进程 A 则应继续正常工作。/resume、/clear、/branch和工作目录迁移的成功、目标冲突、初始化失败及回滚路径。旧会话在提交前应保持可用,失败的切换不得重新指向延迟回调或 runtime sidecar。证据(Before & After)
Before:第二个进程可以从过期的内存尾节点追加;两个答案都表现为成功,但之后重启只恢复物理尾分支,静默隐藏用户已经看到的内容。
After:第二个 writer 会在模型轮次之前收到结构化冲突;原 owner 退出后,新进程会在租约内权威加载 transcript,并沿同一条线性历史继续运行。
测试平台
环境(可选)
macOS 源码工作区,已 rebase 到
1cacbb1bcc;Node.jsv22.22.3;npm10.9.8;本地文件系统;npm run build、安装 prepare 阶段的 bundle、npm run typecheck、npm run lint以及 3,209 项变更文件测试均已通过。风险与范围
OPENAI_API_KEY/认证超时限制,另有一项与本改动无关的 MCP 数量断言失败。关联 Issue
Closes #7164