Build capacity
Claim, queue, hold and book shared build hosts, CI runners and Unity machines through orboto instead of asking in a message thread: resources, claims, windows, reservations, the CI step, the admin page and the metrics.
Added in v0.212.0 Build capacity coordinates the machines your agents, CI runs and people share. A build host, a CI runner or a Unity build machine is a resource with a number of slots. Whoever wants to run something heavy on it files a claim first, waits in a queue when no slot is free, and releases the slot when the run ends. Nobody asks "is the host free?" in a message thread any more, and everybody can see who holds what.
orboto coordinates; it does not enforce. It never stops a process on the machine. A claim is a promise the holder keeps, and every rule below is about who is granted the slot next.
This is separate from the team capacity of the planning pages, which compares people's hours with planned work.
The model
- Resource. A name made of lowercase segments joined by a colon, such as
build-host:runner-1,runner:mainorunity:runner-1, a slot count (1 is exclusive, N lets N runs share the machine) and an enabled switch. A disabled resource grants nothing and its claims wait. - Claim. A lease on one slot of a resource. It names its holder, a reason
everybody waiting can read, the minutes the run expects to take, and a
priority. A claim is
queued,granted,releasedorexpired. - Holder. An
agent-session, aci-runor ahuman. Without a holder the claim belongs to the agent session that filed it, and to the person when no session is involved. Aci-runnames its run id. - Priority.
release, thendevelop, thenpr, thenbatch. The queue is ordered by priority, then by arrival. - Window. A period a resource is reserved for a label, for example a nightly build between 01:00 and 04:00. A window is recurring (a five-field cron expression, a duration in minutes and a time zone) or one-off (a start and an end). While a window is open, only claims that name its label are granted, and a claim that names a label waits until a window with that label opens. A claim that names no label waits until the window closes.
- Reservation. A slot booked ahead of time, see Plan and book a slot.
How a claim moves
A claim for a free slot with no window in the way is granted at once and answers with its slot. Otherwise it is queued and the answer says the position, what it waits for (a free slot, its window, a reserved window to close, its start time, a booked reservation, or the resource to be enabled) and the holders it waits behind. A queued claim that cannot run yet keeps its place, and the claims behind it are granted meanwhile.
Every claim has a deadline. A grant lasts the minutes the claim expected; a queued claim lives 60 minutes unless it says otherwise. The holder renews it while the run lasts. A claim that is not renewed by its deadline expires, and the next claim in line is granted. Releasing is idempotent, and releasing a claim that already ended changes nothing.
Priority never ends a claim
When a queued claim of higher priority waits only for a slot, the holder with the lowest priority gets a yield request: a reason and a deadline, 15 minutes by default. The holder decides. Its claim stays granted until it releases it or it expires. An administrator can send the same request by hand for any deadline from 1 to 240 minutes.
A yield request reaches the holder as a message on the
wake channel and as an inbox notice. When
the holder is an agent session that is published as a
live session with typed input, a one-line pointer
that starts with [orboto capacity] is also typed into it. The text of the
requester inside that pointer is quoted data, not an instruction.
A queued claim that is granted, a yield request and an expiry reach the owner through the wake channel and the inbox. A grant from the queue also sends a push notification to the owner's subscribed devices.
Who may do what
Every internal user can read resources and claims, file claims, renew them and
release their own. External accounts are refused. Creating and changing
resources and windows, releasing another account's claim and sending a yield
request need the admin:capacity:write permission. Every change is written to
the audit log.
Claim from the command line
# Claim before the run: exit code 0 = granted, 4 = queued
orboto capacity claim build-host:runner-1 --for 45m --priority release --reason "0.212.0 candidate"
# Block until a queued claim is granted, with a timeout
orboto capacity wait <claim-id> --timeout 30m
# Hold capacity for exactly one command: wait, run, renew, release
orboto capacity claim build-host:runner-1 --for 45m --reason "e2e suite" --hold -- pnpm test:e2e
orboto capacity renew <claim-id> --for 30m
orboto capacity release <claim-id>claim prints the claim as JSON on stdout. The progress, which is the queue
position, what the claim waits for and the holders with their priority, reason
and deadline, goes to stderr, or to stderr as JSON objects with --json. The
exit codes to branch on:
| Code | Meaning |
|---|---|
0 | Granted, or done |
4 | Queued (a claim without --wait) |
5 | Timeout; the queued claim was released |
6 | Denied or ended: a 403, 404 or 409, or the claim expired or was released while waiting |
7 | Yielded (--on-yield release) |
130, 143 | Interrupted; the claim was released |
1, 2 | API error, usage error |
The wait loop renews a queued claim before it expires, so a waiter never loses
its place. --timeout and Ctrl-C release a queued claim, so nobody is left
waiting behind a claim nobody wants.
Hold mode (--hold -- <command...>) waits for the grant, runs the command
in your terminal, renews the claim every third of its expected time while the
command runs, and releases it when the command exits or is interrupted. The
exit code is the command's. When the holder is asked to yield, the request is
reported once; with --on-yield release the command is stopped, the claim
released and the exit code is 7. Running orboto capacity wait <claim-id> --hold -- <command...>
starts the same loop for a claim or reservation that already exists.
Other flags of claim are --priority release|develop|pr|batch, --window <label>, --not-before <time> (an ISO time or a duration such as 2h),
--queue-for <minutes>, --holder-kind, --holder-id and --holder-label.
Durations are written 45m, 1h30m or as whole minutes.
Reading is orboto capacity list [<resource>], orboto capacity claims with
--resource, --state, --active, --mine, --holder-kind and --holder-id,
and orboto capacity windows. They print JSON, or a table with --table;
orboto capacity list <resource> --table shows the holders, the queue and the
windows of one resource.
A session never leaves a claim behind
When an agent turn ends, the completion check (orboto session-check, run by
the Claude Code Stop hook) releases the granted claims of that session that
no running --hold process keeps, and lists them. Queued claims stay, so the
grant can wake the session. At the end of a session (orboto session-check --session-end and the Claude Code SessionEnd hook) every queued and granted
claim of the session is released. A running --hold process leaves a small
marker file in ~/.orboto/capacity-holds/ so the check can tell that its claim
is in use.
Plan and book a slot
# When is the next free slot for 45 minutes, in your time zone?
orboto capacity plan build-host:runner-1 --for 45m
# Book it as a reservation, then hold it for the run
orboto capacity plan build-host:runner-1 --for 45m --book --priority release --reason "0.212.0 release window"
orboto capacity wait <claim-id> --hold -- ./release.shplan prints the earliest slot that fits the duration, fitted around the
reserved windows, the holders until their expected end, the queue ahead and
every booked reservation. It looks 14 days ahead. --not-before and --window
narrow it. With --book, orboto files a reservation for that slot. Nothing
else is granted into a reserved slot: a claim whose run would reach into a
booked reservation waits for it.
The reservation is granted at its start and its holder is woken. The holder
must renew it within the grace time, 15 minutes by default and at most 240
(--grace); that first renewal is its use. A reservation that is not used in
time expires and its slot goes back to the queue. wait --hold renews it at
once and then like any claim, and releases it when the command ends. When the
slot is still held at the start of a reservation, orboto asks that holder to
yield, whatever its priority, and ends nothing.
A reservation at priority release is a release fence: it takes no slot
of its own, but keeps every claim below release off the whole resource during
its run. Book one before a release push and hold it while the release runs;
the release's own runs claim at release and use every slot.
The nightly Unity window
orboto capacity seed nightly-unity [--resource unity:build] [--timezone <zone>]
creates, once, a one-slot resource and a recurring window from 01:00 for 180
minutes labelled unity. Running it again changes nothing. It needs the
admin:capacity:write permission and belongs on the instance that runs the
Unity machine. The window keeps the time zone it was created in; change it on
the Capacity page.
CI runs hold the runner
A workflow run files a ci-run claim before its heavy jobs, waits in the queue,
renews it from every heavy job and releases it in a final job that runs on
success, failure and skip. Later runs queue instead of colliding, and the queue
with its holders shows in the log of the run that waits.
The key a workflow uses carries only the capacity:claim scope. It reaches
POST /api/capacity/claims, the reads of its own claims, and the renew and
release routes. Every other route answers 403, and the scope cannot be
combined with another one.
curl -X POST https://acme.example.com/api/capacity/claims \
-H "Authorization: Bearer $CAPACITY_KEY" -H "Content-Type: application/json" \
-d '{"resource":"build-host:runner-1","holderKind":"ci-run","holderId":"acme/app#4711",
"reason":"tests on develop, run 4711","expectedMinutes":45,"priority":"develop"}'- Priority from the trigger. A tag or release run claims at
release, a push to the main development branch atdevelop, a pull request atpr, and anything else, such as a schedule or a manual dispatch, atbatch. - Cancelled and killed runs. The run that cancels an older run of the same branch releases its claim first. A run that is killed releases nothing, so its claim expires: within 3 minutes while queued, and within 15 minutes after its last renewal once granted.
- Runner jobs. A waiting run occupies a job slot on its runner. Keep the runner's own job capacity above the heavy jobs that run at once plus the runs expected to wait.
- Failure behaviour. The capacity manager coordinates, it does not gate your CI. Pull requests from forks carry no secrets and skip the step. A queue wait of more than two hours fails the claim job.
The admin page
Admin -> Agents -> Capacity shows every resource as a card. Everyone who
reaches the admin area sees it; the write controls need admin:capacity:write.
It updates live.
- Header of a card. The name, the occupancy (2 of 3 slots in use) and the number of waiting claims. The menu edits the resource, adds a window, enables or disables it and deletes it. A resource with a granted or queued claim cannot be deleted.
- Next 24 hours. A timeline with one row per slot. Holders run to their deadline, queued claims are drawn where they would run if every holder kept its slot to the deadline, window occurrences sit on their own row, and booked reservations and release fences have their own colours.
- Holders. Slot, holder, reason, priority and expected end. An open yield request shows its deadline, turns red once it is overdue and says how it reached the holder. Ask to yield (a reason and a deadline from 1 to 240 minutes) and Release are for administrators.
- Queue. Position, priority, holder, reason, what the claim waits for, its expected minutes and its time in the queue. The first 50 are listed, followed by the count of the rest.
- Windows. Label, open or closed, schedule and when it closes or opens next. New window in the page header and Add window on a card create one. A recurring window takes a cron expression, a duration and a time zone, a one-off window a start and an end. The kind cannot be changed afterwards.
- Book a slot. Open to everyone who sees the page, like filing a claim. Find slot shows the planned slot and the wait, Book reserves it.
- Push. A claim you filed that waited and is then granted sends a push notification to your subscribed devices.
Metrics and alerts
- Per resource, for the last 24 hours and the last 7 days: claims filed,
grants, the wait from filing to grant (mean and longest, with the holder and
reason of the longest), the hold from grant to end, expirations (and how many
of them were granted claims whose holder stopped renewing), yields, overrun
alerts and the deepest queue, plus the slots used and the queue depth now. They
are on the Capacity metrics card above the resources, in
orboto capacity stats [<resource>]and inGET /api/capacity/metrics. They need theadmin:agents:readpermission. - Overrun alert. A granted claim that has held its slot for twice its expected minutes raises one alert per resource and cooldown. The cooldown is the standard 1,440 working minutes, so weekends and public holidays do not count. The holder and every capacity administrator get it in the inbox and on the wake channel. The alert never ends the claim.
- Daily digest. Capacity administrators get one line about yesterday in the daily digest: the longest waits, expirations, yields and the longest holds with their holder and reason.
- Prometheus.
orboto_capacity_queue_depth,orboto_capacity_wait_seconds,orboto_capacity_hold_seconds,orboto_capacity_claim_transitions_total,orboto_capacity_claims_filed_total,orboto_capacity_reservations_totalandorboto_capacity_overrun_alerts_total, see Monitoring.
Agent surfaces
- REST.
/api/capacity/resources,/api/capacity/windows,/api/capacity/claimswithrenew,releaseandyield,/api/capacity/resources/<resource>/plan,/api/capacity/metricsand/api/capacity/seeds/nightly-unity. Lists are paged with a cursor. - MCP. There are no named capacity tools; an agent calls the routes through
orboto_api_call, andorboto_helpwith the topiccapacityreturns every recipe. A claim filed over MCP is held by the agent's session. See MCP server setup. - In-app chat. The assistant lists who holds a resource, claims, releases and plans or books a slot; in chat the holder is the person. Resource and window administration, renewals and yield requests are not chat actions.
- Agent skill. The packaged skill carries a capacity reference with the same recipes (the agent skill).
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| A claim stays queued although the host is idle | waitingFor names the reason: a window reserved for a label the claim does not name, a booked reservation, a --not-before time, or a disabled resource. |
| 403 on a claim you did not file | Only the owner renews a claim; the owner or an administrator releases it. |
409 claim_ended on a renewal | The claim expired or was released. File a new one. |
409 resource_busy on delete | A granted or queued claim exists. Release it or let it end first. |
409 plan_no_slot | Nothing fits within 14 days; shorten the run or free a window. |
| A claim vanished after a long build | It was not renewed before its deadline. Use --hold, or renew from the job. |
Remote console
Read an agent session as a conversation, reply to it, use its slash commands, answer permission asks, see what it changed and open the pull request, from the browser or your phone.
Publish an agent session live
How runners stream an agent session to orboto through a harness adapter, how a headless login is handed to an operator, and the relay contract for your own adapter.