Skip to content

fix(serve): Repair persisted session lifecycle - #9626

Merged
doudouOUC merged 18 commits into
QwenLM:mainfrom
doudouOUC:fix/session-storage-lifecycle-9488
Aug 25, 2026
Merged

fix(serve): Repair persisted session lifecycle#9626
doudouOUC merged 18 commits into
QwenLM:mainfrom
doudouOUC:fix/session-storage-lifecycle-9488

Conversation

@doudouOUC

@doudouOUC doudouOUC commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR lets delete, archive, and unarchive maintain an owned persisted transcript even when the file is empty, has a torn or malformed head, or is a legacy orphan whose parent session no longer exists. Lifecycle classification is based on exact regular files in the selected runtime instead of requiring the transcript to load as a conversation.

Archive and unarchive remain non-mutating on conflicts by default. Batch lifecycle routes return those conflicts as per-session errors in an HTTP 200 response; for workspace-qualified routes, this replaces the earlier HTTP 409 session_conflict envelope. Clients may opt into resolveConflicts: true: archive keeps the archived copy and removes the active copy, while unarchive keeps the active copy and removes the archived copy. Successful repairs are reported through the additive resolvedConflicts response field. Delete keeps its existing compatibility behavior and removes both copies of an active/archive conflict.

Workspace-qualified requests stay inside the selected trusted runtime. Ordinary workspace-less lifecycle requests remain primary-scoped; only an unambiguous internal Conversations owner may be selected outside the primary runtime. Ambiguous ownership and unavailable runtime generations fail closed. The option and response field are exposed consistently through REST, ACP, capability discovery, protocol documentation, and the TypeScript SDK.

The scope is intentionally narrow: this PR does not add general case-insensitive session lookup, list merging, transcript/export changes, organization changes, or new Live/task identity behavior.

Why it's needed

#9513 restored load compatibility for persisted sessions, but lifecycle operations could still report an existing empty, damaged, or orphaned transcript as not found because they depended on conversation loadability or provenance classification. Those files then had no safe delete/archive/unarchive path.

The new maintenance classifier separates physical storage ownership from conversation readability. It rejects foreign-workspace and non-regular entries, preserves writer-lease and runtime-generation fences, snapshots both active and archived paths, and verifies the same files immediately before unlink or rename. This gives owned damaged storage a safe management path without broadening normal routing or changing default conflict behavior.

Reviewer Test Plan

How to verify

  • Create empty, torn/damaged, and legacy-orphan regular transcripts. Confirm delete removes them, archive/unarchive move their original bytes, and both workspace-qualified internal requests and unambiguous internal owner-routed requests succeed.
  • Create active and archived copies for one ID. Confirm archive/unarchive return a per-session conflict and preserve both files by default; confirm archive repair keeps archived content, unarchive repair keeps active content, and delete still removes both copies.
  • Confirm a foreign-workspace transcript is reported as not found, while symlink, directory, and FIFO entries fail closed without following, mutating, or blocking on them.
  • Confirm ambiguous ordinary/internal ownership, a replaced runtime generation, and an occupied writer lease reject the operation without mutating storage.
  • Run the focused Core, daemon lifecycle, multi-workspace route, ACP transport, and TypeScript SDK tests. The post-rebase verification passed 228 Core tests, 58 daemon lifecycle tests, 12 multi-workspace lifecycle tests, 13 ACP lifecycle tests, and 348 SDK tests. The full repository build, workspace typecheck, pre-commit formatting/lint checks, and git diff --check also passed.

Evidence (Before & After)

Before: existing empty, damaged, and orphaned transcripts were reported as not found by lifecycle APIs and remained stranded on disk.

After: owned regular transcripts can be deleted, archived, or unarchived without being loadable; default conflict behavior is unchanged, and explicit keep-one repair is available when the capability is advertised.

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

macOS 26.4.1 arm64, Node.js v22.22.3, npm 10.9.8. Filesystem lifecycle behavior and fresh REST E2E fixtures were verified locally; the final commit was rebuilt and typechecked after rebasing onto the current main.

Risk & Scope

  • Main risk or tradeoff: These are destructive filesystem operations, so the main risk is acting on stale, foreign, or concurrently replaced storage. The implementation keeps the existing lifecycle coordinator and writer lock, adds authoritative active/archive file snapshots, and revalidates identity at the commit boundary.
  • Not validated / out of scope: Windows and Linux were not run locally. General mixed-case compatibility, session-list merging, transcript/export behavior, organization metadata, Live/task identity, transcript merging, and parent-to-child cascading deletion are intentionally out of scope.
  • Breaking changes / migration notes: Workspace-qualified archive/unarchive clients that previously handled conflicts through HTTP 409 session_conflict must now inspect the HTTP 200 batch errors array. The mutation behavior remains non-mutating by default. Ordinary workspace-less lifecycle requests remain primary-scoped, delete retains its delete-both behavior, and resolveConflicts, resolvedConflicts, and session_storage_conflict_repair are additive; clients should preflight the capability before using the option.

Linked Issues

Fixes #9488

Builds on #9513

中文说明

本 PR 做了什么

本 PR 让删除、归档和恢复操作可以维护属于当前 runtime 的持久化 transcript,即使文件为空、头部撕裂或损坏,或者是父会话已经不存在的历史孤儿会话。生命周期分类改为依据所选 runtime 中精确路径的普通文件,而不是要求 transcript 必须能够加载成完整会话。

归档和恢复遇到冲突时默认仍不修改文件。批量生命周期路由通过 HTTP 200 响应中的单会话错误返回这些冲突;对于 workspace-qualified 路由,这取代了原来的 HTTP 409 session_conflict envelope。客户端可以显式传入 resolveConflicts: true:归档保留 archived 副本并删除 active 副本;恢复保留 active 副本并删除 archived 副本。成功修复的 ID 通过增量字段 resolvedConflicts 返回。删除继续保留既有兼容行为,在 active/archive 冲突时删除两份副本。

workspace-qualified 请求始终限制在所选可信 runtime 内。普通 workspace-less 生命周期请求继续只作用于 primary;只有唯一且无歧义的 internal Conversations owner 才可以在 primary 之外被选择。所有权歧义和不可用的运行时代际都会 fail closed。该选项和响应字段在 REST、ACP、能力发现、协议文档和 TypeScript SDK 中保持一致。

本 PR 有意保持范围最小:不会增加通用的大小写不敏感会话查找、列表合并、transcript/export 改动、组织信息改动或新的 Live/任务身份行为。

为什么需要它

#9513 恢复了持久化会话的加载兼容性,但生命周期操作仍可能因为依赖会话可加载性或 provenance 分类,把实际存在的空、损坏或孤儿 transcript 报告为 not found,导致这些文件没有安全的删除、归档或恢复入口。

新的维护分类器把物理存储所有权与会话可读性分离。它拒绝外部工作区和非普通文件,保留 writer lease 与运行时代际 fence,对 active 和 archived 两个路径做快照,并在 unlink 或 rename 前立即验证仍是同一文件。这样可以安全管理属于当前 runtime 的损坏存储,同时不扩大普通路由范围,也不改变默认冲突行为。

Reviewer 测试计划

如何验证

  • 创建空、撕裂/损坏和历史孤儿普通 transcript。确认删除可以移除文件,归档/恢复按原字节移动文件,并且 workspace-qualified internal 请求和唯一 internal owner-routed 请求都能成功。
  • 为同一个 ID 同时创建 active 和 archived 副本。确认归档/恢复默认返回单会话冲突并保留两份文件;确认归档修复保留 archived 内容,恢复修复保留 active 内容,删除仍会删除两份副本。
  • 确认外部工作区 transcript 返回 not found;符号链接、目录和 FIFO fail closed,不会跟随、不被修改,也不会造成阻塞。
  • 确认普通/internal 所有权歧义、运行时代际替换和 writer lease 被占用时,操作会被拒绝且不会修改存储。
  • 运行 Core、daemon 生命周期、多工作区路由、ACP transport 和 TypeScript SDK 聚焦测试。rebase 后验证通过 228 项 Core 测试、58 项 daemon 生命周期测试、12 项多工作区生命周期测试、13 项 ACP 生命周期测试和 348 项 SDK 测试。完整仓库构建、workspace typecheck、pre-commit 格式化/lint 检查以及 git diff --check 也全部通过。

证据(Before & After)

Before:实际存在的空、损坏和孤儿 transcript 会被生命周期 API 报告为 not found,并滞留在磁盘上。

After:属于当前 runtime 的普通 transcript 无需可加载即可删除、归档或恢复;默认冲突行为保持不变,并在能力被声明时提供显式保留一侧的修复操作。

测试平台

OS 状态
🍏 macOS ✅ 已测试
🪟 Windows ⚠️ 未测试
🐧 Linux ⚠️ 未测试

环境(可选)

macOS 26.4.1 arm64、Node.js v22.22.3、npm 10.9.8。本地验证了文件系统生命周期行为和全新的 REST E2E fixture;最终 commit 在 rebase 到当前 main 后重新完成构建和 typecheck。

风险与范围

  • 主要风险或权衡:这些是破坏性文件系统操作,主要风险是作用于陈旧、外部或并发替换的存储。实现保留既有生命周期 coordinator 和 writer lock,新增权威 active/archive 文件快照,并在提交边界重新验证文件身份。
  • 未验证 / 范围外:本地未运行 Windows 和 Linux。通用大小写兼容、会话列表合并、transcript/export 行为、组织元数据、Live/任务身份、transcript 合并和父子级联删除均有意不在本 PR 范围内。
  • 破坏性变更 / 迁移说明:原先通过 HTTP 409 session_conflict 处理 workspace-qualified 归档/恢复冲突的客户端,现在必须检查 HTTP 200 批量响应中的 errors 数组。默认变更行为仍然是不修改文件。普通 workspace-less 生命周期请求继续只作用于 primary,删除继续保留删除两份的行为;resolveConflictsresolvedConflictssession_storage_conflict_repair 都是增量接口,客户端使用该选项前应预检能力。

关联问题

Fixes #9488

基于 #9513

@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Aug 21, 2026
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

E2E Test Report

Environment: macOS 26.4.1 arm64, Node.js v22.22.3, npm 10.9.8, case-sensitive APFS fixture.

  • Routes matrix: PASS. Verified 31 HTTP statuses and 23 final filesystem states across empty/orphaned/corrupt lifecycle, mixed-case qualified and unqualified transcript/export, logical cursor/error identity, foreign active/archived rejection, default conflicts, and both explicit repair directions.
  • Storage matrix: PASS (caseSensitive=true). Verified empty/corrupt/orphan maintenance, mixed-case delete/archive/unarchive/rename, default active+archive delete compatibility, and ordinary/repair replacement-race rejection.
  • Organization alias matrix: PASS. Verified retained archived alias wins archive repair when canonical is absent, canonical logical state is produced, and both physical aliases are removed.
  • Focused unit/integration evidence: Core session 231/231; Core organization/rename/scheduler 228/228; daemon archive/export 77/77; multi-workspace 137/137; ACP bridge 809/809; TypeScript SDK 347/347.
  • Independent final code review: clean, with no remaining Critical/P1 finding.

Known local-only limitation: CLI typecheck is blocked by the shared checkout's pre-existing Ink selection typings mismatch; no diagnostic points at the changed #9488 session files.

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

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

capabilities

field PR base (before) this PR (after)
features[] "session_storage_conflict_repair"

Qwen Code · serve A/B

@github-actions

Copy link
Copy Markdown
Contributor

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)为单个提交。

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Rebase and verification update

Rebased this Draft PR onto the current #9513 head 78031b16bf944bdf6d02c89aabdb8a82f947fbf8 and revalidated the incremental lifecycle changes.

Current-head verification:

Case-sensitive APFS lifecycle matrices previously passed for REST routes, storage mutation, and organization alias reconciliation, covering empty/damaged/orphaned transcripts, foreign and non-regular fail-closed behavior, active/archive default conflict behavior, deterministic archive/unarchive repair, transcript/export logical identity, and auxiliary-state cleanup. The final rebase does not add broad mixed-case session-list projection; normal generated UUID session IDs remain lowercase and #9513's list behavior is unchanged.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC
doudouOUC force-pushed the fix/session-storage-lifecycle-9488 branch from d58d179 to 3c1bdf4 Compare August 22, 2026 14:46
@doudouOUC doudouOUC changed the title fix(serve): unify persisted session storage lifecycle fix(serve): Repair persisted session lifecycle Aug 22, 2026
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Updated minimal-scope E2E report

This report supersedes the earlier broad-scope verification comments. The Draft PR is now rebased onto the current main, which already contains #9513, and the remaining #9488 change is limited to persisted-session delete/archive/unarchive maintenance.

  • Empty, torn/damaged, and legacy-orphan transcripts: PASS for delete, archive, and unarchive through both workspace-qualified internal routes and unambiguous internal owner routing. Delete removed the file; archive/unarchive preserved the original bytes while moving it to the requested state.
  • Active/archive compatibility: PASS. Default archive and unarchive returned per-session conflicts without modifying either copy; delete removed both copies. With resolveConflicts: true, archive kept archived content and unarchive kept active content, with the repaired logical IDs reported in resolvedConflicts.
  • Safety boundaries: PASS. Foreign-workspace transcripts remained untouched and were reported as not found. Symlink, directory, and FIFO entries failed closed; FIFO classification returned without blocking. Ambiguous owners, runtime-generation replacement, writer ownership, and source replacement did not mutate storage.
  • Post-rebase focused suites: Core session service 228/228, daemon lifecycle 58/58, multi-workspace lifecycle 12/12, ACP lifecycle transport 13/13, and TypeScript SDK 348/348.
  • Repository checks: full build, workspace typecheck, pre-commit formatting/lint checks, and git diff --check passed.

Environment: macOS 26.4.1 arm64, Node.js v22.22.3, npm 10.9.8. Windows and Linux were not run locally.

@doudouOUC
doudouOUC marked this pull request as ready for review August 22, 2026 15:13
@doudouOUC
doudouOUC enabled auto-merge August 22, 2026 15:13
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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

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

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

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

Copy link
Copy Markdown
Collaborator

Re-run note: no commits landed since the last pass — this pass re-attests the same head 49b67ebe. What changed is the review record, not the code: after the last pass deferred, the /review lane's round 11 (2026-08-24 20:55 UTC) posted two new Critical findings on this exact head (R11-1 org-entry leak when a fence trips post-deletion; R11-2 permanent sidecar leak when a fence trips mid-cleanup). Stage 2 verifies both against the head code, and Stage 3 moves accordingly.

Template looks good ✓

Problem: unchanged — an observed, well-evidenced bug. Issue #9488 (open, P1) documents the root cause: lifecycle admission goes through the loadability classifier, so empty, torn, or orphaned transcripts that physically exist are reported as not found and cannot be deleted/archived/unarchived. Confirmed on main during issue triage. Not theoretical hardening.

Direction: aligned, unchanged — the PR implements the direction the issue suggested for its second work item (decouple lifecycle mutation from provenance classification while keeping ownership fences). The scope caveat stands: #9488 tracks two work items and the first (mixed-case spelling resolution at the storage boundary) is explicitly out of scope here, yet the body says "Fixes #9488" and would auto-close it. Whoever merges must make that call explicitly.

Size: recomputed at this head — 1459 production-logic lines / 2414 test lines / 101 docs lines across 30 files. Still touches core paths (packages/core/src/services/**, 758 of those lines) and spans core + cli + acp-bridge + sdk-typescript. Not a refactor type, so no hard block — but 500+ production lines in core keeps this escalated to maintainer awareness under the two-tier gate, which means the bot will not auto-approve regardless of review outcome. The 1000+ large-PR advisory also still applies; informational only.

Approach: the two-commit delta from the previous pass remains exactly the requested shape, nothing extra. The feedback commit resolves R9-4 by committing each copy's salvaged usage immediately after that copy's unlink instead of after all unlinks — one Map keyed by file path, explicit null handling, and a test pinning the ordering and failure propagation. The merge is clean: main moved one commit, 15 of the 17 touched files are byte-identical to the main side, and the two files both sides edited (bridge.ts, qwen-serve-protocol.md) combine both contents with zero conflict markers (Stage 2 details). No scope creep, no drive-by edits. What is now open is not scope but correctness of the fence placement this PR introduces — see Stage 2's round-11 verification.

Risk: no elevated revert-risk paths matched (Stage 1e patterns re-checked against the 30 changed files this pass — no hits). CI at this head re-fetched this pass and still fully green, including both required checks.

Moving on to code review. 🔍

中文说明

重跑说明:上次通过后没有新提交——本次继续以同一头部 49b67ebe 为准。变化的是评审记录而非代码:上次延迟决定之后,/review 环节的第 11 轮(2026-08-24 20:55 UTC)在同一头部上新增了两个 Critical(R11-1 删除完成后栅栏中断导致组织条目泄漏;R11-2 清理过程中栅栏中断导致 sidecar 永久泄漏)。Stage 2 已在头部代码上逐一核实,Stage 3 相应调整。

模板完整 ✓

问题:未变 —— 已观测、证据充分的 bug(Issue #9488,开放,P1,已在 main 确认)。非理论性加固。

方向:对齐,未变。范围 caveat 仍然成立:#9488 含两个工作项,第一项(混合大小写)明确不在本 PR 范围,而正文写 "Fixes #9488" 会自动关闭 issue;合入者必须明确作出这个决定。

规模:本头部重新计算 —— 30 个文件中生产逻辑 1459 行、测试 2414 行、文档 101 行。仍触及核心路径(packages/core/src/services/**,其中 758 行)并横跨 core + cli + acp-bridge + sdk。非 refactor 类型,不触发硬阻断——但核心路径 500+ 生产行仍按两级门禁提请维护者关注,机器人不会自动批准。1000+ 大 PR 建议线同样仍适用,仅作信息提示。

方案:上一轮的两个增量仍恰好是所要求的形状,没有多余内容(R9-4 按副本即时提交 usage 抢救 + 干净的 main 合并,详见 Stage 2)。当前未决的不再是范围,而是本 PR 引入的栅栏布点的正确性——见 Stage 2 的第 11 轮核实。

风险:未命中高回滚风险路径(本次已对 30 个变更文件重查 Stage 1e 模式,无命中)。本头部 CI 本次重新抓取,仍全绿,包括两个必填检查。

进入代码审查。🔍

Qwen Code · qwen3.8-max

Reviewed at 49b67ebef391e96148828da9845dd740919edfa3 · re-run with @qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

Re-run at 49b67ebe — the head is unchanged since the last pass, so that pass's review of the two deltas (the R9-4 per-copy salvage-commit fix 782dd86, and the clean one-commit main merge 49b67ebe) carries over as-is. What is new this pass: verifying the round-11 findings the /review lane posted on this head after the last triage update. Static reading of the head tree via API only; no PR code executed.

Round 11 — R11-1, org-entry leak on a post-deletion fence trip: verified in the head code, and introduced by this diff. The PR adds assertCanMutate?.() to removeSessionOrganization's catch (sessionService.ts:1357 at this head) and forwards the fence as assertCanCommit into the org-store atomic write (1350–1352); the diff shows the catch was warn-only before this PR. When the runtime generation is replaced during the org-store commit — the exact race these fences exist for — removeSessionFiles has already deleted every session file, but removeSession now throws post-deletion. A retry resolves location === undefined (2280), returns false, and the if (removed) gate (2233) skips organization cleanup forever; the batch path gates identically (2633 → 2652). I checked every org-removal call site — all are conditioned on successful file removal, so nothing can ever remove an org entry whose files are already gone. Net effect: a permanent store inconsistency plus a false-failure response that invites retries which can never converge. The /review lane's scratch-tree probe at HEAD witnesses every step, and its candidate fixes (restore the warn-only contract for this post-deletion cleanup, or run org cleanup before file deletion) flip the probe.

Round 11 — R11-2, permanent sidecar leak on a mid-cleanup fence trip: verified in the head code. removeSessionFiles unlinks the transcript copies first (per-copy loop at 2299) and only then runs the four reap helpers, with assertCanMutate fences between every step (2304–2311). A generation trip in that window leaves the transcript deleted and the sidecars untouched; the retry returns false at 2280 before ever reaching the reap helpers, and those helpers have no other call sites in this file — the interrupted cleanup is unreachable. <id>.worktree.json, <id>.pr.json, and <id>.ledger.jsonl leak permanently (file-history backups are reclaimed by the age sweeper; these are not). The probe witness and the candidate fix (reap in the location === undefined branch — idempotent, strictly session-id-keyed) are on record in the round-11 comments.

R10-1 (carried since round 1 as R1-4), pathname-vs-inode TOCTOU: unchanged — declined by explicit decision, and the follow-up issue the maintainer asked for (review 5006725473) still does not exist, re-checked this pass. Residual on record for the human approver.

Round 11 also records eight probe-level items deferred under its convergence posture (recorded, not requested) and re-rates the earlier R9-2 protocol-doc finding to Suggestion; those live in the round-11 review's own ledger.

No other new findings at this head. Carried open items, unchanged and non-blocking: the dead knownLocation option (still declared at sessionService.ts:304/319, exercised only by tests) and the dead persistUsageBeforeTranscriptDeletion wrapper (referenced only by its own test file) — a search finds no follow-up issue filed for either, please file them so the cleanup doesn't evaporate; R9-2 protocol-doc wording deferred by the author as a documentation clarification, on record.

Maintainer evidence on the record (theirs, not mine): @wenshao's second full A/B — both arms rebuilt from scratch at the new merge-base, 25-case lifecycle matrix bit-for-bit stable across the rebase, and the usage-salvage question settled by measurement with a control row. Verdict unchanged: good to merge. That evidence covers everything up to 21da77c; @wenshao then approved this head 49b67ebe with the two deltas statically verified by the last triage pass. @yiliang114's re-approval was dismissed by the 21da77c push under branch protection and needs re-earning. None of that evidence covers the round-11 race windows — those are fence-trip paths the A/B endpoints don't provoke.

sequenceDiagram
    participant P1 as Client REST or ACP
    participant P2 as Lifecycle route
    participant P3 as Runtime registry
    participant P4 as Archive coordinator
    participant P5 as Writer lease
    participant P6 as SessionService
    participant P7 as Filesystem
    P1->>P2: delete or archive or unarchive
    P2->>P3: resolve runtime by physical ownership
    P3-->>P2: one unambiguous runtime or fail closed
    P2->>P4: take per session coordinator lock
    P2->>P5: take lease, maintenance fallback for damaged heads
    P5-->>P2: lease granted or live lease conflict rejects
    P2->>P6: snapshot active and archived file identity
    P6->>P7: open nofollow, stat dev and ino, read first record
    P6-->>P2: snapshot, or non regular or foreign or ambiguous rejection
    P2->>P6: revalidate identity and generation then unlink or rename
    P6->>P7: verify same file, fence each sidecar, then mutate
    P6-->>P1: per id buckets plus resolvedConflicts
Loading

Test evidence — the PR's own CI at 49b67ebe, re-fetched this pass

All checks complete, nothing pending, zero failures — the same picture as the last pass: the full required set is green at this head. (Re-fetched 2026-08-25; the only non-success, non-skipped conclusion is a cancelled bot-orchestration route job, not this PR's CI.)

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success (required)
Serve A/B (ubuntu-latest, Node 22.x) ✅ success (required)
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Real daemon E2E / Java 11 ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Live Host (macos-latest) ✅ success
SDK Java (ubuntu Java 11/17/21, macOS Java 21, windows Java 21) ✅ success
Secret scan (TruffleHog) ✅ success
Dependency CVE audit ✅ success
Classify PR / label / precheck ✅ success
Test (macos-latest, Node 22.x) ⏭️ skipped (fork PRs need runner approval)
Test (windows-latest, Node 22.x) ⏭️ skipped (fork PRs need runner approval)
Integration Tests (CLI, No Sandbox) ⏭️ skipped

Sandboxed verification: a sponsored /verify run pinned to this head is in flight right now, triggered by the same comment that started this triage pass (run 32801005267); its report will post in this thread when it completes. The author lacks write access, so this is a sponsored run (pre-execution risk screen plus full workspace wipe before any PR code runs) — read its report with the same skepticism as the fork's own CI logs. One caveat: the round-11 leaks require a runtime-generation trip mid-operation, so a scripted endpoint A/B may not provoke them even when green — the scratch-tree probes attached to the round-11 comments are the evidence that pins those paths, in either direction.

Real-scenario testing: unattended CI run (issue_comment re-run) — no tmux is driven by this lane; the live-behavior signal is the sandboxed lane above plus @wenshao's two A/B rounds cited under maintainer evidence. Not verified by this pass: live behavior at exactly 49b67ebe (default paths covered by unit tests + green CI + the one-commit-earlier live A/B; the fence-trip race paths rest on the round-11 probes).

中文说明

代码审查

49b67ebe 重跑 —— 头部与上次通过时相同,上次对两个增量的审查(R9-4 按副本即时提交 usage 抢救的 782dd86、干净的单提交 main 合并 49b67ebe)原样沿用。本次新增内容:核实上次 triage 更新之后 /review 环节第 11 轮在同一头部上发布的发现。仅通过 API 对头部树做静态阅读,未执行任何 PR 代码。

第 11 轮 —— R11-1,删除完成后栅栏中断导致组织条目泄漏:已在头部代码中核实,且由本 diff 引入。 本 PR 在 removeSessionOrganization 的 catch 中新增 assertCanMutate?.()(头部 sessionService.ts:1357),并把栅栏作为 assertCanCommit 传入 org store 原子写入(1350–1352);diff 显示本 PR 之前该 catch 仅告警。当运行时代际在 org store 提交期间被替换(正是这些栅栏要防的竞态)时,会话文件已全部删除,但 removeSession 现在会在删除后抛错。重试时 location === undefined(2280)返回 falseif (removed) 守卫(2233)永远跳过组织清理;批量路径同样设守(2633 → 2652)。已核对所有组织删除调用点——全部以文件删除成功为前提,因此文件已不存在的组织条目永远无人清理:store 永久不一致,且错误失败响应会诱发永远无法收敛的重试。/review 环节的 HEAD 隔离探针逐步佐证;候选修复(恢复该删除后清理的仅告警契约,或先做组织清理再删文件)可使探针翻转。

第 11 轮 —— R11-2,清理过程中栅栏中断导致 sidecar 永久泄漏:已在头部代码中核实。 removeSessionFiles 先逐副本 unlink transcript(2299 循环),随后才执行四个清理助手,且每步之间都有 assertCanMutate 栅栏(2304–2311)。在该窗口内代际被替换时,transcript 已删除而 sidecar 原封未动;重试在 2280 处返回 false,永远到不了清理助手——这些助手在本文件中没有其他调用点,被中断的清理不可达。<id>.worktree.json<id>.pr.json<id>.ledger.jsonl 永久泄漏(file-history 备份有按房龄清扫器,这些没有)。探针佐证与候选修复(在 location === undefined 分支中回收——幂等且严格按 sessionId 匹配)见第 11 轮评论。

R10-1(自第 1 轮 R1-4 沿承),路径名-vs-inode TOCTOU:未变——经决定不做;维护者要求的跟进 issue(review 5006725473)本次复查仍未创建。残留记录留给人类批准者。

第 11 轮还在其收敛姿态下记录了 8 条探针级延后项(记录在案、本轮不要求修改),并把早先的 R9-2 协议文档发现降为 Suggestion;详见该轮评审自身台账。

本头部无其他新发现。遗留未决项,未变且不阻塞:死代码 knownLocation 选项与死代码 persistUsageBeforeTranscriptDeletion 包装——搜索确认两者仍未建跟进 issue,请补建以免清理蒸发;R9-2 协议文档措辞由作者按文档澄清处理、记录在案。

记录在案的维护者证据(是他们的,不是我的):@wenshao 的第二轮完整 A/B(两条 arm 从头重建、25 用例矩阵逐位稳定、usage 抢救问题以带对照行的实测定性),结论不变:可以合。该证据覆盖到 21da77c@wenshao 随后批准了本头部 49b67ebe@yiliang114 的重新批准已被推送在分支保护下撤销,需要重新取得。这些证据均不覆盖第 11 轮的竞态窗口——那是 A/B 端点不会触发的栅栏中断路径。

测试证据 —— 本头部 49b67ebe 上 PR 自己的 CI,本次重新抓取

所有检查已完成、无进行中、零失败——与上次相同:必填项在本头部全绿。(2026-08-25 重新抓取;唯一非成功非跳过的结论是一个被取消的机器人编排 route 任务,非本 PR 的 CI。)

沙箱验证:一次钉在本头部上的代跑 /verify 此刻正在运行(由触发本次 triage 的同一条评论发起),完成后报告会发布在本线程。作者无写权限,这是代跑(执行前风险筛查加全量工作区清空),报告请保持与 fork 自身 CI 日志同等的怀疑。注意:第 11 轮的泄漏需要操作中代际被替换才会出现,脚本化端点 A/B 即使全绿也未必能触发——钉住这些路径的证据是第 11 轮评论附带的隔离探针,两个方向都是。

真实场景测试:无人值守 CI 运行(issue_comment 重跑)——本车道不驱动 tmux;实测信号为上述沙箱车道与 @wenshao 的两轮 A/B。本次未验证:49b67ebe 精确头部上的实机行为(默认路径由单测 + 全绿 CI + 前一个提交的实测 A/B 覆盖;栅栏中断竞态路径以第 11 轮探针为准)。

Qwen Code · qwen3.8-max

Reviewed at 49b67ebef391e96148828da9845dd740919edfa3 · re-run with @qwen-code /triage

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

CI fix update

The Ubuntu failure was a deterministic test-fixture drift after lifecycle owner resolution switched to the authoritative maintainable-storage classifier. The production behavior was correct; the inherited Live-runtime test mocked only the old readable-session classifier, so its synthetic internal session had no physical-owner proof and the request correctly fell back to primary.

Updated the test fixture to model maintainable storage ownership explicitly. No production code changed.

Verification:

  • isolated failing test: PASS (1/1)
  • Live conversation runtime lifecycle: PASS (19/19)
  • full build and workspace typecheck: PASS
  • pre-commit Prettier/ESLint and git diff --check: PASS

The separate Web Shell E2E failure was runner infrastructure: checkout could not remove a stale permission-owned .qwen/.../.vite/vitest directory. The new commit retriggers CI; no unrelated code was changed.

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@doudouOUC doudouOUC self-assigned this Aug 22, 2026
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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

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

Scripted assertions: 2304 passed · 0 failed · 2304 total

Flakiness gate: ✅ 7 changed test file(s) x 5 identical rounds, no divergence

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

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

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

抖动门:✅ 7 changed test file(s) x 5 identical rounds, no divergence

Verification report

PR #9626 Deep Verification — fix(serve): Repair persisted session lifecycle

Verdict: merge-ready — 2304 scripted assertions executed, 2304 pass / 0 fail. A/B load-bearing proof flips 8 of 20 daemon-level cells from stranded/blocked (base) to maintained (head) with zero regressions in the 12 parity/fail-closed cells. Verified head: 371ea3bdce21ac365d0bbd0a52590e60a2ee5aec (merge commit df0c74f1f6, base tip c10143a9c1).

中文摘要
  • 结论merge-ready。2304 项脚本化断言全部通过(0 失败)。
  • A/B 结论:对 20 个守护进程级场景做了 head 与 base 的对照(见 "A/B cell table")。空文件、撕裂头、损坏冲突、sealed-lease 空文件在 base 上全部被报 notFound 并滞留磁盘(其中 FIFO 场景 base 会阻塞 >6 秒且进程之后对 SIGTERM 无响应),在 head 上全部可删除/归档/恢复且字节原样保留;可加载冲突的默认行为(报错、双份保留)、外部工作区拒绝、活 writer 租约拒绝在两侧完全一致。字节保留经逐字节比对断言。
  • 变异矩阵:7 个定点变异(M1–M7)全部被相应测试以行为级失败杀死(例如 M1「要求可读头部」使 13 个生命周期测试变红、M4「路由退回加载式分类」恰好使 9 宫格路由矩阵中的 6 个空/损坏单元变红而 3 个孤儿单元保持绿)。无存活变异。
  • 发现:无阻塞项。1 个 Suggestion:ArchiveSessionsOptions.knownLocation / UnarchiveSessionsOptions.knownLocation 在本 PR 后成为无任何读取方的死选项(建议删除或注明已忽略)。2 个观察项:非普通文件(符号链接/目录/FIFO)在工作区限定 REST 路径上经 sendBridgeError 表现为 500、在守护进程函数级表现为 200+逐会话 error——两种均 fail-closed 且无变更;首记录读取器对无换行单行文件最坏整文件缓冲(实测线性,10MB≈0.95s,仅本地自有存储)。
  • 未覆盖:逐提交归因(浅克隆深度 2,首个提交 3c1bdf4470 不可达,仅验证聚合差异);Windows/macOS 行为(本轮为 Linux 容器,恰好补上了 PR 自述未测的 Linux);完整 qwen serve 进程级 E2E(以路由层测试套件 + 守护进程函数级真实磁盘 harness 替代);knownLocation 以外的 API 兼容性面(如旧版 SDK 对新守护进程的解析,由类型上的可选字段保证,未实测)。

Scope selection

Central claim — delete/archive/unarchive can maintain owned regular transcripts that are empty, torn/malformed-headed, or legacy orphans, which base reports as notFound because classification required the transcript to load as a conversation.

Secondary claims

  1. resolveConflicts: true repairs active/archive conflicts (archive keeps archived, unarchive keeps active), reported via resolvedConflicts; default conflict behavior and delete-both are unchanged.
  2. Safety fences hold: foreign-workspace entries are not-found, symlink/dir/FIFO fail closed without following/mutating/blocking, live writer leases and replaced generations reject without mutation.

Out of scope (listed under Not covered): transcript loading/listing/export behavior, case-insensitive lookup, Live/task identity, Windows/macOS platforms, per-commit attribution.

A/B load-bearing proof (central claim)

Environment per arm:

arm tree code under test control check
head df0c74f1f6 (merge commit, prebuilt by CI) packages/{core,cli}/dist at head readlink -f node_modules/@qwen-code/qwen-code-core/__w/qwen-code/qwen-code/packages/core
base worktree at HEAD^1 = c10143a9c1, rebuilt in-tree tmp/base-tree/packages/{core,cli}/dist at base readlink -f …/qwen-code-coretmp/base-tree/packages/core (base-tree link web; grep -c getMaintainableSessionLocation base core dist = 0, grep -c resolvedConflicts base cli dist = 0 vs 8 at head)

Both arms' harnesses assert the loaded modules' realpaths stay inside their own tree before running (quoted in Methodology). package.json/package-lock.json are untouched by the PR, so sharing the root third-party node_modules is a clean control; internal @qwen-code/* links were re-pointed into the base tree to avoid the monorepo symlink trap.

Harness: harness/lifecycle-ab.mjs drives the real compiled daemon entry points (deleteDaemonSessions / archiveDaemonSessions / unarchiveDaemonSessions from session-archive.js) against real on-disk fixtures; the only stub is the AcpSessionBridge collaborator (targets live agents, not storage — same stub the project's own tests use). Oracles: result classification bucket + post-operation filesystem state (existence + byte-identity of moved content), all scripted. Raw logs: logs/head-arm-final.log, logs/base-arm-final.log; witnesses: 01-ab-head-arm-all-green.png, 02-ab-base-arm-stranded-and-fifo-block.png.

# scenario (fixture) operation base (c10143a) head (371ea3b) flip?
1 empty transcript delete notFound, file stranded removed, file gone
2 empty transcript archive notFound, stranded archived, moved byte-identical (empty)
3 empty transcript (archived) unarchive notFound, stranded unarchived, moved byte-identical (empty)
4 torn head {"uuid":"torn-head" delete notFound, stranded removed
5 torn head archive notFound, stranded archived, torn bytes preserved
6 orphan child (readable head, dead parent) delete removed removed parity (route-level flip pinned by M4/M1; orphan head here is readable)
7 healthy transcript delete removed removed parity control
8 healthy transcript archive archived, bytes preserved archived, bytes preserved parity control
9 damaged conflict (both copies unloadable) archive default notFound, both stranded error (conflict classified physically), both preserved ✅ classification
10 loadable conflict archive default error, both preserved error, both preserved parity (default unchanged)
11 loadable conflict archive resolveConflicts:true error (option unknown), both preserved archived + resolvedConflicts, active removed, archived copy byte-identical
12 loadable conflict unarchive resolveConflicts:true error, both preserved unarchived + resolvedConflicts, archived removed, active copy byte-identical
13 loadable conflict delete removed, both copies gone removed, both copies gone parity (delete-both kept)
14 foreign-workspace head record delete notFound, untouched notFound, untouched parity fence
15 foreign-workspace head record archive (+resolveConflicts) notFound, untouched notFound, untouched parity fence
16 symlink → outside file archive error SessionWriterUnavailableError (rejected at lease stage), link + target untouched error SessionStorageEntryError (rejected at classification), link + target untouched fence parity, earlier layer at head
17 directory as <id>.jsonl delete notFound, dir intact error non_regular, dir intact fence parity (both fail closed)
18 FIFO as <id>.jsonl archive blocked >6 s (cell timed out; process later unkillable by SIGTERM — blocked thread-pool open) error in 5 ms, FIFO intact ✅ hazard eliminated
19 live writer lease held archive error SessionWriterConflictError, untouched error SessionWriterConflictError, untouched parity fence
20 sealed lease over empty transcript archive notFound (empty file never classifies, never reaches lock), stranded archived via certified takeover, file moved

Counts: head arm 54/54, base arm 42/42 scripted expectations (base expectations encode "fails/stranded as predicted" — a base cell failing as predicted is a passed assertion). No cell mutated storage in any fence case on either arm.

Note on row 20: base cannot even reach the sealed lock because the empty file is invisible to load-based classification — the stranding is total; head's takeoverPolicy:'certified' + maintenance-lease fallback is what restores the path (independently pinned by mutation M3 below).

Secondary-claim wire oracle (SDK surface)

harness/sdk-wire.mjs stands a real loopback HTTP server in front of the compiled SDK DaemonClient and captures the exact bytes on the wire (witness 03-sdk-wire-oracle.png), 11/11 pass:

  • archiveSessionsData([id], { resolveConflicts: true }) → body {"sessionIds":["id-1"],"resolveConflicts":true}, no client-id header.
  • archiveSessionsData([id], 'client-abc') → legacy overload intact: body {"sessionIds":["id-2"]}, header X-Qwen-Client-Id: client-abc.
  • Explicit false is transmitted (not dropped as falsy); omitted option sends no key.
  • workspaceById('ws-1') variants hit /workspaces/ws-1/sessions/{archive,unarchive} with correct bodies.
  • Capability discovery: SERVE_CAPABILITY_REGISTRY.session_storage_conflict_repair = { since: 'v1' } present in the built head.

Mutation matrix (vacuity / positive controls)

Every mutant is a fine-grained, interface-preserving revert applied to head source, run against the suite that should catch it, then restored (sha256-verified byte-identical restore after each). Positive-control rule satisfied: each mutant landed in the same file whose tests kill it, and failures are behavioral (expected-vs-actual), not import/collection breakage.

mutant what it reverts suite expected observed status
M1 classifier requires a readable first record (if (!firstRecord) return undefined) — simulates pre-PR loadability core corruption tests lifecycle-maintenance red 13 red (expected false to be true, archived: [] vs [id]); orphan-cell with readable head stays green killed
M2 conflict repair disabled (if (true) at both !options.resolveConflicts guards) core corruption tests repair tests red 4 red — exactly the repair/conflict tests killed
M3 daemon lease fallback removed (no takeoverPolicy, no maintenance lease) cli session-archive tests sealed-takeover red 1 redtakes over a sealed empty transcript before maintenance, other 57 green killed
M4 route lifecycleMaintenance reverts to load-based getSessionLocation cli multi-workspace tests empty/damaged route cells red 6 red — exactly the 6 empty/damaged cells of the 9-cell HTTP matrix; 3 orphan cells stay green killed
M5 SDK drops resolveConflicts from request bodies sdk DaemonClient tests repair-body test red 2 red (new repair test + exact-body workspace helper test) killed
M6 ACP parseResolveConflicts coerces instead of rejecting cli transport tests non-boolean rejection red 1 redrejects a non-boolean conflict repair option killed
M7 REST parseResolveConflicts coerces instead of 400 cli server tests explicit-boolean test red 1 redrequires an explicit boolean to repair active/archive conflicts killed

No survivors. Witnesses: 04-mutation-m1-readable-head-required.png, 05-mutation-m4-route-load-based.png.

M4's kill set is informative beyond vacuity: the route-level hunk is responsible for the empty/damaged flips, while orphan route cells survive load-based classification because their heads are readable — the unloadable-orphan flip is carried by the same classifier (M1), consistent with the design doc ("cleanup of legacy orphan children without making them loadable").

Targeted gates (head, clean tree)

gate result
core sessionService.corruption.test.ts + sessionService.test.ts 228/228 (matches the PR's claimed "228 Core tests")
cli session-archive.test.ts + multi-workspace-sessions.test.ts + acp-http/transport.test.ts 536/536
cli server.test.ts 1078/1078
sdk test/unit/DaemonClient.test.ts 348/348 (matches the PR's claimed "348 SDK tests")

Findings

No blockers.

  1. Suggestion — dead option knownLocation. ArchiveSessionsOptions.knownLocation?: 'active' and UnarchiveSessionsOptions.knownLocation?: 'archived' (packages/core/src/services/sessionService.ts:292,307) have zero read sites after this PR — grep across packages/core, packages/cli, packages/sdk-typescript production sources finds only the declarations. This PR removed the last consumers (the if (options.knownLocation !== …) branches here, and the knownLocation: 'active'/'archived' call sites in session-archive.ts). Any remaining core-API consumer passing it now silently gets full physical classification instead of the old shortcut. Recommend deleting the option (or a one-line doc note that it is ignored).
  2. Observation — fail-closed surface differs by entry point. A symlink/dir/FIFO entry is rejected as SessionStorageEntryError(non_regular): at the daemon-function level this lands in the per-session errors[] of a 200 response (proven in A/B rows 16–18), but when thrown during route resolution for a workspace-qualified request it propagates to sendBridgeError → 500. Both fail closed with zero mutation; the asymmetry is cosmetic. Not a defect.
  3. Observation — first-record reader buffers a whole newline-less file. readFirstMaintainableRecord accumulates chunks until a newline or EOF. Measured on a single-line non-JSON transcript: 1 MB → 45 ms, 5 MB → 300 ms, 10 MB → 950 ms classification (+1.17 s delete incl. salvage) — linear, no superlinear rung. The input is the user's own local storage (not outsider-authored), so this is informational only.
  4. Note — platform shapes. Linux run (this container): a directory opens with O_RDONLY and is caught by the isFile() check; macOS would fail the open with EISDIR and surface an error entry instead — both fail closed on both platforms (per code path; macOS not exercised here). This round supplies the Linux evidence the PR description marked "not tested".

Corrections

None required — no inaccurate prior-review claims were observed in this round.

Not covered

  • Per-commit attribution: the checkout is depth 2; the metadata lists 2 commits (3c1bdf4470, 371ea3bdce) but only the head commit is reachable (git rev-list HEAD^1..HEAD^2 = 1, repo is shallow). Verified the aggregate HEAD^1..HEAD diff; the first commit's isolated content was out of reach. (The second commit's message says "fix CI failure on PR fix(serve): Repair persisted session lifecycle #9626", consistent with a test-only follow-up, but this was not independently confirmed.)
  • Windows / macOS behavior — this round ran on Linux only (the lane's node:22-bookworm container). The O_NOFOLLOW/O_NONBLOCK flag path has a process.platform === 'win32' branch that was not exercised.
  • Full qwen serve process-level E2E (real daemon socket + workspace registry): covered instead by the project's own express-level route suites (536/536 including the 9-cell HTTP matrix over supertest) plus my daemon-function-level harness on real disk state. A live-daemon smoke would add transport confidence but was not budgeted.
  • Old-SDK-against-new-daemon compatibility beyond types: resolvedConflicts is optional in SDK types (checked by reading), not exercised against a real old client build.
  • Scheduled-task side effects (updateScheduledTaskForMaintenance) beyond what the 536-test cli suites pin.
  • Repo-wide lint/typecheck/full-suite gates — the PR's own CI covers them; this round ran only the affected workspaces' suites named above.
  • verify-capture.mjs worked for all five images; no capture gaps.

Methodology

Single container (node:22-bookworm, Linux), working tree = refs/pull/9626/merge at depth 2; npm ci + npm run build pre-run at head by the workflow. Base control: git worktree add tmp/base-tree HEAD^1 (note: the snapshot's baseRefOid 867ded5b… predates the CI merge; HEAD^1 = current main tip c10143a9c1 is the correct landing base, and the merge commit itself is the trial merge of this PR into that tip). Base tree wired to the already-installed root third-party node_modules (PR touches no package.json/lockfile) with @qwen-code/* re-linked into the base tree plus per-package nested node_modules mirrored; the harness asserts readlink -f of both internal links resolves inside the active tree before executing — head: → /__w/qwen-code/qwen-code/packages/{core,cli}, base: → tmp/base-tree/packages/{core,cli} — and refuses to run otherwise. Base dist marker-checked (getMaintainableSessionLocation absent, resolvedConflicts count 0 vs 8). Harnesses live in harness/ (lifecycle-ab.mjs, sdk-wire.mjs, bigline-probe.mjs), raw per-arm logs and JSON results in logs/, build logs base-build{,2,3}.log (build2 failed on missing nested node_modules — diagnosed and fixed before any base cell was taken; build3 produced the dists used), mutation restores sha256-verified against pre-mutation checksums. One environment quirk kept honest: the base-arm FIFO cell leaves the node process blocked in a thread-pool open() and subsequently unresponsive to SIGTERM — which is itself the hazard row 18 documents.

Flakiness gate log

rounds=5 files=7 skipped=0
file packages/cli/src/serve/acp-http/transport.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/acp-http/transport.test.ts
file packages/cli/src/serve/multi-workspace-sessions.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/multi-workspace-sessions.test.ts
file packages/cli/src/serve/server.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/server.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/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
file packages/sdk-typescript/test/unit/DaemonClient.test.ts: (cd packages/sdk-typescript) npx --no-install vitest run ./test/unit/DaemonClient.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/cli/src/serve/acp-http/transport.test.ts: PPPPP
  packages/cli/src/serve/multi-workspace-sessions.test.ts: PPPPP
  packages/cli/src/serve/server.test.ts: PPPPP
  packages/cli/src/serve/server/session-archive.test.ts: PPPPP
  packages/core/src/services/sessionService.corruption.test.ts: PPPPP
  packages/core/src/services/sessionService.test.ts: PPPPP
  packages/sdk-typescript/test/unit/DaemonClient.test.ts: PPPPP

verdict: pass
summary: 7 changed test file(s) x 5 identical rounds, no divergence

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/serve/acp-http/transport.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/multi-workspace-sessions.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/server.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/server/session-archive.test.ts: P (exit 0)
round 1 · packages/core/src/services/sessionService.corruption.test.ts: P (exit 0)
round 1 · packages/core/src/services/sessionService.test.ts: P (exit 0)
round 1 · packages/sdk-typescript/test/unit/DaemonClient.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/acp-http/transport.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/multi-workspace-sessions.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/server.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/server/session-archive.test.ts: P (exit 0)
round 2 · packages/core/src/services/sessionService.corruption.test.ts: P (exit 0)
round 2 · packages/core/src/services/sessionService.test.ts: P (exit 0)
round 2 · packages/sdk-typescript/test/unit/DaemonClient.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/acp-http/transport.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/multi-workspace-sessions.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/server.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/server/session-archive.test.ts: P (exit 0)
round 3 · packages/core/src/services/sessionService.corruption.test.ts: P (exit 0)
round 3 · packages/core/src/services/sessionService.test.ts: P (exit 0)
round 3 · packages/sdk-typescript/test/unit/DaemonClient.test.ts: P (exit 0)
round 4 · packages/cli/src/serve/acp-http/transport.test.ts: P (exit 0)
round 4 · packages/cli/src/serve/multi-workspace-sessions.test.ts: P (exit 0)
round 4 · packages/cli/src/serve/server.test.ts: P (exit 0)
round 4 · packages/cli/src/serve/server/session-archive.test.ts: P (exit 0)
round 4 · packages/core/src/services/sessionService.corruption.test.ts: P (exit 0)
round 4 · packages/core/src/services/sessionService.test.ts: P (exit 0)
round 4 · packages/sdk-typescript/test/unit/DaemonClient.test.ts: P (exit 0)
round 5 · packages/cli/src/serve/acp-http/transport.test.ts: P (exit 0)
round 5 · packages/cli/src/serve/multi-workspace-sessions.test.ts: P (exit 0)
round 5 · packages/cli/src/serve/server.test.ts: P (exit 0)
round 5 · packages/cli/src/serve/server/session-archive.test.ts: P (exit 0)
round 5 · packages/core/src/services/sessionService.corruption.test.ts: P (exit 0)
round 5 · packages/core/src/services/sessionService.test.ts: P (exit 0)
round 5 · packages/sdk-typescript/test/unit/DaemonClient.test.ts: P (exit 0)

Evidence images

01-ab-head-arm-all-green

02-ab-base-arm-stranded-and-fifo-block

03-sdk-wire-oracle

04-mutation-m1-readable-head-required

05-mutation-m4-route-load-based

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

Qwen Code · sandboxed verification

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 2/5 — two new Critical findings landed on this head after the last pass, and I verified both mechanisms in the head code; not mergeable as-is, but the fix directions are small, named, and probe-verified.

Stepping back: the last pass deferred at 3/5 on the belief that the review had converged — zero new findings, required CI green end to end, a maintainer approval standing. Round 11 broke that premise the same evening: the fences this PR adds to the delete path create two trip windows the cleanup cannot survive — an org entry leaked post-deletion (R11-1) and sidecars leaked mid-cleanup (R11-2) — each witnessed by a scratch-tree probe at HEAD, each leaving a retry that can never converge because the retry path returns before reaching the cleanup code. I did not take the findings on faith: re-reading the head code shows the catch's new assertCanMutate?.() (sessionService.ts:1357), the fenced reap sequence (2304–2311) sitting behind the location === undefined early return (2280), and the if (removed) gate (2233) exactly as filed, and the diff confirms both mechanisms are introduced by this PR rather than inherited. To the PR's credit, these races are the same class its own Risk section names ("acting on stale, foreign, or concurrently replaced storage"), and the candidate fixes are probe-verified flips, not redesigns.

Why there is no bot approval — and no new bot review either:

  1. Open Criticals on the head. R11-1 and R11-2 above. The /review lane's CHANGES_REQUESTED review citing them (2026-08-24 20:55 UTC) already stands on this exact head and gates the PR; stacking a second request-changes review from this account would add noise, not signal. It stays until the fixes land or a maintainer adjudicates otherwise.
  2. Policy cap, unchanged. 1459 production lines across core paths keep this escalated under the Stage 0 two-tier gate — the bot does not approve escalated PRs, so even a clean round 12 would end in deferral, not approval.
  3. Second human approval still needed. @wenshao's approval stands on this head; @yiliang114's was dismissed by the 21da77c push under branch protection and needs re-earning.

Still true from the last pass: CI is fully green at this head with nothing pending, and @wenshao's two rebuilt-from-scratch A/B rounds remain the strongest behavioral evidence on record (covering through 21da77c). The sponsored /verify run pinned to 49b67ebe is in flight now; when it posts, read it with fork-CI skepticism — and note that a scripted A/B cannot settle the two race findings in either direction, only probes of the fence-trip window can.

Open items for the author: fix (or rebut with an equally concrete probe) R11-1 and R11-2; file the two dead-code follow-up issues so the cleanup doesn't evaporate. Open items for the maintainer: the Fixes #9488 auto-close versus the issue's two work items; the still-unfiled R1-4 TOCTOU follow-up issue; @wenshao's three A/B notes; dismissing stale request-changes reviews once satisfied.

⏸️ Deferring to @wenshao and @yiliang114 — the review re-opened on substance, not procedure. The bot's gate stays closed until the round-11 Criticals are resolved on a new head.

中文说明

置信度:2/5 —— 上次通过之后同一头部上新增两个 Critical,且两者机制均已在头部代码中核实;当前形态不宜合入,但修复方向小、明确、并已有探针验证。

退一步看:上次以 3/5 延迟决定,前提是评审已收敛——零新发现、必填 CI 全绿、维护者批准在案。第 11 轮在当晚打破了这一前提:本 PR 为删除路径添加的栅栏制造了两个清理无法幸存的中断窗口——删除完成后组织条目泄漏(R11-1)、清理过程中 sidecar 泄漏(R11-2)——各有 HEAD 隔离探针佐证,且重试永远无法收敛,因为重试路径在到达清理代码之前就已返回。我没有照单全收:重读头部代码,catch 中新增的 assertCanMutate?.()sessionService.ts:1357)、位于 location === undefined 提前返回(2280)之后的带栅栏清理序列(2304–2311)、if (removed) 守卫(2233)均与报告一致;diff 确认两个机制都是本 PR 引入而非既有继承。值得肯定的是,这些竞态正是本 PR 风险章节自己点名的类别("作用于陈旧、外部或并发替换的存储"),候选修复是探针验证过的翻转,不是重新设计。

为何没有机器人批准——也没有新的机器人评审:

  1. 头部上有未决 Critical。 即上述 R11-1 与 R11-2。/review 环节引用它们的 CHANGES_REQUESTED 评审(2026-08-24 20:55 UTC)已立于同一头部并构成门禁;同一账号再叠加一个 request-changes 只会增加噪音。在修复落地或维护者另行裁定之前,该评审保持有效。
  2. 政策上限,未变。 核心路径 1459 生产行使其在 Stage 0 两级门禁下保持升级——机器人不批准升级中的 PR,因此即使第 12 轮全绿也只会延迟决定而非批准。
  3. 仍需第二张人类批准。 @wenshao 的批准立于本头部;@yiliang114 的已被 21da77c 推送在分支保护下撤销,需重新取得。

上次仍然成立的部分:本头部 CI 全绿且无待处理;@wenshao 两轮从头重建的 A/B 仍是在案最强行为证据(覆盖到 21da77c)。钉在 49b67ebe 上的代跑 /verify 正在运行,发布后请以对 fork CI 日志同等的怀疑阅读——且脚本化 A/B 两个方向都无法定性这两个竞态,只有栅栏中断窗口的探针可以。

留给作者:修复(或以同样具体的探针反驳)R11-1 与 R11-2;为两处死代码补建跟进 issue。留给维护者:"Fixes #9488" 自动关闭与 issue 两个工作项的取舍;R1-4 TOCTOU 跟进 issue 仍未创建;@wenshao A/B 的三条备注;满意后撤销过期的 request-changes 评审。

⏸️ 转交 @wenshao@yiliang114 —— 评审因实质问题重开,不是程序性的。在第 11 轮 Critical 于新头部上解决之前,机器人门禁保持关闭。

Qwen Code · qwen3.8-max

Reviewed at 49b67ebef391e96148828da9845dd740919edfa3 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot has neither a verdict nor a deferral on 371ea3bdce21ac365d0bbd0a52590e60a2ee5aec — no APPROVED, CHANGES_REQUESTED, or COMMENTED review of its own. A DISMISSED one does not count: dismiss_stale_reviews voids the bot's approval on every push, which is exactly when a fresh one is needed. If this re-run was meant to review or approve, it did not, and an approval left by another account is a separate vote that does not count as the bot's own.

⚠️ 机器人在 371ea3bdce21ac365d0bbd0a52590e60a2ee5aec既没有裁决也没有 defer —— 没有属于它自己的 APPROVEDCHANGES_REQUESTEDCOMMENTED 评审。DISMISSED 不算:dismiss_stale_reviews 会在每次推送时作废机器人的批准,而那恰恰是需要一次新批准的时刻。如果这次重跑本应评审或批准,那么它没有做到;而其他账号留下的批准是另一张票,不能算作机器人自己的。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

[codex] Thanks — deferring both non-blocking loose ends to maintainer follow-up. This PR has already exceeded five review rounds, so the current pass is limited to correctness/security/data-loss/regression fixes. The mixed-case work item is intentionally outside this PR’s narrowed scope, so whether Fixes #9488 should become a partial reference is a maintainer issue-tracking decision; removing the now-unused knownLocation option is cleanup rather than a landing blocker and can be handled separately.

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

Copy link
Copy Markdown
Collaborator Author

CI fix update

Item Action Result
Ubuntu capability-envelope integration failure Fixed Added the already-advertised session_storage_conflict_repair capability to the exact baseline expectation. No production code changed.
knownLocation cleanup Deferred Non-blocking cleanup; after more than five review rounds this pass is limited to correctness/security/data-loss/regression fixes.
Fixes #9488 vs partial reference Deferred The mixed-case work item is intentionally outside this PR's narrowed scope; issue-closing policy is left to the maintainers.

Commit: 13ebb8b48efcb3e919f7a83640bd40e8ab03cb48

Verification:

  • independent pre-fix reproduction: deterministic, with session_storage_conflict_repair as the only list difference
  • full repository build: PASS
  • bundle: PASS
  • workspace typecheck: PASS
  • targeted real-daemon integration test: PASS (1/1; 35 skipped)
  • pre-commit Prettier/ESLint and git diff --check: PASS

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — the PR could not be built - workflow run

The PR could not be built because npm ci failed twice in a row before any verification started. This is treated as a PR failure verdict rather than an infrastructure failure.

中文 — 判定:❌ 不通过 · PR 构建失败

由于 npm ci 在验证开始前失败(连续两次),无法构建该 PR。判定为 PR 问题而非基础设施故障;安装日志见下方折叠块。

Install/build log

$ npm ci --prefer-offline --no-audit --progress=false --cache "$RUNNER_TEMP/npm-cache"
npm warn deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
npm warn deprecated prebuild-install@7.1.3: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
npm warn deprecated node-domexception@1.0.0: Use your platform's native DOMException instead
npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm warn deprecated @humanwhocodes/object-schema@2.0.3: Use @eslint/object-schema instead
npm warn deprecated @humanwhocodes/config-array@0.13.0: Use @eslint/config-array instead
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm warn deprecated eslint@8.57.1: This version is no longer supported. Please see https://eslint.org/version-support for other options.

> @qwen-code/qwen-code@0.22.0 postinstall
> patch-package

patch-package 8.0.1
Applying patches...
ink@7.0.3 ✔

> @qwen-code/qwen-code@0.22.0 prepare
> node scripts/prepare.js


> @qwen-code/qwen-code@0.22.0 build
> cross-env NODE_OPTIONS="--max-old-space-size=3072" node scripts/build.js


> @qwen-code/qwen-code@0.22.0 generate
> node scripts/generate-git-commit-info.js


> @qwen-code/qwen-code-core@0.22.0 build
> node ../../scripts/build_package.js

src/core/contentGenerator.test.ts(776,21): error TS2339: Property 'countTokens' does not exist on type 'ContentGenerator'.
node:internal/errors:983
  const err = new Error(message);
              ^

Error: Command failed: tsc --build
    at genericNodeError (node:internal/errors:983:15)
    at wrappedFn (node:internal/errors:537:14)
    at checkExecSyncError (node:child_process:916:11)
    at execSync (node:child_process:988:15)
    at file:///__w/qwen-code/qwen-code/scripts/build_package.js:38:1
    at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:681:26)
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:117:5) {
  status: 1,
  signal: null,
  output: [ null, null, null ],
  pid: 7319,
  stdout: null,
  stderr: null
}

Node.js v22.23.2
npm error Lifecycle script `build` failed with error:
npm error code 1
npm error path /__w/qwen-code/qwen-code/packages/core
npm error workspace @qwen-code/qwen-code-core@0.22.0
npm error location /__w/qwen-code/qwen-code/packages/core
npm error command failed
npm error command sh -c node ../../scripts/build_package.js
node:internal/errors:983
  const err = new Error(message);
              ^

Error: Command failed: npm run build --workspace=packages/core
    at genericNodeError (node:internal/errors:983:15)
    at wrappedFn (node:internal/errors:537:14)
    at checkExecSyncError (node:child_process:916:11)
    at execSync (node:child_process:988:15)
    at file:///__w/qwen-code/qwen-code/scripts/build.js:89:3
    at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:681:26)
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:117:5) {
  status: 1,
  signal: null,
  output: [ null, null, null ],
  pid: 7299,
  stdout: null,
  stderr: null
}

Node.js v22.23.2
prepare: npm run build exited with status 1
npm error code 1
npm error path /__w/qwen-code/qwen-code
npm error command failed
npm error command sh -c node scripts/prepare.js
npm error A complete log of this run can be found in: /__w/_temp/npm-cache/_logs/2026-08-24T14_04_22_254Z-debug-0.log

npm ci failed with exit code 1; retrying once.
$ npm ci --prefer-offline --no-audit --progress=false --cache "$RUNNER_TEMP/npm-cache"
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm warn deprecated eslint@8.57.1: This version is no longer supported. Please see https://eslint.org/version-support for other options.

> @qwen-code/qwen-code@0.22.0 postinstall
> patch-package

patch-package 8.0.1
Applying patches...
ink@7.0.3 ✔

> @qwen-code/qwen-code@0.22.0 prepare
> node scripts/prepare.js


> @qwen-code/qwen-code@0.22.0 build
> cross-env NODE_OPTIONS="--max-old-space-size=3072" node scripts/build.js


> @qwen-code/qwen-code@0.22.0 generate
> node scripts/generate-git-commit-info.js


> @qwen-code/qwen-code-core@0.22.0 build
> node ../../scripts/build_package.js

src/core/contentGenerator.test.ts(776,21): error TS2339: Property 'countTokens' does not exist on type 'ContentGenerator'.
node:internal/errors:983
  const err = new Error(message);
              ^

Error: Command failed: tsc --build
    at genericNodeError (node:internal/errors:983:15)
    at wrappedFn (node:internal/errors:537:14)
    at checkExecSyncError (node:child_process:916:11)
    at execSync (node:child_process:988:15)
    at file:///__w/qwen-code/qwen-code/scripts/build_package.js:38:1
    at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:681:26)
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:117:5) {
  status: 1,
  signal: null,
  output: [ null, null, null ],
  pid: 7562,
  stdout: null,
  stderr: null
}

Node.js v22.23.2
npm error Lifecycle script `build` failed with error:
npm error code 1
npm error path /__w/qwen-code/qwen-code/packages/core
npm error workspace @qwen-code/qwen-code-core@0.22.0
npm error location /__w/qwen-code/qwen-code/packages/core
npm error command failed
npm error command sh -c node ../../scripts/build_package.js
node:internal/errors:983
  const err = new Error(message);
              ^

Error: Command failed: npm run build --workspace=packages/core
    at genericNodeError (node:internal/errors:983:15)
    at wrappedFn (node:internal/errors:537:14)
    at checkExecSyncError (node:child_process:916:11)
    at execSync (node:child_process:988:15)
    at file:///__w/qwen-code/qwen-code/scripts/build.js:89:3
    at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:681:26)
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:117:5) {
  status: 1,
  signal: null,
  output: [ null, null, null ],
  pid: 7542,
  stdout: null,
  stderr: null
}

Node.js v22.23.2
prepare: npm run build exited with status 1
npm error code 1
npm error path /__w/qwen-code/qwen-code
npm error command failed
npm error command sh -c node scripts/prepare.js
npm error A complete log of this run can be found in: /__w/_temp/npm-cache/_logs/2026-08-24T14_06_53_378Z-debug-0.log

npm ci failed with exit code 1 after 2 attempts.

Qwen Code · sandboxed verification

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

11 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • knownLocation dead option on Archive/UnarchiveSessionsOptions — already reported (comment 3836892037), author deferred
  • removeSessions batch assertCanCommit dead switch — already reported (comment 3837907185), author deferred
  • ledger-fence regression test throws one fence too early — already reported (round-9 deferred list, corruption.test.ts:1016)
  • maintenance lease cannot take over a sealed writer lock / takeoverPolicy undeclared — resolved as R1-2 (documented intentional fail-closed) and already reported as R1-6 (comment 3836892042), author deferred
  • FIFO test wall-clock flake risk — same test already reported as R5-8 (comment 3838888962), author deferred
  • findOrdinarySessions double scan on the mixed-batch not-found path — already reported (round-8 deferred list, routes/session.ts:2122)
  • enable/removeTasksForSessions assertCanCommit forwarding untested — already reported (comment 3837907184), author deferred
  • resolveConflicts untested on workspace-qualified REST routes — already reported as R1-12 (comment 3836892055), author deferred
  • integration-tests capability assertion unreachable from workspace suites — already reported as R5-9 (comment 3838888965), author deferred; the gate is failing at the reviewed head (branch lag per maintainer analysis)
  • over-window non-object-prefix heads remain maintainable by documented design (foreign-head residual) — adjudicated in the R6-1 cluster (comment 3839400922); making all over-window heads fail closed would regress the resolved blocker at comm…
  • fence trip after the point of no return orphans sidecars/org entry (delete retry does not repair) — already reported (comment 3838367757), author deferred: safe healing needs already-state reconciliation semantics

Not reviewed: build-and-test — the changed integration-tests assertion is collected only by the Integration Tests (CLI, No Sandbox) gate, which is FAILING at the reviewed head (missing typecheck:integration script — branch lag per maintainer analysis); the scoped workspace run could not collect it. Test efficacy also unvalidated (harness control never ran; mutants and hunk probes unprobed).

Not reviewed: reverse audit — stopped before round 1 by the review time budget.

Deferred under the convergence posture (round 10, not a blocker) — recorded, not requested in this round:

  • packages/core/src/services/usageHistoryService.ts:314 — [probe] commit silently drops salvage on unreadable usage history, inverting the documented write-anyway policy; transcript deletion makes the loss permanent

[Critical] R1-4: Still stands — carried since round 1; re-posted by the parallel round-9 review; the mechanism is present at this byte-identical head. The final identity assertion validates a pathname, but the subsequent unlink operates on that pathname without being bound to the validated inode: in removeSessionFiles (packages/core/src/services/sessionService.ts), assertMaintainableSessionUnchanged lstats the path and removeFileIfExists then unlinks that same path — a concurrent process can swap in an unvalidated replacement between the final stat and the mutation, and delete/archive/unarchive destroys a file no identity check ever validated. Witness: round-8 scratch-tree probe (applicable unchanged — the file is byte-identical at this head): removeSession returned true while the swapped-in file's inode differed from the validated identity and its content did not survive. The author's wontfix rationale (a correct fix needs a cross-platform atomic claim/no-replace protocol plus crash recovery) is substantive, but only code retires a blocker; the maintainer asked that this residual be tracked in a follow-up issue (review 5006725473). Fix direction: atomically claim the source into a private staging path (rename), verify the staged inode, then delete or publish — or land the documented diagnostic and file the follow-up before merge.

中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 11 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查:build-and-test — the changed integration-tests assertion is collected only by the Integration Tests (CLI, No Sandbox) gate, which is FAILING at the reviewed head (missing typecheck:integration script — branch lag per maintainer analysis); the scoped workspace run could not collect it. Test efficacy also unvalidated (harness control never ran; mutants and hunk probes unprobed)。

未审查:反向审计——评审时间预算不足,未能开始第 1 轮。

收敛姿态下延后(第 10 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。

[Critical] R1-4: Still stands — carried since round 1; re-posted by the parallel round-9 review; the mechanism is present at this byte-identical head. The final identity assertion validates a pathname, but the subsequent unlink operates on that pathname without being bound to the validated inode: in removeSessionFiles (packages/core/src/services/sessionService.ts), assertMaintainableSessionUnchanged lstats the path and removeFileIfExists then unlinks that same path — a concurrent process can swap in an unvalidated replacement between the final stat and the mutation, and delete/archive/unarchive destroys a file no identity check ever validated. Witness: round-8 scratch-tree probe (applicable unchanged — the file is byte-identical at this head): removeSession returned true while the swapped-in file's inode differed from the validated identity and its content did not survive. The author's wontfix rationale (a correct fix needs a cross-platform atomic claim/no-replace protocol plus crash recovery) is substantive, but only code retires a blocker; the maintainer asked that this residual be tracked in a follow-up issue (review 5006725473). Fix direction: atomically claim the source into a private staging path (rename), verify the staged inode, then delete or publish — or land the documented diagnostic and file the follow-up before merge.

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

Comment thread packages/core/src/services/sessionService.ts Outdated
```

`errors` entries have `{ "sessionId": "<uuid>", "error": "message" }`. Active and archived files with the same id are treated as a conflict and reported in `errors`; no file is overwritten.
`resolveConflicts` is optional and defaults to `false`. By default, active and archived files with the same id are reported in `errors`, and neither copy is moved, removed, or overwritten. Archiving a live session still performs the strict close described above before classifying the conflict, so that close may flush queued records to the active transcript. With `resolveConflicts: true`, archive keeps the archived copy, removes the active copy, and reports the id in both `archived` and `resolvedConflicts`. `errors` entries have `{ "sessionId": "<uuid>", "error": "message" }`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R9-2: Still stands — the head is unchanged since this finding was verified (60118c3fe0). This sentence (and its unarchive mirror below) states unconditionally that resolveConflicts: true repairs a conflict, but for mixed local/foreign archive states — a state this PR itself defines, classifies, and tests — classification throws ambiguous_project before any repair branch, regardless of the flag. The id lands in errors as the redacted 'Session operation failed.' with both copies untouched; the PR's own test 'does not fall back to primary for mixed foreign and local internal storage' posts resolveConflicts: true for exactly this shape and asserts that outcome. 08-session-lifecycle.md carries the fail-closed hedge; this reference, which integrators build clients against, omits it in both sections. A client built on this prose treats the permanent, operator-intervention-required state as a transient failed repair and retries forever, and the redacted message gives no hint that retrying cannot help. Witness: the pinned test passes at this head with archived: [], the redacted error, and both transcripts byte-identical. Add the hedge to both sections, mirroring 08-session-lifecycle.md: ownership-safety checks still fail closed with this option — mixed local/foreign states are reported in errors and neither copy is moved, with or without resolveConflicts.

中文说明

R9-2:仍然存在——自本发现被验证以来 head 未变(60118c3fe0)。该句(以及下方 unarchive 一节的对应句)无条件声称 resolveConflicts: true 会修复冲突;但对于 local/foreign 混合归档状态——本 PR 自己定义、分类并测试了的状态——分类会在任何修复分支之前抛出 ambiguous_project,与该选项无关。该 id 会落入 errors,错误信息为脱敏后的 'Session operation failed.',两份副本均不被移动;本 PR 自己的测试 'does not fall back to primary for mixed foreign and local internal storage' 正是对这种形态发送 resolveConflicts: true 并断言了该结果。08-session-lifecycle.md 写有 fail-closed 限定,而集成方据以开发客户端的本协议参考文档在两处小节中都遗漏了该限定。按此措辞实现的客户端会把这种需要人工介入的永久状态当作可重试的临时失败而无限重试,且脱敏错误信息没有给出任何"重试无用"的线索。证据:上述测试在本 head 通过,断言 archived: []、脱敏错误、两份副本逐字节不变。建议在两个小节都补上限定,与 08-session-lifecycle.md 一致:该选项下所有权安全检查仍会失败关闭——local/foreign 混合状态会进入 errors,无论是否携带 resolveConflicts,两份副本都不会被移动。

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Deferring after independent triage: this is a documentation clarification, not a Critical correctness or data-loss defect. Runtime behavior already fails closed and is covered by the mixed local/foreign ownership tests. This PR is in review round 10 and is restricted to Critical fixes, so the prose clarification should be handled separately.

Comment thread packages/core/src/services/sessionService.ts Outdated
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Synced the PR branch with the latest origin/main in merge commit 1b2c9f3ab71d54e7da59677d56cd2168b8091a63 (no conflicts; no history rewrite).

Item Result
Main sync / prior missing typecheck:integration precheck Fixed by inheriting the current main workflow and script
PR commit stack and effective diff Preserved exactly; range-diff and aggregate numstat are unchanged
Validation Build + bundle, full typecheck, lint, Prettier check, Core 97/97, CLI 550/550, SDK 349/349 passed
No-AK integration gate 128 passed / 21 failed; the same isolated tool-control.test.ts baseline on clean origin/main is also 21 failed / 9 passed with identical empty-tool-call failures, so this is inherited from main/test environment rather than PR #9626

No review threads were addressed by this main-sync commit (0/0 to resolve).

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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

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

Scripted assertions: 47 passed · 0 failed · 47 total

Flakiness gate: ⚠️ timeout — only 4 of 5 rounds fit the 15-minute budget; the completed rounds agreed

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

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

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

抖动门:⚠️ timeout — only 4 of 5 rounds fit the 15-minute budget; the completed rounds agreed

Verification report

Sandboxed verification: ✅ merge-ready — 47/47 scripted assertions passed, verified head 49b67ebef391e96148828da9845dd740919edfa3 (PR #9626, merge base 1fffa5108d).

中文摘要
  • 判定:merge-ready。47/47 条脚本化断言通过,无阻塞性发现。
  • A/B 结论(head 对 base,真实编译产物 + 真实文件系统 + 真实 HTTP):
    • 核心层(core-lifecycle-ab.mjs,head 18/18 vs base 15/15,见 01-core-ab-head-vs-base.png):空、撕裂头 transcript 在 base 上 delete 返回 not-found 且文件滞留、archive/unarchive 报 notFound;head 上全部可删除/按原字节移动。冲突默认行为两臂一致(报错且两份文件不动);resolveConflicts: true 在 base 被忽略、在 head 正确保留应保留的一份。符号链接/FIFO 在 head 以类型化 non_regular 错误 fail closed(不跟随、不阻塞),base 会跟随符号链接并在 FIFO 上阻塞 8s+。外部 workspace 两臂均 not-found 且文件不动。
    • 路由层(route-lifecycle-ab.mjs,两臂各 6/6,见 02-route-ab-orphan-flip.png):legacy 孤儿会话(父会话不存在)在 base 上 qualified 路由 404、unqualified 路由被误路由到 primary 并报 notFound(文件滞留);head 上两族路由均 200 且文件被删除/归档。workspace-qualified 冲突归档在 base 返回 HTTP 409 session_conflict envelope,在 head 改为 HTTP 200 + 每会话错误(与 PR 描述的破坏性变更一致,错误信息保留)。
  • Vacuity:把分类器回退为“不可读头 ⇒ 不存在”后,16 个新 lifecycle 测试以行为断言失败(如 expected false to be true),恢复后 50/50 绿(03-vacuity-mutation-kills-lifecycle-tests.png)。孤儿用例在该突变下仍绿——孤儿的门在路由层,已被路由 A/B 与 multi-workspace 套件覆盖。
  • 门禁:core corruption+usage 97/97、core sessionService 191/191、cli session-archive+scheduled-task 69/69、cli multi-workspace 143/143、cli transport+server 1424 通过 0 失败、acp-bridge attachments 47/47、SDK DaemonClient 349/349。
  • 未覆盖:每 commit 归因(shallow clone)、Windows 行为、TOCTOU 重校验的独立竞态驱动、全仓库套件与 typecheck(交由 CI)。

Previous-round status

The previous round's file was a build-failure notice, not a substantive reportnpm ci failed twice on src/core/contentGenerator.test.ts(776,21): error TS2339: Property 'countTokens' does not exist, before any verification ran. There were no findings to carry forward.

# Previous notice Status at new head
1 npm ci build failure (TS2339 countTokens in contentGenerator.test.ts) fixed — the container's npm ci + npm run build completed green at this head before verification started, and my own full --cli-only build of both arms compiled with 0 TS errors (base-build.log). The file the error named is not touched by this PR.

Central claim and A/B proof

Central claim: delete/archive/unarchive maintain owned persisted transcripts that are empty, torn-headed, or legacy orphans — classification is by physical storage ownership, not conversation loadability.

Secondary claims: (1) default conflict behavior stays non-mutating, resolveConflicts: true repairs keep the correct copy, and workspace-qualified conflict responses move from HTTP 409 to per-session errors in a 200; (2) fences hold: symlink/directory/FIFO fail closed without following or blocking, foreign-workspace stays not-found.

Core-level A/B (core-lifecycle-ab.mjs, compiled SessionService, real fs)

Witness: 01-core-ab-head-vs-base.png. Base arm ran against a HEAD^1 worktree whose @qwen-code/* resolution was verified to point into the base tree (ESM import probe: getMaintainableSessionLocation undefined on base core); core has no internal @qwen-code deps and the PR leaves the lockfile untouched, so the control is clean.

Cell base (1fffa5108d) head (49b67ebe) flip
delete healthy (control) removed, file gone removed, file gone — (parity)
delete empty removed:false, stranded removed:true, gone
delete torn head removed:false, stranded removed:true, gone
delete identity-less {} head refused, intact refused (unknown_project), intact parity, fail-closed
delete 1.5 MiB unterminated JSON line not-found, intact refused (SessionTranscriptIdentityUnavailableError), intact parity, fail-closed
archive empty notFound, unmoved archived, 0 bytes moved verbatim
unarchive torn notFound, unmoved unarchived, bytes verbatim
archive conflict, default error, both copies byte-identical error, both copies byte-identical parity
archive conflict, resolveConflicts:true option ignored, conflict stands archived copy kept, active removed, resolvedConflicts set ✅ additive
unarchive conflict, resolveConflicts:true option ignored active kept, archived removed ✅ additive
delete conflict both copies removed both copies removed parity (compat)
symlink entry followed: link removed (target intact) typed non_regular, link+target untouched ✅ fail-closed
directory entry tolerant not-found, intact typed non_regular, intact parity on safety
FIFO entry blocked (8 s budget, SIGTERM-unkillable open) typed non_regular in 0 ms ✅ fail-closed
foreign-workspace head not-found, untouched not-found, untouched parity (fence held)

Head 18/18, base 15/15 — every base cell failed exactly as the PR's premise predicts.

Route-level A/B (route-lifecycle-ab.mjs, compiled createServeApp on a real loopback listener, ordinary primary + internal Conversations secondary)

Witness: 02-route-ab-orphan-flip.png. The ACP bridge is a call-logging fake honoring the bridge contract (getSessionSummary throws SessionNotFoundError for non-live sessions); routes, registry, SessionService, and filesystem are the real compiled code.

Cell base head
orphan, qualified delete 404 session_not_found, file stranded 200, removed, file gone
orphan, unqualified delete 200 but notFound (misrouted to primary), file stranded 200, removed via internal owner
orphan, qualified archive 404, unmoved 200, archived, bytes verbatim
healthy internal delete (control) 200 removed 200 removed
foreign, qualified delete 404, untouched 404, untouched
conflict, qualified archive default HTTP 409 session_conflict envelope, both preserved HTTP 200 + per-session error naming resolveConflicts, both preserved

6/6 both arms. The base unqualified-orphan cell is the sharpest: the file existed, was owned, and was readable — yet the daemon reported not-found after silently falling back to the primary runtime. The 409→200 envelope change matches the PR's stated breaking change, and the conflict reason survives in the per-session error (exempt from redaction).

Vacuity / mutation matrix (03-vacuity-mutation-kills-lifecycle-tests.png)

Mutation: in readMaintainableSessionIdentity, unreadable head ⇒ state absent (exactly the base's loadability dependence), interface preserved.

Guard Suite Result
unreadable regular files still occupy their state (classifier) sessionService.corruption.test.ts lifecycle block killed — 16 tests fail on behavioral assertions (expected false to be true on removeSession(...).resolves.toBe(true), expected [] to have a length of 1, …); restore → 50/50 green
orphan classification at core level same block survived by design: an orphan has a readable head, so the core classifier was never its gate — the gate is the route resolver's loadability check, pinned instead by the route A/B above and the multi-workspace suite (143/143). Coverage distribution, not a gap

The orphan test staying green under the classifier mutation is the evidence the two layers are independently pinned.

Findings

No blocking findings. Non-blocking observations:

  1. Directory entries change response shape, not safety. Base reported a directory in the chats dir as not-found; head reports a typed per-session error (non_regular). Both leave the directory and its contents untouched (asserted). Clients that treated "not in notFound and not in errors" as absent should note the entry now lands in errors.
  2. Base hazards documented by the control cells (not PR defects): base follows symlinks during classification and blocks indefinitely opening a FIFO — the base arm's FIFO cell required SIGKILL because the blocked open() ignores SIGTERM. Head's O_NOFOLLOW|O_NONBLOCK open fixes both.
  3. Fail-closed shapes for unclassifiable heads ({} first record → unknown_project; >1 MiB unterminated JSON-looking line → SessionTranscriptIdentityUnavailableError) are deliberate and storage-safe; both refuse without mutating.

Not covered

  • Per-commit attribution: shallow depth-2 checkout (18 commits in metadata, only the merge triple reachable); the aggregate HEAD^1..HEAD diff was verified instead.
  • Windows: the fence skips O_NOFOLLOW|O_NONBLOCK on win32; not exercised here (Linux container).
  • TOCTOU revalidation (assertMaintainableSessionUnchanged): not driven by an independent race harness; pinned by the PR's own tests ("replaced after validation", "disappears and reappears during classification", "in-place rewrite during ownership classification") — all green.
  • ACP transport / SDK wire behavior: covered by their suites (cli transport+server 1424 passed 0 failed; SDK 349/349), no independent wire A/B.
  • Full-repo suite and typecheck: left to the PR's CI; targeted workspaces only were run here.
  • Base-arm teardown artifact: the base core harness hangs after printing all results (blocked FIFO open in the tolerant reader); runs were wrapped in timeout -k and all output was captured before the kill. Harness-level, not PR code.

Methodology

Environment: node:22-bookworm container, merge-ref checkout (HEAD = merge, HEAD^1 = base 1fffa5108d, HEAD^2 = verified head 49b67ebe), npm ci + npm run build pre-completed at HEAD. Core A/B drove packages/core/dist SessionService directly; the base arm used a tmp/base-tree worktree rebuilt with scripts/build.js --cli-only (nested per-workspace node_modules mirrored from the lockfile layout; @qwen-code/* overlay symlinks repointed into the base tree and verified by ESM import probe). Route A/B booted each tree's compiled createServeApp on a real HTTP listener with the repo's two-workspace registry topology. Raw per-arm JSON logs, harness scripts, and base-build.log live in this artifact directory; images were produced with scripts/verify-capture.mjs.

Flakiness gate log

integration test, out of gate scope: integration-tests/cli/qwen-serve-routes.test.ts
rounds=5 files=11 skipped=1
file packages/acp-bridge/src/sessionAttachments.test.ts: (cd packages/acp-bridge) npx --no-install vitest run ./src/sessionAttachments.test.ts
file packages/cli/src/serve/acp-http/transport.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/acp-http/transport.test.ts
file packages/cli/src/serve/multi-workspace-sessions.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/multi-workspace-sessions.test.ts
file packages/cli/src/serve/scheduled-task-session-lifecycle.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/scheduled-task-session-lifecycle.test.ts
file packages/cli/src/serve/server.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/server.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/services/session-organization-service.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/session-organization-service.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
file packages/core/src/services/usageHistoryService.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/usageHistoryService.test.ts
file packages/sdk-typescript/test/unit/DaemonClient.test.ts: (cd packages/sdk-typescript) npx --no-install vitest run ./test/unit/DaemonClient.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/acp-bridge/src/sessionAttachments.test.ts: PPPPP
  packages/cli/src/serve/acp-http/transport.test.ts: PPPPP
  packages/cli/src/serve/multi-workspace-sessions.test.ts: PPPPP
  packages/cli/src/serve/scheduled-task-session-lifecycle.test.ts: PPPP
  packages/cli/src/serve/server.test.ts: PPPP
  packages/cli/src/serve/server/session-archive.test.ts: PPPP
  packages/core/src/services/session-organization-service.test.ts: PPPP
  packages/core/src/services/sessionService.corruption.test.ts: PPPP
  packages/core/src/services/sessionService.test.ts: PPPP
  packages/core/src/services/usageHistoryService.test.ts: PPPP
  packages/sdk-typescript/test/unit/DaemonClient.test.ts: PPPP

verdict: timeout
summary: only 4 of 5 rounds fit the 15-minute budget; the completed rounds agreed

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/acp-bridge/src/sessionAttachments.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/acp-http/transport.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/multi-workspace-sessions.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/scheduled-task-session-lifecycle.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/server.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/server/session-archive.test.ts: P (exit 0)
round 1 · packages/core/src/services/session-organization-service.test.ts: P (exit 0)
round 1 · packages/core/src/services/sessionService.corruption.test.ts: P (exit 0)
round 1 · packages/core/src/services/sessionService.test.ts: P (exit 0)
round 1 · packages/core/src/services/usageHistoryService.test.ts: P (exit 0)
round 1 · packages/sdk-typescript/test/unit/DaemonClient.test.ts: P (exit 0)
round 2 · packages/acp-bridge/src/sessionAttachments.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/acp-http/transport.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/multi-workspace-sessions.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/scheduled-task-session-lifecycle.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/server.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/server/session-archive.test.ts: P (exit 0)
round 2 · packages/core/src/services/session-organization-service.test.ts: P (exit 0)
round 2 · packages/core/src/services/sessionService.corruption.test.ts: P (exit 0)
round 2 · packages/core/src/services/sessionService.test.ts: P (exit 0)
round 2 · packages/core/src/services/usageHistoryService.test.ts: P (exit 0)
round 2 · packages/sdk-typescript/test/unit/DaemonClient.test.ts: P (exit 0)
round 3 · packages/acp-bridge/src/sessionAttachments.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/acp-http/transport.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/multi-workspace-sessions.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/scheduled-task-session-lifecycle.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/server.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/server/session-archive.test.ts: P (exit 0)
round 3 · packages/core/src/services/session-organization-service.test.ts: P (exit 0)
round 3 · packages/core/src/services/sessionService.corruption.test.ts: P (exit 0)
round 3 · packages/core/src/services/sessionService.test.ts: P (exit 0)
round 3 · packages/core/src/services/usageHistoryService.test.ts: P (exit 0)
round 3 · packages/sdk-typescript/test/unit/DaemonClient.test.ts: P (exit 0)
round 4 · packages/acp-bridge/src/sessionAttachments.test.ts: P (exit 0)
round 4 · packages/cli/src/serve/acp-http/transport.test.ts: P (exit 0)
round 4 · packages/cli/src/serve/multi-workspace-sessions.test.ts: P (exit 0)
round 4 · packages/cli/src/serve/scheduled-task-session-lifecycle.test.ts: P (exit 0)
round 4 · packages/cli/src/serve/server.test.ts: P (exit 0)
round 4 · packages/cli/src/serve/server/session-archive.test.ts: P (exit 0)
round 4 · packages/core/src/services/session-organization-service.test.ts: P (exit 0)
round 4 · packages/core/src/services/sessionService.corruption.test.ts: P (exit 0)
round 4 · packages/core/src/services/sessionService.test.ts: P (exit 0)
round 4 · packages/core/src/services/usageHistoryService.test.ts: P (exit 0)
round 4 · packages/sdk-typescript/test/unit/DaemonClient.test.ts: P (exit 0)
round 5 · packages/acp-bridge/src/sessionAttachments.test.ts: P (exit 0)
round 5 · packages/cli/src/serve/acp-http/transport.test.ts: P (exit 0)
round 5 · packages/cli/src/serve/multi-workspace-sessions.test.ts: P (exit 0)

Evidence images

01-core-ab-head-vs-base

02-route-ab-orphan-flip

03-vacuity-mutation-kills-lifecycle-tests

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

Qwen Code · sandboxed verification

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
doudouOUC and others added 2 commits August 25, 2026 00:17
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot has neither a verdict nor a deferral on 21da77c6450bf6eafec06b125bf95251e9574a5a — no APPROVED, CHANGES_REQUESTED, or COMMENTED review of its own. A DISMISSED one does not count: dismiss_stale_reviews voids the bot's approval on every push, which is exactly when a fresh one is needed. If this re-run was meant to review or approve, it did not, and an approval left by another account is a separate vote that does not count as the bot's own.

⚠️ 机器人在 21da77c6450bf6eafec06b125bf95251e9574a5a既没有裁决也没有 defer —— 没有属于它自己的 APPROVEDCHANGES_REQUESTEDCOMMENTED 评审。DISMISSED 不算:dismiss_stale_reviews 会在每次推送时作废机器人的批准,而那恰恰是需要一次新批准的时刻。如果这次重跑本应评审或批准,那么它没有做到;而其他账号留下的批准是另一张票,不能算作机器人自己的。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Latest main sync and review fixes are now on the branch. Current head: 49b67ebef3.

Item Decision / handling
Latest origin/main Merged through 1fffa5108d with no conflicts. The original PR commits remain intact.
Retained-copy usage snapshot (R9-3 and duplicate) Fixed in 21da77c645: conflict repair no longer writes a final usage snapshot that can shadow later transcript growth.
Partial multi-copy deletion Fixed in 782dd86dfa: each prepared usage record is committed immediately after its transcript unlink succeeds, so a later unlink failure cannot discard usage for an already-deleted copy.
Losing-copy fallback (R9-1) Partially addressed by removing unsafe conflict-repair snapshots. Rejected the proposed fallback because both copies share one sessionId and a persisted losing-copy snapshot would permanently shadow later growth in the retained copy; safe merging is outside this PR's deterministic keep-destination scope.
Protocol prose hedge (R9-2) Deferred as a non-Critical documentation clarification under the round-10 Critical-only policy. Runtime mixed local/foreign ownership already fails closed and is tested.

Validation on the final tree:

  • npm run build, npm run typecheck, and npm run lint passed.
  • Prettier and git diff --check passed for the effective PR diff.
  • Core lifecycle/usage suites: 288 passed.
  • ACP bridge suite after the latest-main merge: 779 passed.
  • The new partial-deletion regression also passed independently without source changes.
  • The remaining no-AK integration failure family is the model producing no tool calls in tool-control.test.ts; the same assertions reproduce on clean current main, so it is not caused by this PR.

No product decision is needed for the landed Critical fixes.

wenshao added a commit that referenced this pull request Aug 24, 2026
@wenshao

wenshao commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Re-verification at 21da77c6 — rebuilt both arms from scratch

Following up on my earlier report. The PR moved: one new commit plus a merge of latest main, which shifted the merge-base by 106 commits. I rebuilt both arms from the new commits and re-ran the whole harness rather than trusting the old base.

previous run this run
base 867ded5 65c2bb01 (new merge-base)
head 60118c3 21da77c6 (PR tip)

Verdict unchanged: this is still good to merge, and the new commit is a correct narrowing. One item from my earlier report is now resolved by evidence rather than left open.


The lifecycle matrix is bit-for-bit stable across the rebase

lifecycle A/B matrix

I diffed all 25 REST cases four ways (old base / new base / old head / new head). After normalising for fixture path length, which shifted my transcript sizes by one byte:

  • base 867ded5 vs base 65c2bb01: identical on every case. main's 106 commits touched none of the lifecycle files, so none of my "base is broken here" claims were an artifact of a stale base.
  • head 60118c3 vs head 21da77c6: identical on every case. The new commit changes no lifecycle route behaviour.

The ACP transport probe (6 cases) and the FIFO availability demo reproduce exactly as before — base still hangs and burns libuv threadpool threads until all filesystem I/O on the daemon stalls; the PR tip fails closed in ~0.05 s.

FIFO availability

conflict repair

The new commit: dropping usage salvage from conflict repair

21da77c6 removes the prepareUsageBeforeTranscriptDeletion / commit… pair from both conflict-repair branches — the ones that salvaged the retained copy. I measured what that actually does to a session's usage, reading the aggregate through the same code path as GET /usage/dashboard (in a fresh process each time, because the route caches for 60 s).

usage salvage

The control row is what settles it. Plain archiving of an ordinary healthy session already takes its usage to 0 on all three arms — rebuildFromSessionJsonl does a flat readdir of chats/ and never descends into chats/archive/. So archive-repair going 1000 → 0 on the new tip is archive-repair behaving exactly like ordinary archive, not a regression this PR introduces.

What the previous tip did was worse in two ways at once: on archive-repair it reported 7 — the retained archived copy's tokens, having silently discarded the deleted active copy's 1000 — and on unarchive-repair it reached the right total only by pinning a frozen final snapshot into usage_record.jsonl for a session whose transcript is still on disk and still growing. Persisted records win over live replay in loadUsageHistoryWithLive, so that snapshot would have shadowed the session's real usage forever if it were later resumed. The new tip replays the live transcript instead and reaches the same total.

delete still salvages correctly on every arm (pinned=1000), so the #7384 guarantee is intact where it belongs.

So: the change is right, and it makes the repair paths consistent with the archive semantics that already existed. The residual oddity — archived transcripts being invisible to the usage aggregate — is pre-existing, affects ordinary archive equally, and is not this PR's to fix.

Tests

All focused suites pass at 21da77c6, including server.test.ts this round:

suite tests
Core (sessionService, .corruption, usageHistoryService, session-organization-service) 312
CLI (session-archive, multi-workspace-sessions, acp-http/transport, scheduled-task-session-lifecycle, server) 1636
SDK (DaemonClient) 349
acp-bridge (sessionAttachments) 47

Core is up 2 from my last run — exactly the two new tests asserting no salvage happens during repair.


Status of my earlier three points

a) The "breaking change" note still over-states its blast radius. Unchanged at 21da77c6: the target.kind === 'ordinary' early return is still there (now routes/session.ts:1189), so the 409 session_conflict throw only ever fires for internal Conversations runtimes. On the new base an ordinary workspace still returns 200 + errors[]. The migration note should say so — ordinary-workspace clients need to do nothing.

b) The DoS vector is still narrowed, not closed. The session-list route still hangs on a FIFO on both new arms. Pre-existing, not a regression, out of this PR's scope — but worth a follow-up issue.

c) Repaired storage is still undiscoverable. Empty and torn transcripts still do not appear in GET /workspace/:id/sessions on either arm.

Still not independently reproduced in the live stack (unchanged from last time, covered by the passing unit suites): the writer-lease-occupied fence, runtime-generation replacement, and ambiguous internal/ordinary ownership. macOS only.

Evidence for this run is on pr-assets/9626-verify under maintainer-local-verify/2026-08-25/ — wire ledgers, ACP frames, and the usage measurements for all three arms.

中文说明

21da77c6 上重新验证 —— 两条 arm 全部重新构建

上一份报告。PR 更新了:一个新 commit 加上合并了最新 main,merge-base 往前挪了 106 个提交。我把两条 arm 都按新提交重新构建并重跑了整套 harness,没有沿用旧的 base。

上次 本次
base 867ded5 65c2bb01(新 merge-base)
head 60118c3 21da77c6(PR 顶端)

结论不变:仍然可以合,而且新 commit 是一次正确的收窄。 上一份报告里悬着的一项,这次有了实测证据。


生命周期矩阵在 rebase 前后逐位稳定

见上方第一张截图。我把 25 条 REST 用例做了四向比对(旧 base / 新 base / 旧 head / 新 head)。在扣除夹具路径长度导致的 1 字节差异后:

  • base 867ded5 与 base 65c2bb01:所有用例完全一致。 main 的 106 个提交没有碰到任何生命周期文件,所以我之前那些「base 在这里是坏的」的结论,不是陈旧 base 造成的假象。
  • head 60118c3 与 head 21da77c6:所有用例完全一致。 新 commit 没有改变任何生命周期路由行为。

ACP 传输探针(6 条用例)和 FIFO 可用性演示复现结果与之前完全一致 —— base 仍然挂起并耗尽 libuv 线程池,直到 daemon 上所有文件系统 I/O 停摆;PR 顶端仍然在约 0.05 秒内 fail closed。

新 commit:从冲突修复路径里去掉 usage salvage

21da77c6 把两个冲突修复分支里的 prepareUsageBeforeTranscriptDeletion / commit… 去掉了 —— 那两处 salvage 的是被保留的那份副本。我实测了这对会话的用量意味着什么:走的是和 GET /usage/dashboard 完全相同的代码路径(每次开新进程读,因为该路由有 60 秒缓存)。

见上方第四张截图。

对照行是定性的关键。 普通健康会话的常规归档,在三条 arm 上本来就会把用量打到 0 —— rebuildFromSessionJsonlchats/ 做的是平铺 readdir,从不进入 chats/archive/。所以新顶端上归档修复的 1000 → 0,是归档修复表现得和普通归档完全一样,不是本 PR 引入的回归。

之前那版顶端其实同时错了两处:归档修复时它报的是 7,即保留下来的 archived 副本的 token 数,而被删掉的 active 副本那 1000 被静默丢弃了;恢复修复时它虽然拿到了正确的总数,但靠的是往 usage_record.jsonl钉进一份冻结的最终快照——而那个会话的 transcript 还在磁盘上、还会继续增长。在 loadUsageHistoryWithLive 里持久化记录优先级高于实时回放,所以一旦该会话之后被恢复使用,那份快照会永久遮蔽它的真实用量。新顶端改为回放实时 transcript,拿到的是同一个总数。

delete 在所有 arm 上仍然正确 salvage(pinned=1000),#7384 的保证在该生效的地方完好无损。

所以:这个改动是对的,并且让修复路径与既有的归档语义保持了一致。剩下那点别扭 —— 归档后的 transcript 对用量聚合不可见 —— 是既有问题,普通归档同样如此,不该由本 PR 来修。

测试

21da77c6 上所有聚焦套件全绿,这轮还带上了 server.test.ts

套件 数量
Core(sessionService.corruptionusageHistoryServicesession-organization-service 312
CLI(session-archivemulti-workspace-sessionsacp-http/transportscheduled-task-session-lifecycleserver 1636
SDK(DaemonClient 349
acp-bridge(sessionAttachments 47

Core 比上次多 2 项,正好是新增的那两条「修复时不做 salvage」的断言。


上一份报告三点的现状

a)「破坏性变更」说明仍然夸大了影响面。21da77c6 上没有变化:target.kind === 'ordinary' 的提前 return 仍在(现在是 routes/session.ts:1189),所以 409 session_conflict 只会对内部 Conversations runtime 触发。在新 base 上普通 workspace 依然返回 200 + errors[]。迁移说明应当写明这一点 —— 普通 workspace 的客户端什么都不用改。

b)DoS 面仍然是收窄而非关闭。 会话列表路由在两条新 arm 上仍会被 FIFO 挂住。既有问题、非回归、不在本 PR 范围内 —— 但值得开个 follow-up issue。

c)修好的存储仍然发现不了。 空文件和撕裂的 transcript 在两条 arm 的 GET /workspace/:id/sessions 里依然不出现。

仍然没能在真实栈里独立复现的部分(与上次相同,由通过的单测覆盖):writer lease 占用围栏、运行时代际替换、ordinary/internal 所有权歧义。仅测了 macOS。

本轮证据在 pr-assets/9626-verifymaintainer-local-verify/2026-08-25/ 下 —— 线协议台账、ACP 帧,以及三条 arm 的用量测量数据。

@wenshao

wenshao commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Triage re-run completed without submitting a new review.

Head unchanged since the last attested pass (49b67ebef391e96148828da9845dd740919edfa3). What moved is the review record: the /review lane's round 11 posted two new Critical findings on this head after the last pass (R11-1 org-entry leak on a post-deletion fence trip, R11-2 permanent sidecar leak on a mid-cleanup fence trip). This pass verified both mechanisms in the head code — details in the updated Stage 2 comment — and Stage 3 drops to 2/5.

No new review submitted: the bot's round-11 CHANGES_REQUESTED already stands on this exact head and gates the PR; stacking another would be noise. No approval either — the open Criticals and the standing Stage 0 core-size escalation both forbid it. A sponsored /verify run pinned to this head is in flight; its report will post separately in this thread.

重跑完成,未提交新评审。头部与上次通过时相同(49b67ebef391e96148828da9845dd740919edfa3);变化的是评审记录:/review 环节第 11 轮在上次通过之后对同一头部新增两个 Critical(R11-1 删除后栅栏中断致组织条目泄漏、R11-2 清理中栅栏中断致 sidecar 永久泄漏)。本次已在头部代码中逐一核实机制(详见 Stage 2),Stage 3 降至 2/5。不叠加新评审——第 11 轮的 CHANGES_REQUESTED 已立于本头部并构成门禁;也不批准——未决 Critical 与 Stage 0 核心规模升级均不允许。钉在本头部的代跑 /verify 正在运行,报告将另行发布在本线程。

Qwen Code · qwen3.8-max

Reviewed at 49b67ebef391e96148828da9845dd740919edfa3 · re-run with @qwen-code /triage

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI; the changed integration file was run directly in the review (36/36 passed), the full integration suite was not run.

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI; the workspace suites ran on Linux only.

Not reviewed: chunk 4 — the new multi-workspace lifecycle tests could not be executed during the fan-out (no installed dependencies in the worktree at that point); they were covered afterwards by Agent 7's full suite run (green) — stopped at the agent tool budget.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 4)": novelty/intent check for resolveSessionBatchRuntime's whole-request SessionNotFoundError when an owner-routed batch mixes a known session with an id that has no…; chunk 4: could not execute the new tests — the shared worktree has no installed dependencies or built workspace packages, so vitest cannot run without a full install/bui….

Not reviewed: reverse audit — stopped before round 5 by the review time budget.

Deferred under the convergence posture (round 11, not a blocker) — recorded, not requested in this round:

  • packages/core/src/services/sessionService.ts:2365 — [probe] Conflict-repair fence trip after the losing-transcript unlink strands loser-state sidecars; retry masks the loss
  • packages/cli/src/serve/server/session-archive.test.ts:825 — [probe] Conflict repair's losing-side worktree/ledger sidecar deletions are pinned by no test
  • packages/acp-bridge/src/sessionAttachments.test.ts:766 — [probe] Attachment-store fence test pins only the first of three assertCanCommit sites
  • packages/core/src/services/sessionService.test.ts:2709 — [review] Unarchive fence trip after the rename strands sidecars in chats/archive/; alreadyActive retry masks it
  • packages/core/src/services/sessionService.test.ts:2166 — [review] movePrSidecar's own assertCanMutate fences are exercised by no throwing-fence test
  • packages/cli/src/serve/server/session-archive.test.ts:595 — [review] FIFO test's 500ms unblock timer can throw uncaught ENXIO and crash the vitest worker
  • packages/cli/src/serve/multi-workspace-sessions.test.ts:2494 — [review] Scoped-failure tests never assert the absence of an archived copy
  • packages/core/src/services/sessionService.test.ts:2592 — [probe] Conflict-repair success tests never pin which copy was unlinked

Convergence: round 11 posted 3 inline comment(s), 2 of them reported for the first time; the previous round posted 3 (0 new). Findings keep coming back to the same files: packages/core/src/services/sessionService.ts (findings in round 9; 2 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.)

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had none either, so the next review re-reads the whole diff and will keep doing so until a round's marker carries an anchor again. (Stated, not acted on — this changes nothing about what the round posts.)

[Critical] Ledger rulings for the previous round (round 10, review 5008879557 at 60118c3; the commits since are 21da77c, 782dd86, and a merge of main): R9-3 — fixed by this diff: commit 21da77c removed the final usage-snapshot write from both conflict-repair branches (verified at HEAD: neither repair branch prepares or commits any salvage; the added tests 'does not persist a final usage snapshot during conflict repair' and '...for the retained active copy' pin the absence), so a persisted record prepared from the retained copy can no longer shadow live usage. R9-1 — fixed as filed: the mechanism as filed ('repair prepares usage salvage only from the retained copy', whose final snapshot could shadow or undercount) was removed by the same commit; the residual shape (a readable doomed copy's telemetry is not salvaged when the retained copy is damaged) is the round-9 adjudicated trade-off — usage records are final per-sessionId snapshots, and persisting the doomed copy would reproduce a permanent undercount as soon as the retained copy grows — and matches archive's pre-existing no-salvage semantics. R9-2 — the mechanism stands but its severity is Suggestion, not Critical: the runtime is fail-closed and covered by the mixed local/foreign ownership tests, and the doc sentence is silent (not false) about the mixed-ownership edge — an integrator following the docs receives a per-item error entry, never a silent wrong mutation; recorded under the round-11 convergence deferral rather than re-posted. R10-1 — still stands; re-posted inline under its ledger id.

中文说明

仅完成部分审查,审查缺口已披露。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI; the changed integration file was run directly in the review (36/36 passed), the full integration suite was not run。

未审查:build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI; the workspace suites ran on Linux only。

未审查:chunk 4 — the new multi-workspace lifecycle tests could not be executed during the fan-out (no installed dependencies in the worktree at that point); they were covered afterwards by Agent 7's full suite run (green) — stopped at the agent tool budget。

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 4)"novelty/intent check for resolveSessionBatchRuntime's whole-request SessionNotFoundError when an owner-routed batch mixes a known session with an id that has no…;chunk 4:could not execute the new tests — the shared worktree has no installed dependencies or built workspace packages, so vitest cannot run without a full install/bui…

未审查:反向审计——评审时间预算不足,未能开始第 5 轮。

收敛姿态下延后(第 11 轮,非阻断)——已记录,本轮不要求修改:共 8 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 11 轮发布了 3 条行内评论,其中 2 条是首次提出;上一轮发布了 3 条(其中 0 条首次提出)。发现反复回到同一批文件:packages/core/src/services/sessionService.ts(第 9 轮已出过发现,本轮又有 2 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有锚点,因此下一次评审将重读整个 diff——并会一直如此,直到某一轮的标记重新带上锚点。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

[Critical] Ledger rulings for the previous round (round 10, review 5008879557 at 60118c3; the commits since are 21da77c, 782dd86, and a merge of main): R9-3 — fixed by this diff: commit 21da77c removed the final usage-snapshot write from both conflict-repair branches (verified at HEAD: neither repair branch prepares or commits any salvage; the added tests 'does not persist a final usage snapshot during conflict repair' and '...for the retained active copy' pin the absence), so a persisted record prepared from the retained copy can no longer shadow live usage. R9-1 — fixed as filed: the mechanism as filed ('repair prepares usage salvage only from the retained copy', whose final snapshot could shadow or undercount) was removed by the same commit; the residual shape (a readable doomed copy's telemetry is not salvaged when the retained copy is damaged) is the round-9 adjudicated trade-off — usage records are final per-sessionId snapshots, and persisting the doomed copy would reproduce a permanent undercount as soon as the retained copy grows — and matches archive's pre-existing no-salvage semantics. R9-2 — the mechanism stands but its severity is Suggestion, not Critical: the runtime is fail-closed and covered by the mixed local/foreign ownership tests, and the doc sentence is silent (not false) about the mixed-ownership edge — an integrator following the docs receives a per-item error entry, never a silent wrong mutation; recorded under the round-11 convergence deferral rather than re-posted. R10-1 — still stands; re-posted inline under its ledger id.

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

Comment on lines 1356 to 1358
} catch (error) {
assertCanMutate?.();
this.warn(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] This diff added assertCanMutate?.() to removeSessionOrganization's catch and forwards the fence as assertCanCommit into the org-store atomic write. When the workspace generation is replaced during the org-store commit — the exact race these fences exist for — removeSessionFiles has already deleted every session file, but removeSession now throws post-deletion instead of warning (the catch was warn-only before this PR). The daemon then reports mutationApplied: true plus an errors entry for a fully-applied deletion, and the client retry hits removeSessionFiles' location === undefinedfalse, so the if (removed) guard skips organization cleanup forever — no code path removes an org entry whose files are already gone (every org-removal call site is gated on successful file removal). The session's organization entry persists permanently in the store with no removal path (the organized view only renders rows for persisted/live sessions, so the harm is the permanent store inconsistency plus the false-failure response that invites retries which can never converge).

Witness (scratch-tree probe at HEAD, fence tripped at the org-store commit):

CONTROL: clean delete clears the org entry (6 fence calls)
FENCED first attempt threw: generation closed (fence calls: 7)
FENCED transcript gone after throw: true
FENCED org entry survives first attempt: true
FENCED retry returned: false
FENCED org entry survives retry: true
CANDIDATE FIX (org cleanup before file deletion):
FENCED retry returned: true / org entry survives retry: false

Fix direction: restore the warn-only contract for this post-deletion cleanup (the transcript is already gone; org removal is idempotent and transcript-independent), or run org cleanup before removeSessionFiles, or make it retryable when the files are already absent.

中文说明

本 diff 在 removeSessionOrganization 的 catch 中新增了 assertCanMutate?.(),并把 fence 作为 assertCanCommit 传入 org store 的原子写入。当工作区 generation 在 org store 提交期间被替换(正是这些 fence 要防的竞态)时,removeSessionFiles 已经删除了全部会话文件,但 removeSession 现在会在删除完成之后抛错而不是仅告警(本 PR 之前该 catch 只告警)。daemon 会对一个已完整应用的删除返回 mutationApplied: trueerrors 条目;客户端重试时 removeSessionFileslocation === undefined 返回 falseif (removed) 守卫永远跳过组织清理——没有任何代码路径能删除文件已不存在的组织条目(已核对:所有 org 删除调用点都以文件删除成功为前提)。该会话的组织条目将永久滞留在 store 中且没有任何移除路径(组织视图只为持久/存活会话渲染行,因此实际危害是永久性的 store 不一致,以及会诱发永远无法收敛的重试的错误失败响应)。

证据(隔离工作树探针,HEAD 代码,fence 在 org store 提交点触发):对照组干净删除会清除组织条目(6 次 fence 调用);触发组首次尝试抛 generation closed,抛错后 transcript 已删除:true,组织条目在首次尝试后仍存在:true,重试返回 false,重试后组织条目仍存在:true。候选修复(把组织清理移到文件删除之前)可使探针翻转:重试返回 true,重试后组织条目不再存在。

修复方向:恢复该删除后清理的「仅告警」契约(transcript 已删除,组织删除是幂等且不依赖 transcript 的);或把组织清理移到 removeSessionFiles 之前;或在文件已不存在时允许重试组织清理。

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

Comment on lines +2304 to 2305
options.assertCanMutate?.();
this.removeWorktreeSidecars(sessionId);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] removeSessionFiles unlinks the transcript(s) first and only then runs removeWorktreeSidecars / removePrSidecars / removePromptLedgers / removeFileHistoryBackups, with this diff's assertCanMutate fences between the steps. A generation trip in that window leaves the transcript deleted but the chats-dir sidecars untouched; the retry resolves location === undefined and returns false before reaching the four reap helpers, whose only call sites are these fenced lines. The leak is permanent for <id>.worktree.json, <id>.pr.json and <id>.ledger.jsonl (file-history backups are reclaimed by the age sweeper; these sidecars are not).

Witness (scratch-tree probe at HEAD):

PRISTINE: fenceCallsAtTrip=2
 afterTrip chats dir: ["<id>.ledger.jsonl","<id>.pr.json","<id>.worktree.json"] // transcript gone
 retry removeSession returned: false
 afterRetry chats dir: ["<id>.ledger.jsonl","<id>.pr.json","<id>.worktree.json"] // leak persists
CANDIDATE FIX (reap sidecars in the location===undefined branch):
 afterRetryChatsDir: [] // probe flips

Fix direction: reap the sidecars in the location === undefined branch of removeSessionFiles (they are idempotent and keyed strictly on the session-id pattern), so a retried delete completes the interrupted cleanup.

中文说明

removeSessionFiles 先 unlink transcript,随后才执行 removeWorktreeSidecars / removePrSidecars / removePromptLedgers / removeFileHistoryBackups,而本 diff 在这些步骤之间插入了 assertCanMutate fence。generation 在该窗口内被替换时,transcript 已删除但 chats 目录下的 sidecar 原封未动;重试时 location === undefined,在这四个清理助手之前就返回 false——而它们的唯一调用点正是这些被 fence 的行。对 <id>.worktree.json<id>.pr.json<id>.ledger.jsonl 而言,泄漏是永久的(file-history 备份有按房龄的清扫器回收,这些 sidecar 没有)。

证据(隔离工作树探针,HEAD 代码):触发后 chats 目录仍保留全部三个 sidecar(transcript 已删除);重试 removeSession 返回 false;重试后 chats 目录不变(泄漏持续)。候选修复(在 location === undefined 分支中回收这些以 sessionId 为键的 sidecar)可使探针翻转:重试后 chats 目录为空。

修复方向:在 removeSessionFileslocation === undefined 分支中回收这些 sidecar(它们幂等且严格按 session-id 模式匹配),使重试的删除能完成被中断的清理。

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

Comment on lines +2298 to +2299
for (const identity of physicalSnapshot.identities) {
this.removeFileIfExists(identity.filePath);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R10-1: Still stands — carried since round 1 (as R1-4), re-posted under the round-10 ledger id. The final identity assertion validates a pathname, but the subsequent unlink operates on that pathname without being bound to the validated inode: in removeSessionFiles, assertMaintainableSessionUnchanged lstats the path and removeFileIfExists then unlinks that same path — a concurrent process can swap in an unvalidated replacement between the final stat and the mutation, and delete/archive/unarchive destroys a file no identity check ever validated. The mechanism is present at this head inside the rewritten per-copy deletion loop.

Witness: the round-8 scratch-tree probe applies to the unchanged mechanism — removeSession returned true while the swapped-in file's inode differed from the validated identity and its content did not survive.

The author's wontfix rationale (a correct fix needs a cross-platform atomic claim/no-replace protocol plus crash recovery) is substantive, and the maintainer asked that this residual be tracked in a follow-up issue (review 5006725473); no such issue exists yet (verified this round). Fix direction: atomically claim the source into a private staging path (rename), verify the staged inode, then delete or publish — or land the documented diagnostic and file the follow-up before merge.

中文说明

R10-1:仍然存在——自第 1 轮(R1-4)起持续,按第 10 轮台账 id 重新发布。最终身份断言验证的是路径名,但随后的 unlink 直接作用于该路径名,而未与已验证的 inode 绑定:removeSessionFilesassertMaintainableSessionUnchanged 对路径执行 lstat 后,removeFileIfExists 对同一路径执行 unlink——另一进程可在最后一次 stat 与变更之间换入未经验证的文件,生命周期操作将摧毁一个从未经过身份检查验证的文件。该机制在当前 head 的重写后逐副本删除循环中依然存在。

证据:第 8 轮隔离工作树探针适用于未改变的机制——removeSession 返回 true,而换入文件的 inode 与已验证身份不同且内容未幸存。

作者的 wontfix 理由(正确修复需要跨平台原子认领/禁止替换协议加崩溃恢复)成立,维护者已要求用后续 issue 跟踪该残留(review 5006725473);本轮核实该 issue 尚未创建。修复方向:把源文件原子地认领到私有暂存路径(rename),验证暂存 inode 后再删除或发布——或在合并前落实文档化诊断并创建后续 issue。

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

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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: 51 passed · 0 failed · 51 total

Flakiness gate: ❌ 2 of 11 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: merge-ready). A test that can fail with no code changing lands as intermittent red on unrelated PRs, so this run is reported as not passed regardless of the agent verdict — the per-round matrix is in the flakiness gate log below.

中文 — 判定:❌ 不通过 · 测试结果不确定(抖动门)

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

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

抖动门:❌ 2 of 11 changed test file(s) returned different results across identical re-runs (5 full round(s))

确定性抖动门将本 PR 改动的测试文件原样重跑了多轮,得到了不一致的结果(agent 判定:merge-ready)。一个在代码不变时也会失败的测试会以间歇性红灯落在无关的 PR 上,因此无论 agent 判定如何,本次运行按不通过报告——各轮结果矩阵见下方抖动门日志。

Verification report

Sandboxed verification: ✅ merge-ready — 51/51 scripted assertions passed, verified head 49b67ebef391e96148828da9845dd740919edfa3 (PR #9626, base 1fffa5108d). Follow-up round: the previous substantive round verified the identical head and base OIDs; every carried-forward measurement was nonetheless rebuilt and re-run at this head, and every prior non-blocking observation was re-measured.

中文摘要
  • 判定:merge-ready。51/51 条脚本化断言通过,无阻塞性发现。本轮为跟进轮:上一轮验证的 head(49b67ebe)与 base(1fffa5108d)与本轮完全相同,但所有继承的测量均在本轮重新构建、重新执行(见各表与 evidence 截图)。
  • A/B 结论(head 对 base,真实编译产物 + 真实文件系统 + 真实 HTTP 监听器):
    • 核心层(core-lifecycle-ab.mjs,head 16/16、base 15/15 + FIFO 超时,见 01-core-ab-head-vs-base.png):空、撕裂头、1.5 MiB 未终止非 JSON transcript 在 base 上 delete 返回 not-removed/notFound 且文件滞留、archive/unarchive 报 notFound;head 上全部可删除/按原字节移动。冲突默认两臂一致(每会话 conflict 错误、两份文件字节不变);resolveConflicts: true 在 base 被忽略、在 head 正确保留应保留的一份。符号链接在 base 被跟随(链接被删、目标完好)、在 head 以 non_regular fail closed;FIFO 在 base 的 open() 阻塞到被 timeout 杀掉(exit 124)、在 head 0 ms 拒绝;外部 workspace 两臂均 not-found 且文件不动。
    • 路由层(route-lifecycle-ab.mjs,两臂各 6/6,见 02-route-ab-orphan-flip.png):legacy 孤儿(父会话不存在)在 base 上 qualified 路由 404、owner-routed 路由被误路由到 primary 报 notFound(文件滞留);head 上两族路由均 200 且文件被删除/按原字节归档。workspace-qualified 冲突归档在 base 返回 HTTP 409 session_conflict,在 head 改为 HTTP 200 + 每会话错误(含 resolveConflicts 修复提示),两份文件均保留——与 PR 描述的破坏性变更一致。
  • Vacuity:把分类器回退为"不可读头 ⇒ 不存在"后,16 个 lifecycle 测试以行为断言失败(16 failed | 49 passed),恢复后 65/65 绿、源码无 diff(03-vacuity-mutation-kills-lifecycle-tests.png)。孤儿用例在该突变下仍绿——孤儿的门在路由层,由路由 A/B 与 multi-workspace 套件(143/143)独立钉住。
  • 门禁:core corruption 65/65、core sessionService+organization+usageHistory 248/248、cli serve 5 文件 1636/1636(含 multi-workspace 143、session-archive 62、transport 345)、acp-bridge attachments 47/47、SDK DaemonClient 349/349。
  • 未覆盖:每 commit 归因(shallow clone)、Windows 行为、TOCTOU 重校验的独立竞态驱动、全仓库套件与 typecheck(交由 CI)。

Previous-round status

The previous substantive round verified head 49b67ebe / base 1fffa5108d — the same OIDs as this round's HEAD^2/HEAD^1, so the input closure is identical; per the follow-up rule I still rebuilt and re-ran every measurement rather than diffing the old report.

# Previous finding Severity Status at this head
1 Directory entries change response shape: base reported not-found, head reports typed per-session non_regular error; both leave the directory untouched non-blocking stands — re-measured: head refused (non_regular); directory intact=true, base not-removed; directory intact=true (core A/B)
2 Base hazards documented by control cells: base follows symlinks during classification and blocks indefinitely opening a FIFO informational (base defect, fixed by PR) stands — re-measured: base symlink cell FOLLOWED: link removed, target intact; base FIFO cell blocked until timeout -k (exit 124); head refuses both in 0 ms
3 Fail-closed shapes for unclassifiable heads ({}unknown_projection; >1 MiB unterminated JSON-looking line → SessionTranscriptIdentityUnavailableError) refuse without mutating informational stands — re-measured on head, both intact
4 (two rounds back) npm ci build failure TS2339 countTokens infra fixed — container build green at this head; my own --cli-only rebuild of the base arm compiled with 0 TS errors (logs/base-build.log)

No declined or deferred rows existed to re-measure.

Central claim and A/B proof

Central claim: delete/archive/unarchive maintain owned persisted transcripts that are empty, torn-headed, oversized-unreadable, or legacy orphans — classification is by physical storage ownership, not conversation loadability.

Secondary claims: (1) default conflict behavior stays non-mutating, resolveConflicts: true repairs keep the correct copy, and workspace-qualified conflict responses move from HTTP 409 to per-session errors in a 200; (2) fences hold: symlink/directory/FIFO fail closed without following or blocking, foreign-workspace stays not-found.

Core-level A/B (core-lifecycle-ab.mjs, compiled SessionService, real fs)

Witness: 01-core-ab-head-vs-base.png. Base arm ran against a HEAD^1 worktree; @qwen-code/* resolution was re-pointed into the base tree and verified (import.meta.resolve from base cli dist → tmp/base-tree/packages/core/dist/index.js; ESM probe: getMaintainableSessionLocation undefined on base core, present on head). The PR leaves the lockfile untouched, so external deps are a clean control.

Cell base (1fffa5108d) head (49b67ebe) flip
delete healthy (control) removed, file gone removed, file gone parity
delete empty not-removed, stranded removed, gone
delete torn head not-removed, stranded removed, gone
delete {} head refused (TypeError), intact refused (unknown_projection), intact parity, fail-closed
delete 1.5 MiB unterminated JSON not-removed, intact refused (IdentityUnavailable), intact parity, fail-closed
archive empty notFound, unmoved archived, 0 bytes verbatim
unarchive torn notFound, unmoved unarchived, bytes verbatim
archive 1.5 MiB non-JSON blob notFound, unmoved archived, 1.5 MiB verbatim
archive conflict, default conflict error, both copies byte-identical conflict error, both copies byte-identical parity
archive conflict, resolveConflicts:true option ignored, conflict stands archived kept, active removed, resolvedConflicts set ✅ additive
unarchive conflict, resolveConflicts:true option ignored active kept, archived removed ✅ additive
delete conflict both copies removed both copies removed parity (compat)
symlink entry followed: link removed (target intact) typed non_regular, link+target untouched ✅ fail-closed
directory entry not-removed, intact typed non_regular, intact parity on safety
FIFO entry blocked (open hangs; killed by timeout, exit 124) typed non_regular in 0 ms ✅ fail-closed
foreign-workspace not-removed, intact not-removed, intact parity (fence held)

Head 16/16, base 15/15 + FIFO-timeout = 16/16 — every base cell failed exactly as the PR's premise predicts.

Route-level A/B (route-lifecycle-ab.mjs, compiled createServeApp on a real loopback listener; ordinary primary + internal Conversations secondary)

Witness: 02-route-ab-orphan-flip.png. The ACP bridge is a call-logging Proxy fake honouring the contract (getSessionSummary/closeSession/killSession throw SessionNotFoundError for non-live sessions; deleteSessionAttachments/markSessionCatalogChanged are no-ops); routes, registry, SessionService, and filesystem are the real compiled code.

Cell base head
orphan, qualified delete 404 session_not_found, file stranded 200, removed, file gone
orphan, owner-routed delete 200 but notFound (misrouted to primary), file stranded 200, removed via internal owner
orphan, qualified archive 404, unmoved 200, archived, bytes verbatim
healthy internal delete (control) 200 removed 200 removed
foreign, qualified delete 404, untouched 404, untouched
conflict, qualified archive default HTTP 409 session_conflict, both preserved HTTP 200 + per-session error naming resolveConflicts, both preserved

6/6 both arms. The base owner-routed-orphan cell is the sharpest: the file existed, was owned, and was readable — yet the daemon reported not-found after silently falling back to the primary runtime. The 409→200 envelope change matches the PR's stated breaking change, and the conflict reason survives in the per-session error.

Vacuity / mutation matrix (03-vacuity-mutation-kills-lifecycle-tests.png)

Mutation: in readMaintainableSessionIdentity, unreadable head ⇒ state absent (exactly the base's loadability dependence), interface preserved.

Guard Suite Result
unreadable regular files still occupy their state (classifier) sessionService.corruption.test.ts lifecycle block killed — 16 tests fail on behavioral assertions (16 failed | 49 passed); restore → 65/65 green, git diff empty
orphan classification at core level same block survived by design: an orphan has a readable head, so the core classifier was never its gate — the gate is the route resolver's loadability check, pinned by the route A/B above and the multi-workspace suite (143/143)

Findings

No blocking findings. Non-blocking observations (all re-measured this round):

  1. Directory entries change response shape, not safety. Base reported a directory in the chats dir as not-found; head reports a typed per-session error (non_regular). Both leave the directory and its contents untouched (asserted). Clients that treated "not in notFound and not in errors" as absent should note the entry now lands in errors.
  2. Base hazards documented by the control cells (not PR defects): base follows symlinks during classification and blocks indefinitely opening a FIFO — the base arm's FIFO cell required timeout -k because the blocked open() ignores SIGTERM. Head's O_NOFOLLOW|O_NONBLOCK open fixes both.
  3. Fail-closed shapes for unclassifiable heads ({} first record → unknown_projection; >1 MiB unterminated JSON-looking line → SessionTranscriptIdentityUnavailableError) are deliberate and storage-safe; both refuse without mutating.

Not covered

  • Per-commit attribution: shallow depth-2 checkout (18 commits in metadata, only the merge triple reachable); the aggregate HEAD^1..HEAD diff was verified instead.
  • Windows: the fence skips O_NOFOLLOW|O_NONBLOCK on win32; not exercised here (Linux container).
  • TOCTOU revalidation (assertMaintainableSessionUnchanged): not driven by an independent race harness; pinned by the PR's own tests ("replaced after validation", generation-rejection fences) — all green, and the mutation run showed those tests fail when the classifier regresses.
  • ACP transport / SDK wire behavior: covered by their suites (cli transport+server 1636 passed 0 failed; SDK 349/349), no independent wire A/B.
  • Full-repo suite and typecheck: left to the PR's CI; targeted workspaces only were run here.
  • Base-arm teardown artifact: the base FIFO cell is run isolated under timeout -k; the blocked open() is a base defect, not harness or PR code.

Methodology

Environment: node:22-bookworm container, merge-ref checkout (HEAD = merge c865a1f, HEAD^1 = base 1fffa5108d, HEAD^2 = verified head 49b67ebe), npm ci + npm run build pre-completed at HEAD. Core A/B drove packages/core/dist SessionService directly; the base arm used a tmp/base-tree worktree rebuilt with scripts/build.js --cli-only (root node_modules shared, nested per-package node_modules linked, @qwen-code/* overlay symlinks repointed into the base tree and verified by import.meta.resolve + ESM import probe). Route A/B booted each tree's compiled createServeApp on a real loopback listener with the repo's two-workspace registry topology (ordinary primary + provenance: 'live-conversation' secondary). Assertion tally: 32 core A/B cells (16 head + 15 base + 1 FIFO-timeout) + 12 route A/B cells + 2 vacuity runs + 5 targeted-gate runs = 51, all passing. Raw per-arm JSON logs, harness scripts, gate logs, and base-build.log live in this artifact directory; images were produced with scripts/verify-capture.mjs.

Flakiness gate log

integration test, out of gate scope: integration-tests/cli/qwen-serve-routes.test.ts
rounds=5 files=11 skipped=1
file packages/acp-bridge/src/sessionAttachments.test.ts: (cd packages/acp-bridge) npx --no-install vitest run ./src/sessionAttachments.test.ts
file packages/cli/src/serve/acp-http/transport.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/acp-http/transport.test.ts
file packages/cli/src/serve/multi-workspace-sessions.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/multi-workspace-sessions.test.ts
file packages/cli/src/serve/scheduled-task-session-lifecycle.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/scheduled-task-session-lifecycle.test.ts
file packages/cli/src/serve/server.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/server.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/services/session-organization-service.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/session-organization-service.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
file packages/core/src/services/usageHistoryService.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/usageHistoryService.test.ts
file packages/sdk-typescript/test/unit/DaemonClient.test.ts: (cd packages/sdk-typescript) npx --no-install vitest run ./test/unit/DaemonClient.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/acp-bridge/src/sessionAttachments.test.ts: PPPPP
  packages/cli/src/serve/acp-http/transport.test.ts: PPPPP
  packages/cli/src/serve/multi-workspace-sessions.test.ts: PPPPP
  packages/cli/src/serve/scheduled-task-session-lifecycle.test.ts: PPPPP
  packages/cli/src/serve/server.test.ts: PPPPP
  packages/cli/src/serve/server/session-archive.test.ts: PPFFP
  packages/core/src/services/session-organization-service.test.ts: PPPPP
  packages/core/src/services/sessionService.corruption.test.ts: FPFFF
  packages/core/src/services/sessionService.test.ts: PPPPP
  packages/core/src/services/usageHistoryService.test.ts: PPPPP
  packages/sdk-typescript/test/unit/DaemonClient.test.ts: PPPPP

verdict: flaky
summary: 2 of 11 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/acp-bridge/src/sessionAttachments.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/acp-http/transport.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/multi-workspace-sessions.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/scheduled-task-session-lifecycle.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/server.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/server/session-archive.test.ts: P (exit 0)
round 1 · packages/core/src/services/session-organization-service.test.ts: P (exit 0)
round 1 · packages/core/src/services/sessionService.corruption.test.ts: F (exit 1)
--- output tail · round 1 · packages/core/src/services/sessionService.corruption.test.ts ---
e�[2m > �[22mdoes not overwrite a damaged archived copy when the active copy is readable�[32m 1�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mrepairs an archive conflict when only the archived copy is readable�[32m 1�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mdoes not overwrite a damaged active copy when the archived copy is readable�[32m 1�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mrepairs an unarchive conflict when only the active copy is readable�[32m 1�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mreclassifies a conflict that appears during archive validation�[32m 2�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mreclassifies a conflict that appears during unarchive validation�[32m 1�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mdoes not delete a readable archived transcript replaced after validation�[32m 2�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mdoes not unarchive a readable archived transcript replaced after validation�[32m 1�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mdoes not delete a transcript that disappears and reappears during classification�[32m 1�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mdoes not archive a transcript that disappears and reappears during classification�[32m 1�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mdoes not unarchive a transcript that disappears and reappears during classification�[32m 1�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mdoes not delete a readable transcript whose record identifies another session�[32m 1�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mdoes not archive a readable transcript whose record identifies another session�[32m 1�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mdoes not unarchive a readable transcript whose record identifies another session�[32m 1�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mfails closed on a record with missing cwd�[32m 2�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mfails closed on a record with non-string cwd�[32m 2�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mfails closed on a foreign record with missing session id�[32m 3�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mfails closed on a foreign record with non-string session id�[32m 1�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mfails closed on mixed foreign and local storage during delete�[32m 2�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mfails closed on mixed foreign and local storage during archive�[32m 1�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mfails closed on mixed foreign and local storage during unarchive�[32m 1�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mfails closed when delete sees another session id without cwd ownership�[32m 1�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mfails closed when archive sees another session id without cwd ownership�[32m 0�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mfails closed when unarchive sees another session id without cwd ownership�[32m 1�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mmaintains an oversized first physical record without buffering the whole file�[32m 17�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mdoes not delete a just-over-limit readable transcript from another workspace�[32m 15�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mdoes not archive a just-over-limit readable transcript from another workspace�[32m 13�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mdoes not unarchive a just-over-limit readable transcript from another workspace�[32m 13�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mfails closed on a readable transcript whose first record exceeds the bounded read window�[32m 20�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mdoes not swallow a generation rejection at the archive ledger fence�[32m 1�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mdoes not swallow a generation rejection at the unarchive ledger fence�[32m 1�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mrejects an in-place rewrite during ownership classification�[32m 1�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mdoes not delete a readable transcript symlink�[32m 1�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mdoes not archive a readable transcript symlink�[32m 0�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mdoes not unarchive a readable transcript symlink�[32m 0�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mrejects a transcript FIFO without waiting for a writer�[32m 5�[2mms�[22m�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mdoes not delete a damaged transcript replaced after validation�[32m 1�[2mms�[22m�[39m
�[31m   �[31m�[31m SessionService lifecycle maintenance�[2m > �[22mdoes not archive a damaged transcript replaced after validation�[39m�[32m 9�[2mms�[22m�[39m
�[31m     → expected '{"uuid":"torn-head"' to be 'replacement' // Object.is equality�[39m
   �[32m✓�[39m SessionService lifecycle maintenance�[2m > �[22mdoes not unarchive a damaged transcript replaced after validation�[32m 1�[2mms�[22m�[39m
   �[32m✓�[39m SessionService.reconstructHistory (history-gap detection)�[2m > �[22mreports the gap but does NOT reconstruct the earlier island (detectGaps on)�[32m 1�[2mms�[22m�[39m
   �[32m✓�[39m SessionService.reconstructHistory (history-gap detection)�[2m > �[22mpreserves today truncation behavior when de

...truncated -- full content in the run artifacts.

Evidence images

01-core-ab-head-vs-base

02-route-ab-orphan-flip

03-vacuity-mutation-kills-lifecycle-tests

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

Qwen Code · sandboxed verification

@doudouOUC
doudouOUC dismissed qwen-code-ci-bot’s stale review August 25, 2026 02:24

Already have 2 approves,3ks

@doudouOUC
doudouOUC added this pull request to the merge queue Aug 25, 2026
Merged via the queue into QwenLM:main with commit 3133e83 Aug 25, 2026
162 of 163 checks passed
@doudouOUC
doudouOUC deleted the fix/session-storage-lifecycle-9488 branch August 25, 2026 02:25
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot has neither a verdict nor a deferral on 49b67ebef391e96148828da9845dd740919edfa3 — no APPROVED, CHANGES_REQUESTED, or COMMENTED review of its own. A DISMISSED one does not count: dismiss_stale_reviews voids the bot's approval on every push, which is exactly when a fresh one is needed. If this re-run was meant to review or approve, it did not, and an approval left by another account is a separate vote that does not count as the bot's own.

⚠️ 机器人在 49b67ebef391e96148828da9845dd740919edfa3既没有裁决也没有 defer —— 没有属于它自己的 APPROVEDCHANGES_REQUESTEDCOMMENTED 评审。DISMISSED 不算:dismiss_stale_reviews 会在每次推送时作废机器人的批准,而那恰恰是需要一次新批准的时刻。如果这次重跑本应评审或批准,那么它没有做到;而其他账号留下的批准是另一张票,不能算作机器人自己的。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

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

Labels

review/self-reported The linked issue was opened by the PR author (self-reported)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Session lifecycle operations are gated on provenance classification, making unclassifiable sessions unmanageable

4 participants