fix(relay): close client when pty.replay overflows control queue

Replay is never retried, so it uses the control lane where overflow
is fatal — the writer closes the client and reconnect reloads history
rather than stranding a short buffer.
This commit is contained in:
Jinjing
2026-08-01 14:18:49 -07:00
parent 4ac4eda064
commit 233102569d
3 changed files with 24 additions and 0 deletions
+2
View File
@@ -764,6 +764,8 @@ function configureRelaySessionCallbacks(session: SshRelaySession): void {
clearRelayLostBackoff(tid)
return
}
// Why: still mid-transition — re-arm at the max delay without consuming an attempt. It ends once
// the transport settles: 'connected' redeploys, a terminal status or a dropped session clears above.
scheduleRelayRedeploy(RELAY_LOST_MAX_DELAY_MS)
}, delay)
relayLostBackoff.set(tid, state)
@@ -300,6 +300,26 @@ describe('RelayDispatcher bounded-capacity degradation', () => {
}
})
it('closes the client when pty.replay overflows the control queue', () => {
const primary = makeSaturatingClient(65536)
const stderr = vi.spyOn(process.stderr, 'write').mockReturnValue(true)
const bounded = new RelayDispatcher(primary.write, primary.options)
try {
const clientId = bounded.activeClientIds()[0]
for (let index = 0; index < DISPATCHER_CONTROL_QUEUE_MAX_FRAMES; index += 1) {
bounded.notifyClient(clientId, `control.${index}`)
}
expect(primary.closes).toBe(0)
// Replay is never retried, so an unnoticed drop would strand the pane on a short buffer.
bounded.notify('pty.replay', { paneKey: 'tab-1:pane-1', data: 'x' })
expect(primary.closes).toBe(1)
} finally {
stderr.mockRestore()
bounded.dispose()
}
})
it('publishProducerNotification reports its drops like notify', () => {
const primary = makeBoundedClient(65536)
const secondary = makeBoundedClient(16384)
+2
View File
@@ -486,6 +486,8 @@ export class RelayDispatcher {
continue
}
if (method === 'pty.replay') {
// Why: replay is never re-sent, so it takes the control lane where overflow is fatal — the
// writer closes the client and reconnect reloads history rather than stranding a short buffer.
this.enqueueFrame(client, msg, 'control', undefined, frameBytes)
continue
}