Skip to content

Charge the quota-burn cap and run-once ledger exactly once across asynchronous dispatch #6379

Description

@atomantic

Problem / Goal

Quota-burn accounting is currently tied to the synchronous "we queued a prompt" moment. With references dispatching through canonical scheduled-task generation, acceptance is asynchronous: a request can be recorded and later refused. #6372 requires the cap and the run-once ledger to be charged exactly once, when work is actually queued/accepted for execution — not when a request that may still be refused is recorded. Precedent: #3179 (dispatch ledger recorded before task creation was confirmed, undercounting the window budget) and #3182.

Scope

Across server/services/quotaBurnRunner.js, server/services/quotaBurn.js (the gate ladder and recordQuotaBurnDispatch), quotaBurnCompletions.js, quotaBurnDenials.js, and quotaBurnStore.js. Six machine-local, deliberately un-federated files under data/cos/ carry this state:

File Owner Contents
quota-burn.json quotaBurnStore.js the plan
quota-burn-runs.json quotaBurnStore.js run log, capped at RUN_LOG_LIMIT
quota-burn-inflight.json quotaBurnStore.js key -> epochMs, 6h TTL — the existing reservation primitive to build on
quota-burn-dispatches.json server/services/quotaBurn.js per-window dispatch counts
quota-burn-denials.json quotaBurnDenials.js per-family observed-refusal blocks
quota-burn-completions.json quotaBurnCompletions.js family+job key -> ISO instant

Required behavior:

  • Tie accepted requests to their eventual task IDs. Provenance must survive persistence, server restart, and completion so refusal blocks, cooldown exemptions, and completion continuation keep working.
  • Pending reservations. A reservation prevents another cycle queuing the same step; rejection or cancellation before dispatch releases it. An already pending/running equivalent scheduled invocation must not enqueue a second run or consume the cap.
  • Charge once. The dispatch cap and the runOnce ledger (jobIsSpent / quotaBurnJobKey, server/lib/quotaBurnConfig.js) are debited only on accepted-for-execution, and never twice for one accepted unit.
  • Probe/status reads stay side-effect free. getQuotaBurnCompletions() returning null already means unreadable (not empty), and callers must skip rather than re-dispatch — preserve that sentinel distinction (AGENTS.md: sentinel + validate, never collapse absent into empty).
  • Ordinary schedule runs are not burns. A task that runs on its own schedule must not be counted against the burn cap and must not consume a burn step's one-shot state.

Acceptance criteria

  • Workflow tests cover: accepted execution; a deferred request later rejected (cap and ledger untouched, reservation released); a duplicate scheduled or burn invocation of the same step (no second enqueue, no second charge); server restart mid-flight (provenance and reservation recovered); completion continuation; provider refusal; one-shot spend and re-arm; and force semantics.
  • No double accounting in any of the above, and no perpetual-loop revival.
  • An ordinary clock-driven run of the same scheduled task leaves the burn cap and the step's run-once state unchanged.
  • A status/probe read performs no ledger write.
  • The run log (data/cos/quota-burn-runs.json, RUN_LOG_LIMIT) records the accepted task id for each burn.

Notes

Depends on the shared invocation path child. Machine-local storage only (data/cos/, unfederated) — see server/services/quotaBurnStore.js and docs/STORAGE.md.


Part of #6372

When this ships, tick its box in the ## Decomposed into checklist on #6372. If it is the last unchecked box, close #6372.

Activity

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

Metadata

Metadata

Assignees

Labels

area:cos-agentsChief-of-Staff autonomous agentseffort:highDispatch reasoning effort: highenhancementNew feature or requestmodel:heavyDispatch capability: strongest available coding modelplanTracked by /do:replanplanner:opus-5Plan authored by the opus-5 model

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions