Agent Execution & Runner
Overlord coordinates ticket progress, but agent processes run in the host-local environment best suited to the task.
In Overlord, the Next.js backend decides what should run by writing a durable row to the execution_requests queue. A machine capable of running agents runs the Terminal Runner (ovld runner) to claim these rows and start the agent locally using the ovld launch command.
This architecture decouples coordination from execution. Spawning a terminal, managing environment variables, PATH settings, shell configurations, and credentials are all handled locally by the runner process. This means the Electron desktop app is completely optional—you can run a headless terminal runner on your workstation, in a cloud environment, or on a remote server.
Who opens the terminal?
Three layers are involved. Only the bottom layer touches the host shell.
| Layer | Component | Opens a terminal? | Role |
|---|---|---|---|
| Coordination | Overlord backend (Next.js / Supabase) | No | Decides which objective should run next, promotes draft → submitted, inserts a row in execution_requests, emits execution_requested for the UI. |
| Dispatch | ovld runner on a capable machine | Indirectly | Polls (or listens via Realtime), claims a compatible queued row, then spawns ovld launch as a child process. |
| Launch | ovld launch <agent> | Yes | Starts the assigned agent (Claude Code, Cursor, Codex, etc.) in a new terminal session using local PATH, credentials, and working directory. |
The backend never opens a shell on your machine and never spawns a local process. If no runner is running, the request stays queued until something on a capable host runs ovld runner start or ovld runner once, or a human copies the fallback ovld launch command from the UI.
How an objective reaches the runner
- Objective exists on the ticket — usually as
draft(next step) or alreadylaunching(a queued launch request; the legacysubmittedstate is treated identically). - Trigger enqueues work — auto-advance after
deliver, or a human Run /ovld protocol request-execution. Both call the sameexecution_requestsqueue. Creating a request moves the objectivedraft -> launching; a repeat Run for an objective that already has an active request re-queues that request instead of creating a duplicate. - Row is
queued— stores resolved agent, model, thinking, flags, target execution target/resource, and launch mode. The UI can show “waiting for runner.” - Runner claims —
ovld runnercallsPOST /api/protocol/claim-executionwith the shared device fingerprint from~/.ovld/device.json. Desktop and CLI now use the same canonical file; Desktop migrates the legacy~/Library/Application Support/overlord/overlord-device.jsonvalue on first read. The row moves toclaimedwith a lease. The claim payload includes the working directory resolved from project resource directories on that target. - Runner launches — the runner builds
ovld launch …arguments from the claim payload and spawns that process (stdio: inheritso output appears in the runner’s terminal). - Runner reports spawn — on child
spawn, it callscomplete-execution-launch; the row becomeslaunching(the launch process started, but no agent has attached yet). - Next agent attaches — the new agent process calls
ovld protocol attach, loads ticket context, executes the launchable objective, and only then is the matching request markedlaunched(withlaunched_session_id). Attach is the source of truth for a successful launch; aclaimed/launchingrow whose agent never attaches before its lease expires is treated as a stalled launch — it is markedfailed, cleared from the queue, and the user is notified to relaunch manually (it is not auto-relaunched).
Auto-advance and manual Run differ only at step 2 (scheduler vs. Run button / protocol). Steps 3–7 are identical.
Architecture Flow
The following diagram illustrates how triggers (like clicking "Run" or an agent delivering an objective) create execution requests, and how the Terminal Runner claims and spawns the agent locally.
ovld launch.Triggers
Overlord backend (Next.js / Supabase)
Capable machine (local or remote)
ovld runner start / onceovld launch <agent>↩ back to Overlord backend
End-to-End Sequence
The step-by-step lifecycle of an execution request, from the current agent delivering its work to the next agent attaching, is detailed below:
| # | From | to | To | Action |
|---|---|---|---|---|
| 1 | Current agent | Backend | POST /api/protocol/deliver | |
| 2 | Backend | Backend | Complete session; evaluate draft queue | |
| 3 | Backend | Backend | If auto_advance: move next objective to submitted, insert execution_request, emit execution_requested | |
| 4 | Backend | Backend | Else: emit awaiting_approval and notify user (no queue row) | |
| 5 | ovld runner | Backend | POST /api/protocol/claim-execution (fingerprint, hostname) | |
| 6 | Backend | ovld runner | Return launch params (agent, model, working directory, …) | |
| 7 | ovld runner | Next agent | Spawn: ovld launch <agent> --ticket-id <id> [options] | |
| 8 | ovld runner | Backend | POST /api/protocol/complete-execution-launch | |
| 9 | Next agent | Backend | POST /api/protocol/attach — load context and begin execution |
Execution Request Queue
All execution triggers (whether automated or manual) write to the unified execution_requests table. Each row acts as a durable lease:
- Idempotency: A partial unique index on
execution_requests(objective_id) WHERE status IN ('queued','claimed','launching')guarantees at most one active request per objective — this is what suppresses duplicate runs. Themanual_run:<objective_id>:<client_request_id>idempotency key stays non-deterministic on purpose so a terminal-state (failed/launched) row never blocks a legitimate relaunch; auto-advances useauto_advance:<objective_id>. - Leasing: When a runner claims a request, the row transitions to
claimedwith alease_expires_attimestamp. If the agent never attaches before the lease expires (the runner crashed, the launch errored, the terminal was closed, …), the backend does not silently re-claim it — that produced an annoying relaunch loop every lease window. Instead it marks the requestfailed, clears it from the queue, and emits an alert notification so the user can relaunch manually (with a Retry action).
Request Status States
| Status | Meaning |
|---|---|
queued | Waiting for a runner that matches the target device, resource, or kind. |
claimed | Leased by a device fingerprint; runner is preparing to launch. |
launching | Runner spawned the launch process, but no agent has attached yet. A stale launching/claimed row (lease expired with no attach) is marked failed and the user is notified — it is not auto-relaunched. |
launched | An agent attached and created its session; launched_session_id is recorded. Set by attach, not by the runner. |
failed | Spawning error, or a stalled launch whose lease expired before any agent attached. Reason recorded in the last_error column. |
Execution targets and working directories
Before a runner can launch an agent in the right checkout, Overlord needs an execution target (the machine) and project resource directories (paths on that target). The runner matches queued rows by fingerprint; claim-execution picks the explicit target_resource_id or the primary directory for (project, execution_target).
Working directory resolution
For each candidate row the runner resolves the working directory in priority order:
- Explicit
workingDirectoryinlaunch_params— used as-is. target_resource_idset — looks up the path fromproject_resource_directoriesand verifies it lives on the claiming target.- Fallback:
(project, target)primary — selects theis_primary = truerow for(project_id, execution_target_id). Primary is target-scoped — no user filter is applied; all users on a project share the same primary checkout per target. - No primary found — a
ticket_eventbackstop is recorded and the request is skipped (fail-closed). Overlord also validates at request time that a primary exists before inserting a queue row.
See Execution Targets & Resources for the data model, registration flow, primary semantics, target ownership, and protocol commands (get-device, list-project-resources, add-project-resource).
The Terminal Runner (ovld runner)
The Terminal Runner is a lightweight, long-running CLI process that manages local execution. It handles:
- Device Identity: Generates a unique UUID fingerprint stored in the shared canonical file
~/.ovld/device.jsonto identify the machine (linked to a canonical execution target). Desktop migrates its legacy app-support fingerprint into that file automatically. - Project Directories: Resolves working directories from registered project resources on the current execution target fingerprint.
- Queue Polling: Regularly polls the backend (or subscribes via Supabase Realtime) for compatible queued requests.
- Agent Spawning: Spawns agent sessions locally by shelling into
ovld launch.
The runner is org-agnostic: one runner process on a target claims queued work for every organization the authenticated user belongs to that has that target registered. You do not need a separate runner per organization — the server computes the intersection of the user's member orgs and the orgs sharing the target.
CLI Commands
# Start the runner, polling continuously for queued requests (default 3000ms)
ovld runner start
# Claim and execute a single queued request, then exit
ovld runner once
# Inspect the local runner device identity plus visible active queue rows
ovld runner status
# Clear one active queue row by objective id
ovld runner clear <objective-uuid>
# Clear every active queue row visible to the caller
ovld runner clear-all
Command Options
--device-fingerprint <fingerprint>: Manually override the runner's device identity (or set theOVERLORD_DEVICE_FINGERPRINTenvironment variable).--poll-interval-ms <ms>: Adjust the polling interval when running instartmode (default is3000, minimum1000).--project-id <uuid>: Restrict the runner to only claim requests belonging to a specific project.
Troubleshooting: "Why is ovld runner randomly executing?"
Usually it is not random. A previous Run action or an auto-advance objective created an active row in execution_requests, and a local ovld runner later claimed it.
Check the visible queue first:
ovld runner status
Clear one objective if you know the objective id:
ovld runner clear 8974e557-bec4-4984-b12c-be46bd63207c
Or use the protocol command directly:
ovld protocol clear-execution-requests \
--objective-id 8974e557-bec4-4984-b12c-be46bd63207c
Use clear-all only when you want to drop every active queue row visible to the current auth scope.
Manual Run vs. Auto-Advance
Both execution models utilize the same queue mechanism:
| Trigger Mode | Backend Behavior | Local Behavior |
|---|---|---|
| Auto-Advance | Current agent delivers a successful pass. If the next objective has auto_advance = true, the backend automatically enqueues an execution request. | A local running ovld runner automatically claims and spawns the next objective seamlessly. |
| Manual Run | A human clicks Run in the web UI, mobile app, or uses ovld protocol request-execution. | If a runner is active, the request is claimed and launched. If no runner is active, the UI guides the user with a copyable ovld launch fallback. |