gateway: tail-read session transcripts to prevent chat.history freezes
readSessionMessages() previously called fs.readFileSync to load the entire JSONL transcript synchronously, then split and JSON.parse every line. On large sessions — or when a single record is unusually large — this blocked the Node.js event loop long enough to freeze the Gateway WebSocket and make the Web UI / WebChat appear hung. Root cause confirmed via a 447 KB single-line JSONL record that reproduced the hang reliably; moving the file aside restored UI responsiveness immediately. Fix: two-layer guard in readSessionMessages(): 1. Tail-read: files larger than 2 MB are read from the trailing 2 MB only (partial first line is discarded). This covers the common case where a session grows gradually over many turns. 2. Per-line cap: lines longer than 200 KB are skipped without parsing. A normal assistant reply is well under 50 KB; anything beyond 200 KB is a runaway prompt or model output that would stall JSON.parse and bloat the UI. The 200 KB threshold catches the confirmed 447 KB case that was too small to trigger the previous 1 MB guard. readSessionTitleFieldsFromTranscript() already uses head/tail chunk reads and is unaffected.
This commit is contained in:
parent
0fb7add7d6
commit
53aca43ee7
@ -82,14 +82,53 @@ export function readSessionMessages(
|
||||
return [];
|
||||
}
|
||||
|
||||
const lines = fs.readFileSync(filePath, "utf-8").split(/\r?\n/);
|
||||
// NOTE: This is on the Gateway hot path (chat.history). Reading + splitting an entire transcript
|
||||
// file can freeze the UI when a session grows large (or when a single JSONL record is huge).
|
||||
// We therefore tail-read large files and apply a per-line size guard.
|
||||
const MAX_TAIL_BYTES = 2 * 1024 * 1024; // 2MB tail is plenty for the last ~100-1000 messages
|
||||
// 200KB per line: a normal assistant reply is well under 50KB. Anything larger is a runaway
|
||||
// prompt/response that would only stall JSON.parse and bloat the UI — skip it entirely.
|
||||
// (The confirmed 447KB line causing Gateway freezes is caught by this threshold.)
|
||||
const MAX_LINE_CHARS = 200 * 1024;
|
||||
|
||||
let content = "";
|
||||
try {
|
||||
const stat = fs.statSync(filePath);
|
||||
if (stat.size > MAX_TAIL_BYTES) {
|
||||
const fd = fs.openSync(filePath, "r");
|
||||
try {
|
||||
const start = Math.max(0, stat.size - MAX_TAIL_BYTES);
|
||||
const buf = Buffer.allocUnsafe(stat.size - start);
|
||||
fs.readSync(fd, buf, 0, buf.length, start);
|
||||
content = buf.toString("utf-8");
|
||||
// If we started mid-line, drop the partial first line.
|
||||
const firstNewline = content.indexOf("\n");
|
||||
if (firstNewline >= 0 && start > 0) {
|
||||
content = content.slice(firstNewline + 1);
|
||||
}
|
||||
} finally {
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
} else {
|
||||
content = fs.readFileSync(filePath, "utf-8");
|
||||
}
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lines = content.split(/\r?\n/);
|
||||
const messages: unknown[] = [];
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
if (trimmed.length > MAX_LINE_CHARS) {
|
||||
// Skip lines that are too large to safely parse on the RPC path.
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(line);
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (parsed?.message) {
|
||||
messages.push(parsed.message);
|
||||
continue;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user