mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 00:03:52 +00:00
fix(daemon): bound durable snapshot replay
This commit is contained in:
@@ -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<TerminalSnapshot> {
|
||||
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
|
||||
|
||||
@@ -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<string, { incarnationId?: string }[]>
|
||||
@@ -1219,9 +1221,10 @@ export class DaemonPtyAdapter implements IPtyProvider {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const scrollbackRows = normalizeDesktopTerminalSnapshotRows(opts.scrollbackRows)
|
||||
const result = await this.client.request<GetSnapshotResult>('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<GetSnapshotResult['snapshot']>
|
||||
liveSnapshot: NonNullable<GetSnapshotResult['snapshot']>,
|
||||
scrollbackRows?: number
|
||||
): Promise<NonNullable<GetSnapshotResult['snapshot']>> {
|
||||
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
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user