@@ -201,245 +201,271 @@ func trimGlamourMargins(s string) string {
201201// we have the slightest doubt we return -1 and let the caller fall
202202// back to a full render.
203203//
204−// Decision tree, in order of preference (latest boundary wins):
204+// The implementation is a single O(n) forward scan
205+// ([scanSafeBoundaries]) that tracks block state across the whole
206+// document: fenced-code toggle, list open/close depth, HTML-block
207+// opener, and link-reference-definition presence. State is updated
208+// per line, so every blank-line candidate is evaluated against
209+// already-computed state in O(1) — no re-scanning of the prefix.
205210//
206−// 1. Walk backward through every "blank line" position p such that
207−// content[:p] ends with "\n\n" (or "\n[ \t]*\n").
208−// 2. For each candidate, check that content[:p] has an even
209−// number of triple-backtick fence lines (no open fenced
210−// block). Any odd count means we'd be cutting inside a fence
211−// and mis-syntax-highlighting the trailing partial.
212−// 2b. Reject if any line in content[:p] (outside fenced blocks)
213−// is a list-marker line, an HTML-block opener, or a link
214−// reference definition. See [prefixHasOpenHazard] for the
215−// reasoning behind these "anywhere in prefix" rejects.
216−// 3. Reject if the last non-blank line of content[:p] is:
217−// - a list item marker line ("^\s*([-*+]|\d+\.)\s")
218−// - a table line (contains "|")
219−// - a block quote ("^\s*>")
220−// - a setext header underline ("^=+\s*$" or "^-+\s*$")
221−// - an indented code line (4+ leading spaces or a tab)
222−// 4. Reject if the line immediately AFTER the boundary (skipping
223−// leading blank lines) looks like a setext underline (a line
224−// of '=' or '-' only). Rendering the prefix as a paragraph
225−// would change once the underline arrived; that's exactly the
226−// "splitting changes the prefix render" hazard §4.4 calls out.
211+// Closure-aware list tracking (B1) replaces the prior "any list
212+// marker anywhere → reject" rule. A list is considered open from
213+// the first marker line until a blank line is followed by a
214+// non-continuation, non-marker line at the same or shallower
215+// indent. Once closed, prefixes ending after the close are
216+// accepted, so the incremental cache resumes working for the
217+// paragraphs that follow a list — the common case in LLM reasoning
218+// traces. On any ambiguity the list is kept open (conservative —
219+// falls back to -1).
220+//
221+// B2 (HTML block) and B3 (link reference definition) remain
222+// anywhere-in-prefix rejects: the typical assistant output
223+// contains neither, so the perf cost is zero in the common case.
227224//
228225// Returns the byte offset of the first character AFTER the blank
229226// line, i.e. the start of the trailing segment.
230227func findSafeMarkdownBoundary(content string) int {
231− if len(content) == 0 {
228+ boundaries := scanSafeBoundaries(content)
229+ if len(boundaries) == 0 {
232230 return -1
233231 }
234−
235− // Iterate every blank-line position from latest to earliest.
236− for p := blankLineBefore(content, len(content)); p > 0; p = blankLineBefore(content, p-1) {
237− if !isSafeBoundaryAt(content, p) {
238− continue
239− }
240− return p
241− }
242− return -1
232+ return boundaries[len(boundaries)-1]
243233}
244234
245−// blankLineBefore returns the byte offset of the first character
246−// AFTER the latest blank-line separator that ends strictly before
247−// `until`. A blank-line separator is a sequence "\n([ \t]*\n)+"
248−// — one newline, then one or more lines containing only spaces or
249−// tabs and terminated by another newline. The returned offset is
250−// the start of the first non-blank line that follows the
251−// separator (or the position immediately after the final newline,
252−// if no further content remains).
235+// scanSafeBoundaries walks content once, O(n), tracking block
236+// state, and returns the byte offsets of every safe stable-prefix
237+// boundary in increasing order. A boundary is the byte offset
238+// immediately after a blank-line separator at which content[:p]
239+// can be rendered independently of content[p:].
253240//
254−// Returns -1 when no blank-line separator exists before `until`.
255−func blankLineBefore(content string, until int) int {
256− if until <= 0 {
257− return -1
258− }
259− // Walk backward looking for a newline followed (after optional
260− // blank-line content) by another newline. We track the latest
261− // newline we've seen; if the next earlier newline has only
262− // blank chars between them, we have a blank-line separator
263− // and the boundary sits immediately after the latest newline.
264− end := until
265− for end > 0 {
266− nl := strings.LastIndexByte(content[:end], '\n')
267− if nl < 0 {
268− return -1
269− }
270− // Look for an earlier newline whose gap to nl is empty
271− // or whitespace only.
272− prev := strings.LastIndexByte(content[:nl], '\n')
273− for prev >= 0 {
274− gap := content[prev+1 : nl]
275− if isBlankOrSpaces(gap) {
276− return nl + 1
277− }
278− // Gap had non-whitespace; nl is not a blank-line
279− // separator. Move up: try with the earlier newline as
280− // the new "nl" candidate.
281− break
282− }
283− end = nl
284− }
285− return -1
286−}
287−
288−// isBlankOrSpaces reports whether s consists entirely of spaces
289−// and tabs (or is empty).
290−func isBlankOrSpaces(s string) bool {
291− for i := range len(s) {
292− if s[i] != ' ' && s[i] != '\t' {
293− return false
294− }
295− }
296− return true
297−}
298−
299−// isSafeBoundaryAt reports whether content[:p] is a safe stable
300−// prefix. p must be a blank-line boundary (start of a line, with a
301−// blank line immediately preceding).
241+// State carried across the scan:
302242//
303−// Beyond the last-line checks, three "anywhere in the prefix"
304−// hazards force a reject because they cannot be reliably reasoned
305−// about by inspecting the trailing line alone. For each of these
306−// the simplest, safest rule was chosen — see prefixHasOpenHazard.
307−func isSafeBoundaryAt(content string, p int) bool {
308− prefix := content[:p]
309−
310− // (2) Even number of triple-backtick fence lines.
311− if countFenceLines(prefix)%2 != 0 {
312− return false
313− }
314−
315− // (2b) Anywhere-in-prefix hazards: open list (B1), HTML block
316− // opener (B2), reference link definition (B3). Any of these
317− // anywhere in the prefix forces a fallback.
318− if prefixHasOpenHazard(prefix) {
319− return false
243+// - inFence: toggled by each fence line. While true, no boundary
244+// is safe (we're inside a fenced code block) and list/html/ref
245+// detection is skipped so code-block contents don't false-
246+// trigger the hazards.
247+// - listDepth: current open-list nesting. Incremented on a
248+// marker line, decremented on closure (see below). A boundary
249+// is only safe when listDepth == 0 (no list open).
250+// - sawHTMLBlock / sawLinkRef: anywhere-in-prefix flags for B2
251+// and B3. Once set they never clear — the prefix is
252+// permanently unsafe for splitting.
253+// - prevNonBlank: the last non-blank line seen before the
254+// current candidate, for the last-line check
255+// ([lineOpensConstruct]).
256+//
257+// Each blank-line candidate is evaluated in O(1) against the
258+// already-computed state, so the full scan is O(lines) regardless
259+// of how many candidates exist. This replaces the prior O(n²)
260+// backward search that re-scanned the prefix for every candidate.
261+//
262+// List closure detection (B1 fix): a list opens on any marker line
263+// when no list is open. A list closes when, after a blank line, a
264+// non-marker line appears at indent < listBaseIndent+2 (not a
265+// continuation paragraph). On any ambiguity the list is kept open
266+// (conservative — falls back to -1). This replaces the prior "any
267+// list marker anywhere → reject" rule, so boundaries after a
268+// closed list followed by paragraphs are accepted — the common
269+// case in LLM reasoning traces.
270+//
271+// Crucially, boundary evaluation happens BEFORE the state update
272+// for the current line: the boundary at the start of line L has
273+// prefix = content[:L], so the state must reflect everything
274+// BEFORE L, not including L's effects. For example, the boundary
275+// at the start of a fence-opening line is safe (the fence hasn't
276+// opened in the prefix yet); the boundary at the start of a list-
277+// marker line is safe (the list hasn't opened in the prefix yet).
278+func scanSafeBoundaries(content string) []int {
279+ if len(content) == 0 {
280+ return nil
320281 }
321282
322− // (3) Inspect the last non-blank line of the prefix.
323− lastLine := lastNonBlankLine(prefix)
324− if lastLine != "" && lineOpensConstruct(lastLine) {
325− return false
326− }
283+ var boundaries []int
284+ var prevNonBlank string
285+ inFence := false
286+ listDepth := 0
287+ sawHTMLBlock := false
288+ sawLinkRef := false
289+
290+ // listBaseIndent is the indent column of the innermost open
291+ // list's markers. Used for closure detection: a non-marker
292+ // line at indent < listBaseIndent+2 after a blank line closes
293+ // the list.
294+ listBaseIndent := 0
295+
296+ // blankPending tracks whether the line immediately before the
297+ // current line was blank. A boundary candidate exists at the
298+ // start of the first non-blank line after a blank line.
299+ blankPending := false
300+
301+ // processLine handles one line (without its newline
302+ // terminator) starting at byte offset off. It evaluates any
303+ // pending boundary candidate, then updates block state for
304+ // the line.
305+ //
306+ // Boundary evaluation happens BEFORE the state update for the
307+ // current line: the boundary at the start of line L has
308+ // prefix = content[:off], so the state must reflect everything
309+ // BEFORE L. However, when an open list CLOSES at line L (L is
310+ // a non-marker, dedented line after a blank line), the prefix
311+ // contains a COMPLETE list — the boundary is safe even though
312+ // listDepth is still > 0 in the pre-L state. We compute
313+ // listClosesHere before evaluation and pass it as an override.
314+ processLine := func(line string, off int) {
315+ blank := strings.TrimRight(line, " \t") == ""
316+
317+ if blank {
318+ blankPending = true
319+ return
320+ }
327321
328− // (4) If anything follows, make sure it doesn't look like a
329− // setext underline that would retroactively turn the last
330− // paragraph of the prefix into a header.
331− if rest := content[p:]; rest != "" {
332− first := firstNonBlankLine(rest)
333− if isSetextUnderlineCandidate(first) {
334− return false
322+ trimmed := strings.TrimLeft(line, " \t")
323+ indent := len(line) - len(trimmed)
324+ isMarker := isListItemMarker(trimmed)
325+
326+ // Compute whether this line closes the open list.
327+ // A list closes when a blank line is followed by a
328+ // non-marker line at indent < listBaseIndent+2 (not
329+ // a continuation paragraph).
330+ listClosesHere := false
331+ if listDepth > 0 && blankPending && !isMarker && indent < listBaseIndent+2 {
332+ listClosesHere = true
335333 }
336− }
337334
338− return true
339−}
335+ // Non-blank line. If a blank line preceded, this is a
336+ // boundary candidate at offset `off`. Evaluate BEFORE
337+ // updating state — the prefix is content[:off].
338+ if blankPending {
339+ if isSafeBoundaryState(inFence, listDepth, listClosesHere, sawHTMLBlock, sawLinkRef, prevNonBlank, trimmed) {
340+ boundaries = append(boundaries, off)
341+ }
342+ blankPending = false
343+ }
340344
341−// prefixHasOpenHazard reports whether prefix contains any of three
342−// constructs that cannot be safely cut at a blank-line boundary
343−// even when the immediately preceding line looks fine. Each check
344−// uses the SIMPLEST viable conservative rule per the F8 round-2
345−// review:
346−//
347−// B1 (loose lists). A loose list has a blank line between an item
348−// and a continuation paragraph that begins with indentation
349−// but no list marker. If a candidate boundary lands on that
350−// blank line, the prefix's trailing non-blank line is the
351−// continuation paragraph, NOT a list marker, so the last-line
352−// check would accept it even though the list is still open.
353−//
354−// Rule chosen: any list-marker line ANYWHERE in the prefix
355−// forces -1. This is overly conservative — it forfeits
356−// boundary advancement past a closed list — but it eliminates
357−// the entire bug class with zero parsing of CommonMark's
358−// loose-list closure semantics. We retain the most useful
359−// boundary in practice: the one BEFORE the list opens (no
360−// marker has appeared in the prefix yet).
361−//
362−// B2 (HTML blocks). CommonMark defines seven HTML-block opener
363−// patterns (script/pre/style/textarea, comments, processing
364−// instructions, CDATA, declarations, recognised tag names).
365−// If the prefix opens an HTML block that the suffix closes,
366−// splitting renders the prefix as raw HTML and the suffix as
367−// prose.
368−//
369−// Rule chosen: any HTML-block opener anywhere in the prefix
370−// forces -1. Same trade-off as B1 — the typical assistant
371−// output contains no raw HTML, so the perf cost is zero in
372−// the common case.
373−//
374−// B3 (reference link definitions). A line of the form
375−// "[label]: <url>" defines a link reference that the suffix
376−// may later use as "[text][label]". Splitting the document
377−// loses the definition because each half is rendered as an
378−// independent glamour document.
379−//
380−// Rule chosen: any reference link definition line anywhere in
381−// the prefix forces -1. Suffix-side reference detection is
382−// fragile (three syntaxes: [text][label], [label][], [label]),
383−// so the prefix-side check is the simpler safe choice.
384−//
385−// All three rules accept the perf hit of "no boundary after a
386−// list / HTML block / link def" in exchange for guaranteed
387−// soundness. If profiling shows this kills the F8 win on real
388−// streaming traces, the next iteration can promote each rule to
389−// its less-conservative variant (closure-aware list tracking,
390−// per-tag HTML close detection, suffix-aware ref tracking).
391−func prefixHasOpenHazard(prefix string) bool {
392− inFence := false
393− for line := range splitLines(prefix) {
394− // Track fenced state so list/html/ref patterns inside a
395− // fenced code block do not falsely trigger the hazards.
345+ // Now update state for this line.
396346 if isFenceLine(line) {
397347 inFence = !inFence
398− continue
348+ prevNonBlank = line
349+ return
399350 }
351+
400352 if inFence {
401− continue
353+ prevNonBlank = line
354+ return
402355 }
403− trimmed := strings.TrimLeft(line, " \t")
404− if trimmed == "" {
405− continue
356+
357+ // Update list depth (closure-aware B1).
358+ switch {
359+ case listDepth == 0 && isMarker:
360+ listDepth = 1
361+ listBaseIndent = indent
362+ case listDepth > 0 && isMarker:
363+ switch {
364+ case indent > listBaseIndent:
365+ // Nested list opens.
366+ listDepth++
367+ listBaseIndent = indent
368+ case indent < listBaseIndent:
369+ // Marker at shallower indent: close
370+ // current list. If an outer list
371+ // existed we'd need its base indent;
372+ // we don't track the stack, so be
373+ // conservative and close everything,
374+ // then open a new list at this indent.
375+ listDepth = 1
376+ listBaseIndent = indent
377+ }
378+ // Same indent: another item, depth unchanged.
379+ case listDepth > 0 && !isMarker && blankPending:
380+ // Blank line then non-marker line: check for
381+ // closure. A continuation paragraph is indented
382+ // 2+ relative to the marker.
383+ if indent < listBaseIndent+2 {
384+ // Dedent: list closed. Be conservative
385+ // and close all nesting levels.
386+ listDepth = 0
387+ }
388+ // Otherwise: continuation, list stays open.
406389 }
407− // B1: any list-item marker.
408− if isListItemMarker(trimmed) {
409− return true
390+ // listDepth > 0 && !isMarker && !blankPending:
391+ // A non-marker line directly after another line
392+ // (no blank line between). This is either a lazy
393+ // continuation or part of a tight list's item.
394+ // Keep the list open (conservative).
395+
396+ // B2: HTML block opener (anywhere in prefix).
397+ if !sawHTMLBlock && isHTMLBlockOpener(line) {
398+ sawHTMLBlock = true
410399 }
411− // B2: HTML block opener.
412− if isHTMLBlockOpener(line) {
413− return true
400+ // B3: link reference definition (anywhere in prefix).
401+ if !sawLinkRef && isLinkRefDefinition(line) {
402+ sawLinkRef = true
414403 }
415− // B3: link reference definition.
416− if isLinkRefDefinition(line) {
417− return true
404+
405+ prevNonBlank = line
406+ }
407+
408+ start := 0
409+ for i := 0; i < len(content); i++ {
410+ if content[i] != '\n' {
411+ continue
418412 }
413+ processLine(content[start:i], start)
414+ start = i + 1
419415 }
420− return false
416+ // Process the trailing segment (content after the last '\n').
417+ // A non-blank trailing line after a blank line is a valid
418+ // boundary candidate. A blank trailing line is irrelevant.
419+ if start < len(content) {
420+ processLine(content[start:], start)
421+ }
422+
423+ return boundaries
421424}
422425
423−// countFenceLines counts lines that begin a fenced code block in
424−// the CommonMark sense: a line whose first non-whitespace run is
425−// at least three consecutive backticks (or tildes). Each such
426−// line toggles the fenced state, so an even count means every
427−// opened fence has been closed.
426+// isSafeBoundaryState evaluates the safety of a boundary candidate
427+// given the block state computed by the forward scan (reflecting
428+// everything BEFORE the boundary line). All state is passed in
429+// explicitly so this helper stays pure and testable.
428430//
429−// We accept up to three leading spaces of indentation (CommonMark
430−// rule) and require the fence characters to be the FIRST
431−// non-whitespace content of the line. We deliberately do NOT
432−// attempt to parse info-strings or differentiate opener from
433−// closer beyond toggling — a closing fence is just any line
434−// whose first non-whitespace run is >=3 of the same fence char.
435−func countFenceLines(s string) int {
436− n := 0
437− for line := range splitLines(s) {
438− if isFenceLine(line) {
439− n++
440− }
431+// nextLineTrimmed is the first non-blank line of the suffix
432+// (already left-trimmed), used for the setext-underline check
433+// (rule 4). listClosesHere reports whether the next line closes
434+// the currently-open list; when true, the prefix contains a
435+// complete list and the boundary is safe even though listDepth
436+// is non-zero in the pre-line state.
437+func isSafeBoundaryState(inFence bool, listDepth int, listClosesHere, sawHTMLBlock, sawLinkRef bool, prevNonBlank, nextLineTrimmed string) bool {
438+ // (2) Not inside a fence.
439+ if inFence {
440+ return false
441+ }
442+ // (B1) No open list, unless the next line closes it.
443+ if listDepth != 0 && !listClosesHere {
444+ return false
441445 }
442− return n
446+ // (B2) No HTML block opener anywhere in the prefix.
447+ if sawHTMLBlock {
448+ return false
449+ }
450+ // (B3) No link reference definition anywhere in the prefix.
451+ if sawLinkRef {
452+ return false
453+ }
454+ // (3) The last non-blank line of the prefix must not keep a
455+ // construct open. When the list closes at the next line, the
456+ // prefix's last non-blank line is a list marker — that's
457+ // expected and safe because the list is complete, so skip
458+ // the check in that case.
459+ if !listClosesHere && prevNonBlank != "" && lineOpensConstruct(prevNonBlank) {
460+ return false
461+ }
462+ // (4) The first non-blank line of the suffix must not look
463+ // like a setext underline that would retroactively turn the
464+ // last paragraph of the prefix into a header.
465+ if isSetextUnderlineCandidate(nextLineTrimmed) {
466+ return false
467+ }
468+ return true
443469}
444470
445471// isFenceLine reports whether line opens or closes a fenced code
@@ -465,48 +491,6 @@ func isFenceLine(line string) bool {
465491 return run >= 3
466492}
467493
468−// lastNonBlankLine returns the last non-blank line of s, or ""
469−// when every line is blank.
470−func lastNonBlankLine(s string) string {
471− last := ""
472− for line := range splitLines(s) {
473− if strings.TrimSpace(line) != "" {
474− last = line
475− }
476− }
477− return last
478−}
479−
480−// firstNonBlankLine returns the first non-blank line of s, or ""
481−// when every line is blank.
482−func firstNonBlankLine(s string) string {
483− for line := range splitLines(s) {
484− if strings.TrimSpace(line) != "" {
485− return line
486− }
487− }
488− return ""
489−}
490−
491−// splitLines yields the lines of s without their terminators. The
492−// final segment is yielded even if not newline-terminated.
493−func splitLines(s string) func(yield func(string) bool) {
494− return func(yield func(string) bool) {
495− start := 0
496− for i := 0; i < len(s); i++ {
497− if s[i] == '\n' {
498− if !yield(s[start:i]) {
499− return
500− }
501− start = i + 1
502− }
503− }
504− if start <= len(s)-1 {
505− yield(s[start:])
506− }
507− }
508−}
509−
510494// lineOpensConstruct reports whether line keeps a markdown
511495// construct open across the boundary. We err conservatively —
512496// any case that smells like list/table/quote/setext/indented-code