Skip to content

fix: prevent volatile reset timestamps from causing infinite rare can… - #375

Merged
chattymin merged 1 commit into
chattymin:mainfrom
KillianHzr:fix/rearm-candy-volatile-epoch
Oct 15, 2026
Merged

chattymin merged 1 commit into
chattymin:mainfrom
KillianHzr:fix/rearm-candy-volatile-epoch

Conversation

@KillianHzr

Copy link
Copy Markdown
Contributor

Summary

In #334 / #326, candy grants were enhanced to rearm when a limit window's resets_at epoch 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:

  1. Anthropic API dynamically returns ISO8601 strings with fluctuating microsecond timestamps (...17:10:00.928794+00:00 vs ...17:10:00.935515+00:00) on every single poll.
  2. Codex app-server responses can jitter by ±1 second (1790613843 vs 1790613844).
  3. Rolling 5-hour windows continuously drift minute-by-minute.

When a user sits at 100% utilization, every 60-second polling cycle evaluated previousEpoch != epoch as true, repeatedly wiping grantTier and awarding candies on every poll (~2 candies per minute).

This change fixes the issue by:

  1. Normalizing epoch timestamps in UsageStore to ISO8601 second precision without fractional seconds.
  2. Introducing CompanionStore.isNewEpoch which 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.
  3. Anchoring windowEpoch so minor rolling drifts do not ratchet forward undetected.
  4. Adding regression unit tests in RareCandyTests.swift covering microsecond variance, ±1s jitter, and rolling drift.
  5. Documenting the defect and mechanism in docs/reference/defect-log.md.

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Documentation
  • Other:

Checklist

  • swift build and swift test pass locally
  • PR title and description are written in English
  • No copyrighted assets, secrets, or private tooling references are committed (see CONTRIBUTING)
  • Tests were added or updated for this change (testVolatileMicrosecondEpochDoesNotRegrant, testCodexJitterEpochDoesNotRegrant, testRollingWindowMinorDriftDoesNotRegrant, testSessionFullResetWhileStillAt100Regrants, testWeeklyFullResetWhileStillAt100Regrants)

…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.
@KillianHzr
KillianHzr force-pushed the fix/rearm-candy-volatile-epoch branch from 11b0990 to 9d4dc8c Compare October 5, 2026 06:10
@DyRize

DyRize commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

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 resets_at that differed only in its fractional seconds, exactly as described above. Each change rearmed the window, so every refresh granted again: 1 Rare Candy per refresh on the session window, 5 on the weekly one, about every two minutes. It only stopped when the window reset or when the app could no longer read the limits. Anyone who stays at a limit for a while gets the same flood of candy.

I had to repair my save by hand: I went through PokeTokenBar.log and the macOS unified log (each grant posts a notification) to count the duplicate grants, then set the Rare Candy count back to what the legitimate grants would have given.

#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 evaluateCandyGrants, so additional accounts are covered too: their windows go through claudeCandyWindows with their own keys.

Happy to test a build on my setup if that helps.

@DyRize

DyRize commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

@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 companion-state.json by hand to take the extra Rare Candy back out. Keeping them isn't a real option either: spending them skips the growth that makes each hatch feel earned, and the companion and the Pokédex lose most of their fun.

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.

@chattymin

Copy link
Copy Markdown
Owner

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 (c328d5c):

  • All 61 candy-related tests passed.
  • Three additional integration cases through refresh, candy grants, JSON persistence, and restart passed: Claude timestamp jitter plus a real weekly reset, Codex ±1-second jitter, and a real 5-hour Codex reset.
  • The full local suite has six failing cases that reproduce unchanged on main; no new regression was identified.
  • The PR's build-test and secret-scan checks are green.

I'll squash-merge this PR and close #382 as a duplicate. Thanks for the fix, the alternative implementation, and the detailed reproduction report.

@chattymin
chattymin merged commit d888793 into chattymin:main Oct 15, 2026
2 checks passed
@chattymin

Copy link
Copy Markdown
Owner

Thank you for your contribution!

heli-os added a commit to heli-os/PokeTokenBar that referenced this pull request Oct 16, 2026
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>
heli-os added a commit to heli-os/PokeTokenBar that referenced this pull request Oct 16, 2026
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>
CallumAcorn added a commit to LukeAcorn/PokeTokenBar that referenced this pull request Oct 21, 2026
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>
chattymin pushed a commit that referenced this pull request Nov 4, 2026
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>
KillianHzr pushed a commit to KillianHzr/PokeTokenLBI that referenced this pull request Nov 5, 2026
…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)
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.

3 participants