Skip to content

Wrap up bot-social #43 on the FlyBot side: a send into a claimed room must name its room #866

Open

Description

@Birfy

What this is

The FlyBot half of bot-social #43, to be finished now that #50 has landed. A home that joined a platform room by ticket can answer in that room, and nowhere else.

Scope revised 2026-10-23 (owner decision, after triage of the local-ticket path). The first version of this issue assumed one ticket path. There are two, and only one of them needs a room_id:

  • A local FlyBot room (#852 / #864, sendShareTo) has no face_rooms row at all. Its in-room lines are room-share envelopes over plain 1:1 (room-membership.js:1092-1113 → flybotSend via: 'room-share' → social.js:1540 → send_as_member), and their authority is the seat the ticket created. Adding room_id there would be fixing a path that does not exist: roomReach gate 1 (face_rooms row) fails for such an id, so any room_id we sent would come back room_not_shared.
  • The platform room / claimed room case that bot-social #43 describes is the pair that shares only a room: two Bots both current members of an open face room, sending over the 1:1 transport.

Only the second one gets room_id. The first one must not — that is the whole point of the negative control.

Also note what a guest's room looks like on our side: when the host answers a knock with op: 'join', join is not one of the dispatched ops (room-membership.js:1287 ask, :1288 invite, :1304 leave, :1342 line), so it falls through to recordInboundSharedRoom and the guest's room is created as origin: 'inbound-share' with sharedFrom.remoteRoomId = 'war' — a FlyBot room id. No platform room id appears anywhere on that path.

What the platform offers

bot-social #50 (merged 50bbebe4 → master b1b0b7e7) adds room_id to send_as_member and a room-scoped reach: the two Bots are current members of that open room and the room's chat.public grant is alive. Without room_id such a pair is still strangers (no_relationship); naming another room is room_not_shared.

Known gap, tracked separately: on b1b0b7e7 a claimed guest still gets no_relationship with the correct room_id — Birfy/bot-social#59. Acceptance 1 below cannot pass until that lands. Do not paper over it with a mock.

What FlyBot does today

Never passes a room. On main 38629bf4 the three call sites are plugins/social/src/social.js:1540 (the transport for every flybot.bot.send through bot-social), :1731 and :1740 (the owner-typed line). The line numbers in the original report (:1518, :1709, :1718) are stale.

Scope

  1. Step 0 — the fixture, before any code. The fake server in plugins/social/test/social.test.js:67 answers every send_as_member with a message id, and no test in that file mentions room_not_shared or no_relationship. It must learn room reach first: both Bots current members of that open room and the room's chat.public grant alive; no room_id → no_relationship; another room → room_not_shared. Until then a green suite proves nothing and the negative controls cannot stand up.
  2. Pass room_id on the platform-room path only — the pair that shares an open face room and sends over 1:1. Never on a local room line, never on a send that is not part of a room. That is the difference between "this ticket opened this room" and a general reach exception.
  3. Map the new refusals: room_not_shared, and no_relationship from a room-scoped send, each get one sentence instead of a raw platform error.

The four minimum steps that reach the local-ticket path (build the room → mint → redeem → send the first line, with the exact assertions) are in a comment on this issue; run them once to confirm the "no path needs room_id here" reading before writing the platform-room half.

Acceptance

  1. Gated on bot-social #59. A guest that claimed a platform room with a ticket, with no prior relationship, sends its first line naming that room, and the inviter's side receives an ordinary peer line.
  2. The same home sending to a Bot outside that room is still refused no_relationship; a send naming another room gets room_not_shared.
  3. Once the room is left, ended, or the grant dies, the next send is refused, and the person reads a sentence, not a code.
  4. Negative control, both ways: the same platform-room conversation sent without room_id reproduces today's refusal, and a line into a local room never carries a room_id (assert on the fake server's recorded send_as_member arguments, not on the room's shape).
  5. The fixture from step 0 is part of the PR: undoing the far end of the fake (making it answer every send again) must red exactly the room-scoped cases.

Related

FlyBot #853 — cross-repo tracking for a stranger redeeming a link.
FlyBot #852 — the in-repo link for homes that can already reach each other.
bot-social #43 — the platform issue this follows. bot-social #59 — the platform prerequisite for acceptance 1. bot-social #57 item 3 — the same findRoomGrant/roomReach predicate, decided from data.

Activity

  1. added
    area:socialFriends, invites, grants, Bot Social
    bugSomething isn't working
    on Oct 23, 2026
  2. Birfy commented on Oct 23, 2026

    @Birfy
    CollaboratorAuthor

    Status: not startable yet, so it carries no agent-ready. Gate: bot-social #50 must merge (it is at INC now — the session binds to the newest room of a pair), and this issue gets the master SHA it merged at. Triage will mark it ready then.

  3. added
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    on Oct 23, 2026
  4. Birfy commented on Oct 23, 2026

    @Birfy
    CollaboratorAuthor

    Gate cleared: bot-social #50 merged (50bbebe4 → master b1b0b7e7, 12:50:01Z), and #43 closed with it. So the prerequisite named here now exists on master: send_as_member takes room_id, and a claimed room is reach is verified per that room (platform.ts:450-456 at b1b0b7e7).

    agent-ready added. Two facts to start from, measured on FlyBot 5db75d07:

    Nothing else about the scope changed: name the room only on the path that actually carries a room line, and the two negative cases (no_relationship outside the room, room_not_shared for another room) stay as acceptance 2.

  5. added
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    and removed
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    on Oct 23, 2026
  6. self-assigned this
    on Oct 23, 2026
  7. Birfy commented on Oct 23, 2026

    @Birfy
    CollaboratorAuthor

    Minimum steps to reproduce (Triage)

    Nothing in the product mints or redeems one of these tickets yet, so "run it in the app" is not a step anyone can take today: mintRoomInvite exists only on the room record (core/src/host/room-membership.js:987), channel.js has no endpoint, the client never calls it, and parseRoomInviteLink is referenced from tests and docs only. The smallest real run is therefore the core fixture that already exercises the ticket, plus one call out through the social transport. Run against main 38629bf4 (after #864).

    1. Build the local room (host side). In core/test/remote-room.test.js, the #852: the knock asks the room record… block already holds the fixture: host(t, TEAM) → signAliceAndBob(rt) → rt.authenticate(alice.sessionToken) → publish(rt) → grantToFriend(rt, 'chief-a'), then set rt.state.bots['chief-a'].fields = { social: { member: { home: '@alice.home', handle: '@chief.a.home' } } }. The room is war.
    2. Mint the ticket. const invite = await rt.mintRoomInvite('war') (room-membership.js:987). Expect a 22-character invite.ticket and parseRoomInviteLink(invite.url) deep-equal to { kind: 'local', handle: '@chief.a.home', ticket: invite.ticket }. Note the shape: this link carries a FlyBot handle and a ticket — no frm_ platform room id anywhere in it.
    3. Redeem from the guest home. rt.applyInboundRoomShare({ text: encodeRoomShare({ op: 'ask', ticket: invite.ticket, accept: true }), fromHandle: '@coder.bob.home', fromHome: '@bob.home', fromName: 'Coder' }) → { accepted: true }, acceptedBy === '@coder.bob.home', exactly one remote seat. The host then answers with op: 'join' (notifyRoomShare). On the guest side join is not one of the dispatched ops (:1287 ask, :1288 invite, :1304 leave, :1342 line), so it falls through to recordInboundSharedRoom and the guest's room is created as origin: 'inbound-share' with sharedFrom.remoteRoomId = 'war' — a FlyBot room id, not a platform one.
    4. Send the first line from inside that room, and capture what actually leaves. rt.fanoutRoomLine(guestRoom, { who: 'Coder', text: 'hi', authorKind: 'human' }). Because guestRoom.origin === 'inbound-share' (:1092-1113), the line goes out as sendShareTo(..., 'room-share') → rt.flybotSend({ from, to, text, via: 'room-share' }) → the transport at plugins/social/src/social.js:1540 → callTool('send_as_member', { from_bot_id, to, text }). The evidence is that argument object: it carries no room_id. Assert on server.calls in plugins/social/test/social.test.js, not on the shape of the room.

    What these four steps already settle:

    • The line numbers in the body are stale. On 38629bf4 the three call sites are social.js:1540 (transport), :1731 and :1740 (owner-typed line), not :1518 / :1709 / :1718.
    • A local room's line is an envelope over 1:1, not a platform room message. Its authority is the seat the ticket created, and both ends are Bots that are already reachable to each other — the guest can only be seated at all because addRoomParticipant → assertAddable → publishedRemoteOf demands it (the @stranger.eve.home case at test/remote-room.test.js:1404 is refused not-addable). So for this room room_id is not what opens the path, and acceptance 1 as written ("the inviter's side receives an ordinary peer line") is met without it.

    What these four steps do not prove, and must not be reported as proven:

    • The refusal itself. The fake server answers every send_as_member with a message id (plugins/social/test/social.test.js:67: a => ({ conversation_id: 'c1', message: { message_id: … } })), and no test in that file mentions room_not_shared or no_relationship. Until the fake learns room-scoped reach — both Bots current members of that open room and the room's chat.public grant alive; no room_id → no_relationship, another room → room_not_shared — a green suite says nothing about scope item 2.

    So run steps 1-4 once and state in one line which path carried the line. If it is the room-share envelope, then the room_id half belongs to a pair that shares only a platform room (the claimed-room case bot-social #43 describes), which this local ticket cannot produce. Name the room that needs the field before adding it.

  8. Birfy commented on Oct 23, 2026

    @Birfy
    CollaboratorAuthor

    Platform prerequisite is now Birfy/bot-social#59 (opened from your four probes; the body carries them verbatim and adds one correction that matters for the fix).

    Correction to the diagnosis in your message: on b1b0b7e7 a room_id send cannot fail inside activeRelationship (platform.ts:446) — :455 (assert(roomRel, 403, "no_relationship")) fires first, and activeRelationship is only reached on the 1:1 fallback at :457, i.e. when roomId === null. So findActiveRelationship (:874, r.status === 'active') is not on this path. roomReach (:1050-1062) runs four gates, and gates 1-3 are the ones the :450 assert already passed; gate 4 is SELECT * FROM relationships WHERE id = ? AND status = 'room', which is stricter than findRoomGrant's r.status IN ('active','room') (:1067) — the same asymmetry as #57 item 3. #59 asks for one instrumentation run to name the null gate rather than guessing, and says the correction should be decided under #57.

    For the FlyBot half, go ahead as (a). Your reading that room_id belongs only to a pair sharing an open face room, and only on the 1:1 transport path, is consistent with what I measured: a local room (#864 sendShareTo) has no face_rooms row, so any room_id we put on it would fail roomReach gate 1.

  9. Birfy commented on Oct 23, 2026

    @Birfy
    CollaboratorAuthor

    Split (owner decision, (b)): the buildable half is now #881.

    • #881 carries everything that can be decided and pinned today: the fake server learns room reach first (test/social.test.js:67), room_id only on the platform-room 1:1 path, both refusals in sentences, and the two negative controls (platform-room send without room_id still refused; a local-room line never carries one).
    • This issue stays open as the gate until Birfy/bot-social#59 lands. It closes when the guest's first line is actually observed end to end, not when the client half merges — that is the #864/#877 discipline, and the reason not to mark this done early.
    • The PR should read Fixes #881 plus Refs #866, so the guard has a closable reference and this umbrella is not closed by a half.

    Nothing in the scope above changes; #881 restates it without the end-to-end acceptance, and this issue keeps that acceptance as its exit condition.

  10. removed
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    on Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:socialFriends, invites, grants, Bot SocialbugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions