内部会话结构与事件模型
append-only SessionEvent 如何成为模型上下文的事实源
结论:完整日志保存事实,Surface 决定模型此刻看见什么
DeepSeek Harness 的 Session 不是消息数组,也不是 Provider SDK transcript。它是一条不可变、连续编号、可扩展的 SessionEvent 日志;其上再折叠出一个可替换的 model-visible Surface。Compaction、tool-result pruning 和其他上下文改写只会在 Surface 上遮蔽旧节点,原始事件仍留在完整日志中。
这个设计同时保留三种语义:原始事实用于审计和 replay,当前 Surface 用于下一次模型请求,append-origin 事件用于不应被压缩重写的人类 transcript。理解这三层,是理解后续持久化、压缩、恢复和 UI 的前提。
1. SessionHeader 与 SessionEvent Log 分工不同
| 层 | 保存内容 | 变化方式 | 语义 owner |
|---|---|---|---|
SessionHeader | format version、session ID、createdAt、cwd、parent、seedLength、subagent origin/depth、agent preset | 创建时快照,事件日志之外 | 生命周期 identity 与恢复所需 composition SESSION-FORMAT-HEADER |
SessionEvent[] | 对话、请求、工具、策略、压缩、编排和诊断事实 | 只 append;seq 从 0 连续增长 | Agent interaction 的逻辑事实源 |
SessionSurface | 当前 model-visible event seq 顺序 | append 或 positional replace | 下一次 deriveMessages() 的输入 |
Header 中的 parentSession 与 seedLength 是持久 fork lineage;delegationDepth 防止子 Agent resume 后被误当顶层;agentPreset 绑定恢复时的工具与 prompt composition。它们不需要伪装成 conversation event。
2. Event envelope 很小,但约束非常强
每个事件是按 type 区分的 union:{type, seq, time, data},可选字段只有 surfaceOp、sourceEventSeqs 和 ignorable: true。任何额外 envelope key 在 seed/load 时拒绝。SESSION-EVENT-ENVELOPE
seq是会话内唯一排序权威;live append 直接取log.length,seed 必须从 0 无洞连续。time是Date.now()的 wall-clock 记录,不承担严格单调排序;同毫秒或系统时钟回拨都不改变 seq 顺序。data必须是 lossless JSON,而不是“能被JSON.stringify大致处理”即可。- 只有
user/message、assistant/message、tool/result能携带 Surface metadata。 - 未知 required event 不能静默忽略;只有 writer 明确标
ignorable: true才表示丢失它不改变重建。
所有跨事件关联都以 seq,而不是 timestamp、array identity 或数据库 row ID 表达。因此 JSONL、SQLite、内存 replay 和浏览器传输可以共用同一逻辑坐标。
3. 固定基线识别 44 种事件
当前构建的 known-event catalog 从全仓库的 SessionEventMap 声明生成,共 44 种。核心包声明 13 种,其余由各 owner 通过 TypeScript module augmentation 加入。SESSION-CORE-EVENTSSESSION-KNOWN-EVENTS
| 所有权组 | 事件(无省略) | 数量 |
|---|---|---|
| 核心会话 | turn/start, turn/end, step/start, step/end, user/message, assistant/chunk, assistant/message, tool/call, tool/result, todo/write, request/header, request/context, session/end-seed | 13 |
| 控制与策略 | agent-preset/selected, agent/inbox/spliced, approval/asked, approval/decided, approval/policy, permission/preset, sandbox/mode, plan/mode, goal/change, schedule/change | 10 |
| 压缩与重试 | compaction/start, compaction/prune, compaction/summary, compaction/end, llm/retry, llm/retry-started | 6 |
| 辅助模型与元数据 | session/title, session/title-llm-request, web/deepseek-search-llm-request, feedback/record | 4 |
| 命令与 Hook | command/run, command/done, hook/invoked, hook/result | 4 |
| 子 Agent、Workflow 与 Code Mode | subagent/descriptor, tool-workflow/run-start, tool-workflow/run-end, tool-workflow/agent-start, tool-workflow/agent-end, tool/code-dispatch, tool/code-dispatch-start | 7 |
Catalog 说明“这份 build 理解哪些 vocabulary”,不说明所有事件都会进入模型。大多数事件是 log-only audit/control facts;只有三类 Surface event 可能派生 Message。
4. Lossless JSON 是写入边界,不是存储后补救
snapshotJsonValue() 用显式 task stack 遍历 graph,在同一次 property read 中完成校验与复制,避免 stateful getter 在“先检查、后复制”之间换值;深嵌套受内存而非 JavaScript call stack 限制。SESSION-LOSSLESS-JSON
| 接受 | 拒绝 | 原因 |
|---|---|---|
| null、boolean、string、有限 number | -0、NaN、Infinity | 必须 JSON round-trip byte-semantically 等价 |
| dense plain Array | sparse Array、额外 own property、Array subclass | JSON 会补 null 或丢属性/原型 |
| plain/null-prototype object | Date、Map、Set、class instance、伪造 prototype | 不调用 toJSON,不接受隐藏转换 |
| own enumerable string keys | Symbol key、non-enumerable key、function、undefined、BigInt、cycle | 这些值会被丢弃、改形或无法编码 |
事件和嵌套 Message 在接纳时 deep-freeze;派生历史共享这些冻结对象。测试对 content 和 tool result 的修改都会抛 TypeError,非法 graph 在源头拒绝且日志长度不变。SESSION-IMMUTABILITY-TEST
5. Session.append() 是一条两阶段本地事务
拥有输入
Snapshot data 与 Surface metadata;caller 后续 mutation 不再相关。
构造候选
分配 seq = log.length、读取 wall time,deep-freeze 完整 event。
计划 Surface
只验证 candidate,尚不修改已提交 node list。
Precommit dispatch
internal/dispatch 让 invariant/diagnostic 纯验证;任何 veto 都发生在日志变化前。
Commit
log.push(event) 是逻辑提交点,同时使旧 events array cache 失效。
Postcommit publish
按 snapshot 的 listener list 通知 session/event;同步 throw 和 async reject 都逐 listener 记录并隔离。
同一个 frozen candidate 从 precommit validation 穿过 commit 到 observer。Observer 期间的 reentrant append 被拒绝,避免嵌套提交重排后续 listener。SESSION-APPEND-COMMIT
测试证明 internal dispatch veto 不写日志、不发布事件、不推进 Surface;commit 后 hostile observer 不能回滚事件,也不能阻止后续 observer 看见它。SESSION-APPEND-BOUNDARY-TEST
6. Surface 是 append-only log 上的一门小型替换演算
Runtime 只承认 user/message、assistant/message 与 tool/result 为 Surface-eligible。它们必须声明 surfaceOp;boundary、chunk、request 或 plugin audit event 携带 Surface metadata 会失败。SESSION-SURFACE-PROJECTIONSESSION-SURFACE-PROVENANCE
| Operation | 当前 node list 变化 | 完整日志变化 | 主要用途 |
|---|---|---|---|
append | 把当前 event seq 加到 tail | 追加同一 event | 正常 user/assistant/tool result |
{op:'replace', start, end} | 按当前 Surface 顺序,把 inclusive range 原子替换为新 event seq | 只追加 replacement event;旧事件不删除 | Compaction summary、tool-result content rewrite |
start/end 是当前 Surface node 的事件 seq,不是原始日志中的连续切片。两端必须仍在当前 Surface 且顺序合法;被遮蔽节点可能被其他 log-only 事件隔开。SESSION-SURFACE-REPLACE
一个 replacement tool/result 只能目标一个当前 tool result,并且只能改嵌套 result block 的 content;call ID、error flag、message identity、turn/step 与 meta 都必须结构等价。这让 spill/prune 可以缩短输出,却不能伪造另一次工具执行。
7. sourceEventSeqs 是显式血缘,但不是全覆盖 lineage
Source list 若存在,必须 dense、无重复、全是非负 safe integer 且严格早于当前 event。Surface replacement 还必须至少包含每个被遮蔽 node;否则无法证明新节点替代了什么。SESSION-SURFACE-PROVENANCE
| 生产者 | 典型 source | 语义 |
|---|---|---|
| Assistant assembler | 该 attempt 的全部 assistant/chunk seq | 最终 Message 来自哪些 raw stream facts |
| Tool result | tool/call seq | 结果关联哪个已记录的副作用意图 |
| Compaction replacement | 全部 shadowed Surface nodes,另可含摘要输入事实 | 模型可见替换的来源集合 |
| 普通 user/plugin message | 通常缺席 | 自身就是新事实,不声明派生来源 |
8. deriveMessages() 是 Surface 的纯投影
user/message 原样返回 user message;非空 assistant/message 返回模型消息;tool/result 返回 user-role result。空 content assistant 只用来承载 usage,不进入 transcript;其他 41 种事件均返回 null。SESSION-SURFACE-PROJECTION
没有 replacement 时只折叠上次之后的新 Surface node,复杂度是 O(new nodes);replace generation 变化时清空并从当前 node list 重建。每次 API 返回新的 array snapshot,内部 Message object 继续共享冻结 event data。SESSION-DERIVED-CACHE
测试在普通 append、空 assistant 和 replacement 后,用完整事件创建 fresh Session,要求其派生结果与 live incremental cache deep-equal;之前返回的 array 不会因后续 append 自动增长。依赖未安装,本研究未执行。SESSION-DERIVED-ORACLE-TEST
Message framing 由 producer 写进 content,Surface projector 不再按 source 二次包裹。这减少隐藏 prompt mutation,却要求每个 context producer自己维护正确的 model-facing framing。
9. 同一日志支持三个不同读视图
| 视图 | 包含什么 | 不应拿它做什么 |
|---|---|---|
| Complete event log | 44 类事实、raw chunks、被遮蔽消息、所有 control marker | 不能直接当 Provider messages |
| Current model Surface | replacement 后的当前 node 顺序,再经 message projection | 不能当用户“曾经看过什么”的永久 transcript |
| Append-origin transcript | 只取最初以 surfaceOp:'append' 进入的 message events | 不能代表压缩后下一请求的上下文 |
Surface 模块明确警告:如果人类 transcript 直接读取当前 Surface,compaction 一落地就会让用户已经看过的对话消失;replacement copy 是 model-only 视图。反过来,只用 append-origin transcript 构建下一次请求,又会完全忽略压缩。
10. Seed 把恢复前缀与本生命周期分开
Seed 的每个 event 被 detach/adopt、freeze、校验 envelope、request header、连续 seq 和 Surface transition;失败不会留下半个 Surface。构造完成时 firstLiveSeq 记录 seed 长度,若尾部尚无 session/end-seed 则追加 marker。SESSION-SEED-ACCEPTANCE
Fresh Session 不带 marker;显式空 seed 也会得到一个 marker;已有 marker 的 seed 再打开不会继续追加。Seed prefix 原样保留,派生 Message 与原 Session 相同。SESSION-SEED-REPLAY-TEST
Marker 不是“当前是否有其他 writer”的锁,也不是 crash checkpoint。它只表示 constructor seed 与本生命周期新增事件的边界;fork、resume 和测试 replay 可据此区分继承事实与新工作。
11. Version 0 的兼容策略是 fail-closed
SESSION_FORMAT_VERSION 当前固定为 0。Header/envelope/core semantics/Surface mechanism 的破坏性变化应 bump;本基线没有 upgrade chain 或迁移,版本不等即拒绝。普通新事件理论上不必 bump,因为 envelope 有 ignorable。SESSION-FORMAT-HEADER
Persistence normalization 后用 generated known catalog 检查整个日志。未知 required event 会产生 SessionFormatUnsupportedError,而 ignorable:true 的未知 event 会保留在 loaded log,让知道它的消费者仍可读取。SESSION-UNKNOWN-EVENT-GATESESSION-UNKNOWN-EVENT-TEST
默认“required”会导致过度拒绝,但不会让旧 runtime 在不知道新语义的情况下悄悄重建出错误对话。这是把 forward compatibility 的风险偏向可见停机,而不是 silent corruption。
12. Always-on storage guards 与 optional relational invariants 分离
| Always-on Session root | Optional dsh-session/invariant |
|---|---|
| Lossless JSON、snapshot、freeze | Turn 从 1 连续编号且不重叠 |
| Envelope 与 seed seq 连续 | Step 在当前 Turn 内从 1 连续编号且闭合 |
| Surface eligibility、range 与 source coverage | Assistant/chunk/tool 等 execution event 必须在当前 Step |
| Tool-result replacement 只能改 content | 同 Step 的 tool call/result 配对 |
| Request-header shape 的少量兼容校验 | 插件扩展事件由各自 owner 的 invariant 检查 |
Companion 从已有日志重放 trace;新 candidate 在 internal/dispatch 中只生成暂存 transition,真正的 session/event 到达后才推进。后续 precommit listener veto 不会让 invariant state 超前。SESSION-RELATIONAL-INVARIANTSESSION-INVARIANT-COMMIT
13. 三个完整性缺口与两个有意取舍
类型注释称 constructor 是 session/end-seed 的唯一合法 writer,并直接承认 plugin 追加会错误重分类此前 live bracket;但 public append() 接受该 type,optional invariant 也明确不约束它。当前安全性依赖 in-process plugin 自律,而非 capability 或 runtime guard。SESSION-CORE-EVENTSSESSION-APPEND-COMMIT
Envelope 定义 ignorable?: true,loader 也实现了 forward-compatible gate;但 Session.append(type, data, surfaceIntent?) 不接受 envelope options,生产源码没有任何 append 写入 ignorable:true。因此本基线的普通新事件 producer 无法通过公共路径使用文档描述的“无需 bump 的可忽略事件”机制。SESSION-EVENT-ENVELOPESESSION-APPEND-COMMIT
Always-on root 深校验 JSON、三类 Message、request header 和 Surface,但没有为全部 44 个 payload 运行统一 schema。关系校验又在可选 companion。对受信代码这是轻量扩展点;若把第三方插件视为不可信 writer,这不是完整防线。
- 有意取舍:
sourceEventSeqs对普通 append 可缺席,避免每条新事实都维护昂贵 lineage;代价是 provenance 不完整。 - 有意取舍:live commit 与 disk durability 分开,保持 hot-path 同步和无 I/O;代价是调用方必须在外部副作用前正确使用 flush barrier。
14. 优势、代价与验证状态
| 设计选择 | 主要收益 | 主要代价 |
|---|---|---|
| 完整 append-only log + replaceable Surface | 保留审计事实,同时允许上下文治理 | 每个消费者必须选对视图 |
| Lossless JSON + deep-freeze | 跨后端 replay 稳定,消除 caller alias mutation | 无法直接记录 Date/Map/typed object 等丰富 runtime 值 |
| seq 统一坐标 | 日志、Surface、source lineage、client dedup 共用身份 | 跨 Session 因果需要额外 header/event 关联 |
| Module-augmented event vocabulary | Package owner 可独立扩展 | 需要 generated catalog、版本门和 owner invariant 保持同步 |
| Precommit validation + contained observers | Veto 与 notification failure 的语义清晰 | Observer 只能补偿,不能回滚已提交事件 |
本章复核了类型、生产 append/surface/invariant 控制流、generated vocabulary 与测试源码。上游依赖未安装,因此所有测试段落都表示“测试定义的预期合同”,不是本地执行结果。下一章将在这份事件模型之上继续追踪 raw log、fork、repair、history 与 client replay。
我的学习体会
内容仅自动保存到当前浏览器,不上传、不进入仓库。你可以导出 Markdown 自行归档。