{"record":{"id":"179e4df0736fe68e","repo":"stablyai/orca","slug":"pending-host-credential-cleanup-storage-unreadable","errorCode":null,"errorMessage":"pending host credential cleanup storage unreadable","messagePattern":"pending host credential cleanup storage unreadable","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"mobile/src/transport/host-credential-cleanup.ts","lineNumber":100,"sourceCode":"\nasync function loadPendingCleanupState(): Promise<PendingHostCredentialCleanup> {\n  await pendingMutation\n  const result = await readPendingIdsForMutation()\n  const fallback = [...unrecordedPendingIds]\n  if (!result.ok) {\n    // Why: durable queue unreadable — only the session-scoped fallback is\n    // known. Report unreadable so callers can surface a retry rather than\n    // pretend the queue is empty.\n    return { ids: [...new Set(fallback)], storageUnreadable: true }\n  }\n  return { ids: [...new Set([...result.ids, ...fallback])], storageUnreadable: false }\n}\n\nasync function mutatePendingIds(update: (ids: string[]) => string[]): Promise<void> {\n  const mutation = pendingMutation.then(async () => {\n    const current = await readPendingIdsForMutation()\n    if (!current.ok) {\n      throw new Error('pending host credential cleanup storage unreadable')\n    }\n    const next = update(current.ids)\n    if (sameIdList(current.ids, next)) {\n      return\n    }\n    await AsyncStorage.setItem(PENDING_STORAGE_KEY, JSON.stringify(next))\n    notifyPendingListeners()\n  })\n  pendingMutation = mutation.catch(() => {})\n  return mutation\n}\n\nasync function addPendingId(hostId: string): Promise<void> {\n  await mutatePendingIds((ids) => (ids.includes(hostId) ? ids : [...ids, hostId]))\n}\n\nasync function removePendingId(hostId: string): Promise<void> {\n  await mutatePendingIds((ids) => ids.filter((id) => id !== hostId))","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/mobile/src/transport/host-credential-cleanup.ts#L82-L118","documentation":"The pending host-credential-cleanup durable queue (AsyncStorage key 'orca:pending-host-credential-cleanups') could not be read or parsed during a read-modify-write mutation. mutatePendingIds calls readPendingIdsForMutation, which returns { ok: false } when AsyncStorage.getItem throws or JSON.parse yields a non-array. The mutation refuses to proceed because treating unreadable storage as [] would silently wipe durable pending cleanup IDs (orphaning keychain tokens).","triggerScenarios":"AsyncStorage.getItem rejects (native storage I/O failure); the stored JSON is corrupt (partial write after a crash); the parsed value is valid JSON but not an array (schema drift); AsyncStorage module not initialized in the JS engine.","commonSituations":"App crashed mid-write leaving truncated JSON; React Native upgrade broke AsyncStorage; storage quota exceeded; test environment without AsyncStorage polyfill; Hermes serialization edge case producing non-array JSON.","solutions":["Do NOT catch-and-treat-as-empty — that orphans keychain tokens. Surface a 'pending cleanup unknown' state (the loadPendingCleanupState path already sets storageUnreadable: true).","Offer a manual 'retry cleanup' action in Settings (retryPendingHostCredentialCleanups).","If corrupt JSON is confirmed, prompt the user to reset the cleanup queue only after confirming no orphaned tokens remain.","Check AsyncStorage initialization and available storage space on the device."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await recordHostCredentialCleanupIntent(hostId)\n} catch (e) {\n  if (e.message === 'pending host credential cleanup storage unreadable') {\n    // Surface 'pending cleanup unknown' in Settings; offer manual retry\n    markUnrecordedPending(hostId)\n  }\n}","preventionTips":["Never suppress this as empty — that orphans keychain tokens. Always surface storageUnreadable.","Use the session-scoped unrecordedPendingIds fallback (markUnrecordedPending) so Settings can still offer retry.","Check AsyncStorage health and device storage space when this recurs.","Call retryPendingHostCredentialCleanups from Settings to re-attempt durable queue operations."],"tags":["async-storage","credentials","keychain","data-integrity","mobile"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}