{"record":{"id":"201b4aac8c24375e","repo":"stablyai/orca","slug":"pairing-keychain-presence-record-is-invalid","errorCode":null,"errorMessage":"pairing keychain presence record is invalid","messagePattern":"pairing keychain presence record is invalid","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mobile/src/transport/pairing-keychain.ts","lineNumber":77,"sourceCode":"      reliable: false,\n      error: new Error('pairing keychain generation record is invalid')\n    }\n  }\n  return { generation: parsed, pending, reliable: true }\n}\n\nfunction parsePresenceGeneration(raw: string | null): number | null {\n  if (raw === null) {\n    return null\n  }\n  const parsed = Number(raw)\n  if (\n    !Number.isInteger(parsed) ||\n    parsed < 0 ||\n    parsed > MAX_GENERATION ||\n    String(parsed) !== raw\n  ) {\n    throw new Error('pairing keychain presence record is invalid')\n  }\n  return parsed\n}\n\nfunction presenceStorageKey(key: string): string {\n  return `${PRESENCE_STORAGE_PREFIX}${key}`\n}\n\nasync function loadPresenceGeneration(key: string): Promise<number | null> {\n  if (Platform.OS !== 'android') {\n    return null\n  }\n  return parsePresenceGeneration(await AsyncStorage.getItem(presenceStorageKey(key)))\n}\n\nasync function loadGeneration(): Promise<LoadedGeneration> {\n  if (cachedGeneration !== null) {\n    return { ...cachedGeneration, reliable: true }","sourceCodeStart":59,"sourceCodeEnd":95,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/mobile/src/transport/pairing-keychain.ts#L59-L95","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","Inspect every presence value via AsyncStorage.getAllKeys and filter the PRESENCE_STORAGE_PREFIX; rewrite any non-canonical entry with String(Number(raw)).","If hit during a migration, write a one-time normalizer that canonicalizes all presence keys to String(parsed) before parsePresenceGeneration runs.","Reproduce with a debug build that logs the raw value in parsePresenceGeneration before throwing, to identify which key is corrupt."],"exampleFix":"// before (writes non-canonical value)\nawait AsyncStorage.setItem(presenceStorageKey(key), String(0o1)) // '1' is fine but '0o1' is not\n\n// after\nconst gen = 1\nawait AsyncStorage.setItem(presenceStorageKey(key), String(gen)) // canonical: '1'","handlingStrategy":"validation","validationCode":"// Validate before parsePresenceGeneration runs\nfunction isValidPresenceRecord(raw: string | null): boolean {\n  if (raw === null) return true\n  const parsed = Number(raw)\n  return Number.isInteger(parsed) && parsed >= 0 && parsed <= 8 && String(parsed) === raw\n}\n// usage: const raw = await AsyncStorage.getItem(presenceStorageKey(key))\n// if (!isValidPresenceRecord(raw)) await AsyncStorage.removeItem(presenceStorageKey(key))","typeGuard":"function isCanonicalPresenceGeneration(raw: unknown): raw is string {\n  return typeof raw === 'string'\n    && raw !== ''\n    && Number.isInteger(Number(raw))\n    && Number(raw) >= 0\n    && Number(raw) <= 8\n    && String(Number(raw)) === raw\n}","tryCatchPattern":"try {\n  const gen = await loadPresenceGeneration(key)\n} catch (error) {\n  if (error instanceof Error && error.message === 'pairing keychain presence record is invalid') {\n    await AsyncStorage.removeItem(presenceStorageKey(key)) // clear corrupt record\n    return null // treat as absent\n  }\n  throw error\n}","preventionTips":["Always write presence values via commitGeneration-style helpers that emit String(parsed), never template literals or float casts.","Add a startup migration that canonicalizes every PRESENCE_STORAGE_PREFIX key before parsePresenceGeneration can run.","In tests, seed AsyncStorage with String(n) literals, not JSON.stringify(8) (which yields '8' but is a footgun for floats)."],"tags":["android","keychain","asyncstorage","pairing","data-integrity"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}