Skip to content

fix(quota): gate a family model on the higher of its own bucket and the shared weekly - #191

Open
ak2k wants to merge 1 commit into
KarpelesLab:masterfrom
ak2k:fix/weekly-gate-shared-bucket
Open

fix(quota): gate a family model on the higher of its own bucket and the shared weekly#191
ak2k wants to merge 1 commit into
KarpelesLab:masterfrom
ak2k:fix/weekly-gate-shared-bucket

Conversation

@ak2k

@ak2k ak2k commented Aug 24, 2026

Copy link
Copy Markdown

The diagnosis, measurement, and proposed rule here are johnnymo87's, from #175. This implements their offer; read #175 first. Refs #175. If they would rather land their own patch, say so and this closes without hard feelings.

The bug

_governingWeekly returns the family bucket alone whenever it is reported, so a Fable request on an account at unified7d = 1.00 with unified7dFable = 0.20 keeps being selected. #175 measured that family spend meters into both buckets, so this is a one-way ratchet: once the shared weekly caps, family requests are the only ones still admitted and each pushes it further past 1.0. We then watched a production account do exactly this at 99% shared utilization.

The fix

The governing weekly for a family request is the higher of the reported family bucket and the reported shared bucket. null still means unreported, never zero; behavior is unchanged when the governing key already is unified7d; _modelWeeklyExhausted deliberately stays family-only, since "can this account serve this family at all" is a different question from "is it near any cap". The status Models row and the TUI tag read the same gate, so the display agrees with routing once the gate is fixed; on stock they only agreed because both shared the defect.

The recovery time beside a blocked mark is the latest blocking weekly bucket's reset: a block produced by a maximum clears only when every blocking bucket has rolled.

Behavior change

Deliberate, and worth a release note: an account near its shared weekly cap becomes unavailable for family models sooner. That is the bug fix, not a regression, per #175's own framing.

Measured against stock: 5,000 seeded scenarios (2 to 5 accounts, quota levels drawn across the threshold, fixed 40-request mixed schedules), walking both trees in lockstep. 405 diverge, 4,595 identical, zero cases where this branch served a request stock refused. Every divergence falls into two classes: the stricter gate barred the account stock chose (358), or every account is now barred and the one revalidation probe was already spent (47). On the weekly buckets the family gate is now at least as strict as the shared one: over all 81 combinations of the two bucket levels, zero cases where an account can serve Fable but not Opus.

Tests

525 before, 544 after, 0 fail; npm run lint clean. #175's six-row matrix is covered one test per row, plus the null cross-product, the probe ranker inheriting the fix, _modelWeeklyExhausted not inheriting it, the reset staying with the governing window, and six display cases through the real renderer and TUI row. With the fix reverted and the new tests in place: 536 pass, 8 fail. Upstream's own suite does not cover this path: the same reversion with the new file parked passes 525/525.

Limitations
  1. A probe can still be aimed at an account whose shared weekly is spent (deliberate: _modelWeeklyExhausted stays family-scoped).
  2. The soonest-reset tiebreak still ranks by the family window.
  3. The recovery time considers the two weekly buckets; an account blocked longer by the five-hour bucket shows the weekly clearing time. Pre-existing on stock, occurs only while the weekly window is within about five hours of rolling; closing it is gated differently and out of this change's scope.

This collides with #190 and the #192 stack above it in src/account-manager.js and src/model.js (one import name and one adjacent export each); whichever lands second rebases the same day.

…he shared weekly

Diagnosis, measurement and the proposed rule are johnnymo87's in issue KarpelesLab#175.
This implements it.

RELEASE NOTE. An account past its shared weekly cap is now unavailable for Fable
and Sonnet traffic, where before it kept serving them. Operators will see such an
account drop out of the pool sooner, and see fewer accounts reading above 1.00 on
the shared weekly bucket.

Family spend meters twice, once in the family bucket and once in the shared one,
so the two are not independent. Reading the family bucket alone let an account at
unified7d 1.00 with unified7dFable 0.20 keep taking Fable, and each such request
pushed the shared bucket further past its cap. Once the shared bucket is spent,
family requests are the only ones still admitted, which makes it a one-way
ratchet rather than a bounded overshoot. KarpelesLab#175 measured the coupling directly:
per-request increment on the shared bucket [+1.14e-4, +5.21e-4] against
Fable-only traffic, P about 0.999.

The rule is the maximum of the two REPORTED buckets. null keeps meaning
unreported and never becomes zero: a max invites flooring an absent bucket at 0,
and 0 reads as empty, which is the opposite of unknown and errs toward keeping
the account in service. If one side is absent the answer is the other; if both
are, the answer is null and the gate leaves the dimension alone.

The change is confined to the VALUE. _governingWeeklyReset still keys off the
governing bucket, so value and reset can now name different buckets. Nothing
here pairs them: both readers of the reset, _pickBestAvailable and
_pickLeastLoaded, use it as a ranking tiebreak among accounts that already
passed _isAvailable, and neither divides a headroom by it. Maxing the reset
would pair one bucket's level with another bucket's clock.

_modelWeeklyExhausted stays family-only, deliberately. It answers whether an
account can serve a family at all, not whether it is near a cap. Folding the
shared bucket in would skip accounts for probes they could have served, and a
probe is how a stale cached utilization is corrected, so it would harden the
state it exists to escape.

THE DISPLAYS FOLLOW THE GATE. KarpelesLab#175 names the status Models row: KarpelesLab#172 fixed its
blocklist half and left the shared-weekly half reading the family bucket alone,
so the row printed Fable OK on an account the router had already refused. The
TUI's blocked tag (issue KarpelesLab#85) had the same defect. Both now read
gatingUtilization rather than deriving the rule a second time.

Where a mark and a recovery time appear together, the time is the LATEST reset
among the two WEEKLY buckets currently over the threshold. That half of the mark
comes from a maximum, so it clears only once both weekly blockers have rolled;
showing the family reset beside a mark the shared weekly produced told an
operator that a week-long block clears in an hour. An unreported reset among the
blockers leaves the time unknown and the cell says nothing, since a
known-but-earlier one would understate it.

The shared 5h bucket stays outside that set, which leaves a narrow pre-existing
gap: it can raise the mark while contributing no reset, so an account blocked
longer by 5h than by its weekly buckets still shows the weekly clearing time.
With unified5h 0.99 resetting in 10h and unified7dFable 0.99 resetting in 1h,
both stock and this branch render `Fable X 1h` for a block that lasts 10h.
Closing it means restructuring the cell, since the time is gated on the weekly
condition and a 5h-only block suppresses it entirely, so it is left as it is and
ledgered rather than fixed here.

Tests: test/weekly-gate.test.js, 19 cases. The matrix KarpelesLab#175 proposed, one per row
(shared-over/family-under, the reverse with gating still model-scoped, both-low,
family-reported-with-shared-null, both-null, and a model whose governing bucket
already is unified7d), a reported 0 against an absent bucket, the null rule over
the whole 4x4 cross product, Sonnet on both arms, the probe ranker inheriting the
fix, _modelWeeklyExhausted not inheriting it, the reset staying with the
governing window, and six display cases driven through the real renderer and the
real TUI row.
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.

1 participant