qwen-code Daemon 归档会话导出:Workspace-Qualified Archived Session Export 协议与实现解析
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
导读
在 qwen-code 的守护进程(daemon)架构中,活跃持久化会话(active persisted session)可以直接从指定已注册工作区导出,但归档会话(archived transcript)此前必须先移动回活跃存储才能访问。本文围绕设计文档 daemon-archived-session-export.md 展开,深入讲解新增的只读归档会话导出能力:协议路由GET /workspaces/:workspace/session/:id/archive/export、无条件能力workspace_archived_session_export、SDK 方法WorkspaceDaemonClient.exportArchivedSession及其底层实现。读完本文,你将掌握如何通过 REST 协议导出归档会话、理解其与活跃导出的差异、错误语义、并发租约约束与 256 MiB 大小限制,以及它在源码中的完整调用链。
背景与动机:为什么归档会话不能直接导出
在引入本特性之前,daemon 只支持导出活跃持久化会话:
- 导出请求必须选中一个已注册工作区(registered workspace),并解析到该工作区的运行时(runtime);
- 归档会话(位于
chats/archive/<id>.jsonl)被视为"不可访问"状态,若想导出,必须先执行 unarchive 操作将其移回活跃存储; - 这会破坏归档状态机的语义:归档本意是让会话退出活跃视图、释放活跃存储占用,为导出而临时反转状态既繁琐又有副作用(例如可能触发状态迁移竞争、影响其他读取者)。
本设计的核心目标是在不改变活跃导出行为、不触碰归档状态机的前提下,为归档会话提供一条只读导出通道。
设计文档明确列出了新协议的三要素:
- 路由:
GET /workspaces/:workspace/session/:id/archive/export?format=html|md|json|jsonl - 能力:无条件(unconditional)宣告的
workspace_archived_session_export - SDK 方法:
WorkspaceDaemonClient.exportArchivedSession
其中最关键的设计约束是:归档导出路由与能力必须与活跃导出保持独立。这样做的原因在 capabilities.ts 的源码注释中写得很清楚:如果复用同一个能力标签,老版本 daemon 可能"忽略归档意图"(ignore archive intent),返回同 id 的活跃 transcript,造成数据混淆。
// Workspace-qualified full session export from archived persisted storage. // This remains independent from active export so older daemons cannot ignore // archive intent and return an active transcript with the same session id. workspace_archived_session_export: { since: 'v1' },对应的活跃导出能力是workspace_session_export({ since: 'v1' }),两者从 v1 起即同时存在、互不替代。
协议契约:选择器解析、信任检查与错误语义
选择器解析与信任前置
归档导出的选择器(selector)解析规则与活跃导出一致:
- 优先按精确的已注册工作区 id解析;
- 其次按URL 编码的规范化绝对 cwd(canonical absolute cwd)解析;
- 被选中的运行时(runtime)必须处于**受信任(trusted)**状态。
设计文档特别强调:选择器解析与信任检查先于会话与格式校验。也就是说,即使 session id 或 format 参数非法,只要工作区选择器不可解析或运行时不受信任,就会先被拒绝。这是"先定边界、再查数据"的安全顺序,防止未信任运行时被当作数据源探测。
在 routes/session.ts 中,归档导出路由通过handleSessionExport处理,并传入archiveState: 'archived'与resolveQualifiedSessionRuntime(..., 'archived'):
app.get( '/workspaces/:workspace/session/:id/archive/export', async (req, res) => { const route = 'GET /workspaces/:workspace/session/:id/archive/export'; await handleSessionExport(req, res, { route, resolveRuntime: (sessionId) => resolveQualifiedSessionRuntime( req, res, route, [sessionId], 'archived', ), workspaceQualified: true, archiveState: 'archived', }); }, );对比同文件中的活跃导出路由(/workspaces/:workspace/session/:id/export,见 routes/session.ts),两者共用handleSessionExport处理器与resolveQualifiedSessionRuntime,差异仅在于archiveState参数,这正是"复用现有导出收集器、格式化器与响应头"设计的落地体现。
数据源边界:绝不越界查找
归档导出只允许读取被选中工作区自己的chats/archive/<id>.jsonl。设计文档明确列出该路由不会做以下任何一件事:
- 不扫描活跃存储或其他工作区;
- 不回退到 primary 工作区;
- 不解析 live owner、不调用 bridge、不启动 ACP、不附加 client;
- 不加载 settings。
也就是说,归档导出是一条纯粹的本地只读文件投影路径,完全绕开运行时生命周期管理。
状态相关错误语义
由于归档与活跃可能共存或处于迁移中,路由对会话状态做了精确区分,返回以下 HTTP 错误:
| HTTP 状态 | 错误码 | 触发条件 |
|---|---|---|
| 409 | session_not_archived | 会话仅存在于活跃存储(active-only) |
| 404 | session_not_found | 活跃与归档均不存在 |
| 409 | session_conflict | 活跃与归档文件同时存在 |
| 409 | session_archiving | 会话正处于归档/反归档状态迁移中 |
这四类语义让客户端能根据错误码做出精确决策:例如session_not_archived应引导客户端改用活跃导出路由,session_archiving则应稍后重试。
核心消费面:SessionService.loadArchivedSession
唯一的新核心入口
归档导出的核心数据面只有一处新增:SessionService.loadArchivedSession,位于 sessionService.ts。源码注释明确说明其定位:
/** * Reads an archived session without changing its archive state. * Daemon load/resume paths must continue to use {@link loadSession}. */ async loadArchivedSession( sessionId: string, options: { maxBytes: number }, ): Promise<ResumedSessionData | undefined> {它的工作流程分三步:
- 会话 id 合法性校验:用
SESSION_FILE_PATTERN正则校验${sessionId}.jsonl,不合法直接返回undefined(不抛错、不落盘); - 路径解析与大小检查:通过
getSessionFilePath(sessionId, 'archived')定位归档文件;fs.statSync拿到文件大小后,若stats.size > options.maxBytes则抛出SessionTranscriptTooLargeError,否则继续;只有ENOENT(文件不存在)被吞掉并返回undefined,其他 I/O 错误照常抛出; - 委托重建:调用私有方法
loadSessionFromState(sessionId, 'archived', stats)完成 transcript 重建。
复用活跃加载的私有重建逻辑
loadSessionFromState(sessionService.ts)是活跃加载loadSession与归档加载共用的私有重建逻辑,包含:
readAllRecords读取 JSONL 记录并跟踪sourceReadComplete;- 首条记录校验
sessionBelongsToCurrentProject(会话必须属于当前项目,防止跨工作区串读); reconstructHistory重建线性历史,并检测不可恢复的gaps(缺失父节点等),有 gap 时输出 warn 日志;- 构造
ConversationRecord(sessionId、projectHash、startTime、lastUpdated 取自文件 mtime); - 通过
SessionFileHistoryAccumulator提取file_history_snapshot,供/rewind使用; - 通过
includeActiveSideArtifactRecords与rebuildSessionArtifactSnapshot重建 artifact 快照; - 附带
lastCompletedUuid、sourcesUnavailable、historyGaps等字段。
关键点在于:现有 load/resume 调用方仍只走活跃路径,loadArchivedSession是归档专属入口,二者互不干扰——活跃会话的恢复语义(例如 resume 到活跃分支)不会因归档读取而改变。
256 MiB 转录索引上限
归档导出与活跃导出的一个显著差异是大小上限。设计文档指出:
Before reconstruction, the archived-only loader enforces the existing 256 MiB transcript indexing limit and returns
413 transcript_too_largeabove it. Active export retains its shipped no-cap contract.
即归档加载器在重建之前强制实施既有的256 MiBtranscript 索引限制,超过即返回413 transcript_too_large;而活跃导出保持其发布的"无上限"(no-cap)契约不变。实现上,stats.size > options.maxBytes的检查发生在任何记录读取与重建之前,因此超大归档文件不会触发昂贵的 materialization(物化)过程。
对应测试位于 sessionService.test.ts,覆盖了合法加载、../outside这类路径穿越防护、以及超限场景。
并发与租约:SessionArchiveCoordinator 的双向互斥
导出操作(位置检查 + transcript 重建 + 格式化)全程持有既有的共享租约(shared lease)SessionArchiveCoordinator。设计文档对并发语义的描述是:
- 归档、反归档、删除(archive、unarchive、delete)保持排他租约(exclusive lease);
- 因此一次状态迁移要么在导出开始前发生并使导出被拒绝(返回状态相关错误),要么在导出持有的共享租约释放之后才开始;
- 不存在"导出读到半迁移状态"的窗口。
此外,协调器在跨工作区维度上保守地按 session id 加锁(remains conservatively keyed by session id across workspaces)——也就是说,即使两个不同工作区出现同 id 会话,锁仍按 id 串行化,宁可保守也不引入跨工作区锁序复杂度。这一点与"同 id 工作区隔离"测试(same-id workspace isolation)相呼应。
SDK 调用:WorkspaceDaemonClient.exportArchivedSession
方法签名与默认格式
TypeScript SDK 在 DaemonClient.ts 中提供WorkspaceDaemonClient.exportArchivedSession:
/** Export an archived persisted session from this registered workspace. */ exportArchivedSession( sessionId: string, opts: { format?: DaemonSessionExportFormat; clientId?: string; } = {}, ): Promise<DaemonSessionExportResult> { return this.client.sessionExportRequest( `/workspaces/${this.workspaceSelector}/session/${urlEncode(sessionId)}/archive/export`, 'GET /workspaces/:workspace/session/:id/archive/export', opts, ); }与活跃导出exportSession(DaemonClient.ts)相比,二者结构完全一致,仅 URL 路径多了/archive段。
传输细节
底层sessionExportRequest(DaemonClient.ts)的关键行为:
format默认值为html;显式传入 format 时以查询参数?format=<format>附加;- 走原生 REST 传输(
'rest'模式),并使用fetchWithTimeout; - 响应非 2xx 时通过
failOnError抛出带路由标签的 HTTP 错误; - 解析
content-type与content-disposition中的filename="...",取不到时回退为export.<format>; - 返回
{ content, filename, mimeType, format }四元组(DaemonSessionExportResult)。
支持格式与活跃导出完全一致:html | md | json | jsonl。
兼容性:老 daemon 上的行为
设计文档明确:活跃导出路由、workspace_session_export能力、legacy primary 导出、归档变更操作与持久化布局全部保持不变。对于直接调用 SDK 的客户端,如果目标 daemon 是未实现本路由的老版本,exportArchivedSession会收到正常的 HTTP 错误(而不是静默返回活跃 transcript),这正是能力与路由独立设计带来的兼容性保证。
SDK 单测见 DaemonClient.test.ts,覆盖了workspaceById/ cwd 选择器调用归档导出的路径。
验证矩阵:测试覆盖了哪些行为
设计文档列出的测试覆盖范围可以作为完整的验收清单:
- 能力宣告(capability advertisement):
workspace_archived_session_export出现在能力列表中; - id 与 cwd 两种选择器:注册工作区 id 与 URL 编码的规范化 cwd 均可选中工作区;
- 全部四种格式:html / md / json / jsonl 导出一致;
- attachment 元数据:附件的元数据(content-disposition filename、mime 等)正确传递;
- active / missing / conflict / transition 四态:对应
409 session_not_archived、404 session_not_found、409 session_conflict、409 session_archiving; - 信任优先级:选择器与信任检查先于会话/格式校验;
- 同 id 工作区隔离:不同工作区同 id 会话互不串读;
- 无 bridge 活动:导出过程不会触发 bridge / ACP / client 附加等副作用;
- 两种锁方向:排他迁移先于共享导出(拒绝),与共享导出先于排他迁移(等待)两条路径;
- 核心归档重建:
loadArchivedSession的 transcript 重建正确性; - telemetry 归属:导出事件的遥测属性正确归因;
- 原生 REST SDK 传输:SDK 直连 daemon REST 端点的端到端行为;
- 大小测试:恰好等于归档上限的文件被接受,稀疏文件(sparse file)超出一字节即在 transcript 物化之前被拒绝。
与其他文档的衔接
- 协议层面的完整路由表可对照 qwen-serve-protocol.md;
- 能力版本化(capability versioning)的设计约束见 11-capabilities-versioning.md;
- SDK daemon client 的整体用法见 13-sdk-daemon-client.md;
- 活跃导出的用户侧说明见 qwen-serve.md。
小结
归档会话导出特性在 qwen-code daemon 中是一个"小而完整"的协议扩展:一条独立路由 + 一个无条件能力 + 一个 SDK 方法,背后是核心层loadArchivedSession对归档路径的只读重建、256 MiB 上限的前置校验、SessionArchiveCoordinator的共享/排他租约互斥,以及覆盖状态机四态与安全边界的完整测试矩阵。它让归档会话在不改变归档状态、不启动任何运行时副作用的前提下获得与活跃导出一致的四种格式输出,同时通过路由与能力的独立设计确保了与老 daemon 的兼容性。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考