Session Detection
How Irrlicht discovers and tracks AI coding agent sessions.
Detection Methods
Irrlicht uses three complementary methods to detect and track sessions. Together they provide fast, reliable discovery from the moment a process starts until the session ends.
Filesystem Watching
- Technology: fsnotify (kqueue on macOS)
- Recursively watches transcript directories for changes
- Events:
CREATE-- new session,WRITE-- activity,REMOVE-- session ended - Filters: only
.jsonlfiles, ignores files older than max age (5 days default)
Process Scanning
- Polls once per second per adapter via
pgrep -x <name>(exact-name match) orpgrep -f <regex>(command-line match, used by aider); interval backs off when the PID set is stable - Creates pre-sessions (
proc-<pid>) before transcripts exist - Discovers CWD via
lsof -a -p <pid> -d cwd -Fn - Pre-sessions are replaced by real sessions when the transcript arrives
Process Exit Monitoring
- kqueue
EVFILT_PROCNOTE_EXITfor instant (~1ms) exit detection - Fallback: periodic liveness sweep via
syscall.Kill(pid, 0)every 5s - Startup cleanup: synchronous dead PID check in
seedFromDisk()
Supported Agents
Claude Code
- Transcript location:
~/.claude/projects/<project>/<session-id>.jsonl - Flat directory structure (one level under
projects/) - Adapter name:
claude-code
OpenAI Codex
- Transcript location:
~/.codex/sessions/YYYY/MM/DD/<session-id>.jsonl - Deep directory structure (recursive watching)
- Adapter name:
codex - Model detection from
~/.codex/config.toml
Pi Coding Agent
- Transcript location:
~/.pi/agent/sessions/--<cwd-dashed>--/<timestamp>_<uuid>.jsonl - JSONL v3 format — session header on first line with
cwd,parentSession, andversionfields - Messages use
rolefield:user,assistant,toolResult,bashExecution - Turn completion detected from
stopReason: "stop"on assistant messages - Adapter name:
pi - Model detection from
~/.pi/agent/settings.json(defaultModelfield) - Supports multiple LLM providers (Anthropic, OpenAI, Google, xAI, Groq)
Aider
- Transcript location:
<cwd>/.aider.chat.history.md(markdown, per-CWD — not JSONL) - Process discovery via
CommandLineMatch(/aider($| )) since the OS process ispython - Waiting state pinned to a trailing
?contract on the most recent assistant block - Adapter name:
aider
OpenCode
- Storage location: SQLite WAL at
~/.local/share/opencode/storage/opencode.db(watched viaopencode.db-wal— no JSONL files) - fsnotify on the WAL file triggers polling of
session/parttables; cursor uses(time_created, part_id)for dedup across identical-millisecond timestamps - Parent-child linking from the
parent_idcolumn —EventNewSessioncarriesParentSessionIDdirectly; no path-based heuristics EventRemovedemitted onsession.time_archivedso the daemon transitions toreadyimmediately rather than waiting for TTLstep-finishreasons (stop,interrupted,length,error,content-filter) all map toturn_done; cost + token snapshots come fromMetricsProvider, bypassing the JSONL tailer- Adapter name:
opencode - PID discovery via
pgrep -x opencode+ CWD match
Kiro CLI
- Transcript location:
~/.kiro/sessions/cli/<uuid>.jsonl(one file per session, live-appended) - The transcript carries no cwd; the working directory is read from the
<uuid>.jsonmetadata sidecar next to it - A text-only
AssistantMessagemaps toturn_done; one carryingtoolUseblocks keeps the turn open - PID discovery via
pgrep -x kiro-cli+ CWD match; headless--no-interactiveruns persist no session files and are invisible - Adapter name:
kiro-cli
Gemini CLI
- Transcript location:
~/.gemini/tmp/<hash>/session-<timestamp>-<hash>(JSONL; nested subagent chats atchats/<parent-uuid>/<child-uuid>.jsonl) - The cwd is read from the transcript body, not a header line
- No explicit end-of-turn marker — a text-only assistant message maps to
turn_done; benign info notices are ignored - Process discovery via command-line match on
bin/gemini(the OS process isnode) - Adapter name:
gemini-cli
Antigravity
- Transcript location:
transcript.jsonlunder~/.gemini/antigravity-cli/brain/<conversation>/.system_generated/logs/(CLI) and~/.gemini/antigravity/brain/<conversation>/.system_generated/logs/(IDE) - One adapter covers both the
agyCLI and the Antigravity IDE — they write byte-compatible transcripts to sibling brain stores - Discovery is transcript-first: the IDE hosts many conversations in one Electron process with no per-conversation PID, so PID-less sessions are first-class; the
agyCLI is a standalone native binary matched by exact name - Since the transcript filename is the constant
transcript.jsonl, the session id is derived from the conversation directory path - Tokens and the canonical model are read from the sibling conversation store
~/.gemini/antigravity{,-cli}/conversations/<conversation>.db(not the transcript) so the context bar renders - Adapter name:
antigravity
Mistral Vibe
- Transcript location:
messages.jsonlunder~/.vibe/logs/session/<session-id>/ - Since the transcript filename is the constant
messages.jsonl, the session id is derived from the parent directory name - Discovery matches the command line rather than the process name:
vibeis a Python console-script, so the OS process name is the interpreter - The transcript carries no timestamp, working directory, model, or usage — those come from the sibling
meta.jsonsidecar, so the model chip and context bar render - Adapter name:
mistral-vibe
PID Discovery
| Phase | Mechanism |
|---|---|
| Discovery | lsof -t <transcript> with retry at 500ms, 1s, 2s intervals |
| CWD fallback | If lsof fails, matches claude processes by working directory |
| Registration | kqueue EVFILT_PROC NOTE_EXIT |
| Liveness sweep | syscall.Kill(pid, 0) every 5s |
| Startup cleanup | Synchronous in seedFromDisk() |
Subagent Detection
When Claude Code spawns subagents (via the Agent tool), each subagent creates its own transcript file at:
~/.claude/projects/<project>/<parent-session-id>/subagents/<agent-id>.jsonl
The daemon detects these files through the same filesystem watcher and derives the parent-child relationship from the path structure. Each subagent becomes an independent session with its own state machine, linked to the parent via parent_session_id.
Lifecycle
- Detection: Filesystem watcher sees the new
.jsonlfile in thesubagents/directory - Tracking: Session created with
parent_session_idset; exempt from orphan cleanup (no PID of its own) - Display: Parent session shows a purple badge with the count of active subagents
- Cleanup: Child sessions are deleted when they finish (ready state), when their transcript becomes stale (>2min), or when the parent session is deleted (cascade)
File-Based Detection
Current Claude Code writes an isSidechain transcript under subagents/agent-*.jsonl for every Agent tool call — including Explore and Plan — so the filesystem watcher picks up every subagent as its own child session. An older in-process counting path (CountOpenSubagents) is kept as a seam for a future Claude Code revision that reintroduces true in-process subagents, but it currently always returns 0.
The API's subagent summary (session.ComputeSubagentSummary) merges parent and child sessions into a single count.
Session ID
- File-based (UUID filename): UUID extracted from the filename (
<uuid>.jsonl) — Claude Code, Codex, Kiro CLI - File-based (path-derived): Antigravity's transcript filename is the constant
transcript.jsonl, so its session id comes from the conversation directory path instead; Gemini CLI's filename issession-<timestamp>-<hash>, not a bare UUID - Process-based:
proc-<pid>format, used for pre-sessions before a transcript file is found
Git Metadata
- Branch:
git rev-parse --abbrev-ref HEAD(stripsworktree-prefix) - Project name: derived from
git-common-dir(works correctly with worktrees) - CWD: tail-reads the last 32KB of the transcript to find the latest working directory
Transcript Parsing
The tailer reads JSONL transcripts line by line and extracts:
- Model name -- normalized from various format variations
- Token counts -- input, output, cache read, cache creation
- Tool call state -- open/closed tracking with tool names
- Context window size -- used for utilization pressure calculation
- Cost estimation -- computed from token breakdown and model pricing tables