qwen-code Session 存储生命周期修复:损坏、孤儿与 Active/Archive 冲突的确定性维护方案
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
导读
本文讲解 qwen-code 项目中「会话存储生命周期修复」(Session storage lifecycle repair)的设计与实现:在会话 transcript 为空、头部撕裂损坏或属于父级已不存在的遗留子会话(legacy orphan)时,依然能够对物理文件执行确定性的 delete、archive、unarchive 操作,并为 active/archive 目录同时存在同名会话文件的冲突场景提供显式的resolveConflicts修复选项。读完本文,你将掌握该仓库生命周期分类器的判定规则、写入者锁与 generation 防护机制,以及 REST/ACP/SDK 三层 API 的兼容性演进方式。
问题背景:生命周期操作不应依赖「可加载的 transcript」
在 2026-08-22-session-storage-lifecycle-repair.md 描述的问题中,会话生命周期操作(删除、归档、取消归档)此前依赖「将 transcript 加载为可用会话」这一前置条件。这对于 delete、archive、unarchive 而言约束过强:
- transcript 文件为空(empty);
- 文件头部撕裂或格式损坏(torn / malformed head);
- 属于遗留子会话(legacy child),其父会话已不存在。
上述文件虽然无法被加载为对话,但只要「选定的工作区(workspace)」与「归档状态」是明确的,物理文件仍然需要维护。否则这些物理文件会被遗留在磁盘上(stranded),既无法加载,也无法通过正常生命周期操作清理。
该变更的目标是:让 delete、archive、unarchive 直接作用于「所选工作区拥有的、拼写精确匹配的常规 transcript 文件」,并为 active/archive 冲突新增显式修复选项;同时不改变 transcript 加载、列举、导出、组织、Live 会话身份以及通用的大小写不敏感查找逻辑,正常生成的会话 UUID 仍保持规范的小写形式。
存储分类:生命周期分类器的判定规则
分类器(lifecycle classifier)会同时检查请求 UUID 文件在 active 与 archived 两个目录中的存在情况,其判定语义如下:
| 判定条件 | 结果 |
|---|---|
| 路径不存在 | 不贡献任何状态 |
| 常规的空文件或损坏文件 | 贡献「物理状态」(physical state) |
| 可读的首条记录 | 必须标识出请求的会话与所选工作区 |
| 首条记录标识了其他工作区 | 视为未找到(not found) |
| 符号链接、目录或其他非常规条目 | 失败关闭(fails closed) |
| active 与 archived 同时存在 | 构成冲突(conflict) |
分类器会捕获文件的身份信息(file identity),用于纯物理维护。在 unlink 或 rename 之前,Core 会再次验证「同一个常规文件仍占据相同路径」。
在源码层面,该逻辑位于 sessionService.ts 的resolveMaintainableSessionSnapshot:
- 对
active、archived两种状态逐一调用readMaintainableSessionIdentity(sessionService.ts); - 打开文件时使用
O_RDONLY,在非 Windows 平台额外叠加O_NOFOLLOW与O_NONBLOCK,从而对符号链接、目录等非常规条目失败关闭:捕获ELOOP时抛出SessionStorageEntryError(sessionId, 'non_regular'); - 首条可读记录若
sessionId或cwd不是字符串、sessionId大小写不匹配、或cwd不属于当前项目,则分别抛出unknown_project/foreign_project错误(sessionService.ts); - 任一状态出现 foreign 且无任何有效身份时抛出
foreign_project,出现 foreign 但同时存在其他身份时抛出ambiguous_project(拒绝歧义所有权); - 最终根据 active/archived 身份的有无归约为
conflict、active、archived或undefined(未找到)四种位置。
SessionStorageEntryError定义在 sessionService.ts,归档/取消归档循环中捕获reason === 'foreign_project'并归入notFound,而不是返回错误。
路由规则:工作区限定与无工作区路由
路由层面分为两类:
- 工作区限定路由(workspace-qualified):只变更所选受信任运行时的存储;
- 无工作区普通路由(workspace-less):保留既有主工作区行为。Internal Conversations 运行时仅当请求批次具有唯一无歧义的所有者时才会被选中。
关键约束是:运行时选择在持有生命周期协调器锁(lifecycle coordinator lock)期间重复执行;如果 generation 已变化或不可用,返回既有的可重试运行时错误,而不是变更陈旧的存储(stale storage)。
Provenance(来源信息)对加载与列举仍是权威依据,但不再阻止对「归本工作区所有的常规 transcript」的维护——这就允许清理遗留孤儿子会话,同时不使其变为可加载。从代码看,resolveMaintainableSessionSnapshot的可读首条记录只要求 sessionId 与 cwd 归属当前项目,而非要求记录结构完整可重建对话,正是「物理所有权」与「可加载性」解耦的体现。
冲突修复:默认非破坏,显式 resolveConflicts
默认行为:非破坏性的冲突错误
archive 与 unarchive 保持默认的非破坏行为:active/archive 冲突以「按会话错误」(per-session error)返回,两个持久化副本都不会被移动、删除或覆盖。
两个细节值得注意:
- archive 仍会先严格关闭活动会话,再分类冲突:这样队列中待写入的记录有机会 flush 到 active transcript,避免分类在脏数据上进行;
- 批量生命周期路由(含工作区限定路由)以
200响应返回该结果,取代此前工作区限定的409 session_conflict错误封套——冲突不再以整个请求失败的形式呈现,而是体现在批量结果中的 per-sessionerrors数组里。
显式修复:resolveConflicts: true
调用方可以发送resolveConflicts: true来显式修复冲突:
| 操作 | 修复语义 |
|---|---|
archive +resolveConflicts: true | 保留 archived transcript,移除 active transcript |
unarchive +resolveConflicts: true | 保留 active transcript,移除 archived transcript |
响应新增resolvedConflicts数组,包含被该请求修复的会话 ID。delete 保持既有兼容行为,无需任何选项即移除两份副本。
在 sessionService.ts 的archiveSessions实现中,冲突分支(location === 'conflict')在resolveConflicts缺失时直接抛出Session archive conflict: ${sessionId};开启后则:
- 先执行
assertStorageUnchanged与assertCanMutate校验; - 调用
assertMaintainableSessionUnchanged(sessionService.ts)逐状态比对快照身份,任何不一致抛出SessionTranscriptChangedError; - 移除 active 文件,并把 PR sidecar、worktree 路径、prompt ledger 等副产物一并迁移或清理;
- 将 sessionId 同时计入
archived与resolvedConflicts。
unarchiveSessions(sessionService.ts)对称处理:保留 active、移除 archived,并额外通过retitleIfCollidesWithActive避免归档会话的存储标题与活动会话撞名。
写入者与 generation 安全
维护操作在 per-session 生命周期协调器与写入者锁(writer lock)下运行:
- 损坏 transcript 使用同一把写入者锁,但携带维护哨兵(maintenance sentinel):因为常规写入者证明(writer proof)会刻意拒绝撕裂的 JSONL;
- 哨兵不会绕过认证交接(certified handoff):如果写入者封存锁之后 transcript 字节发生变化,接管仍以
SessionTranscriptChangedError失败关闭,需要操作者介入; - Core 在提交变更前仍会对实际 transcript 路径做快照与验证。
Generation 防护共三层检查:获取协调器之后、文件系统提交之前、操作完成之后各检查一次所选运行时 generation。因此,即使运行时被替换,也不可能把进行中的变更静默重定向到另一个运行时。
测试侧,SessionTranscriptChangedError与 generation fencing 的覆盖见 sessionService.corruption.test.ts 以及 sessionService.test.ts(后者的resolveConflicts/resolvedConflicts用例覆盖了显式修复与替换检测)。
API 兼容性:REST、ACP 与 SDK
请求体与能力声明
REST 与 ACP 的 archive/unarchive 请求体接受可选的布尔字段resolveConflicts:
- 省略该字段保留非变更的冲突行为;
- 工作区限定的 REST 传输层按上文所述在批量结果中报告冲突;
- 非布尔值属于无效请求(invalid request)。
服务端通过session_storage_conflict_repair能力位(capability)通告对选项与响应字段的支持,声明位于 capabilities.ts(since: 'v1')。
响应字段与 SDK
archive 与 unarchive 响应新增resolvedConflicts:
- SDK 结果类型将该字段保持为可选(
resolvedConflicts?: string[]),使旧客户端对旧 daemon 保持兼容,而新 daemon 始终返回数组——见 types.ts; - 既有 SDK client-ID 调用形式保持有效,选项通过额外的重载位置传入(见 DaemonClient.ts)。
REST 行为验证
server.test.ts 中的集成测试精确验证了契约:
resolveConflicts: 'yes'(字符串)返回400,code为invalid_request——证明非布尔值被拒绝;resolveConflicts: true归档冲突会话返回200,响应体为{ archived: [sid], resolvedConflicts: [sid], errors: [] },且 active 文件被移除、archived 文件保留;- unarchive 方向对称验证。
能力位session_storage_conflict_repair也在该文件的 capability 清单(server.test.ts)中被断言。
验证矩阵:回归测试的覆盖维度
回归矩阵覆盖 delete、archive、unarchive 在以下三类 transcript 上的行为:
- empty(空文件);
- damaged(损坏文件,如撕裂的 JSON 头部);
- legacy-orphan(遗留孤儿,其记录的 cwd 属于当前项目但父会话已不存在)。
测试分别运行在 Core 层、工作区限定内部路由、以及无工作区的唯一所有者路由(unqualified unique-owner routing)。每个成功用例都会断言最终的 active/archive 文件状态,且移动操作保留原始字节。
在 sessionService.corruption.test.ts 中可以看到具体形态:
unreadableShapes数组定义了{ name: 'empty', content: '' }与{ name: 'damaged', content: '{"uuid":"torn-head"' }两种不可读形态;- 对每种形态,测试断言:delete 后 active 文件不存在;archive 后 active 消失、archived 文件内容与写入时逐字节一致(
fs.readFileSync(paths.archived, 'utf8')等于原 content),证明「不重写、原样移动」; - 冲突修复测试(sessionService.corruption.test.ts)分别以
resolveConflicts: true验证 archive 保留 archived 内容('{"uuid":"torn-archived"')、unarchive 保留 active 内容('{"uuid":"torn-active"'),并断言resolvedConflicts数组。
兼容性测试则保留默认 archive/unarchive 冲突行为与 delete-both 行为,并补充覆盖:双向显式修复、外来/歧义所有权拒绝(foreign_project/ambiguous_project)、无效选项类型、SDK 请求体、generation fencing 与替换检测。
总结:设计要点一览
| 关注点 | 设计决策 |
|---|---|
| 可维护性判定 | 以「物理所有权」替代「可加载性」,空/损坏/孤儿文件仍可维护 |
| 所有权校验 | 首条记录必须匹配会话 ID 且 cwd 属于当前工作区,外来/歧义一律拒绝 |
| 非常规条目 | 符号链接、目录等 fail closed(non_regular) |
| 冲突默认行为 | 非破坏:per-session 错误,两份副本均不动;批量路由返回200 |
| 显式修复 | resolveConflicts: true:archive 留归档删活动,unarchive 留活动删归档 |
| 写入安全 | 维护哨兵 + writer lock,认证交接仍 fail closed;变更前快照验证路径身份 |
| 并发防护 | generation 在协调器获取后、提交前、完成后三处校验 |
| API 演进 | resolvedConflicts可选字段保持旧客户端兼容,新 daemon 恒返回数组;capability 通告 |
通过「物理状态分类 + 显式冲突修复 + 写入者/ generation 双重防护」的组合,该设计让会话存储的维护从「依赖完整对话」变为「依赖确定的物理所有权」,为损坏与遗留数据提供了一条可安全执行的清理路径,值得在同类 AI 编码代理的会话存储层中借鉴。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考