From 482bbf678b541b28a45f317282bb6ed8b9df8f2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Tue, 17 Mar 2026 17:05:35 -0600 Subject: [PATCH] fix: deduplicate parseJSONL and unify MAX_JSONL_BYTES constant (#985) Co-authored-by: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/forensics.ts | 12 +---------- src/resources/extensions/gsd/jsonl-utils.ts | 21 +++++++++++++++++++ .../extensions/gsd/session-forensics.ts | 17 ++------------- 3 files changed, 24 insertions(+), 26 deletions(-) create mode 100644 src/resources/extensions/gsd/jsonl-utils.ts diff --git a/src/resources/extensions/gsd/forensics.ts b/src/resources/extensions/gsd/forensics.ts index a9dcc7b84..0efe2e326 100644 --- a/src/resources/extensions/gsd/forensics.ts +++ b/src/resources/extensions/gsd/forensics.ts @@ -15,6 +15,7 @@ import { fileURLToPath } from "node:url"; import { extractTrace, type ExecutionTrace } from "./session-forensics.js"; import { nativeParseJsonlTail } from "./native-parser-bridge.js"; +import { MAX_JSONL_BYTES, parseJSONL } from "./jsonl-utils.js"; import { loadLedgerFromDisk, getAverageCostPerUnitType, getProjectTotals, formatCost, formatTokenCount, type UnitMetrics, type MetricsLedger, @@ -65,17 +66,6 @@ interface ForensicReport { recentUnits: { type: string; id: string; cost: number; duration: number; model: string; finishedAt: number }[]; } -// ─── JSONL Parser (inline — session-forensics.ts version is module-private) ── - -const MAX_JSONL_BYTES = 5 * 1024 * 1024; - -function parseJSONL(raw: string): unknown[] { - const source = raw.length > MAX_JSONL_BYTES ? raw.slice(-MAX_JSONL_BYTES) : raw; - return source.trim().split("\n").map(line => { - try { return JSON.parse(line); } catch { return null; } - }).filter(Boolean) as unknown[]; -} - // ─── Entry Point ────────────────────────────────────────────────────────────── export async function handleForensics( diff --git a/src/resources/extensions/gsd/jsonl-utils.ts b/src/resources/extensions/gsd/jsonl-utils.ts new file mode 100644 index 000000000..4e0de6112 --- /dev/null +++ b/src/resources/extensions/gsd/jsonl-utils.ts @@ -0,0 +1,21 @@ +/** + * Shared JSONL parsing utilities. + * + * Both forensics.ts and session-forensics.ts need to parse JSONL activity logs + * with an upper byte limit to prevent V8 OOM on bloated files. This module + * provides the single canonical implementation and constant. + */ + +/** Max bytes to parse from a JSONL source. Prevents V8 OOM on bloated activity logs. */ +export const MAX_JSONL_BYTES = 10 * 1024 * 1024; // 10 MB + +/** + * Parse a raw JSONL string into an array of parsed objects. + * If the input exceeds MAX_JSONL_BYTES, only the tail is parsed (most recent entries). + */ +export function parseJSONL(raw: string): unknown[] { + const source = raw.length > MAX_JSONL_BYTES ? raw.slice(-MAX_JSONL_BYTES) : raw; + return source.trim().split("\n").map(line => { + try { return JSON.parse(line); } catch { return null; } + }).filter(Boolean) as unknown[]; +} diff --git a/src/resources/extensions/gsd/session-forensics.ts b/src/resources/extensions/gsd/session-forensics.ts index 4db0a2e12..7e293eab9 100644 --- a/src/resources/extensions/gsd/session-forensics.ts +++ b/src/resources/extensions/gsd/session-forensics.ts @@ -21,6 +21,7 @@ import { readFileSync, readdirSync, existsSync, statSync } from "node:fs"; import { basename, join } from "node:path"; import { nativeParseJsonlTail } from "./native-parser-bridge.js"; +import { MAX_JSONL_BYTES, parseJSONL } from "./jsonl-utils.js"; import { nativeWorkingTreeStatus, nativeDiffStat } from "./native-git-bridge.js"; import { getAutoWorktreePath } from "./auto-worktree.js"; @@ -63,21 +64,7 @@ export interface RecoveryBriefing { } // ─── JSONL Parsing ──────────────────────────────────────────────────────────── - -/** Max bytes to parse from a JSONL source. Prevents V8 OOM on bloated activity logs. */ -const MAX_JSONL_BYTES = 10 * 1024 * 1024; // 10 MB - -function parseJSONL(raw: string): unknown[] { - // If the file is enormous, only parse the tail (most recent entries). - // This prevents the OOM crash path: large file → split → map → parse → OOM. - const source = raw.length > MAX_JSONL_BYTES - ? raw.slice(-MAX_JSONL_BYTES) - : raw; - return source.trim().split("\n").map(line => { - try { return JSON.parse(line); } - catch { return null; } - }).filter(Boolean) as unknown[]; -} +// MAX_JSONL_BYTES and parseJSONL are imported from ./jsonl-utils.js /** * Find the entries belonging to the last session in a JSONL file.