mirror of
https://github.com/stablyai/orca.git
synced 2026-10-28 16:02:55 +00:00
Recover automatically instead of getting stuck on "Multiplexer disposed" when an SSH relay drops (#12216)
* fix(ssh): recover instead of wedging when the relay channel dies mid-connect Three coupled defects made a dropped SSH relay look like a permanent bug: 1. SshRelaySession.establish()/reconnect() ran their last liveness gate before configureRelayGraceTime(), whose mux.notify() can dispose the mux synchronously (writer control-lane admission cap, or a throwing transport). The session then latched _state='ready' + _onReady (status bar "connected") while watchMuxForRelayLoss() silently no-op'd on the dead mux, so the bounded relay backoff in ipc/ssh.ts never ran and the fs/pty/git providers stayed registered against a dead multiplexer. Both sites now re-check mux.isDisposed() after the notify and take the existing failure path. 2. SshChannelMultiplexer.request()/notifyWithSettlement() always reported the permanent-shutdown string 'Multiplexer disposed' with no code, even when the recorded dispose reason was connection_lost. The reason is now recorded and a shared disposedError() factory serves dispose(), request(), notifyWithSettlement(), so a transient drop reports 'SSH connection lost, reconnecting...' / CONNECTION_LOST. onDispose() on an already-disposed mux now fires the handler synchronously with that reason instead of returning a silent no-op (without retaining it). 3. TerminalErrorToast no longer renders a transient relay drop in the red "please file an issue" style. The marker is matched with includes() because the message reaches the toast IPC-wrapped. ssh-git-response-stream-reader registers its onDispose subscriber after the abort wiring, since an already-dead mux now fails synchronously there and the cleanup must be able to drop the caller's abort listener. Closes #11953 * fix(ssh): treat a mux killed during PTY reattach as relay loss reconnect()'s post-reattach gate bare-returned when ownsAttempt() went false, and reattachKnownPtys swallows every per-PTY error, so a control-lane failure during a large reattach burst disposed the mux without ever reaching the catch: providers stayed bound to the dead mux, no relay-loss watcher was installed, and the session wedged in 'reconnecting' until restart. Take the failure path when our own mux is the one that died so ssh.ts's bounded backoff retries. Co-authored-by: Orca <help@stably.ai> * fix(ssh): recover when relay dies during setup instead of wedging Introduce verifyRelayAttempt() to detect mux disposal at each setup phase (consumer session, home resolution, provider registration, PTY reattach). Routes mid-setup connection loss into relay-loss recovery instead of hanging in reconnecting state. * Extract SSH disposal error factory Multiple sites were duplicating the disposal error creation logic with specific message and code values. The renderer uses these to distinguish temporary disconnects (show reconnection overlay) from permanent shutdown (show error toast), so all producers must use the same factory to avoid silent UI degradation. --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
This commit is contained in:
committed by
Jinjing
co-authored by
Orca
Jinjing
parent
7146931cf5
commit
be71206cc6
@@ -1,4 +1,5 @@
|
||||
import type { SshChannelMultiplexer } from './ssh-channel-multiplexer'
|
||||
import { createSshDisposalError } from './ssh-channel-multiplexer'
|
||||
import { RelayErrorCode, isGitResponseStreamMarker } from './relay-protocol'
|
||||
|
||||
const SENTINEL_STREAM_ID = -1
|
||||
@@ -245,18 +246,6 @@ export function requestGitStreamable(
|
||||
handleStreamError(p)
|
||||
})
|
||||
)
|
||||
unsubscribers.push(
|
||||
mux.onDispose((reason) => {
|
||||
const err = new Error(
|
||||
reason === 'connection_lost'
|
||||
? 'SSH connection lost, reconnecting...'
|
||||
: 'Multiplexer disposed'
|
||||
) as Error & { code: string }
|
||||
err.code = reason === 'connection_lost' ? 'CONNECTION_LOST' : 'DISPOSED'
|
||||
fail(err)
|
||||
})
|
||||
)
|
||||
|
||||
if (options?.signal) {
|
||||
const signal = options.signal
|
||||
if (signal.aborted) {
|
||||
@@ -274,6 +263,10 @@ export function requestGitStreamable(
|
||||
unsubscribers.push(() => signal.removeEventListener('abort', onAbort))
|
||||
}
|
||||
|
||||
// Why: registered last because an already-disposed mux fails synchronously here,
|
||||
// and that cleanup must be able to drop the abort listener above (#11953).
|
||||
unsubscribers.push(mux.onDispose((reason) => fail(createSshDisposalError(reason))))
|
||||
|
||||
// Why: forward only the mux-request options (signal/timeoutMs) and omit them
|
||||
// entirely when absent, so callers that previously issued a 2-arg
|
||||
// mux.request keep the same call shape (and their tests). inactivityTimeoutMs
|
||||
|
||||
Reference in New Issue
Block a user