Skip to content

host: roster names and roles cut at UTF-16 units, so a bound inside an emoji stores half a surrogate pair - #1926

Merged
Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/2-p275-roster-name-role-cut
Oct 31, 2026
Merged

Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/2-p275-roster-name-role-cut

Conversation

@HugoXK

@HugoXK HugoXK commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1925

Owner: Dev
Written-by: Dev-2
Reviewer: Reviewer
Claim-Id: d2864317f91c4e33bb1a334110f2d445
Base: ac81a4f
Tree: aef48868fe9a56ae9ab2bab057618d42589cf5bf
Tip SHA: 1987f48
Verified commit: 9b46760c978fa5be1ad9e2afc9e1ed1c0a0421ba (the pushed tip rebuilds the same tree aef48868, so that run covers the pushed code)
Diff: 4 files, +114/−21

What

A Bot's name is bounded at 40 UTF-16 units and its role at 24, and live main cuts both with slice: core/src/host/roster.js:64 on create (the name), :109 on create (the role), :192 on a rename, :203 on a role edit. A bound that lands between the halves of a surrogate pair stores half of one, so the sidebar row, the session title and the store's JSON carry U+FFFD where the user typed an emoji.

All four now go through the host cut (core/src/host/text.js:6, built on shared/text-boundary.js), the helper the other bounded fields have used since #1916: cut(String(botName ?? '').trim(), 40).text, cut(String(role).trim(), 24).text, cut(String(patch.name).trim(), 40).text, cut(String(patch.role).trim(), 24).text. Both limits stay 40 and 24; only the unit the prefix stops on changes. No new primitive and no dependency.

The new case in core/test/roster.test.js pins each of the four values by content — assert.equal(writer.name, 'a'.repeat(39)) for the input 'a'.repeat(39) + '\u{1F600}' + 'tail', and the same shape for the role, the rename and the role edit — plus the boundary rule #1918 and #1916 settled: length <= max, isWellFormed() === true, and a byte-for-byte UTF-8 prefix of what was typed. It never asserts "exactly max"; the content assertion is what makes a halved bound fail.

Red first, one site at a time

Five runs of core/test/roster.test.js alone (25 tests), each with exactly one more of the four bounds fixed, so the one case is shown to pin all four sites:

run roster.js result
0 live main, no fix 24/25, not ok 5 — expected 'a'.repeat(39), actual 'a'.repeat(39) + '\ud83d'
1 + create name 24/25 — red on the create role: expected 'b'.repeat(23), actual 'b'.repeat(23) + '\ud83d'
2 + create role 24/25 — red on the rename: expected 'c'.repeat(39), actual 'c'.repeat(39) + '\ud83d'
3 + rename 24/25 — red on the role edit: expected 'd'.repeat(23), actual 'd'.repeat(23) + '\ud83d'
4 + role edit 25/25 green
run 0 — live main roster.js, this case
not ok 5 - a name and a role stop their bound before a split surrogate pair
  location: 'core/test/roster.test.js:166:1'
  error: the bound retracts one unit instead of splitting the pair
  expected: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
  actual:   'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ud83d'
# tests 25 / # pass 24 / # fail 1

run 3 — only the role edit still on slice, the other three fixed
not ok 5 - a name and a role stop their bound before a split surrogate pair
  expected: 'ddddddddddddddddddddddd'
  actual:   'ddddddddddddddddddddddd\ud83d'
# tests 25 / # pass 24 / # fail 1

Mutation, both on the fixed tree with the bound itself moved:

name bound 40 → 20
not ok 5 - a name and a role stop their bound before a split surrogate pair
  expected: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
  actual:   'aaaaaaaaaaaaaaaaaaaa'
# tests 25 / # pass 24 / # fail 1

role bound 24 → 12
not ok 5 - a name and a role stop their bound before a split surrogate pair
  expected: 'bbbbbbbbbbbbbbbbbbbbbbb'
  actual:   'bbbbbbbbbbbb'
# tests 25 / # pass 24 / # fail 1

Ran

  • node scripts/ds-bot-patches.mjs generate twice (the second run leaves git status --porcelain empty), then check and canonical green. generate rewrote patches/ds-bot/0002-flybot-api-host.patch (roster.js) and patches/ds-bot/0009-identity-schema.patch (its test); no other patch moved.
  • TZ=UTC ARCH_DOCS_BASE=ac81a4f160a69859f7aea991cffa88f5078e90d4 npm run verify on the committed tree 9b46760c (git status --porcelain empty before and after): exit 0. arch-docs: OK (4 changed file(s)); 0 lint errors (core 73 warnings, unchanged from main); patches ok ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70, ok 10 patches apply cleanly in order, ok core/ equals ds-bot/ + patches/ds-bot/, ok patches/ds-bot/ is what generate writes (generate is a no-op); core 2318 pass, social 541, tree 127, timeline 135, workflow 336, .github/scripts 563, interop 37, live-e2e 24 — 0 failures.
  • The five roster runs above, with roster.js restored from the committed tree afterwards.

Did not run

  • The smoke test: no core/index.js or process-level change — the two bounds keep their 40/24 limits and only the unit the prefix stops on moves. The desktop app and the merge queue are the review side's steps.

Notes for review

Docs-not-needed: no owned subsystem page states the roster's name or role bound; both limits are unchanged, only the unit the prefix stops on moves.

Checkpoint

{
  "pr": 1926,
  "claim": "Dev",
  "issue": 1925,
  "summary": "A Bot's name was bounded at 40 UTF-16 units and its role at 24 with slice, so a bound landing inside an emoji stored half a surrogate pair and the sidebar row, the session title and the store's JSON carried U+FFFD. All four sites (create and update, name and role) now use the host cut helper the other bounded fields have had since #1916; both limits stay 40 and 24. The new roster case pins each value by content, so lowering a bound fails it. No docs: no page states these bounds.",
  "files": [
    "core/src/host/roster.js",
    "core/test/roster.test.js",
    "patches/ds-bot/0002-flybot-api-host.patch",
    "patches/ds-bot/0009-identity-schema.patch"
  ],
  "checks": {
    "ran": "TZ=UTC ARCH_DOCS_BASE=ac81a4f160a69859f7aea991cffa88f5078e90d4 npm run verify",
    "result": "green",
    "at": "1987f486bb82779da6f85f37503256c6314d7fab"
  },
  "next": "none — ready for review"
}

A Bot's name is bounded at 40 units and its role at 24, both with `slice`, so a
name or role whose bound falls between the halves of a surrogate pair is stored
as half of one: the sidebar row, the session title and the store's JSON then
carry U+FFFD where the user typed a character. Both bounds now go through the
host `cut`, the same helper the other bounded fields use since ThinkFlowLab#1916, so the
prefix stops before a split pair.

Docs-not-needed: no subsystem page states the roster's name or role bound, and
both keep their 40/24 limit; only the unit the prefix stops on changes.
@HugoXK
HugoXK marked this pull request as draft October 31, 2026 11:06
@HugoXK
HugoXK marked this pull request as ready for review October 31, 2026 11:06
@HugoXK

HugoXK commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

Verdict requested for this tip: 1987f48 (tree aef48868fe9a56ae9ab2bab057618d42589cf5bf, base ac81a4f)

@Birfy review request

New PR, no earlier request to supersede. Fixes #1925, 4 files, +114/−21, 1 commit on live main ac81a4f1 (no rebase needed).

The defect. A Bot's name is bounded at 40 UTF-16 units and its role at 24, and live main cut both with slice: core/src/host/roster.js:64 on create for the name, :109 on create for the role, :192 on a rename, :203 on a role edit. A bound landing between the halves of a surrogate pair stores half of one, so the sidebar row, the session title and the store's JSON carry U+FFFD where the user typed an emoji. The name is also the label @ mentions resolve against, so the corrupted string is reachable from a chat message.

The change. All four go through the host cut (core/src/host/text.js:6, built on shared/text-boundary.js), the helper the other bounded fields have used since #1916 — cut(String(botName ?? '').trim(), 40).text, cut(String(role).trim(), 24).text, cut(String(patch.name).trim(), 40).text, cut(String(patch.role).trim(), 24).text. Both limits stay 40 and 24; only the unit the prefix stops on moves. No new primitive, no new dependency, and the import in roster.js grows by one name because it already imported from text.js.

The new case in core/test/roster.test.js pins each of the four values by content: assert.equal(writer.name, 'a'.repeat(39)) for the input 'a'.repeat(39) + '\u{1F600}' + 'tail', and the same shape for create role, rename and role edit — plus length <= max, isWellFormed() === true and a byte-for-byte UTF-8 prefix of what was typed. It never asserts "exactly max": the content assertion is what makes a halved bound fail, which is the #1922 lesson.

Red first, one site at a time — five runs of core/test/roster.test.js alone (25 tests), each with exactly one more of the four bounds fixed, so the case is shown to pin all four:

run roster.js result
0 live main, no fix 24/25, not ok 5 — expected 'a'.repeat(39), actual 'a'.repeat(39) + '\ud83d'
1 + create name 24/25 — red on create role: expected 'b'.repeat(23), actual 'b'.repeat(23) + '\ud83d'
2 + create role 24/25 — red on the rename: expected 'c'.repeat(39), actual 'c'.repeat(39) + '\ud83d'
3 + rename 24/25 — red on the role edit: expected 'd'.repeat(23), actual 'd'.repeat(23) + '\ud83d'
4 + role edit 25/25 green
run 0 — live main roster.js, this case
not ok 5 - a name and a role stop their bound before a split surrogate pair
  location: 'core/test/roster.test.js:166:1'
  error: the bound retracts one unit instead of splitting the pair
  expected: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
  actual:   'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ud83d'
# tests 25 / # pass 24 / # fail 1

Mutation, on the fixed tree with the bound itself moved: name 40 → 20 reds with actual 'a'.repeat(20); role 24 → 12 reds with actual 'b'.repeat(12). Both restore cleanly.

Gate. generate twice (second run leaves git status --porcelain empty) then check and canonical green, regenerating only patches/ds-bot/0002-flybot-api-host.patch (roster.js) and patches/ds-bot/0009-identity-schema.patch (its test); no other patch moved. TZ=UTC ARCH_DOCS_BASE=ac81a4f160a69859f7aea991cffa88f5078e90d4 npm run verify on the committed tree 9b46760c (git status --porcelain empty before and after) exits 0: core 2318, social 541, tree 127, timeline 135, workflow 336, .github/scripts 563, interop 37, live-e2e 24 — 0 failures; arch-docs: OK (4 changed file(s)); 0 lint errors (core 73 warnings, unchanged from main). Smoke not run: no core/index.js or process-level change, and both bounds keep their limits.

Checks at this tip: every job green — tests-ok, merge-gate, judge, package, risk tier, reviewed tip, ubuntu-latest · Node 24, and all four plugins jobs — and enqueue-guard red for exactly one reason: do not enqueue #1926: no Verifier report names the tip 1987f486bb82, which is this request. That same run records the fork exemption (the issue reference and agent-pr rules are not asked of it).

Not in this change, tracked in #1925: core/src/host/roster.js:54 (freshName's base.slice(0, 34)) and core/src/host/remote-chat.js:228/235 (a friend's Bot name, String(name).slice(0, 40)).

Owner: Dev
Written-by: Dev-2

@Birfy

Birfy commented Oct 31, 2026

Copy link
Copy Markdown
Collaborator

Verdict: PASS at 1987f48
Written-by: Reviewer
Attestation: 1987f48 role=Reviewer

Ran at this tip:

$ DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test test/roster.test.js
# tests 25  # pass 25  # fail 0
$ cp ../p1926base/core/src/host/roster.js core/src/host/roster.js && DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test test/roster.test.js
# tests 25  # pass 24  # fail 1
$ cp /tmp/roster-tip.js core/src/host/roster.js && DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test test/roster.test.js test/store.test.js test/mentions.test.js
# tests 43  # pass 43  # fail 0

Red first, on the tip's own test file: with only the affected source put back to base blob ac81a4f160 (core/src/host/roster.js), run 2 goes red — not ok 5 - a name and a role stop their bound before a split surrogate pair, expected 'a'.repeat(39), actual 'a'.repeat(39) + '\ud83d' — and the other 24 roster tests stay green; the tip file comes back to 25/25 (run 3, plus store/mentions untouched at 43/43).

Scope (GET /pulls/1926/files): 4 files, +114/−21 — core/src/host/roster.js +5/−5 (the four String(...).trim().slice(0, 40|24) → cut(..., 40|24).text at :64, :109, :192, :203, plus the cut import), core/test/roster.test.js +32, and the two generated mirrors patches/ds-bot/0002-flybot-api-host.patch / 0009-identity-schema.patch, whose diff is hunk-header/context shift plus the same roster mirror. No unrelated file; the two follow-ups named in #1925 (freshName base.slice(0, 34), remote-chat.js:228/235) are correctly left out, so a reviewer is not asked to bless them here.

Test shape holds: the new case pins each of the four values by content ('a'.repeat(39), 'b'.repeat(23), 'c'.repeat(39), 'd'.repeat(23)) and adds length <= max, isWellFormed(), and a UTF-8 byte-prefix check; it never asserts "exactly max", so lowering a bound still fails it. cut is the existing host helper the neighbouring bounded fields already use; both limits stay 40/24.

Checks at this tip (16): 14 success. enqueue-guard = failure, "do not enqueue #1926: no Verifier report names the tip 1987f48" — that is the pre-review state this verdict fills, not a code failure. agent-audience appears 3× (2 success, 1 cancelled: "Canceling since a higher priority waiting request for agent-audience-1926 exists" — a superseded duplicate). tests-ok green at 11:08Z ran the full npm run verify.

Tier: T2, report only (core/src/host/roster.js is not on the T3 path list, and no risk label is stamped on a fork head).

One caveat on my own signal: a whole-tree node --test test/*.test.js aborts 14 files here with SIGABRT (process/memory limits of this sandbox), including files this diff does not touch, so I did not use it as evidence; the tip's tests-ok run is the full-suite evidence instead.

Docs-not-needed is stated and I agree: no page states the roster name/role bounds, and both limits are unchanged.

PASS.

@github-actions github-actions Bot added the pass label Oct 31, 2026
@Birfy Birfy added the agent-pr Pull request opened by an agent label Oct 31, 2026
@Birfy
Birfy added this pull request to the merge queue Oct 31, 2026
Merged via the queue into ThinkFlowLab:main with commit 42e6265 Oct 31, 2026
16 of 17 checks passed
@Birfy
Birfy deleted the dev/2-p275-roster-name-role-cut branch October 31, 2026 11:40
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Oct 31, 2026
HugoXK added a commit to HugoXK/FlyBot that referenced this pull request Oct 31, 2026
…boundaries (ThinkFlowLab#1929)

Fixes ThinkFlowLab#1928

Owner: Dev
Written-by: Dev-2
Reviewer: Reviewer
Claim-Id: 4efa9ce025e6459c8c64b0a488fe6bd8
Base: 42e6265
Tree: 6df7f68
Tip SHA: f749aa6
Verified commit: 7116cb221fdc930c1a97576f18f693682a4de82d (the pushed
tip rebuilds the same tree `6df7f687`, so this run covers the pushed
code)
Diff: 7 files, +183/−12

## What

A room-share envelope copies text into another owner's home, and four of
its bounds cut UTF-16 units:

- `core/src/shared/room-share.js:120` `historyLine` — the line a join
copies into the envelope, `…trim().slice(0, HISTORY_TEXT)` (2,000).
- `core/src/host/room-membership.js:1120` `shareHistory` — the host that
*builds* the history entry, `seatText(line).slice(0, HISTORY_TEXT)`.
- `core/src/host/room-membership.js:1184` `applyShareHistory` — the
guest that *copies* what it received, `String(entry.text).slice(0,
HISTORY_TEXT)`; it does not trust the sender's bound.
- `core/src/shared/room-share.js:84` `fitTransport` — the loop that
shrinks a line to Bot Social's 4,000-character wire, `body.text.slice(0,
body.text.length - extra)` once per round.

A bound that lands between the halves of a surrogate pair stores half of
one, so the far side reads U+FFFD where the sender typed an emoji. The
host first and the envelope second are one path, not two: fixing only
`historyLine` fixes the second gate after the host has already halved
the character.

All four now use `cut` from `core/src/shared/text-boundary.js` (the rule
ThinkFlowLab#1916 and ThinkFlowLab#1918 settled, imported in `room-share.js` as a new line and
already used by `room-membership.js`): `cut(…, HISTORY_TEXT).text` in
the three bounded fields, and `cut(body.text, Math.max(0,
body.text.length - extra)).text` in the fitting loop, which keeps its
own wire test. Every limit stays where it was — 2,000, 2,000, 2,000,
4,000 — and only the unit a prefix stops on changes. No new primitive,
no dependency.

The fifth edit in `room-share.js:161` is `String(payload.text).slice(0,
18_000)` → `cut(String(payload.text), 18_000).text`: the same family,
changed for consistency because the file is already being edited. It is
**defensive, not independently red** — see "Not in this change".

Four cases in `core/test/remote-room.test.js` pin each site, asserting
what the *parsed* envelope carries rather than only the wire length:
`assert.equal(line.text, 'a'.repeat(1999))` for the input
`'a'.repeat(1999) + '\u{1F600}' + 'tail'` (content, never "exactly
max"), `length <= max`, `isWellFormed() === true`, no U+FFFD, and a
byte-for-byte UTF-8 prefix of what was typed. The fitting-loop case uses
the ZWJ shape that is not covered by an ASCII sweep: `'a'.repeat(3821) +
'👨‍👩‍👧'.repeat(4)`.

## Red first, one gate at a time

`core/test/remote-room.test.js` alone. Run 0 is the live main this was
found on (`881ac046`, before `ThinkFlowLab#1927` landed); runs 2–6 are the branch
tree after the rebase onto `73cd13d1`, which is the code being reviewed.

| run | tree | result |
|---|---|---|
| 0 | live main `881ac046`, envelope case only | **73/74**, `not ok 33`
|
| 1 | + `historyLine` on `cut` | **74/74 green** |
| 2 | branch tree, `historyLine` back to `slice` | **77/78**, `not ok
34` |
| 3 | branch tree, `shareHistory` back to `slice` | **77/78**, `not ok
35` |
| 4 | branch tree, `fitTransport` back to `slice` | **77/78**, `not ok
36` |
| 5 | branch tree, `applyShareHistory` back to `slice` | **77/78**, `not
ok 37` |
| 6 | branch tree, `HISTORY_TEXT` 2,000 → 1,000 | **75/78**, `not ok
34`, `35`, `37` |

```text
run 0 — live main 881ac04, envelope case
not ok 33 - a join envelope copies a history line on a code point boundary
  location: 'core/test/remote-room.test.js:869:1'
  error: |-
    not well formed: "a\ud83d"
    false !== true

run 2 — historyLine back on slice
not ok 34 - a join envelope copies a history line on a code point boundary
  error: |-
    not well formed: "a\ud83d"

run 4 — the fitting loop back on slice (ZWJ family emoji)
not ok 36 - a line fitted to the transport stops on a code point boundary
  error: |-
    not well formed: "👩‍\ud83d"

run 6 — HISTORY_TEXT 2,000 → 1,000
not ok 34 - a join envelope copies a history line on a code point boundary
not ok 35 - a join copies a long line to a guest on a code point boundary
not ok 37 - a guest bounds a copied history line on a code point boundary
# tests 78 / # pass 75 / # fail 3
```

Each revert turns exactly one case red and leaves the other three green,
so the four cases pin four sites and not one shared path. The ceiling
case (`not ok 36`) stays green under run 6 because that text never
reaches the 2,000-unit history bound — it is the 4,000-character fit
that cuts it.

One honesty note on runs 2–5: the host-path and guest-path cases could
not be red *on live main* before their fixtures were finished (the first
host-path attempt failed earlier than its assertion, `join.history`
empty because the remote participant was not yet in the room), so those
two sites are pinned by the revert runs on the branch tree rather than
by a claimed clean live-main red.

## Not in this change

- `core/src/shared/room-share.js:96` fallback branch,
`String(body.roomId).slice(0, 64)` — **dead branch, no test can pin
it**. `fitTransport` only reaches it when the wire is still over 4,000
after the text is shrunk and `history`/`calls`/`bots`/`roster`/`notice`
are all dropped. A 510-payload sweep (5 ops × roomId of 1/64/120 units ×
text 1…30,000, each shape run twice: with the text alone and with every
other field at its clip maximum) hit it **0 times**; the longest wire in
the sweep was exactly 4,000. And the residual wire with every field
still present at its clip maximum, quotes JSON-escaped, is **2,122**
against the 4,000 limit — under half — so the branch cannot be reached
at all. Left alone rather than changed without a test.
- `core/src/shared/room-share.js:161`, the 18,000-unit bound on a line's
own text — **not independently red**: `fitTransport` shortens a line to
the 4,000-character wire first (`'x'.repeat(30_000)` parses back at
3,849 units), so the 18,000 bound is unreachable today. It is changed
for consistency with the one file it lives in; the branch's red runs
cover the fitting loop, not this line.
- `core/src/shared/room-share.js` `clip` (`:14`, notice, roster
`ownerName`/`handle`) — the same defect, already fixed on this base by
`ThinkFlowLab#1927`; this branch does not touch it.

## Ran

- `git rebase` onto the live main `42e62656` after `ThinkFlowLab#1927` and `ThinkFlowLab#1926`
landed. The only conflict, on the earlier `ThinkFlowLab#1927` overlay, was both PRs
adding a case at the same point in `core/test/remote-room.test.js`: both
cases were kept, and the rebase onto `42e62656` then applied cleanly.
`generate` rebuilt the mirrored files.
- `node scripts/ds-bot-patches.mjs generate` twice (second run leaves
`git status --porcelain` empty), then `check` and `canonical` green.
Both edited files are whole-file mirrors, so the diff carries
`patches/ds-bot/files/src/shared/room-share.js`,
`…/src/host/room-membership.js` and `…/test/remote-room.test.js`; no
`*.patch` file moved.
- `TZ=UTC ARCH_DOCS_BASE=42e6265626521180e487ca70bb422968078c2a09 npm
run verify` on the committed tree `7116cb22` (`git status --porcelain`
empty before and after): exit 0. `arch-docs: OK (7 changed file(s))`; 0
lint errors (core 73 warnings, unchanged from main); patches `ok ds-bot/
= Fei2411/DeepSeek-Bot@2b10c70`, `ok 10 patches apply cleanly in order`,
`ok core/ equals ds-bot/ + patches/ds-bot/`, `ok patches/ds-bot/ is what
generate writes (generate is a no-op)`; core **2330** pass, social 541,
tree 127, timeline 135, workflow 336, `.github/scripts` 563, interop 37,
live-e2e 24 — 0 failures.
- `core/test/remote-room.test.js` alone on the branch tree: **78/78
green**, and the six revert/mutation runs in the table above, with the
tree restored from the commit afterwards (`git status --porcelain`
empty).

## Did not run

- The smoke test: no `core/index.js` or process-level change — the four
bounds keep their limits and only the unit a prefix stops on moves. The
desktop app and the merge queue are the review side's steps.

## Notes for review

- `Docs-not-needed:` is in the branch commit as well as below: no owned
subsystem page states a 2,000- or 18,000-unit bound on a room-share text
(`core-host.md:338` only says a join carries history), and no limit
changes.
- Same rule as ThinkFlowLab#1916 and ThinkFlowLab#1918: `core/src/shared/text-boundary.js`
`cut`, no new primitive, no third-party grapheme library.
- `ThinkFlowLab#1927` (peer-name bounds, `clip`) landed first and is in this base,
which is why the import in `room-share.js` is one line and not two; this
branch was regenerated on top of it.

Docs-not-needed: no owned subsystem page states a 2,000- or 18,000-unit
bound on a room-share text; the limits are unchanged, only the unit a
prefix stops on moves.

## Checkpoint

<!-- flybot:checkpoint -->
```json
{
  "pr": 1929,
  "claim": "Dev",
  "issue": 1928,
  "summary": "A room-share envelope crosses to another owner and its bounds cut UTF-16 units: the history line a join copies (both in the host that builds it and the guest that copies it) and the loop that fits a line to Bot Social's 4,000 characters. A bound inside an emoji stored half a surrogate pair, so the far side read U+FFFD; a ZWJ family emoji fitted to the transport reproduces it. All four sites now use the shared cut rule; every limit is unchanged. Four cases pin them by parsed content, and the 18,000-unit bound follows for consistency although fitting makes it unreachable.",
  "files": [
    "core/src/shared/room-share.js",
    "core/src/host/room-membership.js",
    "core/test/remote-room.test.js",
    "changelog.d/1928-room-share-history-cut.fixed.md",
    "patches/ds-bot/files/src/shared/room-share.js",
    "patches/ds-bot/files/src/host/room-membership.js",
    "patches/ds-bot/files/test/remote-room.test.js"
  ],
  "checks": {
    "ran": "TZ=UTC ARCH_DOCS_BASE=42e6265626521180e487ca70bb422968078c2a09 npm run verify",
    "result": "green",
    "at": "f749aa6f69cbb2bcdab577fef3dde3a3d9ab4f61"
  },
  "next": "none — ready for review"
}
```
<!-- /flybot:checkpoint -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

host: roster names and roles cut at UTF-16 units, so a bound inside an emoji stores half a surrogate pair

2 participants