# Conflicts: # AGENTS.md # docs/config-catalog.md # docs/cordis-catalog/events.md # docs/cordis-catalog/services.md # docs/core-data-structures/core.md # docs/event-producer-consumer.md # docs/persistence-catalog.md # docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md # docs/rfc/implemented/feature/2026-06-15-code-mode.md # docs/rfc/implemented/feature/2026-06-30-hook-bridges.md # docs/rfc/implemented/feature/2026-06-30-interception-seams.md # docs/rfc/implemented/feature/2026-07-08-repeat-tool-guard.md # docs/rfc/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md # examples/AGENTS.md # examples/acp-agent/cordis.yml # examples/acp-agent/tests/acp.snapshot.ts # examples/echo-agent/cordis.yml # examples/sandbox-acp-agent/cordis.yml # packages/cordis/tool-cordis/src/api-catalog.ts # packages/core/agent-core/README.md # packages/core/agent-core/src/index.ts # packages/core/agent-loop/README.md # packages/core/agent-loop/src/loop.ts # packages/core/agent-loop/tests/interception.spec.ts # packages/core/agent/src/types.ts # packages/core/tools/README.md # packages/core/tools/src/code-mode.ts # packages/core/tools/src/index.ts # packages/fs/fs-local/src/index.ts # packages/fs/fs/README.md # packages/fs/fs/src/index.ts # packages/guard/repeat-tool-guard/README.md # packages/guard/repeat-tool-guard/src/index.ts # packages/hooks/hooks-claude/src/index.ts # packages/hooks/hooks-codex/src/index.ts # packages/ui/acp-agent/src/index.ts
11 KiB
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?, { seed?, meta? }?)validates and detaches durable seed/header data, fills the version and id, defaultscreatedAtto now, publishes the session, and binds it to the calling fiber. Persisted reconstruction supplies its originalcreatedAtandseedLength.ctx.sessions.flush(session)dispatches the awaited parallel durability checkpoint through the session's captured scope. Every listener starts and the call waits for all to settle before reporting failure; unpublished, detached, and stale objects reject.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?)validates and constructs without publication.enter(session)performs the collision check, publishes without announcing, and returns an entry-bound idempotent detach. Concurrent same-id preparations are allowed, but only one entry succeeds; a stale detach cannot remove its replacement.announce(session)emits the single creation edge and rejects repeat or reentrant announcements. Detach during that dispatch is deferred and later emits the paired disposal edge; an unannounced entry emits neither lifecycle 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 and surface metadata, commits synchronously, then notifies observers with independent failure containment. Reentrant attached-session appends reject, and runtime checks cover widened unions and loaded logs.session.deriveMessages()incrementally projects each new surface node once and returns a fresh array over shared frozen messages. A surface rewrite rebuilds the projection; there is no raw-log fallback.session.deriveEventMessage(event)is the canonical per-event projection used by reconstruction and invariants.session.surfacelazily folds only newsurfaceOpmarkers;replaceGenerationchanges on every rewrite or invalidation.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.foldSurface(events)— replay the canonical surface transitions into detached current nodes and actual replacement ranges, rejecting surface-eligible events that lack their mandatory marker.SurfaceManagershares the same transitions while retaining its incremental cache.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.
context/message defaults to the canonical tagged context projection. A producer may set envelope: 'raw' when its content already contains the complete model-facing frame, and may attach JSON meta for replayable plugin state; metadata remains durable but is excluded from deriveMessages().
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.
Model Experience
Derived message history
What the model sees: The model receives projections of user/message, assistant/message, and tool/result surface nodes verbatim. A context/message is a user-role message containing exactly <context source="<source-kind>">, its content blocks, and </context>; steering/message uses the identical <steering source="<source-kind>"> / </steering> wrapper. Tool calls live inside assistant messages. Chunks, boundaries, usage, hook records, todo records, and other log-only events add no message.
Token effect: Appended surface nodes are resent on later steps. A replace surface operation removes the shadowed nodes from future inputs without deleting their raw log records.
Crash-repair result
What the model sees: If a persisted turn ended with unanswered tool calls, each synthetic error result contains exactly Tool call interrupted by a crash; no result was recorded.
Token effect: Zero tokens in an intact session. Each repaired call adds this retained error text on resume.
Logged request header
What the model sees: The session reconstructs the system prompt, tool schemas, call config, and session prefix that the loop actually sent. Header events do not add a second copy to message history; the prefix is prepended outside deriveMessages().
Token effect: Zero duplicate tokens from logging. The reconstructed prefix, system text, and schemas still incur their normal per-request cost.
Known Limitations and Deferred Work
- Session branching/tree (pi-style entry tree) — deferred unless needed beyond boundary-based
fork(). fork()cuts only at closed-turn boundaries of live sessions — the boundary must be aturn/endevent and the source must be in the store; forking a persisted-but-unloaded session is excluded from the fork API.SESSION_FORMAT_VERSIONstays pinned at0— pre-release, no compatibility implied: a backend rejects any other version, and no migration path exists until the first release (policy).TurnEndReasonMapomits the ACP-namedrefusal/max_turn_requestsvariants — producer-gated: they land when an adapter or the loop first emits them.