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".

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":0,"id":"...","createdAt":0}
{"type":"user/message","seq":1,"data":{...}}
{"type":"request/header","seq":2,"data":{...}}
{"type":"tool/call","seq":3,"data":{...}}
{"type":"tool/result","seq":4,"data":{...}}

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

Event-envelope fields (each event may have):

FieldMeaning
seqthe monotonically increasing persisted ordering key
sourceEventSeqsseqs of the source events referenced (chunk→message, compaction replacement→shadowed entries)
surfaceOpappend/replace, only valid for 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 SESSION_FORMAT_VERSION doesn't match (final > supported), loading rejects outright (no migration) and prompts upgrading the harness
  • 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