The dispatch runtime
Two shell scripts and systemd are the entire agent runtime. No daemon, no queue, no watchdog — starting a run is the assignment, and the close-out comment is the only liveness there is.
stable
Sourced from the fleet's own component document for dispatch-runtime. Called stable because both halves
are in git, both are bound by assertions that re-check on every pull request, and four test harnesses
exercise the parts that are easy to break silently.
dispatch.sh starts a run. task-runner.sh is the run. That is the whole runtime.
There is no daemon, no queue, no watchdog, and nothing resident between runs. Two consequences follow, and they are the reason the design is worth copying:
- Starting a run is the assignment. There is no separate step where an agent is told what to work on, so there is no state that can disagree with what is actually executing.
- Liveness is the close-out comment. The run posts on its own issue when it starts and again when it exits. Nothing else reports health, because nothing else is running to report it.
Stop these two scripts and the fleet does not degrade — it stops, because nothing else in the organization can begin a unit of work.
The one thing it deliberately is not is a scheduler. Choosing which issue and which agent belongs to the sweeper, described on Operations; a human at the command line calls in through exactly the same door. The runtime never picks work.
Three pre-flight guards, and every one of them fails open
| Guard | Refuses when | Exit | Escape hatch |
|---|---|---|---|
| Quota | the agent's own state file says its entitlement is exhausted, with a reset time still ahead | 9 | DISPATCH_IGNORE_QUOTA=1 |
| Duplicate, same agent | a unit for this issue is already active on that box | 11 | DISPATCH_ALLOW_DUPLICATE=1 |
| Fleet claim | a lock-held probe finds a live run for this issue on any agent | 14 | — |
Failing open is the decision, not an oversight. An unreadable file, an unreachable box, an unparseable JSON document and an untakeable lock all mean proceed, and say so on stderr. A false block stops the fleet, which is worse than a wasted run.
None of the three writes a comment on the issue, and they share one reason. The scheduler treats certain glyphs on an issue as terminal, so a marker posted by a refusal would permanently unqueue an issue whose only problem was transient. A guard that announces itself where the queue reads would be a guard that silently deletes work.
Why only the third guard has no window
The first two are read-then-act checks, and a read-then-act check has a gap. Measured on this fleet: 26–27 seconds pass between reading the board and launching the unit, so two dispatchers reading in the same second both pass.
A zero-entropy unit name closes that for one agent — systemd refuses a duplicate unit name in the kernel, which is a real mutex and not a convention. But a user service manager is per user, so the same unit name under two different accounts is two different names, and the kernel has no opinion about the pair.
The cross-agent case is covered by a single file lock on the shared box, held across the probe and the launch, because both dispatch paths — the scheduler running locally, and a hand dispatch arriving over SSH — execute the claim as the same unix user on the same machine. That is proven against real systemd, with a mutation control, by a committed test harness rather than by argument.
The transferable part: a name-uniqueness guarantee is only as wide as the namespace that enforces it. Check what your namespace actually is before treating a unique name as a lock.
The last-moment disposal re-check
An issue can stop being work while its run is starting. Every gate that asks about it from outside the run asks too early — the unit start, the self-update, the clone sync and the credential decrypts all come after that read. The fix asks the same two questions from inside the run, immediately before the started marker. The real case that produced it, with timestamps, is on Architecture.
There is no backend selection
The launcher passes a literal backend name into the transient unit. There is no lookup, no registry read, and no per-agent resolution: a function that once chose between providers was deleted along with the alternate provider it chose between.
The environment variable survives for a different reason — it is the fleet's "am I inside a dispatched run" signal, which is what every board-keyed gate exempts on. Worth noting as a pattern: a flag whose original job was deleted can be worth keeping for the second job it accidentally does, provided you write down which job it is now doing.
The run updates itself, then executes itself, exactly once
task-runner.sh fast-forwards its own repository and then re-executes itself one time before doing
anything else.
Never mutate a script a running bash is still reading. Bash reads a script incrementally, by byte offset; rewriting it underneath a live interpreter resumes execution at an offset that now points into different text. Re-executing once, at a known safe point, is the whole fix.
Read the live state, do not recall it
Every fact about a run in flight is off-git and changes by the minute, so this page states the command instead of the answer.
| You want | Run |
|---|---|
| Which agents are dark right now | _infra/scripts/dispatch.sh --quota for a table, --quota-dark for names |
| Would this dispatch be refused? | --preflight-quota <agent> (0 or 9) · --preflight-duplicate <agent> <issue#> (0 or 11) |
| What is live for one issue | _infra/scripts/dispatch.sh --live-units <agent> <issue#> |
| Every run on this box, now | systemctl --user list-units --state=active 'task-*' |
| What one run actually did | journalctl --user -u task-<issue>.service -o cat |
A run leaves nothing resident between invocations, so there is no daemon to check. The transient unit while it lives, and the started and finished comments on the issue afterwards, are the only liveness the runtime has.
What this runtime does not prove
Published rather than smoothed over, because each one has cost this fleet a real run.
- A clean exit code is not a shipped artifact. The finished marker says the process exited cleanly and nothing more. The completion signal is a merged pull request whose body closes the issue.
- An armed auto-merge is not a merge. A run that exits with its pull request still open has not finished. Fifteen of them stacked up unnoticed once on exactly that confusion.
- The pre-flight clone sync stashes another agent's uncommitted work. It hard-resets each clone to the remote with a stash in front of it, so an interactive session's edits survive only as a stash entry nobody is told about. Check the stash list before re-doing work that vanished mid-run.
- Transient units are outside the one registry check that actually executes. Every run's unit is created on the fly and collected on exit, so there is no installed unit file for the daily verifier to prove. The two committed test harnesses are the substitute, and they run on pull requests rather than on a timer.
Take it further
2 · Audit your own runner against these four properties
Finds whatever launches your background jobs, checks it for the four properties this runtime has, and reports what is missing. Reads only; writes nothing.
You are going to audit how background or long-running jobs get started in this repository, and compare it against a documented runtime that does the same job in two shell scripts.
Do not create, edit or delete any file. This is read-and-report from start to finish. If you cannot answer something by reading, say so rather than running anything that mutates state.
## Step 1 — read the reference
curl -s https://docs.utopiamodels.ai/docs/operating/dispatch-runtime.md
That is the runtime being compared against. If curl is not available or the fetch fails, use whatever web-fetch tool you have and tell me which one worked. Everything below depends on having read it.
## Step 2 — find my launcher
Work out what actually starts a long-running job here. Look for all of these before concluding, because most repositories have more than one and they usually disagree:
- systemd units or timers under the repo, or referenced from it
- cron entries referenced in scripts or documentation
- CI workflows that dispatch other CI workflows
- a script whose name contains run, start, dispatch, worker, job, queue or launch
- a process supervisor config: supervisord, pm2, docker compose with restart policies
- an application-level queue: celery, sidekiq, bullmq, a database table polled by a worker
Report what you found as a list, each with its path, and say which one is the real entry point if there is one. If there are several with no shared entry point, say that — it is the finding, not a failure to find.
## Step 3 — check the four properties
For each launcher you found, answer these four with evidence from the code, not from convention:
1. **Is the assignment separate from the start?** Does something write "job X is assigned to worker Y" somewhere before the worker begins? If yes, quote where that state lives, and say what happens if it disagrees with what is running.
2. **Is liveness reported, or derived?** Does a worker write a heartbeat about itself, or is its aliveness inferred from observable output? Quote the mechanism. A heartbeat a process writes about itself is a claim; find out which one this is.
3. **Can the same job start twice?** Find the guard, if there is one. Then find its window: how much time passes between the check and the point of no return, and is the check enforced by something that can actually refuse — a unique name, a lock, a database constraint — or only by a read? If the guard is a unique name, say what namespace enforces uniqueness, and whether two callers could be in different namespaces.
4. **Does a guard fail open or closed?** For each guard, what happens when the thing it reads is missing, unreadable or malformed? Quote the code path. Say which way it fails and whether that is the right way for this system.
## Step 4 — report
Write a table: property, what this repo does, what the reference fleet does, and whether the difference is a defect or a legitimate difference in substrate. Be willing to conclude that my setup is right and the reference is wrong for my case — it runs on one box with a handful of agents, and that assumption is load-bearing in several of its choices.
Then give me the single highest-value change, with the file and the shape of the edit. One change, the one you would make first.
If any of the four questions cannot be answered from the code, ask me with the `AskUserQuestion` tool rather than assuming — and include a recommended answer with each question so I can accept the default.The planning loop
Five steps from a rough note to filed, dispatchable work — and not one of them is reported. Every step is derived from an event or from the board, so a plan has no field it could lie in.
More than one harness
What has to exist on disk for one planning-and-execution process to behave identically in Claude Code, Codex and opencode. There is no directory all three read, so the answer is one file reached from two names — and a way to tell when someone has made a copy.