Architecture
How the pieces fit.
Top-level#
Browser ──HTTPS/WS─→ Caddy ──→ Go API ──→ Postgres
└──→ Next.js standalone (dashboard)
Your machine ──WS─→ cu-daemon ──┬──→ pty.Broker (sessions WE spawn)
└──→ discover.Manager (sessions we OBSERVE)
└─ fsnotify-tails .jsonl transcriptsDaemon — pty.Broker#
When dashboard or cu attach joins a daemon-spawned session, the broker fan-outs stdout to every subscriber and routes stdin from the current controller back to the PTY master. 64 KB ring scrollback per session; controller handoff is a one-shot message.
Daemon — discover.Manager#
Polls the OS every 5s for claude / codex processes owned by the daemon user. For each new process resolves the on-disk transcript (~/.claude/projects/… or ~/.codex/sessions/…), opens an fsnotify watcher + 2s safety-net poll, and forwards every JSONL line to backend as a session_event. Offsets persisted in ~/.controlum/discover-offsets.json so daemon restarts resume cleanly.
Backend#
Go API; routes split into REST (/api/v1/*) + WebSocket (/ws/*). Sessions table is the single source of truth — both source=cu and source=external rows live here. Migration 0002_sessions_source added source + log_file_path columns and a partial unique index to prevent duplicate external rows.
Frontend#
Next.js App Router, standalone build. Dashboard uses shadcn/ui + Tailwind v4. Sessions Interactive tab uses @xterm/xterm wired to /ws/sessions/<id> (read + write for source=cu, read-only for external).