Skip to content

feat(core): Durable Managed Session journal and failover - #12693

Merged
doudouOUC merged 8 commits into
QwenLM:mainfrom
doudouOUC:cursor/durable-session-failover-3df3
Sep 25, 2026
Merged

doudouOUC merged 8 commits into
QwenLM:mainfrom
doudouOUC:cursor/durable-session-failover-3df3

Conversation

@doudouOUC

@doudouOUC doudouOUC commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

What this PR does

This is the TypeScript durable Managed Session foundation split from #12358. It adds a semantic session authority, local JSONL and HTTP inline storage adapters, checkpoints, prompt/activation journals, logical Harness handles, and transcript projection. It also includes the writer-lease and record-schema dependencies needed to build this slice independently.

The review fixes preserve complete event-content integrity, include checkpoint resource dependencies in remote commits, serialize competing checkpoint updates and handoff, invalidate an old safety boundary when a new Agent starts, and prevent lease/inbox races from corrupting replay. Local maintenance rejects legacy resume, recording, rename, and fork operations on Managed sessions; sealed-session archive/delete retain the writer fence, deletion removes private resources, and listings display only committed metadata with verified bodies. Legacy listings skip redundant Managed metadata probes while recognizing the execution-engine record that precedes a Managed header. In-session /resume rejects a Managed target before switching the active session in both Ink and OpenTUI, and daemon restore reports the incompatibility as HTTP 409.

Why it's needed

Issue #12380 stage G needs a session authority that can be persisted and restored independently of the hosted tool loop and WebShell. This PR establishes those TypeScript contracts and component behaviors; the Java store, migrations, authenticated Hosted routing, Broker recovery, and production failover proofs are separate integration work.

Reviewer Test Plan

How to verify

  • Create, commit, seal, and reopen a local Managed session. The replacement should verify the schema-3 commit proof, preserve history, and reject an older writer or altered event body.
  • Admit approval and Runtime waits concurrently. Only compatible work should succeed, and every accepted execution identity must remain recoverable. A new Agent cannot hand off using the previous run's turn-complete checkpoint.
  • Commit a checkpoint containing a newly published history reference, then restore through fresh HTTP adapters. Both checkpoint and history should be readable; unrelated staged resources should be absent.
  • Exercise same-millisecond renewal, cancellation before readiness, and renewal completing during seal. Reopening must remain possible, and no renewal timer may fire after sealing.
  • Remove a metadata commit marker or change a referenced body. Session listings must not show the uncommitted or damaged title/source, and must show both for a committed Managed session whose first record is an execution-engine marker. Startup and in-session legacy resume in both Ink and OpenTUI, title recording, rename, and fork must reject Managed sessions without modifying the log; ACP/daemon restore should report a typed error and HTTP 409. Sealed-session archive/unarchive and deletion must retain the writer fence. Legacy listing performance should avoid redundant Managed header reads.

Evidence (Before & After)

Before review fixes, the split failed the repository build and 41 Managed component tests. Deterministic reproductions also confirmed lost checkpoint dependencies, competing checkpoint overwrite, stale safety-boundary handoff, unreplayable queue events, and a renewal/seal timer exception.

After fixes: repository build, typecheck, bundle, PR-file ESLint/Prettier, and bundle version/help smoke passed locally. Across 24 focused Core test files, the combined verified result is 1,051 passed and 10 platform skips. The broad run had 1,050 passes and one Darwin subprocess timeout while a full build ran concurrently; after the build ended, the entire lease file passed independently (106 passed, 10 skipped), including that test. No test timeout was changed. The first review-feedback batch also passed focused Core metadata/lease/recording tests, CLI configuration and archive tests, and a real-bundle E2E reproduction: legacy resume/title attempts left the Managed log byte-identical; sealed archive/delete succeeded with the writer proof intact. The second batch passed 283 focused Core tests, 77 CLI tests and 2 ACP restore tests. An authority-created log verifies list and single-item title/source projection; a real in-session hook test verifies rejection before switching and byte-identical Managed history; ACP and HTTP tests verify the typed error and 409. The third batch reproduced OpenTUI's silent switch with a real Managed log, then verified a visible rejection and persistence of the next user/assistant turn to the original session. On the rebased final commit, build, typecheck, bundle, bundle version smoke, and 37 focused CLI tests passed. Listing performance was not benchmarked locally. HTTP tests use a fake service; these results do not claim Java/MySQL or process-level Hosted failover coverage.

Tested on

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

Environment (optional)

macOS, Node 22.14.0. Unit tests run from packages/core; build, typecheck, and bundle run from the repository root. Tests use real local file/lease adapters and a fake HTTP service.

Risk & Scope

  • Main risk or tradeoff: this is a substantial core feature and still needs maintainer architecture review. Client-generated writer tokens and caller-supplied tenant headers do not authenticate a service; the separate server must enforce caller authentication and tenant/workspace/session authorization before deployment.
  • Not validated / out of scope: Java/MySQL implementation, OSS bodies above 64 KiB, Hosted wiring, deleted-owner-disk recovery, multi-process execution/cancellation recovery, workspace reconciliation, and remote retention/deletion. HTTP resources above 64 KiB fail explicitly.
  • Breaking changes / migration notes: Managed writers use schema-3 locks and complete event-content digests. The earlier identity-only prototype digest is not accepted as a journal integrity proof. Existing legacy sessions retain the legacy engine, and no local session migration is introduced. Explicit local tail repair handles complete JSONL events without a commit marker; incomplete physical lines remain fail-closed.

Design: English · 简体中文.

Linked Issues

Related to #12380. Split from #12358. Does not close either.

中文说明

这是从 #12358 拆出的 TypeScript Durable Managed Session 基础层,包含会话语义权威、本地 JSONL 和 HTTP inline 存储适配器、checkpoint、prompt/activation journal、逻辑 Harness handle 和 transcript 投影,并补齐该切片独立构建所需的 writer lease 和 record schema 依赖。

本轮修复覆盖完整事件内容校验、checkpoint 引用资源同事务持久化、并发 checkpoint 修改与 handoff 串行化、新 Agent 启动后旧安全边界失效,以及避免 lease/inbox 竞态破坏 replay。本地维护拒绝旧 resume、录制、rename、fork 路径修改 Managed 会话;已封存会话的归档和删除保留 writer fence,删除时清理私有资源;列表仅展示已经提交且 body 校验通过的元数据,legacy 列表跳过冗余的 Managed 元数据探测,同时识别位于 Managed header 前的执行引擎记录。Ink 与 OpenTUI 的会话内 /resume 均在切换当前会话前拒绝 Managed 目标,daemon 恢复将此不兼容情况返回为 HTTP 409。

#12380 的 G 阶段需要独立于 hosted 工具循环和 WebShell 的持久化、恢复会话权威。本 PR 提供 TypeScript 契约和组件行为;Java 存储、迁移、经过认证的 Hosted routing、Broker 恢复和生产 failover 证明属于独立集成工作。

评审验证:本地创建、提交、seal 和 reopen 后,替换 writer 必须验证 schema-3 proof,保留历史,拒绝旧 writer 和被修改事件;并发审批/Runtime 等待只能接受相容工作,已接受执行身份必须保留;新 Agent 不能利用上轮 turn-complete checkpoint 交接。Checkpoint 引用新发布的 history 后,经全新 HTTP adapters 恢复必须能读取两者,未引用暂存资源不得发布。同毫秒续租、取消先于 readiness、renewal 与 seal 竞争都不能破坏 reopen 或在 seal 后留下续租 timer。移除 metadata marker 或篡改 body 后,列表不能显示未提交或损坏的 title/source;首条为执行引擎记录的 Managed 日志应展示已提交的标题与来源。启动时及 Ink、OpenTUI 会话内的旧 resume、标题录制、rename 和 fork 必须不修改 Managed 日志地拒绝,ACP/daemon 恢复应返回有类型的错误和 HTTP 409;已封存会话的归档、取消归档和删除须保留 writer fence。legacy 列表应避免重复读取 Managed header。

修复前,该切片构建失败且 41 个 Managed 组件测试失败;确定性复现还确认资源丢失、checkpoint 覆盖、旧安全边界交接、队列不可重放事件及 renewal/seal 定时器异常。修复后 build、typecheck、bundle、PR 文件 ESLint/Prettier 及 bundle version/help smoke 均通过。24 个定向 Core 测试文件合并验证结果为 1051 通过、10 项平台跳过。广测为 1050 通过,另有一项 Darwin 子进程等待在完整构建并行运行期间超时;构建结束后整个 lease 文件单独重跑为 106 通过、10 跳过,包含此前超时用例,未修改测试 timeout。第一轮评审反馈修复另通过定向 Core 元数据、lease、录制测试及 CLI 配置、归档测试;真实 bundle 的 E2E 复现证实旧 resume/标题录制不会改变 Managed 日志,已封存会话归档/删除成功且 writer proof 完整保留。第二轮又通过 283 项 Core、77 项 CLI 和 2 项 ACP 恢复测试。真实 authority 写出的日志验证了列表及单项读取的标题和来源投影;真实会话内 hook 测试验证切换前拒绝,Managed 历史逐字节不变;ACP 和 HTTP 测试验证有类型错误及 409。第三轮用真实 Managed 日志复现 OpenTUI 静默切换,修复后显示明确错误,下一轮 user/assistant 记录写入原会话。rebase 后的最终提交通过 build、typecheck、bundle、bundle version smoke 和 37 项定向 CLI 测试。列表性能未在本地重跑基准。HTTP 使用 fake service,结果不代表 Java/MySQL 或 Hosted 进程级 failover 已验证。

本地验证平台为 macOS、Node 22.14.0;Windows 和 Linux 未本地验证。单元测试从 packages/core 执行,build/typecheck/bundle 从仓库根目录执行。测试使用真实本地文件/lease 适配器和 fake HTTP service。

风险与范围:这是较大的 core feature,仍需维护者架构审阅。客户端生成的 writer token 和调用方自带 tenant header 不能作为服务认证,独立服务端必须在部署前落实调用方认证和 tenant/workspace/session 授权。Java/MySQL、超过 64 KiB 的 OSS body、Hosted 接线、旧 owner 磁盘删除后的恢复、多进程执行及取消恢复、workspace 对账和远端保留/删除均不在本次验证内;超过 64 KiB 的 HTTP 资源明确失败。

Managed writer 使用 schema-3 锁和完整事件内容摘要,不接受此前仅含事件身份的原型摘要作为 journal 完整性证明。既有 legacy 会话保持 legacy engine,不引入本地会话迁移。显式本地尾部修复处理缺 marker 的完整 JSONL 事件,不完整物理行继续 fail-closed。中英文设计说明已完整同步,并区分本 PR 能力与未来部署验收门槛。

关联 #12380,从 #12358 拆出,不关闭这两个条目。

@wenshao

wenshao commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator

Local real-environment verification — PR #12693 @ e362625

Setup: macOS (Darwin 25.6, APFS), Node 24.18.1. Two isolated pnpm worktrees: head e362625 and base 790bd83 (the merge-base with main). Both were built with npm run build and bundled into dist/cli.js. Every run used its own QWEN_HOME and QWEN_RUNTIME_DIR. Managed sessions were created on disk by the PR's production code: openManagedSession(), then sink.write() for user, assistant, custom_title and turn_result records, then close(). The real bundled CLI (qwen --resume TUI) and a real qwen serve daemon then read and acted on those sessions. The model was the repo's fake-openai-server.

TL;DR

Area Result
Triage "does not compile" Critical Stale. It was raised against ae96eee. On e362625, build, typecheck and bundle pass locally, and CI Lint & Static and Test (ubuntu) are green. The bot's CHANGES_REQUESTED still needs a human dismissal.
PR tests 22 files (managed-runtime/*, lease, sessionService, transcript reader, tool protocol, storage utils): 999 passed / 10 skipped
Legacy parity Restore projections of 3 synthetic legacy transcripts (up to ~39 MB) and the 302-session list are byte-identical head vs base. executionEngine was excluded because it is a new field.
Managed read path ✅ Picker title, daemon /transcript, /load replay, and the history the model receives all show the Managed conversation. Base shows (empty prompt) and an empty transcript.
Tamper claims ✅ Removing the commit marker for the metadata event, or changing one byte of the title body, hides the title in the list.
Legacy SessionService.renameSession / forkSession ✅ Both throw SessionExecutionEngineError, and the transcript is byte-identical afterwards. This holds for the sealed and the released lock shapes.
Guard mutations 5/5 caught by the PR's tests: rename guard, fork guard, body digest check, resource rmSync on delete, commit-marker gate.
F1 legacy engines still execute Managed sessions ❌ See below
F2 daemon delete/archive of a closed Managed session ❌ See below
F3 session-list cost for legacy sessions ⚠️ +~45%

Reachability: nothing in the product creates a Managed session today, so F1 and F2 cannot affect users on this commit. They become real as soon as Hosted/local wiring starts writing Managed logs. F1 also contradicts this PR's own test plan item: "Legacy rename/fork must reject Managed sessions without modifying files."

picker A/B
The same on-disk Managed session in qwen --resume: base on the left, PR head on the right.

F1 — Legacy TUI and daemon run Managed sessions and append legacy records to the Managed log (High, latent)

assertSessionExecutionEngine() is added but has no production caller. The rename guard only exists in SessionService.renameSession(). That is the fallback path. The primary paths are renameCommand.ts:224 (TUI /rename) and acpAgent.ts:13504 (live-session rename), and both go through ChatRecordingService.recordCustomTitle(), which has no guard.

What I saw on the real CLI and daemon (PR head bundle):

  1. TUI qwen --resume → pick the Managed session → send one prompt. The resume is accepted. None of the Managed history is shown (the TUI restore path does not use the new projection). The prompt runs, and ChatRecordingService appends 7 legacy records to the Managed JSONL (user, custom_title, attribution_snapshot, file_history_snapshot, ui_telemetry ×2, assistant).
  2. TUI /rename renamed-by-legacy. The TUI prints Session renamed to "renamed-by-legacy" and appends a legacy custom_title. The picker keeps showing the old title, because the Managed reader only trusts committed metadata.
  3. qwen serve: POST /session/:id/load + /prompt. The load replays the Managed history correctly, and the model receives it (good). But the prompt then runs on the legacy ACP engine: the transcript goes from 14 lines to 23.
  4. After any of these, reopening through the PR's own authority fails: openManagedSession({create: undefined}) → SessionTranscriptChangedError: The session transcript changed outside its active writer. The Managed session can no longer be advanced.

A resume without any write leaves the log intact, and it reopens fine afterwards.

managed log evidence

Suggested fix: refuse, or reroute, at the entry points rather than per write. That means TUI --resume//resume and the ACP loadSession/resumeSession restore should check isManagedSessionTranscriptSync(), or executionEngine from the restore projection, before a legacy ChatRecordingService is bound. recordCustomTitle should get the same guard as renameSession. A test that drives the real resume entry point on a sealed Managed log would pin it.

Screenshot: head TUI running a prompt on the resumed Managed session (no Managed history on screen)

head tui

F2 — Daemon delete/archive fail on the state openManagedSession().close() leaves behind (Medium, latent)

close() seals the writer lock (schema 3, state: "sealed"), and the PR's assembly test confirms that SessionWriterLease.acquire then rejects. The daemon's POST /sessions/delete and /sessions/archive both go through runWithDaemonWriterLease (session-archive.ts:230), so they fail:

qwen serve (PR head) Result
legacy session → /sessions/delete removed
Managed, lease released (the shape the metadata test builds) → /sessions/delete removed, resources/<id> removed
Managed, sealed by openManagedSession().close() → /sessions/delete errors: "Session write ownership could not be verified." (the same on retry)
Managed, sealed → /sessions/archive same error

SessionService.removeSession() called directly does work on the sealed shape, and that is what the unit test "maintenance on a sealed managed session" covers. The daemon/Web Shell route is the path that fails. This contradicts "archive/unarchive and deletion remain supported" for any Managed session that was closed normally.

F3 — Legacy session listing is ~45% slower (Low)

getSessionTitleInfo / readSessionSource now call readManagedSessionTitleInfoSync / readManagedSessionSourceSync first for every session. Each call does its own stat, open and 64 KiB head read before the legacy reader runs. I listed 302 synthetic legacy sessions with SessionService.listSessions({size: 500}), taking the median of 20 runs, with base, head, and head with the two helpers stubbed to return undefined interleaved in each round:

round base head head, stubbed
1–8 (ms) 124 / 145 / 115 / 119 / 194* / 126 / 118 / 125 192 / 229 / 170 / 178 / 186 / 183 / 169 / 179 140 / 174 / 129 / 136 / 117 / 121 / 115 / 118

*This round was noisy (machine load).

The stub brings head back to the base numbers, so the added reads are the whole cost. They add about 0.2 ms per session. A cheap fix is to sniff the Managed header once and reuse the result, or to reuse the legacy reader's head buffer. Restore-projection time for legacy transcripts showed no difference beyond noise.

Notes (not blocking)

  • An untitled Managed session lists with prompt: "". The picker would render that as (empty prompt), because first-prompt previews are not projected.
  • Not covered here: Windows and Linux, the HTTP store against a real service (only the PR's fake-service tests ran), and anything on the Java side.

Merge reference

  • As a foundation slice, the new code behaves as described on its own paths: commit/seal/reopen, tamper detection, projection into daemon restore, and the SessionService guards. Legacy behaviour is unchanged apart from F3.
  • F1 and F2 should be fixed before any code path creates Managed sessions for real users. Otherwise the first legacy resume or rename permanently wedges the Managed log, and closed sessions cannot be deleted from the Web Shell. At minimum, track them as explicit follow-ups, and fix the test plan wording for rename (it holds only for SessionService.renameSession).
  • The triage bot's Critical is obsolete (build is green), but its CHANGES_REQUESTED review has to be dismissed by a human.
中文说明

本地真实环境验证 — PR #12693 @ e362625

环境: macOS(Darwin 25.6,APFS),Node 24.18.1。用两个隔离的 pnpm worktree:head 为 e362625,base 为与 main 的 merge-base 790bd83。两边都执行了 npm run build,并打包成 dist/cli.js。每次运行都用独立的 QWEN_HOME 和 QWEN_RUNTIME_DIR。Managed 会话由 PR 自己的生产代码写到磁盘上:先 openManagedSession(),再用 sink.write() 写入 user、assistant、custom_title 和 turn_result,最后 close()。之后由真实的打包 CLI(qwen --resume TUI)和真实的 qwen serve daemon 读取并操作这些会话。模型用的是仓库自带的 fake-openai-server。

结论速览

项目 结果
triage 的「无法编译」Critical 已过期。 它针对的是 ae96eee。e362625 在本地 build、typecheck、bundle 全部通过,CI 的 Lint 和 Test (ubuntu) 也是绿的。bot 的 CHANGES_REQUESTED 仍需人工 dismiss。
PR 测试 22 个文件:999 通过 / 10 跳过
legacy 行为一致性 3 个合成 legacy 会话(最大约 39 MB)的恢复投影,以及 302 个会话的列表,head 与 base 逐字节一致。executionEngine 是新增字段,比较时已排除。
Managed 读取路径 ✅ 选择器标题、daemon /transcript、/load 回放、模型收到的历史都能看到 Managed 对话。base 只显示 (empty prompt),transcript 为空。
篡改检测 ✅ 删掉覆盖 metadata 事件的 commit 标记,或把标题 body 改一个字节,列表里就不再显示该标题。
legacy SessionService.renameSession / forkSession ✅ 都会抛出 SessionExecutionEngineError,transcript 不变。sealed 和 released 两种锁形态都成立。
守卫变异 5 个变异全部被 PR 自带测试抓到。
F1 legacy 引擎仍会执行 Managed 会话 ❌
F2 daemon 删除/归档已关闭的 Managed 会话 ❌
F3 legacy 会话列表耗时 ⚠️ 约 +45%

可达性: 目前产品里没有任何路径会创建 Managed 会话,所以在这个提交上,F1 和 F2 影响不到用户。一旦 Hosted 或本地开始真正写入 Managed 日志,它们就会变成真实问题。F1 还与 PR 自己测试计划中的「Legacy rename/fork must reject Managed sessions without modifying files」相矛盾。

F1 — legacy TUI 和 daemon 会执行 Managed 会话,并向 Managed 日志追加 legacy 记录(High,潜在)

assertSessionExecutionEngine() 没有任何生产调用方。rename 守卫只加在兜底路径 SessionService.renameSession() 上。主路径是 TUI /rename(renameCommand.ts:224)和 live 会话 rename(acpAgent.ts:13504),它们都走 ChatRecordingService.recordCustomTitle(),没有守卫。

实测结果(PR head 打包产物):

  1. TUI resume 后发一条 prompt:界面上看不到 Managed 历史,并向 Managed JSONL 追加了 7 条 legacy 记录。
  2. TUI /rename:提示「Session renamed」,但选择器里的标题不变。
  3. daemon /load 能正确回放 Managed 历史,模型也拿到了这些历史。但随后 /prompt 由 legacy ACP 引擎执行,transcript 从 14 行变成 23 行。
  4. 以上任一操作之后,再用 PR 自己的 openManagedSession 重开会失败:SessionTranscriptChangedError。这个 Managed 会话从此无法再推进。

只 resume、不写入时,日志保持完好,之后可以正常重开。

建议:在入口处拒绝或改道,而不是逐次写入时拦截。也就是在 TUI resume 和 ACP load/resume 绑定 legacy ChatRecordingService 之前,先检查 isManagedSessionTranscriptSync() 或恢复投影里的 executionEngine。recordCustomTitle 也要加上与 renameSession 相同的守卫。再补一个直接驱动真实 resume 入口、针对 sealed Managed 日志的测试。

F2 — daemon 删除/归档对 openManagedSession().close() 留下的状态失败(Medium,潜在)

close() 会把写锁 seal 成 schema 3。daemon 的 /sessions/delete 和 /sessions/archive 都经过 runWithDaemonWriterLease,结果报错「Session write ownership could not be verified.」,重试也一样。作为对照,legacy 会话和 released 形态的 Managed 会话(也就是单测构造的形态)都能正常删除。直接调用 SessionService.removeSession() 在 sealed 形态上可以成功,而单测覆盖的正是这条路径。daemon / Web Shell 走的路径才会失败。这与 PR 描述里「archive/unarchive and deletion remain supported」不符。

F3 — legacy 会话列表慢约 45%(Low)

现在每个会话都会先调用两个 Managed 读取函数,各自做一次 stat、open,再读 64 KiB 文件头,然后才轮到 legacy 读取。302 个会话、每轮取 20 次中位数、8 轮交错测量:base 约 115–145 ms,head 约 170–229 ms。把这两个函数桩成直接 return undefined 后回落到 base 水平,说明增加的耗时全部来自这两次读取,每个会话约多 0.2 ms。建议只嗅探一次 Managed 头并复用结果。legacy 会话的恢复投影耗时没有超出噪声的差异。

合并参考

  • 作为基础层切片,新代码在它自己的路径上表现与描述一致:commit/seal/reopen、篡改检测、daemon 恢复投影、SessionService 守卫。除 F3 外,legacy 行为没有变化。
  • 在任何路径为真实用户创建 Managed 会话之前,应先修复 F1 和 F2。否则第一次 legacy resume 或 rename 就会让 Managed 日志永久卡死,已关闭的会话也无法在 Web Shell 删除。最低要求是把它们列为明确的后续项,并修正测试计划中关于 rename 的表述(它只对 SessionService.renameSession 成立)。
  • triage bot 的 Critical 已经过时(构建是绿的),但它的 CHANGES_REQUESTED 需要人工 dismiss。

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@wenshao Thanks for the real-environment reproduction. Fixed in 981a3a594:

Feedback Action Verification
F1 — legacy resume/title recording corrupts Managed logs Reject Managed transcripts before legacy resume binds a recorder, and guard direct legacy recording. The real bundled CLI rejects resume before appending; direct title recording leaves the log byte-identical, and Managed reopen succeeds.
F2 — daemon archive/delete fails on sealed Managed sessions Hold a unique maintenance claim while verifying the sealed proof and mutating the session; retain the original sealed writer fence. Daemon archive/delete succeed, resources are retained/removed as appropriate, and the sealed lock remains byte-identical. Archive/unarchive and competing-writer tests pass.
F3 — legacy listing reads Managed metadata for every session Reuse the already-read first record to skip Managed title/source probes for legacy sessions. The listing call path no longer performs those two Managed header probes for legacy records; I have not rerun the 302-session benchmark.

The post-commit build, typecheck, bundle, focused tests, and real-bundle verification passed locally. The PR description and bilingual design docs now reflect these paths. The old bot build failure targeted ae96eee and is stale; its CHANGES_REQUESTED review still needs a maintainer to revisit or dismiss it.

wenshao pushed a commit to wenshao/qwen-code that referenced this pull request Sep 25, 2026
@wenshao

wenshao commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator

Round 2 — real-environment re-verification of 981a3a5

Same setup as round 1. The new head 981a3a5 got its own isolated worktree, a full build and bundle, and fresh QWEN_HOME/QWEN_RUNTIME_DIR for every run. Managed sessions were created by the PR's own openManagedSession() and then driven through the real dist/cli.js TUI and a real qwen serve. Round 1's arms (e362625 and base 790bd83) were kept for A/B.

PR tests on 981a3a5 (managed-runtime, lease, sessionService, transcript reader, chatRecordingService, tool protocol, storage utils): 23 files, 1149 passed / 10 skipped.

Summary

Round-1 finding 981a3a5 Verdict
F1: legacy engines write into Managed logs TUI --resume (picker and <id>), TUI in-session /resume and daemon /load no longer append anything. 0 legacy records, and the authority reopens every log. ✅ Fixed. Three UX rough edges remain (below).
F2: daemon delete/archive fail on a sealed Managed session /sessions/delete removes the transcript and resources/<id>. /sessions/archive followed by /sessions/unarchive succeeds, the sealed lock stays byte-identical, no .claim is left behind, and the authority reopens the session afterwards. ✅ Fixed. One crash note (below).
F3: +45% legacy listing cost 302 legacy sessions: base ~121 ms, e362625 ~177 ms, 981a3a5 ~139 ms (medians of 6 interleaved rounds × 20 runs). Legacy restore and list output is still byte-identical to base. ✅ Mostly recovered, but the fix introduced N1
N1 (new): Managed titles and sources disappear from every session list See below ❌ Regression in 981a3a5

round 2 evidence

N1 — The F3 fix hides the title of every real Managed session (High for this feature, latent)

listSessions and the single-session metadata read now compute:

const knownManaged = firstRecord.subtype === 'managed_session_header_v1';

The PR's own authority, however, writes session_execution_engine ({"engine":"managed"}) as line 1 and puts managed_session_header_v1 on line 2. That holds for both openManagedSession() and a directly opened LocalManagedSessionAuthority with a lease (see managed-session-authority.ts:457). knownManaged is therefore always false for real Managed logs, and both Managed probes are skipped:

  • SessionService.listSessions: e362625 returns title: "Managed probe session"; 981a3a5 returns no title.
  • TUI picker: shows (empty prompt) again, the same as main before this PR.
  • qwen serve GET /workspace/:id/sessions: returns displayName: "".

picker e362625 vs 981a3a5
The same on-disk Managed session: e362625 on the left, 981a3a5 on the right.

The PR's tests miss this because the metadata tests assert on readManagedSessionTitleInfoSync() directly and never go through listSessions on an authority-created log.

Candidate fix, verified against the dist. I patched both knownManaged sites:

const knownManaged =
  firstRecord.subtype === 'managed_session_header_v1' ||
  (firstRecord.subtype === 'session_execution_engine' &&
    (firstRecord.systemPayload as { engine?: unknown } | undefined)?.engine ===
      'managed');

With the patch, the title is back ("Managed probe session"), and legacy listing drops to about base cost (~111 ms against base ~116 ms in the same rounds). Please also add a listSessions test over a log written by openManagedSession(), so the first-record shape is pinned.

F1 — Fixed, with three UX notes (not blocking for a foundation slice)

  1. Startup qwen --resume / qwen --resume <id> now refuses before any write. The user sees An unexpected critical error occurred: and a raw stack (SessionService.assertLegacySessionExecution → loadCliConfig), and the process exits. A friendly message would be better, for example "this session is managed by …; open it with …".

    Screenshot

    startup resume

  2. In-session /resume onto a Managed session (this does not go through loadCliConfig): the switch succeeds and the prompt runs, and the model reply is shown. The ChatRecordingService guard then silently drops the turn. The prompt text only reaches logs.json (input history); no transcript receives it, and no error is shown. The Managed log is intact, but the user believes they continued the session. The /resume switch should be refused, like startup resume is.

    Screenshot

    in-session resume

  3. Daemon POST /session/:id/load is refused, but as HTTP 500 {"error":"Internal error","code":-32603}, with the reason only in data.details. Web Shell will treat this as a server fault. A typed 4xx/409 (session_execution_engine_unavailable) would let clients show the right message.

F2 — Fixed, one note about crash recovery

I took the maintenance claim through the real SessionService.acquireSealedManagedMaintenanceLease() and then killed the process (exit 137) before release. This leaves <lock>.claim behind. After that, the authority cannot reopen the session (SessionWriterUnavailableError), and daemon delete is refused, both immediately and 65 s later. Nothing recovers a stale claim automatically. This is the same fail-closed rule the existing takeover claims already follow, so I'm not treating it as blocking. The difference is that the window now covers the whole delete/archive operation. The claim copies the sealed record with a new owner_id, but it doesn't record the maintainer's pid or start identity, so a later liveness-based recovery would have nothing to check. Worth adding while the format is still new.

Merge reference

  • F1 and F2 are fixed in the real CLI and daemon, and legacy behaviour is unchanged.
  • N1 needs fixing before merge. Otherwise the Managed title/source display added by this PR never works for Managed sessions the PR itself creates. The fix is a two-line change plus a listSessions test.
  • F1's UX notes 2 (silent turn loss after in-session /resume) and 3 (500 instead of a typed 4xx), and the stale-claim note, can be follow-ups while nothing creates Managed sessions for users.
  • The triage bot's CHANGES_REQUESTED (the stale build failure against ae96eee) still has to be dismissed by a human.
中文说明

第二轮:981a3a5 真实环境复验

环境与第一轮相同。新 head 981a3a5 使用独立 worktree,完整执行了 build 和 bundle。每次运行都用新的 QWEN_HOME/QWEN_RUNTIME_DIR。Managed 会话由 PR 自己的 openManagedSession() 创建,再交给真实 TUI 和真实 qwen serve 操作。第一轮的 e362625 和 base 保留下来用于 A/B 对照。PR 测试:23 个文件,1149 通过,10 跳过。

第一轮问题 981a3a5 结果 结论
F1:legacy 引擎写坏 Managed 日志 启动时的 --resume(选择器和 <id> 两种)、会话内 /resume、daemon /load 都没有追加任何 legacy 记录,authority 可以正常重开 ✅ 已修复,剩 3 个体验问题
F2:daemon 删除/归档 sealed 会话失败 delete 成功,资源也一并清掉;archive→unarchive 成功,锁文件逐字节不变,没有残留 claim,之后可以重开 ✅ 已修复,另有一条崩溃备注
F3:列表慢约 45% 302 个会话:base 约 121 ms,e362625 约 177 ms,981a3a5 约 139 ms;legacy 输出仍与 base 逐字节一致 ✅ 基本恢复,但修复引入了 N1
N1(新):Managed 标题/来源在所有列表中消失 见下 ❌ 981a3a5 引入的回归

N1: 新代码用 firstRecord.subtype === 'managed_session_header_v1' 判断是否为 Managed。但 PR 自己的 authority 写出的第 1 行是 session_execution_engine(engine=managed),header 在第 2 行,所以真实 Managed 日志的判断结果永远是 false,两个 Managed 探测都会被跳过。结果是:listSessions 不再返回标题,TUI 选择器又显示 (empty prompt),daemon 列表返回 displayName: ""。PR 的测试直接调用 readManagedSessionTitleInfoSync(),没有经过 listSessions,所以没有发现。候选修复(同时认 engine=managed 的 session_execution_engine 首条记录)已在 dist 上实测:标题恢复,列表耗时约 111 ms,与 base 相当。建议另外补一个以 openManagedSession() 写出的日志驱动 listSessions 的测试。

F1 体验问题(不阻塞基础层合并):

  1. 启动时 resume 被拒,但用户看到的是 "An unexpected critical error occurred" 加原始堆栈,进程随即退出。
  2. 会话内 /resume 切到 Managed 会话后,发送消息能拿到回复,但这一轮被静默丢弃:只进了 logs.json(输入历史),没有进任何 transcript,也没有任何提示。建议像启动时一样直接拒绝切换。
  3. daemon /load 的拒绝以 HTTP 500 "Internal error"(-32603)返回,应该改成带类型的 4xx/409。

F2 备注: 用真实代码拿到维护 claim 后以 exit 137 模拟崩溃,会留下 .lock.claim。之后 authority 无法重开,daemon 删除也被拒绝,65 秒后依旧如此,没有自动恢复。这与现有 takeover claim 的 fail-closed 规则一致,所以不算阻塞;不同的是窗口扩大到了整个删除/归档过程。claim 里没有记录维护进程的 pid 和启动标识,以后想按存活状态回收也无从判断,建议趁格式还新时加上。

合并参考: F1 和 F2 在真实 CLI 和 daemon 上都已修复,legacy 行为不变。N1 应在合并前修复:两行改动加一个 listSessions 测试。否则本 PR 新增的 Managed 标题/来源展示,对 PR 自己创建的会话永远不生效。体验问题 2、3 和崩溃 claim 可以作为后续项。triage bot 基于 ae96eee 构建失败给出的 CHANGES_REQUESTED 已经过时,仍需人工 dismiss。

doudouOUC pushed a commit to doudouOUC/qwen-code that referenced this pull request Sep 25, 2026
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

[codex] Round-2 feedback addressed in 3fad955ce.

Finding Action
N1: Managed title/source missing from lists Fixed. Listings recognize the Managed execution-engine marker before the header. An authority-created log now tests both the list and single-item projection.
F1: in-session /resume silently drops a turn Fixed. The legacy command rejects a Managed target before switching core or UI state; a real Managed-log hook test verifies the rejection and unchanged transcript.
F1: daemon /load returns HTTP 500 Fixed. ACP load/resume returns session_execution_engine_unavailable, mapped to HTTP 409 by the daemon.
F1: startup resume shows a raw stack Deferred as a presentation follow-up; the existing guard rejects before any write.
F2: a crash can strand a maintenance claim Deferred to a claim-recovery design. Automatic reclaim without trustworthy owner identity could admit a second writer, so the current path remains fail-closed.

Validation on the commit: repository build, typecheck, bundle, bundle version smoke, targeted lint/format, 283 Core tests, 77 CLI tests, and 2 ACP restore tests passed. I did not run a local listing benchmark. No inline review threads were open (resolved 0/0). The earlier CHANGES_REQUESTED review refers to the old build failure and still requires maintainer re-review or dismissal.

中文:N1 的 Managed 标题/来源列表回归、会话内 /resume 静默丢失记录、以及 daemon /load 返回 500 的问题均已在 3fad955ce 修复。启动恢复的原始堆栈提示留待体验改进;维护 claim 的崩溃回收需要安全的 owner 身份设计,当前继续保持 fail-closed。最终构建、类型检查、bundle、定向测试和格式检查通过;未在本地重跑列表性能基准。没有待处理的行内评审线程(0/0);旧的 CHANGES_REQUESTED 仍需维护者复审或撤销。

@doudouOUC doudouOUC assigned doudouOUC and unassigned yiliang114 Sep 25, 2026
wenshao pushed a commit to wenshao/qwen-code that referenced this pull request Sep 25, 2026
@wenshao

wenshao commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator

Round 3 — real-environment re-verification of 3fad955

This round uses the same rig as round 1 and round 2. 3fad955 got its own isolated worktree with a full build and bundle, and every run got fresh QWEN_HOME/QWEN_RUNTIME_DIR. Managed sessions were created by the PR's own openManagedSession() and then driven through the real dist/cli.js (ink, and OpenTUI under Bun 1.3.14) and a real qwen serve.

Tests on 3fad955:

  • Core suites from rounds 1–2: 23 files, 1149 passed / 10 skipped.
  • CLI (useResumeCommand*, error-response, acpAgent, config): 5 files, 1351 passed.

Summary

Item Result on 3fad955
N1: Managed title missing from lists ✅ Fixed. listSessions returns "Managed probe session". qwen serve GET /workspace/:id/sessions returns the same value as displayName.
F1: ink in-session /resume silently dropped a turn ✅ Fixed. It shows ✕ Failed to resume session: … belongs to managed, stays on the current session, and persists the next turn to the current session's transcript. The Managed log gets 0 legacy records.
F1: daemon /load returned 500 ✅ Fixed. It returns 409 session_execution_engine_unavailable, and the transcript stays at 14 lines.
F1: startup --resume (picker and <id>) ✅ Still refused before any write, with 0 legacy records. The raw-stack presentation is deferred by the author; that is fine as a follow-up.
F2 regression (daemon delete, archive → unarchive, then reopen) ✅ All still pass. The stale-claim recovery is deferred by the author; fine as a follow-up.
Legacy listing cost / output ✅ 302 legacy sessions, medians of 5 interleaved rounds: base 124–140 ms, 3fad955 119–141 ms, so parity. Legacy restore and list digests are identical to base.
N2 (new, same class as round-2 F1-2): OpenTUI /resume <id> onto a Managed session ❌ Still switches silently and loses the turn. See below.

round 3 evidence

N2 — OpenTUI in-session /resume has no engine guard (Medium, latent)

The fix added sessionService.assertLegacySessionExecution(sessionId) to the ink path (ui/hooks/useResumeCommand.ts:160). The OpenTUI renderer has its own switch implementation, ui/opentui/session-switch.ts:139-140, which calls new SessionService(cwd).loadSession(sessionId) without that guard.

I ran it with QWEN_TUI_RENDERER=opentui and QWEN_TUI_RENDERER_STRICT=1 under bun dist/cli.js, typed /resume <managed-id>, then sent hello after otui switch:

  • The switch is accepted with no message; the transcript view is reset to the Managed session.
  • The prompt runs and the model reply is shown.
  • The ChatRecordingService guard then drops the turn. The Managed log gets 0 legacy records, but no transcript receives the turn. This is exactly the round-2 ink behaviour, now on the other renderer.

Candidate fix, verified in the bundle. Add the same guard before loadSession:

const sessionService = new SessionService(cwd);
sessionService.assertLegacySessionExecution(sessionId);
const sessionData = await sessionService.loadSession(sessionId);

I applied it to the bundled start-opentui-ui-*.js. OpenTUI then shows ✕ Failed to resume session: … belongs to managed, cannot execute with legacy. through its existing catch, stays on the current session, and persists the next turn there. The same useResumeCommand.managed.test.ts-style test on the OpenTUI switch would pin it.

OpenTUI resume: 3fad955 vs candidate guard
Left: 3fad955. The /resume line is gone because the view switched to the Managed session, and the turn below it is persisted nowhere. Right: with the candidate guard, the switch is refused and the turn stays in the current session.

ink in-session /resume on 3fad955 (fixed)

ink in-session resume

I also checked the other in-session paths. /branch (ink and OpenTUI) goes through SessionService.forkSession, which already rejects Managed sessions. Export and preview only read.

Merge reference

  • N1, the ink /resume silent turn loss, and the daemon 500 are fixed in the real CLI and daemon. F2 still holds, and legacy behaviour and listing cost match base.
  • N2 is a one-line guard. Nothing creates Managed sessions for users yet, so it is latent. It is the same defect class the author just fixed for ink, so I'd include it before merge rather than track it separately.
  • The deferred items (startup-resume presentation, stale maintenance-claim recovery) are reasonable follow-ups.
  • The triage bot's CHANGES_REQUESTED (the stale build failure against ae96eee) still needs a human dismissal.
中文说明

第三轮:3fad955 真实环境复验

装置与前两轮相同。3fad955 使用独立 worktree,完整执行了 build 和 bundle,每次运行都用新的 QWEN_HOME/QWEN_RUNTIME_DIR。Managed 会话由 PR 自己的 openManagedSession() 创建,再交给真实 CLI(ink,以及 Bun 1.3.14 下的 OpenTUI)和真实 qwen serve 操作。测试:core 23 个文件 1149 通过 / 10 跳过;cli 5 个文件 1351 通过。

项目 3fad955 结果
N1:列表中 Managed 标题缺失 ✅ 已修复:listSessions 和 daemon 列表都能显示标题
F1:ink 会话内 /resume 静默丢失一轮 ✅ 已修复:提示 "Failed to resume session",留在当前会话,下一轮写入当前会话
F1:daemon /load 返回 500 ✅ 已修复:返回 409 session_execution_engine_unavailable
F1:启动时 --resume ✅ 仍在写入前拒绝;原始堆栈的展示问题作者已推迟,作为后续项可以接受
F2 回归 ✅ delete、archive→unarchive、重开都正常;残留 claim 的回收作者已推迟
legacy 列表耗时和输出 ✅ 与 base 持平,digest 一致
N2(新):OpenTUI 的 /resume <id> ❌ 仍会静默切换并丢失这一轮

N2: 本轮只给 ink 的 useResumeCommand.ts 加了守卫。OpenTUI 渲染器用的是自己的 ui/opentui/session-switch.ts:139-140,直接调用 loadSession,没有经过守卫。实测(bun + QWEN_TUI_RENDERER=opentui):/resume <managed-id> 没有任何提示就切了过去;发出的消息拿到了回复,但没有写入任何 transcript。这与第二轮 ink 的问题完全相同。候选修复是在 loadSession 前加一行 sessionService.assertLegacySessionExecution(sessionId)。我在打包产物上实测过:OpenTUI 会提示 "Failed to resume session",留在当前会话,下一轮也正常写入。/branch 走 forkSession,本来就会拒绝 Managed 会话;export 和 preview 只读。

合并参考: N1、ink /resume 和 daemon 500 都已在真实环境中修复,F2 保持正常,legacy 行为与 base 一致。N2 是与作者刚修过的 ink 问题同一类的一行修复,建议合并前一并补上。推迟的两项可以作为后续项。bot 的过时 CHANGES_REQUESTED 仍需人工 dismiss。

doudouOUC pushed a commit to doudouOUC/qwen-code that referenced this pull request Sep 25, 2026
doudouOUC pushed a commit to doudouOUC/qwen-code that referenced this pull request Sep 25, 2026
doudouOUC pushed a commit to doudouOUC/qwen-code that referenced this pull request Sep 25, 2026
@doudouOUC
doudouOUC force-pushed the cursor/durable-session-failover-3df3 branch from 3fad955 to 9fdf1e3 Compare September 25, 2026 15:47
@github-actions

Copy link
Copy Markdown
Contributor

Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration.

中文

请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

[codex] Manual babysit pass: rebased PR #12693 onto f6155f02e and fixed the Ubuntu Test failure in 9fdf1e353.

The merge conflict was an add/add overlap in the Managed tool protocol and its tests. I retained main's newer confirmation-size bound and its regression test; the resolved files match main exactly. The six failing session-swap telemetry tests all stopped at the new legacy execution guard because their SessionService test double lacked that method. The mock now supplies it, and the exact failing file passes 10/10.

On the rebased head, repository build and typecheck passed, as did 83 Managed tool protocol tests and 87 related CLI tests. The previous failing CI run belongs to the replaced SHA; fresh checks are starting. No inline review threads were open (resolved 0/0). The old CHANGES_REQUESTED review remains for maintainer re-review or dismissal.

中文:已将本 PR rebase 到 f6155f02e,保留 main 新增的 Managed tool confirmation 大小限制及测试,并在 9fdf1e353 修复 Ubuntu Test 的 6 项失败。根因是 session-swap telemetry 测试桩缺少新的 legacy execution guard 方法;原失败文件现 10/10 通过。新基线的完整构建、类型检查、83 项 protocol 测试及 87 项相关 CLI 测试通过。旧 CI 结果属于已替换的 SHA;新检查正在启动。行内评审线程 0/0,旧 CHANGES_REQUESTED 仍需维护者复审或撤销。

doudouOUC pushed a commit to doudouOUC/qwen-code that referenced this pull request Sep 25, 2026
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

[codex] Agreed on N2. Fixed in c2844e618: OpenTUI now checks the session execution engine before loadSession, using the same guard as Ink.

The new OpenTUI regression test first failed because the guard was never called. It now passes and checks that a Managed target is rejected before loading or switching, with a visible error. A separate same-path reproduction using a real Managed authority and recorder confirmed that OpenTUI stays on the original session, writes the next user/assistant turn there, leaves the Managed log byte-identical, and can reopen it. Final build, typecheck, bundle, bundle version smoke, and 37 focused CLI tests passed. No inline review threads were open (resolved 0/0).

中文:同意 N2,已在 c2844e618 修复。OpenTUI 现在与 Ink 一样,在 loadSession 前检查执行引擎。新增回归测试先复现缺失守卫,修复后验证拒绝发生在加载与切换前,并显示错误。真实 Managed authority 与 recorder 的同路径复验确认:仍停留在原会话,下一轮 user/assistant 正常写回原会话,Managed 日志逐字节不变且可重开。最终构建、类型检查、bundle、版本检查和 37 项定向 CLI 测试通过。行内评审线程 0/0。

wenshao pushed a commit to wenshao/qwen-code that referenced this pull request Sep 25, 2026
@wenshao

wenshao commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator

Round 4 — real-environment re-verification of c2844e6 (rebased onto f6155f0)

The PR was rebased onto main@f6155f0, so I rebuilt both arms from scratch:

  • head: 9fdf1e3, then c2844e6.
  • base: f6155f0, replacing the old 790bd83 arm.

Each arm got an isolated worktree with a full build and bundle, and every run got a fresh QWEN_HOME/QWEN_RUNTIME_DIR. The rig is the same as rounds 1–3: Managed sessions are created by the PR's own openManagedSession(), then the real dist/cli.js (ink, and OpenTUI under Bun 1.3.14) and a real qwen serve act on them.

Rebase content check. Comparing the PR's own changed lines, f6155f0..9fdf1e3 equals the round-3 3fad955 patch, with three expected differences:

  • managed-tool-protocol.{ts,test.ts} are gone from the PR, because main now carries them.
  • session-swap-telemetry.test.ts gains the mock fix.
  • c2844e6 adds exactly one line to ui/opentui/session-switch.ts, plus its test.

Result: all findings from rounds 1–3 are resolved

Item Result
N2: OpenTUI in-session /resume <managed-id> ✅ Fixed in c2844e6. It shows ✕ Failed to resume session: … belongs to managed and stays on the current session. The next turn is persisted there (1 transcript), the Managed log gets 0 legacy records, and it reopens. Before the fix (9fdf1e3), the switch was accepted silently and the turn was persisted nowhere.
N2 test strength ✅ Deleting the new guard makes the new session-switch.test.ts case fail (`1 failed
F1 (ink) regression ✅ qwen --resume <id> is refused before any write. In-session /resume is refused and the turn stays in the current session. Both Managed logs reopen.
Daemon / N1 / F2 regression (on the 9fdf1e3 bundle; c2844e6 only touches OpenTUI) ✅ The list displayName is Managed probe session. /transcript returns the Managed history. /load returns 409 session_execution_engine_unavailable. /sessions/delete removes the transcript and resources/<id>. Archive → unarchive succeeds and the log reopens.
Legacy parity against the new base ✅ 302 legacy sessions, medians of 5 interleaved rounds: base 118–148 ms, PR 113–131 ms. Restore-projection and list digests are identical to base.
Tests Core: 23 files, 1150 passed / 10 skipped. CLI on 9fdf1e3 (resume hooks, session-swap telemetry, error-response, acpAgent, config, all of ui/opentui): 89 files, 3025 passed. session-switch.test.ts on c2844e6: 10/10.

round 4 evidence

OpenTUI resume: 9fdf1e3 vs c2844e6
Left: 9fdf1e3. The switch is accepted silently, and the turn is persisted nowhere. Right: c2844e6. The switch is refused, and the turn stays in the current session.

Remaining (author-deferred, non-blocking)

  • Startup qwen --resume onto a Managed session still prints An unexpected critical error occurred: and a raw stack (now minified by main's bundle change). It happens before any write, so this is presentation only.
  • A crash while holding the sealed-maintenance claim leaves <lock>.claim behind, and the session then fails closed with no automatic recovery. This matches the existing takeover-claim rule.

Merge reference

In the real CLI (both renderers) and daemon, I found no remaining functional issue from my four rounds, and legacy behaviour matches the new base. From the verification side, this is ready to merge once CI on c2844e6 is green. At the time of posting, Integration (no-AK) had passed, and Lint, Test (ubuntu) and Serve A/B were still running. The triage bot's CHANGES_REQUESTED (the stale build failure against the original first commit) must be dismissed by a maintainer; only a human can clear it.

中文说明

第四轮:c2844e6(已 rebase 到 f6155f0)真实环境复验

PR 已 rebase 到 main@f6155f0,所以两臂都重新构建:head 为 9fdf1e3 和 c2844e6,base 换成 f6155f0。每臂用独立 worktree,完整执行 build 和 bundle;每次运行都用新的 QWEN_HOME/QWEN_RUNTIME_DIR。装置与前三轮相同。

rebase 内容核对: 按 PR 自身的改动行比较,f6155f0..9fdf1e3 与第三轮的 3fad955 相同,只有三处预期内的差异:

  • managed-tool-protocol 已由 main 先合入,不再出现在 PR 中;
  • session-swap-telemetry.test.ts 补了测试桩;
  • c2844e6 只给 OpenTUI 的 session-switch.ts 加了一行守卫,并补了测试。
项目 结果
N2:OpenTUI 会话内 /resume ✅ 已修复。提示 "Failed to resume session" 并留在当前会话;下一轮写入当前会话,Managed 日志没有被写入,可以重开。修复前(9fdf1e3)会静默切换,这一轮没有保存到任何地方。
测试有效性 ✅ 删掉守卫后新测试失败(1 失败 / 9 通过);恢复后 10/10 通过
F1(ink)回归 ✅ 启动时 resume 在写入前被拒;会话内 /resume 被拒,这一轮保留在当前会话
daemon / N1 / F2 回归 ✅ 列表有标题;transcript 有 Managed 历史;/load 返回 409;delete、archive/unarchive 正常,之后可以重开
与新 base 的 legacy 一致性 ✅ 列表耗时持平,恢复和列表的 digest 一致
测试 core 23 个文件 1150 通过 / 10 跳过;cli 89 个文件 3025 通过;session-switch 10/10

剩余(作者已推迟,不阻塞): 启动时 resume 的报错仍直接露出原始堆栈(只是展示问题,拒绝发生在任何写入之前);维护 claim 在崩溃后没有自动恢复(与现有 takeover claim 的 fail-closed 规则一致)。

合并参考: 四轮验证中发现的功能问题已全部解决,legacy 行为与新 base 一致。从验证角度看可以合并,前提是 c2844e6 的 CI 通过:发帖时 Integration (no-AK) 已通过,Lint、Test (ubuntu) 和 Serve A/B 仍在运行。triage bot 过时的 CHANGES_REQUESTED 需要维护者手动 dismiss。

wenshao
wenshao previously approved these changes Sep 25, 2026
@doudouOUC
doudouOUC enabled auto-merge September 25, 2026 16:58

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

Re-approving against new HEAD 1443597a.

Reviewed with AI assistance.

@doudouOUC
doudouOUC dismissed a stale review September 25, 2026 17:00

Already have 2 approves

@doudouOUC
doudouOUC added this pull request to the merge queue Sep 25, 2026
Merged via the queue into QwenLM:main with commit 53afcb6 Sep 25, 2026
66 of 87 checks passed
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code resolved the merge conflicts, but the head branch changed while resolving, so the update was not pushed. Re-run /resolve. The resolved diff is attached as the qwen-resolve-pr-12693-attempt-1 artifact on the workflow run.

Merge resolution — PR #12693

Merge ff415d21cc (parents c2844e618f PR head, be1a0a1fb0 main). Two add/add conflicts, both docs; no code file conflicted.

Root cause. #12358 split into #12692 (Java control plane) and #12693 (TS journal); each carried its own rewrite of the same design-doc path (2026-09-21-managed-session-durable-store, EN + zh-CN). #12692 landed first (34246c6606), so both sides added it independently — a filename collision, not code divergence.

Semantic; kept one side. They share only section numbering 1–14; all prose differs (363 vs 153 lines). Kept main's, both languages, so the pair stays synchronized. Main's landed ManagedSessionStore.java runs SQL against exactly the four tables its §6.1–6.4 specifies; ours deletes those sections, which would leave code already on main undocumented. The contract fixture is byte-identical on both sides (blob bc3251cf23), shared by main's Java contract test and our HTTP store test; main's §7 is that spec. Main's copy is also a superset on our own surface; ours calls Java/migrations future work, now false.

Load-bearing. error-response.ts auto-merged; its two new guards must stay on disjoint properties. Main's isSessionStartupConfigError(err) (L842) tests err.code; ours (L1026) tests err.data.errorKind. Main's runs first, safe only because of that split: widen either guard onto the other's property and main's early return swallows our 409. Merged file is the exact union, no deletions. This PR no longer owns that doc — restate its scope in main's version. Our non-conflicted managed-session-record-foundation.md edit survived.

Not verified (no build/tests run). We changed TS eventsDigest from identity-only to full ordered events. Main's Java only validates and stores events_digest, never recomputes, so TS cannot break Java — but the fixture pins one eventsDigest predating that change; whether our HTTP store test recomputes it is a runtime question in a non-conflicted file. We add two domains plus an enablement gate; main's Java enumerates none.

Only the 2 conflicted files were hand-edited; all 55 PR-only files match c2844e618f.

中文说明

合并提交 ff415d21cc(父:c2844e618f、main be1a0a1fb0)。两处冲突均为文档 add/add,无代码冲突。

根因:#12358 拆为 #12692(Java)与 #12693(TS),两者各自重写同一设计文档路径;#12692 先落地(34246c6606),该路径被双方独立新增,属文件名撞车而非代码分歧。

属语义冲突,保留一侧:两份文档只共享章节编号,正文全不同(363 行 vs 153 行)。中英均保留 main 版本以维持同步:main 已落地的 Java store 执行的 SQL 正对应其 §6.1–6.4 四张表,本 PR 版本删掉了这些章节,会让 main 上已有代码失去文档;契约 fixture 两侧逐字节相同、双方测试共用,main §7 即其规范。main 版本对本 PR 范围亦是超集;本 PR 版本称 Java/迁移属未来工作,现已不成立。

关键约束:error-response.ts 为自动合并,两个新判定必须作用于互不相交的属性——main(L842)判 err.code,本 PR(L1026)判 err.data.errorKind。main 先执行,安全仅依赖此区分:任一侧越界到对方属性,其提前 return 就会吞掉本 PR 的 409。合并结果为两侧新增的精确并集,无删除。本 PR 不再拥有该文档;未冲突的 managed-session-record-foundation.md 改动已保留。

未能验证(未跑构建/测试):本 PR 把 TS eventsDigest 从仅身份改为完整有序事件。main 的 Java 只校验并存储、从不重算,故不会破坏 Java;但 fixture 固定了一个早于该变更的值,本 PR 测试是否重算比对属未冲突文件的运行期问题。另新增两个 domain 与启用门禁,main 的 Java 未枚举 domain。

wenshao added a commit to doudouOUC/qwen-code that referenced this pull request Sep 25, 2026
…s-p0-p8

Brings in main through 53afcb6: QwenLM#12308 (model and reasoning selection
at session creation) and QwenLM#12693 (durable Managed Session journal and
failover), the latter split from this branch.

QwenLM#12308 merges as is. Its startup configuration rides the branch's spawn
path, including the deferred start used with paired execution engines.

From QwenLM#12693, the review fixes are taken:
- a harness-requested event of any kind needs an activation subject,
  while a trusted entry's message.committed does not; the commit digest
  rejects an empty or oversized event list before encoding;
- sealing a Managed lease checks the commit proof before it reads the
  transcript;
- title and source reads skip the Managed probe for a transcript already
  known to be legacy, and prefer the committed Managed record otherwise;
- legacy rename, fork, recording and TUI resume refuse a Managed
  transcript up front; a restore reports an engine refusal as -32024
  with the Session id, and the daemon answers it with 409.

Where the branch already enforces the same rule, its implementation
stays: the event and commit digests keep the Managed Tool canonical
encoder (same bytes; main's pinned digest still matches), the actor
check keeps its harness-subject helper, and loadCliConfig applies main's
up-front legacy check only to a legacy host, since a Managed host
restores through its own execution engine check.
pull Bot pushed a commit to Stars1233/qwen-code that referenced this pull request Sep 29, 2026
…(M4) (QwenLM#12935)

* feat(managed-agent): Record Managed sessions as Managed Session logs (M4)

A core Config whose sessionExecutionEngine is managed now records its
session through the QwenLM#12693 authority, with its local JSONL journal and
resource stores, instead of writing a plain transcript that starts with a
managed owner record.

- The recorder's writer lease is a Managed writer (lock schema 3,
  certified takeover). Config opens the log on that lease before the
  recorder accepts a record: a new session publishes its definition and
  root snapshot and gets the owner record and header; a restore must
  verify a managed owner in the reader's restore projection; a
  transaction a crash left without its commit marker is moved to the
  diagnostic file. Managed execution without chat recording or a lease
  fails before anything is written, and a log the authority cannot open
  fails with the authority's own error rather than writer unavailability.
- The recorder binds the authority's record sink, so every accepted
  record is a committed Managed transaction. Records the format cannot
  carry, and carried kinds in shapes the mapping cannot take, are refused
  before the queue, so they cannot stop recording: an awaited write gets
  ManagedSessionRecordRefusedError, a fire-and-forget one is dropped with
  a debug log. The sink's canCarry now checks those shapes, and it reads
  the range a compaction replaces where it numbers the compaction, so an
  activation renewal that commits meanwhile cannot make it conflict.
- Managed titles stay off the parentUuid chain. The session list reads
  the title and source from the log's head and tail windows, so the
  recorder re-anchors both by the log's own growth, measured once each
  record lands: a due anchor is written right behind that record, a close
  (a handoff included) writes the due anchors, and finalize the due title,
  also after growth from renewals alone. A restore reads the title from
  the whole log and the source from the replayed records. The active
  chain goal verification reads comes from the reader's full projection.
- Close flushes, writes due anchors, stops the activation and seals the
  lease at the authority's commit proof. A close during activation no
  longer releases a Managed writer early. The failed activation seals an
  opened log at the authority's position, gives a lease that took over a
  sealed lock its seal back, releases the lock of a transcript that is
  missing, empty or without Managed evidence in its head, and seals any
  other log at the committed position scanned from it, keeping the lock
  held when the head or the log cannot be read.

The design gains the M4 section, its acceptance criteria and the
first-phase records the format cannot carry yet. Three Legacy refusal
tests proposed by the QwenLM#12906 verification are added.

Part of QwenLM#12737.

* fix(managed-agent): Address M4 review findings

- Refuse a Legacy-owned transcript before a Managed activation takes the
  lease, so a certified takeover never retires a Legacy handoff seal
  (R1-1).
- Write the due anchors on every Managed close, also after a failed
  write (R1-2).
- Read a compaction's range before its summary is published and number
  only the event where it commits (R1-11).
- Report a title whose body cannot be read as none instead of failing the
  restore or a page (R1-13).
- State that the schema-3 lock only stops writers that take the writer
  lease (F1).
- Tests: pin the installed activation and the published definition,
  isolate QWEN_RUNTIME_DIR, reclaim crashed locks instead of deleting
  them, give the handoff close the certified policy, cover a handoff close
  whose last flush failed, check the record before a failed anchor, and
  skip the chmod construct where chmod does not block reads.
- Design: drop the status, slice-table and dependency edits that QwenLM#12920
  owns, correct the refusal split and the compaction range, and record
  the recording cost, the same-process retry and the pre-0.24.6 writers as
  M6 risks.

* test(managed-agent): Pin the projection owner check behind the Legacy refusal

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed. Suggestions are inline.

Not reviewed: 26 of 61 territories — finder agents were killed by provider stalls before returning and the reverse audit (stopped at 12 of 61 chunks) never reached them: chunks 17, 18, 19, 22, 23, 24, 25, 27, 29, 31, 32, 33, 34, 39, 40, 41, 42, 43, 45, 46, 49, 52, 53, 54, 56, 57 — the diff there was opened for coverage, but no findings were returned from those reads.

Not reviewed: reverse audit — stopped after 12 of 61 chunks (6 delivered substantive receipts) by the review time budget after sustained provider stalls; 49 chunks unaudited.

Not reviewed: build-and-test — Test (windows-latest) and Test (macos-latest) are red at the reviewed commit and Integration Tests (CLI, No Sandbox) never ran there; local suite execution was infra-blocked (no built dist in the probe tree), so no test evidence exists for any platform.

Not explored to full depth (tool budget reached): chunk 35: live execution of managed-session-authority.test.ts (no installable/built deps in the shared worktree; offset by the textual trace in item 3).; "agent reverse-audit (round 1)": none — all planned checks completed; no file in my chunk went unread and no verification was cut short by the ceiling.; chunk 4: none — all intended checks completed (brief, full diff page, both implementation modules, 6 test executions).; chunk 30: did not execute the suite (no node_modules/vitest in the review worktree; verified by reading implementation instead); chunk 36: none — all checks I planned completed (no exploration was cut short by the tool ceiling)., and 2 more.

中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

未审查(原文为英文):26 of 61 territories — finder agents were killed by provider stalls before returning and the reverse audit (stopped at 12 of 61 chunks) never reached them: chunks 17, 18, 19, 22, 23, 24, 25, 27, 29, 31, 32, 33, 34, 39, 40, 41, 42, 43, 45, 46, 49, 52, 53, 54, 56, 57 — the diff there was opened for coverage, but no findings were returned from those reads.

未审查(原文为英文):reverse audit — stopped after 12 of 61 chunks (6 delivered substantive receipts) by the review time budget after sustained provider stalls; 49 chunks unaudited.

未审查(原文为英文):build-and-test — Test (windows-latest) and Test (macos-latest) are red at the reviewed commit and Integration Tests (CLI, No Sandbox) never ran there; local suite execution was infra-blocked (no built dist in the probe tree), so no test evidence exists for any platform.

未探索到全部深度(达到工具调用预算):chunk 35:live execution of managed-session-authority.test.ts (no installable/built deps in the shared worktree; offset by the textual trace in item 3).;"agent reverse-audit (round 1)":none — all planned checks completed; no file in my chunk went unread and no verification was cut short by the ceiling.;chunk 4:none — all intended checks completed (brief, full diff page, both implementation modules, 6 test executions).;chunk 30:did not execute the suite (no node_modules/vitest in the review worktree; verified by reading implementation instead);chunk 36:none — all checks I planned completed (no exploration was cut short by the tool ceiling).,另有 2 条。

— glm-5.3-flash via Qwen Code /review (v0.24.6)

Comment on lines +265 to +267
lease =
(await service.acquireSealedManagedMaintenanceLease(sessionId)) ??
(await service.acquireSessionMaintenanceLease(sessionId, leaseOptions));

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.

[Suggestion] R1-4: The archive server's new sealed-Managed-lease fallback is untested at the CLI layer; session-archive.test.ts (96 tests, untouched by this diff) contains zero references to 'managed'.

Failure scenario: acquireSealedManagedMaintenanceLease and its lease semantics are tested only via SessionService directly (managed-session-metadata.test.ts), so the one hop where the daemon actually wires them — this fallback, and the ordering 'sealed-managed first, legacy maintenance lease second' — has no witness. If the sealed-managed attempt is deleted (or mis-ordered so the legacy lease is tried first), every daemon archive/unarchive/delete of a sealed Managed session falls to acquireSessionMaintenanceLease, which cannot take the sealed schema-3 lock → runWithDaemonWriterLease returns mutationApplied: false → the route fails for exactly the sessions the design's acceptance row requires to succeed. All metadata-suite tests stay green under that mutation because they call SessionService methods directly.

中文说明

daemon 归档路径的 sealed-Managed 租约回退在 CLI 层没有测试;若删除该分支,sealed 会话的归档/删除会全部失败,而所有元数据套件仍然全绿。

Witness (verification evidence)
grep sweep: session-archive.test.ts for [Mm]anaged → 0 matches; a legacy acquire's takeOverSealed refuses a sealed schema-3 lock, so deleting the sealed-managed arm fails the route with all metadata-suite tests green.

Suggested fix: Add a session-archive.test.ts case that seeds a sealed Managed session (as the metadata suite does) and drives the archive/unarchive/delete request path through runWithDaemonWriterLease, asserting success and that the lock file is byte-identical afterwards.

The fix must not violate: The legacy fallback must keep working for non-managed sessions — acquireSealedManagedMaintenanceLease deliberately returns undefined for legacy transcripts (sessionService.ts:916-919), so the test matrix must cover both branches, not just the Managed one.

Acceptance criterion: That new case: red if the acquireSealedManagedMaintenanceLease arm is removed (route starts failing on the sealed lock). Please remove the fix and confirm that test goes red before landing.

— glm-5.3-flash via Qwen Code /review (v0.24.6)

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.

[Suggestion] R1-4 (still stands, re-verified 2026-10-03): The archive server's sealed-Managed-lease fallback remains untested at the CLI layer — the sealed-first fallback at :256-268 is verbatim present, and session-archive.test.ts (unchanged by this diff) still has zero 'managed' references across its 96 tests. Head has not moved since round 1 (1443597).

中文说明

与 R1 一致:sealed-first 兜底分支原样存在,CLI 侧 session-archive 测试全库仍无 managed 命中(96 个用例),未修。

— kimi-k3@fda99a87 via Qwen Code /review (v0.24.0)

Comment on lines +183 to +184
if (this.recoveryTimer) clearTimeout(this.recoveryTimer);
this.recoveryTimer = undefined;

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.

[Suggestion] R1-21: pump() clears the pending recovery timer on entry, but its memory-blocked exit (this.memoryBlocked = true; return; at :197-199) returns without rescheduling it — the only exit path that discards an armed wake without a replacement.

Failure scenario: Single-worker deployment, idle (zero active runs). The store holds activation X assigned to a crashed worker with lease expiring at T; a prior pump armed the recovery wake for T. At T−ε an operator submits activation Y → pump() clears the wake timer, sees candidates (Y), finds hasMemoryHeadroom() false, sets memoryBlocked, returns. No active run exists, so no execute() completion ever re-pumps; memory later frees but nothing notifies. X is never reclaimed at expiry and Y never starts, despite both being recoverable, until an unrelated submit or notifyCapacityChanged() arrives — potentially forever.

中文说明

pump() 进入时清掉恢复定时器,但 memory-blocked 退出路径不重新武装——这是唯一丢弃已武装唤醒又不留替代的出口。假定时器探测证明:可恢复的过期租约与排队任务可能永远无人认领。

Witness (verification evidence)
[probe] PR code: 'Timed out waiting for scheduler state.' after 60s of fake time past expiry, handled: [] (nothing fired); with the one-line re-arm added: handled: ['x','y'], both released completed. Deterministic flip in both directions.

Suggested fix: On the memory-blocked exit, reschedule the recovery wake (scheduleRecoveryWake()) — but guard against a zero-delay loop: an assigned head whose lease has already expired is itself runnable (listRunnable admits expired leases), so the re-armed delay must be clamped to a positive floor or only unexpired leases armed.

The fix must not violate: listRunnable() admits assigned session heads whose lease has expired (managed-activation-store.ts:490-507), so any re-arm on the memory-blocked path must not compute a zero delay from an already-expired lease.

Acceptance criterion: Extend embedded-harness-scheduler.test.ts with a fake-timer case: dead-worker assignment as in the existing recovery test, then submit with hasMemoryHeadroom: () => false so the pump exits memory-blocked; advance past the peer lease expiry and assert hasMemoryHeadroom is consulted again (the wake fired). Mutation check: remove the re-arm → the assertion goes red. Please remove the fix and confirm that test goes red before landing.

— glm-5.3-flash via Qwen Code /review (v0.24.6)

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.

[Suggestion] R1-21 (still stands, re-verified 2026-10-03): pump() still clears the pending recovery timer on entry, and its memory-blocked exit (this.memoryBlocked = true; return; at :197-199) still returns without rescheduling it — the only exit path that discards an armed wake without a replacement. Head unchanged since round 1.

中文说明

与 R1 一致:pump 进场清除恢复定时器后,内存阻塞出口仍未重新排期,该唤醒被偷换掉。未修。

— kimi-k3@fda99a87 via Qwen Code /review (v0.24.0)

Comment on lines +387 to +389
expect(error).toMatchObject<Partial<ManagedSessionStoreHttpError>>({
status: 409,
remoteCode: 'managed_session_writer_conflict',

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.

[Suggestion] R1-22: The new HTTP-store suite never drives a failing or inconsistent server response — the fake hard-wires consistent single-page responses and echo-accurate receipts, so every integrity branch of the client's journal read loop (empty page / non-contiguous revisions / byteLength-vs-recordDigest mismatch, nextRevision inconsistency and early termination before head, journal-vs-head mismatch), the multi-page pagination path (limit=100), appendTransaction's commit-receipt validation, and the reachable-but-never-triggered managed_session_resource_missing 409 branch have zero coverage anywhere in the package.

Failure scenario: A refactor that drops or weakens one of these client guards — e.g. removing the nextRevision !== afterRevision check — ships green: every test here and in the contract suite still passes because the fake always returns one consistent page and a valid receipt. Against the real Java store, a >100-transaction journal then fails to restore (hang, truncation, or spurious corrupt error) or a tampered page is accepted as valid history. Likewise a commit whose referenced resource is missing server-side (the exact 409 the fake implements) hits entirely untested client behavior, including whether staged resources are retained for retry (releaseCommitted only runs on success).

中文说明

HTTP store 套件的 fake 永远返回一致的单页响应与回显回执,客户端读循环的全部完整性分支与分页路径零覆盖;探测删除两个完整性守卫后全部 10 个测试仍然全绿。

Witness (verification evidence)
[probe] mutation check in the scratch tree, both suites: unmodified PR — Tests 10 passed (10); with the nextRevision !== afterRevision guard deleted — Tests 10 passed (10), ships green; with the head-mismatch guard also deleted — Tests 10 passed (10). Both integrity guards are uncoverable by the existing tests.

Suggested fix: Add tests with a misbehaving fake: (a) a journal spanning two pages pinning pagination and the hasMore/nextRevision/head invariants; (b) a tampered recordDigest or non-contiguous journalRevision page asserting the corrupt error; (c) a commit receipt with journalRevision !== grant.journalRevision + 1; (d) a commit referencing a resource absent from both inline bytes and the server map, asserting the 409 surfaces and staged resources survive for retry.

The fix must not violate: Any new fake response must satisfy the client's strict page/receipt contract (transaction.journalRevision === afterRevision + 1, nextRevision === afterRevision, receipt journalRevision === grant.journalRevision + 1 and committedSequence === descriptor.lastSequence), otherwise the new tests fail on a different guard than the one under test.

Acceptance criterion: The new tests themselves — mutation check: delete the nextRevision !== afterRevision guard (http-managed-session-store.ts:449-450) or the !hasMore && afterRevision !== head.journalRevision guard (:451-452); the new pagination test must go red. Please remove the fix and confirm that test goes red before landing.

— glm-5.3-flash via Qwen Code /review (v0.24.6)

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.

[Suggestion] R1-22 (still stands, re-verified 2026-10-03): The HTTP-store suite still never drives a failing or inconsistent server response — the fake still hard-wires consistent single-page responses and echo-accurate receipts, so the client's integrity branches remain unexercised. Head unchanged since round 1.

中文说明

与 R1 一致:HTTP store 测试依旧只喂一致的成功响应,客户端各完整性分支没有失败驱动。未修。

— kimi-k3@fda99a87 via Qwen Code /review (v0.24.0)

Comment on lines +896 to +902
if (
Object.hasOwn(record, 'resourceId') &&
Object.hasOwn(record, 'kind') &&
Object.hasOwn(record, 'schemaVersion') &&
Object.hasOwn(record, 'byteLength') &&
Object.hasOwn(record, 'digest')
) {

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.

[Suggestion] R1-24: collectRefs treats any object in a record carrying the five ref field names as a ManagedSessionDurableRef and runs assertManagedSessionDurableRef on it, but event payloads contain free-form JSON positions (e.g. cancel.requested.target, typed 'json' in EVENT_SCHEMAS, validated only by assertJsonValue) that legitimately admit such key names — a schema-valid event then fails the whole transaction, or worse, silently binds a phantom resource dependency.

Failure scenario: appendTransaction is called with a cancel.requested event whose payload.target is {resourceId:'x', kind:'y', schemaVersion:1, byteLength:2, digest:'nothex'} — the event passes parseManagedSessionEvent (target is unrestricted JSON), but collectRefs throws ManagedSessionRecordError('resource ref.digest must be a lowercase SHA-256 hex digest.') and the entire append is rejected. With a valid 64-hex digest instead, the object is accepted as a ref, flows into descriptor.refs → commitResources, and the commit request carries a CommitResource for a resource that was never published — turning a user's cancel into a store-side failure or a bogus dependency.

中文说明

collectRefs 把任何带五个 ref 键名的对象都当作 durable ref 处理:schema 合法的 cancel.requested 事件会被整个事务拒绝,或把幻影资源依赖绑进提交。三案例探测(拒绝/幻影/对照)已复现。

Witness (verification evidence)
[probe] intact PR, schema-valid cancel.requested with the five ref keys + bad digest: appendTransaction rejects 'resource ref.digest must be a lowercase SHA-256 hex digest'; with a valid 64-hex digest: commit succeeds and the POST body's resources array carries the phantom ref with no bytesBase64; control (one key renamed): clean commit, resources: []. Flip: with the skip-on-invalid patch the rejection disappears (probe then fails on the patched tree); restore re-run 3/3 green.

Suggested fix: Discover refs positionally instead of duck-typing: walk only the ref-typed fields declared by EVENT_SCHEMAS/parseManagedSessionHeader (ref, refOrNull, refs), or skip objects whose assertManagedSessionDurableRef validation fails when they occur outside declared ref positions.

The fix must not violate: The contract fixture pins that a genesis commit's resources equal exactly the header's refs (asserted via expect.arrayContaining + toHaveLength at managed-session-store-contract.test.ts:180-186), so any narrowing of ref discovery must still surface the header's definitionRef/rootSnapshotRef/baseTranscriptProof and each checkpoint.committed stateRef — commitResources(descriptor.refs) is what verifies/stages those bytes.

Acceptance criterion: A test appending a transaction containing a cancel.requested event whose target carries the five ref keys with non-ref values, asserting the commit succeeds; removal of the fix must make it fail with the 'resource ref' error. Please remove the fix and confirm that test goes red before landing.

— glm-5.3-flash via Qwen Code /review (v0.24.6)

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.

[Suggestion] R1-24 (still stands, re-verified 2026-10-03): collectRefs still adopts any object carrying the five ref field names as a ManagedSessionDurableRef and runs assert on it — the duck-typing walk over event payloads (a free-form JSON position such as a cancel.requested target can collide with the shape) is unchanged. A same-mechanism round-2 candidate (R2-5) was confirmed and dropped as a duplicate of this entry. Head unchanged since round 1.

中文说明

与 R1 一致:collectRefs 仍按五字段鸭子类型收编任意对象为 durable ref 并断言,自由载荷位置误匹配/幻象注册问题未修;本轮重复的 R2-5 已按本条目吸收。

— kimi-k3@fda99a87 via Qwen Code /review (v0.24.0)

Comment on lines +317 to +319
return this.serial(async () => {
const at = this.getCurrentTime();
const activation = descriptor(input);

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.

[Suggestion] R1-26: Input snapshotting is asymmetric across the five mutators: only cancelQueued clones its caller-supplied object (structuredClone before the serial() queue), so enqueue/claim/renew/release read and validate the caller's live objects inside the deferred callback, and a caller mutation between the call and the queue drain is silently persisted.

Failure scenario: A caller that reuses a mutable descriptor across unawaited calls — const p = store.enqueue(input, limits); input.payloadRef = 'event:a2'; await p; — gets the journal line for activation a1 written with a2's payloadRef: the durable event, the returned snapshot, and every later replay carry the wrong value, and re-submitting the intended a1 values is then permanently rejected ('reused with different data'). Deterministic trigger, exotic in practice; all current in-repo callers build fresh objects per call.

中文说明

五个 mutator 中只有 cancelQueued 在入队前克隆输入;其余四个在延迟回调里读调用方的活对象,调用方在 await 前复用/修改对象会把错误值写进持久日志并永久毒化幂等性。

Witness (verification evidence)
[probe] unmodified PR, caller mutates payloadRef between call and drain: journal line, returned descriptor, and replayed descriptor all carry 'event:a2'; re-submitting the intended a1 values is rejected ('was reused with different data'). Flip with the structuredClone hoist: all three carry 'event:a1'; re-submit accepted. Existing 8/8 suite still passes with the fix.

Suggested fix: Hoist const captured = structuredClone(input) (and a clone of limits in enqueue) above this.serial(...) in enqueue, claim, renew, and release, mirroring cancelQueued, and validate the clone inside the callback.

The fix must not violate: All five mutators return this.serial(...) promises and surface invalid-argument errors as async rejections; the fix must keep validation inside the serial callback so a bad identity/lease still rejects the returned promise rather than throwing synchronously (EmbeddedHarnessScheduler.submit only handles rejections around its await).

Acceptance criterion: A test in managed-activation-store.test.ts calling store.enqueue(input, limits) without awaiting, synchronously mutating input.payloadRef, awaiting, reopening via FileManagedActivationStore.open, and asserting the stored descriptor's payloadRef equals the call-time value — red against current code. Please remove the fix and confirm that test goes red before landing.

— glm-5.3-flash via Qwen Code /review (v0.24.6)

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.

[Suggestion] R1-26 (still stands, re-verified 2026-10-03): Input snapshotting remains asymmetric — only cancelQueued clones its caller-supplied object (structuredClone :370) while enqueue/claim/renew/release still read and validate the caller's live object inside the serial queue (:312/:385/:427/:455). Head unchanged since round 1.

中文说明

与 R1 一致:五个 mutator 中仍只有 cancelQueued 做了入参快照,其余四条仍在串行队列内读调用方活对象。未修。

— kimi-k3@fda99a87 via Qwen Code /review (v0.24.0)

Comment on lines +3072 to +3073
if (indexHasManagedHeader(index)) {
return this.readManagedRestoreProjection(

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.

[Suggestion] R1-3: The entire Managed read path of SessionTranscriptReader — restore (readManagedRestoreProjection, buildManagedSessionRestoreProjection), live restore, navigation turns, and paging (managedPageIndex, managedNavigationTurns, managedReplayPage) — has no test anywhere in this PR.

Failure scenario: buildManagedSessionRestoreProjection has zero references in any *.test.ts; session-transcript-reader.test.ts (the reader's own suite, untouched by this diff) contains no mention of 'managed'; the only tests driving a real Managed transcript (useResumeCommand.managed.test.ts, managed-session-metadata.test.ts) never reach SessionTranscriptReader. Concretely: if the Managed gate misroutes — e.g. indexHasManagedHeader misses a header, or managedReplayPage returns the wrong suffix — a daemon restore/navigation/page request for a Managed session hands back the authority's wrapper envelopes (or an empty/wrong history) as if they were the conversation, and no test in this PR fails. The data source (readManagedSessionRecords) is well tested; the reader's use of it is not.

中文说明

SessionTranscriptReader 的整条 Managed 读取路径(restore、live restore、导航、翻页)在本 PR 中没有任何测试;若 Managed 门控误路由,守护日志被当作会话内容返回且没有任何测试变红。

Witness (verification evidence)
grep sweep: buildManagedSessionRestoreProjection → 2 matches, both source (declaration + call); the seven other named restore/paging symbols → reader source only; session-transcript-reader.test.ts for 'managed' → 0 matches.

Suggested fix: Add a core test that opens a real Managed session via openManagedSession, writes user/assistant/title records through the sink, closes, then drives SessionTranscriptReader (getSelectiveRestoreProjection, readLiveRestoreProjection, navigation/page reads) against it and asserts the projected records — not wrapper envelopes — come back, including one assertion per accumulator path buildManagedSessionRestoreProjection wires (api history, last-assistant model, custom title injection, replay page suffix).

The fix must not violate: The test must not let a projected index enter the index cache — offerFreshIndexToCache skips indexes with projectedRecords — and the first-record authorization gate must stay outside the managedProjections WeakMap cache per its comment; the cache-exclusion branch is itself currently untested and should be pinned by the same test.

Acceptance criterion: That new test itself: it must go red if indexHasManagedHeader is made to return false (legacy index path selected) — today nothing in the repo fails under that mutation. Please remove the fix and confirm that test goes red before landing.

— glm-5.3-flash via Qwen Code /review (v0.24.6)

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.

[Suggestion] R1-3 (still stands, re-verified 2026-10-03): The entire Managed read path of SessionTranscriptReader (restore, live restore, navigation turns, paging) still has zero direct test coverage — session-transcript-reader.test.ts has no reference to readManagedRestoreProjection / indexHasManagedHeader (grep-verified round 2). A same-claim round-2 candidate (R2-26) was confirmed and dropped as a duplicate of this entry. Head unchanged since round 1.

中文说明

与 R1 一致:SessionTranscriptReader 的整条 Managed 读路径仍零测试覆盖(本轮 grep 复核 0 命中);本轮重复候选 R2-26 已按本条目吸收。

— kimi-k3@fda99a87 via Qwen Code /review (v0.24.0)

Comment on lines +3510 to +3511
* Record shapes the sink does not yet admit -- goals, artifacts, file history,
* session source and session model -- cannot appear in a Managed log at all,

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.

[Suggestion] R1-18: readManagedRestoreProjection's invariant comment is factually wrong for three of the five shapes it names: the sink admits and routes goal_state (managed-session-record-sink.ts:87 → commitGoalState), file_history_snapshot (:86 → commitFileHistory), and session_source (:88 → commitSessionSource) into enabled domains today, and the projection feeds their domain.committed bodies back into buildManagedSessionRestoreProjection (RECORD_CARRYING_DOMAINS).

Failure scenario: A maintainer trusting 'absent by construction' skips exactly the defensive handling those shapes need — the probe for R1-10 demonstrated a Managed log containing a file-history record flowing into this projection, so the comment documents an invariant the code does not have.

中文说明

readManagedRestoreProjection 的注释声称 goals/file history/session source 不可能出现在 Managed 日志中,但 sink 今天就接受并路由这三种形状(探测已复现),注释记录的是代码没有的不变量。

Witness (verification evidence)
[probe] the R1-17 probe's cold read returned goal_state and file_history_snapshot records from a sink-written Managed log — shapes the comment claims 'cannot appear in a Managed log at all'; canCarry admits goal_state (:85), file_history_snapshot (:86), session_source (:87).

Suggested fix: Rewrite the comment to name only the shapes actually unadmitted (artifacts, session model), and point at the enabled-domain registry as the source of truth for the rest.

Acceptance criterion: N/A (comment correction; the enabling fact is pinned by the sink's own carry tests). Please remove the fix and confirm that test goes red before landing.

— glm-5.3-flash via Qwen Code /review (v0.24.6)

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.

[Suggestion] R1-18 (still stands, re-verified 2026-10-03): readManagedRestoreProjection's invariant comment (:3510) remains factually wrong for three of the five shapes it names — the sink admits and routes goal_state and file_history_snapshot, so the comment's serialization claim is stale. Head unchanged since round 1.

中文说明

与 R1 一致:不变式注释对五种形状中的三种仍与实际行为不符。

— kimi-k3@fda99a87 via Qwen Code /review (v0.24.0)

Comment on lines +335 to +338
lockSchema?: {
readonly schemaVersion: 3;
readonly formatVersion: number;
};

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.

[Suggestion] R1-27: lockSchema.formatVersion is never validated at the acquire boundary: acquireInternal copies it verbatim into the active record (format_version: lockSchema.formatVersion) with no isManagedFormatVersion check, so a caller-supplied out-of-contract value (0, negative, non-integer — all type-legal) produces a record every subsequent reader classifies as malformed, and every future acquire on that session fails SessionWriterUnavailableError('Existing session writer lock is malformed') until the lock file is manually deleted.

Failure scenario: Fail-closed, and no in-repo caller can trigger it today (both call sites pass MANAGED_SESSION_FORMAT_VERSION = 1); the same one-comparison hardening as R1-25's fix would close it.

中文说明

acquire 边界不校验 lockSchema.formatVersion:一个非法值(如 0)会在单次调用内把会话永久楔死——坏锁文件落盘后,后续所有 acquire(含 baseline 与 certified takeover)都报 lock malformed。建议在构造锁记录前用 isManagedFormatVersion 校验。

Witness (verification evidence)
[probe] intact PR, first acquire with formatVersion 0: throws SessionWriterUnavailableError and leaves the bad lock file behind (format_version: 0, state: active); every re-acquire — same schema, baseline, certified takeover — then throws 'Existing session writer lock is malformed' (session wedged in one call). Flip with the acquire-boundary isManagedFormatVersion check: attributable error 'Managed lock schema formatVersion 0 is not a supported log format', lock file ENOENT, baseline re-acquire resolves.

Suggested fix: Validate lockSchema.formatVersion with isManagedFormatVersion at the acquire boundary before constructing the lock record.

Acceptance criterion: N/A (defense-in-depth hardening on an unreachable-today path; the failure mode is itself fail-closed). Please remove the fix and confirm that test goes red before landing.

— glm-5.3-flash via Qwen Code /review (v0.24.6)

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.

[Suggestion] R1-27 (still stands, re-verified 2026-10-03): lockSchema.formatVersion is still copied verbatim into the active record at acquire (format_version: lockSchema.formatVersion) with no isManagedFormatVersion check — a malicious or buggy caller can wedge a session with one call, all subsequent acquires reading 'lock malformed'. Head unchanged since round 1.

中文说明

与 R1 一致:acquire 边界仍不校验 formatVersion,非法值一次调用即可楔死该会话。

— kimi-k3@fda99a87 via Qwen Code /review (v0.24.0)

Comment on lines +536 to +539
Number.isSafeInteger(record['last_commit_sequence']) &&
(record['last_commit_sequence'] as number) >= 0 &&
typeof record['committed_prefix_hash'] === 'string' &&
/^[0-9a-f]{64}$/.test(record['committed_prefix_hash'])

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.

[Suggestion] R1-11: The managed-sealed branch of isLockRecord re-implements, inline, exactly the validation isValidCommitProof (session-writer-lease.ts:574-581) performs on the same two persisted fields — the same safe-integer/>= 0 check on last_commit_sequence and the same /^[0-9a-f]{64}$/ check on committed_prefix_hash.

Failure scenario: These two validators sit at opposite ends of the seal→takeover round trip: isValidCommitProof gates what sealForHandoff accepts from the authority, the inline copy gates what a later acquire will parse from the sealed lock. They can drift silently — e.g. widening the hash format in one copy but not the other makes every newly sealed schema-3 lock parse as malformed on the next acquire, permanently fencing the session behind a SessionWriterUnavailableError. Today they are textually identical, so nothing is broken; the cost is the drift seed on a barrier mechanism whose whole purpose is exactness.

中文说明

isLockRecord 的 managed-sealed 分支内联重复了 isValidCommitProof 的两处校验;两端格式一旦漂移,新 seal 的锁会在下次 acquire 时被判为损坏、会话被永久隔离。建议直接复用 isValidCommitProof。

Witness (verification evidence)
[probe] applied exactly the suggested substitution (isValidCommitProof(record)) in the scratch tree; schema-3 suite: Tests 7 passed | 109 skipped — every 'managed lock schema 3' case passes unchanged; restored byte-identical.

Suggested fix: In the MANAGED_LOCK_SCHEMA_VERSION sealed branch, replace the four inline proof checks with isValidCommitProof(record) (function declarations hoist; record is Record<string, unknown>, which the unknown parameter accepts).

The fix must not violate: The substituted predicate must keep requiring a safe integer >= 0 for last_commit_sequence and /^[0-9a-f]{64}$/ for committed_prefix_hash — that is exactly the body of isValidCommitProof at session-writer-lease.ts:574-581, so the replacement is semantics-preserving only while that function is unchanged.

Acceptance criterion: N/A (behavior-preserving dedup — the existing 'managed lock schema 3' tests already pin the accepted record shapes both ways). Please remove the fix and confirm that test goes red before landing.

— glm-5.3-flash via Qwen Code /review (v0.24.6)

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.

[Suggestion] R1-11 (still stands, re-verified 2026-10-03): The managed-sealed branch of isLockRecord still re-implements inline exactly the validation isValidCommitProof (:574-581) performs on the same two persisted fields — duplicated logic stays prone to asymmetric drift. Head unchanged since round 1.

中文说明

与 R1 一致:sealed 分支仍内联复刻 isValidCommitProof 的校验逻辑,未抽共用。

— kimi-k3@fda99a87 via Qwen Code /review (v0.24.0)

Comment on lines +2081 to +2084
if (
isManagedLockRecord(observed.record) &&
options.lockSchema === undefined
) {

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.

[Suggestion] R1-25: The managed write barrier is one-directional: SessionWriterLease.takeOverSealed rejects a sealed managed lock only when options.lockSchema === undefined, and isManagedFormatVersion accepts any safe integer >= 1 — no site ever compares the acquirer's lockSchema.formatVersion against the sealed record's format_version, so an older binary can take over a NEWER managed seal and downgrade the lock record to its own older format.

Failure scenario: After a future release bumps MANAGED_SESSION_FORMAT_VERSION (mixed-version fleet — the exact downgrade scenario the barrier documents): the old binary's open() takes over the format-N+1 sealed lock (assertSealedProofMatches checks only the transcript byte proof, not format), installs an active lock with format_version: N; the authority scan then fails on the unreadable log and the assembly's error path calls journal.abort() → lease.release(), deleting the lock while takeOverSealed has already removed the retired .sealed. record — the log is left with NO barrier, so a plain legacy SessionWriterLease.acquire can append legacy records to a Managed transcript. Mechanism verified end-to-end at this commit; the trigger requires a future counterpart format version (only one exists today).

中文说明

managed 写屏障是单向的:takeOverSealed 只检查 lockSchema 是否存在,从不比较 format_version;探测复现了旧版本接管新格式 seal、随后 abort 删除锁文件、裸 legacy 租约追加成功的完整链条。

Witness (verification evidence)
[probe] unmodified PR: new binary seals at format 2, old binary takes over — 'PROBE takeover ACCEPTED'; after journal.abort the lock file is absent (ENOENT); a legacy acquire+append SUCCEEDED (transcript now holds sealed-by-new-binary, managed-v2, legacy-appended). Flip with the suggested format comparison: 'PROBE takeover rejected: SessionWriterUnavailableError', sealed lock intact. (Refinement: in-repo legacy writers still refuse on content, so the demonstrated harm is the at-rest seal barrier deleted, strictly worse than fail-closed.)

Suggested fix: In takeOverSealed, additionally reject when options.lockSchema !== undefined && options.lockSchema.formatVersion < observed.record.format_version (use >= semantics so a newer binary can still continue an older log — plain equality would break forward takeover).

The fix must not violate: isManagedFormatVersion (session-writer-lease.ts:563-565) accepts any safe integer >= 1, so the fix must compare values, not gate on presence; and the acquire options type pins readonly schemaVersion: 3, which both callers hardcode — do not touch the schema gate, only add the format comparison.

Acceptance criterion: session-writer-lease.test.ts, next to the managed-seal takeover cases at L2962-2990: seal a lock with format_version: 2 and assert an acquirer with lockSchema: { schemaVersion: 3, formatVersion: 1 } is rejected — goes red if the new guard is removed. Please remove the fix and confirm that test goes red before landing.

— glm-5.3-flash via Qwen Code /review (v0.24.6)

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.

[Suggestion] R1-25 (still stands, re-verified 2026-10-03): The managed write barrier remains one-directional — takeOverSealed rejects a sealed managed lock only when options.lockSchema === undefined, and isManagedFormatVersion accepts any safe integer >= 1 with no comparison against the expected schema/floor — presence-only gating intact. Head unchanged since round 1.

中文说明

与 R1 一致:sealed 接管屏障仍是单向存在性判断,无格式版本比较。

— kimi-k3@fda99a87 via Qwen Code /review (v0.24.0)

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] Blocking finding(s) follow.

Partially reviewed — gaps disclosed. Suggestions are inline.

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

  • R2-5 (http-managed-session-store.ts:881 — duplicate of R1-24, same collectRefs duck-typing mechanism; R1-24 re-confirmed still-standing)
  • R2-26 (session-transcript-reader.ts:3072 — duplicate of R1-3, same zero-coverage claim on the Managed read path; R1-3 re-confirmed still-standing)

3 candidate finding(s) this round's reviewers re-derived matched entries already carried on this PR and were set aside before verification (R1-26, R1-5, R1-4) — a matched posted finding is ruled in the previous-round status as always, and a matched deferral stays on the standing deferral record.

Not reviewed: verify — all 13 verifier agents failed to deliver across two waves (0/4 round-1 shards, 0/9 round-2 shards); candidate verdicts below were ruled by the review driver from evidence packs, with per-finding witness text citing code regions and greps.

Not reviewed: reverse-audit — convergence not reached: round 3 delivered receipts for 43/61 chunks (never delivered: chunks 2, 3, 20, 30, 31, 32, 33, 34, 36, 44, 45, 48, 49, 50, 55, 57, 58, 60); no two consecutive fully-dry rounds over the 61-chunk diff.

Not reviewed: candidates — 92 later-surfaced candidates (R2-29..R2-120) were never verified and are not posted.

Not reviewed: test-execution — R2-28 environmental-leak causation verified statically only; execution blocked after an external kill partially deleted the review worktree.

Not reviewed: "agent 0" — pointed at diff lines it never opened: it made tool calls, but none of them read the diff.

Not reviewed: verification and reverse audit — both prompts were built, but no agent was launched with either — the posted findings cannot be counted as verified, and the pass that hunts what the rest of the review missed cannot be certified.

⚠️ 11 finding(s) were deferred without a posture licence — the operator turned the posture off (--severity-floor suggestion). They are listed in this body when it has room for them, and always in the terminal report and this run's findings artifact; this verdict is capped either way: findings may be under-posted this round.

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

  • packages/core/src/services/session-transcript-reader.ts:305 — [review] R2-11: executionEngine projection fields written by both engines, read by nothing (verified)
  • packages/core/src/managed-runtime/http-managed-session-store.ts:362 — [review] R2-12: Malformed baseUrl escapes as raw TypeError instead of ManagedSessionRecordError (verified)
  • packages/cli/src/ui/hooks/useResumeCommand.managed.test.ts:128 — [review] R2-13: Managed-reject test never asserts the telemetry swap slot is closed (verified)
  • packages/core/src/managed-runtime/managed-activation-store.test.ts:215 — [review] R2-14: cancelQueued (zero callers repo-wide) and activation.cancelled have zero test coverage (verified, stronger than claimed)
  • packages/core/src/managed-runtime/managed-activation-store.ts:265 — [review] R2-15: Released activations retained for the process lifetime — no eviction path (verified)
  • packages/core/src/managed-runtime/managed-runtime-dispatch-gate.ts:58 — [review] R2-16: handed_off/settled invocation entries and the gates map never evicted (verified in substance; pre-handoff entries do evict)
  • packages/core/src/managed-runtime/managed-session-assembly.ts:33 — [review] R2-17: startActivationRenewal timer has no test (verified)
  • packages/core/src/managed-runtime/managed-session-authority.ts:494 — [review] R2-18: open() fails on any checkpointed log without a resource store, contradicting the options doc at :287 (verified; reachability by an actual resource-less cal…
  • packages/core/src/managed-runtime/managed-session-authority.ts:730 — [review] R2-19: requestToolAction idempotent replay returns a stale action without kind/inputRevision/optionsRef comparison (verified)
  • packages/core/src/services/session-writer-lease.ts:1820 — [review] R2-24: Raw lstatSync errors escape untyped from acquireSealedManagedMaintenance — anchor corrected to the :1820-1821 read and the :1880-1883 raw rethrow (verified structural…
  • packages/core/src/utils/sessionStorageUtils.ts:843 — [review] R2-25: lastCommittedDomainLine's unguarded JSON.parse aborts the whole window scan on a malformed interior line (verified)

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

[Critical] packages/core/src/utils/sessionStorageUtils.ts:904 — [review] Critical [new-surface] R2-1: Quoting-detector misidentifies a legacy transcript that merely quotes the Managed marker as a Managed session (verified by evidence pack) (relocated from the deferral channel — a Critical is deferred only as fails-closed on new surface at a critical floor; this one posts)

[Critical] packages/core/src/utils/sessionStorageUtils.ts:902 — [review] Critical [new-surface] R2-2: Engine-only Managed genesis transcript not recognized as managed by the legacy guards (verified) (relocated from the deferral channel — a Critical is deferred only as fails-closed on new surface at a critical floor; this one posts)

[Critical] packages/core/src/managed-runtime/http-managed-session-store.ts:157 — [review] Critical [new-surface] R2-3: HTTP journal handle abort() seals the journal (posts /writers:seal) instead of releasing the writer; server has no release route (ve… (relocated from the deferral channel — a Critical is deferred only as fails-closed on new surface at a critical floor; this one posts)

[Critical] packages/core/src/managed-runtime/http-managed-session-store.ts:465 — [review] Critical [new-surface] R2-4: readJournal silently re-baselines this writer's grant from a regressed server head (verified code-level; trigger needs a server-side… (relocated from the deferral channel — a Critical is deferred only as fails-closed on new surface at a critical floor; this one posts)

[Critical] packages/core/src/managed-runtime/managed-session-authority.ts:1504 — [review] Critical [new-surface] R2-6: A renewal committed after release revives the activation at the same epoch — phase guard outside the serial queue (verified race) (relocated from the deferral channel — a Critical is deferred only as fails-closed on new surface at a critical floor; this one posts)

中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

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

本轮评审重新推导出的 3 条候选发现与本 PR 已携带的条目匹配,已在验证前搁置(R1-26, R1-5, R1-4)——被匹配的已发布条目照常在上一轮状态区裁定,被匹配的延后条目仍保留在延后清单记录中。

未审查(原文为英文):verify — all 13 verifier agents failed to deliver across two waves (0/4 round-1 shards, 0/9 round-2 shards); candidate verdicts below were ruled by the review driver from evidence packs, with per-finding witness text citing code regions and greps.

未审查(原文为英文):reverse-audit — convergence not reached: round 3 delivered receipts for 43/61 chunks (never delivered: chunks 2, 3, 20, 30, 31, 32, 33, 34, 36, 44, 45, 48, 49, 50, 55, 57, 58, 60); no two consecutive fully-dry rounds over the 61-chunk diff.

未审查(原文为英文):candidates — 92 later-surfaced candidates (R2-29..R2-120) were never verified and are not posted.

未审查(原文为英文):test-execution — R2-28 environmental-leak causation verified statically only; execution blocked after an external kill partially deleted the review worktree.

未审查:"agent 0"——启动 prompt 为它指定了 diff 中的行,但它从未打开:有工具调用,却没有一次读取 diff。

未审查:验证与反向审计——两份 prompt 都已构建,但都没有 agent 用它们启动——发布的发现不能算作已验证,搜寻评审遗漏问题的工序也无法作证。

⚠️ 11 条发现在姿态未授权的情况下被延后——the operator turned the posture off (--severity-floor suggestion)。正文空间允许时会列出清单,完整内容始终在终端报告与本次运行的 findings 工件中;无论如何本判定已被限制:本轮发现可能未被完整发布。

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

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

[Critical] packages/core/src/utils/sessionStorageUtils.ts:904 — [review] Critical [new-surface] R2-1: Quoting-detector misidentifies a legacy transcript that merely quotes the Managed marker as a Managed session (verified by evidence pack) (relocated from the deferral channel — a Critical is deferred only as fails-closed on new surface at a critical floor; this one posts)

[Critical] packages/core/src/utils/sessionStorageUtils.ts:902 — [review] Critical [new-surface] R2-2: Engine-only Managed genesis transcript not recognized as managed by the legacy guards (verified) (relocated from the deferral channel — a Critical is deferred only as fails-closed on new surface at a critical floor; this one posts)

[Critical] packages/core/src/managed-runtime/http-managed-session-store.ts:157 — [review] Critical [new-surface] R2-3: HTTP journal handle abort() seals the journal (posts /writers:seal) instead of releasing the writer; server has no release route (ve… (relocated from the deferral channel — a Critical is deferred only as fails-closed on new surface at a critical floor; this one posts)

[Critical] packages/core/src/managed-runtime/http-managed-session-store.ts:465 — [review] Critical [new-surface] R2-4: readJournal silently re-baselines this writer's grant from a regressed server head (verified code-level; trigger needs a server-side… (relocated from the deferral channel — a Critical is deferred only as fails-closed on new surface at a critical floor; this one posts)

[Critical] packages/core/src/managed-runtime/managed-session-authority.ts:1504 — [review] Critical [new-surface] R2-6: A renewal committed after release revives the activation at the same epoch — phase guard outside the serial queue (verified race) (relocated from the deferral channel — a Critical is deferred only as fails-closed on new surface at a critical floor; this one posts)

— kimi-k3@fda99a87 via Qwen Code /review (v0.24.0)

case 'message.committed':
return { ref: event.payload['contentRef'], inDomainEnvelope: false };
case 'turn.settled':
return { ref: event.payload['resultRef'], inDomainEnvelope: false };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R2-7: The cold projection dereferences a possibly-null turn.settled resultRef: the event schema declares resultRef: 'refOrNull', but readerFacingBody (:300-318) forwards event.payload['resultRef'] into the resource read chain with no null guard, so a schema-legal null crashes the restore with a raw TypeError instead of a ManagedSessionRecordError naming the event.

Failure scenario: A Managed log whose turn.settled persists resultRef: null hard-fails cold restore outside the typed error taxonomy — fail-loud-late. Latent today: no in-tree producer emits null.

中文说明

冷投影未防空的 turn.settled.resultRef:schema 允许 ’refOrNull’,而 readerFacingBody 直接拿去读资源,遇到 null 时抛出的是裸 TypeError 而非可分类的 ManagedSessionRecordError。属潜伏缺陷(当前没有生产端写 null),但恢复路径一旦遇到就会以非类型化错误崩溃。建议:在 readerFacingBody 的 turn.settled(及 message.committed / context.compacted 兄弟分支)加空值守卫,或以带事件语境的 ManagedSessionRecordError 提前失败。

Acceptance criterion: with the guard in place, a synthetic log persisting resultRef: null restores as a typed ManagedSessionRecordError (or is skipped per the warn path) instead of a raw TypeError. Please remove the fix and confirm that test goes red before landing.

— kimi-k3@fda99a87 via Qwen Code /review (v0.24.0)

turnId: record.uuid,
replayPosition: position,
kind: navigationKind,
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R2-8: managedNavigationTurns (:2706-2724) omits promptId entirely and assigns turns with overwrite = — the physical index builder fills promptId from daemonPromptId and uses first-wins ??=. The function's own comment (:2700-2705) claims the rules mirror the index builder's; they do not, on both content and precedence.

Failure scenario: Managed restore consumers reading TranscriptNavigationTurnHint[] cannot associate turns with prompt ids, and duplicate-turn resolution collapses differently for Managed sessions than for legacy ones — the same session navigates/paginates differently depending on which engine built its index.

中文说明

managedNavigationTurns 既不带 promptId,也用覆盖式 = 赋值;物理路径用 daemonPromptId 填 promptId 且首赢 ??=。函数注释自称镜像 index builder,但内容与优先级语义都不一致。建议:在 managedNavigationTurns 中填 promptId 并改用 ??=,与物理 builder 完全对齐。

Acceptance criterion: a Managed restore's navigation hints match the physical builder's (promptId present, first-wins on duplicates) for the same replayed records. Please remove the fix and confirm that test goes red before landing.

— kimi-k3@fda99a87 via Qwen Code /review (v0.24.0)

@@ -2397,7 +2546,74 @@ async function hasSnapshotSignature(
);
}

export async function readSessionTranscriptSnapshot(
filePath: string,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R2-10: readSessionTranscriptSnapshot is exported with zero production callers (its only caller repo-wide is managed-session-authority.test.ts:1383 — grep-verified); buildManagedSessionRestoreProjection is likewise exported beyond its single internal call at :3530. New public surface over the exact restore invariant whose doc contract round 1 showed was wrong for three of five shapes (R1-18).

Failure scenario: None today — the export widens audited surface the project has not committed to; any future caller builds on a contract that is documented incorrectly today.

中文说明

两个恢复相关函数被导出但生产代码零调用(仅测试),且所依赖的 restore 投影契约注释已被本轮确认是错的(见 R1-18)。建议:去掉导出或标 @internal,避免未来调用方建立在错误契约之上。

Suggested fix: Drop the exports or mark @internal unless a consumer is about to land.

— kimi-k3@fda99a87 via Qwen Code /review (v0.24.0)

await harness.close();
});

it('refuses a turn result with no prompt id or state', async () => {

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.

[Suggestion] R2-21: The turn_result rejection guard (sink.ts:286-293) has four disjuncts, but its only rejection test (:476-487) exercises just the missing-promptId shape — turnId === '', outcome === '', and non-string outcome/turnId are unexercised anywhere (grep-verified: zero shapes across the file). The test's name ('refuses a turn result with no prompt id or state') overstates its coverage.

Failure scenario: A regression narrowing the guard to promptId-only goes green silently.

中文说明

turn_result 拒绝守卫有四个析取分支,当前拒绝用例只覆盖「缺 promptId」这一种;空 turnId、空 outcome、非字符串类型三个分支全库零覆盖(已 grep 证实)。建议按四个析取分支参数化补齐。

Suggested fix: Table-drive the four guard disjuncts into explicit cases.

— kimi-k3@fda99a87 via Qwen Code /review (v0.24.0)

}

describe('managed session record sink', () => {
it('carries the record shapes the projection can reproduce', async () => {

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.

[Suggestion] R2-22: session_source and file_history_snapshot success write paths, plus carried system subtypes' write/projection round-trips, have no sink-level tests: zero session_source hits in the suite (grep-verified), file_history_snapshot appears only in the unmapped-rejection case, and carried subtypes are mostly exercised as canCarry booleans only.

Failure scenario: A success-path commit of a malformed session_source / file_history_snapshot record (or a carried subtype the projection cannot reproduce) slips through until cold restore breaks on an integration machine.

中文说明

session_source / file_history_snapshot 的成功写入路径及随载子类型的写入-投影闭环在 sink 层零覆盖(session_source 全库 0 命中;file_history_snapshot 仅出现于未映射拒绝用例)。建议为两者及各携带子类型补 sink 级成功写入与投影闭环。

Suggested fix: Add sink-level success write/projection round trips for session_source, file_history_snapshot, and each carried system subtype.

— kimi-k3@fda99a87 via Qwen Code /review (v0.24.0)

throw new ManagedSessionUnmappedRecordError(record);
}
const fromSequence = this.authority.compactedThroughSequence + 1;
const toSequence = this.authority.committedSequence;

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.

[Suggestion] R2-23: commitContextCompacted captures fromSequence/toSequence (:392-393) before the resources.publish suspension (:397-400 — outside runSerial), then commits sequence: toSequence + 1 (:413). Sibling paths either derive sequence at commit time or publish inside runSerial; a concurrent commit landing in the publish window leaves the compaction's claimed range stale.

Failure scenario: A concurrent sink write during the publish window makes commit() fail closed with ManagedSessionConflictError on the stale sequence (impact is rejection, not corruption); whether sink.write is concurrently re-entered today is unverified — line-level mechanics verified.

中文说明

commitContextCompacted 在 publish 挂起之前就捕获了 toSequence,而在捕获与提交之间若有并发提交落入 publish 窗口,提交出的 compaction 序列范围即已过期,最终以 ManagedSessionConflictError 被拒(fail-closed,非损坏)。兄弟路径要么在提交时取值、要么把 publish 放进 runSerial。是否真有并发调用未证实,行级机制已核实。建议:提交时取序列,或将 publish 移入 runSerial。

Suggested fix: Derive the sequence at commit time like appendExecutionEvent does, or move the publish inside runSerial.

— kimi-k3@fda99a87 via Qwen Code /review (v0.24.0)

expect(addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: 'error',
text: expect.stringContaining('belongs to managed'),

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.

[Suggestion] R2-28: The managed-resume rejection test is not environment-hermetic: nothing in its setup clears QWEN_RUNTIME_DIR, while Storage.getRuntimeBaseDir() prioritizes that ambient variable over setRuntimeBaseDir (priority pinned by storage.test.ts:125) and the hook constructs SessionService(cwd) inheriting it. Run with an ambient QWEN_RUNTIME_DIR, the 'belongs to managed' assertion is the sole red one — causation verified statically (not re-executed: the review worktree was damaged externally).

Failure scenario: Any developer machine or lane leaking QWEN_RUNTIME_DIR goes red on an otherwise-green tree, discrediting the rejection the PR tests for. Note: reddening of the PR's own CI lanes is NOT attributed — CI environment unknown.

中文说明

managed resume 拒绝测试不清理/不隔离 QWEN_RUNTIME_DIR,而 Storage.getRuntimeBaseDir() 对该环境变量的优先级高于 setRuntimeBaseDir(storage.test.ts:125 已钉死该优先级),hook 经 SessionService(cwd) 继承环境变量后把读路径引离测试临时 runtime,出现‘belongs to managed’断言单独红。机制已在源码层确认;本机未复跑(评审 worktree 被外部破坏);未断言 CI 是否因此红。建议:在 setup 清空该变量(看齐 core 侧兄弟测试),并补失败隔离断言。

Suggested fix: Clear or sandbox QWEN_RUNTIME_DIR in the test setup, mirroring the sibling core tests.

— kimi-k3@fda99a87 via Qwen Code /review (v0.24.0)

ShadyAV pushed a commit to ShadyAV/qwen-code that referenced this pull request Oct 5, 2026
QwenLM#13341)

* test(core): close QwenLM#12693 post-merge review test and hygiene gaps

Second batch of QwenLM#12693 post-merge review follow-ups — the test-coverage and
hygiene findings that remained after the correctness PR:

- The HTTP store suite never drove a failing or inconsistent server: the fake
  now takes page/receipt overrides and stored-integrity edits, pinning the
  multi-page read, contiguous-revision, bytes-vs-metadata, empty-page,
  nextRevision, echo-receipt mismatch and 409 resource-missing paths.
- A sealed Managed session can now be seen to survive daemon archive through
  the sealed-writer fallback (red if that arm is removed; mutation-checked).
- The managed-resume rejection test stubs QWEN_RUNTIME_DIR — ambient exports
  outrank setRuntimeBaseDir and redirected the hook to the ambient runtime.
- sink success-path coverage: session_source and file_history_snapshot domain
  round-trips, carried system subtypes through the message channel, and the
  remaining three turn_result guard disjuncts; the unmapped-refusal fixture
  now uses a genuinely unmapped subtype (rewind instead of the mapped
  file_history_snapshot).
- The live-vs-cold projection difference is documented at both sites and
  pinned by a test asserting the deliberate narrower live door.
- parseLine returns the validation it already ran, so transcript indexing no
  longer validates every record twice; the accumulator's four unavailability
  reasons and the empty-transcript default gain a collocated suite.
- isLockRecord's managed-sealed branch reuses isValidCommitProof instead of
  re-implementing it; the misplaced recoverUncommittedTail safety contract
  now sits on the method that truncates; the unused de-facto-private export
  of buildManagedSessionRestoreProjection is dropped.

* test(core): cover the restore head mismatch and align the 409 comment

- The journal-vs-durable-head check now has a witness: a server head that
  overstates committedSequence is refused after the page loop.
- The staged-survival assertion's comment no longer overclaims a literal
  retry of the same missing resource (review R1-3).

* test(core): address the round-1 review on the test batch

Addressing the seven Suggestion findings from the auto-review, each with the
mutation witness the review asked for:

- The restore-journal head guards now have witnesses too, via headOverrides:
  a storageVersion/recoveryStatus mismatch is refused by the grant check, an
  overstated committedSequence fails journal-vs-head, a stale eventsDigest
  fails metadata-vs-records, and a page that promises the end while the head
  is ahead is refused (the 'did not advance' branch was already pinned).
- The 409 test drives a real retry of the same transaction with a staged ref
  it carries, asserting the retried commit body still posts bytesBase64 —
  exactly the regression that would appear if a failure path ever released
  staged entries.
- SessionExecutionEngineAccumulator.parseLine's returned
  `{ value, record }` pair is pinned independently on both halves, proving
  the raw line value survives where the normalized record drops fields.
- The hot/wide projection distinction is documented as width, not lifecycle —
  `projectManagedSessionRecords` is named as the reader-facing list used on
  live paths too (`readActiveTranscriptChain`) — and the pin test's title
  and inline comment say the same.
- `turn_result` case rejection now asserts both halves of the
  canCarry/write agreement on the same record object.
- Carried system subtypes assert their message channel (one
  message.committed, zero domain.committed) before the cold round-trip.
- QWEN_RUNTIME_DIR is deleted once in each package's setup file (preload of
  every suite) instead of two per-test stubs, closing the class: 15 ambient
  failures in session-transcript-reader and 4 ACP lock failures on
  writer-lease reproduce on ambient machines and now pass.

* test(core): address the round-2 review on the test batch

- pin both halves of the parseLine pair: the record half now asserts the
  normalized message (so a raw-as-record substitution goes red), and a new
  case keeps one slot per physical record that fails validation
- witness the non-object owner-payload disjunct that stands between a
  primitive systemPayload on disk and a TypeError from the 'in' operator
- widen the restore-head grant table to all five disjuncts and the
  journal-vs-head agreement test to all three, via disjoint headOverrides
- make the fake store honour the requested page limit so the two-page
  count derives from production's limit=100 request
- merge resolution: opt the fake-host store constructions into
  allowInsecureHttp after main's plaintext-HTTP guard (QwenLM#13210)

Each new row was mutation-probed: it goes red when its own disjunct (or
the limit) is removed and stays green on pristine source.

Co-authored-by: Qwen-Coder <[email protected]>

* test(cli): scope fake renderer scripts as CommonJS under a poisoned tmpdir

Deterministic verification rejected the round-2 commit: 15 packages/cli
tests in mermaidImageRenderer, terminal-image-renderer and test-efficacy
failed with renderer results of kind 'unavailable'. The fake mmdc/chafa/git
executables those suites write under os.tmpdir() are extensionless CommonJS
scripts, and the runner's /tmp/package.json carries "type": "module", so
node executed them as ES modules and they crashed on require/__dirname
('require is not defined in ES module scope'), which the renderers then
report as chafa being unavailable.

Write a {"type":"commonjs"} package.json next to each fake so the
nearest-scope lookup pins the module system regardless of what sits above
the host's temp directory. Verified both ways: the three files are green
with the poisoning in place and stay green with TMPDIR pointed at a clean
directory.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: qwen-code-ci-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
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.

6 participants