From 233102569d4d7b5e363ce2faed3f55450de52f1a Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 1 Aug 2026 14:18:49 -0700 Subject: [PATCH] fix(relay): close client when pty.replay overflows control queue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/main/ipc/ssh.ts | 2 ++ .../dispatcher-capacity-degradation.test.ts | 20 +++++++++++++++++++ src/relay/dispatcher.ts | 2 ++ 3 files changed, 24 insertions(+) diff --git a/src/main/ipc/ssh.ts b/src/main/ipc/ssh.ts index 81a1488f96c..e1f74bff7b0 100644 --- a/src/main/ipc/ssh.ts +++ b/src/main/ipc/ssh.ts @@ -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) diff --git a/src/relay/dispatcher-capacity-degradation.test.ts b/src/relay/dispatcher-capacity-degradation.test.ts index 43a40a9d7cc..a546989d5df 100644 --- a/src/relay/dispatcher-capacity-degradation.test.ts +++ b/src/relay/dispatcher-capacity-degradation.test.ts @@ -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) diff --git a/src/relay/dispatcher.ts b/src/relay/dispatcher.ts index 2a56c187afa..e7be71adf8f 100644 --- a/src/relay/dispatcher.ts +++ b/src/relay/dispatcher.ts @@ -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 }