12 KiB
description, kind
| description | kind |
|---|---|
| The event-sourced session log and in-memory store for users and maintainers building, inspecting, or extending the durable record behind every agent interaction. | package-reference |
@deepseek-ai/dsh-session
English | 中文
Summary
dsh-session provides the append-only session log that records an agent's whole interaction history — the single source of truth every model-visible fact flows through. The LLM message history is derived from the log (deriveMessages()), never stored separately, so replay is re-derivation from the same events and compaction can shadow older surface entries without deleting history. The package also provides the in-memory store (ctx.sessions), the typed SessionEvent vocabulary that plugins extend by declaration merging, and the surface layer that orders message-producing events. Persistence is deliberately a separate concern: backends subscribe to session/event and flush on session/flush. Choose it as the foundation of any agent session; it runs no model calls itself.
Table of Contents
- Use this package
- Understand the implementation
- Further Exploration
- Model Experience
- Known Limitations and Deferred Work
- Dev Note
Use this package
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.
Create and inspect sessions
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
Append and derive
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 (user/message, assistant/message, tool/result) must declare how they join the ordered surface; raw chunks, boundaries, and other log-only events never produce a message.
Fork a session
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.
Flush durable state
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.
Understand the implementation
Implementation internals — click to expand
This section explains how the package realizes the behavior above; the observable contract is covered in Use this package.
Design concept
The package is built on event sourcing: a Session is an append-only log of typed SessionEvents, and everything else — model history, transcripts, telemetry, titles, persistence — derives from that stream. The surface is a derived projection: an incremental manager validates append candidates, advances the ordered view from committed events, and tracks a replaceGeneration that bumps on every committed rewrite. Model-visible means logged: anything that reaches a model request must be reconstructable from the log. The shared row codec losslessly converts event sequences to compact rows and back, preserves unrecognized events verbatim, and rejects malformed rows. Persistence backends decide whether to pack writes; bounded history transports can use the same rows while retaining the complete logical interval and exact decoding for consumers that need token boundaries.
Request headers
request/header stores a full canonical snapshot of the non-history request envelope with reason initial, resume, change, or series. An explicit message-series start or a surface replacement writes a series snapshot when the envelope is unchanged; a simultaneous change uses startsSeries: true. Same-series steps, retries, and ordinary later turns inherit the latest snapshot. adapterDefaults distinguishes values resolved by the adapter from explicit settings, and foldRequestHeader() selects the latest snapshot. This self-contained record supports partial-window rendering and exact reconstruction at the cost of growth per message series; the reconstructable-requests Agent Note owns the detail.
Source map
| File | Role |
|---|---|
src/index.ts |
Plugin entry: SessionStore service, store lifecycle, fork, flush |
src/types.ts |
SessionEventMap, SessionEvent, UserMessage, SessionHeader, TurnEndReasonMap |
src/surface.ts |
Ordered surface projection, replacement validation, deriveEventMessage |
src/request-header.ts |
request/header folding and reconstruction |
src/json.ts |
Lossless JSON validation and snapshotting |
src/chunk-rows.ts |
Shared compact-row storage codec for persistence backends |
src/repair.ts |
Cold repair of crash-orphaned logs |
src/invariant.ts |
Invariant companion: seq, turn/step enclosure, tool call/result pairing |
Append validation
Every append runs one recursive pass that reads, validates, and copies each nested value once, so a stateful getter cannot supply one value to validation and another to storage. Non-lossless-JSON payloads (BigInt, cycles, sparse arrays, -0, exotic prototypes) are rejected at the append site, before any backend flush. Surface events additionally validate marker shape, cited source-event seqs, and complete shadowed-node coverage for replacements.
Derived history
deriveMessages() caches each surface node's projection once and returns a fresh array per call over shared, deep-frozen messages; each of the three surface event types (user/message, assistant/message, tool/result) projects its own message kind — user content verbatim, the assembled assistant message with its provider and model, or a user-role tool result. A surface rewrite rebuilds the projection — there is no raw-log fallback, so the surface is the single source of derived history.
The request header
The loop logs a full canonical request/header snapshot (call config, adapter defaults, rendered system prompt, assembled tool schemas) at each loop-instance boundary and on change; foldRequestHeader(events) reconstructs it by selecting the latest snapshot, making every conversation request a pure function of the log. Route metadata (request/context) is separate logged state appended only when the provider, model, or capacity differs.
Further Exploration
The package-level contract is enough for most consumers; read these when you need the surrounding domain.
- Session subsystem — the full event vocabulary, surface types, and generated service API.
- Persistence subsystem — how backends make this log durable.
- Core subsystem — the loop that writes and derives from sessions.
- Generated persistence catalog — every session event with its payload and declaration site.
- Core group map — how the core packages compose.
Model Experience
Derived message history
What the model sees
The model receives the complete messages from user/message, assistant/message, and tool/result surface entries verbatim — 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. Chunks, boundaries, usage, and other log-only events add no message.
Token effect
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.
KV Cache effect
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.
Crash-repair result
What the model sees
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.
Token effect
Zero tokens in an intact session. Each repaired call adds its retained risk-specific error text on resume.
KV Cache effect
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
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.
KV Cache effect
Logging causes no invalidation, and exact reconstruction preserves request-prefix identity. A later header with changed prefix, prompt, or schemas may invalidate reuse from its first difference.
Known Limitations and Deferred Work
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_VERSIONstays pinned at0— pre-release, no broad compatibility implied:Sessionaccepts only current seed shapes, a backend refuses any other version, and every unknown event type refuses reconstruction (mechanism).TurnEndReasonMapomits the ACP-namedrefusal/max_turn_requestsvariants — producer-gated: they land when an adapter or the loop first emits them.- No session tree beyond fork — a pi-style entry tree over branched sessions is deferred unless a consumer needs more than boundary-based forking.
Dev Note
Working context for maintainers — click to expand
None.