Repository navigation
Description
Owner decision (via Chief, 2026-10-23). The local-room ticket as it landed in #852/#864 admits one Bot handle per room and re-minting replaces the previous link — that cannot express "invite a friend and their Bot", and it cannot invite three people at once. This issue changes the ticket to a list and the redemption to a multi-identity one, in the shape the face-room ticket already uses.
Decision (three parts, all in scope)
- One ticket = one seat for one home, and the redemption may carry several Bot identities of that home. Follow the face-room semantics — bot-social's
/join-invitetakesbots: [...](faceBotsToBring;src/face-rooms.ts) — do not invent a second shape. "Friend + their Bot" then costs one redemption, not two. - A room keeps several live tickets at once, one per invitee. Minting a second link must not revoke the first. This is a list on the room record, not something the UI can fake.
- A local-room invite link cannot be redeemed by a home that cannot already reach the inviter (cross-repo: claim + connector exception) #853's boundary does not move. A home with no prior reach still cannot be seated with a valid ticket (it stays
not-addable); widening "who may sit" is a separate decision and is not smuggled in here.
What today's code does (main 38629bf4)
room.localInviteis one object:{ ticket, url, expiresAt, acceptedBy, acceptedAt }(core/src/host/room-membership.js). One ticket per room, andmintRoomInvitereplaces it.answerLocalInvitecomparesinvite.acceptedBy !== asker(:1027, tip of feat(rooms): answer a local room's invite link (#852) #864) whereasker = event.fromHandle— a handle. A second Bot, even from the same home, is a different asker and is refusedconsumed.addRoomParticipant(room.id, { kind: 'bot', handle: asker })seats exactly the asker.- Rooms seat Bots; a person is not a seat. "Friend + their Bot" is therefore two seats from one home.
assertAddablestill requirespublishedRemoteOf(:555-563) — unchanged, per decision 3.
What we ask
Record — room.localInvites as a list, each entry { ticket, url, expiresAt, acceptedBy?, acceptedAt?, seats? }. Mint appends; revoke removes one named ticket (and keeps the others); room delete / leave still clears them all. The single-field shape must not survive as a second path.
Envelope — the accepting ask may carry the extra Bot handles of the same home. The offer (ask without accept) must answer for the whole set: the room it is for, its expiry, and how many seats it would take.
Verification (fail-closed, and the whole point) — every carried handle must be attributable to the asker's home, as reported by the transport (event.fromHome), not by what the envelope says about itself. If one carried handle cannot be attributed, or the room cannot seat the whole set, refuse the entire knock (not-addable) and leave the ticket un-consumed. No partial seating: a knock that seats two of three Bots is worse than one that seats none.
Inbound side — the guest home resolves the places for the identities it carries against its own roster; an identity it does not own is not carried.
Acceptance
- Same home, two Bots, one ticket: a guest home redeeming with two of its Bots produces three seats (the inviter's Bot plus both carried Bots), all on the one grant path that admission already writes — no second grant, no per-identity ticket.
- Parallel tickets: mint a second link while the first is live → both are valid and both redeem; the first is not invalidated by the second mint. Revoking one leaves the other redeemable.
- Stranger home with a valid ticket → still
not-addable(the A local-room invite link cannot be redeemed by a home that cannot already reach the inviter (cross-repo: claim + connector exception) #853 boundary), and the ticket stays un-consumed after the refusal. - No partial seating: a knock carrying one attributable and one unattributable handle seats nobody, refuses
not-addable, and does not burn the ticket. Same for a set that would exceedmaxMembers. - Cap counts seats, not knocks: the same
maxMembersbound as today, evaluated over the whole carried set. - One ticket consumed once: a second redemption from a different home is still
consumed; the same home retrying is still idempotent and re-sends the lostjoin.
Not in scope
- Who may sit at all (A local-room invite link cannot be redeemed by a home that cannot already reach the inviter (cross-repo: claim + connector exception) #853, unchanged).
- The Members-card row and the redemption dialog (A local room's invite link has no Members-card row and no redemption dialog, and the preview card would be copied a second time #876) — they stay per the decision above ("one ticket = one seat, redemption may carry Bots"), and A local room's invite link has no Members-card row and no redemption dialog, and the preview card would be copied a second time #876 does not block this issue.
- Room-scoped
send_as_member(Wrap up bot-social #43 on the FlyBot side: a send into a claimed room must name its room #866) — the local-room path still authorises on the grant written at admission; this issue does not add aroom_idto it. - This issue does not close A local room has no invite of its own: no link to hand over, only a picker of Bots the account already knows #852; the umbrella closes when A local room's invite link has no Members-card row and no redemption dialog, and the preview card would be copied a second time #876 and this both land.
Activity
Birfy commentedon Oct 23, 2026
Written-by: Dev
This issue has carried
agent-workingsince 13:17Z with no branch, comment or PR. I am folding it into the #884 PR (branchfeat/884-ticket-seat) because both rewriteanswerLocalInviteand the ticket record:room.localInvitesas a list (mint appends, revoke names one ticket, room delete / leave clear all), and the accepting knock seating the whole claimed set of one home or nobody. If someone is mid-way on it, answer here and I will leave #878 out.feat(rooms): seat a stranger's Bots through a link Bot Social records (…
feat(social): a room link's shares reach exactly its seat, under that…
feat(rooms): Invite by link in the Members card, with its QR, and Joi…
Show the group strip the room's live link instead of minting another