On this page
The AI Director's understanding of a live race just got substantially sharper. This post describes the shift from the legacy raw telemetry pipeline to the new Publisher Event model — where driver rigs now send structured, pre-analysed race events instead of a firehose of raw sensor data. The change affects every part of the stack, from what runs on driver rigs to what the Executor model sees when it selects a sequence.
The Problem with Raw Telemetry
The previous architecture used a Python publisher_service.py prototype on each driver rig that read iRacing's shared memory and streamed approximately 150 telemetry variables at 5Hz to Race Control. This created several compounding problems:
| Problem | Impact |
|---|---|
| ~150 raw fields at 5Hz | 25–50 KB/s per rig, 95% of which the AI never used |
| No shared types with Director or Race Control | Separate Python codebase, no type safety |
Manual pip install per rig | No production-grade upgrade path |
| Hard-coded config files | No discovery or dashboard visibility |
| AI received only current snapshot | No sense of what happened during the race, only what is right now |
The last point is the most significant for AI quality. A raw telemetry snapshot tells you that Car #7 is in P3 with a 0.4s gap to P2. It does not tell you that Car #7 has passed three cars in the last two laps, is on fresh tyres after a short pit, and has been battling for position since lap 8. Events encode history.
The New Architecture: Director as Publisher
The fix is built into the Director app's existing director-iracing extension. Rather than a separate process, driver rigs now run the Director app with the iRacing extension in Publisher Mode — the same process that handles camera control and overlays on a media rig, now reading the telemetry variable buffer, detecting race events locally, and POSTing them to Race Control.
Two Distinct Rig Roles
The platform now has two clearly separated rig types:
| Role | Extensions Active | Director Loop | Network Path |
|---|---|---|---|
| Driver Rig | iRacing (publisher mode only) | Disabled — no sequence execution | Outbound to Race Control via internet |
| Media / Director Rig | iRacing, OBS, Discord, YouTube, etc. | Active — polls Race Control for sequences | Outbound to Race Control via internet |
Key constraint: Driver rigs are fire-and-forget. They POST events to Race Control and receive nothing back. There is no back-channel, no command delivery to driver rigs, and no LAN assumption — each rig connects independently over the internet. The AI Director on the media rig retrieves events from Race Control, not from rigs directly.
Driver Rig (publisher mode — outbound only)
┌─────────────────────────────────────────┐
iRacing Shared Memory (5Hz) ──────▶ │ TelemetryReader → EventDetector → │
│ IdentityOverride → Publisher │
└─────────────────┬───────────────────────┘
│ POST /api/telemetry/events
▼
Race Control API
(raceEvents Cosmos container)
▲
│ GET /sequences/next
┌─────────────────┴───────────────────────┐
│ Director Agent → SequenceExecutor → │
│ OBS / Discord / iRacing cameras │
└─────────────────────────────────────────┘
Media / Director Rig (Director Loop — unchanged)
The two rigs never communicate with each other. Race Control is the sole intermediary.
What Gets Read — Eight Fields Instead of 150
The iRacing extension's publisher mode reads only the 8 telemetry variables the AI pipeline actually uses:
| iRacing Variable | Type | Purpose |
|---|---|---|
CarIdxPosition | int[64] | Race position per car |
CarIdxOnPitRoad | bool[64] | Pit road detection |
CarIdxTrackSurface | int[64] | Track / pit / out-of-world surface |
CarIdxLastLapTime | float[64] | Last lap time per car |
CarIdxBestLapTime | float[64] | Best lap time per car |
CarIdxLapCompleted | int[64] | Laps completed per car |
CarIdxClassPosition | int[64] | Class position per car |
SessionFlags | bitfield | Yellow / red / green flag state |
The TelemetryFrame holding these fields is internal — it is never transmitted to Race Control. Only derived RaceEvent payloads leave the rig.
Event Detection on the Rig
The EventDetector compares consecutive TelemetryFrame snapshots against an in-memory SessionState to produce RaceEvent objects. Events are grouped into eight categories:
Lifecycle Events
| Event Type | Trigger | Broadcast Relevance |
|---|---|---|
RACE_GREEN | Race session transitions to active racing | High — activate full coverage |
RACE_CHECKERED | Race ends | High — winner/celebration coverage |
SESSION_STATE_CHANGE | Session phase changes (e.g., Race → Cooldown) | Medium |
SESSION_TYPE_CHANGE | Session type changes (Practice → Qualify → Race) | Medium |
SESSION_ENDED | Session is over | Medium — wrap up broadcast |
SESSION_LOADED | New session loaded on a rig | Low — setup/config |
IRACING_CONNECTED | Rig connected to iRacing | Low — rig online |
IRACING_DISCONNECTED | Rig lost simulator connection | High — coverage gap risk |
PUBLISHER_HELLO | Rig has come online and started publishing | Low — setup |
PUBLISHER_HEARTBEAT | Rig is alive (periodic keepalive) | Low |
PUBLISHER_GOODBYE | Rig is going offline gracefully | Medium — coverage impact |
Flag Events
| Event Type | Trigger | Broadcast Relevance |
|---|---|---|
FLAG_GREEN | Green flag — normal racing | Normal |
FLAG_YELLOW_FULL_COURSE | Full-course yellow / caution | Critical — immediate incident coverage |
FLAG_YELLOW_LOCAL | Local yellow in a sector | High — brief incident shot |
FLAG_RED | Red flag | Critical — show incident, field stopping |
FLAG_WHITE | White flag — final lap | High — build intensity on leaders |
FLAG_BLUE_DRIVER | Blue flag for a specific driver | Normal — brief shot |
FLAG_BLACK_DRIVER | Black flag penalty | Normal — show penalised driver |
FLAG_MEATBALL_DRIVER | Meatball (damage) flag | Normal–High — show damaged car |
FLAG_DEBRIS | Debris flag | Normal |
FLAG_DISQUALIFY | Disqualification | Normal |
Lap & Sector Events
| Event Type | Trigger | Broadcast Relevance |
|---|---|---|
LAP_COMPLETED | CarIdxLapCompleted increment with lap time captured | Low — lap counter and timing |
PERSONAL_BEST_LAP | Driver beat their own best lap time | Medium — stat opportunity |
SESSION_BEST_LAP | Fastest lap of the entire session set | High — cover the lap |
CLASS_BEST_LAP | Fastest lap in class set | High (for that class) |
LAP_TIME_DEGRADATION | Consistent lap time increase | Medium — anticipate pit or issue |
STINT_MILESTONE | Driver has completed a notable stint length | Medium |
STINT_BEST_LAP | Fastest lap this stint — indicator of pace | Medium |
Position & Battle Events
| Event Type | Trigger | Broadcast Relevance |
|---|---|---|
OVERTAKE | Position swap between consecutive frames, excluding pit cycles | High — immediate camera opportunity |
OVERTAKE_FOR_LEAD | Race lead changes hands | Critical — always cover |
OVERTAKE_FOR_CLASS | Class lead changes | High (for that class) |
POSITION_CHANGE | Position change not attributed to an overtake | Medium — leaderboard context |
BATTLE_ENGAGED | Gap drops below 1.0s | High — sustained narrative arc |
BATTLE_CLOSING | Gap below 2.0s and shrinking | Medium — monitor for imminent pass |
BATTLE_BROKEN | Battle resolved (gap > 2.0s) | Medium — show outcome |
LAPPED_TRAFFIC_AHEAD | Class leader approaching slower traffic | Medium |
BEING_LAPPED | Driver is being caught by leader | Low |
Pit & Strategy Events
| Event Type | Trigger | Broadcast Relevance |
|---|---|---|
PIT_ENTRY | CarIdxOnPitRoad transitions false → true | Medium — strategy story |
PIT_STOP_BEGIN | Car is in the pit box | Medium — hold Pit Lane camera |
PIT_STOP_END | Stop complete, car ready | Medium — show rejoining |
PIT_EXIT | CarIdxOnPitRoad transitions true → false | Medium — rejoins and undercut completion |
FUEL_LOW | Fuel below threshold — likely to stop soon | Medium — anticipate pit |
FUEL_LEVEL_CHANGE | Fuel adjusted (refuelling in endurance) | Low — strategy context |
OUT_LAP | Driver on an out lap after pitting | Low — fresh tyres incoming |
Incident & Safety Events
| Event Type | Trigger | Broadcast Relevance |
|---|---|---|
OFF_TRACK | Car leaves the racing surface | High — show immediately |
BACK_ON_TRACK | Car recovers onto the track | Medium |
BIG_HIT | Major contact detected | Critical — interrupt coverage |
SPIN_DETECTED | Car is spinning | High — show immediately |
STOPPED_ON_TRACK | Car has stopped mid-track | High — safety risk |
SLOW_CAR_AHEAD | Slow car creating a hazard | Medium |
INCIDENT_POINT | Driver received incident points | Medium |
TEAM_INCIDENT_POINT | Team-level incident count increased | Medium |
INCIDENT_LIMIT_WARNING | Driver approaching black flag limit | High |
Identity & Roster Events
| Event Type | Trigger | Broadcast Relevance |
|---|---|---|
DRIVER_SWAP_INITIATED | Driver change is starting | High — show handover |
DRIVER_SWAP_COMPLETED | New driver is in the car | High |
IDENTITY_RESOLVED | Driver name confirmed from session data | Low — setup |
IDENTITY_OVERRIDE_CHANGED | Display name updated | Low |
ROSTER_UPDATED | Lineup changed | Low |
Environment Events
| Event Type | Trigger | Broadcast Relevance |
|---|---|---|
WEATHER_CHANGE | Weather conditions shifting | Medium — may affect strategy |
TRACK_TEMP_DRIFT | Track temperature changing | Low — tyre context |
WIND_SHIFT | Wind conditions changed | Low |
TIME_OF_DAY_PHASE | Dawn/dusk transition | Medium — atmospheric shots |
The RaceEvent Wire Format
Every event sent to Race Control takes this shape:
interface RaceEvent {
id: string; // UUID v4 — idempotency key in Cosmos
raceSessionId: string; // Cosmos partition key
type: RaceEventType;
timestamp: number; // Unix ms
lap: number; // Leader lap at time of event
involvedCars: {
carIdx: number;
carNumber: string;
driverName: string; // Real-world booked name (resolved on-rig)
position?: number;
}[];
payload: Record<string, unknown>; // Event-specific data: gap, lapTime, etc.
ttl: number; // 7,776,000 (90 days)
}Events are batched in-memory and flushed every 2 seconds or when 20 events accumulate. Failed POSTs are retried three times with exponential backoff; events older than 30 seconds are discarded rather than retried.
Identity Resolution at the Edge
iRacing identifies drivers by CarIdx (0–63). Race Control and human operators know drivers by their booked stage name or real name. The IdentityOverride service resolves this mapping before any data leaves the rig.
At session check-in, the Director receives the rig's booked driver assignment from Race Control. This creates a carIdx → bookedDriverName map. Every RaceEvent emitted by EventDetector has its involvedCars[].driverName replaced with the booked name before it is buffered for transmission. If no override exists, the iRacing name from session YAML is used as a fallback.
The result: the raceEvents Cosmos container always contains real-world driver identities. The AI prompt never sees CarIdx numbers.
Cloud-Synthesised Events
Some race situations require correlating data from multiple rigs simultaneously — something no single rig can detect. Race Control's event synthesiser runs post-ingestion (non-blocking) and writes additional events back to the raceEvents container with source: 'cloud'.
Cloud-synthesised event types include:
| Event Type | Trigger |
|---|---|
FOCUS_VS_FOCUS_BATTLE | ≥2 publisher rigs are focused on cars with gap <1.0s across 2+ frames |
FOCUS_GROUP_ON_TRACK | Deduplicated group of cars in focus across publisher rigs |
FOCUS_GROUP_SPLIT | Previously grouped team drivers have separated |
STINT_HANDOFF_HANDOVER | DRIVER_SWAP events correlated across rigs in endurance sessions |
STINT_BATON_PASS | The stint baton (lead broadcast role) transferred to a new driver |
RIG_FAILOVER | A rig's heartbeat lapses; another rig covers the same car |
UNDERCUT_DETECTED | PIT_ENTRY timing patterns suggest an undercut attempt vs. cars ahead |
IN_LAP_DECLARED | Lap-time degradation pattern following a PIT_EXIT matches an in-lap |
SESSION_LEADER_CHANGE | Overall or class leader changes, synthesised from POSITION_CHANGE events |
These events are stored alongside rig-sourced events in the same raceEvents container. The AI pipeline sees them identically.
What the AI Executor Now Knows
Before this change, the Executor received a point-in-time AISnapshot containing the current leaderboard, session flags, and a short rolling window of position history. The snapshot was always about the present — it had no memory of what raced the way it was.
After this change, the Executor's constructSessionSnapshot() function includes a recent raceEvents timeline from the Cosmos container. When reasoning about which template to select, the model now has access to:
- OVERTAKE events from the last few laps showing which cars are gaining positions and through what mechanism (racing move vs. pit differential)
- BATTLE_STATE history showing how long a battle has been engaged and whether gaps are growing or shrinking
- PIT_ENTRY / PIT_EXIT events that explain why a car moved on the leaderboard without an on-track pass
- INCIDENT markers that indicate whether damage or off-tracks have affected car pace
- LAP_COMPLETE timing trends showing whether a car is on a hot stint or managing a degrading tyre
This shifts the Executor from "what is true right now?" to "what story has been building over the last few laps?"
Implications for Template Selection
When the Executor evaluates available templates, it should consider:
- A
broadcast.showLiveCamfor a battle is most compelling whenBATTLE_STATEshowsENGAGEDstatus that has persisted for multiple laps — the battle has a narrative history, not just a current gap - An OVERTAKE event in the last 30 seconds is a strong trigger for a replay/highlight template, not just a live camera
- Back-to-back PIT_EXIT events for cars that were in the same battle frame a potential undercut — worth an overlay or commentary sequence
- A car with no recent OVERTAKE or BATTLE_STATE events but a fast LAP_COMPLETE trend may be building for a late charge — a compelling "sleeper" storyline
- Cloud-synthesised
FOCUS_VS_FOCUS_BATTLEevents indicate that two publisher rigs both decided the same battle is their focal point — strong signal for the broadcast to follow
The scan_recent_events Tool
The Executor has access to the scan_recent_events AI tool, which queries the raceEvents container directly. This allows the model to retrieve a filtered event timeline during sequence generation:
// Tool call example
scan_recent_events({
sessionId: "session-abc-123",
eventTypes: ["OVERTAKE", "BATTLE_STATE"],
sinceMs: Date.now() - 120_000, // last 2 minutes
limit: 20
})Use this tool when the current AISnapshot leaderboard alone is insufficient to explain why a car is in a certain position, or when selecting between two similarly-ranked templates where event recency would break the tie.
Practical Effect on Broadcast Quality
The transition from raw telemetry to structured events fixes a fundamental information asymmetry: before, the AI had detailed current state but almost no history. A car sitting in P3 looked identical whether it had been there all race or had just made a three-position charge from P6.
Structured events give the AI the vocabulary to distinguish these cases. A well-timed OVERTAKE followed by another BATTLE_STATE: ENGAGED three laps later is a completely different broadcast story than a P3 car that has been there, uncontested, since lap 2.
The AI Director's job is to tell that story. The publisher gives it the words.