mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 10:08:13 +00:00
* fix(remote-runtime): probe sockets and reset backoff on wake, and keep late resubscribes revivable Rebuilt from #23359 on current main. On OS resume or network return every remote-runtime socket pings once and is replaced if it stays silent for 8 s, the shared-control retry ladder restarts from its short delays, and a resubscribe that fails or opens after the pane's recovery window latched is parked so resume/online can revive it. Co-authored-by: Meapri <104259736+Meapri@users.noreply.github.com> * fix(remote-terminal): revive a latched remote pane when its host answers again A pane whose ~3 min auto-recovery window ran out only came back on an OS online/resume event or a Reconnect click. A Tailscale or VPN route that returns fires neither, so the pane stayed dead over a working link even though the host shell was alive. The environment's shared control socket reaching ready again is now the revive signal: the pane advances its backoff or replays its attach, exactly as Reconnect does. Adds e2e repros for paired runtimes and managed SSH hosts: 30 s silent and reset outages, and a silent outage longer than the recovery window. * fix(remote-terminal): hold the control-ready reviver only while a pane is recovering * fix(remote-runtime): re-probe a wake check whose deadline fired late, and trim the SSH outage spec --------- Co-authored-by: Meapri <104259736+Meapri@users.noreply.github.com>
105 lines
2.9 KiB
TypeScript
105 lines
2.9 KiB
TypeScript
import { scheduleSharedControlReconnect } from './remote-runtime-shared-control-state'
|
|
|
|
const ACTIVE_RECONNECT_DELAYS_MS = [250, 500, 1000, 2000, 4000, 8000, 15_000, 30_000]
|
|
const IDLE_RECONNECT_DELAYS_MS = [...ACTIVE_RECONNECT_DELAYS_MS, 60_000, 120_000, 300_000]
|
|
|
|
export class SharedControlReconnectScheduler {
|
|
private timer: ReturnType<typeof setTimeout> | null = null
|
|
private attempt = 0
|
|
private pendingOpen: (() => void) | null = null
|
|
|
|
get isScheduled(): boolean {
|
|
return this.timer !== null
|
|
}
|
|
|
|
get attemptCount(): number {
|
|
return this.attempt
|
|
}
|
|
|
|
schedule(args: {
|
|
intentionallyClosed: boolean
|
|
delaysMs: readonly number[]
|
|
open: () => void
|
|
}): void {
|
|
if (this.timer || args.intentionallyClosed) {
|
|
return
|
|
}
|
|
// Why: a passive subscription owns recovery until its caller closes it; roaming outages are unbounded.
|
|
this.pendingOpen = args.open
|
|
const scheduled = scheduleSharedControlReconnect({
|
|
...args,
|
|
current: this.timer,
|
|
reconnectAttempt: this.attempt,
|
|
open: () => {
|
|
this.timer = null
|
|
this.pendingOpen = null
|
|
args.open()
|
|
}
|
|
})
|
|
this.timer = scheduled.timer
|
|
this.attempt = scheduled.reconnectAttempt
|
|
}
|
|
|
|
scheduleWithDefaultBackoff(intentionallyClosed: boolean, open: () => void): void {
|
|
this.schedule({
|
|
intentionallyClosed,
|
|
delaysMs: ACTIVE_RECONNECT_DELAYS_MS,
|
|
open
|
|
})
|
|
}
|
|
|
|
scheduleWithIdleBackoff(intentionallyClosed: boolean, open: () => void): void {
|
|
this.schedule({ intentionallyClosed, delaysMs: IDLE_RECONNECT_DELAYS_MS, open })
|
|
}
|
|
|
|
scheduleAfterSocketClose(args: {
|
|
intentionallyClosed: boolean
|
|
manuallyDisconnected: boolean
|
|
capabilityPaused: boolean
|
|
subscriptionCount: number
|
|
open: () => void
|
|
}): void {
|
|
if (
|
|
args.intentionallyClosed ||
|
|
args.manuallyDisconnected ||
|
|
(args.subscriptionCount === 0 && args.capabilityPaused)
|
|
) {
|
|
return
|
|
}
|
|
if (args.subscriptionCount > 0) {
|
|
this.scheduleWithDefaultBackoff(args.intentionallyClosed, args.open)
|
|
return
|
|
}
|
|
this.scheduleWithIdleBackoff(args.intentionallyClosed, args.open)
|
|
}
|
|
|
|
// Why: OS resume / browser online should advance an already-scheduled reconnect, not start a new one.
|
|
retryNow(): boolean {
|
|
// Why: a wake or network change is fresh evidence, so the next failure starts again from the short
|
|
// delays instead of the 5-minute idle cap earned during the outage — also when an open is already
|
|
// in flight and no timer is pending (#9092).
|
|
this.attempt = 0
|
|
if (!this.timer || !this.pendingOpen) {
|
|
return false
|
|
}
|
|
clearTimeout(this.timer)
|
|
this.timer = null
|
|
const open = this.pendingOpen
|
|
this.pendingOpen = null
|
|
open()
|
|
return true
|
|
}
|
|
|
|
clear(): void {
|
|
if (this.timer) {
|
|
clearTimeout(this.timer)
|
|
this.timer = null
|
|
}
|
|
this.pendingOpen = null
|
|
}
|
|
|
|
resetAttempt(): void {
|
|
this.attempt = 0
|
|
}
|
|
}
|