Skip to content

fix(cli): Recover sessions across archive races - #9513

Merged
doudouOUC merged 29 commits into
QwenLM:mainfrom
doudouOUC:fix/pr2a-followup-9489
Aug 22, 2026
Merged

fix(cli): Recover sessions across archive races#9513
doudouOUC merged 29 commits into
QwenLM:mainfrom
doudouOUC:fix/pr2a-followup-9489

Conversation

@doudouOUC

@doudouOUC doudouOUC commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Restores four session-lifecycle behaviors regressed by the PR2A provenance and identity tightening, while keeping broader mixed-case daemon compatibility out of scope.

REST, daemon ACP, and embedded ACP load/resume recover an exact same-spelling session left readable in both active and archived storage from the active copy, including writer-lease activation. Transcript and export reads follow the same active-copy rule, including workspace-qualified internal conversation routes. Differently spelled or ambiguous case conflicts remain rejected; archive and unarchive remain strict, while explicit delete acquires the writer lease and removes both copies.

Creation-metadata reads tolerate a concurrent active-to-archive move and resolve legacy parent identifiers within a path-safe transcript-name boundary. Conversation directories that disappear during identity inspection are treated as already gone.

Case-insensitive persisted-session lookup uses asynchronous directory reads, and restore performs the catalog lookup once under the lifecycle lock instead of probing before and inside the lock. The E2E plan lowercases uuidgen output so fixtures match Qwen Code's canonical lowercase session-ID format.

Why it's needed

After PR2A tightening, interrupted archive operations could leave a readable session that load/resume refused; transcript/export reads and delete then offered inconsistent recovery behavior. Archive moves could make creation metadata appear missing, legacy child sessions could lose parent lineage, and concurrent directory deletion could surface as an identity error. These are the four regressions tracked by #9489. The restore path also scanned the transcript catalog twice, adding avoidable I/O and another race window.

Normal Qwen Code-generated session IDs are already lowercase, so broader mixed-case Live task, listing, organization, and scheduled-task compatibility is intentionally excluded from this PR.

Reviewer Test Plan

How to verify

  • Create active and archived transcripts with the exact same session ID but different marker content. Confirm that REST plus daemon and embedded ACP load/resume use the active copy and retain writer ownership; transcript/export return the active marker and not the archived marker; workspace-qualified internal reads behave the same; and explicit delete removes both copies.
  • Confirm that differently spelled or ambiguous case conflicts still return session_conflict, and that archive/unarchive remain strict for any both-state conflict.
  • Move a transcript from active to archived while creation metadata is being read. Confirm metadata still resolves, safe legacy nil/v6/v7/agent-suffixed parent IDs are followed, and path-unsafe or Windows device-name parents are not read.
  • Remove a conversation directory between identity probes and confirm cleanup treats it as already gone.
  • Inspect restore under the lifecycle lock and confirm it performs one asynchronous case-insensitive catalog lookup.
  • Run the E2E plan's uuidgen snippets and confirm the generated session IDs are lowercase.

Latest verification passed: 176/176 session-service tests; 120/120 multi-workspace session tests; full workspace build and typecheck; and changed-file ESLint, Prettier, and diff checks. The current PR SHA has no failing CI checks; every check that executed is passing.

Evidence (Before & After)

N/A — backend session-lifecycle fix; no TUI or user-interface changes.

Tested on

Environment Status
macOS ✅ Tested locally
Windows ⚠️ Not tested locally; selected CI checks pass
Linux ⚠️ Not tested locally; selected CI checks pass

Environment

Node.js v22.22.3 on macOS; package-local Vitest plus full workspace build and typecheck.

Risk & Scope

  • Main risk: for an exact same-spelling active/archive conflict, the active transcript is now authoritative for restore and read/export, and explicit delete removes both copies. Archive/unarchive and differently spelled or ambiguous conflicts remain strict.
  • Not validated / out of scope: no local Windows or Linux behavior run; broader mixed-case Live task, list, organization, and scheduled-task compatibility is intentionally excluded.
  • Breaking changes / migration: None.

Linked Issues

Fixes #9489

中文说明

本 PR 做了什么

恢复 PR2A provenance 与 identity 收紧后回归的四项 session 生命周期行为,同时明确不在本 PR 中扩展 daemon 的通用大小写兼容能力。

当完全相同拼写的 session ID 同时存在于 active 与 archived 存储且两份都可读时,REST、daemon ACP 与 embedded ACP 的 load/resume 都以 active 副本恢复,包括 writer lease 激活流程。Transcript 与 export 读取遵循相同的 active 副本语义,workspace-qualified 的内部 conversation 路由也一致。拼写不同或存在歧义的大小写冲突仍然拒绝;archive/unarchive 继续严格拒绝,而显式 delete 会先获取 writer lease,再删除两份副本。

创建元数据读取可以容忍并发的 active-to-archive 移动,并在路径安全的 transcript 名称边界内解析旧版 parent 标识。Conversation 目录在 identity 检查期间消失时,会被视为已经删除。

持久化 session 的大小写无关查询改用异步目录读取;restore 在生命周期锁内只执行一次 catalog 查询,不再在加锁前和锁内重复探测。E2E 计划会把 uuidgen 输出转成小写,使测试 fixture 与 Qwen Code 的 canonical 小写 session ID 格式一致。

为什么需要

PR2A 收紧后,中断的 archive 操作可能留下仍可读取、但 load/resume 拒绝恢复的 session;transcript/export 读取与 delete 也因此出现不一致的恢复行为。Archive 移动可能让创建元数据暂时表现为缺失,旧版 child session 可能丢失 parent lineage,并发删除目录可能暴露为 identity 错误。这些就是 #9489 跟踪的四项回归。Restore 路径还会重复扫描 transcript catalog,既增加不必要的 I/O,也扩大了竞态窗口。

Qwen Code 正常生成的 session ID 已经是小写,因此更广泛的 mixed-case Live task、列表、organization 与 scheduled-task 兼容性明确不属于本 PR 范围。

Reviewer 测试计划

如何验证

  • 用完全相同的 session ID 分别创建 active 与 archived transcript,并写入不同的 marker 内容。确认 REST、daemon ACP 和 embedded ACP 的 load/resume 使用 active 副本并保留 writer ownership;transcript/export 返回 active marker 而不包含 archived marker;workspace-qualified 内部读取行为一致;显式 delete 会删除两份副本。
  • 确认拼写不同或存在歧义的大小写冲突仍返回 session_conflict,且 archive/unarchive 对任何双状态冲突都继续严格拒绝。
  • 在读取创建元数据时把 transcript 从 active 移到 archived。确认元数据仍能解析,安全的旧版 nil/v6/v7/agent 后缀 parent ID 可以被跟随,而路径不安全或 Windows 设备名形式的 parent 不会被读取。
  • 在两次 identity 探测之间删除 conversation 目录,确认清理流程把它视为已经删除。
  • 检查生命周期锁内的 restore,确认它只执行一次异步、大小写无关的 catalog 查询。
  • 运行 E2E 计划中的 uuidgen 片段,确认生成的 session ID 为小写。

最新验证已通过:session-service 测试 176/176;multi-workspace session 测试 120/120;全工作区 build 与 typecheck;以及改动文件的 ESLint、Prettier 和 diff 检查。当前 PR SHA 没有失败的 CI check,所有实际执行的 check 均已通过。

前后证据

N/A — 后端 session 生命周期修复,没有 TUI 或用户界面变化。

测试环境

环境 状态
macOS ✅ 已在本地测试
Windows ⚠️ 未在本地测试;选定 CI 检查已通过
Linux ⚠️ 未在本地测试;选定 CI 检查已通过

环境信息

macOS 上的 Node.js v22.22.3;执行了 package-local Vitest、全工作区 build 与 typecheck。

风险与范围

  • 主要风险:对于完全相同拼写的 active/archive 冲突,现在 restore 与 read/export 以 active transcript 为准,显式 delete 会删除两份副本。Archive/unarchive 以及拼写不同或存在歧义的冲突仍保持严格拒绝。
  • 未验证 / 范围外:未在本地运行 Windows 或 Linux 行为测试;更广泛的 mixed-case Live task、列表、organization 与 scheduled-task 兼容性明确不在本 PR 范围内。
  • 破坏性变更 / 迁移:无。

关联 Issue

Fixes #9489

@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Aug 20, 2026
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

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

session-restore-active-and-archived

field PR base (before) this PR (after)
_status 409 200
code "session_conflict"
error "Session \"a0000000-0000-4000-8000-00000000da05\" exists in both active and archived directories. Delete the session with POST /sessions/delete before loading."

session-restore-case-twins

field PR base (before) this PR (after)
error "Multiple persisted sessions match \"a0000000-0000-4000-8000-00000000da03\" by case." "Session \"a0000000-0000-4000-8000-00000000da03\" exists in both active and archived directories. Delete the session with POST /sessions/delete before loading."

Qwen Code · serve A/B

doudouOUC and others added 4 commits August 20, 2026 16:23
…lready gone

A child deleted between the lstat and the realpath, or a root that
vanished mid-inspection, was rewritten as 'identity_changed' and then
surfaced as 'Live conversation directory must be an owned direct child'
— a plain Error with no .code pointing at permissions and symlinks when
the directory was simply deleted. Restore the ENOENT-race -> false
contract of discardEmptyConversationDirectory (QwenLM#9489,
item 4).

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…torage-aligned

Item 2 of QwenLM#9489: readExistingMetadata read the location,
read the metadata, then re-read the location and returned undefined on
mismatch, so an archive landing between the probes made lock-free
resolvers report a healthy session as session_not_found. Creation
metadata is immutable, so one tolerant read per state (active first,
then archived) decides deterministically; the location probes are gone
and a path-safety charset gate keeps the joined transcript path a
single segment.

Item 3: the parent-lineage gate required strict RFC-4122 v1-v5 ids
while the store resolves far looser names, so persisted parents written
by older builds (nil, v6/v7, agent-suffixed ids) turned loadable
children into SessionNotFoundError, and the -agent- allowance could
never resolve. Drop the shape gate and let storage resolution decide,
keeping only the self-reference rejection.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…ck restore scans

Items 1 and 5 of QwenLM#9489.

Item 1: a session persisted in both active and archived states — left
behind by a crash inside archiveSessions — hard-failed ACP session/load
and session/resume with session_conflict while plain CLI --resume kept
loading the active copy. findSessionIdIgnoringCase now resolves the
requested spelling first (and a single both-states candidate) instead
of throwing, and assertSessionLoadable treats 'conflict' as loadable
from the active copy. Mutating surfaces keep refusing: unarchive still
conflicts via assertSessionArchived, and multi-runtime ownership
arbitration stays strict so a conflicted internal copy cannot claim a
session an ordinary workspace serves.

Item 5: both restore handlers ran findSessionIdIgnoringCase twice per
request — once as a pre-lock guard whose result REST discarded and the
ACP twin kept as a stale storageSessionId fallback consumed exactly in
the TOCTOU where the in-lock resolve returned undefined. The pre-lock
guards are gone (the in-lock resolve is authoritative and both handlers
now agree), the exact-spelling fast path removes directory scans from
the common case entirely, and the remaining scan uses async readdir so
a large chats tree no longer blocks the daemon event loop.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Canonicalize live task keys before resident bridge operations, and keep known case-conflict responses when the optional storage recheck fails.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC
doudouOUC force-pushed the fix/pr2a-followup-9489 branch from cd0477b to c6cab84 Compare August 20, 2026 08:25
@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)为单个提交。

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC
doudouOUC marked this pull request as ready for review August 20, 2026 09:11
@doudouOUC
doudouOUC enabled auto-merge August 20, 2026 09:12
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Qwen Triage finished — CI landed green on 97868f7 and the deferred approval was posted. finalize run

Qwen Triage 已完成 —— 97868f7 的 CI 全绿,延迟审批已提交。查看 finalize 运行

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Change since the last gate at 78031b16bf: the author merged origin/main into the branch (97868f7fbd, no force-push), absorbing 19 commits — including the acp-integration/serve boundary refactor and the web-shell session-PR-binding feature, both of which touch files this PR also changes. Gate re-derived at the merged head; the PR's own scope is unchanged.

Template looks good ✓

Problem: observed, with evidence — #9489 tracks four regressions introduced by the merged #9341, each reported by @wenshao against head 04ac635d with the review-discussion link, plus the restore path's double catalog scan. The PR additionally lowercases the E2E plan's uuidgen fixtures (Qwen Code session ids are canonical lowercase).

Direction: aligned — restoring behaviour narrowed by the author's own merged PR, squarely on the daemon/ACP/REST session surface. No auth/sandbox/telemetry/public-contract territory.

Size: 314 production lines vs 870 test lines vs 10 docs lines across 22 files, recomputed at the merged head. Core paths carry 4 production lines (one-line changes each in packages/core/src/config/config.ts and packages/core/src/services/sessionService.ts); the rest is the cli serve layer. This is a fix (no Tier 1 block) and sits below both the 500-line maintainer-awareness threshold and the 1000-line large-PR advisory.

Approach: minimal and 1:1 with the linked issue. The merge itself added no scope: I diffed every PR-owned file between the old and new heads — 13 of 22 are byte-identical, the 9 changed ones are exactly the files main also touched, and the merge commit's combined diff shows a single conflict (one import line in live-session-source.ts, where main's boundary refactor moved the session-id module; the resolution keeps the PR's symbol list at main's new path and the file uses only that symbol). No drive-by edits.

Risk: Stage 1e matches one revert-correlated path — packages/cli/src/acp-integration/acpAgent.ts — so this run keeps the full Stage 2 evidence bar. The author has write access; a @qwen-code /verify round at this exact head is already in flight.

Moving on to code review. 🔍

中文说明

上次 gate(78031b16bf)以来的变化:作者将 origin/main 合入分支(97868f7fbd,未 force-push),吸收了 19 个提交——包括 acp-integration/serve 边界重构与 web-shell session-PR 绑定功能,两者都触及本 PR 也修改的文件。gate 已在合并后的 head 上重新推导;PR 自身范围未变。

模板完整 ✓

问题:已观测到,且有证据——#9489 追踪已合并 #9341 引入的四项回归,均由 @wenshao 在 head 04ac635d 上报告并附 review 讨论链接,另含 restore 路径的重复 catalog 扫描。PR 还把 E2E 计划的 uuidgen fixture 转为小写(Qwen Code 会话 id 规范为小写)。

方向:对齐——恢复作者自己已合并 PR 收窄的行为,完全在 daemon/ACP/REST 会话面,不涉及 auth/沙箱/遥测/公共契约。

规模:314 生产行 vs 870 测试行 vs 10 文档行,共 22 个文件(已在合并后 head 重新计算)。核心路径仅 4 个生产行config.tssessionService.ts 各一行改动),其余在 cli serve 层。这是 fix(不触发 Tier 1),低于 500 行维护者关注阈值与 1000 行大 PR 提示。

方案:最小且与所链 issue 一一对应。合并本身未增加范围:逐一比对了新旧 head 上全部 22 个 PR 文件——13 个字节级一致,9 个有变化的恰好是 main 也修改过的文件;合并提交的 combined diff 只显示一处冲突(live-session-source.ts 的一行 import——main 的边界重构移动了 session-id 模块,解决方案在 main 的新路径上保留了 PR 的符号列表,且该文件只使用这一个符号)。无夹带改动。

风险:Stage 1e 命中一个回滚相关路径——packages/cli/src/acp-integration/acpAgent.ts——本次运行保持完整 Stage 2 证据标准。作者有写权限;针对本 head 的 @qwen-code /verify 轮已在运行中。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at 97868f7fbd5893cc7d8db3886a5a8b9c01e2e1ea · re-run with @qwen-code /triage

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

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

Copy link
Copy Markdown
Collaborator

Code review

The diff itself was reviewed clean at 78031b16bf (eight review rounds, round-8 /review zero findings, sandboxed round 6 at 2991/2991 assertions with the wire-level A/B flip 409 → 200), and the only code change since is the merge of origin/main. So this pass is a merge-integrity review: did the merge preserve the reviewed change and integrate cleanly with the 19 commits it absorbed?

  • One conflict in the whole merge, resolved correctly. git show --cc on the merge commit surfaces exactly one file: live-session-source.ts, where main's boundary refactor (b455bad5) moved the session-id module while this PR was dropping isValidSessionId from the same import. The resolution keeps the PR's symbol list at main's new path; I verified the module exists there, exports both symbols, and the file only uses normalizeSessionIdForLookup.
  • The reviewed diff survived intact. 13 of the 22 PR files are byte-identical between the old and new heads. For the 9 files main also touched, I re-read the full effective diff (2,094 lines) at the merged head: every hunk of the four PR2A provenance/identity tightening: four narrower behaviour regressions #9489 items, the restore single-scan dedup, and the fixture lowercasing is present — resolvePersistedSessionIdForRestore in acpAgent, single resolveSessionIdForRestore under runSharedMany in both REST and ACP restore handlers, assertSessionRestorable/allowActiveConflict threading in session-archive and routes/session, the tolerant one-read-per-state readExistingMetadata, the path-safety parent gate, both ENOENT-race halves, delete-both-copies under the writer lease, and the async readdir in findSessionIdIgnoringCase.
  • Main's changes survived intact too. sessionService.ts's effective delta against new main is exactly the one-line async-readdir change, with feat(web-shell): Bind GitHub PRs to sessions with sidebar badge and search #9543's session-PR additions (readSessionPrs/mergeSessionPrLists, getPrSessionPathForState) present at the head; feat(web-shell): Bind GitHub PRs to sessions with sidebar badge and search #9543's serve routes and the boundary refactor's import rewrites are untouched. Cross-references resolve: core re-exports SessionIdCaseConflictError (index line 316, export * from sessionService), and dispatch's error mapper uses string-name cases, so the removed type imports are safe.

No new findings. The suggestion-level deferrals from rounds 6–8 (D7-1…D7-5) stand as recorded, non-blocking.

Testing evidence (this PR's own CI + sandboxed lane)

Unattended run — PR code is never executed here; this quotes the PR's own CI on the reviewed commit. On 97868f7fbd, Security Checks, Qwen Live Host CI, SDK Java, Desktop Shell (both OSes), Real daemon E2E, and the Java matrix are green; Test (ubuntu-latest, Node 22.x) and Serve A/B are still running. The cancelled rows on this head are a duplicated workflow start deduped by concurrency, not failures; the macos/windows unit legs and the no-sandbox integration job are skipped, same as on the previously green head.

Final CI results for 97868f7 (auto-updated by the triage finalize job after CI completed):

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

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

Sandboxed verification: the central claim is behavioural (a raced-archive session becomes recoverable instead of 409), and the mutation matrix that pinned all 14 guard points was measured at the pre-merge head — the delta since is merge-only, but it is a delta in exactly the files that carry the claim. A @qwen-code /verify round at this exact head is in flight (run 32574190233) and the Serve A/B check above re-measures the daemon-level 409 → 200 flip on it; between them they settle end-to-end that active-copy adoption holds and the strict surfaces stayed strict after the merge. Real-scenario tmux testing: N/A for this unattended (issue_comment) run. The author's PR-body suite numbers remain the author's claim; CI and the sandboxed lane are the evidence.

中文说明

代码审查:diff 本身在 78031b16bf 上已审查干净(八轮 review、第 8 轮 /review 零发现、沙箱第 6 轮 2991/2991 断言且线级 A/B 409 → 200),此后唯一的代码变化是合并 origin/main。因此本轮是合并完整性审查:合并是否保住了已审查的改动,并与所吸收的 19 个提交正确集成?

  • 整个合并只有一处冲突,且解决正确。 合并提交的 combined diff 只显示一个文件:live-session-source.ts——main 的边界重构(b455bad5)移动了 session-id 模块,而本 PR 恰好在同一 import 中移除 isValidSessionId。解决方案在 main 的新路径上保留了 PR 的符号列表;已核实该模块在新路径存在、导出两个符号,且文件只使用 normalizeSessionIdForLookup
  • 已审查的 diff 完整保留。 22 个 PR 文件中 13 个在新旧 head 间字节级一致。对 main 也修改过的 9 个文件,已在合并后 head 通读全部 2094 行有效 diff:PR2A provenance/identity tightening: four narrower behaviour regressions #9489 四项、restore 单次扫描去重、fixture 小写化的每个 hunk 均在——acpAgent 的 resolvePersistedSessionIdForRestore、REST 与 ACP restore 中 runSharedMany 内的单次 resolveSessionIdForRestore、session-archive 与 routes/session 的 assertSessionRestorable/allowActiveConflict 贯通、宽容的每状态一次读取 readExistingMetadata、路径安全父门、ENOENT 竞态两半、writer lease 下删除双副本、findSessionIdIgnoringCase 的异步 readdir
  • main 的改动也完整保留。 sessionService.ts 相对新 main 的有效差异恰为一行异步 readdir 改动,feat(web-shell): Bind GitHub PRs to sessions with sidebar badge and search #9543 的 session-PR 增量(readSessionPrs/mergeSessionPrListsgetPrSessionPathForState)在 head 上存在;feat(web-shell): Bind GitHub PRs to sessions with sidebar badge and search #9543 的 serve 路由与边界重构的 import 改写均未受影响。交叉引用可解析:core 经 index 第 316 行 export *(sessionService)再导出 SessionIdCaseConflictError;dispatch 的错误映射使用字符串名 case,移除类型导入安全。

无新发现。第 6–8 轮的建议级推迟项(D7-1…D7-5)维持记录在案,均非阻塞。

测试证据:无人值守运行——此处从不执行 PR 代码,仅引用 PR 自身 CI。97868f7fbd 上 Security Checks、Qwen Live Host CI、SDK Java、Desktop Shell(双平台)、Real daemon E2E 与 Java 矩阵已绿;Test (ubuntu-latest, Node 22.x)Serve A/B 仍在运行。本 head 上的 cancelled 行是并发去重的重复工作流启动,不是失败;macos/windows 单测腿与无沙箱集成任务为 skipped,与此前全绿 head 相同。沙箱验证:中心主张是行为性的,钉住全部 14 个守卫点的变异矩阵测于合并前 head——其后的差异虽是合并性的,但恰好落在承载该主张的文件上。针对本 head 的 @qwen-code /verify 轮正在运行(run 32574190233),上方 Serve A/B 检查会在本 head 上重测 daemon 级 409 → 200 翻转;两者共同端到端证实合并后 active 副本采用成立、严格表面仍严格。真实场景 tmux 测试:本次无人值守运行 N/A。作者 PR 正文中的测试数字仍为作者声明;证据是 CI 与沙箱通道。

Qwen Code · qwen3.8-max

Reviewed at 97868f7fbd5893cc7d8db3886a5a8b9c01e2e1ea · re-run with @qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — the one open question after eight clean rounds was whether the merge of origin/main would survive contact with this diff; it did (one import-line conflict, resolved correctly, everything else byte-preserved or cleanly combined). The withheld point is CI on this exact head still landing and the in-flight sandboxed round — both mechanical now, not doubtful.

Stepping back over the whole arc:

  • The problem was real, the fix is the minimal version of it, and that hasn't changed. Four witnessed regressions from the merged feat(cli): Add standalone conversation isolation primitives #9341 plus the restore double-scan; every hunk still maps 1:1 onto those items after the merge. My independent proposal for PR2A provenance/identity tightening: four narrower behaviour regressions #9489 and the landed design are the same; I looked for a simpler path in earlier rounds and didn't find one.
  • The merge was the right move and was done carefully. Rather than force-push a rebase over eight reviewed heads, the author merged main in, and the merge commit proves it: one conflict in 22 files, in exactly the file main's boundary refactor renamed, resolved toward the PR's semantics on main's new paths. Main's own additions (feat(web-shell): Bind GitHub PRs to sessions with sidebar badge and search #9543 session-PR binding, the acp/serve boundary refactor) are intact alongside the fix.
  • The contract stays fail-closed everywhere I traced it. Exact same-spelling conflicts adopt the active copy (lease-protected); differently spelled pairs, archive/unarchive, batch routes, and 'any'/'archived' surfaces still reject; delete removes both copies under the writer lease. Round 6's mutation matrix pinned all 14 guard points at the pre-merge head, and the delta since is merge-only.
  • What's still in flight is evidence, not doubt. Test (ubuntu-latest, Node 22.x) and Serve A/B are running on this head; the serve A/B re-measures the daemon-level 409 → 200 flip after the merge, and the sandboxed round re-proves it end-to-end.

Reservations, named for the record: the suggestion-level deferrals from rounds 6–8 (D7-1…D7-5, incl. the untested ordinary-collision guard, which fails toward the safe ambiguous-owner response) are tracked in-thread and should land as a follow-up issue around merge; the standing CHANGES_REQUESTED states on this PR are this bot's own stale reviews at earlier heads — every blocking finding in them is resolved and re-reviewed, and a fresh approval supersedes them.

Verdict: approve — approval deferred until CI lands green on this head; the marker below carries the commit pin.

中文说明

总体判断:八轮干净审查之后唯一悬而未决的问题是合并 origin/main 能否与本 diff 相容——答案是能(22 个文件仅一处 import 行冲突,解决正确,其余要么字节级保留要么干净合并)。扣掉的一分是本 head 的 CI 尚未全部落地、沙箱轮仍在运行——两者如今是机械流程,不是存疑。

回看整条弧线:

  • 问题真实、修复已是最小形态,这一点没有变。 已见证的 feat(cli): Add standalone conversation isolation primitives #9341 四项回归加 restore 重复扫描;合并后每个 hunk 仍与这些条目一一对应。我对 PR2A provenance/identity tightening: four narrower behaviour regressions #9489 的独立方案与落地设计相同;更早几轮里找过更简路径,没有找到。
  • 合并是正确动作,且做得仔细。 作者没有在八个已审查 head 上 force-push 变基,而是合入 main,合并提交也证明了这一点:22 个文件仅一处冲突,恰在 main 边界重构改名过的文件上,并朝着 main 新路径上的 PR 语义解决。main 自身的增量(feat(web-shell): Bind GitHub PRs to sessions with sidebar badge and search #9543 session-PR 绑定、acp/serve 边界重构)与修复完整并存。
  • 我追踪过的每一处契约都保持 fail-closed。 完全同拼写冲突采用 active 副本(有 lease 保护);不同拼写对、archive/unarchive、批量路由与 'any'/'archived' 表面仍拒绝;删除在 writer lease 下移除双副本。第 6 轮变异矩阵在合并前 head 上钉住全部 14 个守卫点,其后的差异仅为合并。
  • 仍在进行中的是证据,不是疑虑。 Test (ubuntu-latest, Node 22.x)Serve A/B 正在本 head 上运行;serve A/B 会在合并后重测 daemon 级 409 → 200 翻转,沙箱轮会端到端复证。

保留意见,记录在案:第 6–8 轮的建议级推迟项(D7-1…D7-5,含未测试的普通碰撞守卫——它朝安全的歧义应答方向失败)已在帖中跟踪,应在合并前后建后续 issue;当前 PR 上的 CHANGES_REQUESTED 状态均为本机器人在更早 head 上的过时评审——其中所有阻塞项均已解决并复审,新批准会取代它们。

结论:批准——批准推迟到本 head 的 CI 全绿,下方标记携带提交钉扎。

Qwen Code · qwen3.8-max

Reviewed at 97868f7fbd5893cc7d8db3886a5a8b9c01e2e1ea · 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.

Needs some rethinking — see my notes above. Two concrete items: (1) the merge-commit CI failures in session-telemetry.test.ts are coupled to the activeInRuntime change (the prior merge in this area, #9512, had this check green), and (2) item 5's exact-spelling fast path is described and test-planned but not implemented. 🙏

doudouOUC and others added 4 commits August 20, 2026 17:45
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Batch case-insensitive transcript lookups for multi-thread waits and preserve organization metadata when live and persisted session IDs differ only by case.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Comment thread packages/cli/src/serve/server/session-list.ts Outdated
Comment thread packages/cli/src/serve/live/live-task-service.ts Outdated
Comment thread packages/cli/src/serve/live/live-task-service.ts Outdated
Comment thread packages/cli/src/serve/server/session-list.ts Outdated
Comment thread packages/cli/src/serve/server/session-list.ts Outdated
Comment thread packages/cli/src/serve/routes/session.ts Outdated
Comment thread packages/core/src/services/sessionService.test.ts Outdated
Comment thread packages/cli/src/serve/live/live-task-service.ts Outdated
Comment thread packages/core/src/services/sessionService.ts Outdated
Comment thread packages/cli/src/serve/acp-http/transport.test.ts Outdated
doudouOUC and others added 3 commits August 20, 2026 21:31
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

@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.

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

  • session-archive.ts:571 non-self case conflicts are flattened into SessionConflictError's hardcoded both-states message — already recorded in the round-6 deferred list (session-archive.ts:567)
  • integration-tests/cli/qwen-serve-routes.test.ts is collected by no workspace test command — already recorded as F6 in round 3; this round ran the suite locally against the bundled build (36/36 green)
  • transport.test.ts:4639 ACP conflict test seeds byte-identical fixtures and cannot detect which copy was adopted — already reported as R2-2 (round 2, byte-identical-fixtures pattern)
  • multi-workspace-sessions.test.ts:4514 both-states export assertion cannot detect which state was served — already recorded as N2 (round-3 deferred list)
  • server.test.ts:23715 'reads and exports the active copy of an exact persisted conflict' cannot detect which copy was served — same byte-identical-fixtures pattern already reported as R2-2 / recorded as N2
  • server.test.ts:25323 '%s restores active/archive conflicted sessions from the active copy' cannot detect which copy the restore used — same byte-identical-fixtures pattern already reported as R2-2 / recorded as N2

Not explored to full depth (tool budget reached): "agent 1c": none — but I did not run the full typecheck/build (soft-budget decision); compilation-level coherence was verified by reading imports and signatures instead..

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

  • packages/cli/src/serve/conversations/session-source.test.ts:276 — [probe] D7-1 conflicted-transcript test cannot detect which copy was adopted (mutant adopting archived passes all 49 tests)
  • packages/cli/src/serve/routes/session.ts:1804 — [probe] D7-2 new ordinary-collision guard on the live-owner branch has no test pairing (disabling mutant keeps 5381 tests green)
  • packages/cli/src/serve/routes/session.ts:1843 — [probe] D7-3 unqualified transcript reads 409 on multi-workspace daemons while qualified reads and load return 200
  • packages/cli/src/serve/conversations/conversation-workspace.ts:213 — [probe] D7-4 discard-path ENOENT narrowing unpinned — broadening to all io_error ships green
  • packages/core/src/services/sessionService.test.ts:2898 — [probe] D7-5 resolver rewrites dropped the pin that the requested spelling itself is returned when readable (occupancy-self-skip mutant passes all 18 tests)
中文说明

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

未探索到全部深度(达到工具调用预算):"agent 1c"none — but I did not run the full typecheck/build (soft-budget decision); compilation-level coherence was verified by reading imports and signatures instead.

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

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

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

Copy link
Copy Markdown
Collaborator Author

Addressed the round-7 blocking feedback in 78031b16bf.

Item Action Result
R7-1 async directory mock Fixed The resolver test now exercises fs.promises.readdir and asserts the legacy-twin existence probe.
R1-6 internal qualified reads Fixed Internal transcript/export routes read exact active/archive conflicts from the active copy; archived and mutation routes remain strict.
Round-7 suggestion-level probes Deferred No scope expansion after the convergence threshold, as recorded by the reviewer.

Validation: core SessionService 176/176; multi-workspace sessions 120/120; repository build and typecheck; changed-file Prettier/ESLint; diff check.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed.

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

  • test-matrix finding — liveOwner-branch ordinary-collision block untested (routes/session.ts:1805) — already recorded as D7-2 (round-7 deferred list)
  • chunk-1 reverse-audit finding — non-self case conflicts flattened into SessionConflictError's hardcoded both-states message — already recorded in the round-6 deferred list (session-archive.ts:567)
  • chunk-1 reverse-audit finding — stale archived copy never reconciled after both-states adoption — already recorded in the round-6 deferred list (session-archive.ts:589) and blocker thread 3832630516
  • chunk-3 reverse-audit finding — allowActiveConflict unthreaded through the internal-entry loop and multi-workspace scan — already recorded as D7-3 (round-7 deferred list), R1-7 thread
  • chunk-6 reverse-audit finding — requested-spelling self-resolution pin dropped by the readdir spy migration — already recorded as D7-5 (round-7 deferred list)
  • chunk-1 reverse-audit (round 4) finding — transport both-states test seeds byte-identical fixtures — already reported as R2-2 (round 2)
  • chunk-4 reverse-audit (round 4) finding — server.test.ts both-states export test seeds byte-identical fixtures — already reported as R2-2 / recorded as N2 (round 3)

Not explored to full depth (tool budget reached): chunk 6: running sessionService.test.ts findSessionIdIgnoringCase tests (worktree has no installed dependencies; install + build prerequisite exceeds budget).

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

  • packages/cli/src/serve/conversations/session-source.ts:109 — [probe] SAFE_TRANSCRIPT_NAME_PATTERN admits '.' and '..' dot segments
  • packages/cli/src/serve/server/session-archive.ts:546 — [probe] Both-states conflict-vs-adoption verdict decided by readdir enumeration order
中文说明

已审查。

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

未探索到全部深度(达到工具调用预算):chunk 6:running sessionService.test.ts findSessionIdIgnoringCase tests (worktree has no installed dependencies; install + build prerequisite exceeds budget)

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

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

@doudouOUC
doudouOUC requested a review from qqqys August 22, 2026 08:01
@doudouOUC doudouOUC changed the title fix(cli): Restore PR2A session behaviors fix(cli): Recover sessions across archive races Aug 22, 2026
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

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

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

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

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

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

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

Verification report

PR #9513 Deep Verification (follow-up round 6) — fix(cli): Recover sessions across archive races

Verdict: merge-ready — 2991/2991 scripted assertions passed, 0 failed. Verified head 78031b16bf944bdf6d02c89aabdb8a82f947fbf8 (git rev-parse HEAD^2; equals the snapshot's headRefOid). Merge commit bd635b1c83, base tip 7bc0d80998 (main moved again since round 5's 04886c4354; the A/B control was rebuilt fresh). Round 5's sole substantive finding F3 is fixed: the narrowing commit f69edcff ("Drop the review-driven mixed-case expansion") removed the case-alias organization collapse entirely, re-measured fresh in both probe forms. F1/F2 nits re-measured (status table below). New this round: a true wire-level A/B — the PR's own integration test run against a BASE-built daemon returns 409, against head 200.

中文 — 判定:✅ 通过(agent 判定)

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

  • 结论merge-ready(跟进轮 6)。2991/2991 条脚本化断言全部通过,0 失败。本轮 head 为 78031b16bf(base tip 7bc0d80998)。上一轮的 F3(低)已被收窄提交 f69edcff 修复——case-alias 组织折叠机制整体移除,两种探针形态重测 10/10,head 与 base 行为一致。
  • 上一轮发现状态:F1(config.test.ts 顺序级联,nit)stands(M-config 突变下 15002 ms 复现,隔离单跑 1/1 通过,字节级同签名);F2(allowActiveConflict 可达性,nit)superseded——门现在也从普通(非 workspace-qualified)路由可达,且两族门均被钉住;F3(低)fixed(机制随收窄移除,重测确认)。
  • A/B 结论:中心主张在新 base 上重测成立——「同一拼写在 active 与 archived 均可读」的会话,base 拒绝(冲突),head 从 active 副本加载/恢复/导出/删除;32/32 服务级单元格 + 真实 daemon 线级 A/B(base 409 / head 200)。见证图 01-ab-both-states-base-vs-head.png04-targeted-gates-and-wire-ab.png
  • 变异:14 个守卫点全部被钉住(0 幸存);M-batch 的钉子在第二个测试文件中(multi-workspace-sessions.test.ts),换文件后杀死。见证图 03-mutation-matrix-live-kill-plus-recorded.png
  • 门槛:13 个套件全绿(2932 测试),含真实 spawn daemon 的 integration 36/36。
  • 未覆盖:逐 commit 归因(浅克隆,链条断裂)、真实模型 E2E、Windows/大小写不敏感文件系统、全仓 typecheck/lint、5 轮抖动门、base 线级全套件回归(仅过滤单测)。

Previous-finding status (follow-up round 6)

Round 5 verified head 561b29761a; this round's head is 78031b16bf. The PR was narrowed in between (commits b9ae2f6778031b16bf, incl. f69edcff dropping the mixed-case expansion), so every row was re-measured fresh — nothing carried by identity. Unlike round 5, several round-5-era objects DO exist in this object store (561b29761a, f69edcff, 03f1dcb1, b9ae2f67, 5517d23f), which let the production-surface delta be quantified: 7 files / +286 −389 on the session surface, incl. session-organization-service.ts losing the collapse (−27). 88fddbe9/de45968b remain absent, so the chain is still broken for per-commit attribution.

# Finding (prior rounds) Severity Status at head 78031b16bf (round 6)
F1 Order-dependent cascade in packages/core/src/config/config.test.ts: releases a pending lease while a real baseline read is gated goes red (~15 s) when an earlier test in the same file fails first; green in isolation nit Stands (test-suite side, not a PR defect). Fresh M3-equivalent mutant: full-file run red with the intended kill (21 ms) plus the lease test at 15002 ms; the same lease test in isolation under the same mutant passed 1/1 in 79 ms (1 passed | 539 skipped). Byte-identical signature to rounds 1–5.
F2 allowActiveConflict branch reachable only for workspace-qualified requests nit Superseded. At the new head the flag is passed from five doors spanning BOTH plain and workspace-qualified routes (plain transcript/export fallback resolver ×3, handleSessionExport, workspace transcript route). All families are pinned: disabling all five doors turned server.test.ts red (1/1077), disabling the two workspace doors turned multi-workspace-sessions.test.ts red (2/121). Residual coverage nit below.
F3 Verified-alias organization collapse absorbs an unreadable case twin's metadata (incl. pin migration) low Fixed. The caseAliasesResolvedToSession flag and the case-insensitive store-key collapse are gone from the codebase (grep: zero hits; updateSessionOrganization now touches only the exact-spelling key). Re-measured with the round-5 fixtures in both forms (P1 absorption, P2 pin migration): 10/10 assertions on both arms — twin entries survive, no field migrates, head behavior now identical to base. Witness 02-f3-corner-probes-base-vs-head.png.

Scope selection

Central claim (re-measured at the new base): a session whose transcript is readable in both active and archived states — the residue of a crash inside archiveSessions — is loadable/resumable from the active copy on the REST load, REST restore, daemon ACP restore, and embedded ACP restore surfaces, where base rejects with session_conflict; transcript/export reads return the active copy; explicit delete removes both copies; archive/unarchive mutations and differently-spelled conflicts stay strict.

Secondary claims (re-covered by gates + mutation kills): creation-metadata reads tolerate a concurrent archive move behind a path-safety gate; legacy parent ids (nil/v6/v7/agent-suffixed) resolve again; ENOENT races during directory-identity inspection report "already gone"; the catalog scan is async and runs once inside the restore lock.

Round-5→6 delta: the narrowing kept items 1–5 of #9489 and dropped the organization/live-task mixed-case expansion (which is what carried F3). The delta's surviving guards were all re-proven by fresh kills (matrix below).

Explicitly out of scope (also under Not covered): daemon E2E with a live model, Windows/case-insensitive-filesystem paths, repo-wide typecheck/lint, the workflow's 5-round flakiness gate.

Central claim — A/B load-bearing proof

Service-level harness (both trees' compiled dist, arm-coded expectations)

Harness harness/ab-both-states.mjs drives each tree's dist by absolute path (head at merge commit bd635b1c83; base rebuilt at 7bc0d80998 in tmp/base-tree, core+cli exit 0, logs build-base-*.log). 16 cells per arm; predicted base reds count as passes. Witness: 01-ab-both-states-base-vs-head.png (re-executed live inside the capture).

Cell Oracle base (7bc0d80998) head (78031b16bf)
C0 sanity findSessionIdIgnoringCase(active-only) resolves ✓ resolves ✓
C1 control getSessionLocation(both-states) 'conflict' 'conflict' (unchanged)
C2 central assertSessionLoadable(+allowActiveConflict) throws SessionConflictError returns 'active'
C3 control assertSessionLoadable (no flag) throws conflict throws conflict (default stays strict)
C4 strictness assertSessionArchived(both-states) throws conflict throws conflict (mutations strict)
C5 fail-closed genuine readable case twins throws, candidate undefined throws, candidate undefined
C6 control lowercase request → uppercase-only persisted resolves uppercase resolves uppercase
C7 flip resolveSessionIdForRestore(both-states) export absent returns requested spelling
C7b strictness resolveSessionIdForRestore(twin) export absent throws SessionConflictError
C8 flip assertSessionRestorable(both-states) export absent returns 'active'
C9 content loadSession(both-states) marker ACTIVE copy ACTIVE copy (pre-existing preference)
C10 flip deleteDaemonSessions(both-states) errors=1, both files stay removed=1, both copies gone
C11 flip metadata read racing an archive move undefined (probe mismatch) metadata resolves
C12 control archived metadata, no race resolves resolves
C13 guard path-unsafe id (../evil-segment) probes location (1 store call) 0 store calls (gated first)
C14 flip nil-UUID parent lineage undefined (shape gate) lineage followed (standalone/legacy)

Result: 32/32 assertions (16 per arm); the seven flip cells (C2, C7, C8, C10, C11, C13, C14) flip, all controls hold on both arms.

Base-control hygiene: the PR changes no package.json/package-lock.json (verified), so reusing the root dependency tree is a clean control; the base tree's node_modules was wired per-entry with @qwen-code/qwen-code-core and @qwen-code/qwen-code re-pointed into the base tree, and readlink -f returned …/tmp/base-tree/packages/{core,cli} before any cell ran.

Wire-level A/B (real spawned daemon)

The PR's own integration test reads exact conflicts from active and maps archived/unavailable snapshots to 409 was run against both daemon builds via a scratch vitest config (the repo's globalSetup.ts force-overwrites TEST_CLI_PATH to the head bundle — see Methodology harness fault 3):

Arm Daemon binary Both-states transcript read
base tmp/base-tree/packages/cli/dist/index.js 409 — red (expected 409 to be 200)
head (validity control) head bundle dist/cli.js 200 + active marker — green
head (gate) head dist, full suite 36/36 green incl. the both-states test

Witness: 04-targeted-gates-and-wire-ab.png. The base-arm harness itself was validated in both directions (same config + head bundle → green, + base build → red), so the flip is attributable to the daemon code, not the harness.

Mutation matrix (14 fresh kills at the new head, 0 survivors)

Scratch worktree tmp/mut-tree at the merge commit; every mutated suite first ran green unmutated there (controls: 540, 181, 1077, 54, 49, 12, 19, 446). Mutations applied as exact string replacements with occurrence-count verification, reverted via git checkout (final git status: empty). Witness: 03-mutation-matrix-live-kill-plus-recorded.png (includes a live M-load kill). Raw logs logs/mut-*.log, logs/ctrl-*.log.

# Guard reverted Suite Red tests (behavioral)
M-config writer-activation conflict tolerance (config.ts) config.test.ts 1 intended — adopts the active transcript when writer activation sees both states (21 ms) — plus the F1 cascade test at 15002 ms (see status table)
M-load assertSessionLoadable allowActiveConflict branch (session-archive.ts) session-archive.test.ts 1 — reads the active copy after restore selected an exact conflict (promise rejected "SessionConflictError…" instead of resolving)
M-restore resolveSessionIdForRestore same-spelling tolerance session-archive.test.ts 1 — resolves an exact active/archive conflict only for restore
M-delete delete's removed conflict rejection session-archive.test.ts 1 — deletes both copies of an exact active/archive conflict
M-src-race tolerant active→archived metadata read (session-source.ts) session-source.test.ts 3 — resolves a transcript that a concurrent archive moves mid-read, reads archived transcripts with the archived state, rejects a transcript whose creation metadata is unreadable (read-sequence ['active','active'] vs expected 4-call sequence)
M-src-path path-safety + Windows-device gate session-source.test.ts 7 — refuses path-unsafe ids without touching the store + all 6 device names
M-src-lineage parent shape gate (isValidSessionId) re-added session-source.test.ts 6 — the malformed-parent (…010), nil-UUID (…016) and v7 (…017) classify/lineage/unproven cases (agent-suffixed parents pass the old gate's regex, so they correctly do not red)
M-identity ENOENT mid-inspection → identity_changed revert (conversation-directory-identity.ts) conversation-directory-identity.test.ts 1 — reports a child deleted between the lstat and the realpath as missing
M-workspace ENOENT root-race branch removed (conversation-workspace.ts) conversation-workspace.test.ts 1 — treats a root that vanishes mid-inspection as already discarded
M-async async readdirreaddirSync (sessionService.ts) sessionService.test.ts 15 — implementation pinning, by design: this PR retargeted the suite's mocks from readdirSync to fs.promises.readdir, so the sync revert starves the resolver through the global mock. Behavioral equivalence proven independently by A/B cells C0/C5/C6 on a real filesystem
M-acp embedded-ACP resolver tolerance (acpAgent.ts) acpAgent.test.ts 2 — load/resume restores an exact persisted session from active when both states exist
M-doors all 5 allowActiveConflict doors off (routes/session.ts) server.test.ts 1 — reads and exports the active copy of an exact persisted conflict
M-doors-wq the 2 workspace-qualified doors only multi-workspace-sessions.test.ts 2 — reads exact active/archive conflicts from the active copy in an internal workspace, enforces workspace, trust, format, and active-session export boundaries
M-batch batch pre-check conflict tolerance (resolveQualifiedSessionRuntime loop) server.test.ts → multi-workspace-sessions.test.ts Survived server.test.ts (1077 green); killed in the pinning file — reads exact active/archive conflicts from the active copy in an internal workspace (see classification below)

Survivors: 0/14 after correction. Positive controls: every cited suite was made red by a point mutation collected through the same command that ran it green unmutated. The M-batch episode is the documented "control lives in a second file" case: the guard protects workspace-qualified internal transcript/export reads, which only multi-workspace-sessions.test.ts exercises; server.test.ts never drives a both-states session through that pre-check. The guard itself is correct and load-bearing (the multiws test reds on transcript.status 409 vs 200 when the tolerance is reverted) — the takeaway is a test-placement note, not a defect.

Targeted gates (at the merge commit)

Witness: 04-targeted-gates-and-wire-ab.png. Raw logs logs/ctrl-*.log, logs/gate-*.log. Controls marked (ctrl) ran in tmp/mut-tree verified git status-clean at the same commit; the rest ran in the main tree.

Gate Result
core: config.test.ts (ctrl) 540/540 passed
core: sessionService.test.ts (ctrl) 181/181 passed
cli: server.test.ts (ctrl) 1077/1077 passed, 0 skipped
cli: session-archive.test.ts (ctrl) 54/54 passed
cli: session-source.test.ts (ctrl) 49/49 passed
cli: conversation-directory-identity.test.ts (ctrl) 12/12 passed
cli: conversation-workspace.test.ts (ctrl) 19/19 passed
cli: acpAgent.test.ts (ctrl) 446/446 passed
cli: transport.test.ts 340/340 passed
cli: multi-workspace-sessions.test.ts 121/121 passed
cli: session-telemetry.test.ts 9/9 passed
cli: workspace-qualified-acp.test.ts 48/48 passed
integration: qwen-serve-routes.test.ts (real spawned daemon, dummy auth, no model calls) 36/36 passed, incl. the new both-states transcript test — the central claim pinned at the daemon wire

Gate liveness is proven by the mutation round itself. (Round 5's ::1 env-skip did not appear this round: 1077/1077 with zero skips.)

Findings (non-blocking notes only)

No blocking findings. The central claim held its A/B at the new base and at the wire; every freshly-run guard held its mutation kill; the sole prior substantive finding (F3) was fixed by the narrowing and re-measured clean.

N1 (nit, carried) — F1 order-dependent cascade in config.test.ts

Re-measured fresh; signature unchanged from rounds 1–5 (see status table). This is a suite-hygiene issue (a failed earlier test leaks state that times out the lease test at ~15 s), not a defect in this PR's production code.

N2 (nit, residual of superseded F2) — fallback door branches pinned collectively

The three activeInRuntime(runtime, true) fallback branches inside resolveTranscriptSessionRuntime (legacy-primary, live-owner, require-primary paths) are pinned collectively by one server.test.ts test; no test distinguishes which branch carried a both-states recovery. The two workspace-qualified doors are individually observable via the multi-workspace suite. A future test that fails one specific fallback path with a both-states session would close this; not required for this PR.

N3 (note) — async-readdir pinned by mock retargeting, not behavior

M-async's 15 reds are the suite's deliberate implementation pin (the PR retargeted the mocks to fs.promises.readdir). Behavioral equivalence of the sync/async scan was proven on a real filesystem by A/B cells C0/C5/C6. No action needed.

Not covered

  • Per-commit attribution. Depth-2 checkout: git rev-list --count HEAD^1..HEAD^2 returns 1 against the snapshot's 28 commits. More round-5-era objects are reachable this round (561b29761a, f69edcff, 03f1dcb1, b9ae2f67, 5517d23f present; 88fddbe9, de45968b absent), which allowed the production-surface delta tree-diff to be quantified (7 files, +286/−389 incl. the collapse removal in session-organization-service.ts), but the commit chain is still broken, so no per-commit behavioral attribution was built. Verified the aggregate HEAD^1..HEAD diff instead; no network fetches made.
  • Round-4 secondary-guard kills remain uncarried (already disclosed round 5); most of that surface was dropped by the narrowing, and the surviving surface is covered by this round's 14 kills.
  • Daemon E2E with real model calls — integration spawns a real daemon with dummy auth and issues no model calls; the PR declares this out of scope.
  • ACP HTTP dispatch at the wiredispatch.ts shares the same exported helpers (resolveSessionIdForRestore, assertSessionRestorable) that the A/B and mutations proved; the ACP transport/workspace-qualified suites passed in the gates. Not driven at the wire separately.
  • Base-arm wire regression beyond the filtered test — the base daemon ran the both-states test only (-t filter), not the full 36-test suite.
  • Windows behavior / case-insensitive filesystem aliasing — Linux unit/integration suites only.
  • Repo-wide typecheck / ESLint / Prettier — all trees built clean during this round's builds (base core+cli, mut-tree rebuilds).
  • The workflow's 5-round flakiness gate — owned by the workflow lane.

Methodology

Environment: node:22-bookworm container, Node v22.23.2, repo at merge commit bd635b1c83 (head 78031b16bf, base tip 7bc0d80998) with npm ci && npm run build pre-run by the workflow; bundle dist/cli.js present. Identity check: git rev-parse HEAD^2 equals the snapshot's headRefOid; the snapshot's baseRefOid (83f2589273) is stale relative to this round's merge ref and was not used — HEAD^1 is authoritative on a merge-ref checkout. Base control: git worktree add tmp/base-tree HEAD^1; root dependency tree reused (PR changes no dependency file) with per-entry symlinks and the two changed-package links re-pointed into the base tree (readlink -f verified before any cell); nested workspace node_modules (channels/feishu etc.) linked; scripts/generate-git-commit-info.js run; core+cli rebuilt there (exit 0). Mutation tree: tmp/mut-tree at the merge commit, same wiring, dist prerequisites symlinked from the head build (guard-only; mutated suites import source via relative paths / the cli vitest alias to ../core/index.ts), git status verified empty after every revert. A/B harness imports each tree's compiled dist by absolute path under mkdtemp QWEN_HOME/QWEN_RUNTIME_DIR sandboxes with arm-coded expectations. F3 probes re-run the round-5 fixtures against both trees' SessionOrganizationService dist. Wire A/B uses scratch vitest configs because the repo's integration-tests/globalSetup.ts:98 unconditionally overwrites TEST_CLI_PATH with the head bundle. Harness faults found and fixed this round: (1) root node_modules/.bin missed by the wiring glob (dotfile) → tsc: Permission denied → linked explicitly; (2) base cli build needed channels/feishu nested deps + generated git-commit.ts; (3) the first base-arm wire run was invalid — globalSetup clobbered TEST_CLI_PATH, so it secretly ran the head bundle and passed; detected because the pass contradicted the service-level prediction, replaced by the scratch-config approach with a both-direction validity control; (4) mutations.mjs red-name regex missed ANSI-split lines and its F1-isolation branch initially re-ran the full file — names extracted via ANSI-stripped greps and the isolation re-run with -t; (5) M-batch first ran against the wrong suite (server.test.ts) and survived — the pin lives in multi-workspace-sessions.test.ts, per the "land the control in the mutated file's suite" rule. Evidence images produced with scripts/verify-capture.mjs; raw per-cell, per-gate, per-mutation logs live in logs/. Assertion accounting: 2932 gate tests + 32 service-level A/B cells + 2 wire-A/B cells + 10 F3 probe checks + 14 mutation kill-verdicts + 1 F1 isolation check = 2991/2991; fail counts unexpected outcomes only — zero this round (predicted base-arm reds and mutant reds are encoded as passing assertions).

Qwen Code · sandboxed verification

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/cli/src/acp-integration/acpAgent.test.ts: (cd packages/cli) npx --no-install vitest run ./src/acp-integration/acpAgent.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/conversations/conversation-workspace.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/conversations/conversation-workspace.test.ts
file packages/cli/src/serve/conversations/session-source.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/conversations/session-source.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/routes/session-telemetry.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/routes/session-telemetry.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/cli/src/utils/conversation-directory-identity.test.ts: (cd packages/cli) npx --no-install vitest run ./src/utils/conversation-directory-identity.test.ts
file packages/core/src/config/config.test.ts: (cd packages/core) npx --no-install vitest run ./src/config/config.test.ts
file packages/core/src/services/sessionService.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/sessionService.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/cli/src/acp-integration/acpAgent.test.ts: PPPP
  packages/cli/src/serve/acp-http/transport.test.ts: PPPP
  packages/cli/src/serve/conversations/conversation-workspace.test.ts: PPPP
  packages/cli/src/serve/conversations/session-source.test.ts: PPPP
  packages/cli/src/serve/multi-workspace-sessions.test.ts: PPPP
  packages/cli/src/serve/routes/session-telemetry.test.ts: PPPP
  packages/cli/src/serve/server.test.ts: PPPP
  packages/cli/src/serve/server/session-archive.test.ts: PPP
  packages/cli/src/utils/conversation-directory-identity.test.ts: PPP
  packages/core/src/config/config.test.ts: PPP
  packages/core/src/services/sessionService.test.ts: PPP

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

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/acp-integration/acpAgent.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/conversations/conversation-workspace.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/conversations/session-source.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/routes/session-telemetry.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/cli/src/utils/conversation-directory-identity.test.ts: P (exit 0)
round 1 · packages/core/src/config/config.test.ts: P (exit 0)
round 1 · packages/core/src/services/sessionService.test.ts: P (exit 0)
round 2 · packages/cli/src/acp-integration/acpAgent.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/conversations/conversation-workspace.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/conversations/session-source.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/routes/session-telemetry.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/cli/src/utils/conversation-directory-identity.test.ts: P (exit 0)
round 2 · packages/core/src/config/config.test.ts: P (exit 0)
round 2 · packages/core/src/services/sessionService.test.ts: P (exit 0)
round 3 · packages/cli/src/acp-integration/acpAgent.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/conversations/conversation-workspace.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/conversations/session-source.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/routes/session-telemetry.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/cli/src/utils/conversation-directory-identity.test.ts: P (exit 0)
round 3 · packages/core/src/config/config.test.ts: P (exit 0)
round 3 · packages/core/src/services/sessionService.test.ts: P (exit 0)
round 4 · packages/cli/src/acp-integration/acpAgent.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/conversations/conversation-workspace.test.ts: P (exit 0)
round 4 · packages/cli/src/serve/conversations/session-source.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/routes/session-telemetry.test.ts: P (exit 0)
round 4 · packages/cli/src/serve/server.test.ts: P (exit 0)

Evidence images

01-ab-both-states-base-vs-head

02-f3-corner-probes-base-vs-head

03-mutation-matrix-live-kill-plus-recorded

04-targeted-gates-and-wire-ab

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

Qwen Code · sandboxed verification

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅

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

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully. The review pipeline failed before a review could be posted. A transient error is retried automatically; if you are seeing this, retry with @qwen-code /review. See workflow logs.

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Resolved the merge conflict with current main in 97868f7fbd without force-pushing.

Item Action Verification
Runtime session-source rename conflicted with the archive-race and legacy-parent fixes Kept the PR behavior at the new runtime/live-session-source location and adopted the new relative import path 49/49 live-session-source tests passed
Session resolver compatibility after the merge Kept the PR resolver behavior while taking current main 181/181 session-service tests passed
Final merge commit Merged origin/main normally; no unrelated manual edits Full workspace build and typecheck passed; pre-commit ESLint/Prettier and diff checks passed

No review threads were involved in this conflict resolution.

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

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

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

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

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

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

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

Verification report

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

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

Scripted assertions: 3021 passed · 0 failed · 3021 total

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

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

  • 结论merge-ready(跟进轮 7)。3021/3021 条脚本化断言全部通过,0 失败。本轮 head 为 97868f7fbd(= 快照 headRefOid;相对轮 6 的新提交是把 origin/main 合入 PR 分支),base tip 移到 b455bad5e5(main 含 acp-integration 与 live-session-source 重构,A/B 控制臂全新重建)。
  • 上一轮发现状态:F1(config.test.ts 顺序级联,nit)stands(M-config 突变下 15002 ms 复现、隔离单跑 1/1 通过,签名与轮 1–6 字节级一致);F2 残余 N2(fallback 门集体钉住)stands;F3(low,轮 6 已修)fixed——机制仍零命中,corner 探针两臂各 10/10 重测,head 与 base 行为一致。
  • A/B 结论:中心主张在新 base 上重测成立——「同一拼写在 active 与 archived 均可读」的会话,base 拒绝/丢元数据/双份残留,head 从 active 副本加载/恢复/导出/删除;服务级 40/40(每臂 20 格,11 个翻转格全翻转),真实 daemon 线级 A/B base 409 / head 200。见证图 01-ab-both-states-base-vs-head.png04-wire-ab-base-409-head-200.png
  • 变异:18 个变异运行;15 个守卫点被钉住;M-batch 在 server.test.ts 存活(钉子位于 multi-workspace 套件,与轮 6 同分类);新增 M-ambig(live-owner 歧义扫描块,本轮 diff 新表面)两套件均存活 → 记为 N4(coverage gap,nit,不阻塞)。
  • 门槛:14 个套件全绿(2939 测试,含真实 spawn daemon 的 integration 36/36 与 acp-serve 边界守卫 8/8);server.test.ts 有 1 个环境性 skip(容器无 IPv6 loopback,测试自带 skipIf 正确生效)。
  • 未覆盖:逐 commit 归因(浅克隆,29 个提交仅 1 个本地可达)、真实模型 E2E、Windows/大小写不敏感文件系统、全仓 typecheck/lint、5 轮抖动门。
Verification report

PR #9513 Deep Verification (follow-up round 7) — fix(cli): Recover sessions across archive races

Verdict: merge-ready — 3021/3021 scripted assertions passed, 0 failed. Verified head 97868f7fbd5893cc7d8db3886a5a8b9c01e2e1ea (git rev-parse HEAD^2; equals the snapshot's headRefOid). Merge commit 812b2c9a7f, base tip b455bad5e5 (main moved again since round 6's 7bc0d80998; the A/B control and every gate were rebuilt/re-run fresh at the new base — nothing carried by identity).

中文 — 判定:✅ 通过(agent 判定)

见上方中文摘要;下文表格为唯一数字来源。

Previous-finding status (follow-up round 7)

Round 6 verified head 78031b16bf at base 7bc0d80998; this round's head is 97868f7f (a merge of origin/main into the PR branch) at base b455bad5e5. The round-6 head object is absent from this round's depth-2 object store (fresh checkout; census: 1 of the snapshot's 29 commits reachable), so no tree-level delta against round-6 head was possible — the main delta 7bc0d80998..b455bad5e5 (89 files, +7642/−1398) was measured instead and overlaps the PR surface (live-session-source moved serve/live/runtime/ and rewritten, acp-integration/session/Session.ts touched, dispatch.ts/routes/session.ts/multi-workspace-sessions.test.ts import-path churn, new scripts/tests/acp-serve-boundary-guard.test.js). Every row below was therefore re-measured fresh; nothing was carried by input-closure identity.

# Finding (prior rounds) Severity Status at head 97868f7f (round 7)
F1 Order-dependent cascade in packages/core/src/config/config.test.ts: releases a pending lease while a real baseline read is gated goes red (~15 s) when an earlier test in the same file fails first; green in isolation nit Stands (suite-hygiene, not a PR defect). Fresh M-config mutant: full-file run red with the intended kill (14 ms) plus the lease test at 15002 ms; the same lease test in isolation under the same mutant passed 1/1 (1 passed | 539 skipped). Byte-identical signature to rounds 1–6.
F2 allowActiveConflict reachable only for workspace-qualified requests nit Superseded (round 6). Re-verified at the new head: the flag is passed from five doors spanning both plain and workspace-qualified routes (2 assertSessionLoadable({allowActiveConflict:true}) literals at routes/session.ts:1327/4047 + 3 activeInRuntime(…, true) calls at 1751/1797/1848). M-doors (all five off) turned server.test.ts red (1/1077); M-doors-wq (workspace door off) turned multi-workspace-sessions.test.ts red (1/121).
N2 (residual of F2) The three activeInRuntime(runtime, true) fallback branches pinned collectively by one server.test.ts test nit Stands. M-doors reds that one test; no test distinguishes which fallback branch carried a both-states recovery. Unchanged in shape from round 6.
F3 Verified-alias organization collapse absorbs an unreadable case twin's metadata (incl. pin migration) low Fixed (round 6, re-measured). Mechanism still absent (caseAliasesResolvedToSession / case-insensitive store-key collapse: zero grep hits). Corner probes re-run fresh in both forms (P1 absorption, P2 pin migration) against both trees' SessionOrganizationService dist: 10/10 on both arms — twin keys survive, no field migrates, head ≡ base. Witness 02-f3-corner-probes-base-vs-head.png.

Scope selection

Central claim (re-measured at the new base): a session whose transcript is readable in both active and archived states — the residue of a crash inside archiveSessions — is loadable/resumable from the active copy on the REST load/restore, daemon ACP restore, and embedded ACP restore surfaces, where base rejects with session_conflict; transcript/export reads return the active copy; explicit delete removes both copies; archive/unarchive mutations and differently-spelled or ambiguous case conflicts stay strict.

Secondary claims (re-covered by gates + mutation kills): creation-metadata reads tolerate a concurrent archive move behind a path-safety gate (SAFE_TRANSCRIPT_NAME_PATTERN + Windows device names); legacy parent ids (nil/v6/v7) resolve again while path-unsafe parents stay gated; ENOENT races during directory-identity inspection report "already gone"; the catalog scan is async (fs.promises.readdir).

Round-6→7 delta probes: (a) the main refactor's new boundary guard scripts/tests/acp-serve-boundary-guard.test.js passes at head (8/8) — the PR's adapted acpAgent.ts respects "keep acp-integration off serve internals"; (b) the effective diff's new surface — the live-owner ordinary-collision scan in resolveTranscriptSessionRuntime (sendAmbiguousSessionOwner on internal-runtime + ordinary-workspace collision) — was mutation-swept (M-ambig) and found unpinned (N4 below); (c) the daemon-ACP dispatch path, not driven at the wire in round 6, is now pinned through the shared helper (M-restore kills transport.test.ts, 2 reds).

Explicitly out of scope (also under Not covered): daemon E2E with a live model, Windows/case-insensitive-filesystem paths, repo-wide typecheck/lint, the workflow's 5-round flakiness gate.

Central claim — A/B load-bearing proof

Service-level harness (both trees' compiled dist, arm-coded expectations)

Harness harness/ab-both-states.mjs drives each tree's dist by absolute path (head at merge commit 812b2c9a7f; base rebuilt at b455bad5e5 in tmp/base-tree, core+cli exit 0, logs/build-base.log). 20 cells per arm; predicted base reds/absent-exports count as passes. Witness: 01-ab-both-states-base-vs-head.png (both arms re-executed live inside the capture).

Cell Oracle base (b455bad5e5) head (97868f7f)
C0 sanity findSessionIdIgnoringCase(active-only) resolves ✓ resolves ✓
C1 control getSessionLocation(both-states) 'conflict' 'conflict' (unchanged)
C2 central assertSessionLoadable(+allowActiveConflict) throws SessionConflictError returns 'active'
C3 control assertSessionLoadable (no flag) throws conflict throws conflict (default stays strict)
C4 strictness assertSessionArchived(both-states) throws conflict throws conflict (mutations strict)
C5 fail-closed readable case twins (one spelling per state) throws, candidate undefined throws, candidate undefined; exact-spelling loadability unaffected on both arms
C5b fail-closed resolveSessionIdForRestore(differently-spelled both-states) export absent throws SessionConflictError
C5c fail-closed assertSessionRestorable(storage ≠ requested spelling) export absent throws SessionConflictError naming the requested id
C6 control lowercase request → uppercase-only persisted resolves uppercase resolves uppercase
C7 flip resolveSessionIdForRestore(both-states) export absent returns requested spelling
C7b strictness resolveSessionIdForRestore(twin) export absent throws SessionConflictError
C8 flip assertSessionRestorable(both-states) export absent returns 'active'
C9 content loadSession(both-states) marker ACTIVE copy ACTIVE copy (pre-existing preference)
C10 flip deleteDaemonSessions(both-states) errors=1, both files stay removed=1, both copies gone
C11 flip metadata read racing an archive move undefined (metadata lost) resolves (kind=standalone)
C12 control archived metadata, no race resolves resolves
C13 guard path-unsafe id (../evil-segment) 2 store calls (location probe + read) 0 store calls (gated first)
C13b guard Windows device name (CON) 2 store calls 0 store calls
C14 flip nil-UUID parent lineage undefined (shape gate) lineage followed (legacy, parent standalone)
C14b control path-unsafe parent undefined undefined (gate holds for parents too)

Result: 40/40 assertions (20 per arm); the eleven flip cells (C2, C5b, C5c, C7, C7b, C8, C10, C11, C13, C13b, C14) flip, all controls hold on both arms.

Base-control hygiene: the PR changes no package.json/package-lock.json (verified in the effective diff), so reusing the root dependency tree is a clean control; tmp/base-tree/node_modules was wired per-entry with @qwen-code/qwen-code-core and @qwen-code/qwen-code re-pointed into the base tree, and readlink -f returned …/tmp/base-tree/packages/{core,cli} before any cell ran; nested per-package node_modules (core's @opentelemetry/ajv/ignore) mirrored. Two harness faults from round 6's list re-manifested and were fixed identically (the node_modules/.bin dotfile missed by the wiring glob; direct node build_package.js lacking npm's PATH augmentation).

Wire-level A/B (real spawned daemon)

The PR's own integration test reads exact conflicts from active and maps archived/unavailable snapshots to 409 was run against both daemon builds via a scratch vitest config (harness/wire-ab.config.mts) because the repo's globalSetup.ts force-overwrites TEST_CLI_PATH with the head bundle:

Arm Daemon binary Both-states transcript read
base tmp/base-tree/packages/cli/dist/index.js 409 — red (expected 409 to be 200)
head (validity control) head bundle dist/cli.js 200 + active marker — green
head (gate) head bundle, full suite 36/36 green incl. the both-states test

Witness: 04-wire-ab-base-409-head-200.png. Same config, only the binary differs between arms, so the flip is attributable to the daemon code.

Mutation matrix (18 runs at the new head; 15 guards pinned, 3 survivors classified)

Scratch worktree tmp/mut-tree at the merge commit (git status verified empty after every revert); every mutated suite ran green unmutated there first (controls in the gate table). Mutations applied as exact string replacements with occurrence-count verification. Witness: 03-mutation-matrix-and-gates.png. Raw logs logs/mut-*.log, logs/gate-*.log.

# Guard reverted Suite Result
M-config writer-activation conflict tolerance (config.ts) config.test.ts 1 intended kill + F1 cascade at 15002 ms
M-load assertSessionLoadable allowActiveConflict branch session-archive.test.ts 1 — reads the active copy after restore selected an exact conflict
M-restore resolveSessionIdForRestore same-spelling tolerance session-archive.test.ts 1 — resolves an exact active/archive conflict only for restore
M-restore (2nd) same, collected on the ACP surface transport.test.ts 2 — session/load and session/resume restore a conflicted session from active (daemon-ACP pin, new this round)
M-delete delete's removed conflict rejection session-archive.test.ts 1 — deletes both copies of an exact active/archive conflict
M-src-race tolerant active→archived metadata read live-session-source.test.ts 3 (same trio as round 6)
M-src-path path-safety + Windows-device gate live-session-source.test.ts 7 (path-unsafe + 6 device names)
M-src-lineage re-added isValidSessionId parent shape gate live-session-source.test.ts 6 (nil/v6/v7 classify/lineage cases)
M-identity ENOENT mid-inspection → identity_changed revert conversation-directory-identity.test.ts 1
M-workspace ENOENT root-race branch removed conversation-workspace.test.ts 1
M-async async readdirreaddirSync sessionService.test.ts 15 (implementation pin by design; behavior proven by C0/C5/C6 on a real filesystem)
M-acp embedded-ACP resolver tolerance (acpAgent.ts) acpAgent.test.ts 2 — load/resume both-states tests
M-doors all 5 allowActiveConflict doors off server.test.ts 1 — reads and exports the active copy of an exact persisted conflict
M-doors-wq the workspace-qualified door off multi-workspace-sessions.test.ts 1 — reads exact active/archive conflicts from the active copy in an internal workspace
M-batch batch pre-check conflict tolerance (resolveQualifiedSessionRuntime) multi-workspace-sessions.test.ts 1 — same test as M-doors-wq
M-batch (2nd) same server.test.ts Survived (1076 green) — pin-placement, per round 6: the guard protects workspace-qualified internal reads only multiws exercises
M-ambig new live-owner ordinary-collision scan removed server.test.ts Survived (1076 green) — see N4
M-ambig (2nd) same multi-workspace-sessions.test.ts Survived (121 green) — see N4

Positive controls: every cited suite was made red by a point mutation collected through the same command that ran it green unmutated. Layered-guard combination check: M-doors (all doors) and M-batch/M-doors-wq (per-door) together show the door set and the batch pre-check each carry independent reds; no hidden redundancy among them.

Targeted gates (at the merge commit)

Witness: 03-mutation-matrix-and-gates.png. Controls ran in tmp/mut-tree verified git-clean; integration + boundary ran in the main tree.

Gate Result
core: config.test.ts 540/540
core: sessionService.test.ts 181/181
cli: server.test.ts 1076 passed + 1 env-skip (1077) — see N5
cli: session-archive.test.ts 54/54
cli: live-session-source.test.ts 49/49
cli: conversation-directory-identity.test.ts 12/12
cli: conversation-workspace.test.ts 19/19
cli: acpAgent.test.ts 446/446
cli: transport.test.ts 340/340
cli: multi-workspace-sessions.test.ts 121/121
cli: session-telemetry.test.ts 9/9
cli: workspace-qualified-acp.test.ts (moved to acp-http/ by the refactor) 48/48
integration: qwen-serve-routes.test.ts (real spawned daemon, dummy auth, no model calls) 36/36
scripts: acp-serve-boundary-guard.test.js (main's refactor #9144 guard, constrains this PR's acpAgent surface) 8/8

Gate liveness is proven by the mutation round itself (15 kills across these exact suites).

Findings (non-blocking notes only)

No blocking findings. The central claim held its A/B at the new base and at the wire; every guard that had a pin kept it; the prior substantive finding (F3) remains fixed.

N1 (nit, carried) — F1 order-dependent cascade in config.test.ts

Re-measured fresh; signature unchanged from rounds 1–6 (status table). Suite-hygiene issue, not a PR defect.

N2 (nit, carried) — fallback doors pinned collectively

Unchanged: one server.test.ts test carries all three activeInRuntime(runtime, true) fallback branches. A future test failing one specific fallback path with a both-states session would close this.

N4 (nit, new) — the live-owner ordinary-collision scan is unpinned

The effective diff adds a fail-closed block to the live-owner path of resolveTranscriptSessionRuntime: when the live owner is an internal workspace runtime, it scans ordinary workspaces with sessionExistsInAnyState(sessionId) and answers ambiguous_session_owner (500) on collision. M-ambig (block removed) left both server.test.ts (1076 green) and multi-workspace-sessions.test.ts (121 green) green — classification: coverage gap (the behavior is a reasonable fail-closed defence; nothing asserts it), not dead code (reachable whenever a session id is live in an internal runtime and persisted in an ordinary workspace) and not redundant (the pre-existing liveOwner.kind === 'ambiguous' check at routes/session.ts:1546 covers two live owners, a different hazard; the test at server.test.ts:24040 drives that older path, which is why it does not catch M-ambig). Suggested follow-up: a test with an internal runtime holding the live session plus a persisted transcript in an ordinary workspace, expecting 500 ambiguous_session_owner. Non-blocking for the same reason as N2.

N5 (note) — one environmental skip in server.test.ts

strips brackets from '[::1]' before passing to app.listen() is skipped because this container has no IPv6 loopback (probe: bind to ::1EADDRNOTAVAIL); the test's own it.skipIf(!hasIpv6Loopback) guard is doing its job. Round 6's container had ::1, hence 1077/1077 there. Not a PR effect.

Not covered

  • Per-commit attribution. Depth-2 checkout: git rev-list --count HEAD^1..HEAD^2 returns 1 against the snapshot's 29 commits; the object census found only 97868f7f itself reachable (round-6-era objects present in round 6's store are absent from this round's fresh checkout). Verified the aggregate HEAD^1..HEAD diff instead; no network fetches made.
  • Daemon E2E with real model calls — integration spawns a real daemon with dummy auth and issues no model calls; the PR declares this out of scope.
  • dispatch.ts at the wire — covered indirectly: the dispatch restore path shares resolveSessionIdForRestore/assertSessionRestorable with the REST surface, and M-restore collected on transport.test.ts turned 2 daemon-ACP tests red this round (new vs round 6). Not driven against a spawned base daemon separately.
  • Windows behavior / case-insensitive filesystem aliasing — Linux unit/integration suites only.
  • Repo-wide typecheck / ESLint / Prettier — all trees built clean during this round's builds (base core+cli, head bundle).
  • The workflow's 5-round flakiness gate — owned by the workflow lane.
  • Round-4 secondary-guard kills remain uncarried (disclosed rounds 5–6); the surviving surface is covered by this round's matrix.

Methodology

Environment: node:22-bookworm container, Node v22.23.2, repo at merge commit 812b2c9a7f (head 97868f7f, base tip b455bad5) with npm ci && npm run build pre-run by the workflow; head bundle produced this round via npm run bundle (root dist/cli.js was absent). Identity check: git rev-parse HEAD^2 equals the snapshot's headRefOid. Base control: git worktree add tmp/base-tree HEAD^1; root dependency tree reused (PR touches no dependency file) with per-entry symlinks, the two changed-package links re-pointed into the base tree (readlink -f verified before any cell), nested per-package node_modules mirrored (core's @opentelemetry/ajv/ignore), .bin linked explicitly (dotfile miss reproduced from round 6's fault list), core+cli rebuilt there (exit 0). Mutation tree: tmp/mut-tree at the merge commit, same wiring plus sibling-workspace dist symlinks and the generated git-commit.ts (the cli vitest globalSetup guard demands them), git status verified empty after every revert. A/B harness imports each tree's compiled dist by absolute path under mkdtemp workspaces with arm-coded expectations; F3 probes drive both trees' SessionOrganizationService dist with a seeded store. Wire A/B uses a scratch vitest config because integration-tests/globalSetup.ts unconditionally overwrites TEST_CLI_PATH; head-bundle green + base-build red on the same config attribute the flip to daemon code. Evidence images produced with scripts/verify-capture.mjs in command mode (its pipe mode's readFileSync(0) failed with EPIPE in this container — noted, not worked around by hand); raw per-cell, per-gate, per-mutation logs live in logs/. Assertion accounting: 2939 gate tests + 40 service-level A/B cells + 2 wire-A/B cells + 20 F3 probe checks + 18 mutation verdicts + 2 F1 cascade/isolation checks = 3021/3021; fail counts unexpected outcomes only — zero this round (predicted base-arm reds, absent-export controls, and mutant reds are encoded as passing assertions).

Qwen Code · sandboxed verification

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/cli/src/acp-integration/acpAgent.test.ts: (cd packages/cli) npx --no-install vitest run ./src/acp-integration/acpAgent.test.ts
file packages/cli/src/runtime/live-session-source.test.ts: (cd packages/cli) npx --no-install vitest run ./src/runtime/live-session-source.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/conversations/conversation-workspace.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/conversations/conversation-workspace.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/routes/session-telemetry.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/routes/session-telemetry.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/cli/src/utils/conversation-directory-identity.test.ts: (cd packages/cli) npx --no-install vitest run ./src/utils/conversation-directory-identity.test.ts
file packages/core/src/config/config.test.ts: (cd packages/core) npx --no-install vitest run ./src/config/config.test.ts
file packages/core/src/services/sessionService.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/sessionService.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/cli/src/acp-integration/acpAgent.test.ts: PPPP
  packages/cli/src/runtime/live-session-source.test.ts: PPPP
  packages/cli/src/serve/acp-http/transport.test.ts: PPPP
  packages/cli/src/serve/conversations/conversation-workspace.test.ts: PPPP
  packages/cli/src/serve/multi-workspace-sessions.test.ts: PPP
  packages/cli/src/serve/routes/session-telemetry.test.ts: PPP
  packages/cli/src/serve/server.test.ts: PPP
  packages/cli/src/serve/server/session-archive.test.ts: PPP
  packages/cli/src/utils/conversation-directory-identity.test.ts: PPP
  packages/core/src/config/config.test.ts: PPP
  packages/core/src/services/sessionService.test.ts: PPP

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

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/acp-integration/acpAgent.test.ts: P (exit 0)
round 1 · packages/cli/src/runtime/live-session-source.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/conversations/conversation-workspace.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/routes/session-telemetry.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/cli/src/utils/conversation-directory-identity.test.ts: P (exit 0)
round 1 · packages/core/src/config/config.test.ts: P (exit 0)
round 1 · packages/core/src/services/sessionService.test.ts: P (exit 0)
round 2 · packages/cli/src/acp-integration/acpAgent.test.ts: P (exit 0)
round 2 · packages/cli/src/runtime/live-session-source.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/conversations/conversation-workspace.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/routes/session-telemetry.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/cli/src/utils/conversation-directory-identity.test.ts: P (exit 0)
round 2 · packages/core/src/config/config.test.ts: P (exit 0)
round 2 · packages/core/src/services/sessionService.test.ts: P (exit 0)
round 3 · packages/cli/src/acp-integration/acpAgent.test.ts: P (exit 0)
round 3 · packages/cli/src/runtime/live-session-source.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/conversations/conversation-workspace.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/routes/session-telemetry.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/cli/src/utils/conversation-directory-identity.test.ts: P (exit 0)
round 3 · packages/core/src/config/config.test.ts: P (exit 0)
round 3 · packages/core/src/services/sessionService.test.ts: P (exit 0)
round 4 · packages/cli/src/acp-integration/acpAgent.test.ts: P (exit 0)
round 4 · packages/cli/src/runtime/live-session-source.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/conversations/conversation-workspace.test.ts: P (exit 0)

Evidence images

01-ab-both-states-base-vs-head

02-f3-corner-probes-base-vs-head

03-mutation-matrix-and-gates

04-wire-ab-base-409-head-200

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

Qwen Code · sandboxed verification

@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 97868f7fbd5893cc7d8db3886a5a8b9c01e2e1ea — 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.

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

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

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship — CI landed green after the review. ✅

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM on the narrowed scope. Verified against head 97868f7:

  • Both-states same-spelling recovery consistently reads the ACTIVE copy across REST, daemon ACP, and embedded ACP (all four resolution sites treat candidate === requested as recoverable-from-active); differently-spelled and ambiguous conflicts still surface session_conflict on every surface, archive/unarchive stay strict, and delete now takes the lease and removes both copies plus sidecars — no stranded-archived wedge.
  • The race surfaces fail as misses, not wrong positives: creation metadata is immutable and keyed by exact id behind the transcript-name gate, the ENOENT-during-identity window returns "already gone" with re-inspection on reappearance, and the restore path does one catalog lookup under the lifecycle lock (the removed pre-lock probe was only a stale fallback, so nothing is lost).
  • Parent-id resolution is gated by the SAFE_TRANSCRIPT_NAME_PATTERN + Windows-device check at the one read site every consumer shares, so transcript-derived ids cannot construct a path outside the store.

Two non-blocking observations for follow-up:

  1. assertSessionLoadable's allowActiveConflict branch discards a successful findSessionIdIgnoringCase re-probe and still throws SessionConflictError — if a concurrent delete/unarchive RESOLVES the conflict between the two probes, the client sees a permanent-looking 409 for a now-loadable session. Worst case equals pre-PR behavior (not a regression), but using the re-probe's result would deliver the race tolerance this PR is for.
  2. Active-copy preference is positional — no writer provenance/integrity distinguishes the two copies. That matches the existing single-uid local-storage trust model (the archived store was always a trusted read source, and this PR creates no new capability), but it is worth stating that rule in the design record, ideally with a log line when a same-spelled both-states conflict is resolved by preference.

CI is green on head (Test ubuntu, Serve A/B, real-daemon E2E, Desktop Shell, secret scan all pass).

@doudouOUC
doudouOUC added this pull request to the merge queue Aug 22, 2026
Merged via the queue into QwenLM:main with commit af25c45 Aug 22, 2026
@doudouOUC
doudouOUC deleted the fix/pr2a-followup-9489 branch August 22, 2026 14:08
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.22.0.

@wenshao wenshao 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: reverse audit — did not converge within the reverse-audit round cap of 5.

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

  • packages/cli/src/serve/routes/session.ts:1812 — [review] Please add route-level coverage for the internal-live-owner versus ordinary-runtime ambiguity check; current tests do not cover unqualified transcript/export with both owners present.

[Critical] packages/cli/src/serve/routes/session.ts — The initial persisted internal-runtime scan calls activeInRuntime(runtime) with the default strict conflict handling. After a daemon restart, a non-resident internal Conversations session that exists under the exact ID in both active and archived storage has no live-owner entry, so this scan records session_conflict and unqualified transcript/export returns 409 instead of selecting the active copy. Pass allowActiveConflict=true for this read-only probe while preserving metadata, generation, and cross-runtime ambiguity checks. (中文:daemon 重启后,未驻留的 internal Conversations 会话若同一 ID 同时存在于 active 和 archived,会被初始扫描按严格冲突处理并返回 409;读取探测应允许精确冲突但仍保留元数据、generation 和跨 runtime 歧义检查。)

[Critical] packages/cli/src/serve/routes/session.ts — The final ordinary-runtime scan also calls activeInRuntime(runtime) strictly. With no live owner and an exact active/archive pair only in a secondary ordinary runtime, unqualified transcript/export returns 409. A focused probe reproduced expected 409 to be 200; changing this scan to activeInRuntime(runtime, true) made it pass while activeRuntimes.length still preserves cross-runtime ambiguity detection. (中文:最终 ordinary runtime 扫描同样使用严格模式;当精确 active/archive 对只存在于次级 ordinary runtime 时会错误返回 409。定向探针复现,改为允许精确冲突后通过,且后续长度检查仍保留跨 runtime 歧义检测。)

中文说明

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

未审查:反向审计——在 5 轮的反审轮数上限内未收敛。

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

[Critical] packages/cli/src/serve/routes/session.ts — The initial persisted internal-runtime scan calls activeInRuntime(runtime) with the default strict conflict handling. After a daemon restart, a non-resident internal Conversations session that exists under the exact ID in both active and archived storage has no live-owner entry, so this scan records session_conflict and unqualified transcript/export returns 409 instead of selecting the active copy. Pass allowActiveConflict=true for this read-only probe while preserving metadata, generation, and cross-runtime ambiguity checks. (中文:daemon 重启后,未驻留的 internal Conversations 会话若同一 ID 同时存在于 active 和 archived,会被初始扫描按严格冲突处理并返回 409;读取探测应允许精确冲突但仍保留元数据、generation 和跨 runtime 歧义检查。)

[Critical] packages/cli/src/serve/routes/session.ts — The final ordinary-runtime scan also calls activeInRuntime(runtime) strictly. With no live owner and an exact active/archive pair only in a secondary ordinary runtime, unqualified transcript/export returns 409. A focused probe reproduced expected 409 to be 200; changing this scan to activeInRuntime(runtime, true) made it pass while activeRuntimes.length still preserves cross-runtime ambiguity detection. (中文:最终 ordinary runtime 扫描同样使用严格模式;当精确 active/archive 对只存在于次级 ordinary runtime 时会错误返回 409。定向探针复现,改为允许精确冲突后通过,且后续长度检查仍保留跨 runtime 歧义检测。)

— gpt-5.6-sol@954e5164 via Qwen Code /review (v0.22.0)

if (
error instanceof SessionIdCaseConflictError &&
error.reason === 'case_conflict' &&
error.candidateSessionId === 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] A sole legacy uppercase UUID persisted in both active and archived storage is still unrestorable. loadSession/resumeSession normalize the request to lowercase before this comparison, while findSessionIdIgnoringCase() returns the uppercase persisted spelling; candidateSessionId === sessionId therefore fails and returns session_conflict although there is only one spelling. A focused probe reproduced both failures, and returning the sole persisted candidate made both pass. Please represent a sole spelling present in both states separately and restore that persisted spelling without requiring equality to the normalized request.

中文

[严重] 仅以旧版大写 UUID 拼写同时存在于 active 和 archived 中的会话仍无法恢复。loadSession/resumeSession 会先把请求规范化为小写,而 findSessionIdIgnoringCase() 返回持久化的大写拼写,因此这里的 candidateSessionId === sessionId 会失败并返回 session_conflict,即使实际上只有一种拼写。定向探针复现了 load 和 resume 的失败;返回唯一持久化候选后两者均通过。请把“同一唯一拼写同时存在于两个状态”建模为独立结果,并直接恢复该持久化拼写,不要要求它等于规范化后的请求。

— gpt-5.6-sol@954e5164 via Qwen Code /review (v0.22.0)

// moved after it was already read correctly.
return (
(await store.readCreationMetadataIfReadable(sessionId, 'active')) ??
(await store.readCreationMetadataIfReadable(sessionId, 'archived'))

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 fallback can classify the selected active transcript with metadata from the archived copy. readCreationMetadataIfReadable() returns undefined not only for absence, but also for incomplete/corrupt transcripts and suppressed read errors. In an exact active/archive conflict, callers then load the active copy while using archived sourceType, sourceId, or parentSessionId, so ownership and lineage can come from a different transcript. Please distinguish an archive-move absence from a present-but-unreadable active file and require metadata from the selected active copy.

中文

[严重] 这里的回退可能用 archived 副本的元数据来分类实际选中的 active transcript。readCreationMetadataIfReadable() 不仅在文件不存在时返回 undefined,在 transcript 不完整、损坏或读取错误被吞掉时也会返回。对于 active/archive 精确冲突,调用方随后加载 active 副本,却使用 archived 的 sourceTypesourceIdparentSessionId,导致所有权和血缘来自另一个 transcript。请区分“因归档移动而暂时不存在”和“active 文件存在但不可读”,并要求元数据来自实际选中的 active 副本。

— gpt-5.6-sol@954e5164 via Qwen Code /review (v0.22.0)

return (
(await store.readCreationMetadataIfReadable(sessionId, 'active')) ??
(await store.readCreationMetadataIfReadable(sessionId, 'archived'))
);

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] The fixed active-then-archived probe loses a transcript that is unarchived between the two reads. If the active read misses while the file is archived, then another process moves it to active before the archived read, both reads return undefined and callers classify the now-active session as absent. A deterministic probe reproduced this; retrying active after the archived miss fixed it. Please use a bounded read/recheck loop that covers both archive directions.

中文

[严重] 固定的 active→archived 探测顺序会漏掉在两次读取之间被取消归档的 transcript。第一次读取 active 时文件仍在 archived;随后另一个进程把它移到 active;第二次读取 archived 也会 miss,最终两次都返回 undefined,调用方把已经 active 的会话判定为不存在。确定性探针已复现;在 archived miss 后重试 active 可以修复。请使用有界的读取/复查循环,同时覆盖两个移动方向。

— gpt-5.6-sol@954e5164 via Qwen Code /review (v0.22.0)

// wider than the storage enumeration pattern: parent ids written by older
// builds (nil/v6/v7 UUIDs, agent-suffixed ids) must stay resolvable, while
// path separators can never reach the joined transcript path.
const SAFE_TRANSCRIPT_NAME_PATTERN = /^[A-Za-z0-9._-]{1,128}$/;

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] The new 128-character cap rejects valid persisted agent-suffixed parent IDs. isValidSessionId accepts an unbounded -agent-... suffix, and IDs longer than 128 characters can still fit safely below the filesystem component limit with .jsonl; this gate rejects them before consulting storage, so children lose lineage classification. Please validate a safe single path component without an arbitrary 128-character cap, or derive the limit from the actual filename limit.

中文

[严重] 新增的 128 字符上限会拒绝合法且已经持久化的 agent 后缀父会话 ID。isValidSessionId 接受不限长度的 -agent-... 后缀,而超过 128 字符的 ID 加上 .jsonl 后仍可能安全低于文件系统组件上限;这里却在访问存储前直接拒绝,导致子会话丢失血缘分类。请仅校验安全的单一路径组件,不要使用任意的 128 字符上限,或根据真实文件名上限推导限制。

— gpt-5.6-sol@954e5164 via Qwen Code /review (v0.22.0)

for (const ordinaryRuntime of workspaceRegistry.list()) {
const ordinaryService =
createWorkspaceRuntimeSessionService(ordinaryRuntime);
if (await ordinaryService.sessionExistsInAnyState(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] This ordinary-runtime collision scan is exact-case only. On a case-sensitive filesystem, an internal runtime can own lowercase id while an ordinary runtime persists uppercase ID; sessionExistsInAnyState(id) calls exact-case getSessionLocation() and returns false, so the internal transcript is selected instead of ambiguous_session_owner. Please use case-insensitive any-state resolution here while preserving conflict and I/O semantics.

中文

[严重] 这里对普通 runtime 的碰撞扫描只检查完全相同的大小写。在大小写敏感文件系统上,internal runtime 可以拥有小写 id,而 ordinary runtime 持久化大写 IDsessionExistsInAnyState(id) 只调用精确大小写的 getSessionLocation(),因此返回 false,最终选择 internal transcript,而不是报告 ambiguous_session_owner。请改用大小写不敏感的任意状态解析,并保留冲突和 I/O 错误语义。

— gpt-5.6-sol@954e5164 via Qwen Code /review (v0.22.0)

if (location === 'conflict') {
if (options.allowActiveConflict) {
try {
await service.findSessionIdIgnoringCase(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] A disappearing archived half still turns a now-unambiguous active session into session_conflict. After the initial location read sees an exact pair, findSessionIdIgnoringCase() can return the sole active ID normally if the archived file disappears; this return value is ignored, so execution falls through to SessionConflictError. Please consume the resolver result and return active when it resolves to the requested persisted spelling.

中文

[严重] archived 一半在解析期间消失时,已经变得无歧义的 active 会话仍会被错误地转成 session_conflict。初始位置读取看到精确冲突后,如果 archived 文件消失,findSessionIdIgnoringCase() 会正常返回唯一 active ID;但这里忽略了返回值,随后直接落入 SessionConflictError。请使用解析结果,在它解析到请求的持久化拼写时返回 active。

— gpt-5.6-sol@954e5164 via Qwen Code /review (v0.22.0)

error.reason === 'case_conflict' &&
error.candidateSessionId === sessionId
) {
return 'active';

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] candidateSessionId === sessionId does not prove the exact active/archive pair is the only case-equivalent spelling. findSessionIdIgnoringCase() throws immediately when it encounters that exact pair, before examining a second mixed-case transcript, and this branch then selects active despite genuine ambiguity. Please enumerate all matching spellings first and expose the exact-pair exception only when no additional spelling exists.

中文

[严重] candidateSessionId === sessionId 并不能证明 active/archive 精确对是唯一的大小写等价拼写。findSessionIdIgnoringCase() 遇到该精确对时会立即抛出,尚未检查另一个混合大小写 transcript;这里随后仍选择 active,掩盖了真实歧义。请先枚举全部匹配拼写,仅在不存在额外拼写时才返回“精确对可接受”的结果。

— gpt-5.6-sol@954e5164 via Qwen Code /review (v0.22.0)

export async function assertSessionRestorable(
workspaceCwd: string,
sessionId: string,
requestedSessionId: string,

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] The final guarded restore check no longer revalidates case-insensitive ambiguity. A spelling can be resolved first, then another case-equivalent transcript can appear before assertSessionRestorable(); this helper checks only the selected exact spelling and restores it although ownership is now ambiguous. A focused probe resolved 'active' instead of rejecting, while final case-insensitive re-resolution fixed it. Please combine resolution and validation under the final guard or repeat the case-insensitive check here.

中文

[严重] 最终受保护的恢复检查不再重新验证大小写不敏感歧义。先解析出某个拼写后,另一个大小写等价 transcript 可以在 assertSessionRestorable() 前出现;该函数只检查已选中的精确拼写,因此即使所有权已经歧义仍会恢复。定向探针中它错误地返回了 'active';在最终阶段重新执行大小写不敏感解析后通过。请把解析和验证合并到最终 guard 内,或在这里再次执行大小写不敏感检查。

— gpt-5.6-sol@954e5164 via Qwen Code /review (v0.22.0)

throw new SessionWriterShutdownError();
}
if (location === 'conflict' || location === 'archived') {
if (location === 'archived') {

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] The newly added collected regression test currently fails. releases a pending lease while a real baseline read is gated cannot find pending-baseline.lock and then reports an unhandled SessionWriterUnavailableError; the isolated changed suite reported this failure after all scoped builds succeeded. Please reconcile conflict adoption with pending lease acquisition/cleanup so the lock remains present while the baseline read is gated and initialization does not reject unhandled.

中文

[严重] 本 PR 新增且会被收集执行的回归测试目前失败。releases a pending lease while a real baseline read is gated 找不到 pending-baseline.lock,随后又出现未处理的 SessionWriterUnavailableError;所有相关构建成功后,隔离运行的变更测试套件仍复现该失败。请协调冲突接管与 pending lease 的获取/清理,使基线读取被阻塞时锁仍存在,并避免初始化产生未处理 rejection。

— gpt-5.6-sol@954e5164 via Qwen Code /review (v0.22.0)

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.

PR2A provenance/identity tightening: four narrower behaviour regressions

5 participants