menubar: early quota resets for every provider, not just Claude - #1339
Open
ozymandiashh wants to merge 6 commits into
Open
ozymandiashh wants to merge 6 commits into
ozymandiashh wants to merge 6 commits into
Conversation
The detector shipped wired to one provider. Codex's goodwill resets are the clearest instance of the thing it was built to catch — a vendor restoring a rate-limit window ahead of schedule — and the refresh lifecycle saw every one of them and dropped it, because `detectEarlyResets` was only ever called from Claude's snapshot capture. Each Capacity Dock provider now hands the monitor its own windows as its fetch succeeds, off the `QuotaSummary` the popover already draws. Claude keeps its existing call, whose window keys are the snapshot store's and must not move; every other provider identifies a window by the label its adapter already shows, slugified, which is the only stable name these adapters give one. Nothing about the thresholds or the guards moved. A window still needs a reset time and a validated length to produce anything, so Claude and Codex announce today and Antigravity, ClinePass, Copilot, Cursor, Gemini, Grok, Kimi Code and Z.ai run the same path in silence until their adapters carry a length. The monitor's record was already per provider; the band, the dedupe, the baseline and now the history summary are all scoped with it, so an early reset on one provider cannot move another's state, and a disconnect drops only its own. The 30-day pattern needs cycle times to read, and only Claude has a snapshot file, so the monitor keeps a small per-window ledger of observed cycles beside its baseline for everyone else. The field is optional, and the defaults key is untouched, so a record written by the build that shipped this still decodes and a Claude reset already announced is not announced again after the update. `usageName` no longer doubles the noun, which it would have for Codex's "Monthly usage limit" row.
Every false-positive guard from the original suite is now parameterised over Claude and Codex, alongside both signals and the copy each produces, so a change that quietly re-narrows the detector to one provider fails here. Four cases the old suite could not have: an early reset on one provider leaves the other's baseline, band and dedupe record untouched and each is announced in its own name; a Codex reset announced once is not announced again across a relaunch; a state record written in the shape and under the exact key the feature first shipped with is still read, so an event the user has already been notified about stays silent — paired with the same record carrying nothing announced, which must post, or the silence would also be produced by a record that can no longer be found or decoded; and the cycle ledger keeps one entry per cycle rather than per fetch, which is what lets a provider with no snapshot file have a 30-day pattern at all. Two more drive the AppStore rather than the detector, because that is where the Claude-only scope actually lived: a dock provider's second fetch produces a band named after it while Claude's stays empty, and a provider whose windows carry no duration stays silent.
The design doc now records that every dock provider runs the same detector under its own record, that a window needs both a reset time and a validated length to say anything, and names the eight live adapters that report the first but not the second and are therefore silent. Claude's provider doc says its snapshot-backed history is the exception, not the rule. Refs getagentseal#725
Brings in getagentseal#1330's menubar localization, which routed the early-reset notification, band and history copy through L(...). One conflict, in EarlyQuotaResetFormat.usageName, where both sides rewrote the same function. Main replaced the suffix rule with a switch on the four known English window names, each routed through its own catalog key, falling back to L("%@ usage", windowNoun(name)). This branch had stopped that fallback doubling the noun for Codex's "Monthly usage limit" row. Both survive: the four routed cases are main's, unchanged, and the default branch keeps the no-doubling rule before routing, so an unknown name whose noun already ends in "usage" reads through instead of becoming "monthly usage usage". No catalog key changes: the rule only decides whether "%@ usage" is applied. Mixing implicit and explicit returns in one switch does not compile, so every case now returns explicitly.
getagentseal#1330 translates the early-reset copy at render, keyed on the four English window names Claude produces, and lets any other name read through untouched. Before this branch no other name could reach that copy. Now every provider's window does, named by windowName(forLabel:), which composes the label with " limit" in English. That word is ours, not the provider's, and was the one piece of new copy on this branch that never reached the catalog: limitName's default returned it verbatim, so a zh-Hans build said "monthly usage limit". limitName's default now routes it as "%@ limit", the same shape usageName already uses for its own default, with the provider's noun substituted and left untranslated. The name is still composed in English and persisted that way, as claudeWindowName(forKey:) is, so a language change never leaves a translated string in a stored event. "Weekly" and "5-hour" compose into names the known set already has, so the common Codex rows translate in full. The empty-label fallback "quota window" was the other English literal. Rather than route copy for a degenerate row, a blank label is now skipped before it is observed: it has no identity to store under, and two of them would share a key. New catalog key: "%@ limit" = "%@ limit" (en), "%@ 限额" (zh-Hans), matching the existing "每周限额" / "5 小时限额" entries.
4 tasks
CI's swift test could not build the test target. Three errors, all in this file, none visible locally because the target never compiles without the Testing module: - The two parameterised tests take EarlyResetProviderCase, which was private, so their default-access methods outranked the type they accept. The type is now internal, keeping the methods at the access every other test uses; a fileprivate type would not have helped, the methods would still outrank it. - earlyResetHistoryCaptions(for:) has a ProviderFilter and a CapacityDockProvider overload, and both have a .claude, so the bare member was ambiguous. Spelled CapacityDockProvider.claude there and on the band assertion beside it. - summary(percent:resetsIn:windowSeconds:) inherited the @mainactor suite's isolation but is called from the adapter dependencies' @sendable closures. It is a pure fixture builder, so it is nonisolated, the same shape as CapacityDockProviderQuotaServiceTests.summary(percent:). Checked by type-checking the file with swiftc in Swift 6 mode with complete strict concurrency against the built module and a stub Testing module: the unfixed file reproduces CI's nine errors at the same lines and columns, and the fixed file and all sixty non-XCTest test files together report none.
This was referenced Sep 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
resets — a vendor restoring a rate-limit window ahead of schedule — are the
clearest instance of exactly what it watches for, and the refresh lifecycle saw
every one of them and dropped it, because
detectEarlyResetswas only calledfrom Claude's snapshot capture and passed
CapacityDockProvider.claudeliterally. Every Capacity Dock provider now hands the monitor its own windows
as its fetch succeeds, off the
QuotaSummarythe popover already draws.twelve-hour dock band and the 30-day history caption are all scoped per
provider and per window: an early reset on Claude cannot move Codex's state,
and a disconnect drops only that provider's record. The
UserDefaultskey andrecord shape are unchanged and the one new field is optional, so a record
written by the build that shipped this still decodes and a Claude event a user
has already been notified about is not announced again after updating.
validated window length. Antigravity, ClinePass, Copilot, Cursor, Gemini, Grok,
Kimi Code and Z.ai run the same path and stay silent until their adapters carry
one — the same "no validated duration, no opinion" guard that was already in
the detector. No thresholds and no false-positive guards changed; only the
provider scope.
already shows, slugified, so sibling rows stay apart. Claude keeps the snapshot
store's keys.
usageNameno longer doubles the noun, which it would have forCodex's "Monthly usage limit" row.
it stays dormant for Codex until this lands.
Testing
cd mac && swift buildclean.swift test— cannot run on this machine (Command Line Tools only:error: no such module 'Testing'). Pre-existing, and true on pristinemain. The pure logic was verified with a standaloneswiftcharnessinstead, built from the real
EarlyQuotaReset.swift,EarlyQuotaResetMonitor.swiftandQuotaPace.swiftwith minimal stubs forSubscriptionSnapshotandUpdateNotifier, mirroring the new suite:107 checks, 0 failures. The test file itself is syntax-checked with
swiftc -parse; CI is the first place it type-checks and runs.defaults key, per-provider state scoping, the cycle ledger's
one-entry-per-cycle rule, label slugification, the window-name rule, and
the ledger field's decode compatibility (twice, including a variant patched
to compile).
cross-provider isolation, Codex dedupe across a relaunch, the stored-record
compatibility case (paired with a not-yet-announced control, so a record
that could no longer be found or decoded fails it), and two
AppStorecases that drive a dock provider's refresh end to end.
Not verified: no notification was delivered and no dock band or hover caption was
seen in a running app. There is no Codex snapshot store to replay the monitor
over the way #1329 replayed Claude's.
Refs #725.
Rebased onto main after #1330 (menubar i18n): the no-doubling rule now lives in the
L(...)-routed fallback ofusageName, and non-Claude window names reach the catalog through a new%@ limitkey (en + zh-Hans). Main's localization scanner passes on this branch (no unrouted literals, no orphaned keys).