Skip to content

feat(web-shell): add headless global turn navigation - #11054

Merged
doudouOUC merged 6 commits into
mainfrom
codex/web-shell-global-turn-navigation-phase2a
Sep 6, 2026
Merged

feat(web-shell): add headless global turn navigation#11054
doudouOUC merged 6 commits into
mainfrom
codex/web-shell-global-turn-navigation-phase2a

Conversation

@doudouOUC

@doudouOUC doudouOUC commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds Phase 2A of session-wide turn navigation as a headless Web Shell data layer. It introduces bounded turn metadata and historical transcript caches, exact live/persisted turn locators, prompt reconciliation, and public React hooks for the later virtual rail. The existing live transcript and sequential history UI remain unchanged.

  • Loads the newest turn metadata page first and supports random access by durable ordinal.
  • Keeps historical pages separate from the connected live tail and reconciles queued/local echoes by exact record or prompt identity, independent of event arrival order.
  • Bounds turn metadata to 16 pages / 4 MiB and historical transcript data to 5 pages / 16 MiB.
  • Slides the historical cache in either direction while preserving the selected page, and recovers evicted gaps on demand, including gaps containing only assistant/tool records.
  • Distinguishes retryable selection-constrained cache pressure from an individually oversized page, without disabling session-wide navigation.
  • Rejects stale async results across session, client, chain, selection, reconnect, rewind, and snapshot changes.
  • Preserves the loaded-only path when session_turn_navigation is unavailable or the transcript is too large to index.
  • Updates the design status and Phase 2 implementation plan.

Why it's needed

The current compact rail can only represent turns already materialized in the live transcript, so restoring a bounded transcript cannot provide navigation across a long session. Phase 1 added the daemon index and anchored reads; this PR consumes those primitives to jump directly to an indexed turn without loading every intervening turn or replacing the live stream. The author-approved gap recovery also makes historical traversal reversible after cache eviction. This establishes the bounded client foundation for the later virtualized rail.

Reviewer Test Plan

How to verify

  1. With session_turn_navigation advertised, confirm the exact durable turn count comes from the newest metadata page while the live transcript stays connected.
  2. Select unloaded first, middle, and last ordinals and confirm each resolves through a bounded anchored read while the live tail remains active.
  3. Move the selection with the historical window and traverse beyond its page cap in both directions. Reverse direction and confirm evicted records return chronologically without duplicates, including long turns whose gap has no user prompt. The selected page must stay retained. If the pinned selection prevents admission, move it and retry successfully.
  4. Queue a prompt and vary admission, display echo, and index arrival order. The same prompt should resolve to its live block and never appear again in historical pages. Also exercise a started event whose local queue row is already absent.
  5. Exercise reconnect, rewind, rapid selection, snapshot replacement, 409, and 413 paths. Stale requests must not mutate the active session. A repaired boundary must not retain its obsolete error, while an independent metadata failure must remain retryable.
  6. Connect to a daemon without the capability. No navigation requests should be issued and the existing loaded-only behavior must remain available.

Evidence (Before & After)

Before: navigation is limited to loaded blocks; the initial Phase 2A cache could permanently reject backward traversal at its cap, and queue echoes could miss exact live reconciliation.

After: the headless navigation model supports bounded session-wide lookup, recoverable bidirectional traversal, and exact live identity. This phase intentionally introduces no visible UI change.

Local verification: 717 focused tests across seven files; repository build, bundle, typecheck, changed-file lint/format, and diff checks. Independent headless probes exercised the real transcript reader, signed cursors, and SDK reducer, including ten traversal/overlap/cancellation scenarios. Hook-to-SDK-to-store probes covered unmatched started events. Four targeted mutation checks and the fallback-identity revert reddened their regressions. The rebuilt CLI also passed an isolated daemon health/UI-serving smoke check. This is not full browser E2E validation.

Additional review-driven validation passed the full Web Shell suite (268 files / 6,107 tests). The expanded CLI suite timed out, including local Git-hook network timeouts; the VS Code companion suite was not run after the shared budget expired. No full-repository test-suite success is claimed.

Native local review reached its 30-minute limit without a composed verdict. Two non-blocking dedicated-guard-test suggestions from completed reports are recorded as follow-ups in the fix summary. No complete review/approval is claimed.

Tested on

OS Status
🍏 macOS
🪟 Windows CI only
🐧 Linux CI only

Environment (optional)

Local repository toolchain with Node.js 22 or newer.

Risk & Scope

  • Main risk or tradeoff: State-management changes are isolated to the Web Shell client. Capability gating, owner/generation checks, fixed cache budgets, and regression coverage constrain the risk. Recovering a backward-only gap can require several bounded reads from the original frozen turn anchor; recovery retains only the current response and its immediate newer candidate. Direct random turn jumps remain anchored reads.
  • Not validated / out of scope: Phase 2B migration of the existing sequential history UI, the Phase 3 virtualized rail, and real-browser accessibility/performance E2E coverage. No daemon or SDK protocol expansion is included.
  • Breaking changes / migration notes: None. Older daemons retain the loaded-only behavior.

Linked Issues

Tracks #10750 without closing it because Phase 2B and Phase 3 remain.

中文说明

本 PR 做了什么

实现会话级全局 turn 导航的 Phase 2A:在 Web Shell 中加入无界面、内存有界的客户端数据层,包括 turn 元数据和历史 transcript 缓存、精确的实时/持久化 turn 定位、prompt 对账及供后续虚拟化导航条使用的公开 React hooks。现有实时 transcript 与顺序加载历史记录的 UI 保持不变。

  • 首次加载最新一页 turn 元数据,支持按持久化 ordinal 随机访问。
  • 历史页与保持连接的实时尾部隔离,排队/本地回显只按精确 record 或 prompt 身份对账,不依赖事件到达顺序。
  • turn 元数据限制为 16 页 / 4 MiB,历史 transcript 限制为 5 页 / 16 MiB。
  • 历史缓存可双向滑动且保留选中页;按需恢复逐出后的缺口,包括仅有 assistant/tool 记录的缺口。
  • 区分选中页导致的可重试窗口压力和单页过大,不因此禁用整个会话的全局导航。
  • 在 session、client、chain、selection、重连、rewind 和 snapshot 变化时丢弃过期异步结果。
  • daemon 未提供 session_turn_navigation 或 transcript 过大而无法索引时,保留仅基于已加载内容的路径。
  • 更新设计状态和 Phase 2 实施计划。

为什么需要

当前紧凑导航条只能表示已物化在实时 transcript 中的 turn,有界恢复无法覆盖整个长会话。Phase 1 已提供 daemon 索引和锚点读取能力;本 PR 消费这些能力,直接跳到索引中的 turn,无需加载所有中间 turn,也不替换实时流。经作者同意纳入的缺口恢复使缓存逐出后的历史遍历可以反向继续,为后续虚拟化全局导航条建立有界客户端基础。

Reviewer 测试计划

如何验证

  1. daemon 声明 session_turn_navigation 时,确认最新元数据页提供精确的持久化 turn 总数,实时 transcript 仍保持连接。
  2. 选择尚未加载的首个、中间和最后一个 ordinal,确认通过有界锚点读取定位,实时尾部仍接收数据。
  3. 随历史窗口移动选择,双向遍历超过缓存页数上限,再反向返回;确认逐出的记录按时间顺序恢复、无重复,也覆盖缺口内没有用户 prompt 的长 turn。选中页必须保留;若选中页阻止准入,移动选择后应能重试成功。
  4. 排队一条 prompt,改变准入、展示回显和索引的到达顺序;应始终定位到同一个实时块,不再重复出现在历史页中。也覆盖本地队列条目已不存在时收到开始事件的情况。
  5. 覆盖重连、rewind、快速切换选择、snapshot 替换、409413。过期请求不得修改当前 session。边界恢复后不能保留旧错误,独立的元数据错误则必须仍可重试。
  6. 连接不支持该能力的 daemon,确认没有导航请求,原有仅基于已加载内容的行为仍可用。

证据(Before & After)

Before:导航仅覆盖已加载的块;初版 Phase 2A 在缓存上限处可能永久拒绝向旧方向遍历,队列回显也可能无法精确匹配实时身份。

After:无界面导航模型提供内存有界的会话级定位、可恢复的双向遍历和精确实时身份。本阶段刻意不引入可见 UI 变化。

本地验证包括七个文件的 717 个聚焦测试,仓库 build、bundle、typecheck、改动文件 lint/format 和 diff 检查。独立无界面探针使用真实 transcript reader、签名游标和 SDK reducer,覆盖十个遍历、重叠及取消场景;hook→SDK→store 探针覆盖无本地队列条目的开始事件。四项定向变异检查和回显身份回退均使对应回归测试变红。重新构建的 CLI 也通过隔离 daemon 的健康检查及 UI 服务冒烟验证。以上不是完整浏览器 E2E 验证。

额外审查中,Web Shell 全量测试通过(268 个文件 / 6,107 项)。扩展的 CLI 测试超时,其中出现本机 Git 钩子的网络超时;共享预算耗尽后未运行 VS Code companion 测试。不宣称全仓测试套件通过。

本地原生 review 达到 30 分钟上限,未形成汇总结论;已完成报告中的两项非阻塞守卫测试建议已在修复总结中记录为后续项。不宣称完整审查通过或批准。

测试平台

OS 状态
🍏 macOS
🪟 Windows 仅 CI
🐧 Linux 仅 CI

环境(可选)

本地仓库工具链,Node.js 22 或更高版本。

风险与范围

  • 主要风险或权衡:状态管理改动限制在 Web Shell 客户端,通过能力门控、owner/generation 校验、固定缓存预算及回归测试约束风险。反向读取产生的缺口可能需要从原始冻结 turn 锚点做多次有界读取;恢复只保留当前响应及紧邻的较新候选页。直接随机跳转仍使用锚点读取。
  • 未验证 / 不在范围内:Phase 2B 对现有顺序历史 UI 的迁移、Phase 3 虚拟化导航条,以及真实浏览器的无障碍/性能 E2E。未扩展 daemon 或 SDK 协议。
  • 破坏性变更 / 迁移说明:无。旧版 daemon 保留仅基于已加载内容的行为。

关联 Issue

跟踪 #10750,但不自动关闭,因为 Phase 2B 与 Phase 3 尚未完成。

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

⚠️ Qwen Triage ended earlyview run. It stopped before finishing; check the run log.

⚠️ Qwen Triage 提前结束 —— 查看运行。未跑完,请查看运行日志。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: Real and already tracked, not theoretical. #10750 is open with status/in-progress and roadmap/session-management; the Phase 1 server contract landed in #10751 and the design in #11020. I verified the gap independently rather than taking the description's word for it: on main, getTurnIndexPage has zero callers anywhere under packages/web-shell/client/ — the daemon-side turn index is advertised and implemented but nothing consumes it. So "the client cannot navigate beyond its loaded window" is an observed absence, not a hypothesised one.

Direction: Aligned in principle, but there is a collision you need to resolve before anyone spends review time here.

#11053 is an open, competing implementation of this same phase, by you, created one minute before this PR. The two do not overlap at the edges — they are the same change twice:

#11053 #11054 (this PR)
New modules turnIndexStore.ts (619), transcriptPageLedger.ts (548) turn-navigation-store.ts (1246), transcript-page-table.ts (842)
Provider delta +844 / −49 in DaemonSessionProvider.tsx +202 / −20 in DaemonSessionProvider.tsx
Production 2,134 lines 2,408 lines
Design doc untouched phase boundaries rewritten

Only one can land; they will conflict textually and semantically in the provider.

The deeper disagreement is about scope, and it is worth being explicit about: this PR rewrites the phase boundaries of an Accepted design document to match what it implemented. The merged design put "turn-index store, tail refresh, provisional reconciliation, canonical locator map" in Phase 3; this diff moves all four into Phase 2 step 1, splits the remainder into a new "2B", and flips the status line to "Phases 1 and 2A implemented". #11053 implements Phase 2 as the design was actually merged and leaves the document alone. So the two PRs do not just duplicate work — they disagree about what Phase 2 is, and one of them is retrofitting the design to the code. That is backwards for a doc marked Accepted, and it needs a deliberate call rather than whichever PR merges first.

Size: The Stage 0 core-module gate does not apply — nothing matches packages/core/src/** or the packages/*/src/{auth,providers,models,config,tools,services}/** patterns (these are client/daemon/session/, client/constants/, plus two barrel files), and the change stays inside a single package, so it is not cross-package. The non-blocking large-PR advisory does: 2,408 production lines vs 2,063 test lines and 638 docs lines, 0 generated/schema. At that scale splitting is worth considering if still feasible — though see Approach, because the more interesting cut here is not size but consumer-less surface.

Approach: This is my main reservation, and it is a scope question rather than a code-quality one. Of those 2,408 production lines, 842 (transcript-page-table.ts) plus the entire random-access path have no production caller at all. I grepped the head for read sites, not just the declarations: locateOrdinal, loadOrdinal, loadOlder, loadNewer, admitAnchor, admitBoundary appear only in the two new test files and in the store's own internal retry(). The two new public hooks and eleven exported types likewise have no non-test reader. What actually executes in a real session is refreshHead() — I detail the consequence in the review comment.

So the honest version of "cut 80% of the scope" is: the head index plus the locator map is the part that runs; the page table is the part Phase 3 needs and nothing else does. If the intent is to land the plumbing separately for reviewability — a fair trade, and the one #11053 argues explicitly — then the page table is arguably Phase 3's to carry, and landing it here means reviewing 842 lines of eviction, boundary-caching and rollback logic against unit tests only, with no integration point to check it against. Worth deciding consciously rather than by accretion.

Nothing here reads as drive-by churn: the diff is on-topic throughout, and the two barrel-file edits and the capability constant are the minimum wiring the new module needs.

Risk: No elevated risk signals — none of the changed files match the revert-correlated high-risk path set. The real risk is process, not code: two competing implementations open at once, and an Accepted design doc being edited by one of them.

Flagging these for discussion before diving deeper — the code review is in the next comment, but the duplicate is the thing that needs answering first.

中文说明

感谢贡献!

模板完整 ✓

问题: 真实且已被跟踪,不是理论性问题。#10750 处于 open 且带 status/in-progressroadmap/session-management;Phase 1 服务端契约在 #10751、设计文档在 #11020 已合并。我没有只采信 PR 描述,而是独立核实了这个缺口:在 main 上,getTurnIndexPagepackages/web-shell/client/没有任何调用方——daemon 侧的 turn index 已经广播并实现,但客户端完全没消费。所以"客户端无法在已加载窗口之外导航"是一个可观测的缺失,而不是假设。

方向: 原则上对齐,但在有人投入 review 之前,有一个冲突需要你先解决。

#11053 是同一个 phase 的另一个竞争性实现,同样由你提交,比本 PR 早一分钟创建。两者不是边缘重叠——而是同一件事做了两遍:#11053 新增 turnIndexStore.ts(619 行)与 transcriptPageLedger.ts(548 行),改动 DaemonSessionProvider.tsx +844/−49,生产代码 2,134 行,不动设计文档;本 PR 新增 turn-navigation-store.ts(1246 行)与 transcript-page-table.ts(842 行),改动同一文件 +202/−20,生产代码 2,408 行,并重写了设计文档。两者只能落地一个,且在 provider 里会产生文本与语义双重冲突。

更深层的分歧在于范围,值得说清楚:本 PR 重写了一份状态为 Accepted 的设计文档的 phase 边界,以匹配它自己实现的内容。已合并的设计把"turn-index store、tail refresh、provisional reconciliation、canonical locator map"放在 Phase 3;本 diff 把这四项全部挪进 Phase 2 第 1 步,把余下部分拆成新的 "2B",并把状态行改成 "Phases 1 and 2A implemented"。而 #11053 是按设计文档原本合并时的 Phase 2 来实现的,且完全不碰该文档。所以这两个 PR 不只是重复劳动——它们对"Phase 2 是什么"存在分歧,而其中一个是在用代码反向修改设计。对一份标记为 Accepted 的文档来说这是本末倒置的,需要一个明确的决定,而不是看哪个 PR 先合并。

规模: Stage 0 核心模块门禁不适用——没有任何文件匹配 packages/core/src/**packages/*/src/{auth,providers,models,config,tools,services}/**(这些是 client/daemon/session/client/constants/ 加两个 barrel 文件),且改动限定在单个 package 内,不属于跨包变更。非阻塞的大 PR 提示适用:2,408 行生产代码 vs 2,063 行测试代码638 行文档,0 行生成/schema 代码。这个体量下如果还可行,值得考虑拆分——但见"方案",这里更值得切的不是行数,而是没有消费者的接口面。

方案: 这是我最主要的保留意见,属于范围问题而非代码质量问题。在 2,408 行生产代码中,842 行(transcript-page-table.ts)加上整条随机访问路径完全没有任何生产调用方。我查的是 head 上的读取点,而不只是声明处:locateOrdinalloadOrdinalloadOlderloadNeweradmitAnchoradmitBoundary 只出现在两个新测试文件以及 store 自身的内部 retry() 里。两个新的公开 hook 与十一个导出类型同样没有非测试的读取方。真实会话中实际执行的只有 refreshHead()——其后果我在 review 评论里展开。

所以"砍掉 80% 范围"的诚实版本是:head 索引加 locator map 是真正会跑的部分;page table 是 Phase 3 需要、而其他任何地方都不需要的部分。如果意图是为了可评审性而单独落地管道层——这是合理的取舍,也是 #11053 明确论证过的——那么 page table 按理应该由 Phase 3 携带,在这里落地意味着只能靠单元测试来 review 842 行的淘汰、边界缓存与回滚逻辑,而没有任何集成点可以对照检验。这值得有意识地决定,而不是让它自然堆积。

这里没有夹带无关改动:整个 diff 都紧扣主题,两个 barrel 文件的改动和 capability 常量已经是新模块所需的最小接线。

风险: 无升级风险信号——所有改动文件都不匹配与 revert 相关的高风险路径集合。真正的风险是流程上的,不是代码上的:同时开着两个竞争实现,而其中一个正在修改一份 Accepted 的设计文档。

先把这些提出来讨论,再深入看代码——代码审查在下一条评论里,但重复 PR 是需要先回答的问题。

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

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code review

My independent proposal first (written from the title and "Why it's needed" before reading the diff): Phase 1 already ships a paged turn index and an anchored transcript read, so the minimal client layer is a capability gate, one small store holding the durable turn count plus a bounded ring of loaded index pages, one selectTurn(ordinal) that fetches a single anchored page, a monotonic generation counter compared after every await to drop stale results, and one hook — wired into the provider as an extra context value with the live transcript path untouched. Call it 300–500 production lines.

How this compares. The PR's store exceeds my proposal on exactly the axis that matters and I would not want to give up: staleness rejection is done properly, with three separate epochs (sessionEpoch, chainEpoch, selectionGeneration) plus owner-identity comparison via isCurrentClient, checked after every await in refreshHead, loadOrdinal, locateOrdinal and loadBoundary. That is the part of a design like this that is easy to get subtly wrong, and it is right here. The head-compatibility check in admitHead (shared ordinals must keep their turnId, and a turnId must not move ordinal, else resetChain()) is a better answer than anything I had in mind for detecting that the durable index shifted underneath a cached page. observeLiveBlocks reuses the existing isSameTranscriptStructure idiom from useAnimationFrameTranscriptBlocks.ts instead of inventing a parallel dedup heuristic, and the live-location check runs before admitAnchor, so a turn that is already live cannot be re-materialised into a historical page. Dedup is by exact persisted record identity as claimed. Filenames are kebab-case, matching daemon-react-sdk.ts in the same tree. Where it exceeds my proposal is 5× the size, and most of that delta is the page table — which brings me to the findings.

I found no correctness blocker in the code. Three things worth naming, in descending order of how much they matter:

1. Most of this PR does not run. Verified by grepping read sites on the head, not declarations: locateOrdinal, loadOrdinal, loadOlder, loadNewer, admitAnchor and admitBoundary have no production caller — only the two new test files and the store's internal retry(). Consequently all 842 lines of transcript-page-table.ts (eviction, boundary caching, cachedBoundaryRequests rollback, the admitBoundary empty-page restore path) are exercised solely by unit tests, and the 16 MiB historical budget can never be reached in a real session because nothing ever admits a historical page. The two new public hooks and the eleven types re-exported from daemon-react-sdk.ts have no non-test reader either. This is the shape AGENTS.md's review rules call a dead switch, at module scale: a large surface declared, exported and tested, but never invoked by anything a user can reach.

2. What does run has a cost, and it is easy to miss. configure() calls refreshHead() whenever the capability is present and a client is connected, and handleSessionEvent() calls it again on every turn_complete, turn_error and replay_complete. session_turn_navigation is a baseline tag in SERVE_CAPABILITY_REGISTRY (packages/cli/src/serve/capabilities.ts:144) and is not in CONDITIONAL_SERVE_FEATURES, so every daemon advertises it unconditionally. Net effect once merged: every Web Shell session against a current daemon issues a 200-entry turn-index fetch on connect and another after every completed turn, indefinitely, with no UI reading the result. I want to be accurate about the magnitude rather than alarmist — steady-state retention is a single index page, i.e. kilobytes for 200 short labels; the 4 MiB and 16 MiB figures are ceilings that only random access can approach, and random access is finding 1. So the cost is one extra round trip per turn and a few KB held, not a memory regression. It is still recurring work shipped for a benefit that does not arrive until Phase 3, which is the strongest argument for landing 2A together with the rail rather than ahead of it.

3. fallbackReason: 'too_large' is overloaded and sticky. recordPromptAdmitted() calls enterTooLargeFallback() once provisionals.length >= indexPageSize — 200 prompts admitted but not yet present in the index. That condition has nothing to do with an oversized transcript, yet it sets tooLarge = true and publishes mode: 'legacy', fallbackReason: 'too_large'. It is also near-permanent: tooLarge is cleared only by resetForSession() (a session switch), and handleSessionEvent('session_rewound') deliberately preserves it through resetChain(). A Phase 3 rail branching on fallbackReason === 'too_large' would tell the user their transcript is too big to navigate when what actually happened is that provisional reconciliation fell behind. Non-blocking — the daemon does populate promptId on index entries (packages/core/src/services/session-transcript-reader.ts:2613, fed from turnResultPromptId at line 2184) and refreshHead() runs every turn, so reaching 200 unindexed provisionals should not happen — but the label will mislead whoever reads it next, and a distinct reason such as provisional_overflow would cost one union member.

One smaller note, not a finding: actions.ts derives optimisticBlockId positionally via store.getSnapshot().blocks.at(-1)?.id immediately after the synchronous appendLocalUserMessage, which is correct today. It is coupled to that append producing exactly one trailing block; if it ever produces two, the alias points at the wrong block and observeLiveBlocks will keep it, since it only prunes aliases whose blockId no longer exists among user blocks. Fine as-is, worth knowing.

sequenceDiagram
    participant P1 as Web Shell provider
    participant P2 as turn navigation store
    participant P3 as daemon session client
    participant P4 as historical page table
    Note over P1,P4: This half runs in production today
    P1->>P2: configure with owner and capability
    P2->>P3: getTurnIndexPage newest 200
    P3-->>P2: totalTurns and snapshot
    P1->>P2: handleSessionEvent turn_complete
    P2->>P3: getTurnIndexPage newest 200
    Note over P1,P4: This half has no production caller yet
    P2->>P3: getTurnIndexPage at an ordinal start
    P2->>P3: getTranscriptPage atRecordId
    P3-->>P2: events for one anchored page
    P2->>P4: admitAnchor with ordinal and turnId
    P4-->>P2: rangeId pageId blockId
Loading
Files changed (all 14 shown)
File What changed
docs/design/web-shell/web-shell-global-turn-navigation.md Status flipped to Phases 1 and 2A implemented. Four items moved from Phase 3 into Phase 2 step 1, remainder renamed 2B. This is the scope disagreement.
docs/plans/2026-09-04-web-shell-global-turn-navigation-phase2.md New 612-line Phase 2 implementation plan.
packages/web-shell/client/constants/sessions.ts Capability tag plus five budget constants: 200-entry index page, 16 pages and 4 MiB for metadata, 5 pages and 16 MiB for historical transcript.
packages/web-shell/client/daemon-react-sdk.ts Re-exports the two new hooks and ten new public types. No consumer outside tests.
packages/web-shell/client/daemon/index.ts Barrel re-exports for the same hooks and thirteen types.
packages/web-shell/client/daemon/session/DaemonSessionProvider.tsx Creates the store, subscribes to live-block changes, materialises historical events in an isolated transcript store, wires configure and prompt callbacks.
packages/web-shell/client/daemon/session/DaemonSessionProvider.test.tsx 278 new lines covering capability gating, head load, selection and staleness.
packages/web-shell/client/daemon/session/actions.ts Two new optional callbacks, onPromptAdmitted and onPromptRemoved, fired on admission, abort and explicit removal.
packages/web-shell/client/daemon/session/actions.test.ts 40 new lines for the admission and removal callbacks.
packages/web-shell/client/daemon/session/index.ts Barrel exports for the store factory and its types.
packages/web-shell/client/daemon/session/transcript-page-table.ts New 842-line immutable page table: ranges, boundaries, eviction, overlap dedup. No production caller.
packages/web-shell/client/daemon/session/transcript-page-table.test.ts New 781-line suite for the page table.
packages/web-shell/client/daemon/session/turn-navigation-store.ts New 1246-line store: epochs, head refresh, ordinal loading, locators, provisional reconciliation, fallbacks.
packages/web-shell/client/daemon/session/turn-navigation-store.test.ts New 964-line suite for the store.

Testing evidence

This run carries no test evidence of its own, and the PR has none either. This is an unattended CI run, so per the triage rules I did not build or execute anything from this branch — the review above is static, from the diff and from reading the surrounding code on main.

More importantly, there is no PR CI to quote. I fetched check-runs for the reviewed commit and workflow runs by head SHA: there are zero workflow runs with event == "pull_request" on this commit. Every check present is pull_request_target bot orchestration (triage itself, review-pr, assign, label, authorize, and the skipped verify/tmux scaffolding). The unit suite, lint, typecheck and build have not registered a single check against this head, so there is no green or red signal to report in either direction. The table below is the complete set, and it is all orchestration.

Not verified: that the 501 focused unit tests the description reports actually pass, that root build, typecheck, lint and diff checks pass, and every claim in the Reviewer Test Plan. Those are the author's local results, attributed as such — they are not evidence, and I have not reproduced them.

Check Conclusion
assign success
authorize (2 runs) success
label success
delay-automatic-review success
ack-review-request skipped
precheck-pr (2 runs) skipped
publish-resolution skipped
publish-tmux skipped
publish-verify skipped
resolve-pr skipped
review-config skipped
tmux-testing skipped
verify skipped
review-pr pending
triage in_progress

No unit, lint, typecheck or build check appears above, because none exists for this commit yet.

Sandboxed verification would settle this: @qwen-code /verify — whether the new store is load-bearing at all cannot be observed from the diff, and that is precisely the concern in finding 1. A green suite here proves the tests pass, not that the code they cover does anything: with locateOrdinal and the whole page table having no production caller, this PR's 2,063 test lines would pass identically if the random-access half were deleted, and would also pass identically if refreshHead() were never invoked from configure(). An A/B run against the base build is what would show whether the head-refresh path actually fires on connect and per turn, and whether the staleness epochs reject anything in a real reconnect rather than only in a mocked one. /tmux is not the right lane — this PR is deliberately headless and changes no TUI surface.

中文说明

代码审查

先说我在读 diff 之前的独立方案:Phase 1 已经提供了分页 turn index 和锚点 transcript 读取,所以最小客户端层就是一个 capability 门控、一个保存持久化 turn 总数与有界索引页环形缓存的小 store、一个抓取单个锚点页的 selectTurn、一个在每次 await 之后比较以丢弃过期结果的单调 generation 计数器,以及一个 hook——作为额外的 context value 接入 provider,完全不动现有实时 transcript 路径。大约 300 到 500 行生产代码。

对比结果:这个 PR 在最关键、也是我最不愿意放弃的一点上超出了我的方案——过期结果拒绝做得很扎实,用了三个独立的 epoch(sessionEpoch、chainEpoch、selectionGeneration)加上通过 isCurrentClient 做的 owner 身份比较,并且在 refreshHead、loadOrdinal、locateOrdinal、loadBoundary 的每一次 await 之后都检查。这类设计里最容易出微妙错误的就是这部分,而这里是对的。admitHead 里的 head 兼容性检查(共享 ordinal 必须保持相同 turnId,且同一 turnId 不能换 ordinal,否则 resetChain)在"持久化索引在缓存页之下发生位移"这个问题上,比我想到的任何方案都更好。observeLiveBlocks 复用了 useAnimationFrameTranscriptBlocks.ts 里已有的 isSameTranscriptStructure 惯用法,而不是另造一套去重启发式;live 位置检查在 admitAnchor 之前执行,因此已经在 live 里的 turn 不会被重新物化进历史页。去重确实按精确的持久化 record 身份进行。文件名是 kebab-case,与同一目录树里的 daemon-react-sdk.ts 一致。它超出我方案的地方是体量大了 5 倍,而这部分增量绝大多数是 page table——这就引出了下面的发现。

代码本身我没有发现正确性阻塞问题。有三点值得点名,按重要性递减:

1. 这个 PR 的大部分代码不会运行。 我在 head 上查的是读取点而不是声明处:locateOrdinal、loadOrdinal、loadOlder、loadNewer、admitAnchor、admitBoundary 都没有生产调用方——只有两个新测试文件和 store 内部的 retry()。因此 transcript-page-table.ts 的全部 842 行(淘汰、边界缓存、cachedBoundaryRequests 回滚、admitBoundary 的空页恢复路径)只被单元测试执行;16 MiB 的历史预算在真实会话里永远达不到,因为没有任何东西会admit一个历史页。两个新的公开 hook 以及从 daemon-react-sdk.ts 再导出的十一个类型同样没有非测试的读取方。这正是 AGENTS.md 审查规则里所说的 dead switch,只不过发生在模块尺度上:一个庞大的接口面被声明、导出、测试,却不被任何用户可达的东西调用。

2. 真正会运行的部分是有代价的,而且容易被忽略。 只要 capability 存在且有 client 连接,configure() 就会调用 refreshHead();handleSessionEvent() 则在每次 turn_complete、turn_error、replay_complete 时再次调用它。session_turn_navigation 是 SERVE_CAPABILITY_REGISTRY(packages/cli/src/serve/capabilities.ts:144)里的基线 tag,并且不在 CONDITIONAL_SERVE_FEATURES 中,所以每个 daemon 都无条件广播它。合并后的净效果是:每个连接当前 daemon 的 Web Shell 会话都会在连接时发起一次 200 条目的 turn-index 抓取,并在每个 turn 完成后再来一次,持续不断,而没有任何 UI 读取结果。关于量级我想说准确而不是危言耸听——稳态驻留只是单个索引页,对 200 条短 label 而言是 KB 级;4 MiB 和 16 MiB 是只有随机访问才可能逼近的上限,而随机访问就是第 1 点。所以代价是每 turn 多一次往返加上几 KB 驻留,不是内存回退。但这仍然是为一个要到 Phase 3 才出现的收益而先行付出的周期性开销,这也是"2A 应该和 rail 一起落地、而不是提前落地"的最有力论据。

3. fallbackReason 的 'too_large' 被复用过载,而且很难恢复。 recordPromptAdmitted() 在 provisionals.length 达到 indexPageSize(即 200 个已admit但尚未出现在索引中的 prompt)时调用 enterTooLargeFallback()。这个条件与 transcript 过大毫无关系,但它会设置 tooLarge = true 并发布 mode 为 legacy、fallbackReason 为 too_large。而且它几乎是永久的:tooLarge 只能由 resetForSession()(切换会话)清除,handleSessionEvent('session_rewound') 会刻意通过 resetChain() 保留它。一个基于 fallbackReason === 'too_large' 分支的 Phase 3 rail 会告诉用户"你的 transcript 太大无法导航",而实际发生的是 provisional 对账落后了。这不是阻塞项——daemon 确实会在索引条目上填充 promptId(packages/core/src/services/session-transcript-reader.ts:2613,由第 2184 行的 turnResultPromptId 提供),且 refreshHead() 每 turn 都跑,所以积累到 200 个未索引 provisional 应该不会发生——但这个标签会误导下一个读它的人,而换一个独立的原因值(例如 provisional_overflow)只需要多加一个联合成员。

一个更小的备注,不算发现:actions.ts 通过 store.getSnapshot().blocks.at(-1)?.id 以位置方式推导 optimisticBlockId,紧跟在同步的 appendLocalUserMessage 之后,今天这是正确的。它与"该 append 恰好产生一个尾部 block"耦合;如果哪天产生两个,alias 就会指向错误的 block,而 observeLiveBlocks 会保留它,因为它只清理 blockId 已不在 user block 中的 alias。现状没问题,但值得知道。

测试证据

本次运行自身不携带任何测试证据,这个 PR 也没有。这是无人值守的 CI 运行,因此按 triage 规则我没有构建或执行这个分支的任何代码——上面的审查是静态的,来自 diff 以及在 main 上阅读周边代码。

更重要的是,没有可引用的 PR CI。 我按被审查的 commit 抓取了 check-runs,并按 head SHA 抓取了 workflow runs:这个 commit 上没有任何 event 为 pull_request 的 workflow run。现存的每个 check 都是 pull_request_target 的机器人编排作业(triage 本身、review-pr、assign、label、authorize,以及被跳过的 verify/tmux 脚手架)。单元测试、lint、typecheck 和 build 尚未对这个 head 注册任何 check,因此无论绿或红都没有信号可报。下面的表格就是完整集合,而它全都是编排作业。

未验证:描述中报告的 501 个聚焦单元测试是否真的通过、根目录 build/typecheck/lint/diff 检查是否通过,以及 Reviewer Test Plan 里的每一条声明。那些是作者的本地结果,按原样归属——它们不是证据,我也没有复现。

上面没有出现任何 unit、lint、typecheck 或 build check,因为这个 commit 目前一个都不存在。

沙箱验证可以解决这个问题:@qwen-code /verify——新 store 是否真的 load-bearing 无法从 diff 观察,而这恰恰是第 1 点的疑虑所在。这里绿色的测试套件只能证明测试通过,不能证明它所覆盖的代码做了任何事:由于 locateOrdinal 和整个 page table 都没有生产调用方,即使把随机访问那一半删掉,这个 PR 的 2,063 行测试也会同样通过;即使 configure() 从不调用 refreshHead(),它们同样会通过。只有针对 base build 的 A/B 运行才能表明 head-refresh 路径是否真的在连接时和每 turn 触发,以及那些 staleness epoch 在真实重连中是否真的拒绝了什么,而不只是在 mock 中。/tmux 不是合适的通道——这个 PR 刻意无界面,不改变任何 TUI 表面。

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

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — the code is good and I found no correctness blocker, but two competing implementations of this same phase are open at once and most of this one has no caller, so which PR should exist is not mine to decide.

Going back to my independent proposal: the PR beats it where it counts. I sketched one generation counter; it uses three epochs plus owner identity, checked after every await, and the head-compatibility rule in admitHead catches index shift underneath a cached page in a way I had not thought through. That part I would merge as written. What I would not have written is the other 80% — I proposed roughly 400 production lines and this is 2,408, and the gap is almost entirely the page table and random-access path that nothing calls. Six months from now, whoever maintains transcript-page-table.ts will be maintaining 842 lines of eviction and boundary-rollback logic whose only integration evidence is a unit test, against a Phase 3 rail that may well have reshaped the contract by the time it lands. That is the version of this PR I would curse rather than thank, and it is a scope decision, not a coding one.

The thing that actually stops me is not in this diff. #11053 is open, by the same author, one minute older, and implements the same phase a different way — different module names, a different provider integration, a different line count, and it leaves the Accepted design document alone where this one rewrites its phase boundaries to fit the code. Reviewing this PR on its own merits means pretending #11053 does not exist, and approving it means silently choosing between two designs nobody has compared. That comparison needs the person who wrote both.

On the process question I want to be plain rather than diplomatic: moving four items out of Phase 3 into Phase 2 and renaming the remainder "2B", in the same commit that implements the result, makes the design doc a record of what was built instead of a constraint on what gets built. If the re-split is genuinely the better plan — and it may well be, since the locator map and the page table are coupled enough that building one without the other is awkward — then it deserves its own doc change with a rationale, reviewed before the implementation rather than arriving inside it.

So: ⏸️ deferring, not approving and not requesting changes. @doudouOUC — you wrote both PRs, and you authored and merged Phase 1 (#10751) and the design (#11020), so this is your call rather than a maintainer's; I could not resolve any other owner deterministically (the PR carries no labels, so the area-owner map has nothing to match, and there is no human reviewer on it yet). What needs deciding before this goes further:

  1. Which of feat(web-shell): add the global turn navigation Phase 2 client data layer #11053 and feat(web-shell): add headless global turn navigation #11054 lands. They cannot both; pick one and close the other, or state what each is for if I have misread the intent.
  2. Whether the design doc's phase re-split is deliberate. If it is, it should be its own change with a rationale, ahead of the implementation.
  3. Whether the page table belongs in this phase at all, given it has no production caller here and Phase 3 is what shapes its contract.

And separately, worth knowing regardless of the answer: there is currently no CI signal on this commit — zero pull_request-event runs, so nothing has verified the 501 local test passes reported in the description. Whatever the scope decision, this needs a real CI run before anyone approves it, and @qwen-code /verify is the lane that would settle whether the store is load-bearing at all rather than merely well-tested.

中文说明

Confidence: 3/5 — 代码质量不错,我没有发现正确性阻塞问题,但同一个 phase 目前同时开着两个竞争实现,而这一个的大部分代码没有调用方,所以"应该存在哪个 PR"不是我能决定的。

回到我的独立方案:这个 PR 在关键处胜过它。我勾勒的是一个 generation 计数器;它用了三个 epoch 加上 owner 身份,并在每次 await 之后检查,而且 admitHead 里的 head 兼容性规则以我没想清楚的方式捕捉到了"索引在缓存页之下发生位移"。那部分我会照原样合并。我不会写的是另外那 80%——我提议大约 400 行生产代码,而这里是 2,408 行,差距几乎全部来自没有任何调用方的 page table 和随机访问路径。六个月后,维护 transcript-page-table.ts 的人要维护 842 行淘汰与边界回滚逻辑,而它唯一的集成证据是一个单元测试,且要面对一个很可能在它落地前就已经重塑了契约的 Phase 3 rail。这就是那个版本里我会骂人而不是道谢的地方,而它是范围决策,不是编码决策。

真正让我停下来的东西不在这个 diff 里。#11053 处于 open,同一作者,早一分钟,用另一种方式实现了同一个 phase——不同的模块名、不同的 provider 集成、不同的行数,而且它不动那份 Accepted 的设计文档,本 PR 却重写了它的 phase 边界以适配代码。只就本 PR 自身的价值来 review,等于假装 #11053 不存在;而批准它,等于在两套无人对比过的设计之间默默做出选择。这个对比需要写出这两者的人来做。

关于流程问题我想说得直白而不是外交辞令:在实现结果的同一个 commit 里,把四项从 Phase 3 挪进 Phase 2 并把余下部分重命名为 "2B",会让设计文档变成"已构建内容"的记录,而不是"将被构建内容"的约束。如果这个重新划分确实是更好的方案——很可能是,因为 locator map 与 page table 耦合得足够紧,只建其中一个会很别扭——那它应该是一次独立的文档变更并附带理由,在实现之前被 review,而不是随实现一起到达。

所以:⏸️ 推迟,不批准也不 request changes。 @doudouOUC —— 两个 PR 都是你写的,Phase 1(#10751)和设计文档(#11020)也是你提交并合并的,所以这是你的决定而不是某位 maintainer 的;我无法确定性地解析出其他 owner(本 PR 没有任何 label,因此 area-owner 映射无从匹配,而且目前还没有人类 reviewer)。在继续推进之前需要决定的:

  1. feat(web-shell): add the global turn navigation Phase 2 client data layer #11053feat(web-shell): add headless global turn navigation #11054 哪个落地。 两者不能并存;选一个并关闭另一个,或者如果我误读了意图,请说明各自的用途。
  2. 设计文档的 phase 重新划分是否是刻意的。 如果是,它应该是独立的一次变更并附带理由,先于实现。
  3. page table 是否根本就应该属于这个 phase,鉴于它在这里没有生产调用方,而塑造其契约的是 Phase 3。

另外,无论答案如何都值得知道:这个 commit 目前没有任何 CI 信号——零个 pull_request 事件的 run,因此没有任何东西验证过描述中报告的 501 个本地测试通过。无论范围决策如何,在任何人为它批准之前,这个 PR 都需要一次真实的 CI 运行;而 @qwen-code /verify 是能够判定这个 store 是否真的 load-bearing(而不只是测试写得好)的通道。

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

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

@wenshao

wenshao commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code resolved the merge conflicts and pushed the branch update.

Merge resolution: PR #11054 ← main

Root cause

419e8d57b2 docs(web-shell): add global turn navigation Phase 2 design (#11020) on main edited the same section of docs/design/web-shell/web-shell-global-turn-navigation.md this PR rewrote: the Phase 2 delivery checklist. Main appended a re-alignment note plus a 4th item (data-layer items moved from Phase 3); this PR replaced the checklist with the delivered Phase 2A scope and updated the Status line. Only one file conflicted; DaemonSessionProvider.{tsx,test.tsx} auto-merged (main's #10553 goal-card fix vs this PR's additions are disjoint regions).

Textual or semantic

Semantic — both sides rewrote the same numbered list. The PR's list supersedes main's planning items (page table, boundaries, pagination migration, turn-index store all reappear, updated to what shipped), so the resolution keeps main's note verbatim, then the PR's checklist:

### Phase 2: bounded client window

(Re-aligned with the #10750 checklist on 2026-09-04: the data-layer items
originally listed under Phase 3 … belong to this phase … The detailed design
lives in `web-shell-global-turn-navigation-phase2.md`.)

1. Add the bounded turn-index store, tail refresh, provisional reconciliation,
   and canonical locator map without coupling metadata residency …
2. Land the immutable historical page table beside the existing connected live
   window, … and random anchored reads.
3. Migrate existing sequential prepend pagination behind the page-table
   boundary while preserving its current public behavior.
4. Expose the complete headless state and locator contract needed by the rail.

What is load-bearing

  • The note stays true only while data-layer items live in Phase 2 and Phase 3 stays rail-UI-only. Putting turn-index-store/locator items back into Phase 3 (both sides renumbered it identically — merged cleanly) would contradict the note, checklist, and Status line.
  • The note references web-shell-global-turn-navigation-phase2.md (main's design doc). This PR's plan doc is a different file (docs/plans/2026-09-04-…-phase2.md); both coexist. Renaming either breaks cross-references.
  • Status ("Phases 1 and 2A implemented, Phases 2B–3 proposed") is this PR's; the checklist is the 2A subset, so a future 2B edit must update both together.

What I could not verify

No build/tests ran (conflict resolution only). Auto-merged DaemonSessionProvider.tsx/.test.tsx combine main's goal-card labeling with this PR's navigation provider state; regions are disjoint but uncompiled. If a non-conflicted provider test fails in CI, look there first.

中文说明

冲突根因:main 的 #11020419e8d57b2)与本 PR 改写了同一设计文档的同一小节——Phase 2 交付清单。main 追加"重新对齐"说明及第 4 项(数据层条目从 Phase 3 移入);本 PR 将清单改写为已交付的 Phase 2A 范围并更新 Status。仅此一文件冲突;DaemonSessionProvider 两文件自动合并(双方区域不重叠)。

语义冲突:双方改写同一编号列表。PR 清单完全覆盖 main 的规划条目,故保留 main 说明段落原文 + PR 的 4 项交付清单(见上方代码块)。

关键约束:数据层条目须留在 Phase 2、Phase 3 仅保留 rail UI,说明才成立;说明引用的 web-shell-global-turn-navigation-phase2.md(main 设计文档)与本 PR 的 docs/plans/2026-09-04-… 计划文档是两个并存文件,重命名任一都会破坏交叉引用;Status 与清单同属 2A 边界,未来 2B 修改需同步更新。

未能验证:只解决冲突,未运行构建或测试。自动合并的 provider 文件组合了双方改动,若 CI 相关测试失败应优先检查该处。

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head ec572f6. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

terminal-turn-error-copy-narrow-dark before/after

terminal-turn-error-copy-narrow-light before/after

Full-resolution recordings (.webm) are attached to the workflow run.

Qwen Code · web-shell visuals

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

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

Comment thread packages/web-shell/client/daemon/session/transcript-page-table.ts
Comment thread docs/design/web-shell/web-shell-global-turn-navigation.md
Comment thread docs/plans/2026-09-04-web-shell-global-turn-navigation-phase2.md
Comment thread docs/plans/2026-09-04-web-shell-global-turn-navigation-phase2.md
Comment thread docs/plans/2026-09-04-web-shell-global-turn-navigation-phase2.md
Comment thread packages/web-shell/client/daemon/session/turn-navigation-store.ts
Comment thread packages/web-shell/client/daemon/session/turn-navigation-store.ts Outdated
Comment thread packages/web-shell/client/daemon/session/turn-navigation-store.ts Outdated
Comment thread packages/web-shell/client/daemon/session/turn-navigation-store.ts
Comment thread packages/web-shell/client/daemon/session/turn-navigation-store.ts
@wenshao

wenshao commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Real-environment verification of #11054 (maintainer review)

I built a real end-to-end environment for this PR instead of reading the diff: a real qwen serve
daemon with a real 300-turn persisted session, and a real headless Chromium page that mounts this
PR's own
DaemonWorkspaceProvider + DaemonSessionProvider and drives the two new public hooks
(useDaemonTurnNavigationState / useDaemonTurnNavigationStore). Because Phase 2A is headless, that
harness page is the only way to exercise the contract as a Phase 3 consumer will.

Verified at 278edd08a2 (merge-base bdbdc459dd).

Harness (click to expand)
daemon   node scripts/dev.js serve --port 4054 --token … --workspace <ws> --web
         --allow-origin http://127.0.0.1:5199 --max-sessions 0
         isolated HOME, OpenAI-compatible mock model, 300 real prompt turns on disk
older    a second daemon on :4055 built from the merge-base with `session_turn_navigation`
daemon   deleted from SERVE_CAPABILITY_REGISTRY  → 135 features instead of 136
client   real Chromium (Playwright) → client/__navharness__/index.html (harness only, not committed)
         <DaemonWorkspaceProvider><DaemonSessionProvider historyPageSize=30 suppressOwnUserEcho>
         window.__nav = the real store; every daemon request/response recorded
scale    15 probe scenarios, 140 assertions

real web shell

Gates (in the PR worktree)

gate result
packages/web-shell vitest 268 files / 6072 tests passed
tsc -p packages/web-shell/tsconfig.json --noEmit exit 0
eslint --max-warnings 0 over the 12 changed .ts/.tsx exit 0
the 4 files this PR touches/adds tests for 504 tests passed

(98 local failures I first saw were @tanstack/react-table missing from this machine's
node_modules, not the PR — installing it makes them pass.)

The Reviewer Test Plan reproduces, item by item

plan item result against the real daemon
1. exact durable count from the newest metadata page, live transcript untouched totalTurns = 300 from one GET /turn-index?limit=200 returning start=100,end=300; the SSE stream was opened once and never restarted by navigation; the loaded window stayed at 10 blocks
2. first / middle / last unloaded ordinal via bounded anchored reads ordinal 0 → 1 index page (start=0) + 1 GET /transcript?atRecordId=…&limit=200; ordinal 150 → same shape; ordinal 300 resolved from the live tail with zero transcript reads. Every resolved block carried the durable turnId and matched the daemon's own label for that ordinal
3. dedup only on exact record / prompt identity anchoring 24 turns below the tail produced a page ending at turn 296 against a live window starting at 297 — no gap, no overlap, newer: 'live'; a real prompt showed effectiveTurnCount = 301 while provisional, then 300 → 301 durable with the provisional gone (no double count)
4. reconnect / rewind / rapid selection / snapshot replacement / 409 / 413 4 concurrent locateOrdinal calls → only the newest committed, the other three rejected with Selection changed and published no error; offline → unavailable + retryable error, cache preserved, retry() recovered; a real POST /session/:id/rewind (301 → 75 turns) reset the chain, dropped all cached pages, cleared the selection, and navigation resumed on the new lineage; changing the session file's inode produced a real 409 transcript_snapshot_unavailable → retryable index error → automatic head refresh → recovery with no reload; growing the live session past the daemon's 256 MiB index ceiling produced a real 413 transcript_too_largelegacy / too_large latch, all caches dropped, live transcript intact, and zero further navigation reads
5. daemon without the capability legacy / unsupported, zero turn-index and zero anchored transcript requests over the whole session, loadOrdinal/locateOrdinal fail closed, a prompt records no provisional, and the existing loaded-only session still streams normally

Budgets held under real traffic: 8 backward continuations + 12 random jumps kept ≤ 5 historical
pages, ≤ 16 index pages, ~290 KB retained, with the selected anchor page pinned throughout
(fig5).

fig1
fig2
fig3


F1 — loadOlder/loadNewer at the page cap: a full daemon read, zero progress, boundary unchanged (please fix before Phase 3)

Once a range holds maxPages (5) pages and the pinned page is at the far end, every further
loadOlder fetches a real 200-record page, admits it, immediately evicts that same page, and
restores the identical older request. The call resolves successfully and the boundary stays
loadable, so a Phase 3 rail that loops "while older.kind === 'loadable'loadOlder()" spins
forever, re-fetching the same page each time.

Measured on the real daemon — steps 5–9 each issued one GET /transcript and changed nothing:

{"step":4,"http":1,"pages":5,"beforeReq":"2d9f4c72","afterReq":"d8d65e67","advanced":true, "oldestUser":"HARNESS-TURN-140 …"}
{"step":5,"http":1,"pages":5,"beforeReq":"d8d65e67","afterReq":"d8d65e67","advanced":false,"oldestUser":"HARNESS-TURN-140 …"}
{"step":6,"http":1,"pages":5,"beforeReq":"d8d65e67","afterReq":"d8d65e67","advanced":false,"oldestUser":"HARNESS-TURN-140 …"}
…  5 of 9 loadOlder calls spent a daemon round-trip with no progress

loadNewer is identical (5 of 9). Root cause is in HistoricalTranscriptPageTable.evict
(transcript-page-table.ts): the victim is active.pageIds[0] whenever the target page is not the
head, which is exactly the page just prepended by an older-direction admission; the removedFirst
arm then rebuilds older from the new head page's firstRecordId — the same record the request
started from. The mirror case is already handled differently: !removedFirst && direction === 'older'
throws HistoricalTranscriptPageTooLargeError, which surfaces as a non-retryable boundary error
and stops the caller. The existing test restores an explicit boundary when an outer active page is evicted (maxPages: 2) pins the non-progressing shape as expected (newer restored to next-2,
the request that had just been served), so nothing goes red today.

Suggested fix: when an admitted page is evicted immediately, treat the boundary the way the mirror
case does (terminal / non-retryable), or return a progress signal from loadOlder/loadNewer so the
caller can stop. Either way please pin it with a test that goes red if a boundary load can resolve
without advancing.

F2 — historical block IDs collide with live block IDs inside one provider epoch (small fix, big Phase 3 time-saver)

The plan states IDs "are only unique within the current provider epoch". They are not: the page
table's nextBlockOrdinal starts at 1 independently of the live transcript store's allocator, so a
cached historical page and the live tail both mint user-1, assistant-2, … Measured: 10 colliding
IDs
between a 10-block live window and the first cached page. user-1 was HARNESS-TURN-296 live
and HARNESS-TURN-001 historical.

DaemonTurnLocation does carry view/rangeId/pageId, so a careful consumer is fine — but Phase 3
renders both in one list, where a colliding React key or a blockId-only scrollToMessage lookup is
an expensive bug to find later. Page-vs-page uniqueness (what the plan's rationale actually covers)
is correct today. Seeding the table's allocator from the live store, or namespacing historical IDs
per page, closes it.

Independent confirmation of the review bot's CRITICAL R1-1

R1-1 reproduces end to end on a real daemon, not just at store level. This client's own prompt keeps
a record-less echo in the live window ({"id":"user-9","sourceRecordIds":[]}) while the same durable
turn is re-materialized as a second user block in a cached historical page — so Phase 3 would
render the same prompt twice:

live      {"id":"user-9",  "sourceRecordIds":[],                "text":"OWNECHO-LMS86 probe prompt"}
historical{"pageId":"history-page-1","blockId":"user-31","kind":"user","text":"OWNECHO-LMS86 probe prompt"}

The second half of R1-1 (pagination marching past the seam) did not reproduce in my
configuration — the page happened to contain a live assistant record, so reachedLive fired and
newer was live. The duplicate-block half is real and worth fixing regardless.

fig4
fig6

Mutation battery on the new suite

8 targeted mutations against the 504 tests of the four touched files — 4 caught, 4 survived:

mutation result
413 too_large latch never fires caught (1 test red)
frozen index-page mismatch accepted caught
provisional reconciliation disabled caught (2 red)
capability gate ignored (supported: true) caught
admitHead newest-page assertion removed survived
live/historical overlap dedup (filterOverlappingBlocks) disabled survived
selection-generation guard removed in locateOrdinal survived
index-page byte budget never enforced in makeIndexPage survived

The survivors are defence-in-depth paths that my real-daemon probes do exercise (the dedup one is
masked because every test's materialize stub already honours excludedRecordIds), so this is a
coverage note, not a correctness claim — it lines up with the bot's R1-18/R1-32/R1-34.

Smaller notes

  • The daemon can return two 413 codes on these routes. isTranscriptTooLarge latches only on
    transcript_too_large; transcript_page_too_large becomes a non-retryable per-anchor error, which
    matches the daemon's own comment ("a page this route cannot serialize returns
    transcript_page_too_large for that anchor"). Correct as written — just undocumented in the plan.
  • A session whose transcript is already past the 256 MiB ceiling cannot even POST /session/:id/load
    (413, retry loop), so the too_large latch is only reachable for a session that crosses the ceiling
    while connected — which is the case I tested. Pre-existing daemon behavior, not this PR.
  • Every connect costs two head turn-index reads (the configure effect, then
    replay_complete). Harmless at 200 entries, worth a look if the head page ever grows.

Verdict

The data layer does what the PR says it does, against a real daemon: bounded metadata, exact
random access, a clean historical/live seam, correct provisional reconciliation, and correct
fail-closed behavior on 409 / 413 / rewind / capability-absent. Gates are green.

I'd like F1 and R1-1(a) fixed before this contract is frozen for Phase 3 — both are latent
today (headless, no production caller) but both are the kind of defect that is much cheaper to fix
here than inside a virtualized rail. F2 is a two-line change worth taking now.

中文说明

#11054 的真实环境验证(维护者复核)

我没有只看 diff,而是搭建了完整的真实验证环境:一个真实的 qwen serve daemon(磁盘上有 300 个真实
turn 的持久化会话),以及一个真实的无头 Chromium 页面——该页面挂载本 PR 自己的
DaemonWorkspaceProvider + DaemonSessionProvider,并驱动两个新公开 hook
useDaemonTurnNavigationState / useDaemonTurnNavigationStore)。因为 Phase 2A 是无界面的,这
是唯一能像 Phase 3 消费者那样验证该契约的方式。

验证提交:278edd08a2(merge-base bdbdc459dd)。 共 15 个探针场景、140 条断言。

门禁(在 PR worktree 内)

门禁 结果
packages/web-shell vitest 268 文件 / 6072 用例全绿
tsc -p packages/web-shell/tsconfig.json --noEmit exit 0
对 12 个改动 .ts/.tsxeslint --max-warnings 0 exit 0
本 PR 涉及的 4 个测试文件 504 用例全绿

(我最初看到的 98 个失败是本机 node_modules@tanstack/react-table,与 PR 无关,装上即恢复。)

Reviewer 测试计划逐项复现

  1. 精确 turn 总数、实时链路不受影响:一次 GET /turn-index?limit=200 返回 start=100,end=300
    totalTurns = 300;SSE 只建连一次,导航过程从未重启;已加载窗口保持 10 个 block。
  2. 首/中/末未加载 ordinal 的有界随机访问:ordinal 0 → 1 个索引页(start=0)+ 1 次
    GET /transcript?atRecordId=…&limit=200;ordinal 150 同形;ordinal 300 直接命中实时尾部,
    次 transcript 读。每个解析出的 block 都带持久化 turnId,文本与 daemon 自己给出的 label 完全一致。
  3. 仅按精确 record / prompt 身份去重:锚定实时窗口下方 24 个 turn,历史页止于 turn 296,实时窗口从
    297 开始——无空洞、无重叠newer: 'live';发一条真实 prompt 时 effectiveTurnCount = 301
    totalTurns = 300,落盘后变为 301 且临时项消失(无重复计数)。
  4. 重连 / rewind / 快速切换 / 快照替换 / 409 / 413:并发 4 个 locateOrdinal,只有最新的提交,
    其余三个以 Selection changed 拒绝且不发布错误;断网后标记 unavailable + 可重试错误、缓存保留,
    retry() 恢复;真实调用 POST /session/:id/rewind(301 → 75 turn)后链路重置、缓存清空、
    选中清除,随后导航在新血统上正常工作;改变会话文件 inode 触发真实 409
    transcript_snapshot_unavailable
    → 可重试的 index 错误 → 自动刷新 head → 无需刷新页面即恢复;
    把在线会话撑过 daemon 的 256 MiB 索引上限触发真实 413 transcript_too_large
    落到 legacy / too_large、清空全部缓存、实时 transcript 完好、之后次导航请求。
  5. 不支持该能力的 daemonlegacy / unsupported,整个会话期间turn-index 与锚点
    transcript 请求,loadOrdinal/locateOrdinal 失败即止,发 prompt 不产生临时 turn,原有
    loaded-only 会话照常流式工作。

真实流量下预算稳定:8 次向后续页 + 12 次随机跳转,历史页 ≤ 5、索引页 ≤ 16、常驻约 290 KB,
且选中的锚点页全程被 pin 住。

F1 — 到达页数上限后 loadOlder/loadNewer 白跑一次 daemon 读且毫无进展(建议 Phase 3 之前修)

当某个 range 已有 maxPages(5)页、且被 pin 的页在远端时,之后每次 loadOlder 都会真实拉取一整页
200 条记录、准入、随即把刚拉进来的那一页逐出,并把 older 请求恢复成完全相同的那一个。调用
返回成功、边界仍是 loadable,因此 Phase 3 的导航条若按 "只要 older.kind === 'loadable'
loadOlder()" 循环,会无限反复拉取同一页。真实测得:9 次里有 5 次「发了请求、边界未变」(loadNewer
同样 5/9)。

根因在 HistoricalTranscriptPageTable.evict:当目标页不是头页时,被逐出的正是 older 方向刚刚前插的
那一页;随后 removedFirst 分支又用新头页的 firstRecordId 重建 older,正好等于本次请求的起点。
镜像情形其实已有不同处理——!removedFirst && direction === 'older'
HistoricalTranscriptPageTooLargeError,转成不可重试的边界错误让调用方停下。现有测试
restores an explicit boundary when an outer active page is evicted(maxPages: 2)恰好把这种
「不推进」的形状固化为期望值,所以今天不会变红。

建议:当准入的页被立即逐出时,按镜像分支的方式给出终态/不可重试边界,或让 loadOlder/loadNewer
返回是否推进的信号;并补一条「边界加载不可能在无进展的情况下成功返回」的测试。

F2 — 同一 provider epoch 内历史 block id 与实时 block id 冲突(小改动,能省下 Phase 3 大量排查)

计划文档写的是这些 id「只在当前 provider epoch 内唯一」,实际并非如此:page table 的
nextBlockOrdinal 从 1 开始,与实时 transcript store 的分配器彼此独立,于是缓存的历史页与实时尾部
都会产生 user-1assistant-2……实测:10 个 block 的实时窗口与第一张缓存页之间有10 个冲突 id
user-1 在实时是 HARNESS-TURN-296,在历史页是 HARNESS-TURN-001

DaemonTurnLocation 确实带 view/rangeId/pageId,谨慎的消费者不会出错;但 Phase 3 会把两者渲染
进同一个列表,届时 React key 冲突或只按 blockIdscrollToMessage 查找都很难排查。页与页之间
(也就是计划文档理由真正覆盖的部分)目前是正确的。让 page table 的分配器从实时 store 取种子,或给历史
id 加页级前缀即可。

独立确认 review 机器人的 CRITICAL R1-1

R1-1 在真实 daemon 上端到端复现,不只是 store 层:本客户端自己发出的 prompt 在实时窗口里是不带
record 的回显({"id":"user-9","sourceRecordIds":[]}),而同一个持久化 turn 会在缓存历史页中再次
物化成第二个 user block
——Phase 3 会把同一条 prompt 渲染两次。R1-1 的后半段(分页越过实时接缝)
在我的配置下复现:该页恰好包含实时助手 record,reachedLive 触发,newerlive
但重复 block 这半段是真实存在的,值得修。

对新增测试的变异测试

针对四个改动文件的 504 条用例做了 8 处定向变异,4 处被抓、4 处存活:

变异 结果
413 too_large 锁存永不触发 被抓
冻结索引页不一致被接受 被抓
关闭临时 prompt 对账 被抓(2 条红)
忽略能力门控(supported: true 被抓
移除 admitHead 的最新页断言 存活
关闭历史/实时重叠去重(filterOverlappingBlocks 存活
移除 locateOrdinal 中的 selection generation 守卫 存活
makeIndexPage 不再校验索引页字节预算 存活

存活的都是纵深防御路径,而我的真实 daemon 探针确实覆盖了它们(去重那条之所以看不出来,是因为测试里的
materialize 桩本身就已经遵守 excludedRecordIds)。所以这是覆盖率提示而非正确性结论,与机器人的
R1-18/R1-32/R1-34 一致。

其他小结论

  • 这两个路由可能返回两种 413。isTranscriptTooLarge 只对 transcript_too_large 锁存;
    transcript_page_too_large 会成为该锚点的不可重试错误——这与 daemon 自己的注释一致
    ("a page this route cannot serialize returns transcript_page_too_large for that anchor")。
    实现是对的,只是计划文档没写。
  • 如果一个会话的 transcript 本来就超过 256 MiB,连 POST /session/:id/load 都会 413 并进入重试
    循环,因此 too_large 锁存只在「连接期间越过上限」时可达——也正是我测的这条路径。这是既有 daemon
    行为,与本 PR 无关。
  • 每次建连会发两次 head turn-index 读(configure effect 一次、replay_complete 一次)。
    200 条时无害,若 head 页规模变大值得再看。

结论

这层数据层确实做到了 PR 所声称的一切:有界元数据、精确随机访问、干净的历史/实时接缝、正确的临时
prompt 对账,以及 409 / 413 / rewind / 能力缺失下正确的 fail-closed 行为,门禁全绿。

我希望在这份契约被 Phase 3 冻结之前先修掉 F1R1-1(a):两者今天都是潜伏问题(无界面、无生产
调用方),但都属于「现在修便宜、进了虚拟化导航条再修很贵」的那一类。F2 是两行的改动,建议顺手带上。

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

[codex] Manual babysit cycle for #11054 only. Pushed bdddf53 after the repository-required local gate. No action was taken on #11053.

The maintainer's E2E findings were reproduced before fixing. This remains a headless Phase 2A change, not the Phase 2B migration or Phase 3 UI.

Disposition Findings Result
Fixed F1 A boundary load can no longer successfully evict its own newly requested page and restore the same request. If the pinned selection prevents admission, the old cache is restored and that boundary becomes explicitly non-retryable.
Fixed F2 Historical block IDs and tool parent references are page-namespaced, disjoint from the live allocator. Durable record/tool IDs are unchanged.
Fixed R1-1 Exact durable aliases of retained live echoes now participate in history exclusion and live-seam detection. Queue-display identity is the separate R1-11 follow-up below.
Fixed Additional reproduced anchor-overlap gap A new anchor replaces overlapping cached ranges, retaining its complete materialized response instead of skipping deduplicated records behind its continuation cursor.
Fixed Final-commit audit: orphaned boundary error Publishing a new page table discards an error whose historical range has disappeared, while preserving errors for retained ranges and independent head refreshes.
Fixed R1-7, R1-12 Isolated history skips a malformed event while retaining healthy events; active-session server removals and bounded recent-removal tombstones prevent phantom provisionals, including removal-before-admission.
Fixed R1-30, R1-31, R1-33 Superseded metadata failures do not overwrite a newer selection; operation-local success preserves unrelated head errors, and failed head refreshes remain independently retryable.
Addressed R1-2, R1-3, R1-6, R1-17 Synchronized design/plan contracts and delivery slices; decoupled transcript request size from index size using the existing history constant; removed the unreachable terminal branch without removing cursor validation. Malformed protocol responses remain manually retryable, not automatically retried.
Coverage improved R1-10, R1-14, R1-15, R1-16, R1-19, R1-21, R1-28, R1-32 Exact populated optimistic-block identity, head eviction, terminal boundary refusal, repeated anchor, published-state waits, both removal orderings, and the older request mapping are now exercised. R1-10 does not independently mutate all four capture sites.
Partially addressed / deferred R1-26 Retryable locate recovery is covered; the trailing range-error sweep matrix remains a follow-up.
Pushed back on immediate cross-package expansion / deferred R1-11 The built-in queue's optional display-block locator is explicitly documented as unavailable until durable lookup can be used. The supported optimistic submitPrompt capture is retained and tested. Queue-to-SDK prompt identity wiring remains Phase 2B/3 work tracked by #10750; no text-matching workaround was added.
Deferred coverage, not claimed fixed R1-4, R1-5, R1-8, R1-9, R1-13, R1-18, R1-20, R1-22, R1-23, R1-24, R1-25, R1-27, R1-29, R1-34 Retain these explicit Phase 2B/3 (#10750) verification tasks: multi-provider/session/owner isolation; historical side effects; older cached-range restore and overlap backstop; cached revisit/pinning and multi-record locators; shrink/no-overlap/rewind/terminal coalescing; partial-page and protocol-validation rejection. Existing guards remain; happy-path tests are not evidence for all these mutations.

Of the 34 original inline threads, this records 18 addressed, one partially addressed and 15 deferred dispositions. Thread resolution records the disposition, not a claim that deferred work was implemented. Every original thread has an individual explanation; reviewers can reopen any disputed decision.

Validation at bdddf53:

  • Root npm run build, npm run typecheck (including integration type checking), and npm run bundle: passed.
  • Five affected test files: 520 passed. Post-build independent rerun of Provider plus dedicated regressions: 304 passed (291 + 13).
  • Targeted ESLint, Prettier and diff checks: passed. Two clean source self-audit passes followed the final corrections.
  • The broad local qwen review run was cancelled without a completed verdict after its pre-fix capture became stale; its intermediate compile failure was corrected and the final build gate rerun. A separate bounded final-commit source audit found the orphaned-boundary-error case above, then independently verified its correction in both pagination directions without further findings. This is not a full automated /review approval.
  • Reproduction used the actual SDK block allocator and exported headless APIs. The malformed-event witness is JSON-shaped and was red against the original provider loaded read-only from 278edd0, green against the fix.
  • The bundled CLI reports 0.23.0; an isolated bundled daemon passed /health and was stopped. This is bundle smoke + headless test-script verification, not a new full-browser E2E run. No accessibility, visual rail or long-session browser-performance claim is made.

This does not dismiss the existing CHANGES_REQUESTED review or certify merge readiness. CI and review approval must be evaluated on the new head.


中文摘要:本轮只处理 #11054,提交 bdddf53。修复分页原地循环、历史/实时块 ID 冲突、精确 prompt 别名对应的历史重复、重叠锚点缺口、畸形事件隔离、移除广播导致的幽灵 provisional,以及过期选择、头刷新重试和区间被替换后的残留错误。原有 34 条评论逐条记录为 18 条已处理、1 条部分处理、15 条延期;延期清单仍是 #10750 的 Phase 2B/3 工作,关闭讨论不代表这些工作已经实现。全仓构建、类型检查、bundle 和 520 项相关测试通过,构建后独立复验 304 项通过。验证是 bundle 冒烟与 headless 测试脚本,并非新的完整浏览器 E2E。旧快照的完整自动审查已取消;最终代码由独立定向审查复核,但这不等于完整 /review 批准。仍需等待新提交的 CI 与 reviewer 审批。

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

Not reviewed: reverse audit — rounds 1 and 2 both reported new findings, so the loop stopped before convergence; round 3 was not run within this session's budget.

Not reviewed: reverse audit findings from rounds 1 and 2 — the verifier never ruled on them: roughly 40 new candidates (6 of them Critical-shaped, including the isRetryable status===undefined default classifying a server-diagnosed partial/replayError page as retryable, the live-record set mutating after admission so records trimmed from the live window exist on neither side of the seam, and a stale targetPageId killing the newer direction) are terminal-only this round and reach the PR as no claim at all.

Not reviewed: reverse audit round 2 of chunk 8 — the auditor failed to execute and was not relaunched before the loop was closed.

Not reviewed: test-efficacy probe — harnessValidated null: the mutation and hunk probe never ran because a path-filter artifact starved its green baseline, so mutation coverage of the new suite is unmeasured (Agent 7's build, the 6088-test web-shell suite and the base-tree test-delta all did run).

Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": the ~50-row Verification matrix in this chunk was not walked row-by-row against the four test files for missing witnesses — I stopped at the rows the existing f…; chunk 5: executing the eight new tests to confirm they pass and to mutation-check the two guards (the worktree has no built @qwen-code/sdk dist and no local vitest bin…; "agent reverse-audit (round 1)": did not determine whether pending_prompt_completed{state:'removed'} is journaled for SSE replay ( bridge.ts:1922 lists the type in an array I did not open), ….

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

  • docs/plans/2026-09-04-web-shell-global-turn-navigation-phase2.md:498 — [review] The plan's own 1,000-added-line split rule is falsified by the slice it ships in the same PR (2,499 production lines, page table and provider integration togeth…
  • docs/plans/2026-09-04-web-shell-global-turn-navigation-phase2.md:504 — [review] The 15 deferred verification rows are deferred to a 'Phase 2B/3' slice list that names none of them, so the issue's own Phase 2 test deliverable loses its owner
  • packages/web-shell/client/daemon-react-sdk.ts:229 — [review] The new public turn-navigation type set exports the page container DaemonTurnIndexPage but not DaemonSessionTurnIndexEntry, the element type of its turns array
  • packages/web-shell/client/daemon/session/DaemonSessionProvider.tsx:1203 — [review] The structural-equality predicate gating live-block republication is hand-rolled a second time instead of reusing isSameTranscriptStructure, so two copies of…
  • packages/web-shell/client/daemon/session/actions.test.ts:4148 — [review] The actions harness stub makes store.getSnapshot().blocks permanently empty, so the blockId half of the new admission payload is unobservable in every actions.test.ts …
  • packages/web-shell/client/daemon/session/actions.ts:2313 — [review] The if (result.removed) guard on onPromptRemoved has no witness: every test asserts only the removed:true half, so deleting the condition ships green
  • packages/web-shell/client/daemon/session/turn-navigation-store.ts:127 — [review] handleSessionEvent takes type: string, so the store's hard-coded literal vocabulary has no compile-time tie to either SDK event union and two of its six compar…
  • packages/web-shell/client/daemon/session/turn-navigation-store.ts:383 — [review] Losing the client while the initial head load is in flight leaves the snapshot latched at mode 'loading' with no request in flight, no error, and no operation …
  • packages/web-shell/client/daemon/session/turn-navigation-store.ts:705 — [review] locateOrdinal's post-metadata selection-generation guard has no witness at any layer - the mutant the maintainer's battery reported as surviving still survives…
  • packages/web-shell/client/daemon/session/turn-navigation-store.ts:955 — [review] Head admission discards every cached index page that overlaps the new head window - including the page holding the current selection - which the store's own ev…
  • packages/web-shell/client/daemon/session/turn-navigation-store.ts:1249 — [review] compactLabel is the only byte bound on a provisional turn's label and no test at any layer exercises either the whitespace compaction or the 160-code-point ca…
中文说明

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

未审查(原文为英文):reverse audit — rounds 1 and 2 both reported new findings, so the loop stopped before convergence; round 3 was not run within this session's budget.

未审查(原文为英文):reverse audit findings from rounds 1 and 2 — the verifier never ruled on them: roughly 40 new candidates (6 of them Critical-shaped, including the isRetryable status===undefined default classifying a server-diagnosed partial/replayError page as retryable, the live-record set mutating after admission so records trimmed from the live window exist on neither side of the seam, and a stale targetPageId killing the newer direction) are terminal-only this round and reach the PR as no claim at all.

未审查(原文为英文):reverse audit round 2 of chunk 8 — the auditor failed to execute and was not relaunched before the loop was closed.

未审查(原文为英文):test-efficacy probe — harnessValidated null: the mutation and hunk probe never ran because a path-filter artifact starved its green baseline, so mutation coverage of the new suite is unmeasured (Agent 7's build, the 6088-test web-shell suite and the base-tree test-delta all did run).

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)"the ~50-row Verification matrix in this chunk was not walked row-by-row against the four test files for missing witnesses — I stopped at the rows the existing f…;chunk 5:executing the eight new tests to confirm they pass and to mutation-check the two guards (the worktree has no built @qwen-code/sdk dist and no local vitest bin…"agent reverse-audit (round 1)"did not determine whether pending_prompt_completed{state:'removed'} is journaled for SSE replay ( bridge.ts:1922 lists the type in an array I did not open), …

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

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

Comment thread packages/web-shell/client/daemon/session/turn-navigation-store.ts
Comment thread packages/web-shell/client/daemon/session/transcript-page-table.ts Outdated
Comment thread packages/web-shell/client/daemon/session/turn-navigation-store.ts Outdated
Comment thread packages/web-shell/client/daemon/session/transcript-page-table.ts
Comment thread packages/web-shell/client/daemon/session/turn-navigation-store.test.ts Outdated
Comment thread packages/web-shell/client/daemon/session/turn-navigation-store.ts
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

[codex] Round 2 fixes pushed in ec572f68e16c954927d20fee57a0507411c72702. The author approved including necessary gap recovery in this Phase 2A PR; the earlier scope-decision reply on R2-2 is now superseded by the implemented recovery described below. Its existing reply is not duplicated.

Feedback Disposition Result
R2-1 Fixed Exact queued-echo identity joins independently of all six admission/echo/index orders; also covers started events with no local queue row.
R2-2 Fixed; proposed cursor reuse rejected Direction-aware eviction plus bounded frozen-anchor/backfill recovery. Real backward cursors remain backward; tool-only gaps need no invented turn anchor.
R2-3 Fixed Obsolete boundary errors disappear when the corresponding boundary is no longer failed.
R2-4 Fixed Selection-constrained window pressure is retryable; a truly oversized page and invalid capacity remain separate cases.
R2-5 Addressed Unconditional rollback/retry/request-count assertions plus successful sliding/recovery coverage.
R2-6 Addressed All 13 prior review regressions moved beside their subject; standalone review-test file removed.
R2-7 Addressed without extra abstraction One collocated store fixture processes every event using real record IDs and SDK block allocation; encountered IDs survive exclusion.
R2-8 Fixed Public metadata loads capture the selection generation and cannot publish stale failures over a later selection.
R2-9 Addressed; suggested cursor expectation corrected Empty-response tail eviction restores the retained page's cursor and actually re-admits the missing page; the removed page's cursor would skip it.
R2-10 Addressed Independent metadata-error slot survives successful boundary loading and still drives retry.

Verification: 717 focused tests across seven files; repository build, bundle, typecheck, changed-file lint/format, and diff checks. Independent real-reader/signed-cursor probes cover ten ordinary/tool-only traversal, overlap, and cancellation scenarios; hook→SDK→store probes cover the unmatched started-event sibling. Four targeted mutants and the one-line fallback revert each redden the corresponding checked-in regression. An isolated rebuilt-daemon smoke verified health and UI serving and shut down cleanly. These are headless behavioral probes plus startup smoke, not full browser E2E.

Recovery uses existing daemon/SDK capabilities only. A forward-fetched retained page keeps its genuine forward cursor; a backward-fetched page keeps a frozen original turn anchor and retained record edge. Gap recovery may require multiple bounded backward reads, holding only the current response and immediate newer candidate, then admits the nearest missing page/suffix. Recovery metadata is included in the retained cache budget and is discarded with its page. Phase 2B UI migration and Phase 3 virtual rail remain out of scope.

Additional review-driven validation: the full Web Shell suite passed (268 files / 6,107 tests). The expanded CLI suite timed out, including local Git-hook network timeouts; the VS Code companion suite was not run after that shared budget expired. Those broader checks are not claimed green and do not replace the completed changed-scope gate.

Native local review reached its 30-minute limit (completed=false, no composed verdict). Completed reports yielded two unique non-blocking follow-ups: pin the already-present empty-gap rejection guard and mismatched-anchor rejection guard with dedicated tests. These are deferred under the repository's late-round scope rule; neither is a missing production guard. The late unmatched-started-event hunk was independently reproduced, fixed, regression-tested, mutation-checked, and self-audited, but was not in the review's captured diff. No complete review/approval is claimed.

中文

本地原生 review 达到 30 分钟上限(completed=false,没有汇总结论)。已完成报告中的两项去重后非阻塞建议是:给现有空缺口拒绝守卫和锚点不匹配拒绝守卫补专门测试。按仓库多轮评审的范围控制规则留作后续项,两处生产守卫均已存在。最后补充的开始事件回显修复不在 review 捕获的 diff 中,但已独立复现、修复、回归/变异验证及自查。不宣称完整审查通过或批准。

额外审查验证:Web Shell 全量测试通过(268 个文件 / 6,107 项)。扩展的 CLI 测试超时,其中出现本机 Git 钩子的网络超时;共享预算耗尽后未运行 VS Code companion 测试。这些更广检查不算通过,也不替代已完成的改动范围验证。

已在 ec572f68e16c954927d20fee57a0507411c72702 推送第二轮修复。作者已同意将必要的缺口恢复纳入本 Phase 2A PR;R2-2 之前等待范围确认的回复现由本次实现结果更新,不重复发送同一线程回复。

反馈 处理 结果
R2-1 已修复 队列回显按精确身份匹配,覆盖准入/回显/索引的六种顺序及无本地队列条目的开始事件。
R2-2 已修复,未采用错误游标复用建议 双向淘汰和有界锚点回填恢复;反向游标不冒充正向游标,纯工具消息缺口也可恢复。
R2-3 已修复 边界不再失败时,移除其过期错误。
R2-4 已修复 选中页导致的窗口压力可重试;单页过大和非法容量分别处理。
R2-5 已处理 直接断言回滚、可重试状态和请求次数,并验证成功滑动/恢复。
R2-6 已处理 13 个旧 review 回归用例迁回对应代码旁,移除独立 review 测试文件。
R2-7 已处理,未新增无必要抽象 合并后的 fixture 遍历全部事件,使用真实 record 字段和 SDK 块分配器,排除前保留 encountered IDs。
R2-8 已修复 公开元数据加载自动捕获选择代数,旧失败不能覆盖新选择。
R2-9 已处理并纠正建议中的游标预期 空响应触发尾页淘汰后,使用保留页的游标真正恢复缺失页;被删页之后的游标会跳过它。
R2-10 已处理 边界成功不清除独立元数据错误,retry 仍会重新请求该元数据。

验证包含七个文件的 717 个聚焦测试、仓库 build/bundle/typecheck、改动文件 lint/format 和 diff 检查;真实 reader/签名游标探针覆盖十个普通/纯工具消息缺口、重叠及取消场景;hook→SDK→store 探针覆盖开始事件遗漏。四项定向变异与回显身份的一行回退均使对应已提交回归测试变红。重新构建的隔离 daemon 通过健康检查和 UI 服务冒烟测试并正常退出。以上不等同完整浏览器 E2E。

仅使用现有 daemon/SDK 能力。正向加载页保留真实正向游标,反向加载页保留冻结的原 turn 锚点和当前记录边界;恢复可能需要多次有界回填,只保留当前响应及紧邻较新候选页,逐次准入最近缺页/后缀。恢复元数据计入缓存预算,随页面释放。Phase 2B UI 迁移和 Phase 3 虚拟化导航条仍不在本次范围内。

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

wenshao commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Round-2 real-environment verification of #11054 — the three round-1 findings are fixed, and the fixes are load-bearing

Follow-up to my round-1 report. I rebuilt the
rig from scratch against the new head and re-ran it: a real qwen serve daemon over a real persisted session, and a
real Chromium page mounting this PR's own DaemonWorkspaceProvider + DaemonSessionProvider and driving the two
new public hooks. Because Phase 2A ships no UI, that harness is the only way to exercise the contract the way a Phase 3
rail will.

Verified at ec572f68e1 (merge-base bdbdc459dd). The round-1 arm is 278edd08a2, run as an A/B in the same
worktree by swapping only the four runtime files this round touched, so the daemon, the session on disk and the anchors
are byte-for-byte the same on both arms.

rig

Verdict

The three findings I asked to be fixed before the contract freezes — F1, F2 and R1-1(a) — are all fixed, and
each one is demonstrably load-bearing: reverting it reproduces the original defect on a real daemon, and mutating it
reddens the checked-in regressions. All six Reviewer-Test-Plan items reproduce. Gates are green. I would take this.

Two things I found are cost properties of the new recovery path rather than defects; I've written them up below as Phase 3
follow-ups, not merge blockers. One mutant survives, and it is exactly the guard the author already listed as a deferred
follow-up.


F1 and F2 — same daemon, same session, same anchor, four files swapped

f1 f2

round-1 278edd08a2 head ec572f68e1
loadOlder calls that spent a daemon read, made no progress and reported no error 6 of 10 0 of 10
loadNewer, same 8 of 12 0 of 12
what the caller sees at the cap resolves; boundary stays loadable throws HistoricalTranscriptWindowFullError; boundary becomes a retryable error naming the fix
historical block ids colliding with the live window 10 (12 in the own-echo run) 0 — ids are history-page-N:user-M, tool parentBlockId too

The Phase 3 loop I was worried about (while (older.kind === 'loadable') loadOlder()) can no longer spin silently.

R1-1(a) — the duplicate own-prompt

Real prompt through submitPrompt; once durable, a historical page is anchored 10 ordinals below it. Identical setup on
both arms.

own echo

arm live historical duplicated id collisions
round-1 (marker OWNECHO-JOA1N) user-11, 0 source records user-11, 1 record, same text yes 11
head (marker OWNECHO-RTW4N) user-11, 0 source records no 0

On round-1 the two blocks even carried the same id, so F2 and R1-1(a) compounded into one React key rendering two
different messages. On head the durable turn resolves to the live block through the prompt alias and the page stops at
newer: live. Mutating the one line back (syncLiveRecordIds feeding livePromptAliases.keys()) reddens 7 tests.

Plan item 3 / R2-2 — traversal past the cap, and gap recovery

Selection moved with the window so pages were genuinely evicted from the newer end, then reversed.

gap

Everything came back: 356 records / 138 user turns recovered chronologically, 0 duplicate records, 0 duplicate user
blocks, and no missing turn inside the recovered span
— including the gap with no user prompt (RIG-TURN-201 is a single
turn with 60 real tool calls, a ≈130-record stretch containing no user record). The page and byte caps held
throughout, and re-selecting an ordinal already inside the retained window costs 0 daemon reads and keeps the window
(that is the deferred R1-22 shape, and it behaves).

Plan items 4–6 and the round-2 error semantics

failures

  • R2-4 / plan item 3 tail — window-full is retryable; retry() while the selection still pins the far page makes no
    progress, and after moving the selection to ordinal 感觉token消耗量大的,推荐用kimi的k2模型,我测试了,消费还能接受。 #137 the same retry() advances RIG-TURN-130 → RIG-TURN-097.
  • R2-3 — the obsolete boundary error is gone once that boundary is healthy (snapshot.error === null).
  • R2-8 — a network-aborted turn-index read for selection A, superseded 60 ms later by selection B: A rejects
    locally, and the final state is selected #300 ready, error: null. The stale failure never reaches the snapshot.
  • Offline / real 409 — aborting every GET /transcript leaves the cache intact and retry() recovers; replacing the
    session .jsonl with a new inode produces a real 409 transcript_snapshot_unavailable, which heals on retry()
    with the live transcript untouched.
  • Plan item 6 — a second daemon with session_turn_navigation deleted from the registry (138 features instead of
    139): legacy / unsupported, zero navigation requests for the whole session even after explicit calls, and a real
    prompt still completes with no provisional turn.
  • Plan item 4 — a prompt queued behind a running 40-tool-call turn resolves to a single live block, leaves no
    provisional behind, and never reappears in a historical page.
  • Plan items 1–2totalTurns comes from one head metadata page; ordinals 0 / 100 / 229 cost 1–2 bounded reads each
    and 457 / 458 resolve from the live tail with zero transcript reads. Every located turn matched the daemon's own
    turnId and label for that ordinal.

Gates, in the PR worktree

gate result
packages/web-shell vitest 268 files / 6107 tests passed (108 s) — matches the author's number exactly
tsc -p packages/web-shell/tsconfig.json --noEmit exit 0
eslint --max-warnings 0 over the 15 changed .ts/.tsx exit 0
prettier --check over the same files exit 0
the 6 suites this PR touches 711 tests, 0 failed

CI's Test (ubuntu-latest, Node 22.x) on this head was cancelled at the 2 h job limit with vitest still running
(the cleanup step terminated orphan node (vitest) processes) — it did not report a test failure, and I could not
reproduce one locally.

Mutation battery — 14 mutants, 13 killed

mutations

Every mutant compiles (tsc exit 0) and is run against the PR's own 711 tests. The single survivor, M5 — removing the
gap-recovery anchor-mismatch guard
, is precisely the follow-up the author already recorded ("pin the mismatched-anchor
rejection guard with dedicated tests"). It is not dead code: rewriting a real daemon response so the anchor page's
targetRecordId no longer matches makes the guard fire after one read, leaves the 5-page cache with 0 duplicate
records, and turns the boundary into a retryable error. Two lines of test would close it. The sibling guard (M6,
non-advancing walk) is already covered.


N1 — the frozen-anchor walk is quadratic in the gap, and a failed retry pays the whole walk

Not a correctness problem, and the PR does say recovery "may require several bounded reads". Here is what that costs on a
real daemon:

evicted gap (turns) 67 166 228 294
GET /transcript calls for one loadNewer 4 7 10 12
records transferred / admitted ~400 / ≤100 ~700 / ≤100 ~1000 / ≤100 ~1200 / ≤100

The walk restarts from the frozen anchor every call, so consecutive recovery steps in one run cost 8, then 7, then 6
reads — closing a gap of G pages is about G(G+1)/2 daemon reads for G pages of content. Memory stays bounded, so this is
latency and traffic, not a leak. The sharper edge is that a failed attempt pays the same walk: with the selection
pinned at the far end I measured 12 reads (~1200 records) spent before the window-full refusal, repeated on every retry.
A Phase 3 rail that auto-retries a retryable boundary error will do that. Worth either a progress/cost signal on the
boundary, or memoising the walk's newest candidate across attempts.

N2 — the page cap binds ~40× before the byte cap, which is what forces the expensive path

budget value peak observed headroom when "window is full" fired
WEB_SHELL_HISTORICAL_MAX_PAGES 5 5 0 — this is what binds
WEB_SHELL_HISTORICAL_MAX_BYTES 16 MiB 426 KB 97.5 % unused

Every window-full error in this run, and therefore every gap that then had to be re-walked, came from the page count while
the byte budget was 2.5 % consumed. The byte cap already protects memory; raising the page cap would make N1's expensive
path rare rather than routine. One constant, no behaviour change.

N3 — carried over from round 1, unchanged

Every connect still costs two head turn-index reads (the configure effect, then replay_complete). Harmless at
200 entries; worth a look if the head page grows.


Rig, for reproduction

daemon    node packages/cli/dist/index.js serve --port 4054 --workspace <git repo> --web
          built from ec572f68e1 · 139 features incl. session_turn_navigation · isolated HOME
daemon-2  port 4055 · same build with session_turn_navigation removed from the registry · 138 features
model     local OpenAI-compatible SSE server; every reply tagged so each persisted turn is identifiable on disk
session   459 → 465 real turns written by the daemon, incl. one turn with 60 real tool calls
client    real Chromium → harness page mounting this PR's providers; every fetch recorded
A/B       one worktree; git checkout 278edd08a2 -- <4 runtime files> and back; blob ids recorded per arm
scale     11 probe scripts, ~120 assertions, 14 mutants

The harness is not part of the PR and was not committed. This is not a full accessibility or long-session browser
performance run, and I did not re-test the 256 MiB 413 transcript_too_large latch this round — it was verified in
round 1 and the code path is unchanged.

中文说明

#11054 的第二轮真实环境验证 —— 三个第一轮问题都已修复,且修复确实承重

接续我的第一轮报告。我针对新 head 重新搭建了整套验证台:
真实 qwen serve daemon + 磁盘上真实持久化的长会话,以及一个真实 Chromium 页面挂载本 PR 自己的
DaemonWorkspaceProvider + DaemonSessionProvider 并驱动两个新公开 hook。因为 Phase 2A 无界面,这是唯一能像
Phase 3 消费者那样验证该契约的方式。

验证提交:ec572f68e1(merge-base bdbdc459dd)。 对照臂是 278edd08a2,在同一棵 worktree 内只回退本轮改动的
四个运行时文件做 A/B,因此两臂的 daemon、磁盘上的会话和锚点完全一致。

结论

我在第一轮要求「契约冻结前修掉」的 F1F2R1-1(a) 全部已修,且每一项都被证明是承重的:回退它就能在真实
daemon 上复现原缺陷,变异它就能让已提交的回归用例变红。Reviewer 测试计划六项全部复现,门禁全绿。我认为可以合入。

我发现的另外两点是新恢复路径的成本性质而非缺陷,下面作为 Phase 3 后续项写明,不作为合入阻塞。存活的那一个变异体,
正是作者自己已经登记的延期项。

F1 与 F2 —— 同 daemon、同会话、同锚点,只换四个文件

第一轮 278edd08a2 head ec572f68e1
loadOlder 发了请求、无进展、且不报错 10 次中 6 次 10 次中 0 次
loadNewer 同上 12 次中 8 次 12 次中 0 次
到达上限时调用方看到什么 成功返回,边界仍是 loadable HistoricalTranscriptWindowFullError,边界变成可重试错误并说明如何恢复
历史 block id 与实时窗口冲突 10 个(own-echo 那轮 12 个) 0 —— id 形如 history-page-N:user-M,工具块的 parentBlockId 同样加了前缀

我担心的那个 Phase 3 死循环(while (older.kind === 'loadable') loadOlder())已经不可能静默空转。

R1-1(a) —— 自己发的 prompt 被渲染两次

通过 submitPrompt 发真实 prompt,落盘后在其下方 10 个 ordinal 处锚定一页历史。两臂设置完全相同。

实时 历史 是否重复 id 冲突
第一轮(标记 OWNECHO-JOA1N user-11,0 个 source record user-11,1 个 record,文本相同 11
head(标记 OWNECHO-RTW4N user-11,0 个 source record 0

第一轮里两个块连 id 都相同,F2 与 R1-1(a) 叠加成「同一个 React key 渲染两条不同消息」。head 上该持久化 turn 经 prompt
别名解析到实时块,历史页停在 newer: live。把那一行(syncLiveRecordIds 纳入 livePromptAliases.keys())变异回去,
7 条用例变红。

测试计划第 3 项 / R2-2 —— 越过上限的双向遍历与缺口恢复

让选择跟随窗口移动,使较新端的页真的被逐出,然后反向。

全部恢复:按时间顺序取回 356 条 record / 138 个用户 turn,0 条重复 record、0 个重复用户块、恢复区间内无缺失 turn ——
包含没有用户 prompt 的缺口(RIG-TURN-201 是一个含 60 次真实工具调用的 turn,约 130 条 record 中没有用户 record)。
页数与字节上限全程保持;重新选中已在窗口内的 ordinal 零次 daemon 读且窗口保留(这正是被延期的 R1-22 形态,行为正确)。

测试计划第 4–6 项与本轮错误语义

  • R2-4 / 第 3 项末段:窗口满可重试;选择仍钉在远端时 retry() 无进展,把选择移到 ordinal 感觉token消耗量大的,推荐用kimi的k2模型,我测试了,消费还能接受。 #137 后同一个 retry()
    推进 RIG-TURN-130 → RIG-TURN-097。
  • R2-3:边界恢复健康后旧错误消失(snapshot.error === null)。
  • R2-8:把选择 A 的 turn-index 请求在网络层中止并延迟 900 ms,60 ms 后切到选择 B —— A 只在本地 reject,最终状态是
    selected #300 ready, error: null,过期失败没有写进快照。
  • 断网 / 真实 409:中止全部 GET /transcript 后缓存完好且 retry() 恢复;把会话 .jsonl 换成新 inode 会产生真实
    409 transcript_snapshot_unavailableretry() 后自愈且实时 transcript 不受影响。
  • 第 6 项:另起一个把 session_turn_navigation 从注册表删掉的 daemon(138 个 feature,而非 139):legacy / unsupported
    整个会话期间次导航请求(即便显式调用也是零),发真实 prompt 仍正常且不产生临时 turn。
  • 第 4 项:在一个 40 次工具调用的 turn 后面排队的 prompt,最终只对应一个实时块,不留临时项,也不会再出现在历史页中。
  • 第 1–2 项totalTurns 来自一页 head 元数据;ordinal 0 / 100 / 229 各花 1–2 次有界读,457 / 458 直接命中实时尾部、
    次 transcript 读。每个定位到的 turn 的 turnId 与 label 都与 daemon 自己给出的完全一致。

门禁(在 PR worktree 内)

门禁 结果
packages/web-shell vitest 268 文件 / 6107 用例全绿(108 秒),与作者给出的数字一致
tsc -p packages/web-shell/tsconfig.json --noEmit exit 0
对 15 个改动 .ts/.tsxeslint --max-warnings 0 exit 0
同批文件 prettier --check exit 0
本 PR 涉及的 6 个测试文件 711 用例,0 失败

CI 的 Test (ubuntu-latest, Node 22.x) 在该 head 上是跑到 2 小时 job 上限被取消(收尾步骤终止了残留的
node (vitest) 进程),并没有报告测试失败;我在本地也复现不出失败。

变异测试 —— 14 个变异体,13 个被杀

每个变异体都能编译(tsc exit 0),对 PR 自己的 711 条用例运行。唯一存活的 M5 —— 移除缺口恢复的锚点不匹配守卫
恰好是作者已经登记的延期项(「给锚点不匹配拒绝守卫补专门测试」)。它不是死代码:把真实 daemon 响应的
targetRecordId 改掉,守卫会在第 1 次读之后触发,5 页缓存保持 0 条重复 record,边界变成可重试错误。两行测试即可闭合。
兄弟守卫(M6,反向走查不推进)已有覆盖。

N1 —— 冻结锚点回溯的开销随缺口平方增长,且失败的重试要付全程

这不是正确性问题,PR 也写了恢复「可能需要多次有界读取」。真实 daemon 上的代价是:

逐出缺口(turn 数) 67 166 228 294
一次 loadNewer 发出的 GET /transcript 4 7 10 12
传输 / 实际准入的 record ~400 / ≤100 ~700 / ≤100 ~1000 / ≤100 ~1200 / ≤100

每次调用都从冻结锚点重新走,所以同一轮里连续三次恢复分别花 8、7、6 次读 —— 补完 G 页的缺口约需 G(G+1)/2 次 daemon
读。内存始终有界,因此这是延迟与流量问题,不是泄漏。更值得注意的是失败的尝试同样要付全程:把选择钉在远端时,我实测
在窗口满被拒之前已经花掉 12 次读(约 1200 条 record),且每次重试都重复一遍。自动重试可重试边界错误的 Phase 3 导航条会
一直这样。建议给边界加一个进展/成本信号,或者在多次尝试之间缓存走查得到的最新候选页。

N2 —— 页数上限比字节上限早约 40 倍触顶,正是它逼出了昂贵路径

预算 取值 实测峰值 「窗口已满」时的余量
WEB_SHELL_HISTORICAL_MAX_PAGES 5 5 0 —— 真正的约束
WEB_SHELL_HISTORICAL_MAX_BYTES 16 MiB 426 KB 97.5 % 未用

本轮每一次窗口满错误、以及随之必须重走的每一个缺口,都来自页数上限,而字节预算只用掉 2.5 %。字节上限已经能保护内存;
把页数上限调高就能让 N1 的昂贵路径从常态变成罕见。改一个常量,不改行为。

N3 —— 第一轮遗留,未变

每次建连仍发两次 head turn-index 读(configure effect 一次、replay_complete 一次)。200 条时无害,若 head 页
规模变大值得再看。

验证台(便于复现)

daemon    node packages/cli/dist/index.js serve --port 4054 --workspace <git 仓库> --web
          由 ec572f68e1 构建 · 139 个 feature(含 session_turn_navigation)· 隔离 HOME
daemon-2  端口 4055 · 同一构建,但注册表里删掉 session_turn_navigation · 138 个 feature
模型      本地 OpenAI 兼容 SSE 服务;每条回复带标记,磁盘上每个 turn 都可单独识别
会话      daemon 真实写入 459 → 465 个 turn,其中一个 turn 含 60 次真实工具调用
客户端    真实 Chromium → 挂载本 PR providers 的验证页;记录每一次 fetch
A/B       同一棵 worktree;git checkout 278edd08a2 -- <4 个运行时文件> 再切回,每臂记录 blob id
规模      11 个探针脚本、约 120 条断言、14 个变异体

验证台不属于本 PR,也未提交。本轮不是完整的无障碍或长会话浏览器性能验证;256 MiB 413 transcript_too_large 锁存本轮
未重测 —— 第一轮已验证且该代码路径未变。

@wenshao

wenshao commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

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

APPROVE (verified at head ec572f6)

Historical blockers — both Criticals re-verified fixed in this head's code

  • R1-1 (record-ID-blind live seam): the store now seeds durable live-prompt aliases from live-block prompt identity and remaps provisionals accordingly (turn-navigation-store.ts:439-479: liveBlockIdByPromptId built from the live window's blocks, index entries and provisionals joined by exact promptId before falling back to block-id presence), so the originator's record-less echo can no longer be re-admitted as a duplicate historical turn, and the seam test asserts the no-duplicate/live-boundary regression per the finding's prescription.
  • R2-1 (the fix-induced half: queued-prompt path skipped the alias because it carries no blockId): the seeding is now keyed on exact prompt identity independently of admission/echo/index arrival order — including unmatched started-event echoes — and the parameterized test reconciles exact queued identity in %s/%s/%s order (all six permutations) exists at turn-navigation-store.test.ts:163, each asserting live locator, no historical duplicate, and the live seam. I confirmed the mechanism and the test at head rather than trusting the reply text.

Earlier-round Suggestions (doc-consistency, coverage claims, ordinal-capture pins, fixture consolidation) were landed in bdddf535/ec572f68 per their threads and none gates.

Review-state note and CI

The flow's last full pass was at bdddf535 (its round-2 CR blocker population is the pair verified above); no flow review has run on ec572f68 itself yet — the GitHub CHANGES_REQUESTED flag will clear procedurally with the next round, and the human maintainer approved this head at 07:22Z after the replies. CI at head: zero failed checks; the only completed non-green is the web-shell E2E Smoke cancellation that has followed the pool all week. My verification was blocker-focused (the seam/dedup core and the store's aliasing) plus CI and thread reconciliation — the 7062-line feature's remaining bulk rests on the two deep flow rounds at prior heads (whose content this head largely preserves) and the new store's own 1624-line test file, not on my reading of every line.

@doudouOUC
doudouOUC enabled auto-merge September 6, 2026 07:26
@doudouOUC
doudouOUC dismissed qwen-code-ci-bot’s stale review September 6, 2026 07:28

Already have 2 approves.

@doudouOUC
doudouOUC added this pull request to the merge queue Sep 6, 2026
Merged via the queue into main with commit 9c0fcbb Sep 6, 2026
324 of 335 checks passed
@doudouOUC
doudouOUC deleted the codex/web-shell-global-turn-navigation-phase2a branch September 6, 2026 07:29
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ⚠️ not run — skipped - workflow run

Skipped because the PR is not open for verification (state=MERGED, draft=false).

中文 — 判定:⚠️ 未运行 · 已跳过

跳过原因:the PR is not open for verification (state=MERGED, draft=false)。

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.23.1.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants