Fix V8 OOM crash on 30-day period with Buffer-based line reader and large-line parser

Three-layer fix for V8 heap exhaustion when parsing heavy session data:

1. Buffer-based readSessionLines (fs-utils.ts): Replace readline with raw
   Buffer streaming using Buffer.indexOf(0x0a). Eliminates ConsString trees
   that caused OOM when regex-flattening 100MB+ lines. Two-state machine
   (ACCUMULATING/SCANNING) skips old lines at ~2KB cost instead of 200MB.

2. Large-line streaming parser (parser.ts): Hand-written JSON scanner for
   lines >32KB extracts only cost/token/tool fields without JSON.parse,
   avoiding full object graph allocation. Dual string/Buffer paths.

3. Dashboard memory management (dashboard.tsx): Disable auto-refresh for
   heavy periods (30d/month/all), clear old dataset before reload via
   nextTick to allow GC, prevent overlapping reloads with mutex, lazy
   optimize scanning on keypress instead of useEffect.

Also fixes three race conditions in dashboard reload deduplication:
- Early return after nextTick bypassing finally block (permanent mutex lock)
- A->B->A period switching dropping final reload (stale pending)
- Stale pendingReloadRef not cleared when in-flight matches request
This commit is contained in:
iamtoruk 2026-05-15 23:15:26 -07:00
parent 36e94169fb
commit 2fb078bdfb
14 changed files with 1420 additions and 80 deletions

View file

@ -9,13 +9,12 @@ import { parseAllSessions, filterProjectsByName } from './parser.js'
import { loadPricing } from './models.js'
import { getAllProviders } from './providers/index.js'
import { scanAndDetect, type WasteFinding, type WasteAction, type OptimizeResult } from './optimize.js'
import { estimateContextBudget, discoverProjectCwd, type ContextBudget } from './context-budget.js'
import { estimateContextBudget, type ContextBudget } from './context-budget.js'
import { dateKey } from './day-aggregator.js'
import { CompareView } from './compare.js'
import { getPlanUsageOrNull, type PlanUsage } from './plan-usage.js'
import { planDisplayName } from './plans.js'
import { getDateRange, PERIODS, PERIOD_LABELS, type Period, formatDateRangeLabel } from './cli-date.js'
import { join } from 'path'
import { patchStdoutForWindows } from './ink-win.js'
type View = 'dashboard' | 'optimize' | 'compare'
@ -25,6 +24,7 @@ const ORANGE = '#FF8C42'
const DIM = '#555555'
const GOLD = '#FFD700'
const PLAN_BAR_WIDTH = 10
const HEAVY_PERIODS = new Set<Period>(['30days', 'month', 'all'])
const LANG_DISPLAY_NAMES: Record<string, string> = {
javascript: 'JavaScript', typescript: 'TypeScript', python: 'Python',
@ -101,6 +101,14 @@ function getPeriodRange(period: Period): { start: Date; end: Date } {
return getDateRange(period).range
}
function isHeavyPeriod(period: Period): boolean {
return HEAVY_PERIODS.has(period)
}
function nextTick(): Promise<void> {
return new Promise(resolve => setImmediate(resolve))
}
type Layout = { dashWidth: number; wide: boolean; halfWidth: number; barWidth: number }
function getLayout(columns?: number): Layout {
@ -659,8 +667,8 @@ function StatusBar({ width, showProvider, view, findingCount, optimizeAvailable,
<Text color={ORANGE} bold>5</Text><Text dimColor> 6 months</Text>
</>
)}
{!isOptimize && optimizeAvailable && findingCount != null && findingCount > 0 && (
<><Text dimColor> </Text><Text color={ORANGE} bold>o</Text><Text dimColor> optimize</Text><Text color="#F55B5B"> ({findingCount})</Text></>
{!isOptimize && optimizeAvailable && (
<><Text dimColor> </Text><Text color={ORANGE} bold>o</Text><Text dimColor> optimize</Text>{findingCount != null && findingCount > 0 ? <Text color="#F55B5B"> ({findingCount})</Text> : null}</>
)}
{!isOptimize && compareAvailable && (
<><Text dimColor> </Text><Text color={ORANGE} bold>c</Text><Text dimColor> compare</Text></>
@ -716,6 +724,7 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
const [detectedProviders, setDetectedProviders] = useState<string[]>([])
const [view, setView] = useState<View>('dashboard')
const [optimizeResult, setOptimizeResult] = useState<OptimizeResult | null>(null)
const [optimizeLoading, setOptimizeLoading] = useState(false)
const [projectBudgets, setProjectBudgets] = useState<Map<string, ContextBudget>>(new Map())
const [planUsage, setPlanUsage] = useState<PlanUsage | undefined>(initialPlanUsage)
// Cursor for the OptimizeView's findings window. Reset whenever the user
@ -726,13 +735,16 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
const { columns } = useWindowSize()
const { dashWidth } = getLayout(columns)
const multipleProviders = detectedProviders.length > 1
const optimizeAvailable = activeProvider === 'all' || activeProvider === 'claude'
const optimizeAvailable = !isCustomRange && (activeProvider === 'all' || activeProvider === 'claude')
const modelCount = new Set(
projects.flatMap(p => p.sessions.flatMap(s => Object.keys(s.modelBreakdown)))
).size
const compareAvailable = modelCount >= 2
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const reloadGenerationRef = useRef(0)
const reloadInFlightRef = useRef(false)
const currentReloadRef = useRef<{ period: Period; provider: string } | null>(null)
const pendingReloadRef = useRef<{ period: Period; provider: string } | null>(null)
const findingCount = optimizeResult?.findings.length ?? 0
useEffect(() => {
@ -749,13 +761,11 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
useEffect(() => {
let cancelled = false
async function loadBudgets() {
const claudeDir = join(homedir(), '.claude', 'projects')
const budgets = new Map<string, ContextBudget>()
for (const project of projects.slice(0, 8)) {
if (cancelled) return
const cwd = await discoverProjectCwd(join(claudeDir, project.project))
if (!cwd) continue
budgets.set(project.project, await estimateContextBudget(cwd))
if (!project.projectPath.startsWith('/')) continue
budgets.set(project.project, await estimateContextBudget(project.projectPath))
}
if (!cancelled) setProjectBudgets(budgets)
}
@ -763,23 +773,30 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
return () => { cancelled = true }
}, [projects])
useEffect(() => {
if (!optimizeAvailable) { setOptimizeResult(null); return }
let cancelled = false
async function scan() {
if (projects.length === 0) { setOptimizeResult(null); return }
const result = await scanAndDetect(projects, getPeriodRange(period))
if (!cancelled) setOptimizeResult(result)
}
scan()
return () => { cancelled = true }
}, [projects, period, optimizeAvailable])
const reloadData = useCallback(async (p: Period, prov: string) => {
if (reloadInFlightRef.current) {
const current = currentReloadRef.current
if (current?.period === p && current.provider === prov) {
pendingReloadRef.current = null
return
}
reloadGenerationRef.current++
pendingReloadRef.current = { period: p, provider: prov }
return
}
reloadInFlightRef.current = true
currentReloadRef.current = { period: p, provider: prov }
const generation = ++reloadGenerationRef.current
setLoading(true)
setOptimizeLoading(false)
setOptimizeResult(null)
try {
if (isHeavyPeriod(p)) {
setProjects([])
setProjectBudgets(new Map())
await nextTick()
if (reloadGenerationRef.current !== generation) return
}
const range = getPeriodRange(p)
const data = await parseAllSessions(range, prov)
if (reloadGenerationRef.current !== generation) return
@ -797,11 +814,37 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
if (reloadGenerationRef.current === generation) {
setLoading(false)
}
reloadInFlightRef.current = false
currentReloadRef.current = null
const pending = pendingReloadRef.current
pendingReloadRef.current = null
if (pending) {
void reloadData(pending.period, pending.provider)
}
}
}, [projectFilter, excludeFilter])
const loadOptimizeResult = useCallback(async () => {
if (!optimizeAvailable || projects.length === 0 || optimizeLoading) return
setView('optimize')
setFindingsCursor(0)
if (optimizeResult) return
const generation = reloadGenerationRef.current
setOptimizeLoading(true)
try {
const result = await scanAndDetect(projects, getPeriodRange(period))
if (reloadGenerationRef.current === generation) setOptimizeResult(result)
} catch (error) {
console.error(error)
} finally {
if (reloadGenerationRef.current === generation) setOptimizeLoading(false)
}
}, [optimizeAvailable, projects, period, optimizeLoading, optimizeResult])
useEffect(() => {
if (!refreshSeconds || refreshSeconds <= 0) return
if (isHeavyPeriod(period)) return
const id = setInterval(() => { reloadData(period, activeProvider) }, refreshSeconds * 1000)
return () => clearInterval(id)
}, [refreshSeconds, period, activeProvider, reloadData])
@ -831,7 +874,7 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
useInput((input, key) => {
if (input === 'q') { exit(); return }
if (input === 'o' && findingCount > 0 && view === 'dashboard' && optimizeAvailable) { setView('optimize'); return }
if (input === 'o' && view === 'dashboard' && optimizeAvailable) { void loadOptimizeResult(); return }
if ((input === 'b' || key.escape) && view === 'optimize') { setView('dashboard'); setFindingsCursor(0); return }
if (view === 'optimize') {
const total = optimizeResult?.findings.length ?? 0
@ -869,7 +912,7 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
const headerLabel = customRangeLabel ?? PERIOD_LABELS[period]
if (loading) {
if (loading || optimizeLoading) {
return (
<Box flexDirection="column" width={dashWidth}>
{!isCustomRange && <PeriodTabs active={period} providerName={activeProvider} showProvider={view !== 'compare' && multipleProviders} />}
@ -882,7 +925,9 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
<Text dimColor>Loading {headerLabel} model data...</Text>
</Box>
</Box>
: <Panel title="CodeBurn" color={ORANGE} width={dashWidth}><Text dimColor>Loading {headerLabel}...</Text></Panel>}
: view === 'optimize'
? <Panel title="CodeBurn Optimize" color={ORANGE} width={dashWidth}><Text dimColor>Scanning {headerLabel}...</Text></Panel>
: <Panel title="CodeBurn" color={ORANGE} width={dashWidth}><Text dimColor>Loading {headerLabel}...</Text></Panel>}
{view !== 'compare' && <StatusBar width={dashWidth} showProvider={multipleProviders} view={view} findingCount={0} optimizeAvailable={false} compareAvailable={false} customRange={isCustomRange} />}
</Box>
)

File diff suppressed because one or more lines are too long

View file

@ -1,12 +1,11 @@
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.
// Hard cap well below V8's 512 MB string limit. Callers that need line-by-line
// processing should use readSessionLines(), which avoids materializing the
// whole file and can return large lines as Buffers.
export const MAX_SESSION_FILE_BYTES = 128 * 1024 * 1024
export const STREAM_THRESHOLD_BYTES = 8 * 1024 * 1024
export const LARGE_STREAM_LINE_BYTES = 32 * 1024
// Line-by-line streaming has bounded memory (one line at a time) and is not
// constrained by V8's string limit, so it can safely handle multi-GB session
@ -23,14 +22,6 @@ 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 {
@ -46,7 +37,6 @@ export async function readSessionFile(filePath: string): Promise<string | 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'}`)
@ -76,7 +66,27 @@ export function readSessionFileSync(filePath: string): string | null {
}
}
export async function* readSessionLines(filePath: string): AsyncGenerator<string> {
export type SessionLine = string | Buffer
type ReadSessionLinesOptions = {
largeLineAsBuffer?: boolean
largeLineThresholdBytes?: number
}
export function readSessionLines(
filePath: string,
shouldSkipHead?: (head: string) => boolean,
): AsyncGenerator<string>
export function readSessionLines(
filePath: string,
shouldSkipHead?: (head: string) => boolean,
options?: ReadSessionLinesOptions & { largeLineAsBuffer: true },
): AsyncGenerator<SessionLine>
export async function* readSessionLines(
filePath: string,
shouldSkipHead?: (head: string) => boolean,
options: ReadSessionLinesOptions = {},
): AsyncGenerator<SessionLine> {
let size: number
try {
size = (await stat(filePath)).size
@ -92,10 +102,103 @@ export async function* readSessionLines(filePath: string): AsyncGenerator<string
return
}
const stream = createReadStream(filePath, { encoding: 'utf-8' })
const rl = createInterface({ input: stream, crlfDelay: Infinity })
// Raw Buffers — no encoding. This avoids readline's ConsString trees
// which OOM on V8 when regex-flattening 100 MB+ lines.
const stream = createReadStream(filePath)
const SKIP_HEAD = 2048
const largeLineThreshold = options.largeLineThresholdBytes ?? LARGE_STREAM_LINE_BYTES
const formatLine = (buf: Buffer, lineLen: number, head?: string): SessionLine => {
if (options.largeLineAsBuffer && lineLen > largeLineThreshold) return buf
return head !== undefined && lineLen <= SKIP_HEAD ? head : buf.toString('utf-8')
}
let parts: Buffer[] = []
let len = 0
let skipping = false
let headChecked = false
try {
for await (const line of rl) yield line
for await (const raw of stream) {
const chunk = raw as Buffer
let pos = 0
while (pos < chunk.length) {
const nl = chunk.indexOf(0x0a, pos)
if (skipping) {
if (nl === -1) {
pos = chunk.length
} else {
skipping = false
pos = nl + 1
}
continue
}
if (nl !== -1) {
if (pos < nl) {
parts.push(chunk.subarray(pos, nl))
len += nl - pos
}
pos = nl + 1
if (len === 0) {
parts = []
headChecked = false
continue
}
const buf = parts.length === 1 ? parts[0]! : Buffer.concat(parts, len)
const lineLen = len
parts = []
len = 0
headChecked = false
if (shouldSkipHead) {
const head = lineLen > SKIP_HEAD
? buf.subarray(0, SKIP_HEAD).toString('utf-8')
: buf.toString('utf-8')
if (shouldSkipHead(head)) continue
yield formatLine(buf, lineLen, head)
} else {
yield formatLine(buf, lineLen)
}
} else {
const slice = chunk.subarray(pos)
parts.push(slice)
len += slice.length
pos = chunk.length
// Mid-line skip: once we have enough bytes to check the head,
// enter scanning mode — just look for \n without accumulating.
if (shouldSkipHead && !headChecked && len >= SKIP_HEAD) {
headChecked = true
const headBuf = parts.length === 1
? parts[0]!.subarray(0, SKIP_HEAD)
: Buffer.concat(parts, len).subarray(0, SKIP_HEAD)
if (shouldSkipHead(headBuf.toString('utf-8'))) {
skipping = true
parts = []
len = 0
}
}
}
}
}
if (!skipping && len > 0) {
const buf = parts.length === 1 ? parts[0]! : Buffer.concat(parts, len)
const lineLen = len
if (shouldSkipHead) {
const head = lineLen > SKIP_HEAD
? buf.subarray(0, SKIP_HEAD).toString('utf-8')
: buf.toString('utf-8')
if (!shouldSkipHead(head)) {
yield formatLine(buf, lineLen, head)
}
} else {
yield formatLine(buf, lineLen)
}
}
} catch (err) {
warn(`stream read failed for ${filePath}: ${(err as NodeJS.ErrnoException).code ?? 'unknown'}`)
} finally {

View file

@ -2,7 +2,7 @@ import { Command } from 'commander'
import { installMenubarApp } from './menubar-installer.js'
import { exportCsv, exportJson, type PeriodExport } from './export.js'
import { loadPricing, setModelAliases } from './models.js'
import { parseAllSessions, filterProjectsByName, clearSessionCache } from './parser.js'
import { parseAllSessions, filterProjectsByName, filterProjectsByDateRange, clearSessionCache } from './parser.js'
import { convertCost } from './currency.js'
import { renderStatusBar } from './format.js'
import { type PeriodData, type ProviderCost } from './menubar-json.js'
@ -615,13 +615,19 @@ program
process.exit(1)
}
const periods: PeriodExport[] = customRange
? [{ label: formatDateRangeLabel(opts.from, opts.to), projects: fp(await parseAllSessions(customRange, pf)) }]
: [
{ label: 'Today', projects: fp(await parseAllSessions(getDateRange('today').range, pf)) },
{ label: '7 Days', projects: fp(await parseAllSessions(getDateRange('week').range, pf)) },
{ label: '30 Days', projects: fp(await parseAllSessions(getDateRange('30days').range, pf)) },
]
let periods: PeriodExport[]
if (customRange) {
periods = [{ label: formatDateRangeLabel(opts.from, opts.to), projects: fp(await parseAllSessions(customRange, pf)) }]
clearSessionCache()
} else {
const thirtyDayProjects = fp(await parseAllSessions(getDateRange('30days').range, pf))
clearSessionCache()
periods = [
{ label: 'Today', projects: filterProjectsByDateRange(thirtyDayProjects, getDateRange('today').range) },
{ label: '7 Days', projects: filterProjectsByDateRange(thirtyDayProjects, getDateRange('week').range) },
{ label: '30 Days', projects: thirtyDayProjects },
]
}
if (periods.every(p => p.projects.length === 0)) {
console.log('\n No usage data found.\n')

View file

@ -43,7 +43,7 @@ type Bucket = {
}
type ModelKey = string
type CategoryKey = string
type CategoryKey = TaskCategory
function bucketKey(provider: string, model: string, category: TaskCategory | null): string {
return `${provider} ${model} ${category ?? ''}`

View file

@ -6,6 +6,7 @@ import { homedir } from 'os'
import { readSessionLines, readSessionFileSync } from './fs-utils.js'
import { discoverAllSessions } from './providers/index.js'
import { parseJsonlLine, shouldSkipLine } from './parser.js'
import type { DateRange, ProjectSummary } from './types.js'
import { formatCost } from './currency.js'
import { formatTokens } from './format.js'
@ -141,6 +142,8 @@ const SHELL_PROFILES = ['.zshrc', '.bashrc', '.bash_profile', '.profile']
const TOP_ITEMS_PREVIEW = 3
const GHOST_NAMES_PREVIEW = 5
const GHOST_CLEANUP_COMMANDS_LIMIT = 10
const OPTIMIZE_TEXT_CAP = 2000
const OPTIMIZE_FIELD_CAP = 500
// ============================================================================
// Types
@ -209,7 +212,33 @@ type ScanData = {
// JSONL scanner
// ============================================================================
const FILE_READ_CONCURRENCY = 16
function cappedString(value: unknown, cap = OPTIMIZE_FIELD_CAP): string | undefined {
return typeof value === 'string' ? value.slice(0, cap) : undefined
}
function compactOptimizeInput(name: string, input: unknown): Record<string, unknown> {
if (!input || typeof input !== 'object') return {}
const raw = input as Record<string, unknown>
if (isReadTool(name)) {
const filePath = cappedString(raw['file_path'], OPTIMIZE_TEXT_CAP)
return filePath ? { file_path: filePath } : {}
}
if (name === 'Agent' || name === 'Task') {
const subagentType = cappedString(raw['subagent_type'])
return subagentType ? { subagent_type: subagentType } : {}
}
if (name === 'Skill') {
const skill = cappedString(raw['skill'])
const skillName = cappedString(raw['name'])
return {
...(skill ? { skill } : {}),
...(skillName ? { name: skillName } : {}),
}
}
return {}
}
const FILE_READ_CONCURRENCY = 4
const RESULT_CACHE_TTL_MS = 60_000
const RECENT_WINDOW_HOURS = 48
const RECENT_WINDOW_MS = RECENT_WINDOW_HOURS * 60 * 60 * 1000
@ -286,10 +315,19 @@ export async function scanJsonlFile(
const sessionId = basename(filePath, '.jsonl')
let lastVersion = ''
for await (const line of readSessionLines(filePath)) {
if (!line.trim()) continue
let entry: Record<string, unknown>
try { entry = JSON.parse(line) } catch { continue }
const skipThreshold = dateRange
? new Date(dateRange.start.getTime() - 86_400_000).toISOString()
: null
const skipFn = dateRange
? (head: string) => shouldSkipLine(head, skipThreshold!)
: undefined
const lines = readSessionLines(filePath, skipFn, { largeLineAsBuffer: true })
for await (const line of lines) {
if (typeof line === 'string' && !line.trim()) continue
if (Buffer.isBuffer(line) && line.length === 0) continue
const parsed = parseJsonlLine(line)
if (!parsed) continue
const entry = parsed as Record<string, unknown>
if (entry.version && typeof entry.version === 'string') lastVersion = entry.version
@ -304,11 +342,15 @@ export async function scanJsonlFile(
const msg = entry.message as Record<string, unknown> | undefined
const msgContent = msg?.content
if (typeof msgContent === 'string') {
userMessages.push(msgContent)
userMessages.push(msgContent.slice(0, OPTIMIZE_TEXT_CAP))
} else if (Array.isArray(msgContent)) {
let remaining = OPTIMIZE_TEXT_CAP
for (const block of msgContent) {
if (remaining <= 0) break
if (block && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string') {
userMessages.push(block.text)
const text = block.text.slice(0, remaining)
userMessages.push(text)
remaining -= text.length
}
}
}
@ -330,9 +372,10 @@ export async function scanJsonlFile(
for (const block of blocks) {
if (block.type !== 'tool_use') continue
const name = typeof block.name === 'string' ? block.name : ''
calls.push({
name: block.name as string,
input: (block.input as Record<string, unknown>) ?? {},
name,
input: compactOptimizeInput(name, block.input),
sessionId,
project,
recent,

View file

@ -32,7 +32,18 @@ function normalizeProjectPathKey(projectPath: string): string {
return (normalized.replace(/\/+$/, '') || normalized).toLowerCase()
}
function parseJsonlLine(line: string): JournalEntry | null {
const LARGE_JSONL_LINE_BYTES = 32 * 1024
export function parseJsonlLine(line: string | Buffer): JournalEntry | null {
if (Buffer.isBuffer(line)) {
if (line.length > LARGE_JSONL_LINE_BYTES) return parseLargeJsonlBuffer(line)
try {
return JSON.parse(line.toString('utf-8')) as JournalEntry
} catch {
return null
}
}
if (line.length > LARGE_JSONL_LINE_BYTES) return parseLargeJsonlLine(line)
try {
return JSON.parse(line) as JournalEntry
} catch {
@ -40,6 +51,721 @@ function parseJsonlLine(line: string): JournalEntry | null {
}
}
const RAW_HEAD_BYTES = 2048
type JsonValueBounds = {
start: number
end: number
kind: 'string' | 'object' | 'array' | 'scalar'
}
function findJsonStringEnd(source: string, start: number, limit = source.length): number {
for (let i = start + 1; i < limit; i++) {
const ch = source.charCodeAt(i)
if (ch === 0x5c) {
i++
continue
}
if (ch === 0x22) return i
}
return -1
}
function findJsonContainerEnd(source: string, start: number, open: number, close: number, limit = source.length): number {
let depth = 0
let inString = false
for (let i = start; i < limit; i++) {
const ch = source.charCodeAt(i)
if (inString) {
if (ch === 0x5c) {
i++
} else if (ch === 0x22) {
inString = false
}
continue
}
if (ch === 0x22) {
inString = true
} else if (ch === open) {
depth++
} else if (ch === close) {
depth--
if (depth === 0) return i
}
}
return -1
}
function findJsonValueBounds(source: string, start: number, limit = source.length): JsonValueBounds | null {
let i = start
while (i < limit && /\s/.test(source[i]!)) i++
if (i >= limit) return null
const ch = source.charCodeAt(i)
if (ch === 0x22) {
const end = findJsonStringEnd(source, i, limit)
return end === -1 ? null : { start: i, end: end + 1, kind: 'string' }
}
if (ch === 0x7b) {
const end = findJsonContainerEnd(source, i, 0x7b, 0x7d, limit)
return end === -1 ? null : { start: i, end: end + 1, kind: 'object' }
}
if (ch === 0x5b) {
const end = findJsonContainerEnd(source, i, 0x5b, 0x5d, limit)
return end === -1 ? null : { start: i, end: end + 1, kind: 'array' }
}
let end = i
while (end < limit) {
const c = source.charCodeAt(end)
if (c === 0x2c || c === 0x7d || c === 0x5d || /\s/.test(source[end]!)) break
end++
}
return { start: i, end, kind: 'scalar' }
}
function findObjectFieldValue(source: string, objectStart: number, objectEnd: number, field: string): JsonValueBounds | null {
if (source.charCodeAt(objectStart) !== 0x7b) return null
let i = objectStart + 1
while (i < objectEnd - 1) {
while (i < objectEnd && /\s/.test(source[i]!)) i++
if (source.charCodeAt(i) === 0x2c) {
i++
continue
}
if (source.charCodeAt(i) !== 0x22) {
i++
continue
}
const keyEnd = findJsonStringEnd(source, i, objectEnd)
if (keyEnd === -1) return null
const key = source.slice(i + 1, keyEnd)
i = keyEnd + 1
while (i < objectEnd && /\s/.test(source[i]!)) i++
if (source.charCodeAt(i) !== 0x3a) continue
const value = findJsonValueBounds(source, i + 1, objectEnd)
if (!value) return null
if (key === field) return value
i = value.end
}
return null
}
function readJsonString(source: string, bounds: JsonValueBounds | null, cap = Number.POSITIVE_INFINITY): string | undefined {
if (!bounds || bounds.kind !== 'string') return undefined
let out = ''
for (let i = bounds.start + 1; i < bounds.end - 1 && out.length < cap; i++) {
const ch = source[i]!
if (ch !== '\\') {
out += ch
continue
}
const next = source[++i]
if (!next) break
if (next === 'n') out += '\n'
else if (next === 'r') out += '\r'
else if (next === 't') out += '\t'
else if (next === 'b') out += '\b'
else if (next === 'f') out += '\f'
else if (next === 'u' && i + 4 < bounds.end) {
const hex = source.slice(i + 1, i + 5)
const code = Number.parseInt(hex, 16)
if (Number.isFinite(code)) out += String.fromCharCode(code)
i += 4
} else {
out += next
}
}
return out
}
function readJsonNumberField(source: string, objectBounds: JsonValueBounds | null, field: string): number | undefined {
if (!objectBounds || objectBounds.kind !== 'object') return undefined
const bounds = findObjectFieldValue(source, objectBounds.start, objectBounds.end, field)
if (!bounds) return undefined
const value = Number(source.slice(bounds.start, bounds.end))
return Number.isFinite(value) ? value : undefined
}
function parseLargeUsage(source: string, usageBounds: JsonValueBounds | null) {
const usage: AssistantMessageContent['usage'] = {
input_tokens: readJsonNumberField(source, usageBounds, 'input_tokens') ?? 0,
output_tokens: readJsonNumberField(source, usageBounds, 'output_tokens') ?? 0,
cache_creation_input_tokens: readJsonNumberField(source, usageBounds, 'cache_creation_input_tokens'),
cache_read_input_tokens: readJsonNumberField(source, usageBounds, 'cache_read_input_tokens'),
}
if (usageBounds?.kind === 'object') {
const cacheCreation = findObjectFieldValue(source, usageBounds.start, usageBounds.end, 'cache_creation')
const ephemeral5m = readJsonNumberField(source, cacheCreation, 'ephemeral_5m_input_tokens')
const ephemeral1h = readJsonNumberField(source, cacheCreation, 'ephemeral_1h_input_tokens')
if (ephemeral5m !== undefined || ephemeral1h !== undefined) {
;(usage as AssistantMessageContent['usage']).cache_creation = {
...(ephemeral5m !== undefined ? { ephemeral_5m_input_tokens: ephemeral5m } : {}),
...(ephemeral1h !== undefined ? { ephemeral_1h_input_tokens: ephemeral1h } : {}),
}
}
const serverToolUse = findObjectFieldValue(source, usageBounds.start, usageBounds.end, 'server_tool_use')
const webSearch = readJsonNumberField(source, serverToolUse, 'web_search_requests')
const webFetch = readJsonNumberField(source, serverToolUse, 'web_fetch_requests')
if (webSearch !== undefined || webFetch !== undefined) {
;(usage as AssistantMessageContent['usage']).server_tool_use = {
...(webSearch !== undefined ? { web_search_requests: webSearch } : {}),
...(webFetch !== undefined ? { web_fetch_requests: webFetch } : {}),
}
}
const speed = readJsonString(source, findObjectFieldValue(source, usageBounds.start, usageBounds.end, 'speed'))
if (speed === 'standard' || speed === 'fast') usage.speed = speed
}
return usage
}
function extractLargeToolBlocks(source: string, contentBounds: JsonValueBounds | null): ToolUseBlock[] {
if (!contentBounds || contentBounds.kind !== 'array') return []
const tools: ToolUseBlock[] = []
let i = contentBounds.start + 1
while (i < contentBounds.end - 1 && tools.length < MAX_TOOL_BLOCKS) {
while (i < contentBounds.end && /\s/.test(source[i]!)) i++
if (source.charCodeAt(i) === 0x2c) {
i++
continue
}
if (source.charCodeAt(i) !== 0x7b) {
i++
continue
}
const objectEnd = findJsonContainerEnd(source, i, 0x7b, 0x7d, contentBounds.end)
if (objectEnd === -1) break
const objectBounds = { start: i, end: objectEnd + 1, kind: 'object' as const }
const blockType = readJsonString(source, findObjectFieldValue(source, objectBounds.start, objectBounds.end, 'type'))
if (blockType === 'tool_use') {
const name = readJsonString(source, findObjectFieldValue(source, objectBounds.start, objectBounds.end, 'name')) ?? ''
const id = readJsonString(source, findObjectFieldValue(source, objectBounds.start, objectBounds.end, 'id')) ?? ''
const inputBounds = findObjectFieldValue(source, objectBounds.start, objectBounds.end, 'input')
const input: Record<string, unknown> = {}
if (inputBounds?.kind === 'object') {
if (name === 'Skill') {
const skill = readJsonString(source, findObjectFieldValue(source, inputBounds.start, inputBounds.end, 'skill'), 200)
const skillName = readJsonString(source, findObjectFieldValue(source, inputBounds.start, inputBounds.end, 'name'), 200)
if (skill !== undefined) input['skill'] = skill
if (skillName !== undefined) input['name'] = skillName
} else if (name === 'Read' || name === 'FileReadTool') {
const filePath = readJsonString(source, findObjectFieldValue(source, inputBounds.start, inputBounds.end, 'file_path'), BASH_COMMAND_CAP)
if (filePath !== undefined) input['file_path'] = filePath
} else if (name === 'Agent' || name === 'Task') {
const subagentType = readJsonString(source, findObjectFieldValue(source, inputBounds.start, inputBounds.end, 'subagent_type'), 200)
if (subagentType !== undefined) input['subagent_type'] = subagentType
} else if (BASH_TOOLS.has(name)) {
const command = readJsonString(source, findObjectFieldValue(source, inputBounds.start, inputBounds.end, 'command'), BASH_COMMAND_CAP)
if (command !== undefined) input['command'] = command
}
}
tools.push({ type: 'tool_use', id, name, input })
}
i = objectEnd + 1
}
return tools
}
function extractLargeUserText(source: string, contentBounds: JsonValueBounds | null): string | undefined {
if (!contentBounds) return undefined
if (contentBounds.kind === 'string') return readJsonString(source, contentBounds, USER_TEXT_CAP)
if (contentBounds.kind !== 'array') return undefined
let text = ''
let i = contentBounds.start + 1
while (i < contentBounds.end - 1 && text.length < USER_TEXT_CAP) {
while (i < contentBounds.end && /\s/.test(source[i]!)) i++
if (source.charCodeAt(i) === 0x2c) {
i++
continue
}
if (source.charCodeAt(i) !== 0x7b) {
i++
continue
}
const objectEnd = findJsonContainerEnd(source, i, 0x7b, 0x7d, contentBounds.end)
if (objectEnd === -1) break
const objectBounds = { start: i, end: objectEnd + 1, kind: 'object' as const }
const type = readJsonString(source, findObjectFieldValue(source, objectBounds.start, objectBounds.end, 'type'))
if (type === 'text' || type === 'input_text') {
const part = readJsonString(
source,
findObjectFieldValue(source, objectBounds.start, objectBounds.end, 'text'),
USER_TEXT_CAP - text.length,
)
if (part) text += (text ? ' ' : '') + part
}
i = objectEnd + 1
}
return text || undefined
}
function extractLargeAddedNames(source: string, attachmentBounds: JsonValueBounds | null): string[] {
if (!attachmentBounds || attachmentBounds.kind !== 'object') return []
const attachmentType = readJsonString(source, findObjectFieldValue(source, attachmentBounds.start, attachmentBounds.end, 'type'))
if (attachmentType !== 'deferred_tools_delta') return []
const addedNames = findObjectFieldValue(source, attachmentBounds.start, attachmentBounds.end, 'addedNames')
if (!addedNames || addedNames.kind !== 'array') return []
const names: string[] = []
let i = addedNames.start + 1
while (i < addedNames.end - 1 && names.length < MAX_ADDED_NAMES) {
while (i < addedNames.end && /\s/.test(source[i]!)) i++
if (source.charCodeAt(i) === 0x2c) {
i++
continue
}
if (source.charCodeAt(i) !== 0x22) {
i++
continue
}
const end = findJsonStringEnd(source, i, addedNames.end)
if (end === -1) break
const name = readJsonString(source, { start: i, end: end + 1, kind: 'string' }, 500)
if (name) names.push(name)
i = end + 1
}
return names
}
function parseLargeJsonlLine(line: string): JournalEntry | null {
const rootEnd = findJsonContainerEnd(line, 0, 0x7b, 0x7d)
if (rootEnd === -1) return null
const rootStart = 0
const rootLimit = rootEnd + 1
const type = readJsonString(line, findObjectFieldValue(line, rootStart, rootLimit, 'type'))
if (!type) return null
const entry: JournalEntry = { type }
const timestamp = readJsonString(line, findObjectFieldValue(line, rootStart, rootLimit, 'timestamp'))
const sessionId = readJsonString(line, findObjectFieldValue(line, rootStart, rootLimit, 'sessionId'))
const cwd = readJsonString(line, findObjectFieldValue(line, rootStart, rootLimit, 'cwd'))
if (timestamp !== undefined) entry.timestamp = timestamp
if (sessionId !== undefined) entry.sessionId = sessionId
if (cwd !== undefined) entry.cwd = cwd
const addedNames = extractLargeAddedNames(line, findObjectFieldValue(line, rootStart, rootLimit, 'attachment'))
if (addedNames.length > 0) {
;(entry as Record<string, unknown>)['attachment'] = { type: 'deferred_tools_delta', addedNames }
}
if (type === 'user') {
const message = findObjectFieldValue(line, rootStart, rootLimit, 'message')
if (message?.kind === 'object') {
const content = findObjectFieldValue(line, message.start, message.end, 'content')
const text = extractLargeUserText(line, content)
if (text !== undefined) entry.message = { role: 'user', content: text }
}
return entry
}
if (type !== 'assistant') return entry
const message = findObjectFieldValue(line, rootStart, rootLimit, 'message')
if (message?.kind !== 'object') return entry
const model = readJsonString(line, findObjectFieldValue(line, message.start, message.end, 'model'))
const usageBounds = findObjectFieldValue(line, message.start, message.end, 'usage')
if (!model || usageBounds?.kind !== 'object') return entry
const id = readJsonString(line, findObjectFieldValue(line, message.start, message.end, 'id'))
const contentBounds = findObjectFieldValue(line, message.start, message.end, 'content')
entry.message = {
type: 'message',
role: 'assistant',
model,
...(id !== undefined ? { id } : {}),
content: extractLargeToolBlocks(line, contentBounds),
usage: parseLargeUsage(line, usageBounds),
}
return entry
}
type BufferJsonValueBounds = {
start: number
end: number
kind: 'string' | 'object' | 'array' | 'scalar'
}
function isJsonWhitespaceByte(ch: number | undefined): boolean {
return ch === 0x20 || ch === 0x0a || ch === 0x0d || ch === 0x09
}
function findJsonStringEndBuffer(source: Buffer, start: number, limit = source.length): number {
for (let i = start + 1; i < limit; i++) {
const ch = source[i]
if (ch === 0x5c) {
i++
continue
}
if (ch === 0x22) return i
}
return -1
}
function findJsonContainerEndBuffer(source: Buffer, start: number, open: number, close: number, limit = source.length): number {
let depth = 0
let inString = false
for (let i = start; i < limit; i++) {
const ch = source[i]
if (inString) {
if (ch === 0x5c) {
i++
} else if (ch === 0x22) {
inString = false
}
continue
}
if (ch === 0x22) {
inString = true
} else if (ch === open) {
depth++
} else if (ch === close) {
depth--
if (depth === 0) return i
}
}
return -1
}
function findJsonValueBoundsBuffer(source: Buffer, start: number, limit = source.length): BufferJsonValueBounds | null {
let i = start
while (i < limit && isJsonWhitespaceByte(source[i])) i++
if (i >= limit) return null
const ch = source[i]
if (ch === 0x22) {
const end = findJsonStringEndBuffer(source, i, limit)
return end === -1 ? null : { start: i, end: end + 1, kind: 'string' }
}
if (ch === 0x7b) {
const end = findJsonContainerEndBuffer(source, i, 0x7b, 0x7d, limit)
return end === -1 ? null : { start: i, end: end + 1, kind: 'object' }
}
if (ch === 0x5b) {
const end = findJsonContainerEndBuffer(source, i, 0x5b, 0x5d, limit)
return end === -1 ? null : { start: i, end: end + 1, kind: 'array' }
}
let end = i
while (end < limit) {
const c = source[end]
if (c === 0x2c || c === 0x7d || c === 0x5d || isJsonWhitespaceByte(c)) break
end++
}
return { start: i, end, kind: 'scalar' }
}
function bufferKeyEquals(source: Buffer, keyStart: number, keyEnd: number, field: string): boolean {
if (keyEnd - keyStart !== field.length) return false
return source.subarray(keyStart, keyEnd).equals(Buffer.from(field))
}
function findObjectFieldValueBuffer(source: Buffer, objectStart: number, objectEnd: number, field: string): BufferJsonValueBounds | null {
if (source[objectStart] !== 0x7b) return null
let i = objectStart + 1
while (i < objectEnd - 1) {
while (i < objectEnd && isJsonWhitespaceByte(source[i])) i++
if (source[i] === 0x2c) {
i++
continue
}
if (source[i] !== 0x22) {
i++
continue
}
const keyEnd = findJsonStringEndBuffer(source, i, objectEnd)
if (keyEnd === -1) return null
const keyStart = i + 1
i = keyEnd + 1
while (i < objectEnd && isJsonWhitespaceByte(source[i])) i++
if (source[i] !== 0x3a) continue
const value = findJsonValueBoundsBuffer(source, i + 1, objectEnd)
if (!value) return null
if (bufferKeyEquals(source, keyStart, keyEnd, field)) return value
i = value.end
}
return null
}
function appendBufferJsonSegment(source: Buffer, start: number, end: number, current: string, cap: number): string {
if (start >= end || current.length >= cap) return current
const remaining = cap - current.length
const cappedEnd = Number.isFinite(cap) ? Math.min(end, start + remaining * 4) : end
return current + source.subarray(start, cappedEnd).toString('utf-8').slice(0, remaining)
}
function readJsonStringBuffer(source: Buffer, bounds: BufferJsonValueBounds | null, cap = Number.POSITIVE_INFINITY): string | undefined {
if (!bounds || bounds.kind !== 'string') return undefined
let out = ''
let segmentStart = bounds.start + 1
for (let i = bounds.start + 1; i < bounds.end - 1 && out.length < cap; i++) {
const ch = source[i]
if (ch !== 0x5c) continue
out = appendBufferJsonSegment(source, segmentStart, i, out, cap)
if (out.length >= cap) break
const next = source[++i]
if (next === undefined) break
if (next === 0x6e) out += '\n'
else if (next === 0x72) out += '\r'
else if (next === 0x74) out += '\t'
else if (next === 0x62) out += '\b'
else if (next === 0x66) out += '\f'
else if (next === 0x75 && i + 4 < bounds.end) {
const hex = source.subarray(i + 1, i + 5).toString('ascii')
const code = Number.parseInt(hex, 16)
if (Number.isFinite(code)) out += String.fromCharCode(code)
i += 4
} else {
out += String.fromCharCode(next)
}
segmentStart = i + 1
}
return appendBufferJsonSegment(source, segmentStart, bounds.end - 1, out, cap)
}
function readJsonNumberFieldBuffer(source: Buffer, objectBounds: BufferJsonValueBounds | null, field: string): number | undefined {
if (!objectBounds || objectBounds.kind !== 'object') return undefined
const bounds = findObjectFieldValueBuffer(source, objectBounds.start, objectBounds.end, field)
if (!bounds) return undefined
const value = Number(source.subarray(bounds.start, bounds.end).toString('ascii'))
return Number.isFinite(value) ? value : undefined
}
function parseLargeUsageBuffer(source: Buffer, usageBounds: BufferJsonValueBounds | null) {
const usage: AssistantMessageContent['usage'] = {
input_tokens: readJsonNumberFieldBuffer(source, usageBounds, 'input_tokens') ?? 0,
output_tokens: readJsonNumberFieldBuffer(source, usageBounds, 'output_tokens') ?? 0,
cache_creation_input_tokens: readJsonNumberFieldBuffer(source, usageBounds, 'cache_creation_input_tokens'),
cache_read_input_tokens: readJsonNumberFieldBuffer(source, usageBounds, 'cache_read_input_tokens'),
}
if (usageBounds?.kind === 'object') {
const cacheCreation = findObjectFieldValueBuffer(source, usageBounds.start, usageBounds.end, 'cache_creation')
const ephemeral5m = readJsonNumberFieldBuffer(source, cacheCreation, 'ephemeral_5m_input_tokens')
const ephemeral1h = readJsonNumberFieldBuffer(source, cacheCreation, 'ephemeral_1h_input_tokens')
if (ephemeral5m !== undefined || ephemeral1h !== undefined) {
;(usage as AssistantMessageContent['usage']).cache_creation = {
...(ephemeral5m !== undefined ? { ephemeral_5m_input_tokens: ephemeral5m } : {}),
...(ephemeral1h !== undefined ? { ephemeral_1h_input_tokens: ephemeral1h } : {}),
}
}
const serverToolUse = findObjectFieldValueBuffer(source, usageBounds.start, usageBounds.end, 'server_tool_use')
const webSearch = readJsonNumberFieldBuffer(source, serverToolUse, 'web_search_requests')
const webFetch = readJsonNumberFieldBuffer(source, serverToolUse, 'web_fetch_requests')
if (webSearch !== undefined || webFetch !== undefined) {
;(usage as AssistantMessageContent['usage']).server_tool_use = {
...(webSearch !== undefined ? { web_search_requests: webSearch } : {}),
...(webFetch !== undefined ? { web_fetch_requests: webFetch } : {}),
}
}
const speed = readJsonStringBuffer(source, findObjectFieldValueBuffer(source, usageBounds.start, usageBounds.end, 'speed'))
if (speed === 'standard' || speed === 'fast') usage.speed = speed
}
return usage
}
function extractLargeToolBlocksBuffer(source: Buffer, contentBounds: BufferJsonValueBounds | null): ToolUseBlock[] {
if (!contentBounds || contentBounds.kind !== 'array') return []
const tools: ToolUseBlock[] = []
let i = contentBounds.start + 1
while (i < contentBounds.end - 1 && tools.length < MAX_TOOL_BLOCKS) {
while (i < contentBounds.end && isJsonWhitespaceByte(source[i])) i++
if (source[i] === 0x2c) {
i++
continue
}
if (source[i] !== 0x7b) {
i++
continue
}
const objectEnd = findJsonContainerEndBuffer(source, i, 0x7b, 0x7d, contentBounds.end)
if (objectEnd === -1) break
const objectBounds = { start: i, end: objectEnd + 1, kind: 'object' as const }
const blockType = readJsonStringBuffer(source, findObjectFieldValueBuffer(source, objectBounds.start, objectBounds.end, 'type'))
if (blockType === 'tool_use') {
const name = readJsonStringBuffer(source, findObjectFieldValueBuffer(source, objectBounds.start, objectBounds.end, 'name')) ?? ''
const id = readJsonStringBuffer(source, findObjectFieldValueBuffer(source, objectBounds.start, objectBounds.end, 'id')) ?? ''
const inputBounds = findObjectFieldValueBuffer(source, objectBounds.start, objectBounds.end, 'input')
const input: Record<string, unknown> = {}
if (inputBounds?.kind === 'object') {
if (name === 'Skill') {
const skill = readJsonStringBuffer(source, findObjectFieldValueBuffer(source, inputBounds.start, inputBounds.end, 'skill'), 200)
const skillName = readJsonStringBuffer(source, findObjectFieldValueBuffer(source, inputBounds.start, inputBounds.end, 'name'), 200)
if (skill !== undefined) input['skill'] = skill
if (skillName !== undefined) input['name'] = skillName
} else if (name === 'Read' || name === 'FileReadTool') {
const filePath = readJsonStringBuffer(source, findObjectFieldValueBuffer(source, inputBounds.start, inputBounds.end, 'file_path'), BASH_COMMAND_CAP)
if (filePath !== undefined) input['file_path'] = filePath
} else if (name === 'Agent' || name === 'Task') {
const subagentType = readJsonStringBuffer(source, findObjectFieldValueBuffer(source, inputBounds.start, inputBounds.end, 'subagent_type'), 200)
if (subagentType !== undefined) input['subagent_type'] = subagentType
} else if (BASH_TOOLS.has(name)) {
const command = readJsonStringBuffer(source, findObjectFieldValueBuffer(source, inputBounds.start, inputBounds.end, 'command'), BASH_COMMAND_CAP)
if (command !== undefined) input['command'] = command
}
}
tools.push({ type: 'tool_use', id, name, input })
}
i = objectEnd + 1
}
return tools
}
function extractLargeUserTextBuffer(source: Buffer, contentBounds: BufferJsonValueBounds | null): string | undefined {
if (!contentBounds) return undefined
if (contentBounds.kind === 'string') return readJsonStringBuffer(source, contentBounds, USER_TEXT_CAP)
if (contentBounds.kind !== 'array') return undefined
let text = ''
let i = contentBounds.start + 1
while (i < contentBounds.end - 1 && text.length < USER_TEXT_CAP) {
while (i < contentBounds.end && isJsonWhitespaceByte(source[i])) i++
if (source[i] === 0x2c) {
i++
continue
}
if (source[i] !== 0x7b) {
i++
continue
}
const objectEnd = findJsonContainerEndBuffer(source, i, 0x7b, 0x7d, contentBounds.end)
if (objectEnd === -1) break
const objectBounds = { start: i, end: objectEnd + 1, kind: 'object' as const }
const type = readJsonStringBuffer(source, findObjectFieldValueBuffer(source, objectBounds.start, objectBounds.end, 'type'))
if (type === 'text' || type === 'input_text') {
const part = readJsonStringBuffer(
source,
findObjectFieldValueBuffer(source, objectBounds.start, objectBounds.end, 'text'),
USER_TEXT_CAP - text.length,
)
if (part) text += (text ? ' ' : '') + part
}
i = objectEnd + 1
}
return text || undefined
}
function extractLargeAddedNamesBuffer(source: Buffer, attachmentBounds: BufferJsonValueBounds | null): string[] {
if (!attachmentBounds || attachmentBounds.kind !== 'object') return []
const attachmentType = readJsonStringBuffer(
source,
findObjectFieldValueBuffer(source, attachmentBounds.start, attachmentBounds.end, 'type'),
)
if (attachmentType !== 'deferred_tools_delta') return []
const addedNames = findObjectFieldValueBuffer(source, attachmentBounds.start, attachmentBounds.end, 'addedNames')
if (!addedNames || addedNames.kind !== 'array') return []
const names: string[] = []
let i = addedNames.start + 1
while (i < addedNames.end - 1 && names.length < MAX_ADDED_NAMES) {
while (i < addedNames.end && isJsonWhitespaceByte(source[i])) i++
if (source[i] === 0x2c) {
i++
continue
}
if (source[i] !== 0x22) {
i++
continue
}
const end = findJsonStringEndBuffer(source, i, addedNames.end)
if (end === -1) break
const name = readJsonStringBuffer(source, { start: i, end: end + 1, kind: 'string' }, 500)
if (name) names.push(name)
i = end + 1
}
return names
}
function parseLargeJsonlBuffer(line: Buffer): JournalEntry | null {
let rootStart = 0
while (rootStart < line.length && isJsonWhitespaceByte(line[rootStart])) rootStart++
if (line[rootStart] !== 0x7b) return null
const rootEnd = findJsonContainerEndBuffer(line, rootStart, 0x7b, 0x7d)
if (rootEnd === -1) return null
const rootLimit = rootEnd + 1
const type = readJsonStringBuffer(line, findObjectFieldValueBuffer(line, rootStart, rootLimit, 'type'))
if (!type) return null
const entry: JournalEntry = { type }
const timestamp = readJsonStringBuffer(line, findObjectFieldValueBuffer(line, rootStart, rootLimit, 'timestamp'))
const sessionId = readJsonStringBuffer(line, findObjectFieldValueBuffer(line, rootStart, rootLimit, 'sessionId'))
const cwd = readJsonStringBuffer(line, findObjectFieldValueBuffer(line, rootStart, rootLimit, 'cwd'))
if (timestamp !== undefined) entry.timestamp = timestamp
if (sessionId !== undefined) entry.sessionId = sessionId
if (cwd !== undefined) entry.cwd = cwd
const addedNames = extractLargeAddedNamesBuffer(line, findObjectFieldValueBuffer(line, rootStart, rootLimit, 'attachment'))
if (addedNames.length > 0) {
;(entry as Record<string, unknown>)['attachment'] = { type: 'deferred_tools_delta', addedNames }
}
if (type === 'user') {
const message = findObjectFieldValueBuffer(line, rootStart, rootLimit, 'message')
if (message?.kind === 'object') {
const content = findObjectFieldValueBuffer(line, message.start, message.end, 'content')
const text = extractLargeUserTextBuffer(line, content)
if (text !== undefined) entry.message = { role: 'user', content: text }
}
return entry
}
if (type !== 'assistant') return entry
const message = findObjectFieldValueBuffer(line, rootStart, rootLimit, 'message')
if (message?.kind !== 'object') return entry
const model = readJsonStringBuffer(line, findObjectFieldValueBuffer(line, message.start, message.end, 'model'))
const usageBounds = findObjectFieldValueBuffer(line, message.start, message.end, 'usage')
if (!model || usageBounds?.kind !== 'object') return entry
const id = readJsonStringBuffer(line, findObjectFieldValueBuffer(line, message.start, message.end, 'id'))
const contentBounds = findObjectFieldValueBuffer(line, message.start, message.end, 'content')
entry.message = {
type: 'message',
role: 'assistant',
model,
...(id !== undefined ? { id } : {}),
content: extractLargeToolBlocksBuffer(line, contentBounds),
usage: parseLargeUsageBuffer(line, usageBounds),
}
return entry
}
function getTopLevelRawJsonStringField(head: string, field: string): string | null {
let i = 0
while (i < head.length && /\s/.test(head[i]!)) i++
if (head.charCodeAt(i) !== 0x7b) return null
i++
while (i < head.length) {
while (i < head.length && /\s/.test(head[i]!)) i++
if (head.charCodeAt(i) === 0x2c) {
i++
continue
}
if (head.charCodeAt(i) === 0x7d) return null
if (head.charCodeAt(i) !== 0x22) return null
const keyEnd = findJsonStringEnd(head, i)
if (keyEnd === -1) return null
const key = head.slice(i + 1, keyEnd)
i = keyEnd + 1
while (i < head.length && /\s/.test(head[i]!)) i++
if (head.charCodeAt(i) !== 0x3a) return null
const value = findJsonValueBounds(head, i + 1)
if (!value) return null
if (key === field) return readJsonString(head, value) ?? null
i = value.end
}
return null
}
export function shouldSkipLine(line: string, threshold: string): boolean {
const head = line.length > RAW_HEAD_BYTES ? line.slice(0, RAW_HEAD_BYTES) : line
const type = getTopLevelRawJsonStringField(head, 'type')
if (type !== 'user' && type !== 'assistant') return false
const ts = getTopLevelRawJsonStringField(head, 'timestamp')
if (!ts || ts.length < 10) return false
return ts < threshold
}
const USER_TEXT_CAP = 2000
const BASH_COMMAND_CAP = 2000
const MAX_TOOL_BLOCKS = 500
@ -100,6 +826,12 @@ export function compactEntry(raw: JournalEntry): JournalEntry {
const ri = (tb.input ?? {}) as Record<string, unknown>
if (typeof ri['skill'] === 'string') input['skill'] = (ri['skill'] as string).slice(0, 200)
if (typeof ri['name'] === 'string') input['name'] = (ri['name'] as string).slice(0, 200)
} else if (tb.name === 'Read' || tb.name === 'FileReadTool') {
const ri = (tb.input ?? {}) as Record<string, unknown>
if (typeof ri['file_path'] === 'string') input['file_path'] = (ri['file_path'] as string).slice(0, BASH_COMMAND_CAP)
} else if (tb.name === 'Agent' || tb.name === 'Task') {
const ri = (tb.input ?? {}) as Record<string, unknown>
if (typeof ri['subagent_type'] === 'string') input['subagent_type'] = (ri['subagent_type'] as string).slice(0, 200)
} else if (BASH_TOOLS.has(tb.name)) {
const ri = (tb.input ?? {}) as Record<string, unknown>
if (typeof ri['command'] === 'string') {
@ -518,7 +1250,18 @@ async function parseSessionFile(
const entries: JournalEntry[] = []
let hasLines = false
for await (const line of readSessionLines(filePath)) {
// When a dateRange is given, skip user/assistant lines whose timestamp
// is older than range.start - 24h without calling JSON.parse. Huge lines
// that cannot be skipped are yielded as Buffers and compact-parsed without
// converting the whole line into a V8 string.
const earlySkipThreshold = dateRange
? new Date(dateRange.start.getTime() - 86_400_000).toISOString()
: null
const skipFn = earlySkipThreshold
? (head: string) => shouldSkipLine(head, earlySkipThreshold)
: undefined
for await (const line of readSessionLines(filePath, skipFn, { largeLineAsBuffer: true })) {
hasLines = true
const entry = parseJsonlLine(line)
if (entry) entries.push(compactEntry(entry))
@ -825,6 +1568,35 @@ export function filterProjectsByName(
return result
}
function turnIsInDateRange(turn: ClassifiedTurn, dateRange: DateRange): boolean {
if (turn.assistantCalls.length === 0) return false
const firstCallTs = turn.assistantCalls[0]!.timestamp
if (!firstCallTs) return false
const ts = new Date(firstCallTs)
return ts >= dateRange.start && ts <= dateRange.end
}
export function filterProjectsByDateRange(projects: ProjectSummary[], dateRange: DateRange): ProjectSummary[] {
const filtered: ProjectSummary[] = []
for (const project of projects) {
const sessions: SessionSummary[] = []
for (const session of project.sessions) {
const turns = session.turns.filter(turn => turnIsInDateRange(turn, dateRange))
if (turns.length === 0) continue
sessions.push(buildSessionSummary(session.sessionId, session.project, turns, session.mcpInventory))
}
if (sessions.length === 0) continue
filtered.push({
project: project.project,
projectPath: project.projectPath,
sessions,
totalCostUSD: sessions.reduce((s, sess) => s + sess.totalCostUSD, 0),
totalApiCalls: sessions.reduce((s, sess) => s + sess.apiCalls, 0),
})
}
return filtered.sort((a, b) => b.totalCostUSD - a.totalCostUSD)
}
export async function parseAllSessions(dateRange?: DateRange, providerFilter?: string): Promise<ProjectSummary[]> {
const key = cacheKey(dateRange, providerFilter)
const cached = sessionCache.get(key)

View file

@ -65,6 +65,8 @@ type CodexTokenUsage = {
}
const CHARS_PER_TOKEN = 4
const RAW_HEAD_BYTES = 64 * 1024
const LARGE_TEXT_CAP = 2000
function getCodexDir(override?: string): string {
return override ?? process.env['CODEX_HOME'] ?? join(homedir(), '.codex')
@ -126,6 +128,116 @@ async function isValidCodexSession(filePath: string): Promise<{ valid: boolean;
return { valid, meta: valid ? entry : undefined }
}
function getRawJsonStringField(head: string, field: string): string | undefined {
const re = new RegExp(`"${field}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`)
const match = re.exec(head)
if (!match) return undefined
try {
return JSON.parse(`"${match[1]}"`) as string
} catch {
return match[1]
}
}
function payloadHead(head: string): string {
const idx = head.indexOf('"payload"')
return idx === -1 ? head : head.slice(idx)
}
function countJsonStringBytes(source: Buffer, valueStart: number): number {
let count = 0
for (let i = valueStart; i < source.length; i++) {
const ch = source[i]
if (ch === 0x5c) {
i++
count++
continue
}
if (ch === 0x22) return count
count++
}
return count
}
function extractFirstJsonText(source: Buffer, cap = LARGE_TEXT_CAP): string {
const key = Buffer.from('"text"')
const idx = source.indexOf(key)
if (idx === -1) return ''
const colon = source.indexOf(0x3a, idx + key.length)
if (colon === -1) return ''
const qStart = source.indexOf(0x22, colon + 1)
if (qStart === -1) return ''
const chunks: number[] = []
for (let i = qStart + 1; i < source.length && chunks.length < cap; i++) {
const ch = source[i]
if (ch === 0x5c) {
const next = source[++i]
if (next === 0x6e) chunks.push(0x0a)
else if (next === 0x72) chunks.push(0x0d)
else if (next === 0x74) chunks.push(0x09)
else if (next !== undefined) chunks.push(next)
continue
}
if (ch === 0x22) break
chunks.push(ch)
}
return Buffer.from(chunks).toString('utf-8')
}
function countFirstJsonText(source: Buffer): number {
const key = Buffer.from('"text"')
const idx = source.indexOf(key)
if (idx === -1) return 0
const colon = source.indexOf(0x3a, idx + key.length)
if (colon === -1) return 0
const qStart = source.indexOf(0x22, colon + 1)
if (qStart === -1) return 0
return countJsonStringBytes(source, qStart + 1)
}
function parseCodexLine(line: string | Buffer): CodexEntry | null {
if (typeof line === 'string') {
const trimmed = line.trim()
if (!trimmed) return null
try {
return JSON.parse(trimmed) as CodexEntry
} catch {
return null
}
}
if (line.length === 0) return null
const head = line.subarray(0, RAW_HEAD_BYTES).toString('utf-8')
const type = getRawJsonStringField(head, 'type')
if (!type) return null
const pHead = payloadHead(head)
const payloadType = getRawJsonStringField(pHead, 'type')
const role = getRawJsonStringField(pHead, 'role')
const entry: CodexEntry = {
type,
timestamp: getRawJsonStringField(head, 'timestamp'),
payload: {
type: payloadType,
role,
cwd: getRawJsonStringField(pHead, 'cwd'),
model_provider: getRawJsonStringField(pHead, 'model_provider'),
originator: getRawJsonStringField(pHead, 'originator'),
session_id: getRawJsonStringField(pHead, 'session_id'),
model: getRawJsonStringField(pHead, 'model'),
name: getRawJsonStringField(pHead, 'name'),
},
}
if (type === 'response_item' && payloadType === 'message' && role === 'user') {
entry.payload!.content = [{ type: 'input_text', text: extractFirstJsonText(line) }]
} else if (type === 'response_item' && payloadType === 'message' && role === 'assistant') {
entry.payload!.content = [{ type: 'output_text', text: 'x'.repeat(Math.min(countFirstJsonText(line), LARGE_TEXT_CAP)) }]
}
return entry
}
async function discoverSessionsInDir(codexDir: string): Promise<SessionSource[]> {
const sessionsDir = join(codexDir, 'sessions')
const sources: SessionSource[] = []
@ -224,18 +336,12 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
// Stream the session file line by line. Heavy Codex sessions can exceed
// 250 MB on disk; reading the entire file into a string would either hit
// the readSessionFile cap or push V8 toward its 512 MB string limit
// after split('\n'). readSessionLines streams via readline so memory
// stays bounded to the longest line.
for await (const rawLine of readSessionLines(source.path)) {
// after split('\n'). readSessionLines streams raw buffers and hands
// huge lines to the compact parser without full string conversion.
for await (const rawLine of readSessionLines(source.path, undefined, { largeLineAsBuffer: true })) {
sawAnyLine = true
const line = rawLine.trim()
if (!line) continue
let entry: CodexEntry
try {
entry = JSON.parse(line) as CodexEntry
} catch {
continue
}
const entry = parseCodexLine(rawLine)
if (!entry) continue
if (entry.type === 'session_meta') {
sessionId = entry.payload?.session_id ?? basename(source.path, '.jsonl')

View file

@ -5,7 +5,6 @@ import { join } from 'path'
import {
MAX_SESSION_FILE_BYTES,
STREAM_THRESHOLD_BYTES,
readSessionFile,
readSessionLines,
} from '../src/fs-utils.js'
@ -34,11 +33,12 @@ describe('readSessionFile', () => {
expect(await readSessionFile(p)).toBe('hello\nworld\n')
})
it('returns content for files at the stream threshold via stream path', async () => {
const p = await tmpPath(Buffer.alloc(STREAM_THRESHOLD_BYTES, 'a'))
it('returns content for large files under the full-file cap', async () => {
const size = 8 * 1024 * 1024
const p = await tmpPath(Buffer.alloc(size, 'a'))
const got = await readSessionFile(p)
expect(got).not.toBeNull()
expect(got!.length).toBe(STREAM_THRESHOLD_BYTES)
expect(got!.length).toBe(size)
})
it('returns null and skips files over the cap', async () => {
@ -88,6 +88,28 @@ describe('readSessionLines', () => {
expect(lines).toEqual(['line1', 'line2', 'line3'])
})
it('skips old large lines before materializing the full line', async () => {
const oldLine = `{"type":"assistant","timestamp":"2026-01-01T00:00:00Z","payload":"${'x'.repeat(100_000)}"}`
const newLine = '{"type":"assistant","timestamp":"2026-05-01T00:00:00Z"}'
const p = await tmpPath(`${oldLine}\n${newLine}\n`)
const lines: string[] = []
for await (const line of readSessionLines(p, head => head.includes('2026-01-01'))) {
lines.push(line)
}
expect(lines).toEqual([newLine])
})
it('yields large lines as Buffers when requested', async () => {
const largeLine = `{"type":"assistant","timestamp":"2026-05-01T00:00:00Z","payload":"${'x'.repeat(100_000)}"}`
const p = await tmpPath(`${largeLine}\nsmall\n`)
const lines: Array<string | Buffer> = []
for await (const line of readSessionLines(p, undefined, { largeLineAsBuffer: true })) {
lines.push(line)
}
expect(Buffer.isBuffer(lines[0])).toBe(true)
expect(lines[1]).toBe('small')
})
it('does not leak file descriptors when generator is abandoned early', async () => {
const content = Array.from({ length: 1000 }, (_, i) => `line-${i}`).join('\n')
const p = await tmpPath(content)

View file

@ -325,7 +325,7 @@ describe('scanJsonlFile', () => {
message: { content: [{ type: 'tool_use', name: 'Bash', input: {} }] },
}))
await scanJsonlFile(filePath, 'p1', undefined)
expect(readSessionLinesSpy).toHaveBeenCalledWith(filePath)
expect(readSessionLinesSpy).toHaveBeenCalledWith(filePath, undefined, { largeLineAsBuffer: true })
expect(readSessionFileSpy).not.toHaveBeenCalled()
readSessionLinesSpy.mockRestore()
readSessionFileSpy.mockRestore()

View file

@ -141,7 +141,7 @@ describe('compactEntry', () => {
expect(msg.content).toHaveLength(2)
expect(msg.content[0]!.name).toBe('Read')
expect(msg.content[0]!.id).toBe('tu1')
expect(msg.content[0]!.input).toEqual({})
expect(msg.content[0]!.input).toEqual({ file_path: '/foo' })
expect(msg.content[1]!.name).toBe('Edit')
expect(msg.content[1]!.id).toBe('tu2')
expect(msg.content[1]!.input).toEqual({})
@ -290,6 +290,44 @@ describe('compactEntry', () => {
expect(msg.content[0]!.input['description']).toBeUndefined()
})
it('keeps Read file_path capped and drops unrelated input fields', () => {
const raw = entry({
type: 'assistant',
message: {
type: 'message' as const,
role: 'assistant' as const,
model: 'claude-opus-4-6',
usage: { input_tokens: 10, output_tokens: 10 },
content: [
{ type: 'tool_use', id: 'tu', name: 'Read', input: { file_path: '/tmp/' + 'x'.repeat(3000), content: 'big' } },
],
},
})
const c = compactEntry(raw)
const msg = c.message as { content: Array<{ input: Record<string, unknown> }> }
expect((msg.content[0]!.input['file_path'] as string).length).toBe(2000)
expect(msg.content[0]!.input['content']).toBeUndefined()
})
it('keeps Agent subagent_type capped and drops prompt text', () => {
const raw = entry({
type: 'assistant',
message: {
type: 'message' as const,
role: 'assistant' as const,
model: 'claude-opus-4-6',
usage: { input_tokens: 10, output_tokens: 10 },
content: [
{ type: 'tool_use', id: 'tu', name: 'Agent', input: { subagent_type: 'reviewer'.repeat(50), prompt: 'big' } },
],
},
})
const c = compactEntry(raw)
const msg = c.message as { content: Array<{ input: Record<string, unknown> }> }
expect((msg.content[0]!.input['subagent_type'] as string).length).toBe(200)
expect(msg.content[0]!.input['prompt']).toBeUndefined()
})
it('handles entry with no message field', () => {
const raw = entry({ type: 'system', timestamp: 't1', cwd: '/x' })
const c = compactEntry(raw)

View file

@ -0,0 +1,87 @@
import { describe, expect, it } from 'vitest'
import { parseJsonlLine } from '../src/parser.js'
function largeUserLine(): string {
return JSON.stringify({
type: 'user',
sessionId: 's1',
timestamp: '2026-05-01T00:00:00Z',
cwd: '/repo',
message: {
role: 'user',
content: [
{ type: 'image', source: { data: 'x'.repeat(40_000) } },
{ type: 'text', text: 'hello ' + 'a'.repeat(3000) },
],
},
})
}
function largeAssistantLine(): string {
return JSON.stringify({
type: 'assistant',
sessionId: 's1',
timestamp: '2026-05-01T00:00:01Z',
cwd: '/repo',
message: {
id: 'm1',
type: 'message',
role: 'assistant',
model: 'claude-sonnet-4-5',
content: [
{ type: 'text', text: 'x'.repeat(40_000) },
{ type: 'tool_use', id: 'read1', name: 'Read', input: { file_path: '/tmp/file.ts', content: 'drop me' } },
{ type: 'tool_use', id: 'agent1', name: 'Agent', input: { subagent_type: 'reviewer', prompt: 'drop me' } },
],
usage: {
input_tokens: 100,
output_tokens: 20,
cache_read_input_tokens: 300,
},
},
})
}
describe('large JSONL compact scanner', () => {
it('extracts user text from array content without full JSON.parse', () => {
const parsed = parseJsonlLine(largeUserLine())
expect(parsed?.type).toBe('user')
const content = parsed?.message?.role === 'user' ? parsed.message.content : ''
expect(content).toBeTypeOf('string')
expect((content as string).startsWith('hello ')).toBe(true)
expect((content as string).length).toBe(2000)
})
it('extracts capped tool inputs needed by optimize', () => {
const parsed = parseJsonlLine(Buffer.from(largeAssistantLine()))
const msg = parsed?.message
expect(msg?.role).toBe('assistant')
if (msg?.role !== 'assistant') return
expect(msg.usage.input_tokens).toBe(100)
expect(msg.usage.output_tokens).toBe(20)
expect(msg.usage.cache_read_input_tokens).toBe(300)
expect(msg.content).toEqual([
{ type: 'tool_use', id: 'read1', name: 'Read', input: { file_path: '/tmp/file.ts' } },
{ type: 'tool_use', id: 'agent1', name: 'Agent', input: { subagent_type: 'reviewer' } },
])
})
it('extracts deferred MCP inventory from large attachment lines', () => {
const line = JSON.stringify({
type: 'attachment',
sessionId: 's1',
timestamp: '2026-05-01T00:00:02Z',
padding: 'x'.repeat(40_000),
attachment: {
type: 'deferred_tools_delta',
addedNames: ['Bash', 'mcp__svc__tool'],
},
})
const parsed = parseJsonlLine(Buffer.from(line)) as Record<string, unknown>
expect(parsed['attachment']).toEqual({
type: 'deferred_tools_delta',
addedNames: ['Bash', 'mcp__svc__tool'],
})
})
})

View file

@ -64,6 +64,11 @@ function assistantLine(sessionId: string, timestamp: string, messageId: string,
})
}
function messageFirstLargeAssistantLine(sessionId: string, timestamp: string, messageId: string): string {
const hugeText = 'y'.repeat(3_000_000)
return `{"parentUuid":"u1","isSidechain":false,"message":{"model":"claude-sonnet-4-5","id":"${messageId}","type":"message","role":"assistant","content":[{"type":"text","text":"${hugeText}"},{"type":"tool_use","id":"tu-large","name":"Edit","input":{"file_path":"/tmp/x","old_string":"a","new_string":"b"}}],"usage":{"input_tokens":1000,"output_tokens":100,"cache_read_input_tokens":5000}},"uuid":"a1","timestamp":"${timestamp}","type":"assistant","sessionId":"${sessionId}","cwd":"/projects/app"}`
}
function attachmentLine(sessionId: string, timestamp: string): string {
return JSON.stringify({
type: 'attachment',
@ -145,4 +150,31 @@ describe('parseAllSessions with large Claude fixture', () => {
const sess = projects[0]!.sessions[0]!
expect(sess.apiCalls).toBeGreaterThanOrEqual(1)
})
it('parses huge message-first assistant lines without full JSON.parse expansion', async () => {
const projectDir = join(home, '.claude', 'projects', 'messagefirst')
await mkdir(projectDir, { recursive: true })
const lines = [
userLine('s1', '2026-04-10T10:00:00Z', 100),
messageFirstLargeAssistantLine('s1', '2026-04-10T10:00:01Z', 'msg-large'),
]
await writeFile(join(projectDir, 'session.jsonl'), lines.join('\n'))
const range: DateRange = {
start: new Date('2026-04-10T00:00:00Z'),
end: new Date('2026-04-10T23:59:59Z'),
}
const projects = await parseAllSessions(range, 'claude')
expect(projects.length).toBeGreaterThan(0)
const sess = projects[0]!.sessions[0]!
expect(sess.apiCalls).toBe(1)
expect(sess.totalInputTokens).toBe(1000)
expect(sess.totalOutputTokens).toBe(100)
expect(sess.totalCacheReadTokens).toBe(5000)
expect(sess.toolBreakdown['Edit']?.calls).toBe(1)
})
})

View file

@ -0,0 +1,86 @@
import { describe, it, expect } from 'vitest'
import { shouldSkipLine } from '../src/parser.js'
const threshold = '2026-04-01T00:00:00.000Z'
function makeLine(type: string, timestamp: string, payloadSize = 0): string {
const payload = payloadSize > 0 ? `,"content":"${'x'.repeat(payloadSize)}"` : ''
return `{"type":"${type}","sessionId":"s1","timestamp":"${timestamp}"${payload}}`
}
function makeLineWithLongCwd(type: string, timestamp: string, cwdLength: number): string {
const cwd = '/projects/' + 'a'.repeat(cwdLength)
return `{"type":"${type}","sessionId":"s1","cwd":"${cwd}","timestamp":"${timestamp}","message":{"role":"user","content":"hi"}}`
}
describe('shouldSkipLine', () => {
it('skips old user lines', () => {
expect(shouldSkipLine(makeLine('user', '2026-03-01T10:00:00Z'), threshold)).toBe(true)
})
it('skips old assistant lines', () => {
expect(shouldSkipLine(makeLine('assistant', '2026-03-15T10:00:00Z'), threshold)).toBe(true)
})
it('does not skip in-range user lines', () => {
expect(shouldSkipLine(makeLine('user', '2026-04-05T10:00:00Z'), threshold)).toBe(false)
})
it('does not skip in-range assistant lines', () => {
expect(shouldSkipLine(makeLine('assistant', '2026-04-10T10:00:00Z'), threshold)).toBe(false)
})
it('never skips attachment lines regardless of timestamp', () => {
expect(shouldSkipLine(makeLine('attachment', '2026-01-01T00:00:00Z'), threshold)).toBe(false)
})
it('never skips system lines regardless of timestamp', () => {
expect(shouldSkipLine(makeLine('system', '2026-01-01T00:00:00Z'), threshold)).toBe(false)
})
it('never skips summary lines regardless of timestamp', () => {
expect(shouldSkipLine(makeLine('summary', '2026-01-01T00:00:00Z'), threshold)).toBe(false)
})
it('does not skip lines with no timestamp field', () => {
expect(shouldSkipLine('{"type":"user","sessionId":"s1"}', threshold)).toBe(false)
})
it('does not skip lines with unparseable timestamp', () => {
expect(shouldSkipLine('{"type":"user","timestamp":"bad"}', threshold)).toBe(false)
})
it('does not skip malformed JSON', () => {
expect(shouldSkipLine('not json at all', threshold)).toBe(false)
})
it('only reads top-level type and timestamp fields', () => {
const line = '{"message":{"type":"assistant","timestamp":"2026-03-01T10:00:00Z"},"type":"user","timestamp":"2026-04-05T10:00:00Z"}'
expect(shouldSkipLine(line, threshold)).toBe(false)
})
it('handles timestamp pushed past 200 chars by long cwd', () => {
const line = makeLineWithLongCwd('user', '2026-03-01T10:00:00Z', 300)
expect(line.indexOf('"timestamp"')).toBeGreaterThan(200)
expect(shouldSkipLine(line, threshold)).toBe(true)
})
it('handles timestamp at the edge of the 2048 head window', () => {
const line = makeLineWithLongCwd('user', '2026-03-01T10:00:00Z', 1900)
expect(line.indexOf('"timestamp"')).toBeGreaterThan(1900)
expect(shouldSkipLine(line, threshold)).toBe(true)
})
it('returns false when timestamp is beyond the head window', () => {
const line = makeLineWithLongCwd('user', '2026-03-01T10:00:00Z', 2100)
expect(line.indexOf('"timestamp"')).toBeGreaterThan(2048)
expect(shouldSkipLine(line, threshold)).toBe(false)
})
it('skips old assistant line with large payload without parsing it', () => {
const line = makeLine('assistant', '2026-02-01T10:00:00Z', 50_000_000)
expect(line.length).toBeGreaterThan(50_000_000)
expect(shouldSkipLine(line, threshold)).toBe(true)
})
})