State Machine
Deterministic session state transitions. Files → State → Light.
Session Lifecycle
Every session follows a deterministic lifecycle from process discovery through transcript creation to eventual cleanup. The state machine never guesses — it reacts to concrete filesystem and process events.
| Event | From | To | Mechanism |
|---|---|---|---|
| User opens Claude Code | no session | ready | pgrep -x claude process scanner |
| User types first message | pre-session | real session | fsnotify CREATE on .jsonl |
| Real transcript appears | pre-session | real session takes over | Pre-session deleted, real session registered |
| User exits | any | deleted | kqueue NOTE_EXIT |
| Process killed | any | deleted | kqueue NOTE_EXIT |
| Transcript deleted | any | ready | fsnotify REMOVE |
| Daemon starts with dead PID | session file | deleted | syscall.Kill check |
State Transitions
Once a session is active, it moves between exactly three states: working, waiting, and ready. These map directly to the three light colors in the menu bar.
| Trigger | From | To |
|---|---|---|
| User sends message | ready | working |
Tool called (stop_reason=tool_use) |
working | working |
| Tool result returned | working | working |
Turn finished (end_turn) |
working | ready |
| User cancelled (ESC) | working | ready |
AskUserQuestion opened |
working | waiting |
ExitPlanMode opened |
working | waiting |
| User answers / approves | waiting | working |
Impossible Transitions
Two transitions are structurally impossible and the state machine enforces this:
| Transition | Why Impossible |
|---|---|
| ready → waiting | Cannot skip working. A blocking tool call can only appear while the agent is actively running. |
| waiting → ready via content | The agent cannot finish a turn while a blocking tool call is open. The only path from waiting to ready is process exit. |
Detection Logic
The state machine reads raw transcript events and applies two key predicates to determine transitions.
NeedsUserAttention() — triggers waiting
Evaluates to true when all of the following hold:
HasOpenToolCall = trueLastOpenToolNameis one of{AskUserQuestion, ExitPlanMode}
When this predicate fires, the session transitions to waiting. The light turns orange.
IsAgentDone() — triggers ready
Two detection paths, checked in order:
- Primary:
LastEventType == "turn_done" - Fallback:
HasOpenToolCall = falseANDLastEventTypeis one of{assistant_message, assistant_output}
When either path matches, the session transitions to ready. The light turns green.
Turn Completion Signals
Two transcript event types signal the end of an agent turn:
turn_duration— emitted at the end of each agent turn. This is the primary signal.stop_hook_summary— emitted after stop hooks run. Used as a fallback whenturn_durationis not present.
Pre-Sessions
When the daemon's process scanner (pgrep -x claude) discovers a new Claude Code process, it creates a synthetic pre-session with the ID proc-<pid>. This pre-session exists because the user has opened Claude Code but has not yet sent a message — no transcript file exists yet.
The pre-session lifecycle:
- Process scanner detects a new
claudePID not already tracked. - A synthetic session
proc-<pid>is created in ready state. - The daemon watches for a
.jsonltranscript file to appear via fsnotify. - When the real transcript appears, the pre-session is deleted and replaced by a real session keyed to the transcript path.
- If the process exits before any transcript appears, the pre-session is simply cleaned up.
Subagent Detection
Claude Code can spawn sub-agents (child tasks). Irrlicht detects parent-child relationships between sessions and exposes this through the SubagentSummary structure.
The summary tracks subagent counts by state:
type SubagentSummary struct {
Working int // subagents currently executing
Waiting int // subagents blocked on user input
Ready int // subagents that have finished their turn
}
The parent session's state display incorporates subagent status. If the parent is ready but a child subagent is working, the parent's effective state adjusts accordingly — the light reflects the most "active" state across the tree.
Orthogonal Axes
Beyond the three core states, the state machine tracks several orthogonal dimensions that do not affect the primary state transition logic but provide additional context.
CompactionState
Tracks whether the agent is compacting its context window:
| Value | Meaning |
|---|---|
not_compacting |
Normal operation. No compaction in progress. |
compacting |
Agent is actively summarizing context to fit within the window. |
post_compact |
Compaction just completed. Transient state before reverting to not_compacting. |
Adapter
Identifies which AI coding agent is running:
claude-code— Anthropic's Claude Code CLIcodex— OpenAI's Codex CLI
The adapter determines which transcript format is parsed and which process signatures are scanned.
PressureLevel
Tracks context window utilization as a pressure indicator:
| Level | Meaning |
|---|---|
safe |
Plenty of context remaining. |
caution |
Context usage is notable but not urgent. |
warning |
Context window is filling up. Compaction may occur soon. |
critical |
Context window nearly full. Compaction is imminent or underway. |
Cancellation
When the user presses ESC during an agent turn, Claude Code cancels the current operation. Irrlicht detects this through the transcript: the last tool result will have is_error set to true.
Cancellation maps directly to ready. There is no intermediate "cancelled" state. The detection logic:
LastToolResultWasError = truesignals the cancellation occurred.- The agent stops executing, so
IsAgentDone()fires. - The session transitions to ready.
This is a deliberate design choice. From the user's perspective, a cancelled turn is equivalent to a completed turn — the agent is idle and waiting for the next message. The three-state model (working / waiting / ready) remains clean and unambiguous.