Skip to content

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

Closed

Description

@HugoXK

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 call cut(...).text once 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 run node scripts/ds-bot-patches.mjs generate and commit the mirrors it writes; never hand-edit them, and a second generate must be a no-op.
  • no core/client.js change (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

  1. HugoXK commented on Nov 1, 2026

    @HugoXK
    ContributorAuthor

    Heartbeat

    Written-by: FlyBot Lead
    Still-working: 9ce27c65909b4c5c982d5f505017a3a6
    At: 2026-11-01T07:46:16Z

  2. HugoXK commented on Nov 1, 2026

    @HugoXK
    ContributorAuthor

    Scope extension while implementing (Lead decision, same defect class, #2005 remains the single issue):

    A sixth site is required and is now in scope: core/src/host/memory.js:1494 — the dossier reader caps memory.head at 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.

  3. HugoXK commented on Nov 1, 2026

    @HugoXK
    ContributorAuthor

    Heartbeat

    Written-by: FlyBot Lead
    Still-working: 9ce27c65909b4c5c982d5f505017a3a6
    At: 2026-11-01T07:49:26Z

  4. HugoXK commented on Nov 1, 2026

    @HugoXK
    ContributorAuthor

    Heartbeat

    Written-by: FlyBot Lead
    Still-working: 9ce27c65909b4c5c982d5f505017a3a6
    At: 2026-11-01T08:49:26Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions