ruvnet/ruflo · error · SessionOnlyExpiredError
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
SessionOnlyExpiredError thrown at the first site in getValidAccessToken(): the profile exists but has no keychainRef, meaning no refresh token was ever persisted (session-only login), and the in-memory access token is absent or inside the 60-second refresh window. There is no way to mint a token without user interaction, so the message directs to `ruflo auth login`.
Solutions
- Re-login with a durable credential: ruflo auth login --profile <profile>
- If using --token-stdin, include a refresh_token in the JSON so a keychainRef gets persisted
- For CI, provision a fresh access token per job instead of expecting it to survive process restarts
- Log in via the interactive PKCE flow, which always persists a refresh token in the OS keychain
Example fix
// before
echo '{"access_token":"eyJ...","expires_in":3600}' | ruflo auth login --token-stdin
// after (include refresh_token so the credential survives restarts)
echo '{"access_token":"eyJ...","refresh_token":"rt_...","expires_in":3600}' | ruflo auth login --token-stdin Defensive patterns
Strategy: try-catch
Validate before calling
// Before relying on a profile across restarts, ensure it persists a refresh token
const prof = await loadProfileState(profileName);
if (!prof?.keychainRef && !process.env.RUFLO_EPHEMERAL) {
warn('session-only login detected; it will not survive process restarts');
} Type guard
import { SessionOnlyExpiredError } from '@claude-flow/cli/dist/auth/client.js';
function isSessionOnlyExpired(e: unknown): e is SessionOnlyExpiredError {
return e instanceof Error && e.name === 'SessionOnlyExpiredError';
} Try / catch
try {
token = await getValidAccessToken(profileName);
} catch (e) {
if (isSessionOnlyExpired(e)) {
// message already carries the re-login command; surface it and stop
console.error(e.message);
process.exit(3);
}
throw e;
} Prevention
- Always include refresh_token when using --token-stdin for durable sessions
- In CI, mint a fresh token per job instead of expecting session survival
- Prefer interactive PKCE login on developer machines — it persists the refresh token
When it happens
Trigger: Logging in via `--token-stdin` with only an access_token (no refresh_token), then letting that token expire (or restarting the process so the memory cache is gone) before calling getValidAccessToken(); any session-only profile after process restart.
Common situations: Short-lived CI tokens piped via stdin: the process restarts between pipeline jobs and the token is gone; a user who logged in with an opaque token from a vault that provides no refresh token; expiry window reached during a long-running command.
Related errors
- Cognitum auth service returned an unexpected response
- Cognitum refresh response did not contain an access token
- authorization was denied or failed
- Could not reach the Cognitum auth service. ruflo core…
- login cancelled: no code was entered
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/37924aa2c4174250.
Report an issue: GitHub.
Appendix: 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 fa13ee4ad6)