fix(terminal): bound checkpoint serialization

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-07-29 19:46:44 -07:00
co-authored by Orca
parent d0ce3c7ec3
commit fa33c094b6
2 changed files with 301 additions and 48 deletions
@@ -1,60 +1,135 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import type { TerminalSnapshot } from './types'
import { serializeTerminalCheckpointWithinLimit } from './terminal-checkpoint-serializer'
function snapshotWithCountedLinks(
linkCount: number,
uri: string
): {
snapshot: TerminalSnapshot
uriReads: () => number
} {
let reads = 0
const oscLinks = Array.from({ length: linkCount }, (_, row) => ({
row,
startCol: 0,
endCol: 1,
get uri(): string {
reads += 1
return uri
}
}))
function snapshot(overrides: Partial<TerminalSnapshot> = {}): TerminalSnapshot {
return {
snapshot: {
snapshotAnsi: 'visible',
scrollbackAnsi: '',
oscLinks,
rehydrateSequences: '',
cwd: '/workspace',
modes: {
bracketedPaste: false,
mouseTracking: false,
applicationCursor: false,
alternateScreen: false
},
cols: 80,
rows: 24,
scrollbackLines: 0
snapshotAnsi: 'visible',
scrollbackAnsi: '',
rehydrateSequences: '',
cwd: '/workspace',
modes: {
bracketedPaste: false,
mouseTracking: false,
applicationCursor: false,
alternateScreen: false
},
uriReads: () => reads
cols: 80,
rows: 24,
scrollbackLines: 0,
...overrides
}
}
const metadata = {
cwd: '/workspace',
generation: 1,
checkpointedAt: '2026-07-29T00:00:00.000Z'
}
describe('terminal checkpoint serializer', () => {
it('traverses a large checkpoint payload once', async () => {
const linkCount = 4_096
const { snapshot, uriReads } = snapshotWithCountedLinks(
linkCount,
`https://example.com/${'x'.repeat(4_096)}`
)
it('matches JSON.stringify exactly at the UTF-8 byte limit', async () => {
const input = snapshot({
snapshotAnsi: `é漢😀${String.fromCharCode(0xd800, 0xdc00)}"\\${String.fromCharCode(
0x00,
0x08,
0x09,
0x0a,
0x0c,
0x0d,
0x1f,
0xd800,
0xdc00
)}`,
oscLinks: [{ row: 0, startCol: 0, endCol: 1, uri: 'https://example.com/😀\n' }]
})
const expected = JSON.stringify({
snapshotAnsi: input.snapshotAnsi,
scrollbackAnsi: input.scrollbackAnsi,
oscLinks: input.oscLinks,
rehydrateSequences: input.rehydrateSequences,
cwd: metadata.cwd,
cols: input.cols,
rows: input.rows,
modes: input.modes,
scrollbackLines: input.scrollbackLines,
generation: metadata.generation,
checkpointedAt: metadata.checkpointedAt
})
const exactBytes = Buffer.byteLength(expected, 'utf8')
const json = await serializeTerminalCheckpointWithinLimit(
snapshot,
{ cwd: snapshot.cwd, generation: 1, checkpointedAt: '2026-07-29T00:00:00.000Z' },
20 * 1024 * 1024
await expect(serializeTerminalCheckpointWithinLimit(input, metadata, exactBytes)).resolves.toBe(
expected
)
})
expect(Buffer.byteLength(json, 'utf8')).toBeLessThanOrEqual(20 * 1024 * 1024)
expect(uriReads()).toBe(linkCount)
it('does not materialize and rescan a passing candidate', async () => {
let reads = 0
const input = snapshot()
Object.defineProperty(input, 'snapshotAnsi', {
enumerable: true,
get: () => {
reads += 1
return 'visible'
}
})
const stringify = vi.spyOn(JSON, 'stringify')
const byteLength = vi.spyOn(Buffer, 'byteLength')
try {
await serializeTerminalCheckpointWithinLimit(input, metadata, 20 * 1024)
expect({
fullCandidateStringifies: stringify.mock.calls.filter(
([value]) => typeof value === 'object' && value !== null
).length,
byteLengthCalls: byteLength.mock.calls.length,
snapshotReads: reads
}).toEqual({ fullCandidateStringifies: 0, byteLengthCalls: 0, snapshotReads: 1 })
} finally {
stringify.mockRestore()
byteLength.mockRestore()
}
})
it('rejects an oversized escaped candidate before scanning or materializing it fully', async () => {
const oversized = String.fromCharCode(0).repeat(100_000)
const originalCharCodeAt = String.prototype.charCodeAt
let inspectedCodeUnits = 0
const charCodeAt = vi.spyOn(String.prototype, 'charCodeAt').mockImplementation(function (
this: string,
index: number
): number {
if (String(this) === oversized) {
inspectedCodeUnits += 1
}
return originalCharCodeAt.call(this, index)
})
const stringify = vi.spyOn(JSON, 'stringify')
try {
await serializeTerminalCheckpointWithinLimit(
snapshot({ snapshotAnsi: oversized }),
metadata,
512
)
const materializedOversizedCandidate = stringify.mock.calls.some(([value]) => {
return (value as { snapshotAnsi?: unknown })?.snapshotAnsi === oversized
})
expect({
inspectedCodeUnits,
inspectedEntireString: inspectedCodeUnits === oversized.length,
materializedOversizedCandidate
}).toEqual({
inspectedCodeUnits: expect.any(Number),
inspectedEntireString: false,
materializedOversizedCandidate: false
})
expect(inspectedCodeUnits).toBeGreaterThan(0)
} finally {
stringify.mockRestore()
charCodeAt.mockRestore()
}
})
})
@@ -31,9 +31,187 @@ function checkpointFile(
}
}
class BoundedJsonWriter {
private output = ''
private chunk = ''
private bytes = 0
private exceeded = false
constructor(private readonly maxBytes: number) {}
append(value: string, bytes: number): boolean {
if (this.bytes + bytes > this.maxBytes) {
this.exceeded = true
this.output = ''
this.chunk = ''
return false
}
this.bytes += bytes
this.chunk += value
if (this.chunk.length >= 16 * 1024) {
this.output += this.chunk
this.chunk = ''
}
return true
}
result(): string | null {
return this.exceeded ? null : this.output + this.chunk
}
}
function escapedCodeUnit(codeUnit: number): string | null {
switch (codeUnit) {
case 0x08:
return '\\b'
case 0x09:
return '\\t'
case 0x0a:
return '\\n'
case 0x0c:
return '\\f'
case 0x0d:
return '\\r'
case 0x22:
return '\\"'
case 0x5c:
return '\\\\'
default:
return codeUnit < 0x20 ? `\\u${codeUnit.toString(16).padStart(4, '0')}` : null
}
}
function appendJsonString(writer: BoundedJsonWriter, value: string): boolean {
if (!writer.append('"', 1)) {
return false
}
let spanStart = 0
let spanBytes = 0
for (let index = 0; index < value.length; index += 1) {
const codeUnit = value.charCodeAt(index)
let escaped = escapedCodeUnit(codeUnit)
if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
const next = value.charCodeAt(index + 1)
if (next >= 0xdc00 && next <= 0xdfff) {
spanBytes += 4
index += 1
} else {
escaped = `\\u${codeUnit.toString(16)}`
}
} else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
escaped = `\\u${codeUnit.toString(16)}`
} else if (escaped === null) {
spanBytes += codeUnit < 0x80 ? 1 : codeUnit < 0x800 ? 2 : 3
}
if (escaped !== null) {
if (
(index > spanStart && !writer.append(value.slice(spanStart, index), spanBytes)) ||
!writer.append(escaped, escaped.length)
) {
return false
}
spanStart = index + 1
spanBytes = 0
} else if (index + 1 - spanStart >= 16 * 1024) {
if (!writer.append(value.slice(spanStart, index + 1), spanBytes)) {
return false
}
spanStart = index + 1
spanBytes = 0
}
}
if (spanStart < value.length && !writer.append(value.slice(spanStart), spanBytes)) {
return false
}
return writer.append('"', 1)
}
function omittedByJson(value: unknown): boolean {
return value === undefined || typeof value === 'function' || typeof value === 'symbol'
}
function appendJsonValue(
writer: BoundedJsonWriter,
value: unknown,
activeObjects: Set<object>
): boolean {
if (value === null) {
return writer.append('null', 4)
}
switch (typeof value) {
case 'string':
return appendJsonString(writer, value)
case 'boolean':
return writer.append(value ? 'true' : 'false', value ? 4 : 5)
case 'number': {
const json = Number.isFinite(value) ? JSON.stringify(value) : 'null'
return writer.append(json, json.length)
}
case 'bigint':
throw new TypeError('Do not know how to serialize a BigInt')
case 'undefined':
case 'function':
case 'symbol':
return false
case 'object':
break
}
if (activeObjects.has(value)) {
throw new TypeError('Converting circular structure to JSON')
}
activeObjects.add(value)
try {
if (Array.isArray(value)) {
if (!writer.append('[', 1)) {
return false
}
for (let index = 0; index < value.length; index += 1) {
if (index > 0 && !writer.append(',', 1)) {
return false
}
const entry = value[index]
if (omittedByJson(entry)) {
if (!writer.append('null', 4)) {
return false
}
} else if (!appendJsonValue(writer, entry, activeObjects)) {
return false
}
}
return writer.append(']', 1)
}
if (!writer.append('{', 1)) {
return false
}
let entries = 0
for (const key of Object.keys(value)) {
const entry = (value as Record<string, unknown>)[key]
if (omittedByJson(entry)) {
continue
}
if (
(entries > 0 && !writer.append(',', 1)) ||
!appendJsonString(writer, key) ||
!writer.append(':', 1) ||
!appendJsonValue(writer, entry, activeObjects)
) {
return false
}
entries += 1
}
return writer.append('}', 1)
} finally {
activeObjects.delete(value)
}
}
function stringifyWithinLimit(checkpoint: TerminalCheckpointFile, maxBytes: number): string | null {
const json = JSON.stringify(checkpoint)
return Buffer.byteLength(json, 'utf8') <= maxBytes ? json : null
const writer = new BoundedJsonWriter(maxBytes)
appendJsonValue(writer, checkpoint, new Set())
return writer.result()
}
async function replaySnapshot(snapshot: TerminalSnapshot): Promise<HeadlessEmulator> {