Orion’s secret sauce isn’t either runtime — it’s the normalized event layer that lets the frontend treat them identically. Both engines emit native events. message-processor.mjs translates them into a unified camelCase SidecarEvent stream serialized by Rust serde to the frontend XState machine.

The flow

Canonical SidecarEvent variants

The full list, all camelCase always:
Token-level streaming text. thinking is reasoning content (Claude extended thinking, Pi thinking_delta).
Tool lifecycle. toolStarting is a precursor for some Claude SDK tools (permission about-to-run). toolComplete carries isError: boolean extracted from the user message’s tool_result block.
Terminal states. complete is normal completion. error carries failure details. cancelled is emitted when a turn is aborted mid-stream.
Token accounting. usageUpdate flows per-turn from both engines; contextTokens is initial context size at session start.
Health-check response for sidecar liveness.
Emitted when canUseTool needs user input. Frontend renders an approval prompt and replies via IPC.
Subagent lifecycle. parentToolUseId is set so the frontend can nest under the spawning tool call. See Subagents.
Bookkeeping events. userMessageTracked records that a user turn was committed. rewindResult is the response to a file-checkpoint rewind. projectContextChange fires when PARA context auto-switches.

Mapping tables

Claude SDK → SidecarEvent

SDK MessageSidecarEvent
stream_event text deltatext
assistant thinking blockthinking
assistant tool_use blocktoolStart (with toolStarting precursor)
user tool_resulttoolComplete (with isError)
system initusageUpdate (initial context tokens)
result finalcomplete + usageUpdate
permission_request IPCpermissionRequest

Pi → SidecarEvent

Pi EventSidecarEvent
text_deltatext
thinking_deltathinking
Pi tool lifecycle starttoolStart
Pi tool lifecycle completetoolComplete
usage (per-turn)usageUpdate
donecomplete
errorerror
Pi subagent startsubagentStart
Pi subagent donesubagentComplete

The Rust enum

The whole IPC contract is defined by the Rust SidecarEvent enum at src-tauri/src/sidecar/events.rs. serde’s tag = "type", rename_all = "camelCase" is what makes the JSON wire format camelCase.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum SidecarEvent {
    #[serde(rename = "toolStart")]
    ToolStart { id: String, name: String, /* ... */ },
    #[serde(rename = "toolComplete")]
    ToolComplete { id: String, is_error: bool, /* ... */ },
    // ... 18+ variants
}
camelCase is the only correct form. The Node SDK runner emits {"type": "toolStart"} (camelCase). Rust deserializes via rename_all = "camelCase". If you mistakenly emit tool_start (snake_case) from JS, serde will fail with unknown variant 'tool_start', expected one of 'text', 'thinking', 'toolStart', 'toolComplete', .... See .claude/rules/sdk/sdk-ipc-naming-convention.md. ✓ VERIFIED

How the frontend consumes

The frontend’s XState v5 streamingMachine (src/machines/streaming/machine.ts) dispatches events:
SidecarEventXState event
textCHUNK
thinkingTHINKING
toolStartTOOL_START
toolCompleteTOOL_COMPLETE
completeCOMPLETE
errorERROR
usageUpdateUSAGE_UPDATE
The activity stream rendered in the chat UI is built incrementally inside XState actions (pushToolToStream, updateToolInStream in actions/activity-stream.ts), not from raw IPC. By the time useCanvasToolInterceptor sees a tool — it’s already an activityStream[i] entry with status: 'complete'. See Frontend Surfaces for the downstream story.

Key files

Per-API-turn span construction. Both engines’ events flow through here. Session id extraction at L733. ✓ VERIFIED
Rust SidecarEvent enum. The serde contract.
Exhaustive-match arms routing each variant: broadcast (window-scoped) vs id-keyed (response-routed).
TypeScript SidecarEvent union type. Must match Rust serde output exactly.
Frontend XState v5 machine. SidecarEvent → XState event mapping happens in the Tauri event listeners on StreamingSession.

Next

Subagents

subagentStart/subagentComplete carry parentToolUseId. How does nesting actually work in each engine?

Frontend Canvas

Once SidecarEvents reach XState, how do they drive the canvas?