Architecture
Orca is a self-hosted AI agent orchestration daemon. It manages a queue of tasks, spawns AI coding agents (Claude Code, OpenCode, Codex) in isolated tmux sessions, and monitors their progress. A Next.js dashboard (web/) drives everything over the HTTP API. Daemon code is plain TypeScript (Hono + better-sqlite3), no framework magic.
Core runtime
The daemon starts a set of independent timer loops:
| Loop | Interval | Purpose |
|---|---|---|
| Overseer (engine tick) | 90 s | Tick active missions: pick ready tasks, spawn agents |
| Scheduler | 30 s | Launch due scheduled/autostart tasks |
| Janitor | 60 s | Kill zombie tmux sessions whose task is already closed/cancelled |
| Stuck detector | 60 s | Revert tasks whose agent died without orca close (bounded), escalate after 2 relaunch attempts |
| Deriver | 5 s | Poll tmux panes, detect agent state, auto-approve known prompts |
| Overseer watchdog | 60 s | Re-park missing overseer agents for active/stalled missions (crash recovery) |
| Token purge | 1 h | Delete expired auth tokens |
| Event purge | 1 h | Drop events rows past the 30-day retention window |
| Ticket sweep | 60 s | Sweep expired terminal-WS single-use tickets |
| PR feedback | 60 s | Poll open PRs for fresh actionable review feedback, re-engage mission with fix phases |
Token purge and Event purge also run once on startup, then every hour.
Startup reconcile
On boot the daemon runs two one-shot recovery passes before the loops start:
- Zombie reconcile — tasks left
in_progresswhose tmux session is gone are reverted toopenso they can be picked up again. No grace or relaunch counter: a restart isn't an agent death, so it shouldn't spend the budget. - Overseer reconcile — when an agent overseer is configured, re-park one per active mission (their tmux sessions died with the daemon) and kill orphan overseer sessions whose mission is no longer active.
VAPID keypair generation
On every boot, ensureVapidKeys(config) checks for an existing web-push VAPID keypair in the config store. On first boot it generates one via webpush.generateVAPIDKeys() and persists the public + private keys. The public key is exposed at GET /push/vapid-public-key; the private key never leaves the daemon.
Event-bus subscribers
Two EventBus subscribers are wired at boot:
- PushDispatcher — maps lifecycle events to web-push notifications for the mission's owner + admins.
- UsageRecorder — snapshots each task's token/cost usage into
task_usagethe moment it settles.
Request / spawn flow
The agent works in the tmux pane, then calls node <cli> close <taskId> … back to the daemon (PATCH/close path) to mark its task done.
Data flow
Modules
src/daemon/ — Entry point
index.ts— starts the HTTP server on port 4400.bootstrap.ts— DI container: opens the DB, instantiates services, wires them together, starts timer loops.uniqueName.ts— generates agent session names from a curated list (Nova, Atlas, Iris, …), cycles with a numeric suffix on wrap.
src/api/ — REST API (Hono)
server.ts— all route definitions in one file (tasks, missions, sessions, projects, users, auth, config, integrations, file editor, git surface, planner, plan jobs, overseer decision routes, web push, usage stats, system info, advisor, MCP, activity log, events SSE).sse.ts—EventBusfor real-time SSE notifications.auth.ts— bearer token middleware (also accepts?token=for SSE)./ws/terminalis public here — the terminal-WS ticket is its capability.
src/terminal/ — Real-PTY terminal streaming
Streams a true PTY (tmux attach via node-pty) over a WebSocket to the browser's xterm, for the assistant dock, enlarged modal terminals, and the pop-out terminal window. A short-lived single-use ticket is the capability — minted by the authenticated POST /sessions/:name/ws-ticket and shared with the daemon's /ws/terminal handler.
src/advisor/ & src/mcp/ — Per-user Assistant
The assistant is a persistent, per-user agent session (orca-advisor-<userId>) that drives Orca with a full-scope token. The module is opt-in — when the daemon's DB is :memory: (tests), AdvisorService is not instantiated and the /advisor/* routes degrade gracefully. The MCP server (/mcp endpoint) is stateless: each request gets a fresh McpServer bound to the caller's bearer token, so every connection acts with exactly its user's rights.
src/overseer/ — Orchestration logic
missionEngine.ts— tick loop, session counting, task-to-agent dispatch, engage/pause/resume/disengage, stalled detection.routing.ts— mapsexec:<spec>labels to agent programs viaresolveExecutor().scheduler.ts— launches due scheduled tasks (30s poll).janitor.ts— sweeps zombie sessions whose task is closed/cancelled (60s).stuckDetector.ts— reverts tasks whose agent died without closing; bounded relaunch; escalates toblockedaftermaxRelaunch(60s).decision.ts— LLM-based prompt and task approval; centralisedgateVerdict()applies theMIN_CONFIDENCEthreshold; broadened destructive heuristic.decisionQueue.ts— per-mission FIFO: engine/deriver enqueue decisions, the parked overseer polls and resolves; timeout escalates conservatively.overseerAgent.ts— lifecycle of the parked per-mission overseer agent.pilotAgent.ts— spawns the planning agent for agent-mode plan jobs.planner.ts— AI goal decomposition forPOST /tasks/plan(relay backend).planJob.ts— async planning job registry (shared state between relay and agent backends).llmParse.ts— sharedextractJson()for robust LLM JSON output extraction.sessionInfo.ts—classifySession()maps everyorca-*session to structuredSessionInfo.
src/spawn/ & src/tmux/
spawn.ts—SpawnServicecreates tmux sessions with agent commands; nudges Enter at 4s/8s/13s for OpenCode's TUI.commandBuilder.ts— builds the shell command per agent type (claude-code, opencode TUI, codex).driver.ts—RealTmuxDriverwrapping tmux CLI commands (spawn, sendKeys, capturePane, capturePaneAnsi, list, kill, resize).fakeDriver.ts—FakeTmuxDriverfor tests (in-memory session simulation).
src/deriver/
deriver.ts— polls tmux panes every 5s, interprets output withshellPatterns.ts, detects state (working, needs_input, complete), auto-approves known prompts for L1–L3 missions (L1 uses 0.85 vs 0.6).shellPatterns.ts— regex patterns for detecting permission prompts per program.
src/store/ — Data layer
SQLite with WAL mode (better-sqlite3). Tables: projects, tasks (tree via parent_id), task_deps, agents, missions, settings, users, auth_tokens, events, task_usage, user_push_subscriptions, mission_pr, user_projects.
src/inference/, src/git/, src/integrations/
inference/client.ts—RelayClientfor OpenAI-compatible APIs,FakeInferencefor tests.integrations/hermesInstall.ts— registers orca as an MCP server in a same-host Hermes instance.integrations/projectFiles.ts— safe file tree, read, write, and diff operations for the Monaco editor.integrations/usage/— reads token/cost usage from each executor CLI's local session storage (portable, no relay).integrations/git/worktree.ts— mission worktree management for PR-native workflow.integrations/github/— GitHub authentication detection + PR lifecycle helpers.
src/cli/ & src/shared/
CLI commands: ls, ready, sessions, close, plan submit, overseer poll, overseer decide, plus orca api <METHOD> <path> [jsonBody] and lifecycle commands. src/shared/apiClient.ts holds callOrcaApi() — the single HTTP-forward core shared by the CLI and every MCP tool, so there is no duplicated request logic. src/shared/execs.ts is the single source of truth for executor metadata (program prefixes, known execs, default bins, well-formedness rules).
src/push/
vapid.ts— keypair generation on first boot.pushSender.ts— delivers web-push payloads; prunes dead endpoints (404/410).pushDispatcher.ts— singleEventBussubscriber mapping lifecycle events to push payloads.recipients.ts— resolves the mission owner + admins; deduped.messages.ts— payload builders with Czech user-facing text and inline action buttons.
src/prompts/
render(name, vars) loads .md templates and substitutes {{placeholder}} variables; rawTemplate() for the editable planner default. Templates live in the repo-root prompts/ directory (copied to dist/prompts/ on build) and cache until _resetPromptCache(). See the Concepts page for the full template table.
Autonomy levels
| Level | Name | Auto-spawn | Prompt gate | Confidence |
|---|---|---|---|---|
| L0 | Recommend | Never | Always escalate to human | — |
| L1 | Assist | Yes | Overseer gate (stricter) | 0.85 |
| L2 | Pilot | Yes | Overseer gate (standard) | 0.6 |
| L3 | Auto | Yes | Overseer gate (standard) | 0.6 |
Autopilot modes
POST /tasks/plan supports two backends:
- Relay backend (default) — the planner LLM (
config.autopilot.model) decomposes the goal viaRelayClient. Requires an API key. - Agent backend — when
config.autopilot.pilotExecis set, spawns a Pilot agent in the repo. The Pilot reads the codebase and submits its plan viaorca plan submit. No API key needed for planning.
A parked Overseer agent (config.autopilot.overseerExec) can replace the relay-based decision backend: one long-polling agent per active mission answers task/prompt/review decisions through the decisionQueue.
Access control / multi-tenancy
Three token scopes govern what an API caller may do:
| Scope | Purpose |
|---|---|
full | Interactive user session. Bounded by the user's role and project assignments. |
agent | Spawned agent (worker, overseer, pilot). Restricted to a narrow verb + path allow-list, confined to its live working set via agentProjects() — never the admin bypass. |
advisor | Per-user assistant session. Mapped to full at the guard so it has the user's own rights, but isolated from login tokens. |
With a userProjects store present (multi-user mode):
- A global middleware rejects non-admins not assigned to the daemon's project on the tasks/missions/sessions/activity/events/usage surface (403).
- Per-route
canAccessProject/accessibleProjects/resolveTarget/missionAccessiblefilter list endpoints and gate item operations. - Per-user model allow-list (
allowed_execs) restricts which exec a non-admin may use. - Admins and open/single-user mode (no
userProjects) pass everything unrestricted. - The agent scope's
agentProjects()confines access to projects with liveagent:-labelled tasks or active missions. Overseers keep access to the project of every active mission's epic. Final-phase agents retain access until their epic actually closes.
Testing
Tests use Vitest with fake implementations: FakeTmuxDriver (in-memory session simulation), FakeClock (deterministic time control), and FakeInference (predictable LLM responses). This allows full integration-style tests without real tmux or network dependencies.
- Daemon tests: ~833
it/testcases across 108 test files intests/. - Web tests: ~445 cases in 103 test files in
web/tests/(Vitest + React Testing Library).
That's the whole map. Head back to the docs overview, or browse the CLI reference.
© 2026 ORCA · MIT Licensed · View source on GitHub