Skip to content

fix(desktop): stop a single message from crashing or freezing the chat - #47664

Merged
OutThisLife merged 3 commits into
mainfrom
bb/desktop-markdown-spread-overflow
Jun 17, 2026
Merged

fix(desktop): stop a single message from crashing or freezing the chat#47664
OutThisLife merged 3 commits into
mainfrom
bb/desktop-markdown-spread-overflow

Conversation

@OutThisLife

@OutThisLife OutThisLife commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

A single message could either crash the desktop chat (RangeError: Maximum call stack size exceeded, hitting the root error boundary so the whole app died) or freeze it hard (uninterruptible long task, no error, requires killing the process). Both are triggered by routine large content in long sessions — backgrounded terminal dumps, cat of a minified bundle, base64 blobs, newline-heavy JSON. Auto-compression doesn't prevent it: it gates on tokens vs. window, not a single message's line count, and the protected recent tail renders verbatim.

Fixed in layers, cheapest first:

Crash fixes

  • Spread overflow (markdown-preprocess.ts): out.push(...lines) passed every line as a call argument; a block over V8's argument-count limit threw. Replaced with an iterative extend() helper — identical output, no limit.
  • Render-time throws escape to root (markdown-text.tsx, directive-text.tsx): Streamdown runs our preprocess inside its own useMemo and the user bubble runs extractEmbeddedImages/directive parsing in theirs; a throw there bubbles to the root boundary. Wrapped both in try/catch that degrade to raw text — one bad message renders plain instead of nuking the transcript.

Freeze fix

  • Message gate (markdown-text.tsx): past 200KB, bypass the markdown pipeline entirely (no preprocess regex, no marked, no Shiki) and render raw text in content-visibility:auto line-chunks. Synchronous work is bounded to a string split; the browser virtualizes layout natively; all content stays in the DOM.
  • Code-block highlight budget (shiki-highlighter.tsx): past 3k lines / 150KB, skip Shiki (a span per token) and render plain, chunked the same way.

UX

  • Collapse/expand (expandable-block.tsx): reusable ExpandableBlock clamps code blocks and the huge-text fallback to a 120px preview with a bottom gradient + chevron, expanding to 300px. Inner is always a scroll container, so the content-visibility chunks stay lazily laid out in both states. Copy button (card header) always yields the full block.

Not JS thread-virtualization — no scroll math / windowing library. A size gate bounds processing; CSS content-visibility lets the engine virtualize layout.

Test plan

  • markdown-preprocess + markdown-text suites green (200k-line block doesn't throw)
  • shiki-highlighter budget + chunking unit tests
  • tsc --noEmit + ESLint clean
  • Manual: realistic multi-turn session exercises normal markdown, highlighted code, the >3k-line chunked path, and the >200KB fallback — loads smoothly; a 250k-line dump that previously froze the renderer now loads

`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.
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: bb/desktop-markdown-spread-overflow vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 10993 on HEAD, 10993 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 5791 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@alt-glitch alt-glitch added type/bug Something isn't working comp/tui Terminal UI (ui-tui/ + tui_gateway/) P3 Low — cosmetic, nice to have labels Jun 17, 2026
Streamdown runs our `preprocess` inside its own useMemo, and the user
bubble runs `extractEmbeddedImages`/directive parsing inside theirs — so
anything thrown while rendering one message (a regex/stack overflow on
adversarial content) escapes to the ROOT error boundary and takes down
the entire app, as seen in a reported `RangeError: Maximum call stack
size exceeded` from a single message.

Wrap both the assistant preprocess pipeline and the user-message
directive passes in try/catch that degrade to the raw text. One bad
message now renders plain instead of nuking the transcript.
@OutThisLife OutThisLife changed the title fix(desktop): avoid stack overflow rendering huge fenced blocks fix(desktop): stop a single message from crashing the chat view Jun 17, 2026
@OutThisLife OutThisLife changed the title fix(desktop): stop a single message from crashing the chat view fix(desktop): stop a single message from crashing or freezing the chat Jun 17, 2026
A multi-MB message (logged bundle, huge tool dump) froze the renderer
before any paint: Streamdown runs `preprocess` + `marked` lex over the
whole string synchronously in a useMemo, an uninterruptible long task
that no try/catch or content-visibility can help (our JS runs before the
browser ever skips layout). Tiered fix:

- Message gate: past 200KB, bypass markdown entirely and render the raw
  text in `content-visibility:auto` line-chunks — synchronous work is
  bounded to a string split, the browser virtualizes layout natively,
  and every line stays in the DOM (selectable, find-in-page).
- Code-block budget: past 3k lines / 150KB, skip Shiki (which emits a
  span per token) and render plain, chunked the same way.
- Collapse/expand: a reusable ExpandableBlock clamps code blocks and the
  huge-text fallback to a 120px preview with a gradient + chevron,
  expanding to 300px. The inner element is always a scroll container so
  the content-visibility chunks stay lazily laid out in both states.

No content is ever dropped; the copy button (card header) always yields
the full block.
@OutThisLife
OutThisLife force-pushed the bb/desktop-markdown-spread-overflow branch from c09fb0b to 0138282 Compare June 17, 2026 13:25
@teknium1

Copy link
Copy Markdown
Contributor

Reviewed against current main. Verified the claims hold, not just the diff.

Crash class fully fixed. All 6 out.push(...lines) / out.push(...bodyLines) spread sites in markdown-preprocess.ts (lines 159, 244, 267, 270, 291, 299 on main) are rerouted through the iterative extend() helper — so this closes the whole RangeError: Maximum call stack size exceeded class, not just the one site the reporter hit. Output is byte-identical to the spread. Good.

Render-throw containment is correct. preprocessWithTailRepair (Streamdown's own useMemo) and the directive path (safeEmbeddedImages / safeDirectiveSegments) are exactly the two places a throw escapes to the root boundary, and both now degrade to raw text instead of nuking the transcript. [...hermesDirectiveFormatter.parse(text)] is array-literal spread (not function-arg spread), so it can't reintroduce the overflow it's wrapping, and the resulting mutable array matches the downstream segments type.

Hooks ordering is safe. The text.length > MAX_MARKDOWN_CHARS early return in MarkdownTextSurface sits after all the useMemo calls, so Rules of Hooks aren't violated. useMessagePartText() already exposes text (used at lines 386/421), so pulling it at the surface is fine.

Chunking is lossless and the budget check is cheap. chunkByLines reconstructs via join('\n') (unit-tested both ways), and exceedsHighlightBudget short-circuits on the char cap before walking newlines with indexOf rather than allocating a split. content-visibility:auto + containIntrinsicSize keeps all content in the DOM while letting the engine virtualize layout — the right call over a windowing lib for a fallback path.

Minor / non-blocking:

  • code-card.tsx drops p-1.5 from CodeCardBody. In shiki-highlighter the <ExpandableBlock> wrapper is rendered without className="p-2" (only HugeTextFallback passes padding), so the card body loses its inner pad — but the retained [&_pre]:px-2 [&_pre]:py-1.5 selectors cover it, so visually it should be unchanged. Worth a glance in the running app to confirm there's no tightened gutter on highlighted blocks.
  • ExpandableBlock's overflow threshold is a magic 121 (px) that has to stay in sync with the max-h-[7.5rem] (120px) clamp. A shared const would prevent drift if the clamp height ever changes.

apps/desktop-only, owner-authored, real reported symptom, sibling sites covered, unit tests added for both the overflow repro and the chunking invariant. LGTM. (Couldn't run the vitest suite here — no node_modules in this worktree — so the "suites green" / tsc / ESLint claims are taken on the author's word; CI will confirm.)

@OutThisLife
OutThisLife merged commit f10f711 into main Jun 17, 2026
35 checks passed
@OutThisLife
OutThisLife deleted the bb/desktop-markdown-spread-overflow branch June 17, 2026 13:37
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…rkdown-spread-overflow

fix(desktop): stop a single message from crashing or freezing the chat
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
…rkdown-spread-overflow

fix(desktop): stop a single message from crashing or freezing the chat
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
…rkdown-spread-overflow

fix(desktop): stop a single message from crashing or freezing the chat
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
…rkdown-spread-overflow

fix(desktop): stop a single message from crashing or freezing the chat
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
…rkdown-spread-overflow

fix(desktop): stop a single message from crashing or freezing the chat
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tui Terminal UI (ui-tui/ + tui_gateway/) P3 Low — cosmetic, nice to have type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants