From 547a014e7eae5bf5677d1e8dd96638a096fcd446 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Wed, 17 Jun 2026 00:34:59 -0500 Subject: [PATCH] fix(desktop): avoid stack overflow rendering huge fenced blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `normalizeFenceBlocks`/`pushProseFence` appended block bodies with `out.push(...lines)`, which spreads every line as a separate call argument. A single message carrying a large fenced block (a logged minified bundle, base64 blob, or big tool dump — common in long sessions) overflows V8's argument-count limit and throws `RangeError: Maximum call stack size exceeded`, breaking the transcript render. Compression doesn't save us: it gates on tokens vs. window, not a single message's line count, and the protected recent tail renders verbatim regardless. Append iteratively via a small `extend()` helper. Behavior is identical for normal-sized blocks. --- .../assistant-ui/markdown-text.test.ts | 9 ++++++++ apps/desktop/src/lib/markdown-preprocess.ts | 22 ++++++++++++++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/components/assistant-ui/markdown-text.test.ts b/apps/desktop/src/components/assistant-ui/markdown-text.test.ts index fad9944741f..b3ea416d066 100644 --- a/apps/desktop/src/components/assistant-ui/markdown-text.test.ts +++ b/apps/desktop/src/components/assistant-ui/markdown-text.test.ts @@ -201,4 +201,13 @@ describe('preprocessMarkdown', () => { expect(output).toContain('') }) + + it('handles a fenced block larger than V8 spread-argument limit', () => { + // A single huge code block (e.g. a logged minified bundle) used to throw + // `RangeError: Maximum call stack size exceeded` via `out.push(...lines)`. + const body = Array.from({ length: 200_000 }, (_, i) => `line ${i}`).join('\n') + const input = `\`\`\`js\n${body}\n\`\`\`` + + expect(() => preprocessMarkdown(input)).not.toThrow() + }) }) diff --git a/apps/desktop/src/lib/markdown-preprocess.ts b/apps/desktop/src/lib/markdown-preprocess.ts index aea5af1b82c..4fc61e48e00 100644 --- a/apps/desktop/src/lib/markdown-preprocess.ts +++ b/apps/desktop/src/lib/markdown-preprocess.ts @@ -151,12 +151,22 @@ function normalizeVisibleProse(text: string): string { .join('') } +// `out.push(...lines)` spreads every element as a separate call argument, so a +// single fenced block with tens of thousands of lines (a logged minified +// bundle, base64 blob, huge tool dump) overflows V8's argument-count limit and +// throws `RangeError: Maximum call stack size exceeded`. Append iteratively. +function extend(out: string[], lines: string[]) { + for (const line of lines) { + out.push(line) + } +} + function pushProseFence(out: string[], indent: string, info: string, lines: string[]) { if (info) { out.push(`${indent}${info}`.trimEnd()) } - out.push(...lines) + extend(out, lines) } function findClosingFence(lines: string[], start: number, marker: string): number { @@ -241,7 +251,7 @@ function normalizeFenceBlocks(text: string): string { } if (closeIndex !== -1 && isUrlOnlyBlock(bodyLines)) { - out.push(...bodyLines) + extend(out, bodyLines) index = closeIndex + 1 continue @@ -264,10 +274,10 @@ function normalizeFenceBlocks(text: string): string { // any literal `$$` characters in the body don't collide with // an outer math wrapper. No close emitted yet — streaming. out.push(`${indent}${marker}math`) - out.push(...bodyLines) + extend(out, bodyLines) } else { out.push(`${indent}${marker}${language}`) - out.push(...bodyLines) + extend(out, bodyLines) } break @@ -288,7 +298,7 @@ function normalizeFenceBlocks(text: string): string { // colliding with our wrapper. Without this rewrite the block // would render as a syntax-highlighted "latex" code listing. out.push(`${indent}${marker}math`) - out.push(...bodyLines) + extend(out, bodyLines) out.push(`${indent}${marker}`) index = closeIndex + 1 @@ -296,7 +306,7 @@ function normalizeFenceBlocks(text: string): string { } out.push(`${indent}${marker}${language}`) - out.push(...bodyLines) + extend(out, bodyLines) out.push(`${indent}${marker}`) index = closeIndex + 1 }