mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-21 10:22:18 +00:00
Re-parsing the full message markdown every reveal frame is O(N^2) over a long answer and dominated stream CPU. - Throttle useSmoothReveal commits to ~1 frame (REVEAL_MIN_COMMIT_MS). - Memoize block parsing with an LRU keyed on source text so only changed blocks re-parse. - Replace Streamdown's full-text parseIncompleteMarkdown with a tail-bounded remend: scan to the last top-level boundary outside fences/math and repair only the trailing open block. New remend-tail.ts is proven render-equivalent to full remend at every streaming prefix (remend-tail.test.ts), minus an intentional, documented divergence on cross-block dangling openers.
108 lines
3 KiB
TypeScript
108 lines
3 KiB
TypeScript
import remend from 'remend'
|
|
|
|
// Tail-bounded incomplete-markdown repair.
|
|
//
|
|
// Streamdown's built-in `parseIncompleteMarkdown` runs `remend` over the whole
|
|
// accumulated message on every streaming flush (~18% of script time on 50KB+
|
|
// messages). But repairs only ever matter in the trailing block: inline
|
|
// constructs can't cross a blank line, and Streamdown splits into blocks AFTER
|
|
// the repair, so a dangling opener in an earlier block can't reach the tail.
|
|
// We run `remend` on just that block instead.
|
|
|
|
const BACKTICK = 96 // `
|
|
const TILDE = 126 // ~
|
|
const SPACE = 32
|
|
const TAB = 9
|
|
const BACKSLASH = 92
|
|
|
|
const isSpace = (c: number) => c === SPACE || c === TAB
|
|
|
|
/**
|
|
* Index of the last top-level block start — the char after the most recent
|
|
* blank line that sits outside any open code fence or `$$` math block. An
|
|
* unclosed fence/math always begins after that blank, so it stays wholly
|
|
* inside the window without separate tracking. One cheap char pass, no regex.
|
|
*/
|
|
export function findRemendWindowStart(text: string): number {
|
|
const n = text.length
|
|
let inFence = false
|
|
let fenceChar = 0
|
|
let fenceRun = 0
|
|
let inMath = false
|
|
let boundary = 0
|
|
let pending = -1 // a blank line, committed to `boundary` once content follows
|
|
|
|
for (let lineStart = 0; lineStart <= n; ) {
|
|
let lineEnd = text.indexOf('\n', lineStart)
|
|
|
|
if (lineEnd === -1) {
|
|
lineEnd = n
|
|
}
|
|
|
|
let i = lineStart
|
|
|
|
while (i < lineEnd && isSpace(text.charCodeAt(i))) {
|
|
i += 1
|
|
}
|
|
|
|
const first = i < lineEnd ? text.charCodeAt(i) : -1
|
|
let marker = false
|
|
|
|
// Fence open/close (``` or ~~~, ≤3 spaces indent).
|
|
if ((first === BACKTICK || first === TILDE) && i - lineStart <= 3) {
|
|
let run = i
|
|
|
|
while (run < lineEnd && text.charCodeAt(run) === first) {
|
|
run += 1
|
|
}
|
|
|
|
if (run - i >= 3) {
|
|
marker = true
|
|
|
|
if (!inFence) {
|
|
inFence = true
|
|
fenceChar = first
|
|
fenceRun = run - i
|
|
} else if (first === fenceChar && run - i >= fenceRun && onlyWhitespace(text, run, lineEnd)) {
|
|
inFence = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// Toggle `$$` math state on plain lines ($$ inside a fence is literal).
|
|
if (!inFence && !marker) {
|
|
for (let s = text.indexOf('$$', lineStart); s !== -1 && s < lineEnd - 1; s = text.indexOf('$$', s + 2)) {
|
|
if (s === 0 || text.charCodeAt(s - 1) !== BACKSLASH) {
|
|
inMath = !inMath
|
|
}
|
|
}
|
|
}
|
|
|
|
if (first === -1 && !inFence && !inMath) {
|
|
pending = lineEnd + 1
|
|
} else if (pending !== -1) {
|
|
boundary = pending
|
|
pending = -1
|
|
}
|
|
|
|
lineStart = lineEnd + 1
|
|
}
|
|
|
|
return boundary
|
|
}
|
|
|
|
function onlyWhitespace(text: string, from: number, to: number): boolean {
|
|
for (let i = from; i < to; i += 1) {
|
|
if (!isSpace(text.charCodeAt(i))) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
export function tailBoundedRemend(text: string): string {
|
|
const start = findRemendWindowStart(text)
|
|
|
|
return start <= 0 ? remend(text) : text.slice(0, start) + remend(text.slice(start))
|
|
}
|