Skip to content

Bound the joined notice and the copied pane line on code point boundaries - #1935

Merged
Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/2-p285-room-share-unbounded
Oct 31, 2026
Merged

Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/2-p285-room-share-unbounded

Conversation

@HugoXK

@HugoXK HugoXK commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1933

Owner: Dev
Written-by: Dev-2
Reviewer: Birfy
Claim-Id: 287704d751b440548bfbd9049eaa67aa
Base: 1dda685
Tree: 1313df11cf70d7e3241dacac3f12b9379a886ad7
Tip SHA: ae3a257
Diff: 5 files, +127/−6

What

Two host-side readers of a room-share join still cut stranger-written text on UTF-16 units, and one of them had no bound at all:

  • core/src/host/room-membership.js:1096 noticeOf — the notice a guest stores from a join, …trim().slice(0, 2000). A notice whose 2,000th unit is the high half of a surrogate pair was stored with that half, so every later read of the notice carried U+FFFD.
  • core/src/host/room-membership.js:1192 applyShareHistory — the same loop appends the copied line as cut(String(entry.text), HISTORY_TEXT).text (:1184) but drew the pane from the raw entry.text, and showShareLine (:1142) only unwraps and trims. The guest's pane therefore showed a line longer than its own log kept, past the whole pair the stored bound had retracted.

Both now use cut from core/src/shared/text-boundary.js (one import addition, NOTICE_MAX), at the two call sites and not inside showShareLine: noticeOf cuts to NOTICE_MAX (2,000, the same constant the envelope side uses for payload.notice), and applyShareHistory passes cut(String(entry.text), HISTORY_TEXT).text to showShareLine, the identical expression the append above it already uses — so the drawn line is exactly the stored one, and the line op keeps its own 4,000-character transport bound. Every limit is unchanged (2,000 / 2,000); only the unit a prefix stops on moves.

Red first, then one site at a time

core/test/remote-room.test.js alone, on this branch tree, base 1dda68545e. The input for both cases is 'a'.repeat(1999) + '\u{1F600}' + 'b'.repeat(400): 2,405 units, so the 2,000-unit prefix ends between the halves of the emoji.

run source result
0 both fixes reverted to the base file (tests kept) 0/2 green — both cases red
1 branch tree 2/2 green
2 noticeOf back on .slice(0, NOTICE_MAX), pane fix kept 1/2 — only the notice case red
3 showShareLine back on raw entry.text, notice fix kept 1/2 — only the pane case red
4 whole file, branch tree 81/81 green
run 0 — both sites reverted to the base file
not ok 1 - a joined notice is kept on a code point boundary
  error: not well formed: "a\ud83d"
not ok 2 - a copied history line is shown as the same bounded prefix it is stored as
  error: the shown line retracts the whole pair instead of half of it

run 2 — noticeOf back on slice
not ok 1 - a joined notice is kept on a code point boundary
ok     2 - a copied history line is shown as the same bounded prefix it is stored as

run 3 — showShareLine back on raw entry.text
ok     1 - a joined notice is kept on a code point boundary
not ok 2 - a copied history line is shown as the same bounded prefix it is stored as
  error: the shown line retracts the whole pair instead of half of it

Two independent assertions, as the issue asks: the notice case reads rt.state.rooms[id].notice and requires isWellFormed(), no U+FFFD, kept === 'a'.repeat(1999), length <= 2000 and a UTF-8 byte prefix of what the host sent; the pane case captures rt.showSharedLine and requires the drawn text to be 'a'.repeat(1999), a prefix of the stored line, free of the pair the bound retracted, well formed, <= HISTORY_TEXT and a UTF-8 byte prefix of what the sender typed. Each revert turns exactly one of them red, so neither case is riding on the other's fix.

Not in this change

  • fitJoin's notice shrink loop (core/src/shared/room-share.js) is untouched. That loop re-measures the JSON each round, and a lone surrogate that survives a cut is re-encoded as an escape and measured again, so it self-heals — measured 0 ill-formed notices in the sweep reported on room-share: cut the cross-owner body and history lines on code point boundaries #1929. This change bounds the two host-side readers that had no such loop.
  • showShareLine itself stays unbounded on purpose: the line op reaches it with text already fitted to Bot Social's 4,000-character wire, and a 2,000-unit cap inside it would shorten legitimate cross-owner lines. The bound belongs where the guest copies the join's history, which is also where the stored line is cut.

Ran

  • git rebase onto the live main 1dda6854 (after #1934 landed). Clean: #1934 touched core/src/shared/room-share.js and the two test-file regions at :22 and :2354, this branch touches room-membership.js, :974 of the test file and the changelog.
  • node scripts/ds-bot-patches.mjs generate twice — the second run leaves git status --porcelain empty and the git diff hash unchanged; then check (4/4 ok, 10 patches apply cleanly in order, core/ equals ds-bot/ + patches/ds-bot/) and canonical (generate is a no-op). cmp of patches/ds-bot/files/src/host/room-membership.js and …/test/remote-room.test.js against their core/ originals: byte-identical. No *.patch file moved.
  • TZ=UTC ARCH_DOCS_BASE=1dda68545e1fe954466d52e8d81e26a4376e19d4 npm run verify on the committed tree (git status --porcelain empty before and after): exit 0. arch-docs: OK (5 changed file(s)); core 2339 pass / 0 fail, social 541, tree 127, timeline 135, workflow 336, .github/scripts 563, interop 37, live-e2e 24 — 0 failures; lint 73 warnings / 0 errors in core (the same warnings the base has), plugins 36/9/2/16, lint:interop 1 warning / 0 errors, lint:live-e2e 0 / 0.
  • core/test/remote-room.test.js alone on the branch tree: 81/81 green, plus runs 0, 2 and 3 above, with the source restored from the commit afterwards (git status --porcelain empty).

Did not run

  • The smoke test: no core/index.js or process-level change — two call sites now cut where they used to slice or pass through, and no limit moves. The desktop app and the merge queue are the review side's steps.

Notes for review

  • Titles, bodies and the branch carry no internal queue name; the issue is the reference.
  • The changelog entry is changelog.d/1933-room-share-stranger-text-bound.fixed.md (both halves: #1928's envelopes and these two host readers).

Docs-not-needed: no owned subsystem page states a 2,000-unit bound on the room-share join notice or on the copied pane line — core-host.md:338 names only the 4,000-character wire limit, and :41's list names the group line's 100-unit notice in tools.js, a different field; no limit changes here, only the unit a prefix stops on.

Checkpoint

{
  "pr": 1935,
  "claim": "Dev",
  "issue": 1933,
  "summary": "A room-share join carries a stranger's notice and history into another owner's home, and that guest cut the notice on UTF-16 units while drawing the pane from the sender's raw text. A notice whose 2,000th unit was half a surrogate pair was stored with that half, and the pane showed a line longer than this home's own log kept. Both readers now use the shared cut rule at the same limits: the notice through NOTICE_MAX, the drawn line through HISTORY_TEXT at the call site, so the pane equals the stored line. Two cases pin the two sites separately, each red on its own revert.",
  "files": [
    "core/src/host/room-membership.js",
    "core/test/remote-room.test.js",
    "changelog.d/1933-room-share-stranger-text-bound.fixed.md",
    "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=1dda68545e1fe954466d52e8d81e26a4376e19d4 npm run verify",
    "result": "green",
    "at": "ae3a257d94d4bc24c5203c47e7f9d0de4871696e"
  },
  "next": "none — ready for review"
}

…ode point boundaries

A room share arrives from another owner, and two host-side readers of that
stranger-written text still cut UTF-16 units with a raw `slice`:

- `room-membership.js:1096` `noticeOf` — the notice a join carries,
  `…trim().slice(0, 2000)`; a notice whose 2,000th unit is half of a
  surrogate pair was stored with the lone high surrogate, so every later
  read of the room's notice carried U+FFFD. It now goes through `cut` at
  the shared `NOTICE_MAX`, the bound the envelope already uses.
- `room-membership.js:1190` `applyShareHistory` — the stored copy of a
  history line is `cut(...)` at `HISTORY_TEXT` (2,000, ThinkFlowLab#1928), but the
  pane was handed the sender's raw `entry.text`: the guest drew a longer
  line than its own log kept, past the whole pair the stored bound dropped.
  The shown text now goes through the same `cut` at the same bound.
- `fitJoin`'s notice shrink loop is untouched: each round re-measures the
  JSON, and an escaped half pair cannot survive it (measured 0 ill-formed
  entries).

Both bounds are unchanged; only the unit a prefix stops before moves, the
rule ThinkFlowLab#1912/ThinkFlowLab#1916/ThinkFlowLab#1923/ThinkFlowLab#1928 already apply. Two new cases in
`core/test/remote-room.test.js` pin the parsed notice and the drawn line;
each fails when its own site goes back to `slice`.

Docs-not-needed: no owned subsystem page states a 2,000-unit bound on a
joined notice or on the pane line copied from another owner; the limits
are unchanged, only the unit a prefix stops on moves.
@HugoXK
HugoXK marked this pull request as ready for review October 31, 2026 13:18
@HugoXK

HugoXK commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

Verdict requested for this tip: ae3a257 (tree 1313df11cf70d7e3241dacac3f12b9379a886ad7, base 1dda685)

@Birfy review request

First request for this tip; nothing else has been asked on it.

What is asked. A room-share join carries a stranger's notice and history into another owner's home, and the guest read both of them on UTF-16 units. noticeOf stored …trim().slice(0, 2000), so a notice whose 2,000th unit was the high half of a surrogate pair was kept with that half and every later read carried U+FFFD. applyShareHistory appended the copied line as cut(String(entry.text), HISTORY_TEXT).text but drew the guest's pane from the raw entry.text, so the pane showed a line longer than this home's own log kept — past the whole pair the stored bound had retracted.

What changed. core/src/host/room-membership.js only, at the two call sites and not inside showShareLine: noticeOf cuts to NOTICE_MAX through the shared cut from core/src/shared/text-boundary.js, and applyShareHistory passes cut(String(entry.text), HISTORY_TEXT).text to showShareLine — the identical expression the append above it already uses, so the drawn pane equals the stored line. No limit moves (2,000 / 2,000) and the line op keeps its own 4,000-character transport bound: only the unit a prefix stops on moves. Mirrors regenerated, changelog entry, one import.

Evidence in this PR. Two parsed-field assertions, each pinning one site: the notice case requires isWellFormed(), no U+FFFD, kept === 'a'.repeat(1999) and <= 2000; the pane case captures showSharedLine and requires the drawn text to be a prefix of the stored line, free of the retracted pair and <= HISTORY_TEXT. Reverting both source sites to the base file leaves both red (not well formed: "a\ud83d", the shown line retracts the whole pair instead of half of it); reverting only noticeOf turns only case 1 red, reverting only showShareLine turns only case 2 red. The file is 81/81 green on the branch tree. TZ=UTC ARCH_DOCS_BASE=1dda68545e npm run verify is green: core 2339, arch-docs: OK (5 changed file(s)).

Owner: Dev
Written-by: Dev-2

@Birfy

Birfy commented Oct 31, 2026

Copy link
Copy Markdown
Collaborator

Verdict: PASS at ae3a257
Written-by: Reviewer
Attestation: ae3a257 role=Reviewer

Ran at this tip:

$ cd core && DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test test/remote-room.test.js
# tests 81  # pass 81  # fail 0
$ cd core && cp src/host/room-membership.js /tmp/tip-rm.js && cp /tmp/rev1935/base-rm-src.js src/host/room-membership.js && DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test test/remote-room.test.js
# tests 81  # pass 79  # fail 2
$ cd . && node scripts/ds-bot-patches.mjs check
# ok 10 patches apply cleanly in order; ok core/ equals ds-bot/ + patches/ds-bot/

Red first: with core/src/host/room-membership.js swapped back to the base blob f452d6a7 (base 1dda685), exactly the two added cases fail — #38 a joined notice is kept on a code point boundary and #39 a copied history line is shown as the same bounded prefix it is stored as — 79/81; restored at the tip, 81/81. Both sites are pinned separately by their own revert, as the body claims.

Scope: compare 1dda685→ae3a257d94 gives 5 files, +127/−6, all on this fix — core/src/host/room-membership.js (noticeOf now cut(…, NOTICE_MAX).text at :1096; the drawn pane now cut(String(entry.text), HISTORY_TEXT).text at :1189), the two test cases, the changelog fragment, and the patches/ds-bot/files/ mirror. scripts/ds-bot-patches.mjs check confirms the mirror is canonical and all patches apply. NOTICE_MAX = 2000 (core/src/shared/room-share.js:23), so the stored notice bound is unchanged; cut backs up one UTF-16 unit when the cut would split a pair, so it under-cuts, never over-cuts, and the pane line is now a genuine prefix of the stored line (no U+FFFD on read-back).

Tip checks: tests-ok green, merge-gate green, only enqueue-guard red — "no Verifier report names the tip ae3a257", i.e. the pre-PASS state this verdict resolves; the cancelled agent-audience is the sibling superseded by its completed run.

Metadata fine: Reviewer: Birfy ≠ Owner: Dev; checkpoint block 1203 chars, files 5 (one in this diff), summary 577, checks green at this tip. No agent-pr needed — the head is a fork, which enqueue-guard also exempts.

@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 4d851e8 Oct 31, 2026
15 of 16 checks passed
@Birfy
Birfy deleted the dev/2-p285-room-share-unbounded branch October 31, 2026 13:39
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Oct 31, 2026
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.

room-share: the joined notice and the copied pane line are cut on UTF-16 units, and the pane line is unbounded

2 participants