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
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.
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 andrecordQuotaBurnDispatch),quotaBurnCompletions.js,quotaBurnDenials.js, andquotaBurnStore.js. Six machine-local, deliberately un-federated files underdata/cos/carry this state:quota-burn.jsonquotaBurnStore.jsquota-burn-runs.jsonquotaBurnStore.jsRUN_LOG_LIMITquota-burn-inflight.jsonquotaBurnStore.jskey -> epochMs, 6h TTL — the existing reservation primitive to build onquota-burn-dispatches.jsonserver/services/quotaBurn.jsquota-burn-denials.jsonquotaBurnDenials.jsquota-burn-completions.jsonquotaBurnCompletions.jsRequired behavior:
runOnceledger (jobIsSpent/quotaBurnJobKey,server/lib/quotaBurnConfig.js) are debited only on accepted-for-execution, and never twice for one accepted unit.getQuotaBurnCompletions()returningnullalready 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).Acceptance criteria
forcesemantics.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) — seeserver/services/quotaBurnStore.jsanddocs/STORAGE.md.Part of #6372
When this ships, tick its box in the
## Decomposed intochecklist on #6372. If it is the last unchecked box, close #6372.