subagents
Work off the turn, not off the machine.
rig has three ways to put work beside your turn: a delegate for a bounded subtask, the scheduler for work that owns a clock, and the swarm for draining a board. They are not three runtimes. They are three doors onto one worker path, one store, and one priority queue.
That is the whole design argument: a worker is just another rig request. Where the model runs — a local child with two places, or a hosted endpoint that queues for you — changes nothing above it.
the three
Delegation, schedule, swarm
| axis | delegate | scheduler | swarm |
|---|---|---|---|
| the job | one bounded subtask, now | a job that fires on a clock | drain a board, continuously |
| who starts it | a tool call in the turn | the crontab (or a one-shot at) | swarm start N |
| the shape | a headless rig -p run, or one deterministic bash line as a command worker | a one-shot rig -p in the job's own cwd, fired by rig run-job <key> | N workers claiming tasks, plus reviewers taking verdicts |
| the bound | timeoutMs is the kill bound for a command worker; a model worker has no clock | the job's timeout and budget, per fire | the swarm's own context — no stall kill; budget= caps the spend |
| how it returns | a WorkerDone message at priority 70: in hand at the turn's boundary | a recorded run in the scheduler store; the dashboard has the audit | the board: the worker completes, a reviewer accepts or rejects |
| the record | a run in the scheduler store, a resumable transcript, a log | the event log is the spine; the rows a projection | the todo board is the state; notes carry findings |
| the jail | the same runner as a scheduled fire | bubblewrap, landlock, or off by name | the same jail, the same allow-list narrowing |
A delegated batch can take two rows of its own in the TUI — the count and elapsed over the most recent call of any of its workers. The swarm band carries the live counts per role. Both are display, not state:
delegate #1 returned · exit 0 · 41s · session 7d41d9c 214 tests pass; two flaky under -race, noted on t2 /swarm · start 2 swarm: added 2 agents (role worker · model local) w1 claimed t1 · w2 waits for t1 workers 2 · +3 ✓5 ✕1 · w2 t388 12s reviewer 1 · ⧗1 ✓1 ✕0
backend-agnostic
The model is a row, not a scheduler detail
Nothing in the worker path names a backend. A worker is a request, and the row says where the request goes:
hosted row
remote: true or a named provider. A worker's request is just a second request: the endpoint queues it, rig counts the cost, and a 429 or 5xx retries with bounded backoff. The room's residency read is skipped entirely.
local row
The row's baseUrl is the endpoint and its child half knows how to start it. A worker claims the model's key in the room like every other client; what is loaded is read live from the port, and a model that is not resident refuses by name, naming the holder.
The scheduler never asks which one it is. It fires rig -p; the row answers. The swarm never asks either: each worker is a one-shot run, and the board is the shared state. Swapping a local row for a hosted one changes one file, not one worker.
one slot, many workers
Places, not slots, and one counted key
Here is the part most fleets get wrong: a model is one counted key, and the row says how many places it serves at once — two with two sequences, three for an -np 3 child. The holder set is the guarded state. There is deliberately no key per slot: the child serves N interchangeable sequences, and per-place keys would invent identity the server does not have.
An instance claims the key, and when the claim is final it sends its own request to the child's port. Its completion is the release — the stream's terminal event mints the next version on the chain.
A strictly higher-priority claim may enter while the places are full: one holder beyond the places, never two, and only strictly above every holder. Entry, never eviction — nothing kills a request mid-flight.
Within one priority, the claim naming the conversation already in the model goes first. A dropped reply costs a full prompt re-read, and a slot's conversation state is the one thing the engine keeps.
maxWorkers is the delegate's hand-off cap on rig's own workers. It is not the model's places: one is how much work you start, the other is what the server can hold.
So "multiple workers on one slot" is not a special mode. It is N claims on one key, ordered by priority and arrival, released by their own completions, with the continuation rule keeping each conversation's cache warm. On a hosted row the same N requests simply queue at the endpoint instead. Either way the worker code is identical, because the worker never sees the slot.
the priority queue
The scheduler is the queue
One consumer runs the session; work arrives as closures ordered by priority first, arrival second. The fleet costs no extra machinery because the queue orders by priority, not by source: your input, a worker's return, a tool completion and a reviewer's verdict are all just producers.
90 input your line, at the boundary 70 delegate a worker's return — ahead of the turn in flight 60 write an effect, inline, in call order 50 read overlap, eight at a time 50 stream · tool 30 fleet swarm notices and claims, in the gaps of a turn 10 review the reviewer's bite, only when nothing else is queued
A delegate's return posts at 70, above the turn's own events. The work you asked for is in hand at the boundary of the turn in flight, however many turns queue behind it.
The fleet's other deliveries post at 30, below the turn. A busy session delays them and loses nothing: the stores hold the facts. Starvation is the documented property here, not a bug to chase.
The reviewer's batch posts at 10 and starts only when nothing else is queued. Labelling gets the idle time; nothing waits on the labels.
No timer. A step that waits on the world spawns and posts its completion back: the event is the wakeup. That is why a fleet of workers adds producers, not threads you have to babysit.
the board
The swarm is a board with workers
swarm start <count> [role=worker|reviewer] [model=<id>] [budget=<dollars>] puts drain workers and reviewers on the same loop, jail and return path as you. The count rides start and is capped at 16; a budget stops the controller's claims at the cap with a notice, and the spend is summed from the recorded run costs.
Each worker claims the next unblocked task, spawns a one-shot run through the delegate path, and submits for review. A reviewer takes the worker's verdict and calls accept or reject. The board's two links are the whole gate:
I wait for that task. claim takes the next task nothing waits for; a blocked claim refuses, naming the blocker.
That task waits for me. The DAG is validated at create, so a cycle never lands.
Who completes decides: a solo session lands done; a worker submits for review; the reviewer's verdict settles it. A dead claim is released through the store's reap door.
A task carries notes in order with their session and time — the worker's findings go there and in rem, not into tasks it creates.
The dashboard shows the same board, and its stop button is the same interrupt gesture: it stops every running worker and lets their returns name the interrupt as their exit.
when to use which
Pick by who owns the clock
Your turn needs the result, but not before it ends. A test sweep, a broad read, one bounded sub-question. One line costs the turn; the return heads the next one.
The clock owns it. A health check every two hours, a nightly training run, a job that must outlive the session and be auditable afterwards.
The board owns it. Many independent tasks of the same shape, with a review gate and a budget — a backlog you want drained while you review the results.
Everything else. A worker is not free: it is another request against the same model. The smallest correct thing is often the turn you are already in.