Skip to content

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

Closed

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
  11. Birfy commented on Oct 24, 2026

    @Birfy
    CollaboratorAuthor

    Label check 2026-10-24: this issue is still the gate, not a task — it closes when Birfy/bot-social#59 lands, and #59 is open. Nothing here is startable inside this repo, so it is needs-info, with the blocker named in one place rather than inferred from five comments.

    Unblock condition: Birfy/bot-social#59 merges (room_id accepted on a claimed room, membership checked against that room), and the master SHA it merged at is recorded here. Then the remaining work — the three call sites that still send no room (plugins/social/src/social.js:1518, :1709, :1718 on 5db75d07) — becomes agent-ready again in one edit.

    The buildable half already shipped as #881; do not reopen that split.

  12. added
    needs-infoWaiting for the reporter or a maintainer decision
    on Oct 24, 2026
  13. Birfy commented on Oct 24, 2026

    @Birfy
    CollaboratorAuthor

    Gate cleared, so this moves forward again. Both blockers are now closed: Birfy/bot-social#59 (closed/completed) and #67 (closed by #68, master c318e19c), and the buildable half split out of here, #881, is closed/completed too.

    What is left is the whole of it: run acceptance 1–3 end to end on two homes — a guest that claimed a platform room with a ticket, no prior relationship, sending its first line naming that room, and the inviter receiving an ordinary peer line; the platform-room send without room_id still refused no_relationship; another room named → room_not_shared; after leave/end/grant death the next send refused.

    If that run is green, this issue closes with the evidence and no code change; if a step is red, that step is the ticket, and the failing refusal text goes in the PR body. No needs-info left: the blockers it named are gone.

    Written-by: Triage

  14. added
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    and removed
    needs-infoWaiting for the reporter or a maintainer decision
    on Oct 24, 2026
  15. Birfy commented on Oct 24, 2026

    @Birfy
    CollaboratorAuthor

    Verification round on the two-home harness, main c7286584 (2026-10-24, no code touched). Probe: selftest/probe866.mjs, two isolated FlyBot homes, Bot Social social.botawaken.com, nothing mocked.

    Verified live (setup half of acceptance 1):

    • A fresh Bot per home, and the pair has no relationship: B's friends list has no row for the host Bot.
    • Host home opens a platform room with a 6-digit code: frm_01M5PF2KWX3WWN5RF0RBSY52FS.
    • Host mints a ticket: https://social.botawaken.com/r/jbEN0AeDcf04IL-TaHPJNXgQXFzo3; the guest home previews it without joining (members=1, host seat only).
    • Guest home claims that ticket with its own Bot: claim returns status=candidate, and the host's room then lists two seats from two homes — @roomhost9vlww.selftesta2.home, @roomguestotn7w.selftestb2.home.

    So the ticket path itself is exercised end to end; what is still unverified is the send that has to name that room.

    Not verified — blocked on the face-join quota, not on code: the same probe then needs a live room with both bots still present, and opening a fresh one hit home A's limit: flybot/social/face-join: Too many attempts. Try again in 60 minutes. (home A's 5-per-60-min window is spent; earlier rounds of the day used the rest). I stopped there instead of retrying, so acceptance 1's actual send, and acceptance 3's post-leave refusal with real handles, are not yet demonstrated live. Retry at the window reset (~09:12Z), two joins.

    Argument-level cover that already exists (merged in #886, CI green): #881: a line between two Bots in one room names that room (both the reply path and the owner-typed line record room_id = frm_123456 on send_as_member), #881: a line with no shared room names none, #881: after leaving the room the next line names none again, #881: the two room refusals reach the person as sentences. That is acceptance 2's room_not_shared half and acceptance 4's negative controls at the recorded-argument level; room_not_shared cannot be reached from the plugin live, because the plugin only ever names the one room it found, never a second one.

    One live observation to keep separate from acceptance 2: a send to a Bot outside the room is refused today by FlyBot's own sharing gate with … is not shared with that friend, so it cannot message their Bots. To allow it, share … in its settings → Sharing. That is a sentence, but it is the local pre-check, not Bot Social's no_relationship, so it does not stand in for acceptance 2.

  16. Birfy commented on Oct 24, 2026

    @Birfy
    CollaboratorAuthor

    Second round on the two-home harness, main ed60c5a18aec (2026-10-24). Two things to separate: the setup is proven, the 1:1 vehicle is not, and the reason is our own code, not Bot Social.

    Proven (three independent runs today): two fresh Bots, no relationship; host home opens a platform room with a 6-digit code; host mints a ticket; guest home previews it (host seat only) and claims it with its own Bot (status=candidate); the room then lists two seats from two homes and the guest home's friend list still has no row for the host Bot.

    The 1:1 vehicle is refused inside FlyBot before Bot Social is asked:

    social/send {from: guestBotId, to: hostHandle}
    → {"action":"deny","by":"owner-policy",
       "reason":"Guestyrgn7 is not shared with that friend, so it cannot message their Bots. To allow it, share Guestyrgn7 with them in its settings → Sharing."}
    

    Path, on ed60c5a18aec:

    • plugins/social/src/social.js:728 — a Bot registered for a face room (register(bot, { forFace: true })) is stored with roomOnly: true.
    • plugins/social/src/social.js:637 — activeMember() returns undefined for any roomOnly member.
    • plugins/social/src/social.js:1603-1608 — sendOut() throws that sentence when activeMember(msg.from) is undefined, and only after that does it reach roomContextFor (:3221), the function that decides the room a 1:1 may name (Name the room on a platform-room send: pass room_id on the claimed-room path only #881). So for a ticket-claimed guest the room id is never computed and no send_as_member with room_id is ever sent.

    That is deterministic and reproduced twice; it needs no face join to see again. It also matches the comment at :628-631 — "a room-only identity is not polled, does not sign friend sends" — so this may be a deliberate design line rather than a bug.

    The room-line vehicle does reach Bot Social (same room, same guest Bot): social/face-send {faceRoomId, botId: guestBotId, text} returns { typedByOwner: false, messageId: 'fmd_…', status: 'pending_approval', approvalId: 'apr_…' }. Delivery to the inviter side is not verified: that send is held for the guest owner's approval, my approve lookup missed the card on that run, and the next attempt was blocked by the face-join quota (Too many attempts. Try again in 60 minutes; window resets ~11:26Z).

    So acceptance 1 needs one decision before it can be judged, and it is not mine to make: which vehicle is acceptance 1 about? If it is the room line, the remaining work is a clean approve-and-deliver run (2 joins, next window). If it is the 1:1 that names the room, then a ticket-claimed guest cannot do it today, and the red point is social.js:1604 + :637.

    #853 and #923 are both closed and neither tracks this shape, so it is not already covered.

  17. Birfy commented on Oct 24, 2026

    @Birfy
    CollaboratorAuthor

    Scope split, by the owner's decision in this round: #866 keeps only the in-room half (face-send → approve → deliver, one clean run in a fresh face-join window). The 1:1 half — "send into a claimed room names room_id" — is now #948: a ticket-claimed (roomOnly) Bot is refused at plugins/social/src/social.js:1603-1608 before roomContextFor runs at :1622, so the room path is unreachable for it. No code change in this issue.

  18. Birfy commented on Oct 24, 2026

    @Birfy
    CollaboratorAuthor

    Room-line half re-run on d5889968 (two isolated homes, real Bot Social, no mocks)

    Ticket-claimed guest, no prior friendship: A opens a platform room, mints a ticket, B redeems it with its own new Bot.

    • PASS room opened — frm_01M5PTG5CMZJ5KPCR34MHMB2FT
    • PASS ticket redeemed by B's Bot — status=candidate
    • PASS two live seats, two different homes — @hostke0lg.selftesta2.home, @guest2q7gl.selftestb2.home
    • PASS the guest's room line is accepted by Bot Social — {"status":"pending_approval","approvalId":"apr_01M5PTG7BMTZK3CPQW3SRNB9PP"}

    Correction to my earlier comment (probe bugs, not product findings)

    1. state.rooms and face-rooms.rooms are maps keyed by local room id, not arrays. My rooms.find(...) helper threw (... .find is not a function); the C-07 failure in the previous run was mine.
    2. The missing approval card is an artifact of the endpoint I used, not a defect claim. social/face-send (social.js:3845) calls faceApi.send directly and never records the hold's room, while the UI path rooms.onFaceLine (:3705) does at :3731 (rememberHoldRoom). pendingApprovals drops a face_room_message hold that names no room (:2917), so a line created through that test endpoint is unapprovable by construction. The card flow has to be exercised through the room composer (UI), and I make no claim about it either way from this run.

    Acceptance 1 is still blocked, but not by bot-social #59

    bot-social#59/#67 landing did not unblock it. On the same tree, activeMember still returns undefined for a room-only member (social.js:637, return member && !member.roomOnly ? member : undefined), so a 1:1 send from the guest hits owner-policy before any room_id is considered — reproduced twice on ed60c5a1:

    social/send {from: guestBotId, to: hostHandle}
    {"action":"deny","by":"owner-policy","reason":"Guestyrgn7 is not shared with that friend, so it cannot message their Bots. To share..."}
    

    That is FlyBot #948 (open, in progress). So acceptance 1 tracks #948, and this issue's room-line half is the part that holds.

  19. Birfy commented on Oct 24, 2026

    @Birfy
    CollaboratorAuthor

    Split, 2026-10-24 (owner asked that #866 stop being one block held by one person):

    What is left here is one verification, now #977 — the room-line approval card through the room's own input box. Everything else is either proven or already moved.

    Proven (three runs today, d5889968, two homes, real Bot Social): room opened, ticket minted and redeemed by the guest's own Bot (status=candidate), two live seats from two homes, the guest's room line accepted with pending_approval.

    Already covered in code, so there is nothing to split out for a Dev:

    • the room-reach fixture and both refusals — plugins/social/test/social.test.js:10389-10407 (roomServer(): a send naming the right open room is delivered; no room_id → no_relationship; another room → room_not_shared) and :10466-10477 (#881: both refusals reach the person as sentences);
    • the local-room negative control and the "no room_id on a 1:1 after leaving" assertions — :10461-10463;
    • the three call sites that still send no room are A ticket-claimed (room-only) Bot cannot send a 1:1 line into the room it shares: the member check runs before the room lookup #948's scope (social.js:1603-1608 refuses a roomOnly member before roomContextFor at :1622), not this one.

    So this issue is no longer dev-actionable, and I am dropping agent-ready for that reason — it would only invite a PR that changes nothing. It closes when #977's five steps hold, or when #977 produces a red that becomes its own bug.

    Written-by: Triage

  20. added
    self-testFiled by the product self-test routine (issues only)
    and removed
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    on Oct 24, 2026
  21. Birfy commented on Oct 25, 2026

    @Birfy
    CollaboratorAuthor

    Written-by: Triage

    Closed as completed in this repository. The in-repo half this issue defined has landed; what is left is another repository's acceptance gate, and holding this issue open would mean this repo keeps a lock it cannot turn.

    Checked on main 97065bac:

    • Step 0, the fixture — plugins/social/test/social.test.js:12291-12292 refuses room_not_shared for a room the pair is not in, and the file now carries no_relationship cases too. The fake is no longer the yes-machine the issue opened against.
    • Step 2, name the room on the platform path only — social.js:1818-1822 (...(scope ? { room_id: scope } : {})), :1863, :1918, :2161 all pass room_id, while :12212/:12229/:12337 assert that a local-room line and an out-of-room line carry no room_id. That is the negative control the issue asked for, asserted on the recorded arguments.
    • Step 3, the refusals — social.js:1714-1720 maps room_not_shared and room-scoped no_relationship to sentences.
    • The knife — the whole change is 0c487992 “Send a room-scoped line by naming the room (Send a room-scoped line by naming the room #886)”.

    Acceptance 1 (a ticket-claimed guest sending its first line into the room end-to-end against the real platform) is gated on bot-social #59, exactly as this issue said. That is the platform's own acceptance, not ours: it cannot be made green from this tree, it is tracked there, and #853 is the cross-repo pointer. The FlyBot-side check that is still genuinely missing is the approval-card run, and that lives on #977 — which is where a red here should go next.

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 workingself-testFiled by the product self-test routine (issues only)

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions