Skip to content

fix(core): Preserve ownership during session cleanup - #10286

Closed
doudouOUC wants to merge 1 commit into
QwenLM:mainfrom
doudouOUC:fix/session-lifecycle-cleanup-followup
Closed

fix(core): Preserve ownership during session cleanup#10286
doudouOUC wants to merge 1 commit into
QwenLM:mainfrom
doudouOUC:fix/session-lifecycle-cleanup-followup

Conversation

@doudouOUC

Copy link
Copy Markdown
Collaborator

What this PR does

This PR separates the pre-mutation runtime-generation fence from post-transcript cleanup ownership. After delete, archive, unarchive, or conflict repair commits the primary transcript mutation, companion cleanup now continues only while the exact daemon writer lease is still owned. The ownership check rejects missing, replaced, malformed, non-regular, or symlinked lock entries and protects worktree, pull-request, prompt-ledger, file-history, and organization cleanup without falling back to another workspace runtime.

It also clarifies that conflict repair remains limited to regular transcript copies that the selected workspace may maintain; foreign or ambiguous copies continue to fail closed without mutation.

Why it's needed

A workspace runtime can close immediately after the transcript is deleted or moved. Reusing that runtime-generation fence for subsequent companion cleanup causes a committed lifecycle operation to stop halfway, leaving stale sidecars and metadata behind. Removing the fence entirely would create the opposite risk: a replacement writer could create same-session artifacts that the old operation then deletes. The writer-lease cleanup fence preserves both properties by allowing committed cleanup to finish while refusing to touch artifacts once ownership is lost.

Reviewer Test Plan

How to verify

Run the focused Core lifecycle and writer-lease tests and the CLI daemon lifecycle tests. Confirm that delete, archive, unarchive, and both conflict-repair directions finish companion cleanup after the runtime generation closes, while missing or atomically replaced writer locks preserve replacement-generation artifacts and return a writer-ownership error. Confirm that selected-runtime routes remain scoped to their trusted workspace and that PR refresh/backfill operations stay serialized with lifecycle mutations.

Local verification completed: Core focused tests passed with 363 passed and 3 skipped; CLI lifecycle tests passed 65/65; selected-runtime and Live organization boundary tests passed; PR refresh/backfill coordinator tests passed; the real filesystem/daemon harness passed twice plus one independent verification run; build, typecheck, lint, Prettier, and diff checks passed.

Evidence (Before & After)

N/A — lifecycle persistence and documentation changes with no user-interface change.

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

macOS local workspace, Node.js v22.22.3, npm 10.9.8, sandbox disabled for the filesystem harness.

Risk & Scope

  • Main risk or tradeoff: Cleanup after the primary transcript mutation now depends on exact writer-lock ownership instead of the runtime-generation fence; the implementation validates both the opened descriptor and current path identity before each destructive companion operation.
  • Not validated / out of scope: Windows and Linux were not exercised locally. Scheduled-task updates, attachment cleanup, and final route responses intentionally remain runtime-generation scoped. This PR does not broaden mixed-case lookup, transcript merging, list merging, or parent-child cascading deletion.
  • Breaking changes / migration notes: None.

Linked Issues

Follow-up to #9626.

中文说明

本 PR 的改动

本 PR 将主变更前的运行时 generation 栅栏与 transcript 变更后的清理所有权分离。删除、归档、取消归档或冲突修复提交主 transcript 变更后,只有在 daemon 仍持有完全相同的 writer lease 时才继续清理伴随数据。所有权检查会拒绝缺失、被替换、格式损坏、非普通文件或符号链接形式的锁,并在不回退到其他 workspace runtime 的前提下保护 worktree、PR、prompt ledger、file history 和 organization 清理。

同时补充说明:冲突修复仍然只适用于所选 workspace 可维护的普通 transcript 副本;foreign 或所有权不明确的副本继续以不修改数据的方式 fail closed。

为什么需要

workspace runtime 可能在 transcript 被删除或移动后立即关闭。如果后续伴随清理继续复用 runtime generation 栅栏,已经提交的生命周期操作会中途停止,遗留过期 sidecar 和元数据。如果完全移除栅栏,则存在相反风险:替代 writer 可能创建同 session 的新数据,而旧操作随后将其删除。writer lease cleanup 栅栏同时保证两点:仍持有所有权时完成已提交的清理,所有权丢失后拒绝触碰新数据。

Reviewer 测试计划

如何验证

运行 Core lifecycle、writer lease 聚焦测试和 CLI daemon lifecycle 测试。确认 delete、archive、unarchive 以及两个方向的冲突修复在 runtime generation 关闭后仍能完成伴随清理;同时确认 writer lock 缺失或被原子替换时会保留替代 generation 的数据,并返回 writer ownership 错误。确认 selected-runtime 路由仍限制在其可信 workspace 内,PR refresh/backfill 操作仍与生命周期变更串行化。

本地验证已完成:Core 聚焦测试 363 passed、3 skipped;CLI lifecycle 测试 65/65;selected-runtime 和 Live organization 边界测试通过;PR refresh/backfill coordinator 测试通过;真实 filesystem/daemon harness 主流程运行两次并由测试工程师独立复跑一次,全部通过;build、typecheck、lint、Prettier 和 diff 检查均通过。

证据(修改前与修改后)

N/A——这是 lifecycle persistence 和文档改动,没有用户界面变化。

测试平台

OS 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

macOS 本地 workspace,Node.js v22.22.3,npm 10.9.8;filesystem harness 在 sandbox 关闭的环境中运行。

风险与范围

  • 主要风险或权衡:主 transcript 变更后的清理现在依赖精确 writer lock 所有权,而不是 runtime generation 栅栏;实现在每次破坏性伴随操作前同时校验已打开 descriptor 与当前路径身份。
  • 未验证或不在范围内:本地未运行 Windows 和 Linux。scheduled-task 更新、attachment 清理和最终 route 响应有意继续使用 runtime generation scope。本 PR 不扩展 mixed-case lookup、transcript merging、list merging 或 parent-child cascading deletion。
  • 破坏性变更或迁移说明:无。

关联 Issue

#9626 的 follow-up。

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

qwen-code-ci-bot commented Aug 27, 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 27, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the follow-up!

Template looks good ✓

Problem: real, not theoretical. This is the deferred class from the #9626 review — rounds 10–11 recorded findings (several probe-verified) that a generation trip between the transcript unlink/rename and the companion cleanup (worktree / PR / prompt-ledger / file-history / organization) aborts an already-committed lifecycle operation halfway and strands sidecars. The runtime generation is exactly what closes when the transcript is deleted or moved, so this window is reachable in production; the mechanism shipped in v0.22.2.

Direction: aligned. Core daemon lifecycle reliability, paying down review debt from a merged PR. The change is additive — new optional fence parameters, no public-contract change — and the protocol-doc clarification was itself a deferred item from #9626.

Size: core paths touched (packages/core/src/services). 156 production lines (sessionService.ts 84, session-writer-lease.ts 57, session-archive.ts 15), 275 test lines, 4 docs lines — well under the 500/1000 advisory thresholds.

Approach: right shape. Keeping the generation fence in front of the primary transcript mutation and switching post-mutation cleanup to a byte-exact writer-lease ownership check delivers both properties the review asked for: committed cleanup finishes after the runtime closes, and a replacement writer's same-session artifacts are never deleted by stale cleanup. Making the check synchronous and fd/inode-bound (O_NOFOLLOW / O_NONBLOCK, fstat vs lstat identity) closes the event-loop interleaving window between fence and destructive syscall. Nothing in the diff beyond the stated goal.

Risk: no high-risk path signals. No elevated risk indicators.

Moving on to code review. 🔍

中文说明

感谢这个 follow-up!

模板完整 ✓

问题:真实存在,不是理论问题。这是 #9626 评审中遗留的一类问题:第 10–11 轮记录了(多项经探针验证的)发现——在 transcript unlink/rename 与伴随清理(worktree / PR / prompt-ledger / file-history / organization)之间发生 generation 跳变,会使已提交的生命周期操作中途停止、遗留孤儿 sidecar。而 runtime generation 恰恰会在 transcript 被删除或移动时关闭,因此该窗口在生产环境可达;相关机制已随 v0.22.2 发布。

方向:对齐。核心 daemon 生命周期可靠性问题,偿还在已合并 PR 上的评审债务。改动是增量式的——新增可选围栏参数、无公共契约变更——协议文档的澄清本身也是 #9626 的遗留项。

规模:触及核心路径(packages/core/src/services)。生产逻辑 156 行(sessionService.ts 84、session-writer-lease.ts 57、session-archive.ts 15),测试 275 行,文档 4 行——远低于 500/1000 的建议阈值。

方案:形态正确。主 transcript 变更前保留 generation 围栏,变更后将清理切换为按字节精确校验的 writer-lease 所有权,同时满足评审要求的两个属性:已提交的清理在 runtime 关闭后仍能完成;替代 writer 的同 session 数据不会被旧清理误删。校验做成同步且绑定 fd/inode(O_NOFOLLOW / O_NONBLOCK、fstat 与 lstat 身份比对),关闭了围栏与破坏性系统调用之间的事件循环交错窗口。diff 中没有超出目标的内容。

风险:无高风险路径信号,无升级风险信号。

进入代码审查 🔍

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

Re-run on the same head — I re-read the diff and re-verified the load-bearing claims rather than re-posting the earlier pass on faith. My independent take before reading the diff: once the transcript unlink/rename has committed, the generation fence is the wrong gate for the remaining cleanup, because the runtime generation closes precisely when the transcript is deleted or moved — but dropping the gate entirely would let stale cleanup delete a replacement writer's same-session artifacts. So: keep the generation fence for the primary mutation, then switch to a writer-lease ownership check for cleanup, fail closed when ownership is lost. That is exactly what this PR does, and I did not find a simpler path it missed.

What I verified against the code:

  • No unfenced cleanup path. All three daemon surfaces (REST routes in session.ts and ACP-over-HTTP dispatch) reach the service through deleteDaemonSessions / archiveDaemonSessions / unarchiveDaemonSessions, and every one of those goes through runWithDaemonWriterLease, which now supplies both fences. I audited every caller of removeSession / archiveSessions / unarchiveSessions in the tree: no production caller passes assertCanMutate to the service lifecycle methods without the lease wrapper providing assertCleanupOwned alongside — in removeSessionFiles the only remaining assertCanMutate call sits strictly before the transcript unlink, and all four movePrSidecar call sites now hand it the ownership fence. TUI and other non-daemon callers pass no fences at all, and every new option is optional — their behavior is unchanged.
  • Ownership loss is never swallowed. Every best-effort sidecar/ledger/org catch now re-throws SessionWriterError before falling back to re-fence-and-warn, so a lost lease surfaces as a per-session error instead of the silent-abort class that fix(serve): Repair persisted session lifecycle #9626's R3-3 finding described. Genuine I/O failures stay warn-only, re-fenced first.
  • The new check is sound. assertCleanupOwned is fully synchronous, so there is no event-loop interleaving between the fence and the destructive syscall that follows it. It opens the lock with O_NOFOLLOW | O_NONBLOCK, compares the opened descriptor's dev/ino against the path's lstat, and requires a byte-exact active record — missing, replaced, symlinked, non-regular, malformed, or sealed locks all fail closed with SessionWriterLostError, matching the FIFO/symlink conventions established in fix(serve): Repair persisted session lifecycle #9626. Not reusing the async readOwnedLock is deliberate: that helper is promise-based (an await would reopen the interleaving window) and lacks the fd/inode binding.
  • Tests pin the mechanism, both directions, all five surfaces. "Generation closes → committed cleanup finishes" and "ownership lost → cleanup stops before touching the next artifact" are covered for delete, archive, unarchive, and both conflict-repair directions, plus lease-level tests for replaced and symlinked locks. The flipped corruption-suite expectation (ledger move now finishes after a generation close) correctly replaces the old generation-fence contract rather than weakening it — the ownership-loss twin test carries the new guarantee, and the new tests assert assertCanMutate is called exactly once (pre-mutation only).
  • Scope is exactly the stated goal. The doc change is the conflict-repair clarification deferred from fix(serve): Repair persisted session lifecycle #9626's round 10; nothing else is carried along.

One observation, non-blocking: a transient SessionWriterUnavailableError while re-reading the lock now surfaces as a per-session error even after the primary mutation succeeded, where the old generation fence would have stayed open and the cleanup would have completed with a warning. That is the fail-closed side of the stated contract ("cleanup continues only while ownership is verifiable"), and runWithDaemonWriterLease's mutation-applied-after-error handling covers the resulting shape — but a maintainer should know a delete can report an error while the transcript is already gone.

sequenceDiagram
    participant P1 as Daemon route
    participant P2 as runWithDaemonWriterLease
    participant P3 as SessionWriterLease
    participant P4 as SessionService
    participant P5 as Companion storage
    P1->>P2: delete, archive, or unarchive
    P2->>P3: acquire writer lease
    P2->>P4: mutate with two fences
    Note over P4: generation fence guards the primary mutation
    P4->>P5: unlink or rename transcript
    Note over P4: runtime generation may now close, cleanup switches to ownership
    P4->>P3: assertCleanupOwned before each cleanup step
    alt still owned
        P4->>P5: remove or move sidecars, ledgers, org data
    else ownership lost
        Note over P4: stop fail closed, spare replacement-generation artifacts
    end
Loading

Testing evidence — the PR's own CI (fetched once via API, not re-run)

Unattended run — PR code is never executed here. At this re-run's fetch time no check had failed: every completed check is green, including precheck-pr / precheck, Security Checks, SDK Java (all matrix entries), Dependency CVE audit, Secret scan (TruffleHog), the Desktop Shell builds, and Serve A/B (ubuntu-latest, Node 22.x), which landed green at 12:47 UTC. One load-bearing check is still in flight: Test (ubuntu-latest, Node 22.x) — the unit suite that pins the fence wiring. The skipped macOS/Windows unit-matrix and integration entries match the CI shape of recent PRs in this area (#9626 ran the same skips). The table region below is maintained by the finalize workflow once CI settles.

CI results for f4fee5b — this table auto-updates as CI workflows complete:

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

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

Not verified: the real-daemon race behavior itself. The unit suite pins the fence wiring through mocked fences; the author's real filesystem/daemon harness results are a self-reported claim, and the author tested on macOS only (Windows/Linux not exercised locally). Sandboxed verification is now running — triggered from this thread in parallel with this re-run — and its report will post to the verification comment here; the claim it settles is that committed companion cleanup finishes after the runtime generation closes, and stops fail-closed when the writer lock is replaced — race semantics no static read of the diff can prove.

中文说明

代码审查(同一 head 的 re-run:重新通读 diff 并逐项复核关键结论,而非直接复用上一轮):在读 diff 之前我的独立方案是——主变更(transcript unlink/rename)提交之后,generation 围栏已不再适合作为后续清理的门槛,因为 runtime generation 恰恰在 transcript 被删除或移动时关闭;但完全去掉围栏又会让旧清理误删替代 writer 的同 session 数据。因此:主变更保留 generation 围栏,清理切换为 writer-lease 所有权校验、所有权丢失时 fail closed。这正是本 PR 的做法,我没有找到更简单的路径。

逐项核对:所有三个 daemon 面(REST 路由与 ACP-over-HTTP)都经由 runWithDaemonWriterLease 到达 service,该包装现在同时提供两个围栏。我审计了 removeSession / archiveSessions / unarchiveSessions 在代码树中的全部调用方——不存在只传 assertCanMutate 的无围栏清理路径:removeSessionFiles 中唯一保留的 assertCanMutate 调用严格位于 transcript unlink 之前,movePrSidecar 的四处调用点全部改传所有权围栏;TUI 等非 daemon 调用方不传围栏,所有新选项均可选,行为不变。所有 best-effort catch 都先重抛 SessionWriterError 再回退到"重围栏+告警",所有权丢失不会被吞成静默中止(#9626 R3-3 一类的问题保持封堵),真实 I/O 错误仍仅告警。新的 assertCleanupOwned 完全同步,围栏与破坏性系统调用之间没有事件循环交错窗口;以 O_NOFOLLOW | O_NONBLOCK 打开锁、比对已打开描述符与路径 lstat 的 dev/ino、要求字节级一致的 active 记录——缺失、被替换、符号链接、非普通文件、损坏或 sealed 的锁全部以 SessionWriterLostError fail closed,延续 #9626 确立的 FIFO/符号链接约定。不复用异步 readOwnedLock 是有意的:它基于 Promise(await 会重新打开交错窗口)且没有 fd/inode 绑定。测试从两个方向(generation 关闭后已提交清理继续完成;所有权丢失后清理在触碰下一份数据前停止)固定了 delete、archive、unarchive 与两个方向冲突修复共五个面,外加锁被替换/符号链接的 lease 级用例;corruption 套件中翻转的用例正确地以新契约取代旧的 generation 围栏契约,而非削弱断言,新用例同时断言 assertCanMutate 仅被调用一次(只在主变更前)。diff 范围与目标完全一致,文档改动是 #9626 第 10 轮遗留的澄清。

一个非阻塞观察:清理期间重读锁遇到瞬时 SessionWriterUnavailableError 时,现在会在主变更已成功后仍按会话报错(旧的 generation 围栏会保持开放、清理以告警完成)。这是"仅当所有权可验证时才继续清理"这一契约的 fail-closed 面,runWithDaemonWriterLease 的 mutation-applied-after-error 处理覆盖了该形态——但维护者应知道:删除可能在 transcript 已不存在的情况下报错。

测试证据:无人值守运行,此处从不执行 PR 代码。本次 re-run 抓取时无失败检查:所有已完成检查全部通过(含 precheck-pr / precheckSecurity ChecksSDK Java 全部矩阵项、Dependency CVE auditSecret scan (TruffleHog)、Desktop Shell 构建,以及 12:47 UTC 转绿的 Serve A/B (ubuntu-latest, Node 22.x));仍有一项关键检查在途:Test (ubuntu-latest, Node 22.x)(固定围栏接线的单测套件)。跳过的 macOS/Windows 单测矩阵与集成项与该领域近期 PR(如 #9626)的 CI 形态一致。表格区由最终化工作流在 CI 结束后原地更新。

未验证:真实 daemon 竞态行为本身。单测通过 mock 围栏固定接线;作者的真实文件系统/daemon harness 结果属自述,且仅在 macOS 上运行(Windows/Linux 未在本地执行)。沙箱验证已在运行——由本线程触发、与本次 re-run 并行——报告将发布在本帖的验证评论中;它要定论的是"已提交的伴随清理在 runtime generation 关闭后完成、在 writer 锁被替换时 fail-closed 停止"这类静态读 diff 无法证明的竞态语义。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — clean static review with the mechanism pinned by tests in both directions; withholding the fifth until CI lands green and because the daemon-level race behavior rests on the author's self-reported harness (sandboxed verification of exactly that claim is now running).

Re-run reflection, same head: I re-read the diff and re-audited every caller of the three lifecycle methods before re-posting this verdict, so this is a fresh pass, not a repost. My independent proposal — keep the generation fence for the primary transcript mutation, switch post-mutation cleanup to a writer-lease ownership check, fail closed on lost ownership — is exactly what the PR implements, and I never found a simpler path it missed. The problem is not framed into existence: it is the deferred, recorded finding class from #9626's review rounds, living in code that shipped in v0.22.2, and this PR pays down exactly that debt without carrying anything else. The diff is 156 production lines against 275 test lines, every new test pins a mechanism (both directions × all five lifecycle surfaces) rather than an implementation detail, and the two-fence contract is legible enough that maintaining it in six months reads as a thank-the-author change. The one semantic nuance I'd want a maintainer to have in mind — a transient lock-read failure now surfaces as an error after a committed mutation, fail-closed by design — is noted in my review comment, not a blocker.

The remaining gap is honest evidence, not doubt about the code: the unit suite proves the fence wiring through mocks, and the real race semantics (cleanup finishing after the runtime closes; stopping cold on a replaced lock) are backed so far by the author's macOS-only harness claim. The sandboxed /verify run that settles that claim is in flight now and will report in this thread; CI since the last pass has moved too — Serve A/B landed green, leaving the unit suite as the last in-flight check.

Approval remains deferred until CI lands green on f4fee5b21042936a50ee212f0552aab9b296b6d4 — the marker below carries that instruction, and the commit-pinned approval will post only if every check on that head completes green.

中文说明

回顾全局(同一 head 的 re-run:重新通读 diff、重新审计三个生命周期方法的全部调用方后才再次给出结论,这是新一轮审查而非转发):我在读 diff 之前写下了独立方案——主 transcript 变更保留 generation 围栏,变更后的清理切换为 writer-lease 所有权校验、所有权丢失即 fail closed——本 PR 恰好落在这个设计上,我没有找到它遗漏的更简路径。问题不是被框架出来的:它是 #9626 评审轮次中被记录、被遗留的发现类,存在于 v0.22.2 已发布的代码中,本 PR 正是偿还这笔债务,且没有夹带任何其他内容。生产逻辑 156 行对测试 275 行,每个新测试固定的是机制(两个方向 × 五个生命周期面)而非实现细节,双围栏契约足够清晰,六个月后维护它应当是感谢作者而非抱怨作者。唯一希望维护者心中有数的语义细节——锁的瞬时读取失败现在会在主变更已提交后以错误形式上报,这是设计内的 fail-closed——已写在审查评论中,不构成阻塞。

剩余的差距是证据层面的诚实问题,而非对代码的怀疑:单测通过 mock 围栏证明了接线,而真实竞态语义(runtime 关闭后清理完成;锁被替换时立即停止)目前仅有作者自述的、仅在 macOS 上运行的 harness 支撑。用于定论的沙箱 /verify 运行正在进行中,报告将发布在本线程;自上一轮以来 CI 也有进展——Serve A/B 已转绿,仅剩单测套件一项在途。

批准继续推迟到 f4fee5b21042936a50ee212f0552aab9b296b6d4 的 CI 全绿之后——下方标记承载该指令,只有该 head 的所有检查全部通过时才会发布绑定到该提交的批准。

Qwen Code · qwen3.8-max

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

@doudouOUC doudouOUC self-assigned this Aug 27, 2026
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — non-deterministic tests (flakiness gate) - workflow run

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

Scripted assertions: 885 passed · 0 failed · 885 total

Flakiness gate: ❌ 1 of 4 changed test file(s) returned different results across identical re-runs (5 full round(s))

The deterministic flakiness gate re-ran the test files this PR changes and got different outcomes from identical runs (agent verdict: merge-ready). A test that can fail with no code changing lands as intermittent red on unrelated PRs, so this run is reported as not passed regardless of the agent verdict — the per-round matrix is in the flakiness gate log below.

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

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

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

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

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

Verification report

PR #10286 Deep Verification — fix(core): Preserve ownership during session cleanup

Verdict: merge-ready — 885 / 885 scripted assertions passed (assertions.json). Verified head OID: f4fee5b21042936a50ee212f0552aab9b296b6d4 (git rev-parse HEAD^2), against base 10c5b3ece1b970c217c4c2f9fb06efcfd0356623 (HEAD^1). Single-commit PR; depth-2 checkout matches the metadata commits array (1 reachable commit == 1 listed). No blocking findings; two non-blocking observations under Findings.

中文摘要

结论:merge-ready(885/885 断言通过)。

  • A/B 结论(核心主张成立、且是承重的):本 PR 把「主变更前的 runtime-generation 栅栏」与「变更后的清理所有权栅栏」拆开。我用真实 SessionService + 真实 SessionWriterLease + 真实文件系统,对 HEAD 与 base 各建一套构建做 A/B:
    • generation 在提交后关闭 时:HEAD 的 delete / archive / unarchive / 冲突修复都能继续完成伴随清理(40/40);base 在同一场景中途停摆、遗留 sidecar / ledger / file-history(32/32 断言精确命中预期)。这正是 PR 声称修复的行为。
    • 替代 writer 接管锁 时:HEAD 拒绝继续清理、保留替代 generation 的产物并抛 SessionWriterLostError;base 则因 generation 仍开着而误删替代产物(A/B 明确复现该风险)。
  • assertCleanupOwned 边界:11/11 形状分类正确(缺失 / 换主 / 改字节 / 损坏 / 空 / sealed / 目录 / 符号链接 / 只读 → lost;chmod 000 → unavailable;已释放 → lost)。
  • CLI 接线:真实 archiveDaemonSessions / deleteDaemonSessions 端到端跑通(18/18),证明 daemon 路径确实以租约 assertCleanupOwned 驱动清理。
  • 空转/变异矩阵:base 源回退使 14 个新行为测试全部变红(非空转);assertCleanupOwned 置空使 2 个租约测试变红;符号链接防御为「O_NOFOLLOW + lstat 身份」双层,单层移除各自存活(互为冗余)、双层同移才变红(该守卫集合承重)。
  • Findings(非阻塞):① daemon 清理栅栏完全依赖 assertCleanupOwned,但 CLI 测试未钉住该接线:把 session-archive.ts 回退到 base 后 session-archive.test.ts 仍 65/65 全绿(空转探针,见正文),已用 CLI harness 验证行为本身正确;② assertCleanupOwned 为新代码,符号链接防御采用「O_NOFOLLOW + lstat 身份」双层设计,Windows 上依赖第二层(runIf !== win32 测试不覆盖),建议 Windows reviewer 复核(见正文)。
  • 未覆盖:真实多进程守护进程的并发抢锁、跨进程崩溃恢复、性能/大 transcript、Windows/macOS 平台差异(本轮在 Linux node:22 容器)。

Central claim + A/B

Central claim: after the primary transcript mutation commits, companion cleanup must keep going while the exact writer lease is still owned (independent of runtime generation), and must stop without touching replacement-generation artifacts once ownership is lost.

Two builds from the same tree: HEAD (packages/core/dist) and base (worktree at HEAD^1, rebuilt with only packages/core, node_modules symlinked since the PR touches no lockfile). A/B harness 01-ab-core.mjs drives the real built SessionService against real filesystem fixtures; the runtime-generation fence is encoded as a stateful function that passes while the transcript exists and throws WorkspaceGenerationClosedError from the moment it is gone (matching the PR's stated trigger: runtime closes right after the mutation). The replacement-writer cell swaps the lock record to a different owner and rewrites the same-session sidecars at the first post-mutation fence.

Cell Oracle base (HEAD^1) head (HEAD^2)
C1 delete, generation closes after commit return + leftover artifacts rejects at first cleanup fence; worktree/pr/ledger/file-history left behind resolves true; all removed
C2 archive, generation closes after move errors + sidecar location 1 WorkspaceGenerationClosedError; sidecars stay in active errors=[]; sidecars moved to archived
C3 unarchive, generation closes after move mirror of C2 mirror (left in archived) mirror (moved to active)
C4 archive conflict repair, generation closes resolvedConflicts + leftovers 1 error; active sidecars left behind resolved; sidecars cleaned
S1 delete, replacement writer takes lock error identity + replacement artifacts resolves; replacement artifacts destroyed (hazard) SessionWriterLostError; replacement artifacts preserved byte-for-byte

Head arm: 40/40. Base arm: 32/32 (each base cell asserts the predicted broken behaviour, so the base failing to clean up counts as a passed assertion of intent). Witnesses: evidence/01-ab-head-cleanup-completes.png, evidence/02-ab-base-cleanup-stalls.png. Raw per-cell logs: logs/ab-head-results.json, logs/ab-base-results.json.

assertCleanupOwned boundary probe (02-boundary.mjs, real lease, 11/11): witness evidence/03-boundary-matrix.png. CLI end-to-end wiring (03-cli-wiring.mjs, real daemon entry points, 18/18): witness evidence/04-cli-wiring.png.

Targeted gates

Gate Result
Core focused suites (sessionService, sessionService.corruption, session-writer-lease) 364 passed, 2 skipped (366), 3 files green
CLI daemon lifecycle (session-archive, scheduled-task-session-lifecycle) 72 passed (65 + 7), 2 files green
npm run typecheck (repo-wide, all workspaces + integration) exit 0

I ran the gates from a clean, unmutated tree; the mutation runs below are separate scratch edits, never the gate tree.

Vacuity / mutation matrix

All mutants are scratch edits in a throwaway worktree (tmp/mutant-tree); the gate tree was never mutated. Positive control: the unmutated baseline (M0) is green, so a red below is attributable to the mutant, and each red is quoted where it matters.

# Mutant Suite(s) Outcome Read
M0 none (baseline) 3 core suites 364 green runner is live
M1 sessionService.ts reverted to base (no assertCleanupOwned) 2 service suites 14 red every new behavioral test flips; the PR's tests are not vacuous
M2 assertCleanupOwned body → return; lease suite 2 red (toThrow(SessionWriterLostError) not thrown) lease tests pin the check
M3a remove O_NOFOLLOW only symlink test green redundant defence (lstat layer holds)
M3b remove lstat identity block only symlink test green redundant defence (O_NOFOLLOW/ELOOP holds)
M3c remove both O_NOFOLLOW and lstat identity symlink test 1 red ("expected function to throw, but it didn't") the layered guard set is load-bearing
M4 drop SessionWriterError re-throw in archive pr-sidecar cleanup pr-sidecar test 1 red (expected "spy" to be called 2 times, but got 3) ownership-loss propagation is pinned

M1 red sample (behavioral, expected-vs-actual): expected [ { …(2) } ] to deeply equal [] — received an errors entry { error: Error("generation changed"), sessionId }. M3c witness: evidence/06-mutation-m3c-symlink-combo.png. M1/M3 raw vitest logs: logs/mutation-m1-vitest.txt, logs/mutation-m3c-vitest.txt.

Vacuity probe on the CLI wiring (source of Finding 1): reverting only packages/cli/src/serve/server/session-archive.ts to base (so assertCleanupOwned is never threaded into removeSession/archiveSessions/unarchiveSessions) and re-running session-archive.test.ts gives 65/65 green — the existing CLI suite does not observe the cleanup fence at all. That is expected given the fence is a core-level concern, but it means the CLI-level regression is caught only by the core suites + the harness here, not by a CLI test that fails on the wiring. Recorded as Finding 1.

Findings (non-blocking)

  1. No CLI-level test pins the cleanup-ownership wiring (coverage gap, Suggestion). Reverting the CLI plumbing to base leaves all 65 session-archive.test.ts tests green (see vacuity probe above). The behaviour itself is verified correct by the core A/B and the CLI harness in this round, so this is a coverage observation, not a defect: a future regression that drops assertCleanupOwned from runWithDaemonWriterLease would not be caught by the CLI suite. A small CLI test asserting that a replacement-generation lock leaves companion sidecars intact after a committed delete would close it.
    Repro: git show HEAD^1:packages/cli/src/serve/server/session-archive.ts > <scratch>/packages/cli/src/serve/server/session-archive.ts && cd <scratch>/packages/cli && npx vitest run src/serve/server/session-archive.test.ts → 65/65 green on base wiring + head core.
  2. Symlink defence relies on O_NOFOLLOW, with a ?? 0 fallback (informational). assertCleanupOwned opens with O_RDONLY | (O_NOFOLLOW ?? 0) | (O_NONBLOCK ?? 0). On platforms where O_NOFOLLOW is undefined the open falls through to the lstat identity block, which independently rejects symlinks (M3a shows the lstat layer alone still passes the symlink test), so the two layers are genuinely redundant rather than one being dead. The PR's own symlink test is runIf !== win32; the Windows path therefore rests on the lstat/dev-ino layer rather than ELOOP. Calling it out so a Windows reviewer can confirm lstatSync().isSymbolicLink()/dev/ino behave as expected there; M3a on Linux demonstrates the lstat layer alone holds when O_NOFOLLOW is absent.
    Repro: apply M3a (delete the (nodeConstants.O_NOFOLLOW ?? 0) | line in session-writer-lease.ts) and run npx vitest run src/services/session-writer-lease.test.ts -t "rejects a symlinked cleanup lock" → green; apply M3c (also delete the lstat identity if block) → red.

Neither finding changes the verdict: the first is a test-coverage gap (behaviour verified correct this round), the second documents an existing cross-platform design.

Not covered

  • Real multi-process contention / crash recovery for the writer lease (the suite covers it via forked helpers; I did not add a live two-daemon race).
  • Performance / very large transcripts — out of scope for a correctness PR; no size claim made by the PR.
  • Windows and macOS — run on Linux (node:22-bookworm). The symlink runIf guard and O_NOFOLLOW ?? 0 fallback are the platform-sensitive spots (Finding 2).
  • Repo-wide npm run test and bundle (npm run bundle) — only the affected workspaces' focused suites plus typecheck were run, per scope discipline.

Methodology

One Linux node:22 container, PR merge-ref checked out at depth 2 (HEAD = merge, HEAD^1 = base tip, HEAD^2 = PR head). npm ci + npm run build had completed at HEAD before this round. Base arm: git worktree add tmp/base-tree HEAD^1, rebuilt only packages/core with node_modules symlinked from the root (the PR changes no package.json/package-lock.json, so the dependency closure is identical; no internal workspace symlink confound for core). Harnesses 01-ab-core.mjs, 02-boundary.mjs, 03-cli-wiring.mjs import the compiled dist/ directly via file:// URLs and drive real SessionService/SessionWriterLease/daemon entry points against real temp-directory filesystems — no stub of the unit under test; the only injected pieces are the runtime-generation fence function (the scenario trigger) and, in CLI-B, a prototype-level assertCleanupOwned that reports lost ownership (standing in for the already-proven real check on a replaced lock). Each cell's expected outcome is encoded as an assertion, so a control arm failing as predicted counts as a pass and assertions.json fail counts only unexpected outcomes (zero here). Disclosure: the first run of 03-cli-wiring.mjs printed 16/18 because the harness itself never seeded the CLI-A sidecars (fixture bug, not a PR defect); the harness was fixed and re-run (18/18), and only the fixed run is counted. Mutation/vacuity work ran in tmp/mutant-tree via npx vitest run <files> with targeted -t filters; the gate tree stayed pristine. Evidence images were produced with scripts/verify-capture.mjs. Raw logs in logs/.

Flakiness gate log

rounds=5 files=4 skipped=0
file packages/cli/src/serve/server/session-archive.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/server/session-archive.test.ts
file packages/core/src/services/session-writer-lease.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/session-writer-lease.test.ts
file packages/core/src/services/sessionService.corruption.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/sessionService.corruption.test.ts
file packages/core/src/services/sessionService.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/sessionService.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/cli/src/serve/server/session-archive.test.ts: FPPFF
  packages/core/src/services/session-writer-lease.test.ts: FFFFF
  packages/core/src/services/sessionService.corruption.test.ts: FFFFF
  packages/core/src/services/sessionService.test.ts: PPPPP

verdict: flaky
summary: 1 of 4 changed test file(s) returned different results across identical re-runs (5 full round(s))

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/serve/server/session-archive.test.ts: F (exit 1)
--- output tail · round 1 · packages/cli/src/serve/server/session-archive.test.ts ---
rror": SessionTranscriptChangedError {�[39m
�[31m+         "message": "The session transcript changed outside its active writer.",�[39m
�[31m+         "name": "SessionTranscriptChangedError",�[39m
�[31m+         "rpcCode": -32022,�[39m
�[31m+         "errorKind": "session_transcript_changed",�[39m
�[31m+         "httpStatus": 409,�[39m
�[31m+       },�[39m
�[31m+       "sessionId": "550e8400-e29b-41d4-a716-446655440010",�[39m
�[31m+     },�[39m
�[2m    ],�[22m
�[32m-   "archived": [],�[39m
�[32m-   "errors": [],�[39m
�[2m    "notFound": [],�[22m
�[2m    "resolvedConflicts": [],�[22m
�[2m  }�[22m

�[36m �[2m❯�[22m src/serve/server/session-archive.test.ts:�[2m849:20�[22m�[39m
    �[90m847| �[39m    })�[33m;�[39m
    �[90m848| �[39m
    �[90m849| �[39m    �[34mexpect�[39m(result)�[33m.�[39m�[34mtoEqual�[39m({
    �[90m   | �[39m                   �[31m^�[39m
    �[90m850| �[39m      archived�[33m:�[39m []�[33m,�[39m
    �[90m851| �[39m      alreadyArchived�[33m:�[39m [sessionId]�[33m,�[39m

�[31m�[2m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/6]⎯�[22m�[39m

�[41m�[1m FAIL �[22m�[49m src/serve/server/session-archive.test.ts�[2m > �[22marchiveDaemonSessions�[2m > �[22mrepairs an active/archive conflict by keeping the archived copy
�[31m�[1mAssertionError�[22m: expected { archived: [], …(4) } to match object { …(3) }
(2 matching properties omitted from actual)�[39m

�[32m- Expected�[39m
�[31m+ Received�[39m

�[2m  {�[22m
�[32m-   "archived": [�[39m
�[32m-     "550e8400-e29b-41d4-a716-446655440116",�[39m
�[32m-   ],�[39m
�[32m-   "errors": [],�[39m
�[32m-   "resolvedConflicts": [�[39m
�[32m-     "550e8400-e29b-41d4-a716-446655440116",�[39m
�[31m+   "archived": [],�[39m
�[31m+   "errors": [�[39m
�[31m+     {�[39m
�[31m+       "error": SessionTranscriptChangedError {�[39m
�[31m+         "message": "The session transcript changed outside its active writer.",�[39m
�[31m+         "name": "SessionTranscriptChangedError",�[39m
�[31m+         "rpcCode": -32022,�[39m
�[31m+         "errorKind": "session_transcript_changed",�[39m
�[31m+         "httpStatus": 409,�[39m
�[31m+       },�[39m
�[31m+       "sessionId": "550e8400-e29b-41d4-a716-446655440116",�[39m
�[31m+     },�[39m
�[2m    ],�[22m
�[31m+   "resolvedConflicts": [],�[39m
�[2m  }�[22m

�[36m �[2m❯�[22m src/serve/server/session-archive.test.ts:�[2m897:20�[22m�[39m
    �[90m895| �[39m    })�[33m;�[39m
    �[90m896| �[39m
    �[90m897| �[39m    �[34mexpect�[39m(result)�[33m.�[39m�[34mtoMatchObject�[39m({
    �[90m   | �[39m                   �[31m^�[39m
    �[90m898| �[39m      archived�[33m:�[39m [sessionId]�[33m,�[39m
    �[90m899| �[39m      resolvedConflicts�[33m:�[39m [sessionId]�[33m,�[39m

�[31m�[2m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/6]⎯�[22m�[39m

�[41m�[1m FAIL �[22m�[49m src/serve/server/session-archive.test.ts�[2m > �[22munarchiveDaemonSessions�[2m > �[22mrepairs an active/archive conflict by keeping the active copy
�[31m�[1mAssertionError�[22m: expected { unarchived: [], …(4) } to match object { …(3) }
(2 matching properties omitted from actual)�[39m

�[32m- Expected�[39m
�[31m+ Received�[39m

�[2m  {�[22m
�[32m-   "errors": [],�[39m
�[32m-   "resolvedConflicts": [�[39m
�[32m-     "550e8400-e29b-41d4-a716-446655440117",�[39m
�[32m-   ],�[39m
�[32m-   "unarchived": [�[39m
�[32m-     "550e8400-e29b-41d4-a716-446655440117",�[39m
�[31m+   "errors": [�[39m
�[31m+     {�[39m
�[31m+       "error": SessionTranscriptChangedError {�[39m
�[31m+         "message": "The session transcript changed outside its active writer.",�[39m
�[31m+         "name": "SessionTranscriptChangedError",�[39m
�[31m+         "rpcCode": -32022,�[39m
�[31m+         "errorKind": "session_transcript_changed",�[39m
�[31m+         "httpStatus": 409,�[39m
�[31m+       },�[39m
�[31m+       "sessionId": "550e8400-e29b-41d4-a716-446655440117",�[39m
�[31m+     },�[39m
�[2m    ],�[22m
�[31m+   "resolvedConflicts": [],�[39m
�[31m+   "unarchived": [],�[39m
�[2m  }�[22m

�[36m �[2m❯�[22m src/serve/server/session-archive.test.ts:�[2m1150:20�[22m�[39m
    �[90m1148| �[39m    })�[33m;�[39m
    �[90m1149| �[39m
    �[90m1150| �[39m    �[34mexpect�[39m(result)�[33m.�[39m�[34mtoMatchObject�[39m({
    �[90m   | �[39m                   �[31m^�[39m
    �[90m1151| �[39m      unarchived�[33m:�[39m [sessionId]�[33m,�[39m
    �[90m1152| �[39m      resolvedConflicts�[33m:�[39m [sessionId]�[33m,�[39m

�[31m�[2m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[4/6]⎯�[22m�[39m

�[41m�[1m FAIL �[22m�[49m src/serve/server/session-archive.test.ts�[2m > �[22munarchiveDaemonSessions�[2m > �[22mkeeps independent unarchive sessions moving when one classification fails
�[31m�[1mAssertionError�[22m: expected [] to deeply equal [ Array(1) ]�[39m

�[32m- Expected�[39m
�[31m+ Received�[39m

�[32m- [�[39m
�[32m-   "550e8400-e29b-41d4-a716-446655440022",�[39m
�[32m- ]�[39m
�[31m+ []�[39m

�[36m �[2m❯�[22m src/serve/server/session-archive.test.ts:�[2m1231:31�[22m�[39m
    �[90m1229| �[39m    })�[33m;�[39m
    �[90m1230| �[39m
    �[90m1231| �[39m    �[34mexpect�[39m(result�[33m.�[39munarchived)�[33m.�[39m�[34mtoEqual�[39m([availableId])�[33m;�[39m
    �[90m   | �[39m                              �[31m^�[39m
    �[90m1232| �[39m    expect(result.errors).toEqual([{ sessionId: failedId, error: failu…
    �[90m1233| �[39m  })�[33m;�[39m

�[31m�[2m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[5/6]⎯�[22m�[39m

�[41m�[1m FAIL �[22m�[49m src/serve/server/session-archive.test.ts�[2m > �[22mdeleteDaemonSessions�[2m > �[22mdeletes the transcript when killSession throws SessionNotFoundError
�[31m�[1mAssertionError�[22m: promise rejected "SessionTranscriptChangedError: The sessio… { …(3) }" instead of resolving�[39m
�[36m �[2m❯�[22m src/serve/server/session-archive.test.ts:�[2m1772:5�[22m�[39m
    �[90m1770| �[39m        coordinator�[33m:�[39m �[35mnew�[39m �[33mSessionArchiveCoordinator�[39m()�[33m,�[39m
    �[90m1771| �[39m      })�[33m,�[39m
    �[90m1772| �[39m    )�[33m.�[39mresolves�[33m.�[39m�[34mtoBe�[39m(�[35mtrue�[39m)�[33m;�[39m
    �[90m   | �[39m    �[31m^�[39m
    �[90m1773| �[39m    expect(fs.existsSync(sessionPath(workspaceDir, sessionId, 'active'…
    �[90m1774| �[39m      �[35mfalse�[39m�[33m,�[39m

�[31m�[1mCaused by: SessionTranscriptChangedError�[22m: The session transcript changed outside its active writer.�[39m
�[36m �[2m❯�[22m SessionService.readMaintainableSessionIdentity ../core/src/services/sessionService.ts:�[2m1095:15�[22m�[39m
�[90m �[2m❯�[22m SessionService.resolveMaintainableSessionSnapshot ../core/src/services/sessionService.ts:�[2m984:26�[22m�[39m
�[90m �[2m❯�[22m SessionService.getMaintainableSessionLocation ../core/src/services/sessionService.ts:�[2m959:15�[22m�[39m
�[90m �[2m❯�[22m deletePersistedSessionWithLease src/serve/server/session-archive.ts:�[2m413:27�[22m�[39m
�[90m �[2m❯�[22m SessionArchiveCoordinator.runExclusiveMany src/serve/server/session-archive.ts:�[2m109:14�[22m�[39m
�[90m �[2m❯�[22m deleteDaemonSessionIfOrphan src/serve/server/session-archive.ts:�[2m635:18�[22m�[39m
�[90m �[2m❯�[22m src/serve/server/session-archive.test.ts:�[2m1760:5�[22m�[39m

�[31m�[2m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯�[22m�[39m
�[31m�[1mSerialized Error:�[22m�[39m �[90m{ rpcCode: -32022, errorKind: 'session_transcript_changed', httpStatus: 409 }�[39m
�[31m�[2m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[6/6]⎯�[22m�[39m


�[2m Test Files �[22m �[1m�[31m1 failed�[39m�[22m�[90m (1)�[39m
�[2m      Tests �[22m �[1m�[31m6 failed�[39m�[22m�[2m | �[22m�[1m�[32m59 passed�[39m�[22m�[90m (65)�[39m
�[2m   Start at �[22m 12:53:01
�[2m   Duration �[22m 14.96s�[2m (transform 4.02s, setup 95ms, collect 5.91s, tests 340ms, environment 345ms, prepare 112ms)�[22m

JUNIT report written to /__w/qwen-code/qwen-code/packages/cli/junit.xml

round 1 · packages/core/src/services/session-writer-lease.test.ts: F (exit 1)
--- output tail · round 1 · packages/core/src/services/session-writer-lease.test.ts ---
ionWriterLease�[2m > �[22mseals a transcript proof and permits only certified takeover�[32m 20�[2mms�[22m�[39m
�[31m   �[31m�[31m SessionWriterLease�[2m > �[22mwaits for an accepted append before sealing the transcript�[39m�[32m 56�[2mms�[22m�[39m
�[31m     → The session transcript changed outside its active writer.�[39m
   �[32m✓�[39m SessionWriterLease�[2m > �[22mreconciles a sealing error reported after the sealed primary is installed�[32m 8�[2mms�[22m�[39m
   �[32m✓�[39m Ses

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

Evidence images

01-ab-head-cleanup-completes

02-ab-base-cleanup-stalls

03-boundary-matrix

04-cli-wiring

05-mutation-m1-base-source

06-mutation-m3c-symlink-combo

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

🩺 serve daemon A/B

Built the PR base vs this PR head f4fee5b, 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 12 scenario(s).

Qwen Code · serve A/B

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Superseded by #10300, which rebases the required writer-lease cleanup ownership fix onto the lifecycle implementation merged in #10179. The replacement keeps the narrowed scope, adds byte-identical lock-replacement protection, and carries the updated verification evidence. Closing this PR to avoid duplicate review and CI.

@doudouOUC doudouOUC closed this Aug 27, 2026
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.

2 participants