Skip to content

[epic] AI budget & circuit-breaker limits are user-configurable via UI + MCP (today they are hardcoded constants) #478

Closed

Description

@serge-ivo

Goal

Every AI budget/limit a user can hit should be viewable and settable by the user via the UI and via MCP — not a hardcoded constant that requires a code edit + deploy to change.

Background (verified in code)

  • Account circuit breaker = two bare constants in workers/api/src/lib/delegation-budget-store.ts: DAILY_CEILING_MICROS ($50 charged/24h, line 95) and DAILY_TOKEN_CEILING (250M tokens/24h, line 112). Enforced in reserve(); nothing reads an env var, per-account column, or admin setting.
  • The only existing platform-AI setting is a boolean kill switch (PUT /v1/admin/settings/platform-ai).
  • The breaker is invisible until it fires — the number only shows in the refusal message, and the agent-facing guidance even implies a nonexistent "contact support to raise it" lever.
  • Cloudflare Access on the /v1/admin perimeter is currently off (see Enable Cloudflare Access on the /v1/admin perimeter before paid launch #108); admin is role-gated only.

Surfaced while investigating why 4 Coder agents went idle overnight — all tripped the 250M token breaker, and there was no way to see or change it.

Children

Done when

A user can see their rolling-24h consumption against every ceiling and raise/lower each limit from both the console and MCP, with a clamped safe maximum — no code change required.

Activity

  1. serge-ivo commented on Aug 10, 2026

    @serge-ivo
    ContributorAuthor

    Reframed by #485: rather than calibrate the ceilings now, AI budget enforcement is demoted to observe-only (BUDGET_ENFORCE, default off) until paid launch — usage is metered, structural caps stay, nothing hard-blocks. The D1 config resolver from #474 stays in place for when enforcement returns. Children #475/#476/#477 parked.

  2. serge-ivo commented on Aug 11, 2026

    @serge-ivo
    ContributorAuthor

    Verdict: delivered — two small remainders, then close

    All four children are closed and I have read the code that delivers each, plus confirmed the surfaces in production. This epic's premise ("today they are hardcoded constants") has expired.

    Child Shipped Verified at HEAD
    #474 resolver 32f48a1 + migration 0113 resolveAccountCeilings, lib/delegation-budget-store.ts:130-172 — per-account row → '__platform__' row → env → constant, clamped to MAX_OVERRIDE_*, fails open on a D1 error. Read by reserve() (:305) and openBudget
    #475 surface 2d8f9d5 (+ 5805332 for enforced) GET/PUT /v1/budget/limits, routes/budget.ts; console BudgetPanel, store/console/src/pages/Usage.tsx:566-780. Both are live — the deployed console bundle contains the panel, and production GET /v1/budget/limits returns effective + per-field tier + consumption + remaining + maxOverride + enforced
    #476 MCP ca476cd get_budget_limits / set_budget_limits, workers/mcp/src/instance-tools/account.ts:108-153
    #477 per-run knobs c363fa9 + migration 0115 perTreeCostMicros / perTreeDelegations / perTreeMaxDepth / loopMaxIterations in the same four-tier stack; loopMaxIterations reaches sanitizeMaxIterations via lib/agent-loop.ts:60-64

    Live proof of "no code change required", production, 2026-08-11 — every tier resolving, distance-to-limit present:

    "effective": { "tokenCeiling": 250000000, "tokenCeilingTier": "default",
                   "chargedMicrosCeiling": 50000000, "chargedMicrosCeilingTier": "default",
                   "loopMaxIterations": 50, "loopMaxIterationsTier": "default", ... },
    "consumption": { "chargedMicros": 7303714, "tokens": 221731036 },
    "remaining":   { "chargedMicros": 42696286, "tokens": 28268964 },
    "enforced": false

    What remains

    1. MCP write parity — 2 of 6 fields, and it clears the other 4. set_budget_limits (account.ts:122-151) sends only tokenCeiling and chargedMicrosCeiling; because PUT /v1/budget/limits is a full replace (routes/budget.ts:250-274), calling it wipes any per-tree or loopMaxIterations override set from the console. Filed as #501 with the fix. Read parity is fine — get_budget_limits proxies the GET, so it already returns all six.

    2. The refusal message still points at a lever that is now the user's own. lib/delegation-budget-store.ts:321 and :331 both end:

    To raise this limit, ask your platform operator.

    This epic's own background objected to exactly this — "the agent-facing guidance even implies a nonexistent 'contact support to raise it' lever" — and #474 listed "fix the refusal message" as a deliverable. The denomination half was fixed (the message now names what was counted, over what window, and why subscription tokens are excluded — #343's finding). The lever half was not: since #475 the owner raises it themselves in Usage → Budget limits, or with set_budget_limits. A one-line copy change to something like "Raise it in Usage → Budget limits, or ask your platform operator." finishes it. Not worth its own issue; fold it into whichever of #501/#502 lands first, or do it standalone.

    Correction to the 2026-08-10 08:15 comment on this issue

    That comment says #475/#476/#477 are "parked" under #485. They are not — all three were closed delivered between 08:20 and 08:56 the same morning, i.e. within 46 minutes of the parking decision being written. Anyone reading this epic top-to-bottom would conclude three quarters of it is deferred when in fact all of it shipped. The Children checklist above the comment is also still unticked.

    Recommendation

    Tick the four children, correct or strike the "parked" comment, and close this epic as delivered, tracking the two remainders under #501 (MCP write parity) and the one-line message fix. The enforcement posture question is separate and is answered on #485 — nothing in this epic depends on it, and the machinery it built is what makes the recommended split-enforcement option there a config change rather than a code change.

    Verified vs inferred

    Verified: every file:line, the migrations, the production /v1/budget/limits payload, and the presence of the budget panel in the deployed console bundle. Inferred: nothing load-bearing. Not checked: whether any other client sends a partial PUT and relies on the clearing behaviour — I grepped store/, workers/, packages/ and found only the console (all six fields) and MCP (two).

  3. serge-ivo commented on Aug 18, 2026

    @serge-ivo
    ContributorAuthor

    Seven days on: the "close as delivered" verdict still holds, and one of the two named remainders is done. The other is still shipping a false statement to the agent.

    Re-verified 2026-08-18.

    The delivered half is unchanged

    GET /v1/budget/limits on production today:

    {"enforced":false,
     "effective":{"tokenCeiling":250000000,"tokenCeilingTier":"default",
                  "chargedMicrosCeiling":50000000,"chargedMicrosCeilingTier":"default"},
     "consumption":{"chargedMicros":2073017,"tokens":56743055},
     "remaining":{"chargedMicros":47926983,"tokens":193256945}}

    Every tier still resolving, distance-to-limit present, all four children still closed. The epic's premise — "today they are hardcoded constants" — is still expired.

    Remainder 1 is done

    #501 is closed — MCP set_budget_limits no longer wipes the four fields it does not expose.

    Remainder 2 is not, and it is now false in three places, not two

    workers/api/src/lib/delegation-budget-store.ts:

    • :321 — "…it resets as older usage ages out. To raise this limit, ask your platform operator."
    • :331 — "…It resets as older usage ages out. To raise this limit, ask your platform operator."
    • :377 — "Work already done is intact; nothing new can draw on this pool. There is no self-service way to raise it — ask your platform operator."

    The third one is the sharpest, and it was not in the previous comment's list. It fires on an exhausted per-tree pool — and routes/budget.ts:61-64 makes every field of that pool owner-settable:

    { column: "per_tree_cost_micros", field: "perTreeCostMicros", max: MAX_PER_TREE_MICROS },
    { column: "delegations",          field: "perTreeDelegations", max: MAX_PER_TREE_DELEGATIONS },
    { column: "max_depth",            field: "perTreeMaxDepth",   max: MAX_PER_TREE_DEPTH },
    { column: "loop_max_iterations",  field: "loopMaxIterations", max: MAX_LOOP_ITERATIONS },

    So :377 tells the owner "there is no self-service way to raise it" about four values he can raise from Usage → Budget limits or with set_budget_limits, both of which #477 shipped. That is not stale copy — it is the agent asserting the absence of a lever the same release added, which is exactly the defect this epic's own background objected to:

    "the agent-facing guidance even implies a nonexistent 'contact support to raise it' lever"

    It has been inverted rather than fixed: the lever now exists and the message denies it.

    Recommendation, unchanged and now cheaper

    Tick the four children, strike the stale "parked" comment, fix the three strings, close. The string fix is one commit with no design question in it:

    • :321 / :331 → "Raise it in Usage → Budget limits, or ask your platform operator."
    • :377 → "Raise the per-run pool in Usage → Budget limits (per-tree budget, delegations, depth), or ask your platform operator."

    Worth pinning with a test, because this is the third time these two halves have drifted: assert that no refusal string in delegation-budget-store.ts claims a limit cannot be raised while routes/budget.ts's FIELDS table lists the corresponding column. That is a small mute-invariant.test.ts-shaped scan and it would have caught :377 on the day #477 landed.

    Verified: the live payload; delegation-budget-store.ts:321, :331, :377; routes/budget.ts:61-64; the closed states of #474–#477 and #501. Not attempted: I set no limit and flipped no flag.

  4. added
    P3: laterDesign, strategy, or deferred pending a decision or demand
    on Aug 18, 2026
  5. serge-ivo commented on Aug 18, 2026

    @serge-ivo
    ContributorAuthor

    Closing as superseded by its own premise expiring. This epic asks for AI budget and circuit-breaker limits to be user-configurable because they are hardcoded constants. They are no longer enforced at all — d951b5fb demoted them to observe-only (#485, now closed), so there is nothing whose hardcoding currently constrains anyone.

    Measured today: 24h spend $2.07 against a $50 ceiling, enforced:false, both tiers "default".

    The right time to build configurable limits is when enforcement returns, which #56 (cost governance, gating the public open) already owns and which is correctly deferred behind #68. Re-filing then against live numbers will produce a better issue than progressing this one against numbers from before enforcement was switched off.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3: laterDesign, strategy, or deferred pending a decision or demandenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions