{"record":{"id":"59588dbcd4f9607f","repo":"stablyai/orca","slug":"mobile-relay-pairing-recovery-pending","errorCode":null,"errorMessage":"mobile relay pairing recovery pending","messagePattern":"mobile relay pairing recovery pending","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mobile/src/transport/mobile-relay-pairing-journal-store.ts","lineNumber":37,"sourceCode":"\nexport async function saveMobileRelayPairingJournal(\n  journal: MobileRelayPairingJournal\n): Promise<void> {\n  requireNativeSecretStore()\n  const metadata = MobileRelayPairingJournalMetadataSchema.parse(journal.metadata)\n  const secrets = MobileRelayPairingJournalSecretsSchema.parse(journal.secrets)\n  if (metadata.journalId !== secrets.journalId) {\n    throw new Error('mobile relay pairing journal identity mismatch')\n  }\n  const mutation = journalMutation.then(async () => {\n    const existingRaw = await AsyncStorage.getItem(JOURNAL_STORAGE_KEY)\n    const existing = existingRaw ? parseMetadata(existingRaw) : null\n    if (\n      existing &&\n      existing.journalId !== metadata.journalId &&\n      (existing.winner !== undefined || existing.authorizationMode !== undefined)\n    ) {\n      throw new Error('mobile relay pairing recovery pending')\n    }\n    // Why: no install RPC can run before winner+authorization are durable, so\n    // a new user-initiated scan may safely supersede a pre-authorization attempt.\n    // Why: metadata-first makes a crash before the keychain write recover as\n    // an incomplete journal, never as an untracked bearer secret.\n    await AsyncStorage.setItem(JOURNAL_STORAGE_KEY, JSON.stringify(metadata))\n    await writePairingKeychainItem(JOURNAL_SECRET_KEY, JSON.stringify(secrets))\n  })\n  journalMutation = mutation.catch(() => {})\n  return mutation\n}\n\nexport async function loadMobileRelayPairingJournal(): Promise<MobileRelayPairingJournal | null> {\n  requireNativeSecretStore()\n  const load = journalMutation.then(async () => {\n    const rawMetadata = await AsyncStorage.getItem(JOURNAL_STORAGE_KEY)\n    if (rawMetadata === null) {\n      await deletePairingKeychainItem(JOURNAL_SECRET_KEY).catch(() => {})","sourceCodeStart":19,"sourceCodeEnd":55,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/mobile/src/transport/mobile-relay-pairing-journal-store.ts#L19-L55","documentation":"Thrown inside the serialized save mutation when an existing journal metadata is found with a different journalId that already has winner or authorizationMode set. A prior pairing advanced past the authorization commit point, so overwriting it would abandon an in-flight install/recovery; the save is refused.","triggerScenarios":"A second saveMobileRelayPairingJournal with a fresh journalId while a previous journal already recorded a winner or authorization mode (i.e. reached or passed the install-commit step).","commonSituations":"User started a new scan/pair while a previous attempt is mid-install; two pairing orchestrators racing; recovery flow still in progress when a new pair begins.","solutions":["Call clearMobileRelayPairingJournal(existingJournalId) first to deliberately abandon the prior attempt.","Block new pairing in the UI until the in-flight one resolves or is explicitly cancelled.","Ensure only one pairing orchestrator is active at a time via a mutex/flag."],"exampleFix":"// before\nawait saveMobileRelayPairingJournal(newJournal) // throws if old journal is mid-install\n// after\nconst existing = await loadMobileRelayPairingJournal()\nif (existing && (existing.metadata.winner || existing.metadata.authorizationMode)) {\n  await clearMobileRelayPairingJournal(existing.metadata.journalId)\n}\nawait saveMobileRelayPairingJournal(newJournal)","handlingStrategy":"validation","validationCode":"import { loadMobileRelayPairingJournal, clearMobileRelayPairingJournal } from './mobile-relay-pairing-journal-store'\nasync function canSaveNewJournalSafely(): Promise<boolean> {\n  const existing = await loadMobileRelayPairingJournal()\n  if (!existing) return true\n  return !(existing.metadata.winner !== undefined || existing.metadata.authorizationMode !== undefined)\n}","typeGuard":null,"tryCatchPattern":"try {\n  await saveMobileRelayPairingJournal(newJournal)\n} catch (error) {\n  if (error instanceof Error && error.message === 'mobile relay pairing recovery pending') {\n    // prompt user to abandon the in-flight attempt, then clear + retry\n  }\n}","preventionTips":["Serialize pairing orchestrators with a single in-flight flag so a second attempt cannot race a committed one.","Before saving a new journal, load the existing one and clear it if it is past the authorization commit.","Reflect 'pairing in progress' in the UI so users cannot start a concurrent pair."],"tags":["relay","pairing","storage","concurrency","state-machine"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}