Repository navigation
Description
Decision needed (one line): Adopt the shared-contract design — one derived QuotaSnapshot signal, gates filter candidates before weighting, the burn-rate controller writes only through the existing tuner Adjustment channel — or keep #566/#567/#568 as three independent features with separate quota reads and config?
Summary
Three features filed the same day — #566 (quota-aware session placement), #567 (time-based weighting), #568 (native quota intelligence / burn-rate controller) — each presuppose a quota signal and a shared weight model that none of them defines, and each sketches a different config surface to do it. This design is the missing contract: (1) a QuotaSnapshot value derived from data the fork already owns (ModelPlan windows + windowReport() usage), with one staleness rule where unknown/stale is never treated as headroom; (2) a layering rule — gates (#566) filter candidates before weighting, weights rank survivors through the existing weightOf layers, the controller (#568) writes only expiring Adjustments like the tuner already does, and credential failover (#6) stays orthogonal inside an attempt; (3) per-layer weight attribution in the decision log so "why is this model ranked here?" is answerable for all three.
This extends — does not duplicate — the existing thread: #566, #567, #568, #6 stay as the implementation vehicles for phases 3–5. It also closes a roadmap gap: ROADMAP.md → "Routing, cost and plans" lists #6–#10 but has no row for #566/#567/#568. Full design doc: files/designs/opencode-quota-signal-routing.md in the keep-the-four-repos-healthy goal workspace (registered in files/potential-projects.md).
Current state (evidence)
packages/core/src/model-route.ts→weightOf()already composes layers in order: scheduledWeightRules (time windows incl.from/until— most of Time-based model/provider weighting: peak/off-peak profiles and temporary priority windows #567's mechanism exists) → userpreferoverrides → learned tuner adjustments (non-fixedroutes only) → quota demotion, a binaryDEMOTED_FACTOR = 0.01after quota errors (quotaCooldownMs: 900_000). Quota enters selection only as this reactive demotion.- Quota knowledge is reporting-only:
packages/core/src/model-plan.tsholds plans +QuotaWindows;packages/core/src/model-usage.ts→windowReport()derivespercent/remaining/resetRulefrom recorded usage. The selection path never reads it. packages/core/src/model-route-tuning.ts→suggest()already produces expiring, id-replacedAdjustments (auto:ids) installed viasetAdjustments/addAdjustment, gated by the route'sautonomylevel — the channel the controller should reuse.- The decision log records final effective weights per candidate but not which layer produced them, so neither Time-based model/provider weighting: peak/off-peak profiles and temporary priority windows #567's inspection acceptance nor Native quota intelligence: burn-rate estimation and dynamic reprioritization to consume quota before reset #568's audit acceptance can be met from the log today.
- The three issues' own words: Quota-aware session placement: filter by quota % and session type (root vs subsession) #566 "using the existing quota sources; a stale read… is treated as unknown"; Time-based model/provider weighting: peak/off-peak profiles and temporary priority windows #567 "should share the weight model"; Native quota intelligence: burn-rate estimation and dynamic reprioritization to consume quota before reset #568 "should adjust the same weight model they use rather than inventing a parallel one" — followed by three incompatible config sketches (
quota.gates.*,schedules:/boosts:, a controller ladder), none matching the code's actual config shapes.
Proposal (point of view)
- One snapshot, one staleness rule.
QuotaSnapshot { planID, window, providerID, percent|null, remaining|null, resetAt|null, observedAt, stale }, derived purely fromModelPlan+windowReport()in v1 — no external polling.percent === nullorstale⇒ unknown: never headroom, never a wall; each consumer's configured fallback applies (gates default to admit = today's behaviour). - Gates filter, weights rank. Quota-aware session placement: filter by quota % and session type (root vs subsession) #566's gates run as a candidate filter in
nextTargetbefore weighting — a refusal, not a weight of 0 — so the decision log can say "refused" and sticky/continuity logic can't resurrect a gated target. The fleet rule (burn to 100%, then stop, never PAYG) is a refusal and is modelled as one. - The controller is a second adjustment producer, beside the tuner, with its own id prefix, expiring adjustments, observe mode = compute + log without installing, act mode gated per-route by the existing
autonomylevels. The more restrictive adjustment wins on collision; both are logged. - Per-layer attribution in the
decisiondetail: weight after each layer (base,scheduled,prefer,adjustment,demotion) plus gate verdicts — one inspection feature serving Time-based model/provider weighting: peak/off-peak profiles and temporary priority windows #567 and Native quota intelligence: burn-rate estimation and dynamic reprioritization to consume quota before reset #568. - feat(routing): automatic multi-credential failover on 429 quota exhaustion via credential.activate #6 stays orthogonal: credential failover inside an attempt on a selected target; it neither reads snapshots nor writes weights.
- Alternatives that lose, with reasons, are in the design doc: per-feature quota reads/config; external polling first; gates-as-weights; a new controller weight layer above
weightOf.
Phased breakdown
- Phase 1 —
QuotaSnapshotderivation + staleness contract. Pure module overModelPlan+windowReport()data; unknown/stale rules; unit tests (missing limit, missingwindowHours, stale observation, threshold boundaries). No selection behaviour change. (Filed as a sub-issue of this design issue and enqueued.) - Phase 2 — Per-layer weight attribution in the decision log. Traced weight computation; traced weights must equal
weightOfweights; surfaced via existing route-review tooling. No behaviour change. (Filed as a sub-issue of this design issue and enqueued.) - Phase 3 — Quota gates on the snapshot. The existing Quota-aware session placement: filter by quota % and session type (root vs subsession) #566, re-scoped onto this contract once the decision lands. (Not re-filed — Quota-aware session placement: filter by quota % and session type (root vs subsession) #566 is the vehicle.)
- Phase 4 — Controller as adjustment producer. The existing Native quota intelligence: burn-rate estimation and dynamic reprioritization to consume quota before reset #568; its telemetry acceptance is narrowed by Phase 1. (Not re-filed.)
- Phase 5 — Time-based model/provider weighting: peak/off-peak profiles and temporary priority windows #567 close-out. Verify
WeightRulewindows/boosts against Time-based model/provider weighting: peak/off-peak profiles and temporary priority windows #567's acceptance; inspection surface on Phase 2's attribution; retire its separate config sketch. (Not re-filed.)
Open questions (in the design doc, deliberately not filed as issues)
- Which reset rules can yield a concrete
resetAt, and does the controller need it, or is percent + remaining enough? - Per-credential quota dimension once feat(routing): automatic multi-credential failover on 429 quota exhaustion via credential.activate #6 lands, or is provider-level percent the right gate granularity?
- Snapshot max-age default (the wider fleet uses a ≤60-minute quota read for placement decisions).
Non-goals
- No PAYG routing, ever (inherited from Quota-aware session placement: filter by quota % and session type (root vs subsession) #566/Native quota intelligence: burn-rate estimation and dynamic reprioritization to consume quota before reset #568 and the fleet pool rule).
- No change to
ModelRouteSchedulesemantics. - No cross-repo quota sync with developing-today/code's provider-quota registry in v1.
Related
- Quota-aware session placement: filter by quota % and session type (root vs subsession) #566, Time-based model/provider weighting: peak/off-peak profiles and temporary priority windows #567, Native quota intelligence: burn-rate estimation and dynamic reprioritization to consume quota before reset #568, feat(routing): automatic multi-credential failover on 429 quota exhaustion via credential.activate #6 (the features this contract serves), fix(core): route_usage silently omits plan quota windows that have no windowHours — monthly/reset-rule windows never appear #550 (windowReport omits windows without
windowHours— snapshot must surface those as unknown) - Files:
packages/core/src/model-route.ts(weightOf,nextTarget),packages/core/src/model-route-tuning.ts,packages/core/src/model-plan.ts,packages/core/src/model-usage.ts(windowReport),ROADMAP.md
Acceptance criteria
- The decision in the first line is recorded on this issue (adopt the shared contract, or keep the three features independent), with the decider named.
- If adopted: Phases 1–2 land as their sub-issues specify, and Quota-aware session placement: filter by quota % and session type (root vs subsession) #566/Time-based model/provider weighting: peak/off-peak profiles and temporary priority windows #567/Native quota intelligence: burn-rate estimation and dynamic reprioritization to consume quota before reset #568 are re-scoped onto the contract (snapshot + layering) in their bodies.
- If not adopted: the rationale is recorded here and the three issues' "shared weight model" expectations are removed or replaced, so the thread does not stay dangling.
- No code changes land under this design issue itself.
Activity
developing-today commentedon Oct 27, 2026
OPENCODE-95
dezren39 commentedon Oct 27, 2026
🤖 Agent note · filed
dezren39 commentedon Oct 29, 2026
DREW DECISION (2026-10-29, main chat): ADOPT the shared QuotaSnapshot contract — one quota read shape consumed by the placement features (#566, #567, #568), not three private reads. Drew's note: this is what should have been done to begin with. #566 is unblocked by this ruling.
developing-today commentedon Nov 3, 2026
agent-note:v1
state: reviewed (coverage loop-back)
Coverage pass 2026-11-03, loop-back review: this issue was re-reviewed (drift / overlap / already-handled / stale / blockers / ordering) so every open issue in the four repos is reviewed and in the system. Verdict: still live; in the system as task 7041 (triage, queued, priority 50) in repo-health-opencode-pilot. Labels audited and synced. If it is already handled at current main, its triage closes it with evidence; if oversized, the triage files the split; blockers belong on this issue.
Found via: coverage loop-back 2026-11-03
dezren39 commentedon Nov 4, 2026
Triage (task 7041): design state confirmed — DECIDED, not awaiting a ruling. Drew ruled on this issue on 2026-10-29: ADOPT the shared
QuotaSnapshotcontract ("one quota read shape consumed by #566, #567, #568, not three private reads"), explicitly unblocking #566.Routing from here:
files/designs/opencode-quota-signal-routing.md).blockedand Quota-aware session placement: filter by quota % and session type (root vs subsession) #566 carriesqueue/blocked+user/escalated. If that block rests on this design decision, the condition no longer holds — the ruling is above. The Quota-aware session placement: filter by quota % and session type (root vs subsession) #566 triage owner should re-check its blocker against the 2026-10-29 comment.This epic stays open as the tracker for the contract; it closes when the phase vehicles land, not on the decision alone.
dezren39 commentedon Nov 4, 2026
Decision of record: ADOPT — recorded late. Drew ruled ADOPT on this design on 2026-10-29 (adopt the shared QuotaSnapshot contract and weight layer for #566/#567/#568, Phase 3 scope as designed). The ruling was never transcribed onto the issue, so #566 sat
user/escalatedand its queue task 7038 sat blocked waiting on a decision that had already been made. That blockage is cleared now: #566's escalation labels are removed and task 7038 is requeued. This issue stays open as the epic tracker for the three consumers.dezren39 commentedon Nov 8, 2026
Partial verification — staying open (2026-11-07 done-task-only audit, tracking: developing-today/code#7016).
Queue task(s) [7041] were marked done, and some work did land:
No merged change closes this issue's full scope, so the done status is not treated as completion. Remainder re-queued as triage task 11490.
dezren39 commentedon Nov 8, 2026
🤖 Agent note · triaged
queue/queued+status/in-progressand its triage task was requeued when the ruling was transcribed, so it is live in the queue system, not lost.