Skip to content

🤖 Chunked streaming rows: bound oversized blocks without a transition, narrow document-scoped detection #5664

Open

Description

@ThomasK33

Follow-up to #5663 (#5647), from the automatic code review on its second round. Both cases fall back to main's existing behavior, so neither is a regression. #5663 deferred them to stay inside its review budget.

  1. Oversized single block. A chunk above STATIC_STREAMING_MOUNT_MAX_CHARS (20k) cannot be split. Examples: a huge code fence, or a reply with footnotes, which parseMarkdownIntoBlocks returns whole. Such a chunk keeps MarkdownCore's deferred streaming render, the same as main, because one synchronous render of it would break the 50 ms task rule. While the stream is live, the sync-lane store bumps (perf: streaming store notifications (~80/s, sync lane) starve every React transition while a stream is live #5655) can starve that one chunk, so it stays blank until completion. The other chunks still show. Possible fix: split a long fence into line ranges and render them synchronously, or fix perf: streaming store notifications (~80/s, sync lane) starve every React transition while a stream is live #5655.
  2. Document-scoped detection is broad. hasDocumentScopedMarkdown also matches [^...] inside code (for example a regex character class). At completion such a reply then does one whole static render, the same render and cost as main at completion, instead of staying chunked. Possible fix: ignore fenced and inline code, or constrain footnote labels.

Trigger: the next change to ChunkedStreamingMarkdown, or the fix for #5655.


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high • Cost: $21.66

Activity

  1. self-assigned this
    on Oct 5, 2026
  2. ThomasK33 commented on Oct 5, 2026

    @ThomasK33
    MemberAuthor

    Two more items from the final independent check on #5663 (head 987acf5), both limited to rows above 20k chars that remount mid-stream:

    1. A sealed chunk is not always final for loose lists. parseMarkdownIntoBlocks returns the blank lines between blocks as their own block ("- a\n\n " → ["- a", "\n\n "]), so a list item can be sealed before its continuation arrives. Example: intro\n\n- item one\n\n continued item text\n- item two ends as the chunks "- item one", " continued item text\n" and "- item two", while one render gives one list. The continuation renders outside the list until the row remounts. No text is lost. Proposed fix: seal a group only when the block after it is more than blank lines and the group ends with a newline. Add a loose-list case to the equivalence test.
    2. The oversized-chunk comment overstates the bound. MarkdownCore uses streaming mode only when parseIncompleteMarkdown && !renderSynchronously. Sealed chunks, and all chunks after completion, have parseIncompleteMarkdown=false, so a block above 20k renders statically and synchronously once later text seals it. On main the same row pays a larger synchronous render at completion, so the task moves earlier. Fix the code comment at ChunkedStreamingMarkdown.tsx (the renderSynchronously line), and either bound this case or test the mode that actually runs instead of the mocked data-sync prop.

    Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high • Cost: $21.66

  3. ThomasK33 commented on Oct 5, 2026

    @ThomasK33
    MemberAuthor

    Perf owner decision: hold until the #5655 fix lands, then re-measure, then dispatch. The perf owner decides after the re-measurement.

    Reason: #5655's root cause is remend running over the whole reply on every frame (quadratic, 47 ms per call at 60k). Its fix repairs only the trailing block in MarkdownCore, which the chunked rows from #5663 also use. That changes this issue's first item, oversized-block starvation while live, directly. A fix designed now would target costs that are about to move.

    After the #5655 PR merges, re-run the #5663 dense benchmarks plus an oversized single block (one 20k+ code fence, one 20k+ table) on main. Dispatch only the items that still fail the #5647 targets. The doc-scoped regex fallback and loose-list sealing are correctness items, not perf. They can go to the coordinator at that point.


    Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

  4. ThomasK33 commented on Oct 5, 2026

    @ThomasK33
    MemberAuthor

    Measurement on main 31616698f2 (after #5667), 5 runs per case, loopback fixture at 900 chars/s, production renderer:

    Next: the loose-list fix lands first. The single-block design (#5666 + #5664 item 1) follows in a separate PR, because it touches the same files.


    Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

  5. ThomasK33 commented on Oct 5, 2026

    @ThomasK33
    MemberAuthor

    The loose-list split is fixed in #5668 (d91c3079f3). The fix had two causes and two parts. Both regression tests fail on main and pass after the fix:

    1. Chunker: a tail made only of blank lines or bare digits (a list marker still arriving) no longer seals the list chunk before it.
    2. Stale render: while streaming, the open chunk now remounts when it is sealed and when the stream ends. Streamdown kept the tight-list render of the first item, which caused the missing <p>. Sealed chunks keep their keys and their memo.

    Fixture after the fix (2 runs): 1 <ol>, 319 of 319 items with <p>, and output identical to a single render.

    Still open on this issue:

    • Item 1 (switch-back for a single huge block), together with perf: a live reply that is one huge markdown block (list, paragraph, table) still stops growing while streaming #5666. This is the next lane.
    • Item 2 ([^…] inside code triggers a whole render): not fixed. A correct fix needs code-span and escape parsing. A wrong strip would hide a real footnote, which is worse than one extra render.
    • New, minor: the remount at seal and at completion drops DOM state inside that one chunk, such as a text selection or a code block's "copied" state. While streaming, the open chunk can also show the tight-list render for a moment. For a final block close to 20k chars, the remount at completion can take more than 50 ms.

    Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions