stablyai/orca · error

relay pairing client closed

Error message

relay pairing client closed

What it means

Thrown in the returned client's sendRequest when, after awaiting authenticatedPromise, closed || !authenticated is true. The socket has been torn down (via close(), a transport error, or onclose) or never authenticated, so no further RPCs can be sent.

Source

Thrown at mobile/src/transport/mobile-relay-physical-client.ts:166

      log('info', 'Relay: pairing socket closed', cellHost)
    } else {
      log('warn', 'Relay: pairing socket closed', pairingRelayErrorDetail(error))
    }
    channel.dispose()
    rejectAuthenticated(error)
    for (const request of pending.values()) {
      clearTimeout(request.timer)
      request.reject(error)
    }
    pending.clear()
    socket.close()
  }

  return {
    async sendRequest(method, params) {
      await authenticatedPromise
      if (closed || !authenticated) {
        throw new Error('relay pairing client closed')
      }
      const id = `relay-pair-${++requestCounter}`
      return new Promise<RpcResponse>((resolve, reject) => {
        const timer = setTimeout(() => {
          pending.delete(id)
          reject(new Error(`relay pairing RPC timed out: ${method}`))
        }, requestTimeoutMs)
        pending.set(id, { resolve, reject, timer })
        if (
          !channel.sendText(JSON.stringify({ id, deviceToken: args.deviceToken, method, params }))
        ) {
          clearTimeout(timer)
          pending.delete(id)
          reject(new Error('relay E2EE channel not ready'))
        }
      })
    },
    close: () => {

View on GitHub (pinned to 1136503c6a)

Solutions

  1. Await the previous request and handle its rejection before sending again.
  2. Treat this error as terminal for the client; create a new one via connectMobileRelayForPairing to reconnect.
  3. Surface a disconnected state in the UI and disable further send attempts.

Example fix

// before
const resp = await client.sendRequest('install')
// ...later, after a close/error:
await client.sendRequest('resume-confirm') // throws 'relay pairing client closed'
// after
try {
  await client.sendRequest('install')
} catch {
  client = connectMobileRelayForPairing(args) // new client for retry
  await client.sendRequest('install')
}
Defensive patterns

Strategy: try-catch

Validate before calling

// The client does not expose `closed`; track it yourself alongside the handle.
let clientClosed = false
const client = connectMobileRelayForPairing(args)
const wrappedSend = async (method: string, params?: unknown) => {
  if (clientClosed) throw new Error('caller-side guard: client already closed')
  try {
    return await client.sendRequest(method, params)
  } catch (error) {
    clientClosed = true
    throw error
  }
}

Try / catch

try {
  await client.sendRequest('install')
} catch (error) {
  if (error instanceof Error && error.message === 'relay pairing client closed') {
    // terminal: build a new client via connectMobileRelayForPairing to retry
  }
}

Prevention

When it happens

Trigger: Calling sendRequest after close(), after a prior transport error/onclose triggered fail(), or while a previously awaited request was rejecting due to disconnect.

Common situations: The caller does not observe the rejection of a prior request and issues another; racing a request with a user-initiated close; a network drop mid-RPC.

Related errors


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