Files
deepseek-harness/docs/core-data-structures/persistence.zh.md
T

7.0 KiB
Raw Blame History

会话持久化

English | 中文

事件日志的持久性 seamsession.md 描述了内存中的 Session:仅追加的 SessionEvent 日志即为真源。本页描述如何使该日志持久化:抽象的 SessionPersistence 服务、它的后端、flush 检查点、崩溃恢复,以及随日志一同存储的元数据头。日志承载的事件词汇在生成的持久化日志事件目录中逐项列举。

该 seam 是典型的能力 seam:一个抽象服务(dsh-session-persistencectx.sessionPersistence)在现有 SessionEvent 上定义 locate/create/append/load/list——没有平行的持久化类型——以及两个可互换、通过同一套 runPersistenceContract 的后端。见 session-persistence Agent Noteagent 决策记录)

flush 检查点

session/event 是一个同步通知;持久化插件会将其缓冲(write-behind)至 session/flush。循环会 await 普通轮次的检查点后再领取下一个队列项;同步的 idle inject() 会调度自己的检查点而不阻塞 send(),dispose(资源释放)仍会将其排空。成功 flush 会把已关闭轮次作为一个单元持久提交;被拒绝的 flush 通过 agent/error 与 logger 报告——绝不会作为已关闭轮次之后的会话事件——而后端会保留已缓冲事件供下次 flush 使用。

崩溃恢复保留被中断的轮次

后端重新加载一个在轮次中途崩溃的日志时,会发现一个已打开的 turn/start 却没有 turn/end。它不会截断日志:在长周期任务中,单个轮次可能非常庞大(许多步骤、大量工具输出),而这些事件在崩溃前已被持久追加。后端改为用一个合成的 turn/end { reason: { kind: 'interrupted' } } 关闭这个遗留轮次,保持日志平衡与轮次闭合不变式。interrupted 是唯一一个不由循环发出的 TurnEndReason(见 session.md)。

SessionLocation——可选的逐会话产物目标

SessionPersistence.locate(meta) 会同步解析一个归后端所有的独立产物,而不会读取、创建或 flush 它。JSONL 返回其绝对目标路径;SQLite 因各会话共享一个数据库而返回 undefined。因此,返回的路径可能指向尚不存在、或还不包含当前尚未 flush 的轮次;它是位置提示,不是授权或新鲜度保证。

/**
 * 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
}

SessionHeader:日志旁的元数据

每个会话的元数据与事件日志分开存储:格式版本、cwd、血统与 seed 边界是存储层关注点而非对话事件,因此不进入 SessionEventMap,也不会到达 deriveMessages()。header 通过 session.header 附加到 Session 上。

源码:packages/core/session/src/types.ts

/**
 * Immutable validated storage metadata, kept outside the conversation event log.
 */
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
  /**
   * How many leading events were inherited through a seed. Persisting this
   * boundary lets resume and replay distinguish parent history from child work.
   */
  readonly seedLength?: number
  /**
   * 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
}

CreateSessionOptionsseed 与元数据

通过 store 创建 Session 时会接收 seed(回放/fork 现有事件日志)与 metastore 折叠进 SessionHeader 的存储层字段)。store 填充 version/id 并为 createdAt 提供默认值;调用方提供已校验的绝对 cwdparentSession 谱系、seedLength 种子边界、delegationDepth,以及——仅在重建已持久化会话时——需要保留的原始 createdAt

/**
 * 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}.
 */
interface CreateSessionOptions {
  /** Events to seed the new session with (replay/fork). */
  readonly seed?: readonly SessionEvent[]
  /**
   * Storage metadata read once before publication. `seedLength` is explicit
   * because a resumed seed contains the full stored log, not only its inherited prefix.
   */
  readonly meta?: {
    readonly cwd?: string
    readonly parentSession?: SessionId
    readonly createdAt?: number
    readonly seedLength?: number
    readonly delegationDepth?: number
  }
}

因此,回放/fork 的调用方式为 ctx.sessions.create(id, { seed: seedEvents });将一个持久化会话恢复为活跃 agent 的调用方式为 ctx.agents.resume({ resumeSessionId })

后端

两者都实现同一个抽象 SessionPersistence(在 SessionEvent 上执行 locate/create/append/load/list),并通过 runPersistenceContract,证明该 seam 确实与后端无关:

  • dsh-session-persistence-jsonl——每个会话一份仅追加的逻辑 JSONL 日志,默认存储为带 checksum 的连续 Zstandard frame,也可配置为原始行;支持崩溃安全的原子写入、被中断轮次的恢复以及读取/回放路径。
  • dsh-session-persistence-sqlite:基于 node:sqlite,每个 SessionEvent 一行。行结构 (session_id, seq, type, time, data, source_event_seqs, surface_op) 与事件 1:1 映射(包含可选的 surface 元数据),因此没有需要保持同步的并行持久化 schema。