Skip to content

fix(supervise): a refused spawn names the channel and the amounts - #1271

Merged
drewstone merged 3 commits into
mainfrom
fix/spawn-refusal-names-the-channel
Sep 16, 2026
Merged

drewstone merged 3 commits into
mainfrom
fix/spawn-refusal-names-the-channel

Conversation

@drewstone

Copy link
Copy Markdown
Contributor

Problem

spawn_worker answered a budget refusal with "the conserved pool refused this spawn (budget-exhausted); the run has no allocation left to give this worker". It named no channel and no amounts. It was also the answer for every other refusal kind.

Measured 2026-09-16 on a Discovery director placed on the Tangle sandbox (mech-interp-foundations-sandbox-a-20260916b): its first research child asked for maxIterations: 100 against a 60-iteration pool. The director spent a 3-iteration probe child to learn the pool still had room, concluded the pool could not fund real work, and moved its research to local processes.

Change

  • BudgetPool.reserve returns shortfall: { channel, requested, free, closedByUnknownSpend? } with budget-exhausted (ReservationShortfall, exported). free never reads negative.
  • scope.spawn passes it through. spawn_worker returns it, with a reason naming the largest request that fits: the run pool has 58 iterations free and this spawn asked for budget.maxIterations 100; spawn again with budget.maxIterations at most 58, or ask the caller for a larger root budget.
  • max-live-workers, depth-exceeded, duplicate-key, key-conflict, invalid-identity, scope-aborted each get their own reason instead of the empty-pool sentence. usd-unbudgeted, in-doubt, scope-settled texts are unchanged.
  • Released as 0.237.0 (release:prepare run; CHANGELOG, api-surface, docs updated).

A consumer that compared a refusal with toEqual({ ok: false, reason: 'budget-exhausted' }) now also receives shortfall; eleven such assertions in this repo now pin the amounts.

Proof (local)

  • tests/runtime/spawn-refusal-shortfall.test.ts: pool shortfall per channel, pass-through on a real supervisor, driver text, non-budget refusals. Fails 5 of 5 with src/ stashed.
  • Full suite: 4124 passed, 11 skipped (313 files). biome check: clean. build + check:api-surface: clean.
  • One full-profile-bridge test failed once under full-suite load and passes alone with and without this change.

Release: I will cut v0.237.0 from the merged main tip and say so here before tagging. #1270 is open on unrelated files; if it merges first I will rebase and re-run.

🤖 Generated with Claude Code

A budget-exhausted reservation now carries shortfall { channel,
requested, free } (ReservationShortfall, exported), scope.spawn passes
it through, and spawn_worker returns it with a reason a driver can act
on: "the run pool has 58 iterations free and this spawn asked for
budget.maxIterations 100; spawn again with budget.maxIterations at most
58, or ask the caller for a larger root budget". A channel closed by
unmeasured spend says no smaller request fits.

Measured 2026-09-16 on a Discovery director placed on the Tangle
sandbox: its first research child asked for 100 iterations against a
60-iteration pool, was told "the run has no allocation left to give
this worker", spent a 3-iteration probe child to learn the pool still
had room, and then moved its research to local processes.

The same sentence also answered every other refusal: max-live-workers,
depth-exceeded, duplicate-key, key-conflict, invalid-identity and
scope-aborted each read as an empty pool. Each now names its own cause.
The usd-unbudgeted, in-doubt and scope-settled texts are unchanged.

Tests: tests/runtime/spawn-refusal-shortfall.test.ts (pool shortfall per
channel, the scope.spawn pass-through on a real supervisor, the driver
text, and non-budget refusals) fails 5 of 5 on the unmodified source.
Eleven existing assertions that compared a refusal exactly now pin the
amounts. Full suite 4124 passed, 11 skipped; lint and api surface clean.
Review of #1271 found the first version overclaimed. A refusal named
only the first channel that did not fit, so a driver that shrank that
request could be refused again on a channel it was never told about.
And "spawn again with budget.maxTokens at most N" was false for tokens
and dollars: a driver's next turn is metered from the same pool before
its retry reaches admission, so a retry at exactly N is refused again
(reproduced with a scripted driver: 20000 -> "at most 9850" -> 9850 ->
"at most 9700" -> never admitted).

- reserve() now returns shortfalls: every channel that does not fit.
  Resource validation throws before any shortfall is computed.
  usd-unbudgeted still yields to an exhausted channel.
- Iterations and named resources say "at most N fits"; driver turns
  charge neither. Tokens and dollars say N is free right now and to ask
  for well under it.
- A channel closed by unmeasured spend says the run admits no further
  spawn at any budget (it closes every spawn, not only larger ones).
- max-live-workers no longer tells a driver to cancel a worker (drivers
  have no cancel verb); scope-aborted names the intensity breaker;
  invalid-identity says to spawn again without a key.

Tests: new cases for multi-channel shortfalls and the tokens/dollars
advice; two existing assertions now also carry the iterations
shortfall they always had. Full suite 4127 passed, 11 skipped; lint,
tsc, build and api surface clean.
@drewstone

Copy link
Copy Markdown
Contributor Author

Addressed an independent review (4 agents, one finding reproduced by a verifier): the tokens/usd "at most N" advice was false because driver turns meter from the same pool before a retry; refusals named only the first short channel; and five non-budget texts named the wrong next step. All fixed in the latest commit (shortfall -> shortfalls, per-channel advice). Local: 4127 passed / 11 skipped, lint, tsc, build, api surface clean. I am cutting v0.237.0 from the merged main tip once CI is green.

The review commit renamed shortfall to shortfalls but the generated API reference still described the first version, which failed the docs freshness gate in CI. Regenerated with pnpm run docs:api; docs:freshness passes locally.
@drewstone
drewstone merged commit 43bb561 into main Sep 16, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant