Skip to content

model a Quota Burn step as a scheduled-task reference plus per-invocation overrides - #6390

Merged
atomantic merged 2 commits into
mainfrom
claim/issue-6377
Sep 6, 2026
Merged

model a Quota Burn step as a scheduled-task reference plus per-invocation overrides#6390
atomantic merged 2 commits into
mainfrom
claim/issue-6377

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

A Quota Burn step used to be its work: a copied prompt in a free-form params bag keyed by a quota-only jobType, with no durable link to the scheduled task it came from. Preset identity was never persisted at all — the server (upgradeStoredAuditPrompt) and the client (matchedPreset) both re-derived it by comparing prompt text, so editing one word of a shipped prompt orphaned the step.

A step now stores a discriminated scheduled-task reference plus an explicit per-invocation overrides bag:

  • taskRef is either { kind: 'builtin', taskType, appId } — with the target app validated against the same scope contract the schedule's own request gate uses — or { kind: 'custom', jobId }, which deliberately stores no app scope because the job record owns it.
  • overrides holds provider / model / effort / run params. An unset override inherits the task's saved settings, and nothing written on a step ever edits the schedule.
  • Step identity and settings survive normalization untouched: id, order, enabled, label, runOnce, family settings, and the overrides themselves round-trip byte-identical.
  • Dangling, disabled, wrong-scope and ineligible references resolve to a retained-but-unavailable step carrying an actionable reason, with every stored setting intact and quotaBurnStepIsDispatchable() false.

Zod now rejects — each naming the offending field — a reference missing a required target app, an install-wide type given one, a malformed custom job id, a step naming two kinds of work or none, and a provider pin belonging to another quota family, so a plan can no longer save itself into spending the wrong subscription.

Data model, normalization and validation only. data/cos/quota-burn.json stays machine-local and unfederated.

Deliberate boundaries

  • The legacy executor keeps running. An un-migrated jobType payload still loads with every setting intact and normalizes to unavailable-with-a-migration-reason, but quotaBurnRunner.js / JOB_MODULES are untouched — flipping the model and cutting execution here would stop quota burn dead on every install for the length of the epic. Add a shared quota-burn invocation path over scheduled tasks with a countPending contract #6378's reference path is gated on quotaBurnStepIsDispatchable() from the start, so it never inherits a legacy step, and Migrate legacy Quota Burn prompt plans to scheduled-task references and retire JOB_MODULES #6381 retires the old executor.
  • Top-level providerId/model/effort/params remain as derived compat mirrors of overrides, because the shipped editor still writes them (it moves to overrides in Replace the Quota Burn editor with a scheduled-task picker #6382). They are recomputed on every read, so they cannot drift. The tie-break is key presence, not truthiness — a payload carrying a top-level key wins with it, including a deliberate null clear, so clearing a pinned model can't resurrect the stale one.
  • The Zod family-pin check is intentionally conservative. The binary is what really decides a provider's family and a schema cannot read the live provider list, so the new familyForProviderId() rejects only ids that unambiguously name another family; a user's own wrapper name falls through to dispatch-time resolution, which stays authoritative.
  • saveQuotaBurnConfig's shallow family merge and the client mirror mergeQuotaBurnPatch are unchanged — "extend both or neither", and this needs neither.

Rolled in

  • INSTALL_WIDE_TASK_TYPES / MANAGED_APP_TARGET_TASK_TYPES / requiresManagedAppTarget / requiresInstallWideTarget move down to server/lib/taskTargetScope.js, re-exported unchanged from services/taskScheduleRegistry.js. server/lib may not import upward into server/services (lib/layering.test.js), and the reference schemas need the scope contract.
  • familyForProviderId(id) added to lib/providerFamilies.js — the id-only classifier a Zod schema can use, null when the id names no family.
  • lib/importScoping.test.js budget raised 89,500 → 91,400. This change measures 89,889; its ~389 share is two leaf vocabulary modules with no subtree behind them, reached by the ~200 suites that cross quotaBurnConfig.js. That is the ordinary growth the budget documents as tolerable, so the raise restores the documented ~1.5k headroom rather than inching the number up.

Test plan

  • cd server && npm test — full suite green (2011 files).
  • cd client && npm test — full suite green (870 files); no client source changed.
  • New server/lib/quotaBurnTaskRef.test.js: no kind inference from stray fields, custom refs carry no app scope, one code per distinct unavailability cause, catalog-dependent verdicts withheld with no catalog while scope faults still report, non-mutating whole-config stamping, dispatchability gates.
  • server/lib/quotaBurnConfig.test.js: reference round-trip (save → reload → re-normalize identical), top-level clear beating a stale override, legacy step loading with every setting plus a migration reason and never being rewritten or duplicated.
  • server/routes/quotaBurn.test.js: each rejection asserted by its details[].path, plus in-family and unclassifiable pins accepted.
  • server/services/quotaBurnJobs/providerPick.test.js: an unpinned step resolves inside its own family and never falls back to another's subscription.

Closes #6377

…tion overrides (#6377)

A burn step used to BE its work: a copied prompt in a free-form params bag keyed
by a quota-only jobType, with no durable link to the scheduled task it came from.
Preset identity was never persisted at all — server and client both re-derived it
by comparing prompt text, so editing one word of a shipped prompt orphaned the
step.

A step now stores a discriminated reference — a built-in scheduled task type plus
the managed app it targets when the type requires one, or a custom scheduled job
id whose own app scope is authoritative — alongside an explicit per-invocation
overrides bag (provider / model / effort / run params). An unset override inherits
the task's saved settings, and nothing written on the step ever edits the schedule.

Old jobType plans still load with every setting intact; they normalize to a
retained-but-unavailable step carrying a migration reason rather than being
guessed into a reference. The existing executor is untouched, so installs keep
burning until #6381 converts them. Dangling, disabled, wrong-scope and ineligible
references resolve to the same retained-unavailable shape against a catalog passed
in by the caller, so normalization stays pure and the verdict never reaches disk.

Zod now rejects a reference missing a required target app, a malformed custom job
id, a step naming two kinds of work or none, and a provider pin that belongs to
another quota family — each naming the offending field, so a plan can no longer
save itself into spending the wrong subscription.

Rolls in a supporting move: the scheduled-task target-scope vocabulary drops from
services/taskScheduleRegistry.js (which re-exports it) to lib/taskTargetScope.js,
because server/lib may not import upward into server/services.

Part of #6372.
…e existence checks (#6377)

Reviewer caught the doc overstating the empty-catalog guarantee: target scope is
a property of the reference itself, so a step missing a required app is broken
with or without a loaded catalog and is reported either way. Only existence,
enabled state and eligibility are withheld. Behavior unchanged; the contract and
its test now say what the code does.
@atomantic
atomantic merged commit 7fcb062 into main Sep 6, 2026
12 checks passed
@atomantic
atomantic deleted the claim/issue-6377 branch September 6, 2026 15:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Model a Quota Burn step as a scheduled-task reference plus per-invocation overrides

1 participant