stablyai/orca · warning

mobile pairing timed out

Error message

mobile pairing timed out

What it means

Thrown by the .catch() wrapper around runPairing in startPreProfilePairing (line 101-106). When the timeout timer (setTimeout at line 95) fires it sets timedOut=true and calls dispose(); if runPairing then rejects for ANY reason, the catch swaps the real error for this generic 'mobile pairing timed out' message. The original error is intentionally masked because once the timeout elapsed, the proximate user-facing condition is 'too slow', not whatever internal step happened to fail last.

Source

Thrown at mobile/src/transport/pre-profile-pairing-coordinator.ts:103

    if (timer) {
      clearTimeout(timer)
      timer = null
    }
    for (const client of clients) {
      client.close()
    }
    clients.clear()
  }

  timer = setTimeout(() => {
    timedOut = true
    dispose()
  }, args.timeoutMs)

  const result = runPairing(args.offer, args.connectOptions, dependencies, clients, () => disposed)
    .catch((error: unknown) => {
      if (timedOut) {
        throw new Error('mobile pairing timed out')
      }
      throw error
    })
    .finally(() => {
      if (timer) {
        clearTimeout(timer)
        timer = null
      }
      for (const client of clients) {
        client.close()
      }
      clients.clear()
    })

  return {
    result,
    get timedOut() {
      return timedOut

View on GitHub (pinned to 1136503c6a)

Solutions

  1. Increase args.timeoutMs passed to startPreProfilePairing to reflect realistic slow-network ceilings.
  2. Inspect connection logs ('Relay: cell dial failed', 'sendRequest waited for connect') to identify which phase consumed the budget.
  3. Ensure the desktop host is awake and the relay invite is not near expiry before initiating pairing.
  4. If reproducible, wrap individual phases (resolveHostIdentity, saveJournal, racePairingCandidates) with their own shorter timeouts to localize the stall.

Example fix

// before
const attempt = startPreProfilePairing({ offer, timeoutMs: 15_000 })

// after — size budget to the slowest plausible phase sum
const attempt = startPreProfilePairing({
  offer,
  timeoutMs: 60_000 // host identity + journal + candidate race + 2 RPC round-trips on a slow link
})
Defensive patterns

Strategy: try-catch

Validate before calling

// Before starting, sanity-check the budget against the slowest plausible phase
function isTimeoutMsAdequate(timeoutMs: number): boolean {
  // host identity + journal write + candidate race + 2 RPC round-trips
  return timeoutMs >= 30_000
}

Type guard

function isPairingTimeoutError(error: unknown): boolean {
  return error instanceof Error && error.message === 'mobile pairing timed out'
}

Try / catch

try {
  const { hostId } = await attempt.result
} catch (error) {
  if (isPairingTimeoutError(error)) {
    showUser('Pairing is taking too long. Check both devices' network and retry.')
    return
  }
  throw error
}

Prevention

When it happens

Trigger: args.timeoutMs elapsed before runPairing returned a hostId. Any of these during the window causes it: direct+relay candidates both slow to connect (network latency, relay cell migration via recoverThroughDirector), resolveHostIdentity or saveJournal blocked on storage, racePairingCandidates not producing a winner, or provisionRelay/getEndpoints round-trips exceeding the budget.

Common situations: Users on congested/mobile networks where the direct LAN socket and the relay wss handshake both exceed the configured timeoutMs. Also hit when the desktop is asleep (relay invite near inviteExpiresAt) or when AsyncStorage/SecureStore writes are slow under disk pressure.

Understand the failure class

Related errors


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