stablyai/orca · warning · Error

pending host credential cleanup storage unreadable

Error message

pending host credential cleanup storage unreadable

What it means

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).

Source

Thrown at mobile/src/transport/host-credential-cleanup.ts:100

async function loadPendingCleanupState(): Promise<PendingHostCredentialCleanup> {
  await pendingMutation
  const result = await readPendingIdsForMutation()
  const fallback = [...unrecordedPendingIds]
  if (!result.ok) {
    // Why: durable queue unreadable — only the session-scoped fallback is
    // known. Report unreadable so callers can surface a retry rather than
    // pretend the queue is empty.
    return { ids: [...new Set(fallback)], storageUnreadable: true }
  }
  return { ids: [...new Set([...result.ids, ...fallback])], storageUnreadable: false }
}

async function mutatePendingIds(update: (ids: string[]) => string[]): Promise<void> {
  const mutation = pendingMutation.then(async () => {
    const current = await readPendingIdsForMutation()
    if (!current.ok) {
      throw new Error('pending host credential cleanup storage unreadable')
    }
    const next = update(current.ids)
    if (sameIdList(current.ids, next)) {
      return
    }
    await AsyncStorage.setItem(PENDING_STORAGE_KEY, JSON.stringify(next))
    notifyPendingListeners()
  })
  pendingMutation = mutation.catch(() => {})
  return mutation
}

async function addPendingId(hostId: string): Promise<void> {
  await mutatePendingIds((ids) => (ids.includes(hostId) ? ids : [...ids, hostId]))
}

async function removePendingId(hostId: string): Promise<void> {
  await mutatePendingIds((ids) => ids.filter((id) => id !== hostId))

View on GitHub (pinned to 1136503c6a)

Solutions

  1. Do NOT catch-and-treat-as-empty — that orphans keychain tokens. Surface a 'pending cleanup unknown' state (the loadPendingCleanupState path already sets storageUnreadable: true).
  2. Offer a manual 'retry cleanup' action in Settings (retryPendingHostCredentialCleanups).
  3. If corrupt JSON is confirmed, prompt the user to reset the cleanup queue only after confirming no orphaned tokens remain.
  4. Check AsyncStorage initialization and available storage space on the device.
Defensive patterns

Strategy: try-catch

Try / catch

try {
  await recordHostCredentialCleanupIntent(hostId)
} catch (e) {
  if (e.message === 'pending host credential cleanup storage unreadable') {
    // Surface 'pending cleanup unknown' in Settings; offer manual retry
    markUnrecordedPending(hostId)
  }
}

Prevention

When it happens

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

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

Related errors


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