System Design
Hexagonal architecture with clean ports and adapters.
Overview
Irrlicht follows hexagonal (ports-and-adapters) architecture. Every external dependency is behind an interface. Every component is testable in isolation.
┌─────────────────────────────────────────┐
│ HTTP/WebSocket Interface │
│ GET /api/v1/sessions(/stream), /state │
└────────────────────┬────────────────────┘
│
┌────────────────────┴────────────────────┐
│ Application Layer (Services) │
│ SessionDetector · PushService │
│ OrchestratorMonitor │
└────────────────────┬────────────────────┘
│
┌────────────────────┴────────────────────┐
│ Domain Model │
│ SessionState · SessionMetrics │
│ agent.Event · orchestrator.State │
└────────────────────┬────────────────────┘
│
┌────────────────────┴────────────────────┐
│ Inbound Adapters │ Outbound Adapters│
│ Claude Code │ Filesystem │
│ Codex · Pi · Aider │ WebSocket Hub │
│ OpenCode (SQLite) │ kqueue Process │
│ Process Scanner │ mDNS · Git · Log │
│ Gas Town │ │
└──────────────────────┴───────────────────┘
Project Structure
core/
├── cmd/
│ ├── irrlichd/ # Daemon entry point
│ └── irrlicht-ls/ # CLI listing tool
├── domain/ # Pure domain types
│ ├── session/ # SessionState, SessionMetrics
│ ├── agent/ # Agent event types
│ ├── orchestrator/ # Orchestrator state
│ └── config/ # Configuration
├── ports/
│ ├── inbound/ # Watcher, OrchestratorWatcher
│ └── outbound/ # Repository, Logger, ProcessWatcher, etc.
├── adapters/
│ ├── inbound/ # Event sources
│ └── outbound/ # System integrations
├── application/services/ # Orchestration logic
└── pkg/ # Shared utilities (tailer, capacity)
Port Interfaces
Inbound Ports
type Watcher interface {
Identity() agent.Identity
Watch(ctx context.Context) error
Subscribe() <-chan agent.Event
Unsubscribe(ch <-chan agent.Event)
}
Each watcher carries the identity of the adapter that produced it
(Identity().Name — e.g. "claude-code").
The SessionDetector's merge pipeline captures
Identity() once per watcher and wraps every event with it,
so adapter identity is a property of the watcher, not the event payload.
Outbound Ports
Key interfaces that define outbound boundaries:
SessionRepository— Persist and retrieve session stateProcessWatcher— Monitor process lifecycle (kqueue)MetricsCollector— Compute session metrics from transcriptsGitResolver— Resolve project names from git repositoriesPushBroadcaster— Fan-out state changes to connected clientsLogger— Structured logging abstraction
Component Wiring
The main.go entry point wires all components together following dependency injection:
- Logger — structured JSON output
- Config — loaded from environment variables
- SessionRepository — filesystem adapter for state persistence
- GitResolver — resolves project names via git commands
- MetricsCollector — transcript tailer for session metrics
- ProcessWatcher — process exit detection behind a portable
ports.ProcessObserverseam (build-taggedprocess_{darwin,linux,other}.go): kqueue on macOS,pidfdon Linux. Adds the Linux daemon without touching any adapter. - Watchers —
cmd/irrlichd/wiring.godispatches on eachagent.Agent'sSourcevariant:FilesUnderRootadapters (Claude Code, Codex, Pi) get a shared fswatcher plus a process scanner;FilesUnderCWD(Aider) andProcessOwnedStore(OpenCode) adapters get a process scanner only. Every watcher exposesIdentity()so per-event adapter naming flows through the merge pipeline rather than being stamped at emit time. - SessionDetector — thin coordinator that delegates to:
- StateClassifier — pure functions for working/waiting/ready transitions
- MetadataEnricher — git metadata, CWD, model, and metrics resolution
- PIDManager — PID discovery with retry/backoff, process exit handling, liveness sweeps, subagent cleanup
- PushService — WebSocket fan-out to connected clients
- HTTP Server — API endpoints + embedded web UI
Data Flow
- Inbound adapter emits
agent.Event - SessionDetector receives event, creates or updates session
- MetricsCollector computes metrics from transcript
- State machine determines working / waiting / ready
- PushService broadcasts change via WebSocket
- SessionRepository persists to filesystem
Performance
| Metric | Value |
|---|---|
| Memory | <5 MB typical footprint |
| Detection latency | ~50–200 ms via FSEvents |
| Process exit | ~1 ms via kqueue |
| Concurrency | Tested up to 8 simultaneous sessions |
| Disk (state files) | <100 KB |
| Disk (logs) | <50 MB |
Safety Guarantees
- Zero configuration — works out of the box with no setup required
- Idempotent operations — safe to restart at any time
- Non-destructive — never corrupts existing configs or data
- Atomic writes — temp file + rename pattern prevents partial writes