fix(claude): persist usage cooldowns across invocations

Merged
JU
2mo ago
+627-39

Claude's Retry-After cooldown was process-local, so rerunning tokscale usage immediately sent another request after HTTP 429. Persist bounded cooldown records in Tokscale's cache and consult them before the next Claude usage request.

Records contain only a version and timestamps, with filenames keyed by an access-token hash. Config roots and credentials have separate cooldowns. Updates use a bounded file lock and atomic replacement; the longest valid disk/in-memory deadline wins, while an unwritable cache retains protection in the current process. Expired, malformed, oversized-duration, and clock-skewed records fail open. Claude Code's credentials remain read-only.

Closes #1207.

Validation:

  • CLI unit suite: 1,307 passed, 1 ignored.
  • cargo clippy --locked -p tokscale-cli --all-features -- -D warnings and cargo fmt --all -- --check passed.
  • Regressions cover actual child-process visibility, HTTP suppression, concurrent writers, clock rollback, malformed record replacement, longer disk deadlines, credential/config isolation, and in-memory fallback when persistence fails.

Persistence is best-effort when the cache is inaccessible or its lock remains busy. Rotating the access token starts a new cooldown scope; the integration does not expose a stable account ID to bind across token rotation. HTTP tests use a local fixture server.


Summary by cubic

Persists Claude Retry-After cooldowns across invocations so rerunning tokscale usage after a 429 respects the cooldown instead of making another request.

Implementation notes

  • Stores bounded cooldown records in the cache, keyed by access-token hash.
  • Keeps the longer of disk and in-memory deadlines; invalid records fail open.
  • Re-validates records with lock-time clock samples so delayed older writers can't replace newer cooldowns.
  • Uses bounded file locking and atomic replacement; interrupted staging files are reclaimed only under the account's own lock.
  • Preserves in-process protection when the cache is unwritable.
  • Scopes cooldowns per config root and token; rotating tokens starts a fresh scope.
  • Claude Code credentials remain read-only.