{"record":{"id":"fe55cb6808cbbef5","repo":"stablyai/orca","slug":"mobile-pairing-cancelled","errorCode":null,"errorMessage":"mobile pairing cancelled","messagePattern":"mobile pairing cancelled","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"info","filePath":"mobile/src/transport/pre-profile-pairing-coordinator.ts","lineNumber":314,"sourceCode":"function assertCommittedInstall(\n  status:\n    | { state: 'not-found' }\n    | { state: 'committed'; result: DeviceCredentialInstalled }\n    | undefined,\n  installed: DeviceCredentialInstalled\n): void {\n  if (\n    !status ||\n    status.state !== 'committed' ||\n    JSON.stringify(status.result) !== JSON.stringify(installed)\n  ) {\n    throw new Error('relay credential install was not authoritatively reconciled')\n  }\n}\n\nfunction assertActive(isDisposed: () => boolean): void {\n  if (isDisposed()) {\n    throw new Error('mobile pairing cancelled')\n  }\n}\n","sourceCodeStart":296,"sourceCodeEnd":317,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/mobile/src/transport/pre-profile-pairing-coordinator.ts#L296-L317","documentation":"Thrown by assertActive(isDisposed) (line 312-316) when the pairing flow was disposed between async steps. dispose() (line 80-93) sets disposed=true, clears the timer, and closes all candidates; assertActive is called at four checkpoints in runPairing (after resolveHostIdentity, after saveJournal, after racePairingCandidates, and before writeCredentialBundle) to abort promptly if the user cancelled or the timeout fired. Unlike error 482, this fires when the disposal was observed synchronously at a checkpoint rather than manifesting as a timeout-masked rejection.","triggerScenarios":"PreProfilePairingAttempt.dispose() was called (user cancelled pairing UI, app backgrounded with single-pairing-attempt policy, or a newer pairing offer superseded this one) while runPairing was awaiting an async step. On resumption of the next checkpoint, isDisposed() returns true and assertActive throws.","commonSituations":"User taps 'Cancel' on the pairing screen mid-handshake; a second scan of a pairing QR code supersedes the first; the app is killed/backgrounded and the foreground handler disposes the in-flight attempt. Expected behavior, not a bug.","solutions":["Treat as user-initiated cancellation — no recovery action needed; surface 'Pairing cancelled' in the UI.","If unexpected, audit callers of attempt.dispose() to confirm none fire spuriously (e.g., a React effect cleanup that runs on every render).","Ensure only one startPreProfilePairing attempt is live at a time; dispose the previous before starting a new one.","In tests, await attempt.result (which will reject with this message) before asserting downstream state."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isPairingCancelledError(error: unknown): boolean {\n  return error instanceof Error && error.message === 'mobile pairing cancelled'\n}","tryCatchPattern":"try {\n  const { hostId } = await attempt.result\n} catch (error) {\n  if (isPairingCancelledError(error)) {\n    // user-initiated cancellation — no recovery\n    return\n  }\n  throw error\n}","preventionTips":["Treat 'mobile pairing cancelled' as expected cancellation in UI/error reporting.","Audit callers of attempt.dispose() to ensure none fire spuriously (e.g., React effect cleanups running on every render).","Dispose the previous attempt before starting a new one to avoid concurrent pairing races."],"tags":["pairing","cancellation","lifecycle","concurrency"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}