fix(core): unblock history pagination on oversized transcript turns - #8335
Conversation
Backward history pagination dead-ended with HTTP 413 whenever a single turn exceeded the 4 MiB page budget: a turn cannot be split across pages, so the reader threw SessionTranscriptPageTooLargeError and the Web Shell latched a permanent pagination error banner. Take at least one indivisible unit per page (one aggregate record forward, one turn backward) so pagination always makes progress; the 32 MiB response serialization cap remains the hard ceiling. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
A non-retryable transcript page failure (4xx, partial replay) latched paginationError with no in-UI recovery short of reloading the session. The banner now offers a retry that force-clears the latch and refetches the same page, whose cursor was never advanced by the failed attempt. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
✅ Qwen Triage finished — CI landed green on ✅ Qwen Triage 已完成 —— |
|
Thanks for the PR — this is a clean, well-evidenced fix. Template: complete ✓ Problem: observed bug, not theoretical. A transcript turn larger than the 4 MiB reader page budget made the reader throw, the route return a deterministic Direction: aligned. Web Shell session-history loading is core qwen-code functionality, and the fix keeps the HTTP surface unchanged (an over-budget Size: touches core ( Approach: the scope feels right. The reader change is genuinely minimal — "always take at least one indivisible record so pagination terminates" — and the 32 MiB response cap correctly remains the hard ceiling. One honest question, not a blocker: the retry button is a logically separate concern from the pagination fix (it addresses genuine non-retryable failures like 4xx / partial replay, which the pagination fix does not cause). The two are thematically unified and both well-tested, so I don't think it needs splitting, but it's worth being aware they ride together. Risk: no elevated risk signals — none of the changed files match the revert-correlated high-risk paths. Moving on to code review. 🔍 中文说明感谢贡献——这是一个干净、证据充分的修复。 模板: 完整 ✓ 问题: 已观测到的 bug,而非理论性问题。当某个 transcript turn 超过 4 MiB 的分页读取预算时,读取器抛错、路由确定性地返回 方向: 对齐。Web Shell 会话历史加载是 qwen-code 的核心功能,且修复保持 HTTP 接口不变(用超预算的 规模: 触及核心( 方案: 范围合理。reader 改动确实最小化——"始终至少取一条不可拆分记录以保证分页终止"——而 32 MiB 响应上限正确地仍是硬性上限。一个诚实的疑问(非阻断):重试按钮与分页修复在逻辑上是独立的关注点(它处理真正的不可重试失败,如 4xx / 部分重放,而分页修复并不会导致这些)。两者主题统一且都测试充分,因此我认为无需拆分,但值得了解它们捆绑在一起。 风险: 无升级风险信号——改动文件均未命中与 revert 相关的高风险路径。 进入代码审查 🔍 — Qwen Code · qwen3.8-max-preview Reviewed at |
Code reviewMy independent take before reading the diff: an indivisible unit (one aggregate record, or one turn that can't be split across pages) that exceeds the page budget should ride over the budget rather than throw — always take at least one unit so pagination terminates — while keeping a hard ceiling further down the stack so a pathological transcript can't produce an unbounded response. That's exactly what the PR does, and it does it minimally:
No correctness blockers, no security concerns, no convention violations. Tests are load-bearing — the reader tests now assert the over-budget record is returned rather than rejected, so reverting the fix would fail them. One non-blocking caveat for the maintainer: the ACP consumer ( Test evidence (this PR's own CI)This is an unattended CI run, so I'm quoting the PR's CI signal rather than running anything. At review time the primary unit suite and the serve/web-shell jobs are still in progress and nothing has landed red; the Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 Sandboxed verification would settle the behavioural claim: 中文说明代码审查我在读 diff 之前的独立判断:一个不可拆分的单元(一条聚合记录,或一个无法跨页拆分的 turn)超过页预算时,应当超出预算放行而非抛错——始终至少取一个单元以保证分页终止——同时在栈的更下层保留一个硬性上限,使病态 transcript 不会产生无界响应。 PR 正是这么做的,且做得很最小化:
无正确性阻断、无安全隐患、无规范违反。测试是承重的——reader 测试现在断言超预算记录被返回而非被拒绝,因此回退修复会使测试失败。 一个供维护者参考的非阻断注意点: ACP 消费者( 测试证据(本 PR 自身的 CI)这是无人值守的 CI 运行,因此我引用 PR 的 CI 信号而非自行运行任何东西。审查时主单元测试套件与 serve/web-shell 任务仍在进行中,尚无红色结果; 沙箱验证可以坐实行为性声明: — Qwen Code · qwen3.8-max-preview Reviewed at |
|
Confidence: 4/5 — a tight, well-evidenced fix that does exactly what it says; the only reservation is the ACP path's missing 32 MiB cap, which is non-blocking and already disclosed by the author. Stepping back: the problem is real and reproduced (a deterministic dead-end whenever a transcript turn outgrows the 4 MiB page budget), and the fix matches my independent proposal almost line-for-line — always take at least one indivisible unit so pagination terminates, and lean on the existing 32 MiB serialization cap as the hard ceiling. The implementation is minimal where it matters (the reader change is a few lines) and the bulky provider diff is re-indentation, not new complexity. Every edit in the diff serves the stated goal; the retry button is the one arguable extra, but it's thematically unified and well-tested rather than scope creep. The tests genuinely pin the behavior — revert the fix and they fail. The single thing I'd want a maintainer to eyeball is the ACP consumer: it shares the reader change but has no 32 MiB cap behind it, so Zed now receives a large replay page where it previously got a I'm approving on the merits. CI is still running on the reviewed commit (unit suite, Serve A/B, and web-shell visuals in progress), so approval is deferred until CI lands green on 中文说明置信度:4/5——一个紧凑、证据充分的修复,完全做到了它声称的事;唯一的保留是 ACP 路径缺少 32 MiB 上限,属非阻断且作者已声明。 退一步看:问题是真实且可复现的(只要某个 transcript turn 超过 4 MiB 页预算就会确定性死胡同),而修复几乎逐行符合我的独立方案——始终至少取一个不可拆分单元以保证分页终止,并依赖现有的 32 MiB 序列化上限作为硬性上限。实现在关键处很最小化(reader 改动仅数行),臃肿的 provider diff 是重新缩进而非新增复杂度。diff 中每一处改动都服务于既定目标;重试按钮是唯一可争论的额外项,但它主题统一且测试充分,并非范围蔓延。测试确实承重的——回退修复它们就会失败。 我唯一希望维护者看一眼的是 ACP 消费者:它共享了 reader 改动,但其后没有 32 MiB 上限,因此 Zed 现在会收到一个较大的重放页,而此前是 我在实质上是认可的。CI 仍在被审查的提交上运行(单元测试套件、Serve A/B、web-shell 视觉均在进行中),因此批准推迟到 CI 在 — Qwen Code · qwen3.8-max-preview Reviewed at |
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
🖼️ web-shell visual previewRendered against a mock daemon (no real backend): the PR base vs this PR head Screenshots · before / afterℹ️ No screenshot changed against the PR base — but this PR edits 4 render-shaping files:
Either the change has no visual effect (logic, plumbing, a state the scenarios never reach), or no scenario renders this UI — in which case the preview cannot see it, and an empty result is a coverage gap rather than a clean bill of health. To make it visible, add a scenario to Full-resolution recordings (.webm) are attached to the workflow run. — Qwen Code · web-shell visuals |
🩺 serve daemon A/BBuilt the PR base vs this PR head ✅ No response changes against the PR base across 4 scenario(s). — Qwen Code · serve A/B |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅
中文说明
未发现问题。LGTM!✅
— qwen3.8-max-preview via Qwen Code /review (v0.21.3)
yiliang114
left a comment
There was a problem hiding this comment.
LGTM — the 'always take at least one indivisible unit' invariant correctly prevents pagination dead-ends, and the 32 MiB route-level cap remains the hard ceiling. Retry button implementation is clean with proper cursor preservation.
Non-blocking:
- Add
.catch(() => {})toretryOlderHistory'svoid loadOlderHistory(true, true)to suppress unhandled rejection on failed retry - Consider a soft warning or early-exit at the reader level when aligned selection greatly exceeds maxBytes (avoids expensive aggregation for pages doomed to fail at the 32 MiB route cap)
- Add boundary test: single record bytes === maxBytes (strict > means it's included)
wenshao
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
中文说明
已审查。 建议见行内评论。
— qwen3.8-max-preview via Qwen Code /review (v0.21.2)
| if ( | ||
| selectedStart === position && | ||
| selectedStart < position && | ||
| maxBytes !== undefined && | ||
| bytes > maxBytes | ||
| selectedBytes + bytes > maxBytes | ||
| ) { | ||
| throw new SessionTranscriptPageTooLargeError(sessionId, bytes, maxBytes); | ||
| break; | ||
| } |
There was a problem hiding this comment.
[Suggestion] The backward-pagination oversized-turn fix (replacing throw with break) can be reverted on its own with every affected test staying green — no test in this diff independently gates the backward path's first-record-exceeds-budget handling. — Concrete cost: a future refactor re-introducing a throw in selectBackwardPageUuids would not be caught.
The existing test sets maxBytes exactly equal to the first backward record's byte count, so the old bytes > maxBytes throw condition is never true. A test with maxBytes strictly less than the first record's bytes (e.g. Buffer.byteLength(JSON.stringify(finalAnswer)) - 1) would trigger the old throw and pin the fix.
中文说明
向后分页的超大 turn 修复(将 throw 替换为 break)可以被单独回退而所有受影响的测试仍为绿色——本 diff 中没有测试独立地固定住向后路径的首条记录超预算处理。具体代价:未来在 selectBackwardPageUuids 中重新引入 throw 的重构不会被捕获。
现有测试将 maxBytes 设为恰好等于首条向后记录的字节数,因此旧的 bytes > maxBytes 抛出条件永远不为真。建议将 maxBytes 设为严格小于首条记录字节数(如 Buffer.byteLength(JSON.stringify(finalAnswer)) - 1),即可触发旧抛出并固定住此修复。
— qwen3.8-max-preview via Qwen Code /review (v0.21.3)
|
Released in v0.21.4. |
What this PR does
Two related changes to loading earlier session history in the Web Shell. First, transcript pagination no longer hard-fails when a single indivisible unit — one aggregate record, or one turn that cannot be split across pages — exceeds the 4 MiB reader page budget: such pages now ride over budget so paging always makes progress, with the existing 32 MiB response serialization cap remaining the hard ceiling. Second, when loading earlier history does fail non-retryably (4xx, partial replay), the "Earlier history could not be loaded" banner now offers a retry button that re-fetches the exact same page instead of requiring a full session reload.
Why it's needed
Sessions containing a turn larger than 4 MiB (e.g. a huge tool result) permanently showed "Earlier history could not be loaded / 无法加载更早的历史记录" when the user scrolled up. The reader threw a page-too-large error, the route returned HTTP 413, and the client latched a non-retryable pagination error — restarting the daemon, reconnecting, or scrolling again could never help because the failure was deterministic per transcript content. Reproduced against real transcripts: a 53 MiB session consistently failed on page 2 (a single 13.6 MiB turn); after the fix it paginates all the way to the oldest record.
Reviewer Test Plan
How to verify
cd packages/core && npx vitest run src/services/session-transcript-reader.test.ts— 58 tests pass; an over-budget single record or turn is now returned whole instead of throwing, while normal pages still trim at record boundaries and glued-line fragments are still counted conservatively.cd packages/cli && npx vitest run src/serve/multi-workspace-sessions.test.ts— 93 tests pass; a 5 MiB single record is served with 200, a 33 MiB record still returns 413 via the 32 MiB serialization cap, and untrusted workspaces still never spawn the bridge in either case.cd packages/webui && npx vitest run src/daemon/session/DaemonSessionProvider.test.tsx(181 pass — a forced load clears the latched error and refetches with the same untouched cursor) andcd packages/web-shell && npx vitest run client/components/MessageList.dom.test.tsx(the retry button issues a forced load; scroll-triggered auto-load stays blocked while the error is latched).Evidence (Before & After)
Before:
GET /workspaces/:id/session/:id/transcriptdeterministically returned413 transcript_page_too_largeonce paging reached a >4 MiB turn, and the banner "Earlier history could not be loaded." offered no recovery short of reloading the session. After: the same request returns 200 carrying the oversized turn (under the 32 MiB cap) and pagination continues to the oldest record; if a page still fails non-retryably, the banner offers a working Retry button. Verified against real local transcripts: all 12 recent sessions paginate end-to-end, including the previously fatal 53 MiB transcript with a 13.6 MiB single turn.Tested on
Environment (optional)
Unit + integration tests; pagination replayed locally over real
~/.qwen/projectstranscripts via the reader and replay pipeline (no daemon needed).Risk & Scope
Linked Issues
N/A
中文说明
本 PR 做了什么
与 Web Shell 加载更早会话历史相关的两项改动。第一,当单个不可拆分单元(一条聚合记录,或一个无法跨页拆分的 turn)超过 4 MiB 的分页读取预算时,transcript 分页不再硬失败:这类页允许超出预算放行,保证分页始终能够前进;现有的 32 MiB 响应序列化上限仍是硬性上限。第二,当加载更早历史确实发生不可重试的失败(4xx、部分重放)时,"无法加载更早的历史记录"横幅现在提供重试按钮,可以重新获取完全相同的某一页,而无需整体重载会话。
为什么需要
包含大于 4 MiB turn(例如巨大的工具结果)的会话,在用户向上滚动时会永久显示"无法加载更早的历史记录"。读取器抛出页过大错误,路由返回 HTTP 413,客户端锁定为不可重试的分页错误——重启 daemon、重连或再次滚动都无济于事,因为该失败对 transcript 内容是确定性的。已在真实 transcript 上复现:一个 53 MiB 的会话稳定地在第 2 页失败(单个 13.6 MiB 的 turn);修复后它可以一直分页到最老的记录。
Reviewer 测试计划
如何验证
cd packages/core && npx vitest run src/services/session-transcript-reader.test.ts—— 58 个测试通过;超预算的单条记录或 turn 现在整体返回而不再抛错,普通页仍按记录边界裁剪,粘连行片段仍按保守方式计数。cd packages/cli && npx vitest run src/serve/multi-workspace-sessions.test.ts—— 93 个测试通过;5 MiB 单记录返回 200,33 MiB 记录仍通过 32 MiB 序列化上限返回 413,且两种情况下 untrusted workspace 都不会启动 bridge。cd packages/webui && npx vitest run src/daemon/session/DaemonSessionProvider.test.tsx(181 通过——强制加载会清除锁定的错误并用未前移的同一游标重新拉取)以及cd packages/web-shell && npx vitest run client/components/MessageList.dom.test.tsx(重试按钮发起强制加载;错误锁定期间滚动触发的自动加载仍被阻止)。证据(前后对比)
修复前:一旦分页触及 >4 MiB 的 turn,
GET /workspaces/:id/session/:id/transcript确定性地返回413 transcript_page_too_large,且"无法加载更早的历史记录"横幅除重载会话外没有任何恢复手段。修复后:同一请求返回 200 并携带该超大 turn(在 32 MiB 上限内),分页继续直到最老记录;若某页仍不可重试地失败,横幅提供可用的重试按钮。已在本地真实 transcript 上验证:全部 12 个近期会话端到端分页完成,包括此前必然失败的、含 13.6 MiB 单 turn 的 53 MiB transcript。测试平台
macOS ✅;Windows / Linux 未本地验证(交由 CI)。
环境(可选)
单元 + 集成测试;通过 reader 与 replay 管线在本地真实
~/.qwen/projectstranscript 上重放分页(无需 daemon)。风险与范围
关联 Issue
N/A