Skip to content

Commit 24d306b

Browse files
Birfycursoragent
andauthored
Seat a room-link claim without waiting for the knock (#1082)
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> Owner: Dev Reviewer: Reviewer Fixes #1064 ## What changed A guest who joins a group link claims the ticket at Bot Social and then knocks. The host seated those Bots only when that accepting knock was delivered. A knock that was denied, held, or never read — a room-only Bot is not polled — left the host listing only its own Bots and the guest stuck on “Asked to join the group…”. The inbox poll now reads every open server claim (`flybot.rooms.seatOpenClaims`) and seats the Bots the claim names. The claim is the authority. A knock that does arrive is still answered, and a seat already told is not told again. A claim the host cannot finish (a Bot in the group that cannot be shown, a notice waiting on approval, or a join that did not leave) is named on the group’s Members card, and **Seat them** asks the host to finish it. Join no longer says the group was asked when nothing was claimed. No `CHANGELOG.md` edit and no version bump. The user-visible note is `changelog.d/1064-invite-claim-seat.fixed.md`. ## Verification at the tip Tip SHA: a73cbdc Rebased onto `origin/main` `2ce0e7f1e6056ff55540e07960d30264b8fd3b61` (includes #1075, #1097, #1094, #1095, #1092, #1090, #1071, #1093). `git diff origin/main...HEAD` lists only the files in this change. Conflicts were only in generated patches (`0001-core-fixes`, `0003-flybot-api-client`, `0008-client-bundle`). Sources auto-merged. `client.js` was rebuilt and `node scripts/ds-bot-patches.mjs generate` then `check` passed (`core/` equals `ds-bot/` + patches). Ran: - `TZ=UTC npm run verify` at this tip → exit 0. Core lint (arch-docs OK, 25 changed files), plugin lint, `build-client.mjs --check`, `ds-bot-patches.mjs check`, core suite 1439, social 475, tree 125, timeline 125, script tests 417, interop 37, live-e2e 16. All `# fail 0`. That run includes the #1064 seat tests and the tests that landed on main (#1048, #1053, #1054, #1087, #1088, #1089). Not run (and why): - Smoke bench (`core/dev/bench.sh`): `src/host/` changed, and there is no `deepseek-harness` checkout beside this worktree for the bench to start. Unit coverage is the two-home room harness in `core/test/room-ticket-seat.test.js`. - Desktop and a real-model steward. <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-51aa2612-d9b8-5428-bcce-8b1518847321?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/background-agent?bcId=bc-51aa2612-d9b8-5428-bcce-8b1518847321&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div> Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 2ce0e7f commit 24d306b

25 files changed

Lines changed: 670 additions & 91 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
A group invite link seats the guest's Bots when they claim it, even if the accepting knock never arrives, and the group opens in the guest's sidebar. A claim the host cannot finish is named on the group's Members card ([#1064](https://github.com/ThinkFlowLab/FlyBot/issues/1064)).

‎core/client.js‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ var ZH = {
9494
"1 earlier link still works.": "还有 1 个之前的链接仍然有效。",
9595
"{count} earlier links still work.": "还有 {count} 个之前的链接仍然有效。",
9696
"Withdraw all": "全部撤回",
97+
"{names} cannot be shown to someone who joins by link. Share them (Bot settings → Sharing) or remove them from the group, then seat the guest.": "{names} 不能给通过链接加入的人看到。请在 Bot 设置 → 共享里共享他们,或把他们移出群,然后再安排客人入座。",
98+
"A Bot in this group cannot be shown to someone who joins by link. Share that Bot or remove it, then seat the guest.": "这个群里有一个 Bot 不能给通过链接加入的人看到。请共享该 Bot 或把它移出群,然后再安排客人入座。",
99+
"A guest is in the group, but telling their FlyBot is waiting for your approval.": "客人已经在群里,但通知对方 FlyBot 还在等你批准。",
100+
"A guest is in the group, but their FlyBot has not opened it yet. Seat them again.": "客人已经在群里,但对方的 FlyBot 还没有打开这个群。请再安排一次入座。",
101+
"Seat them": "安排入座",
97102
"Paste an invite link or code": "粘贴邀请链接或邀请码",
98103
"A coding session without a Bot": "不带 Bot 的编码会话",
99104
"New Bot “{name}”": "新建 Bot「{name}」",

‎core/src/client/i18n.js‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ const ZH = {
5757
'1 earlier link still works.': '还有 1 个之前的链接仍然有效。',
5858
'{count} earlier links still work.': '还有 {count} 个之前的链接仍然有效。',
5959
'Withdraw all': '全部撤回',
60+
'{names} cannot be shown to someone who joins by link. Share them (Bot settings → Sharing) or remove them from the group, then seat the guest.': '{names} 不能给通过链接加入的人看到。请在 Bot 设置 → 共享里共享他们,或把他们移出群,然后再安排客人入座。',
61+
'A Bot in this group cannot be shown to someone who joins by link. Share that Bot or remove it, then seat the guest.': '这个群里有一个 Bot 不能给通过链接加入的人看到。请共享该 Bot 或把它移出群,然后再安排客人入座。',
62+
'A guest is in the group, but telling their FlyBot is waiting for your approval.': '客人已经在群里,但通知对方 FlyBot 还在等你批准。',
63+
'A guest is in the group, but their FlyBot has not opened it yet. Seat them again.': '客人已经在群里,但对方的 FlyBot 还没有打开这个群。请再安排一次入座。',
64+
'Seat them': '安排入座',
6065
'Paste an invite link or code': '粘贴邀请链接或邀请码',
6166
'A coding session without a Bot': '不带 Bot 的编码会话',
6267
'New Bot “{name}”': '新建 Bot「{name}」',

‎core/src/host/authz.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const BOT_TARGETED = new Set([
5050
const ROOM_TARGETED = new Set(['update-room', 'delete-room', 'room-progress', 'create-room', 'room-history', 'room-post',
5151
// #876: the room's own link. Reading it is a member's read; minting and withdrawing hand out
5252
// or kill a way in, so they follow the room's admin rule.
53-
'room-invite', 'room-invite-mint', 'room-invite-revoke'])
53+
'room-invite', 'room-invite-mint', 'room-invite-revoke', 'room-invite-seat'])
5454

5555
// Endpoints whose body names a Session.
5656
const SESSION_TARGETED = new Set(['earlier', 'send', 'answer-question', 'dismiss-question', 'approval-answer', 'exchange'])
@@ -343,7 +343,7 @@ export function install(rt) {
343343
const me = currentParticipants(room).find(p => p.kind === 'human' && p.id === actor.memberId)
344344
return roomOwnedBy(actor, id) || roomMemberOf(actor, id) || me?.role === 'admin' ? ALLOW : deny('bot/forbidden', 'You are not a member of that room')
345345
}
346-
if (ep === 'room-invite-mint' || ep === 'room-invite-revoke') {
346+
if (ep === 'room-invite-mint' || ep === 'room-invite-revoke' || ep === 'room-invite-seat') {
347347
const id = body?.id ?? body?.roomId
348348
if (roomOwnedBy(actor, id)) return ALLOW
349349
const room = state.rooms?.[id]

‎core/src/host/channel.js‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,14 @@ export function install(rt) {
412412
const bots = Array.isArray(body.bots) ? body.bots.filter(id => typeof id === 'string').slice(0, 8) : undefined
413413
return ok(await rt.askLocalInvite(String(body.link ?? body.url ?? ''), { accept: body.accept === true, ...(bots ? { bots } : {}) }, actor))
414414
}
415+
// #1064: the owner finishes a claim the poll could not (a Bot that cannot be shown, a notice
416+
// that did not leave). Same read as the poll, with the retries reset.
417+
case 'room-invite-seat': {
418+
await rt.load()
419+
const roomId = String(body.roomId ?? body.id ?? '')
420+
const seated = await rt.seatOpenClaims({ roomId, force: true })
421+
return ok({ ...seated, invite: rt.localInviteOf(roomId) ?? null, invites: rt.localInvitesOf(roomId) })
422+
}
415423
case 'folders':
416424
case 'folder-pick':
417425
case 'folder-list':

‎core/src/host/hooks.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,8 @@ export function install(rt) {
11511151
// transport lets a room share through to exactly these, under `scope`, and to nobody else off
11521152
// the roster.
11531153
ticketPeers: () => rt.ticketPeers?.() ?? [],
1154+
// #1064: seat homes that claimed an open server link, without waiting for their knock.
1155+
seatOpenClaims: (opts) => rt.seatOpenClaims?.(opts) ?? Promise.resolve({ seated: [], waiting: [] }),
11541156
},
11551157
remote: {
11561158
publish: list => remoteMembers.publish(plugin, list),

‎core/src/host/room-membership.js‎

Lines changed: 151 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,17 +1255,18 @@ export function install(rt) {
12551255
// Membership notice: the add/remove click is the owner's consent, so this does not wait
12561256
// for a chat line. Bot Social is 1:1; the envelope is the group record the other FlyBot sees.
12571257
rt.notifyRoomShare = async function notifyRoomShare(room, { op = 'join', handle } = {}) {
1258-
if (!room || isFaceRoom(room) || room.origin === 'inbound-share') return
1258+
if (!room || isFaceRoom(room) || room.origin === 'inbound-share') return []
12591259
const wanted = handle ? String(handle) : undefined
12601260
const remotes = (op === 'leave' ? (room.participants ?? []) : currentParticipants(room))
12611261
.filter(p => isRemoteParticipant(p) && (!wanted || p.handle === wanted || p.id === wanted))
1262-
if (remotes.length === 0) return
1262+
if (remotes.length === 0) return []
12631263
const from = shareInviter(room)
1264+
const results = []
12641265
for (const remote of remotes) {
12651266
if (isTicketSeat(remote)) {
12661267
const seat = seatSender(room, remote)
1267-
if (!seat) continue
1268-
await sendShareTo(room, remote, {
1268+
if (!seat) { results.push(undefined); continue }
1269+
results.push(await sendShareTo(room, remote, {
12691270
op,
12701271
roomId: room.id,
12711272
name: room.name,
@@ -1274,20 +1275,21 @@ export function install(rt) {
12741275
ticket: remote.ticket,
12751276
...(op === 'join' ? { roster: roomRoster(room), notice: room.notice || '', history: shareHistory(room) } : {}),
12761277
...seat.from,
1277-
}, 'room-share', seat.botId, room.id)
1278+
}, 'room-share', seat.botId, room.id))
12781279
continue
12791280
}
12801281
if (op !== 'leave' && !publishedRemoteOf(state, remote.handle)) continue
1281-
await sendShareTo(room, remote, {
1282+
results.push(await sendShareTo(room, remote, {
12821283
op,
12831284
roomId: room.id,
12841285
name: room.name,
12851286
invitedHandle: remote.handle,
12861287
seq: room.seq ?? 0,
12871288
...(op === 'join' ? { roster: roomRoster(room), notice: room.notice || '', history: shareHistory(room) } : {}),
12881289
...from,
1289-
}, 'room-share')
1290+
}, 'room-share'))
12901291
}
1292+
return results
12911293
}
12921294

12931295
// #852, #878, #884: the link invitations for a room that lives here. A room keeps a list of tickets
@@ -1334,6 +1336,8 @@ export function install(rt) {
13341336
...(invite.server === true ? { server: true } : {}),
13351337
...(seatedBy(invite) ? { joined: Object.keys(homesOf(invite)).length } : {}),
13361338
...(invite.withdrawnAt ? { withdrawn: true } : {}),
1339+
// #1064: a claim the poll could not finish. Local Bot names only — a guest handle is not shown.
1340+
...(invite.waiting?.reason ? { waiting: { reason: String(invite.waiting.reason), bots: (Array.isArray(invite.waiting.bots) ? invite.waiting.bots : []).map(name => cleanPeerName(name, '')).filter(Boolean).slice(0, TICKET_SEATS_MAX) } } : {}),
13371341
}
13381342
}
13391343

@@ -1686,40 +1690,24 @@ export function install(rt) {
16861690
return { handled: true, room, accepted: true }
16871691
}
16881692

1689-
// #884: the accepting knock on a ticket Bot Social minted. The knock is a request, never the
1690-
// authority: the claim is read back from the server with this home's own credential, and the seats
1691-
// are the Bots it names — all of them or none. Each way it can fail has its own reason.
1692-
async function seatByTicket(room, invite, data, event) {
1693-
const asker = String(event.fromHandle ?? '')
1694-
if (typeof rt.roomInviteServer?.verify !== 'function') return { handled: true, refused: 'lookup-failed' }
1695-
let claim
1696-
try {
1697-
claim = await rt.roomInviteServer.verify(invite.ticket)
1698-
} catch (error) {
1699-
const why = refusalOf(String(error?.code ?? ''))
1700-
return { handled: true, refused: why === 'unknown' ? 'revoked' : why }
1701-
}
1702-
if (!claim || String(claim.roomId ?? '') !== room.id) return { handled: true, refused: 'wrong-room' }
1703-
// A link admits every home it is given to (Bot Social #65): the minter reads each home's claim, and
1704-
// the knock is answered from the one whose Bots include the asker. An older server names one claimer.
1705-
const claims = Array.isArray(claim.claimers) ? claim.claimers : claim.claimer ? [claim.claimer] : []
1706-
const botsOf = c => (Array.isArray(c?.bots) ? c.bots : []).map(String).filter(handle => handle.startsWith('@'))
1707-
if (!claims.some(c => botsOf(c).length > 0)) return { handled: true, refused: 'unclaimed' }
1708-
const mine = claims.find(c => botsOf(c).includes(asker))
1709-
const seats = botsOf(mine)
1693+
const botsOfClaim = c => (Array.isArray(c?.bots) ? c.bots : []).map(String).filter(handle => handle.startsWith('@'))
1694+
1695+
// Seat one home's claim, all of its Bots or none. The claim was already read back from the server.
1696+
async function admitClaim(room, invite, mine, asker) {
1697+
const seats = botsOfClaim(mine)
17101698
const home = typeof mine?.home === 'string' ? mine.home : undefined
1711-
// The Bot that knocked must be one a claim brought, from the home that claimed it.
1712-
if (!mine || (home && event.fromHome && event.fromHome !== home)) return { handled: true, refused: 'claimed-elsewhere' }
1713-
if (seats.length > TICKET_SEATS_MAX) return { handled: true, refused: 'too-many' }
1699+
if (seats.length > TICKET_SEATS_MAX) return { refused: 'too-many' }
17141700
const names = new Map((Array.isArray(mine?.members) ? mine.members : []).map(m => [String(m?.handle ?? ''), m]))
17151701
const result = await withRoomLock(async () => {
17161702
materialize(room)
17171703
// A Bot of this room that may not be shown outside would be read by the stranger: refuse rather
17181704
// than seat a room whose lines could not all go out.
17191705
const blocked = unsharable(room).filter(p => p.id !== invite.botId)
1720-
if (blocked.length > 0) return { refused: 'not-addable' }
1706+
if (blocked.length > 0) return { refused: 'not-addable', bots: blocked.map(p => state.bots?.[p.id]?.name).filter(name => typeof name === 'string' && name) }
17211707
const fresh = seats.filter(handle => !currentParticipants(room).some(p => p.remote === true && (p.handle === handle || p.id === handle)))
17221708
const at = Date.now()
1709+
const homesBefore = JSON.stringify(homesOf(invite))
1710+
const legacy = invite.acceptedBy !== undefined || invite.seats !== undefined
17231711
invite.homes = { ...homesOf(invite), [home ?? asker]: seats }
17241712
delete invite.acceptedBy
17251713
delete invite.seats
@@ -1749,13 +1737,140 @@ export function install(rt) {
17491737
})
17501738
}
17511739
syncMembers(room)
1752-
await rt.save?.()
1740+
// A poll reads this again every few seconds: a claim that is already seated must not rewrite the team.
1741+
if (fresh.length > 0 || legacy || homesBefore !== JSON.stringify(homesOf(invite))) await rt.save?.()
17531742
return { fresh }
17541743
})
1755-
if (result.refused) return { handled: true, refused: result.refused }
1744+
if (result.refused) return { refused: result.refused, bots: result.bots }
1745+
return { seats, fresh: result.fresh }
1746+
}
1747+
1748+
// #884: the accepting knock on a ticket Bot Social minted. The knock is a request, never the
1749+
// authority: the claim is read back from the server with this home's own credential, and the seats
1750+
// are the Bots it names — all of them or none. Each way it can fail has its own reason.
1751+
async function seatByTicket(room, invite, data, event) {
1752+
const asker = String(event.fromHandle ?? '')
1753+
if (typeof rt.roomInviteServer?.verify !== 'function') return { handled: true, refused: 'lookup-failed' }
1754+
let claim
1755+
try {
1756+
claim = await rt.roomInviteServer.verify(invite.ticket)
1757+
} catch (error) {
1758+
const why = refusalOf(String(error?.code ?? ''))
1759+
return { handled: true, refused: why === 'unknown' ? 'revoked' : why }
1760+
}
1761+
if (!claim || String(claim.roomId ?? '') !== room.id) return { handled: true, refused: 'wrong-room' }
1762+
// A link admits every home it is given to (Bot Social #65): the minter reads each home's claim, and
1763+
// the knock is answered from the one whose Bots include the asker. An older server names one claimer.
1764+
const claims = Array.isArray(claim.claimers) ? claim.claimers : claim.claimer ? [claim.claimer] : []
1765+
if (!claims.some(c => botsOfClaim(c).length > 0)) return { handled: true, refused: 'unclaimed' }
1766+
const mine = claims.find(c => botsOfClaim(c).includes(asker))
1767+
const home = typeof mine?.home === 'string' ? mine.home : undefined
1768+
// The Bot that knocked must be one a claim brought, from the home that claimed it.
1769+
if (!mine || (home && event.fromHome && event.fromHome !== home)) return { handled: true, refused: 'claimed-elsewhere' }
1770+
const admitted = await admitClaim(room, invite, mine, asker)
1771+
if (admitted.refused) return { handled: true, refused: admitted.refused }
17561772
// Every seat hears the join, a retry included (the reply was lost): no second seat is made.
1757-
for (const handle of seats) await rt.notifyRoomShare?.(room, { op: 'join', handle })
1758-
return { handled: true, room, accepted: true, seats }
1773+
// A send that left is remembered so the claim poll does not send it again.
1774+
const told = new Set(Array.isArray(invite.told) ? invite.told : [])
1775+
const held = new Set(Array.isArray(invite.held) ? invite.held : [])
1776+
for (const handle of admitted.seats) {
1777+
const sent = (await rt.notifyRoomShare?.(room, { op: 'join', handle }))?.[0]
1778+
const action = sent?.action
1779+
if (action === 'allow' || action === 'sent') { told.add(handle); held.delete(handle) }
1780+
else if (action === 'hold') held.add(handle)
1781+
}
1782+
invite.told = [...told]
1783+
if (held.size > 0) invite.held = [...held]
1784+
else delete invite.held
1785+
await rt.save?.()
1786+
return { handled: true, room, accepted: true, seats: admitted.seats }
1787+
}
1788+
1789+
// Tell a seated handle once. `allow` / `sent` is delivered; `hold` is already on a card, so it is
1790+
// not sent again; a refusal is retried a few times and then left for the owner to finish.
1791+
async function tellSeats(room, invite, handles, { force = false } = {}) {
1792+
if (force) {
1793+
delete invite.tellTries
1794+
delete invite.held
1795+
}
1796+
const told = new Set(Array.isArray(invite.told) ? invite.told : [])
1797+
const held = new Set(force ? [] : (Array.isArray(invite.held) ? invite.held : []))
1798+
let outcome = 'told'
1799+
for (const handle of handles) {
1800+
if (told.has(handle)) continue
1801+
if (held.has(handle)) { outcome = 'held'; continue }
1802+
const tries = invite.tellTries?.[handle] ?? 0
1803+
if (!force && tries >= 3) { outcome = 'not-delivered'; continue }
1804+
invite.tellTries = { ...(invite.tellTries ?? {}), [handle]: tries + 1 }
1805+
const sent = (await rt.notifyRoomShare?.(room, { op: 'join', handle }))?.[0]
1806+
const action = sent?.action
1807+
if (action === 'allow' || action === 'sent') {
1808+
told.add(handle)
1809+
held.delete(handle)
1810+
const rest = { ...(invite.tellTries ?? {}) }
1811+
delete rest[handle]
1812+
invite.tellTries = rest
1813+
} else if (action === 'hold') {
1814+
held.add(handle)
1815+
outcome = 'held'
1816+
} else outcome = 'not-delivered'
1817+
}
1818+
invite.told = [...told]
1819+
if (held.size > 0) invite.held = [...held]
1820+
else delete invite.held
1821+
if (invite.tellTries && Object.keys(invite.tellTries).length === 0) delete invite.tellTries
1822+
return outcome
1823+
}
1824+
1825+
// #1064: a guest's Join claims the ticket at Bot Social and then knocks. The knock is a request,
1826+
// and it can be refused, held, or never read (a room-only Bot is not polled). The claim is the
1827+
// authority either way: read every open server link and seat the homes it names. A room that
1828+
// cannot be shown, or a notice that does not leave, stays on the link as `waiting` for the owner.
1829+
rt.seatOpenClaims = async function seatOpenClaims({ roomId, force = false } = {}) {
1830+
if (typeof rt.roomInviteServer?.verify !== 'function') return { seated: [], waiting: [] }
1831+
const wanted = roomId === undefined || roomId === null || roomId === '' ? undefined : String(roomId)
1832+
const rooms = wanted ? [rt.roomOf(wanted)].filter(Boolean) : Object.values(state.rooms || {})
1833+
const seated = []
1834+
const waiting = []
1835+
for (const room of rooms) {
1836+
if (!room || room.origin === 'inbound-share' || isFaceRoom(room)) continue
1837+
for (const invite of invitesOf(room)) {
1838+
if (invite.server !== true || !openInvite(invite)) continue
1839+
let claim
1840+
try {
1841+
claim = await rt.roomInviteServer.verify(invite.ticket)
1842+
} catch (error) {
1843+
rt.warn?.('ds-bot: a room link claim was not read: %s', error)
1844+
continue
1845+
}
1846+
if (!claim || String(claim.roomId ?? '') !== room.id) continue
1847+
const claims = (Array.isArray(claim.claimers) ? claim.claimers : claim.claimer ? [claim.claimer] : []).filter(c => botsOfClaim(c).length > 0)
1848+
const mark = () => JSON.stringify({ waiting: invite.waiting ?? null, told: invite.told ?? null, held: invite.held ?? null, tries: invite.tellTries ?? null })
1849+
const before = mark()
1850+
let blocked = null
1851+
if (claims.length === 0) {
1852+
if (invite.waiting) delete invite.waiting
1853+
} else for (const mine of claims) {
1854+
const admitted = await admitClaim(room, invite, mine, botsOfClaim(mine)[0])
1855+
if (admitted.refused === 'not-addable') {
1856+
blocked = { reason: 'not-addable', bots: admitted.bots ?? [] }
1857+
continue
1858+
}
1859+
if (admitted.refused) continue
1860+
seated.push(...(admitted.fresh ?? []))
1861+
const outcome = await tellSeats(room, invite, admitted.seats ?? [], { force })
1862+
if (outcome === 'not-delivered') blocked = blocked ?? { reason: 'not-delivered', bots: [] }
1863+
else if (outcome === 'held') blocked = blocked ?? { reason: 'held', bots: [] }
1864+
}
1865+
if (claims.length > 0) {
1866+
if (blocked) invite.waiting = { reason: blocked.reason, bots: blocked.bots, at: invite.waiting?.at ?? Date.now() }
1867+
else if (invite.waiting) delete invite.waiting
1868+
}
1869+
if (mark() !== before) await rt.save?.()
1870+
if (invite.waiting) waiting.push({ roomId: room.id, ticket: invite.ticket, reason: invite.waiting.reason, bots: invite.waiting.bots ?? [] })
1871+
}
1872+
}
1873+
return { seated, waiting }
17591874
}
17601875

17611876
// A line or a leave that a remote seat sent into a room of this home (#884). The seat must be current

0 commit comments

Comments
 (0)