Skip to content

One local-room ticket admits one Bot: a friend and their Bot need two links, and minting a second link kills the first #878

Closed

Description

@Birfy

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)

  1. 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-invite takes bots: [...] (faceBotsToBring; src/face-rooms.ts) — do not invent a second shape. "Friend + their Bot" then costs one redemption, not two.
  2. 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.
  3. 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.localInvite is one object: { ticket, url, expiresAt, acceptedBy, acceptedAt } (core/src/host/room-membership.js). One ticket per room, and mintRoomInvite replaces it.
  • answerLocalInvite compares invite.acceptedBy !== asker (:1027, tip of feat(rooms): answer a local room's invite link (#852) #864) where asker = event.fromHandle — a handle. A second Bot, even from the same home, is a different asker and is refused consumed.
  • 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.
  • assertAddable still requires publishedRemoteOf (: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

  1. 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.
  2. 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.
  3. 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.
  4. 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 exceed maxMembers.
  5. Cap counts seats, not knocks: the same maxMembers bound as today, evaluated over the whole carried set.
  6. 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 lost join.

Not in scope

Activity

  1. added
    enhancementNew feature or request
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    area:socialFriends, invites, grants, Bot Social
    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
  2. self-assigned this
    on Oct 23, 2026
  3. Birfy commented on Oct 23, 2026

    @Birfy
    CollaboratorAuthor

    Written-by: Dev

    This issue has carried agent-working since 13:17Z with no branch, comment or PR. I am folding it into the #884 PR (branch feat/884-ticket-seat) because both rewrite answerLocalInvite and the ticket record: room.localInvites as 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.

  4. 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 SocialenhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions