fix(terminal): detach when recovery snapshot is unavailable

This commit is contained in:
OrcaWin
2026-07-22 22:15:59 -07:00
parent 871f416c4c
commit dd5879469e
2 changed files with 21 additions and 12 deletions
+10 -10
View File
@@ -1716,23 +1716,23 @@ export const TERMINAL_METHODS: RpcAnyMethod[] = [
if (closed || streams.get(stream.streamId) !== stream) {
return
}
const size = runtime.getTerminalSize(stream.ptyId)
if (!serialized) {
throw new Error('Remote terminal recovery snapshot unavailable.')
}
const displayMode = runtime.getMobileDisplayMode(stream.ptyId)
// Why: dropped ACK-pending output breaks live replay; send a fresh snapshot before resuming output.
// Why: clients discard truncated snapshots, so mark truncated only when serialization actually failed.
sendSnapshotFrames((opcode, payload) => sendFrame(stream.streamId, opcode, payload), {
kind: 'scrollback',
cols: serialized?.cols ?? size?.cols ?? 80,
rows: serialized?.rows ?? size?.rows ?? 24,
cols: serialized.cols,
rows: serialized.rows,
displayMode,
reason: 'ack-pending-overflow',
seq: serialized?.seq,
source: serialized?.source,
truncated: !serialized,
truncatedByByteBudget: serialized?.truncatedByByteBudget,
data: serialized?.data ?? ''
seq: serialized.seq,
source: serialized.source,
truncatedByByteBudget: serialized.truncatedByByteBudget,
data: serialized.data
})
if (serialized && typeof serialized.seq === 'number') {
if (typeof serialized.seq === 'number') {
// Why: chunks queued before the snapshot serialized are already in it; replaying them would duplicate output.
const snapshotSeq = serialized.seq
const retained = stream.ackPendingOutput.filter(
@@ -1676,14 +1676,23 @@ describe('terminal multiplex RPC', () => {
await dispatchPromise
})
it('ends a stream when ACK overflow recovery serialization fails', async () => {
it.each([
{
failure: 'throws',
recover: () => Promise.reject(new Error('snapshot unavailable'))
},
{
failure: 'returns no snapshot',
recover: () => Promise.resolve(null)
}
])('ends a stream when ACK overflow recovery serialization $failure', async ({ recover }) => {
const dataListenerRef: {
current?: (data: string, meta?: { seq?: number; rawLength?: number }) => void
} = {}
const serializeTerminalBuffer = vi
.fn()
.mockResolvedValueOnce({ data: 'initial snapshot', cols: 120, rows: 40 })
.mockRejectedValue(new Error('snapshot unavailable'))
.mockImplementation(recover)
const harness = startDesktopMultiplexSubscribe({
serializeTerminalBuffer,
subscribeToTerminalData: vi.fn(