fix(core): Preserve ownership during session cleanup - #10300
Conversation
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
E2E and verification reportBaseline verification on the exact current Post-fix real-filesystem verification confirmed that all five scenarios converge when the original writer lease remains owned. Missing, malformed, non-regular, symlinked, replaced, and byte-identical atomically replaced writer locks all fail closed; the byte-identical replacement case preserves the new generation's worktree, PR, prompt-ledger, file-history, and organization state. Local automated verification:
Deliberately deferred because they are not required for this ownership fix: scheduled-task updates, attachment cleanup, final route-response semantics, mixed-case lookup, transcript/list merging, parent-child cascading deletion, and the existing standalone exact-retry result bucket behavior. None of these deferrals broadens or weakens the writer-lease ownership boundary implemented here. |
|
Re-run after the head moved Template looks good ✓ Problem: observed, not theoretical. @wenshao's round-2 report reproduces the permanent auxiliary residue on Direction: aligned. A correctness fix for the daemon session-lifecycle machinery merged in #10179 (supersedes #10286); the writer lease — which the daemon still holds across a generation closure — is a strictly more precise post-commit ownership signal than the generation counter. No external CHANGELOG signal applies (qwen-serve is internal); the area is squarely in scope. Size: core paths touched ( Approach: the scope still feels right — one ownership primitive composed into the existing fences, fail-closed on every ambiguous lock state. The delta since the last gate pass is R9-1 and R11-1 plus their pinning tests: regressions found in this PR's own review, so related work rather than scope creep. Risk: no elevated risk signals — none of the changed files match the repo's revert-correlated high-risk paths. Gate passes. Moving on to code review. 🔍 中文说明因 head 从 模板完整 ✓ 问题:已观测到,并非理论性问题。@wenshao 的第二轮报告在 方向:对齐。这是对 #10179 合入的 daemon session 生命周期机制的正确性修复(替代 #10286);daemon 在 generation 关闭后仍持有的 writer lease 是比 generation 计数更精确的提交后所有权信号。外部 CHANGELOG 无参考信号(qwen-serve 为内部机制),但该领域完全在范围内。 规模:触及核心路径( 方案:范围依然合理——一个所有权原语组合进现有 fence,对所有模糊锁状态保持 fail-closed。上次准入门之后的增量是 R9-1 与 R11-1 及其钉扎测试:均为本 PR 自身评审发现的回归,属于相关工作而非范围蔓延。 风险:无升级风险信号——变更文件均未命中与 revert 相关的高风险路径。 准入门通过。进入代码审查。🔍 — Qwen Code · qwen3.8-max Reviewed at |
|
Re-run after the head moved Code review — no blockersThe implementation is stricter than the minimal fix the problem statement calls for, in the right direction:
Known open items — none blocking, all ranked as follow-ups in @wenshao's round-2 verdict: R10-1 (conflict repair removing a copy with no ownership evidence) is byte-for-byte identical on Files changed (11)
Test evidence — this PR's own CI at
|
| Check | Conclusion |
|---|---|
| Qwen Code CI / Test (ubuntu-latest, Node 22.x) | ❌ cancelled — killed in the unit-suite step after a ~7 h self-hosted-runner queue; all 34 lint/format gates in the job passed first |
| Qwen Code CI / Test (macos-latest / windows-latest) | skipped |
| Qwen Code CI / Integration Tests (CLI, No Sandbox) | skipped |
| Qwen Code CI / Integration Tests (no-AK, No Sandbox) | ✅ success |
| Qwen Code CI / web-shell E2E Smoke | ❌ cancelled — killed during dependency install |
| Qwen Code CI / Desktop Shell (ubuntu-22.04 / windows-2022) | ✅ success |
| Serve A/B (ubuntu-latest, Node 22.x) | ✅ success |
| SDK Java (ubuntu 11/17/21, windows 21, macOS 21) | ✅ success (red on an earlier attempt of this head with HTTP-timeout failures in DaemonSessionClientTest — no Java in this PR) |
| Qwen Live Host CI | ✅ success |
| Security Checks (TruffleHog, dependency CVE audit) | ✅ success |
Reading the cancellation: the ubuntu unit job started 7 h 19 m after the run was created (self-hosted queue), passed every lint/format/schema gate, and was killed in "Run tests and generate reports" at ~60 min — the same lost-run pattern documented on the previous head, not a red caused by this change. The trust boundary applies in reverse too: that classification comes from the diff (this PR touches nothing the cancelled steps exercise differently) and the job timeline, not from anything in the log body.
The behavioural claim at this head does not rest on the author's word: it rests on @wenshao's round-2 three-arm A/B on Linux (real daemon, real filesystem, mutation testing that kills the R9-1 variant, plus a full local gate on the merged tree). What never completed is the repo's own unit matrix on this head — the cancelled ubuntu job and the skipped macos/windows legs. A manual re-run of the cancelled Qwen Code CI workflow closes that gap (flaky patrol auto-reruns only failure/timed_out, not cancelled). If you also want a second, bot-run proof of the fence semantics rather than one maintainer report, a sponsored @qwen-code /verify re-proves the A/B against the base build — the author lacks write access, so it runs as a sponsored run; read its report with the same skepticism as fork CI, since the code under verification is adversarial input.
Not verified: Windows and Linux unit legs at this head (matrix never ran here; the author's table marks both
中文说明
因 head 移动到 ff01d1e0 重跑。已完整重读该 head 的生产代码 diff,结论为重新推导,而非沿用旧结论。
代码审查——无阻塞项。 实现比问题陈述所需的最小修复更严格,且方向正确:所有权 fence 在获取租约时以 TOCTOU 抗竞态方式(O_NOFOLLOW 打开、fstat、读前后两次"路径↔描述符"见证)捕获锁文件的 dev/ino 身份,提交后由 assertCleanupOwned() 重新证明;缺失、被替换、损坏、非常规文件或符号链接的锁全部 fail-closed。R9-1 修复已在 diff 中核实:moveLedgerSidecar 方向感知——archive 把较新的 active 半边追加到较旧的 archived 半边之后,unarchive 以原子写把较旧半边置于较新之前,均由新测试钉住(即变异测试中能杀死方向无关变体的 M1)。重试路径已修复:alreadyArchived/alreadyActive 分支现在会在组合 fence 下清理滞留 sidecar,而不再提前返回——这正是 PR 描述里的"永久残留"。standalone 路径将运行时校验与所有权校验组合使用,所有权丢失后跳过附件与暂存目录清理;租约释放的簿记竞态也已修复。复用而非新造机制:unarchive 方向复用现有 atomicWriteFileSync;config.test.ts 的 8 行是租约改为按数字 fd 读锁所必需的 mock 修复,不是顺手改动。
已知未决项——均不阻塞,@wenshao 第二轮结论已按后续项排序:R10-1 经第二轮 A/B 证实与 main 逐字节一致(非本 PR 引入或扩大,作者的语义决定已在案);archive 方向仍是原地 append(仅重写方向崩溃安全,部分写入重试会重复记录但不丢失);M2/M3 锁替换测试仍缺(fence 已经 fs 探针证明会触发,只是没有测试钉住)。
测试证据(见上方英文表格):本 head 的 Qwen Code CI 单测作业在自托管队列排队约 7 小时后、通过全部 34 个 lint/格式门禁、于"运行测试"步骤约 60 分钟时被杀——与上一 head 记录的"损失的运行"模式一致,不是本改动造成的红灯。行为主张不依赖作者自述:依据是 @wenshao 在 Linux 上的第二轮三臂 A/B(真实 daemon、真实文件系统、杀死 R9-1 变异的变异测试,以及合并树上的完整本地门禁)。未落地的是本 head 的仓库单测矩阵——被取消的 ubuntu 作业与跳过的 macos/windows 侧。请手动重跑被取消的 Qwen Code CI 工作流关闭该缺口(flaky patrol 只自动重跑 failure/timed_out,不含 cancelled);如还需机器人独立复证 fence 语义,可由维护者发起赞助式 @qwen-code /verify(作者无写权限;报告请保持与 fork CI 同等的怀疑——被验证代码是对抗性输入)。
未验证:本 head 的 Windows/Linux 单测侧(矩阵未运行;作者表格两处均标
— Qwen Code · qwen3.8-max
Reviewed at ff01d1e075d02652380807f0a0e22cdb6c7011ee · re-run with @qwen-code /triage
|
Confidence: 4/5 — clean review at this head with the behavioural claims independently proven; what remains is documented follow-ups and a CI unit matrix that needs a manual re-run. Stepping back. My independent proposal for this bug was a byte-content lock re-check after the transcript move commits; the PR does that and adds inode-identity tracking with a TOCTOU-resistant verification — strictly stronger, and justified by the lease-takeover threat model. The delta since the last gate pass (R9-1 direction-aware ledger merge, R11-1 archive commit fence, and their tests) I re-read rather than trusting the fix summaries: the merge is order-correct in both directions and the retry path now reconciles stranded sidecars instead of answering The one honest caveat: this head's Verdict: approve. Approving pinned to the reviewed commit below — this is the bot's own vote alongside @wenshao's standing approval, which branch protection counts separately. It does not waive the CI re-run above. 中文说明置信度:4/5 —— 本 head 审查干净,行为主张已被独立证明;剩下的只有已记录在案的后续项,以及需要手动重跑的 CI 单测矩阵。 回顾。我对这个 bug 的独立方案是转录移动提交后对锁内容做字节级复核;本 PR 做到了这一点,并额外加入了 TOCTOU 抗竞态验证的 inode 身份追踪——更严格,且由"租约被接管"的威胁模型所正当化。上次准入门之后的增量(R9-1 方向感知 ledger 合并、R11-1 archive 提交 fence 及其测试)我重新读过而非轻信修复说明:合并在两个方向上的顺序都正确,重试路径现在会清理滞留 sidecar,而不是返回 唯一如实的保留:本 head 的 结论:批准。 以下方被审查提交为锚批准——这是机器人与 @wenshao 已存在的批准并列的一票,分支保护分别计数。这不豁免上述 CI 重跑。 — Qwen Code · qwen3.8-max Reviewed at |
🩺 serve daemon A/BBuilt the PR base vs this PR head ✅ No response changes against the PR base across 12 scenario(s). — Qwen Code · serve A/B |
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
CI failure diagnosed and fixed in The failed Ubuntu unit job was branch-related: the new lock-identity verification correctly reads the owned lock through a numeric file descriptor, while the existing Config test globally mocked every synchronous file read to return The fix is test-only. The real-filesystem test now delegates numeric-descriptor reads to the actual Verification on the pushed commit:
No review thread required a reply or resolution (resolved 0/0). The bot's two observations remain non-blocking and were not expanded into this CI-only fix; positive/status comments were ignored. No decision is needed from maintainers at this point; CI is re-running on the new head. |
chiga0
left a comment
There was a problem hiding this comment.
Deep-tier review of the writer-lease ownership fence. No blocking findings.
Core mechanism — readVerifiedLockIdentity(): Opens the lock file with O_RDONLY | O_NOFOLLOW | O_NONBLOCK, calls fstatSync on the file descriptor, then lstatSync on the path and compares {dev, ino} pairs. This is the standard TOCTOU mitigation: the fd-open prevents symlink following at open time; the subsequent lstat verifies the path still resolves to the same inode after open. Atomic replacement is caught because rename changes the inode under the path. The initial identity is captured once at acquisition time and stored in lockFileIdentity.
assertCleanupOwned() vs assertOwnedAndUnchanged(): Correctly separates two invariants. After the primary transcript move/unlink, the snapshot has changed by design so assertOwnedAndUnchanged would always fail. Switching cleanup steps to assertCleanupOwned lets worktree, PR sidecar, prompt-ledger, file-history, and organization cleanup complete under the owner that began the transaction, fixing the stale-sidecar indefinite leak.
SessionWriterError propagation in catch blocks: Updated catch paths correctly re-throw instanceof SessionWriterError immediately (stops cleanup) while warning on other filesystem errors (cleanup continues). A writer-lease error should abort; a sidecar-specific error should not prevent the next sidecar from proceeding.
Backwards compat: assertCleanupOwned ?? assertCanMutate fallback ensures callers that don't pass the new option are unaffected.
Platform handling: O_NOFOLLOW ?? 0 gracefully degrades on Windows. Symlink and atomic-replacement tests correctly gated runIf(process.platform !== 'win32').
config.test.ts mock fix: The fd-based readFileSync delegation to actualFs is necessary because readVerifiedLockIdentity() calls nodeFs.readFileSync(descriptor, 'utf8') with a numeric fd, which would otherwise hit the blanket mock.
CI: Full green — unit tests, Desktop Shell (ubuntu+windows), web-shell E2E, Serve A/B, Real daemon E2E, and Live Host (macos) all pass.
Reviewed with AI assistance.
|
@qwen-code /triage |
|
Sandboxed verification: ❌ not passed — non-deterministic tests (flakiness gate) - 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: 105 passed · 0 failed · 105 total Flakiness gate: ❌ 2 of 6 changed test file(s) returned different results across identical re-runs (5 full round(s)) The deterministic flakiness gate re-ran the test files this PR changes and got different outcomes from identical runs (agent verdict: 中文 — 判定:❌ 不通过 · 测试结果不确定(抖动门)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:105 通过 · 0 失败 · 105 总计 抖动门:❌ 2 of 6 changed test file(s) returned different results across identical re-runs (5 full round(s)) 确定性抖动门将本 PR 改动的测试文件原样重跑了多轮,得到了不一致的结果(agent 判定: Verification reportPR #10300 Deep Verification —
|
| cell | scenario | base @ HEAD^1 |
head @ HEAD^2 |
|---|---|---|---|
| D-nominal | delete, generation open, lock intact | resolved; residue 0 | resolved; residue 0 (identical) |
| D-genclose | delete, generation closes after unlink | rejected runtime generation closed; 3 sidecars + file-history left |
resolved; residue 0 |
| D-lockswap | delete, lock replaced (new inode) after unlink | resolved; cleanup proceeded under foreign lock | rejected SessionWriterLostError; residue preserved |
| D-lockcontent | delete, lock rewritten in place (foreign owner bytes) | resolved; cleanup proceeded under foreign lock | rejected SessionWriterLostError; residue preserved |
| A-nominal | archive, generation open, lock intact | archived; 3 sidecars moved | identical |
| A-genclose | archive, generation closes after rename | rejected; transcript archived, 3 active sidecars never moved | resolved; 3 sidecars moved to archived |
| A-lockswap | archive, lock replaced after rename | resolved; sidecars moved under foreign lock | rejected SessionWriterLostError; sidecars untouched |
Result: 105/105 scripted assertions pass (driver-ab.mjs, expectation table per arm/cell), identical across two runs. Witness: evidence/01-ab-matrix-base-vs-head.png. All five non-nominal cells flip between arms: two (D/A-genclose) from "residue forever" to "cleanup completes" (the fix), three (D-lockswap, D-lockcontent, A-lockswap) from "mutates sidecars with no ownership proof" to "fail closed" (the safety fence). The two nominal cells are byte-equivalent across arms (no regression). Base-cell failures are the expected control outcomes and count as passing assertions.
Reviewer Test Plan, per step: step 1 (generation closes after primary mutation → everything completes) — measured for delete and archive on head (D/A-genclose); unarchive and the two conflict-repair paths share the identical assertCleanupOwned call sites and were verified by code walk, not separate cells. Step 2 (replace/invalidate lock → cleanup stops with per-session writer error) — measured (D-lockswap, D-lockcontent, A-lockswap: SessionWriterLostError, sidecars preserved). Step 3 (standalone through selected workspace runtime checks both runtime identity and lease) — code walk confirms the composition at all four call sites; end-to-end execution not done (see Not covered), and the mutation matrix shows the runtime half of that composition is unpinned (finding below). Step 4 (ordinary behavior unchanged) — D/A-nominal byte-equivalent outcomes on both arms plus the full changed-file gates below.
Findings
S1 (Suggestion, completeness): the runtime-identity half of the standalone composed cleanup check is asserted by no test
The standalone service composes assertCleanupOwned: () => { assertRuntimeCurrent(runtime); lease.assertCleanupOwned(); } at four sites. Mutation VM4b deleted the assertRuntimeCurrent line from one of them and the whole standalone suite stayed green (81/81), while the sibling mutation VM4a (delete the lease.assertCleanupOwned() line) killed exactly one test. So the lease half is pinned; the fail-closed routing half that the PR description's step 3 rests on is not. The runtime behavior itself is correct in code and assertRuntimeCurrent is pre-existing, tested machinery — this is a completeness gap, not a defect: the fixture that would pin it is one that calls the composed callback and asserts the runtime assertion fired. (Mutation-matrix convention: completeness reporting, not a merge condition.)
No higher-severity findings. Specifically checked and not found: callers of archiveSessions/unarchiveSessions/removeSessions that lost a fence (the archive/unarchive cleanup sites call options.assertCleanupOwned?.() without an assertCanMutate fallback — every production caller is routed through session-archive.ts / standalone-session-service.ts, which both pass assertCleanupOwned; the remove path keeps the ?? assertCanMutate fallback for plain callers, which is behavior-identical to base for them); divergent issue/accept ends (the fence is checked where the cleanup executes); lock-tamper shapes beyond swap/rewrite (symlink and byte-identical-rename shapes are pinned by the new lease tests, confirmed non-vacuous via VM1/VM5).
Mutation matrix (vacuity of the new tests)
Unmutated controls green first (core 385 passed | 2 skipped; cli 146 passed). Witness: evidence/02-mutation-matrix.png.
| mutation | suite | result | classification |
|---|---|---|---|
VM1: assertCleanupOwned() → no-op |
lease suite | killed 3 (expected function to throw an error, but it didn't) |
pinned |
VM2: all options.assertCleanupOwned → options.assertCanMutate |
sessionService + corruption suites | killed 14 (promise rejected "Error: generation changed" instead of resolving) |
pinned |
VM3 (control): delete the pre-existing pre-mutation assertCanMutate |
sessionService suite | killed 2 (expected "spy" to not be called at all, but actually been called 1 times) |
positive control — the existing guard test plus one whose fence-call sequence shifted; same file, proves the harness turns tests red |
| VM4a: composed standalone callback, drop lease half | standalone suite | killed 1 (expected "spy" to be called once, but got 0 times) |
pinned |
| VM4b: composed standalone callback, drop runtime half | standalone suite | survived (81/81) | coverage gap (S1 above) |
VM5: readOwnedLock minus the new dev/ino clause |
lease suite | killed 2 (promise resolved "SessionWriterLease{…}" instead of rejecting) |
pinned |
Layered-guard check: VM1 and VM5 each kill tests the other cannot (cleanup-phase assertion vs acquisition/release-phase identity clause) — the two identity checks defend different phases; neither is redundant, and no survivor was misread as vacuity of a guarded hunk. All quoted failures are expected-vs-actual behavioral assertions, not import/compile breakage.
Targeted gates
| gate | result |
|---|---|
vitest run core: sessionService.test.ts, sessionService.corruption.test.ts, session-writer-lease.test.ts |
385 passed | 2 skipped (387) — 3 files green |
vitest run cli: standalone-session-service.test.ts, session-archive.test.ts |
146 passed (146) — 2 files green, matching the PR body's CLI count |
Not covered
- Per-commit attribution: the checkout is depth 2; only the second commit (
429da3ca, "codex: fix CI failure on PR fix(core): Preserve ownership during session cleanup #10300") is reachable — the first (3171d115, the fix itself) sits behind the shallow boundary (git rev-list HEAD^1..HEAD^2returns 1 vs 2 in the metadata snapshot). The aggregateHEAD^1..HEADdiff is what was verified; which test adjustments belonged to which commit could not be split. - Trial merge: the workflow's merge commit already places the PR on the current base tip (
40c77fce, ahead of the metadatabaseRefOida3ec41a2), conflict-free — so the A/B verifies what actually lands. No separate re-merge was needed;main's delta between the two bases was not itself re-examined. - Standalone-session lifecycle was not executed end to end (step 3): verified by code walk + mutation characterization only.
- Unarchive-genclose and the two conflict-repair-genclose cells were not run as separate A/B cells; their cleanup paths call the same
assertCleanupOwnedsites measured for delete/archive. - Windows behavior:
readVerifiedLockIdentityrequires a verifiable inode (hasVerifiableInode= ino ≠ 0) for the lock file; this run proves the Linux path end to end. The PR marks Windows/Linux as locally untested (⚠️ ); on a filesystem returning inode 0 the lease now fails closed at acquisition — consistent with the PR's stated fail-closed intent, but not exercised here. - Repo-wide test/lint gates were not run (scope: changed files only).
- Replay calibration: N/A — not a workflow/CI PR.
Methodology
Environment: CI verify container (node:22-bookworm), working tree at refs/pull/10300/merge (depth 2), npm ci + npm run build pre-run at HEAD. Base control: git worktree at HEAD^1 with only packages/core rebuilt (scripts/build_package.js, root node_modules reused — PR changes no package.json/lockfile, so the control is dependency-clean; core's nested node_modules symlinked since it is dependency-identical). Harnesses import each arm's compiled dist/ by absolute file URL, so no workspace symlink can cross-contaminate arms; a symbol control confirmed the base dist contains zero assertCleanupOwned/lockFileIdentity occurrences while head contains them. One pre-existing oddity: worktree builds report a single TS7016 (@lydell/node-pty declaration resolution, shellExecutionService.ts — untouched by this PR) yet emit complete JS; it reproduces identically when building base sources, so it is an artifact of building a worktree against the root install, not a PR effect (A/A-equivalent observation). Mutation runs used a scratch worktree at HEAD, rebuilding the mutated package before each run so both relative and package-entry imports saw the mutant; every mutant was restored afterward (tree verified clean). Raw per-cell logs (logs/base-*.log, logs/head-*.log), the driver run (logs/driver-ab-run.txt), mutation logs (logs/vm*.log), build logs, and both harness scripts live in this artifact directory alongside evidence/01-ab-matrix-base-vs-head.png and evidence/02-mutation-matrix.png.
Flakiness gate log
rounds=5 files=6 skipped=0
file packages/cli/src/serve/conversations/standalone-session-service.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/conversations/standalone-session-service.test.ts
file packages/cli/src/serve/server/session-archive.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/server/session-archive.test.ts
file packages/core/src/config/config.test.ts: (cd packages/core) npx --no-install vitest run ./src/config/config.test.ts
file packages/core/src/services/session-writer-lease.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/session-writer-lease.test.ts
file packages/core/src/services/sessionService.corruption.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/sessionService.corruption.test.ts
file packages/core/src/services/sessionService.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/sessionService.test.ts
per-file results (P=pass F=fail I=infra-exit, one letter per run):
packages/cli/src/serve/conversations/standalone-session-service.test.ts: PPPPP
packages/cli/src/serve/server/session-archive.test.ts: FPFFF
packages/core/src/config/config.test.ts: PPPPP
packages/core/src/services/session-writer-lease.test.ts: FFFFF
packages/core/src/services/sessionService.corruption.test.ts: FPFFF
packages/core/src/services/sessionService.test.ts: PPPPP
verdict: flaky
summary: 2 of 6 changed test file(s) returned different results across identical re-runs (5 full round(s))
--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/serve/conversations/standalone-session-service.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/server/session-archive.test.ts: F (exit 1)
--- output tail · round 1 · packages/cli/src/serve/server/session-archive.test.ts ---
n one batch to a single archive�[32m 8�[2mms�[22m�[39m
�[32m✓�[39m archiveDaemonSessions�[2m > �[22mdisables a scheduled task bound to the archived session�[32m 16�[2mms�[22m�[39m
�[32m✓�[39m archiveDaemonSessions�[2m > �[22mreports task maintenance failure after archiving the transcript�[32m 15�[2mms�[22m�[39m
�[32m✓�[39m archiveDaemonSessions�[2m > �[22mdoes not acquire writer leases for ids already archived or missing�[32m 3�[2mms�[22m�[39m
�[32m✓�[39m archiveDaemonSessions�[2m > �[22mdoes not archive while another writer holds the lease�[32m 27�[2mms�[22m�[39m
�[32m✓�[39m archiveDaemonSessions�[2m > �[22mtakes over a sealed empty transcript before maintenance�[32m 30�[2mms�[22m�[39m
�[32m✓�[39m archiveDaemonSessions�[2m > �[22mrejects a transcript FIFO without waiting for a writer�[32m 7�[2mms�[22m�[39m
�[32m✓�[39m archiveDaemonSessions�[2m > �[22mkeeps independent batch sessions moving when one writer conflicts�[32m 18�[2mms�[22m�[39m
�[32m✓�[39m archiveDaemonSessions�[2m > �[22mreports a gate race per session after another batch item was archived�[32m 9�[2mms�[22m�[39m
�[32m✓�[39m archiveDaemonSessions�[2m > �[22mkeeps independent batch sessions moving when one classification fails�[32m 14�[2mms�[22m�[39m
�[32m✓�[39m archiveDaemonSessions�[2m > �[22mdoes not acquire a lease or mutate when closing the owner fails�[32m 2�[2mms�[22m�[39m
�[32m✓�[39m archiveDaemonSessions�[2m > �[22muses the classification made after acquiring the lease�[32m 12�[2mms�[22m�[39m
�[32m✓�[39m archiveDaemonSessions�[2m > �[22mdoes not lock an active/archive conflict�[32m 3�[2mms�[22m�[39m
�[32m✓�[39m archiveDaemonSessions�[2m > �[22mrepairs an active/archive conflict by keeping the archived copy�[32m 17�[2mms�[22m�[39m
�[32m✓�[39m archiveDaemonSessions�[2m > �[22mdoes not report success after release fails but reconciles the task to the applied archive�[32m 8�[2mms�[22m�[39m
�[32m✓�[39m archiveDaemonSessions�[2m > �[22mreleases the lease when scheduled-task reconciliation fails�[32m 19�[2mms�[22m�[39m
�[32m✓�[39m archiveDaemonSessions�[2m > �[22mchecks only the selected runtime root for transcripts and locks�[32m 21�[2mms�[22m�[39m
�[32m✓�[39m archiveDaemonSessions�[2m > �[22mrejects with DaemonDrainingError after the coordinator is sealed�[32m 2�[2mms�[22m�[39m
�[32m✓�[39m archiveDaemonSessions�[2m > �[22mrecovers an enabled task whose session is already archived�[32m 8�[2mms�[22m�[39m
�[32m✓�[39m unarchiveDaemonSessions�[2m > �[22mdeduplicates ids and does not lock already active or missing ids�[32m 17�[2mms�[22m�[39m
�[32m✓�[39m unarchiveDaemonSessions�[2m > �[22mcollapses case-variant spellings in one batch to a single unarchive�[32m 11�[2mms�[22m�[39m
�[31m �[31m�[31m unarchiveDaemonSessions�[2m > �[22mrepairs an active/archive conflict by keeping the active copy�[39m�[32m 19�[2mms�[22m�[39m
�[31m → expected { unarchived: [], …(4) } to match object { …(3) }
(2 matching properties omitted from actual)�[39m
�[32m✓�[39m unarchiveDaemonSessions�[2m > �[22mdoes not unarchive while another writer holds the lease�[32m 14�[2mms�[22m�[39m
�[32m✓�[39m unarchiveDaemonSessions�[2m > �[22mreports a single error per archived id when unarchive batch fails�[32m 12�[2mms�[22m�[39m
�[32m✓�[39m unarchiveDaemonSessions�[2m > �[22mkeeps independent unarchive sessions moving when one classification fails�[32m 11�[2mms�[22m�[39m
�[32m✓�[39m unarchiveDaemonSessions�[2m > �[22mreports a gate race per session after another batch item was unarchived�[32m 13�[2mms�[22m�[39m
�[32m✓�[39m unarchiveDaemonSessions�[2m > �[22mre-enables an archive-disabled task bound to the unarchived session�[32m 187�[2mms�[22m�[39m
�[32m✓�[39m unarchiveDaemonSessions�[2m > �[22mrecovers a stranded task on an ALREADY-active session�[32m 15�[2mms�[22m�[39m
�[32m✓�[39m unarchiveDaemonSessions�[2m > �[22mrejects with DaemonDrainingError after the coordinator is sealed�[32m 1�[2mms�[22m�[39m
�[32m✓�[39m deleteDaemonSessions�[2m > �[22mdeletes both copies of an exact active/archive conflict�[32m 22�[2mms�[22m�[39m
�[32m✓�[39m deleteDaemonSessions�[2m > �[22mremoves a scheduled task bound to the deleted session�[32m 18�[2mms�[22m�[39m
�[32m✓�[39m deleteDaemonSessions�[2m > �[22mrepairs task maintenance on retry after deleting the transcript�[32m 17�[2mms�[22m�[39m
�[32m✓�[39m deleteDaemonSessions�[2m > �[22mcollapses case-variant spellings in one batch to a single delete�[32m 12�[2mms�[22m�[39m
�[32m✓�[39m deleteDaemonSessions�[2m > �[22mdoes not delete while another writer holds the lease�[32m 8�[2mms�[22m�[39m
�[32m✓�[39m deleteDaemonSessions�[2m > �[22mreports attachment cleanup failures and allows an idempotent retry�[32m 9�[2mms�[22m�[39m
�[32m✓�[39m deleteDaemonSessions�[2m > �[22mreports a gate race per session after another batch item was deleted�[32m 8�[2mms�[22m�[39m
�[32m✓�[39m deleteDaemonSessions�[2m > �[22mskips orphan deletion when a new owner attached�[32m 2�[2mms�[22m�[39m
�[32m✓�[39m deleteDaemonSessions�[2m > �[22mrejects with DaemonDrainingError after the coordinator is sealed�[32m 1�[2mms�[22m�[39m
�[32m✓�[39m deleteDaemonSessions�[2m > �[22mdeletes the transcript when killSession resolves true�[32m 8�[2mms�[22m�[39m
�[32m✓�[39m deleteDaemonSessions�[2m > �[22mreturns true when task maintenance fails after orphan deletion�[32m 15�[2mms�[22m�[39m
�[32m✓�[39m deleteDaemonSessions�[2m > �[22mdeletes the transcript when killSession throws SessionNotFoundError�[32m 14�[2mms�[22m�[39m
�[32m✓�[39m deleteDaemonSessions�[2m > �[22mthrows when the lease is held by another writer�[32m 9�[2mms�[22m�[39m
�[31m⎯⎯⎯⎯⎯⎯⎯�[39m�[1m�[41m Failed Tests 1 �[49m�[22m�[31m⎯⎯⎯⎯⎯⎯⎯�[39m
�[41m�[1m FAIL �[22m�[49m src/serve/server/session-archive.test.ts�[2m > �[22munarchiveDaemonSessions�[2m > �[22mrepairs an active/archive conflict by keeping the active copy
�[31m�[1mAssertionError�[22m: expected { unarchived: [], …(4) } to match object { …(3) }
(2 matching properties omitted from actual)�[39m
�[32m- Expected�[39m
�[31m+ Received�[39m
�[2m {�[22m
�[32m- "errors": [],�[39m
�[32m- "resolvedConflicts": [�[39m
�[32m- "550e8400-e29b-41d4-a716-446655440117",�[39m
�[32m- ],�[39m
�[32m- "unarchived": [�[39m
�[32m- "550e8400-e29b-41d4-a716-446655440117",�[39m
�[31m+ "errors": [�[39m
�[31m+ {�[39m
�[31m+ "error": SessionTranscriptChangedError {�[39m
�[31m+ "message": "The session transcript changed outside its active writer.",�[39m
�[31m+ "name": "SessionTranscriptChangedError",�[39m
�[31m+ "rpcCode": -32022,�[39m
�[31m+ "errorKind": "session_transcript_changed",�[39m
�[31m+ "httpStatus": 409,�[39m
�[31m+ },�[39m
�[31m+ "sessionId": "550e8400-e29b-41d4-a716-446655440117",�[39m
�[31m+ },�[39m
�[2m ],�[22m
�[31m+ "resolvedConflicts": [],�[39m
�[31m+ "unarchived": [],�[39m
�[2m }�[22m
�[36m �[2m❯�[22m src/serve/server/session-archive.test.ts:�[2m1150:20�[22m�[39m
�[90m1148| �[39m })�[33m;�[39m
�[90m1149| �[39m
�[90m1150| �[39m �[34mexpect�[39m(result)�[33m.�[39m�[34mtoMatchObject�[39m({
�[90m | �[39m �[31m^�[39m
�[90m1151| �[39m unarchived�[33m:�[39m [sessionId]�[33m,�[39m
�[90m1152| �[39m resolvedConflicts�[33m:�[39m [sessionId]�[33m,�[39m
�[31m�[2m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯�[22m�[39m
�[2m Test Files �[22m �[1m�[31m1 failed�[39m�[22m�[90m (1)�[39m
�[2m Tests �[22m �[1m�[31m1 failed�[39m�[22m�[2m | �[22m�[1m�[32m64 passed�[39m�[22m�[90m (65)�[39m
�[2m Start at �[22m 16:37:29
�[2m Duration �[22m 19.38s�[2m (transform 5.58s, setup 141ms, collect 8.43s, tests 767ms, environment 417ms, prepare 138ms)�[22m
JUNIT report written to /__w/qwen-code/qwen-code/packages/cli/junit.xml
round 1 · packages/core/src/conf
...truncated -- full content in the run artifacts.
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
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.
Reviewed — no blockers. Suggestions are inline.
中文说明
已审查——无阻断问题。 建议见行内评论。
— qwen3.8-max via Qwen Code /review (v0.22.2)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
e773f6c
Review follow-up for
|
| Item | Decision | Action |
|---|---|---|
| R1-8 terminal lost lease remains pending forever | Agree — correctness regression | Fixed. SessionWriterLostError now evicts only the matching pending lease and still returns failure for the current cleanup; the next recovery performs a fresh authoritative acquisition. |
| R1-1 extract ownership assertion closure | Defer | Non-blocking maintainability extraction; the explicit scoped call sites are correct and an abstraction would widen this critical-fix round. |
| R1-5 add daemon forwarding tests | Defer | Test hardening without a demonstrated exact-head defect; current forwarding is verified and this round is limited to the reproduced regression. |
| R1-6 add ownership-loss diagnostics | Defer | Useful observability hardening, but not a correctness fix; it also expands identity logging beyond this round. |
| R1-3 unify partial-options fallback policy | Defer | No production caller uses the partial-options shape; changing the policy is a separate API decision. |
| R1-4 rename the sidecar callback parameter | Defer | Non-behavioral naming cleanup. |
| R1-2 centralize catch guards | Defer | Maintainability refactor; the explicit guards are currently correct. |
Verification: npm run build, npm run typecheck, npm run lint, changed-file Prettier check, and standalone-session-service.test.ts (82/82) all pass. An independent reproduction also confirmed the stale pending entry is gone while retryable durability-pending release behavior remains unchanged.
Resolved review threads: 7/7 after the individual replies. Decisions needed: none.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R1-1 four-way duplicated assertCleanupOwned closure in standalone-session-service.ts — already reported in round 1 (inline comment 3874017847), author explicitly deferred; re-derived this round on unchanged code and not re-posted
Not explored to full depth (tool budget reached): "agent 1a": none — but note I could not run npm run typecheck /lint on the changed files (install's prepare fails only on a missing husky binary; I built core and ran …; "agent 6b": executing packages/cli/src/serve/conversations/standalone-session-service.test.ts to confirm the new tests pass (worktree has no node_modules or built core d….
中文说明
已审查。 建议见行内评论。
本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未探索到全部深度(达到工具调用预算):"agent 1a":none — but note I could not run npm run typecheck /lint on the changed files (install's prepare fails only on a missing husky binary; I built core and ran …;"agent 6b":executing packages/cli/src/serve/conversations/standalone-session-service.test.ts to confirm the new tests pass (worktree has no node_modules or built core d…。
— qwen3.8-max via Qwen Code /review (v0.22.2)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Addressed the current review feedback in commit 79f5c29:
Verification: focused CLI tests 85/85, focused core tests 97 passed with 3 platform skips, independent focused verification 8/8, repository build, typecheck, lint, scoped Prettier check, and clean diff audits. Resolved 4/4 handled review threads. |
chiga0
left a comment
There was a problem hiding this comment.
Tier: Deep(并发安全 + 文件身份校验)— 增量 429da3ca → 79f5c29f
已核验内容
R2-1(TOCTOU:path identity check 与 content read 之间的替换窗口)
readVerifiedLockIdentity 现在对 assertPathMatchesDescriptor() 做了两次调用:
- 在
readFileSync(descriptor, 'utf8')之前:确认 path 指向已打开的 fd(inode 匹配) - 在 content 验证(
parseLockRecord+owner_id/lockRecordRaw校验)之后:再次确认 path 仍指向同一 inode
第二次检查关闭了这个窗口:攻击者在 fstat+内容读取之间原子替换 lockPath 后,重新 lstat 会发现 inode 不匹配并抛 SessionWriterLostError。配套测试通过 descriptorReadHook.afterRead 在 readFileSync 返回后同步执行 renameSync 来重现此时序,并验证 assertCleanupOwned() 会抛出。✓
R2-2(清理失败后 catalog/cache 未更新)
archive 和 unarchive 的错误路径现在都调用 reconcileCatalogAfterLifecycleError(即 markSessionCatalogChanged + invalidateSessionListCache)。两个新测试分别验证:move 完成但 cleanup 失败时,catalog 和 cache 均被 invalidate。✓
CI
1 success,1 in_progress(review-pr),28 skipped。核心测试无失败。
无阻塞项。
|
Review follow-up summary for 4fa1b30:
Validation: the focused session archive test passed (68/68), and repository build, typecheck, lint, targeted formatting, CI retries were not attempted because the watcher did not return |
yiliang114
left a comment
There was a problem hiding this comment.
Re-approving on head 4fa1b30: the new commit is a bounded, correct improvement — moveLedgerSidecar now keeps write order directionally (unarchive prepends the older half before the newer instead of appending), with new tests (+44 in session-archive.test.ts). All threads remain resolved; the only red check is the known Java 11 session-detach 'Connection reset by peer' flake (pre-existing on main, unrelated to this change).
yiliang114
left a comment
There was a problem hiding this comment.
Re-approving at head 4fa1b30. The new commit is a bounded correctness fix with tests: moveLedgerSidecar now preserves write order in both directions (archive appends the older half after the newer, unarchive prepends it before) plus coverage in session-archive.test.ts; the fence/ownership behavior verified in the previous round is unchanged. All 22 threads resolved. Only red check is the proven SDK Java 11 DaemonSessionClientTest networking flake (recurs on main, unrelated to session cleanup).
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- archive/unarchive assertCanMutate fallback asymmetry (agent 3c candidate) — already reported as R1-3 (comment 3874017866), author explicitly deferred; maintainer @yiliang114 accepted the author's rebuttal that all production callers pass bo…
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Deferred under the convergence posture (round 10, not a blocker) — recorded, not requested in this round:
packages/cli/src/serve/server/session-archive.test.ts:1966 — [review] D10-1 test re-derives the prompt-ledger path layout four times instead of an accessor over getPromptLedgerPathForStatepackages/core/src/services/sessionService.ts:1621 — [review] D10-2 prepend/append invariant (records are only ever appended to the active-state ledger) not stated in moveLedgerSidecar's doc commentdocs/design/2026-08-19-prompt-terminal-ledger-design.md:106 — [review] D10-3 design doc still describes the ledger merge as append-only; the round-10 unarchive merge prepends via a rewrite
Convergence: round 10 posted 2 inline comment(s), 2 of them reported for the first time; the previous round posted 2 (2 new). Findings keep coming back to the same files: packages/core/src/services/sessionService.ts (findings in round 9; 2 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push keeps the loop from re-deriving the same set; this PR's reviews already resolve to a critical posting floor. (Observation only — nothing was withheld from this review because of this observation.)
Residual risk: this loop is persistently critical — Criticals stood in the previous round's work-list and stand again this round (2 Critical(s)), the rate of first-time findings is not falling (this round 2, previous 2), and the standing Critical backlog is not shrinking. The severity floor will not converge it. Recommendation: land-with-residual-risk — the exit is a maintainer risk-acceptance decision (merge, carrying the residual risk), not another review round. Residual-risk inventory for that decision (maintainer to complete):
| standing Critical | attack surface | attacker-dependency | blast radius |
|---|---|---|---|
| (each standing Critical) | … | … | … |
Advisory only — it does not block this review.
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
收敛姿态下延后(第 10 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 10 轮发布了 2 条行内评论,其中 2 条是首次提出;上一轮发布了 2 条(其中 2 条首次提出)。发现反复回到同一批文件:packages/core/src/services/sessionService.ts(第 9 轮已出过发现,本轮又有 2 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,可以避免循环反复推导同一组发现;本 PR 的评审已解析为 critical 发布下限。(仅为观察——本轮评审未因此扣留任何内容。)
残余风险:本循环处于 persistently-critical 形态——上一轮工作清单中的 Critical 本轮依然存在(本轮 2 条 Critical),首次发现的速率没有下降(本轮 2,上一轮 2),且未决 Critical 积压没有减少。severity floor 无法使其收敛。建议:land-with-residual-risk——出口是 maintainer 的风险接受决定(合入并承担残余风险),而非再开一轮评审。供该决定使用的残余风险清单(maintainer 填写):按每条未决 Critical 列出「攻击面 · 攻击者依赖性 · 影响范围」三栏。仅为建议——不阻断本次评审。
— qwen3.8-max via Qwen Code /review (v0.22.3)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Round 10 follow-up is pushed in
Verification on the pushed commit:
The previous Java 11 networking/capabilities failure and Serve A/B timeout were unrelated to this change. Their old-SHA retry is superseded by this review-fix push; the new SHA starts a fresh CI run. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R11-3 five hand-derived prompt-ledger paths in session-archive.test.ts — already recorded as D10-1 in the round-10 deferral list (review 5058776275); the round-11 ENOSPC test added the fifth site
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Deferred under the convergence posture (round 11, not a blocker) — recorded, not requested in this round:
packages/core/src/services/sessionService.ts:1636 — [probe] R11-2 new ledger-merge lease guards have no regression witness
Convergence: round 11 posted 2 inline comment(s), 1 of them reported for the first time; the previous round posted 2 (2 new). Findings keep coming back to the same files: packages/core/src/services/sessionService.ts (findings in rounds 9, 10; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
收敛姿态下延后(第 11 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 11 轮发布了 2 条行内评论,其中 1 条是首次提出;上一轮发布了 2 条(其中 2 条首次提出)。发现反复回到同一批文件:packages/core/src/services/sessionService.ts(第 9、10 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.3)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Post-fix update for ff01d1e:
Verification: npm run build; npm run typecheck; npm run lint; Prettier check; core sessionService tests 222/222; CLI session-archive tests 69/69; git diff --check; two clean self-audit passes. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
3 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- S12-1 unpinned SessionWriterError rethrow guards (removeSessionOrganization catch and both conflict pr-merge catches) — already recorded in the round-5 deferral list (review 5050581338)
- S12-2 archive/unarchive assertCanMutate fallback asymmetry — already reported as R1-3 (comment 3874017866), author explicitly deferred
- S12-3 movePrSidecar parameter still named assertCanMutate — already reported as R1-4 (comment 3874017874), author explicitly deferred
Not reviewed: build-and-test — Agent 7's test phase ended before any suite ran (600s budget consumed by install + 18 workspace builds); the round-1 reverse auditor then ran the two changed suites green at HEAD (sessionService.test.ts 222 passed, sessionService.corruption.test.ts 69 passed).
Not reviewed: build-and-test — test-efficacy probe harness inconclusive (the scoped runner tripped the repo's vitest globalSetup dist guard; harnessValidated null — neither validated nor refuted).
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not reviewed: build-and-test — Test (ubuntu-latest, Node 22.x) and web-shell E2E Smoke were cancelled in CI at the reviewed SHA (infrastructure); the two changed suites ran green locally at HEAD.
Not reviewed: issue-fidelity — closing-issue discovery failed (gh 2.45.0 is older than the 2.72.0 the closingIssuesReferences query requires); the closing-issue set is UNKNOWN, not empty; the incident replay over the PR's own narrative ran instead.
Deferred under the convergence posture (round 12, not a blocker) — recorded, not requested in this round:
packages/core/src/services/sessionService.ts:1628 — [probe] D12-1 unarchive-direction ledger merge branch has no testpackages/core/src/services/sessionService.ts:2884 — [probe] D12-2 heal-branch ownership-loss outcome bucket unpinned by tests
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 3 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:build-and-test — Agent 7's test phase ended before any suite ran (600s budget consumed by install + 18 workspace builds); the round-1 reverse auditor then ran the two changed suites green at HEAD (sessionService.test.ts 222 passed, sessionService.corruption.test.ts 69 passed)。
未审查:build-and-test — test-efficacy probe harness inconclusive (the scoped runner tripped the repo's vitest globalSetup dist guard; harnessValidated null — neither validated nor refuted)。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未审查:build-and-test — Test (ubuntu-latest, Node 22.x) and web-shell E2E Smoke were cancelled in CI at the reviewed SHA (infrastructure); the two changed suites ran green locally at HEAD。
未审查:issue-fidelity — closing-issue discovery failed (gh 2.45.0 is older than the 2.72.0 the closingIssuesReferences query requires); the closing-issue set is UNKNOWN, not empty; the incident replay over the PR's own narrative ran instead。
收敛姿态下延后(第 12 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.3)
|
@qwen-code /triage |
|
Sandboxed verification: The verification run did not complete, so the phases below may be partial or missing entirely. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Flakiness gate: 中文 — 判定:
|
Maintainer verification report — round 2, local real-environment A/BI re-built both trees locally and re-ran the verification at the current head, on Linux (which the PR table still marks Summary: R9-1 reproduces at
1. R9-1 — the blocking item reproduces, and the last four commits fix itOne harness file, byte-identical in all three arms, drives the real production entry points ( Scenario is exactly the one @yiliang114 described: an interrupted unarchive left the older ledger half in The third arm isolates the fix: it is the PR head with only the direction-aware merge reverted, so every other change in the PR is held constant. Two observables matter more than the raw record order:
The false 2. Same thing end-to-end through a real
|
| gate | result |
|---|---|
| PR's changed core tests (4 files) | 984 passed, 1 skipped, 1 failed |
| PR's changed CLI tests (2 files) | 158 passed |
packages/core/src/services sweep |
2682 passed, 1 skipped, 1 failed (the same one) |
packages/cli/src/serve sweep |
6639 passed, 56 skipped, 17 failed across 7 files |
tsc --noEmit on packages/core and packages/cli |
clean |
eslint --max-warnings 0 on the 10 changed .ts files |
clean |
prettier --check on all 11 changed files |
clean |
Both failure groups are environmental and I confirmed each against main in the same worktree layout:
- The one core failure is
classifies an unreadable owned lock as unavailable. Itchmod 0o000s the lock and expects the read to fail — which root ignores. It is a pre-existing test from fix(core): Fence concurrent ACP session writers #7237 and fails identically onmainhere. - All 17 CLI failures (
fast-path,capabilities-docs-contract,run-qwen-serve-live,workspace-agents,workspace-memory,workspace-file-system,conversation-runtime-ownership) reproduce identically onmain— 17 failed / 318 passed on the same seven files. None of them is a file this PR touches. - Repo-wide
npm run buildandnpm run typecheckboth fail only inpackages/web-shell, on a@tanstack/react-tablethat is missing from this box'snode_modules. 36 identical errors onmain.
On CI: the red SDK Java job is DaemonSessionClientTest HTTP-timeout failures. This PR touches no Java, and the same workflow failed on several unrelated branches in the same window (cx/9437-rewind-stable-identity, feat/8975-channel-state-restore, autofix/issue-10529). The two CANCELLED Test jobs were superseded by the next push.
8. Verdict
The correctness claim holds, the defect reproduces on main in every scenario the PR describes, and the blocking R9-1 regression is both fixed and pinned by a test that a mutation kills. From my side this is mergeable as it stands.
Everything left is a follow-up, in the order I'd rank it:
- Make the archive-direction ledger merge atomic, symmetric with the unarchive one (§6).
- Add the lock-swap test that closes M2/M3 (§4).
- R10-1 is pre-existing (§5) — worth its own issue against
mainif the semantics should change, but not a reason to hold this PR.
Harness files, raw observation JSON, the daemon E2E script and the mutation runner are in wenshao/qwen-code@assets-pr10300-r2 so every run above can be reproduced independently.
中文版本
维护者验证报告 —— 第二轮,本地真实环境 A/B
我在当前 head 上重新构建了两棵树并重跑了验证,运行平台是 Linux(PR 的测试平台表格里仍标记为
结论:R9-1 在 dc689848 上可复现,在 ff01d1e075 上已修复。原有的所有权 fence 结论在新 head 上依然成立。唯一仍未关闭的评审线程(R10-1)所描述的行为,在 main 上逐字节完全一致 —— 本 PR 既没有引入它,也没有扩大它。
| PR head | ff01d1e075 |
| 基线 | main @ 74e71c5945 |
| 实测树 | 2d98626a60(PR head 合入 74e71c5945,无冲突,tree 930435603b) |
| 第三个对照臂 | PR head 仅把 moveLedgerSidecar 的方向感知分支回退到 dc689848 时的写法 |
| 环境 | Debian 13(Linux 6.12.63, x86_64)、Node v22.22.2、npm 10.9.7、uid 0、关闭 sandbox |
1. R9-1 —— 阻塞项可复现,最后四个 commit 已修复
同一个 harness 文件在三个臂里逐字节相同,驱动真实生产入口(unarchiveDaemonSessions / archiveDaemonSessions),跑在真实临时工作区上,使用真实的 SessionService、真实的 SessionArchiveCoordinator 和真实的 acp-bridge ledger reader。没有 mock 任何产品模块。
场景正是 @yiliang114 描述的那个:一次被中断的 unarchive 把较早的 ledger 半边留在了 chats/archive/,随后 session 被复用,较新的 terminal 记录写进了 active 半边。
第三个臂用于隔离这次修复:它就是 PR head,只回退方向感知的合并逻辑,PR 的其他改动全部保持不变。
有两个观测点比原始记录顺序更重要:
recentPromptTerminalRecords按文件顺序截取最后 64 条。 当 70 条滞留的旧 terminal 被合并到 10 条新记录之前时,"新在前"的合并会把全部 10 条最新 terminal 挤出 load 响应。修复后 10 条全部保留。readRecentPromptTerminals只读最后 256 KiB。 一旦合并后的 ledger 超过这个窗口,"新在前"的合并会让新记录完全落在窗口之外 —— 可见数量从 3/3 变成 0/3。
danglingInFlightPromptIds 的误报确实存在,但被兜住了:reconcileDanglingPromptTerminals 会跳过任何在列表中已存在 terminal 记录的 promptId,所以我没能让它合成出错误的 terminal,归因守卫是有效的。上面 promptTerminals 的丢失才是没有第二道防线的那部分。
2. 通过真实 qwen serve daemon 的端到端验证
三个臂都返回 200,alreadyActive 信封完全相同,错误数为零。差异对客户端完全不可见,只体现在磁盘上 —— 这正是它需要用文件系统级 A/B 而不是响应断言来验证的原因。
3. 所有权 fence 在新 head 上依然成立
generation guard 的关闭由观察真实文件系统触发 —— 转录一旦离开原路径就关闭 —— 因此复现竞态不依赖调用计数。第 4 行就是 PR 描述里的 "indefinitely",已确认:在 main 上普通重试返回 alreadyArchived 且零错误,不会再回头处理任何 sidecar。
4. 对新代码的变异测试
M1 是关键:把 moveLedgerSidecar 回退成方向无关的 append 会被两个新增的 reconciliation 测试杀死。 说明 R9-1 不只是被修复,而是被测试真正钉住了。
三个存活的变异,都不是缺陷:
- M2 / M3 —— unarchive 合并内部的
assertCanCommit以及 source unlink 之前的那次校验没有任何测试覆盖。确定性的node:fs故障注入探针(第二张表,与 PR 自己的 ENOSPC 测试用同一个 seam)表明两处 fence 确实会触发:在任一时刻替换锁文件都会抛出SessionWriterLostError,并且 source 半边逐字节完好。补一个"在合并提交与 source unlink 之间替换锁"的测试就能同时关掉这两个缺口。 - M5 ——
readVerifiedLockIdentity忽略同 inode 原地重写锁记录的变异仍然存活,与上一轮相同。仍不是缺陷:readOwnedLock和 release 都会重新比较字节内容。
5. 唯一未关闭的评审线程(R10-1)是既有行为
四行结果在两个臂之间完全一致。记录可归属到其他 workspace 的副本依然会被 SessionStorageEntryError 拒绝;只有完全没有所有权证据的副本(空文件,或损坏到无法恢复)才会被删除,而且必须显式传入非默认的 resolveConflicts: true。这与 R6-5 是同一个设计问题,并不是本 PR 引入的回归 —— 因此我认为不应该以它继续阻塞本 PR。如果确实需要改变这个语义,应该作为针对 main 的独立改动。
6. 其余观察
ENOSPC 的不对称是本轮唯一新发现的问题,而且是 nice-to-have,不阻塞:最后几个 commit 重写的那个方向现在是崩溃安全的,另一个方向仍然是原地 append。两种情况都不会丢记录,但重试一次部分完成的 append 会产生重复记录。把 archive 分支也改成原子写就能让两个方向对称,代价只有几行。
上一轮我提出的文档缺口已经补上 —— POST /sessions/archive 现在说明了 alreadyArchived 路径同样需要 lease,与 unarchive 章节一致。
7. 门禁
全部在合并树上执行:
| 门禁 | 结果 |
|---|---|
| PR 修改的 core 测试(4 个文件) | 984 通过,1 跳过,1 失败 |
| PR 修改的 CLI 测试(2 个文件) | 158 通过 |
packages/core/src/services 全量 |
2682 通过,1 跳过,1 失败(同一个) |
packages/cli/src/serve 全量 |
6639 通过,56 跳过,17 失败,分布在 7 个文件 |
packages/core 与 packages/cli 的 tsc --noEmit |
干净 |
对 10 个变更 .ts 文件的 eslint --max-warnings 0 |
干净 |
对全部 11 个变更文件的 prettier --check |
干净 |
两组失败都是环境问题,我在相同的 worktree 布局下逐一对照了 main:
- core 的那一个失败是
classifies an unreadable owned lock as unavailable。它把锁文件chmod 0o000后期望读取失败 —— 而 root 会忽略权限位。这是 fix(core): Fence concurrent ACP session writers #7237 引入的既有测试,在这里对main同样失败。 - CLI 的 17 个失败(
fast-path、capabilities-docs-contract、run-qwen-serve-live、workspace-agents、workspace-memory、workspace-file-system、conversation-runtime-ownership)在main上完全一致地复现 —— 同样 7 个文件、17 失败 / 318 通过。其中没有任何一个是本 PR 修改过的文件。 - 仓库级
npm run build与npm run typecheck都只在packages/web-shell失败,原因是本机node_modules缺少@tanstack/react-table。在main上是同样的 36 个错误。
关于 CI:变红的 SDK Java job 是 DaemonSessionClientTest 的 HTTP 超时失败。本 PR 没有改动任何 Java 代码,而且同一个 workflow 在同一时间窗口内的多个无关分支上也失败(cx/9437-rewind-stable-identity、feat/8975-channel-state-restore、autofix/issue-10529)。两个 CANCELLED 的 Test job 是被后续 push 取代的。
8. 结论
正确性主张成立,PR 描述的每一个场景都能在 main 上复现缺陷,阻塞性的 R9-1 回归既已修复,也被一个能杀死变异的测试钉住。就我这边而言,当前状态可以合并。
剩下的都是后续项,按我的优先级排序:
- 把 archive 方向的 ledger 合并改成原子写,与 unarchive 方向对称(§6)。
- 补上关闭 M2/M3 的锁替换测试(§4)。
- R10-1 是既有行为(§5)—— 如果确实要改语义,值得针对
main单开一个 issue,但不应成为阻塞本 PR 的理由。
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
3 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- pre-unlink ledger-merge fence unpinned by tests (packages/core/src/services/sessionService.ts:1637) — already recorded as R11-2 in the round-11 deferral list (review 5059133122)
- unpinned SessionWriterError rethrow guards in removeSessionOrganization and the conflict-path twins (packages/core/src/services/sessionService.ts:1554) — already recorded as S12-1 in the round-12 body over the round-5 deferral list (review …
- unarchive-merge assertCanCommit pass-through unpinned by tests (packages/core/src/services/sessionService.ts:1630) — already recorded as R11-2 in the round-11 deferral list (review 5059133122)
Not reviewed: build-and-test — Agent 7's test phase ended before any suite ran (whole-call budget consumed by install + 18 workspace builds); verification agents ran the two changed core suites (291/291) and the CLI session-archive suite (69/69) green at HEAD.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not reviewed: build-and-test — test-efficacy probe harness inconclusive (the probe baseline ran in an unbuilt tree; harnessValidated null — neither validated nor refuted).
Residual risk: this loop is persistently critical — Criticals stood in the previous round's work-list and stand again this round (1 Critical(s)), the rate of first-time findings is not falling (this round 0, previous 0), and the standing Critical backlog is not shrinking. The severity floor will not converge it. Recommendation: land-with-residual-risk — the exit is a maintainer risk-acceptance decision (merge, carrying the residual risk), not another review round. Residual-risk inventory for that decision (maintainer to complete):
| standing Critical | attack surface | attacker-dependency | blast radius |
|---|---|---|---|
| (each standing Critical) | … | … | … |
Advisory only — it does not block this review.
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 3 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:build-and-test — Agent 7's test phase ended before any suite ran (whole-call budget consumed by install + 18 workspace builds); verification agents ran the two changed core suites (291/291) and the CLI session-archive suite (69/69) green at HEAD。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未审查:build-and-test — test-efficacy probe harness inconclusive (the probe baseline ran in an unbuilt tree; harnessValidated null — neither validated nor refuted)。
残余风险:本循环处于 persistently-critical 形态——上一轮工作清单中的 Critical 本轮依然存在(本轮 1 条 Critical),首次发现的速率没有下降(本轮 0,上一轮 0),且未决 Critical 积压没有减少。severity floor 无法使其收敛。建议:land-with-residual-risk——出口是 maintainer 的风险接受决定(合入并承担残余风险),而非再开一轮评审。供该决定使用的残余风险清单(maintainer 填写):按每条未决 Critical 列出「攻击面 · 攻击者依赖性 · 影响范围」三栏。仅为建议——不阻断本次评审。
— qwen3.8-max via Qwen Code /review (v0.22.3)
|
@qwen-code /triage |
|
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: 464 passed · 0 failed · 464 total Flakiness gate: 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:464 通过 · 0 失败 · 464 总计 抖动门: Verification reportSandboxed verification: ✅ passed — merge-ready (agent verdict, follow-up round at a NEW head) — central claim re-proven by live 3-arm A/B, the three delta commits isolated by an intermediate build, previous findings re-measured, flakiness gate re-run clean. Scripted assertions: 464 passed · 0 failed · 464 total Flakiness gate: ✅ 4 rounds × 6 changed files = 24/24 P, zero flakes, per-file passed-counts stable across rounds. 中文摘要
Verified head: Previous-finding status (follow-up round)Re-measured live at
Central claim and A/B proofCentral claim (PR body): after the primary transcript mutation commits in delete/archive/unarchive lifecycle operations, auxiliary cleanup (worktree / PR sidecars, prompt ledger, file history, organization) completes iff the daemon still owns the exact writer lease (same inode and byte-identical lock content, verified by Secondary claims: (2) a replaced/invalidated lock after the primary mutation stops auxiliary cleanup with a per-session writer error (fail closed); (3) standalone-session lifecycle composes the same fence with the selected-runtime check; (4) delta: the ledger merge itself is ownership-fenced and unarchive orders the older (archived) half before the newer (active) half, atomically. The harness ( Controls (all scripted):
Result, identical across three official runs: 116 harness assertions per run (base 38, inter 39, head 39; Delta attribution (what the 3-arm buy over a 2-arm): the inter arm carries the whole PR except the final three commits. Every fence-related cell already flips base→inter (lease fence and R-resume reconciliation predate the delta), while L-unarch-order flips only inter→head — base and inter both merge the older half after the newer one, which under the reader's latest-per-promptId semantics leaves Reviewer Test Plan, per step: step 1 (generation closes after primary mutation → everything completes) — measured for delete and archive (D/A-genclose); for unarchive, archive/unarchive conflict repair, and committed deletion it is pinned at this head by VM2's 17 kills, which include Mutation matrix (vacuity of the new tests, delta included)Un-mutated control green first (standalone suite 89/89,
Layered-guard note for VM10: the unarchive branch still has the VM1 count note (vs the previous round's 6): the previous round's broader no-op mutant also induced a lease-helper protocol timeout in Targeted gates (at head, 4 rounds, single writer)Flakiness gate: the six changed test files, run sequentially 4 rounds with a single writer, at head
24/24 slots P (P = exit 0 and zero failed tests), zero FindingsS1 (Suggestion, completeness, carried from previous rounds — stands, re-measured): the runtime-identity half of the standalone composed cleanup check is asserted by no testAt this head the composed callback assertCleanupOwned: () => {
this.options.assertRuntimeCurrent(runtime);
lease.assertCleanupOwned();
},appears at four sites in S2 (Suggestion, completeness, new this round): the unarchive ledger merge's atomic-write
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
…eate Resolves sessionService with QwenLM#10300 (cleanup ownership): main's moveArchiveSidecars helper and assertCleanupOwned naming are kept, and the PR sidecar leg inside it runs the locked moveSessionPrSidecar this branch introduced instead of the unlocked private movePrSidecar (which does not come back). The ownership tests written against the private method now assert the fence is handed to the locked move and that a SessionWriterLostError rejection from it is not downgraded to a warning; the split-pair merge cases stay pinned in session-pr-service.test.ts.













What this PR does
This PR preserves exact daemon writer-lease ownership across the post-transcript cleanup phase of delete, archive, unarchive, and conflict repair. The primary transcript mutation remains guarded by the existing transcript snapshot and runtime-generation checks; once that mutation commits, auxiliary worktree, PR, prompt-ledger, file-history, and organization cleanup may continue only while the same active writer lock is still present and byte-for-byte owned by the lease holder.
The same ownership fence is composed with the selected-runtime check for standalone-session archive, unarchive, deletion, and deletion-journal recovery, so workspace routing remains fail closed. Missing, replaced, malformed, non-regular, or symlinked locks stop auxiliary cleanup. The protocol documentation also clarifies that conflict repair is limited to regular workspace-maintainable transcript copies and never bypasses ownership checks.
This replaces #10286 with a version based on the lifecycle implementation merged in #10179.
Why it's needed
On current
main, a daemon runtime generation may close immediately after the transcript has already been unlinked, moved, or chosen as the losing conflict copy. The old generation fence then rejects auxiliary cleanup even though the daemon still owns the exact writer lease. Retrying the same lifecycle request sees the primary operation as already complete and does not revisit those sidecars, leaving stale worktree, PR, prompt-ledger, file-history, or organization state indefinitely.Using the writer lease as the post-commit ownership boundary lets the transaction finish under the owner that began it without allowing a stale or foreign runtime to mutate another owner's state.
Reviewer Test Plan
How to verify
Local verification passed 384 core assertions (3 skipped), 146 CLI assertions, the repository build, typecheck, lint, and exact changed-file formatting checks. An isolated real-filesystem baseline on current
mainreproduced permanent auxiliary residue in all five ordinary daemon lifecycle scenarios; the same matrix is covered by the post-fix verification.Evidence (Before & After)
N/A — non-UI lifecycle correctness change.
Tested on
Environment (optional)
macOS 26.4.1, Node.js 22.22.3, npm 10.9.8, sandbox disabled.
Risk & Scope
Linked Issues
Supersedes #10286. Builds on #10179.
中文说明
本 PR 做了什么
本 PR 在删除、归档、取消归档和冲突修复的转录主操作之后,继续保留精确的 daemon writer lease 所有权校验。转录主操作仍由已有的转录快照与 runtime generation 校验保护;一旦主操作提交,只有在同一个活跃 writer lock 仍存在且其原始内容与 lease 持有者完全一致时,才允许继续清理辅助 worktree、PR、prompt ledger、file history 和 organization 状态。
对于 standalone session 的归档、取消归档、删除和删除日志恢复,同一个所有权 fence 会与 selected runtime 校验组合,因此 workspace 路由仍然保持 fail closed。writer lock 缺失、被替换、格式损坏、不是普通文件或为符号链接时,辅助清理都会停止。协议文档同时明确:冲突修复仅适用于当前 workspace 可维护的普通转录文件,并且不会绕过所有权校验。
本 PR 基于 #10179 已合入的生命周期实现,替代 #10286。
为什么需要
当前
main上,daemon runtime generation 可能在转录已经被删除、移动或作为冲突中的失败副本移除后立即关闭。旧的 generation fence 随后会拒绝辅助清理,即使 daemon 仍然持有精确的 writer lease。重试相同生命周期请求时,主操作已被识别为完成,不会再次清理这些 sidecar,从而使陈旧的 worktree、PR、prompt ledger、file history 或 organization 状态永久残留。将 writer lease 作为提交后的所有权边界,可以让发起事务的所有者安全完成清理,同时不会允许陈旧或外部 runtime 修改其他所有者的状态。
Reviewer 测试计划
如何验证
本地验证已通过 384 个 core 断言(3 个跳过)、146 个 CLI 断言、仓库 build、typecheck、lint 以及精确变更文件的格式检查。在当前
main的隔离真实文件系统基线中,五个普通 daemon 生命周期场景都能稳定复现永久辅助残留;修复后验证覆盖同一矩阵。证据(变更前后)
N/A — 非 UI 的生命周期正确性变更。
测试平台
环境(可选)
macOS 26.4.1、Node.js 22.22.3、npm 10.9.8、禁用 sandbox。
风险与范围
关联 Issue
替代 #10286,基于 #10179。