orbotodocs
Agents & AI

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:main or unity: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, released or expired.
  • Holder. An agent-session, a ci-run or a human. Without a holder the claim belongs to the agent session that filed it, and to the person when no session is involved. A ci-run names its run id.
  • Priority. release, then develop, then pr, then batch. 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:

CodeMeaning
0Granted, or done
4Queued (a claim without --wait)
5Timeout; the queued claim was released
6Denied or ended: a 403, 404 or 409, or the claim expired or was released while waiting
7Yielded (--on-yield release)
130, 143Interrupted; the claim was released
1, 2API 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.sh

plan 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 at develop, a pull request at pr, and anything else, such as a schedule or a manual dispatch, at batch.
  • 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 in GET /api/capacity/metrics. They need the admin:agents:read permission.
  • 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_total and orboto_capacity_overrun_alerts_total, see Monitoring.

Agent surfaces

  • REST. /api/capacity/resources, /api/capacity/windows, /api/capacity/claims with renew, release and yield, /api/capacity/resources/<resource>/plan, /api/capacity/metrics and /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, and orboto_help with the topic capacity returns 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

SymptomCause and fix
A claim stays queued although the host is idlewaitingFor 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 fileOnly the owner renews a claim; the owner or an administrator releases it.
409 claim_ended on a renewalThe claim expired or was released. File a new one.
409 resource_busy on deleteA granted or queued claim exists. Release it or let it end first.
409 plan_no_slotNothing fits within 14 days; shorten the run or free a window.
A claim vanished after a long buildIt was not renewed before its deadline. Use --hold, or renew from the job.

On this page