Events & Session Lifecycle
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. The session state machine ensures that every session progresses through a rigorous, finite-state lifecycle that prevents invalid states and enables crash recovery.Event Pipeline
PodiumEventMapper
ThePodiumEventMapper is a pure, table-driven 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 & Sandbox Events
Terminal & Sandbox Events
Maps
terminal.stream and terminal.complete for command execution output. Maps sandbox.provisioning, sandbox.init, and sandbox.removed for sandbox lifecycle tracking.Plan & Memory Events
Plan & Memory Events
Maps
plan.created, plan.step_started, plan.step_completed, plan.revised for structured task planning, and memory.extracted for cross-session memory extraction. See Agent Capabilities for details.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. Sequence numbers are per-session, not global — this enables clients to request events after a specificseq via the afterSeq parameter on join_session, and to detect gaps if events arrive out of order.
Ephemeral vs. Persistent Events
Not all events need to survive a restart:- Ephemeral Events
- Persistent Events
Broadcast to connected clients but never written to SQLite. High-frequency, transient signals that would bloat storage without providing replay value:
text_delta, thinking.progress, terminal.stream, tool.call_delta, heartbeat, pong, plan.step_started, plan.step_completed, and others.Handler Decomposition
After the mapper produces gateway events, each event is routed by type to one of seven specialized handler modules:| 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 |
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 |
(ctx: EventHandlerContext, event?) => Effect<void>. Handler modules have zero imports from infrastructure layers — they interact exclusively through a context interface, making them trivially testable with stub implementations.
Broadcaster
TheBroadcaster wraps Bun’s native WebSocket pub/sub with topic lifecycle tracking:
| Scope | Topic Pattern | Subscribers |
|---|---|---|
| Session | session:{sessionId} | All clients that have joined this session |
| Tenant | tenant:{tenantId}:sessions | All authenticated clients for this tenant |
broadcastShutdown(reason) iterates all known topics and publishes a server_shutdown event before the server terminates.
State Snapshots for Late Joiners
When a client sendsjoin_session, the gateway constructs a state_snapshot — a point-in-time view of the session including the current turn’s textSoFar (read from the active ConnectionState.fullContent ref), recent message history, sandbox state, and subscriber count. A client joining mid-stream immediately sees all text generated so far, without needing to replay individual deltas.
The Session State Machine
Every agent session passes through a rigorous, finite-state lifecycle. The gateway defines seven states, an explicit transition guard map, and a deterministic mapping from agent-reported status values to state transitions.The Seven States
- inactive — no Podium connection. Metadata-only row in the tenant registry.
- activating — creating a Podium instance and establishing a WebSocket connection. Transient.
- ready — connection established, agent idle, awaiting user input.
- running — agent actively processing a turn (streaming text, invoking tools, reasoning).
- waiting — agent blocked on user interaction (
question_requestedorpermission_requested). - deactivating — Podium instance being stopped, WebSocket closing. Resolves to
inactiveorerror. - error — unrecoverable failure. Recovery requires passing through
inactiveoractivatingfirst.
Transition Guard Map
Agent Status Mapping
TheapplySessionTransition pure function computes the next state from the current state and an agent-reported status. It returns null for invalid transitions, allowing the caller to log and skip rather than silently apply:
| Agent Status | Target State | Notes |
|---|---|---|
created | activating | Podium instance created |
connected | ready | WebSocket handshake complete |
turn_started | running | Agent began processing |
turn_complete | ready | Turn finished successfully |
turn_error | ready or error | ready if currently running/waiting; error otherwise |
question_requested | waiting | Agent needs user input |
approval_resolved | running | User responded |
terminating | deactivating | Graceful shutdown initiated |
terminated | inactive | Shutdown complete |
error | error | Unrecoverable failure |
Enforcement
ThetransitionSessionState helper is the single point through which all state transitions flow. It validates against the guard map, updates the ConnectionState ref, persists to the registry database, and broadcasts to all subscribers. Invalid transitions are logged and skipped — never silently applied, and never thrown.
ConnectionState
Each active session maintains aConnectionState — a struct of 15+ Effect Ref values tracking turn ID, accumulated text, pending tool calls, thinking state, billing reservation, and session state. At the start of each new turn, resetTurnState clears all turn-specific refs, ensuring no state leaks between turns.
Stale Session Recovery
After a gateway restart, no Podium connections survive.reconcileStaleSessions runs on startup for each tenant, querying all sessions not in inactive and resetting them — driven by the error → inactive transition. Runs with concurrency of 5 to avoid overwhelming the SQLite writer.
Legacy Migration
The state machine provides a migration path from the earlier 4-state model (idle, running, awaiting_question, error) to the current 7-state model via migrateLegacyStatus().