Back to blog
director-apparchitecturesessions

Director Orchestrator & Session Lifecycle

How the Director App manages sessions and coordinates its subsystems through a mode-based finite state machine — from discovery through check-in to active broadcast direction.

·Sim RaceCenter Team·7 min read
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:

ModeCloudPollerManual SequencesDescription
stoppedDisabledDisabledNo session selected, nothing running
manualDisabledEnabledSession selected, operator triggers sequences from the UI
autoEnabledEnabled (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 manual or auto without 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 to stopped.
  • A config option autoStartOnSessionSelect can skip manual and go directly to auto when 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:

  1. none → searching: When discover() is called. The SessionManager fetches available sessions from GET /api/director/v1/sessions?centerId=X&status=ACTIVE.
  2. searching → discovered: When sessions are returned from the API.
  3. discovered → selected: When the operator picks a session from the list (or the app auto-selects).
  4. selected → checked-in: When checkinSession() successfully posts capabilities to Race Control.
  5. checked-in → selected: When wrapSession() releases the check-in.
  6. selected → discovered: When clearSession() deselects (wraps first if checked in).
  7. 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 .../checkin with X-Checkin-Id header
  • The session becomes available for other Directors

Wrapping also happens automatically when:

  • The session is cleared
  • The app is closing (will-quit handler)
  • 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 headers
  • lastSequenceId — ID of the last completed sequence (for chaining/logging)

Response Handling

StatusMeaningAction
200 OKSequence receivedNormalize to PortableSequence, invoke onSequence callback, wait for completion
204 No ContentNo sequence available right nowRespect Retry-After header or use default 5s idle interval
410 GoneSession has ended (COMPLETED or CANCELED)Stop polling, invoke onSessionEnded callback
5xxServer errorLog 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:

  1. App Launch: Orchestrator starts in stopped mode. Extensions initialize and connect to OBS, iRacing, Discord.
  2. Discover: Operator clicks "Discover Sessions." SessionManager queries Race Control, transitions none → searching → discovered.
  3. Select: Operator picks a session. SessionManager transitions discovered → selected. Orchestrator transitions stopped → manual.
  4. 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.
  5. Start Auto: Operator switches to auto mode (or autoStartOnSessionSelect does it). Orchestrator creates CloudPoller and starts the polling loop.
  6. Broadcast: CloudPoller receives sequences from the Executor tier, routes them to SequenceScheduler, which dispatches steps to extensions.
  7. Session End: Race ends, API returns 410 Gone. CloudPoller stops. Orchestrator wraps the check-in and transitions to stopped.

Every state change emits events that update the UI in real time — the operator always sees exactly what's happening.