Skip to content

feat(cli): Add standalone conversation isolation primitives - #9341

Merged
doudouOUC merged 30 commits into
QwenLM:mainfrom
doudouOUC:feat/standalone-pr2a-primitives
Aug 20, 2026
Merged

feat(cli): Add standalone conversation isolation primitives#9341
doudouOUC merged 30 commits into
QwenLM:mainfrom
doudouOUC:feat/standalone-pr2a-primitives

Conversation

@doudouOUC

@doudouOUC doudouOUC commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR delivers standalone-session PR2A: the internal source, identity, and admission primitives required before the standalone lifecycle service is added. It classifies explicit standalone, compatible legacy projectless, and Live transcripts without creating a second runtime or catalog; validates active/archive location, and validates parent lineage for legacy children; reserves the standalone source from generic creation; and carries the classification through REST, ACP, and Live-task restore paths.

It also adds deterministic private-directory identity checks, makes case-insensitive session lookup authoritative across active and archived storage, and fails closed when transcript creation metadata is truncated, malformed, missing, conflicting, or changes location during lookup. Existing tolerant transcript recovery remains available for ordinary readers, including complete }{-glued records.

The standalone architecture document and the reviewed PR2 implementation plan are updated to lock the PR2A/PR2B split and the downstream containment boundaries. This PR does not publish standalone routes, capabilities, SDK APIs, or UI behavior.

Why it's needed

Standalone sessions need a trustworthy way to distinguish their persisted source and private directory from Live and workspace sessions before lifecycle operations can be implemented. Treating an empty metadata result as proof of a legacy standalone session can otherwise promote a damaged Live or child transcript into a top-level standalone session, while non-authoritative case lookup or directory comparison can bind the wrong persisted identity.

These primitives give PR2B one fail-closed foundation for source provenance, parent lineage, session spelling, runtime ownership, and private-directory identity without falling back to the primary workspace or expanding the public product surface prematurely.

Reviewer Test Plan

How to verify

  1. Verify that explicit standalone top-level sessions and children, compatible legacy projectless sessions, and Live sessions are classified only when their active/archive location is valid; reserved-source conflicts, a forged source id, self lineage, a syntactically invalid parent, location races, and unreadable creation metadata should be rejected. For legacy children, a missing parent and grandparent or cyclic lineage should also be rejected. An explicit standalone child is self-describing, so it is deliberately still classified when its parent has been archived or deleted — that is what keeps it independently loadable — and depth-1 is enforced by the creation-time parent gate rather than at read time.
  2. Verify that a clean metadata-free legacy transcript remains loadable, a fully recoverable glued creation record remains accepted, and truncated, garbage, invalid-fragment, scalar, or array JSONL input fails closed for provenance classification without changing tolerant reads used elsewhere.
  3. Verify that unique mixed-case session IDs resolve to the persisted spelling, while case-only duplicates backed by distinct transcripts across active or archived storage produce the typed conflict instead of taking an exact-match shortcut. On a case-insensitive filesystem, two spellings that alias one physical transcript should collapse to the spelling whose own directory entry backs it rather than conflicting. A filename the classifier cannot parse, and an unreadable transcript head under the requested spelling itself, should each report the id as free rather than permanently occupied.
  4. Verify that equivalent canonical private-directory paths are accepted and that missing, replaced, escaped, ambiguous, or otherwise mismatched directory identities are rejected before downstream workspace use. Verify that restoring a legacy mixed-case transcript keeps storage on the persisted spelling while the private directory stays keyed on the canonical id, so a later Live or task call resolves to the same directory instead of creating a second, empty one.
  5. Verify that generic session creation cannot claim the reserved standalone source and that existing REST, ACP, Live, workspace, and project-session behavior remains unchanged outside these internal admission paths.

Local validation completed with npm run build, npm run typecheck, and npm run lint, plus final touched-file ESLint and Prettier checks. Targeted final tests passed for 209 Core JSONL/SessionService cases and 19 CLI source-classification cases; the broader PR2A verification also passed the source/workspace/identity, server/ACP Agent/Live-task, and ACP transport suites.

Evidence (Before & After)

N/A — internal source, identity, admission, tests, and design changes with no user-visible or TUI output.

Tested on

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

Environment (optional)

macOS 26.4.1, Node.js 24.12.0, npm 10.9.8, local workspace runtime with sandbox-independent unit and build verification.

Risk & Scope

  • Main risk or tradeoff: Internal restore and task admission now reject ambiguous or damaged projectless transcripts instead of attempting a legacy standalone fallback; this intentionally favors provenance safety over recovery when classification evidence is incomplete. The fail-closed window is the first ten non-empty transcript lines: creation metadata is only accepted when every one of them is fully recoverable, so a Live or standalone conversation that crashed within its first ten records is no longer loadable through the internal path, while the same tear later in the file stays loadable. That blast radius is worth a release note.
  • Not validated / out of scope: Local Windows and Linux execution; the PR2B containment and standalone service transaction; public standalone routes, capability publication, SDK types, WebUI/WebShell integration, lifecycle operations, and deletion recovery.
  • Breaking changes / migration notes: No public API or persisted-format migration. Existing legacy projectless transcripts remain read-compatible when their scanned metadata is intact, and existing tolerant JSONL readers retain their recovery behavior.

Linked Issues

Refs #8908

Builds on #9181 and follows #8890.

中文说明

本 PR 做了什么

本 PR 交付 standalone session 的 PR2A:在增加 standalone 生命周期服务之前所需的内部 source、identity 与 admission primitives。它在不创建第二套 runtime 或 catalog 的前提下,对 explicit standalone、兼容的 legacy projectless 与 Live transcript 进行分类;验证 active/archive location,并对 legacy child 验证 parent lineage;禁止 generic creation 使用保留的 standalone source;并把分类结果贯通到 REST、ACP 与 Live-task restore 路径。

它还增加 deterministic private-directory identity 校验,使大小写不敏感的 session lookup 在 active 与 archived storage 上保持 authoritative,并在 transcript creation metadata 被截断、损坏、缺失、冲突或读取期间发生 location 变化时 fail closed。普通 reader 仍保留既有 tolerant transcript recovery,包括完整的 }{ glued records。

Standalone 架构文档与已审计的 PR2 实施计划同步更新,锁定 PR2A/PR2B 拆分及后续 containment 边界。本 PR 不发布 standalone routes、capabilities、SDK APIs 或 UI 行为。

为什么需要

在实现 lifecycle operations 之前,standalone session 需要一种可信方式,把其 persisted source 与 private directory 同 Live 和 workspace session 区分开。否则,把空 metadata result 当作 legacy standalone 的证明,可能把损坏的 Live 或 child transcript 提升为 top-level standalone;非 authoritative 的大小写 lookup 或 directory comparison 也可能绑定错误的 persisted identity。

这些 primitives 为 PR2B 提供一套 fail-closed 基础,统一处理 source provenance、parent lineage、session spelling、runtime ownership 与 private-directory identity,同时不回退到 primary workspace,也不提前扩大公开产品表面。

Reviewer 测试计划

如何验证

  1. 验证 explicit standalone top-level session 与 child、兼容的 legacy projectless session 以及 Live session,只有在 active/archive location 有效时才会被分类;reserved-source conflict、伪造的 source id、self lineage、语法无效的 parent、location race 与不可读 creation metadata 都应被拒绝。对 legacy child,missing parent 以及 grandparent/cyclic lineage 同样应被拒绝。explicit standalone child 是自描述的,因此在其 parent 已 archive 或 delete 后仍会被分类——这正是它可独立加载的前提——depth-1 由创建时的 parent gate 强制,而非在 read 时强制。
  2. 验证干净且没有 metadata 的 legacy transcript 仍可加载,完整可恢复的 glued creation record 仍被接受;truncated、garbage、invalid-fragment、scalar 或 array JSONL 输入在 provenance classification 中 fail closed,同时不改变其他位置使用的 tolerant read 行为。
  3. 验证唯一的 mixed-case session ID 会解析为 persisted spelling,而 active 或 archived storage 中由不同物理 transcript 支撑的 case-only duplicate 会返回 typed conflict,不再走 exact-match shortcut。在大小写不敏感的文件系统上,指向同一物理 transcript 的两个拼写应折叠为其自身目录项所对应的拼写,而不是判为 conflict。分类器无法解析的文件名,以及请求拼写自身 transcript head 不可读的情况,都应把该 id 报告为可用,而不是永久被占用。
  4. 验证等价的 canonical private-directory path 被接受,而 missing、replaced、escaped、ambiguous 或其他不匹配的 directory identity 会在下游 workspace 使用前被拒绝。验证恢复 legacy mixed-case transcript 时,storage 保留 persisted spelling,而 private directory 仍以 canonical id 为 key,使后续 Live 或 task 调用解析到同一个目录,而不是新建第二个空目录。
  5. 验证 generic session creation 不能声明保留的 standalone source,并确认既有 REST、ACP、Live、workspace 与 project-session 行为在这些内部 admission paths 之外保持不变。

本地验证已完成 npm run buildnpm run typechecknpm run lint,并对最终变更文件执行 ESLint 与 Prettier 检查。最终定向测试通过 209 个 Core JSONL/SessionService 用例和 19 个 CLI source-classification 用例;更广泛的 PR2A 验证还通过了 source/workspace/identity、server/ACP Agent/Live-task 与 ACP transport 套件。

证据(Before & After)

N/A —— 仅涉及内部 source、identity、admission、测试与设计,无用户可见或 TUI 输出变化。

测试平台

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

环境(可选)

macOS 26.4.1、Node.js 24.12.0、npm 10.9.8,本地 workspace runtime,执行了不依赖 sandbox 的单元测试与构建验证。

风险与范围

  • 主要风险或取舍:内部 restore 与 task admission 现在会拒绝 ambiguous 或 damaged projectless transcript,而不再尝试 legacy standalone fallback;当分类证据不完整时,这一行为有意优先保证 provenance safety,而非恢复可用性。fail-closed 的窗口是 transcript 的前十个非空行:creation metadata 只在这十行全部完整可恢复时才被接受,因此在前十条记录内崩溃的 Live/standalone 会话不再能通过内部路径加载,而更靠后的截断仍可加载。该影响范围值得写入 release note。
  • 未验证 / 不在范围内:Windows 与 Linux 本地执行;PR2B containment 与 standalone service transaction;公开 standalone routes、capability publication、SDK types、WebUI/WebShell integration、lifecycle operations 与 deletion recovery。
  • Breaking changes / migration notes:没有 public API 或 persisted-format migration。只要扫描到的 metadata 完整,既有 legacy projectless transcript 仍保持读取兼容;既有 tolerant JSONL reader 也保留恢复行为。

关联 Issue

Refs #8908

基于 #9181,并承接 #8890

doudouOUC and others added 4 commits August 17, 2026 13:35
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>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC
doudouOUC marked this pull request as ready for review August 17, 2026 07:51
@doudouOUC
doudouOUC enabled auto-merge August 17, 2026 07:51
@doudouOUC doudouOUC self-assigned this Aug 17, 2026
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (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: 2069 passed · 7 failed · 2076 total

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

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

脚本断言:2069 通过 · 7 失败 · 2076 总计

Verification report

PR 9341 Deep Verification — feat(cli): Add standalone conversation isolation primitives

Verdict: findings — assertion totals 2069 pass / 7 fail (2076). Verified head OID e00ed9f (HEAD^2); base a35a23c (HEAD^1). The central fail-closed claim is proven load-bearing by a clean A/B, but the PR ships 7 red unit tests on the repo's own Linux CI platform (5 interface-migration gaps + 2 macOS-only inode tests). Production logic is sound in every case and each failure has a measured fix; none of these are behavioral regressions, but the PR is not merge-ready as delivered because its own suites are red.

中文摘要
  • 结论findings(2069 通过 / 7 失败)。核心"对损坏元数据 fail-closed"主张经 A/B 证实确为承重;但 PR 自带的 7 个单测在仓库的 Linux CI 平台上红了,故按现状不可合并。
  • A/B 结论:见下表。base 会把 4 种损坏形状(截断/垃圾/标量/数组)以及"窗口内损坏"全部误判为可加载的 legacy standalone(promote),head 对这 5 个 cell 全部 fail-closed 拒绝;4 个良性形状(干净 legacy、显式 live、}{ 完全可恢复粘连、窗口外损坏)在两臂均保持可加载。次级主张(大小写权威查找抛 SessionIdCaseConflictError、reserved standalone source 守卫、self/cyclic lineage 拒绝)也全部 A/B 证实。
  • Findings(按严重度):
    1. 接口迁移未同步测试桩:head 的 restore/分类路径改为经 store 接口调用新方法 readCreationMetadataIfReadable,但 server.test.ts(3 个)与 live-task-service.test.ts(2 个)仍只 stub 旧的 readCreationMetadata / mock 类缺该方法,导致 delivered head 上 5 个测试失败(404 或 not a function)。生产代码无碍(真实 SessionService 有该方法),但套件在 Linux 上红。已测修复补丁使 1000/1000 转绿。
    2. macOS-only inode 假设conversation-directory-identity.test.ts 等 2 个新增测试假设"rm+mkdir 会换 inode",但 Linux(ext4 与 overlay 均实测复用 inode)上会确定性失败。生产守卫本身正确(rename-over 探针证实 inode 真变时能拒绝),仅是测试平台相关。已测 rename 修复使 18/18 转绿。
    3. 覆盖缺口(非缺陷):变异 M3(去掉 self-lineage 检查)在 PR 自带 session-source 套件中存活——交付的 SELF_ID 用例其实由"父必须为顶层"规则兜住,未真正钉住显式 self-lineage 比较;我补的探针(自引用 explicit-standalone 子会话)证明该守卫对这一形状是承重的。生产代码正确,仅测试未覆盖该形状。
  • 未覆盖:见正文 Not covered(Windows、真实多进程 daemon 端到端、性能/并发压测等)。

Central claim + A/B

Central claim: provenance classification must fail closed when a transcript's creation metadata is truncated/malformed/missing, while clean legacy transcripts and fully-recoverable }{-glued records stay loadable; tolerant readers elsewhere are unchanged.

Mock-free A/B (01-failclosed-ab.mjs): real SessionService over real transcript files under a temp runtime dir. Base arm drives readLoadableLiveConversationMetadata with the pre-PR callback; head arm passes the service as the new store. Build fingerprints asserted on both arms (SessionIdCaseConflictError present only at head). Witness: evidence/01-failclosed-ab-base-vs-head.png.

cell base head verdict
clean-legacy loadable loadable preserved
explicit-live loadable loadable preserved
truncated creation record loadable (promoted) rejected flipped — fixed
garbage bytes loadable (promoted) rejected flipped — fixed
scalar line (42) loadable (promoted) rejected flipped — fixed
array line ([1,2,3]) loadable (promoted) rejected flipped — fixed
}{-glued complete record loadable loadable preserved (recovery kept)
damage past 10-record window loadable loadable preserved (bounded scan)
valid head + damage within window loadable (promoted) rejected flipped — fixed

Mechanism nuance (accurate attribution): the four corrupt-head cells are rejected at head's new location gate (getSessionLocation → undefined because the head record is unreadable) plus integrity; the valid-head/damage-within-window cell is rejected purely by the new readLinesWithIntegrity complete flag in readCreationMetadataIfReadable. Base promotes all five because readCreationMetadata coerces an empty tolerant read to {}, which classifies as legacy standalone. 20/20 scripted assertions.

Secondary claims (all confirmed):

  • Case lookup (02-case-conflict.mjs, 8/8, witness 02-case-conflict-ab.png): unique mixed-case resolves the persisted spelling on both arms; case-only duplicates and active+archived same-spelling throw SessionIdCaseConflictError at head but return a first-match/id at base.
  • Reserved source + lineage (03-reserved-source.mjs, 19/19, witness 03-reserved-source-ab.png): base's restore predicate continues for a transcript claiming sourceType:'standalone' (the hole); head rejects it (404 decision). Both arms refuse explicit standalone and standalone+sourceId on the Live path; legacy child of legacy parent loadable on both; self/cyclic lineage refused on both; explicit standalone child loadable without parent at head (design line 184).

Corrections

None to prior rounds (first verification round). One factual note for reviewers: the PR body states "targeted final tests passed … the server/ACP Agent/Live-task suites." That is not reproducible on Linux — see Finding 1.

Findings

F1 — Interface migration left test stubs behind: 5 unit tests red at delivered head (highest severity)

The PR moved restore/classification to the store interface, calling the new SessionService.readCreationMetadataIfReadable. The delivered tests still stub only the legacy method:

  • packages/cli/src/serve/server.test.tsallows exact organization updates…, rejects generic REST creation and relocates loadable Live restores, and the PR's own new uses authoritative persisted spelling… all get 404 (expected 200) because the real (unstubbed) readCreationMetadataIfReadable reads an absent file → undefined → 404.
  • packages/cli/src/serve/live/live-task-service.test.tsresumes an existing projectless task… and restores Live source identity… throw TypeError: store.readCreationMetadataIfReadable is not a function (the PR added getSessionLocation to the mock class but not this method).

Repro: cd packages/cli && npx vitest run src/serve/server.test.ts src/serve/live/live-task-service.test.ts5 failed | 1753 passed (witness 04-gate-red-server-livetask-at-head.png). A/A control proves causation: both files are green at base (aa-live-task-base.txt 15/15, aa-server-lifecycle-base.txt lifecycle 18/18). Production code is not broken (real SessionService implements the method; real files exist), but the PR ships red tests on Linux CI and the restore behaviors these tests encode are currently pinned by nothing green.

Measured fix (witness 05-gate-green-after-measured-fix.png, patch logs/candidate-fix-interface-gap.patch): add a readCreationMetadataIfReadable mock delegating to the existing readCreationMetadata stub, and add the method to the live-task mock class. Result: server.test.ts lifecycle 19/19 and full server.test.ts + live-task-service.test.ts 1000/1000; hostile fixture (delivered) red, benign fixtures unchanged.

Suggested fix (measured)
// server.test.ts, beside each existing readCreationMetadata spy:
const readCreationMetadataIfReadable = vi
  .spyOn(SessionService.prototype, 'readCreationMetadataIfReadable')
  .mockImplementation((candidateId) => readCreationMetadata(candidateId));
// …and readCreationMetadataIfReadable.mockRestore() in the finally block.

// live-task-service.test.ts, in the mock SessionService class:
async readCreationMetadataIfReadable(sessionId: string) {
  return (await this.sessionExists(sessionId))
    ? await this.readCreationMetadata(sessionId)
    : undefined;
}

F2 — Two new tests encode a macOS-only inode assumption; deterministic failure on Linux

conversation-directory-identity.test.ts > rejects same-path replacement against an expected identity and conversation-workspace.test.ts > …rejects replaced standalone child identities replace a directory via rm + mkdir at the same path and expect a new inode. Measured on this box: both overlay (/tmp) and ext4 (/__w) reuse the freed inode (04-identity-inode-probe.mjs; witness 07-identity-tests-red-on-linux-inode-reuse.png), so the guard legitimately reports the "replacement" as identical and the tests fail. This holds regardless of TMPDIR (re-run on ext4 gate-cli-identity-ext4.txt, still 2 failed). The production guard is correct: a rename-over replacement that truly changes the inode is rejected with unexpected_identity (04-identity-inode-probe.mjs 3/3). The PR's own test matrix marks Linux "未测试".

Repro: cd packages/cli && npx vitest run src/utils/conversation-directory-identity.test.ts src/serve/conversations/conversation-workspace.test.ts2 failed | 16 passed.

Measured fix: build the replacement by rename-ing a sibling directory over the target (a fresh inode on every POSIX filesystem) instead of rm+mkdir. Result: those two files 18/18 and the 5-file small gate 59/59 (fix-measure-identity.txt, fix-measure-smallgate.txt).

F3 — Coverage gap: self-lineage guard not pinned for the explicit-standalone-child shape (surviving mutant)

Mutation M3 (delete the normalizeSessionIdForLookup(parent)===normalizeSessionIdForLookup(sessionId) check in session-source.ts) survived the delivered session-source.test.ts (19/19 still green): the delivered SELF_ID case ({parentSessionId: SELF_ID}, no sourceType) is rejected anyway because a self-parent is never top-level. But the guard is load-bearing for a self-referencing explicit standalone child ({sourceType:'standalone', parentSessionId: SELF}), which hits the explicit-child early return before any parent read: with the guard removed that shape classifies as loadable. My probe test (mutation-M3-selfprobe-mutated.txt red, -restored.txt green) demonstrates the flip. Classification: coverage gap, not dead code and not a production defect — the guard exists and is correct; no delivered test asserts it for this shape. Suggest adding the probe fixture to session-source.test.ts.

Mutation matrix summary

mutant guard removed pinning suite result
M1 integrity complete flag forced true core jsonl-utils + corruption killed (4 red) — witness 06-mutation-m1-integrity-disabled.png
M2 cross-spelling case-conflict throw core sessionService case block killed (1 red)
M3 self-lineage comparison cli session-source survived delivered suite; killed by added probe (coverage gap F3)

M1's reds land in core, not in session-source.test.ts, because that suite's store is stubbed at the contract boundary — the leaf is genuinely pinned by the core corruption/jsonl-utils tests, so the two layers are each covered by the suite closest to them. Positive control: each killed mutant failed on the exact intended assertion (quoted in logs/mutation-M*.txt).

Targeted gates (head, as delivered)

gate result
core: jsonl-utils + sessionService + corruption 209/209 pass
cli small: session-source, session-id-admission, error-response, directory-identity, conversation-workspace 57/59 (2 fail = F2)
cli big: server, acp-http transport, acpAgent, live-task-service 1753/1758 (5 fail = F1); transport 326/326 and acpAgent 432/432 pass

With the measured fixes the small gate is 59/59 and server.test.ts + live-task-service.test.ts is 1000/1000. No repo-wide gate was claimed or run.

Not covered

  • Per-commit attribution: checkout is depth 2 (merge, base, head only); the 4 commits in $QWEN_VERIFY_CONTEXT are not all individually reachable, so the aggregate HEAD^1..HEAD diff was verified, not each commit.
  • Windows: isSameConversationPath win32 branches and case-insensitive FS behavior not exercised (Linux container).
  • True multi-process daemon E2E: restore/admission verified at dist/component level plus the PR's own in-process server.test.ts/transport.test.ts; no live daemon over a real socket was booted.
  • Performance / concurrency: no load or race testing of the new scan-window reads; MAX_PROMPT_SCAN_LINES=10 bounds the read, but no timing ladder was run (input is daemon-written transcripts, not outsider text, so ReDoS-style scaling was deemed low-value).
  • Lint/format/typecheck repo-wide: CI builds head (typecheck) green per the environment contract; no separate npm run lint run.
  • Docs-only changes (standalone-daemon-sessions.md, 2026-08-14-standalone-pr2-core.md): reviewed for claim context, not behaviorally tested.

Methodology

Environment: CI merge-ref checkout at f1123dc2 (base a35a23cd68, head e00ed9f9f6), node:22-bookworm-class container, Node v22.23.2; npm ci + npm run build pre-run. A/B base side built in git worktree tmp/base-tree at HEAD^1, rebuilding only packages/core + packages/cli; internal @qwen-code/qwen-code-core/qwen-code links were re-pointed into the base tree and base nested node_modules symlinked from head, then control validated by build fingerprint — base dist lacks SessionIdCaseConflictError/readCreationMetadataIfReadable, head dist has both, so neither cell could silently load the other's code. Harnesses 0104 drive compiled dist/ output with real files (no stubs of the unit under test); mutation matrix applied single-point source edits in-tree, ran the pinning vitest suite, then git checkout -- restored (tree left clean, git status --porcelain empty). Evidence PNGs rendered via scripts/verify-capture.mjs; raw per-run logs live in logs/. Assertion counts in assertions.json map 1:1 to executed scripted checks (harness check() calls + vitest tests at delivered head); the 7 fail entries are the delivered-head test failures in F1/F2.

Re-running harnesses 0103: they import BOTH builds, and the scratch base worktree was removed after the A/B cells were captured (per the verify workflow). Rebuild it with: git worktree add tmp/base-tree <baseRefOid>; ln -s head's packages/{core,cli}/node_modules into the worktree's packages; mirror root node_modules/@qwen-code/* into tmp/base-tree/node_modules/@qwen-code/ with qwen-code-core/qwen-code pointing at the base tree and everything else at head; symlink root node_modules/@lydell into the base tree (the core tsconfig paths mapping resolves ../../node_modules/@lydell/... relative to the tree); run npm run generate, then npm run build -w packages/core -w packages/cli inside the worktree. Harness 04 is head-only and reruns as-is.

Evidence images

01-failclosed-ab-base-vs-head

02-case-conflict-ab

03-reserved-source-ab

04-gate-red-server-livetask-at-head

05-gate-green-after-measured-fix

06-mutation-m1-integrity-disabled

07-identity-tests-red-on-linux-inode-reuse

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

Qwen Code · sandboxed verification

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Re-run (round 10), triggered by the author after @wenshao's re-verification at this head. Head moved f716cc6e62d5c5a1 — two PR fix commits (49dec69a3, e4e9ef529) plus a merge of origin/main; all reviewed in the Stage 2 pass below.

Gate passes on template/problem/direction/approach; the final verdict waits on Stage 3. 🔍

中文说明

重跑(第 10 轮),由作者在 @wenshao 于本 head 完成重新验证后触发。Head 从 f716cc6e 前进到 62d5c5a1——两个 PR 修复 commit(49dec69a3e4e9ef529)加一次 origin/main 合并,均在下方 Stage 2 中完成审查。

门禁在模板/问题/方向/方案上通过;最终结论见 Stage 3。🔍

Qwen Code · qwen3.8-max

Reviewed at 62d5c5a193ea0180350564ea66e3f5dd6ef2b3e5 · re-run with @qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

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

No response changes against the PR base across 4 scenario(s).

Qwen Code · serve A/B

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

Reviewed the full delta f716cc6e..62d5c5a1 in-tree: two PR fix commits (49dec69a3, e4e9ef529) plus a clean merge of origin/main. My independent baseline for this round's problem set — batch lifecycle deadlocks on case twins, the CLI still minting mixed-case transcripts, the resolver carrying two divergent arms — would have been: normalize at the batch dedupe boundary, normalize at the CLI write boundary, and dedupe the resolver. The PR does exactly that, and the resolver unification goes one step beyond the baseline: it deletes the duplicated algorithm instead of patching both copies, which is what let the occupancy escape land in only one arm in the first place.

  • 49dec69a3 (three defects). The inode fallback is the substantive one: isSameDirectoryIdentity used to make a directory fail to equal itself on any filesystem reporting inode 0 (exFAT/FAT, some SMB), so Conversations could never start there. The root is now established with inodeVerifiable: false recorded and comparisons degrade to device + canonical path + stat shape; the reduced guarantee is disclosed in the type's doc comment and carried on the identity for callers to surface. Where inodes exist they are still required to match — both directions are pinned by tests. The occupancy escape moved from per-arm to per-candidate (again pinned both directions), and ConversationWorkspace's live-path materialize/inspect now canonicalize through directoryKey(), so one session resolves to one private directory by construction.
  • e4e9ef529 (batch + CLI). Delete/archive/unarchive all dedupe through normalizeSessionIdForLookup before locking — three new tests pin an UPPER+lower batch collapsing to a single operation on the persisted spelling. loadCliConfig stores the canonical lowercase spelling for caller-supplied ids, closing the write side that kept growing the mixed-case cohort.
  • No blockers found in the delta. One observation for PR2B: the standalone-directory primitives (prepareStandaloneDirectory / inspectStandaloneDirectory / ensureStandaloneDirectory) do not canonicalize — they take storageSessionId as-is. They have test-only callers at this head, so nothing is broken today; but PR2B's wiring must decide deliberately whether those entry points receive canonical ids, not inherit the live-path invariant by accident.

Status of the two Criticals from the bot's round-7 review, checked against this head

  • R7-1 (transcript/export 404s for mixed-case restored sessions) — still stands, verified in-tree this run: resolveTranscriptSessionRuntime's activeInRuntime (session.ts:1624) still feeds the exact-case request spelling to assertSessionLoadable. The author's stated position: not patching this consumer in isolation — the fix is the single storage-boundary resolver tracked in Session lifecycle operations are gated on provenance classification, making unclassifiable sessions unmanageable #9488 — and the CLI write side is now closed, so the affected cohort stops growing. Whether that deferral is acceptable is a maintainer call; it is the main open question on this PR.
  • R2-3 (10-record fail-closed window) — unchanged, intentionally: readCreationMetadataInternal still requires whole-head line integrity over MAX_PROMPT_SCAN_LINES (verified in sessionService.ts). This is the disclosed O1 trade-off in the PR body's Risk & Scope ("worth a release note"). @wenshao's re-verification at this head recommends merge with the release note; @yiliang114 approved at this head. The review thread's stated closure condition — explicit maintainer acceptance recorded on-thread — is not yet literally satisfied.

Polish item carried from @wenshao's re-verification (his finding, attributed as such)

The CLI misattributes an unreadable session store to an id collision — "already exists" advice that cannot help when enumeration itself failed. Reusing the daemon's 503 session_id_admission_unavailable wording in the CLI branch would cost one if. Non-blocking.

Test evidence

Unattended run — no PR code is built or executed here; the evidence is the PR's own CI at 62d5c5a1, fetched once via the API (no polling). The decisive legs are green for the first time on this PR's fix commits: Test (ubuntu-latest, Node 22.x) (full unit suite) and Serve A/B both passed — round 9 flagged that no suite-level run had ever landed on the fix commits; this head settles that. One red lane: ubuntu-latest / Java 21 failed with ZipException opening "logback-core-1.3.16.jar": zip END header not found — a corrupt jar in the self-hosted runner's Maven cache. Classified environmental, not PR-caused: the diff touches no Java code, pom.xml, or dependencies; the same SDK suite is green on the four sibling Java legs (ubuntu Java 11/17, macOS Java 21, windows Java 21); and the workflow was green at the previous head. A re-run or clearing that runner's cache entry should settle it. Test (macos/windows) and Integration Tests (CLI, No Sandbox) remain skipped by CI design (merge-queue/post-approval gated) — pre-existing, not caused by this PR. The still-running review-pr check is the bot's own orchestration job, not PR CI.

Real-scenario coverage at this exact head exists and is on this thread: @wenshao's re-verification drove real daemons, the real CLI, and a case-insensitive ext4 mount across base vs head with nine negative controls — maintainer-produced evidence, attributed as such, not re-run here. The one behavioural surface no available lane can settle: the inodeVerifiable fallback needs a filesystem reporting inode 0, which no CI host provides (and a sponsored /verify sandbox would not either) — it rests on the PR's unit tests plus code reading, disclosed rather than papered over.

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Serve A/B (ubuntu-latest, Node 22.x) ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Live Host (macos-latest) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Real daemon E2E / Java 11 ✅ success
ubuntu-latest / Java 11 ✅ success
ubuntu-latest / Java 17 ✅ success
macos-latest / Java 21 ✅ success
windows-latest / Java 21 ✅ success
ubuntu-latest / Java 21 ❌ failure (environmental — corrupt jar in runner Maven cache, see prose)
Classify PR ✅ success
Dependency CVE audit ✅ success
Secret scan (TruffleHog) ✅ success
precheck-pr / precheck ✅ success
Test (macos-latest, Node 22.x) ⏭️ skipped (merge-queue gated)
Test (windows-latest, Node 22.x) ⏭️ skipped (merge-queue gated)
Integration Tests (CLI, No Sandbox) ⏭️ skipped (merge-queue gated)
中文说明

代码审查

在树内完整审查了 f716cc6e..62d5c5a1 的 delta:两个 PR 修复 commit(49dec69a3e4e9ef529)加一次干净的 origin/main 合并。对本轮问题集(批量生命周期在大小写孪生上死锁、CLI 仍在铸造 mixed-case transcript、resolver 存在两条分叉的分支)我的独立基线方案是:在批量去重边界归一化、在 CLI 写入边界归一化、合并 resolver。PR 正是这样做的,且 resolver 统一比基线更进一步——删除了重复的算法而不是修补两份拷贝,而正是这份重复让 occupancy 逃逸当初只落进其中一条分支。

  • 49dec69a3(三个缺陷)。inode 兜底是实质项:isSameDirectoryIdentity 过去会让目录在报告 inode 0 的文件系统(exFAT/FAT、部分 SMB)上无法等于自身,Conversations 在这些文件系统上永远无法启动。现在建立 root 时记录 inodeVerifiable: false,比较降级为 device + canonical path + stat shape;保证的削弱在类型的 doc comment 中披露,并记录在 identity 上供调用方呈现。inode 存在时仍要求匹配——两个方向均有测试钉住。occupancy 逃逸从按分支改为按候选(同样双向钉住);live 路径的 materialize/inspect 现在经 directoryKey() 归一化,一个会话 by construction 只解析到一个私有目录。
  • e4e9ef529(批量 + CLI)。delete/archive/unarchive 全部在上锁前经 normalizeSessionIdForLookup 去重——三个新测试钉住大写+小写批量折叠为对持久化拼写的单次操作。loadCliConfig 对 caller-supplied id 存储规范小写拼写,关闭了持续扩大 mixed-case 群体的写入侧。
  • delta 中未发现阻断项。一点给 PR2B 的观察:standalone 目录原语(prepareStandaloneDirectory / inspectStandaloneDirectory / ensureStandaloneDirectory归一化——直接接收 storageSessionId。本 head 上它们只有测试调用方,因此当下没有破坏;但 PR2B 接线时必须有意决定这些入口是否接收 canonical id,而不是意外继承 live 路径的不变量。

bot 第 7 轮 review 的两个 Critical 在本 head 上的状态

  • R7-1(mixed-case 恢复会话的 transcript/export 404)——依旧成立(本轮已在树内核实):resolveTranscriptSessionRuntimeactiveInRuntime(session.ts:1624)仍把精确大小写的请求拼写给 assertSessionLoadable。作者立场:不单点修补该 consumer——修复是被跟踪在 Session lifecycle operations are gated on provenance classification, making unclassifiable sessions unmanageable #9488 的单一 storage-boundary resolver——且 CLI 写入侧现已关闭,受影响群体不再扩大。该延迟是否可接受是维护者裁断,也是本 PR 当前最主要的未决问题。
  • R2-3(前 10 条记录 fail-closed 窗口)——机制未变,且为有意readCreationMetadataInternal 仍要求整个扫描头部的行完整性(已在 sessionService.ts 中核实)。这是 PR 正文 Risk & Scope 中披露的 O1 取舍("值得写入 release note")。@wenshao 在本 head 的重新验证建议带着 release note 合并;@yiliang114 已在本 head 批准。该线程声明的关闭条件——在线程内记录维护者的明确接受——尚未被字面满足。

承接 @wenshao 重新验证的打磨项(他的发现,如实归属)

CLI 把"session store 不可读"误报为 id 冲突——枚举本身失败时,"already exists" 的建议无法生效。在 CLI 分支复用 daemon 的 503 session_id_admission_unavailable 措辞只需一个 if。非阻断。

测试证据

无人值守运行——此处不构建、不执行任何 PR 代码;证据为 API 一次性拉取的、62d5c5a1 上 PR 自身的 CI(不轮询)。决定性通道首次在本 PR 的修复 commit 上落绿:Test (ubuntu-latest, Node 22.x)(完整单测套件)与 Serve A/B 均通过——第 9 轮曾指出修复 commit 从未有套件级运行落地,本 head 了结了这一点。一条红通道:ubuntu-latest / Java 21ZipException opening "logback-core-1.3.16.jar": zip END header not found 失败——自托管 runner Maven 缓存中的损坏 jar。判定为环境问题、非 PR 所致:diff 不触及任何 Java 代码、pom.xml 或依赖;同一 SDK 套件在其余四条 Java 通道(ubuntu Java 11/17、macOS Java 21、windows Java 21)上为绿;该 workflow 在上一 head 上为绿。重跑或清理该 runner 缓存即可落绿。Test (macos/windows)Integration Tests (CLI, No Sandbox) 按 CI 设计保持 skipped(merge-queue/批准后门禁)——既有设计,非本 PR 所致。仍在运行的 review-pr 是 bot 自身的编排任务,不属于 PR CI。

本 head 上的真实场景覆盖已存在于本线程:@wenshao 的重新验证以真实 daemon、真实 CLI 与大小写不敏感的 ext4 挂载对 base 与 head 做了 A/B,附九组反向对照——维护者产出的证据,如实归属,此处未重跑。唯一没有任何通道可以落定的行为面:inodeVerifiable 兜底需要报告 inode 0 的文件系统,任何 CI 主机都不具备(sponsored /verify 沙箱同样不具备)——它仅由 PR 单测加代码审读覆盖,如实披露,不粉饰。

(CI 明细见上方表格,finalize 任务会在 CI 落定后原地更新该表。)

Qwen Code · qwen3.8-max

Reviewed at 62d5c5a193ea0180350564ea66e3f5dd6ef2b3e5 · re-run with @qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — clean delta, decisive CI finally green on the fix commits, and both open Criticals are tracked decisions rather than unknowns; the cap is the standing Stage 0 policy for a fork PR with 1,570 production lines across core packages, not doubt about this head.

Stepping back: this is what ten review rounds should look like when the discipline holds. The two fix commits close exactly what they name — the batch lifecycle deadlocking on case twins, the CLI still minting mixed-case transcripts — and the resolver unification removes the duplicated algorithm that was the root cause of the escape landing in one arm only. Every fix is pinned by tests running both directions. The deferrals are tracked in three real, open issues (#9488, #9489, #9490) instead of bloating a tenth-round diff, which is the right posture at this round count. And the evidence at this head is unusually strong: @wenshao re-ran the full real-daemon A/B with nine negative controls hours before this trigger, and @yiliang114 approved at exactly this commit.

What keeps this from an approval is not any of that. It is, first, policy: the Stage 0 escalation — fork PR, 1,570 production lines across packages/core and packages/cli — caps this gate at defer-and-hand-off no matter how clean the stages look, so the bot's approval stays withheld. And second, the two gating findings of the bot's own round-7 review are still open at this head, both now in the maintainers' hands rather than the author's:

  1. R7-1 — transcript/export routing 404s for mixed-case restored sessions (activeInRuntime still exact-case; re-verified in-tree this run). The author declined a one-off patch and deferred to the storage-boundary resolver in Session lifecycle operations are gated on provenance classification, making unclassifiable sessions unmanageable #9488, with the write side closed so the cohort stops growing. Accepting that deferral — or requiring the fix in this PR — is a maintainer call.
  2. R2-3 — the 10-record fail-closed window, unchanged and intentional: the disclosed O1 trade-off from the PR body. @wenshao recommends merge with a release note and @yiliang114 approved; the thread's stated closure condition (explicit maintainer acceptance recorded on-thread) still wants a sentence on the thread itself.

No review is submitted this round: this run found no new blocking issues, so there is nothing new to request changes on, and approval is capped by policy. The bot's round-7 CHANGES_REQUESTED review is left as-is too — its gating findings are exactly the two items above, which still stand as tracked deferrals rather than fixed code, so this run neither re-submits nor dismisses it. If the maintainers accept the deferral and the trade-off, dismissing that review is theirs to do.

One CI note: ubuntu-latest / Java 21 is red at this head from a corrupt logback-core jar in the self-hosted runner's Maven cache — environmental, not PR-caused (four sibling Java legs green, no Java or dependency changes in the diff, workflow green at the previous head). A re-run or cache clear settles it.

⏸️ Deferring to @wenshao and @yiliang114. What this PR needs from here: (1) the call on R7-1 — accept the #9488 deferral or require the fix in-PR; (2) the on-thread acceptance of the O1 trade-off for R2-3 (a release-note commitment suffices); (3) final sign-off — main wants two approvals, the bot's is withheld by the Stage 0 cap, and a sponsored @qwen-code /verify remains available first if an independent sandboxed A/B at this head is wanted beyond the manual runs already on the thread.

中文说明

置信度:3/5 —— delta 干净,决定性 CI 首次在修复 commit 上落绿,两个未决 Critical 都是被跟踪的裁断项而非未知项;封顶来自 Stage 0 对"fork PR、1,570 行生产改动横跨核心包"的既有策略,而非对本 head 的疑虑。

退一步看:在纪律保持的前提下,这正是第十轮 review 应有的形态。两个修复 commit 恰好关闭了它们点名的内容——批量生命周期在大小写孪生上死锁、CLI 仍在铸造 mixed-case transcript——resolver 统一则移除了让 occupancy 逃逸只落进单臂的根因(重复的算法)。每个修复都有双向测试钉住。延迟项被跟踪在三个真实且开放的 issue(#9488#9489#9490)中,而不是膨胀第十轮 diff——在这个轮次上这是正确姿态。本 head 上的证据也异常充分:@wenshao 在本次触发前数小时刚以九组反向对照重跑了完整的真实 daemon A/B,@yiliang114 恰在本 commit 上批准。

使它不能被批准的不是这些。其一是策略:Stage 0 升级——fork PR、横跨 packages/corepackages/cli 的 1,570 行生产改动——使本门禁无论各阶段多干净都只能移交人工,bot 的批准按策略扣留。其二是 bot 自己第 7 轮 review 的两个门禁项在本 head 上仍未决,且都已从作者手中转入维护者手中:

  1. R7-1 —— mixed-case 恢复会话的 transcript/export 路由 404(activeInRuntime 仍用精确拼写;本轮已重新在树内核实)。作者拒绝单点修补,移交 Session lifecycle operations are gated on provenance classification, making unclassifiable sessions unmanageable #9488 的 storage-boundary resolver,且写入侧已关闭、受影响群体不再扩大。接受该延迟——还是要求在本 PR 内修复——是维护者裁断。
  2. R2-3 —— 前 10 条记录 fail-closed 窗口,未变且为有意:PR 正文披露的 O1 取舍。@wenshao 建议带 release note 合并、@yiliang114 已批准;该线程声明的关闭条件(在线程内记录维护者的明确接受)仍差线程内的一句话。

本轮不提交 review:本轮未发现新的阻断项,无可要求修改之事;批准又被策略封顶。bot 第 7 轮的 CHANGES_REQUESTED 也原样保留——其门禁项恰是上述两条,它们作为被跟踪的延迟项依然成立而非已被代码修复,因此本轮既不重提也不 dismiss。若维护者接受该延迟与该取舍,dismiss 该 review 由维护者执行。

一条 CI 说明:ubuntu-latest / Java 21 在本 head 上因自托管 runner Maven 缓存中损坏的 logback-core jar 而红——环境问题、非 PR 所致(其余四条 Java 通道为绿,diff 无 Java 或依赖改动,该 workflow 在上一 head 为绿)。重跑或清缓存即可落绿。

⏸️ 移交 @wenshao@yiliang114。本 PR 接下来需要:(1) 对 R7-1 的裁断——接受 #9488 延迟,或要求在本 PR 内修复;(2) 在线程内对 R2-3 的 O1 取舍的接受(承诺写入 release note 即可);(3) 最终签核——main 需要两个批准,bot 的批准被 Stage 0 封顶扣留;如签核前想要独立于线程内人工运行的沙箱 A/B,可发起 sponsored @qwen-code /verify

Qwen Code · qwen3.8-max

Reviewed at 62d5c5a193ea0180350564ea66e3f5dd6ef2b3e5 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot has neither a verdict nor a deferral on e00ed9f9f6314b335ec18b7f3dbe67040c369da8 — 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.

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

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

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

The ubuntu Test job failed on ten PR2A cases that pass on macOS:

- Export SessionIdCaseConflictError from the worktree test's core mock
  and give its SessionService double findSessionIdIgnoringCase, since
  loadSession now resolves persisted spelling before reading metadata.
- Add readCreationMetadataIfReadable to the Live task fake and mirror
  it onto the three server lifecycle spies so the fail-closed store
  interface sees the same fixture metadata as the legacy tolerant
  readCreationMetadata path it replaced.
- Pin the original inode via rename in the two same-path replacement
  cases. ext4/overlayfs recycle a freed inode immediately, so rm+mkdir
  at the same path could satisfy the recorded device+inode identity on
  Linux runners and make a real replacement look valid.
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

CI fix pushed (commit 9736211)

The ubuntu Test job failed 10 PR2A cases that pass on macOS. Root causes and fixes, all test-side — no production code changed:

Failed case(s) Root cause Fix
acpAgent.worktree.test.ts VP1/VP2/VP2b Core mock did not export SessionIdCaseConflictError, and the SessionService double lacked findSessionIdIgnoringCase, which loadSession now calls before reading metadata Exported the error class from the mock; added findSessionIdIgnoringCase resolving to the fixture session id
live-task-service.test.ts ×2 Fake SessionService lacked readCreationMetadataIfReadable, the fail-closed store method the classification path now uses Mirrored it on the fake, delegating to the existing readCreationMetadata fixture
server.test.ts ×3 (organization proof, generic-creation/relocation, case-spelling restore) Lifecycle spies only stubbed legacy readCreationMetadata; the store interface read the real (missing) filesystem and returned undefined → 404 Added parallel readCreationMetadataIfReadable spies delegating to the same mocks
conversation-directory-identity.test.ts, conversation-workspace.test.ts replacement cases rm + mkdir at the same path can satisfy the recorded device+inode identity on Linux (ext4/overlayfs recycle freed inodes immediately), so the replacement looked valid Pin the original inode by renameing it aside before creating the replacement — deterministic on every platform

Also noted in the job log: an # ENOSPC has failed test steps mid-suite banner on the runner — unrelated to these ten cases.

Verification (macOS, this branch)

  • acpAgent.worktree.test.ts 3/3, live-task-service.test.ts 15/15, conversation-directory-identity.test.ts 7/7, conversation-workspace.test.ts 11/11 — all pass.
  • server.test.ts full file twice: the three repaired cases pass; exactly one unrelated case drifted per run (known whole-file shared-state flake, same as documented in refactor(cli): Generalize the Conversations runtime foundation #8890; the drifting case differs between runs and passes in isolation).
  • Repo npm run typecheck, ESLint and Prettier on the five changed files — clean.

Note for reviewers: the two same-path replacement assertions previously relied on inode numbers differencing after rm+mkdir, which is filesystem-dependent. The inode-pinning rewrite keeps the assertion's intent (a same-path replacement must be rejected) while making it deterministic on inode-recycling filesystems.

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 17, 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: 2113 passed · 0 failed · 2113 total

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

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

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

Verification report

PR 9341 Deep Verification (round 2) — feat(cli): Add standalone conversation isolation primitives

Verdict: merge-ready — assertion totals 2113 pass / 0 fail (2113). Verified head OID 9736211 (HEAD^2); base 18c9763 (HEAD^1). This is a follow-up round: round 1 (head e00ed9f9f6) reported findings because the PR shipped 7 red unit tests on Linux. The new commit 4 (test(cli): repair PR2A CI doubles and identity replacement cases) repairs exactly those failures; every carried-forward measurement was rebuilt and re-run at the new head against the moved base. The central fail-closed claim is again proven load-bearing by a clean A/B, all three targeted gates are green as delivered, and no new finding emerged. The only open item is round 1's F3 — a test-coverage gap on a load-bearing guard — which is carried forward as a suggestion, not a merge condition.

中文 — 判定:✅ 通过(agent 判定)
  • 结论merge-ready(2113 通过 / 0 失败)。本轮为跟进轮:第 1 轮(head e00ed9f9f6)因 PR 在 Linux 上自带 7 个红色单测而判 findings;新增 commit 4 正好修复这些失败。本轮在新 head(97362119bd)+ 新 base(18c9763f46,base 已前移)上重新构建并重跑了全部携带测量,未复用旧报告数字。
  • A/B 结论:见下表。5 种损坏形状(截断/垃圾/标量/数组/窗口内损坏)base 全部误 promote、head 全部 fail-closed 拒绝;4 种良性形状两臂均保持可加载;tolerant reader 行为未变(同一文件上 tolerant 与 strict 方法的分叉已断言)。次级主张(大小写权威查找、reserved standalone source 守卫、lineage 验证、目录 identity 守卫)全部重新证实。
  • 此前 findings 状态:F1(接口迁移遗留测试桩,5 测红)已修复(big gate 1786/1786 转绿);F2(macOS-only inode 假设,2 测红)已修复(small gate 61/61 转绿,测试改用 rename 钉住 inode);F3(self-lineage 守卫未被测试钉住)仍存在——变异 M3 在交付套件中继续存活,仅补充探针能杀死,属覆盖缺口建议,非合并条件。
  • Findings:仅 F3 携带项(建议级)+ 一条 nit(见正文)。无新增阻塞项。
  • 未覆盖:Windows、真实多进程 daemon 端到端、性能/并发、逐 commit 归因(depth 2)等,见 Not covered

Previous-finding status (round 1 → new head)

# finding (round 1) severity status at head 97362119bd
F1 Interface migration left test stubs behind — 5 unit tests red on Linux (server.test.ts ×3 got 404; live-task-service.test.ts ×2 threw readCreationMetadataIfReadable is not a function) blocking fixed. Re-measured at new head: server.test.ts + live-task-service.test.ts green inside the 5-file big gate (1786/1786, round 1: 1753/1758 with 5 red). Commit 4 added readCreationMetadataIfReadable to the three server lifecycle spies (verified at server.test.ts:31564/31803/31933) and to the live-task fake (live-task-service.test.ts:79). Witness 06-big-gate-green-formerly-f1.png.
F2 Two new tests encoded a macOS-only inode assumption (rm+mkdir at the same path reuses the freed inode on Linux) — deterministic red on Linux blocking fixed. Re-measured: both suites green in the small gate (61/61, round 1: 57/59 with 2 red). Both tests now pin the original inode via rename (verified conversation-directory-identity.test.ts:103, conversation-workspace.test.ts:312); harness 04's rename-over probe independently confirms the production guard rejects a real inode change on this Linux box. Witness 05-small-gate-green-formerly-f2.png.
F3 Coverage gap: self-lineage guard survives the delivered session-source.test.ts for the explicit-standalone-child shape (mutant M3) suggestion stands. Re-measured, not diffed: mutant M3 (guard → false) again leaves the delivered suite 19/19 green, while the recreated probe goes red with expected { kind: 'standalone', … } to be undefined. No test for this shape was added in commit 4. See Findings. Witness 08-mutation-m3-selflineage-survivor.png.

Central claim + A/B

Central claim: provenance classification fails closed when a transcript's creation metadata is truncated/malformed/missing, while clean legacy transcripts and fully-recoverable }{-glued records stay loadable; tolerant readers elsewhere are unchanged.

Mock-free A/B (01-failclosed-ab.mjs): real SessionService from each arm's own dist build over real transcript files under a temp runtime dir. Base arm drives readLoadableLiveConversationMetadata with the pre-PR callback; head arm passes the service as the new store. Build fingerprints asserted on both arms (base dist lacks SessionIdCaseConflictError/readCreationMetadataIfReadable/readLinesWithIntegrity; head dist has all three — grep counts 0 vs 5/1/4), so neither cell could silently load the other's code. Witness: 01-failclosed-ab-base-vs-head.png.

cell base head verdict
clean-legacy (no metadata) loadable loadable preserved
explicit-live (default + realtime_voice:*) loadable loadable preserved
truncated creation record loadable (promoted) rejected flipped — fixed
garbage bytes loadable (promoted) rejected flipped — fixed
scalar line (42) loadable (promoted) rejected flipped — fixed
array line ([1,2,3]) loadable (promoted) rejected flipped — fixed
}{-glued complete records loadable loadable preserved (recovery kept)
damage past 10-record window loadable loadable preserved (bounded scan)
valid head + damage within window loadable (promoted) rejected flipped — fixed
tolerant-vs-strict on same damaged file tolerant {}, strict undefined tolerant reader unchanged

Mechanism attribution (re-verified): the four corrupt-head cells are rejected at head's location gate (unreadable head record → getSessionLocation undefined) plus integrity; the valid-head/damage-within-window cell is rejected purely by the integrity complete flag — an attribution probe (logs/attribution-probe.txt) shows that cell resolves location=active while the strict read returns undefined. Harness self-caught a fixture bug on first run (non-hex session ids tripped SESSION_FILE_PATTERN and masked the mechanism); after fixing to hex-only ids all 15/15 assertions pass.

Secondary claims (all re-confirmed):

  • Case lookup (02-case-conflict-ab.mjs, 11/11, witness 02-case-conflict-ab.png): unique mixed-case resolves the persisted spelling on both arms; case-only duplicates within active and same-spelling-across-active+archived throw SessionIdCaseConflictError at head (with getSessionLocation reporting conflict for the latter) while base returns a first match / the conflicted id; absent id → undefined on both arms.
  • Reserved source + lineage (03-reserved-source-ab.mjs, 18/18, witness 03-reserved-source-ab.png): the hole cell — a transcript claiming sourceType:'standalone'+sourceId — passes base's generic-restore decision (raw tolerant read continues) and is rejected by head's new reserved-source check; explicit standalone top-level and explicit standalone child-without-parent are loadable at head only (new feature, live path still refuses both); legacy child of legacy parent loadable on both arms; self-lineage and cyclic lineage refused on both arms; the F3 shape (self-referencing explicit standalone child) refused at head.
  • Directory identity (04-identity-probe.mjs, 12/12, witness 04-identity-probe.png, head-only — the module is new): root created 0700 with dev+inode recorded; equivalent canonical root path accepted, foreign path unexpected_identity; rename-over replacement (inode verified to change: recorded → new) rejected unexpected_identity; symlink escape rejected not_directory; group/world-readable rejected wrong_mode and re-inspects clean after chmod 0700; unknown id inspects as undefined.

Corrections

  • Round 1 noted the PR body's claim "targeted final tests passed … the server/ACP Agent/Live-task suites" was not reproducible on Linux at head e00ed9f9f6. At the new head 97362119bd those suites are green on Linux (big gate 1786/1786), so the PR body's statement is now accurate — this corrects round 1's note, not the code.
  • Round 1's big gate did not include acpAgent.worktree.test.ts; commit 4's message reports the Ubuntu Test job failed ten PR2A cases (vs round 1's measured seven), the difference being worktree-test cases round 1 never ran. Round 1's red count was therefore a lower bound on CI failures; this round's gate includes the worktree file (green at head; it needed the SessionIdCaseConflictError export in its core mock plus a findSessionIdIgnoringCase double — verified at acpAgent.worktree.test.ts:176/331).

Findings

F3 (carried over, suggestion) — self-lineage guard not pinned for the explicit-standalone-child shape

Mutant M3 (delete normalizeSessionIdForLookup(parentSessionId) === normalizeSessionIdForLookup(sessionId) in session-source.ts) survives the delivered session-source.test.ts (19/19 green under mutation): the delivered SELF_ID case has no sourceType, so it is rejected anyway by the parent-must-be-top-level rule and never exercises the explicit comparison. The guard is load-bearing for a self-referencing explicit standalone child ({sourceType:'standalone', parentSessionId: SELF}), which reaches the explicit-child early return before any parent read — with the guard deleted, that shape classifies {kind:'standalone', persistence:'explicit'} (probe failure: expected { kind: 'standalone', … } to be undefined). Classification unchanged from round 1: coverage gap, not dead code, not a production defect.

Suggested fix (probe fixture that kills the mutant; green at delivered head)
// in session-source.test.ts
const SELF_EXPLICIT = '550e8400-e29b-41d4-a716-44665544000f';
const explicitSelfStore: ConversationSessionMetadataStore = {
  async getSessionLocation() {
    return 'active';
  },
  async readCreationMetadataIfReadable() {
    return { sourceType: 'standalone', parentSessionId: SELF_EXPLICIT };
  },
};
// rejects a self-referencing explicit standalone child:
await expect(
  readLoadableConversationSession(SELF_EXPLICIT, explicitSelfStore),
).resolves.toBeUndefined();
// control — non-self parent stays loadable:
// readCreationMetadataIfReadable returns parentSessionId of a DIFFERENT id
// -> result is { kind: 'standalone', persistence: 'explicit' }.

Nit (new, optional): the repaired server.test.ts spies delegate readCreationMetadataIfReadable to the same tolerant readCreationMetadata stub, so the route-level tests cannot distinguish a regression that swaps the strict store method back for the tolerant one. The strict/tolerant split is pinned elsewhere (core corruption tests under M1, plus harness 01's tolerant-vs-strict assertion end to end), so this only narrows what the server suite would catch.

Mutation matrix (round 2)

mutant guard removed pinning suite result
M1 integrity complete flag forced true core jsonl-utils + corruption killed — 5 red: "reports incomplete recovery for a truncated record / trailing garbage / a non-object value / an invalid middle fragment" + "distinguishes clean legacy metadata from an unreadable transcript head" (witness 07-mutation-m1-integrity-disabled.png)
M2 case-conflict throws → first-candidate returns core sessionService killed — 2 red: "rejects case-only duplicate spellings instead of choosing by enumeration order", "rejects one spelling that exists in both active and archive state"
M3 self-lineage comparison → false cli session-source survived delivered 19/19; killed by added probe → coverage gap F3 (witness 08-mutation-m3-selflineage-survivor.png)
M4 TOCTOU location recheck removed cli session-source killed — 1 red: "rejects a transcript that disappears while its metadata is read"

Positive controls: M1 and M2 each failed on exactly the intended assertions (quoted above; full logs logs/mutation-M*.txt); unmutated controls are the green gate rows below. Mutation runs are evidence for suite liveness and are not counted in assertions.json (same convention as round 1).

Targeted gates (head, as delivered)

gate result
core: jsonl-utils + sessionService + corruption 209/209 pass
cli small: session-source, session-id-admission, error-response, directory-identity, conversation-workspace 61/61 pass (round 1: 57/59)
cli big: server, acp-http transport, acpAgent, acpAgent.worktree, live-task-service 1786/1786 pass (round 1: 1753/1758; worktree file now included)

No repo-wide gate was claimed or run.

Not covered

  • Per-commit attribution: checkout is depth 2 (merge, base tip, PR head only); git rev-list HEAD^1..HEAD^2 returns 1 commit while the metadata lists 4 — the shallow-boundary gap. The aggregate HEAD^1..HEAD diff was verified; the delta since round 1 (commit 4) was reconstructed by verifying each claimed repair is present in the delivered test files, since the previous head e00ed9f9f6 is not locally reachable.
  • Windows: isSameConversationPath win32 branches and case-insensitive FS behavior not exercised (Linux container).
  • True multi-process daemon E2E: restore/admission verified at dist/component level plus the PR's own in-process server/transport suites; no live daemon over a real socket was booted.
  • Performance / concurrency: no load or race testing of the scan-window reads; MAX_PROMPT_SCAN_LINES=10 bounds the read and the inputs are daemon-written transcripts, not outsider text, so no timing ladder was run.
  • Lint/format/typecheck repo-wide: CI builds head (typecheck) green per the environment contract; no separate lint run.
  • Docs-only changes (standalone-daemon-sessions.md, 2026-08-14-standalone-pr2-core.md): reviewed for claim context, not behaviorally tested.
  • Mutation coverage beyond M1–M4: other guards the PR introduces (e.g. getSessionLocation's pattern pre-filter, directory-identity owner/mode checks under a different uid) were not mutated.

Methodology

Environment: CI merge-ref checkout at 3cc1b7808c (base 18c9763f46 = HEAD^1, head 97362119bd = HEAD^2), node:22-bookworm-class container, Node v22.23.2; npm ci + npm run build pre-run at HEAD. The base moved since round 1 (a35a23cd6818c9763f46), so both arms were re-measured from scratch rather than carried. A/B base side built in git worktree tmp/base-tree at HEAD^1, rebuilding only packages/core + packages/cli (~80 s); the base worktree got its own node_modules/@qwen-code/ with qwen-code-core/qwen-code symlinked into the base tree (realpath asserted: …/tmp/base-tree/packages/core) and all other entries pointing at head's, @lydell mirrored for the core tsconfig paths mapping, and head's package-level node_modules symlinked (neither contains @qwen-code, so no confound); the PR leaves package.json/package-lock.json untouched, making the shared root node_modules a clean control. Controls validated by build fingerprint on every harness run. Harnesses 0104 drive compiled dist/ output with real files (no stubs of the unit under test). Mutation runs applied single-point source edits in-tree, ran the pinning vitest suite, then git checkout -- restored (final git status --porcelain empty; scratch probe test removed). Gates ran at delivered head before any mutation. Evidence PNGs rendered via scripts/verify-capture.mjs; raw per-run logs live in logs/. assertions.json counts map 1:1 to executed scripted checks: 209 + 61 + 1786 gate tests + 56 harness check() calls + 1 attribution probe = 2113, with 0 unexpected outcomes.

Re-running harnesses 0103: they import BOTH builds; the base worktree was removed after capture. Rebuild it with: git worktree add tmp/base-tree <baseRefOid>; create tmp/base-tree/node_modules/@qwen-code/ with qwen-code-core/qwen-code../../packages/{core,cli} and every other entry → head's node_modules/@qwen-code/<name>; symlink head's node_modules/@lydell and head's packages/{core,cli}/node_modules into the base tree; then npm run generate && npm run build -w packages/core -w packages/cli inside the worktree. Harness 04 is head-only and reruns as-is.

Evidence images

01-failclosed-ab-base-vs-head

02-case-conflict-ab

03-reserved-source-ab

04-identity-probe

05-small-gate-green-formerly-f2

06-big-gate-green-formerly-f1

07-mutation-m1-integrity-disabled

08-mutation-m3-selflineage-survivor

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

Qwen Code · sandboxed verification

@wenshao

wenshao commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Local verification — real qwen serve daemon, base vs head

I verified head 97362119bd against base 5492009bb2 (the merge-base with main) on a local machine. Both trees were built from source in separate worktrees (npm install && npm run build && npm run bundle) and I drove the two resulting dist/cli.js serve daemons. Every result below is an HTTP status/body from a live daemon process reading genuine chats/<sessionId>.jsonl files off disk — no vitest doubles, no stubbed SessionService, no mocked filesystem.

Two things were needed to make the interesting paths reachable:

  • The classification code only runs for the internal Conversations runtime, so the harness publishes it via POST /live/start and then addresses sessions with cwd = ~/Documents/Qwen Code/Conversations. A healthy Live transcript restores with 200 and materialises its private conversation-<sha256> directory — that is what makes the rest of the matrix meaningful.
  • Case-only duplicates cannot exist on a stock macOS volume, so the daemon's HOME lives on a case-sensitive APFS volume created for this run, reproducing the ext4 behaviour of the CI runners.

1. Behaviour matrix — 16 scenarios, 7 changes

A/B matrix

scenario base 5492009 head 9736211
L0 healthy Live transcript restores 200 200
S1 explicit standalone, top-level 404 session_not_found 404 session_not_found
S2 explicit standalone, child 404 session_not_found 404 session_not_found
S3 legacy standalone parent (control) 200 200
C1 truncated creation record 200 404 session_not_found
C2 garbage first line 200 404 session_not_found
C3 scalar JSONL line 200 404 session_not_found
C4 complete }{-glued creation record 200 200
G1 clean legacy transcript, no metadata 200 200
P1 orphan child, parent missing 200 404 session_not_found
P2 self-referential lineage 404 session_not_found 404 session_not_found
A1 same id in active and archive 409 session_conflict 409 session_conflict
M1 lowercase request, UPPERCASE on disk 404 session_not_found 200
M2 case-only duplicate spellings 200 409 session_conflict
R1 POST /session with sourceType: "standalone" 200 — session created 400 reserved_session_source
R2 POST /session with sourceType: "webshell" 200 200

All five claims in the reviewer test plan hold end to end, and every new guard is load-bearing — base does the wrong thing in all seven changed rows. Equally important, the nine unchanged rows show no regression: a healthy Live transcript, a clean metadata-free legacy transcript, a legacy standalone parent, and a fully recoverable }{-glued creation record all still restore.

M1 is the sharpest single piece of evidence. With only AAAAAAAA-7777-….jsonl on disk and a lowercase id in the request, head returned 200 and materialised conversation-<sha256(UPPERCASE id)> — it bound the persisted spelling, not the caller's. base returned 404 and created no directory at all.

live run

2. The fail-closed rule stays inside the Conversations runtime

I planted the identical damaged transcript in an ordinary workspace project and in the Conversations project:

base head
ordinary workspace — GET …/transcript 200 200
ordinary workspace — POST /session/:id/load 200 200
Conversations — GET …/transcript 200 404

Ordinary project sessions are untouched, exactly as the PR claims. Worth stating explicitly for the record: inside Conversations the change closes the reader route too, not only restore — a damaged Live transcript becomes unreadable through the daemon, not merely unrestorable.

3. Private-directory identity is behaviour-preserving

I materialised three private conversation directories through real restores, then tampered with them on disk and restored again:

tamper base head
directory replaced by a symlink pointing outside the root 500 (rejected) 500 (rejected)
directory made world-writable (0777) 500 (rejected) 500 (rejected)
directory deleted and recreated (same path, new inode) 200 200

Identical on both arms. The new conversation-directory-identity.ts is an extraction with typed failure reasons for PR2B, not a behaviour change at the daemon boundary — the right shape for a PR that says it publishes no new surface.

4. Test suites — including the platform the bot flagged

The earlier sandboxed run reported 7 red cases on the Linux leg, and 97362119b was pushed to fix them. I re-ran the 13 touched test files on both platforms, and also on the pre-fix commit, to confirm the fix is what closed them:

test summary

tree platform result
head 9736211 macOS 26 · node 24.18.1 2015 passed · 0 failed
head 9736211 Linux · node 22.23.2 · uid 1000 2015 passed · 0 failed
pre-fix e00ed9f Linux · node 22.23.2 · uid 1000 10 failed · 2005 passed

The 10 red cases I reproduce at e00ed9f are exactly the set 97362119b claims to fix (acpAgent.worktree VP1/VP2/VP2b, live-task-service ×2, server.test ×3, conversation-directory-identity, conversation-workspace) — my count matches the author's fix note rather than the bot's 7, which is presumably a difference in how that harness aggregates. The CI regression is genuinely closed.


Observations — none blocking

observations

O1 · The fail-closed rule is wider than "creation metadata", and it is permanent.
readCreationMetadataIfReadable requires every one of the first MAX_PROMPT_SCAN_LINES (10) records to be fully recoverable, not just the records that carry creation metadata. Measured on a transcript of 1 creation record + 24 turns with a single truncated line:

torn line base head
record #3 200 404
record #6 200 404
record #10 200 404
record #11 200 200
record #21 200 200

A tear at record #10 is as fatal as a tear at record #1, and nothing ever rewrites those lines, so the conversation is permanently unrestorable. The PR body describes this as "truncated creation metadata", which understates the blast radius. If you want the safety without the extra reach, the integrity requirement could stop at the last record that actually contributed a session_source / parent_session payload rather than covering the whole 10-line scan window. Your call — the current behaviour is a defensible reading of "fail closed", it is just broader than the PR text suggests.

O2 · Restore is now O(sessions on disk).
head replaces the exact-match stat with findSessionIdIgnoringCase, which readdir()s both the active and the archive chats directories on every restore. Median POST /session/:id/load round trip:

sessions on disk base head delta
50 5.90 ms 6.42 ms +0.5 ms
5,000 5.83 ms 10.93 ms +5.1 ms
20,000 6.23 ms 22.63 ms +16.4 ms

base is flat; head grows linearly. The absolute cost is small, but it is paid on every restore, and the call site in acpAgent.ts is explicitly wrapped in profiler.time('existence_check', …) — someone considered it latency-sensitive. Worth keeping in mind for PR2B rather than changing here.

O3 · Minor: the new sanitizes standalone child filesystem errors test is root-fragile.
It chmods a directory to 0o000 and expects an io_error; root bypasses that, so the case fails when the suite runs as root (I hit it before switching my container to uid 1000). GitHub Actions runs non-root so CI is unaffected, and the repo already has unguarded 0o000 tests elsewhere — but the guarded idiom (it.skipIf(process.platform === 'win32' || process.getuid?.() === 0), e.g. cost-ledger.test.ts) exists if you'd like this one to survive container-based runs.

O4 · Minor: a redundant parent read.
For a legacy standalone child, readLoadableConversationSession reads and classifies the parent, and then readLoadableLiveConversationMetadata reads and classifies it again — three extra filesystem ops (getSessionLocation ×2 plus one metadata read) per restore. The second read is also a small TOCTOU window, though it fails closed either way. Passing the already-classified parent through would remove both.

What I did not verify

  • The session/new reserved-source guard on the ACP transport: my WebSocket probe never completed the /acp upgrade (the request landed on the SSE handler and returned 406), so I have no live evidence there. That path is covered by this PR's own transport.test.ts additions, which pass on both platforms.
  • Windows.
  • Anything in PR2B — no standalone routes, capabilities, SDK types or UI exist yet to exercise.

Verdict

From my side this is good to merge. The primitives do what the PR says they do, every new guard is load-bearing against the base build, nothing regresses on healthy or legacy transcripts, the containment discipline holds, and the Linux CI regression is genuinely fixed. O1 is the one thing I would want accepted consciously rather than by default, since it turns a torn line anywhere in a conversation's first ten records into permanent loss of access to that conversation.

中文版

本地验证 —— 真实 qwen serve daemon,base vs head

我在本地把 head 97362119bd 与 base 5492009bb2(与 main 的 merge-base)做了对照验证。两棵树分别在独立 worktree 里从源码构建(npm install && npm run build && npm run bundle),然后分别驱动各自的 dist/cli.js serve下面每一条结果都来自一个真实运行中的 daemon 进程返回的 HTTP 状态码/响应体,读的是磁盘上真实的 chats/<sessionId>.jsonl —— 没有 vitest 替身,没有打桩的 SessionService,没有 mock 文件系统。

要让这些关键路径真正可达,需要两个前提:

  • 这些分类代码只在内部 Conversations runtime 上运行,所以 harness 通过 POST /live/start 把它发布出来,再用 cwd = ~/Documents/Qwen Code/Conversations 定位会话。一条健康的 Live transcript 能以 200 恢复并物化出私有的 conversation-<sha256> 目录 —— 这一点成立,后面的矩阵才有意义。
  • macOS 默认卷上造不出仅大小写不同的重名文件,所以 daemon 的 HOME 放在为本次验证创建的大小写敏感 APFS 卷上,用来复现 CI runner 上 ext4 的行为。

1. 行为矩阵 —— 16 个场景,7 处变化

场景 base 5492009 head 9736211
L0 健康 Live transcript 可恢复 200 200
S1 explicit standalone,顶层 404 session_not_found 404 session_not_found
S2 explicit standalone,子会话 404 session_not_found 404 session_not_found
S3 legacy standalone 父会话(对照) 200 200
C1 被截断的 creation record 200 404 session_not_found
C2 首行是垃圾数据 200 404 session_not_found
C3 JSONL 行是标量 200 404 session_not_found
C4 完整可恢复的 }{ 粘连记录 200 200
G1 干净的 legacy transcript,无 metadata 200 200
P1 孤儿子会话,父会话不存在 200 404 session_not_found
P2 自引用 lineage 404 session_not_found 404 session_not_found
A1 同一 id 同时在 active 和 archive 409 session_conflict 409 session_conflict
M1 请求用小写 id,磁盘上是大写 404 session_not_found 200
M2 仅大小写不同的重名 transcript 200 409 session_conflict
R1 POST /sessionsourceType: "standalone" 200 —— 会话被创建 400 reserved_session_source
R2 POST /sessionsourceType: "webshell" 200 200

Reviewer 测试计划里的五条主张端到端都成立,而且每个新加的闸门都是有效的 —— 在全部 7 处变化的行上,base 的行为都是错的。同样重要的是,另外 9 行没有变化,说明没有回归:健康的 Live transcript、干净无 metadata 的 legacy transcript、legacy standalone 父会话、以及完整可恢复的 }{ 粘连 creation record,都仍然能正常恢复。

M1 是单条最有力的证据。 磁盘上只有 AAAAAAAA-7777-….jsonl,请求用小写 id,head 返回 200,并且物化出的目录是 conversation-<sha256(大写 id)> —— 它绑定的是持久化的拼写,而不是调用方给的拼写。base 返回 404,什么目录都没建。

2. fail-closed 规则被限制在 Conversations runtime 内部

我把完全相同的损坏 transcript 分别放进一个普通 workspace 项目和 Conversations 项目:

base head
普通 workspace —— GET …/transcript 200 200
普通 workspace —— POST /session/:id/load 200 200
Conversations —— GET …/transcript 200 404

普通项目会话完全不受影响,与 PR 的说法一致。有一点需要明确记录:在 Conversations 内部,这个改动同时关闭了读取路由,而不只是 restore —— 一条损坏的 Live transcript 通过 daemon 变得不可读,而不仅仅是不可恢复。

3. 私有目录 identity 的行为保持不变

我通过真实 restore 物化出三个私有会话目录,然后在磁盘上做手脚,再次 restore:

篡改方式 base head
目录被替换成指向根目录之外的软链 500(拒绝) 500(拒绝)
目录被改成 world-writable(0777 500(拒绝) 500(拒绝)
目录被删除后重建(同路径、新 inode) 200 200

两侧完全一致。新增的 conversation-directory-identity.ts 是为 PR2B 抽取出的、带类型化失败原因的模块,在 daemon 边界上并没有改变行为 —— 对一个声称不发布任何新表面的 PR 来说,这正是应有的形状。

4. 测试套件 —— 包括 bot 标红的那条平台腿

之前的沙箱运行在 Linux 腿上报了 7 个红用例,随后推了 97362119b 来修。我在两个平台上重跑了这 13 个被改动的测试文件,并且在修复前的提交上也跑了一遍,用来确认确实是这个提交关掉了它们:

平台 结果
head 9736211 macOS 26 · node 24.18.1 2015 通过 · 0 失败
head 9736211 Linux · node 22.23.2 · uid 1000 2015 通过 · 0 失败
修复前 e00ed9f Linux · node 22.23.2 · uid 1000 10 失败 · 2005 通过

我在 e00ed9f 上复现出的这 10 个失败用例,正好就是 97362119b 声称修复的那一组(acpAgent.worktree VP1/VP2/VP2b、live-task-service ×2、server.test ×3、conversation-directory-identityconversation-workspace)—— 我的计数与作者的修复说明一致,而不是 bot 的 7,差异应该出在那套 harness 的聚合方式上。CI 回归确实已经修好了。

观察项 —— 都不阻塞合并

O1 · fail-closed 的影响面比「creation metadata」更宽,而且是永久的。
readCreationMetadataIfReadable 要求前 MAX_PROMPT_SCAN_LINES(10)条记录每一条都完整可恢复,而不只是携带 creation metadata 的那几条。在「1 条 creation record + 24 轮对话、只截断其中一行」的 transcript 上实测:

被截断的行 base head
第 3 条记录 200 404
第 6 条记录 200 404
第 10 条记录 200 404
第 11 条记录 200 200
第 21 条记录 200 200

第 10 条被截断和第 1 条被截断一样致命,而且这些行永远不会被重写,所以这个会话就永久不可恢复了。PR 正文把它描述为「truncated creation metadata」,低估了影响面。如果希望保留这份安全性但收窄影响,可以把完整性要求只覆盖到「最后一条真正贡献了 session_source / parent_session 载荷的记录」,而不是整个 10 行扫描窗口。这个由你们定 —— 现在的行为是对「fail closed」的一种合理解读,只是比 PR 文字所说的更宽。

O2 · restore 现在是 O(磁盘上会话数)。
head 把精确匹配的 stat 换成了 findSessionIdIgnoringCase,后者在每次 restore 时都会 readdir() active 与 archive 两个 chats 目录。POST /session/:id/load 往返中位数:

磁盘上的会话数 base head 增量
50 5.90 ms 6.42 ms +0.5 ms
5,000 5.83 ms 10.93 ms +5.1 ms
20,000 6.23 ms 22.63 ms +16.4 ms

base 是平的,head 线性增长。绝对开销不大,但它落在每一次 restore 上;而且 acpAgent.ts 里的调用点明确包在 profiler.time('existence_check', …) 里 —— 说明有人认为这条路径对延迟敏感。建议在 PR2B 里留意,而不是在本 PR 改。

O3 · 小问题:新增的 sanitizes standalone child filesystem errors 用例对 root 敏感。
它把目录 chmod0o000 并期望得到 io_error;root 会绕过权限检查,所以以 root 跑套件时这个用例会失败(我在把容器切到 uid 1000 之前就撞到了)。GitHub Actions 以非 root 运行,因此 CI 不受影响;仓库里也已有其它未加守卫的 0o000 用例。但如果希望它在基于容器的运行环境里也稳,仓库已有现成写法可用(it.skipIf(process.platform === 'win32' || process.getuid?.() === 0),例如 cost-ledger.test.ts)。

O4 · 小问题:父会话被重复读取。
对 legacy standalone 子会话,readLoadableConversationSession 会读取并分类父会话,随后 readLoadableLiveConversationMetadata 又读取并分类了一次 —— 每次 restore 多出 3 次文件系统操作(getSessionLocation ×2 加一次 metadata 读)。第二次读取也带来一个很小的 TOCTOU 窗口,尽管两种情况下都是 fail closed。把已分类的父会话结果传下去就能同时消除这两点。

我没有验证的部分

  • ACP 传输上的 session/new 保留 source 闸门:我的 WebSocket 探针始终没能完成 /acp 升级(请求落到了 SSE 处理器并返回 406),所以这条路径我没有实测证据。它由本 PR 自带的 transport.test.ts 覆盖,且在两个平台上都通过。
  • Windows。
  • PR2B 的任何内容 —— 目前还没有 standalone 路由、capability、SDK 类型或 UI 可供驱动。

结论

从我这边看,可以合并。这些 primitives 确实做到了 PR 所声称的事情,每个新闸门相对 base 构建都是有效的,健康与 legacy transcript 都没有回归,containment 纪律成立,Linux CI 回归也确实修好了。O1 是唯一一件我希望被有意识地接受、而不是默认接受的事情:它会把「一条会话前 10 条记录里任意一行被截断」变成对该会话访问权限的永久丢失。

@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 97362119bd897ec066dd503c65584356da7f7408 — 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.

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

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

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

yiliang114
yiliang114 previously approved these changes Aug 17, 2026

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

Approved after a full pass over the diff at 97362119 plus spot-checks against the head tree.

What I verified:

  • Scope matches the PR2A claim: classification / identity / admission primitives only. No new routes, capabilities, SDK, or UI surface (no new app.<verb> registrations; the only additions are reserved-source guards).
  • Source classification (session-source.ts): explicit standalone requires the reserved standalone sourceType with sourceId === undefined; lineage rejects self-parents via normalizeSessionIdForLookup comparison and requires the parent to classify as a valid top-level session; the location is re-checked after every metadata read so an archive race yields undefined, not a stale classification.
  • Fail-closed reads: readCreationMetadataIfReadable goes through readLinesWithIntegrity; truncated/malformed heads and ENOENT return undefined, while the tolerant readLines/parseLineTolerant behavior is preserved untouched for ordinary readers (glued }{ records still recover when complete).
  • Case handling: findSessionIdIgnoringCase now scans both active and archived storage before deciding and throws the typed SessionIdCaseConflictError on case-only duplicates instead of taking an exact-match shortcut. It maps consistently to REST 409 session_conflict, RPC errorKind: session_conflict, and the ACP agent session_id_conflict. Restore paths use the persisted spelling end-to-end (metadata read, materialize, worktree sidecar), and requireSessionId boundary normalization keeps lock keys and resolution consistent for UUID-shaped ids.
  • Directory identity: lstat/realpath with dev+inode pinning, owner/mode checks, and root revalidation before and after child inspection. The Live path maps typed errors back to the pre-PR messages, so existing behavior is preserved; the standalone prepare/inspect/ensure helpers fail closed on compromised, replaced, or non-empty directories.
  • Guard parity: reserved-standalone creation is rejected on both creation surfaces (REST 400 reserved_session_source, ACP INVALID_PARAMS), and generic restore rejects reserved-standalone metadata with 404 on both REST and ACP. The two remaining parseSessionSource call sites are session-list filters, not creation surfaces.
  • SessionIdCaseConflictError reaches the CLI via the core barrel's existing export * from './services/sessionService.js'.

CI on head 97362119: Test (ubuntu), Serve A/B, web-shell E2E smoke, Desktop Shell, secret scan, and dependency audit all pass; the round-2 sandboxed verification reports 2113/2113 assertions.

One non-blocking observation: the restore path's archiveCoordinator lock is keyed on the requested sessionId while the file operations use the resolved persisted spelling. Boundary lowercasing makes this a no-op for UUID-shaped caller ids today; worth keeping in mind when PR2B adds more lifecycle entry points.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 4)": verify skipGeminiInitialization exists on ConfigParameters and that Config.relocateWorkingDirectory() actually refreshes memory/file-discovery/file-history/…; "agent reverse-audit (round 4)": confirm existence of #bindGoalRuntime / #restoreWorktreeOnResume / #restoreBackgroundAgentsOnResume / NativeLspService / createAndStoreSession / hydrateSession…; "agent reverse-audit (round 2)": trace the bridge restoreSession spawn path in packages/acp-bridge/src/bridge.ts end-to-end to confirm the Session's transcript read/write is keyed by the br…; "agent reverse-audit (round 5)": did not read the acpAgent.ts sessionCd handler to confirm the "current no-op return" same-path premise behind the repair bullet.; "agent reverse-audit (round 5)": did not individually verify ensureAuthenticated/setupFileSystem/startNonInteractiveOpenAILogHousekeeping/createAndStoreSession/hydrateSessionRestoreFileHistory/…, and 5 more.

Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.

中文说明

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

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 4)"verify skipGeminiInitialization exists on ConfigParameters and that Config.relocateWorkingDirectory() actually refreshes memory/file-discovery/file-history/…"agent reverse-audit (round 4)"confirm existence of #bindGoalRuntime / #restoreWorktreeOnResume / #restoreBackgroundAgentsOnResume / NativeLspService / createAndStoreSession / hydrateSession…"agent reverse-audit (round 2)"trace the bridge restoreSession spawn path in packages/acp-bridge/src/bridge.ts end-to-end to confirm the Session's transcript read/write is keyed by the br…"agent reverse-audit (round 5)"did not read the acpAgent.ts sessionCd handler to confirm the "current no-op return" same-path premise behind the repair bullet."agent reverse-audit (round 5)"did not individually verify ensureAuthenticated/setupFileSystem/startNonInteractiveOpenAILogHousekeeping/createAndStoreSession/hydrateSessionRestoreFileHistory/…,另有 5 条。

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

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

Comment thread packages/cli/src/serve/routes/session.ts
Comment thread docs/plans/2026-08-14-standalone-pr2-core.md Outdated
Comment thread docs/plans/2026-08-14-standalone-pr2-core.md
Comment thread docs/plans/2026-08-14-standalone-pr2-core.md Outdated
Comment thread docs/plans/2026-08-14-standalone-pr2-core.md
Comment thread packages/cli/src/serve/routes/session.ts Outdated
Comment thread packages/cli/src/serve/acp-http/dispatch.ts Outdated
Comment thread packages/cli/src/serve/conversations/conversation-workspace.test.ts
Comment thread packages/cli/src/serve/acp-http/dispatch.ts
Comment thread packages/cli/src/acp-integration/acpAgent.test.ts Outdated
The restore handlers resolved the persisted (possibly uppercase)
spelling of a session id only inside the shared coordinator guard,
while batch delete locks its exclusive guard on the raw caller ids.
A restore of the normalized request id therefore raced a concurrent
batch delete of the persisted-spelled id on case-sensitive volumes.

Resolve the persisted spelling before acquiring the shared guard and
key runSharedMany on the resolved id so both sides contend on the
same key, in both the REST and ACP restore handlers. Regression
tests assert the guard key at both transports.
@doudouOUC
doudouOUC enabled auto-merge August 20, 2026 02:00
…I create

Batch delete/archive/unarchive locked on canonical keys but still
deduped raw spellings, so two case variants of one id deadlocked
the batch. CLI --session-id now stores the lowercase spelling so
new mixed-case transcripts stop accumulating.

Co-authored-by: Cursor <cursoragent@cursor.com>
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Review-comment round (e4e9ef5)

Landed the two Critical items this PR introduced and should own. Everything else stays deferred per the five-round rule.

Thread Action
Batch delete/archive/unarchive deadlocks on two case spellings of one id Fixed — helpers now dedupe with normalizeSessionIdForLookup
CLI --session-id still mints mixed-case transcripts Fixed — stores the canonical lowercase spelling; occupancy errors log at debug
Restore-only resolver / transcript-export 404 / undeletable mixed-case sessions Deferred9488 (storage-boundary resolver + deletion uncoupled from classifier)
Orphaned legacy children unclassifiable and therefore undeletable Deferred — same issue, 9488
Lock-key folding for undashed / v7 ids Deferred9490
Reserved standalone source only gated at two routes Deferred — 9490 / PR2B containment
10-record integrity window Deferred — disclosed O1, PR2B
Case-conflict re-check copy-paste / 409→500 Deferred — same helper as 9488
Dead reserved-source restore check Not taking — unreachable nit
Four private-directory mode gates Not taking — out of PR2A scope

9488 is the first follow-up after merge: mixed-case restore now succeeds, so the sibling-endpoint gap is newly reachable.

CI Test checks out PR HEAD, then runs check-workflow-size.sh added
on main in QwenLM#9517. Without this merge the script is missing and the
job exits 127.

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

Code review is clean: all 50 review threads across rounds R1-R4 are resolved (Critical + Suggestion), no merge conflict. Approving the code.

Heads-up on CI (not code-related):

  1. Live Host (macos-latest) failed at bun install with GET api.github.com/repos/whiskeysockets/eslint-config/tarball/299e838 - 504 (transient network).
  2. Test (ubuntu-latest) failed at the Check workflow file size step with .github/scripts/check-workflow-size.sh: No such file or directory — that script exists on main but not on this branch, so the branch is behind main.

Please sync with main (merge/rebase) so the missing script is picked up and CI can actually run the test suite, then re-run the failed checks.

@wenshao

wenshao commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Re-verification at 62d5c5a1 — 8 new commits since my last report

I re-ran the whole environment against the updated branch: base = new merge-base 3b3818db vs PR head = 62d5c5a1, both rebuilt from source into runnable dist/cli.js bundles, plus the same loop-mounted +casefold ext4 mount for the case-insensitive rows. Where a behaviour changed since my last pass I also measured the previous head 04ac635d so the delta is attributable to this review round rather than to the merge from main.

Everything I reported before still holds at the new head, and the four new behaviours this round introduces all reproduce on real processes.

1. Still true at the new head

The reserved standalone source is refused on REST (400 reserved_session_source) and on ACP (-32602), a caller-supplied id that case-collides with a crash-torn transcript is refused with 409 session_id_conflict, the 17 provenance scenarios still fail closed exactly as before, the case resolver still throws the typed conflicts, and a pre-gate standalone-sourced transcript written by the base daemon is still resumable and listable by the PR daemon. Controls (sourceType: "myapp", the Live reserved source, an unused caller id) are unchanged on both arms.

real daemon A/B

2. Explicit standalone lineage is now proven, not assumed

readLoadableConversationSession() reads the parent and reports the classification it proved. Three shapes that the previous head accepted are now rejected; the self-describing "parent is gone" case is deliberately still accepted.

lineage

3. Batch lifecycle, private directory and CLI ids now collapse case variants

Three new real-process results, all absent from my previous report:

  • POST /sessions/archive/sessions/unarchive/sessions/delete, each addressed by the UPPER-case spelling of a lower-case transcript: base answers notFound three times and leaves the file on disk; the PR archives, unarchives and finally deletes the persisted spelling.
  • materializeConversationDirectory() for both spellings of one id: base creates two private directories under the Conversations root, the PR creates one.
  • qwen --session-id <UPPER> for a brand-new id: base writes <UPPER>.jsonl, the PR writes <lower>.jsonl, so later case variants all resolve to the same transcript.

CLI

ACP, lifecycle and migration

4. Classification and case resolution matrices, re-measured

classification matrix

case resolution

Two rows are new this round and both confirm the occupancy fix from 49dec69a3b: when the requested spelling and a case twin are both present but unreadable the id is reported occupied (unreadable_transcript), and when the requested spelling is unreadable while a twin is readable the resolver returns the readable spelling.

5. Negative controls, re-run and extended

Nine controls now, including two for the new fixes. Every one flips the evidence back.

negative controls

6. Suites, lint, typecheck at 62d5c5a1

Check Result
Touched core suites (3 files) 223 passed / 0 failed
Touched CLI suites + all of src/serve/conversations (17 files) 2,489 passed, 1 skipped / 0 failed
packages/core src/services + jsonl-utils 2,398 passed, 1 failed
packages/cli src/serve (full) 5,291 passed, 6 failed
eslint --max-warnings 0 on all 31 touched TypeScript files clean
tsc --noEmit for packages/core and packages/cli clean

The 7 failures reproduce identically on the new base worktree (re-confirmed this round: 6 in packages/cli/src/serve, 1 in session-writer-lease) — root-user environment artifacts, not regressions.

Findings

Still no blocking defect. Status of my three earlier findings, plus two new notes:

1. (unchanged, still the release-note item) The fail-closed window is the first 10 records. Re-measured at this head: a Live transcript torn at line 5 is not loadable through the internal path, the same tear at line 13 is. Blast radius is unchanged — conversations that crashed within their first ten records.

2. (unchanged) Agent-suffixed ids are never classified, because getSessionLocation() enforces SESSION_FILE_PATTERN. Every REST/ACP restore path still pre-gates on getSessionLocation/assertSessionLoadable, so it stays latent; LiveTaskService.ensureResident() remains the only caller without that pre-gate.

3. (unchanged) A transcript present in both active and archive is terminal — unclassifiable and 409 from the resolver, with no repair affordance.

4. (new, small) The CLI misattributes an unreadable session store to an id collision. ceaf5a9476 makes loadCliConfig treat any resolver failure as "occupied", which is the right default. But the user-facing line is then Error: Session Id <id> already exists (active or archived). Delete or unarchive it first. — advice that cannot help, because the id does not exist. Probe: replace <project>/chats/archive with a regular file, so enumeration fails with ENOTDIR; base starts the session, the PR prints the "already exists" error and exits. Worth noting that the daemon already gets this right — POST /session answers 503 session_id_admission_unavailable ("Unable to verify persisted state…") on both arms. Reusing that wording in the CLI branch would cost one if.

5. (new, informational) An explicit standalone child is accepted whenever its parent cannot be produced — not only when the parent was archived or deleted. Measured: a parent whose transcript is torn, and a parent sitting in an active+archive conflict, both read as "parent gone" and the child is classified standalone/explicit. That follows from the self-describing rule and is arguably correct, but the code comment and the PR description both frame the exemption as "archived away or deleted"; a torn or ambiguous parent is a different situation and may deserve a word in the comment.

One coverage gap worth stating plainly: the new inodeVerifiable fallback (FAT/exFAT and SMB mounts where stat().ino === 0) could not be exercised end-to-end here. No filesystem available on this Linux host reports inode 0 — the kernel's FAT drivers synthesise inode numbers, and there is no FAT tooling installed — so I verified hasVerifiableInode() as a pure function (0 and 0n → false, real inodes → true) and read the call sites. That degraded path rests on the PR's unit tests alone.

Cost check, re-measured with 5,001 sibling transcripts: findSessionIdIgnoringCase() came out at 2.8 ms on the PR and 4.4 ms on base this run, against 4.4/2.9 the other way round last time — the two arms are within noise of each other.

Verdict

The review round did real work: three lineage shapes that were silently accepted are now rejected, the private directory and the batch lifecycle both collapse case variants, and the resolver's two duplicated arms are now one. Everything reproduces on real daemons, a real CLI and a real case-insensitive filesystem, every fix is discriminated by a negative control, and the 7,699 surrounding tests show no regression. Still recommending merge, with finding 1 in the release notes and findings 4 and 5 as optional polish.

中文说明

62d5c5a1 上的重新验证 —— 距上次报告新增 8 个提交

我把整套环境针对更新后的分支重跑了一遍:base = 新的 merge-base 3b3818db 对比 PR head = 62d5c5a1,两臂都从源码重新构建出可运行的 dist/cli.js,大小写不敏感的用例仍跑在 loop 挂载的 +casefold ext4 上。凡是与上次相比发生变化的行为,我额外测量了上一个 head 04ac635d,以便把差异归因到这一轮评审,而不是归因到从 main 的 merge。

上次报告的结论在新 head 上全部成立,本轮新增的四项行为也都在真实进程上复现。

1. 新 head 上依然成立

保留的 standalone source 在 REST(400 reserved_session_source)与 ACP(-32602)上都被拒绝;与崩溃截断 transcript 发生大小写碰撞的 caller-supplied id 仍被 409 session_id_conflict 拒绝;17 个 provenance 场景仍然按原样 fail closed;大小写解析器仍抛出带类型的冲突;由 base daemon 写下的、闸门之前的 standalone source transcript,仍可被 PR daemon resume 并出现在列表中。对照组(sourceType: "myapp"、Live 保留 source、未占用的 caller id)在两臂上均无变化。(见上方第 1 张截图)

2. explicit standalone 的 lineage 现在需要被证明,而不是被假定

readLoadableConversationSession() 会读取 parent 并报告它所证明的分类。上一个 head 接受的三种形态现在被拒绝;而"parent 已消失"这一自描述场景仍被有意接受。(见 lineage 截图)

3. 批量生命周期、私有目录与 CLI id 现在都会折叠大小写变体

三项新的真实进程结果,上次报告中均未涉及:

  • 大写拼写依次调用 POST /sessions/archive/sessions/unarchive/sessions/delete(磁盘上是小写 transcript):base 三次都返回 notFound 且文件原封不动;PR 依次归档、取消归档,并最终删除了持久化拼写对应的文件。
  • 对同一个 id 的两种拼写调用 materializeConversationDirectory():base 在 Conversations 根下创建两个私有目录,PR 只创建一个
  • 对全新 id 执行 qwen --session-id <大写>:base 写出 <大写>.jsonl,PR 写出 <小写>.jsonl,后续任何大小写变体都会解析到同一个 transcript。

(见 CLI 与 ACP/生命周期/迁移两张截图)

4. 分类与大小写解析矩阵,已重新测量

本轮新增两行,都印证了 49dec69a3b 的 occupancy 修复:当请求拼写大小写孪生文件同时存在且都不可读时,该 id 被判为已占用(unreadable_transcript);当请求拼写不可读而孪生可读时,解析器返回可读的那个拼写。(见分类与大小写解析两张截图)

5. 反向对照,已重跑并扩充

现在共 9 组,其中两组针对本轮新修复。每一组都能让证据翻转回去。(见反向对照截图)

6. 62d5c5a1 上的测试、lint、typecheck

检查项 结果
触及的 core 套件(3 个文件) 223 通过 / 0 失败
触及的 CLI 套件 + 整个 src/serve/conversations(17 个文件) 2,489 通过、1 跳过 / 0 失败
packages/coresrc/services + jsonl-utils 2,398 通过,1 失败
packages/clisrc/serve(全量) 5,291 通过,6 失败
对 31 个触及的 TypeScript 文件执行 eslint --max-warnings 0 干净
packages/corepackages/clitsc --noEmit 干净

这 7 个失败在新的 base worktree 上完全一致地复现(本轮已重新确认:packages/cli/src/serve 6 个,session-writer-lease 1 个)—— 属于 root 用户环境造成的,并非回归。

结论性发现

仍无阻塞性缺陷。此前三点发现的状态,外加两点新说明:

1.(不变,仍建议写进 release note)fail-closed 的窗口是前 10 条记录。 在新 head 上重测:第 5 行被截断的 Live transcript 在内部路径下不可加载,同样的截断发生在第 13 行则可加载。影响面不变 —— 在前十条记录内崩溃的会话。

2.(不变)agent 后缀 id 永远不会被分类,因为 getSessionLocation() 强制 SESSION_FILE_PATTERN。所有 REST/ACP restore 路径仍先经过 getSessionLocation/assertSessionLoadable,因此这仍是潜在问题;LiveTaskService.ensureResident() 仍是唯一没有该前置检查的调用点。

3.(不变)同时存在于 activearchive 的 transcript 是终局状态 —— 无法分类,解析器只回 409,且没有修复入口。

4.(新增,轻微)CLI 把"session store 不可读"误报成"id 已被占用"。 ceaf5a9476loadCliConfig 把解析器的任何失败都当作"已占用",这个默认是对的。但用户看到的那一行是 Error: Session Id <id> already exists (active or archived). Delete or unarchive it first. —— 这条建议无法生效,因为该 id 根本不存在。探针:把 <project>/chats/archive 换成普通文件,使枚举以 ENOTDIR 失败;base 正常启动会话,PR 打印"already exists"并退出。值得一提的是 daemon 侧已经处理得很好 —— POST /session两臂上都返回 503 session_id_admission_unavailable("Unable to verify persisted state…")。在 CLI 分支复用这套措辞只需要一个 if

5.(新增,说明性)只要 parent 无法被产出,explicit standalone child 就会被接受 —— 不限于 parent 被归档或删除的情形。实测:parent 的 transcript 被截断,以及 parent 处于 active+archive 冲突态,两种情况都被读成"parent 已消失",child 仍被分类为 standalone/explicit。这是自描述规则的自然结果,也说得通;但代码注释与 PR 描述都把这个豁免表述为"archived away or deleted",而"被截断"或"处于歧义态"的 parent 是另一回事,注释里或许值得补一句。

另有一处覆盖缺口需要如实说明:新增的 inodeVerifiable 兜底路径(FAT/exFAT 与部分 SMB 挂载,stat().ino === 0在这里无法端到端复现。本机没有任何文件系统会报告 inode 0 —— 内核的 FAT 驱动会合成 inode 号,而且机器上没有 FAT 工具链 —— 因此我只验证了 hasVerifiableInode() 作为纯函数的行为(00n → false,真实 inode → true)并通读了调用点。该降级路径目前仅由本 PR 自带的单测覆盖。

开销复测(单 project 下 5,001 个同级 transcript):本轮 findSessionIdIgnoringCase() 在 PR 上 2.8 ms、base 上 4.4 ms,而上一轮是 4.4/2.9 反过来 —— 两臂在噪声范围内没有差别。

结论

这一轮评审确实解决了实际问题:三种此前被静默接受的 lineage 形态现在被拒绝;私有目录与批量生命周期都会折叠大小写变体;解析器原本重复的两条分支合并为一条。所有行为都在真实 daemon、真实 CLI 与真实大小写不敏感文件系统上复现,每项修复都有反向对照加以判别,周边 7,699 条测试没有回归。仍然建议合并,把第 1 条发现写入 release note,第 4、5 条作为可选打磨。


Verified locally with Claude Code (model: Claude Opus 5, 1M context).

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 20, 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: 2823 passed · 0 failed · 2823 total

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

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

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

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

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

Verification report

PR 9341 Deep Verification (round 8) — feat(cli): Add standalone conversation isolation primitives

Verdict: merge-ready — assertion totals 2823 pass / 0 fail (2823). Verified head OID 62d5c5a (HEAD^2, matches the snapshot's headRefOid — no drift this round); base 3b3818d (HEAD^1, matches the snapshot's baseRefOid). Follow-up round: round 7 (head ceaf5a9476, base 39fc769d3a) was merge-ready with carried suggestions F3/F4 and a nit. Since then the PR added 3 substantive commits + a merge of origin/main (f716cc6e keep the directory identity module out of the core barrel; 49dec69a correct three defects from the previous review round; e4e9ef52 collapse case-variant ids in batch lifecycle + CLI create; 62d5c5a1 merge main for the workflow-size script), and the base moved to 3b3818d. The merge is a clean auto-merge — git diff --cc HEAD is empty. Every carried measurement was rebuilt and re-run at the new head/base (fresh container; harnesses reconstructed from the round-7 cell specifications). All three delta fixes verified behaviorally: D1 inode-less root identity (fix 1), D2 per-candidate occupancy escape (fix 2), D3 workspace canonical directory keying (fix 3), D4 serve fast-path bundle closure, D5 batch case-variant collapse. Round-7 findings F3/F4/nit re-measured and carry unchanged; one new suggestion-level coverage gap (F7): the D3 canonical-keying guard survives the delivered suite (probe-killed). No blocking findings.

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

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

  • 结论merge-ready(2823 通过 / 0 失败)。第 8 轮跟进:自第 7 轮起新增 3 个实质 commit + 一次 origin/main 合并f716cc6e 将目录 identity 模块移出 core barrel;49dec69a 修复上一评审轮的三个缺陷;e4e9ef52 batch 生命周期与 CLI create 中折叠大小写变体 id;62d5c5a1 合并 main 以引入 workflow-size 脚本);base 前移到 3b3818d--cc 组合 diff 为空(干净自动合并)。三个缺陷修复均经行为验证:D1 无 inode 文件系统的根 identity 降级(fix 1)、D2 按候选占用逃逸(fix 2)、D3 workspace 规范化目录键控(fix 3)、D4 serve fast-path bundle 闭包、D5 batch 大小写折叠。见各 A/B 表与截图。
  • A/B 结论:中心 fail-closed 声明再次证实(harness 01,29/29,5 个损坏单元由 base 放行翻转为 head 拒绝);大小写冲突(02,24/24,含 D2 新单元 J/K);reserved source + lineage(03,19/19);目录 identity(04,26/26,含 D1/D3 单元);coordinator + batch(05,25/25,D5:base 对同一会话双重处理、head 折叠为一次);admission(06,14/14)。变异矩阵 10 个变异体:7 个被套件直接杀死(M8/M9/M11/M13/M14/M16/M17),M15 存活后被探针杀死(新缺口 F7),M3/M6 存活(M3 被探针杀死 → F3;M6 有阳性对照 M8 佐证套件活性 → F4;均为覆盖缺口)。
  • 状态变化:F1/F2/F5/F6 修复保持;F3/F4/nit 维持(均为建议级覆盖缺口,非合入条件);新增 F7(D3 键控未被交付套件钉住,建议级)。
  • 未覆盖:Windows/真实大小写不敏感文件系统、无 inode 文件系统的实体测试(D1 经 lstat 注入的交付测试 + 真实 inode 对照单元验证)、逐 commit 归因(depth 2)等,见 Not covered

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

Previous-finding status (round 7 → new head 62d5c5a1)

# finding (round 7) severity status at new head
F1 Interface migration left test stubs behind blocking fixed (holds). Big gate re-run green at the new head/base merged tree: 2323/2323 across 8 files, two agreeing rounds (logs/gate-cli-big.txt, gate-cli-big-r2.txt).
F2 macOS-only inode assumption in replacement tests blocking fixed (holds). Harness 04's replacement cell rename-pins the child directory and measures a real inode change on Linux (ino 7866215 -> 7866218) → unexpected_identity against the expected identity, then restores and revalidates clean (04-identity-probe.png).
F3 Self-lineage conjunct unpinned (mutant M3) suggestion stands (re-measured). M3 (conjunct → false) again survives the delivered session-source.test.ts 35/35; the tsx probe kills it: a case-variant self-parent is REJECTED at head but classifies {standalone/explicit} under M3 (parent read misses on a case-sensitive FS, self-describing branch admits). Coverage gap; code correct.
F4 Cleanup-guard normalization unpinned (mutant M6) suggestion stands (re-measured). M6 (three cleanup-guard reads at acpAgent.ts:5184/5594/5762 → raw config.getSessionId()) survives the full acpAgent.test.ts 441/441 (logs/m6.txt). Positive control M8 (load-path adoption removed) killed on exactly expected '550e8400-…' to be '550E8400-…' with the resume variant green (logs/m8.txt) — the suite is live on the spelling axis; the survival is specific to the cleanup-guard axis. Coverage gap, not a production defect.
F5 Entries-after-inspect ordering unpinned suggestion fixed (holds). M9 (entries read before the final identity re-inspection) killed by exactly rejects as not_empty when an entry appears during the final identity re-inspection — 1 failed | 14 passed (logs/m9.txt).
F6 Third case-spelling race on the coordinator suggestion fixed (holds). Harness 05 cells A–D: base proceeds / head throws SessionArchivingError on every case-variant race; M11 (all three canonicalization sites reverted) killed by the case-fold test — 1 failed | 46 passed (logs/m11.txt).
nit strict-reader spies delegate to the tolerant stub optional stands. Re-read at the new head: three sites — server.test.ts:32985, :33225, :33355 — all .mockImplementation(async (…) => readMetadata(…)/readCreationMetadata(…)), so route-level tests still cannot distinguish a regression swapping readCreationMetadataIfReadable back for the tolerant method. The strict/tolerant split remains pinned by harness 01's tolerant-vs-strict cell + the core gate.

Central claim + A/B

Central claim (unchanged): provenance classification fails closed when a transcript's creation metadata is truncated/malformed/missing, while clean legacy transcripts and fully-recoverable }{-glued records stay loadable; tolerant readers elsewhere are unchanged.

Mock-free A/B (harness/01-failclosed-ab.mjs, 29/29, witness 01-failclosed-ab-base-vs-head.png): real SessionService from each arm's own dist build over real transcript files under a per-arm hermetic runtimeBaseDir. Base arm drives the pre-PR tolerant callback wiring; head arm passes the service as the strict store. Build fingerprints asserted in-harness on both arms (base dist lacks readCreationMetadataIfReadable / SessionIdCaseConflictError / readLoadableConversationSession; head has all three).

cell base head verdict
clean-legacy (no metadata) loadable loadable (standalone/legacy) preserved
explicit-live (default + realtime_voice:bridge-1) loadable loadable (live/explicit) preserved
truncated creation record loadable (promoted) rejected flipped — fixed
garbage bytes loadable (promoted) rejected flipped — fixed
scalar line (42) loadable (promoted) rejected flipped — fixed
array line ([1,2,3]) loadable (promoted) rejected flipped — fixed
valid head + damage within window loadable (promoted) rejected flipped — fixed
}{-glued complete records loadable loadable (standalone/legacy) preserved (recovery kept)
damage past the 10-record scan window loadable loadable preserved (bounded scan)
tolerant-vs-strict on same damaged file tolerant {}, location active tolerant {}, strict undefined, location active tolerant reader unchanged

Secondary claims (all re-confirmed at new head/base)

  • Case lookup (02-case-conflict-ab.mjs, 24/24, witness 02-case-conflict-ab.png): unique mixed-case resolves the persisted spelling on both arms; case-only duplicate pair → base silently picks, head throws SessionIdCaseConflictError with no candidate; same spelling readable in both states → conflict location on both arms, base resolves silently / head throws naming the spelling; R5-1 (present-but-unreadable different-spelling twin): base FREE (the hole) / head unreadable_transcript naming the twin; R5-2 (valid session + unreadable same-spelling state): resolves on both arms; crashed-first-run 0-byte own file FREE on both arms; foreign-project head under a different spelling flips free→occupied; hardlinked two-owner pair → base picks / head refuses; non-UUID names invisible on both arms. Delta D2 cells: own 0-byte + readable twin resolves the twin on both arms (per-candidate escape does not over-block, cell J); own 0-byte + unreadable twin — the exact 49dec69a fix-2 shape — base FREE / head OCCUPIES (cell K).
  • Reserved source + lineage (03-reserved-source-ab.mjs, 19/19, witness 03-reserved-source-ab.png): forged source handed back verbatim by tolerant reads on both arms while head classification rejects it; isReservedStandaloneSessionSource is sourceType-only by design (the sourceId === undefined conjunct lives at the call sites — proven by the forged cell); explicit standalone top-level and child classify at head only, with proven parentSource; legacy child of legacy parent reports parentSource {standalone/legacy}; self-describing child with a gone parent still classifies; exact-self, case-variant-self (F3 axis), and cyclic lineage rejected; child of a damaged parent flips base-promoted → head-rejected; invalid parent id rejected.
  • Directory identity (04-identity-probe.mjs, 26/26, head-only, witness 04-identity-probe.png): root 0700 with dev+inode+inodeVerifiable:true on ext4; equivalent path accepted; foreign path unexpected_identity; rename-pinned child replacement against the captured identity rejected with a measured inode change, restore revalidates clean; symlinked child not_directory; 0755 wrong_mode then clean after chmod; unknown id missing; materialize idempotent, name conversation-<sha256(id)>; distinct spellings hash to distinct keys; prepare empty-ok / not_empty; ensure recreatedready; rename-pinned foreign replacement vs expected → unexpected_identity.
  • Coordinator + batch (05-coordinator-ab.mjs, 25/25, witness 05-coordinator-ab.png): cells A–D (case-variant races) proceed on base / throw SessionArchivingError at head; E same-spelling contention throws on both arms (validity control); F/G agent-suffixed and non-UUID ids NOT case-folded on either arm (by design); I/J head-only collapse integrity (one canonical lock key for two spellings, drained after). Delta D5 cells (e4e9ef5): batch delete/archive/unarchive of a [lower, UPPER] pair — base processes 2 results per session (double-processing the one transcript), head processes 1 canonical operation with zero errors and the correct end state each time.
  • Create admission (06-admission-ab.mjs, 14/14, witness 06-admission-ab.png): absent id granted on both arms; persisted exact spelling and mixed-case caller vs lowercase transcript rejected on both arms; R5-1 twin: base ADMITS (mints the case-only twin) → head rejects session_id_conflict/persisted; readable duplicate pair rejected on both arms; EACCES on the archived chats dir and ELOOP on the worktree sidecar both fail closed as retryable session_id_admission_unavailable on both arms (the ceaf5a9 guard holds).

Delta verification (the 3 commits + merge since round 7)

Depth-2 checkout: only the aggregate HEAD^1..HEAD diff is locally reachable; the delta was scoped from the metadata's commit bodies + the aggregate diff, and each theme verified behaviorally:

  • 49dec69a fix 1 (D1) — the inode guard made a directory fail to equal itself. The three comparison sites now degrade on inode-less filesystems: createConversationRootIdentity records inodeVerifiable: false instead of throwing, hasRootIdentity falls back to device, and hasExpectedDirectoryIdentity requires inode match only when both sides are provable (code read). Behavior: harness 04 D1 cells drive the real comparators with the identity objects an inode-less filesystem produces — an inode-less root revalidates via the fallback; a replaced root under inode-less identity still establishes (the documented weaker guarantee — "cannot prove unchanged" is not "changed"); the same replacement IS detected (identity_changed) when inodes exist. Pinned: mutant M17 (fallback removed) killed by the delivered degrades instead of failing on a filesystem that reports no inode test (1 failed | 9 passed), which simulates ino: 0 at the lstat seam. The creation-side half on a real inode-less filesystem is Not covered (no exFAT/SMB mount in this container).
  • 49dec69a fix 2 (D2) — the occupancy escape was per-arm instead of per-candidate. Head resolver skips only the requested spelling's own file in the occupancy loop. Harness 02 cells J/K prove both directions (readable twin still resolves; unreadable twin still occupies). Pinned: mutant M14 (per-arm early return restored) killed by findSessionIdIgnoringCase > lets an unreadable case twin keep occupying the idpromise resolved "undefined" instead of rejecting (1 failed | 170 passed). The commit body also notes the resolver's two arms were unified — verified by inspection: one findSessionIdIgnoringCase implementation exists at head (the duplication is why the escape had landed in only one copy).
  • 49dec69a fix 3 (D3) — private directory canonicalized inside the workspace. ConversationWorkspace.directoryKey() normalizes before hashing; harness 04 D3 cells: mixed-case and lowercase ids materialize the same directory named from the lowercase id, and discard via mixed case removes it. Not pinned by the delivered suite: mutant M15 (canonicalization removed) survives conversation-workspace.test.ts 15/15; the tsx probe kills it (same directory → two directories, conversation-355ca9… vs conversation-691dab…). New finding F7 below.
  • f716cc6e (D4) — keep the identity module out of the core barrel. Verified three ways: the cli module imports only node:* (no @qwen-code import — code read); core's packages/core/src/index.ts exports no hasVerifiableInode / file-identity (grep); and the repo's own closure gate passes — DEV=true npm run bundle + node scripts/check-serve-fast-path-bundle.js"Startup bundle closure checks passed" (exit 0, logs/fast-path-check.txt). The predicate is restated locally with a comment recording why it is not imported.
  • e4e9ef52 (D5) — batch collapse + CLI lowercase create. Batch delete/archive/unarchive dedupe through normalizeSessionIdForLookup (all three sites): harness 05 D5 cells flip base double-processing → head single canonical operation; mutant M16 (raw Set at the three batch sites, coordinator untouched) killed by exactly the three delivered collapses case-variant spellings in one batch to a single … tests (3 failed | 44 passed). CLI --session-id lowercasing: config.ts:2098 normalizes through normalizeSessionIdForLookup; pinned by config.test.ts:2015 (getSessionId() === lowercase), green in both big-gate rounds. Base-arm note: the measured base manifestation is double-processing (the racing second entry landed notFound/alreadyArchived in this run rather than erroring); the commit body's "deadlock" framing is the same root cause under different timing — either way head collapses the pair to one operation.
  • 62d5c5a1 — merge of origin/main to bring check-workflow-size.sh into the CI test checkout (CI plumbing). Clean auto-merge (git diff --cc HEAD empty); all measurements ran on the merged tree.

Findings

No blocking findings this round. In severity order (all carried/new suggestions are coverage-gap reports, not merge conditions):

F7 (new, suggestion) — the D3 canonical directory keying is unpinned

Mutant M15 (directoryKey returns the raw caller spelling) survives the delivered conversation-workspace.test.ts 15/15; the tsx probe kills it (mixed-case and lowercase ids materialize two distinct directories instead of one). The guard is load-bearing: LiveTaskService.ensureResident() passes ids originating in tool arguments, and without the canonicalization one session resolves to two private directories (the defect 49dec69a fix 3 describes). Classification: coverage gap on a fix-3 guard; the code itself is correct (harness 04 D3 cells green at head). A fixture that materializes a mixed-case id through ConversationWorkspace and asserts the directory name derives from the lowercase spelling would kill M15. Not shipped here (verifier does not author PR tests). Not a merge condition.

F3 (carried from round 6, suggestion) — self-lineage conjunct's decisive axis unpinned

Re-measured: M3 survives the delivered suite 35/35; the probe kills it (case-variant self-parent flips REJECTED → {standalone, explicit}). Unchanged classification: coverage gap; both probes green at head. The round-6 suggested fixture (one session-source case whose parentSessionId is a case variant of the session id) would kill M3.

F4 (carried from round 3, suggestion) — cleanup-guard normalization still unpinned

Re-measured: M6 survives 441/441; M8 positive control killed with clean attribution (load variant red on expected '550e8400-…' to be '550E8400-…', resume variant green). Unchanged classification: coverage gap; the guard is load-bearing on the live-restore failure path with non-canonical persisted spellings.

nit (carried from round 3, optional) — strict-reader spies delegate to the tolerant stub

Stands; three sites at the new head (server.test.ts:32985/33225/33355). Route-level tests cannot distinguish a regression swapping readCreationMetadataIfReadable back for the tolerant readCreationMetadata; the split remains pinned by harness 01's tolerant-vs-strict cell and the core gate.

Mutation matrix (round 8)

mutant guard removed / reverted pinning suite result
M3 self-lineage conjunct → false session-source (35) survived — then probe-killed: case-variant self-parent flips REJECTED → standalone/explicit → F3 carries
M6 three cleanup-guard reads → raw config.getSessionId() acpAgent (full, 441) survived 441/441 — coverage gap F4
M8 load-path persisted-spelling adoption removed acpAgent (filtered) killedexpected '550e8400-…' to be '550E8400-…', resume variant green; positive control for M6
M9 prepareStandaloneDirectory entries-before-inspect conversation-workspace (15) killed — 1 red: rejects as not_empty when an entry appears during the final identity re-inspection → F5 fix holds
M11 coordinator canonicalization at all three sites → raw keys session-archive (47) killed — 1 red: case-fold race test resolves 'exclusive' instead of rejecting → F6 fix holds
M13 admission sidecar loop swallows non-ENOENT session-id-admission (14) killed — 1 red: returns retryable unavailable for a non-ENOENT sidecar errorceaf5a9 guard holds
M14 resolver per-candidate escape → per-arm early return sessionService (171) killed — 1 red: lets an unreadable case twin keep occupying the id49dec69 fix 2 pinned
M15 directoryKey canonicalization removed (D3) conversation-workspace (15) survived — then probe-killed: same directory → two distinct directories → new gap F7
M16 batch dedupe at three functions → raw Set session-archive (47) killed — 3 red: collapses case-variant spellings in one batch × delete/archive/unarchive → e4e9ef5 pinned
M17 inode-less identity fallback removed (D1) conversation-directory-identity (10) killed — 1 red: degrades instead of failing on a filesystem that reports no inode49dec69 fix 1 pinned

Every mutant was applied by exact-match replacement with occurrence-count assertions and restored with git checkout --; git status --porcelain empty after each restore. Mutation runs are suite-liveness evidence and are not counted in assertions.json (same convention as rounds 1–7); probe head-arm assertions (2) are counted, mutant arms are not. Witness 07-mutation-matrix.png. Not re-run this round: round-6/7 mutants M1/M2/M2b/M4/M5/M5b/M10a–c/M12/M12b — their guards are unchanged by the 3 delta commits, and their pinning suites all ran green in this round's gates.

Targeted gates (head, as delivered)

gate round 1 round 2
core: jsonl-utils + sessionService + corruption 223/223 pass 223/223 pass
cli small: session-source (35), session-id-admission (14), error-response, conversation-directory-identity (10), conversation-workspace (15), dispatch-error, session-archive (47) 137/137 pass across 7 files 137/137 pass
cli big: server, acp-http transport, acpAgent (441), acpAgent.worktree, live-task-service, config, multi-workspace-sessions, conversation-runtime-manager (touched by the diff, added to the gate this round) 2323/2323 pass across 8 files 2323/2323 pass

Witness 08-gates-two-rounds.png. Gate liveness: the mutation matrix turned these same suites red on their intended assertions (M9/M11/M13/M14/M16/M17/M8). Counts grew vs round 7 (core 222→223, small 133→137, big 2297→2323) from the delta commits' new tests. No repo-wide gate was claimed or run.

Not covered

  • Per-commit attribution: checkout is depth 2; git rev-list HEAD^1..HEAD^2 returns 1 while the metadata lists 30 commits — the shallow-boundary gap. The 3 delta commits were verified as themes against the aggregate diff + their commit bodies, not individually.
  • Base-move enumeration: intermediate base commits between round-7's base (39fc769d3a) and HEAD^1 (3b3818d) are not locally reachable; the overlap was measured on the merge result instead (git diff --cc HEAD empty = clean auto-merge). All measurements ran on the merged tree.
  • Real inode-less filesystem (D1 creation half): no exFAT/FAT/inode-less SMB mount in this container. The comparison fallbacks were driven through the real comparators with the identity objects an inode-less FS produces (harness 04 D1 cells); the creation path under ino: 0 is pinned by the delivered lstat-seam test (M17 killed it). A real mount would add end-to-end confirmation; not available here.
  • Windows / a real case-insensitive filesystem: alias collapse verified via hardlink (two backed owners → conflict, not a silent pick); the single-owner collapse path (owners.length === 1) and the isSameConversationPath win32 branch are unreachable on this ext4 container by construction.
  • True multi-process daemon E2E: restore/admission/batch verified at dist/component level plus the PR's own in-process server/transport suites (real loopback HTTP); no live daemon over a real socket was booted.
  • Real-race interleavings: harness 05's lock cells use deterministic interposition (one side holds the lock set, the other attempts). They reproduce the shape of each race, not a contended-writer trigger; the base-arm batch double-processing manifested as a raced second entry (notFound/alreadyArchived) rather than the commit body's deadlock framing.
  • F3/F4/F7 pinning fixtures not shipped (verifier does not author PR tests; all three fixtures described in Findings).
  • Timing ladder: not run — no new scanner over free-form untrusted text this round; the delta adds no regexes over outsider input.
  • Performance/concurrency load: not run.
  • Lint/format/typecheck repo-wide: CI builds head green per the environment contract; no separate lint run.
  • Docs-only changes (standalone-pr2-core.md plan, architecture doc): reviewed for claim context, not behaviorally tested.
  • Mutation coverage beyond the matrix: other guards (e.g. getSessionLocation's pattern pre-filter, identity owner checks under a different uid, retryPendingConfigCleanup normalization at acpAgent.ts:12881) were not mutated; most are pinned by delivered tests that ran green in the gates.
  • PR text: no verifier-directed instructions found in the PR title/body/commit messages this round (none to report).

Methodology

Environment: CI merge-ref checkout (HEAD = merge commit 35974f89, base tip 3b3818d = HEAD^1, PR head 62d5c5a1 = HEAD^2), node:22-bookworm-class container, Node v22.23.2, uid 1000 (non-root — chmod-based EACCES fixtures are effective); npm ci + npm run build pre-run at HEAD. The PR leaves package.json/package-lock.json untouched — measured: the two lockfiles are byte-identical (0 package diffs between HEAD^1 and HEAD), making the shared root node_modules a clean control. A/B base side built in git worktree tmp/base-tree at HEAD^1 (rebuild only packages/core + packages/cli; log logs/base-build.log, exit 0). The base worktree got its own node_modules: every root entry re-linked except the @qwen-code scope, which was relinked into the base tree's packages, plus the per-package nested node_modules version-conflict nests (ajv/fdir/ignore/mime under packages/core; missing them reproduced wrong-version type errors on the first attempt — fixed and rebuilt). Realpath assertions before trusting the control: readlink -f tmp/base-tree/node_modules/@qwen-code/qwen-code-core…/tmp/base-tree/packages/core (and qwen-code → base packages/cli). Harnesses 01–06 import each arm's compiled dist directly and drive real files through the SessionService(cwd, { runtimeBaseDir }) seam into per-arm scratch storage; M3/M15 probes import src via tsx so single-file source mutations take effect without a rebuild; every other mutant ran through the suites' normal source compilation. Mutants: applied by exact-match text replacement (occurrence counts asserted), run, restored with git checkout --; git status --porcelain verified empty after every restore. D4 verified with the repo's own scripts/check-serve-fast-path-bundle.js against a DEV=true npm run bundle metafile. Raw logs in logs/ (h01h06, m3m17, gate-*, base-build.log, bundle.log, fast-path-check.txt); harness sources in harness/ are rerunnable. Evidence captures produced with scripts/verify-capture.mjs (8 images in evidence/). Assertion counting: harness checks 137 + probe head-arm assertions 2 + fast-path bundle check 1 + core gate 223 + cli small gate 137 + cli big gate 2323 = 2823, all scripted, all executed; fail counts only unexpected outcomes — none. Gate flakiness: two full rounds of all three gate groups agreed (36 green file-runs).

Flakiness gate log

rounds=5 files=18 skipped=0
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/acp-integration/acpAgent.worktree.test.ts: (cd packages/cli) npx --no-install vitest run ./src/acp-integration/acpAgent.worktree.test.ts
file packages/cli/src/config/config.test.ts: (cd packages/cli) npx --no-install vitest run ./src/config/config.test.ts
file packages/cli/src/serve/acp-http/dispatch-error.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/acp-http/dispatch-error.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-runtime-manager.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/conversations/conversation-runtime-manager.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/live/live-task-service.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/live/live-task-service.test.ts
file packages/cli/src/serve/multi-workspace-sessions.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/multi-workspace-sessions.test.ts
file packages/cli/src/serve/server.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/server.test.ts
file packages/cli/src/serve/server/error-response.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/server/error-response.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/serve/session-id-admission.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/session-id-admission.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/services/sessionService.corruption.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/sessionService.corruption.test.ts
file packages/core/src/services/sessionService.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/sessionService.test.ts
file packages/core/src/utils/jsonl-utils.test.ts: (cd packages/core) npx --no-install vitest run ./src/utils/jsonl-utils.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/cli/src/acp-integration/acpAgent.test.ts: PPP
  packages/cli/src/acp-integration/acpAgent.worktree.test.ts: PPP
  packages/cli/src/config/config.test.ts: PPP
  packages/cli/src/serve/acp-http/dispatch-error.test.ts: PPP
  packages/cli/src/serve/acp-http/transport.test.ts: PPP
  packages/cli/src/serve/conversations/conversation-runtime-manager.test.ts: PPP
  packages/cli/src/serve/conversations/conversation-workspace.test.ts: PPP
  packages/cli/src/serve/conversations/session-source.test.ts: PPP
  packages/cli/src/serve/live/live-task-service.test.ts: PPP
  packages/cli/src/serve/multi-workspace-sessions.test.ts: PPP
  packages/cli/src/serve/server.test.ts: PPP
  packages/cli/src/serve/server/error-response.test.ts: PPP
  packages/cli/src/serve/server/session-archive.test.ts: PPP
  packages/cli/src/serve/session-id-admission.test.ts: PPP
  packages/cli/src/utils/conversation-directory-identity.test.ts: PPP
  packages/core/src/services/sessionService.corruption.test.ts: PP
  packages/core/src/services/sessionService.test.ts: PP
  packages/core/src/utils/jsonl-utils.test.ts: PP

verdict: timeout
summary: only 2 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/acp-integration/acpAgent.worktree.test.ts: P (exit 0)
round 1 · packages/cli/src/config/config.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/acp-http/dispatch-error.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-runtime-manager.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/live/live-task-service.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/multi-workspace-sessions.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/server.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/server/error-response.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/server/session-archive.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/session-id-admission.test.ts: P (exit 0)
round 1 · packages/cli/src/utils/conversation-directory-identity.test.ts: P (exit 0)
round 1 · packages/core/src/services/sessionService.corruption.test.ts: P (exit 0)
round 1 · packages/core/src/services/sessionService.test.ts: P (exit 0)
round 1 · packages/core/src/utils/jsonl-utils.test.ts: P (exit 0)
round 2 · packages/cli/src/acp-integration/acpAgent.test.ts: P (exit 0)
round 2 · packages/cli/src/acp-integration/acpAgent.worktree.test.ts: P (exit 0)
round 2 · packages/cli/src/config/config.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/acp-http/dispatch-error.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-runtime-manager.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/live/live-task-service.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/multi-workspace-sessions.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/server.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/server/error-response.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/server/session-archive.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/session-id-admission.test.ts: P (exit 0)
round 2 · packages/cli/src/utils/conversation-directory-identity.test.ts: P (exit 0)
round 2 · packages/core/src/services/sessionService.corruption.test.ts: P (exit 0)
round 2 · packages/core/src/services/sessionService.test.ts: P (exit 0)
round 2 · packages/core/src/utils/jsonl-utils.test.ts: P (exit 0)
round 3 · packages/cli/src/acp-integration/acpAgent.test.ts: P (exit 0)
round 3 · packages/cli/src/acp-integration/acpAgent.worktree.test.ts: P (exit 0)
round 3 · packages/cli/src/config/config.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/acp-http/dispatch-error.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-runtime-manager.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/live/live-task-service.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/multi-workspace-sessions.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/server.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/server/error-response.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/server/session-archive.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/session-id-admission.test.ts: P (exit 0)
round 3 · packages/cli/src/utils/conversation-directory-identity.test.ts: P (exit 0)

Evidence images

01-failclosed-ab-base-vs-head

02-case-conflict-ab

03-reserved-source-ab

04-identity-probe

05-coordinator-ab

06-admission-ab

07-mutation-matrix

08-gates-two-rounds

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 62d5c5a193ea0180350564ea66e3f5dd6ef2b3e5 — 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.

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

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

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

@doudouOUC
doudouOUC dismissed stale reviews from qwen-code-ci-bot and wenshao August 20, 2026 06:15

Already have 2 approves,3ks.

@doudouOUC
doudouOUC added this pull request to the merge queue Aug 20, 2026
Merged via the queue into QwenLM:main with commit a659539 Aug 20, 2026
61 of 63 checks passed
@doudouOUC
doudouOUC deleted the feat/standalone-pr2a-primitives branch August 20, 2026 06:17
@chiga0

chiga0 commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Review 补充说明(对应 review #4979624548)

No blocking findings.

Checked:

  • Classification logic: classifyTopLevelConversationSource — explicit standalone, live, and legacy paths are mutually exclusive; all three guards are tested.
  • Fail-closed transcript reader: readCreationMetadataIfReadable with requireCompleteLines=true — incomplete head (truncated/torn) returns undefined while the tolerant reader (readCreationMetadata) still recovers; verified by corruption tests.
  • Case-conflict resolution: findSessionIdIgnoringCase handles alias-collapsing on case-insensitive filesystems via resolveAliasedReadableCandidate, conservative on genuine conflicts (SessionIdCaseConflictError).
  • Reserved source admission: isReservedStandaloneSessionSource gate fires at both REST and ACP session/new entry points; persistedSessionExists treats case-conflict as occupied.
  • Private-directory identity: lstatrealpath→re-lstat anti-swap; before/after inode pinning; root revalidation at entry and exit of inspectConversationDirectoryIdentity.
  • isSameConversationPath's win32-only case-fold is identical to the pre-existing isSamePath in the base — behaviour unchanged.
  • Lock semantics in ACP restore: coordinator canonicalizes keys; locking on sessionId (request spelling) is correct per inline comment.
  • Cross-checked against prior reviews: CI bot R1–R25 (at 97362119) and yiliang114 round-2 "no blocking findings" (at 3b8869d6) — no disagreement.

Unreviewed dimensions (verdict capped, not a clean result):

  • Test (macos-latest, Node 22.x) — SKIPPED. PR adds resolveAliasedReadableCandidate for alias-collapsing on case-insensitive APFS; macOS CI did not run.
  • Test (windows-latest, Node 22.x) — SKIPPED.
  • Integration Tests (CLI, No Sandbox) — SKIPPED (noted in prior CI-bot review; no change at this head).
  • Execution rungs 1–3 not run (no local toolchain); mutation probe and A/B build not performed.

Reviewed with AI assistance.

wenshao added a commit to wenshao/qwen-code that referenced this pull request Aug 20, 2026
Conflict: the SDK browser-bundle budget — main bumped 198→199KB for
persistent session attachments while this branch needed headroom for the
session PR binding types; resolved at 200KB with both reasons recorded.

Also fixes a latent type error currently on main: QwenLM#9477 added
deleteSessionAttachments to the deleteDaemonSessions bridge contract, but
the batch-delete test from QwenLM#9341 still passes a closeSession-only mock;
CI did not catch it because the verify check is skipped on main pushes.
chiga0 pushed a commit to chiga0/qwen-code that referenced this pull request Aug 20, 2026
…nLM#9551)

`deleteDaemonSessions` takes `bridge: Pick<AcpSessionBridge,
'closeSession' | 'deleteSessionAttachments'>` since QwenLM#9477, which
updated every mock that existed when it was written. QwenLM#9341 landed in
parallel and added one more — "collapses case-variant spellings in one
batch to a single delete" — with a `{ closeSession }` bridge.

Each PR was green on its own merge ref; main is red combined, so
`npm ci` fails the build for every branch cut from it:

    src/serve/server/session-archive.test.ts(1069,7): error TS2741:
    Property 'deleteSessionAttachments' is missing in type
    '{ closeSession: Mock<Procedure> }'

Adds the same `vi.fn().mockResolvedValue(undefined)` its neighbours
already pass. The test asserts on the delete result, not on the spy, so
its meaning is unchanged: 48/48 still pass.
pull Bot pushed a commit to bhardwajRahul/qwen-code that referenced this pull request Aug 20, 2026
* test(ci): stage on-disk session state in the serve A/B

The serve A/B drives every scenario against a freshly started, empty daemon,
so the entire session-admission surface — case resolution, transcript
integrity, active/archive conflicts, reserved sources — is unreachable and a
PR that rewrites it diffs as "no response changes". QwenLM#9341 is the worked
example: the posted A/B reported no change across 4 scenarios while the same
build pair, driven with transcripts on disk, answers differently on six
requests.

Scenarios can now stage transcripts before their request and capture a reduced
projection of the response, and the HTTP status is recorded on every capture so
a status-only difference is visible. Six session-admission scenarios use that:
a healthy restore, the legacy uppercase spelling, case-only twins, an
unreadable transcript, one id in both the active and the archive directory, and
creation carrying a source type.

The staged fixtures depend on the on-disk project layout, which the harness
mirrors rather than imports. If that mirror ever drifts the transcripts land
nowhere and every staged scenario would quietly answer 404 on both arms, so the
healthy restore doubles as a canary that fails the drive instead of publishing
a reassuring all-clear.

* test(ci): address the R1 review round on the serve A/B harness

Clears the capture directory before a drive writes into it, so a re-run can
never let an earlier run's files stand in for scenarios this run did not
capture, and writes a completion marker once every scenario is captured. A
baseline without that marker is now reported as partial, because a base drive
that stopped part-way leaves the scenarios it never reached rendering as "this
PR adds these responses" — the same shape a genuinely new scenario produces.

Both arms are driven by the head checkout's harness, so a capture pair always
carries the status field on both sides and the compatibility shim for a base
that predates it was unreachable; it and its tests are removed rather than left
to teach a transition the wiring cannot produce. Non-object response bodies are
now nested instead of spread, which dropped scalars and re-keyed arrays.

The source-type scenario probed a type today's daemon does not reserve, so it
never reached the refusal branch it was named for. It is split: one scenario
pins the source the daemon actually reserves, the other keeps an ordinary type
that a future reservation would move from admitted to refused. A second canary
covers the archive directory, which nothing certified before — a drifted
archive name would have left the conflict scenario loading from the active copy
on both arms and diffing clean. The remaining inert request body key is gone
too; the client id is read from a header, never the body.

The harness tests were passing under mutations they appeared to cover: the
staging routing, the projection guards and the fixed-id requirement are now
pinned by assertions that fail when those are inverted.

* test(ci): close the R2 gaps in the serve A/B harness

The completion marker was declared twice, once by the writer and once by the
reader, with nothing pinning the copies together: renaming one side left both
suites green while CI would either flag every complete baseline as truncated or
stop noticing truncated ones. The drive now owns the constant and the diff
imports it.

Two invariants the code asserted in comments were not enforced. A response body
carrying its own status key overwrote the status the harness saw, so a
status-only regression on such a route would have diffed as an unchanged body;
the harness value now wins. And the canary check — the harness's only drift
alarm — had no test at all: inverting it so it could never fire left every test
passing. It is now a named helper with tests on both branches.

The archive canary pinned an exact status, which conflates its precondition
with the product's decision: if an archived-only load ever becomes loadable,
the precondition still held, but the drive would abort and suppress the very
row the captures already contained. It now fails only on the one answer that
means the staged file was never seen.

Finally, nothing pinned that a staged scenario probes an id it actually staged.
Staging the wrong id answers 404 on both arms, captures identically, and drops
that branch out of coverage with every test green.

* test(ci): close the R3 gaps in the serve A/B harness

The completion marker proved that some drive finished, never that this run's
did. The only reset lived inside the drive script, which does not run when an
arm is skipped before it starts — no merge-base resolved, the base checkout
failing, or its build dying — and on the persistent pool the capture paths
outlive a run. An inherited baseline then arrived complete, marker included, so
neither degraded-baseline warning fired and the comment would have diffed this
head against another run's base. The workflow now clears both capture paths in
an unconditional step, which is the only place that covers a skipped arm.

The in-script reset also turned a write-only script into an unguarded recursive
delete of a path taken straight off the command line, which the documented
local usage invites a reader to mistype. It now refuses any directory that
holds something other than captures.

The healthy canary's premise was wrong: the product validates transcripts
record by record and fails open, so a fixture whose records stop validating
restores as an empty session and still answers 200. Measured against a real
daemon, a wholly drifted fixture passed the canary and left every staged
scenario probing an empty daemon — the false all-clear this harness exists to
prevent. The canary now keeps a replay-size witness in its capture and fails
when it is zero.

Three test gaps behind the same theme: the marker's writer, the comment
subcommand that CI actually invokes, and the mixed-case scenario's existence
were all unpinned, and the staged-id check asserted against the union of every
scenario's staging rather than the one under test. The capture loop is
extracted so its ordering is testable without a daemon.

* test(ci): cover the setup-failure abort in the serve A/B capture loop

The capture loop was extracted so its ordering could be pinned without a
daemon, and three of its four abort branches were covered — but not the one
that fires when a scenario's setup request fails. Dropping that throw left the
whole suite green while a capture would be recorded against a daemon where the
setup never took effect, which is the masked diff the branch exists to prevent.

* test(ci): close the R5 gaps in the serve A/B harness

* fix(ci): send an admitted source in the serve A/B unreserved-source witness

---------

Co-authored-by: wenshao <nigolaschao777@gmail.com>
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.15.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants