Repository navigation
Replay a WorkOS refresh inside the 30 s grace window - #947
Conversation
WorkOS honours a replay of the just-retired refresh token for 30 s and answers with the same rotated pair, so a reply lost at the deadline is recoverable only inside that window. The 20 s budget plus one 5 s attempt keeps every replay under it; a 4xx is never replayed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
PR Summary by QodoReplay WorkOS refreshes within the grace window
AI Description
Diagram
High-Level Assessment
Files changed (4)
|
Code Review by Qodo
1.
|
…lock (#946) A delay stretched by a suspended machine can resume outside WorkOS's replay window, where a replay is refused for good. Lock waiters derive their wait from the same budget so a peer never abandons a holder that is one replay from persisting a fresh token. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…946) Every vendor kills a hook well inside the refresh lock wait, so client creation is bounded and the payload spooled rather than the hook dying on its way to the spool. One unreadable success proves the token spent, whatever later replays return. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A hook that gives up on client creation may exit with a rotation in flight, losing the rotated pair. The detached refresh-token process takes the same lock and either finds the token fresh or replays it inside WorkOS's window. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
#946) A URL override outranks the profile pin and resolves to no profile, so the child drops it. Inherited hook pipes would make a host waiting for EOF wait on the child too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The hand-off drops KCAP_URL from the child, so a profile that only ever had a URL override would otherwise be turned away at the no-server gate before it could refresh. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Claude hook tests build the command with a fake starter so the abandon path can never spawn the test host as a detached kcap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Closes #946 — AI-2790
What & why
A WorkOS refresh was sent once with a 5 s deadline. When WorkOS processed the exchange but the reply missed the deadline, the rotated pair was lost; the next refresh, a minute or more later, presented the retired token outside WorkOS's 30-second replay window and got
invalid_grant, signing out every process that shares the token store. WorkOS documents that a replay inside the window returns the same rotated pair and that timeouts, 5xx and 429 should be retried with the same token. The client now replays a lost reply, a transient status or an unreadable success body while the next attempt can still complete inside a 20 s budget (5 s per attempt), re-checked after every backoff; a 4xx is still never replayed. Lock waiters derive their deadline from that budget so a peer never abandons a holder about to persist a fresh token, and hook client creation is bounded at 3 s: past it the hook spools (or reports the lapse on the no-spool path) and hands the refresh to a detachedkcap refresh-tokenprocess pinned to its config root and profile, which either finds the token fresh or replays it inside the window, since every vendor kills a hook well inside the lock wait.Where to look
The out-of-window classification: any unreadable success means the token is spent, so the outcome is
Rejectedhowever later replays fail; a reply that never arrived ends asTransportFailed.SequencedHttpScriptexists because WireMock's cold start exceeds any deadline short enough to test a stall.BoundedAuthis the Claude hook's auth race moved out of the vendor directory so the other seven hooks can share it.RefreshTokenHandoffdetaches before repository resolution and dropsKCAP_URLfrom the child, which would otherwise outrank the profile pin.Verification
Daemon log, 2026-09-12: refresh sent 21:54:40.751, "Proactive token refresh failed" 21:54:45.977, next refresh 21:55:41 → HTTP 400, then "Proactive token refresh was rejected" hourly until the next login.
🤖 Generated with Claude Code