stablyai/orca · error

pairing keychain presence record is invalid

Error message

pairing keychain presence record is invalid

What it means

Thrown by parsePresenceGeneration when reading the per-key 'presence' marker from Android AsyncStorage during pairing-keychain generation rotation. The stored raw string must be a canonical non-negative integer (Number.isInteger, 0..MAX_GENERATION=8) AND round-trip exactly (String(parsed) === raw), so values like '08', '1e0', ' ', or '-1' are all rejected. Unlike parseGeneration (which degrades gracefully to generation 0), the presence path throws because a corrupt presence record cannot be safely interpreted — it is consulted only on Android to detect whether a specific generation still holds a durable credential.

Source

Thrown at mobile/src/transport/pairing-keychain.ts:77

      reliable: false,
      error: new Error('pairing keychain generation record is invalid')
    }
  }
  return { generation: parsed, pending, reliable: true }
}

function parsePresenceGeneration(raw: string | null): number | null {
  if (raw === null) {
    return null
  }
  const parsed = Number(raw)
  if (
    !Number.isInteger(parsed) ||
    parsed < 0 ||
    parsed > MAX_GENERATION ||
    String(parsed) !== raw
  ) {
    throw new Error('pairing keychain presence record is invalid')
  }
  return parsed
}

function presenceStorageKey(key: string): string {
  return `${PRESENCE_STORAGE_PREFIX}${key}`
}

async function loadPresenceGeneration(key: string): Promise<number | null> {
  if (Platform.OS !== 'android') {
    return null
  }
  return parsePresenceGeneration(await AsyncStorage.getItem(presenceStorageKey(key)))
}

async function loadGeneration(): Promise<LoadedGeneration> {
  if (cachedGeneration !== null) {
    return { ...cachedGeneration, reliable: true }

View on GitHub (pinned to 1136503c6a)

Solutions

  1. Clear the offending presence key (AsyncStorage.removeItem with the 'orca:pairing-keychain-presence:<key>' prefix) and let the keychain reseed generation 0 on next pairing.
  2. Inspect every presence value via AsyncStorage.getAllKeys and filter the PRESENCE_STORAGE_PREFIX; rewrite any non-canonical entry with String(Number(raw)).
  3. If hit during a migration, write a one-time normalizer that canonicalizes all presence keys to String(parsed) before parsePresenceGeneration runs.
  4. Reproduce with a debug build that logs the raw value in parsePresenceGeneration before throwing, to identify which key is corrupt.

Example fix

// before (writes non-canonical value)
await AsyncStorage.setItem(presenceStorageKey(key), String(0o1)) // '1' is fine but '0o1' is not

// after
const gen = 1
await AsyncStorage.setItem(presenceStorageKey(key), String(gen)) // canonical: '1'
Defensive patterns

Strategy: validation

Validate before calling

// Validate before parsePresenceGeneration runs
function isValidPresenceRecord(raw: string | null): boolean {
  if (raw === null) return true
  const parsed = Number(raw)
  return Number.isInteger(parsed) && parsed >= 0 && parsed <= 8 && String(parsed) === raw
}
// usage: const raw = await AsyncStorage.getItem(presenceStorageKey(key))
// if (!isValidPresenceRecord(raw)) await AsyncStorage.removeItem(presenceStorageKey(key))

Type guard

function isCanonicalPresenceGeneration(raw: unknown): raw is string {
  return typeof raw === 'string'
    && raw !== ''
    && Number.isInteger(Number(raw))
    && Number(raw) >= 0
    && Number(raw) <= 8
    && String(Number(raw)) === raw
}

Try / catch

try {
  const gen = await loadPresenceGeneration(key)
} catch (error) {
  if (error instanceof Error && error.message === 'pairing keychain presence record is invalid') {
    await AsyncStorage.removeItem(presenceStorageKey(key)) // clear corrupt record
    return null // treat as absent
  }
  throw error
}

Prevention

When it happens

Trigger: loadPresenceGeneration(key) on Android reads a value under 'orca:pairing-keychain-presence:<key>' that is non-null but fails the canonical-integer check. Caused by: external tampering with AsyncStorage, a partial write that left a malformed value, a downgrade from a future schema, or a debugging tool that wrote a float/string into the presence slot.

Common situations: Android-only: iOS short-circuits to null at line 87. Hits developers inspecting/migrating AsyncStorage during pairing-keychain debugging, after a schema change that didn't migrate presence keys, or when a test fixture seeds the storage with a non-canonical integer literal.

Related errors


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