Files
deepseek-harness/packages/session
Tianyi Cui 2a4d89a4bd feat(agent): return an AgentHandle with an async per-agent disposer
The agent factory (`ctx.agents.create`/`resume`, the `AgentFactory` seam)
now returns `AgentHandle = { agent; dispose(): Promise<void> }` instead of a
bare `Agent`. The disposer is a capability: only the holder can tear down
exactly this agent — stop its loop, await the loop's exit (true quiescence,
not just the `disposed` status flip), unregister it, and remove its session
from the store.

The teardown ORDER is load-bearing for durability. The loop appends its
final `turn/end` + runs `session/flush` AFTER an abort, delivered through
`session.onAppend` → `session/event`; if the session-store effect (which
detaches `onAppend`) were torn down first, those closing events would never
reach persistence. So `dispose()`:
  1. runs the register+start effect disposer (sync: request loop stop),
  2. `await agent.done` (loop exits, final flush captured), THEN
  3. runs the session disposer (detach onAppend + delete store entry).

`SessionStore.createOwned()` exposes the session-create effect's disposer
(plain `create()` discards it — fiber-owned). `AgentLoop` funnels both
factory entrypoints (`createAgent`, `resumeWith`) through a shared
`startOwned` that composes the ordered teardown; the config path keeps a
fiber-owned agent by discarding the handle.

`ctx.agents.get(id)` still returns a bare `Agent` — the handle is only for
the owner that created it.
2026-06-20 06:44:35 +08:00
..
2026-06-16 14:55:37 +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; createdAt?: number } }): 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 and defaults createdAt to now; a caller reconstructing a persisted session passes the original createdAt to preserve it. 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 session metadata, written once: { version, id, createdAt, cwd?, parentSession? }. Owned here (beside SessionId) because Session.header is typed by it; persistence backends re-export it rather than own it (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, 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.