2026-07-15 23:11:25 -07:00
|
|
|
|
# 会话持久化
|
|
|
|
|
|
|
|
|
|
|
|
[English](persistence.md) | 中文
|
|
|
|
|
|
|
2026-07-22 03:06:01 -07:00
|
|
|
|
事件日志的**持久性 seam**。[session.md](session.md) 描述了内存中的 `Session`:仅追加的 `SessionEvent` 日志即为真源。本页描述如何使该日志持久化:抽象的 `SessionPersistence` 服务、它的后端、flush 检查点、崩溃恢复,以及随日志一同存储的元数据头。日志承载的事件词汇在生成的[持久化日志事件目录](../persistence-catalog.md)中逐项列举。
|
2026-07-15 23:11:25 -07:00
|
|
|
|
|
2026-07-22 22:58:05 +08:00
|
|
|
|
该 seam 是典型的[能力 seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md):一个抽象服务([dsh-session-persistence](../../packages/session-persistence/session-persistence),`ctx.sessionPersistence`)在现有 `SessionEvent` 上定义 locate/create/append/load/list——**没有平行的持久化类型**——以及两个可互换、通过同一套 `runPersistenceContract` 的后端。见 [session-persistence Agent Note(agent 决策记录)](../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md)。
|
2026-07-15 23:11:25 -07:00
|
|
|
|
|
|
|
|
|
|
## flush 检查点
|
|
|
|
|
|
|
2026-07-23 14:03:37 +08:00
|
|
|
|
`session/event` 是一个*同步*通知;持久化插件会将其缓冲(write-behind)至 `session/flush`。循环会 await 普通轮次的检查点后再领取下一个队列项;同步的 idle `inject()` 会调度自己的检查点而不阻塞 `send()`,dispose 仍会将其排空。成功 flush 会把已关闭轮次作为一个单元持久提交;被拒绝的 flush 通过 `agent/error` 与 logger 报告——绝不会作为已关闭轮次之后的会话事件——而后端会保留已缓冲事件供下次 flush 使用。
|
2026-07-15 23:11:25 -07:00
|
|
|
|
|
2026-07-23 14:03:37 +08:00
|
|
|
|
## 崩溃恢复保留被中断的轮次
|
2026-07-15 23:11:25 -07:00
|
|
|
|
|
2026-07-23 14:03:37 +08:00
|
|
|
|
后端重新加载一个在轮次中途崩溃的日志时,会发现一个已打开的 `turn/start` 却没有 `turn/end`。它**不会**截断日志:在长周期任务中,单个轮次可能非常庞大(许多步骤、大量工具输出),而这些事件在崩溃前已被持久追加。后端改为用一个合成的 `turn/end { reason: { kind: 'interrupted' } }` 关闭这个遗留轮次,保持日志平衡与轮次闭合不变式。`interrupted` 是唯一一个不由循环发出的 `TurnEndReason`(见 [session.md](session.md#why-a-turn-ended-turnendreasonmap))。
|
2026-07-15 23:11:25 -07:00
|
|
|
|
|
2026-07-22 22:58:05 +08:00
|
|
|
|
## `SessionLocation`——可选的逐会话制品目标
|
|
|
|
|
|
|
2026-07-23 14:03:37 +08:00
|
|
|
|
`SessionPersistence.locate(meta)` 会同步解析一个归后端所有的独立制品,而不会读取、创建或 flush 它。JSONL 返回其绝对目标路径;SQLite 因各会话共享一个数据库而返回 `undefined`。因此,返回的路径可能指向尚不存在、或还不包含当前尚未 flush 的轮次;它是位置提示,不是授权或新鲜度保证。
|
2026-07-22 22:58:05 +08:00
|
|
|
|
|
|
|
|
|
|
```ts type-equiv
|
|
|
|
|
|
/**
|
|
|
|
|
|
* A backend-resolved, per-session local artifact location. The path is an
|
|
|
|
|
|
* absolute target path and can name an artifact that has not materialized yet.
|
|
|
|
|
|
* Consumers must treat it as a location hint, never as an authorization token.
|
|
|
|
|
|
*/
|
|
|
|
|
|
interface SessionLocation {
|
|
|
|
|
|
/** Backend-specific artifact kind, for example `jsonl`. */
|
|
|
|
|
|
readonly kind: string
|
|
|
|
|
|
/** Absolute path to this session's backend-owned artifact. */
|
|
|
|
|
|
readonly path: string
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-15 23:11:25 -07:00
|
|
|
|
## `SessionHeader`:日志旁的元数据
|
|
|
|
|
|
|
2026-07-22 03:06:01 -07:00
|
|
|
|
每个会话的元数据与事件日志**分开**存储:格式版本、cwd、血统与 seed 边界是存储层关注点而非对话事件,因此不进入 `SessionEventMap`,也不会到达 `deriveMessages()`。header 通过 `session.header` 附加到 `Session` 上。
|
2026-07-15 23:11:25 -07:00
|
|
|
|
|
|
|
|
|
|
源码:[`packages/core/session/src/types.ts`](../../packages/core/session/src/types.ts)
|
|
|
|
|
|
|
|
|
|
|
|
```ts type-equiv
|
2026-07-22 22:58:05 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Immutable validated storage metadata, kept outside the conversation event log.
|
|
|
|
|
|
*/
|
2026-07-15 23:11:25 -07:00
|
|
|
|
interface SessionHeader {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* On-disk format version, stamped from {@link SESSION_FORMAT_VERSION} when the
|
|
|
|
|
|
* session is created. A persistence backend rejects any other version on load
|
|
|
|
|
|
* (no migration — see the constant).
|
|
|
|
|
|
*/
|
|
|
|
|
|
readonly version: number
|
|
|
|
|
|
/** The session's id (mirrors the {@link Session}'s id). */
|
|
|
|
|
|
readonly id: SessionId
|
|
|
|
|
|
/** Unix epoch milliseconds when the session was created. */
|
|
|
|
|
|
readonly createdAt: number
|
|
|
|
|
|
/** Absolute working directory the session was created in (if any). */
|
|
|
|
|
|
readonly cwd?: string
|
|
|
|
|
|
/** The session this one was forked from (seed lineage), if any. */
|
|
|
|
|
|
readonly parentSession?: SessionId
|
|
|
|
|
|
/**
|
2026-07-22 22:58:05 +08:00
|
|
|
|
* How many leading events were inherited through a seed. Persisting this
|
|
|
|
|
|
* boundary lets resume and replay distinguish parent history from child work.
|
2026-07-15 23:11:25 -07:00
|
|
|
|
*/
|
|
|
|
|
|
readonly seedLength?: number
|
2026-07-22 22:58:05 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Delegation depth: absent (zero) for a top-level session, parent depth + 1
|
|
|
|
|
|
* for a subagent child. Persisted so a recursion budget survives restart and
|
|
|
|
|
|
* resume — a runtime-only depth would reset a resumed child to top-level.
|
|
|
|
|
|
*/
|
|
|
|
|
|
readonly delegationDepth?: number
|
2026-07-15 23:11:25 -07:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## `CreateSessionOptions`:seed 与元数据
|
|
|
|
|
|
|
2026-07-23 01:05:50 +08:00
|
|
|
|
通过 store 创建 `Session` 时会接收 `seed`(回放/fork 现有事件日志)与 `meta`(store 折叠进 `SessionHeader` 的存储层字段)。store 填充 `version`/`id` 并为 `createdAt` 提供默认值;调用方提供已校验的绝对 `cwd`、`parentSession` 谱系、`seedLength` 种子边界、`delegationDepth`,以及——仅在重建已持久化会话时——需要保留的原始 `createdAt`。
|
2026-07-15 23:11:25 -07:00
|
|
|
|
|
|
|
|
|
|
```ts type-equiv
|
2026-07-22 22:58:05 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Options for creating a {@link Session} via the store. `seed` replays/forks
|
|
|
|
|
|
* an existing event log; `meta` carries the caller-supplied storage fields the
|
|
|
|
|
|
* store folds into a {@link SessionHeader}.
|
|
|
|
|
|
*/
|
2026-07-15 23:11:25 -07:00
|
|
|
|
interface CreateSessionOptions {
|
|
|
|
|
|
/** Events to seed the new session with (replay/fork). */
|
|
|
|
|
|
readonly seed?: readonly SessionEvent[]
|
|
|
|
|
|
/**
|
2026-07-22 22:58:05 +08:00
|
|
|
|
* Storage metadata read once before publication. `seedLength` is explicit
|
|
|
|
|
|
* because a resumed seed contains the full stored log, not only its inherited prefix.
|
2026-07-15 23:11:25 -07:00
|
|
|
|
*/
|
|
|
|
|
|
readonly meta?: {
|
|
|
|
|
|
readonly cwd?: string
|
|
|
|
|
|
readonly parentSession?: SessionId
|
|
|
|
|
|
readonly createdAt?: number
|
|
|
|
|
|
readonly seedLength?: number
|
2026-07-22 22:58:05 +08:00
|
|
|
|
readonly delegationDepth?: number
|
2026-07-15 23:11:25 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-22 03:06:01 -07:00
|
|
|
|
因此,回放/fork 的调用方式为 `ctx.sessions.create(id, { seed: seedEvents })`;将一个*持久化*会话恢复为活跃 agent 的调用方式为 `ctx.agents.resume({ resumeSessionId })`。
|
2026-07-15 23:11:25 -07:00
|
|
|
|
|
|
|
|
|
|
## 后端
|
|
|
|
|
|
|
2026-07-22 22:58:05 +08:00
|
|
|
|
两者都实现同一个抽象 `SessionPersistence`(在 `SessionEvent` 上执行 locate/create/append/load/list),并通过 `runPersistenceContract`,证明该 seam 确实与后端无关:
|
2026-07-15 23:11:25 -07:00
|
|
|
|
|
2026-07-23 14:03:37 +08:00
|
|
|
|
- **[dsh-session-persistence-jsonl](../../packages/session-persistence/session-persistence-jsonl)**——每个会话一份仅追加的逻辑 JSONL 日志,默认存储为带 checksum 的连续 Zstandard frame,也可配置为原始行;支持崩溃安全的原子写入、被中断轮次的恢复以及读取/回放路径。
|
2026-07-22 03:06:01 -07:00
|
|
|
|
- **[dsh-session-persistence-sqlite](../../packages/session-persistence/session-persistence-sqlite)**:基于 `node:sqlite`,每个 `SessionEvent` 一行。行结构 `(session_id, seq, type, time, data, source_event_seqs, surface_op)` 与事件 1:1 映射(包含可选的 surface 元数据),因此没有需要保持同步的并行持久化 schema。
|
2026-07-15 23:11:25 -07:00
|
|
|
|
|
2026-07-23 01:05:50 +08:00
|
|
|
|
共享同一磁盘会话的多个后端通过[共享持久化写入协调器](../../.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md)协调写入。
|