Date: 2026-02-25 Status: Current state Scope: Full system architecture β core, adapters, consumer, relay, daemon
- Overview
- System Architecture
- Core Design Principles
- Module Overview
- Core Modules
- Consumer Plane
- Backend Plane
- Pure Functions
- Session Data Model
- Command and Event Flow
- Session Lifecycle State Machine
- Backend Adapters
- React Consumer
- Daemon
- Security Architecture
- Cross-Cutting Infrastructure
- Module Dependency Graph
- File Layout
- Key Interfaces
BeamCode is a message broker β it routes messages between remote consumers (browser/phone via WebSocket) and local AI coding backends (Claude CLI, Codex, ACP, Gemini, OpenCode) with session-scoped state.
The core is built around a per-session actor (SessionRuntime) that is the sole owner of session state. All state transitions flow through a pure reducer that returns new state plus a list of effects (side-effect descriptions). The runtime executes effects after applying the state transition. Persistence is automatic and debounced.
Core invariant: Only
SessionRuntime.process()can transition session state. The reducer is pure:(SessionData, SessionEvent) β [SessionData, Effect[]]. Effects are descriptions, not executions β the runtime's executor handles I/O. Persistence is automatic on every state change (debounced, no manual calls).
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BEAMCODE SYSTEM ARCHITECTURE β
β β
β ββββββββββββββββββββββββ βββββββββββββ β
β β React Consumer β β Desktop β Consumers β
β β (web/) β β Browser β (any WebSocket client) β
β β React 19 + Zustand β βββββββ€ββββββ β
β β + Tailwind v4 + Vite β β β
β βββββββββ€βββββββββββββββ β β
β β β β
β β HTTPS β ws://localhost β
β β β (direct, no tunnel) β
β βββββββββΌββββββββββ β β
β β Cloudflare β β β
β β Tunnel Edge β β LOCAL PATH β
β βββββββββ¬ββββββββββ β β
β βββββββββΌββββββββββ β β
β β cloudflared β β βββ sidecar process (Go binary) β
β β reverse proxy β β proxies HTTPS β localhost:PORT β
β βββββββββ¬ββββββββββ β β
β β localhost:PORT β β
β β β β
β βββββββββΌββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββ β
β β HTTP + WS SERVER (localhost:9414) β β
β β β β
β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β http/ β HTTP Request Router β β β
β β β ββββββββββββββββ ββββββββββββββββ βββββββββββββββββββββββββββ β β β
β β β β api-sessions β β consumer- β β health β β β β
β β β β REST CRUD β β html (serves β β GET /health β β β β
β β β β /api/sessionsβ β React app) β β β β β β
β β β ββββββββββββββββ ββββββββββββββββ βββββββββββββββββββββββββββ β β β
β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β β
β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β server/ β WebSocket Layer β β β
β β β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββββ β β β
β β β β Origin β β Auth Token β β Reconnection Handler β β β β
β β β β Validation β β Gate β β Stable consumer IDs β β β β
β β β ββββββββββββββββ ββββββββββββββββ β Message replay β β β β
β β β ββββββββββββββββββββββββββ β β β
β β β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββββ β β β
β β β β Consumer β β Consumer β β Api-Key β β β β
β β β β Channel β β Rate Limit β β Authenticator β β β β
β β β β (per-client β β token-bucket β β β β β β
β β β β send queue) β β β β β β β β
β β β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββββ β β β
β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββ β
β β β
β ConsumerMessage (30+ subtypes, typed union) β
β InboundMessage (user_message, permission_response, interrupt, ...) β
β β β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β core/ β Actor + Reducer + Effects β β
β β β β
β β SessionCoordinator β SessionRuntime.process(event) β β
β β β SessionReducer (pure) β β
β β β EffectExecutor (I/O) β β
β ββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββ β
β β β
β ββββββββββββββββββββββββββββββΌβββββββββββββββββββ¬βββββββββ β
β β β β β β β
β βΌ βΌ βΌ βΌ βΌ β
β ββββββββββββ ββββββββββββββ ββββββββββββββββ ββββββββ ββββββββ β
β β Claude β β ACP β β Codex β βGeminiβ βOpen- β β
β β Adapter β β Adapter β β Adapter β βAdapt β βcode β β
β β NDJSON/ β β JSON-RPC/ β β JSON-RPC/WS β βwraps β βAdapt β β
β β WS --sdk β β stdio β β app-server β βACP β βREST+ β β
β β stream, β β β β Thread/Turn/ β β β βSSE β β
β β perms, β β β β Item model β β β β β β
β β teams β β β β β β β β β β
β ββββββ¬ββββββ βββββββ¬βββββββ ββββββββ¬ββββββββ ββββ¬ββββ ββββ¬ββββ β
β βΌ βΌ βΌ βΌ βΌ β
β βββββββββββ ββββββββββββββββ βββββββββββββ βββββββββ βββββββββ β
β β Claude β β Goose/Kiro/ β β Codex CLI β βGemini β βopen- β β
β β Code CLIβ β Gemini (ACP) β β (OpenAI) β β CLI β β code β β
β βββββββββββ ββββββββββββββββ βββββββββββββ βββββββββ βββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| # | Rule | Rationale |
|---|---|---|
| 1 | Only SessionRuntime.process() can change session state |
Enforced by compiler (readonly SessionData) β not convention |
| 2 | State transitions are pure: (SessionData, SessionEvent) β [SessionData, Effect[]] |
90%+ business logic testable with zero mocks |
| 3 | Side effects are descriptions (Effect[]), not inline I/O | Effects are enumerable, testable, and traceable |
| 4 | Persistence is automatic and debounced on every state change | Zero manual persistSession() calls β impossible to forget |
| 5 | Transport modules emit commands, never trigger business side effects directly | Clean separation between I/O and logic |
| 6 | Policy services observe state and emit commands β they never mutate | Reconnect, idle, capabilities are advisors |
| 7 | Explicit lifecycle states for each session | Testable state machine, no implicit status inference |
| 8 | Session-scoped domain events flow from runtime; coordinator emits only global lifecycle events | Typed, meaningful events replace forwarding chains |
| 9 | Synchronous processing of events to avoid interleaving | Each process() call completes state transition before next one starts |
| Context | Responsibility | Modules |
|---|---|---|
| SessionControl | Global lifecycle, per-session actor ownership, persistence | SessionCoordinator, session/SessionRuntime (per-session), session/SessionRepository, policies/*, capabilities/* |
| BackendPlane | Adapter abstraction, connect/send/stream | backend/BackendConnector, AdapterResolver, BackendAdapter(s) |
| ConsumerPlane | WebSocket transport, auth, rate limits, outbound push | consumer/ConsumerGateway, consumer/ConsumerBroadcaster, consumer/ConsumerGatekeeper |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β COMPOSITION ROOT β
β (bin/beamcode.ts) β
β β
β Creates all modules, injects dependencies, starts coordinator β
ββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββ
β constructs
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SessionCoordinator β
β β
β Top-level owner: wires services, manages runtime map, routes events β
β Delegates event wiring to CoordinatorEventRelay β
β Delegates relaunch dedup to BackendRecoveryService β
β Delegates log redaction to ProcessLogService β
β Delegates startup restore to StartupRestoreService β
βββββ¬βββββββββββ¬βββββββββββββ¬ββββββββββββββββ¬ββββββββββββββββββββββββββββββββββ
β β β β
βΌ βΌ βΌ βΌ
ββββββββββ βββββββββββ βββββββββββ βββββββββββββββββββ
βDomain β βConsumer β β Backend β β Runtime Map β
βEventBusβ β Gateway β βConnectorβ β Map<id, β
ββββββββββ βββββββββββ βββββββββββ β SessionRuntime>β
β ββββββββ¬βββββββββββ
βΌ β
ββββββββββββββββββββββββΌβββββββ
β SessionRuntime β
β (one per session) β
β β
β process(event) β
β βββββββββββββββββββββββ β
β β SessionReducer β β
β β (pure function) β β
β β β [Data, Effects] β β
β βββββββββββ¬ββββββββββββ β
β β β
β βββββββββββΌββββββββββββ β
β β EffectExecutor β β
β β (I/O dispatcher) β β
β βββββββββββββββββββββββ β
β β
β SOLE STATE OWNER β
βββββββββββββββββββββββββββββββ
File: src/core/session-coordinator.ts
Context: SessionControl
Writes state: No (delegates to runtime via process())
The SessionCoordinator is the top-level orchestrator and the only composition root for session infrastructure. It directly owns the runtime map, service registry, transport hub, policies, and extracted services. It uses a SessionLeaseCoordinator to ensure only one instance of the bridge can mutate a given session (lease-based ownership).
Responsibilities:
- Create sessions: Routes to the correct adapter (inverted vs direct connection), initiates the backend, seeds session state
- Delete sessions: Orchestrates teardown β kills CLI process, clears dedup state, closes WS connections, removes from registry
- Route events to runtimes: Specialized routing callbacks (
routeConsumerMessage,routeUnifiedMessage, etc.) lookup the runtime and callruntime.process(event) - Own the service registry: Constructs
SessionServices(broadcaster, connector, storage, tracer, logger) once at startup - Restore from storage: Delegates to
StartupRestoreService - React to domain events: Delegates to
CoordinatorEventRelay
Extracted services (in src/core/coordinator/):
| Service | Responsibility |
|---|---|
CoordinatorEventRelay |
Subscribes to domain events, dispatches to handlers |
ProcessLogService |
Buffers and redacts process stdout/stderr |
BackendRecoveryService |
Timer-guarded relaunch dedup, graceful kill before relaunch |
ProcessSupervisor |
Process spawn/track/kill for CLI backends |
StartupRestoreService |
Ordered restore: launcher β registry β runtimes |
Does NOT do:
- Mutate any session-level state (runtime does)
- Forward events between layers (delegates to relay)
- Route messages (runtime does)
class SessionCoordinator {
readonly launcher: SessionLauncher;
readonly registry: SessionRegistry;
readonly domainEvents: DomainEventBus;
readonly store: SessionRepository;
readonly broadcaster: ConsumerBroadcaster;
readonly backendConnector: BackendConnector;
async start(): Promise<void>
async stop(): Promise<void>
async createSession(options): Promise<SessionInfo>
async deleteSession(id: string): Promise<boolean>
renameSession(id: string, name: string): SessionInfo | null
async executeSlashCommand(sessionId: string, command: string): Promise<SlashResult>
// (routes events to runtimes via internal getOrCreateRuntime(session).process())
}File: src/core/session/session-runtime.ts
Context: SessionControl
Writes state: Yes β sole writer (compiler-enforced)
The SessionRuntime is a per-session actor. One instance exists per active session. It owns immutable SessionData (readonly at the type level) and mutable SessionHandles (runtime references). Its single entry point is process(event).
Responsibilities:
- Own all session state:
SessionData(immutable, serializable) +SessionHandles(mutable runtime refs) - Process events through the reducer:
process(event)calls the puresessionReducer(), applies the state transition, then executes the returned effects - Auto-persist: Every state change triggers
markDirty()(debounced 50ms). Critical transitions (result, session close) callpersistNow()for immediate flush - Execute effects: Dispatches
Effect[]to the appropriate I/O handler (broadcast, send-to-backend, emit event, async workflow) - Manage consumers: Add/remove WebSocket connections in
SessionHandles - Manage backend state: Store/clear the
BackendSessionreference inSessionHandles - Lifecycle state machine: Lifecycle is part of
SessionDataβ transitions enforced by the reducer
Does NOT do:
- Contain business logic β all state transitions are in the pure
SessionReducer - Know about WebSocket protocols β delegates to
ConsumerBroadcaster - Know about adapter specifics β delegates to
BackendConnector
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SessionRuntime β
β (per-session, actor model) β
β β
β ββββββββββββ PRIVATE STATE (compiler-enforced) βββββββββββββββββββββ β
β β β β
β β data: SessionData (readonly β immutable record) β β
β β ββ id, lifecycle, state, messageHistory, lastStatus β β
β β ββ pendingPermissions, pendingMessages, queuedMessage β β
β β ββ adapterName, adapterSupportsSlashPassthrough β β
β β β β
β β handles: SessionHandles (mutable β runtime references) β β
β β ββ backendSession, backendAbort β β
β β ββ consumerSockets, consumerRateLimiters β β
β β ββ teamCorrelation, registry, pendingPassthroughs β β
β β ββ adapterSlashExecutor, pendingInitialize β β
β β β β
β β βββββββ SessionData is readonly β NO OTHER MODULE CAN WRITE βββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β ββββ Single Entry Point ββββββββββββββββββββββββββββββββββ βββββββββ β
β β β β
β β process(event: SessionEvent): void β β
β β 1. [nextData, effects] = sessionReducer(this.data, event) β β
β β 2. if (nextData !== this.data) { this.data = nextData; dirty }β β
β β 3. executeEffects(effects) β β
β β β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β ββββ Auto-Persistence ββββββββββββββββββββββββββββββββββββββββββββββ β
β β β β
β β markDirty() β debounced 50ms, batches rapid updates β β
β β persistNow() β immediate flush for critical transitions β β
β β β β
β β ZERO manual persistSession() calls anywhere in the codebase β β
β β β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β ββββ Emits (notifications, never commands) ββββββββββββββββββββββββββ β
β β β β
β β bus.emit(DomainEvent) β β
β β β’ session:lifecycle_changed β β
β β β’ backend:session_id β β
β β β’ session:first_turn β β
β β β’ capabilities:ready β β
β β β’ permission:requested / permission:resolved β β
β β β’ slash:executed / slash:failed β β
β β β’ team:* events β β
β β β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Serialization: Event processing is synchronous. Only one process() call runs at a time per runtime instance, avoiding race conditions on session state.
File: src/core/session/session-reducer.ts
Context: Pure function (no module context)
Writes state: No β returns new state + effects
The SessionReducer is the single pure function that contains all state-transition logic. It takes current SessionData and a SessionEvent, and returns a tuple of [SessionData, Effect[]].
Responsibilities:
- State reduction for all backend messages: session_init, status_change, assistant, result, stream_event, permission_request, control_response, tool_progress, tool_use_summary, auth_status, configuration_change, session_lifecycle
- State reduction for inbound commands: user_message (echo + normalize), permission_response, interrupt, set_model, queue operations
- State reduction for system signals: backend connected/disconnected, consumer connected/disconnected, idle reap, reconnect timeout, capabilities timeout, session closed, git info resolved, process output received, team state diffed, etc.
- History management: Append, replace (dedup), trim to max length
- Status inference: result β idle, status_change β update lastStatus
- Permission tracking: Store pending permissions from backend requests
- Effect determination: For each event, compute which side effects need to happen (broadcast, send-to-backend, emit domain event, auto-send queued, etc.)
Composed from sub-reducers:
function sessionReducer(data: SessionData, event: SessionEvent): [SessionData, Effect[]] {
switch (event.type) {
case "BACKEND_MESSAGE":
return reduceBackendMessage(data, event.message);
case "INBOUND_COMMAND":
return reduceInboundCommand(data, event.command);
case "SYSTEM_SIGNAL":
return reduceSystemSignal(data, event.signal);
}
}Each sub-reducer further delegates to focused pure functions:
| Sub-reducer | From file | Responsibility |
|---|---|---|
reduce |
session-state-reducer.ts |
AI context: model, cwd, tools, team state, capabilities, cost |
reduceHistory |
history-reducer.ts |
Append, replace, dedup assistant messages, trim to max |
reduceStatus |
inline | status_change β update lastStatus; result β idle |
reducePermissions |
inline | Store/clear pending permission requests |
reduceLifecycle |
session-lifecycle.ts |
Enforce lifecycle state machine transitions |
reduceTeamState |
team/team-state-reducer.ts |
Team member/task state from tool-use messages |
mapInboundCommandEffects |
effect-mapper.ts |
Determine side effects for each command |
Key property: Same-reference optimization β returns the original data reference if no fields changed. This allows nextData !== this.data check in the runtime to skip persistence when nothing changed.
Does NOT do:
- Execute any I/O (broadcasting, persistence, backend sends)
- Access runtime handles (WebSockets, AbortControllers)
- Emit domain events directly
File: src/core/session/effect-executor.ts
Context: SessionControl (owned by SessionRuntime)
Writes state: No (dispatches I/O)
The EffectExecutor translates Effect descriptions into actual I/O operations. It is called by SessionRuntime.process() after each state transition.
Responsibilities:
- Broadcast to consumers:
BROADCASTβConsumerBroadcaster.broadcast() - Broadcast to participants:
BROADCAST_TO_PARTICIPANTSβConsumerBroadcaster.broadcastToParticipants() - Broadcast state patch:
BROADCAST_SESSION_UPDATEβConsumerBroadcaster.broadcast()withsession_updatetype - Emit domain events:
EMIT_EVENTβ injectssessionIdand callsemitEvent(type, payload) - Queue drain:
AUTO_SEND_QUEUEDβMessageQueueHandler.autoSendQueuedMessage() - Send to backend:
SEND_TO_BACKENDβBackendConnector.sendToBackend() - Resolve git info:
RESOLVE_GIT_INFOβGitInfoTracker.resolveGitInfo() - Flush to disk:
PERSIST_NOWβSessionRepository.persist()
Does NOT do:
- Decide which effects to produce (the reducer does that)
- Hold any state
File: src/core/events/domain-event-bus.ts (~52 lines), types in src/core/interfaces/domain-events.ts
Context: Infrastructure
Writes state: No
A flat, typed pub/sub bus. All domain events are emitted exactly once at the source and consumed directly by subscribers β no forwarding chains.
Responsibilities:
- Typed event dispatch: Single
emit(event)method accepts theDomainEventunion type - Typed subscription:
on(type, handler)with TypeScript narrowing viaExtract<DomainEvent, { type: T }> - Lifecycle management: Returns
Disposablefromon()for easy cleanup
Event categories:
- Session lifecycle: created, closed, first_turn, lifecycle_changed
- Backend: connected, disconnected, session_id, relaunch_needed
- Consumer: connected, disconnected, authenticated
- Process: spawned, exited
- Messages: inbound (for tracing), outbound (for tracing)
- Permissions: requested, resolved
- Slash commands: executed, failed
- Capabilities: ready, timeout
- Team: created, deleted, member:joined/idle/shutdown, task:created/claimed/completed
- Errors: error with source + optional sessionId
Key constraint: Transport modules (ConsumerGateway, BackendConnector) do not publish DomainEvents directly. They emit commands/signals to SessionRuntime, which is the canonical event source for session-scoped events.
File: src/core/consumer/consumer-gateway.ts (~287 lines)
Context: ConsumerPlane
Writes state: No (emits commands to runtime)
The ConsumerGateway handles all WebSocket I/O for consumer connections. No business logic. On receiving a valid message, it wraps it as a SessionEvent and routes it to the runtime via coordinator.process(sessionId, event).
Responsibilities:
- Accept connections: Look up the target
SessionRuntimeby session ID. If not found, reject with 4004. Delegate authentication toConsumerGatekeeper. On success, callruntime.process({ type: 'SYSTEM_SIGNAL', signal: 'CONSUMER_CONNECTED', ws, identity }) - Replay state: After accepting a consumer, tell
ConsumerBroadcasterto send the full replay - Validate inbound messages: Size check (256KB), JSON parse, Zod schema validation, RBAC authorization, rate limiting β all delegated to
ConsumerGatekeeper - Route valid messages: Wrap as
SessionEventand callcoordinator.process(sessionId, event) - Handle disconnection:
runtime.process({ type: 'SYSTEM_SIGNAL', signal: 'CONSUMER_DISCONNECTED', ws })
Does NOT do:
- Parse message semantics (that's the reducer's job)
- Mutate session state
- Broadcast to consumers (that's
ConsumerBroadcaster)
File: src/core/consumer/consumer-broadcaster.ts (~170 lines)
Context: ConsumerPlane
Writes state: No (reads handles from runtime)
Pushes ConsumerMessage data to WebSocket clients. Called by the EffectExecutor when processing BROADCAST effects.
Responsibilities:
- Broadcast to all consumers: Iterate over the runtime's consumer socket map, JSON-serialize, send with backpressure protection (skip if
bufferedAmount > 1MB) - Broadcast to participants only: Same but skip
OBSERVERrole - Send replay on reconnect: Full state replay to a newly-connected socket
- Presence updates: Broadcast when consumers connect/disconnect
- Session name updates: Broadcast when auto-naming completes
File: src/core/consumer/consumer-gatekeeper.ts (~157 lines)
Context: ConsumerPlane
Writes state: No (pure validation)
Auth + RBAC + rate limiting. Validates consumer connections and messages. Pluggable Authenticator interface for different auth strategies.
File: src/core/backend/backend-connector.ts (~644 lines)
Context: BackendPlane
Writes state: No (routes messages as SessionEvents to runtime)
The BackendConnector manages adapter lifecycle, the backend message consumption loop, and passthrough interception.
Responsibilities:
- Connect: Resolve the adapter, call
adapter.connect(), callruntime.attachBackendConnection()with theBackendSession, start the consumption loop. The coordinator then emitsBACKEND_CONNECTEDsignal to the runtime - Disconnect: Routes as
process({ type: 'SYSTEM_SIGNAL', signal: { kind: 'BACKEND_DISCONNECTED', reason } }) - Consumption loop:
for await (msg of backendSession.messages)β for each message, routes asprocess({ type: 'BACKEND_MESSAGE', message: msg }) - Passthrough interception: Intercept matching slash command responses during the consumption loop
- Stop adapters: Call
AdapterResolver.stopAll?.()for graceful shutdown
Inverted connection path (CLI calls back via WebSocket):
SessionTransportHubroutes/ws/cli/:sessionIdcallbacks toCliGatewayCliGatewayvalidates launch state, resolves an inverted adapterBufferedWebSocketbuffers early inbound messages until the adapter registers its handler
Does NOT do:
- Own adapter implementation details
- Decide what to do with messages (the reducer does)
- Know about consumer WebSockets
These modules are stateless, have no side effects, and contain no transport knowledge. They are independently testable and form the leaves of the dependency graph.
| Module | File | Boundary | Responsibility |
|---|---|---|---|
| SessionReducer | session/session-reducer.ts |
β | Top-level pure reducer: (SessionData, SessionEvent) β [SessionData, Effect[]]. Composes all sub-reducers |
| SessionStateReducer | session/session-state-reducer.ts |
β | AI context reduction: (SessionState, UnifiedMessage) β SessionState |
| HistoryReducer | session/history-reducer.ts |
β | Message history: append, replace, dedup, trim |
| EffectMapper | session/effect-mapper.ts |
β | Determines which effects to produce for each event |
| InboundNormalizer | messaging/inbound-normalizer.ts (~124L) |
T1 | InboundCommand β UnifiedMessage |
| ConsumerMessageMapper | messaging/consumer-message-mapper.ts (~343L) |
T4 | UnifiedMessage β ConsumerMessage (30+ subtypes) |
| ConsumerGatekeeper | consumer/consumer-gatekeeper.ts (~157L) |
β | Auth + RBAC + rate limiting |
| GitInfoTracker | session/git-info-tracker.ts (~110L) |
β | Git branch/repo resolution |
| TeamToolCorrelationBuffer | team/team-tool-correlation.ts (~92L) |
β | Per-session tool result β team member pairing |
| MessageTracer | messaging/message-tracer.ts (~631L) |
β | Debug tracing at T1/T2/T3/T4 boundaries |
| TraceDiffer | messaging/trace-differ.ts (~143L) |
β | Diff computation for trace inspection |
| TeamStateReducer | team/team-state-reducer.ts (~272L) |
β | Team member/task state from tool-use messages |
| TeamToolRecognizer | team/team-tool-recognizer.ts (~138L) |
β | Recognizes team-related tool patterns |
| TeamEventDiffer | team/team-event-differ.ts (~104L) |
β | Team state diffs for domain event emission |
The single source of truth for a session. All fields are readonly. Only the reducer can produce a new SessionData β the runtime replaces its reference atomically.
interface SessionData {
readonly lifecycle: LifecycleState;
readonly backendSessionId?: string;
readonly state: SessionState;
readonly pendingPermissions: ReadonlyMap<string, PermissionRequest>;
readonly messageHistory: readonly ConsumerMessage[];
readonly pendingMessages: readonly UnifiedMessage[];
readonly queuedMessage: QueuedMessage | null;
readonly lastStatus: "compacting" | "idle" | "running" | null;
readonly adapterName?: string;
readonly adapterSupportsSlashPassthrough: boolean;
readonly teamCorrelation: ReadonlyMap<string, PendingToolUse>;
}Persisted to disk as PersistedSession (subset: state, messageHistory, pendingMessages, pendingPermissions, queuedMessage, adapterName).
Session (from session-repository.ts) wraps SessionData and SessionHandles and adds readonly id: string β the immutable lookup key.
Non-serializable runtime references. Managed by SessionRuntime directly (not through the reducer). These do not survive restarts.
interface SessionHandles {
backendSession: BackendSession | null;
backendAbort: AbortController | null;
consumerSockets: Map<WebSocketLike, ConsumerIdentity>;
consumerRateLimiters: Map<WebSocketLike, RateLimiter>;
anonymousCounter: number;
lastActivity: number;
pendingInitialize: { requestId: string; timer: ReturnType<typeof setTimeout> } | null;
registry: SlashCommandRegistry;
pendingPassthroughs: Array<{...}>;
adapterSlashExecutor: AdapterSlashExecutor | null;
}All inputs to the runtime are typed as one of three SessionEvent variants:
type SessionEvent =
| { type: "BACKEND_MESSAGE"; message: UnifiedMessage }
| { type: "INBOUND_COMMAND"; command: InboundCommand; ws: WebSocketLike }
| { type: "SYSTEM_SIGNAL"; signal: SystemSignal };
type SystemSignal =
| { kind: "BACKEND_CONNECTED"; backendSession: BackendSession; ... }
| { kind: "BACKEND_DISCONNECTED"; reason: string }
| { kind: "CONSUMER_CONNECTED"; ws: WebSocketLike; identity: ConsumerIdentity }
| { kind: "CONSUMER_DISCONNECTED"; ws: WebSocketLike }
| { kind: "GIT_INFO_RESOLVED" }
| { kind: "CAPABILITIES_READY" }
| { kind: "IDLE_REAP" }
| { kind: "RECONNECT_TIMEOUT" }
| { kind: "CAPABILITIES_TIMEOUT" }
| { kind: "BACKEND_RELAUNCH_NEEDED" }
| { kind: "SESSION_CLOSING" }
| { kind: "SESSION_CLOSED" }
| { kind: "STATE_PATCHED"; patch: Partial<SessionState>; broadcast?: boolean }
| { kind: "LAST_STATUS_UPDATED"; status: string }
| { kind: "QUEUED_MESSAGE_UPDATED"; message: QueuedMessage | null }
| { kind: "MODEL_UPDATED"; model: string }
| { kind: "ADAPTER_NAME_SET"; name: string }
| { kind: "SLASH_PASSTHROUGH_RESULT"; ... }
| { kind: "SLASH_PASSTHROUGH_ERROR"; ... }
| { kind: "PASSTHROUGH_ENQUEUED"; entry: ... }
| { kind: "SESSION_SEEDED"; cwd?: string; model?: string }
| { kind: "WATCHDOG_STATE_CHANGED"; watchdog: ... }
| { kind: "RESUME_FAILED"; sessionId: string }
| { kind: "CIRCUIT_BREAKER_CHANGED"; circuitBreaker: ... }
| { kind: "SESSION_RENAMED"; name: string }
| { kind: "PROCESS_OUTPUT_RECEIVED"; stream: string; data: string }
| { kind: "PERMISSION_RESOLVED"; requestId: string; behavior: string }
| { kind: "PENDING_MESSAGE_ADDED"; message: UnifiedMessage }
| { kind: "TEAM_STATE_DIFFED"; prevTeam: TeamState; currentTeam: TeamState; ... }
| { kind: "CAPABILITIES_APPLIED"; commands: ... }
| { kind: "MESSAGE_QUEUED"; queued: QueuedMessage }
| { kind: "QUEUED_MESSAGE_EDITED"; content: string; ... }
| { kind: "QUEUED_MESSAGE_CANCELLED" }
| { kind: "QUEUED_MESSAGE_SENT" };Side effects returned by the reducer. Never executed inside the reducer β the runtime's EffectExecutor handles them.
type Effect =
// Broadcast to consumers
| { type: "BROADCAST"; message: ConsumerMessage }
| { type: "BROADCAST_TO_PARTICIPANTS"; message: ConsumerMessage }
| { type: "BROADCAST_SESSION_UPDATE"; patch: Partial<SessionState> }
// Domain events
| { type: "EMIT_EVENT"; eventType: string; payload: unknown }
// Queue drain
| { type: "AUTO_SEND_QUEUED" }
// I/O
| { type: "SEND_TO_BACKEND"; message: UnifiedMessage }
| { type: "RESOLVE_GIT_INFO" }
| { type: "PERSIST_NOW" }; ββββββββββββββββββββ
β Events flow IN β SessionEvent = requests to change state
ββββββββββ¬ββββββββββ
β
β INBOUND_COMMAND (from ConsumerGateway)
β ββ user_message
β ββ permission_response
β ββ slash_command
β ββ interrupt / set_model / set_permission_mode
β ββ queue_message / cancel / update
β
β BACKEND_MESSAGE (from BackendConnector)
β ββ session_init, assistant, result, status_change
β ββ permission_request, control_response
β ββ stream_event, tool_progress, tool_use_summary, ...
β
β SYSTEM_SIGNAL (from policies, connector, gateway)
β ββ BACKEND_CONNECTED / DISCONNECTED
β ββ CONSUMER_CONNECTED / DISCONNECTED
β ββ RECONNECT_TIMEOUT / IDLE_REAP / CAPABILITIES_TIMEOUT
β ββ GIT_INFO_RESOLVED / CAPABILITIES_READY
β
βΌ
ββββββββββββββββ
βSessionRuntimeβ process(event):
β β [data, effects] = reducer(data, event)
β β execute(effects)
ββββββββ¬ββββββββ
β
β Effect[] (descriptions of what to do)
β ββ BROADCAST β ConsumerBroadcaster
β ββ SEND_TO_BACKEND β BackendConnector
β ββ EMIT_EVENT β DomainEventBus
β ββ RESOLVE_GIT_INFO β GitInfoResolver β feeds back SYSTEM_SIGNAL
β ββ AUTO_SEND_QUEUED β MessageQueueHandler
β
β DomainEvent (notifications of what happened)
β ββ session:lifecycle_changed, session:first_turn
β ββ backend:connected / disconnected / session_id
β ββ consumer:connected / disconnected / authenticated
β ββ permission:requested / resolved
β ββ slash:executed / failed
β ββ capabilities:ready / timeout
β ββ team:* events
β
βΌ
βββββββββββββββββββββ
β Events flow OUT β DomainEvent = facts about what changed
βββββββββββββββββββββ
β
ββββββββΌβββββββββββββββββββββββββββ
βΌ βΌ βΌ
ββββββββ βββββββββββββββββββ ββββββββββββββ
βCoord.β βProcessSupervisorβ β Policies β
β(auto-β β(cleanup on β β(start/stop β
βname, β β disconnect) β β watchdogs) β
βrelaunβ βββββββββββββββββββ ββββββββββββββ
βch) β
ββββββββ
Publishers DomainEventBus Subscribers
ββββββββββ ββββββββββββββ βββββββββββββ
SessionRuntime βββββββ βββββββββββββββββββββββ βββ SessionCoordinator
(via EMIT_EVENT β β β β (relaunch, auto-name)
effects) β β Flat typed bus β β
β β β βββ ReconnectPolicy
β β β’ emit(event) β β
βββββΆβ β’ on(type, fn) ββββββ€ββ IdlePolicy
β β β β
β β ONE HOP β no β βββ CapabilitiesPolicy
β β forwarding chain β β
SessionCoordinator βββ€ β β βββ HTTP API / Metrics
session:created β β β β
session:closed βββββΆβ ββββββ€ββ MessageTracer
β β (transport modules β β
ProcessSupervisor ββββ€ β DO NOT publish β βββ ProcessSupervisor
process:* βββββΆβ DomainEvents) β (process telemetry)
β β β
βββββΆβ β
βββββββββββββββββββββββ
Consumer β Backend:
Browser/Phone
β
β WebSocket connect
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ConsumerGateway β
β (transport only β no business logic) β
β β
β handleConnection(ws, ctx) β
β βββ coordinator.getRuntime(sessionId) / reject 4004 β
β βββ gatekeeper.authenticate(ws, ctx) / reject 4001 β
β βββ runtime.process({ β
β type: 'SYSTEM_SIGNAL', β
β signal: { kind: 'CONSUMER_CONNECTED', ws, identity } β
β }) β
β β
β handleMessage(ws, sessionId, data) β
β βββ size check, JSON.parse, Zod validate, RBAC, rate limit β
β βββ runtime.process({ β
β type: 'INBOUND_COMMAND', command: validated, ws β
β }) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SessionRuntime.process(event) β
β β
β ββββββββββββββββββββββββββββββββββββββββ β
β β 1. REDUCER (pure) β β
β β [nextData, effects] = β β
β β sessionReducer(this.data, event)β β
β ββββββββββββββββββββββββββββββββββββββββ β
β β
β ββββββββββββββββββββββββββββββββββββββββ β
β β 2. STATE UPDATE (atomic) β β
β β this.data = nextData β β
β β this.markDirty() // auto-persist β β
β ββββββββββββββββββββββββββββββββββββββββ β
β β
β ββββββββββββββββββββββββββββββββββββββββ β
β β 3. EFFECTS (I/O dispatch) β β
β β β β
β β user_message effects: β β
β β BROADCAST(echoMsg) βββββββββββββββββββΆ Consumers β
β β SEND_TO_BACKEND(unified) ββββββββββββββΆ Backend β
β β β β
β β permission_response effects: β β
β β SEND_TO_BACKEND(response) βββββββββββββΆ Backend β
β β EMIT_EVENT(permission:resolved) β β
β β β β
β β slash_command effects: β β
β β varies by strategy (local/native/ β β
β β passthrough/unsupported) β β
β ββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Backend β Consumers:
Backend (Claude CLI / Codex / ACP)
β
β async iterable: UnifiedMessage
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BackendConnector β
β β
β startConsumptionLoop(runtime, backendSession) β
β β β
β β for await (msg of backendSession.messages): β
β β β β
β β βββ interceptPassthrough? β buffer + emit result, skip β
β β β β
β β βββ coordinator.process(sessionId, { β
β β type: 'BACKEND_MESSAGE', message: msg β
β β }) β
β β β
β β [stream ends] β
β β βββ coordinator.process(sessionId, { β
β β type: 'SYSTEM_SIGNAL', β
β β signal: { kind: 'BACKEND_DISCONNECTED', reason } β
β β }) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SessionRuntime.process(event) β
β β
β ββββββββββββββββββββββββββββββββββββββββ β
β β 1. REDUCER (pure) β β
β β [nextData, effects] = β β
β β sessionReducer(data, event) β β
β β β β
β β State transitions applied: β β
β β β’ reduceSessionState (model, cwd) β β
β β β’ reduceHistory (append/dedup) β β
β β β’ reduceStatus (idle inference) β β
β β β’ reducePermissions (store/clear) β β
β β β’ reduceLifecycle (active/idle) β β
β ββββββββββββββββββββββββββββββββββββββββ β
β β
β ββββββββββββββββββββββββββββββββββββββββ β
β β 2. STATE UPDATE + AUTO-PERSIST β β
β ββββββββββββββββββββββββββββββββββββββββ β
β β
β ββββββββββββββββββββββββββββββββββββββββ β
β β 3. EFFECTS (per message type) β β
β β β β
β β session_init: β β
β β BROADCAST(session_init) ββββββββββΆ Consumers β
β β (runtime: git resolve + caps req) β
β β β β
β β assistant: β β
β β BROADCAST(consumerMsg) ββββββββββΆ Consumers β
β β β β
β β result: β β
β β BROADCAST(resultMsg) ββββββββββΆ Consumers β
β β AUTO_SEND_QUEUED ββββββββββΆ drain queue β
β β EMIT_EVENT(first_turn?) β β
β β β β
β β status_change: β β
β β BROADCAST(statusMsg) ββββββββββΆ Consumers β
β β AUTO_SEND_QUEUED (if idle) ββββββββββΆ drain queue β
β β β β
β β permission_request: β β
β β BROADCAST_TO_PARTICIPANTS ββββββββββΆ Participants only β
β β EMIT_EVENT(permission:requested) β β
β β β β
β β stream_event, tool_progress, β β
β β tool_use_summary, auth_status, β β
β β configuration_change, β β
β β session_lifecycle: β β
β β BROADCAST(mapped) ββββββββββΆ Consumers β
β β β β
β β control_response: β β
β β (runtime: apply capabilities) β β
β ββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ConsumerBroadcaster β
β (consumer/consumer-broadcaster.ts) β
β β
β broadcast(runtime, msg) β
β for each ws in runtime.handles.consumerSockets: β
β if ws.bufferedAmount > 1MB: skip (backpressure) β
β ws.send(JSON.stringify(msg)) β
β β
β broadcastToParticipants(runtime, msg) β
β same but skip observer role β
β β
β sendReplayTo(ws, runtime) β full state replay on reconnect β
β broadcastPresence(...) β presence_update β
β broadcastNameUpdate(...) β session_name_update β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
All consumer
WebSockets
The system has four named translation boundaries (T1βT4) that are pure mapping functions:
Inbound path:
ConsumerGateway
ββ SessionRuntime.process(INBOUND_COMMAND)
ββ reducer calls InboundNormalizer.normalize(...) [T1]
InboundCommand -> UnifiedMessage
Backend path:
reducer returns SEND_TO_BACKEND effect
ββ EffectExecutor β Adapter session outbound translator [T2]
UnifiedMessage -> backend-native payload
Adapter session inbound translator [T3]
backend-native payload -> UnifiedMessage
ββ BackendConnector β coordinator.process(BACKEND_MESSAGE)
Outbound path:
SessionReducer (inside reducer)
ββ ConsumerMessageMapper [T4]
UnifiedMessage -> ConsumerMessage
(returned as BROADCAST effect)
Each session has an explicit LifecycleState stored in SessionData.lifecycle. Transitions are enforced by the reducer via isLifecycleTransitionAllowed().
type LifecycleState =
| "starting" // Session created, process spawning or connecting
| "awaiting_backend" // Process spawned, waiting for CLI to connect back
| "active" // Backend connected, processing messages
| "idle" // Backend connected, waiting for user input
| "degraded" // Backend disconnected unexpectedly, awaiting relaunch
| "closing" // Shutdown initiated, draining
| "closed" // Terminal state, ready for removal createSession()
β
βΌ
βββββββββββββ
β starting β
βββββββ¬ββββββ
β
ββββββββββββ΄βββββββββββ
β β
(inverted) (direct)
β β
βΌ β
ProcessSupervisor β
.spawn() β
β β
βΌ β
ββββββββββββββββββββ β
β awaiting_backend β β
ββββββββ¬ββββββββββββ β
β β
β CLI connects β adapter.connect()
β β
ββββββββββββ¬βββββββββββββ
β
βΌ
βββββββββββββ
ββββββΆβ active βββββ user_message received
β ββββββ¬βββββββ
β β
β result received
β β
β βΌ
β βββββββββββββ
β β idle βββββ user_message ββββΆ active
β ββββββ¬βββββββ
β β
β backend disconnects unexpectedly
β β
β βΌ
β βββββββββββββ
β β degraded βββ relaunch succeeds βββ
β βββββββ¬ββββββ β
β β β
β relaunch fails / idle_reap β
β β β
β βΌ β
β βββββββββββββ β
β β closing β β
β βββββββ¬ββββββ β
β β β
β βΌ β
β βββββββββββββ β
ββββββ closed βββββββββββββββββββββββββ
βββββββββββββ (if session removed)
Policies react to lifecycle transitions (via DomainEventBus):
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ReconnectPolicy: awaiting_backend β start watchdog timer β
β IdlePolicy: idle + no consumers β start reap timer β
β CapabilitiesPolicy: active β start capabilities timeout β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
All adapters implement the BackendAdapter + BackendSession interfaces β a clean async iterable contract.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BackendAdapter interface β
β name: string β
β capabilities: BackendCapabilities β
β connect(options): Promise<BackendSession> β
β stop?(): Promise<void> β graceful adapter teardownβ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β BackendSession interface β
β sessionId: string β
β send(msg: UnifiedMessage): void β
β messages: AsyncIterable<UnifiedMessage> β
β close(): Promise<void> β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β COMPOSED EXTENSIONS (additive, not baked in) β
β Interruptible: interrupt(): void β
β Configurable: setModel(), setPermissionMode() β
β PermissionHandler: request/response bridging β
β Reconnectable: onDisconnect(), replay() β
β Encryptable: encrypt(), decrypt() β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Adapter | Protocol | Backend | Notes |
|---|---|---|---|
| Claude | NDJSON/WS --sdk |
Claude Code CLI (child process) | Streaming, permissions, teams |
| ACP | JSON-RPC/stdio | Goose, Kiro, Gemini (ACP mode) | Agent Client Protocol |
| Codex | JSON-RPC/WS | Codex CLI (OpenAI) | Thread/Turn/Item model, app-server |
| Gemini | Wraps ACP | Gemini CLI | Spawns gemini --experimental-acp |
| OpenCode | REST+SSE | opencode | Demuxed sessions |
UnifiedMessage is the canonical internal envelope:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β UnifiedMessage β
β id, timestamp, type, role, content[], metadata β
β Supports: streaming (Claude), request/response (ACP), β
β JSON-RPC (Codex/OpenCode) β
β + metadata escape hatch for adapter-specific data β
β + parentId for threading support β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
State hierarchy:
CoreSessionState β DevToolSessionState β SessionState
(adapter-agnostic) (git branch, repo) (model, tools,
team, circuit
breaker, ...)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β REACT CONSUMER (web/) β
β React 19 + Zustand + Tailwind v4 + Vite β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β App.tsx (ErrorBoundary + Bootstrap) β β
β β β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β Layout β β β
β β β ββββββββββ βββββββββββββββββββββββββββββββ ββββββββββββ β β β
β β β βSidebar β β Main Area β βAgentPane β β β β
β β β β β β βββββββββββββββββββββββββ β β β β β β
β β β βSessionsβ β β TopBar β β βAgentGrid β β β β
β β β βby date β β β model, ContextGauge, β β βAgentCol β β β β
β β β β β β β connection status β β βAgentRostrβ β β β
β β β βArchive β β βββββββββββββββββββββββββ β β β β β β
β β β βmgmt β β ββββββββββββββββββββββββββ β ββββββββββββ β β β
β β β β β β β ChatView / MessageFeed β β β β β
β β β βSettingsβ β β AssistantMessage β β β β β
β β β βfooter β β β MessageBubble β β β β β
β β β β β β β UserMessageBubble β β β β β
β β β βSound / β β β ToolBlock / ToolGroup β β β β β
β β β βNotifs β β β ToolResultBlock β β β β β
β β β βDark β β β ThinkingBlock β β β β β
β β β βmode β β β CodeBlock / DiffView β β β β β
β β β β β β β ImageBlock β β β β β
β β β β β β β PermissionBanner β β β β β
β β β β β β β StreamingIndicator β β β β β
β β β β β β β ResultBanner β β β β β
β β β ββββββββββ β ββββββββββββββββββββββββββ β β β β
β β β β βββββββββββββββββββββββββ β β β β
β β β β β Composer β β β β β
β β β β β SlashMenu β β β β β
β β β β β QueuedMessage β β β β β
β β β β βββββββββββββββββββββββββ β β β β
β β β β βββββββββββββββββββββββββ β β β β
β β β β β StatusBar β β β β β
β β β β β adapter, git, model, β β β β β
β β β β β permissions, worktree β β β β β
β β β β βββββββββββββββββββββββββ β β β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β β
β β ββββββββββββ Overlays ββββββββββββββββββββββββββββββββββββ β β
β β β ToastContainer, LogDrawer, ConnectionBanner, β β β
β β β AuthBanner, TaskPanel, QuickSwitcher, β β β
β β β ShortcutsModal, NewSessionDialog β β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β store.ts β Zustand State β
β ws.ts β WebSocket (auto-reconnect, session handoff, presence) β
β api.ts β HTTP Client (REST CRUD for sessions) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DAEMON β
β βββββββββββββ βββββββββββββ ββββββββββββ ββββββββββββββββββββββ β
β β Lock File β β State β β Health β β Control API β β
β β O_CREAT| β β File β β Check β β HTTP 127.0.0.1:0 β β
β β O_EXCL β β PID, port β β 60s loop β β β β
β β β β heartbeat β β β β β’ list sessions β β
β β β β version β β β β β’ create session β β
β β β β β β β β β’ stop session β β
β β β β β β β β β’ revoke-device β β
β βββββββββββββ βββββββββββββ ββββββββββββ ββββββββββββββββββββββ β
β βββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββ β
β β ChildProcessSupervisor β β SignalHandler β β
β β spawns/tracks beamcode β β SIGTERM/SIGINT graceful stop β β
β β server child processes β β β β
β βββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SECURITY LAYERS β
β β
β LAYER 1: Transport β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β’ WebSocket origin validation (reject untrusted origins) β β
β β β’ CLI auth tokens (?token=SECRET per session) β β
β β β’ ConsumerGatekeeper: pluggable Authenticator interface β β
β β β’ ApiKeyAuthenticator: header-based auth β β
β β β’ RBAC: PARTICIPANT vs OBSERVER role-based message filter β β
β β β’ Per-consumer rate limiting: token-bucket β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β LAYER 2: E2E Encryption β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β’ libsodium sealed boxes (XSalsa20-Poly1305) β β
β β β’ sodium_malloc for key material (mlock'd, not swappable) β β
β β β’ Per-message ephemeral keys (limited forward secrecy) β β
β β β’ Relay MUST NOT persist encrypted blobs (stateless only) β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β LAYER 3: Authentication β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β’ Permission signing: HMAC-SHA256(secret, β β
β β request_id + behavior + timestamp + nonce) β β
β β β’ Anti-replay: nonce set (last 1000), 30s timestamp window β β
β β β’ One-response-per-request (pendingPermissions in data) β β
β β β’ Secret established locally (daemonβCLI, never over relay)β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β LAYER 4: Device Management β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β’ Session revocation: revoke-device β new keypair β re-pairβ β
β β β’ Pairing link expires in 60 seconds β β
β β β’ Single device per pairing cycle β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β LAYER 5: Resilience β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β’ SlidingWindowBreaker: circuit breaker with snapshot API β β
β β β’ Structured error types (BeamCodeError hierarchy) β β
β β β’ Secret redaction in process output forwarding β β
β β β’ Watchdog timers for reconnect grace periods β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β KNOWN METADATA LEAKS (documented, acceptable for MVP): β
β β’ Session ID (required for routing, random UUID) β
β β’ Message timing (reveals activity patterns) β
β β’ Message size (large = code output, small = user input) β
β β’ Connection duration, IP addresses, message count β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Module | Responsibility |
|---|---|
| BeamCodeError | Structured error hierarchy (StorageError, ProcessError, etc.) |
| FileStorage | Debounced file writes with schema versioning and flush() for shutdown durability |
| StateMigrator | Schema version migration chain (v0 β v1+) |
| StructuredLogger | JSON-line logging with component context and level filtering |
| SlidingWindowBreaker | Circuit breaker with snapshot API for UI visibility |
| ProcessManager | Spawn, kill, isAlive β signal handling |
| AdapterResolver | Resolves adapter by name, factory for all adapters |
| TokenBucketLimiter | Per-consumer rate limiting |
| ConsoleMetricsCollector | Metrics collection β console output |
| SessionOperationalHandler | Privileged operations (list/close/archive sessions) |
SessionCoordinator
β± β β β²
β± β β β²
β± β β β²
βΌ βΌ βΌ βΌ
ββββββββββββββββ βββββββ ββββββββββ βββββββββββββββββ
β coordinator/ β βeventβ βRuntime β β Process β
β β’EventRelay β βs/ β β Map β β Supervisor β
β β’Recovery β βdom- β β(direct)β β (coordinator/β
β β’LogService β βain- β β β β ~278L) β
β β’Restore β βeventβ β β β β
β β’ProcSupvsr β β-bus β β β βββββββββββββββββ
ββββββββββββββββ β(~52)β β β
ββββ¬βββ βββββ¬βββββ
β β
β βΌ
β ββββββββββββββββ ββββββββββββββββββ
ββββββββββββββββ€ βsession/ β β policies/ β
β β βSessionRuntimeβ β β’Reconnect β
βΌ βΌ β (actor) β β (~119L) β
ββββββββββββββββββ β β β β’Idle (~141L) β
β capabilities/ β β data: β β β
β β’Caps (~191L) β β SessionData β β capabilities/ β
ββββββββββββββββββ β (readonly) β β β’Caps (~191L) β
β β ββββββββββββββββββ
β handles: β
β SessionHndlsβ
ββββββββ¬ββββββββ
delegates to
β
ββββββββββββ΄βββββββββββββββ
βΌ βΌ
βββββββββββββββββββββ ββββββββββββββββββββ
β session/ β β session/ β
β SessionReducer β β EffectExecutor β
β (PURE FUNCTION) β β (I/O dispatch) β
β β β β
β Composes: β β Dispatches to: β
β β’StateReducer β β β’Broadcaster β
β β’HistoryReducer β β β’BackendConnectorβ
β β’EffectMapper β β β’DomainEventBus β
β β’LifecycleRules β β β’GitResolver β
β β’TeamReducer β β β’QueueHandler β
βββββββββββββββββββββ ββββββββββββββββββββ
β β
uses (pure) uses (I/O)
β β
βββββββββ΄βββββββ βββββββββ΄βββββββ
βΌ βΌ βΌ βΌ
ββββββββββββ ββββββββββββ βββββββββββ βββββββββββ
βmessaging/β βteam/ β βconsumer/β βbackend/ β
ββ’Mapper β ββ’Reducer β βBrdcstr β βConnectorβ
β (~343L) β ββ’Recog β β(~170L) β β(~644L) β
ββ’Normal β ββ’Correltn β βββββββββββ βββββββββββ
β (~124L) β ββ’Differ β
ββ’Tracer β ββββββββββββ
β (~631L) β
ββββββββββββ
No cycles. Pure functions at leaves.
Runtime delegates to pure reducer + effect executor.
consumer/ + backend/ modules emit SessionEvents to coordinator.
policies/ observe and advise via DomainEventBus.
coordinator/ services handle cross-session concerns.
src/core/
βββ session-coordinator.ts β top-level orchestrator + service registry
βββ index.ts β barrel exports
β
βββ backend/ β BackendPlane
β βββ backend-connector.ts β adapter lifecycle + consumption + passthrough (~644L)
β
βββ capabilities/ β Capabilities handshake policy
β βββ capabilities-policy.ts β observe + advise (~178L)
β
βββ consumer/ β ConsumerPlane
β βββ consumer-gateway.ts β WS accept/reject/message, emits SessionEvents (~291L)
β βββ consumer-broadcaster.ts β broadcast + replay + presence (~170L)
β βββ consumer-gatekeeper.ts β auth + RBAC + rate limiting (~157L)
β
βββ coordinator/ β Cross-session services for SessionCoordinator
β βββ coordinator-event-relay.ts β domain event wiring (~163L)
β βββ process-log-service.ts β stdout/stderr buffering + secret redaction (~41L)
β βββ backend-recovery-service.ts β timer-guarded relaunch dedup (~138L)
β βββ process-supervisor.ts β process spawn/track/kill (~278L)
β βββ startup-restore-service.ts β ordered restore (~78L)
β
βββ events/ β Domain event infrastructure
β βββ domain-event-bus.ts β flat typed pub/sub bus (~52L)
β βββ typed-emitter.ts β strongly-typed EventEmitter base (~55L)
β
βββ interfaces/ β Contract definitions
β βββ backend-adapter.ts β BackendAdapter + BackendSession interfaces
β βββ domain-events.ts β DomainEvent union type, DomainEventBus interface
β βββ extensions.ts β Composed adapter extensions
β βββ runtime-commands.ts β InboundCommand, PolicyCommand types
β βββ session-coordination.ts β Coordinator port interfaces
β βββ session-coordinator-coordination.ts β Transport integration interfaces
β βββ session-launcher.ts β Session launcher interface
β βββ session-registry.ts β Session registry interface
β βββ adapter-names.ts β Adapter name constants
β
βββ messaging/ β Pure translation boundaries
β βββ consumer-message-mapper.ts β pure T4 mapper (~343L)
β βββ inbound-normalizer.ts β pure T1 mapper (~124L)
β βββ message-tracer.ts β debug tracing at T1/T2/T3/T4 (~666L)
β βββ trace-differ.ts β diff computation for trace inspection (~143L)
β
βββ policies/ β Policy services (observe + advise)
β βββ idle-policy.ts β idle session sweep (~141L)
β βββ reconnect-policy.ts β awaiting_backend watchdog (~119L)
β
βββ session/ β Per-session state + lifecycle + reducer
β βββ session-runtime.ts β per-session actor: process(event) (~733L)
β βββ session-reducer.ts β top-level pure reducer (~946L)
β βββ session-state-reducer.ts β AI context sub-reducer (~273L)
β βββ history-reducer.ts β message history sub-reducer (~133L)
β βββ effect-mapper.ts β event β Effect[] mapping (~104L)
β βββ effect-executor.ts β Effect β I/O dispatch (~95L)
β βββ effect-types.ts β Effect union type (~40L)
β βββ session-event.ts β SessionEvent, SystemSignal types (~55L)
β βββ session-data.ts β SessionData, SessionHandles types (~78L)
β βββ session-repository.ts β in-memory store + persistence + Session type (~240L)
β βββ session-lease-coordinator.ts ββ per-session lease ownership coordinator
β βββ session-lifecycle.ts β lifecycle state transitions
β βββ session-transport-hub.ts β transport wiring per session
β βββ cli-gateway.ts β CLI WebSocket connection handler
β βββ buffered-websocket.ts β early message buffering proxy
β βββ git-info-tracker.ts β git branch/repo resolution (~110L)
β βββ message-queue-handler.ts β queued message drain logic
β βββ async-message-queue.ts β async message queue implementation
β βββ simple-session-registry.ts β in-memory session registry
β
βββ slash/ β Slash command subsystem
β βββ slash-command-service.ts β one execute() entrypoint (~70L)
β βββ slash-command-chain.ts β chain-of-responsibility strategies (~394L)
β βββ slash-command-executor.ts β strategy execution (~104L)
β βββ slash-command-registry.ts β command registration (~176L)
β
βββ team/ β Team/multi-agent state
β βββ team-state-reducer.ts β pure reducer for team state (~272L)
β βββ team-tool-correlation.ts β tool result β team member pairing (~92L)
β βββ team-tool-recognizer.ts β recognizes team tool patterns (~138L)
β βββ team-event-differ.ts β team state diff β domain events (~104L)
β
βββ types/ β Core type definitions
βββ unified-message.ts β UnifiedMessage envelope (~363L)
βββ core-session-state.ts β CoreSessionState base type
βββ team-types.ts β Team member/task types
βββ sequenced-message.ts β Sequence-numbered message wrapper
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β RUNTIME CONTRACTS β
β β
β SessionData β readonly immutable session state β
β SessionHandles β mutable runtime references β
β SessionEvent β BACKEND_MESSAGE | INBOUND_COMMAND | SIGNAL β
β Effect β BROADCAST | BROADCAST_TO_PARTICIPANTS | β
β BROADCAST_SESSION_UPDATE | EMIT_EVENT | β
β AUTO_SEND_QUEUED β
β SessionServices β broadcaster, connector, storage, tracer... β
β β
β BackendAdapter β connect(options): Promise<BackendSession> β
β BackendSession β send(), messages (AsyncIterable), close() β
β SessionStorage β save(), saveSync(), flush?(), load(), ... β
β Authenticator β authenticate(context) β
β Logger β debug(), info(), warn(), error() β
β ProcessManager β spawn(), kill(), isAlive() β
β RateLimiter β check() β
β CircuitBreaker β attempt(), recordSuccess/Failure() β
β MetricsCollector β recordTurn(), recordToolUse() β
β WebSocketServerLike β listen(), close() β
β WebSocketLike β send(), close(), on() β
β GitInfoResolver β resolveGitInfo(cwd) β
β DomainEventBus β emit(event), on(type, handler): Disposable β
β SessionRepository β persist(data), remove(id), restoreAll() β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
---
## Violations to Core Design Principles
### Tier 1: Accepted Pragmatic Choices β Handle Mutations
`SessionHandles` is explicitly designed as mutable runtime state outside the reducer. These are non-serializable references (timers, WebSocket maps, registries, counters) that cannot be expressed as pure `SessionData`. The architecture has a two-tier model:
- **SessionData** (immutable, serializable): All changes through reducer
- **SessionHandles** (mutable, runtime refs): Managed by runtime in post-reducer orchestration hooks
| # | Principle | Violation | Rationale |
|---|-----------|-----------|-----------|
| 1 | P1 β Only `process()` changes state | `touchActivity()` mutates `this.session.lastActivity` directly. | `lastActivity` is a handle field (non-serializable timestamp), not `SessionData`. Adding a signal would add overhead for every message with no testability benefit. |
| 2 | P1 β Only `process()` changes state | `setPendingInitialize()` mutates `pendingInitialize` handle (timer + requestId). | Timer handles are non-serializable. Now set only in the runtime's `CAPABILITIES_INIT_REQUESTED` post-reducer hook β the correct place for non-serializable handle mutations. |
| 3 | P1 β Only `process()` changes state | `allocateAnonymousIdentityIndex()` increments `anonymousCounter`. Called by `ConsumerGateway`. | Ephemeral counter β not persisted, not part of business logic. |
| 4 | P1 β Only `process()` changes state | `closeAllConsumers()` clears `consumerSockets` and `consumerRateLimiters` without individual `CONSUMER_DISCONNECTED` signals. | Teardown path β session is being deleted. Emitting disconnect signals during teardown would cause cascading effects on a dying session. |
| 5 | P1 β Only `process()` changes state | `registerCLICommands()`, `registerSlashCommandNames()`, `registerSkillCommands()`, `clearDynamicSlashRegistry()` mutate the slash registry handle directly. | Registry is a non-serializable handle. Called from post-reducer orchestration hooks (`orchestrateSessionInit`, `CAPABILITIES_APPLIED`). |
| 6 | P1 β Only `process()` changes state | `shiftPendingPassthrough()` destructively removes entries from `pendingPassthroughs` array. | Request tracking array β non-serializable, used by `BackendConnector` during passthrough interception. |
| 7 | P1 β Only `process()` changes state | `checkRateLimit()` lazily creates and inserts rate limiters into `consumerRateLimiters`. | Rate limiter objects are non-serializable. Lazy creation is simpler than pre-allocating in a signal handler. |
| 8 | P1, P3 β Only `process()` changes state; effects are descriptions | `closeBackendConnection()` aborts `backendAbort`, calls `backendSession.close()`, then dispatches `BACKEND_DISCONNECTED` via `process()`. | Teardown I/O on non-serializable handles. The self-dispatch to `process()` ensures the state transition is properly recorded. |
| 9 | P1 β Only `process()` changes state | `hydrateSlashRegistryFromState()` hydrates the slash registry during initialization, bypassing the reducer. | Registry is a handle. Called once during session restore. Subsumed by #5. |
### Tier 2: Accepted Pragmatic Choices β Other
| # | Principle | Violation | Rationale |
|---|-----------|-----------|-----------|
| 10 | P2 β State transitions are pure | `orchestrateSessionInit()` performs inline I/O: `gitResolver.resolve()` (subprocess), registry mutations, `capabilitiesPolicy` send. | Post-reducer orchestration hook. Git resolution, registry hydration, and capabilities are all handle-level operations that cannot be expressed as pure state. |
| 11 | P3 β Effects are descriptions, not inline I/O | `trySendRawToBackend()` performs direct backend I/O from a runtime method. | Called exclusively from the `CAPABILITIES_INIT_REQUESTED` post-reducer hook β runtime-internal I/O in the correct orchestration layer. Not a violation of the reducer-effect pipeline. |
| 12 | P5 β Transport modules never trigger business side effects | `BackendConnector.annotateSlashTrace()` mutates `UnifiedMessage.metadata` in-place before routing to runtime. | Trace metadata enrichment in the transport layer. The mutation happens before the message enters the reducer, so it doesn't affect state consistency. |
| 13 | P8 β Session-scoped events flow from runtime | `SessionCoordinator.renameSession()` emits `session:renamed` directly to `_bridgeEmitter`. | Consistent with coordinator emitting other global lifecycle events (`session:created`, `session:closed`). Low impact. |
| 14 | P4 β Zero manual persistence calls | `ClaudeLauncher.persistState()` manually saves launcher state (PID/session mappings). | Launcher state is global (not session-specific), required for process tracking across restarts. Not part of the session persistence system. |
| 15 | P2 β State transitions are pure | `Date.now()` calls in the reducer (`session-reducer.ts`) for timestamps on `CAPABILITIES_APPLIED` and `user_message` echo. | Universal pragmatic choice. Injecting a clock adds complexity for zero practical benefit β timestamps don't affect control flow and don't need to be deterministic in tests. |