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,attempt_id, 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 with its process identity to receive its first
assignment.
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,
"attempt_id": "a1b2c3d4e5f6",
"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,
"attempt_id": 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 prior assignment remains
unacknowledged —
/registerfirst checks the recorded worker's liveness. 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. When the worker is not verifiably alive, 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 processed-run reports) and is recorded as failed witherror="abandoned_after_<policy>"when something settled (or plain"abandoned"otherwise). For an identity-tracked harness run, an unsettled reaper outcome makes/registerreturn HTTP 409 rather than dispatching a second writer. A remote loopy worker cannot provide the same local proof because the coordinator cannot reach its processes. 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,
"failure_kind": null,
"usage": {
"prompt_tokens": 5210,
"completion_tokens": 902,
"turns": 3,
"turns_without_usage": 0
},
"duration_s": 187.4,
"attempt_id": "a1b2c3d4e5f6",
"worker": {
"hostname": "buildbox",
"pid": 4242,
"starttime": "lstart:..."
}
}attempt_id must echo the live task's value. worker identifies the caller
that will own the next task. failure_kind, usage, and duration_s are
optional; missing usage means unknown, never zero.
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,iteration, and the liveattempt_id. Only then does the coordinator record the result, read session artifacts, and dispatch the next task. A stale call from a different identified worker gets HTTP 409 rather than receiving the live owner's 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 + live attempt_id does not match the current task,
the call is stale. The coordinator does not mutate state or change the
current task. It returns the current task only to that task's recorded owner (or
for a pre-identity legacy task); a different identified caller gets HTTP 409.
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 task interrupted mid-run. With nothing recoverable, /register
applies the configured recovery policy to identity-tracked agent processes the
prior worker's harness run left behind — by default bounded drain: let
in-flight agents finish while their edits survive in the working tree. 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 the reaper reported, so surviving edits are auditable rather than a mystery
diff. The task's result is never fabricated: abandonment consumes a turn, after
which normal scheduling continues only if no stop condition fires. 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. For an identity-tracked harness run, an unsettled reaper outcome
refuses replacement work (HTTP 409) and is captured in salvage.json. A remote
loopy worker skips reaping and falls back to legacy abandonment; when local
worker liveness is unverifiable, team-harness recovery is still attempted if its
run data supports it. The recovery settings are coordinator-side only and are
not part of the wire config_snapshot.