Repository navigation
Description
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.
Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 8a26b27271334418813ce8a5a9f25e72
Branch: dev/5-memory-lone-surrogate
Base: 24bb90d
At: 2026-11-06T00:34:50Z
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.
Written-by: FlyBot Lead
The memory review's line renderer flattens an ill-formed message only on its cut paths. A message that fits its budget is answered untouched, so an unpaired surrogate in a user line or a Bot message reaches the review prompt (and the log) as half a character.
Reproduction (live main 24bb90d1)
In core/src/host/memory.js:
:1281-1285—ends():if (fits(text, max)) return textanswers the raw string; thetoWellFormed()that follows only runs below that line.:1294-1296—reviewLine(): the non-user branch hands the labelled text toclip()unchanged, andclip()'s ownfitsanswer (core/src/host/text.js:61) returns it unchanged too.
Probe, using the same render() helper the review tests already have:
- user line
'x'.repeat(100) + '\uD800' + 'y'.repeat(100)— 201 characters, rendered line 207 (User:+ 201), far inside the 3,000-character budget →{ wellFormed: false, loneSurrogate: true, cut: false, replacementCount: 0 }. - Bot line
'z'.repeat(80) + '\uD800' + 'w'.repeat(80)— 160 characters, inside the 800-character bound →{ promptWellFormed: false, loneSurrogate: true, cut: false }.
Both surrogates sit inside the part the review keeps, so this is not a pair that a cut dropped.
Expected behaviour
Flatten before bounding on both paths: the lone surrogate becomes one U+FFFD, whole and in its place, and the rest of the line and the line's length stay as they were.
- user line:
line.isWellFormed() === true,replacementCount === 1,replacementAt === 'User: '.length + 100(106),line.slice(6, 106) === 'x'.repeat(100),line.slice(107, 207) === 'y'.repeat(100). - Bot line: the prompt is well formed,
replacementCount === 1, and U+FFFD sits at the label's length + 80.
Scope
core/src/host/memory.js—ends()flattens once and answers the flattened string on both branches;reviewLine()'s non-user branch flattens the labelled text beforeclip().core/test/memory.test.js— one test carrying both lines, red first on this issue's base.docs/subsystems/core-host.md— thecore/src/host/text.jsrow's memory-review sentence gains this rule.patches/ds-bot/0001-core-fixes.patch— regenerated (memory.jsand its test are inside the ds-bot + patches check).changelog.d/<number>-memory-review-flattened-lines.fixed.md.
Deliberately not changed: clip() in core/src/host/text.js. It is imported by more than a dozen client/*.js modules, so flattening inside it would open the client-bundle lane; the host review flattens at its own two call sites instead.
Dedup
Searched this repository's open and closed issues and live main (24bb90d1) for surrogate and well-formed: #1907 and #1912/#1916/#1963 built the cut/codePoint* boundary rule, #1943/#1944/#1960/#1979 covered ill-formed strings arriving on other paths, #2339 and #2553 made this review's budgets count characters. None covers ends()'s fits answer or the non-user clip path, and live main has exactly one toWellFormed() in memory.js, on the cut path (:1285). The only open pull request is #2554 (steward, steward.js), which touches neither line.
Activity
HugoXK commentedon Nov 6, 2026
Release
Written-by: FlyBot Lead
Released: 8a26b27271334418813ce8a5a9f25e72
Reason: merged as c907f46 (PR #2557)
At: 2026-11-06T01:21:18Z