{"record":{"id":"7e8546d2db631d8e","repo":"stablyai/orca","slug":"mobile-relay-upgrade-host-was-removed","errorCode":null,"errorMessage":"mobile relay upgrade host was removed","messagePattern":"mobile relay upgrade host was removed","errorType":"exception","errorClass":"MobileRelayUpgradeHostRemovedError","httpStatus":null,"severity":"warning","filePath":"mobile/src/transport/host-store.ts","lineNumber":193,"sourceCode":"  let tokenCommittedBeforeMetadata = false\n  try {\n    await mutateStoredHosts(async (hosts) => {\n      const index = hosts.findIndex((h) => h.id === stored.id)\n      for (const candidate of hosts) {\n        if (candidate.id !== stored.id && candidate.publicKeyB64 === stored.publicKeyB64) {\n          duplicateHostIds.add(candidate.id)\n        }\n      }\n      let next: StoredHostProfile[]\n      if (index !== -1) {\n        updatedExistingHost = true\n        // Why: an authoritative save is the safe point to collapse pre-existing duplicate rows to the preserved host id.\n        next = hosts\n          .filter(({ id }) => !duplicateHostIds.has(id))\n          .map((candidate) => (candidate.id === stored.id ? stored : candidate))\n      } else if (requireExisting) {\n        // Why: an in-flight relay upgrade must not resurrect a host the user removed.\n        throw new MobileRelayUpgradeHostRemovedError('mobile relay upgrade host was removed')\n      } else {\n        next = [...hosts.filter(({ id }) => !duplicateHostIds.has(id)), stored]\n      }\n      if (duplicateHostIds.size > 0) {\n        if (index === -1) {\n          // Why: process death between the early token write and metadata publication must leave cleanup discoverable.\n          await recordHostCredentialCleanupIntent(stored.id)\n          cleanupIntentRecordedBeforeMetadata = true\n        }\n        for (const duplicateHostId of duplicateHostIds) {\n          await recordHostCredentialCleanupIntent(duplicateHostId)\n        }\n        // Why: never remove the only usable same-key row until its replacement credential is durable.\n        await commitDeviceToken(stored.id, validated.deviceToken)\n        tokenCommittedBeforeMetadata = true\n      }\n      return next\n    })","sourceCodeStart":175,"sourceCodeEnd":211,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/mobile/src/transport/host-store.ts#L175-L211","documentation":"persistHost was called with requireExisting=true (via saveExistingHostRelayUpgrade) but the host ID was not found in the stored list. The code throws MobileRelayUpgradeHostRemovedError specifically to prevent an in-flight relay upgrade from silently resurrecting a host the user already removed between when the upgrade started and when it committed.","triggerScenarios":"User started a relay-direct upgrade, then removed the host from Settings before the upgrade's metadata write landed; a duplicate-key collapse removed the row; a concurrent removeHost won the mutation-chain race.","commonSituations":"User taps 'Upgrade to direct connection' then quickly swipes to delete the host; relay upgrade retries after the host was already removed; rapid pair/unpair actions racing the upgrade flow.","solutions":["Catch MobileRelayUpgradeHostRemovedError specifically and abort the upgrade silently (the user's remove intent is authoritative).","Do NOT retry the upgrade — it would resurrect a deliberately-removed host.","Surface 'Host removed — upgrade cancelled' as an informational toast, not an error.","Guard the upgrade entry point by re-checking the host exists immediately before saveExistingHostRelayUpgrade."],"exampleFix":"// before\ntry {\n  await saveExistingHostRelayUpgrade(host)\n} catch (e) {\n  showError(e.message)\n}\n\n// after — the removal is intentional, abort quietly\ntry {\n  await saveExistingHostRelayUpgrade(host)\n} catch (e) {\n  if (e instanceof MobileRelayUpgradeHostRemovedError) {\n    navigateAway() // user already removed it\n    return\n  }\n  throw e\n}","handlingStrategy":"try-catch","validationCode":"// Re-check the host exists immediately before a relay upgrade\nconst hosts = await loadHosts()\nif (!hosts.some((h) => h.id === host.id)) {\n  return // host already removed\n}","typeGuard":"import { MobileRelayUpgradeHostRemovedError } from './host-store'\n\nfunction isRelayHostRemoved(e: unknown): e is MobileRelayUpgradeHostRemovedError {\n  return e instanceof MobileRelayUpgradeHostRemovedError\n}","tryCatchPattern":"try {\n  await saveExistingHostRelayUpgrade(host)\n} catch (e) {\n  if (e instanceof MobileRelayUpgradeHostRemovedError) {\n    // User removed the host — abort the upgrade silently\n    return\n  }\n  throw e\n}","preventionTips":["Catch MobileRelayUpgradeHostRemovedError specifically — it is intentional, not a bug.","Do NOT retry the upgrade after this error; it would resurrect a removed host.","Re-check host existence right before saveExistingHostRelayUpgrade to avoid the race.","Cancel any in-flight upgrade when the user initiates a host removal."],"tags":["relay","host-store","concurrency","mobile","intentional-failure"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}