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
- 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.
- Ensure racePairingCandidates closes losing candidates only after the winner is fully selected, not speculatively.
- In tests, await any in-flight sendRequest promise before invoking close() on the candidate.
- 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
- Await in-flight sendRequest promises before calling close() on a relay candidate.
- Treat 'relay pairing client closed' as a cancellation signal in error-reporting/UI layers, not a transport failure.
- In racePairingCandidates, close losing candidates only after the winner is committed.
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
- mobile relay pairing recovery pending
- mobile pairing cancelled
- mobile relay upgrade host was removed
- relay credential install result does not match pairing journ
- mobile relay pairing journal identity mismatch
AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12).
Data as JSON: /api/errors/6409f9b95815ab65.
Report an issue: GitHub.