Users with long-running teams (37+ tasks, 10+ agents for an hour) were hitting constant renderer crashes (issue #36). Two hot paths were serializing unbounded histories across IPC on every tick: - Provisioning progress: emitLogsProgress and updateProgress both joined the full provisioningOutputParts array (~20 event-driven call sites) plus the full CLI log tail, then fanned that out to the renderer. After an hour, each tick shipped multi-megabyte payloads and Zustand OOM'd on the immutable state clone. - Session detail cache: SessionDetail.messages (the raw parsed JSONL) was being cached and returned over IPC/HTTP even though the renderer only reads session/chunks/processes/metrics. This roughly doubled the per-entry cache footprint on large sessions. Fixes: - Add progressPayload helpers that cap the log tail to 200 lines and assistant output to the last 20 parts; empty/whitespace joins collapse to undefined so the noop guard is explicit rather than coincidental. - Apply the cap inside emitLogsProgress, updateProgress, and the two inline emission paths (stall warning, retry error). Throttle the log-progress tick 300ms -> 1000ms so Zustand can keep up. - Add stripSessionDetailMessages and call it at every SessionDetail production site that crosses IPC/HTTP (both sessions.ts routes, both cache stores). - Raise MAX_CACHE_SESSIONS 5 -> 20 now that the per-entry SessionDetail footprint is bounded. Previously 5 forced constant re-parsing on every session switch. Tests: 15 new unit tests covering the helpers (tail slicing, empty parts, whitespace-only parts, non-mutation of inputs).
52 lines
2 KiB
TypeScript
52 lines
2 KiB
TypeScript
/**
|
|
* Helpers that shape provisioning progress payloads before they are emitted
|
|
* to the renderer over IPC.
|
|
*
|
|
* Rationale: the renderer only renders a small "tail" preview of CLI logs
|
|
* and assistant output in ProvisioningProgressBlock / CliLogsRichView. Sending
|
|
* the full accumulated history on every throttled progress tick (≈ every
|
|
* second under load) serialized a multi-megabyte string over IPC and forced
|
|
* Zustand to produce a new immutable state object — which triggered renderer
|
|
* V8 OOM crashes for users with long-running teams. These helpers keep the
|
|
* hot emission path bounded while leaving the full history in-process for
|
|
* diagnostics and completion-time reports.
|
|
*/
|
|
|
|
export const PROGRESS_LOG_TAIL_LINES = 200;
|
|
export const PROGRESS_OUTPUT_TAIL_PARTS = 20;
|
|
|
|
/**
|
|
* Return the trailing `maxLines` of a line-buffered CLI log, joined with "\n"
|
|
* and trimmed. Returns `undefined` when the tail is empty so callers can
|
|
* skip emitting a noop update.
|
|
*/
|
|
export function buildProgressLogsTail(
|
|
lines: readonly string[],
|
|
maxLines: number = PROGRESS_LOG_TAIL_LINES
|
|
): string | undefined {
|
|
if (lines.length === 0) {
|
|
return undefined;
|
|
}
|
|
const effectiveMax = Math.max(1, maxLines);
|
|
const tail = lines.length > effectiveMax ? lines.slice(-effectiveMax) : lines;
|
|
const joined = tail.join('\n').trim();
|
|
return joined.length === 0 ? undefined : joined;
|
|
}
|
|
|
|
/**
|
|
* Return the trailing `maxParts` of assistant output parts joined with a
|
|
* blank line, matching the renderer's rendering contract. Returns `undefined`
|
|
* when no parts are available.
|
|
*/
|
|
export function buildProgressAssistantOutput(
|
|
parts: readonly string[],
|
|
maxParts: number = PROGRESS_OUTPUT_TAIL_PARTS
|
|
): string | undefined {
|
|
if (parts.length === 0) {
|
|
return undefined;
|
|
}
|
|
const effectiveMax = Math.max(1, maxParts);
|
|
const tail = parts.length > effectiveMax ? parts.slice(-effectiveMax) : parts;
|
|
const joined = tail.join('\n\n');
|
|
return joined.trim().length === 0 ? undefined : joined;
|
|
}
|