Skip to main content
PathDocs

Event System

One-liner: DSH has two event families. Cordis in-process events (ctx.on, a hook chain for inter-plugin communication) and SessionEvent session logs (persistent, event-sourced). They are not the same thing: don't conflate "listening" with "persisting".

Audit baseline 0.1.5-alpha.1 @ 5dda764ed3: event names, dispatch modes, and envelope fields are all verified point-by-point against the official source.

This page is the master overview of "how plugins communicate, and how sessions are recorded". After reading it you'll know: when to use ctx.on, when to use session.append, what events exist, and the difference between (plural tools/) and (singular tool/).

1. Two event planes​

Cordis in-process eventsSessionEvent session logs
Carriermemory, ctx.on/ctx.emitsession append-only log (default zstd-compressed)
Semanticstransient notifications, hook chainsevent sourcing, recoverable session facts
Lifetimeauto-unregistered on plugin unmountpersisted, survives restarts
Examplesagent/status, tools/pre-execute, session/createduser/message, request/header, tool/call, tool/result

Key: the session log ("single source of truth") carries telemetry, projections, and recovery; Cordis events are in-process reactive extension. Don't think mounting ctx.on persists anything: for persistence use session.append.

2. Five dispatch modes (ctx.emit/*)​

Cordis event dispatch modes determine how listeners get composed:

ModeSemanticsTypical
emitfire synchronously, don't wait for listenersagent/error
parallelasync in parallel, await allsession/flush
serialserially, one by onesome agent/*
bailstop at the first bail result—
waterfalllisteners form a next() chain; next()'s return value flows to the nexttools/pre-execute, agent/request

waterfall must call next(): missing it short-circuits the entire chain. Publishing side uses ctx.emit() / ctx.parallel() / ctx.waterfall().

3. agent/*: agent lifecycle and control plane​

EventTiming / dispatch
agent/created / agent/disposedcreated / destroyed (paired)
agent/statusrunning-state transitions
agent/pre-stepper-step assembly (waterfall)
agent/requestrequest-assembly replacement (waterfall)
agent/request-errormodel-request failure recovery (waterfall; dsh-llm-retry hangs here)
agent/turn-stoppinga turn begins stopping (serial)
agent/error / agent/session-starterror / session start
agent/inbox/*inserted / claimed / discarded (see the main loop)

agent/* is the control plane; the actual conversation content flows through the session log.

4. The tools pipeline (Cordis, plural tools/)​

The extension points of each tool call, extending from Agent Main Loop:

tools/result (plural, a Cordis notification) ≠ tool/result (singular, a session-log event): the former is an in-process notification, the latter a persisted session event. This is the pair that's easiest to confuse.

5. SessionEvent session-log vocabulary​

Singular tool/, persisted. One session log:

{"type":"session","version":3,"id":"...","createdAt":0,"isSeeded":false,"delegationDepth":0}
{"type":"user/message","seq":1,"time":0,"data":{...},"surfaceOp":"append"}
{"type":"request/header","seq":2,"time":0,"data":{...}}
{"type":"tool/call","seq":3,"time":0,"data":{...}}
{"type":"tool/result","seq":4,"time":0,"data":{...},"surfaceOp":"append"}

Common types: system/message, user/message, assistant/message, assistant/attempt, request/header, request/context, tool/call, tool/result, step/start, step/end, turn/start, turn/end, session/end-seed, compaction/*, permission/preset, feedback/record, todo/write, goal/change, llm/retry, llm/retry-started. For the full catalog see $SRC/packages/core/session/src/known-event-types.ts.

Event-envelope fields (optional by event type):

FieldMeaning
seqthe monotonically increasing persisted ordering key
timeUnix epoch milliseconds
sourceEventSeqsseqs of the source events referenced (compaction replacement→shadowed entries); present only on surface events
surfaceOpappend or { op:'replace', startSeq, endSeq }, only valid for system/message/user/message/assistant/message/tool/result
ignorableunknown types with this set can be skipped; absent = required, unknown types reject rebuild

6. Event-type conventions and the format gate​

  • seq is monotonically increasing (the ordering key)
  • format-version gate: when the log's first-line version is newer than the current SESSION_FORMAT_VERSION (=3), loading rejects outright and prompts upgrading the harness; released v0/v1/v2 are migrated to v3 by the format catalog
  • unknown types: no ignorable → reject; with ignorable → skip
  • telemetry / projection / recovery consume the same event stream (that's what "the log is the single source of truth" means)

7. The two ways a plugin participates​

What you wantHow to do it
listen for reactive extensionctx.on('tools/result', handler) (Cordis)
publish a persisted factsession.append(type, data), then await ctx.sessions.flush(session)
custom persisted eventsfirst declare module '@deepseek-ai/dsh-session/types' to augment SessionEventMap, otherwise append rejects unknown types
custom Cordis eventsctx.emit, auto-cleaned on plugin unmount

Details on the two event planes + code in Listening to Events.

8. Several seams consuming the same stream​

SeamWhat it does with the event stream
session-persistencestorage/reload (eager write-behind)
session-projectionderived views (cache)
session-telemetryOTel export
session-titletitle generation
UIsession/event + agent/* control events render the conversation

9. Verification​

# the event stream = the full session log (default zstd-compressed, two-level directories)
zstdcat ~/.dsh/sessions/*/*/session*.jsonl.zstd | jq -r '.type' | sort | uniq -c | sort -rn | head -12

# inspect agent control events
zstdcat ~/.dsh/sessions/*/*/session*.jsonl.zstd | grep -E '"agent/' | head

Next steps​