Skip to content

Weekly Pace: pick which weekly bucket it measures - #10

Merged
pcvelz merged 2 commits into
pcvelz:mainfrom
alexbartok:weekly-pace-source
Sep 25, 2026
Merged

pcvelz merged 2 commits into
pcvelz:mainfrom
alexbartok:weekly-pace-source

Conversation

@alexbartok

Copy link
Copy Markdown

Weekly Pace can measure one model's weekly bucket instead of only the overall seven day one. s in the items editor cycles weekly / Sonnet / Opus / Fable. Sources derive from WEEKLY_MODEL_USAGE_BUCKETS.

Pace: [░░░█|░░░░] D3/7 -23% │ Fable Pace: [░░░█|░░░░] D3/7 -23%

Omitting source renders byte identical output to before.

Also fixes two existing bugs

Usage lock held open. FABLE_USAGE_FIELDS was hand-listed, directly under the comment saying per-model entries derive from WEEKLY_MODEL_USAGE_BUCKETS so they cannot drift. A required weeklySonnetUsage or weeklyOpusUsage that the account never reports was never satisfiable, so clearUsageLock() never ran, the 30 second lock survived, and every second render returned {error: 'timeout'}. Widgets that check data.error before their own field then showed [Timeout] permanently. weekly-sonnet-usage and weekly-opus-usage hit this today.

Phantom verdict. getUsageApiBucketUtilization(null) returns 0, and usage-types.ts warns the flat seven_day_* keys return null even for models with real usage. Pace printed Opus D4/7: Underusing -50% off that. It now applies the rule from isPlaceholderUsageApiLimit: 0% with no reset of its own is a placeholder, not a reading.

Notes

  • A per-model source with no reading shows [No data] rather than hiding, which would undo v2.4.9. The overall bucket still hides itself, unchanged.
  • The saved source id is an explicit paceSourceId, not modelDisplayName.toLowerCase(). That string tracks the API and would break saved configs if retuned.
  • The missing-required-fields lock test moved to extraUsageEnabled, which is still genuinely unresolved.
  • resolveWeekly{Sonnet,Opus,Fable}UsageWindow are now special cases of resolveWeeklyPaceWindow.
  • Version 2.4.14 with a README entry.

Checks

2154 tests, was 2123. Lint and tsc --noEmit clean. scripts/test-inventory-gate.sh reports no lost tests. New coverage: unmocked resolvers, off hours with a per-model reset, Sonnet and Opus render paths, placeholder zero, absent bucket, raw value mode, lock release for all three buckets.

The widget always measured the overall seven-day bucket. A new `source`
metadata value (weekly / sonnet / opus / fable) picks a per-model bucket
instead, derived from WEEKLY_MODEL_USAGE_BUCKETS so a new model bucket
gets a pace source without another table to update.

- `s` in the items editor cycles the source; the model name shows as
  editor modifier text and prefixes the rendered label ("Fable Pace: ").
- usage-prefetch resolves Weekly Pace requirements per item, so a
  fable-source widget fetches weeklyFableUsage instead of weeklyUsage,
  and accepts the overall weekly reset when the model bucket has none.
- Omitting `source` keeps the previous fields, resolver, and labels, so
  existing configs render unchanged.
Follow-up to the source option, plus a pre-existing bug it made reachable.

Conclusive-absence rule now covers every model bucket. It was hand-listed
for Fable only, directly under a comment saying per-model entries are
derived from WEEKLY_MODEL_USAGE_BUCKETS so they cannot drift. A required
weeklySonnetUsage/weeklyOpusUsage the account never reports could never
be satisfied, so clearUsageLock() was never reached and the 30-second
in-flight lock survived: every second render returned {error: 'timeout'},
and widgets that check data.error before their own field showed
[Timeout] indefinitely. Also fixes the existing weekly-sonnet-usage and
weekly-opus-usage widgets.

No phantom pace verdicts. A present-but-null legacy bucket parses to
utilization 0 with no resets_at (sirmalloc#343). Pace now applies the same
placeholder guard isPlaceholderUsageApiLimit already applies to limits[],
so it no longer states "Opus D4/7: Underusing -50%" about a bucket the
user may be actively burning. A per-model source with no usable reading
renders [No data] rather than hiding, so a mis-set source stays visible;
the overall bucket still hides itself, unchanged.

Stable source ids. The persisted `source` value came from
modelDisplayName.toLowerCase(). That string exists to match
limits[].scope.model.display_name and may be retuned when the API
changes, which would silently invalidate every saved config. It is now
an explicit paceSourceId on the bucket.

One code path. The overall source no longer branches around
resolveWeeklyPaceWindow, and the prefetch requirements have one shape for
every source. The branch existed only to keep existing test mocks alive;
those now spy on the resolver the widget actually calls.

Tests: resolveWeeklyPaceResetAt/resolveWeeklyPaceWindow are now executed
rather than mocked away, off-hours is covered with a per-model reset,
Sonnet and Opus render paths are covered, and the lock release is
asserted for all three buckets. The existing missing-required-fields lock
test keeps its coverage on extraUsageEnabled, which is still genuinely
non-conclusive.
@pcvelz
pcvelz force-pushed the main branch 2 times, most recently from d75dd47 to 18afe8d Compare September 10, 2026 11:02
@pcvelz

pcvelz commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Good catch on the lock bug. I traced it on main and it reproduces exactly as you describe: USAGE_WIDGET_REQUIREMENTS requires weeklySonnetUsage for the Sonnet widget, hasRequiredUsageField only forgives the two hand-listed Fable fields, so on an account that never reports a Sonnet limit clearUsageLock() at usage-fetch.ts:811 is unreachable and every second render comes back [Timeout]. Deriving the set from WEEKLY_MODEL_USAGE_BUCKETS is the fix. That part I'd take on its own.

Two things before this goes in.

The resolvers didn't actually consolidate. The description says resolveWeekly{Sonnet,Opus,Fable}UsageWindow are now special cases of resolveWeeklyPaceWindow, but all three are still verbatim in usage-windows.ts:93-105 and the new function sits beside them as a fourth path:

export function resolveWeeklyFableUsageWindow(usageData, nowMs = Date.now()) {
    return getWeeklyUsageWindowFromResetAt(usageData.weeklyFableResetAt ?? usageData.weeklyResetAt, nowMs);
}

// ...and separately
export function resolveWeeklyPaceWindow(usageData, source, nowMs = Date.now()) {
    return getWeeklyUsageWindowFromResetAt(resolveWeeklyPaceResetAt(usageData, source), nowMs);
}

Collapsing the three into resolveWeeklyPaceWindow(data, getWeeklyPaceSource('fable')) is my preference. Keeping the named resolvers is fine too, just reword the claim.

[No data] uses the pendulum label in text mode. WeeklyPace.ts:202 passes getPendulumLabel(source) unconditionally, so text mode renders Fable Pace: [No data] instead of Fable [No data]. Branch on displayMode.

Also worth a line in the notes: a real 0% early in the week on a bucket without its own resets_at now shows [No data]. Right trade, but it should be stated.

Ignore the conflicts, they're our release churn. I'll rebase, retarget to 2.4.16, and move the comment block above hasTrustworthyPaceUsage (it describes getNextWeeklyPaceSourceId).

@pcvelz pcvelz self-assigned this Sep 25, 2026
@pcvelz
pcvelz merged commit df4c444 into pcvelz:main Sep 25, 2026
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.

2 participants