Event System
Diminuendo’s event system is the central nervous system of the gateway. Every agent action — streaming a text token, invoking a tool, requesting user permission, reporting token usage — flows through a structured pipeline that transforms raw Podium agent events into the gateway’s typed protocol, routes them through specialized handlers, persists the durable ones, and broadcasts them to connected clients. This page traces that pipeline from end to end.Event Pipeline
PodiumEventMapper
ThePodiumEventMapper is a pure function that transforms raw Podium agent events into the gateway’s own event protocol. A single Podium event may map to zero, one, or multiple gateway events.
Mapping Categories
The mapper handles events across six categories:Tool Lifecycle
Tool Lifecycle
Maps
tool.call_start, tool.call_delta, tool.call, tool.result, and tool.error from Podium into corresponding gateway events. Both messageType and content.event_type are checked, providing resilience against upstream format changes.Interactive Events
Interactive Events
Maps
tool.question_requested, tool.permission_requested, and tool.approval_resolved. These events drive the session state machine into the waiting state and back.Thinking Events
Thinking Events
Maps
thinking.start, thinking.progress (including the legacy thinking_update type), and thinking.complete. Empty thinking content is filtered — the mapper returns an empty array rather than emitting a no-op event.Terminal Events
Terminal Events
Maps
terminal.stream and terminal.complete for command execution output.Sandbox Events
Sandbox Events
Maps
sandbox.provisioning, sandbox.init, and sandbox.removed for sandbox lifecycle tracking.Turn Lifecycle and Message Streaming
Turn Lifecycle and Message Streaming
Maps the core message flow:
created / stream_start become turn_started; update / stream_update become text_delta; complete / stream_end / stream_complete become turn_complete; and error becomes turn_error. A fallback rule treats any unrecognized event with textual content as a text_delta.Per-Session Sequence Numbers
Every event emitted by the mapper is assigned a monotonically increasing sequence number scoped to its session:seq via the afterSeq parameter on join_session, and to detect gaps if events arrive out of order.
When a session is deleted,
resetSessionSeq(sessionId) clears its counter. This prevents stale counters from accumulating memory for sessions that no longer exist.Handler Decomposition
After the mapper produces gateway events, each event is routed by type to one of seven specialized handler modules. Every handler is a pure function with the signature(ctx: EventHandlerContext, event?) => Effect<void>:
| Module | Events Handled | Responsibility |
|---|---|---|
message-complete | turn_complete, turn_error | Persists accumulated text, transitions state, settles billing |
tool-lifecycle | tool_call_start, tool_call, tool_result, tool_error | Tracks pending/completed tool calls in ConnectionState |
interactive | question_requested, permission_requested, approval_resolved | Manages deferred interactive messages, transitions to/from waiting |
thinking | thinking_start, thinking_progress, thinking_complete | Tracks thinking state in ConnectionState refs |
terminal | terminal_stream, terminal_complete | Forwards terminal output (handled inline) |
sandbox | sandbox_provisioning, sandbox_ready, sandbox_removed | Tracks sandbox lifecycle |
| inline | text_delta, session_state | Text accumulation and session state transitions handled directly in the dispatcher |
Dispatch Implementation
ThedispatchToHandler function in MessageRouterLive.ts is a switch over the event type string:
EventHandlerContext
TheEventHandlerContext is a struct threaded through every handler. It decouples handlers from infrastructure by providing method handles rather than direct service references:
Ephemeral vs. Persistent Events
Not all events need to survive a restart. The gateway classifies events into two categories:- Ephemeral Events
- Persistent Events
Broadcast to connected clients but never written to SQLite. These are high-frequency, transient signals that would bloat storage without providing replay value.
The Event Envelope
Every event sent to clients conforms to theEventEnvelope structure defined in events.ts:
EventDataMap is a mapped type that associates each of the 51 event types with its specific data interface, enabling fully typed event handling on both the server and in client SDKs.
The 51-Type Event Protocol
The gateway defines 51 event types spanning ten categories:Connection Lifecycle
connected, heartbeatSession Lifecycle
session_created, session_updated, session_archived, session_unarchived, session_deleted, session_stateTurn Lifecycle
turn_started, turn_complete, turn_errorMessage Streaming
message.delta, message.complete, text_deltaTool Lifecycle
tool.call_start, tool.call_delta, tool.call, tool.result, tool.errorInteractive
tool.question_requested, tool.permission_requested, tool.approval_resolvedThinking & Terminal
thinking.start, thinking.progress, thinking.complete, terminal.stream, terminal.completeSandbox
sandbox.init, sandbox.provisioning, sandbox.ready, sandbox.removedState & Reliability
state_snapshot, stream_snapshot, gap, replay_complete, steer_sent, stop_acknowledgedSystem & Auth
welcome, authenticated, session_list, pong, error, server_shutdown, usage.update, usage.context, file_list, file_content, file_changed, file_history_result, history, eventsBroadcaster
TheBroadcaster is an Effect service that wraps Bun’s native WebSocket pub/sub with topic lifecycle tracking. It provides two primary publishing methods:
Topic Naming Convention
| Scope | Topic Pattern | Subscribers |
|---|---|---|
| Session | session:{sessionId} | All clients that have joined this session |
| Tenant | tenant:{tenantId}:sessions | All authenticated clients for this tenant |
Topic Tracking
The Broadcaster maintains aSet<string> of all topics that have received at least one publish. This enables two critical features:
-
Graceful shutdown:
broadcastShutdown(reason)iterates over all known topics and publishes aserver_shutdownevent to each, ensuring every connected client is notified before the server terminates. -
Topic cleanup:
forgetTopic(topic)removes a topic from the tracking set when the last subscriber disconnects, preventing unbounded memory growth.
Heartbeat
A 30-second heartbeat timer publishes aheartbeat event to every active session topic. Clients use this to detect stale connections and trigger reconnection logic.
State Snapshots for Late Joiners
When a client sendsjoin_session, the gateway does not replay the entire event history. Instead, it constructs a state_snapshot — a point-in-time view of the session’s current state:
textSoFar field is read from the active session’s ConnectionState.fullContent ref — the same ref that the text_delta handler accumulates into. This means a client joining mid-stream immediately sees all text generated so far, without needing to replay individual deltas.
The
stream_snapshot event type exists for an even more detailed mid-stream view, including thinking content and pending tool calls. It is classified as ephemeral and not persisted.Event Persistence Flow
The persistence path for a durable event follows this sequence:1
Mapper assigns seq
mapPodiumEvent calls nextSeq(sessionId) to get a monotonic sequence number.2
Broadcast to subscribers
The event is published to the
session:{sessionId} topic via the Broadcaster.3
Check persistence set
The event type is checked against
PERSISTENT_EVENT_TYPES.4
Write to SQLite
If persistent,
persistEvent(sessionId, seq, eventType, data) fires a write command to the writer worker. The data is JSON-stringified. This is a fire-and-forget operation.5
Handler dispatch
The event is dispatched to its handler module for side effects (state transitions, billing settlement, etc.).
Gap Detection and Replay
Clients can detect missing events by tracking the last receivedseq and comparing it to incoming events. If a gap is detected, the gateway supports two mechanisms:
-
get_eventsmessage: Clients send{ type: "get_events", sessionId, afterSeq, limit }to request events from the persistent store. The reader worker queriesSELECT * FROM events WHERE seq > ? LIMIT ?. -
gapevent: The server can proactively emit agapevent withfromSeqandtoSeqfields, signaling to clients that events in that range may have been lost (e.g., due to a Podium connection drop).
replay_complete event signals that the server has finished sending replayed events, and the client can resume processing real-time events.