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: 5f3d0601ea2e404ca032699771583be0
Branch: dev/2-p295-inbound-line-text-wf
Base: 1016520
At: 2026-10-31T15:17:10Z
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.
Written-by: FlyBot Lead
Owner: FlyBot Lead
Branch: dev/2-p295-inbound-line-text-wf
Base: 1016520
What happens
An inbound room-share envelope with op: 'line' carries its body in text. The receiver cleans the name (who goes through cleanPeerName, which now calls toWellFormed() after #1941) but takes the body straight from the envelope: room-membership.js:2513 does const who = cleanPeerName(data.who || transportName, transportName) and const text = data.text || ''.
So a lone surrogate in text is stored as-is, shown as-is and handed to the turn as-is. It is not a display-only problem: the line is accepted and, when its calls name a handle of ours, really starts an inbound turn.
Reproduction
Prerequisite: a clean join first (origin: 'inbound-share'), otherwise refused: 'not-seated' and nothing shows. recordInboundSharedRoom(...) or an op: 'join' envelope both work.
const raw = 'FLYBOT-ROOM-SHARE\n' + JSON.stringify({
v: 1, op: 'line', roomId: 'alice-war', name: 'War room', inviterName: 'Alice',
inviterHome: '@alice.home', inviterHandle: '@chief.alice.home', invitedHandle: '@coder.bob.home',
seq: 41, who: 'W\ud83dho', authorKind: 'human', authorId: 'alice',
text: 'hello \ud83d world', calls: ['@coder.bob.home'],
})
await rt.applyInboundRoomShare({ fromHandle: '@chief.alice.home', fromHome: '@alice.home', text: raw }, actorB)Measured on ff4a3cfb (and on old 1028aa9a / base 4d851e83; room-membership.js is untouched by both #1938 and #1941, so the shape is the same on current main): handled: true, called: ["spec-b"], no refused.
| where | text |
|---|---|
stored row rt.roomHistory(room.id, actorB).at(-1) |
"<untrusted-message from=\"W\ufffdho\">\nhello \ud83d world\n</untrusted-message>" — isWellFormed() === false |
pane (the showSharedLine spec) |
"hello \ud83d world" — false |
startInboundTurn args (botId: 'spec-b') |
"hello \ud83d world" — false |
who is already "W\ufffdho", isWellFormed() === true on that same tip: #1941 fixed the name face only.
Worth keeping in the record: inside the stored row the fence attribute from="…" is already U+FFFD, while the body keeps the raw lone surrogate. The fence builder cleans its own attribute; the body path was never covered.
Expected
A text that arrives ill-formed is made well formed before it is stored, shown, or passed to a turn — one U+FFFD per lone surrogate, length unchanged otherwise, the same shape wellFormedShare already applies to notice, roster[].name/ownerName and history[].who/text. handled: true and called: ["spec-b"] must not change: this is a repair, not a refusal.
Red-first
On base, the line above (in the room-share / room-membership suites): stored row, pane spec and turn args each isWellFormed() === false. With the fix, all three true; an already well-formed body is byte-identical; a body with a real emoji keeps its code point.
Scope
core/src/host/room-membership.js— theop: 'line'branch (:2513area): make the body well formed before it is used, next to where the name is cleaned.- Whatever the source tree carries for that file (the
patches/ds-bot/copy andfiles/mirror) —generatedecides, no hand edits. - One regression test in the existing suite, one changelog fragment, one docs line if the subsystem doc names the cleaned fields.
Not in scope: who (fixed by #1941), the notice/roster/history fields (fixed by #1938), and the server-side slices in roster.js (separate report).
Dedupe
Searched the repo (open + closed) for toWellFormed, well-formed, surrogate, untrusted-message, op:'line', line envelope: #1937/#1938/#1940 are the name face, #1908/#1927/#1928/#1930/#1933 are cut-on-code-point bounds, #1923/#1925 are roster cuts. No issue covers an inbound line's body.
Activity
host: a seat line's body keeps a lone surrogate — the seat path is ne…