跳转至

05 · Session:记录事实,重建模型看见的历史

Session 是整套架构最值得先掌握的数据结构。它不是 Chat UI 的消息数组,也不是 Agent 私有缓存。本章连续研究 types.ts、index.ts、surface.ts、fork.ts 和 repair.ts;当前逻辑格式 SESSION_FORMAT_VERSION = 4 与 npm 包版本 0.2.0-rc.2 不是同一种版本。类型定义。

5.1 身份、序号、偏移各有含义

SessionId 用 branded string 区分领域身份;SessionSeq 代表已经存在的事件位置,非负 safe integer;SessionLogOffset 代表间隙/前缀长度/读取起点,允许等于事件总数。同为 number 不能把“读到第几条”与“共有几条”混为一谈:最后一个事件 seq 是 N−1,下一 append 的 offset 是 N。两个 constructor 都拒绝负数、非整数、不安全整数和负零。数值品牌。

Header 放在日志外,包含 version、id、createdAt、绝对 cwd、parentSession、isSeeded、origin、delegationDepth、agentPreset。它深冻结,表示创建和存储归属;goal、turn、tool 与 prompt 更新是日志事件,不能塞到 mutable header 当作状态。旧 seedLength 字段被拒绝,精确 inheritedEventCount 属于 Session 状态/存储编码而非普通 header 字段。Header 验证。

5.2 完整的 core 事件词汇

事件 持久事实 加入 model surface
turn/start、turn/end 输入 claim 前开启与最终结局 否
step/start、step/end 一次请求及相应工具的步骤边界 否
system/message 渲染后的系统提示词 是,必须标 surfaceOp
user/message 人类输入、注入上下文、自动续轮等,source 区分 是
developer/message 当前增删工具的声明变化,addition 关联 headerSeq 是
assistant/message assembled content、同源精确 compact stream、可选 usage/interrupted 是
assistant/attempt 没有形成 surface 消息的尝试 stream 否
tool/call 模型工具名及原始 JSON string,callId 关联结果 否
tool/result model-facing message、错误身份、UI meta 是
request/header call config、adapterDefaults、当前 tools 否,但用于请求重建
request/context route/capacity/systemPromptUpdate 元数据 否,不参与 header equality
session/end-seed fork 或恢复的生命周期分隔 否

这些是 core 的字段,其他插件通过声明合并扩充 SessionEventMap,例如 inbox/spliced、goal/change、team/task、compaction。扩展事件默认 required-on-read;旧 reader 不认识则拒绝,只有事件 envelope 显式 ignorable:true 才允许忽略。不能为了版本兼容把会影响模型输入的事件标可忽略。Core 词汇,已知类型表。

5.3 assistant/message 与 assistant/attempt 为何要分开

一次模型 attempt 可能报错并重试,而同一 step 最后成功。若把前一个失败尝试的半截输出当 assistant/message,下一请求就会读到模型本不应承接的失败内容。DSH 把这种 settled failed/retried/stream-error attempt 记录为 assistant/attempt,保留用于诊断、计费或 replay 的 stream,但不加入 model history。

成功 assistant/message 同时保存 assembled blocks 和原始 compact timed stream;它们来源于同一组 chunks,不另存一套独立异步 message。AssistantStreamAttempt.push 一次 snapshot 后分别喂 accumulator、BlockAssembler 和 live emit;settle 先 append,成功以后才发 committed end frame。append 失败发 abandoned,不能向远程 UI 宣称已持久化。累积与结算。

取消途中若已有安全 text/reasoning prefix,循环记录 assistant/message interrupted:true;尚未执行的 tool-call 不加入该 prefix。没有可见 prefix 时只记录 attempt。未结算之前硬断电不会留下 durable attempt stream,这个数据模型没有承诺每个实时 chunk 都已 fsync。取消结算路径。

5.4 append 的逐阶段检查与提交点

Session.append 的顺序如下:完整 append。

  1. 从 opts 提取 surfaceOp/sourceEventSeqs,并对 data 和 metadata 分别做 lossless JSON snapshot。
  2. 拒绝 reentrant append,建立 {type,seq:log.length,time:Date.now(),data,…} 深冻结事件。
  3. 运行 event-local validation 和 SurfaceManager.validateNext,在日志改变前拒绝不合法替换和引用。
  4. 有 live store attachment 时取得 listener 快照;此时内部 dispatch 的同步异常仍可阻止提交。
  5. log.push(event) 是内存提交点,并清除 eventsSnapshot 缓存。
  6. 逐 observer contained 调用:同步 throw 或 Promise reject 写警告,其他 observer 继续。
  7. finally 清掉 publishing flag,处理在发布期间延后的 detach。

snapshotJsonValue 不是 JSON.stringify 的别名:拒绝 BigInt、function、symbol、undefined、负零、非有限数字、循环、稀疏数组、Map/Date/class instance 等不能无损表示的值。单次读取验证并复制也避免 stateful getter 在“校验”和“存储”返回不同值。append 的返回 data 是提交的快照,不是调用者之后仍可改动的对象。提交约束和来源说明。

模型事件有额外条件:request/header 不准有 system、空 tools、空 adapterDefaults;tool/result.error 必须对应 message.isError=true;developer 的 tool-addition 必须指向此前完整 request/header 中唯一同名工具定义。强 TypeScript 同进程边界与 seed/wire 边界验证强度不同,不可宣称 append 完整验证每个插件事件和每个 stream chunk 的所有字段。

5.5 内存提交不等于磁盘已经确认

session/event 是 append 后 fire-and-forget 的订阅,持久化插件可以 buffer。turn/end 明确没有自动 await flush。agent.whenIdle() 等待 loop 活动收敛,不保证你的远程数据库已经提交。

ctx.sessions.flush(session) 通过拥有者 carrier 取得所有 durability listeners,await 全部,并返回是否有至少一个 listener 参与;无 listener 的 false 与成功 fsync 不一样。需要读存储或确认调度投递的消费者应主动 flush;checkpoint policy 在请求边界提供另一种部署策略。flush 精确返回语义。

5.6 SessionStore 的 prepare / enter / announce

prepare 先构造未发布 Session,检查 id 与 header/seed;enter 重新检查 id 碰撞、建立 attachment 和 store entry,但不通知创建;announce 才发 session/created。这个三段事务使 Agent 可以先建立完整 scoped world,再让任何 observer 看见它。

SessionStore.create 把 enter 的 detach disposer 先 yield,再 announce;同步 creation observer 抛错时已 yield 的 detach 会回滚。creation observer 返回的 Promise reject 只记录日志,不能事后撤销同步创建。AgentLoop 使用同样原语,但更大 owner 还包括 loop quiescence、write handle 与 AgentRegistry。创建原语,prepare。

单独 ctx.sessions.create() 得到的 live Session 不必然持久化成一个已发布 Agent;正式 Agent 创建与持久化路径应使用 ctx.agents.create,让 factory 处理 seed、scope、所有权和 setup。不要创建两个各自同 id 的 Session 和 Agent 然后希望持久化插件自动配对。

5.7 seed 与恢复的三种所有权

Session.create 对借来的 seed/header 做 snapshot/validate/deepFreeze;Session.fromRestore 接受明确转移来的 independently-owned 或 shared-frozen seed,避免重复复制/冻结,仍验证 envelope、序号连续、surface transition 和 header。embedded stream 由 stream consumer/storage verifier 另查,不在这里全量解码。构造器连续实现。

字段 精确意义
inheritedEventCount durable fork 父前缀长度;不随恢复改变
firstLiveSeq 本次构造收到的完整 seed 长度,constructor marker 在它之后
firstLifecycleSeq 新 fork 以 inherited cut 开始;resume 以完整 stored prefix 开始

fork child 自己拥有 {inherited:true} 的 end-seed marker,位置恰好在 cut。resume 不修改 durable inherited cut,而追加普通 marker(若 tail 已是 marker 可能不需要)。插件不应自行写 end-seed;代码没有把其他 writer 全部禁止,错误写它会影响 bracket 所属生命周期分类。marker 构造规则。

5.8 fork:复制前缀,源日志保持原样

SessionStore.fork 只接受当前 live store 实例或 live id;错实例、不存在 id、无效 boundary 都有稳定错误。boundary 为 inclusive event seq,不是“第 N 个 turn”;缺省为最后事件,空 source 缺省可得到空 child。

buildForkSeed(events,boundary) 复制 [0,boundary] 前缀,紧接 tagged end-seed,再为开放 tail 合成缺失工具结果、step/end、turn/end(forked)。source 的后续结果不会自动进入 child。child metadata 标 isSeeded/parentSession/cwd,inherited count 只包含复制的真实前缀,marker 和修复是 child-owned。fork source 和边界检查,前缀算法。

flowchart LR O[父日志 seq 0 到 cut] --> N[child inherited prefix] O --> P[父继续执行] N --> M[child end-seed inherited] M --> R[若 tail 开放则补工具错误与边界] R --> C[child 后续新事件]

Subagent 的 fork provider 是一个更严格的消费者:只取父日志最后已完成 turn/end 之前的平衡前缀,排除正在执行的 delegation turn。不能把通用 Session fork 能切进 open tail 的能力推给该 provider。Subagent fork。

5.9 resume:先拿 write ownership,再语义修复

AgentLoop.resume 先要求 sessionPersistence,open(id,'write') 保证同进程 live handle 写归属,然后 read 日志;interruptedTurnClosers 处理 crash orphan,append closers 到同一个 write handle;再 prepare Session 和 scope,执行 setup,flush 未存 seed suffix,enter/announce。

caller cancel、owner unload 与 factory teardown 的 abort 组合覆盖 load/setup 窗口。backend 操作迟到成功时还要 release abandoned handle,不能在 Promise.race 失败以后忘掉迟到资源。创建失败不会把旧 stored generation 删除;后面的存储章解释物理迁移。恢复实现。

5.10 未回答工具的保守修复

ToolCallRecovery 观察 assistant/message 里的 tool-call 建 pending Map;tool/call 标 started seq;只有对应 turn/step 且 surfaceOp append 的 tool/result 删除 pending。step/end/turn 边界清 pending,所以已经封闭的历史并不会无限回补。

可见事实 合成结果 能否盲目重试
assistant 请求,没 tool/call TOOL_NOT_STARTED live/crash 文案允许仍需要时重试;fork 还需考虑父后来执行
有 tool/call,无 durable result TOOL_OUTCOME_UNKNOWN 写入可能发生,先查外部状态/幂等性
已 committed result 保持原结果 修复不覆盖

openTurnClosers 按 tool error → 若开放则 step/end → turn/end 顺序,seq 接着 tail 连续,time 复用最后真实事件以确定性重建。forked 与 interrupted 文案不同,但错误码共享。它修复 provider 所需 paired history,不是在重新执行工具,更不是给数据库副作用提供 exactly-once。修复完整源码。

5.11 自建 Agent 的 Session 设计清单

新增 durable 业务状态先定义 SessionEventMap 字段和 pure fold;新增 model-facing instruction 通过 inject 或 deferred user message 提交;只用于 UI 的 tool meta 不应误当模型内容;重写历史用 logged replacement/projection;脱机 reader 要显式加载日志需要的纯解释器。身份使用 branded ID,读取使用 seq/offset 的精确语义,外部写操作在恢复后查询事实再决定 retry。

最有价值的离线练习是:删除一个 required message projection,确认恢复拒绝;构造 open tool tail 再 fork,确认父不变且 child 补 conservative result;取消 request preparation,确认已 claim 的输入未进入 model surface。完整实验与审查记录见实验章。