diff --git a/src/runtime/supervise/budget-floor.ts b/src/runtime/supervise/budget-floor.ts index ef99f9b1..208b24aa 100644 --- a/src/runtime/supervise/budget-floor.ts +++ b/src/runtime/supervise/budget-floor.ts @@ -48,8 +48,12 @@ export const WORKER_TOKEN_FLOOR = { * * A `null` harness is the router/inline arm: it holds only coordination verbs and re-sends no * harness scaffolding, so it has no floor of this kind. + * + * Accepts a bare `string` because `AgentProfile.harness` is not exactly `BackendType` — an + * unrecognised name resolves to `null` (no floor) rather than throwing, which keeps an unknown + * harness admissible instead of refusing work on a name this table has not heard of. */ -export function workerTokenFloor(harness: BackendType | null): number | null { - if (harness === null) return null +export function workerTokenFloor(harness: BackendType | string | null | undefined): number | null { + if (harness === null || harness === undefined) return null return (WORKER_TOKEN_FLOOR as Record)[harness] ?? null } diff --git a/src/runtime/supervise/scope.ts b/src/runtime/supervise/scope.ts index e9d65e0e..acfbcf80 100644 --- a/src/runtime/supervise/scope.ts +++ b/src/runtime/supervise/scope.ts @@ -564,7 +564,13 @@ export function createScope(args: ScopeArgs): Scope { // `budget-exhausted` invites a caller to retry SMALLER, which here is the exact wrong move. // Checked after resolution because that is where the harness is known, and refunded the way // every sibling failure on this path is. - const floor = workerTokenFloor(spec.harness) + // `spec.harness` is null whenever the harness rides in the backend config instead of the + // spec — which is the path that actually spawns workers through the bridge. Reading only it + // made this guard miss the case it was written for: a root authored a 6,000-token child + // against a measured 31,211 floor, the guard never fired, and the child died having produced + // nothing (discovery-lab run proof-bridge-20260801f). The AgentProfile is where a root + // declares its child's harness, so it is the more reliable of the two. + const floor = workerTokenFloor(spec.harness ?? spec.profile.harness ?? null) if (floor !== null && opts.budget.maxTokens < floor) { args.pool.reconcile(reservation.ticket, zeroSpend()) permit.release() diff --git a/tests/kernel/budget-floor.test.ts b/tests/kernel/budget-floor.test.ts index 2ad5a495..b1ac52d6 100644 --- a/tests/kernel/budget-floor.test.ts +++ b/tests/kernel/budget-floor.test.ts @@ -45,3 +45,25 @@ describe('worker token floor', () => { } }) }) + +describe('the floor reads the harness a root actually declared', () => { + // The guard originally read only `spec.harness`, which is null whenever the harness rides in the + // backend config rather than the spec — the path that actually spawns workers through the + // bridge. So it missed the case it exists for: run proof-bridge-20260801f authored a + // 6,000-token child against a 31,211 floor, admitted it, and the child produced nothing. + it('resolves a floor from an AgentProfile harness when the spec carries none', () => { + const specHarness = null + const profileHarness = 'pi' as const + expect(workerTokenFloor(specHarness ?? profileHarness)).toBe(31_211) + }) + + it('still has no floor when neither names a harness (router/inline arm)', () => { + const specHarness = null + const profileHarness = null + expect(workerTokenFloor(specHarness ?? profileHarness)).toBeNull() + }) + + it('refuses the exact budget that slipped through', () => { + expect(6_000).toBeLessThan(workerTokenFloor('pi')!) + }) +})