qwen-code Web Shell 历史记录记录边界分页:用 beforeRecordId 替代 HMAC 游标,彻底告别 invalid_transcript_cursor qwen-code Web Shell 历史记录记录边界分页用 beforeRecordId 替代 HMAC 游标彻底告别 invalid_transcript_cursor【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-codeWeb Shell 恢复长会话历史时需要在“按记录 ID 反向翻页”beforeRecordId与“按不透明游标翻页”nextCursor两种机制之间做选择。本文基于 web-shell-history-boundary-pagination 设计文档结合 Web Shell 会话 Provider 与 serve 路由 的实际源码讲解为什么 HMAC 签名的不透明游标在多运行时环境下会失效、如何用页面内最早一条持久化记录 ID 作为下一个独占边界持续反向翻页以及nextCursor如何退居兼容兜底。读完后你可以理解 Web Shell 历史分页的完整游标生命周期、错误码invalid_transcript_cursor的触发条件以及该设计的验证要点。背景Web Shell 历史分页的两条路径要理解本设计先要理解 Web Shell 加载长会话历史的整体机制其完整设计见 web-shell-history-pagination分页加载session/loadWeb Shell 通过POST /session/:id/load恢复会话并可通过 TypeScript SDK 的historyPageSize选项serve 路由按 transcript pager 的 1–500 区间校验只请求最近一页历史。支持该能力的服务端会在/capabilities.features中声明session_transcript_pagination特性Web Shell 仅在特性存在时发送该选项老服务端保持整包加载bulk-load行为。反向翻页GET /session/:id/transcript更早的历史通过 transcript 端点按需加载。该端点只在第一次反向请求中接受独占exclusive的beforeRecordId参数后续请求则使用响应里现有的nextCursor字段——一个签名signed游标内部记录了翻页方向为 backward并冻结了文件身份、活动 leaf、字节大小、位置与回放方向等快照信息。也就是说初始加载页的边界来自回放快照中最早一条记录的 UUID之后的每一页客户端理论上应该继续带上新的边界。问题恰恰出在“之后的每一页”用什么来定位。问题不透明游标可能跨运行时失效HMAC 签名的游标有一个固有约束验证它的一方必须持有签发时的密钥上下文。设计文档Problem 一节指出的故障场景是Web Shell 一开始用beforeRecordId发起反向翻页第一页响应后客户端切换到端点返回的不透明nextCursor当下一次请求被一个无法验证原始 HMAC 的运行时处理时例如 daemon 重启、多工作区/多运行时接管等场景下密钥上下文不再一致游标校验失败服务端返回invalid_transcript_cursor错误结果是用户已经拿到一半历史剩余的历史再也加载不出来且没有任何合法的翻页凭据可用。在 serve 路由源码中invalid_transcript_cursor是一个成体系的错误族cursor、direction、snapshot、start、beforeRecordId、turnAnchor等查询参数各自有独立的解析函数任一参数不合法都会返回 400 并携带code: invalid_transcript_cursor。例如 parseTranscriptRecordBoundaryQuery 要求beforeRecordId为长度不超过 200 的非空字符串而 parseTranscriptDirectionQuery 只接受directionbackward。这与设计文档“非法边界与游标返回既有 invalid-transcript-cursor 错误族”的约定一致。关键洞察是beforeRecordId本身只是一个记录 UUID 字符串不依赖任何运行时密钥。只要客户端能从已加载的页面里提取出下一个边界就可以完全绕开 HMAC 游标的验证问题。设计核心用记录边界持续反向翻页设计文档给出的方案非常克制只有两条原则只要页面中包含持久化记录 ID就用记录边界beforeRecordId继续反向翻页。保留nextCursor作为兼容兜底仅用于老 daemon 或不暴露任何持久化记录 ID 的异常页面。daemon API 与游标的安全语义均不改变。这个方案能成立依赖两个既有的实现事实事实一回放事件已打上qwen.session.recordId戳历史回放replay在把持久化的ChatRecord转成 ACP 更新时已经会把该记录的 UUID 写入私有更新元数据。Web Shell 侧的提取逻辑见 DaemonSessionProvider.tsx 中的getPersistedReplayRecordIdfunction getPersistedReplayRecordId(event: DaemonEvent): string | undefined { // A history_truncated marker may carry a recordId anchor stamped by // the daemons compaction engine — the last recordId it saw before the // truncation point. ... if (event.type history_truncated) { // 从截断标记中取 recordId 锚点兜底锚点 return getString(event.data, recordId); } if (event.type ! session_update) { return undefined; } // 从 session_update 的 _meta 中读取 qwen.session.recordId const meta isRecord(update) ? update[_meta] : event.data[_meta]; return isRecord(meta) ? getString(meta, qwen.session.recordId) : undefined; }可以看到该函数有两条取 ID 的路径正常情况读取回放session_update事件_meta中的qwen.session.recordId当保留窗口丢失了所有回合边界的session_update例如单次超长回合期间命中 live-journal 上限则回退到history_truncated标记上由 daemon 压缩引擎打上的recordId锚点。回放侧的戳写实现位于 acp-bridge 的 transcript-replay。事实二beforeRecordId是独占的端点契约中beforeRecordId语义是“返回该记录之前更早的记录”。因此一个已返回页面中最早的那条记录 ID恰好就是下一页的合法独占边界——不会与已渲染内容重复也不需要时间戳比较时间戳冲突无法区分同刻记录。游标更新逻辑边界优先游标兜底Web Shell 每处理完一页历史后的游标状态更新完整体现了“边界优先”策略。见 DaemonSessionProvider.tsxhistory.cursor nextBeforeRecordId undefined ? page.nextCursor : undefined; history.beforeRecordId nextBeforeRecordId;含义是若能从本页事件中提取到nextBeforeRecordId页面内最早的持久化记录 ID则下一次请求只用beforeRecordId同时清空nextCursor不再向服务端回传 HMAC 游标只有当本页没有任何持久化记录 ID老 daemon、畸形页面时才退回使用page.nextCursor。这一行代码就是本设计从“游标链”切换到“记录边界链”的落地点。由此多页历史中每一次反向请求都以纯记录 ID 定位跨运行时、跨重启都不存在 HMAC 验证问题。配套保障去重、容量与错误行为不变设计文档的 Verification 一节 列出了四条验收标准它们对应 Web Shell 客户端已有的完整机制边界分页不引入任何新的破坏面多页 transcript 的每一次 Web Shell 请求都使用beforeRecordId如上所述由nextBeforeRecordId提取逻辑保证已显示事件保持去重Provider 维护displayedRecordIds集合按块携带的sourceRecordIds过滤重复回放对不带 recordId 的块如边界处的成对事件还有一层内容感知的二级去重见 DaemonSessionProvider.tsx 中 “Secondary content-aware dedup” 的注释防止边界对boundary pair在翻页接缝处重复出现无持久化记录 ID 的页面仍以nextCursor前进即上文兜底分支既有的重试、终止错误、容量、畸形事件行为保持不变页面被保留窗口maxBlocks块数与字节预算拒绝时走既有容量capacity路径——要么记录rejectedPagefootprint 等待淘汰腾出空间后重开要么对“单页即超出整个窗口”的页面抛出终止性失败terminal failure而不是无限重试。这些逻辑在 DaemonSessionProvider.tsx 的历史页准入分支 中可见边界分页不改动它们。另外两条与整体分页契约一致、同样适用于本设计的约束继承自 web-shell-history-pagination快照被替换、回退rewind、归档或删除时返回transcript_snapshot_unavailableProvider 停止翻页并保留已渲染内容会话变更会使在途页结果失效stale-request protection防止旧页面污染新 transcript。影响范围与 API 稳定性本设计的刻意之处同样体现在它不做什么daemon API 不变GET /session/:id/transcript的查询参数cursor、direction、snapshot、start、beforeRecordId、turnAnchor与响应字段nextCursor、hasMore等均无变化参数校验逻辑集中在 serve 路由游标安全语义不变HMAC 签名游标仍按原语义签发与验证只是 Web Shell 在可提取记录 ID 的场景下不再依赖它TypeScript SDK 无需改动SDK 的 daemon 会话客户端 已同时支持beforeRecordId与nextCursor两种翻页凭据类型定义见 packages/sdk-typescript/src/daemon/types.ts本设计只是调整了 Web Shell 优先选用哪一种老客户端不受影响不传historyPageSize/beforeRecordId的客户端保留整包加载与正向翻页契约。测试侧多页边界行为在 transcript-page-table 测试 与 DaemonSessionProvider 测试 中有覆盖——后者包含大量围绕beforeRecordId重锚re-anchor、保留窗口淘汰后重定位锚点、以及“stale 页面不得合并回 transcript”的场景正是设计文档四条验收标准的自动化体现。小结Web Shell 历史记录边界分页的核心是把“下一页从哪里开始”的定位依据从依赖运行时密钥的 HMAC 游标切换为页面自身携带的持久化记录 ID故障根因是nextCursor的 HMAC 验证绑定签发运行时跨运行时即invalid_transcript_cursor修复手段是利用回放已存在的qwen.session.recordId戳与beforeRecordId的独占语义令每页最早记录 ID 成为下一边界游标降级为老 daemon / 异常页面的兜底daemon API 与游标安全语义零改动去重、容量、重试与错误行为全部沿用既有机制验证清单四条可逐条在源码与测试中核对。这套“优先使用内容自带身份、把不透明凭据降级为兜底”的模式对任何需要跨重启、跨进程保持分页会话的长列表/长 transcript 系统都有直接借鉴价值。【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考