mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-21 02:07:12 +00:00
Adds readSessionFile / readSessionFileSync / readSessionLines with a 128 MB hard cap and 8 MB streaming threshold. Verbose mode (CODEBURN_VERBOSE=1) logs skipped and failed reads to stderr. Prepares the MEDIUM-1 migration of all provider read paths.
93 lines
3 KiB
TypeScript
93 lines
3 KiB
TypeScript
import { readFile, stat } from 'fs/promises'
|
|
import { readFileSync, statSync, createReadStream } from 'fs'
|
|
import { createInterface } from 'readline'
|
|
|
|
// Hard cap well below V8's 512 MB string limit even with split('\n') doubling.
|
|
// Stream threshold chosen as empirical breakeven between readFile+split peak
|
|
// memory and createReadStream+readline overhead for typical session files.
|
|
export const MAX_SESSION_FILE_BYTES = 128 * 1024 * 1024
|
|
export const STREAM_THRESHOLD_BYTES = 8 * 1024 * 1024
|
|
|
|
function verbose(): boolean {
|
|
return process.env.CODEBURN_VERBOSE === '1'
|
|
}
|
|
|
|
function warn(msg: string): void {
|
|
if (verbose()) process.stderr.write(`codeburn: ${msg}\n`)
|
|
}
|
|
|
|
async function readViaStream(filePath: string): Promise<string> {
|
|
const chunks: string[] = []
|
|
const stream = createReadStream(filePath, { encoding: 'utf-8' })
|
|
const rl = createInterface({ input: stream, crlfDelay: Infinity })
|
|
for await (const line of rl) chunks.push(line)
|
|
return chunks.join('\n')
|
|
}
|
|
|
|
export async function readSessionFile(filePath: string): Promise<string | null> {
|
|
let size: number
|
|
try {
|
|
size = (await stat(filePath)).size
|
|
} catch (err) {
|
|
warn(`stat failed for ${filePath}: ${(err as NodeJS.ErrnoException).code ?? 'unknown'}`)
|
|
return null
|
|
}
|
|
|
|
if (size > MAX_SESSION_FILE_BYTES) {
|
|
warn(`skipped oversize file ${filePath} (${size} bytes > cap ${MAX_SESSION_FILE_BYTES})`)
|
|
return null
|
|
}
|
|
|
|
try {
|
|
if (size >= STREAM_THRESHOLD_BYTES) return await readViaStream(filePath)
|
|
return await readFile(filePath, 'utf-8')
|
|
} catch (err) {
|
|
warn(`read failed for ${filePath}: ${(err as NodeJS.ErrnoException).code ?? 'unknown'}`)
|
|
return null
|
|
}
|
|
}
|
|
|
|
export function readSessionFileSync(filePath: string): string | null {
|
|
let size: number
|
|
try {
|
|
size = statSync(filePath).size
|
|
} catch (err) {
|
|
warn(`stat failed for ${filePath}: ${(err as NodeJS.ErrnoException).code ?? 'unknown'}`)
|
|
return null
|
|
}
|
|
|
|
if (size > MAX_SESSION_FILE_BYTES) {
|
|
warn(`skipped oversize file ${filePath} (${size} bytes > cap ${MAX_SESSION_FILE_BYTES})`)
|
|
return null
|
|
}
|
|
|
|
try {
|
|
return readFileSync(filePath, 'utf-8')
|
|
} catch (err) {
|
|
warn(`read failed for ${filePath}: ${(err as NodeJS.ErrnoException).code ?? 'unknown'}`)
|
|
return null
|
|
}
|
|
}
|
|
|
|
export async function* readSessionLines(filePath: string): AsyncGenerator<string> {
|
|
let size: number
|
|
try {
|
|
size = (await stat(filePath)).size
|
|
} catch (err) {
|
|
warn(`stat failed for ${filePath}: ${(err as NodeJS.ErrnoException).code ?? 'unknown'}`)
|
|
return
|
|
}
|
|
|
|
if (size > MAX_SESSION_FILE_BYTES) {
|
|
warn(`skipped oversize file ${filePath} (${size} bytes > cap ${MAX_SESSION_FILE_BYTES})`)
|
|
return
|
|
}
|
|
|
|
const stream = createReadStream(filePath, { encoding: 'utf-8' })
|
|
const rl = createInterface({ input: stream, crlfDelay: Infinity })
|
|
try {
|
|
for await (const line of rl) yield line
|
|
} catch (err) {
|
|
warn(`stream read failed for ${filePath}: ${(err as NodeJS.ErrnoException).code ?? 'unknown'}`)
|
|
}
|
|
}
|