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: ff8983e4bac246018ebec8223f15e611
Branch: dev/1-published-name-cut
Base: ac81a4f
At: 2026-10-31T10:43:48Z
This block is FlyBot Lead's own claim record for this issue — it is not a third-party claim.
Dedupe (checked before opening): issue search for cleanPublishedName returns 0 results; room-share clip returns 7 issues, all closed except #1916 (merged by #1917, the host text cuts) and none about these four call sites; published name hits are older sanitising/identity issues (#75/#87) that do not touch these lines. Cross-checked at live main ac81a4f1 that no open issue names hooks.js:47, tools.js:84, reach.js:157 or shared/room-share.js:13.
Problem
#1916/#1917 replaced the host's code-unit cuts with cut() from core/src/shared/text-boundary.js, and #1918/#1921 is closing the memory-review pair. Four more display-name bounds still call .slice() on UTF-16 units, and they are the ones whose text crosses an owner boundary (a friend's Bot name into our roster; our names into a room envelope):
core/src/host/hooks.js:47—cleanPublishedNameends with.trim().slice(0, 60); called onbot.name,bot.homeNameandbot.ownerat:947-949, and the result goes intostate.flybot.remote.core/src/host/tools.js:84— the group member line:displayName.slice(0, 60),ownerName.slice(0, 40). This line reaches the model.core/src/host/reach.js:157—labelends with.trim().slice(0, 60); the labels are rendered byothersInRoom(:166/:167).core/src/shared/room-share.js:13-14— localclip(value, max = 60)ends with.trim().slice(0, max). Every roster entry, chat line and invite field goes through it (:107-:112,:120-:158,:165,:178), i.e. our own names as other owners' Bots receive them.
cut() already exists on main (core/src/shared/text-boundary.js), is exported as cut(text, max) => ({ text, cut }), and is imported by core/src/host/text.js:4 via '../shared/text-boundary.js'. room-share.js sits in the same shared/ directory, so it imports './text-boundary.js' — no new layer edge, no shared/ → host/ import.
Reproduction (live main ac81a4f1, probe test only, source untouched)
cleanPublishedName('a'.repeat(59) + '😀tail') returns a 60-unit string with isWellFormed() === false: the last code unit is a lone high surrogate (\ud83d) and the UTF-8 tail is 61 ef bf bd, i.e. …a + U+FFFD. Through the real call path (hooks.js:947-949) that ill-formed name lands in state.flybot.remote, so the sidebar, the @ menu and the roster all show U+FFFD. The same shape reproduces for tools.js:84 (60/40), reach.js:157 (60) and clip('a'.repeat(59) + '😀tail') in room-share.js (60).
Expected behaviour
- Every one of the four bounds returns a well-formed string whose UTF-8 bytes are a prefix of the input's, dropping the straddling character whole instead of splitting it (the core-host: the remaining code-unit cuts (claim cue, read_own_chat query/around, page title, diagnostics lines, peer name) #1916/host: cut the remaining code-unit bounds on a code point boundary #1917 rule).
- The numeric bound does not change: 60 / 40 / 60 / 60, and
room-share's per-field caps (120,64,32,16,NOTICE_MAX) stay as they are. room-shareenvelopes cross an owner boundary: after the fix, every clipped field is stillString(field).length <= maxand well-formed, and an envelope built from a 59-a-plus-emoji name still parses and round-trips throughparseRoomShare/the roster builder with the name's bytes unchanged for the part that was kept.cleanPublishedNamestill strips@, control characters and collapses whitespace before the bound; only the bound changes.
Red first
Add the cases first, run them against ac81a4f1 with only the tests applied, and paste the failing output:
core/test/flybot.test.js(the existing:811group that already has 7 cases) — one 60-boundary emoji case through the real path: build the remote roster the wayhooks.js:947-949does and assert the stored name is well-formed, is a byte prefix of the input, and is<= 60. Illegal strength: do not assert "exactly 60"; assert the kept prefix and, for this input, that the straddling emoji is absent — otherwise a 60→40 mutation stays green.core/test/tools.test.js— the member line withdisplayNameat its 60 boundary andownerNameat its 40 boundary.core/test/reach.test.js— theothersInRoomlabel at 60.core/test/room-share.test.js—clipat 60 and120(roster handle), assertinglength <= max, well-formed, and that the parsed envelope keeps a byte prefix.room-shareis the cross-owner case, so assert both properties in the same case.
Scope
core/src/host/hooks.js(:47),core/src/host/tools.js(:84),core/src/host/reach.js(:157),core/src/shared/room-share.js(:13-14) — the four bounds go throughcut().- The four test files above;
docs/subsystems/core-host.md(and the shared-envelope doc if one ownsroom-share); onechangelog.d/<issue>-published-name-cut.fixed.md. - Regenerate
patches/ds-bot/0001-core-fixes.patchand0007-version-and-tooling.patch;check/canonical/generateno-op /TZ=UTC ARCH_DOCS_BASE=<base> npm run verifyall green on the pushed tip.
Not in this change
hooks.js:353/:358 (verdict.recipient.slice(0, 120)), hooks.js:693/:697 (attachment name 200 / mime 120) and hooks.js:942 (avatarUpdatedAt 40) are the same shape but a different surface (queue verdicts and attachments); they get their own issue after this one. recall's 30-hit cap is a separate enhancement.
Activity
fix(host): cut peer-name bounds on code point boundaries (ThinkFlowLa…