stablyai/orca · info

relay pairing client closed

Error message

relay pairing client closed

What it means

Thrown inside recoverThroughDirector after the relay candidate's close() was called externally between persistMove(moved) and the next connect attempt. It is a cooperative-cancellation signal: the wrapper sets closed=true on close() (line 41-42), and after backOff(attempt) the recovery loop checks `if (closed)` and throws this so the in-flight sendRequest rejects cleanly rather than dialing a fresh client on a dead candidate. It is NOT a transport failure — it means the caller abandoned the relay candidate mid-recovery.

Source

Thrown at mobile/src/transport/pairing-relay-candidate.ts:85

        'info',
        `Relay: resolving director (attempt ${attempt + 1}/${maxAttempts})`,
        redactSocketEndpoint(relay.directorUrl)
      )
      try {
        const moved = await args.resolveDirector(relay)
        // Why: the authenticated newer assignment must be durable before a
        // target dial so a crash cannot revert to the known-stale cell.
        await args.persistMove(moved)
        log(
          'info',
          'Relay: cell moved',
          `${redactSocketEndpoint(relay.cellUrl)} → ${redactSocketEndpoint(moved.cellUrl)}`
        )
        client.close()
        relay = moved
        await backOff(attempt)
        if (closed) {
          throw new Error('relay pairing client closed')
        }
        client = args.connect(relay, args.onLog)
        return await client.sendRequest(method, params)
      } catch (error) {
        lastError = error
        log('warn', `Relay: recovery attempt ${attempt + 1} failed`, pairingRelayErrorDetail(error))
        if (!isDirectorRecoverable(error) || attempt + 1 >= maxAttempts) {
          log('error', 'Relay: recovery gave up', `after ${attempt + 1} attempt(s)`)
          throw error
        }
        await backOff(attempt)
      }
    }
    throw lastError
  }
}

function pairingRelayFromJournal(journal: MobileRelayPairingJournal): PairingRelay {

View on GitHub (pinned to 1136503c6a)

Solutions

  1. Treat this as cancellation, not an error: if your caller initiated close()/dispose(), swallow it; if not, audit who is calling close() on the relay candidate concurrently.
  2. Ensure racePairingCandidates closes losing candidates only after the winner is fully selected, not speculatively.
  3. In tests, await any in-flight sendRequest promise before invoking close() on the candidate.
  4. If surfacing to users, map this message to a 'pairing cancelled' UI state rather than a relay failure.

Example fix

// before
try { await client.sendRequest(method, params) } catch (e) { throw e }

// after — distinguish cancellation from real relay failure
try { await client.sendRequest(method, params) }
catch (error) {
  if (error instanceof Error && /relay pairing client closed/.test(error.message)) return // expected on dispose
  throw error
}
Defensive patterns

Strategy: try-catch

Type guard

function isRelayClientClosedError(error: unknown): boolean {
  return error instanceof Error && error.message === 'relay pairing client closed'
}

Try / catch

try {
  await candidate.sendRequest(method, params)
} catch (error) {
  if (isRelayClientClosedError(error)) {
    // cancellation — caller invoked close() during recovery; not a relay fault
    return
  }
  throw error
}

Prevention

When it happens

Trigger: Concretely: (1) the pre-profile pairing coordinator's dispose() ran (timer fired or caller cancelled), which iterates clients and calls client.close(); (2) racePairingCandidates declared another candidate the winner, prompting the loser (this relay candidate) to be closed; (3) startPreProfilePairing's .finally() block at line 112-115 closed the client while recovery was awaiting backOff.

Common situations: Appears during normal pairing races where the direct path wins while the relay is still resolving a director move, or when the user cancels pairing during a slow relay cell migration. Also surfaces in tests that don't await the recovery loop before tearing down.

Related errors


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