description: "The event-sourced session log and in-memory store for users and maintainers building, inspecting, or extending the durable record behind every agent interaction."
English | 中文
dsh-session records every model-visible fact in an append-only session log and derives model history from that record. Consumers can inspect, replay, fork, and flush sessions while preserving historical events; compaction hides superseded entries from the active conversation without deleting them. Sessions remain in memory unless a persistence backend is added, and durability checkpoints wait for configured backends. Choose this package wherever an agent needs a reconstructable session record; it does not call models.
Mount dsh-session wherever a session must exist. It creates and holds event-sourced Session instances in memory; durable storage is layered on by a persistence plugin that subscribes to the session/event feed.
ctx.sessions.create() builds a live session bound to the calling fiber; get(id) and list() find sessions, and fork() creates a child session from a stable prefix of a live one.
const session = ctx.sessions.create(sessionId, { meta: { cwd: '/workspace' } })
ctx.sessions.get(sessionId) // the live session
ctx.sessions.list() // every live session, in creation order
session.append(type, data, opts?) commits one typed event — it snapshots and freezes the payload, validates it as lossless JSON, and notifies observers. session.deriveMessages() projects the log into the Message[] the model sees, incrementally and cached:
session.append('user/message', { role: 'user', content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } },
{ surfaceOp: 'append' })
session.deriveMessages() // the derived model history
Surface events (system/message, user/message, assistant/message, tool/result) require surfaceOp in both typed events and append input. A replacement uses exactly { op: 'replace', startSeq, endSeq }, with inclusive SessionSeq endpoints in current surface order. An Assistant message embeds its exact compact provider stream and forbids sourceEventSeqs. Known log-only events forbid both metadata fields and never produce a message.
Append, seed/restore, and event adoption/snapshot reject any header.system and exactly empty optional request-header fields (tools: [], adapterDefaults: {}) instead of normalizing input. Tool-result data.error is allowed only when message.content[0].isError === true; failure identity remains optional. Rejected appends do not change the log, derived state, or event feed. Adoption validates event-local metadata but not referenced history or replacement membership.
system/message holds the rendered system prompt: the first one is surface node 0, the prepared call capability governs admission, with a non-empty rendering consolidated at the first system node on an incapable route or appended after cached history inside a continuing in-history series; empty system nodes project to no message, so clearing the prompt requires logged empty replacements of all active system nodes, not just the latest; the surface fold rejects a replacement covering node 0 while it is a system/message unless the replacing event is itself a system/message over exactly that node, while later system nodes carry no protection and a compaction range may shadow them (decision).
session.seq reads the current log length without materializing an array, and session.eventAt(seq) reads one accepted, deeply frozen event by sequence number. session.snapshotEvents(fromSeq?, toSeqExclusive?) materializes a frozen, stable snapshot of a half-open range; a complete current snapshot is cached until the next append. eventAt(), snapshotEvents(), and ownEvents() are deprecated: existing logic may remain unmigrated for now, but new production calls are prohibited. Repository test files may use these three readers under their scoped lint allowance (policy). Callers that only need a length use seq.
Session log positions use two numeric types. SessionSeq identifies an existing event or inclusive event watermark; SessionLogOffset identifies a gap, prefix length, or read boundary and may equal the event count. SessionSeqCursor adds the -1 “no event yet” value, while OptionalSessionSeq uses null when absence is data. The constructors validate non-negative safe integers, and the brands disappear at runtime, so durable JSON and wire values remain ordinary numbers.
ctx.sessions.fork(source, boundary?, childSessionId?) selects source events through an inclusive boundary seq (default: the current last event), requires the prefix to end outside an open turn, and creates a live child session with lineage metadata. A tool-time delegation that must branch mid-turn clips to a completed prefix instead.
The logical SessionHeader.isSeeded field reports whether fork history exists without exposing a positional integer. Session.inheritedEventCount retains the exact checked SessionLogOffset; ownEvents() returns events at and after that cut, and isOwnSeq(seq) accepts only an existing child-owned position. A low-level seeded constructor must supply an explicit seed and inheritedEventCount because the constructor seed can contain child-owned setup events after the inherited prefix.
ctx.sessions.flush(session) dispatches the awaited durability checkpoint: every persistence listener flushes and the call settles after all of them. A producer that needs an immediate durability barrier awaits it instead of assuming the write-behind drained.
The package-level contract is enough for most consumers; read these when you need the surrounding domain.
The model receives the complete messages from system/message, user/message, assistant/message, and tool/result surface entries verbatim, the system prompt first — identities, roles, sources, and content blocks are the same values established at creation, and projections never mint identities. Direct prompts and injected context remain separate user/message events whose sources preserve their provenance. Embedded streams, assistant/attempt, boundaries, and other log-only facts add no message.
Appended surface entries are resent on later steps. A replace surface operation removes the shadowed entries from future inputs without deleting their raw log records.
Appended surface entries preserve reusable prefixes. A replace operation invalidates reuse from the first shadowed message even though the underlying event log stays append-only.
If recovery finds an assistant tool request with no durable tool/call, its synthetic TOOL_NOT_STARTED result says The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed. If a durable tool/call has no result, its TOOL_OUTCOME_UNKNOWN result says The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly.
Zero tokens in an intact session. Each repaired call adds its retained risk-specific error text on resume.
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
The session reconstructs the tool schemas and call config that the loop actually sent; the system prompt is part of deriveMessages() as surface node 0 and, after an in-history update, as the latest system node. Header events add no message to history and hold no copy of the prompt.
Zero duplicate tokens from logging. The system nodes and schemas still incur their normal per-request cost.
Logging causes no invalidation, and exact reconstruction preserves request-prefix identity. A later header with changed config or schemas may invalidate reuse from its first difference; a prompt change that replaces surface node 0 invalidates reuse from the first token, while an in-history append keeps the prefix through the cached history reusable.
These limits define when the session store needs special care. They are current package constraints, not a task backlog.
fork() cuts only at stable boundaries of live sessions — the selected prefix must end outside an open turn and the source must be in the store; forking a persisted-but-unloaded session is excluded from the fork API.SESSION_FORMAT_VERSION names the current logical representation — the current reader rejects retired header.system and validates system/message payloads and protected-head rewrites. Historical headers and events belong to adjacent format packages; the adjacent migration chain converts supported history before constructing Session, and the write-open path publishes only the current-format successor. Equal-version unknown events require the envelope's explicit ignorable marker, which does not promise safe structural migration (mechanism).TurnEndReasonMap omits the ACP-named refusal / max_turn_requests variants — producer-gated: they land when an adapter or the loop first emits them.