HTTP Contract
The coordinator exposes exactly two HTTP endpoints, and a worker only ever talks to
these two. POST /register asks for the next assignment; POST /finished reports a
completed one and receives the next. Both return the same TaskResponse shape, and
both carry the whole loop's decision-making in a small, precise contract. This page
documents that contract exactly — the payloads, the config snapshot, and the rules
that make it safe across worker crashes and stale retries.
The response shape
Every response from either endpoint is a TaskResponse whose action is either
"run" or "stop".
- A run response carries
workflow_set,workflow_id,session_id,iteration, and aconfig_snapshot. The worker loads the named workflow, renders its prompt, and executes it. - A stop response carries only a
stop_reason; the other fields arenull. The worker exits.
POST /register
A worker calls /register to receive its first assignment. The request body is
empty.
Request — the worker's process identity is required (a breaking change in 0.3; pre-0.3 workers are rejected with HTTP 400):
{
"worker": { "hostname": "buildbox", "pid": 4242, "starttime": "lstart:..." }
}The coordinator stamps the identity onto the dispatched task, so a later
/register can verify whether that worker is still alive before reclaiming its
task, and a stale /finished is only ever replayed to the task's recorded owner.
The same worker field rides on /finished (the caller runs the next dispatched
task); starttime is null when the worker's team-harness predates process
identity, and verification then degrades to "unknown".
Run response:
{
"action": "run",
"workflow_set": "main",
"workflow_id": "planner",
"session_id": "20260419_143022_71393ee22450_ab12cd34",
"iteration": 3,
"config_snapshot": {
"goal": "Ship a minimal working landing page",
"goal_hash": "71393ee22450",
"workflow_set": "main",
"completion_criteria": ["Homepage renders without errors"],
"stop_criteria": ["A workflow updates session control.json to stopped"],
"max_turns": 20,
"goal_check_consecutive_failures_cap": 3,
"team_harness_provider": "openai_compat",
"team_harness_model": "gpt-5.5",
"team_harness_agents": ["codex"],
"team_harness_agent_models": {"codex": "gpt-5.5"},
"team_harness_agent_reasoning_efforts": {"codex": "high"},
"team_harness_max_retries": null,
"team_harness_retry_base_delay_s": null,
"team_harness_retry_max_delay_s": null,
"team_harness_api_base": "https://openrouter.ai/api/v1",
"team_harness_api_key_env": "OPENROUTER_API_KEY",
"team_harness_system_prompt_extension": ""
},
"stop_reason": null
}Stop response:
{
"action": "stop",
"stop_reason": "goal_met",
"workflow_set": null,
"workflow_id": null,
"session_id": null,
"iteration": null,
"config_snapshot": null
}Rules for /register:
config_snapshot.goalis the resolved goal text, loaded from thegoal_filenamed inloopy_loop_config.yaml. Workers and team-harness never receive the goal file path as the goal.workflow_settells the worker which.loopy_loop/workflow_sets/<workflow_set>/workflows/<workflow_id>/directory to load.- If a task is already current — meaning the previous worker crashed without calling
/finished—/registerfirst verifies whether the recorded worker is actually dead: a verifiably-alive worker (same host, matching pid + start-time token, and not a zombie) gets HTTP 409 instead of having its task reclaimed. If that worker is hung, kill its process (the 409 names the pid) and register again. For a confirmed-dead or unverifiable worker, the coordinator checks that iteration directory forpending_finished_request.jsonorresult.json. If either proves the task completed, the coordinator records the completed task in history before checking stop conditions. A task with no recoverable local completion has its orphaned agent processes drained or reaped perrecovery_policy(asalvage.jsonrecords what was handled) and is recorded as failed witherror="abandoned_after_<policy>"(or plain"abandoned"when nothing settled). If any orphan may still be running after recovery,/registerreturns HTTP 409 rather than dispatching a second writer. See Crash recovery. - If the loop is already in a terminal state,
/registerimmediately returnsaction: "stop".
The config snapshot
The config_snapshot is the coordinator's resolved, immutable view of the run's
configuration, sent with every run response so the worker never has to re-read the
repo config. Its most notable fields:
goal— the resolved goal text (never a file path).goal_hash— the deterministic hash of the goal, also embedded in the session id.max_turnsandgoal_check_consecutive_failures_cap— the loop's stop limits.- The
team_harness_*fields — provider, model, agents, per-agent model and reasoning overrides, retry controls, API base, the name of the API key env var, and any system prompt extension.
For where these come from and how they are validated, see Concepts and Configuration.
POST /finished
A worker calls /finished to report a completed assignment. The response is the
same TaskResponse shape as /register, so the worker learns its next task (or to
stop) in the same round trip.
Request:
{
"workflow_id": "planner",
"session_id": "20260419_143022_71393ee22450_ab12cd34",
"iteration": 3,
"success": true,
"text": "done",
"error": null
}Response: same shape as the /register response (action is "run" or "stop").
Rules for /finished:
- Match required. The report must match the current task on
session_id,workflow_id, anditeration. Only then does the coordinator record the result, read the session artifacts, and dispatch the next task. control.jsonandgoal_check.json. After a matched finish, the coordinator readscontrol.jsononly from the session directory, and readsgoal_check.jsononly from the current iteration directory when the workflow isgoal_checkor hasemits_goal_check: true. A validgoal_check.jsonis an eval artifact, not a stop switch — workflows stop the loop by updatingcontrol.json. See Success & Control.- Child sessions. A parent workflow can request a depth-first child loop by
writing a
schema_version: 1JSON request withworkflow_setandgoalunder the session'schild_requests/directory. The next response dispatches the child workflow set; after the child stops, the coordinator resumes the parent. See Child Sessions.
Crash recovery and stale retries
The contract is designed so that a killed worker, a duplicate retry, or a lost acknowledgment never corrupts loop state.
Stale finish (mismatch). If a /finished call's session_id +
workflow_id + iteration does not match the current task, the call is stale. The
coordinator does not mutate state and does not change the current task; it
returns the current task's run response so the caller sees what is actually running.
Attempt ids. Every dispatched task carries a unique attempt_id, echoed by
the worker on /finished. A call from a superseded attempt — same
session/workflow/iteration, different attempt — is treated as stale, so a late
result from work that was already recovered or abandoned can never be recorded
as the current iteration's outcome.
Session-stack recovery on --resume. The coordinator walks the durable
parent→child pointers to the deepest live session: a running child continues
where it was (previously a restart silently reopened the parent and orphaned
the child), a terminal child is finalized and its parent resumed, and every
interrupted-dispatch crash window reconciles deterministically.
Finish with no active task. If /finished arrives when no task is current, the
coordinator dispatches the next available task as if /register had been called —
or returns action: "stop" if the state is terminal. This makes a post-crash retry
safe.
Worker crash mid-handoff. A worker writes result.json, then
pending_finished_request.json, then calls /finished. If it dies inside that
window, the next /register recovers the completed result from
pending_finished_request.json (or reconstructs it from result.json if the
pending file is missing) and records the task as completed — rather than marking it
abandoned. A locally written result is treated as authoritative. The per-iteration
files behind this are described in Session Layout.
Worker crash mid-run. With nothing recoverable, /register applies the
configured recovery policy to any agent processes the dead worker's harness run
left behind — by default bounded drain: let in-flight agents finish (their
completed repo edits survive in the working tree; git is the source of truth),
then re-run the iteration from that better starting point. One
recovery_drain_timeout_s deadline is shared across all of the iteration's
interrupted runs. A salvage.json in the interrupted iteration directory records
what was drained or reaped, so surviving edits are auditable rather than a mystery
diff. The iteration's own result is never fabricated. Recovery runs outside the
state lock (loopy status/stop stay usable), but /register itself can block
roughly up to the drain deadline plus kill grace periods — the bundled worker uses
an unbounded read timeout on /register only. Process recovery is same-host-only:
a worker identity from another hostname skips reaping. If any orphan may still be
running afterwards (unverifiable identity, probe failure, or a kill that did not
land), the coordinator refuses to dispatch replacement work (HTTP 409) and the
salvage record names the unresolved processes. This requires a team-harness version
with the process reaper — older versions skip orphan recovery and fall back to
plain abandonment. The recovery settings are coordinator-side only; they are not
part of the wire config_snapshot.