Skip to content

room-share: cut the cross-owner body and history lines on code point boundaries #1928

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: 4efa9ce025e6459c8c64b0a488fe6bd8
Branch: dev/2-room-share-body-cut
Base: 881ac04
At: 2026-10-31T11:31:29Z

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.

Written-by: FlyBot Lead

What

A room-share envelope is the one place this home's text leaves for another owner's home. Three of its text fields still stop on a UTF-16 unit, so a bound that lands between the halves of a surrogate pair carries half of one across the wire — the receiving home parses it, stores it in its room log and renders U+FFFD:

  • core/src/shared/room-share.js fitTransport — the body.text shrink loop (body.text.slice(0, body.text.length - extra)) that fits a line into the 4,000-unit transport.
  • core/src/shared/room-share.js historyLine — .slice(0, HISTORY_TEXT) on a copied chat line (2,000).
  • core/src/host/room-membership.js shareHistory — seatText(line).slice(0, HISTORY_TEXT) before the envelope is even built.
  • core/src/host/room-membership.js applyShareHistory — String(entry.text).slice(0, HISTORY_TEXT) on the receiving side.

The 18,000 cap on payload.text in encodeRoomShare is the same shape and is fixed for consistency, but it cannot be shown red on its own: fitTransport always shrinks the body to 4,000 first, so that bound is unreachable.

Reproduction (live main 881ac046)

// shared/room-share.js, run on its own
encodeRoomShare({ op: 'line', roomId: 'r1', text: 'a'.repeat(3821) + '👨‍👩‍👧'.repeat(4) })
// -> wire exactly 4000 units; parseRoomShare(...).text is 3844 units,
//    isWellFormed() === false, lone high surrogate at index 3843

encodeRoomShare({ op: 'join', roomId: 'abc123', seq: 3,
  history: [{ who: 'Ann', text: 'a'.repeat(1999) + '😀' + 'b'.repeat(20) }] })
// -> parseRoomShare(...).history[0].text is 2000 units ending in '\ud83d'

Expected

Every value the envelope carries is length <= max, isWellFormed() === true, has no U+FFFD, and is a byte-for-byte UTF-8 prefix of what was typed. The numeric caps do not change (2,000 for a history line, 4,000 for the transport, 18,000 for the body).

Tests

New cases in core/test/remote-room.test.js, each pinned by content and each red on main with only the tests added:

  • fitTransport: the WZJ sample above → wire <= 4000, parsed text well formed, no U+FFFD, prefix of the input.
  • historyLine: a join envelope copies a line on a code point boundary.
  • host path: shareHistory → encodeRoomShare → parseRoomShare (assert history[0].text === 'a'.repeat(1999) — by content, never === max).
  • receiving side: applyShareHistory bounds a copied line on a code point boundary.
  • mutation HISTORY_TEXT 2000 → 1000 and each site reverted to slice must turn exactly its own case red.

Not in this change

  • room-share.js's fallback branch slices roomId to 64; it needs every droppable field gone and the wire still over 4,000 — 696 probe payloads (roomId 1/64/120, text 1…30000, every clip-capped field full) triggered it 0 times, largest wire exactly 4,000. Dead branch, and no test could pin a change to it.
  • remote-chat.js's 40-unit friend-name cut and delivery.js's 60-unit cleanName fence attribute are tracked in the display-length family (P-282), not here.

Dedupe

Checked before opening: no open or closed issue covers this (search for room-share 18000, history TEXT 2000 surrogate, fitTransport → only PR #1927, which is the peer-name change and does not touch the body or the history path). Live main grep confirms the four sites are still slice at 881ac046.

Claim record

Claimed-by: FlyBot Lead
This block is FlyBot Lead's own claim record for this issue — it is not a third-party claim.

Activity

  1. added a commit that references this issue on Oct 31, 2026
    77c9bc6
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