Repository navigation
host: five Project-panel bounds still cut text on UTF-16 units, so a dossier or a board card can carry half an emoji #2005
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: 9ce27c65909b4c5c982d5f505017a3a6
Branch: dev/1-project-panel-code-point-cuts
Base: a989587
At: 2026-11-01T07:36:07Z
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.
Written-by: FlyBot Lead
What happens
Five bounds in the Project surfaces still cut with String.prototype.slice, which counts UTF-16 units, so a cut that lands inside a surrogate pair leaves half an emoji in the answer. The Dossier panel and the board cards render � where the emoji was, and the half pair travels through the host/client JSON as an unpaired surrogate.
Live main a989587f, all slice(0, N) on a string the user or another home supplied:
| site | what it bounds |
|---|---|
core/src/host/project-assistant.js:132 |
the assistant card's task title — title.slice(0, 77) + '…' when the title is over 80 |
core/src/host/project-board.js:105 |
oneLine for a decision / board line, 200 |
core/src/host/project-board.js:163 |
a meeting note's title after the markdown heading is stripped, 200 |
core/src/host/project-team.js:22 |
capped() — bot name, role, persona, model, color (:60–:64) |
core/src/host/project-team.js:67 |
the dossier's memory.head, 4000 |
The same file already does it right one line away: core/src/host/project-team.js:49 uses cut(text(room.name), 200).text (from #1981). That is the shape to copy.
Reproduction (node, the live-main expressions)
- assistant title
'a'.repeat(76) + '😀' + 'b'.repeat(10)→slice(0,77) + '…'= 78 units,isWellFormed() === false, one lone high surrogate, tail"aaaa\ud83d…" - board
oneLine:'a'.repeat(199) + '😀tail'→ 200 units, not well formed, tail"aaaaa\ud83d" - board title:
'## ' + 'a'.repeat(199) + '😀'→ heading stripped,slice(0,200)→ not well formed, tail"aaa\ud83d" - dossier
capped(name, 200):'a'.repeat(199) + '😀'→ 200 units, not well formed - dossier
memory.head:'a'.repeat(3999) + '😀'→ 4000 units, not well formed
With the bound moved back onto the code point (cut), the same inputs give 76 / 199 / 199 / 199 / 3999 units and are all well formed — the emoji is dropped whole, never halved.
Expected
Every one of those answers stays well formed. Use the shared cut() (core/src/shared/text-boundary.js, re-exported from core/src/host/text.js), not a manual slice — a slice on an already well-formed string is still wrong at the bound.
Scope
- the five sites above;
capped()can callcut(...).textonce and cover all its call sites. - tests: one case per site in
core/test/project-assistant.test.js,core/test/project-board.test.js,core/test/project-team.test.js— red first on live main, and each case must be able to fail alone (mutate one site, only its case goes red). - generated files: all three sources have full-file mirrors under
patches/ds-bot/files/src/host/, so runnode scripts/ds-bot-patches.mjs generateand commit the mirrors it writes; never hand-edit them, and a secondgeneratemust be a no-op. - no
core/client.jschange (these are host answers). - changelog fragment.
Dedup
Searched open and closed issues on live main: the room-name bound in project-team.js was fixed by #1981 (cut(text(room.name), 200).text); #1916 and #1917 swept other host modules and list no project-* file; no open issue covers these five. In flight today, #1998 touches member-status.js / group-status.test.js and #2004 touches room-membership.js, so neither collides.
Activity
HugoXK commentedon Nov 1, 2026
Heartbeat
Written-by: FlyBot Lead
Still-working: 9ce27c65909b4c5c982d5f505017a3a6
At: 2026-11-01T07:46:16Z
HugoXK commentedon Nov 1, 2026
Scope extension while implementing (Lead decision, same defect class,
#2005remains the single issue):A sixth site is required and is now in scope:
core/src/host/memory.js:1494— the dossier reader capsmemory.headat 4,000 units before the Project panel ever sees it, so a lone high surrogate is created there. Bounding only the panel side can at best turn that half into U+FFFD, which is still the damaged character this issue reports.Expected behaviour there matches the issue: 3,999 units,
isWellFormed()true, the emoji dropped whole (not replaced).#2006(this repo's other memory.js item) covers:569,:909,:1042,:1084— different lines, no overlap; both land serially on the patch mirror.Everything else is unchanged: the five Project-panel sites in the body stay, each with a case that fails on its own.
HugoXK commentedon Nov 1, 2026
Heartbeat
Written-by: FlyBot Lead
Still-working: 9ce27c65909b4c5c982d5f505017a3a6
At: 2026-11-01T07:49:26Z
HugoXK commentedon Nov 1, 2026
Heartbeat
Written-by: FlyBot Lead
Still-working: 9ce27c65909b4c5c982d5f505017a3a6
At: 2026-11-01T08:49:26Z