{"record":{"id":"22296300b5d5147d","repo":"stablyai/orca","slug":"mobile-relay-pairing-journal-identity-mismatch","errorCode":null,"errorMessage":"mobile relay pairing journal identity mismatch","messagePattern":"mobile relay pairing journal identity mismatch","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mobile/src/transport/mobile-relay-pairing-journal-store.ts","lineNumber":27,"sourceCode":"import {\n  deletePairingKeychainItem,\n  readPairingKeychainItem,\n  resetPairingKeychainForTests,\n  writePairingKeychainItem\n} from './pairing-keychain'\n\nconst JOURNAL_STORAGE_KEY = 'orca:mobile-relay:pairing-journal:v1'\nconst JOURNAL_SECRET_KEY = 'orca.mobile-relay.pairing-journal.v1'\nlet journalMutation: Promise<void> = Promise.resolve()\n\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  })","sourceCodeStart":9,"sourceCodeEnd":45,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/mobile/src/transport/mobile-relay-pairing-journal-store.ts#L9-L45","documentation":"Thrown in saveMobileRelayPairingJournal when metadata.journalId !== secrets.journalId after both halves parse. Metadata (AsyncStorage) and secrets (native keychain) must share one journalId since they are persisted to different stores and rejoined on load.","triggerScenarios":"The caller assembled a MobileRelayPairingJournal from mismatched halves: metadata from one pairing attempt and secrets from another, or a hand-built journal with divergent ids.","commonSituations":"A bug in the caller constructing the journal from stale cached pieces; test fixtures with copy-paste journalId errors; reusing a metadata object but regenerating secrets.","solutions":["Always construct journals via createMobileRelayPairingJournal, which derives one journalId and emits both halves.","If assembling manually, generate journalId once and inject the same value into both metadata and secrets."],"exampleFix":"// before\nconst journal = {\n  metadata: { ...prevMetadata, journalId: 'pair-A' },\n  secrets: { ...freshSecrets, journalId: 'pair-B' } // mismatch\n}\nawait saveMobileRelayPairingJournal(journal)\n// after\nconst journal = createMobileRelayPairingJournal({ offer, hostId, hostName })\nawait saveMobileRelayPairingJournal(journal)","handlingStrategy":"validation","validationCode":"function assertJournalIdsMatch(journal: MobileRelayPairingJournal): void {\n  if (journal.metadata.journalId !== journal.secrets.journalId) {\n    throw new Error('programmer error: journal halves have divergent ids')\n  }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Construct journals only via createMobileRelayPairingJournal so a single journalId seeds both halves.","If you must assemble manually, derive journalId once and inject into both metadata and secrets.","Add a unit test asserting metadata.journalId === secrets.journalId for any fixture."],"tags":["relay","pairing","storage","data-integrity"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}