{"record":{"id":"37924aa2c4174250","repo":"ruvnet/ruflo","slug":"profile-profile-has-no-persisted-refresh-toke","errorCode":null,"errorMessage":"profile \"${profile}\" has no persisted refresh token and its in-memory access token is absent or expiring — run: ruflo auth login --profile ${profile}","messagePattern":"profile \"(.+?)\" has no persisted refresh token and its in-memory access token is absent or expiring — run: ruflo auth login --profile (.+?)","errorType":"exception","errorClass":"SessionOnlyExpiredError","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/auth/client.ts","lineNumber":251,"sourceCode":" * token, then update metadata and the process cache. Refresh is deliberately\n * demand-driven: offline-safe commands such as plain `auth status` never call\n * this function and therefore never create background traffic or retry loops.\n */\nexport async function getValidAccessToken(profileName = 'default'): Promise<string> {\n  const profile = getProfile(profileName);\n  if (!profile) throw new NotLoggedInError(profileName);\n\n  const scopesWithoutConsent = profile.scopes.filter((scope) => {\n    const domain = domainForScope(scope);\n    return domain !== undefined && !hasConsent(domain);\n  });\n  if (scopesWithoutConsent.length > 0) {\n    throw new ScopeConsentMismatchError(profileName, scopesWithoutConsent);\n  }\n\n  const cached = getSessionToken(profileName, ACCESS_TOKEN_REFRESH_WINDOW_MS);\n  if (cached) return cached;\n  if (!profile.keychainRef) throw new SessionOnlyExpiredError(profileName);\n\n  const sec = await loadSecurityOAuth();\n  const keychain = await sec.createKeychainAdapter();\n  const refreshTokenValue = await keychain.getSecret(KEYCHAIN_SERVICE, profile.keychainRef);\n  if (!refreshTokenValue) throw new SessionOnlyExpiredError(profileName);\n\n  const refreshed = await refreshAccessToken(refreshTokenValue);\n  if (!refreshed.access_token) throw new Error('Cognitum refresh response did not contain an access token');\n\n  // Cognitum rotates refresh tokens with reuse detection. Commit the rotated\n  // credential first; if this write fails, do not publish/cache the access\n  // token and do not retry the already-spent old refresh token here.\n  if (refreshed.refresh_token) {\n    await keychain.setSecret(KEYCHAIN_SERVICE, profile.keychainRef, refreshed.refresh_token);\n  }\n\n  const expiresAtMs = Date.now() + Math.max(0, refreshed.expires_in ?? 0) * 1000;\n  setSessionToken(profileName, refreshed.access_token, expiresAtMs);","sourceCodeStart":233,"sourceCodeEnd":269,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/cli/src/auth/client.ts#L233-L269","documentation":"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`.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\necho '{\"access_token\":\"eyJ...\",\"expires_in\":3600}' | ruflo auth login --token-stdin\n// after (include refresh_token so the credential survives restarts)\necho '{\"access_token\":\"eyJ...\",\"refresh_token\":\"rt_...\",\"expires_in\":3600}' | ruflo auth login --token-stdin","handlingStrategy":"try-catch","validationCode":"// Before relying on a profile across restarts, ensure it persists a refresh token\nconst prof = await loadProfileState(profileName);\nif (!prof?.keychainRef && !process.env.RUFLO_EPHEMERAL) {\n  warn('session-only login detected; it will not survive process restarts');\n}","typeGuard":"import { SessionOnlyExpiredError } from '@claude-flow/cli/dist/auth/client.js';\nfunction isSessionOnlyExpired(e: unknown): e is SessionOnlyExpiredError {\n  return e instanceof Error && e.name === 'SessionOnlyExpiredError';\n}","tryCatchPattern":"try {\n  token = await getValidAccessToken(profileName);\n} catch (e) {\n  if (isSessionOnlyExpired(e)) {\n    // message already carries the re-login command; surface it and stop\n    console.error(e.message);\n    process.exit(3);\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["auth","oauth","session","refresh-token","keychain"],"backgroundTag":"missing-refresh-token","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}