fix(ai-vault): bound streamed remote JSONL records (#20700)

This commit is contained in:
Neil
2026-09-14 13:30:40 -07:00
committed by GitHub
parent 2186a885dd
commit c3372aeadc
5 changed files with 114 additions and 6 deletions
@@ -4,6 +4,8 @@ import { throwIfAiVaultScanCancelled } from './ai-vault-scan-cancellation'
export type RemoteSessionContent = string | AsyncIterable<string>
const MAX_REMOTE_SESSION_RECORD_BYTES = 10 * 1024 * 1024
const REMOTE_CONTENT_YIELD_LINE_COUNT = 200
const REMOTE_CONTENT_YIELD_CHAR_COUNT = 256 * 1024
@@ -80,7 +82,7 @@ export async function* streamedSessionContentLines(
): AsyncGenerator<string> {
let count = 0
let chars = 0
for await (const record of splitTranscriptStreamLines(bytes)) {
for await (const record of splitTranscriptStreamLines(bytes, MAX_REMOTE_SESSION_RECORD_BYTES)) {
throwIfAiVaultScanCancelled(signal)
const line =
record.line.endsWith('\r') && (record.terminated || signal)
@@ -17,6 +17,35 @@ const jsonl = (rows: unknown[]) => `${rows.map((row) => JSON.stringify(row)).joi
const filler = jsonl([{ type: 'irrelevant_event', payload: 'x'.repeat(1024) }]).repeat(11000)
describe('large remote history through real relay filesystem', () => {
it('reports an oversized record without losing healthy sessions or publishing a partial session', async () => {
const home = await mkdtemp(join(tmpdir(), 'orca-history-record-limit-'))
try {
const directory = join(home, '.codex', 'sessions')
await mkdir(directory, { recursive: true })
const metadata = (id: string) =>
jsonl([{ type: 'session_meta', payload: { id, cwd: '/repo' } }])
const badPath = join(directory, 'bad.jsonl')
await writeFile(badPath, metadata('bad') + 'x'.repeat(11 * 1024 * 1024))
await writeFile(join(directory, 'good.jsonl'), metadata('good'))
const result = await scanRemoteAiVaultSessions({
provider: createRelayAiVaultFilesystemProvider(),
executionHostId: 'ssh:record-limit',
remoteHome: home,
hostPlatform: platform,
unlimited: true
})
expect(result.sessions.map((session) => session.sessionId)).toEqual(['good'])
expect(result.issues).toEqual([
expect.objectContaining({
path: badPath,
message: 'Session transcript record exceeds 10485760 byte limit'
})
])
} finally {
await rm(home, { recursive: true, force: true })
}
})
it('lists a large Codex rollout with middle messages and usage intact', async () => {
const home = await mkdtemp(join(tmpdir(), 'orca-history-17744-'))
try {
@@ -4,6 +4,26 @@ import { readStreamedSessionDocument } from './session-document-stream'
import { limitRemoteScanFilesystemConcurrency } from './remote-session-scan-concurrency'
describe('stream lifetime and retained document work', () => {
it('aborts a newline-free record at the byte ceiling and closes the source', async () => {
let closed = false
let reads = 0
async function* bytes() {
const chunk = Buffer.alloc(1024 * 1024, 'x')
try {
for (; reads < 100;) {
reads++
yield chunk
}
} finally {
closed = true
}
}
const lines = streamedSessionContentLines(bytes())
await expect(lines.next()).rejects.toThrow('record exceeds 10485760 byte limit')
expect(reads).toBe(11)
expect(closed).toBe(true)
})
it('releases the source when a line consumer finishes early', async () => {
let closed = false
async function* bytes() {
@@ -1,6 +1,6 @@
import { Readable } from 'node:stream'
import { describe, expect, it, vi } from 'vitest'
import { decodeTranscriptStream } from './transcript-stream-lines'
import { decodeTranscriptStream, splitTranscriptStreamLines } from './transcript-stream-lines'
const decode = (line: string, id: string) => ({
id,
@@ -171,3 +171,36 @@ describe('decodeTranscriptStream', () => {
expect(result.consumedBytes).toBe(Buffer.byteLength(complete, 'utf8'))
})
})
describe('bounded transcript records', () => {
async function collect(chunks: (Buffer | string)[], limit: number) {
const records: string[] = []
for await (const record of splitTranscriptStreamLines(Readable.from(chunks), limit)) {
records.push(record.line)
}
return records
}
it.each(['', '\n', '\nnext\n'])('rejects an oversized record ending in %j', async (ending) => {
await expect(collect(['1234', `5${ending}`], 4)).rejects.toThrow('record exceeds 4 byte limit')
await expect(collect([`12345${ending}`], 4)).rejects.toThrow('record exceeds 4 byte limit')
})
it('resets the byte budget per record and accepts the exact limit', async () => {
expect(await collect(['1234\n123', '4\n1234'], 4)).toEqual(['1234', '1234', '1234'])
})
it('counts UTF-8 bytes across split codepoints', async () => {
const bytes = Buffer.from('😀é')
const chunks = [bytes.subarray(0, 2), bytes.subarray(2, 5), bytes.subarray(5)]
expect((await collect(chunks, 6))[0]).toBe('😀é')
await expect(collect(chunks, 5)).rejects.toThrow('record exceeds 5 byte limit')
expect((await collect(['\ud83d', '\ude00'], 4))[0]).toBe('😀')
})
it('checks the decoder tail before emitting it', async () => {
await expect(collect([Buffer.from([0x61, 0xf0, 0x9f])], 3)).rejects.toThrow(
'record exceeds 3 byte limit'
)
})
})
@@ -42,12 +42,13 @@ export async function decodeTranscriptStream(
type TranscriptLine = { line: string; byteLength: number; terminated: boolean }
export async function* splitTranscriptStreamLines(
stream: AsyncIterable<Buffer | string>
stream: AsyncIterable<Buffer | string>,
maxRecordBytes = Infinity
): AsyncGenerator<TranscriptLine> {
let records: TranscriptLine[] = []
const framer = createTranscriptLineFramer((line, byteLength, terminated) => {
records.push({ line, byteLength, terminated })
})
}, maxRecordBytes)
for await (const chunk of stream) {
framer.write(chunk)
for (const record of records) {
@@ -63,10 +64,12 @@ export async function* splitTranscriptStreamLines(
/** Frame chunks synchronously so native decoding avoids a promise per record. */
function createTranscriptLineFramer(
emit: (line: string, byteLength: number, terminated: boolean) => void
emit: (line: string, byteLength: number, terminated: boolean) => void,
maxRecordBytes = Infinity
): { write(chunk: Buffer | string): void; end(): void } {
const decoder = new StringDecoder('utf8')
let pending: string[] = []
let pendingBytes = 0
return { write, end }
function write(chunk: Buffer | string): void {
@@ -75,23 +78,44 @@ function createTranscriptLineFramer(
let newlineIndex = text.indexOf('\n')
while (newlineIndex !== -1) {
let segment = text.slice(lineStart, newlineIndex + 1)
checkRecordBytes(segment.slice(0, -1))
if (pending.length > 0) {
pending.push(segment)
segment = pending.join('')
pending = []
}
pendingBytes = 0
emit(segment.slice(0, -1), Buffer.byteLength(segment, 'utf8'), true)
lineStart = newlineIndex + 1
newlineIndex = text.indexOf('\n', lineStart)
}
if (lineStart < text.length) {
pending.push(text.slice(lineStart))
const segment = text.slice(lineStart)
checkRecordBytes(segment)
pending.push(segment)
}
}
function checkRecordBytes(segment: string): void {
if (maxRecordBytes === Infinity) {
return
}
pendingBytes += Buffer.byteLength(segment, 'utf8')
const previous = pending.at(-1)
// Separately encoded surrogate halves become one four-byte codepoint when joined.
if (previous && /[\uD800-\uDBFF]$/.test(previous) && /^[\uDC00-\uDFFF]/.test(segment)) {
pendingBytes -= 2
}
if (pendingBytes > maxRecordBytes) {
pending = []
throw new Error(`Session transcript record exceeds ${maxRecordBytes} byte limit`)
}
}
function end(): void {
const tail = decoder.end()
if (tail) {
checkRecordBytes(tail)
pending.push(tail)
}
const line = pending.join('')