Skip to content

Enforce a per-run token/cost budget: park or halt a run that exceeds its budget #186

Description

@khaliqgant

Blocked by: #185

Motivation

Cursor's economics work shows unbounded swarms are how you get a $10k run. Once Factory has a per-run cost ledger (#185), the next lever is a budget it actually enforces. The TeamSpec tokenBudget / timeBudgetSeconds fields exist upstream but are parsed and never enforced by a live executor. This makes a run's cost/time budget a real gate: exceed it and the run is parked for human review, not silently overspent.

Goal

Add a configurable per-run token + USD + wall-time budget; when a run's accumulated spend (from the #185 ledger) crosses the budget, stop dispatching further work, park the issue with a clear reason, and emit a bounded event — never a silent overspend, never a hard crash mid-writeback.

What to build

  • Budget config in src/config/schema.ts: budget: { maxUsd?, maxTokens?, maxWallSeconds?, onExceed: 'park' | 'halt' } (default park), with sane repo-level defaults and per-run override.
  • A budget check on the ledger-update path (subscribe to the Cost & token accounting for every run: per-role, per-model ledger surfaced on completion #185 CostLedger): after each recorded usage, compare the run aggregate against the budget.
  • On exceed: stop new dispatch/spawn for that run, transition the issue to a parked/human-review state with a bounded run.budget.exceeded.v1 event (role/model/usd/tokens/limit only — allowlist), and post the reason via the existing writeback path. In-flight agents finish their current step; no partial-writeback corruption.
  • Fail-open on missing price data: an unpriced run (usd null) must fall back to the token/time budget, and if none is set, must not block (log a bounded budget.unpriced.skipped event).

Acceptance

  • Budget config parsed + validated; onExceed defaults to park.
  • Crossing maxUsd / maxTokens / maxWallSeconds parks (or halts) the run and stops further dispatch.
  • A bounded run.budget.exceeded.v1 event is emitted; the reason is written back to the issue/PR thread.
  • Unpriced runs fall back to token/time budget and never hard-block on missing price.
  • Manifest updated (feature entry + catalog.feature_count / tier_counts bump) so featuremap:check stays green.

✅ End-to-end verification — Factory success criteria (REQUIRED)

"Compiles" is not done. Run a lifecycle that deliberately blows the budget on the fake fleet and observe the run park with a written-back reason — then check the harness in.

Run it

npm run build
npm run featuremap:check
npm test
npm run verify:e2e

The E2E (test/e2e/run-budget-gate.test.ts) must:

  1. Configure a tiny maxUsd; drive a run on the in-process fake fleet whose synthetic usage exceeds it.
  2. Assert further dispatch/spawn stops after the crossing, the issue is parked, and exactly one run.budget.exceeded.v1 bounded event fires with allowlisted attributes only.
  3. Assert a within-budget run completes normally with no budget event (no false trip).
  4. Assert an unpriced run with only maxTokens set trips on tokens; with no budget set, it does not block.

Deliverable

  • test/e2e/run-budget-gate.test.ts proving over-budget parks and within-budget passes, one command.
  • Fails loudly if an over-budget run keeps dispatching or if a within-budget run trips (no false green).

Anchor files

Out of scope

  • Cost-aware model selection (separate follow-up).
  • Cancelling an agent mid-step — parking stops new work; in-flight steps drain cleanly.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    factory-readyApproved for the Factory to pick up and implement

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions