Repository navigation
Wrap up bot-social #43 on the FlyBot side: a send into a claimed room must name its room #866
Description
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 noface_roomsrow at all. Its in-room lines areroom-shareenvelopes 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. Addingroom_idthere would be fixing a path that does not exist:roomReachgate 1 (face_roomsrow) fails for such an id, so anyroom_idwe sent would come backroom_not_shared. - The platform room / claimed room case that bot-social
#43describes 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
- Step 0 — the fixture, before any code. The fake server in
plugins/social/test/social.test.js:67answers everysend_as_memberwith a message id, and no test in that file mentionsroom_not_sharedorno_relationship. It must learn room reach first: both Bots current members of that open room and the room'schat.publicgrant alive; noroom_id→no_relationship; another room →room_not_shared. Until then a green suite proves nothing and the negative controls cannot stand up. - Pass
room_idon 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. - Map the new refusals:
room_not_shared, andno_relationshipfrom 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
- 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. - The same home sending to a Bot outside that room is still refused
no_relationship; a send naming another room getsroom_not_shared. - Once the room is left, ended, or the grant dies, the next send is refused, and the person reads a sentence, not a code.
- Negative control, both ways: the same platform-room conversation sent without
room_idreproduces today's refusal, and a line into a local room never carries aroom_id(assert on the fake server's recordedsend_as_memberarguments, not on the room's shape). - 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
Birfy commentedon Oct 23, 2026
Status: not startable yet, so it carries no
agent-ready. Gate: bot-social#50must 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.Birfy commentedon Oct 23, 2026
Gate cleared: bot-social #50 merged (
50bbebe4→ masterb1b0b7e7, 12:50:01Z), and#43closed with it. So the prerequisite named here now exists on master:send_as_membertakesroom_id, and a claimed room is reach is verified per that room (platform.ts:450-456atb1b0b7e7).agent-readyadded. Two facts to start from, measured on FlyBot5db75d07:plugins/social/src/social.js:1518(transport),:1709/:1718(owner-typed line).ask/inviteandanswerLocalInviteare in feat(rooms): answer a local room's invite link (#852) #864 (0d1f5db8, PASS, not merged). Do not rebuild a guest by hand — land feat(rooms): answer a local room's invite link (#852) #864 first, join with a ticket, then reproduce.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_relationshipoutside the room,room_not_sharedfor another room) stay as acceptance 2.Birfy commentedon Oct 23, 2026
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:
mintRoomInviteexists only on the room record (core/src/host/room-membership.js:987),channel.jshas no endpoint, the client never calls it, andparseRoomInviteLinkis 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 main38629bf4(after#864).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 setrt.state.bots['chief-a'].fields = { social: { member: { home: '@alice.home', handle: '@chief.a.home' } } }. The room iswar.const invite = await rt.mintRoomInvite('war')(room-membership.js:987). Expect a 22-characterinvite.ticketandparseRoomInviteLink(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 — nofrm_platform room id anywhere in it.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 withop: 'join'(notifyRoomShare). On the guest sidejoinis not one of the dispatched ops (:1287ask,:1288invite,:1304leave,:1342line), so it falls through torecordInboundSharedRoomand the guest's room is created asorigin: 'inbound-share'withsharedFrom.remoteRoomId = 'war'— a FlyBot room id, not a platform one.rt.fanoutRoomLine(guestRoom, { who: 'Coder', text: 'hi', authorKind: 'human' }). BecauseguestRoom.origin === 'inbound-share'(:1092-1113), the line goes out assendShareTo(..., 'room-share')→rt.flybotSend({ from, to, text, via: 'room-share' })→ the transport atplugins/social/src/social.js:1540→callTool('send_as_member', { from_bot_id, to, text }). The evidence is that argument object: it carries noroom_id. Assert onserver.callsinplugins/social/test/social.test.js, not on the shape of the room.What these four steps already settle:
38629bf4the three call sites aresocial.js:1540(transport),:1731and:1740(owner-typed line), not:1518/:1709/:1718.addRoomParticipant→assertAddable→publishedRemoteOfdemands it (the@stranger.eve.homecase attest/remote-room.test.js:1404is refusednot-addable). So for this roomroom_idis 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:
send_as_memberwith a message id (plugins/social/test/social.test.js:67:a => ({ conversation_id: 'c1', message: { message_id: … } })), and no test in that file mentionsroom_not_sharedorno_relationship. Until the fake learns room-scoped reach — both Bots current members of that open room and the room'schat.publicgrant alive; noroom_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-shareenvelope, then theroom_idhalf belongs to a pair that shares only a platform room (the claimed-room case bot-social#43describes), which this local ticket cannot produce. Name the room that needs the field before adding it.Birfy commentedon Oct 23, 2026
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
b1b0b7e7aroom_idsend cannot fail insideactiveRelationship(platform.ts:446) —:455(assert(roomRel, 403, "no_relationship")) fires first, andactiveRelationshipis only reached on the 1:1 fallback at:457, i.e. whenroomId === null. SofindActiveRelationship(:874,r.status === 'active') is not on this path.roomReach(:1050-1062) runs four gates, and gates 1-3 are the ones the:450assert already passed; gate 4 isSELECT * FROM relationships WHERE id = ? AND status = 'room', which is stricter thanfindRoomGrant'sr.status IN ('active','room')(:1067) — the same asymmetry as#57item 3.#59asks 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_idbelongs 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 (#864sendShareTo) has noface_roomsrow, so anyroom_idwe put on it would failroomReachgate 1.Birfy commentedon Oct 23, 2026
Split (owner decision, (b)): the buildable half is now
#881.#881carries everything that can be decided and pinned today: the fake server learns room reach first (test/social.test.js:67),room_idonly on the platform-room 1:1 path, both refusals in sentences, and the two negative controls (platform-room send withoutroom_idstill refused; a local-room line never carries one).Birfy/bot-social#59lands. It closes when the guest's first line is actually observed end to end, not when the client half merges — that is the#864/#877discipline, and the reason not to mark this done early.Fixes #881plusRefs #866, so the guard has a closable reference and this umbrella is not closed by a half.Nothing in the scope above changes;
#881restates it without the end-to-end acceptance, and this issue keeps that acceptance as its exit condition.Send a room-scoped line by naming the room
Send a room-scoped line by naming the room