You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Type: performance · Area:src/streaming-frozen-tail.ts · Follow-up to #21 / #23
Summary
The frozen/tail split (#23) makes committed-prefix rendering O(tail) per commit — except when the trailing top-level group never settles. settledTailStart keeps an entire open list (or blockquote) in the tail because a later item can still change it, so streaming a single long top-level list re-renders/re-sanitizes/re-morphs the whole list on every item commit. That is the old O(n²) behaviour for arguably the most common long LLM output shape (a bulleted list). Correct output, but no speedup for list-dominated content.
Documented as a known bound in the streaming-frozen-tail.ts header today.
Why it's hard
The current frozen region is a whole number of top-level group nodes (a <ul> is one node). Bounding a list's tail means freezing individual <li> nodes inside a shared <ul>, which the top-level-boundary mechanism can't express — rendering the tail <li>s separately would emit a second <ul>.
Two correctness hazards:
Tight → loose flip. Loose/tight is a whole-list property (collectListGroup, render-blocks.ts). A frozen tight prefix becomes wrong the moment a blank-separated continuation makes the list loose (items gain <p> wrappers). So a tight list's items can never be frozen while the list is still open.
Same-list continuation across blanks (#306) — a blank run followed by a same-marker item continues the list, so "the list ended" is only knowable after a non-continuing block.
Sketch
Introduce a sub-group freezing mode: the frozen region may end inside a trailing <ul>/<ol>, and the tail morph appends <li>s into that existing list element instead of creating a new one.
Only engage it once the list is provably loose (a blank-separated continuation has been observed → loose is monotonic for that list), so a frozen item renders identically (loose) whether frozen or in-tail. Tight lists stay wholly in the tail.
Add a list-looseness analogue of the tokenStraddles guard: if the observed looseness or marker type flips against a frozen assumption, fall back to full morph.
Type: performance · Area:
src/streaming-frozen-tail.ts· Follow-up to #21 / #23Summary
The frozen/tail split (#23) makes committed-prefix rendering O(tail) per commit — except when the trailing top-level group never settles.
settledTailStartkeeps an entire open list (or blockquote) in the tail because a later item can still change it, so streaming a single long top-level list re-renders/re-sanitizes/re-morphs the whole list on every item commit. That is the old O(n²) behaviour for arguably the most common long LLM output shape (a bulleted list). Correct output, but no speedup for list-dominated content.Documented as a known bound in the
streaming-frozen-tail.tsheader today.Why it's hard
The current frozen region is a whole number of top-level group nodes (a
<ul>is one node). Bounding a list's tail means freezing individual<li>nodes inside a shared<ul>, which the top-level-boundary mechanism can't express — rendering the tail<li>s separately would emit a second<ul>.Two correctness hazards:
collectListGroup,render-blocks.ts). A frozen tight prefix becomes wrong the moment a blank-separated continuation makes the list loose (items gain<p>wrappers). So a tight list's items can never be frozen while the list is still open.Sketch
<ul>/<ol>, and the tail morph appends<li>s into that existing list element instead of creating a new one.tokenStraddlesguard: if the observed looseness or marker type flips against a frozen assumption, fall back to full morph.Acceptance
bench-streaming.mts).References
src/streaming-frozen-tail.ts—settledTailStart,isMultiTokenGroupKind,FrozenTailRenderer.updatesrc/render-blocks.ts—collectListGroup(loose/tight, #306),collectBlockquoteGroup