Skip to content

room-share: one brace in a chat line's text drops the whole envelope #2447

Closed

Description

@HugoXK

Written-by: Delivery-1

Implementation: FlyBot (ours) — PR follows within the hour.

What happens

parseRoomShare finds the end of a room-share envelope by counting { and } from the first brace after the marker. It never tracks JSON string literals, so a { or } inside a chat line's text is counted as structure: the object looks finished too early, JSON.parse fails, and the whole envelope is dropped.

core/src/shared/room-share.js:209 at 5e71a07e (base delivery/ai-centric-development-service):

let depth = 0
let end = -1
for (let i = start; i < raw.length; i += 1) {
  const ch = raw[i]
  if (ch === '{') depth += 1
  else if (ch === '}') {
    depth -= 1
    if (depth === 0) { end = i; break }
  }
}

Braces are ordinary characters in a message, and people type them constantly (code, if (x) { … }, a lone } at the end of a dict). A balanced pair happens to survive because the two cancel out; one unbalanced brace loses the entire line.

Reproduction (at 5e71a07e, no source change)

import { encodeRoomShare, parseRoomShare } from './src/shared/room-share.js'
parseRoomShare(encodeRoomShare({ op: 'line', roomId: 'r1', seq: 7, who: 'Alice', text: 'if (x) { y()' }))
// actual:   undefined
// expected: an envelope with text 'if (x) { y()'

parseRoomShare(encodeRoomShare({ op: 'line', roomId: 'r1', seq: 7, who: 'Alice', text: 'so the closing brace is }' }))
// actual:   undefined

encodeRoomShare writes text with JSON.stringify, which leaves braces inside the string raw, so the envelope it produces is the one that cannot be read back.

What a person sees

  • A sent line never arrives. The host routes a delivered share line through parseRoomShare (core/src/host/room-membership.js:2257 ingestRemoteRoomLine, :2371 applyInboundRoomShare). An unparsed envelope is not a room line at all, so the message is dropped instead of shown.
  • A join loses its history. fitJoin/historyLine copy a room's recent lines into the join envelope; one brace in a copied line makes that whole join unparsable, so the guest's mirror stays empty and the room row reads as if no share arrived.
  • The same shape was fixed once already on the client side (plugins/social/client.js envelopeTail, fix(social): read an envelope body carrying "}", and correct the stale blanket-hold comment #855) for held-line display; the shared parser that the host actually trusts was left with the naive scan, so the same text now breaks delivery instead of only a card's wording.

Expected behaviour

  1. A line envelope whose text carries a single { or a single } parses, with op, roomId and the exact text preserved.
  2. A brace right after an escaped quote (say "{" now) is still inside the string: the scan tracks \ escapes, so a \" does not end the literal.
  3. A copied history line keeps its braces through a join envelope.
  4. Controls, unchanged: plain text and balanced braces parse as today, and an envelope that really is truncated (no closing brace) still answers undefined.

Scope

  • core/src/shared/room-share.js — parseRoomShare's object-end scan tracks string literals and escapes (the algorithm plugins/social/client.js envelopeTail already uses after fix(social): read an envelope body carrying "}", and correct the stale blanket-hold comment #855). Nothing else in the module moves: no field bound, no op rule, no marker rule, and isRoomShareText is untouched.
  • core/test/room-share-braces.test.js — the six cases above (three red on the base tree).
  • Generated, not hand-edited: patches/ds-bot/files/src/shared/room-share.js, patches/ds-bot/files/test/room-share-braces.test.js. core/client.js is unchanged — src/client/ does not import this module.

Red first (base 5e71a07e, before the source change)

$ cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/room-share-braces.test.js
not ok 1 - a line whose text opens a brace is read back, not dropped
not ok 2 - a line whose text closes a brace is read back, not dropped
not ok 3 - a brace right after an escaped quote is still inside the string
# tests 6 / # pass 3 / # fail 3

Cases 4–6 (history braces, plain/balanced controls, truncated envelope) pass before and after, so the fix cannot loosen the envelope's rejection of a genuinely broken body.

Duplicate check

Searched room share envelope brace, parseRoomShare, room-share brace: #855 (closed) is the client-side envelopeTail fix in plugins/social/client.js — same class, different file, and it explicitly left core/src/shared/room-share.js alone; #1928/#1933/#1937 are code-point/well-formedness bounds in the same module, none about the object-end scan. No open or closed issue covers this shared parser, and no open or closed PR carries this file change.

Activity

  1. added
    needs-humanThe agent loop stopped; a person must decide (reason in the last comment)
    on Nov 4, 2026
  2. Birfy commented on Nov 4, 2026

    @Birfy
    Collaborator

    Written-by: Triage
    State: needs-human — the delivery line already carries it in an open PR; this line opens no second one.

    #2447 is carried by the open PR #2448 (HugoXK:delivery1/room-share-brace-scan → delivery/ai-centric-development-service, Fixes #2447). One issue, one PR (AGENTS.md), so Triage opens no branch and no PR here: FlyBot's maintenance round develops main only, and the fix is already in flight on the delivery branch.

    Blocked on: line ownership, not on the report — the site (core/src/shared/room-share.js:209) and the reproduction in the body are specific, and nothing in them needs a rewrite. Missing: a person's decision that the delivery line keeps it. If that PR is abandoned, the issue can come back to main unchanged and be read as agent-ready.

    Written-by: Triage

  3. added a commit that references this issue on Nov 5, 2026
    58c7118
  4. removed
    needs-humanThe agent loop stopped; a person must decide (reason in the last comment)
    on Nov 5, 2026
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