The chat stats line took its token totals from the loaded conversation nodes, so paging changed them and compaction erased the billing behind replaced content. It also had no way to show context occupancy: the numerator and capacity never reached the browser. Both now come from token-meter session projections read through the standard useProjection seat. Window nodes keep supplying turn and step counts plus LLM and tool wall times, which are correctly window-scoped facts about what is on screen; accounting no longer comes from there. `tokenUsage` supplies billing and cache hit. `contextPressure` supplies occupancy, pairing the newest provider-reported prompt size with the newest capacity recorded by `request/context`. Deployments without token-meter drop the token groups; a route whose adapter advertises no capacity drops the occupancy group rather than rendering a placeholder. Occupancy is deliberately approximate: the numerator and capacity are independent last-wins fields, not one atomic request observation, so switching models pairs a fresh capacity with the prior route's pressure until the next request reports usage. It is a user-facing reference figure that nothing in the harness makes decisions from, and it matches how the TUI status line has always computed occupancy. The Agent Note and token-meter README state this as a decision, including why the atomic alternative was implemented and rejected, so it is not re-litigated as a defect. Snapshot delta is one added `Context N% of 128K` segment across eight web goldens; the preceding commit absorbed master's pre-existing golden drift.
17 KiB
dsh-session
English | 中文
事件溯源的会话日志和内存存储。Session 是 agent(智能体)全部交互历史的仅追加真源,LLM(大语言模型)消息历史由它派生。原始日志之上维护一个 surface 层(产生消息事件的有序投影),以便高效派生和压缩(compaction)。
可选配套入口 @deepseek-ai/dsh-session/invariant 将此包(package)的关系轨迹检查注册到 ctx.invariants:序号单调递增、轮次/步骤闭合,以及同一步骤内的工具调用/结果配对。加载或重新加载时,它会回放现有会话;存储校验、快照、冻结、溯源信息和 surface 准入仍始终由根会话包负责。
服务:SessionStore(ctx 键:sessions)
创建并持有事件溯源的 Session 实例。这里有意不实现持久化:插件订阅 session/event,在 session/flush 时刷新,并可镜像成对的 session/created/session/disposed 生命周期。
公共 API
ctx.sessions.create(id?, { seed?, meta? }?)校验持久种子/头部数据并生成脱离副本,补齐版本和 id,在未提供createdAt时使用当前时间,发布会话并将其绑定到调用方 fiber。持久化重建会提供原始的createdAt、seedLength和delegationDepth。ctx.sessions.flush(session)通过会话捕获的作用域分发受等待的并行持久性检查点。每个监听器都会启动;调用会等待全部结算后才报告失败。未发布、已脱离和陈旧的对象会被拒绝。findLastMessageTurnEnd(events)将由消息触发的开始与结束配对,并返回最近匹配的turn/end。结果消费方使用该折叠逻辑,而不直接取日志中最近的事件,因为轮次间记录和非消息轮次没有提示词结果。ctx.sessions.fork(source, boundary?, childSessionId?): Session:解析实时会话对象或 id,选取截至boundary事件序号(含该事件)的种子(默认为当前最后一个事件),要求所选前缀结束时没有开放轮次,再创建带谱系元数据的实时子会话。ctx.sessions.get(id: SessionId): Session | undefinedctx.sessions.list(): Session[]
高级:有序清理生命周期原语
仅在清理必须与另一项资源排序时使用拆分生命周期:
prepare(id?, options?)校验并构造,但不发布。enter(session)执行冲突检查,在不通知的情况下发布,并返回一个绑定到该条目的幂等脱离函数。允许并发准备相同 id,但只有一个条目能够成功进入;陈旧的脱离函数无法移除其替代项。announce(session)发出唯一一次创建边,并拒绝重复或重入通知。该次分发期间请求的脱离操作会延后,之后再发出成对的释放边;未通知的条目不会发出任何生命周期边。
dsh-agent-loop 使用这一拆分,以保证循环的最终刷新先于会话脱离;详见所有权 Agent Note(agent 决策记录)。
实时服务事件
会话存储会将已通知的创建与释放配对,在提交后发布追加通知并逐个监听器收容失败,同时提供受等待的持久性检查点。确切签名和作用域行为见生成的事件目录;载荷见持久化目录。
类:Session
普通类(不是 Cordis 服务)。通过 ctx.sessions.create() 创建。
session.append(type, data, opts?)会为持久数据和 surface 元数据制作快照并冻结它们,校验标记形态、溯源信息、替换覆盖完整性,以及仅修改内容的单个tool/result重写,随后同步提交,再在彼此独立的失败收容下通知观察者。对已附加会话的重入追加会被拒绝,运行时检查也覆盖扩宽后的联合类型和已加载日志。session.deriveMessages()对每个新的 surface 条目只做一次增量投影,并返回一个新数组,其中包含这些条目存储的完整、带标识且冻结的消息。assistant 消息会在其模型来源中保留提供方/模型溯源信息及适配器私有回放状态。surface 重写会重建投影;不存在原始日志回退。session.deriveEventMessage(event)是重建和请求检查使用的规范逐事件投影。session.surface暴露只读SessionSurface视图,由会话唯一的增量 surface 管理器所有;每次提交重写,replaceGeneration都会变化。session.events是按追加失效的缓存冻结快照;已接受事件保持深度冻结。session.seq、session.id:当前序号和只读类型化身份。session.header: SessionHeader:脱离、深冻结的创建元数据(version、id、createdAt,以及可选的cwd/parentSession/seedLength/delegationDepth)。构造时会校验持久记录,并要求其中的 id 与session.id一致。
无损 JSON 工具
持久值需要一种已接受的表示,不能先检查再二次读取。isJsonValue(value) 是布尔判断函数;snapshotJsonValue(value) 在一趟迭代中校验并复制普通值,无效输入返回 undefined,getter 抛出的异常则向外传播。快照辅助函数接受除 -0 外的有限 JSON 数值(JSON 会将其改写为 0)、稠密普通数组、普通对象或 null 原型对象;它会在规范化前拒绝循环引用、不支持的标量和特殊原型,同时不施加调用栈深度限制。
分片行存储编解码器(chunk-rows.ts)
提供方以 token 大小的增量流式输出,因此原始日志会存储数百行 assistant/chunk,其 JSON 封装远大于载荷。packChunkRuns(events) 将每段至少 3 个连续、同块的增量分片打包为一个存储行:text-chunks、reasoning-chunks 或 tool-call-chunks(不含斜杠的裸标签,属于存储词汇而不是 SessionEventMap 成员)。decodeStorageRecord(value) 则将已解析行展开回完全一致的事件(seq0/time0 加上每个成员的 dt 间隔,可重建每个 seq/time)。编码器只允许精确形态,并逐字存储任何无法识别的内容;解码器校验带行标签的值,形态错误时抛出异常。编解码器由此包所有,使 JSONL 后端和 fixture(测试前置数据)读取器(dsh-llm-replay、dsh-acp-snapshot)共享同一编解码器;后端默认启用的 packChunks 配置只控制写入。
Surface 类型
SurfaceOp:事件进入有序 surface 的方式,即'append'(正常尾部追加)或{ op: 'replace', start, end }(替换从start到end的条目,含两端;二者都必须是有效的 surface 序号;start === end时替换一个条目)。压缩用它遮蔽旧事件而不删除它们。SurfaceIntent:{ surfaceOp: SurfaceOp; sourceEventSeqs?: number[] },可进入 surface 的类型调用session.append()时必需的第三个参数。SessionSurface:实时只读nodes和replaceGeneration投影,由session.surface暴露;候选校验仍由Session私有。foldSurface(events):回放规范 surface 契约,得到脱离的当前事件序列与实际替换范围。同一趟处理会拒绝不连续序号、错位或畸形元数据、空或重复溯源信息、来源并非更早事件、无效位置范围,以及没有引用所有已遮蔽 surface 条目的替换。如果一个tool/result替换修改了当前某个结果的content之外的任何内容,也会被拒绝;SurfaceManager共享该原子状态转换,但只保留自己的增量序列缓存。isSurfaceEvent(event)/isSurfaceEligibleType(type):前者将SessionEvent收窄为形态完整的 surface 事件;后者在校验种子或已加载日志时,检测缺少标记的可进入 surface 事件。
请求头重建(request-header.ts)
request/header 记录非历史请求封装的完整规范快照,其原因为 initial、resume 或 change。foldRequestHeader() 选择最新快照;旧版增量事件和已移除的 fallback 原因会被拒绝。详见可重建请求 Agent Note。
request/context 记录请求所解析到的路由的、绑定注册项的 contextWindow,在其所属步骤内紧随 request/header 追加,且仅在提供方、模型或容量与上一条记录不同时追加。session.requestContext() 以增量方式归并最新一条,与 requestHeader() 保持一致。容量刻意不进入 EpochHeader:它是描述路由的适配器元数据,不是构建该请求所依据的输入,因此绝不可进入请求重建或请求头相等性判断:容量变化不构成请求头 change。适配器不公布容量的路由不追加任何记录。
user/message 会直接存储完整的 UserMessage,其中包括路由或提示词准入前创建的标识。无论它是直接人类提示词、合成注入,还是已准入的 Goal Round,都会原样呈现其 content;带类型的 source 是区分三者的唯一通道,并携带各领域专有的持久事实。assistant/message、tool/result 和 steering(中途引导)对应的 steering/message 也会存储完整的消息值。轮次执行仍由 turn/start 与 turn/end 包围,而空闲注入可以在轮次之间追加并刷新一条 user/message,无需运行模型。
tool/result 持久保存一条带标识、user-role 的工具结果消息,以及可选内部失败标识和可选呈现元数据。工具成功时的规范 value 和便于人类阅读的规范失败消息只存在于执行本地;渲染后的错误内容是回放权威消息。
会话事件词汇(types.ts)
生成的持久化日志事件目录逐成员列举仅追加日志的事件类型、载荷、surface 标记和溯源信息。Token 记账读取每个步骤的 assistant/chunk { type: 'usage' } 记录;如果没有用量分片,则将 assistant/message.usage 作为已提交步骤的后备。失败的模型请求尝试没有 assistant 消息。提供方/模型/回放溯源信息随 assistant/message 一同保存;运行错误的步骤记录在 turn/end.reason 上(此时为 kind: 'error'),最终模型请求失败时还包含结构化的提供方事实。
SessionEventMap 可通过合并扩展:插件使用声明合并添加自身类型(压缩 seam 的 compact/*、有界恢复的非 surface llm/retry、hook(钩子)桥接层的 hook/*);合并成员会出现在同一目录中。插件拥有其合并事件的关系不变量,包括是否允许纯日志事件出现在轮次之间。需要持久性的生产方通过 Session 追加,再等待 ctx.sessions.flush(session),无需虚构一个执行轮次。
此包还定义 TurnTriggerMap 和 TurnEndReasonMap(用于类型化轮次边界、可合并扩展的和类型;以 kind 为标签而不是字符串)。最终模型请求错误保留一个结构化 LlmFailure;其他轮次错误保留消息/代码,两者均标识失败步骤。
被中断的实时轮次以粗粒度的 { kind: 'aborted' } 结果结束。调用方身份属于 Agent 的运行时取消信号,不属于持久 transcript(文本记录);资源释放仍是独立的 { kind: 'disposed' } 终态。
每个 SessionEvent 都有两个可选顶层字段(结构元数据):
sourceEventSeqs?: number[]:溯源信息的源序号(例如assistant/chunk的序号,它们是assistant/message的来源;或压缩替换条目背后被遮蔽的条目)。对于assistant/message,存在的[]记录已知为空的提供方流;省略则表示旧版或其他未记录的溯源信息。其他 surface 事件若有此字段,则要求非空列表。surfaceOp?: SurfaceOp:事件进入 surface 的方式。非 surface 事件(边界、分片、用量、错误)不含该字段。
元数据类型(types.ts)
SessionHeader:会话元数据,在发布为Session.header时写入一次;脱离和深冻结保证运行时不可变:{ version, id, createdAt, cwd?, parentSession?, seedLength?, delegationDepth? }。持久化 loader 可返回相同数据类型的可变脱离副本。该类型由此包与SessionId一同所有,因为Session.header以它为类型;持久化后端只是重新导出而不拥有它,否则会形成包循环依赖。
扩展点
- 持久化插件:订阅
session/event(延后写入),并在session/flush(受等待)及 fiber dispose(资源释放)时排空。持久后端读取日志并重新加载到实时会话;这类后端会把元数据 seam(SessionHeader、session.header)与日志一同存储。 - 回放/fork:
create(id, { seed })校验并冻结连续的当前格式日志,再重建 surface;请求头必须包含提供方/模型,assistant 消息必须包含提供方/模型溯源信息,而粗粒度中止结果必须只含{ kind: 'aborted' }(带旧版原因的记录会被拒绝)。fork(source, boundary?, childSessionId?)选择已完成轮次前缀并记录谱系。 - 压缩:
dsh-compact-basic为摘要检查点追加一个替换用user/message,而dsh-compact-tool-result-prune追加仅修改内容的tool/result替换。工具配对边界策略及其缓存归dsh-compactseam 所有;此包拥有有序 surface 成员关系、替换校验与replaceGeneration。
模型体验
派生消息历史
模型看到的内容
模型会原样接收 user/message、assistant/message、tool/result 和 steering/message surface 条目中的完整消息。其标识、角色、来源和内容块都与创建时确定的值相同;投影不会生成标识。提示词封装只改变面向人的呈现;其前缀上下文和请求分隔符已经位于事件内容中。工具调用包含在 assistant 消息内。分片、边界、用量、hook 记录、todo 记录以及其他仅日志事件不会添加消息。
Token 影响
追加的 surface 条目会在后续步骤中重新发送。replace surface 操作会从未来输入中移除被遮蔽条目,但不删除其原始日志记录。
KV Cache 影响
追加的 surface 条目会保留可复用前缀。即使底层事件日志保持仅追加,replace 操作也会从首条被遮蔽消息起使缓存复用失效。
崩溃修复结果
模型看到的内容
如果恢复发现 assistant 工具请求没有持久 tool/call,其合成 TOOL_NOT_STARTED 结果内容为 The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed.。如果持久 tool/call 没有结果,其 TOOL_OUTCOME_UNKNOWN 结果内容为 The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly.。
Token 影响
完整会话的 token 增量为零。恢复时,每个修复后的调用都会添加保留的、针对具体风险的错误文本。
KV Cache 影响
保持仅追加;新可见内容位于可复用请求前缀之后,不会使现有 KV Cache 条目失效。
已记录的请求头
模型看到的内容
会话会重建循环实际发送的系统提示词、工具 schema、调用配置和会话前缀。请求头事件不会向消息历史加入第二份副本;前缀在 deriveMessages() 外部前置。
Token 影响
日志记录不产生重复 token。重建的前缀、系统文本和 schema 仍会产生正常的逐请求开销。
KV Cache 影响
记录日志不会导致失效,精确重建会保持请求前缀一致。后续请求头若更改前缀、提示词或 schema,可能从第一处差异开始使复用失效。
已知限制与暂缓工作
- 会话分支/树(pi 风格条目树):除非需要超越基于边界的
fork()能力,否则暂缓。 fork()仅在实时会话的稳定边界处切分:所选前缀结束时不得有开放轮次,且源会话必须位于存储中;fork API 不支持对已持久化但未加载的会话进行 fork。SESSION_FORMAT_VERSION固定为0:预发布阶段不承诺广泛兼容性;Session只接受当前 seed 形状,后端会拒绝其他任何版本。范围受限的存储导入升级应由持久化边界负责(政策、消息标识机制引入前的消息恢复)。TurnEndReasonMap不含 ACP(Agent Client Protocol)命名的refusal/max_turn_requests变体:受生产方约束;只有当适配器或循环首次产生这些变体时才加入。