stablyai/orca · error

mobile relay pairing journal identity mismatch

Error message

mobile relay pairing journal identity mismatch

What it means

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.

Source

Thrown at mobile/src/transport/mobile-relay-pairing-journal-store.ts:27

import {
  deletePairingKeychainItem,
  readPairingKeychainItem,
  resetPairingKeychainForTests,
  writePairingKeychainItem
} from './pairing-keychain'

const JOURNAL_STORAGE_KEY = 'orca:mobile-relay:pairing-journal:v1'
const JOURNAL_SECRET_KEY = 'orca.mobile-relay.pairing-journal.v1'
let journalMutation: Promise<void> = Promise.resolve()

export async function saveMobileRelayPairingJournal(
  journal: MobileRelayPairingJournal
): Promise<void> {
  requireNativeSecretStore()
  const metadata = MobileRelayPairingJournalMetadataSchema.parse(journal.metadata)
  const secrets = MobileRelayPairingJournalSecretsSchema.parse(journal.secrets)
  if (metadata.journalId !== secrets.journalId) {
    throw new Error('mobile relay pairing journal identity mismatch')
  }
  const mutation = journalMutation.then(async () => {
    const existingRaw = await AsyncStorage.getItem(JOURNAL_STORAGE_KEY)
    const existing = existingRaw ? parseMetadata(existingRaw) : null
    if (
      existing &&
      existing.journalId !== metadata.journalId &&
      (existing.winner !== undefined || existing.authorizationMode !== undefined)
    ) {
      throw new Error('mobile relay pairing recovery pending')
    }
    // Why: no install RPC can run before winner+authorization are durable, so
    // a new user-initiated scan may safely supersede a pre-authorization attempt.
    // Why: metadata-first makes a crash before the keychain write recover as
    // an incomplete journal, never as an untracked bearer secret.
    await AsyncStorage.setItem(JOURNAL_STORAGE_KEY, JSON.stringify(metadata))
    await writePairingKeychainItem(JOURNAL_SECRET_KEY, JSON.stringify(secrets))
  })

View on GitHub (pinned to 1136503c6a)

Solutions

  1. Always construct journals via createMobileRelayPairingJournal, which derives one journalId and emits both halves.
  2. If assembling manually, generate journalId once and inject the same value into both metadata and secrets.

Example fix

// before
const journal = {
  metadata: { ...prevMetadata, journalId: 'pair-A' },
  secrets: { ...freshSecrets, journalId: 'pair-B' } // mismatch
}
await saveMobileRelayPairingJournal(journal)
// after
const journal = createMobileRelayPairingJournal({ offer, hostId, hostName })
await saveMobileRelayPairingJournal(journal)
Defensive patterns

Strategy: validation

Validate before calling

function assertJournalIdsMatch(journal: MobileRelayPairingJournal): void {
  if (journal.metadata.journalId !== journal.secrets.journalId) {
    throw new Error('programmer error: journal halves have divergent ids')
  }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12). Data as JSON: /api/errors/22296300b5d5147d. Report an issue: GitHub.