diff --git a/src/renderer/src/runtime/remote-runtime-terminal-multiplexer.ts b/src/renderer/src/runtime/remote-runtime-terminal-multiplexer.ts index 8078bc77736..3b2761504bf 100644 --- a/src/renderer/src/runtime/remote-runtime-terminal-multiplexer.ts +++ b/src/renderer/src/runtime/remote-runtime-terminal-multiplexer.ts @@ -110,6 +110,7 @@ type RemoteRuntimeMultiplexedTerminalState = { // Track it so a gap triggers a self-healing snapshot resync instead of // silently rendering corrupt/missing output (frame-drop resync). expectedSeq: number | undefined + // Why: compare command probes when initial/live frames supplied no high-water. commandProbeBaselineSeq: number | undefined recoverySnapshotSeq: number | undefined resyncInFlight: boolean @@ -1069,7 +1070,7 @@ class RemoteRuntimeTerminalMultiplexer { this.recoverStalledStream(stream) return } - stream.commandProbeBaselineSeq = snapshot.seq + stream.commandProbeBaselineSeq ??= snapshot.seq } else if ( typeof snapshot?.seq === 'number' && typeof stream.expectedSeq === 'number' && diff --git a/src/renderer/src/runtime/remote-runtime-terminal-stall-recovery.test.ts b/src/renderer/src/runtime/remote-runtime-terminal-stall-recovery.test.ts index e12c0d7c280..8162b475df4 100644 --- a/src/renderer/src/runtime/remote-runtime-terminal-stall-recovery.test.ts +++ b/src/renderer/src/runtime/remote-runtime-terminal-stall-recovery.test.ts @@ -221,10 +221,30 @@ describe('remote terminal stalled stream recovery', () => { emitSnapshot(stream.streamId, samePayload?.requestId ?? 0, 'first-output', 12) await vi.advanceTimersByTimeAsync(0) + expect(onTransportClose).not.toHaveBeenCalled() + expect(stream.sendInput('regressed-snapshot\r')).toBe(true) + await vi.advanceTimersByTimeAsync(REMOTE_TERMINAL_COMMAND_RESPONSE_TIMEOUT_MS) + const regressedRequest = sentFrames(TerminalStreamOpcode.SnapshotRequest)[2] + const regressedPayload = regressedRequest + ? decodeTerminalStreamJson<{ requestId: number }>(regressedRequest.payload) + : null + emitSnapshot(stream.streamId, regressedPayload?.requestId ?? 0, 'partial-output', 8) + await vi.advanceTimersByTimeAsync(0) + + expect(onTransportClose).not.toHaveBeenCalled() + expect(stream.sendInput('still-below-baseline\r')).toBe(true) + await vi.advanceTimersByTimeAsync(REMOTE_TERMINAL_COMMAND_RESPONSE_TIMEOUT_MS) + const belowBaselineRequest = sentFrames(TerminalStreamOpcode.SnapshotRequest)[3] + const belowBaselinePayload = belowBaselineRequest + ? decodeTerminalStreamJson<{ requestId: number }>(belowBaselineRequest.payload) + : null + emitSnapshot(stream.streamId, belowBaselinePayload?.requestId ?? 0, 'partial-output', 10) + await vi.advanceTimersByTimeAsync(0) + expect(onTransportClose).not.toHaveBeenCalled() expect(stream.sendInput('echo second-output\r')).toBe(true) await vi.advanceTimersByTimeAsync(REMOTE_TERMINAL_COMMAND_RESPONSE_TIMEOUT_MS) - const advancedRequest = sentFrames(TerminalStreamOpcode.SnapshotRequest)[2] + const advancedRequest = sentFrames(TerminalStreamOpcode.SnapshotRequest)[4] const advancedPayload = advancedRequest ? decodeTerminalStreamJson<{ requestId: number }>(advancedRequest.payload) : null