Repository navigation
host: roster names and roles cut at UTF-16 units, so a bound inside an emoji stores half a surrogate pair - #1926
Conversation
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.
|
Verdict requested for this tip: 1987f48 (tree aef48868fe9a56ae9ab2bab057618d42589cf5bf, base ac81a4f) @Birfy review request New PR, no earlier request to supersede. The defect. A Bot's name is bounded at 40 UTF-16 units and its role at 24, and live main cut both with The change. All four go through the host The new case in Red first, one site at a time — five runs of
Mutation, on the fixed tree with the bound itself moved: name 40 → 20 reds with actual Gate. Checks at this tip: every job green — Not in this change, tracked in Owner: Dev |
|
Verdict: PASS at 1987f48 Ran at this tip: Red first, on the tip's own test file: with only the affected source put back to base blob Scope ( Test shape holds: the new case pins each of the four values by content ( Checks at this tip (16): 14 success. Tier: T2, report only ( One caveat on my own signal: a whole-tree Docs-not-needed is stated and I agree: no page states the roster name/role bounds, and both limits are unchanged. PASS. |
…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 -->
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:64on create (the name),:109on create (the role),:192on a rename,:203on 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 onshared/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.jspins 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.jsalone (25 tests), each with exactly one more of the four bounds fixed, so the one case is shown to pin all four sites:roster.jsnot ok 5— expected'a'.repeat(39), actual'a'.repeat(39) + '\ud83d''b'.repeat(23), actual'b'.repeat(23) + '\ud83d''c'.repeat(39), actual'c'.repeat(39) + '\ud83d''d'.repeat(23), actual'd'.repeat(23) + '\ud83d'Mutation, both on the fixed tree with the bound itself moved:
Ran
node scripts/ds-bot-patches.mjs generatetwice (the second run leavesgit status --porcelainempty), thencheckandcanonicalgreen.generaterewrotepatches/ds-bot/0002-flybot-api-host.patch(roster.js) andpatches/ds-bot/0009-identity-schema.patch(its test); no other patch moved.TZ=UTC ARCH_DOCS_BASE=ac81a4f160a69859f7aea991cffa88f5078e90d4 npm run verifyon the committed tree9b46760c(git status --porcelainempty before and after): exit 0.arch-docs: OK (4 changed file(s)); 0 lint errors (core 73 warnings, unchanged from main); patchesok 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/scripts563, interop 37, live-e2e 24 — 0 failures.roster.jsrestored from the committed tree afterwards.Did not run
core/index.jsor 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:is in the branch commit as well as below: no subsystem page states the roster's name or role bound, and both keep their limits.core/src/host/text.jsonly re-exportscut, so the import inroster.jsgrows by one name and no primitive is added.core/src/host/roster.js:54(freshName'sbase.slice(0, 34)) andcore/src/host/remote-chat.js:228/235(a friend's Bot name,String(name).slice(0, 40)).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" }