mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 00:02:38 +00:00
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:
@@ -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?.()
|
||||
|
||||
Reference in New Issue
Block a user