On this page
Before the AI Director can switch a single camera, a carefully orchestrated startup sequence must complete. The Director App manages this through two complementary state machines: a SessionManager that handles the session lifecycle with Race Control, and a DirectorOrchestrator that coordinates the app's operating mode.
This post covers exactly how these pieces work, because when Race Control generates sequences, it helps to know what state the Director is in and how it got there.
The Director Orchestrator
The DirectorOrchestrator is a thin finite state machine that sits at the top of the Director's architecture. It coordinates three subsystems — SessionManager, CloudPoller, and SequenceScheduler — without doing real work itself.
Mode Model
The orchestrator operates in three modes:
| Mode | CloudPoller | Manual Sequences | Description |
|---|---|---|---|
| stopped | Disabled | Disabled | No session selected, nothing running |
| manual | Disabled | Enabled | Session selected, operator triggers sequences from the UI |
| auto | Enabled | Enabled (priority) | Full AI director mode — cloud generates sequences continuously |
Transitions follow this pattern:
┌──────────┐ select session ┌──────────┐ start auto ┌──────────┐
│ STOPPED │ ──────────────────→ │ MANUAL │ ─────────────→ │ AUTO │
└──────────┘ └──────────┘ └──────────┘
↑ clear session │ ↑ stop auto │
└─────────────────────────────────┘ └──────────────────────────┘
410 Gone
↓
┌──────────┐
│ STOPPED │
└──────────┘
Key rules:
- You cannot enter
manualorautowithout a selected session. - Clearing the session from any mode forces a transition to
stopped. - When the API returns
410 Gone(session ended), the orchestrator automatically wraps the check-in and transitions tostopped. - A config option
autoStartOnSessionSelectcan skipmanualand go directly toautowhen a session is selected.
Orchestrator State
The orchestrator exposes a unified state object that the renderer (Electron UI) consumes:
interface DirectorOrchestratorState {
mode: 'stopped' | 'manual' | 'auto';
status: 'IDLE' | 'BUSY' | 'ERROR';
sessionId: string | null;
currentSequenceId?: string | null;
totalCommands?: number;
processedCommands?: number;
lastError?: string;
// From SessionManager
checkinStatus: CheckinStatus;
checkinId?: string | null;
sessionConfig?: SessionOperationalConfig | null;
checkinWarnings?: string[];
}This state is pushed to the renderer via director:stateChanged IPC events — the UI never polls for status. The orchestrator subscribes to the SequenceScheduler's progress events to track which sequence is currently executing and how many steps have completed.
The Session Lifecycle
Before the orchestrator can do anything useful, the SessionManager must discover, select, and check in to a race session. This is a state machine in its own right.
SessionManager State Machine
type SessionState = 'none' | 'searching' | 'discovered' | 'selected' | 'checked-in';Transitions:
- none → searching: When
discover()is called. The SessionManager fetches available sessions fromGET /api/director/v1/sessions?centerId=X&status=ACTIVE. - searching → discovered: When sessions are returned from the API.
- discovered → selected: When the operator picks a session from the list (or the app auto-selects).
- selected → checked-in: When
checkinSession()successfully posts capabilities to Race Control. - checked-in → selected: When
wrapSession()releases the check-in. - selected → discovered: When
clearSession()deselects (wraps first if checked in). - any → none: When no sessions are available or an error occurs.
The SessionManager emits stateChanged events that the orchestrator listens to. When it sees selected or checked-in, it transitions the orchestrator to the appropriate mode. When it sees none or discovered, it forces the orchestrator to stopped.
Check-In: The Capability Handshake
Check-in is the critical moment where the Director tells Race Control what it can do. The request body includes:
interface SessionCheckinRequest {
directorId: string; // Stable ID for this installation
version: string; // App version (e.g., "0.1.0")
capabilities: {
intents: IntentCapability[]; // Active intent handlers with schemas
connections: Record<string, ConnectionHealth>; // Extension connection status
};
sequences?: PortableSequence[]; // Local operator sequences for Planner training
}The capabilities.intents array tells Race Control exactly which intents the Director can execute right now. Each entry includes the intent name (e.g., broadcast.showLiveCam), the extension that provides it, whether it's active, and optionally a JSON Schema describing the payload. This is how Race Control knows to include or exclude certain step types from generated sequences.
The connections map reports the health of each integration — whether OBS is connected, whether iRacing is running. Race Control uses this to emit warnings like "OBS not connected — obs.switchScene steps will be omitted."
The response includes:
interface SessionCheckinResponse {
status: 'standby';
checkinId: string; // UUID for subsequent requests
checkinTtlSeconds: number; // How long before the check-in expires (default: 120)
sessionConfig: SessionOperationalConfig; // Drivers, OBS scenes, timing config
warnings?: string[]; // Non-fatal capability warnings
}The checkinId is sent on every subsequent sequence poll (as both X-Checkin-Id header and checkinId query parameter) so Race Control can identify which Director is requesting.
Check-In TTL and Heartbeat
Check-ins have a default TTL of 120 seconds. If the Director stops polling, the check-in expires in Cosmos DB (via TTL) and the session becomes available for another Director.
There is no dedicated heartbeat endpoint. The sequence polling loop (GET .../sequences/next) serves as an implicit keepalive — Race Control resets the TTL on every successful poll.
The Director enforces a heartbeat floor rate:
Poll at
min(Retry-After, checkinTtlSeconds / 4)regardless of the Retry-After value.
With a 120-second TTL, this caps the maximum poll interval at 30 seconds — ensuring at least 4 heartbeats per TTL window. Even during long Retry-After intervals (e.g., during cautions when less camera switching is needed), the check-in stays alive.
Conflict Handling
If a Director tries to check in to a session that's already claimed, Race Control returns 409 Conflict:
{
"existingCheckin": {
"directorId": "d_inst_a1b2c3d4...",
"displayName": "Broadcast Rig #1"
}
}The operator can force-checkin with the X-Force-Checkin: true header, which displaces the existing Director.
Wrap: Releasing the Session
When the operator stops directing, the Director wraps (releases) the check-in:
- State transition:
checked-in → selected - API call:
DELETE .../checkinwithX-Checkin-Idheader - The session becomes available for other Directors
Wrapping also happens automatically when:
- The session is cleared
- The app is closing (
will-quithandler) - The operator switches sessions
Re-Check-In on Extension Changes
The orchestrator listens for extension connection events (obs.connectionStateChanged, iracing.connectionStateChanged, youtube.status). When an extension connects or disconnects while checked in, the orchestrator triggers a capability refresh via PATCH .../checkin — updating Race Control's view of what the Director can do without re-triggering the full Planner pipeline.
CloudPoller: The Sequence Request Loop
When the orchestrator enters auto mode, it creates a CloudPoller bound to the current session. The CloudPoller is a focused, single-responsibility component:
Request Loop
The poller calls GET /api/director/v1/sessions/{id}/sequences/next with these query parameters:
intents— Comma-separated list of active intent handlers (e.g.,system.wait,broadcast.showLiveCam,obs.switchScene)checkinId— Fallback for the header, in case SWA strips custom headerslastSequenceId— ID of the last completed sequence (for chaining/logging)
Response Handling
| Status | Meaning | Action |
|---|---|---|
| 200 OK | Sequence received | Normalize to PortableSequence, invoke onSequence callback, wait for completion |
| 204 No Content | No sequence available right now | Respect Retry-After header or use default 5s idle interval |
| 410 Gone | Session has ended (COMPLETED or CANCELED) | Stop polling, invoke onSessionEnded callback |
| 5xx | Server error | Log error, retry with default interval |
After receiving a 200 OK, the poller does NOT request the next sequence immediately. It waits for the onSequenceCompleted callback (fired when the SequenceScheduler finishes executing the sequence), then triggers an immediate retry. This prevents the poller from requesting new sequences while one is still executing.
The Retry-After header on 204 responses lets Race Control dynamically adjust idle poll intervals — longer during caution periods, shorter during restarts, higher when no telemetry is flowing.
Putting It All Together
A typical auto-direction session flows like this:
- App Launch: Orchestrator starts in
stoppedmode. Extensions initialize and connect to OBS, iRacing, Discord. - Discover: Operator clicks "Discover Sessions." SessionManager queries Race Control, transitions
none → searching → discovered. - Select: Operator picks a session. SessionManager transitions
discovered → selected. Orchestrator transitionsstopped → manual. - Check-In: Operator clicks "Check In." SessionManager posts capabilities to Race Control, triggering the Planner. Transitions
selected → checked-in. Orchestrator passes the checkinId to CloudPoller. - Start Auto: Operator switches to auto mode (or
autoStartOnSessionSelectdoes it). Orchestrator creates CloudPoller and starts the polling loop. - Broadcast: CloudPoller receives sequences from the Executor tier, routes them to SequenceScheduler, which dispatches steps to extensions.
- Session End: Race ends, API returns
410 Gone. CloudPoller stops. Orchestrator wraps the check-in and transitions tostopped.
Every state change emits events that update the UI in real time — the operator always sees exactly what's happening.