From 9c554b3d345da7019aa4030d016b88d3a7a02d3e Mon Sep 17 00:00:00 2001 From: dtzp555 Date: Tue, 7 Jul 2026 22:43:46 +1000 Subject: [PATCH] =?UTF-8?q?fix(server):=20drain=20F3=20fallback=20queue=20?= =?UTF-8?q?immediately=20=E2=80=94=20invalidate=20F5=20keychain=20cache=20?= =?UTF-8?q?before=20the=20serialized=20re-check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the F3/F5/F6 fix (independent-review observation). F5's 30s keychain TTL cache could make F3's post-refresh re-check see the stale (expiring) cached creds for up to ~30s, so a waiter admitted right after the prior real-HOME holder's claude refreshed the keychain would needlessly fall back to real HOME again instead of proceeding ISOLATED. Serialization safety was never at risk (still one real-HOME spawn at a time, no double-refresh); only the drain-to-fast-path optimization lagged. Fix: invalidateKeychainReadCache() clears the F5 TTL cache; resolveSpawnDecision() calls it under the fallback mutex, immediately before the re-check, so the admitted waiter reads FRESH keychain state and drains to the isolated fast path at once. The extra keychain read happens only on the rare real-HOME fallback path and only under the mutex (serialized, one at a time). Alignment: unchanged from the parent commit — proxy-internal keychain/HOME-isolation process logic, no cli.js analogue (cli.js citation DECLARED ABSENT, ALIGNMENT.md Rule 2), no endpoint/header/body, /health shape unchanged, OCP still never performs a refresh_token grant itself (#112). 249 tests pass. Co-Authored-By: Claude Fable 5 --- server.mjs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/server.mjs b/server.mjs index 73ed83e..5329b8d 100644 --- a/server.mjs +++ b/server.mjs @@ -471,6 +471,10 @@ async function resolveSpawnDecision() { // piling every request into the real HOME. const release = await realHomeFallbackMutex.acquire(); try { + // Drop the 30s keychain TTL cache so the re-check reads FRESH keychain state — otherwise a + // waiter admitted right after the prior holder's claude refreshed the token could still see the + // stale (expiring) cached creds and needlessly fall back to real HOME again for up to ~30s. + invalidateKeychainReadCache(); const retry = resolveSpawnToken(); if (retry) { release(); @@ -1641,6 +1645,14 @@ function readKeychainCreds() { }); } +// F3 drain helper: drop the F5 keychain TTL cache so the NEXT getOAuthCredentials() re-reads the +// keychain from scratch. Called under the real-HOME fallback mutex just before the re-check, so a +// waiter admitted after the prior holder's claude refreshed the keychain sees the FRESH token +// immediately (and proceeds ISOLATED) instead of waiting out the ≤30s TTL on the stale creds. +function invalidateKeychainReadCache() { + _keychainCache.clear(); +} + function getOAuthCredentials() { // 1. Env var fallback — highest precedence for explicit overrides. if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {