fix(remote): preserve terminal recovery across control refresh (#11513)

* fix(remote): recover stalled terminal streams

* fix(i18n): localize manual disconnect error

* fix(remote): park paired terminals with host snapshots

* test(remote): mock authoritative resync snapshots

* fix(terminal): defer startup mounts until hydration

* fix(remote): raise paired terminal stream capacity

* fix(remote): harden terminal recovery lifecycle

* fix(remote): preserve calls across control refresh

* test(remote): harden paired recovery oracle

* test(workspace): seed Jira source context

* test(remote): assert raw host terminal identities

* test(terminal): keep restore sentinels atomic

* test(terminal): keep restore sentinel on one row

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-07-30 03:05:10 -07:00
committed by GitHub
co-authored by OrcaWin
parent 9eede0084d
commit ab665a3ce7
9 changed files with 152 additions and 43 deletions
@@ -138,6 +138,29 @@ describe('RemoteRuntimeSharedControlConnection', () => {
expect(open).toHaveBeenCalledOnce()
})
it('keeps a waiting request alive when a reachability probe replaces its pre-ready socket', async () => {
const server = await createServer({ suppressReadyFrameCount: 1 })
const connection = new RemoteRuntimeSharedControlConnection(server.pairing)
const response = connection.request('worktree.ps', undefined, 1000)
await vi.waitFor(() => expect(server.connectionCount()).toBe(1))
connection.reconnectNow()
await expect(response).resolves.toMatchObject({
ok: true,
result: { method: 'worktree.ps' }
})
expect(server.connectionCount()).toBe(2)
expect(server.requests.map(({ method }) => method)).toEqual(['worktree.ps'])
expect(connection.getDiagnostics().pendingRequestCount).toBe(0)
expect(getRemoteRuntimeRequestAdmissionEvidence()).toEqual({
pendingRequestCount: 0,
retainedBytes: 0
})
connection.close()
})
it('logs unknown response ids without breaking pending requests', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
const server = await createServer({ sendUnknownResponseBeforeResponse: true })
@@ -665,6 +688,7 @@ async function createServer(
closeAfterFirstStreamingResponse?: boolean
closeBeforeResponse?: boolean
suppressReadyFrame?: boolean
suppressReadyFrameCount?: number
// Why: half-open simulation — the socket stays open but never answers
// protocol pings, like a wedged tunnel that swallows frames silently.
disableAutoPong?: boolean
@@ -696,7 +720,10 @@ async function createServer(
serverKeyPair.secretKey,
publicKeyFromBase64(hello.publicKeyB64)
)
if (options.suppressReadyFrame) {
if (
options.suppressReadyFrame ||
connectionCount <= (options.suppressReadyFrameCount ?? 0)
) {
return
}
ws.send(JSON.stringify({ type: 'e2ee_ready' }))
@@ -130,7 +130,10 @@ export class RemoteRuntimeSharedControlConnection {
return
}
// Why: a successful one-shot status probe proves the restarted endpoint is reachable; replace even a stuck CONNECTING/awaiting-ready socket instead of waiting behind stale backoff.
this.closeSocket(remoteRuntimeUnavailableError('Refreshing remote runtime control transport.'))
this.closeSocket(
remoteRuntimeUnavailableError('Refreshing remote runtime control transport.'),
true
)
this.open()
}
@@ -208,7 +211,11 @@ export class RemoteRuntimeSharedControlConnection {
sendEncrypted: (payload) => this.sendEncrypted(payload),
markReady: () => {
this.lastConnectedAt = Date.now()
this.scheduleReconnectAttemptReset()
this.readyStableReset.schedule({
getState: () => this.state,
getSocket: () => this.ws,
reset: () => this.reconnect.resetAttempt()
})
},
replaySubscriptions: () => this.replaySubscriptions()
})
@@ -293,7 +300,7 @@ export class RemoteRuntimeSharedControlConnection {
}
}
private closeSocket(error?: Error): void {
private closeSocket(error?: Error, preserveReadyWaitersAndPendingRequests = false): void {
closeSharedControlSocket({
environmentId: this.options.environmentId,
state: this.state,
@@ -304,18 +311,11 @@ export class RemoteRuntimeSharedControlConnection {
socketCleanup: this.socketCleanup,
ws: this.ws,
error,
preserveReadyWaitersAndPendingRequests,
clearReadyStableTimer: () => this.readyStableReset.clear()
})
this.ws = this.sharedKey = null
this.socketCleanup = null
this.state = 'closed'
}
private scheduleReconnectAttemptReset(): void {
this.readyStableReset.schedule({
getState: () => this.state,
getSocket: () => this.ws,
reset: () => this.reconnect.resetAttempt()
})
}
}
@@ -19,6 +19,7 @@ export function closeSharedControlSocket(args: {
readyWaiters: SharedControlReadyWaiter[]
lastClose: { code: number; reason: string } | null
error?: Error
preserveReadyWaitersAndPendingRequests?: boolean
clearReadyStableTimer: () => void
}): void {
if (args.ws || args.socketCleanup) {
@@ -39,6 +40,7 @@ export function closeSharedControlSocket(args: {
subscriptions: args.subscriptions,
socketCleanup: args.socketCleanup,
ws: args.ws,
error: args.error
error: args.error,
preserveReadyWaitersAndPendingRequests: args.preserveReadyWaitersAndPendingRequests
})
}
@@ -186,9 +186,15 @@ export function closeSharedControlSocketState(args: {
socketCleanup: (() => void) | null
ws: { close: () => void } | null
error?: Error
preserveReadyWaitersAndPendingRequests?: boolean
}): void {
rejectSharedControlReadyWaiters(args.readyWaiters, args.error ?? remoteRuntimeUnavailableError())
rejectAllSharedControlPendingRequests(args.pendingRequests, args.error)
if (!args.preserveReadyWaitersAndPendingRequests) {
rejectSharedControlReadyWaiters(
args.readyWaiters,
args.error ?? remoteRuntimeUnavailableError()
)
rejectAllSharedControlPendingRequests(args.pendingRequests, args.error)
}
markSharedControlSubscriptionsUnsent(args.subscriptions)
try {
args.socketCleanup?.()