Studio: stop a failed markdown chunk from taking the whole app down by danielhanchen · Pull Request #9236 · unslothai/unsloth · GitHub
Skip to content

Studio: stop a failed markdown chunk from taking the whole app down - #9236

Merged
danielhanchen merged 11 commits into
mainfrom
studio-markdown-chunk-boundary
Aug 23, 2026
Merged

Studio: stop a failed markdown chunk from taking the whole app down#9236
danielhanchen merged 11 commits into
mainfrom
studio-markdown-chunk-boundary

Conversation

@danielhanchen

@danielhanchen danielhanchen commented Aug 18, 2026

Copy link
Copy Markdown
Member

Fixes #9235.

The bug

Streamdown renders both the syntax highlighted code body and the Mermaid diagram through React.lazy inside its own <Suspense>, and it fetches those chunks the first time a reply contains a fence or a diagram, on whatever network the app has at that moment. A rejected dynamic import rethrows during render, and there was no error boundary between the thread and the router, so the throw landed in TanStack Router's CatchBoundaryImpl.

The result was not a missing code block. The whole of Studio was replaced by the router's "Something went wrong!" page, the assistant-ui runtime was unmounted with it, and the reply's own stream was left with nothing consuming it, so the generator stopped being pulled and the message never completed. The part of the reply that had already rendered went too.

Before and after, same experiment on both trees

Streaming reasoning-pane fixture at 12,000 characters, chromium, aborting exactly one request:

page.route("**/highlighted-body*", lambda route: route.abort())
main with this change
what the page shows "Something went wrong!" the reply
total elements in the document 122 -> 21 intact
reasoning panes mounted 1 -> 0 1 throughout, collapsing normally at the end
readable text in the pane 11,968 -> 0 characters 11,968 of 12,000
stream delivered frozen at 720 of 12,000, forever 12,000 of 12,000, done: true
code fences none 5, as plain readable code, 0 coloured spans

The stream row is the one that matters. A boundary that saves the page while leaving the generator dead would pass a "does not crash" check and fail this one: the test here is that the reply CONTINUES ARRIVING past the fence and finishes.

It was also seen once with nothing injected, one run in twelve on firefox, same fingerprint, reported by the browser as TypeError: error loading dynamically imported module for the same chunk.

Note that pageerror never fires for this. React routes it to console.error and the router boundary swallows the throw, so a harness listening only for page errors records nothing.

What the fix does

MarkdownBlockBoundary wraps every markdown block. A block that cannot render degrades to its own source as readable text, with the fence scaffolding stripped and the language header kept, so a fence whose highlighter is missing is still the same characters in the same order. Deliberately not an error card and deliberately not blank: a reader who asked for a shell command needs the command.

Per block, so one fence losing its colours costs its own colours and nothing else; every other block in the same reply keeps rendering, and the runtime, thread and stream are all outside the boundary.

No retry, on purpose. React and the browser's module map both cache a rejected dynamic import (whatwg/html#6768), so re-importing rethrows without issuing a new request. On a streaming reply a boundary that resets from props would throw and catch on every frame and get nothing for it. SettingsPanelBoundary in studio/frontend/src/features/settings/settings-dialog.tsx independently reached the same conclusion for the same reason, and its comment already records the same class of failure: "Nothing above this root-mounted dialog catches, so unguarded that unmounts all of Studio rather than one panel."

Scope: one boundary covers the whole class

There are exactly two React.lazy imports in the reply render path, both from streamdown/dist/chunk-BO2N2NFS.js:

lazy(() => import('./highlighted-body-OFNGDK62.js'))   // every code fence
lazy(() => import('./mermaid-GHXKKRXX.js'))            // every mermaid diagram

Both are rendered inside StreamdownBlockContent, so the same boundary encloses both.

  • The highlighted code body path is verified LIVE, by aborting that request and measuring the table above.
  • The Mermaid path is verified STRUCTURALLY, not by abort: it is the same component under the same boundary. Stated plainly so nobody has to infer it.

Separately checked so that "one boundary is enough" is a bounded claim rather than a hope: @streamdown/math and @streamdown/mermaid contain no dynamic imports of their own, math is not lazy, and artifacts and images in a reply are not lazy. The app's other lazy imports are outside the reply, where settings panels already have SettingsPanelBoundary and the rest are route-level, where replacing the route is defensible.

Tests

Nine tests over the pure fallback plus the wiring. They do not assert "the app does not crash"; they assert that the reader still has the content: a degraded fence is the same characters, the same line count, the same leading whitespace (in python the indentation is the program), an unclosed fence still degrades because the failure happens mid-fence, and prose is handed back untouched rather than rendered as code.

16 assertions, every one of them individually killed by a mutation, 12 mutations run one at a time in a throwaway worktree.

Worth stating rather than smoothing: four of those mutations had to be ADDED after the first pass, because seven of the sixteen assertions were only dying behind an earlier assertion in the same test. A test stops at its first failure, so an assertion that never independently kills anything is decoration. That is a real result about the first draft of the test file, not a formality.

One existing test changed, and why it is not weakened

markdown-block-remount-on-complete.test.ts pinned the exact spelling memo(StreamdownBlockContent). memo is still the OUTERMOST wrapper here, so the property that test protects is unchanged: on a memo hit React skips the boundary and the block together, exactly as before. The pin was moved to the new exact spelling rather than loosened, and the reason is in the comment in the diff: a version with memo INSIDE the boundary would render one component per block per token, and no OUTPUT test could tell the two apart. That last clause is what justifies a spelling-pinned test existing at all.

CI

Four checks fail here that are not from this change: (Python 3.13), Frontend build + bundle sanity, Frontend unit tests (Windows) and Repo tests (CPU). They are main-side and fail identically across other open PRs; #9220 fixes them. queued-model-capabilities in the frontend suite also fails on pristine main and is covered by the same PR. Local gates on this branch: npm test 3,786 pass with that one pre-existing failure, npm run typecheck clean, npm run build clean, and eslint adds zero problems over the baseline for the touched files.

On the failure rate, so it is not misquoted

This was found by a performance comparison in which it occurred in 4 of 52 runs. That 8% is not a user-facing frequency: every one of those runs was against a vite dev server, the correlate is the test driver's own page.route("**/*") interception (4 in 52 with it, 0 in 24 across two probe designs without it, 1 in 12 when it was added back), a cold vite dependency optimiser was tested as an alternative explanation and ruled out at 0 in 12, and 40 of 40 production-build cells were clean across chromium at 1x and 6x, firefox and Playwright WebKit.

So the fetch failure itself is most likely a dev-server plus harness artefact. The consequence of a failed fetch is not, and the consequence is what this changes. A flaky network, a CDN 404 after a redeploy hitting every open tab at once, or Desktop shipping the same bundle under default-src 'self' with no network to fall back on all reach the same code path.

Two consequences of the fallback, stated here rather than found in production

Neither is a defect being hidden. Both are things a reviewer should be able to weigh without reading the diff twice.

The latch is positional, and a rewind can move it onto a neighbour. Streamdown keys blocks by index, `${useId}-${index}`, so the boundary instance at position N outlives whatever block happens to sit at position N. The boundary does not reset, deliberately, so a failure latches. If IncrementalMarkdownCache then rewinds and re-segments the document, which is what preprocessLaTeX closing a \(...\) or resolving a \[...\] makes it do, and the block count BEFORE N changes, the latched boundary renders a different block than the one that threw. That block shows as raw Markdown source rather than rendered Markdown. No content is lost, since a non-fence is handed back unchanged, and a renderGeneration bump remounts the subtree and re-attributes every latch correctly. It needs a chunk failure and a count-changing rewrite strictly behind it, so it is rare, and by then every fence in the reply is degraded anyway.

Designing around positional keys is not free: it is what closed PR 9073 after seven review rounds. Recording the behaviour is the cheaper trade here.

A degraded fence keeps its Copy and Download controls. It did not, at first, and the reason is worth recording because it is not where anyone would look. CodeBlockActions lives inside FenceBlock, so a boundary that wraps the whole block unmounts the copy and download bar along with the fence that threw, and a completed SVG fence loses its already-sanitized SvgPreview too. That is the wrong trade at the wrong moment: a reader whose block did not render is a reader about to copy it out and read it elsewhere.

So Block, the only thing in a block that loads code at render time, has a narrower boundary of its own inside the wrapper that carries the controls, which makes the controls its siblings rather than its descendants. That alone was not enough, and only a browser measurement showed it. getCodeFence needs the CLOSING fence, so a fence that is still arriving has none and falls past the fence branch to the bare Block at the end of StreamdownBlockContent. That is the render that first asks for the highlighter, which is exactly when it fails. With that route unguarded the whole-block boundary caught it and latched, so the completed fence never re-entered FenceBlock and the controls never mounted: on a streamed reply the narrower boundary was unreachable and the document was identical to the one without it, 1,350 elements and zero copy buttons either way. Mermaid escaped only by accident, because it shows a placeholder while incomplete and so defers its import until the block is complete.

Both routes are guarded now. Measured on production builds with the same abort, streamed at field cadence: 3 copy, 3 download and the SVG preview all survive, against 0, 0 and 0 before, the block count holds at 11 rather than dropping to 10, the copy button puts all 453 bytes of the fence on the clipboard and the download writes a 453-byte snippet.py. The console confirms which boundary fired: the renderer boundary 6 times, the whole-block boundary 0. Degraded, a fence ends as the same plain shell an unreached fence already shows, carrying the same data-streamdown attributes, and a Mermaid diagram degrades to its source with its copy button intact.

The healthy render is untouched by all of it, checked rather than assumed: with nothing aborted, this branch and main agree on all of 1,530 document elements, 11 blocks, 1,693 assistant characters, 112 highlight spans, 3 copy and 3 download buttons, 1 SVG preview, and the per-block heights and text lengths element by element.

This narrows what is REPLACED and never what is CAUGHT. The whole-block boundary stays exactly where it was, so a throw from a hook, a sanitizer or an artifact card still degrades the whole block rather than reaching the router. The two rungs compose: the inner fallback is an element built by the caller, so if it throws in turn it throws as the inner boundary's own output and the outer boundary catches it, landing on the behaviour that was already shipped.

Update: rebased onto current main, and two fallback parse fixes

The branch was from 18 August and no longer merged cleanly. It now carries current main. The only conflict was in markdown-text.tsx, where main's #9462 extracted the fence branch into a new FenceBlock and left const StreamdownBlock = memo(StreamdownBlockContent) alone, while this branch replaced exactly that line. The two changes are orthogonal and git only conflicted because they abut, so the resolution keeps FenceBlock in full and keeps the boundary-wrapped StreamdownBlock. It was resolved twice independently, in two worktrees, and both resolutions produced a byte-identical file.

The four red checks were inherited, not caused here, and the control is the main-branch run at this PR's own merge base: Backend CI 32141907885 and Frontend CI 32141907900 fail job for job with the same signatures, including (Python 3.11) passing alongside them. Main was broken by #9173 and fixed by #9189 and #9192 on 19 August, after this branch was cut.

Separately, two parse defects in the fallback itself, which matter because that path only ever runs once rendering has already failed:

  • A closing fence may carry MORE characters than the opening one (CommonMark 0.31.2, "at least as many"), which is how a model closes a fence whose body contains a fence. The back-reference demanded an exact match, so a four-backtick close was shown as if it were the last line of the code.
  • An empty fence closes on the line after it opens, and the closing run was being returned as the body, so ``` rendered inside the code block as though the model had written it.

Both are now scanned rather than matched, which also removes a quadratic backtrack: the old pattern put [^\r\n]* directly after `{3,}, and the two competed for the same backticks whenever an opening run had no line break after it. A 400,000 character run of backticks now parses in 2.1 ms.

Each fix has a regression test confirmed failing against the unfixed code first. npm test is 4,403 passing, up from 4,400 by exactly the three tests added, with npm run typecheck clean and eslint adding zero problems over main's own baseline for the touched files.

Streamdown loads the highlighted code body and the Mermaid renderer with React.lazy, and fetches them the first time a reply contains a fence or a diagram. A rejected import rethrows during render, and nothing between the thread and the router caught it, so one chunk that would not load handed the throw to TanStack Router's CatchBoundaryImpl: all of Studio was replaced with the router error page, the assistant-ui runtime went with it, and the reply's stream was left with nothing consuming it.

Measured by aborting exactly that one request on an otherwise unmodified tree: the document went from 122 elements to 21, the pane's readable text went from 11,968 characters to 0, and the stream stopped at 720 of 12,000 characters and never resumed. With the boundary the same abort leaves the page intact, the stream runs to 12,000 of 12,000, and the five fences render as plain readable code.

Per block, so a fence that cannot be highlighted costs its own colours and nothing else. No retry: React and the browser's module map both cache a rejected dynamic import (whatwg/html#6768), so retrying would throw on every frame of a streaming reply and issue no new request. SettingsPanelBoundary reaches the same conclusion for the same reason.
@danielhanchen

Copy link
Copy Markdown
Member Author

Confirmed main still renders the block bare at studio/frontend/src/components/assistant-ui/markdown-text.tsx:412, so a rejected streamdown lazy chunk still escapes to the router and takes the reply and its stream with it. Will get this reviewed.

@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 19, 2026
chatgpt-codex-connector[bot]

This comment was marked as resolved.

# Conflicts:
#	studio/frontend/src/components/assistant-ui/markdown-text.tsx
…lback

The fallback runs only once a block has already failed to render, so it is
the last thing between the reader and a lost block, and a misparse there
puts stray backticks in the only remaining view of the answer.

Two parse defects on legitimate input:

- A closing fence may carry MORE characters than the opening one (CommonMark
  0.31.2: "at least as many"), which is how a model closes a fence whose body
  contains a fence. The back-reference demanded the exact same run, so a
  four-backtick close was left on screen as if it were the last line of code.
- An empty fence closes on the line after it opens, and the closing run was
  returned as the body, so ``` rendered inside the code block as though the
  model had written it.

Both are now scanned rather than matched. The old pattern put `[^\r\n]*`
directly after `` `{3,} ``, and the two compete for the same backticks, so an
opening run with no line break after it backtracked quadratically; scanning
takes the run once and never reconsiders. A 400,000 character run of
backticks with no newline now parses in 2.1 ms.

Also drops a dead clause in "a block with content never degrades to nothing".
Its `|| content.trim() === "```\n```"` could never be evaluated, because the
unfixed code returned "```" for that input rather than the empty string the
clause assumed, and an assertion encoding a false belief is worse than none.
The empty fence now has a test of its own asserting what it really does.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

…ails

The whole-block boundary catches everything, which means that when it fires
it replaces everything: the copy and download bar, and a completed SVG
fence's already-sanitized preview, go with the fence that failed. That is a
bad trade at exactly the wrong moment. A reader whose block did not render
is a reader about to copy it out and read it somewhere else, and losing the
copy button is close to the opposite of what degrading gracefully means.
The SVG preview is the same argument: it was sanitized before the failure
and was safe to show, and it disappeared for a reason unrelated to it.

`Block` is the only thing in a block that loads code at render time, so it
is the only thing that needs a boundary of its own. It gets one, inside the
wrapper that carries the controls, so the controls are its SIBLINGS rather
than its descendants.

Narrowing what is REPLACED, never what is CAUGHT. The whole-block boundary
stays exactly where it was, so a throw from a hook, a sanitizer or an
artifact card still degrades the whole block instead of reaching the router.
The two rungs compose: the inner fallback is an element built by the caller,
so if it throws in turn it throws as the inner boundary's own output, which
the outer one catches, landing on the behaviour already shipped.

Degraded, a fence is now the same plain shell an unreached fence already
shows, carrying the same data-streamdown attributes, so it looks like a
fence that has not been highlighted rather than like a broken block. A
Mermaid diagram degrades to its source as readable code.

The regression test asserts the RELATIONSHIP rather than the absence: a
control must be a sibling of a boundaried Block, its ancestors being the
Block's ancestors with the boundary removed. Asserting only "the control is
not inside a boundary" passes on a tree with no boundary at all, which is
the tree the test exists to reject. Confirmed failing against the previous
commit before landing.
The previous commit put a narrower boundary around the two renderers that
have controls beside them, and browser measurement then showed it never
fired for a code fence on a streamed reply. Head and the commit before it
produced identical documents under the abort this PR is built around: 1,350
elements, 10 blocks, 0 copy buttons, 0 download buttons, 0 SVG previews.

`getCodeFence` needs the CLOSING fence. A fence that is still arriving has
none, so it falls past the fence branch to the bare `Block` at the end of
`StreamdownBlockContent`, and that is the render that first asks for the
highlighter. Unguarded, the whole-block boundary caught it and latched with
no reset, so when the closing fence landed the block never re-entered
`FenceBlock` and the controls never mounted. `FenceBlock` and its boundary
were unreachable on the path that matters. Mermaid escaped only by accident:
it shows a placeholder while incomplete, so its import waits for a complete
block, by which time the boundary is in the tree.

So the bare `Block` is guarded too, and the fallback presentation is now one
component shared by both boundaries rather than two spellings of it.

The test that was missing is the one that would have caught this: EVERY
`Block` render site must sit inside the renderer boundary, not just the ones
with controls beside them. It fails against the previous commit. Checking
only the sites that have siblings is what let an unguarded route ship, since
that route has no siblings to notice the absence of.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

…lid backtick info string

An opening line the reply ends on, such as ```python with no line break,
is an unclosed empty fenced block to the parser Streamdown uses, so the
highlighter chunk is requested for it and this fallback is reachable. It
was read as prose, which put the delimiter and the language tag on screen
as literal text.

A backtick fence's info string may not contain a backtick (CommonMark
0.31.2), so ```py`bad opens nothing. The scanner accepted it anyway and
degraded the block to its middle line, dropping the model's opening line
and the closing delimiter with it. CODE_FENCE_RE already spells the same
rule.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

CommonMark 0.31.2 removes up to N spaces from each content line when the
opening fence is indented N spaces, and the parser Streamdown uses agrees:
three-space indented ```python with a three-space indented body is
code(value="x = 1"). The scanner already skipped the indent to find the
opener but left it on every body line, so the degraded block showed text
the model never wrote and pasting it into a file raised IndentationError.

Up to N, not N: a deeper line keeps the remainder, which in Python is the
program. extractHtmlFences already applies the same rule.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b774d7bc65

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +97 to +100
for (let n = 0; n < lines.length; n += 1) {
let take = 0;
while (take < indent && lines[n][take] === " ") take += 1;
lines[n] = lines[n].slice(take);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle tabs when removing fence indentation

When an indented fence has a content line beginning with a tab, this loop removes no indentation because it recognizes only literal spaces. Fresh evidence after the space-only indentation fix is that the repository's Markdown parser reads ```\n\tx\n ``` as code containing three spaces followed by x, while this fallback returns the original tab; copying the degraded block can therefore change indentation width or introduce mixed-indentation errors.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not taking this one. The divergence needs an INDENTED opener, and on every fence we actually see there is none.

With no indent the two agree exactly, which I checked rather than assumed: mdast-util-from-markdown reads a bare fence whose body line starts with a tab as value "\tx", tab preserved, and stripIndent returns early at indent 0 and hands back the same "\tx". So the case you describe only arises for a fence indented one to three spaces that also has a tab-led content line.

Two reasons that stays as it is. The repository already made this call for the same construct: extractHtmlFences builds its dedent as ^ {0,N}, spaces only, so handling tabs here would diverge from the helper the previous comment correctly pointed me at. And over the frozen corpus quoted in markdown-text.tsx, 2,467,069 characters and 1,456 fence delimiters, not one is indented. Doing tabs properly means column-wise tab-stop expansion rather than character removal, and a half-implementation of that is a new wrong answer on a path nothing reaches.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-checked this at head 5d15d07, because the reasoning above was measured three commits back and I did not want to stand on a stale number.

The mechanism holds and so does the boundary. Driving mdast-util-from-markdown against markdownBlockFallback at head: a bare fence with a tab-led body gives "\tx" from both, an identical answer. A one-space opener gives " x" from the parser against "\tx" from the fallback, and a two-space opener gives " x" against "\tx". That is CommonMark 0.31.2 section 2.2, tabs behaving as a tab stop of 4 where spaces define block structure, feeding section 4.5, which removes up to N spaces. So the divergence needs the indented opener, which is what I said.

One number in my earlier reply was narrower than I made it sound, so here is the wider one. Those 1,456 unindented delimiters were the frozen corpus quoted in markdown-text.tsx, 2.47M characters. I have now swept 1.5 billion characters of real assistant replies, 339,293 texts, 29,539 fenced blocks. Indented openers are not unheard of there: 903 of 29,539, about 3 percent. The pair this actually needs is 9 of 29,539, about 0.03 percent, and every one of those nine is a fence the model closed inline rather than on its own line, so per section 4.5 it runs to end of document and swallows a tab-led line that was never code to begin with.

Verdict unchanged. It is a cosmetic indentation difference on the degraded path, no text is lost, and doing tabs properly means column-wise tab-stop expansion rather than character removal, which extractHtmlFences does not do either. Half of that, in one of the two places, is a new wrong answer for 0.03 percent of blocks that are already malformed.

…ence

Streamdown 2.5's parseMarkdownIntoBlocks returns an entire reply as a
single block once it contains a footnote. Measured on the installed
version: the same reply splits into 5 blocks without one and into 1 with
one. A fence at the top of such a block is followed by its own close and
then by prose, and reading all of that as the fence's body put the closing
delimiter, the prose and the footnote definition on screen as if the model
had written them as Python.

The close is now looked for from the top, and where it lands decides. On
the last line the fence is the block. Earlier, the block continues past it
and belongs to the caller unchanged. Never, the fence is still streaming
and all of it is content, which is the case the reader needs most.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: bee92109fb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…ery line

Scanning from the top is what makes a block that continues past its fence
detectable, but doing it line by line cost about 50ns per LINE, and the
fallback re-runs on every render of a failed block. Measured against the
commit before it: an ordinary closed 400KB block went from 0.0006ms to
0.39ms, the same block at 40,000 lines to 1.83ms, and a still streaming
fence, the case the reader needs most, to 0.38ms. At 40,000 lines that was
also 3.7x slower than the regex this file replaced, which defeats the
point of replacing it.

A close carries at least three of the opening character, so indexOf on the
delimiter goes straight to the only lines worth reading; an ordinary code
block holds exactly one. The per-line slice and the per-line carriage
return regex, together two thirds of the cost, are gone with it: bounds go
to closesFenceAt instead of a line string.

Behaviour is unchanged, checked over 21,780 structured inputs against the
previous implementation with no differences. Same shapes as above, now
0.0051ms, 0.0104ms and 0.0047ms.
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 23, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 23, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 23, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 23, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 23, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 23, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: 14f6f40419

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

The scan that replaced the per-line walk accepted a carriage return anywhere
in a closing fence's trailing whitespace, where the line based code stripped
one CR from the END of the line and then allowed only spaces and tabs. That
closed fences the previous code did not: over 1,500,000 generated inputs the
two disagree on 200, every one of them carrying a bare CR.

Unreachable through the app, since parseMarkdownIntoBlocks normalises every
CR out of a block before the fallback sees it, and no rendered cell moved.
It still made a commit that claimed to be behaviour neutral not be, so the
tail now allows one CR only as the line's last character. Same 1,500,000
inputs, zero differences.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 9abb321eb9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

14f6f40 rewrote the closing-fence scan to find the delimiter with indexOf
instead of walking every line. That is 150x to 370x faster and it is worth
having, but it also changed the carriage return rule: a CR was accepted
ANYWHERE in the closing line's tail instead of only as that line's last
character. A differential run over 2,550,265 inputs puts the divergence at
393,620 of them.

9abb321 restored the original rule and the same differential now scores 0
against the commit before the rewrite. Neither commit added a test, so the
suite would pass again tomorrow if the rule broke again.

Verified by reverting the file to 14f6f40: this test fails there and passes
on the current head.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

@danielhanchen
danielhanchen merged commit d3acce8 into main Aug 23, 2026
36 checks passed
@danielhanchen
danielhanchen deleted the studio-markdown-chunk-boundary branch August 23, 2026 04:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Studio: one failed markdown chunk takes down the whole app instead of degrading to plain text

1 participant