Model concurrency is not machine capacity

Why admitting sub-agent sessions and admitting work to a shared machine are separate problems, handled with queues, resource preflights, leases and useful refusals.

November 20264 min readAgent systemsPlatformsDirecting an agent fleet

Short sienna and yellow ochre dabs crowd down onto thick knifed bottle green, heaping and smearing, beside flat thin violet-grey and a few abrupt chalk slashes.

In the agent runtime I worked on through the summer of 2026, a sub-agent was not a different kind of agent. It was another session on another lane of the same durable session tree, with the same system prompt and essentially the same tools as its parent, and only its model and its brief differed. Starting one took a single call that returned at once. To the model provider it was one more concurrent request, and providers will accept a lot of those. That makes fan-out feel free, but the thinking runs on someone else’s hardware and the compiling runs on yours. The children share a repository, a disk and whatever build capacity the sandbox has, and the campaign defaults I ended up with assumed sandboxes of 2 CPUs and 7 GB of memory.

That left two problems to keep apart. One was what a parent should retain when it hands work to a child whose start may be delayed and whose result may be larger than the conversation can carry. The other was who gets to say no when the machine is full, given that no single agent can see what its siblings are doing to it.

Admission began with a bug worth remembering. A simple check in front of the asynchronous routing step let a parallel batch straight through, because a model that asks for several children in one response can get every one of them past the check before any has been counted. So admission became a gate rather than a rejection. At the end of August the limits were 16 running and 64 pending, and a child that could not start yet waited in a queue where it could still be cancelled, while the parent already held its ID. Children did not inherit everything, either. A child could not ask the user a question, and it could not spawn children of its own, because recursive spawning had been the single biggest cost blow-up in our older agent tree.

What the parent needed to keep turned out to be small, because the durable state lived elsewhere. A child saw its brief and nothing else, so nothing leaked from the parent’s context, and each child’s prompt was frozen so that its prefix could not drift under it. Its lane persisted, so its transcript survived a restart as the parent’s did. An idempotency key meant that spawning the same work twice returned the existing child instead of starting a second, and a result that nobody was waiting for was pushed to the parent rather than left to be forgotten.

The result itself was the harder part. The inline limit for a handoff was 24,000 characters, and the most useful thing a coding child produces can be bigger than that: a 30 KB diff, for example. Truncating that diff leaves the parent with something that looks like a change and no longer is one, and pasting all of it spends the parent’s context on every later call. So the full output was kept as an artifact in the workspace, byte for byte, and the parent received a bounded inline reference to it. Partial handoffs were marked as partial, and an empty handoff counted as a failure rather than a child with nothing to say.

None of that knew anything about the machine. The gate of 16 counted sessions, which is to say it counted model concurrency, and every one of those sessions could still decide to install dependencies in the same repository on the same disk. The machine needed its own owner, because prompt guidance is not an admission controller. A system prompt can ask an agent to watch the disk. It cannot stop several agents from each deciding, on the evidence available to each of them, that there is room. I added an opt-in campaign runtime at the end of August to own per-repository sub-agent slots, disk and transient-output preflights, a lease for heavyweight commands, the build environment and a checkpoint journal. Before dispatching a child, creating a worktree, installing dependencies or running a heavyweight command, it checked the machine against a 25 GiB disk floor and a cap on transient output, and heavy commands held a 90-minute lease.

A refusal was a typed, recoverable result naming the reason, such as low disk, too much transient output or a required checkpoint. It came with specific guidance: remove only reconstructible output after verifying who owns it, never delete dirty or unique source, preserve dirty source as a scoped patch or commit, then retry the same operation. That wording matters because low disk is exactly when an agent is likely to go looking for something to delete, and the largest directory nearby may hold another agent’s only copy of its work.

The refusal that best shows the intent is the one for an incomplete scan. The runtime measured transient output by walking it, and the walk stopped at a safety bound of 250,000 entries. If it hit the bound before finishing, the amount of transient output was unknown, and the runtime refused rather than guessing from a partial count. The guidance allowed raising the bound only with an explicit override and enough capacity.

Heavy commands took their lease through a lock directory and a lease file on disk, so two runtime managers using that lock shared a limit. This coordinated commands routed through the guard, while unrelated processes could still consume the same machine’s resources.

The runtime looked at the disk just before an operation and answered for that operation. When it said no, the agent could checkpoint, clean up what could be rebuilt and try again. An agent admitted onto a full disk has far fewer good options.

Chris Chabot, November 2026