Skip to content
GitHub
Section: Architecture
Under the hood

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

bootstrap → open DB → instantiate stores/services → create Hono server → startup reconcile → start loops

The daemon starts a set of independent timer loops:

LoopIntervalPurpose
Overseer (engine tick)90 sTick active missions: pick ready tasks, spawn agents
Scheduler30 sLaunch due scheduled/autostart tasks
Janitor60 sKill zombie tmux sessions whose task is already closed/cancelled
Stuck detector60 sRevert tasks whose agent died without orca close (bounded), escalate after 2 relaunch attempts
Deriver5 sPoll tmux panes, detect agent state, auto-approve known prompts
Overseer watchdog60 sRe-park missing overseer agents for active/stalled missions (crash recovery)
Token purge1 hDelete expired auth tokens
Event purge1 hDrop events rows past the 30-day retention window
Ticket sweep60 sSweep expired terminal-WS single-use tickets
PR feedback60 sPoll 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:

  1. Zombie reconcile — tasks left in_progress whose tmux session is gone are reverted to open so 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.
  2. 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:

Request / spawn flow

HTTP request → api/server.ts (route handler, auth via Bearer token) → overseer/missionEngine.ts (mission tick: pick ready tasks) OR overseer/scheduler.ts (scheduled/autostart) → spawn/spawn.ts SpawnService.launch() → spawn/commandBuilder.ts buildAgentCommand() (cd + env + cli + prompt) → tmux/driver.ts spawn() → tmux new-session (session = orca-<agentName>)

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

┌───────────┐ │ Client │ │ (CLI/Web) │ └─────┬─────┘ │ HTTP/SSE ▼ ┌──────────────────┐ │ Hono Server │ │ port 4400 │ └──────┬───────────┘ │ ┌───────────────────────┼───────────────────────┐ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────────┐ ┌──────────────┐ │ TaskStore │ │ MissionEngine │ │ EventBus │ │ (CRUD) │ │ (90s tick) │ │ (SSE push) │ └──────┬───────┘ └──────┬───────────┘ └──────────────┘ │ │ │ ┌────────▼────────┐ │ │ Routing │ │ │ (resolveExecutor)│ │ └────────┬────────┘ │ │ │ ┌────────▼────────┐ │ │ SpawnService │ │ │ (tmux launch) │ │ └────────┬────────┘ │ │ │ ┌────────▼────────┐ │ │ Deriver │ │ │ (5s poll loop) │ │ └────────┬────────┘ │ │ ▼ ▼ ┌──────────────────────────────────────┐ │ SQLite (WAL) │ │ tasks / missions / agents / users │ └──────────────────────────────────────┘

Modules

src/daemon/ — Entry point

src/api/ — REST API (Hono)

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

src/spawn/ & src/tmux/

src/deriver/

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/

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/

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

LevelNameAuto-spawnPrompt gateConfidence
L0RecommendNeverAlways escalate to human
L1AssistYesOverseer gate (stricter)0.85
L2PilotYesOverseer gate (standard)0.6
L3AutoYesOverseer gate (standard)0.6

Autopilot modes

POST /tasks/plan supports two backends:

  1. Relay backend (default) — the planner LLM (config.autopilot.model) decomposes the goal via RelayClient. Requires an API key.
  2. Agent backend — when config.autopilot.pilotExec is set, spawns a Pilot agent in the repo. The Pilot reads the codebase and submits its plan via orca 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:

ScopePurpose
fullInteractive user session. Bounded by the user's role and project assignments.
agentSpawned agent (worker, overseer, pilot). Restricted to a narrow verb + path allow-list, confined to its live working set via agentProjects() — never the admin bypass.
advisorPer-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):

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.

That's the whole map. Head back to the docs overview, or browse the CLI reference.

© 2026 ORCA · MIT Licensed · View source on GitHub