diff --git a/src/main/daemon/daemon-durable-history-snapshot.ts b/src/main/daemon/daemon-durable-history-snapshot.ts index 8328dab9712..cad0fcc7e2d 100644 --- a/src/main/daemon/daemon-durable-history-snapshot.ts +++ b/src/main/daemon/daemon-durable-history-snapshot.ts @@ -23,7 +23,7 @@ export function terminalSnapshotFromColdRestore( ): TerminalSnapshot { return { snapshotAnsi: info.snapshotAnsi, - scrollbackAnsi: info.scrollbackAnsi, + scrollbackAnsi: info.modes.alternateScreen ? info.scrollbackAnsi : '', oscLinks: info.oscLinks, rehydrateSequences: info.rehydrateSequences, ...(info.pendingEscapeTailAnsi ? { pendingEscapeTailAnsi: info.pendingEscapeTailAnsi } : {}), @@ -42,12 +42,17 @@ export async function buildDurableCheckpointSnapshot(opts: { liveSnapshot: TerminalSnapshot restoreInfo: ColdRestoreInfo | null pendingRecords?: readonly PendingOutputRecord[] + scrollbackRows?: number }): Promise { const pendingRecords = opts.pendingRecords ?? [] if (!opts.restoreInfo && pendingRecords.length === 0) { return opts.liveSnapshot } - if (opts.restoreInfo && pendingRecords.length === 0) { + if ( + opts.restoreInfo && + pendingRecords.length === 0 && + (opts.scrollbackRows === undefined || opts.scrollbackRows >= DAEMON_RESTORE_SCROLLBACK_ROWS) + ) { return terminalSnapshotFromColdRestore(opts.restoreInfo, { outputSequence: opts.liveSnapshot.outputSequence, frameRestoreAnsi: opts.liveSnapshot.frameRestoreAnsi @@ -57,7 +62,10 @@ export async function buildDurableCheckpointSnapshot(opts: { const emulator = new HeadlessEmulator({ cols: opts.restoreInfo?.cols ?? opts.liveSnapshot.cols, rows: opts.restoreInfo?.rows ?? opts.liveSnapshot.rows, - scrollback: DAEMON_RESTORE_SCROLLBACK_ROWS + scrollback: Math.min( + opts.scrollbackRows ?? DAEMON_RESTORE_SCROLLBACK_ROWS, + DAEMON_RESTORE_SCROLLBACK_ROWS + ) }) const replay = new ColdRestoreReplayWriter(emulator) try { @@ -104,7 +112,7 @@ export async function buildDurableCheckpointSnapshot(opts: { function restoreBaseFrom(restoreInfo: ColdRestoreInfo): RestoreBase { return { - scrollbackAnsi: restoreInfo.scrollbackAnsi, + scrollbackAnsi: restoreInfo.modes.alternateScreen ? restoreInfo.scrollbackAnsi : '', rehydrateSequences: restoreInfo.rehydrateSequences, snapshotAnsi: restoreInfo.snapshotAnsi, ...(restoreInfo.pendingEscapeTailAnsi diff --git a/src/main/daemon/daemon-pty-adapter.ts b/src/main/daemon/daemon-pty-adapter.ts index c651503e25f..7af91fbf370 100644 --- a/src/main/daemon/daemon-pty-adapter.ts +++ b/src/main/daemon/daemon-pty-adapter.ts @@ -70,6 +70,7 @@ import { resolveSafePtyDefaultCwd } from '../providers/pty-default-cwd' import { PtyWriteUnavailableError } from '../providers/pty-write-unavailable-error' import { ColdRestorePayloadCache, type ColdRestorePayload } from './cold-restore-payload-cache' import { buildDurableCheckpointSnapshot } from './daemon-durable-history-snapshot' +import { DAEMON_SESSION_SCROLLBACK_ROWS } from './daemon-session-scrollback-window' import { PtyProcessListAdmission } from '../providers/pty-process-list-admission' import { iterateTerminalHistorySeedChunks, @@ -87,6 +88,7 @@ import { } from './daemon-audit-classifier' import type { DaemonEvidenceSource, ExactDaemonIncarnation } from './daemon-incarnation-evidence' import { createDaemonAuditEligibilityTracker } from './daemon-audit-eligibility-event' +import { normalizeDesktopTerminalSnapshotRows } from '../../shared/terminal-scrollback-policy' type PendingDaemonSpawnOperation = { exitsBySessionId: Map @@ -1219,9 +1221,10 @@ export class DaemonPtyAdapter implements IPtyProvider { return null } try { + const scrollbackRows = normalizeDesktopTerminalSnapshotRows(opts.scrollbackRows) const result = await this.client.request('getSnapshot', { sessionId: id, - ...(typeof opts.scrollbackRows === 'number' ? { scrollbackRows: opts.scrollbackRows } : {}) + ...(scrollbackRows !== undefined ? { scrollbackRows } : {}) }) const snapshot = result.snapshot // Why: older v19 daemons lack an absolute output sequence, so their snapshot can't reconcile bytes queued on the other socket. @@ -1229,8 +1232,10 @@ export class DaemonPtyAdapter implements IPtyProvider { return null } const restored = - this.historyManager && this.historyReader && opts.scrollbackRows !== 0 - ? await this.overlayDurableRestoreSnapshot(id, snapshot) + this.historyManager && + this.historyReader && + (scrollbackRows === undefined || scrollbackRows > DAEMON_SESSION_SCROLLBACK_ROWS) + ? await this.overlayDurableRestoreSnapshot(id, snapshot, scrollbackRows) : snapshot return this.toProviderBufferSnapshot(restored) } catch { @@ -1268,7 +1273,8 @@ export class DaemonPtyAdapter implements IPtyProvider { private async overlayDurableRestoreSnapshot( sessionId: string, - liveSnapshot: NonNullable + liveSnapshot: NonNullable, + scrollbackRows?: number ): Promise> { if (!this.historyManager || !this.historyReader) { return liveSnapshot @@ -1291,7 +1297,8 @@ export class DaemonPtyAdapter implements IPtyProvider { } return await buildDurableCheckpointSnapshot({ liveSnapshot, - restoreInfo + restoreInfo, + scrollbackRows }) } catch { return liveSnapshot diff --git a/src/main/daemon/daemon-restore-scrollback-depth.test.ts b/src/main/daemon/daemon-restore-scrollback-depth.test.ts index cccd8c1a660..c5aa21fefa4 100644 --- a/src/main/daemon/daemon-restore-scrollback-depth.test.ts +++ b/src/main/daemon/daemon-restore-scrollback-depth.test.ts @@ -191,6 +191,7 @@ describe('STA-4091 previously recoverable restore depth', () => { restoreInfo }) expect(durable.outputSequence).toBe(9) + expect(durable.scrollbackAnsi).toBe('') expect(snapshotText(durable)).toContain(PREVIOUSLY_RECOVERABLE_LINE) } finally { live.dispose() @@ -272,6 +273,23 @@ describe('STA-4091 previously recoverable restore depth', () => { expect(text).toContain(NEWEST_WRITTEN_LINE) expect(text).toContain(PREVIOUSLY_RECOVERABLE_LINE) expect(text).toContain(OLDEST_WRITTEN_LINE) + expect(text.split(OLDEST_WRITTEN_LINE)).toHaveLength(2) + }) + + it('honors a bounded remount snapshot depth', async () => { + const { id } = await adapter.spawn({ + cols: 80, + rows: 24, + sessionId: 'bounded-remount-depth', + cwd: '/tmp' + }) + lastSubprocess.emitData(numberedOutput(DESKTOP_TERMINAL_SCROLLBACK_ROWS_DEFAULT)) + + const snapshot = await adapter.getBufferSnapshot(id, { scrollbackRows: 24 }) + const text = `${snapshot?.scrollbackAnsi ?? ''}${snapshot?.data ?? ''}` + expect(text).toContain(NEWEST_WRITTEN_LINE) + expect(text).not.toContain(PREVIOUSLY_RECOVERABLE_LINE) + expect(text).not.toContain(OLDEST_WRITTEN_LINE) }) it('restores that depth after a keepHistory restart compact', async () => {