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. A surface layer (a linked list of message-producing events) is maintained on top of the raw log for efficient derivation and compaction.
Service: SessionStore (ctx key: sessions)
Creates and holds event-sourced Session instances. Persistence is intentionally not implemented here — plugins subscribe to session/event, flush on session/flush, and may mirror the paired session/created/session/disposed lifecycle.
Public API
ctx.sessions.create(id?, options?)validates and detaches durable seed/header data, publishes the session, and binds it to the calling fiber.ctx.sessions.flush(session)dispatches the awaited parallel durability checkpoint through the session's captured scope. It rejects unpublished, detached, or stale objects.ctx.sessions.fork(source, boundary?, childSessionId?): Session— Resolve a live session object or id, select a seed through the inclusiveboundaryevent seq (default: current last event), require that boundary to beturn/end, and create a live child session with lineage metadata.ctx.sessions.get(id: SessionId): Session | undefinedctx.sessions.list(): Session[]
Advanced: ordered-teardown lifecycle primitives
Use the split lifecycle only when teardown must be ordered with another resource:
prepare(id?, options?)constructs without publication.enter(session)performs the collision check, publishes without announcing, and returns an entry-bound idempotent detach.announce(session)emits the single creation edge. Detach during that dispatch is deferred and later emits the paired disposal edge.
dsh-agent-loop uses this split so final loop flush precedes session detach; see the ownership RFC.
Live service events
The store pairs announced creation with disposal, publishes post-commit append notifications with per-listener containment, and provides an awaited durability checkpoint. Exact signatures and scope behavior live in the generated event catalog; payloads live in the persistence catalog.
Class: Session
Plain class (not a Cordis Service). Create via ctx.sessions.create().
session.append(type, data, opts?)snapshots and freezes durable data, commits synchronously, then notifies observers with failure containment. Reentrant attached-session appends reject.session.deriveMessages()incrementally projects the derived surface and returns a fresh array over frozen messages.session.deriveEventMessage(event)is the canonical per-event projection used by reconstruction and invariants.session.surfacelazily folds newsurfaceOpmarkers;replaceGenerationchanges on rewrites.session.eventsis a cached frozen snapshot invalidated by append; accepted events remain deeply frozen.session.seq,session.id— current sequence and readonly typed identity.session.header: SessionHeader— detached, deep-frozen creation metadata (version,id,createdAt, optionalcwd/parentSession/seedLength). Construction validates the durable record and requires its id to matchsession.id.
Lossless JSON utilities
Durable values need one accepted representation, not a check followed by a second read. isJsonValue(value) is the boolean predicate; snapshotJsonValue(value) recursively validates and copies a plain value in one pass, returning undefined for invalid input and propagating a throwing getter. The snapshot helper accepts finite JSON numbers except -0 (JSON rewrites it to 0), dense ordinary arrays, and plain or null-prototype objects; it rejects cycles, unsupported scalars, and exotic prototypes before normalization.
Surface types
SurfaceOp— how a surface node entered the linked list:'append'(normal tail append) or{ op: 'replace', start, end }(replace nodes fromstartthroughendinclusive — both must be valid surface node seqs;start === endreplaces a single node). Used by compaction to shadow old nodes without deleting them.SurfaceIntent—{ surfaceOp: SurfaceOp; sourceEventSeqs?: number[] }, the required third parameter tosession.append()for surface-eligible types.SurfaceNode—{ seq: number; prev: number | null; next: number | null }, one node in the surface linked list.isSurfaceEvent(event)/isSurfaceEligibleType(type)— the first narrows aSessionEventto a fully-formed surface node (type is surface-eligible ANDsurfaceOppresent); the second is the type-only check (is this one of the fiveSurfaceEventTypevalues?), used to detect a surface-eligible event MISSING its marker — e.g. when validating a seed/load log.
Request-header reconstruction (request-header.ts)
request/header and request/header-delta make the non-history request envelope reconstructable from the log. foldRequestHeader() reconstructs the active header, diffHeader() encodes changes, and applyHeaderDelta() replays them; unsupported deltas fall back to a full snapshot. messagePrefix remains separate from derived history. See the reconstructable-requests RFC.
Session event vocabulary (types.ts)
The append-only log's event types, enumerated member by member — payloads, surface badges, provenance — in the generated persistence log event catalog. Token usage rides on assistant/message.usage; an operational error's step is on turn/end.reason for kind: 'error'.
Merge-extensible via SessionEventMap — a plugin declaration-merges its own types (the compaction seam's compact/*, the hook bridges' hook/*); merged members appear in the same catalog.
Also defines TurnTriggerMap and TurnEndReasonMap (merge-extensible sum types for typed turn boundaries — kind-tagged instead of strings).
Every SessionEvent carries two optional top-level fields (structural metadata):
sourceEventSeqs?: number[]— seq numbers of provenance sources (e.g., theassistant/chunkseqs behind anassistant/message, or the shadowed nodes behind a compaction replace node).surfaceOp?: SurfaceOp— how this event entered the surface. Absent for non-surface events (boundaries, chunks, usage, errors).
Metadata types (types.ts)
SessionHeader— session metadata written once when published asSession.header, where detachment and deep-freezing enforce immutability at runtime:{ version, id, createdAt, cwd?, parentSession?, seedLength? }. Persistence loaders may return mutable detached copies of the same data type. Owned here (besideSessionId) becauseSession.headeris typed by it; persistence backends re-export it rather than own it (which would force a package cycle).
Extension points
- Persistence plugins: subscribe to
session/event(write-behind) and drain onsession/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:
create(id, { seed })validates and freezes a contiguous log and rebuilds its surface.fork(source, boundary?, childSessionId?)selects a completed-turn prefix and records lineage. - Compaction: the
dsh-compact-basicplugin appends auser/messagewithsurfaceOp: { op: 'replace', start, end }to shadow old surface nodes behind a summary checkpoint.
What is NOT here (TODO)
- Session branching/tree (pi-style entry tree) — deferred unless needed beyond boundary-based
fork().