Skip to content

[factory] Refuse dispatch when the workspace is over budget #212

Description

@khaliqgant

Problem

Nothing consults spend before spawning. relayburn ledgers cost after the
fact and burn mcp-server exposes it read-only and advisory, so a stuck recipe
burns until a human notices. Factory has a safety gate for what it may work
on and no gate for how much that may cost.

Fix

Two halves. The second only works if the first lands.

1. Attribution

Stamp every agent Factory spawns with the burn enrichment tags that already
exist:

  • workflowId = the dispatch lifecycle runId
  • tag issue=<workUnitId>
  • tag repo=<slug>

Without this, spend cannot be attributed to a run and any budget number is a
guess. With it, burn hotspots --workflow <runId> answers "what did this issue
cost" directly, and burn summary --group-by-tag repo answers it per
repository.

2. The gate

Add a budget block to factory.config.json, alongside safety and resolved
nearest-first the same way:

"budget": {
  "source": "burn-cloud",
  "window": "24h",
  "maxUsd": 40,
  "onExceeded": "refuse"    // "warn" to log only
}

Evaluate it in the src/safety/ path where scope is already evaluated, so a
refusal lands in the existing skipped: Array<{ issue, reason }> channel on
IterationReport (src/types.ts:169) and surfaces through factory status and
the Slack thread instead of crashing a run.

The read is GET /v1/summary?since=<window> against burn-cloud with the
brn_at_* token.

Behavior:

  • No budget block → no gate, unchanged behavior.
  • Over maxUsd with onExceeded: "refuse" → every ready issue is skipped with
    reason budget exceeded: $X of $Y in <window>.
  • burn-cloud unreachable → refuse. A dispatch gate fails closed.

What this is not

/v1/summary is scoped by org, model, and machine, and burn-cloud has no
reservation concept. N concurrent dispatches can each pass the check and
collectively overshoot by up to N runs. This is a coarse circuit breaker, not an
atomic per-run debit.

Making it atomic means burn-cloud grows POST /v1/reserve plus
commit-on-completion. That is a burn-cloud change, not a Factory one, and is
worth filing only if the circuit breaker proves insufficient in practice.

Definition of done

  • Agents spawned by a dispatch carry that dispatch's workflowId and the
    issue / repo tags; burn hotspots --workflow <runId> returns its spend.
  • A workspace over maxUsd reports ready issues as skipped with the budget
    reason rather than dispatching them.
  • burn-cloud unreachable refuses rather than dispatching.
  • Absent budget config produces no behavior change.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions