Skip to content

fix(cli): seed headless promptIds from the resumed transcript - #11441

Merged
yiliang114 merged 4 commits into
mainfrom
fix/11408-headless-resume-prompt-id
Sep 9, 2026
Merged

yiliang114 merged 4 commits into
mainfrom
fix/11408-headless-resume-prompt-id

Conversation

@yiliang114

@yiliang114 yiliang114 commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Headless runs now continue prompt numbering from the transcript they resumed instead of restarting it in every process. Both headless entry points are covered: the stream-json session, whose counter started over at the first turn, and the single-shot -p run, which always minted turn 0. Both are seeded from the resumed transcript using the core helper that already backs the same seeding in interactive mode and in ACP: the highest turn the transcript claims, falling back to the number of resumed user turns. A run that resumes nothing keeps the exact ids it mints today.

Why it's needed

--resume and --continue reuse the previous session's id, so a headless chain that restarts numbering re-mints prompt ids the previous run already persisted, and one transcript ends up carrying several turns under a single prompt id. That id is the key the rewind mapping added in #9466 anchors on — repeated ids make that lookup fail closed to the positional walk — and it is the prompt_id persisted on ui_telemetry records, which is itself what the next resume reads back to seed from, so the ambiguity compounds along the chain. Interactive mode and ACP already seed their counters on resume; only the two headless paths were missing it, which is the deferred review finding tracked in #11408.

Correction to an earlier version of this description, from the sandboxed /verify run: file-history snapshots are not dropped on these paths. The de-dupe is real (SessionFileHistoryAccumulator keeps the last snapshot per prompt id), but fileCheckpointingEnabled defaults to !sdkMode && interactive and nothing in packages/cli overrides it, so makeSnapshot no-ops and headless turns write no snapshots at all — measured in the sandbox as 10 headless processes, 6 real write_file executions, 0 snapshot records. That is why interactive mode has always seeded, and it is a cost this PR does not remove on the headless paths.

Reviewer Test Plan

How to verify

Unit tests cover both paths and are the intended reviewer evidence: npx vitest run src/nonInteractive/session.test.ts src/llm.test.tsx in packages/cli. The stream-json cases assert that a session with nothing resumed still starts at turn 1, that a resumed transcript whose highest claimed turn is 5 makes the next turn 6, and that a transcript with no persisted prompt id falls back to counting resumed user turns. The -p case asserts that a resumed run continues past the last claimed turn while the existing fresh-session expectation of turn 0 is unchanged.

End to end, the oracle is the prompt ids persisted in the session transcript (<runtime>/projects/<cwd>/chats/<sid>.jsonl), read back after each process exits — not file-history snapshots, which headless never writes. Run -p once with --session-id, then -p --resume <id>, and compare: before this change the transcript holds one turn number for both processes, after it one per process. The sandboxed /verify run drove exactly that against real built base and head artifacts: -p went [0] → [0] on base versus [0] → [0,2] on head, and stream-json over three processes went [1] → [1] → [1] versus [1] → [1,2] → [1,2,3].

Evidence (Before & After)

N/A — no user-visible or TUI change; the difference is in persisted prompt ids and the snapshots keyed by them.

Tested on

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

Not verified locally on any platform — the machine this was written on cannot run the build or test suite, so CI is the verification for lint, typecheck and unit tests.

Environment (optional)

N/A — unit tests only.

Risk & Scope

  • Main risk or tradeoff: a resumed -p run changes the single prompt id it emits (previously always turn 0), which shifts the id seen by telemetry and by any consumer that keyed on turn 0 for resumed headless runs; fresh runs are byte-identical to today.
  • Not validated / out of scope: the ACP and interactive paths are untouched because they already seed, and --continue, --fork-session and resumed-from-interactive chains were not driven (the sandbox exercised --session-id--resume only). /rewind itself and refactor: anchor rewind mapping to stable prompt identity #9466's identity mapping were not driven either — the sandbox verified that ids repeat, not what the mapping then does with them.
  • Breaking changes / migration notes: none; no persisted format or public API changes, and createNonInteractivePromptId's new parameter is optional.

Linked Issues

Closes #11408 — the single deferred finding it tracks (ic:5582642849, from #9466).

中文说明

这个 PR 做了什么

Headless 运行现在会从恢复的会话记录中续接 prompt 编号,而不是每个进程都重新开始。两条 headless 入口都覆盖到了:stream-json 会话(计数器每次都从第一轮重新开始)和单次 -p 运行(永远铸造第 0 轮)。两者都使用 core 中已有的 helper 从恢复的记录里播种——取记录中已声明的最大轮次,没有则回退到恢复的用户轮次数量;交互模式和 ACP 早就用同样的方式播种。没有恢复任何会话的运行,铸造出的 id 与今天完全一致。

为什么需要

--resume--continue 会复用上一个会话的 id,因此重新开始编号的 headless 链会重复铸造上一次运行已经持久化的 prompt id,导致同一份 transcript 里多轮共用一个 prompt id。这个 id 正是 #9466 引入的 rewind 映射所锚定的键——id 重复会让该查找 fail-closed 退回按位置游走——同时也是持久化在 ui_telemetry 记录上的 prompt_id,而下一次 resume 又要读回它来为自己播种,于是歧义会沿着链条累积。交互模式和 ACP 在恢复时已经播种,只有这两条 headless 路径遗漏了,也就是 #11408 中记录的 deferred review finding。

对本描述早先版本的更正(来自沙箱 /verify 运行):这两条路径上 file-history 快照不会被丢弃。去重机制确实存在(SessionFileHistoryAccumulator 每个 prompt id 只保留最后一份),但 fileCheckpointingEnabled 默认为 !sdkMode && interactive,且 packages/cli 中无人覆盖它,因此 makeSnapshot 直接返回,headless 轮次根本不写快照——沙箱实测:10 个 headless 进程、6 次真实 write_file 执行、0 条快照记录。这也正是交互模式一直播种的原因,而这项代价在 headless 路径上并不是本 PR 所消除的。

评审验证计划

如何验证

单元测试覆盖了两条路径,也是本 PR 提供给评审者的证据:在 packages/cli 下执行 npx vitest run src/nonInteractive/session.test.ts src/llm.test.tsx。stream-json 的用例断言:未恢复任何会话时仍从第 1 轮开始;恢复的记录中最大轮次为 5 时下一轮为 6;记录中没有持久化 prompt id 时回退到恢复的用户轮次计数。-p 的用例断言:恢复运行会续接到最后已声明轮次之后,而原有的「全新会话为第 0 轮」的期望保持不变。

端到端的 oracle 是持久化在会话 transcript(<runtime>/projects/<cwd>/chats/<sid>.jsonl)里的 prompt id,在每个进程退出后读回——而不是 file-history 快照,headless 根本不写快照。先带 --session-id 跑一次 -p,再跑 -p --resume <id> 并对比:改动前两个进程在 transcript 里只留下一个轮次号,改动后每个进程一个。沙箱 /verify 用真实构建的 base 与 head 产物驱动了这一点:-p 在 base 上是 [0] → [0],head 上是 [0] → [0,2];stream-json 三个进程在 base 上是 [1] → [1] → [1],head 上是 [1] → [1,2] → [1,2,3]

证据(改动前后)

N/A —— 没有用户可见或 TUI 变化,差异体现在持久化的 prompt id 以及以其为键的快照上。

测试平台

三个平台均未在本地验证:撰写本 PR 的机器无法运行构建和测试套件,lint、typecheck 与单元测试以 CI 为准。

运行环境(可选)

N/A —— 仅单元测试。

风险与范围

  • 主要风险或取舍:恢复的 -p 运行所发出的那一个 prompt id 会发生变化(此前恒为第 0 轮),这会改变 telemetry 以及任何针对「恢复的 headless 运行为第 0 轮」做假设的消费方所看到的 id;全新运行与今天完全一致。
  • 未验证 / 范围之外:ACP 与交互路径未改动,因为它们已经播种;--continue--fork-session 以及「从交互会话恢复」的链路未被驱动(沙箱只跑了 --session-id--resume)。/rewind 本身与 refactor: anchor rewind mapping to stable prompt identity #9466 的身份映射也未被驱动——沙箱验证的是 id 会重复,而不是映射拿到重复 id 之后的行为。
  • 破坏性变更 / 迁移说明:无;没有持久化格式或公开 API 变更,createNonInteractivePromptId 新增的参数是可选的。

关联 Issue

Closes #11408 —— 该 issue 追踪的唯一一条 deferred finding(ic:5582642849,来自 #9466)。

Both headless entry points restart prompt numbering at the start of every
process, so a `--resume` / `--continue` chain re-mints promptIds the
previous run already persisted:

- stream-json (`Session`) starts `promptIdCounter` at 0, so the first turn
  of every resumed process is again `<sessionId>########1`;
- single-shot `-p` always mints `<sessionId>########0`.

The session id is reused on resume, so those ids are not merely duplicated
in telemetry. `SessionService.loadSession` keeps only the LAST file-history
snapshot per promptId, so the earlier run's snapshot for that turn is
dropped and `/rewind` restores the wrong workspace state; rewind's
prompt-identity mapping also fails closed to a positional walk when ids
repeat.

Seed both from the resumed transcript with the existing
`computeInitialTurnFromHistory` helper — the same seeding interactive mode
does via `seedPromptCount` and ACP does via `primeTurnFromHistory`. The
stream-json counter is seeded lazily on first use, because resumed data
only becomes authoritative after `config.initialize()`, which that class
defers until the first control request. A `-p` run with nothing resumed
keeps the historical `########0`.

Refs #11408 (deferred finding ic:5582642849 from #9466)
@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

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

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

`eslint --max-warnings 0` failed on import/no-duplicates: the new
`ChatRecord` type import sat alongside the file's existing type-only
import from the same module. Fold it into that import.
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR — and for turning the body around onto the template so quickly after the first pass.

Template looks good ✓ — all the required sections are there, the Risk & Scope tradeoffs are stated honestly rather than optimistically, and the Chinese translation is complete rather than summarised.

Problem: real, and I verified the mechanism rather than taking the description's word for it. Three things confirm it:

  • SessionFileHistoryAccumulator keeps one snapshot per promptId and overwrites on a repeat — snapshotsByPromptId.set(promptId, snapshot) inside the seenPromptIds.has(...) branch (packages/core/src/services/session-file-history-state.ts:34-40). So a duplicated promptId genuinely does replace the earlier run's snapshot, and loadSession consumes exactly that accumulator (sessionService.ts:3040). Last-wins, as described.
  • Interactive mode already seeds for precisely this reason, and its own comment says so: "Seed the prompt counter from the resumed conversation so new promptIds don't collide with restored file history snapshots" (AppContainer.tsx:1199-1207). The codebase already treats this collision as a bug worth preventing; the two headless paths were the gap.
  • The finding is tracked in Deferred review findings from PR #9466: refactor: anchor rewind mapping to stable prompt identity #11408, which I checked carries exactly one deferred item (ic:5582642849, from refactor: anchor rewind mapping to stable prompt identity #9466) — so Closes #11408 is accurate and won't orphan a partially-addressed tracking issue.

No end-to-end reproduction was run (the body says so plainly, and I'd rather it said that than imply otherwise). For this one the static evidence is strong enough to establish the defect exists, so I'm not holding the gate on a repro — but see the Stage 2 note about what would settle the consequence end to end.

Direction: aligned. This is turn-identity integrity for --resume / --continue, feeding /rewind correctness — squarely core mission, and it closes out a deferred finding from a PR that already merged rather than opening new surface. One thing I want on the record because the body is upfront about it: the promptId a resumed -p run emits does change value, and that id lands in telemetry records. I read this as a turn-identity fix that telemetry happens to observe, not a change to the telemetry subsystem, so I'm not escalating it — but any downstream consumer that assumed resumed headless runs are always turn 0 would notice, and the Risk & Scope section already names that.

Size: not applicable for the core-module gate. All four files sit under packages/cli/src, none match the core paths, and it's a single package rather than a cross-package change. For reference: 54 production lines (llm.tsx 27, session.ts 27) and 130 test lines (llm.test.tsx 35, session.test.ts 95) — tests outweigh the change by better than 2:1, which is the right shape.

Approach: minimal, and it reuses instead of adding. Seeding through the existing computeInitialTurnFromHistory — the same helper ACP already uses — is the right call over writing a new promptId parser; that helper already handles both the <sessionId>########N scan and the user-turn fallback. Both headless entry points genuinely need the fix, so there's no 80% to cut. No drive-by refactors: the second commit only merges a duplicate type import in the test file it added. The doc comments run longer than this repo's "comments default to none" habit, but they document a non-obvious why (the last-wins de-dupe and the lazy-seed ordering constraint), which is the case AGENTS.md carves out — I'd keep them.

Risk: no elevated risk signals. The Stage 1e high-risk path scan matched nothing.

Moving on to code review. 🔍

中文说明

感谢贡献——也感谢在第一轮之后这么快就把正文改成了模板格式。

模板完整 ✓ ——所有必需小节都在,Risk & Scope 里的取舍写得很诚实而不是过于乐观,中文翻译也是完整的而不是摘要。

问题:真实存在,而且我自己验证了机制,没有只采信描述。 三点确认:

  • SessionFileHistoryAccumulator 对每个 promptId 只保留一份快照,并在重复时覆盖——即 seenPromptIds.has(...) 分支里的 snapshotsByPromptId.set(promptId, snapshot)packages/core/src/services/session-file-history-state.ts:34-40)。所以 promptId 重复确实会顶掉上一次运行的快照,而 loadSession 消费的正是这个 accumulator(sessionService.ts:3040)。如描述所言,后者胜出。
  • 交互模式早已为此播种,其自身注释也写明了:「Seed the prompt counter from the resumed conversation so new promptIds don't collide with restored file history snapshots」(AppContainer.tsx:1199-1207)。代码库本身已经把这种冲突当作需要预防的缺陷;缺的就是两条 headless 路径。
  • 该发现记录在 Deferred review findings from PR #9466: refactor: anchor rewind mapping to stable prompt identity #11408,我确认过它只追踪一条 deferred item(ic:5582642849,来自 refactor: anchor rewind mapping to stable prompt identity #9466)——因此 Closes #11408 是准确的,不会把一个只解决了一半的追踪 issue 遗留下来。

没有执行端到端复现(正文如实说明了,我宁愿它这样写也不要含糊其辞)。就本 PR 而言,静态证据已足以确认缺陷存在,所以我不以复现为门禁条件——但关于如何端到端确认后果,见 Stage 2 的说明。

方向:一致。 这是 --resume / --continue 的轮次身份完整性,直接影响 /rewind 的正确性——完全属于核心使命,而且它收尾的是已合并 PR 遗留的 deferred finding,没有开辟新面。有一点我要记录在案,因为正文也主动说明了:resume 的 -p 运行所发出的 promptId 取值确实会变,而该 id 会进入 telemetry 记录。我的判断这是 telemetry 恰好观测到的轮次身份修复,而不是对 telemetry 子系统本身的改动,因此不做升级——但任何假设「resume 的 headless 运行恒为第 0 轮」的下游消费方会感知到变化,Risk & Scope 一节已经点名了这一点。

规模:核心模块门禁不适用。 四个文件都在 packages/cli/src 下,均未命中 core 路径,且只涉及单个 package 而非跨包改动。供参考:54 行生产代码(llm.tsx 27、session.ts 27)、130 行测试代码(llm.test.tsx 35、session.test.ts 95)——测试与改动之比超过 2:1,比例是健康的。

方案:最小化,且是复用而非新增。 通过已有的 computeInitialTurnFromHistory(ACP 已在用的同一个 helper)播种,比另写一个 promptId 解析器更合适;该 helper 已经同时处理了 <sessionId>########N 扫描与用户轮次回退。两条 headless 入口确实都需要修,没有可砍掉的 80%。没有顺手重构:第二个 commit 只是合并了它自己新增测试文件里重复的类型 import。文档注释比本仓库「默认不写注释」的习惯要长,但它们记录的是不显然的为什么(last-wins 去重与惰性播种的时序约束),这正是 AGENTS.md 允许的例外——我建议保留。

风险:无升级风险信号。 Stage 1e 高风险路径扫描无命中。

进入代码审查 🔍

Qwen Code · qwen3.8-max-2026-09-02

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

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Code review

I wrote my own proposal before opening the diff: on learning that headless resume re-mints promptIds, I'd have seeded both headless entry points from the resumed transcript using whatever helper ACP already uses, rather than adding a new parser — seeding lazily in the stream-json Session because that class defers config.initialize(), and eagerly in -p because main() initialises before it mints the id. The PR lands on the same shape, including the same split of lazy vs eager, so there's no simpler path I found that it missed.

What I actually verified in the code, since the whole change rests on when resumed data is readable:

  • -p ordering holds. await config.initialize() runs at llm.tsx:1350, inside the inputFormat !== STREAM_JSON branch, and createNonInteractivePromptId is called at 1422 — after it. So getResumedSessionData() is genuinely populated at the call site, and the fix is live rather than a no-op.
  • The stream-json arm of that same line is dead code, harmlessly. In stream-json mode initialize() is deliberately deferred, so at 1422 resumed data is not yet available — but that branch calls runNonInteractiveStreamJson(...) without passing prompt_id and then process.exit()s before logUserPrompt / runNonInteractive ever read it. With no records the new argument yields ########0, identical to today. No regression, and the stream-json path is seeded properly by the Session change instead.
  • The lazy seed cannot fire early. All three getNextPromptId() call sites — processUserMessage (session.ts:478), processContinueTurn (573), processMonitorNotificationBatch (638) — are each preceded by await this.waitForInitialization(), which only resolves after ensureConfigInitialized()config.initialize(). This matters more than it looks: because the counter caches on first use, a single early call would pin the wrong base for the whole process and reproduce the exact bug being fixed. The ordering claim in the doc comment is accurate.
  • Test arithmetic matches the helper. computeInitialTurnFromHistory takes the max parsed <sessionId>########N across both record.promptId and systemPayload.uiEvent.prompt_id, and falls back to the count of user records matching sessionId only when that max is 0 (session-turn-state.ts:50-58). The three stream-json cases (1 / 6 / 4) and the -p case (4 from a claimed 3) all follow correctly from that, including that the fallback test's records carry the matching sessionId — a mismatch there would silently assert 1 instead of 4.
  • Reuse checks out. computeInitialTurnFromHistory is exported both from the package barrel (packages/core/src/index.ts:384, used by llm.tsx) and from the deep path (services/session-turn-state.ts:107, used by session.ts), so both import styles resolve. No new utility was invented where one existed.

One non-blocking observation, offered as a question rather than a defect: createNonInteractivePromptId returns ########0 whenever computeInitialTurnFromHistory yields 0, so a resumed transcript whose highest claim is turn 0 and which contains no matching user records would re-mint ########0 — the one case where the implementation doesn't quite match its own "continues past the last claimed turn" comment. I don't think it's reachable: a -p turn always records a user prompt, so the user-turn fallback is ≥1 whenever a ########0 id exists, and the second run then mints ########2. Worth a thought, not worth a change. (The stream-json arm has no equivalent gap — 0 seeds to ########1.)

Config.getResumedSessionData is a non-optional method (config.ts:4905), so the ?.() is defensive rather than load-bearing; ESLint passed on it, so nothing objects.

CI evidence — one red check, and it is this PR's

The PR's own CI on the reviewed commit is red on Lint & Static (ubuntu-latest, Node 22.x), and this is not infra noise — it is caused by these changes. The failing step is Prettier, and it names exactly the two source files this PR touches (llm.tsx and llm.test.tsx are clean):

Checking formatting...
[warn] packages/cli/src/nonInteractive/session.test.ts
[warn] packages/cli/src/nonInteractive/session.ts
[warn] Code style issues found in 2 files. Run Prettier with --write to fix.
##[error]Process completed with exit code 1.

ESLint ran before Prettier in that job (node scripts/lint.js --eslint--prettier, under bash -e -o pipefail) and did not fail, so the only lint problem is formatting. Measuring the diff against the repo's printWidth: 80 finds six added lines over the limit, distributed across exactly those two files — which is consistent with the warning list:

session.test.ts   expect(runNonInteractiveMock.mock.calls[0][3]).toBe('test-session########1');
session.test.ts   it('seeds the promptId counter past turns the resumed transcript claims', async () => {
session.test.ts   expect(runNonInteractiveMock.mock.calls[0][3]).toBe('test-session########6');
session.test.ts   it('falls back to the resumed user-turn count when no promptId is persisted', async () => {
session.test.ts   expect(runNonInteractiveMock.mock.calls[0][3]).toBe('test-session########4');
session.ts        import { computeInitialTurnFromHistory } from '@qwen-code/qwen-code-core/services/session-turn-state.js';

npm run format (or npx prettier --write on those two files) settles it. Given the body's note that this machine cannot run the build or test suite, this is the expected shape of miss rather than a carelessness signal — but it does have to be fixed before this can merge.

Everything else is either green or still running: OpenTUI no-flicker gate, TUI parity snapshots (ink vs opentui) and both Desktop Shell jobs passed; Test (ubuntu-latest, Node 22.x) and Integration Tests (no-AK, No Sandbox) were still in progress when this ran, with the macOS and Windows Test legs and Integration Tests (CLI, No Sandbox) skipped by the matrix. Not verified: unit tests, integration tests and typecheck — typecheck does not run in the Lint & Static job at all (no occurrence in its log), and the suite had not reported yet, so no green test evidence exists for this commit in either direction. I did not run any of it myself; per the gate's rules PR-derived code is never executed here, and the author's own "not run locally" statement is their claim, not evidence.

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

Check Conclusion
Lint & Static (ubuntu-latest, Node 22.x) ❌ failure
web-shell E2E Smoke (ubuntu-latest, Node 22.x) 🚫 cancelled
Classify PR ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Integration Tests (no-AK, No Sandbox) ✅ success
OpenTUI no-flicker gate ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
TUI parity snapshots (ink vs opentui) ✅ success

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

Sandboxed verification would settle the part none of the above can: @qwen-code /verify — the unit tests pin the promptId arithmetic, but nothing pins the consequence the PR exists for, namely that a two-run headless resume chain persists distinct ids, that both file-history snapshots survive loadSession instead of the second overwriting the first, and that /rewind to the earlier turn restores the right workspace state. A suite that passed with the seeding removed would still pass today's tests for the -p arm, and the author states plainly that no real end-to-end headless resume was executed. Since the author has write access, @qwen-code /tmux is also available if you want the /rewind surface driven as a real user rather than inferred from the accumulator.

中文说明

代码审查。 我在看 diff 之前先写了自己的方案:得知 headless resume 会重复铸造 promptId 后,我会用 ACP 已在用的那个 helper 从 resume 的 transcript 为两条 headless 入口播种,而不是另写解析器——stream-json 的 Session 里惰性播种(该类把 config.initialize() 推迟了),-p 里即时播种(main() 在铸造 id 之前就已初始化)。本 PR 的形态与此一致,包括惰性/即时的划分,所以我没有找到它遗漏的更简路径。

由于整个改动取决于「何时能读到 resume 数据」,我实际在代码里验证了以下几点:

  • -p 的时序成立。 await config.initialize()llm.tsx:1350(位于 inputFormat !== STREAM_JSON 分支内),而 createNonInteractivePromptId1422 调用——在其之后。所以调用点上 getResumedSessionData() 确实已填充,修复是生效的而不是空操作。
  • 同一行在 stream-json 分支上是死代码,但无害。 stream-json 模式下 initialize() 是刻意推迟的,因此在 1422 处 resume 数据尚不可用——但该分支调用 runNonInteractiveStreamJson(...) 时并不传 prompt_id,随后在 logUserPrompt / runNonInteractive 读到它之前就 process.exit() 了。没有 records 时新参数得到 ########0,与今天完全一致。无回归,且 stream-json 路径改由 Session 的改动正确播种。
  • 惰性播种不可能提前触发。 三个 getNextPromptId() 调用点——processUserMessagesession.ts:478)、processContinueTurn573)、processMonitorNotificationBatch638)——之前都有 await this.waitForInitialization(),而它只在 ensureConfigInitialized()config.initialize() 之后才 resolve。这一点比看上去更重要:因为计数器只在首次使用时缓存,一次提前的调用就会把错误基数固定到整个进程,从而复现它正要修的那个缺陷。文档注释里的时序声明是准确的。
  • 测试算术与 helper 一致。 computeInitialTurnFromHistory 会在 record.promptIdsystemPayload.uiEvent.prompt_id 两处取解析出的 <sessionId>########N 最大值,仅当该最大值为 0 时才回退到 sessionId 匹配的用户记录数(session-turn-state.ts:50-58)。三个 stream-json 用例(1 / 6 / 4)与 -p 用例(由已声明的 3 得到 4)都能由此正确推出,包括回退用例的记录携带了匹配的 sessionId——若不匹配,断言会静默变成 1 而不是 4。
  • 复用成立。 computeInitialTurnFromHistory 既从包入口导出(packages/core/src/index.ts:384llm.tsx 使用),也从深路径导出(services/session-turn-state.ts:107session.ts 使用),两种 import 写法都能解析。没有在已有实现的地方新造工具。

一条不阻塞的观察,作为问题而非缺陷提出:createNonInteractivePromptIdcomputeInitialTurnFromHistory 返回 0 时一律返回 ########0,因此「已声明的最大轮次为 0 且不含匹配用户记录」的 resume transcript 会重新铸造 ########0——这是实现与自身「续接到最后已声明轮次之后」注释唯一不完全吻合的情形。我认为它不可达:-p 轮次总会记录一条用户 prompt,所以只要存在 ########0 这个 id,用户轮次回退值就 ≥1,第二次运行会铸造 ########2。值得想一想,不值得改。(stream-json 一侧没有对应缺口——0 会播种为 ########1。)

Config.getResumedSessionData 是非可选方法(config.ts:4905),因此 ?.() 属于防御性写法而非必需;ESLint 通过,没有规则反对。

CI 证据——一个红灯,且是本 PR 造成的。 本 PR 自己在被审 commit 上的 CI 在 Lint & Static (ubuntu-latest, Node 22.x) 上是红的,而且这不是基础设施噪音,是这些改动导致的。失败步骤是 Prettier,点名的正是本 PR 改动的两个源码文件(llm.tsxllm.test.tsx 是干净的)。ESLint 在该 job 中先于 Prettier 运行且未失败,所以唯一的 lint 问题是格式。按仓库的 printWidth: 80 度量 diff,可找到 6 行超限的新增行,分布恰好落在这两个文件里。执行 npm run format(或对这两个文件跑 npx prettier --write)即可解决。考虑到正文说明本机无法运行构建与测试套件,这属于意料之中的遗漏形态而非粗心信号——但合并前必须修掉。

其余检查不是绿就是仍在运行:OpenTUI no-flicker gateTUI parity snapshots (ink vs opentui) 与两个 Desktop Shell job 已通过;Test (ubuntu-latest, Node 22.x)Integration Tests (no-AK, No Sandbox) 在本次运行时仍在进行中,macOS 与 Windows 的 Test 分支以及 Integration Tests (CLI, No Sandbox) 被矩阵跳过。未验证:单元测试、集成测试与 typecheck——typecheck 完全不在 Lint & Static job 中运行(其日志里没有一次出现),而测试套件尚未出结果,因此该 commit 目前没有任何方向的绿色测试证据。我没有自己运行任何一项;按门禁规则此处绝不执行 PR 派生代码,而作者「本地未运行」的说明是其自述,不构成证据。

沙箱验证可以确认上述都无法确认的部分:@qwen-code /verify——单元测试钉住的是 promptId 的算术,但没有任何东西钉住本 PR 存在的理由,即后果:两段式 headless resume 链会持久化不同的 id、两份 file-history 快照在 loadSession 后都存活而不是第二份覆盖第一份、以及对较早那一轮执行 /rewind 会恢复到正确的工作区状态。即使把播种逻辑删掉,今天 -p 一侧的测试仍会全部通过;而作者也明确说明没有执行真正的端到端 headless resume。由于作者具备写权限,如果希望以真实用户方式驱动 /rewind 界面而不是从 accumulator 推断,也可以使用 @qwen-code /tmux

Qwen Code · qwen3.8-max-2026-09-02

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — the change itself I'd take; I can't approve against a red Prettier check and a test suite that hasn't reported yet.

Stepping back: this is a good, small PR that closes a gap the codebase already knew about. Interactive mode seeds its prompt counter on resume with a comment explaining that unseeded ids collide with restored file-history snapshots, ACP seeds through computeInitialTurnFromHistory, and the two headless paths simply never got the same treatment. Seeding them from the same helper is the obvious right answer, and it's what the PR does — no new abstraction, no parallel utility, 54 production lines against 130 test lines.

It also slightly exceeds what I'd have written independently. My instinct was to mirror interactive mode, but interactive seeds from a plain user-turn count, whereas this uses the helper that takes the highest turn the transcript actually claims and only falls back to the count. That's strictly more robust for a chain that has already been resumed several times, and it's the reason the "highest claimed turn is 5 → next is 6" case works at all. If anything, interactive mode is now the weaker of the three seeding sites.

The reason I'm not approving is concrete and not about the design:

  1. Lint & Static is red, and this PR caused it. Prettier flags session.ts and session.test.ts — six added lines over the repo's 80-column limit. ESLint passed; formatting is the whole failure. npm run format fixes it. A red required check means this cannot merge as it stands, so there is nothing to approve yet.
  2. No test evidence exists for this commit in either direction. Test (ubuntu-latest) and Integration Tests (no-AK, No Sandbox) were still running, the macOS/Windows legs were skipped by the matrix, and typecheck doesn't run in the lint job at all. The body is upfront that nothing was run locally, so CI is the only oracle — and it hasn't spoken.
  3. The consequence is still inferred, not observed. I verified the mechanism statically and I'm satisfied the defect is real: SessionFileHistoryAccumulator genuinely does overwrite on a repeated promptId, so the earlier run's snapshot is genuinely dropped. But that's me reading the accumulator, not anybody watching a resume chain lose a /rewind target. The tests pin the id arithmetic; nothing pins the end-to-end outcome. @qwen-code /verify would close that, and it's the piece I'd want before considering this fully settled rather than merely correct-looking.

None of these are reasons to rethink the approach, and I want to be clear that I'm not requesting changes on the design — the ########0 edge case I raised in Stage 2 I regard as unreachable and would not ask anyone to write code for. Once Prettier is clean and the suite reports green, this is an approve on a re-run; if /verify gets driven in the meantime, all the better.

Deferring rather than approving, and deliberately not leaving an approve-on-green marker: with Lint & Static already red on this head there is no green state for it to fire against, so a marker here would be a promise nothing can keep. @yiliang114 — the one action that unblocks this is a formatting pass on the two files.

中文说明

Confidence: 3/5 —— 改动本身我会收下;但在 Prettier 红灯、测试套件尚未出结果的情况下我无法批准。

退一步看:这是一个好而小的 PR,补上了代码库自己早就知道的一处缺口。交互模式在 resume 时会为 prompt 计数器播种,其注释明确说明未播种的 id 会与恢复的 file-history 快照冲突;ACP 通过 computeInitialTurnFromHistory 播种;只有两条 headless 路径一直没有得到同样处理。用同一个 helper 为它们播种是显而易见的正确答案,本 PR 正是如此——没有新抽象、没有并行工具,54 行生产代码对应 130 行测试代码。

它甚至略微超出了我独立写出的方案。我的直觉是照搬交互模式,但交互模式是用朴素的用户轮次计数播种,而本 PR 用的 helper 取的是 transcript 实际声明的最大轮次,仅在拿不到时才回退到计数。对于已经被 resume 过多次的链路,这严格更稳健,也正是「已声明最大轮次为 5 → 下一轮为 6」这个用例能够成立的原因。真要比较,交互模式现在是三个播种点里较弱的那一个。

我不批准的原因很具体,且与设计无关:

  1. Lint & Static 是红的,且由本 PR 造成。 Prettier 点名 session.tssession.test.ts——6 行新增代码超过仓库的 80 列限制。ESLint 已通过;格式就是全部问题。npm run format 即可修复。必需检查红灯意味着当前状态无法合并,因此还没有可批准的东西。
  2. 该 commit 目前没有任何方向的测试证据。 Test (ubuntu-latest)Integration Tests (no-AK, No Sandbox) 仍在运行,macOS/Windows 分支被矩阵跳过,而 typecheck 根本不在 lint job 中运行。正文如实说明本地未运行任何东西,所以 CI 是唯一的裁判——而它还没开口。
  3. 后果仍是被推断出来的,而非被观测到的。 我静态验证了机制,也确信缺陷真实存在:SessionFileHistoryAccumulator 确实在 promptId 重复时覆盖,因此上一次运行的快照确实会被丢弃。但那是我在读 accumulator,而不是有人亲眼看到一条 resume 链丢失了 /rewind 目标。测试钉住了 id 算术,没有任何东西钉住端到端结果。@qwen-code /verify 可以补上这一点,也是我在认为此事「彻底落定」而不只是「看起来正确」之前想要的那一块。

这些都不是重新考虑方案的理由,我也要说清楚:我并非在设计层面要求修改——Stage 2 里提出的 ########0 边界情形我判断为不可达,不会要求任何人为它写代码。等 Prettier 干净、套件报绿之后,重跑一次即可批准;如果期间还跑了 /verify,那就更好。

选择 defer 而不是批准,并且刻意留下 approve-on-green 标记:该 head 上 Lint & Static 已经是红的,不存在可供它触发的绿色状态,此时留标记等于许下一个没人能兑现的承诺。@yiliang114 —— 解开阻塞只需一步:对那两个文件做一次格式化。

Qwen Code · qwen3.8-max-2026-09-02

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

`node scripts/lint.js --prettier` runs `prettier --experimental-cli`,
which breaks these two lines differently from the classic CLI. Format
them the way the gate expects.
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Thanks — the one blocking item is fixed. 581ac5b formats the two files the Prettier step named, and CI on that head is green: Lint & Static (ubuntu) ✅ 13m46s, Test (ubuntu) ✅ 15m26s, Integration Tests (no-AK) ✅, OpenTUI no-flicker, TUI parity, both Desktop Shell legs ✅.

One detail worth recording for whoever hits this next, because it explains the miss rather than excusing it: the two files were clean under prettier --check with the classic CLI, and only the --experimental-cli frontend that scripts/lint.js --prettier actually invokes rejected them. So the local repro that matches the gate is node node_modules/.bin/prettier --experimental-cli --config-path .prettierrc.json --check <files> — note that the experimental CLI renamed --config to --config-path and exits 1 with a rename notice if you pass the old flag, which is easy to misread as a formatting failure.

On the Stage 2 ########0 observation: agreed it's unreachable for the reason you gave — a -p turn always records a user prompt, so the user-turn fallback is ≥1 whenever a ########0 id exists — and I'd rather not add code for it.

The end-to-end consequence is still unpinned, as you say. I can't drive it on my machine; if a maintainer wants it settled before merge, @qwen-code /verify is the right call.

@qwen-code /triage

中文说明

阻塞项已修复:581ac5b 对 Prettier 点名的两个文件做了格式化,该 head 上 CI 全绿——Lint & Static (ubuntu) ✅ 13m46s、Test (ubuntu) ✅ 15m26s、Integration Tests (no-AK) ✅,以及 OpenTUI no-flickerTUI parity、两个 Desktop Shell 均通过。

有一个细节值得记下来给后来人,它解释了这次遗漏的成因(而非为其开脱):这两个文件在经典 Prettier CLI 的 --check 下是干净的,只有 scripts/lint.js --prettier 实际调用的 --experimental-cli 前端才会判它们不合格。因此与门禁一致的本地复现命令是 node node_modules/.bin/prettier --experimental-cli --config-path .prettierrc.json --check <files>——注意实验版 CLI 把 --config 改名为 --config-path,传旧参数会以 exit 1 加一条改名提示结束,很容易被误读成格式失败。

关于 Stage 2 提出的 ########0 情形:同意按你给出的理由它不可达——-p 轮次总会记录一条用户 prompt,因此只要存在 ########0 这个 id,用户轮次回退值就 ≥1——我也倾向于不为它增加代码。

端到端后果确实仍未钉住。我的机器无法驱动它;如果维护者希望在合并前把这一点落定,@qwen-code /verify 是合适的做法。

@yiliang114
yiliang114 enabled auto-merge September 9, 2026 06:19
@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /verify

Focus, since the unit tests already pin the promptId arithmetic and the open question is the consequence:

  1. Two-run headless resume chain on one session (-p once, then -p --resume <id>, and the same for --input-format stream-json): the second run must mint a promptId distinct from every id the first run persisted.
  2. After loadSession on the resumed transcript, both runs' file-history snapshots survive — on base the second overwrites the first (SessionFileHistoryAccumulator is last-wins per promptId), so this is the cell that should flip.
  3. /rewind to the earlier turn restores that turn's workspace state rather than the later one's.

A useful mutant: revert the seeding in either entry point and confirm the suite goes red — for the -p arm today's tests would still pass, which is exactly the gap this run is meant to close.

中文说明

单测已经钉住了 promptId 的算术,未决的是后果,因此建议聚焦:

  1. 单个会话上的两段式 headless resume 链(先 -p,再 -p --resume <id>--input-format stream-json 同理):第二次运行铸造的 promptId 必须与第一次持久化的所有 id 都不同。
  2. 对 resume 的 transcript 执行 loadSession 之后,两次运行的 file-history 快照都应存活——在 base 上第二份会覆盖第一份(SessionFileHistoryAccumulator 按 promptId 后者胜出),这正是应该翻转的那一格。
  3. 对较早那一轮执行 /rewind,恢复的是该轮的工作区状态,而不是后一轮的。

一个有用的变异:把任一入口的播种回退,确认套件变红——就 -p 一侧而言,今天的测试仍会通过,而这正是本次验证要补上的缺口。

@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /verify

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run

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

Scripted assertions: 39 passed · 0 failed · 39 total

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

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

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

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

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

Verification report

PR #11441 — deep verification

Verdict: findings — 39 scripted assertions, 39 pass / 0 fail.
Verified head: 581ac5bce98aed43fa2dbb9668faeaa3f42ead4f (git rev-parse HEAD^2), base cfb173ec4669b36fb5459bd721b90a388bd0542e (HEAD^1).

The central claim is proven load-bearing on both headless entry points: a
base↔head A/B over real built artifacts and real resumed processes flips the
observable from "N turns share one promptId" to "one promptId per turn". The
verdict is findings rather than merge-ready because the -p half of the
fix fails open in a shape I reproduced end-to-end (Finding 1), and because
the PR's stated rationale is measurably wrong about the consequence it names
(Correction 1).

中文摘要

结论:findings —— 39 条脚本化断言全部通过(39 pass / 0 fail)。

A/B 结论:中心主张成立。用真实构建产物(head 的 packages/cli/dist,base 为
HEAD^1 独立 worktree 重新编译的 packages/cli/dist)对同一个 mock OpenAI 服务跑了
真实的 --session-id--resume 进程链,以会话 transcript JSONL 里持久化的
promptId 轮次为 oracle:

  • 单次 -p:base 两个进程后 transcript 只有轮次 {0}(第二个进程重新铸造 ########0);
    head 为 {0,2}
  • stream-json:base 三个进程后仍是 {1}(三轮共用一个 id);head 为 {1,2,3}

findings

  1. -p 分支的 lastTurn > 0 ? lastTurn + 1 : 0 守卫,在 computeInitialTurnFromHistory
    返回 0 时会重新铸造 transcript 已声明的 ########0 —— 正是本 PR 要消除的冲突。
    已端到端复现(首轮 prompt 为纯空白)。候选修复已实测:12/12 边界形状不再冲突,
    三个「全新会话」形状仍为 ########0(零附带影响),单测仍 128/128。
  2. -p 调用点没有任何测试覆盖:把 config.getResumedSessionData?.() 实参删掉,
    128/128 依然全绿(突变体 M6 存活)。
  3. PR 描述与新注释把「file-history 快照被丢弃 → /rewind 恢复到错误工作区状态」
    作为主要理由,但 headless 路径根本不写快照(见下方 Correction 1)。

未覆盖范围:交互式与 ACP 路径未做端到端验证;/rewind#9466 的 prompt
身份映射未驱动;三平台均未实测;按 commit 归因不可达(浅克隆)。详见 Not covered

Central claim and A/B

Central claim. A headless run that resumes a session continues prompt
numbering from the resumed transcript instead of restarting it, on both
headless entry points.

Secondary claims. (a) A run that resumes nothing keeps the ids it mints
today. (b) The collision was dropping file-history snapshots — see Correction 1.

Oracle for every cell: the turn numbers persisted in the session transcript
JSONL (<runtime>/projects/<cwd>/chats/<sid>.jsonl), read back after each
process exits. Witness: 01-ab-promptid-base-vs-head.png.

# path cell processes transcript turns after each oracle verdict
1 -p base 2 (--session-id, then --resume) [0][0] run 2 minted no new turn — re-minted ########0
2 -p head 2 (same chain) [0][0,2] run 2 minted ########2 — distinct
3 stream-json base 3 (--session-id, then --resume ×2) [1][1][1] 3 turns, 1 id
4 stream-json head 3 (same chain) [1][1,2][1,2,3] one id per process

Secondary claim (a) holds: the fresh -p cell mints ########0 on head
exactly as on base (row 1 vs row 2, first process), and the fresh stream-json
cell mints ########1 on both (rows 3 and 4, first process). 2/2 base cells
fail as predicted and 2/2 head cells pass
— that pair of counts is the
load-bearing proof.

Every run exited 0 with clean stderr, completed the stream-json initialize
handshake, and really executed write_file (assertions V1–V5), so the empty
snapshot census below is not a dead probe.

Corrections

These are corrections to the description and the new code comments, not
requests to change behaviour.

Correction 1 — headless runs never write file-history snapshots, so the
named consequence cannot occur on the paths this PR changes.
The description
and both new doc comments justify the fix with: "SessionService.loadSession
keeps only the LAST file-history snapshot per promptId, so the earlier run's
snapshot for that turn is dropped and /rewind restores the wrong workspace
state."
The dedup mechanism is real — SessionFileHistoryAccumulator.add()
(packages/core/src/services/session-file-history-state.ts:35-40) does
overwrite snapshotsByPromptId on a repeated promptId. But nothing on a
headless path ever feeds it:

packages/core/src/config/config.ts:2713
  this.fileCheckpointingEnabled =
    params.fileCheckpointingEnabled ?? (!params.sdkMode && (params.interactive ?? false));

makeSnapshot() returns immediately when !this.enabled
(fileHistoryService.ts:725), and nothing in packages/cli passes
fileCheckpointingEnabled. Census, not reading: 10 headless processes driven
(4 -p, 6 stream-json), 6 successful write_file executions confirmed on
disk, 0 system/file_history_snapshot records persisted, 0 snapshots surviving
the real accumulator
(assertions C1–C3). The remaining justification that
does hold is duplicate ids in telemetry and in the rewind prompt-identity
mapping — worth stating plainly, because a reviewer weighing "silently
corrupts /rewind" against "duplicate telemetry key" is weighing a cost this
PR does not actually remove on these paths.

Correction 2 — interactive mode does not seed "the same way". The new
comment at session.ts:156 says "Interactive mode seeds the same way
(seedPromptCount in AppContainer), and ACP via
computeInitialTurnFromHistory."
Only the ACP half is accurate
(Session.ts:4348 primeTurnFromHistorycollectSessionTurnState).
AppContainer.tsx:1199-1207 uses its own inline rule — a plain count of
type === 'user' records excluding two subtypes — and never consults
maxPromptTurn. The two rules disagree whenever the transcript claims a higher
turn than it has counting user records. Pre-existing, out of this PR's declared
scope, but the comment asserts a three-way symmetry that is not there.

Findings

1. The -p guard re-opens the exact collision it closes when the seed computes to 0 — reproduced end-to-end

llm.tsx:1513-1518:

const lastTurn = resumedRecords?.length
  ? computeInitialTurnFromHistory(resumedRecords, sessionId)
  : 0;
return `${sessionId}########${lastTurn > 0 ? lastTurn + 1 : 0}`;

computeInitialTurnFromHistory returns maxPromptTurn > 0 ? maxPromptTurn : userMessageCount (session-turn-state.ts:53-56). When it returns 0 on a
non-empty transcript
— the transcript claims ########0 and no record
satisfies isUserPromptRecord (which requires non-blank text,
session-turn-state.ts:145-153) — the : 0 branch fires and -p mints
########0 again. session.ts has no such branch: it seeds the counter and
++s unconditionally, so the two headless paths disagree on the same input.

Boundary probe over the real built createNonInteractivePromptId
(boundary-probe.mjs, 12 shapes): head re-mints an already-claimed turn in
2/12 cases, and the stream-json rule in 0/12 (assertions B1, B2, W4b).

That shape is reachable, and I drove it through the shipped binary:

node tmp/pr11441-verify-20260909-065809/ws-probe.mjs \
  packages/cli/dist/index.js head        # run1: -p '   '   run2: --resume -p 'real prompt'
arm user turns persisted distinct ids in transcript
base 2 [0] — collision
head 2 [0]collision, unchanged from base
head + candidate fix 2 [0,1] — distinct

Bounding it honestly: this needs the first run's prompt to be whitespace-only
(-p ' ', or a shell variable that expanded to nothing — llm.tsx only
rejects a falsy input, so ' ' proceeds). It is not a regression — base
collides identically — and the blast radius is the same as the bug being fixed:
a duplicate prompt_id on ui_telemetry records and a repeated key in the
rewind identity mapping. Per Correction 1 it does not corrupt file-history
snapshots. It is worth fixing because it is the PR's own bug class surviving
its own guard, silently.

Suggested minimal fix — measured, preserves the commit's intent

Make the -p rule identical to the stream-json rule by moving the fresh-run
special case into the seed instead of the output:

  const lastTurn = resumedRecords?.length
    ? computeInitialTurnFromHistory(resumedRecords, sessionId)
    : -1;
  return `${sessionId}########${lastTurn + 1}`;

Applied to a scratch copy of both the source and the built dist/src/llm.js
and driven back through the same harnesses (logs/validate-fix.txt):

  • hostile fixtures go clean — boundary probe 12/12 rows re-mint nothing
    (was 10/12); the whitespace end-to-end chain yields [0,1] (was [0]).
  • benign fixtures byte-identical — the three fresh-run shapes still mint
    sess-1########0; cases 3, 4, 5, 6, 10, 11 are unchanged (########2,
    ########4, ########6, ########2, ########2, ########9007199254740992).
  • suite counts unchanged — 128/128 green with and without the patch.

That last line is the unpinned-axis signal: the suite cannot tell head from
head-plus-fix, so the fix should ship with a fixture — a resumed transcript
whose highest claimed turn is 0 and whose only user record has blank text,
asserting ########1.

2. Nothing pins the -p call site — 128/128 stay green with the fix disconnected

Mutation matrix over the production files, each mutant run against
npx vitest run src/nonInteractive/session.test.ts src/llm.test.tsx
(mutation-matrix.mjs). Witness: 02-mutation-matrix-kills-and-survivors.png.

mutant change result attribution
M0 control: break the ######## delimiter killed (3 failed / 125) harness can make the suite red
M1 revert createNonInteractivePromptId to always ########0 killed (1 / 127) llm.test.tsx > continues the prompt id chain when the -p run resumes a session — the PR's own new test
M2 revert the stream-json lazy seed killed (2 / 126) both new session.test.ts tests
M3 keep the lazy block, seed 0 instead of the helper killed (2 / 126) both new session.test.ts tests
M4 drop the lastTurn > 0 guard killed (1 / 127) pre-existing creates non-interactive prompt ids that preserve session correlation — so the new -p test does not pin that guard
M7 sever the getResumedSessionData seam in session.ts killed (2 / 126) both new session.test.ts tests
M5 drop the resumedRecords?.length guard in llm.tsx survived redundant defence — the helper returns 0 for an empty array, so the clause cannot decide any outcome
M6 stop passing the argument at the llm.tsx:1424 call site, function left intact survived coverage gap

M6 is the one that matters. The new llm.test.tsx test calls
createNonInteractivePromptId directly with a hand-built record array, so
it pins the function and not the wiring: delete
config.getResumedSessionData?.()?.conversation.messages from the call site
and every one of the 128 tests still passes while the shipped -p path reverts
to ########0 on every resume. My end-to-end A/B is what discriminates — rows
1 and 2 of the A/B table are exactly this mutation, measured through the real
binary. A main()-level test that stubs getResumedSessionData on the config
and asserts the prompt_id reaching runNonInteractive would close it; note
session.ts has that coverage (M7 killed) and llm.tsx does not.

M5 is reported as completeness, not a merge condition: it is redundant defence,
correct exactly as it stands.

Disclosure on the raw log: logs/mutation-matrix.txt and
02-mutation-matrix-kills-and-survivors.png print unexpected=2 and label M5
and M6 UNEXPECTED. That is a string-comparison artifact in my scoring script
(it compared the expectation string survives against the outcome string
survived), not a disagreement with the table above — both mutants survived
exactly as hypothesised. The killed/survived column is the substantive result.

3. Two notes on the changed expression, no action required

  • Id sequence skips 1 on the first resume. computeInitialTurnFromHistory
    returns userMessageCount (=1) when the transcript's only claimed turn is 0,
    so a -p chain mints 0, 2, 3, 4, … (measured: A/B row 2, and probe case 6).
    Unique and monotonic, which is all the contract needs — noted only because a
    consumer assuming contiguity would be surprised.
  • Precision at 2^53. Probe case 11: a transcript claiming ########9007199254740993
    yields ########9007199254740992 — lossy, on both paths, and not reachable
    by any real chain. Called out because the skill asks for lossy results even
    when every assertion passes.

Not covered

  • Per-commit attribution. The checkout is depth 2 (git rev-parse --is-shallow-repositorytrue): git rev-list HEAD^1..HEAD^2 yields 1
    commit locally while the metadata snapshot lists 3. Only the aggregate
    HEAD^1..HEAD diff was verified; the two follow-up commits (an import merge
    and a prettier --experimental-cli reformat) were not individually exercised.
  • The Reviewer Test Plan's end-to-end step could not be performed as
    written.
    It asks the reviewer to confirm that "the first turn's
    file-history snapshot is gone after reload" and that "both /rewind targets
    remain reachable". Neither is observable from a headless chain — Correction 1
    shows headless writes no snapshots at all, so there is nothing to be gone and
    no /rewind target on either side. The plan needs rewriting to the
    observable oracle (transcript prompt ids), which is what this round used.
  • /rewind and the refactor: anchor rewind mapping to stable prompt identity #9466 prompt-identity mapping were never driven. The
    claim that repeated ids make that lookup "fail closed to the positional walk"
    is untested here; I verified only that ids repeat.
  • Interactive and ACP paths. Verified by reading that they seed
    (AppContainer.tsx:1206, acp-integration/session/Session.ts:4348), not by
    execution. Correction 2 is a reading-level finding about the comment, not a
    measured interactive defect.
  • --continue, --fork-session, and resumed-from-interactive chains. Only
    --session-id--resume was driven. sessionService.ts:4411-4417
    remaps snapshot prompt ids across a fork; that remap interacting with the new
    seeding is untested.
  • Gates run: only the two touched test files (128/128) and packages/cli
    typecheck (exit 0).
    No repo-wide lint, no repo-wide test, no bundle. I did
    not re-run anything the PR's own CI covers.
  • Harness self-inflicted contamination, disclosed. The first typecheck run
    (logs/typecheck-cli.log, exit 2, TS6133 'computeInitialTurnFromHistory' is declared but its value is never read) executed while the mutation matrix
    had M1 applied
    to llm.tsx. It is my race, not a PR defect. Re-run on a
    verified-clean tree it exits 0 (logs/typecheck-cli-clean.log). Every
    mutation script restores via trap and git status --short was confirmed empty
    before and after each phase; the patched dist/src/llm.js was restored and
    verified by sha256sum -c (4cbff46d…).
  • Base-arm build cost. npm run build -w packages/cli in the base worktree
    needed three attempts: tsc was not on PATH outside npm run, and the
    worktree had neither the gitignored src/generated/git-commit.ts nor the
    per-package node_modules (packages/core, packages/cli,
    packages/channels/feishu) that the root install does not hoist. Total ≈ 6
    minutes. No base npm ci was needed.

Methodology

Environment: the CI verify container (node:22-bookworm, node v22.23.2),
working tree at refs/pull/11441/merge, npm ci + npm run build already
complete at head. Control construction. git worktree add tmp/base-tree HEAD^1 (cfb173ec), src/generated/git-commit.ts regenerated with the repo's
own scripts/generate-git-commit-info.js, per-package node_modules symlinked
from the head install, then packages/cli rebuilt with tsc --build +
copy_files.js (exit 0). The control is clean and I checked it two ways: the
PR diff touches zero files under packages/core, package.json, or
package-lock.json, so the internal workspace symlink
(readlink -f node_modules/@&#8203;qwen-code/qwen-code-core
/__w/qwen-code/qwen-code/packages/core, i.e. the head tree) introduces no
difference; and diff -rq packages/cli/dist tmp/base-tree/packages/cli/dist
reports 0 "Only in" entries with differences confined to llm.js,
nonInteractive/session.js, their .map/.d.ts/test emits, the generated
git-commit, and tsconfig.tsbuildinfo. The base dist carries the pre-PR code
(${sessionId}########0 at llm.js:1121, promptIdCounter = 0 at
session.js:31). The worktree was removed with git worktree remove --force
after the cells were captured. How the harnesses drove the code. Both A/B
harnesses spawn the real built dist/index.js as a child process against a
zero-dependency mock OpenAI chat-completions server on 127.0.0.1:8791
(specialized from .qwen/skills/e2e-testing/scripts/mock-openai-server.js to
emit a write_file tool call at an absolute path carried in the prompt), each
arm in its own mkdtemp work dir and QWEN_RUNTIME_DIR/HOME, no stub of the
code under test anywhere. The stream-json harness speaks the real wire protocol
(control_request/initialize, then a user message, then stdin EOF) and
waits for the control_response before sending. The snapshot census feeds the
persisted transcript to the real SessionFileHistoryAccumulator imported from
the built core dist — the exact class loadSession uses. The boundary probe
imports the real built createNonInteractivePromptId. Images were produced with
node scripts/verify-capture.mjs. Raw logs live in
tmp/pr11441-verify-20260909-065809/logs/: ab-{base,head}-p.json,
ab-{base,head}-sj.json, ws-{base,head}.txt, boundary-probe.txt,
mutation-matrix.txt, attribution.txt, validate-fix.txt,
vitest-head-clean.log, typecheck-cli{,-clean}.log, base-cli-build{,2,3}.log,
assertions.txt / assertions-detail.json. Harness scripts are
ab-p-path.mjs, ab-streamjson.mjs, ws-probe.mjs, boundary-probe.mjs,
mutation-matrix.mjs, validate-fix.sh, print-ab.mjs, assertions.mjs
all rerunnable as-is.

Flakiness gate log

rounds=5 files=2 skipped=0
file packages/cli/src/llm.test.tsx: (cd packages/cli) npx --no-install vitest run ./src/llm.test.tsx
file packages/cli/src/nonInteractive/session.test.ts: (cd packages/cli) npx --no-install vitest run ./src/nonInteractive/session.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/cli/src/llm.test.tsx: PPPPP
  packages/cli/src/nonInteractive/session.test.ts: PPPPP

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

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/llm.test.tsx: P (exit 0)
round 1 · packages/cli/src/nonInteractive/session.test.ts: P (exit 0)
round 2 · packages/cli/src/llm.test.tsx: P (exit 0)
round 2 · packages/cli/src/nonInteractive/session.test.ts: P (exit 0)
round 3 · packages/cli/src/llm.test.tsx: P (exit 0)
round 3 · packages/cli/src/nonInteractive/session.test.ts: P (exit 0)
round 4 · packages/cli/src/llm.test.tsx: P (exit 0)
round 4 · packages/cli/src/nonInteractive/session.test.ts: P (exit 0)
round 5 · packages/cli/src/llm.test.tsx: P (exit 0)
round 5 · packages/cli/src/nonInteractive/session.test.ts: P (exit 0)

Evidence images

01-ab-promptid-base-vs-head

02-mutation-matrix-kills-and-survivors

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

Qwen Code · sandboxed verification

qqqys
qqqys previously approved these changes Sep 9, 2026

@qqqys qqqys 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-only review at head 581ac5bce98aed43fa2dbb9668faeaa3f42ead4f (base main). Approving: no blocking finding stands at this head, and the one blocking item raised in the thread is resolved.

Historical blocking item — resolved

This PR has no reviews and no inline review comments, so the only blocking item on record is the one in the triage thread: Lint & Static (ubuntu-latest, Node 22.x) was red at the previous commit because Prettier named session.ts and session.test.ts. At this head that lane is green — of the 23 check runs on this commit, the only one not concluded successfully is review-pr, still in progress. So the formatting blocker is closed and nothing else is red.

Critical-only scan — nothing blocking

I read both production hunks in full (llm.tsx +24/−3, nonInteractive/session.ts +26/−1) and then verified the one thing the whole change rests on: whether resumed data is actually readable at the moment each entry point seeds from it.

Ordering holds at both entry points, checked in the head files rather than taken from the comments:

  • -p: await config.initialize() runs at llm.tsx:1352 and createNonInteractivePromptId(...) is called at :1424, so config.getResumedSessionData() is populated when the id is minted and the seed is live rather than a no-op.
  • stream-json: getNextPromptId() has exactly three call sites — session.ts:503, :598 and :663 — and each is preceded by await this.waitForInitialization() at :501, :596 and :637. That matters more than it looks, because the counter caches on first use: a single early call would pin the wrong base for the whole process lifetime and reproduce the very collision this PR removes. The lazy-seed comment's claim matches the code.

A run that resumes nothing keeps the ids it mints today. createNonInteractivePromptId returns ${sessionId}########0 when there are no resumed records, and the Session counter seeds to 0 and pre-increments, so the first stream-json id is still ########1. The common fresh-session path is byte-identical in behavior, which is the regression risk worth checking first.

The two paths cannot drift. Both delegate the "highest turn the transcript claims, else the count of resumed user turns" decision to the same core helper computeInitialTurnFromHistory, and their arithmetic agrees: a transcript claiming turn 5 yields ########6 on both (seed 5 then increment, and lastTurn + 1). Both imports resolve — the barrel for llm.tsx, the deep services/session-turn-state.js path for session.ts — which the green typecheck in Lint & Static corroborates, along with the number | null counter narrowing.

The reads are defensive: config.getResumedSessionData?.()?.conversation.messages at both sites, and the helper is only called when the records array is non-empty, so a Config that cannot answer yields no seed instead of throwing during startup.

CI

Green at this head: Test (ubuntu-latest, Node 22.x) — the lane that runs the two touched suites — Lint & Static, Integration Tests (no-AK, No Sandbox), the Desktop Shell lanes, Classify PR, label, assign, authorize and the rest; 23 check runs in total with only review-pr still in progress, which is not a gate. The author-requested sandboxed verification run was still in flight at the time of this review and is not treated as a gate either.

Recorded, not gating: when computeInitialTurnFromHistory yields 0 for a resumed transcript, the -p path returns ########0 rather than continuing past it, which is narrower than its own doc comment claims; I could not construct a reachable case (a -p turn records a user prompt, so the fallback count is at least 1 whenever a ########0 id exists), and the stream-json arm has no equivalent gap. Likewise the stream-json branch of the -p call site computes an id it does not use, and with no resumed records that value equals today's, so neither is a defect worth blocking on.

中文说明

在 head 581ac5bc 上执行 Critical-only 评审,结论为 Approve:本 head 上不存在阻塞发现,线程中唯一一条阻塞项已解决。

历史阻塞项已解决: 本 PR 没有任何 Review 与行内评审评论,因此记录在案的唯一阻塞项来自 triage 线程——上一个提交因 Prettier 点名 session.tssession.test.ts 而使 Lint & Static (ubuntu-latest, Node 22.x) 变红。在本 head 上该检查为绿:该提交共 23 条检查,唯一未成功结束的只有仍在运行的 review-pr。格式阻塞已关闭,且没有其他红。

Critical-only 扫描: 我完整读了两处生产改动(llm.tsx +24/−3、nonInteractive/session.ts +26/−1),随后核实了整个改动所依赖的那一点——两个入口在播种时 resume 数据是否真的可读。两条路径的时序都成立,且是我在 head 文件里核对的、不是采信注释:-p 路径的 await config.initialize()llm.tsx:1352,而 createNonInteractivePromptId(...):1424 调用,因此铸 id 时 config.getResumedSessionData() 已有数据,播种是生效的而不是空操作;stream-json 路径的 getNextPromptId() 只有三个调用点(session.ts:503:598:663),每个之前都有 await this.waitForInitialization():501:596:637)。这一点比看上去更重要:计数器首次使用即缓存,只要有一次提前调用就会把错误基线钉死在整个进程生命周期里,从而复现本 PR 要消除的重复 id;惰性播种注释的说法与代码一致。

不 resume 的运行保持今天的 id: 无 resume 记录时 createNonInteractivePromptId 返回 ${sessionId}########0Session 的计数器种子为 0 且先自增,因此首个 stream-json id 仍是 ########1;占绝大多数的全新会话行为逐字不变,这是首先要查的回归风险。两条路径不会漂移: 二者都把「取转录声明的最高轮次,否则数 resume 的用户轮次」交给同一个 core helper computeInitialTurnFromHistory,算术也一致——声明到第 5 轮时两条路径都得 ########6(种子 5 后自增,以及 lastTurn + 1)。两种导入方式都能解析(llm.tsx 走 barrel,session.tsservices/session-turn-state.js 深路径),Lint & Static 里的 typecheck 变绿也佐证了这一点,同时佐证了 number | null 计数器的收窄。读取是防御性的: 两处都用 config.getResumedSessionData?.()?.conversation.messages,且只有记录数组非空时才调用 helper,因此无法应答的 Config 只会得到「无种子」而不会在启动期抛错。

CI: 本 head 全绿——含会跑两个被改套件的 Test (ubuntu-latest, Node 22.x)Lint & Static、no-AK 集成、两端 Desktop Shell、Classify PRlabelassignauthorize 等,共 23 条检查中仅 review-pr 仍在运行,不作为门禁;作者请求的沙箱验证在评审时仍在进行,同样不作为门禁。

仅记录、不作为门禁:computeInitialTurnFromHistory 对某个 resume 转录返回 0 时,-p 路径会返回 ########0 而不是接续其后,这比它自己的文档注释所述更窄;我构造不出可达场景(一次 -p 轮次总会记录用户提示,因此只要存在 ########0 这个 id,回退计数至少为 1),stream-json 一侧也没有同类缺口。同样,-p 调用点的 stream-json 分支会算出一个它并不使用的 id,而在无 resume 记录时该值与今天相同,因此二者都不构成值得阻塞的缺陷。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully. Run review failed. See workflow logs for details. A transient error is retried automatically; if you are seeing this, retry with @qwen-code /review. See workflow logs.

@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /review

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Qwen Code review request accepted. Review is running in workflow run. A command-triggered review is not listed under the checks of this PR; the result is posted here as a review when it finishes.

Sandboxed verification (`/verify` on this PR) proved the central claim on both
entry points via a base/head A/B over real resumed processes, and reported
three things worth acting on.

1. The `-p` guard re-opened the collision it closes. `lastTurn > 0 ? lastTurn
   + 1 : 0` re-minted `########0` whenever `computeInitialTurnFromHistory`
   returns 0 for a non-empty transcript — highest claimed turn 0 and no
   record with non-blank user text for its fallback to count, reachable with
   `-p '   '` since only a falsy input is rejected. Seed -1 for a run that
   resumes nothing instead, so the shared `+ 1` keeps the historical
   `########0` there and every resumed shape continues past what the
   transcript claims. This makes the rule identical to the stream-json one.

2. The `-p` call site was unpinned: deleting the `getResumedSessionData()`
   argument left the whole suite green while the shipped path reverted to
   `########0` on every resume. Add a `main()`-level test that stubs resumed
   data on the config and asserts the promptId reaching `runNonInteractive`,
   plus a fixture for the claimed-turn-0 boundary above.

3. The stated rationale was wrong about the consequence. File-history
   snapshots are NOT dropped on these paths: `fileCheckpointingEnabled`
   defaults to `!sdkMode && interactive` and nothing in packages/cli
   overrides it, so `makeSnapshot` no-ops and headless turns write no
   snapshots at all (verified: 10 headless processes, 6 real write_file
   executions, 0 snapshot records). What duplicate ids actually cost is the
   key #9466's rewind mapping anchors on and the `prompt_id` on persisted
   `ui_telemetry` records — which is what the next resume reads back to seed
   from. Both doc comments now say that instead. The same round also found
   the claim that interactive "seeds the same way" inaccurate: AppContainer
   counts resumed user turns inline and never consults the claimed turns.

Refs #11408
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Thanks — that run earned its runtime. 8d85961 acts on all three items.

Finding 1 (-p guard re-opened the collision) — fixed, exactly as suggested. lastTurn > 0 ? lastTurn + 1 : 0 became a -1 seed for a run that resumes nothing plus an unconditional + 1, so the fresh-run output is still ########0 and every resumed shape continues past what the transcript claims. That also makes the -p rule identical to the stream-json one, which is the property that stops the two paths disagreeing on the same input. Shipped with the fixture you asked for: a transcript whose highest claimed turn is 0 and whose only user record has blank text now asserts ########1.

Finding 2 (call site unpinned, M6) — closed. Added a main()-level test that stubs getResumedSessionData on the config and asserts the promptId reaching runNonInteractive is ########4 for a transcript claiming turn 3. Deleting the argument at llm.tsx:1424 now reds that test rather than leaving the suite green.

Correction 1 (headless writes no file-history snapshots) — accepted, and it was the load-bearing claim in my description. I re-derived it rather than taking it on faith: fileCheckpointingEnabled = params.fileCheckpointingEnabled ?? (!params.sdkMode && (params.interactive ?? false)) (config.ts:2713), no caller in packages/cli passes that param, and makeSnapshot returns early when disabled (fileHistoryService.ts:725). So the /rewind-corruption framing was wrong for the paths this PR touches. Both doc comments and the PR description now state the consequence that does hold — the key #9466's rewind mapping anchors on, and the prompt_id on persisted ui_telemetry records, which is what the next resume reads back to seed from. The Reviewer Test Plan's end-to-end step has been rewritten to your oracle (transcript prompt ids), since the one it named is not observable on a headless chain.

Correction 2 (interactive does not seed "the same way") — comment fixed. It now says ACP seeds through this helper and interactive seeds by its own inline count of resumed user turns, which ignores the claimed turns. I left AppContainer alone as out of scope.

Not taken: M5 (the resumedRecords?.length guard) — agreed it is redundant defence, and it still reads better than relying on the helper's behaviour for an empty array. The 0, 2, 3, 4… non-contiguity and the 2^53 precision note are recorded and unchanged.

One caveat on this commit: I still cannot run the suite locally, so the two new tests are unverified until CI reports — the main()-level one copies the scaffolding of the existing writes non-interactive warnings discovered during config initialization test, which drives the same headless path.

@qqqys sorry for the dismissed approval — the push was to fix a real defect this verification found in the -p half, not a cosmetic follow-up.

中文说明

三条都已处理,见 8d85961

Finding 1(-p 守卫重新打开了它要关闭的冲突)——已按建议修复。 lastTurn > 0 ? lastTurn + 1 : 0 改为「不 resume 时种子取 -1」加统一的 + 1:全新运行仍输出 ########0,而所有 resume 形态都会接续 transcript 已声明的轮次之后。这也使 -p 的规则与 stream-json 完全一致,从根本上消除两条路径对同一输入给出不同结论的可能。并按要求补了 fixture:最大声明轮次为 0、唯一用户记录文本为空白的 transcript,现在断言 ########1

Finding 2(调用点未被钉住,M6)——已关闭。 新增 main() 级测试:在 config 上桩掉 getResumedSessionData,断言到达 runNonInteractive 的 promptId 为 ########4(transcript 声明到第 3 轮)。现在删掉 llm.tsx:1424 的实参会让该测试变红,而不是全绿放行。

Correction 1(headless 根本不写 file-history 快照)——接受,且这正是我描述里最吃重的论断。 我重新推导而非直接采信:fileCheckpointingEnabled = params.fileCheckpointingEnabled ?? (!params.sdkMode && (params.interactive ?? false))config.ts:2713),packages/cli 中无人传该参数,makeSnapshot 在禁用时直接返回(fileHistoryService.ts:725)。因此「破坏 /rewind」的说法对本 PR 涉及的路径是错的。两处文档注释与 PR 描述现已改为真正成立的后果——#9466 的 rewind 映射所锚定的键,以及持久化在 ui_telemetry 上的 prompt_id(下一次 resume 正是读它来播种)。Reviewer Test Plan 的端到端步骤也改用了你的 oracle(transcript 里的 prompt id),因为它原本写的那个在 headless 链上不可观测。

Correction 2(交互模式并非「同样方式」播种)——注释已修正。 现在写明:ACP 通过本 helper 播种,交互模式则用它自己的「恢复的用户轮次计数」,并不考虑 transcript 声明的轮次。AppContainer 本身不在本 PR 范围内,未改动。

未采纳: M5(resumedRecords?.length 守卫)——同意它是冗余防御,但显式写出仍比依赖 helper 对空数组的行为更易读。0, 2, 3, 4… 的非连续性与 2^53 精度两条已记录,不做改动。

本次提交的一点说明:我仍无法在本地跑测试套件,因此两个新测试在 CI 报结果前属于未验证——其中 main() 级测试沿用了现有 writes non-interactive warnings discovered during config initialization 测试的脚手架,走的是同一条 headless 路径。

@yiliang114
yiliang114 requested a review from qqqys September 9, 2026 08:27

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

No blocking findings.
Approval blockers: none.

Checked

  • computeInitialTurnFromHistory (packages/core/src/services/session-turn-state.ts): accumulates maxPromptTurn from ui_telemetry records and promptId fields, falls back to userMessageCount — logic correct.
  • Lazy seeding in session.ts getNextPromptId(): all three call sites (processUserMessage, processContinueTurn, processMonitorNotificationBatch) call await this.waitForInitialization() before getNextPromptId(), so config.initialize() — which populates getResumedSessionData — is guaranteed to have run before the counter is seeded.
  • createNonInteractivePromptId in llm.tsx: called after config.initialize() in the -p path; for the stream-json path, the computed prompt_id is unused (stream-json manages its own counter via session.ts) — intentional and consistent.
  • Tests cover the three required cases: fresh session (########1 / ########0), resumed with telemetry (max claimed turn + 1), resumed without telemetry (user turn count + 1).
  • CONTRIBUTING.md at base SHA: no AI-review ban.

Not reviewed

  • No working tree available; unit tests not executed locally — CI is the execution witness.
  • getResumedSessionData implementation in the core Config class not traced; callers in ACP and interactive mode are untouched by the diff.

Cross-check

  • One prior review from qqqys (dismissed) at the same head confirmed no blockers and that the earlier Prettier formatting issue was fixed at commit 581ac5b; my independent findings are consistent.

Reviewed with AI assistance.

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM✅

@yiliang114
yiliang114 added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit 5f65099 Sep 9, 2026
91 of 92 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.23.2.

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.

Deferred review findings from PR #9466: refactor: anchor rewind mapping to stable prompt identity

6 participants