Files
deepseek-harness/packages/session
Tianyi Cui 0731ed374b feat(session): metadata seam + JSON-serializability invariant
Adds the durable-session metadata seam and enforces the log's
JSON-serializability invariant at the source:

- SessionHeader / SessionSummary / SessionMeta and CreateSessionOptions in
  dsh-session; Session gains a readonly `header`; SessionStore.create takes
  `(id?, options?: { seed?; meta? })` (validated absolute cwd, parentSession
  lineage). The injection TurnTrigger variant is added for the idle-inject
  one-shot turn that a later change introduces.
- isJsonValue (new json.ts): a value round-trips through JSON losslessly —
  rejects BigInt, function, symbol, undefined, non-finite numbers, sparse
  arrays, circular refs, and exotic objects (Map/Set/Date/class instances).
- Session.append throws on non-JSON-serializable data, and the Session
  constructor validates every seed event (isJsonValue + contiguous seq from
  0), so a replay/fork seed can never build a live log no backend can
  persist — the source-level guarantee a durable backend relies on.

Migrates the ~3 internal positional-seed `create(id, seed)` call sites to
`{ seed }`, and adapts the invariants tests forced by the new guard (the
bad-seq seed is now caught by the constructor; the cyclic deep-freeze test
drives via session/event since append rejects cyclic data; a direct
session/event drives the invariants seq-monotonicity check). Docs kept
backend-agnostic (the persistence packages arrive in a later PR).
2026-06-15 17:54:55 +08:00
..

dsh-session

Event-sourced session log and in-memory store. A Session is the append-only source of truth for an agent's whole interaction history — the LLM message history is derived from it.

Service: SessionStore (ctx key: sessions)

Creates and holds event-sourced Session instances. Persistence is intentionally not implemented here — plugins subscribe to session/event and flush on session/flush.

Public API

  • ctx.sessions.create(id?: string, options?: { seed?: SessionEvent[]; meta?: { cwd?: string; parentSession?: SessionId } }): Session — Create a session. options.seed replays/forks an existing event log; options.meta attaches creation metadata (validated absolute cwd, parentSession lineage) as the immutable SessionHeader (the store fills version/id/createdAt). Disposed with the calling fiber.
  • ctx.sessions.get(id: string): Session | undefined
  • ctx.sessions.list(): Session[]

Events

Event Mode Purpose
session/created emit A session was created
session/event emit An event was appended (sync, fire-and-forget)
session/flush parallel Awaited durability checkpoint (persistence plugins drain buffers here)

Class: Session

Plain class (not a Cordis Service). Create via ctx.sessions.create().

  • session.append(type, data): SessionEvent — synchronous, never blocks on I/O. Throws if data is not losslessly JSON-serializable (BigInt, function, symbol, undefined, non-finite number, circular ref, or an exotic object like Map/Set/Date) — the event log is the durable source of truth, so this invariant is enforced at the source (exported as isJsonValue for backends to reuse on their replay/fork entry points).
  • session.deriveMessages(): Message[] — derive the LLM message history from the event log. Raw assistant/chunk events are skipped; context/message and steering/message render as tagged synthetic user messages.
  • session.events, session.seq, session.id
  • session.header: SessionHeader — immutable creation metadata (version, id, createdAt, optional cwd/parentSession). Kept out of the event log (a storage concern, not replayable state); a minimal v1 header is synthesized for bare Session construction.

Metadata types (types.ts)

  • SessionHeader — immutable, written once: { version, id, createdAt, cwd?, parentSession? }.
  • SessionSummary — mutable, updateable without touching the log: { updatedAt, title?, firstPrompt? }.
  • SessionMeta = SessionHeader & SessionSummary — owned here (beside SessionId) because Session.header is typed by it; persistence backends re-export these rather than own them (which would force a package cycle).

Session event vocabulary (types.ts)

The append-only log: turn/start, turn/end, step/start, step/end, user/message, assistant/message, assistant/chunk, tool/call, tool/result, steering/message, context/message, usage, error.

Merge-extensible via SessionEventMap — a compaction plugin adds compaction/marker, etc.

Also defines TurnTriggerMap and TurnEndReasonMap (merge-extensible sum types for typed turn boundaries — kind-tagged instead of strings).

Extension points

  • Persistence plugins: subscribe to session/event (write-behind) and drain on session/flush (awaited) and fiber dispose. A durable backend reads the log and reloads it into a live session; the metadata seam (SessionHeader/SessionSummary/SessionMeta, session.header) is what such a backend stores beside the log.
  • Replay/fork: ctx.sessions.create(id, { seed }) seeds a new session with an existing event log.

What is NOT here (TODO)

  • Session branching/tree (pi-style entry tree) — defered unless needed beyond seed-based forking.