ruvnet/ruflo · error · SessionOnlyExpiredError
profile "${profile}" has no persisted refresh token and its
Error message
profile "${profile}" has no persisted refresh token and its in-memory access token is absent or expiring — run: ruflo auth login --profile ${profile} What it means
Thrown at line 251 (SessionOnlyExpiredError) when profile.keychainRef is absent. The profile was session-only (e.g. --token-stdin login) and never persisted a refresh token to the OS keychain; with the in-memory access token expired or expiring, no refresh is possible.
Source
Thrown at v3/@claude-flow/cli/src/auth/client.ts:251
* token, then update metadata and the process cache. Refresh is deliberately
* demand-driven: offline-safe commands such as plain `auth status` never call
* this function and therefore never create background traffic or retry loops.
*/
export async function getValidAccessToken(profileName = 'default'): Promise<string> {
const profile = getProfile(profileName);
if (!profile) throw new NotLoggedInError(profileName);
const scopesWithoutConsent = profile.scopes.filter((scope) => {
const domain = domainForScope(scope);
return domain !== undefined && !hasConsent(domain);
});
if (scopesWithoutConsent.length > 0) {
throw new ScopeConsentMismatchError(profileName, scopesWithoutConsent);
}
const cached = getSessionToken(profileName, ACCESS_TOKEN_REFRESH_WINDOW_MS);
if (cached) return cached;
if (!profile.keychainRef) throw new SessionOnlyExpiredError(profileName);
const sec = await loadSecurityOAuth();
const keychain = await sec.createKeychainAdapter();
const refreshTokenValue = await keychain.getSecret(KEYCHAIN_SERVICE, profile.keychainRef);
if (!refreshTokenValue) throw new SessionOnlyExpiredError(profileName);
const refreshed = await refreshAccessToken(refreshTokenValue);
if (!refreshed.access_token) throw new Error('Cognitum refresh response did not contain an access token');
// Cognitum rotates refresh tokens with reuse detection. Commit the rotated
// credential first; if this write fails, do not publish/cache the access
// token and do not retry the already-spent old refresh token here.
if (refreshed.refresh_token) {
await keychain.setSecret(KEYCHAIN_SERVICE, profile.keychainRef, refreshed.refresh_token);
}
const expiresAtMs = Date.now() + Math.max(0, refreshed.expires_in ?? 0) * 1000;
setSessionToken(profileName, refreshed.access_token, expiresAtMs);View on GitHub (pinned to 6b01dc5a68)
Solutions
- Re-login with a persistent method (interactive/manual PKCE) so a refresh token is stored in the keychain.
- Re-inject a fresh access token via --token-stdin for the next session.
Defensive patterns
Strategy: validation
Validate before calling
if (!profile.keychainRef) {
throw new Error('Session-only profile has no persisted refresh token; run a persistent login');
} Type guard
function isPersistentProfile(p: { keychainRef?: string }): boolean {
return typeof p.keychainRef === 'string' && p.keychainRef.length > 0;
} Prevention
- For long-lived processes, use keychain-backed login rather than --token-stdin.
- Detect session-only profiles early and warn the user.
- Document which login methods are persistent vs session-only.
When it happens
Trigger: A session-only login (no keychainRef) whose in-memory token is absent or within the refresh window, so getValidAccessToken cannot refresh and has nothing to fall back on.
Common situations: Logged in via --token-stdin in a previous process; long-running process whose memory token expired; CI without keychain access.
Related errors
- state mismatch — the OAuth callback did not match the reques
- login cancelled: no code was entered
- --token-stdin: no input received on stdin
- --token-stdin expects a single JSON object: {"access_token",
- --token-stdin: JSON is missing required field "access_token"
AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12).
Data as JSON: /api/errors/37924aa2c4174250.
Report an issue: GitHub.