DSH plugin session log: DeepSeek Harness SessionEventMap

Plugin DevelopmentPublished 2026-10-07Author: DeepSeek Plugin Market
DSH pluginDeepSeek Harnesssession logSessionEventMapmessage projections
How does a DSH plugin extend the session log? This DSH plugin guide covers SessionEventMap declaration merging, message projections, and ignorable semantics.

A DSH plugin extends the session log not by editing a stored message array but by appending typed events to an append-only log and letting message history be derived from it (source). This article covers the "log is the sole source of truth" foundation, how declaration merging adds events to SessionEventMap and what the ignorable refusal semantics mean, and the contract for plugin-owned message projections and ctx.sessionProjections; where sessions live and how to query them is covered in the session persistence explainer, and event dispatch is covered in plugin events, while this article covers the contract a developer must implement.

The DSH plugin session log as the sole source of truth

A Session is an append-only log of typed SessionEvents and the sole source of truth for an agent's complete interaction history; the LLM message history is derived from the log and never stored separately (source). Three invariants decide how you may write:

  1. Contiguous sequence numbers — seq is a monotonic position in the log following the seq = log.length contiguity contract, and time is Unix epoch milliseconds. SessionSeq and SessionLogOffset are compile-time-branded types that accept only non-negative safe integers and reject negative zero; arithmetic returns a plain number that callers must re-admit by role.
  2. Data must be lossless JSON — Session.append runtime-validates all event data with isJsonValue, rejecting BigInt, functions, symbol, undefined, negative zero, non-finite numbers, circular references, sparse arrays, and exotic objects such as Map/Set/Date/class instances. A non-serializable meta is therefore rejected at the source, not later during a backend flush.
  3. append returns a log snapshot — append(type, data, opts) returns the logged event (its assigned seq/time plus the data snapshot that entered the log), so reading event.data back sees the logged value, never the caller's still-mutable input.

A bad event fails at the append site, because the event log is the durable source of truth. Once an event enters the log the append is committed: observer failures are logged and contained per listener without changing the return value. How the log is persisted (the persistence seam, backends, crash recovery) is the concern of persistence.

Extending SessionEventMap in a DSH plugin: merging and ignorable

Plugins declare additional event types into SessionEventMap through declaration merging; since SessionEventType = keyof SessionEventMap, a merged type automatically becomes a legal event type (source). Two official precedents:

  • The compaction seam added compaction/start / compaction/summary / compaction/end.
  • @deepseek-ai/dsh-hook-protocol added the log-only hook/invoked / hook/result records for hook bridging, correlated by handlerId.

All of these are log-only events: not SurfaceEventTypes (they carry no surfaceOp and do not participate in derived history). The event owner decides whether they belong to an open execution turn or may sit independently between turns, and enforces the required relationships in its own invariant companion plugin. If one event family must assemble into a Conversation node, every start, update, result, resource, or interruption event in that family must carry or independently derive the same stable business id — so a client never has to guess ownership from adjacency (node assembly is covered in conversation nodes).

SessionEvent<T> is a proper discriminated union over type (not independent type/data unions), so switch (event.type) narrows event.data without casts. Its ignorable semantics deserve their own note (source):

  • ignorable: true means a reader may safely skip a type it does not recognize.
  • Absence means required: a reader meeting an unrecognized event without this marker MUST refuse to reconstruct the session instead of silently dropping it — an unrecognized required event may change how the rest of the log is interpreted.
  • Writers set true only on purely informational records; defaulting to required means a forgotten marker over-refuses (an inconvenience) rather than silently resuming a gutted session (a disaster).

DSH plugin message projections and ctx.sessionProjections

Plugins that change existing message content mark the event declaration with @messageProjection and register a pure handler through ctx.sessions.registerMessageProjection() (source). A SessionMessageProjection declares the type it interprets and implements project(event, context) returning an immutable message map keyed by the original event seq; its contract is to validate the complete durable decision before returning any updates, preserve message identities, publish immutable copies without mutating the input, and throw when the decision cannot be applied.

Session validates the complete decision through the handler before committing, applies immutable message updates, and advances contentGeneration. A missing handler refuses operations (including restore and standalone folds), and unloading a handler already in use refuses cached reads. Standalone readers pass handlers explicitly to foldSurface(events, projections) and apply projectedMessages through deriveEventMessage(), so they never diverge from the cache.

Session.surface returns a session-stable read-only SessionSurface view exposing nodes, replaceGeneration (positional replacement count), and contentGeneration (replacements plus plugin-owned message changes); callers can observe membership and replacement generations but cannot validate. deriveMessages() walks the ordered sequences maintained by surfaceOp to derive the Message[] the model sees — it is cached (pure tail growth is O(new nodes); a replacement or projection rebuilds) and frozen (each call returns a fresh array referencing shared, deep-frozen messages).

The architecture layer has a separate projection seam: dsh-session-projection provides ctx.sessionProjections, where registered units incrementally fold committed events, host consumers read a single typed state via stateOf(), and carriers fetch the trimmed client view in bulk via snapshot() (source). Two hard rules:

  1. A host reader either requires the service on activation or fails explicitly when the registry or a required key is absent — never guess.
  2. A contributor may keep a ctx.inject(['sessionProjections'], ...) registration but must not silently provide default values for missing host values (the agent loop registers the shared turnBoundary state for readers).

Three self-checks when done: does a new event settle whether it is log-only or message-producing; is ignorable set only on purely informational records; and does a message projection handler guarantee purity and immutable returns. To find community implementations of session plugins, search the DSH Plugin Hub; for extension-point ownership see the feature-to-mechanism map.

FAQ

How does a DSH plugin add a new event type to the session log?

A DSH plugin extends SessionEventMap through TypeScript declaration merging: add the new key to the SessionEventMap interface in your plugin, then write it with session.append(type, data). Such events are log-only by default — they are not SurfaceEventTypes, carry no surfaceOp, and do not participate in derived history; the compaction seam's compaction/start, compaction/summary, and compaction/end, and dsh-hook-protocol's hook/invoked and hook/result are added this way.

What does the ignorable marker mean in a DSH plugin session log?

In a DSH plugin, setting ignorable: true on a SessionEvent means a reader may safely skip a type it does not recognize; absence means required. A reader meeting an unrecognized event without this marker MUST refuse to reconstruct the session instead of silently dropping it, because an unrecognized required event may change how the rest of the log is interpreted. Writers set true only on purely informational records; defaulting to required means a forgotten marker over-refuses rather than silently resuming a gutted session.

Which DSH plugin session events produce model messages?

Only the SurfaceEventType subset in a DSH plugin projects into model messages: system/message, user/message, assistant/message, and tool/result; only those variants carry surfaceOp and sourceEventSeqs. turn/*, step/*, assistant/attempt, and plugin-owned log-only events are structural information that never projects to a message. Session.deriveMessages() walks the ordered sequences maintained by surfaceOp to derive the Message[] the model sees.

How does a DSH plugin register a message projection?

A DSH plugin marks the event declaration with @messageProjection and registers a pure handler through ctx.sessions.registerMessageProjection(): a SessionMessageProjection declares the type it interprets and implements project(event, context) returning an immutable message map keyed by the original event seq. Session validates the complete decision before any updates, applies immutable message updates, and advances contentGeneration; a missing handler refuses operations including restore and standalone folds, and unloading a used handler also refuses cached reads.

What is ctx.sessionProjections for in a DSH plugin?

In a DSH plugin, ctx.sessionProjections is provided by dsh-session-projection: registered units incrementally fold committed events, host consumers read a single typed state via stateOf(), and carriers fetch the trimmed client view in bulk via snapshot(). Host readers either require the service on activation or fail explicitly when the registry or a required key is absent; contributors may keep a ctx.inject(['sessionProjections'], ...) registration but must not silently provide default values for missing host values.

Related Terms

SessionEventMap
SessionEventMap is the append-only event vocabulary in DeepSeek Harness, extensible by declaration merging, holding turn/start, turn/end, step/start, step/end, user/message, developer/message, system/message, assistant/message, assistant/attempt, tool/call, tool/result, request/header, request/context, and session/end-seed; a Session's complete interaction history is defined by it.— https://deepseek-harness.github.io/deepseek-harness/en/reference/subsystems/session
SurfaceEventType
SurfaceEventType is the subset of event types that produce messages — system/message, user/message, assistant/message, and tool/result; only those variants carry surfaceOp and sourceEventSeqs on a SessionEvent and are the sole source of derived model history, while other types carry no surface metadata.— https://deepseek-harness.github.io/deepseek-harness/en/reference/subsystems/session
SessionMessageProjection
SessionMessageProjection is the pure interpretation of one plugin-owned event that changes existing message content: it declares the type it interprets and implements project(event, context) returning an immutable message map keyed by original event seq; it must validate the complete durable decision before returning updates, preserving message identities without mutating the input.— https://deepseek-harness.github.io/deepseek-harness/en/reference/subsystems/session
ctx.sessionProjections
ctx.sessionProjections is the projection seam provided by dsh-session-projection: registered units incrementally fold committed events, host consumers read a single typed state via stateOf(), and carriers fetch the trimmed client view in bulk via snapshot(); contributors must not silently provide default values for missing host values.— https://deepseek-harness.github.io/deepseek-harness/en/reference/

Sources