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 │ WebSocket Hub │ │ Process Scanner │ kqueue Process │ │ Gas Town │ mDNS · Git · Log │ └──────────────────────┴───────────────────┘

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/           # AgentWatcher, 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 AgentWatcher interface {
    Watch(ctx context.Context) error
    Subscribe() <-chan agent.Event
    Unsubscribe(ch <-chan agent.Event)
}

Outbound Ports

Key interfaces that define outbound boundaries:

  • SessionRepository — Persist and retrieve session state
  • ProcessWatcher — Monitor process lifecycle (kqueue)
  • MetricsCollector — Compute session metrics from transcripts
  • GitResolver — Resolve project names from git repositories
  • PushBroadcaster — Fan-out state changes to connected clients
  • Logger — Structured logging abstraction

Component Wiring

The main.go entry point wires all components together following dependency injection:

  1. Logger — structured JSON output
  2. Config — loaded from environment variables
  3. SessionRepository — filesystem adapter for state persistence
  4. GitResolver — resolves project names via git commands
  5. MetricsCollector — transcript tailer for session metrics
  6. ProcessWatcher — kqueue adapter for process exit detection
  7. AgentWatchers — Claude Code + Codex + Process Lifecycle
  8. 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
  9. PushService — WebSocket fan-out to connected clients
  10. HTTP Server — API endpoints + embedded web UI

Data Flow

  1. Inbound adapter emits agent.Event
  2. SessionDetector receives event, creates or updates session
  3. MetricsCollector computes metrics from transcript
  4. State machine determines working / waiting / ready
  5. PushService broadcasts change via WebSocket
  6. 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
  • Kill switch — set IRRLICHT_DISABLED=1 to disable entirely