Files
magpie/docs
forever dfde3acafc provider, gateway: a Claude /usage reset with no year is read within its window, so a reading a few days old no longer benches an account for a year (#1212)
* provider: a Claude reset read in the hour the clocks repeat, or the night they skip it, is the coming one, and a Dec 31 with no year read just after New Year is last year's

claudeResetTime turns /usage's reset text into the ResetsAt of the
"5 hours" and "7 days" windows, which smart routing orders accounts by and
rests a full account until.

- Where the clocks go back and read a time twice, time.Date gives either
  instant: the first west of UTC, the second east of it. Each reading is
  now considered (nextReading), and the one not yet past is taken.
  Before, at 2026-11-01 01:20 EST (the second 01:20), "Nov 1, 1:30am
  (America/New_York)" was 01:30 EDT, 50 minutes past, so a full window
  read as renewed; "1:30am" was 24h10m away instead of 10m. In Berlin at
  02:20 CEST on 2026-10-25, "2:30am" was 02:30 CET, 1h10m away instead of
  10m (Sydney and Lord Howe alike).
- A date with no year is in the first of last year, this year and next
  that is less than a day past, as the doc comment already said. Before,
  only this year and next were tried, so at 2027-01-01 00:00:05
  "Dec 31, 11:59pm" was 2027-12-31, 8760h away, and a 7-day window that
  had just renewed read as renewing in a year.
- A time alone the clocks skip today is the next day that has it,
  rebuilt from the clock read rather than from the normalized time.
  Before, the evening after New York skipped 02:00-02:59, "2:30am" was
  tomorrow's 1:30am; at 23:31 before Santiago skips midnight, "12:30am"
  was 23:30 that night, a minute past, instead of 00:30 the day after
  tomorrow.

What Claude Code prints, from the /usage formatter in the 2.1.290 binary:
every line of `claude -p /usage` carries a date, with the year only when
it isn't this one, so the New Year case needs /usage run just before
midnight and parsed after, and the repeated hour shows in the dated form.
Whether "at" or a comma joins date and time follows the platform's ICU,
and both are parsed. A time alone is the older form, which the TUI still
prints for the session within a day.

No other parser in internal/provider fills in a missing date or year:
zhipuTime and mimoTime read full dates in UTC+8, and the rest read
RFC3339 or Unix times.

Verified:
- TestClaudeResetTimeAroundClockChanges and TestClaudeResetTimeAtNewYear
  are new. With upstream's claudeResetTime put back, 15 rows fail, e.g.
  `"Nov 1, 1:30am (America/New_York)" at 2026-11-01 01:20:00 -0500 EST:
  2026-11-01 01:30:00 -0400 EDT true (in -50m0s)` and `"Dec 31, 11:59pm
  (America/New_York)" at 2027-01-01 00:00:05 -0500 EST: 2027-12-31
  23:59:00 -0500 EST true (in 8759h58m55s)`; the 16 controls pass. With
  the change, all pass, as does TestParseClaudeUsage, also with
  -race -count=20.
- An oracle that lists every instant a zone shows a clock at was swept
  over 518 zones, every transition 2025-2030, 85M calls: no deviation in
  anything Claude Code can print. What remains is Feb 29 in a non-leap
  year and dated times the clocks skip, both unchanged from upstream.
- Under a temporary HOME with GOPATH, GOMODCACHE and GOCACHE pinned:
  go vet ./..., go build, GOOS=linux go build -tags nogui,
  GOOS=windows go build, go test -tags nogui ./... all pass.
- Not run: claude -p /usage on a real account across a clock change or
  New Year.

* provider: a Claude reset further ahead than its window can reach is the one already past, so a /usage reading days old no longer rests an account for a year

claudeResetTime now gets the window's length (5 hours for "Current
session", 7 days for "Current week ...") and never reads a reset further
ahead than that plus claudeResetSlack (2h, for this machine's clock being
behind Anthropic's; Claude Code prints the reset from Anthropic's instant
with seconds dropped, so never later).

- A date with no year is in the first of last year, this year and next
  that is less than a day past and within the window. Before, it was the
  first less than a day past: read on Oct 5, a week's "Oct 1, 3pm" was
  2027-10-01, 8667h away, and routing (candidate.full, Allowance.Full)
  rested a full account that long. Now it is 2026-10-01.
- Where no year fits, the latest year where it is already past is taken,
  rather than dropping ResetsAt. The date read fine, and the window's
  length rules out every later year, so the reset has passed and the
  window has renewed, which is what elapsed() does with a past reset.
  Dropping it would keep a 100% window with no time to clear it: Pooled
  counts a full model window with no reset as out, so Opus on the account
  stayed refused until the next read, and the card showed 100% with no
  reset.
- A 5-hour window's date 6 days ahead ("Oct 11, 3pm" on Oct 5) is likewise
  last year's and reads as renewed; a week's keeps Oct 11.
- A time alone (the older form) is the next one within the window, else
  the latest day where it is already past: at noon, "11am" is today's for
  the session (was tomorrow's, 23h on) and tomorrow's for the week.
- A fresh /usage read now goes through elapsed() as a kept one does, so a
  reset it gives as past reads as renewed at once. Before, only the next
  look did, and the switch (NextLogin's usedPast) and quota alerts saw the
  fresh 100%.
- Dates with a year are unchanged.

Tests: TestClaudeResetTimeWithinItsWindow covers the cases above, the
inclusive bound (exactly span+slack is kept, a minute more is not, for a
date and a time alone), a week across New York's fall-back, and
claudeWindows' fresh and kept reads of a 4-day-old reset (Used 0, no
reset, Full zero). The DST rows and the other New Year rows pass
unchanged, at the week's length. Changed existing rows, each of which
asserted a reset further ahead than any window reaches: TestParseClaudeUsage's
"Current week (Opus) ... Jan 2 at 2pm" read Oct 1 (now 2026-01-02), the
#631 "Jan 2, 9:05am (UTC)" row (2026-01-02), and the New Year "Dec 30,
11pm" read Jan 1 01:00, 26h past (2026-12-30, not 2027-12-30). The #631
format rows are read on Oct 3 instead of Oct 1, so "Oct 9, 2:59pm" stays
within its week. Fixtures that gave a 5-hour session a reset the next day
(soon(1) at 3:30pm in claude_usage, claude_seats, claude_usage_switched;
claude_made_first in gateway; "Jul 20 at 10am" at midnight in
claude_scoped) now give one 3 hours on, as /usage would.

Fails without: with the bound removed and fresh reads not through
elapsed, TestClaudeResetTimeWithinItsWindow fails ("Oct 1, 3pm (UTC)" for
168h at 2026-10-05: 2027-10-01 15:00 (in 8667h), want 2026-10-01 15:00;
"Oct 11, 3pm (UTC)" for 5h: 2026-10-11 (in 147h), want 2025-10-11;
"read true: held until 2027-10-03 15:00"), as do TestParseClaudeUsage and
TestClaudeResetTimeAtNewYear on the changed rows. With the bound but
fresh reads not through elapsed, it fails with "read true: 5 hours
Used:100 ResetsAt:<4 days ago>".

Verified, under a temp HOME with GOPATH, GOMODCACHE and GOCACHE pinned:
go vet ./..., go build, GOOS=linux go build -tags nogui, GOOS=windows go
build, go test -tags nogui ./... (all pass), and go test -race -count=20
-run 'TestClaudeResetTime|TestParseClaudeUsage' ./internal/provider.
Not run: claude /usage on a real account read days late. Unchanged: a
week's time alone read minutes after its reset still reads as tomorrow's
(at most a day).

* gateway: a subscription account that fails with a window full rests until it renews, but no more than 8 days, so a reset time read a year off no longer benches it for that year

restAfter's default branch (a failure that isn't credit, quota, a rate
limit or verification: a 502, a sign-in error) rests a subscription
account until its fullest window renews, by c.full(now). That time is
the vendor's reading, not the failure's: Claude Code's /usage text, read
by claudeResetTime, takes a date with no year for next year when it reads
it wrong, and the account then sat out ~365 days until the user noticed
and lifted it by hand. The failQuota branch of the same function already
holds every rest, window included, to longestQuota (a week's window and a
day over); the window rest here now takes the same bound:
min(t.Sub(now), longestQuota). A window renewing within 8 days rests
exactly as before.

Why longestQuota and not the full window's own Span: Allowance.Full
returns only the renewal time, so the call site doesn't know which window
was full or how long it runs; Span is zero where a reading doesn't say
(plugin windows may not); and a failure that isn't about quota must not
sit out longer than one that is, which the quota branch already caps at
8 days. Bounding by Span would only shorten a misread 5-hour window from
8 days to 5 hours, and would have to change the quota branch with it, so
it is left for a change of its own.

Siblings: restAfterMarked is the only place that stores a rest
(restingUntil). Its other branches are bounded already: failQuota by
longestQuota (resets in the body, the noted header, the window and
Retry-After alike), failRate by longestWait and longestRateRest, credit
and verify by constants; a key never takes the default branch's window
rest (c.p.Account != nil). Every other use of a window's reset (capHeld,
Allowance.Full/Renewal/Restarts/Pace in weighing, the sink condition) is
computed again from the latest reading on each request and stores
nothing. Not changed here: cappedError's soonest renewal still becomes
the agent's Retry-After uncapped, and a stored window rest isn't lifted
when a later reading corrects the time (only OnRenewed or a manual
Unrest does), so a bad reading can still bench an account for up to 8
days. Claude's parsing is fixed in the commit before this one, and a
Claude account's window rest is lifted by a renewed reading in #1200.

docs/subsystems/gateway-routing.md says so in the runtime path; its
gateway test list already takes the new test by its TestRouting prefix.

Verified under a temporary HOME with GOPATH, GOMODCACHE and GOCACHE
pinned: go vet ./..., go build, GOOS=linux go build -tags nogui and
GOOS=windows go build pass. TestRoutingHoldsAWindowRestToLongestQuota (new): a
Claude account whose window is full with ResetsAt a year out, failing
502, rests at most longestQuota; a control renewing in 3h rests 3h.
Without the change it fails: "window read a year off: rests
8759h59m59.999682292s by window (other), want at most 192h0m0s". It
passes with -race -count=20, with TestOrderedKeepsAnAccountAt98 and
TestQuotaRestsUntilItsReset. The routing reference's gateway test set
passes.

go test -tags nogui ./... ran with the machine at a load average of
~125 on 12 cores, and three packages failed there, none on this path:
internal/davsync timed out at 10m (it passes alone in 353s);
TestSetAnswersWhileDevinAndProvidersHang fails on e95852e3 too (4 of 20
with -race); TestQuietStreamsKeptAliveEveryProtocol/chat_mid-reply (a
keepalive written before the reply already passed) failed 6, then 1,
then 0 of 20 here with -race and 0 of 60 on e95852e3. Its provider is a
key that answers, so restAfter, the only code changed, never runs in it;
it is left for a change of its own, not called a flake.
2026-10-08 06:20:03 +08:00
..