feat(web-shell): add headless global turn navigation - #11054
Conversation
|
Thanks for the PR! Template looks good ✓ Problem: Real and already tracked, not theoretical. #10750 is open with 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:
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 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 ( 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 且带 方向: 原则上对齐,但在有人投入 review 之前,有一个冲突需要你先解决。 #11053 是同一个 phase 的另一个竞争性实现,同样由你提交,比本 PR 早一分钟创建。两者不是边缘重叠——而是同一件事做了两遍:#11053 新增 更深层的分歧在于范围,值得说清楚:本 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 核心模块门禁不适用——没有任何文件匹配 方案: 这是我最主要的保留意见,属于范围问题而非代码质量问题。在 2,408 行生产代码中,842 行( 所以"砍掉 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 |
Code reviewMy 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 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 ( 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: 2. What does run has a cost, and it is easy to miss. 3. One smaller note, not a finding: 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
Files changed (all 14 shown)
Testing evidenceThis 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 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 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.
No unit, lint, typecheck or build check appears above, because none exists for this commit yet. Sandboxed verification would settle this: 中文说明代码审查 先说我在读 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 · qwen3.8-max-2026-09-02 Reviewed at |
|
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 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:
And separately, worth knowing regardless of the answer: there is currently no CI signal on this commit — zero 中文说明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)。在继续推进之前需要决定的:
另外,无论答案如何都值得知道:这个 commit 目前没有任何 CI 信号——零个 pull_request 事件的 run,因此没有任何东西验证过描述中报告的 501 个本地测试通过。无论范围决策如何,在任何人为它批准之前,这个 PR 都需要一次真实的 CI 运行;而 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
|
@qwen-code /resolve |
|
Qwen Code resolved the merge conflicts and pushed the branch update. Merge resolution: PR #11054 ← mainRoot cause
Textual or semanticSemantic — 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
What I could not verifyNo build/tests ran (conflict resolution only). Auto-merged 中文说明冲突根因:main 的 #11020( 语义冲突:双方改写同一编号列表。PR 清单完全覆盖 main 的规划条目,故保留 main 说明段落原文 + PR 的 4 项交付清单(见上方代码块)。 关键约束:数据层条目须留在 Phase 2、Phase 3 仅保留 rail UI,说明才成立;说明引用的 未能验证:只解决冲突,未运行构建或测试。自动合并的 provider 文件组合了双方改动,若 CI 相关测试失败应优先检查该处。 |
🖼️ web-shell visual previewRendered against a mock daemon (no real backend): the PR base vs this PR head Screenshots · before / afterFull-resolution recordings (.webm) are attached to the workflow run. — Qwen Code · web-shell visuals |
|
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 |
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 Verified at Harness (click to expand)Gates (in the PR worktree)
(98 local failures I first saw were The Reviewer Test Plan reproduces, item by item
Budgets held under real traffic: 8 backward continuations + 12 random jumps kept ≤ 5 historical F1 —
|
| 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.
isTranscriptTooLargelatches only on
transcript_too_large;transcript_page_too_largebecomes a non-retryable per-anchor error, which
matches the daemon's own comment ("a page this route cannot serialize returns
transcript_page_too_largefor 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 thetoo_largelatch 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-indexreads (theconfigureeffect, 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/.tsx 跑 eslint --max-warnings 0 |
exit 0 |
| 本 PR 涉及的 4 个测试文件 | 504 用例全绿 |
(我最初看到的 98 个失败是本机 node_modules 缺 @tanstack/react-table,与 PR 无关,装上即恢复。)
Reviewer 测试计划逐项复现
- 精确 turn 总数、实时链路不受影响:一次
GET /turn-index?limit=200返回start=100,end=300,
totalTurns = 300;SSE 只建连一次,导航过程从未重启;已加载窗口保持 10 个 block。 - 首/中/末未加载 ordinal 的有界随机访问:ordinal 0 → 1 个索引页(
start=0)+ 1 次
GET /transcript?atRecordId=…&limit=200;ordinal 150 同形;ordinal 300 直接命中实时尾部,
零次 transcript 读。每个解析出的 block 都带持久化turnId,文本与 daemon 自己给出的 label 完全一致。 - 仅按精确 record / prompt 身份去重:锚定实时窗口下方 24 个 turn,历史页止于 turn 296,实时窗口从
297 开始——无空洞、无重叠,newer: 'live';发一条真实 prompt 时effectiveTurnCount = 301
而totalTurns = 300,落盘后变为 301 且临时项消失(无重复计数)。 - 重连 / 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 索引上限触发真实 413transcript_too_large→
落到legacy / too_large、清空全部缓存、实时 transcript 完好、之后零次导航请求。 - 不支持该能力的 daemon:
legacy / 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-1、assistant-2……实测:10 个 block 的实时窗口与第一张缓存页之间有10 个冲突 id;
user-1 在实时是 HARNESS-TURN-296,在历史页是 HARNESS-TURN-001。
DaemonTurnLocation 确实带 view/rangeId/pageId,谨慎的消费者不会出错;但 Phase 3 会把两者渲染
进同一个列表,届时 React key 冲突或只按 blockId 做 scrollToMessage 查找都很难排查。页与页之间
(也就是计划文档理由真正覆盖的部分)目前是正确的。让 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 触发,newer 为 live。
但重复 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 returnstranscript_page_too_largefor that anchor")。
实现是对的,只是计划文档没写。 - 如果一个会话的 transcript 本来就超过 256 MiB,连
POST /session/:id/load都会 413 并进入重试
循环,因此too_large锁存只在「连接期间越过上限」时可达——也正是我测的这条路径。这是既有 daemon
行为,与本 PR 无关。 - 每次建连会发两次 head
turn-index读(configureeffect 一次、replay_complete一次)。
200 条时无害,若 head 页规模变大值得再看。
结论
这层数据层确实做到了 PR 所声称的一切:有界元数据、精确随机访问、干净的历史/实时接缝、正确的临时
prompt 对账,以及 409 / 413 / rewind / 能力缺失下正确的 fail-closed 行为,门禁全绿。
我希望在这份契约被 Phase 3 冻结之前先修掉 F1 与 R1-1(a):两者今天都是潜伏问题(无界面、无生产
调用方),但都属于「现在修便宜、进了虚拟化导航条再修很贵」的那一类。F2 是两行的改动,建议顺手带上。
|
[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.
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:
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
left a comment
There was a problem hiding this comment.
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 ownerpackages/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 arraypackages/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 greenpackages/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)
|
[codex] Round 2 fixes pushed in
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 ( 中文本地原生 review 达到 30 分钟上限( 额外审查验证:Web Shell 全量测试通过(268 个文件 / 6,107 项)。扩展的 CLI 测试超时,其中出现本机 Git 钩子的网络超时;共享预算耗尽后未运行 VS Code companion 测试。这些更广检查不算通过,也不替代已完成的改动范围验证。 已在
验证包含七个文件的 717 个聚焦测试、仓库 build/bundle/typecheck、改动文件 lint/format 和 diff 检查;真实 reader/签名游标探针覆盖十个普通/纯工具消息缺口、重叠及取消场景;hook→SDK→store 探针覆盖开始事件遗漏。四项定向变异与回显身份的一行回退均使对应已提交回归测试变红。重新构建的隔离 daemon 通过健康检查和 UI 服务冒烟测试并正常退出。以上不等同完整浏览器 E2E。 仅使用现有 daemon/SDK 能力。正向加载页保留真实正向游标,反向加载页保留冻结的原 turn 锚点和当前记录边界;恢复可能需要多次有界回填,只保留当前响应及紧邻较新候选页,逐次准入最近缺页/后缀。恢复元数据计入缓存预算,随页面释放。Phase 2B UI 迁移和 Phase 3 虚拟化导航条仍不在本次范围内。 |
Round-2 real-environment verification of #11054 — the three round-1 findings are fixed, and the fixes are load-bearingFollow-up to my round-1 report. I rebuilt the Verified at VerdictThe three findings I asked to be fixed before the contract freezes — F1, F2 and R1-1(a) — are all fixed, and Two things I found are cost properties of the new recovery path rather than defects; I've written them up below as Phase 3 F1 and F2 — same daemon, same session, same anchor, four files swapped
The Phase 3 loop I was worried about ( R1-1(a) — the duplicate own-promptReal prompt through
On round-1 the two blocks even carried the same id, so F2 and R1-1(a) compounded into one React key rendering two Plan item 3 / R2-2 — traversal past the cap, and gap recoverySelection moved with the window so pages were genuinely evicted from the newer end, then reversed. Everything came back: 356 records / 138 user turns recovered chronologically, 0 duplicate records, 0 duplicate user Plan items 4–6 and the round-2 error semantics
Gates, in the PR worktree
CI's Mutation battery — 14 mutants, 13 killedEvery mutant compiles ( N1 — the frozen-anchor walk is quadratic in the gap, and a failed retry pays the whole walkNot a correctness problem, and the PR does say recovery "may require several bounded reads". Here is what that costs on a
The walk restarts from the frozen anchor every call, so consecutive recovery steps in one run cost 8, then 7, then 6 N2 — the page cap binds ~40× before the byte cap, which is what forces the expensive path
Every window-full error in this run, and therefore every gap that then had to be re-walked, came from the page count while N3 — carried over from round 1, unchangedEvery connect still costs two head Rig, for reproductionThe harness is not part of the PR and was not committed. This is not a full accessibility or long-session browser 中文说明对 #11054 的第二轮真实环境验证 —— 三个第一轮问题都已修复,且修复确实承重接续我的第一轮报告。我针对新 head 重新搭建了整套验证台: 验证提交: 结论我在第一轮要求「契约冻结前修掉」的 F1、F2、R1-1(a) 全部已修,且每一项都被证明是承重的:回退它就能在真实 我发现的另外两点是新恢复路径的成本性质而非缺陷,下面作为 Phase 3 后续项写明,不作为合入阻塞。存活的那一个变异体, F1 与 F2 —— 同 daemon、同会话、同锚点,只换四个文件
我担心的那个 Phase 3 死循环( R1-1(a) —— 自己发的 prompt 被渲染两次通过
第一轮里两个块连 id 都相同,F2 与 R1-1(a) 叠加成「同一个 React key 渲染两条不同消息」。head 上该持久化 turn 经 prompt 测试计划第 3 项 / R2-2 —— 越过上限的双向遍历与缺口恢复让选择跟随窗口移动,使较新端的页真的被逐出,然后反向。 全部恢复:按时间顺序取回 356 条 record / 138 个用户 turn,0 条重复 record、0 个重复用户块、恢复区间内无缺失 turn —— 测试计划第 4–6 项与本轮错误语义
门禁(在 PR worktree 内)
CI 的 变异测试 —— 14 个变异体,13 个被杀每个变异体都能编译( N1 —— 冻结锚点回溯的开销随缺口平方增长,且失败的重试要付全程这不是正确性问题,PR 也写了恢复「可能需要多次有界读取」。真实 daemon 上的代价是:
每次调用都从冻结锚点重新走,所以同一轮里连续三次恢复分别花 8、7、6 次读 —— 补完 G 页的缺口约需 G(G+1)/2 次 daemon N2 —— 页数上限比字节上限早约 40 倍触顶,正是它逼出了昂贵路径
本轮每一次窗口满错误、以及随之必须重走的每一个缺口,都来自页数上限,而字节预算只用掉 2.5 %。字节上限已经能保护内存; N3 —— 第一轮遗留,未变每次建连仍发两次 head 验证台(便于复现)验证台不属于本 PR,也未提交。本轮不是完整的无障碍或长会话浏览器性能验证;256 MiB |
|
@qwen-code /triage |
qqqys
left a comment
There was a problem hiding this comment.
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:liveBlockIdByPromptIdbuilt from the live window's blocks, index entries and provisionals joined by exactpromptIdbefore 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 testreconciles exact queued identity in %s/%s/%s order(all six permutations) exists atturn-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.
Already have 2 approves.
|
Sandboxed verification: Skipped because the PR is not open for verification (state=MERGED, draft=false). 中文 — 判定:
|
|
Released in v0.23.1. |














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.
session_turn_navigationis unavailable or the transcript is too large to index.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
session_turn_navigationadvertised, confirm the exact durable turn count comes from the newest metadata page while the live transcript stays connected.409, and413paths. 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.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
Environment (optional)
Local repository toolchain with Node.js 22 or newer.
Risk & Scope
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 保持不变。
session_turn_navigation或 transcript 过大而无法索引时,保留仅基于已加载内容的路径。为什么需要
当前紧凑导航条只能表示已物化在实时 transcript 中的 turn,有界恢复无法覆盖整个长会话。Phase 1 已提供 daemon 索引和锚点读取能力;本 PR 消费这些能力,直接跳到索引中的 turn,无需加载所有中间 turn,也不替换实时流。经作者同意纳入的缺口恢复使缓存逐出后的历史遍历可以反向继续,为后续虚拟化全局导航条建立有界客户端基础。
Reviewer 测试计划
如何验证
session_turn_navigation时,确认最新元数据页提供精确的持久化 turn 总数,实时 transcript 仍保持连接。409和413。过期请求不得修改当前 session。边界恢复后不能保留旧错误,独立的元数据错误则必须仍可重试。证据(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 分钟上限,未形成汇总结论;已完成报告中的两项非阻塞守卫测试建议已在修复总结中记录为后续项。不宣称完整审查通过或批准。
测试平台
环境(可选)
本地仓库工具链,Node.js 22 或更高版本。
风险与范围
关联 Issue
跟踪 #10750,但不自动关闭,因为 Phase 2B 与 Phase 3 尚未完成。