Repository navigation
fix: prevent volatile reset timestamps from causing infinite rare can… - #375
Conversation
…dy grants Normalize candy window epoch timestamps and require window-scale advancement (>= 30m for session, >= 24h for weekly) before rearming candy grants. This prevents Anthropic API microsecond fluctuations, Codex ±1s timestamp jitter, and rolling window drift from repeatedly rearming at 100% utilization.
11b0990 to
9d4dc8c
Compare
|
Confirming this one on v2.5.5: I hit the bug on both Claude windows, the 5-hour session limit and the weekly limit, on an additional Claude account (the multi-account setup from #327). While a window sat at 100%, every refresh came back with a I had to repair my save by hand: I went through #382 fixes the same bug, so only one of the two needs to land. I'd go with this one: it compares instants rather than strings, and it also absorbs the rolling drift of the 5-hour window and the ±1 s Codex jitter, not only sub-second differences, with regression tests for a real session reset and a real weekly reset while still at 100%. The change sits in Happy to test a build on my setup if that helps. |
|
@chattymin a gentle ping on this one: the bug is still live in v2.5.5 for anyone who stays at a limit for a while. The workaround is painful. Every time one of my windows sits at 100%, I have to quit the app, count the duplicate grants in the logs, and edit This PR is mergeable with CI green, and #382 fixes the same bug, so either one in a v2.5.6 would end it. Happy to test a build if that helps. |
|
I've reviewed #375 and #382 and will take #375 forward as the single fix for duplicate Rare Candy grants. Both address the reported Claude fractional-timestamp issue. This implementation also handles Codex integer reset timestamps with ±1-second jitter. The two PRs replace the same rearming logic, so applying both would introduce a conflict rather than an additional fix. Validation against current main (
I'll squash-merge this PR and close #382 as a duplicate. Thanks for the fix, the alternative implementation, and the detailed reproduction report. |
|
Thank you for your contribution! |
Codex keeps serving requests on credits after a subscription limit hits 100%, so a reset that moves past the chattymin#375 window-scale threshold while usage stays at 100% must not clear the grant ledger. Codex primary and secondary windows now pass requiresUtilizationDropToRearm=true and only rearm after a sub-100% sample. Claude and Antigravity keep the epoch rearm from chattymin#326. Rebased onto chattymin#375: the earlier jitter fix on this branch is dropped in favour of CompanionStore.isNewEpoch. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Codex keeps serving requests on credits after a subscription limit hits 100%, so a reset that moves past the chattymin#375 window-scale threshold while usage stays at 100% must not clear the grant ledger. Codex primary and secondary candy windows no longer carry a reset epoch, so they use the existing nil-epoch path and only rearm after a sub-100% sample. Claude and Antigravity keep the epoch rearm from chattymin#326 and chattymin#375. Also move the evaluateCandyGrants doc comment back above the function; chattymin#375 inserted its helpers between the two. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Reset stamps were compared as strings, so microsecond (Anthropic) or one-second (Codex) jitter read as a new window and re-granted candy on every poll at 100%. A new window now needs the reset to move forward by 30 minutes (five-hour) or 24 hours (weekly). Ported from upstream chattymin#375 (d888793). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Codex keeps serving requests on credits after a subscription limit hits 100%, so a reset that moves past the #375 window-scale threshold while usage stays at 100% must not clear the grant ledger. Codex primary and secondary candy windows no longer carry a reset epoch, so they use the existing nil-epoch path and only rearm after a sub-100% sample. Claude and Antigravity keep the epoch rearm from #326 and #375. Also move the evaluateCandyGrants doc comment back above the function; #375 inserted its helpers between the two. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tymin#434) Codex keeps serving requests on credits after a subscription limit hits 100%, so a reset that moves past the chattymin#375 window-scale threshold while usage stays at 100% must not clear the grant ledger. Codex primary and secondary candy windows no longer carry a reset epoch, so they use the existing nil-epoch path and only rearm after a sub-100% sample. Claude and Antigravity keep the epoch rearm from chattymin#326 and chattymin#375. Also move the evaluateCandyGrants doc comment back above the function; Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> (cherry picked from commit f026afc)
Summary
In #334 / #326, candy grants were enhanced to rearm when a limit window's
resets_atepoch changes, even if the intermediate utilization dip was missed (e.g. while sleeping across reset boundaries).However, raw epoch strings were compared with simple inequality (
previousEpoch != epoch). In practice:...17:10:00.928794+00:00vs...17:10:00.935515+00:00) on every single poll.1790613843vs1790613844).When a user sits at 100% utilization, every 60-second polling cycle evaluated
previousEpoch != epochastrue, repeatedly wipinggrantTierand awarding candies on every poll (~2 candies per minute).This change fixes the issue by:
UsageStoreto ISO8601 second precision without fractional seconds.CompanionStore.isNewEpochwhich parses epoch timestamps and requires window-scale advancement (at least 30 minutes for session windows, 24 hours for weekly windows) before considering an epoch truly reset/rearmed.windowEpochso minor rolling drifts do not ratchet forward undetected.RareCandyTests.swiftcovering microsecond variance, ±1s jitter, and rolling drift.docs/reference/defect-log.md.Type of change
Checklist
swift buildandswift testpass locallytestVolatileMicrosecondEpochDoesNotRegrant,testCodexJitterEpochDoesNotRegrant,testRollingWindowMinorDriftDoesNotRegrant,testSessionFullResetWhileStillAt100Regrants,testWeeklyFullResetWhileStillAt100Regrants)