@ai-matrx/meet 0.7.84 → 0.7.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3048 @@
1
+ import { P as ParticipantIdentity, $ as NoteTakerState, a as MeetParticipant, M as MeetingRecord, a7 as RoomPhase, e as ParticipantRole, S as StageLayout, L as LobbyEntry, a6 as RecordingState, U as UserId, av as RecordingSource, ab as TranscriptSegment, g as MeetingNote, W as WrapUpAnnouncement, i as CollabSnapshot, o as ConnectionQuality, j as AdmissionState, h as MeetingId, R as RoomName, b as MeetApi, d as CallInvite, f as CallMode, O as OrganizationId, l as CallId, m as CallState, X as MeetingKind, a4 as RecordingPolicy, a5 as RecordingRecord, N as MeetingAttendee, _ as MeetingSettings, Q as MeetingChanges, a1 as OccurrenceOverride, Z as MeetingOccurrence, ac as UpcomingOccurrence, V as MeetingInvitee, I as InviteeInput, p as InviteeRole, a8 as RsvpAnswer, a9 as RsvpInvitation, c as MeetIdentity, C as CollabPanel } from './collab-Bss-_Jfp.cjs';
2
+ export { A as AccessTokenSource, k as CAPTION_LANGUAGES, n as CallablePerson, E as EMPTY_COLLAB, F as FILE_SESSION_PATH, J as JsonObject, q as JsonValue, r as MeetApiOptions, s as MeetApiRequest, t as MeetBreakoutBroadcast, u as MeetBreakoutPlan, v as MeetBreakoutRoom, w as MeetCollabSettings, x as MeetFileMedia, y as MeetPoll, z as MeetPollOption, B as MeetPollState, D as MeetQuestion, G as MeetQuestionState, H as MeetStroke, K as MeetingActivities, Y as MeetingNoteKind, a0 as ORGANIZATION_HEADER, a2 as OccurrenceState, a3 as ROLE_RANK, aa as RsvpState, T as TranscriptSource, ad as applyCollabPacket, ae as asCallId, af as asMeetingId, ag as asOrganizationId, ah as asParticipantIdentity, ai as asRoomName, aj as asUserId, ak as assignedRoom, al as autoAssign, am as breakoutRemainingMs, an as collabFromState, ao as createMeetApi, ap as defaultCaptionLanguage, aq as fileDownloadPath, ar as isGuest, as as isHostRole, at as loadMeetingActivities, au as withItem } from './collab-Bss-_Jfp.cjs';
3
+ import { RealtimeManager } from '@ai-matrx/realtime';
4
+ import { MessagingSupabaseClient, ActionHandler } from '@ai-matrx/messaging';
5
+ import { RoomOptions, ConnectionQuality as ConnectionQuality$1, Participant } from 'livekit-client';
6
+
7
+ /**
8
+ * ONE error classification for the whole package (C22).
9
+ *
10
+ * The banned alternative is a host catching a LiveKit or PostgREST error and
11
+ * deciding for itself whether the camera is busy, the token expired, or the
12
+ * meeting is locked — which is how the same five-branch `if` gets copy-pasted
13
+ * into every consumer and drifts. Everything this package throws is a
14
+ * `MeetError` with a stable `code` and a `remedy` SENTENCE. Nothing fails
15
+ * silently, and nothing fails without saying what to do about it.
16
+ *
17
+ * The device codes exist because "it doesn't work" in a video product is almost
18
+ * always one of four specific, differently-remediable things: permission
19
+ * denied, no such device, device already in use by another app, or insecure
20
+ * context (http, where `getUserMedia` does not exist at all). A single
21
+ * "media-error" would make the UI say the wrong sentence three times out of
22
+ * four.
23
+ */
24
+ type MeetErrorCode = "session-unavailable" | "forbidden" | "not-found" | "conflict" | "token" | "transport" | "invalid-response" | "misconfigured" | "permission-denied" | "device-not-found" | "device-in-use" | "insecure-context" | "media-unsupported" | "room-locked" | "lobby-denied" | "capacity"
25
+ /** Meet wave 1: the meeting was cancelled — its link resolves, its room never opens. */
26
+ | "cancelled" | "unknown";
27
+ declare class MeetError extends Error {
28
+ readonly code: MeetErrorCode;
29
+ /** What to DO. Rendered to the user for the device codes. */
30
+ readonly remedy: string;
31
+ readonly cause?: unknown;
32
+ constructor(code: MeetErrorCode, message: string, remedy: string, cause?: unknown);
33
+ /** True when retrying after the host re-establishes a session is the fix. */
34
+ get isRetryable(): boolean;
35
+ /** True when the user, not the code, must act — the UI shows `remedy`. */
36
+ get isUserActionable(): boolean;
37
+ }
38
+ /**
39
+ * THE ONE PLACE a raw database/transport error becomes a typed one. Called at
40
+ * every repository boundary; nothing else in the package inspects a raw error,
41
+ * and no consumer should ever have to.
42
+ */
43
+ declare function normalizeMeetError(error: unknown, operation: string): MeetError;
44
+ /**
45
+ * THE DEVICE ERROR TRANSLATOR — the second-most-copied block in every video app.
46
+ *
47
+ * `getUserMedia` rejects with DOMException names, and every one of them means
48
+ * something a user could fix if we told them. This is the whole reason the
49
+ * device layer lives in the package (C22 test 3: less-than-perfect unless done
50
+ * perfectly).
51
+ */
52
+ declare function normalizeDeviceError(error: unknown, operation: string): MeetError;
53
+ /** A response whose SHAPE is wrong — the ingress boundary, not the wire. */
54
+ declare function invalidResponse(operation: string, detail: string): MeetError;
55
+
56
+ /**
57
+ * WHO SAID THAT — one answer, every surface, both lanes (MRI-A16).
58
+ *
59
+ * 🚨 **A RAW LIVEKIT IDENTITY IS NEVER A PERSON'S NAME.** On production
60
+ * 2026-09-09 the host's caption rail read *"Ada Lovelace: I planted a marigold
61
+ * in the courtyard"* and the guest's — the same meeting, the same second —
62
+ * read *"guest:c8-spk-1"*, because the host resolved the name from the durable
63
+ * rows it can read and the guest had no fallback at all and printed the token
64
+ * (MRI-C8). Two surfaces disagreeing about who is speaking is worse than either
65
+ * one being ugly: a person reading the transcript cannot tell whether those are
66
+ * one speaker or two.
67
+ *
68
+ * The cure is that nothing resolves a name for itself. Every source that
69
+ * happens to KNOW a name teaches this book — the LiveKit roster, a durable
70
+ * `meet_transcript_segments` row, a durable `meet_participants` row, a chat
71
+ * message, our own identity — and every surface asks it. Three rules:
72
+ *
73
+ * 1. **A name is learned, never unlearned.** Someone who leaves the room keeps
74
+ * the name they had; a caption that lands after them still says who spoke.
75
+ * This is the "last known name" rule, and it is why the book is a store
76
+ * field rather than a derivation over the current roster.
77
+ * 2. **An identity is not a name.** `learn` refuses a "name" that is the
78
+ * identity itself — which is exactly what the LiveKit roster hands back for
79
+ * a participant whose token carried no name, and exactly how the token got
80
+ * onto the screen in the first place.
81
+ * 3. **When nobody knows, SAY nobody knows.** `displayNameFor` answers with an
82
+ * honest placeholder — "Unnamed guest" / "Unnamed participant" — never a
83
+ * token, and never an invented name. The placeholder is deliberately not
84
+ * unique, so surfaces that must keep two unnamed people apart group by
85
+ * IDENTITY rather than by the rendered name.
86
+ */
87
+
88
+ /** Identity → the best display name anything has ever known for it. */
89
+ type NameBook = Readonly<Record<string, string>>;
90
+ /** A guest's identity is minted `guest:<something>` by the token route. */
91
+ declare function isGuestIdentity(identity: ParticipantIdentity | string): boolean;
92
+ /**
93
+ * Would this string be a real name for this identity? A name equal to the
94
+ * identity is the roster's way of saying it has none.
95
+ */
96
+ declare function isRealName(identity: ParticipantIdentity | string, name: string | null | undefined): boolean;
97
+ /**
98
+ * Teach the book one identity's name. Returns the SAME book when nothing was
99
+ * learned, so a store commit can be skipped rather than waking every subscriber
100
+ * on every caption.
101
+ */
102
+ declare function learnName(book: NameBook, identity: ParticipantIdentity | string, name: string | null | undefined): NameBook;
103
+ /** Teach the book several at once — a roster replace, or a durable read. */
104
+ declare function learnNames(book: NameBook, entries: readonly {
105
+ identity: ParticipantIdentity | string;
106
+ displayName?: string | null | undefined;
107
+ }[]): NameBook;
108
+ /**
109
+ * The name a surface renders for `identity`. Never a raw token, never invented.
110
+ *
111
+ * `preferred` is what the arriving thing itself claimed (a caption's roster
112
+ * lookup, a durable row's `speaker` column); it wins when it is a real name,
113
+ * because it is the freshest, and teaches the book on its way past.
114
+ */
115
+ declare function displayNameFor(book: NameBook, identity: ParticipantIdentity | string, preferred?: string | null | undefined): string;
116
+
117
+ /**
118
+ * LISTEN-ONLY ATTENDANCE — being IN the room is never contingent on a device.
119
+ *
120
+ * 0.3.1 and every version before it enabled the microphone as the last step of
121
+ * `connect()`, inside the same `try` as the join. So a denied, absent, or busy
122
+ * microphone did not produce a muted participant — it threw, the join was
123
+ * failed with a device remedy, and the user was dropped back to `<PreJoin>`
124
+ * having never entered the meeting. The whole in-room surface (tiles, captions,
125
+ * live notes, Q&A, the note-taker consent notice) was unreachable to anyone
126
+ * without a working mic, which is ordinary: a locked-down laptop, a conference
127
+ * room dial-in, a phone on a call, a browser profile that denies capture by
128
+ * policy, or a person who simply wants to listen. Every competitor allows it.
129
+ *
130
+ * THE CLASS, and how it is closed here:
131
+ *
132
+ * 1. **Connecting and publishing are two different operations.** The engine
133
+ * reaches `connected` the moment the transport is up. Media is attempted
134
+ * AFTER that, per track, and a rejection never unwinds the join.
135
+ * 2. **A track that could not be acquired is STATE, not an exception.** The
136
+ * reason and its remedy — the browser's own, translated by
137
+ * `normalizeDeviceError` — ride in the snapshot so the UI says WHY the mic
138
+ * is off instead of showing a mute button that silently does nothing.
139
+ * 3. **Enabling later runs the SAME code path.** `setMicrophoneEnabled(true)`
140
+ * from the control bar records exactly what the join attempt records, so a
141
+ * person who plugs in a headset mid-meeting is one click from speaking and
142
+ * a person whose second attempt also fails gets the same honest sentence.
143
+ * A separate "retry" path is how the two drift.
144
+ * 4. **The notice is one sentence, defined once.** Both the package's own room
145
+ * chrome and any host that renders its own read `listenOnlyNotice()`, so
146
+ * there is no second wording to keep in sync.
147
+ */
148
+
149
+ /** Why a local track is not publishing, in the browser's terms plus a remedy. */
150
+ interface MediaUnavailable {
151
+ readonly code: string;
152
+ readonly message: string;
153
+ readonly remedy: string;
154
+ }
155
+ interface LocalTrackState {
156
+ /** True only when the track is actually published. Never optimistic. */
157
+ readonly enabled: boolean;
158
+ /**
159
+ * Set when acquisition was ATTEMPTED and refused. `null` covers both "on"
160
+ * and "deliberately off" — a user who unchecked the mic in pre-join is not
161
+ * looking at an error, they are looking at a choice they made.
162
+ */
163
+ readonly unavailable: MediaUnavailable | null;
164
+ }
165
+ interface LocalMediaState {
166
+ readonly microphone: LocalTrackState;
167
+ readonly camera: LocalTrackState;
168
+ }
169
+ declare const IDLE_TRACK: LocalTrackState;
170
+ declare const IDLE_MEDIA: LocalMediaState;
171
+ declare function unavailableFrom(error: MeetError): MediaUnavailable;
172
+ /**
173
+ * THE SENTENCE. One definition, so the room chrome, a host's own chrome and the
174
+ * tests cannot say three different things about the same fact.
175
+ */
176
+ declare const LISTEN_ONLY_NOTICE = "You're attending without a microphone \u2014 others can't hear you";
177
+ /**
178
+ * True when this participant is in the room with NO USABLE microphone — the
179
+ * device was refused or is missing. A person who simply muted themselves has a
180
+ * working microphone and is not "attending without" one, so a mute never
181
+ * counts: `unavailable` is the only signal.
182
+ */
183
+ declare function isListenOnly(media: LocalMediaState): boolean;
184
+ /**
185
+ * The notice to render while in the room, or `null` when the mic is live.
186
+ * Returns the plain sentence; the reason and remedy, when there is one, are
187
+ * rendered beside it from `media.microphone.unavailable`.
188
+ */
189
+ declare function listenOnlyNotice(media: LocalMediaState): string | null;
190
+
191
+ /**
192
+ * THE NOTE-TAKER, AS A SURFACE MUST RENDER IT.
193
+ *
194
+ * 🚨 **THE PACKAGE NEVER GUESSES WHAT THE AGENT IS DOING.** Two facts decide
195
+ * everything here and both come from outside this client: the meeting row's
196
+ * `metadata.note_taker` (written by the server with a CAS, MRI-A2/D9) and
197
+ * whether the agent participant is actually in the LiveKit room. A spinner —
198
+ * "starting…" held open by a client-side timer — is the exact lie this file
199
+ * exists to prevent: it survives a worker that never came, a dispatch nobody
200
+ * claimed, and a job that died on a release.
201
+ *
202
+ * 🚨 **A REFUSAL IS RENDERED VERBATIM.** `/agent/start` answers a 503 whose body
203
+ * carries `reason` and `remedy` written by the readiness gate (D7) — "no worker
204
+ * is registered under matrx-note-taker", "every worker is at capacity",
205
+ * "the fleet is draining". Those sentences are the truth about the system and
206
+ * they reach the screen unedited. This package adds no sentence of its own on
207
+ * top of one the server already wrote.
208
+ *
209
+ * The agent's identity is DETERMINISTIC — `agent:note_taker:<meeting_id>` (D9) —
210
+ * which is what makes "is it in the room" answerable without a round trip, and
211
+ * what makes a second note-taker impossible to mistake for the first.
212
+ */
213
+
214
+ /** The one spelling of the agent's participant identity (D9). */
215
+ declare const NOTE_TAKER_IDENTITY_PREFIX = "agent:note_taker:";
216
+ declare function noteTakerIdentity(meetingId: string): string;
217
+ /**
218
+ * The participant attribute the note-taker sets on itself so EVERY client —
219
+ * including one that cannot read the token metadata — knows a notice is owed
220
+ * (D10).
221
+ */
222
+ declare const CONSENT_NOTICE_ATTRIBUTE = "matrx.agent.consent_notice";
223
+ /** Where the state lives on the meeting row. One spelling, one file. */
224
+ declare const NOTE_TAKER_METADATA_KEY = "note_taker";
225
+ /** The server's refusal, as it wrote it. Never paraphrased. */
226
+ interface NoteTakerRefusal {
227
+ readonly reason: string;
228
+ readonly remedy: string;
229
+ readonly status: number;
230
+ }
231
+ interface NoteTakerStatus {
232
+ readonly state: NoteTakerState;
233
+ /** The server's own words for why it is in this state, when it gave any. */
234
+ readonly detail: string | null;
235
+ /** True when the agent participant is in the room RIGHT NOW. */
236
+ readonly present: boolean;
237
+ /** The last dispatch refusal, rendered verbatim. */
238
+ readonly refusal: NoteTakerRefusal | null;
239
+ }
240
+ /** Is this participant the note-taker (or any agent that owes a notice, D10)? */
241
+ declare function isNoteTaker(participant: MeetParticipant): boolean;
242
+ /** The agent participant in the room, if any. */
243
+ declare function noteTakerParticipant(participants: readonly MeetParticipant[]): MeetParticipant | null;
244
+ /**
245
+ * THE STATE, from the two facts that decide it.
246
+ *
247
+ * The durable row is the authority for work. Exact room presence corroborates
248
+ * only a durable `transcribing` state; an arbitrary agent, a consent marker,
249
+ * or a stale note-taker participant never proves speech capture.
250
+ */
251
+ declare function noteTakerStatus(args: {
252
+ meeting: MeetingRecord | null;
253
+ participants: readonly MeetParticipant[];
254
+ refusal?: NoteTakerRefusal | null;
255
+ }): NoteTakerStatus;
256
+ /**
257
+ * THE SENTENCE FOR EACH STATE. One place, so the room, the panel and any future
258
+ * surface say the same true thing — and so a state can never render as a bare
259
+ * enum name.
260
+ */
261
+ declare function noteTakerLabel(status: NoteTakerStatus): string;
262
+
263
+ /**
264
+ * THE ROOM SNAPSHOT — one immutable object the UI renders and nothing else.
265
+ *
266
+ * Deliberately NOT Redux, NOT a context full of `useState`, and NOT a bag of
267
+ * refs: one store with `getSnapshot`/`subscribe` is exactly what
268
+ * `useSyncExternalStore` wants, which is what makes tearing impossible when a
269
+ * roster event and a speaker event land in the same tick.
270
+ *
271
+ * The rules encoded here:
272
+ *
273
+ * - **The roster is replaced wholesale, never patched.** The driver hands us
274
+ * the full truth on every media event (see `room-driver.ts`); merging deltas
275
+ * from six event sources is how a tile for someone who left stays on screen.
276
+ * - **Ephemera EXPIRE.** Reactions and speaking flags are time-boxed in the
277
+ * snapshot, so a dropped "stop" event cannot leave a permanent 🎉 or a
278
+ * permanently-speaking ring. (Nothing here relies on receiving an end event.)
279
+ * - **An interim transcript line is REPLACED in place by its final.** Appending
280
+ * both is how a transcript ends up saying everything twice.
281
+ */
282
+
283
+ interface RoomChatMessage {
284
+ readonly id: string;
285
+ readonly identity: ParticipantIdentity;
286
+ readonly displayName: string;
287
+ readonly text: string;
288
+ readonly at: string;
289
+ /**
290
+ * Set for a PRIVATE message: the one identity it was addressed to (from the
291
+ * sender's side) or this participant (from the receiver's). Private messages
292
+ * travel the data plane only and are never saved (Meet wave 2).
293
+ */
294
+ readonly to?: ParticipantIdentity | null | undefined;
295
+ /** True once the server has SAVED it — a message everyone sees, late joiners too. */
296
+ readonly saved?: boolean | undefined;
297
+ /** A send the server refused, with its sentence. The message stays visible, marked. */
298
+ readonly failed?: string | undefined;
299
+ }
300
+ /**
301
+ * A HOST ACTED ON *ME* (Meet wave 2) — the server's own `matrx.meet.moderation`
302
+ * packet, addressed to this participant. Rendered once, dismissible, in words.
303
+ */
304
+ interface ModerationNotice {
305
+ readonly kind: "muted" | "ask-unmute" | "removed" | "role";
306
+ readonly text: string;
307
+ readonly at: number;
308
+ }
309
+ /** What this participant chose for their own media. Remembered per browser. */
310
+ interface MediaEffects {
311
+ readonly noiseSuppression: boolean;
312
+ readonly background: "none" | "blur" | "image";
313
+ readonly backgroundImage: string | null;
314
+ }
315
+ declare const DEFAULT_EFFECTS: MediaEffects;
316
+ interface RoomReaction {
317
+ readonly id: string;
318
+ readonly identity: ParticipantIdentity;
319
+ readonly emoji: string;
320
+ readonly at: number;
321
+ }
322
+ /**
323
+ * 🚨 WHAT THE SERVER GRANTED THIS JOINER, straight off the token response
324
+ * (MRI-A11). It is placed in the snapshot the instant the token lands — before
325
+ * the LiveKit roster exists at all — so a host's controls are correct on the
326
+ * first frame of the room rather than one roster event later. The roster entry
327
+ * for the local participant carries the SAME decision as a LiveKit attribute;
328
+ * the two agree because one server statement produced both, and the UI reads
329
+ * whichever has arrived.
330
+ */
331
+ interface MeetGrants {
332
+ readonly role: ParticipantRole;
333
+ readonly canRecord: boolean;
334
+ readonly canPublish: boolean;
335
+ readonly canSubscribe: boolean;
336
+ }
337
+ /** No token yet grants nothing. Never "assume host until told otherwise". */
338
+ declare const NO_GRANTS: MeetGrants;
339
+ interface MeetSnapshot {
340
+ readonly phase: RoomPhase;
341
+ readonly meeting: MeetingRecord | null;
342
+ readonly localIdentity: ParticipantIdentity | null;
343
+ /** The server's word on what THIS joiner may do. See `MeetGrants`. */
344
+ readonly grants: MeetGrants;
345
+ readonly participants: readonly MeetParticipant[];
346
+ readonly activeSpeakers: readonly ParticipantIdentity[];
347
+ readonly pinned: ParticipantIdentity | null;
348
+ readonly layout: StageLayout;
349
+ readonly lobby: readonly LobbyEntry[];
350
+ readonly locked: boolean;
351
+ /**
352
+ * 🚨 THE DURABLE `meet_recordings.state`, not a client's hope. Through 0.4.7
353
+ * this only ever held what THIS browser optimistically patched — `starting`
354
+ * on the click, `stopping` on the stop — so the indicator read "Starting
355
+ * recording…" for the whole meeting while a perfectly healthy egress ran, and
356
+ * "Stopping recording…" forever after (MRI-C8). The server's transitions
357
+ * arrive on two transports now: the `meet_recordings` row for a member, and
358
+ * a `matrx.meet.recording` room event for a guest, who has no database.
359
+ */
360
+ readonly recording: RecordingState;
361
+ readonly recordingStartedBy: UserId | null;
362
+ /**
363
+ * WHICH transport last spoke, so the two cannot undo each other (D16). The
364
+ * database wins: `"record"` is never overwritten by `"room"` or `"local"`.
365
+ */
366
+ readonly recordingSource: RecordingSource;
367
+ /**
368
+ * WHICH recording this state is about. It is what tells a LATE packet for a
369
+ * finished recording apart from the FIRST packet of the next one — without
370
+ * it, "the database wins" either blocks a second recording forever or lets a
371
+ * stale packet drag a finished one back to `recording`.
372
+ */
373
+ readonly recordingId: string | null;
374
+ readonly chat: readonly RoomChatMessage[];
375
+ /** Whether the SAVED chat has been read for this meeting (late joiners see history). */
376
+ readonly chatLoaded: boolean;
377
+ /** Why the saved chat could not be read on join, in words — or null. */
378
+ readonly chatHistoryError: string | null;
379
+ readonly reactions: readonly RoomReaction[];
380
+ /**
381
+ * The participant a HOST spotlighted for everyone, from the room's metadata
382
+ * (server-set, delivered on join). A local pin outranks it on this screen only.
383
+ */
384
+ readonly spotlight: ParticipantIdentity | null;
385
+ /** The last thing a host did to THIS participant, in words. Dismissible. */
386
+ readonly moderationNotice: ModerationNotice | null;
387
+ /**
388
+ * The server's answer to the last host action THIS participant took ("Muted 3
389
+ * people", or the refusal). One place, so whichever surface acted — a tile's
390
+ * menu, the People panel, More options — the sentence survives the menu closing.
391
+ */
392
+ readonly actionNotice: {
393
+ readonly text: string;
394
+ readonly failed: boolean;
395
+ readonly at: number;
396
+ } | null;
397
+ /**
398
+ * "Message privately" asked for from ANY per-person menu — a tile's, the
399
+ * People panel's — and answered by the one `ControlBar` that owns the chat
400
+ * panel (0.7.8). Through 0.7.7 only the People panel could open a private
401
+ * chat, so the Board's tile menu (and the Room's) had no such item while the
402
+ * People panel did: two menus for one person that disagreed.
403
+ */
404
+ readonly privateChatRequest: {
405
+ readonly to: ParticipantIdentity;
406
+ readonly at: number;
407
+ } | null;
408
+ readonly effects: MediaEffects;
409
+ /**
410
+ * Captions AND durable rows, reconciled into one list (D16). A record
411
+ * replaces the caption it supersedes; a caption never overwrites a record.
412
+ */
413
+ readonly transcript: readonly TranscriptSegment[];
414
+ readonly notes: readonly MeetingNote[];
415
+ /**
416
+ * 🚨 IDENTITY → THE BEST NAME ANYTHING HAS EVER KNOWN (MRI-A16). Learned from
417
+ * every source that has one — the roster, a durable segment, a durable
418
+ * attendance row, a chat message — and never unlearned, so a caption that
419
+ * lands after somebody leaves still says who spoke. It exists because the
420
+ * host and the guest rendered DIFFERENT names for the same speaker in the
421
+ * same second, one of them a raw LiveKit token (MRI-C8). See `core/names.ts`.
422
+ */
423
+ readonly names: NameBook;
424
+ /**
425
+ * The wrap-up pass ANNOUNCED itself (MRI-A12). `null` means it has not been
426
+ * announced in this room — which is not the same as "no summary exists", and
427
+ * no surface may render it as such.
428
+ */
429
+ readonly wrapUp: WrapUpAnnouncement | null;
430
+ /**
431
+ * The server's last refusal to dispatch the note-taker, in ITS words (D7).
432
+ * Rendered verbatim; never replaced by a spinner or a house sentence.
433
+ */
434
+ readonly noteTakerRefusal: NoteTakerRefusal | null;
435
+ /**
436
+ * What the LOCAL participant is actually publishing, and why not when it is
437
+ * not. Attendance never depends on it (see `attendance.ts`): a refused
438
+ * microphone is a state with a remedy, not a failed join.
439
+ */
440
+ readonly media: LocalMediaState;
441
+ /** The last failure, with its remedy. Never null-and-hoping. */
442
+ readonly failure: {
443
+ readonly message: string;
444
+ readonly remedy: string;
445
+ } | null;
446
+ /**
447
+ * Meet wave 5: polls, Q&A, the shared whiteboard and the breakout plan — what the
448
+ * server said, per viewer (`core/collab.ts`). Read once per join, then packets.
449
+ */
450
+ readonly collab: CollabSnapshot;
451
+ /** The breakout room THIS participant is in, or null for the main room. */
452
+ readonly breakoutRoomId: string | null;
453
+ }
454
+ /** Reactions live this long. A dropped end event cannot outlive it. */
455
+ declare const REACTION_TTL_MS = 5000;
456
+ interface MeetStore {
457
+ snapshot(): MeetSnapshot;
458
+ subscribe(listener: () => void): () => void;
459
+ patch(next: Partial<MeetSnapshot>): void;
460
+ setParticipants(participants: readonly MeetParticipant[]): void;
461
+ setSpeaking(identities: readonly ParticipantIdentity[]): void;
462
+ setQuality(identity: ParticipantIdentity, quality: ConnectionQuality): void;
463
+ /**
464
+ * Raise or lower a hand. `at` is WHEN it went up, which is the hand queue's
465
+ * order — first raised, first called on, the way Meet orders it.
466
+ */
467
+ setHand(identity: ParticipantIdentity, raised: boolean, at?: number): void;
468
+ addChat(message: RoomChatMessage): void;
469
+ /** The saved chat, read once per join. Merged with anything that arrived live. */
470
+ loadChat(messages: readonly RoomChatMessage[]): void;
471
+ /** Replace a pending message (by id) with what the server saved, or mark it failed. */
472
+ settleChat(localId: string, next: RoomChatMessage | {
473
+ failed: string;
474
+ }): void;
475
+ addReaction(reaction: RoomReaction): void;
476
+ /** Drops reactions past their TTL. Called by the UI's animation frame. */
477
+ expireReactions(now: number): void;
478
+ /**
479
+ * The recording reached a state, and WHO says so. `"local"` is this browser's
480
+ * optimistic `starting`/`stopping`; `"room"` is the server's packet on the
481
+ * media plane; `"record"` is the durable row. Precedence is D16's: the
482
+ * database's word stands until the database changes it.
483
+ */
484
+ setRecording(state: RecordingState, by: UserId | null, source: RecordingSource, recordingId?: string | null): void;
485
+ upsertLobby(entry: LobbyEntry): void;
486
+ setAdmission(identity: ParticipantIdentity, state: AdmissionState, by: UserId | null): void;
487
+ /**
488
+ * Teach the name book. Every source that knows a name calls this, and only
489
+ * a REAL name is learned — an identity handed back as its own name is not one.
490
+ */
491
+ learn(entries: readonly {
492
+ identity: ParticipantIdentity;
493
+ displayName?: string | null;
494
+ }[]): void;
495
+ /** A live caption from the media plane. A HINT (D16). */
496
+ ingestCaption(segment: TranscriptSegment): void;
497
+ /** A durable `meet_transcript_segments` row. THE TRANSCRIPT (D16). */
498
+ ingestRecord(segment: TranscriptSegment): void;
499
+ /** Durable notes are re-written in place — a re-run rewrites the same ids. */
500
+ upsertNote(note: MeetingNote): void;
501
+ /** The wrap-up pass finished. Announced on the media plane only (MRI-A12). */
502
+ announceWrapUp(announcement: WrapUpAnnouncement): void;
503
+ /** Record what a local track ACTUALLY did — published, or refused and why. */
504
+ setLocalTrack(kind: "microphone" | "camera", state: LocalTrackState): void;
505
+ reset(): void;
506
+ }
507
+ declare function createMeetStore(initial?: Partial<MeetSnapshot>): MeetStore;
508
+
509
+ /**
510
+ * AI-NATIVE, ON BY DEFAULT — and SERVER-OWNED as of 0.3.0 (R4, D2, D16).
511
+ *
512
+ * 🚨 **THE PACKAGE NO LONGER KNOWS WHO ANSWERS.** Through 0.2.1 a host injected
513
+ * `MeetAgents` — four database row ids — and this module ran the agent itself,
514
+ * shipping the transcript up as `variables`. Both halves of that are gone:
515
+ *
516
+ * 1. **No agent id crosses this boundary, and none is injected.** Reasoning runs
517
+ * behind Mandates whose Holder is resolved from the database at run time
518
+ * (`meet.live_notes`, `meet.live_intelligence`, `meet.wrap_up`), so an
519
+ * organization rebinds the job in the Mandate admin with no release of this
520
+ * package and no change in the host. A host that had to supply an id was a
521
+ * host that could supply the WRONG id, and a capability that silently
522
+ * disappeared when nobody had filled a config line.
523
+ * 2. **No transcript crosses this boundary either.** `POST /v1/meet/intelligence/ask`
524
+ * takes `{meeting_id, question}` and NOTHING else; the answer is built
525
+ * server-side from durable rows. Captions are hints and this client holds
526
+ * only hints (D16) — sending them would be sending the model our guesses.
527
+ *
528
+ * What is left here is small and honest: ask a question and render the answer
529
+ * as it streams, summon or dismiss the note-taker, end the meeting for
530
+ * everyone, and report the server's refusal in the server's own words.
531
+ *
532
+ * 🚨 **THE USER-INPUT LAW still holds** and is now the server's to keep:
533
+ * `question` is the one genuine human utterance in this module, and it is the
534
+ * only free text this package sends anywhere near a model.
535
+ */
536
+
537
+ interface MeetAiOptions {
538
+ /** The ONE org-aware aidream client (MRI-A16). Owns base url and headers. */
539
+ api: MeetApi;
540
+ store: MeetStore;
541
+ /**
542
+ * Resolves the current meeting's room pass for a guest Q&A request. The
543
+ * provider owns this because it alone has both the active room and its
544
+ * persisted guest pass. `null` leaves the header absent for signed-in record
545
+ * Q&A, where the session remains the credential.
546
+ */
547
+ resolveRoomToken?: (() => string | null) | undefined;
548
+ }
549
+ /** What the answer was built from. The server measures it; we never guess it. */
550
+ interface MeetAnswerCoverage {
551
+ readonly segmentsUsed: number;
552
+ readonly segmentsTotal: number;
553
+ readonly truncated: boolean;
554
+ readonly speakers: readonly string[];
555
+ }
556
+ interface MeetAiAnswer {
557
+ readonly text: string;
558
+ readonly coverage: MeetAnswerCoverage | null;
559
+ }
560
+ interface AskArgs {
561
+ readonly meetingId: MeetingId;
562
+ readonly question: string;
563
+ readonly signal?: AbortSignal | undefined;
564
+ /** Called with the answer so far, every time it grows. Rendering is live. */
565
+ readonly onText?: ((text: string) => void) | undefined;
566
+ /** The server's phase, verbatim, so a slow answer is never a bare spinner. */
567
+ readonly onPhase?: ((phase: string) => void) | undefined;
568
+ }
569
+ interface NoteTakerDispatch {
570
+ readonly dispatched: boolean;
571
+ /** The server's sentence. Never empty on a refusal. */
572
+ readonly detail: string;
573
+ }
574
+ interface MeetAi {
575
+ /** In-meeting Q&A. The ONE human utterance this package sends (D16). */
576
+ ask(args: AskArgs): Promise<MeetAiAnswer>;
577
+ /** Ask aidream to dispatch the note-taker INTO the room. Host-only. */
578
+ startNoteTaker(args: {
579
+ meetingId: MeetingId;
580
+ roomName: RoomName;
581
+ }): Promise<NoteTakerDispatch>;
582
+ /** Dismiss it. Nothing automatic brings a dismissed note-taker back (D9). */
583
+ stopNoteTaker(args: {
584
+ meetingId: MeetingId;
585
+ }): Promise<NoteTakerDispatch>;
586
+ }
587
+ /** The event vocabulary `/intelligence/ask` emits, pinned by the server's FEATURE.md. */
588
+ declare const MEET_ANSWER_EVENTS: {
589
+ readonly chunk: "chunk";
590
+ readonly info: "info";
591
+ readonly phase: "phase";
592
+ readonly error: "error";
593
+ readonly end: "end";
594
+ };
595
+ /** The `info` event carrying what the answer was built from. */
596
+ declare const MEET_ANSWER_CONTEXT_EVENT = "meet_intelligence_context";
597
+
598
+ interface ServerRefusal {
599
+ readonly reason: string;
600
+ readonly remedy: string;
601
+ }
602
+ /**
603
+ * THE SERVER'S REFUSAL, READ RATHER THAN RE-WRITTEN.
604
+ *
605
+ * aidream answers a refused dispatch with a body carrying its own `reason` and
606
+ * `remedy` — "no worker is registered under matrx-note-taker", and what to do
607
+ * about it. FastAPI nests a raised `HTTPException` under `detail`, and a
608
+ * validation failure produces a list. All three shapes are read here so the
609
+ * screen shows the system's own words instead of "something went wrong".
610
+ */
611
+ declare function readServerRefusal(body: unknown): ServerRefusal | null;
612
+ declare function createMeetAi(options: MeetAiOptions): MeetAi;
613
+
614
+ /**
615
+ * CAPTIONS ARE HINTS; SEGMENTS ARE TRUTH (D16).
616
+ *
617
+ * Two independent streams describe the same speech and they arrive out of order
618
+ * with different identities:
619
+ *
620
+ * 1. **Captions** ride the LiveKit `lk.transcription` text stream, keyed by
621
+ * `lk.segment_id`, interim-then-final, milliseconds behind the voice. They
622
+ * exist so a person can READ the room live. Their `sender_identity` is
623
+ * supplied by the publishing client and LiveKit does not validate it, so
624
+ * nothing downstream — notes, Q&A, the wrap-up — may ever read them.
625
+ * 2. **Records** are `communication.meet_transcript_segments` rows, written
626
+ * server-side by the note-taker with a deterministic id (D6) and delivered
627
+ * here by Postgres Changes. They are the meeting's transcript.
628
+ *
629
+ * The reconciliation below is what keeps the screen from saying everything
630
+ * twice. A record REPLACES the caption it supersedes — matched by the caption
631
+ * id the worker carries into the row, and failing that by speaker + text — and
632
+ * a caption never overwrites a record, however late it lands.
633
+ *
634
+ * Everything here is a pure function over a list so the rules are provable
635
+ * without a store, a socket, or a browser.
636
+ */
637
+
638
+ /** Comparable text: case, punctuation and spacing are not differences. */
639
+ declare function normalizeSpoken(text: string): string;
640
+ /**
641
+ * A caption arrived. Interim replaces interim in place; a final never loses to a
642
+ * late interim; a record is never touched.
643
+ */
644
+ declare function mergeCaption(list: readonly TranscriptSegment[], caption: TranscriptSegment): readonly TranscriptSegment[];
645
+ /**
646
+ * A durable row arrived. It replaces the caption it supersedes — in place, so
647
+ * the line does not jump down the panel — or lands as a new line if no caption
648
+ * ever showed it (a surface that joined late, or captions that never came).
649
+ */
650
+ declare function mergeRecord(list: readonly TranscriptSegment[], record: TranscriptSegment): readonly TranscriptSegment[];
651
+ /** The transcript a person reads: chronological, and never an empty line. */
652
+ declare function readableTranscript(list: readonly TranscriptSegment[]): readonly TranscriptSegment[];
653
+ /** Build a caption line from the media plane. */
654
+ declare function captionSegment(args: {
655
+ captionId: string;
656
+ meetingId: TranscriptSegment["meetingId"];
657
+ identity: ParticipantIdentity;
658
+ speaker: string;
659
+ text: string;
660
+ final: boolean;
661
+ startedAt: string;
662
+ }): TranscriptSegment;
663
+
664
+ /**
665
+ * THE DATA CONTRACT — the ONE place a meet table or RPC name exists.
666
+ *
667
+ * 🚨 **STATE OF THE SCHEMA (read this before believing a green test).** The
668
+ * `communication.meet_*` tables below are the canonical schema DESIGNED by this
669
+ * package per R8; they are **not deployed to Matrx Main yet**. They were placed
670
+ * by inspecting the live database on 2026-08-31: `communication` is the schema
671
+ * that already holds `dm_*` (messaging) and `sms_*` (telephony), so meet takes
672
+ * the `meet_*` prefix in the same schema rather than inventing a new one. Until
673
+ * the migration lands, every read here fails LOUDLY with `not-found` and a
674
+ * remedy naming this file — never silently, and never with a fabricated row.
675
+ * The migration and the aidream server half are one deliverable; see FEATURE.md
676
+ * § Deliberately NOT in this package.
677
+ *
678
+ * Four rules hold this file together — the same four that hold messaging's:
679
+ *
680
+ * 1. **Org is explicit on every write (R5).** Not defaulted by a trigger we
681
+ * hope fires — passed, and refused in-package when absent.
682
+ * 2. **Authorization is the database's job.** There is no permission branch in
683
+ * this file. An RPC that says no is a `forbidden` MeetError, never a
684
+ * client-side re-decision. Guest capability is scoped by the room TOKEN, not
685
+ * by a row this client could write.
686
+ * 3. **Writes go through SECURITY DEFINER RPCs; reads are RLS-backed table
687
+ * reads** (C17's hybrid ruling). Every write here is an RPC, which is also
688
+ * what keeps admission and settle-a-call ATOMIC — two hosts clicking "admit"
689
+ * and a callee accepting on two devices are the ordinary case, not the edge.
690
+ * 4. **Pagination is keyset, terminated by a unique column.** `(created_at,
691
+ * id)` — never a timestamp alone, which duplicates or skips rows whenever
692
+ * two share a millisecond.
693
+ *
694
+ * The Supabase client is accepted STRUCTURALLY, through the shape
695
+ * `@ai-matrx/messaging` already owns. One definition of "a Supabase client" for
696
+ * the whole communications family; this package adds no second copy.
697
+ *
698
+ * 🚨 AND THE PUBLIC BOUNDARY IS THE SHALLOW ONE (0.2.1). `RepositoryOptions`
699
+ * and `<MeetProvider client>` take `MessagingSupabaseClient` — method names and
700
+ * arity, results as `unknown` — never the rich `SupabaseLike`. Checking a
701
+ * host's real `SupabaseClient<Database>` (built from generated types) against
702
+ * the rich shape makes tsc chase PostgREST's schema machinery until it bails
703
+ * with `TS2589: Type instantiation is excessively deep and possibly infinite`,
704
+ * so the only way to pass the client a host ALREADY HAS was a cast — which is
705
+ * what the first real consumer had to write (register MRI-D1). Messaging 0.9.0
706
+ * learned this and wrote it down; this package did not apply the lesson until
707
+ * now. The rich shape stays the INTERNAL contract, and we narrow to it ONCE,
708
+ * here, in-package.
709
+ */
710
+
711
+ /** The schema. Meet is never in `public`. */
712
+ declare const MEET_SCHEMA = "communication";
713
+ declare const TABLES: {
714
+ readonly meetings: "meet_meetings";
715
+ readonly participants: "meet_participants";
716
+ readonly invites: "meet_call_invites";
717
+ readonly recordings: "meet_recordings";
718
+ readonly transcripts: "meet_transcript_segments";
719
+ readonly notes: "meet_notes";
720
+ /** Wave 1 (0.8): who a meeting was sent to, and what they answered. */
721
+ readonly invitees: "meet_invitees";
722
+ /** Wave 1 (0.8): one occurrence of a series cancelled or moved. */
723
+ readonly occurrenceOverrides: "meet_occurrence_overrides";
724
+ };
725
+ declare const RPCS: {
726
+ /** Atomic: create the invite row and stamp its expiry in one statement. */
727
+ readonly createCallInvite: "meet_create_call_invite";
728
+ /** Atomic settle. Refuses a transition out of a terminal state. */
729
+ readonly settleCallInvite: "meet_settle_call_invite";
730
+ /** Every invite still ringing this user — the reconnect backfill door. */
731
+ readonly pendingCallInvites: "meet_pending_call_invites";
732
+ /** Instant meeting, or the durable one behind a recurring link. */
733
+ readonly getOrCreateMeeting: "meet_get_or_create_meeting";
734
+ /** Resolve a durable public link, before/during/after its scheduled time. */
735
+ readonly meetingBySlug: "meet_meeting_by_slug";
736
+ readonly admit: "meet_admit_participant";
737
+ readonly setLock: "meet_set_lock";
738
+ readonly endMeeting: "meet_end_meeting";
739
+ readonly recordConsent: "meet_record_consent";
740
+ /** Schedule with every setting at create; unset settings come from the knobs. */
741
+ readonly scheduleMeeting: "meet_schedule_meeting";
742
+ /** Edit / reschedule — a JSON patch with a version compare-and-swap. */
743
+ readonly updateMeeting: "meet_update_meeting";
744
+ /** Cancel is a state (`cancelled_at`); the link keeps resolving. */
745
+ readonly cancelMeeting: "meet_cancel_meeting";
746
+ /** Archive (soft delete) — the invitations and exceptions cascade. */
747
+ readonly archiveMeeting: "meet_archive_meeting";
748
+ readonly restoreMeeting: "meet_restore_meeting";
749
+ /** Cancel, move or restore ONE occurrence of a series by its original start. */
750
+ readonly setOccurrence: "meet_set_occurrence";
751
+ readonly addInvitees: "meet_add_invitees";
752
+ readonly setInviteeRole: "meet_set_invitee_role";
753
+ readonly removeInvitee: "meet_remove_invitee";
754
+ /** A signed-in invitee answers for themselves. */
755
+ readonly respond: "meet_respond";
756
+ /** The emailed RSVP link (no account): read and answer. */
757
+ readonly invitationByToken: "meet_invitation_by_token";
758
+ readonly respondByToken: "meet_respond_by_token";
759
+ /** One meeting's occurrences between two instants (INVOKER — the reader's RLS). */
760
+ readonly meetingOccurrences: "meet_meeting_occurrences";
761
+ /** Every occurrence the reader can see between two instants — the Upcoming list. */
762
+ readonly occurrencesBetween: "meet_occurrences_between";
763
+ /** The recordings library. */
764
+ readonly myRecordings: "meet_my_recordings";
765
+ /** Titles, agendas, current notes and final transcript lines; snippets mark hits with ⟦ ⟧. */
766
+ readonly search: "meet_search";
767
+ };
768
+ type Row = Record<string, unknown>;
769
+ interface CreateCallInviteInput {
770
+ readonly roomName: RoomName;
771
+ readonly mode: CallMode;
772
+ readonly callerUserId: UserId;
773
+ readonly callerName: string;
774
+ readonly callerAvatarUrl: string | null;
775
+ readonly calleeUserIds: readonly UserId[];
776
+ readonly organizationId: OrganizationId;
777
+ readonly conversationId: string | null;
778
+ readonly expiresAt: string;
779
+ }
780
+ interface CreateMeetingInput {
781
+ readonly organizationId: OrganizationId;
782
+ readonly hostUserId: UserId;
783
+ readonly title: string;
784
+ readonly kind: MeetingKind;
785
+ readonly scheduledFor?: string | null;
786
+ readonly scheduledDurationMinutes?: number | null;
787
+ readonly lobbyEnabled?: boolean;
788
+ readonly recordingPolicy?: RecordingPolicy;
789
+ readonly aiEnabled?: boolean;
790
+ /** Reuse a durable slug (a recurring meeting keeps ONE link — R3). */
791
+ readonly slug?: string | undefined;
792
+ }
793
+ interface MeetRepository {
794
+ readonly schema: string;
795
+ readonly tables: typeof TABLES;
796
+ projectInvite(row: Row): CallInvite;
797
+ projectMeeting(row: Row): MeetingRecord;
798
+ createCallInvite(input: CreateCallInviteInput): Promise<CallInvite>;
799
+ settleCallInvite(id: CallId, state: CallState, declineMessage: string | null): Promise<CallInvite>;
800
+ pendingInvites(userId: UserId): Promise<readonly CallInvite[]>;
801
+ getOrCreateMeeting(input: CreateMeetingInput): Promise<MeetingRecord>;
802
+ meetingBySlug(slug: string): Promise<MeetingRecord>;
803
+ meeting(id: MeetingId): Promise<MeetingRecord>;
804
+ /**
805
+ * Keyset paged, `(scheduled_for, id)`. Every meeting the person can access, in
806
+ * ALL their organizations — `organizationId` is an OPTIONAL page filter
807
+ * (absent/null = all organizations), never the active organization
808
+ * (active-org-is-never-a-list-filter.md). Access is decided by RLS.
809
+ */
810
+ listMeetings(args: {
811
+ organizationId?: OrganizationId | null;
812
+ limit?: number;
813
+ before?: {
814
+ scheduledFor: string;
815
+ id: MeetingId;
816
+ } | null;
817
+ }): Promise<{
818
+ readonly items: readonly MeetingRecord[];
819
+ readonly hasMore: boolean;
820
+ }>;
821
+ admit(args: {
822
+ meetingId: MeetingId;
823
+ identity: string;
824
+ admitted: boolean;
825
+ byUserId: UserId;
826
+ }): Promise<void>;
827
+ setLock(meetingId: MeetingId, locked: boolean, byUserId: UserId): Promise<MeetingRecord>;
828
+ endMeeting(meetingId: MeetingId, byUserId: UserId): Promise<MeetingRecord>;
829
+ projectSegment(row: Row): TranscriptSegment;
830
+ projectNote(row: Row): MeetingNote;
831
+ /** One `meet_recordings` row, however it arrived (row or room packet). */
832
+ projectRecording(row: Row): RecordingRecord;
833
+ /** One durable `meet_participants` row — attendance, and the lobby queue. */
834
+ projectAttendee(row: Row): MeetingAttendee;
835
+ projectAttendee(row: Row): MeetingAttendee;
836
+ /**
837
+ * WHO ATTENDED, oldest arrival first. Everyone the meeting ever admitted —
838
+ * including the note-taker, which is a labelled participant and never a hidden
839
+ * listener (R4) — so the record can say who was in the room AND that it was
840
+ * transcribed.
841
+ */
842
+ attendees(meetingId: MeetingId): Promise<readonly MeetingAttendee[]>;
843
+ /**
844
+ * THE DURABLE TRANSCRIPT (D16). Final rows only — the table holds nothing
845
+ * else — oldest first, capped so a four-hour meeting cannot hand a surface
846
+ * fifty thousand rows in one read.
847
+ */
848
+ transcript(meetingId: MeetingId, limit?: number): Promise<readonly TranscriptSegment[]>;
849
+ /** Live notes, decisions, action items and the wrap-up summary, oldest first. */
850
+ notes(meetingId: MeetingId): Promise<readonly MeetingNote[]>;
851
+ /** R5: consent is RECORDED, not assumed. One row per participant per recording. */
852
+ recordConsent(args: {
853
+ meetingId: MeetingId;
854
+ identity: string;
855
+ acknowledgedAt: string;
856
+ }): Promise<void>;
857
+ recordings(meetingId: MeetingId): Promise<readonly RecordingRecord[]>;
858
+ /** Meet wave 3: every recording of the reader's own meetings, newest first. */
859
+ myRecordings(args?: {
860
+ includeArchived?: boolean;
861
+ limit?: number;
862
+ }): Promise<readonly LibraryRecording[]>;
863
+ /** Meet wave 3: search the reader's own meetings. */
864
+ searchMeetings(query: string, limit?: number): Promise<readonly MeetingSearchHit[]>;
865
+ /** Meet wave 3: name a recording (`null` = back to the meeting's title and date). */
866
+ renameRecording(recordingId: string, title: string | null): Promise<void>;
867
+ /** Meet wave 3: archive (soft delete) or restore a recording. Never a purge. */
868
+ setRecordingArchived(recordingId: string, archived: boolean): Promise<void>;
869
+ scheduleMeeting(input: ScheduleMeetingInput): Promise<MeetingRecord>;
870
+ /** `expectedVersion` from the row you edited; a stale one is a `conflict`. */
871
+ updateMeeting(meetingId: MeetingId, changes: MeetingChanges, options?: {
872
+ expectedVersion?: number | null;
873
+ byUserId?: UserId | null;
874
+ }): Promise<MeetingRecord>;
875
+ cancelMeeting(meetingId: MeetingId, reason: string | null, byUserId?: UserId | null): Promise<MeetingRecord>;
876
+ archiveMeeting(meetingId: MeetingId, byUserId?: UserId | null): Promise<MeetingRecord>;
877
+ restoreMeeting(meetingId: MeetingId, byUserId?: UserId | null): Promise<MeetingRecord>;
878
+ setOccurrence(args: {
879
+ meetingId: MeetingId;
880
+ originalStart: string;
881
+ action: "cancel" | "move" | "restore";
882
+ newStart?: string | null;
883
+ newDurationMinutes?: number | null;
884
+ reason?: string | null;
885
+ byUserId?: UserId | null;
886
+ }): Promise<OccurrenceOverride>;
887
+ meetingOccurrences(meetingId: MeetingId, range: {
888
+ from: string;
889
+ to: string;
890
+ limit?: number;
891
+ }): Promise<readonly MeetingOccurrence[]>;
892
+ /** The Upcoming list: every occurrence the reader may see. `organizationId` FILTERS; RLS decides. */
893
+ occurrencesBetween(range: {
894
+ from: string;
895
+ to: string;
896
+ organizationId?: OrganizationId | null;
897
+ limit?: number;
898
+ }): Promise<readonly UpcomingOccurrence[]>;
899
+ invitees(meetingId: MeetingId): Promise<readonly MeetingInvitee[]>;
900
+ addInvitees(meetingId: MeetingId, people: readonly InviteeInput[], byUserId?: UserId | null): Promise<readonly MeetingInvitee[]>;
901
+ setInviteeRole(inviteeId: string, role: InviteeRole, byUserId?: UserId | null): Promise<MeetingInvitee>;
902
+ removeInvitee(inviteeId: string, byUserId?: UserId | null): Promise<MeetingInvitee>;
903
+ respond(meetingId: MeetingId, answer: RsvpAnswer, note?: string | null): Promise<MeetingInvitee>;
904
+ invitationByToken(secret: string): Promise<RsvpInvitation>;
905
+ respondByToken(secret: string, answer: RsvpAnswer, note?: string | null): Promise<RsvpInvitation>;
906
+ projectInvitee(row: Row): MeetingInvitee;
907
+ }
908
+ interface ScheduleMeetingInput {
909
+ readonly organizationId: OrganizationId;
910
+ readonly hostUserId: UserId;
911
+ readonly title: string;
912
+ readonly scheduledFor: string | null;
913
+ /** IANA zone, e.g. `America/Los_Angeles`. The wall-clock time a series keeps across DST. */
914
+ readonly timeZone: string;
915
+ /** Omit to use the person's `meet.default_duration_minutes`. */
916
+ readonly durationMinutes?: number | null | undefined;
917
+ readonly agenda?: string | null | undefined;
918
+ /** RRULE subset: FREQ=DAILY|WEEKLY|MONTHLY, INTERVAL, BYDAY, BYMONTHDAY, COUNT, UNTIL. */
919
+ readonly recurrenceRule?: string | null | undefined;
920
+ readonly settings?: MeetingSettings | undefined;
921
+ }
922
+ interface RepositoryOptions {
923
+ /**
924
+ * Any Supabase client. Deliberately the SHALLOW public shape — pass the
925
+ * `SupabaseClient<Database>` you already have, with no cast. See the header.
926
+ */
927
+ client: MessagingSupabaseClient;
928
+ /** Resolve a session; used to retry ONCE across the sign-in gap. */
929
+ session?: (() => Promise<unknown>) | undefined;
930
+ }
931
+ /**
932
+ * 🚨 THE PROJECTIONS ARE MODULE-LEVEL AND EXPORTED, DELIBERATELY (MRI-A12).
933
+ *
934
+ * A durable Meet row reaches a client on one of two transports: as a
935
+ * `meet_notes` / `meet_transcript_segments` row over Postgres Changes, which
936
+ * needs a Supabase session and an org grant, or as a `matrx.meet.*` packet on
937
+ * the LiveKit data plane, which needs only the room token — and is therefore the
938
+ * ONLY one a session-less guest can receive (MRI-A10 settled that `anon` never
939
+ * gets a grant on an org-scoped table).
940
+ *
941
+ * Both carry the same row. If each transport projected it with its own function,
942
+ * a guest and a member would render the same meeting differently the first time
943
+ * the two drifted. So there is one function per row shape, it takes no client,
944
+ * and `core/room-events.ts` calls exactly these.
945
+ */
946
+ declare function projectSegment(row: Row): TranscriptSegment;
947
+ declare function projectNote(row: Row): MeetingNote;
948
+ declare function projectInvitee(row: Row): MeetingInvitee;
949
+ declare function projectOccurrence(row: Row): MeetingOccurrence;
950
+ declare function projectUpcoming(row: Row): UpcomingOccurrence;
951
+ declare function projectOverride(row: Row): OccurrenceOverride;
952
+ /** The jsonb the RSVP doors return, typed. A body that is neither shape is refused loudly. */
953
+ declare function projectRsvpInvitation(body: unknown): RsvpInvitation;
954
+ /** Settings → the door's snake_case object, carrying ONLY what was set. */
955
+ declare function settingsPatch(settings: MeetingSettings): Record<string, unknown>;
956
+ /**
957
+ * A partial edit → the `meet_update_meeting` patch. `undefined` = leave alone,
958
+ * `null` = clear — the difference a JSON patch exists to carry.
959
+ */
960
+ declare function changesPatch(changes: MeetingChanges): Record<string, unknown>;
961
+ declare function createMeetRepository(options: RepositoryOptions): MeetRepository;
962
+ /** One row of the recordings library (`meet_my_recordings`). */
963
+ interface LibraryRecording {
964
+ readonly id: string;
965
+ readonly meetingId: MeetingId;
966
+ readonly meetingTitle: string;
967
+ readonly meetingSlug: string;
968
+ /** The reader's strongest role on the meeting. */
969
+ readonly role: "host" | "cohost" | "invitee" | "attendee";
970
+ readonly state: RecordingState;
971
+ /** A name a host gave it; null = show the meeting title. */
972
+ readonly title: string | null;
973
+ readonly fileId: string | null;
974
+ readonly startedAt: string | null;
975
+ readonly endedAt: string | null;
976
+ readonly durationMs: number | null;
977
+ readonly sizeBytes: number | null;
978
+ readonly failureReason: string | null;
979
+ readonly archivedAt: string | null;
980
+ readonly organizationId: OrganizationId;
981
+ }
982
+ declare function projectLibraryRecording(row: Row): LibraryRecording;
983
+ /** Where a search hit was found. */
984
+ type MeetingSearchSource = "title" | "summary" | "decision" | "action_item" | "note" | "transcript";
985
+ /** One hit from `meet_search`. `snippet` marks matched words with ⟦ ⟧. */
986
+ interface MeetingSearchHit {
987
+ readonly meetingId: MeetingId;
988
+ readonly meetingTitle: string;
989
+ readonly meetingSlug: string;
990
+ readonly meetingAt: string | null;
991
+ readonly source: MeetingSearchSource;
992
+ /** The note or transcript segment id (the meeting id for a title hit). */
993
+ readonly refId: string;
994
+ readonly speaker: string | null;
995
+ readonly snippet: string;
996
+ /** When it was said / written. */
997
+ readonly at: string | null;
998
+ readonly rank: number;
999
+ }
1000
+ declare function projectSearchHit(row: Row): MeetingSearchHit;
1001
+ /** A snippet split into plain and matched runs, for highlighting without HTML. */
1002
+ declare function snippetRuns(snippet: string): readonly {
1003
+ readonly text: string;
1004
+ readonly hit: boolean;
1005
+ }[];
1006
+
1007
+ /**
1008
+ * THE CALL CENTER — ringing, answering, and the six ways a call ends badly.
1009
+ *
1010
+ * A call reaches you on whatever surface you are on (R2), which is why this is
1011
+ * mounted by the provider and not by a call page. It is also why delivery is
1012
+ * DUAL by design, not by belt-and-braces nervousness:
1013
+ *
1014
+ * - **The durable leg** is the invite ROW plus its Postgres Changes event. It
1015
+ * survives a reload, a late-joining tab, and a dropped socket, and it is what
1016
+ * makes a missed call a fact rather than a lost packet.
1017
+ * - **The instant leg** is a broadcast on the callee's user channel, which
1018
+ * arrives without a database round trip.
1019
+ *
1020
+ * Both are deduplicated on the invite id, so a call rings ONCE however it
1021
+ * arrives. Neither is a fallback for the other: removing the durable leg loses
1022
+ * calls, and removing the instant leg makes ringing feel slow.
1023
+ *
1024
+ * 🚨 THE FOUR RULES THAT MAKE RINGING SURVIVABLE
1025
+ *
1026
+ * 1. **A ring always ends.** Every outgoing call arms a timeout that writes
1027
+ * `missed`. A ring with no terminal state is a phone that never stops.
1028
+ * 2. **Answering on one device stops every other.** The `settled` signal on
1029
+ * your OWN user channel silences the other tabs. Without it, answering on a
1030
+ * laptop leaves a phone ringing in the room.
1031
+ * 3. **Busy is answered, not ignored.** Being in an active call auto-declines
1032
+ * with `busy`, so the caller learns something instead of ringing out.
1033
+ * 4. **The state machine is a MACHINE.** Transitions are validated: a
1034
+ * `declined` call cannot become `active` because a late accept arrived.
1035
+ */
1036
+
1037
+ declare function canTransition(from: CallState, to: CallState): boolean;
1038
+ interface CallCenterOptions {
1039
+ manager: RealtimeManager;
1040
+ repository: MeetRepository;
1041
+ store: MeetStore;
1042
+ identity: MeetIdentity;
1043
+ /** Ring for this long before the call is `missed`. Org knob, not a constant. */
1044
+ ringTimeoutMs?: number | undefined;
1045
+ onIncoming?: ((invite: CallInvite) => void) | undefined;
1046
+ onSettled?: ((invite: CallInvite) => void) | undefined;
1047
+ onDiagnostic?: ((event: {
1048
+ level: "info" | "warn" | "error";
1049
+ message: string;
1050
+ remedy?: string;
1051
+ }) => void) | undefined;
1052
+ timers?: {
1053
+ setTimeout: (run: () => void, ms: number) => unknown;
1054
+ clearTimeout: (handle: unknown) => void;
1055
+ now: () => number;
1056
+ };
1057
+ }
1058
+ interface CallCenter {
1059
+ /** Subscribe to this user's calling channel. Call once, from the provider. */
1060
+ start(): Promise<void>;
1061
+ /** Ring someone. Returns the invite the UI shows as "calling…". */
1062
+ call(args: {
1063
+ people: readonly UserId[];
1064
+ mode: CallMode;
1065
+ conversationId?: string | undefined;
1066
+ }): Promise<CallInvite>;
1067
+ accept(id: CallId, mode: CallMode): Promise<CallInvite>;
1068
+ decline(id: CallId, message?: string): Promise<CallInvite>;
1069
+ cancel(id: CallId): Promise<CallInvite>;
1070
+ /** Every invite currently ringing this device. */
1071
+ incoming(): readonly CallInvite[];
1072
+ outgoing(): CallInvite | null;
1073
+ /** Feed a ring that arrived through another path (a messaging action chip). */
1074
+ ingest(invite: CallInvite): void;
1075
+ dispose(): void;
1076
+ }
1077
+ declare function createCallCenter(options: CallCenterOptions): CallCenter;
1078
+
1079
+ /**
1080
+ * THE MEET CHANNEL NAMESPACES.
1081
+ *
1082
+ * Zero hand-rolled `.channel(` in this package (the realtime doctrine). Every
1083
+ * topic is declared through `@ai-matrx/realtime`'s registry, which refuses a
1084
+ * colliding re-declaration.
1085
+ *
1086
+ * THREE channels, each because it has a different AUDIENCE:
1087
+ *
1088
+ * - `meet-user` (per user): where a call REACHES you — the ring, the cancel,
1089
+ * the "answered on another device". You are subscribed to this everywhere the
1090
+ * provider is mounted, whether or not you are in a meeting.
1091
+ * - `meet-room` (per room): the pre-media room — who is knocking, who was
1092
+ * admitted, whether recording started. It carries the moments that happen
1093
+ * BEFORE and AFTER media, which LiveKit's own data plane cannot: a guest in
1094
+ * the lobby is not in the LiveKit room yet, by construction (D6).
1095
+ * - `meet-meeting` (per meeting): the durable record's feed — transcript
1096
+ * segments, notes, the recording row going `available` — so a surface that is
1097
+ * NOT in the call (a meeting page, an admin console) stays live.
1098
+ *
1099
+ * 🚨 The in-MEDIA control plane (hand raise, reactions, in-room chat) rides
1100
+ * LiveKit data messages, NOT these channels — see `DATA_TOPICS`. Splitting them
1101
+ * this way is deliberate: anything that must reach a guest with no Supabase
1102
+ * session travels on the media plane, where their room token is the authority.
1103
+ */
1104
+
1105
+ declare function userTopic(userId: UserId): string;
1106
+ declare function roomTopic(roomName: RoomName): string;
1107
+ declare function meetingTopic(meetingId: MeetingId): string;
1108
+ /** Broadcast event names. One place, so a sender and a receiver cannot drift. */
1109
+ declare const MEET_EVENTS: {
1110
+ /** A call is ringing you. Carries the whole invite; no follow-up read. */
1111
+ readonly ring: "mx.call.ring";
1112
+ /** The caller hung up before you answered. */
1113
+ readonly cancel: "mx.call.cancel";
1114
+ /** You answered (or declined) on another device. Stop ringing here. */
1115
+ readonly settled: "mx.call.settled";
1116
+ /** Someone is in the lobby. Hosts render the knock. */
1117
+ readonly knock: "mx.lobby.knock";
1118
+ /** A host decided. The knocker learns it here — they have no DB read. */
1119
+ readonly admission: "mx.lobby.admission";
1120
+ /** The room was locked or unlocked. */
1121
+ readonly lock: "mx.room.lock";
1122
+ /** Recording started or stopped. EVERY participant renders the indicator. */
1123
+ readonly recording: "mx.room.recording";
1124
+ };
1125
+ /**
1126
+ * 🚨 `mx.meeting.transcript` and `mx.meeting.note` WERE declared here and
1127
+ * emitted and subscribed by nothing — a broadcast fallback that existed only as
1128
+ * a name (MRI-A10 found it; feedback `b25db4d4`). They are gone. The thing they
1129
+ * described is real now and lives somewhere better: the platform publishes the
1130
+ * durable rows into the LIVEKIT ROOM under `matrx.meet.*` (`core/room-events.ts`,
1131
+ * MRI-A12), where a session-less guest's room token is the authority. Sending
1132
+ * them over a Supabase broadcast channel would have required the guest to hold a
1133
+ * Supabase connection, which is the exact thing they do not have.
1134
+ */
1135
+ type MeetEvent = (typeof MEET_EVENTS)[keyof typeof MEET_EVENTS];
1136
+
1137
+ /**
1138
+ * THE PLATFORM'S OWN VOICE IN THE ROOM — and the ONLY live feed a guest has.
1139
+ *
1140
+ * 🚨 **Why this file exists.** Everything durable a meeting produces — a live
1141
+ * note, a decision, an action item, the note-taker's state, the ending — lands
1142
+ * in `communication.meet_*`, and a signed-in member learns about it over
1143
+ * Postgres Changes. A GUEST cannot: `anon` holds no SELECT grant on
1144
+ * `meet_notes`, `meet_transcript_segments` or `meet_participants`, and MRI-A10
1145
+ * settled that widening it is forbidden, because those are org-scoped tables and
1146
+ * a meeting link is not org membership. A guest's authority is the ROOM TOKEN.
1147
+ * So the platform publishes the same rows into the room, where the token is the
1148
+ * authorization, and this file turns those packets into exactly the same store
1149
+ * calls the database path makes.
1150
+ *
1151
+ * Three rules hold it together:
1152
+ *
1153
+ * 1. **Same row, same projection, same reducer.** A `matrx.meet.note` packet
1154
+ * carries a `meet_notes` row and is projected by `projectNote` — the very
1155
+ * function the Postgres Changes handler uses — then handed to
1156
+ * `store.upsertNote`. A guest and a member therefore converge on one code
1157
+ * path rather than on two implementations that agree until they don't.
1158
+ * 2. **Keyed by the row id, so both copies are one thing.** A member receives
1159
+ * every note TWICE. They render once because the id is the row's id on both
1160
+ * transports, and where the two could disagree the database wins
1161
+ * (`store.upsertNote`'s precedence rule, D16).
1162
+ * 3. **`matrx.meet.*` is the server's namespace and nothing else's.** LiveKit
1163
+ * does not validate a data packet's sender, so any participant could publish
1164
+ * any bytes under any topic. What makes these trustworthy is that publishing
1165
+ * them requires a `roomAdmin` grant, which only the server's own control
1166
+ * token carries — a participant token cannot claim it. The in-room control
1167
+ * plane participants DO publish (hand, reaction, chat) lives under `mx.*`
1168
+ * and is handled separately, precisely so the two can never be confused.
1169
+ */
1170
+
1171
+ /**
1172
+ * The server's namespace. Pinned against `aidream/services/meet/livekit.py`'s
1173
+ * `ROOM_EVENT_PREFIX` — a rename on either side must break a named test here.
1174
+ */
1175
+ declare const ROOM_EVENT_PREFIX = "matrx.meet.";
1176
+ declare const ROOM_EVENTS: {
1177
+ /** One durable `meet_notes` row was written. Payload `{ note: <row> }`. */
1178
+ readonly note: "matrx.meet.note";
1179
+ /** `meet_meetings.metadata.note_taker` changed. Payload `{ meeting_id, note_taker }`. */
1180
+ readonly noteTaker: "matrx.meet.note_taker";
1181
+ /** A wrap-up pass finished. Payload `{ meeting_id, note_ids, segment_count }`. */
1182
+ readonly wrapUp: "matrx.meet.wrap_up";
1183
+ /** The meeting ended for everyone. Payload `{ meeting_id, ended_at, trigger }`. */
1184
+ readonly ended: "matrx.meet.ended";
1185
+ /**
1186
+ * A `meet_recordings` row changed state. Payload
1187
+ * `{ meeting_id, recording: { id, state, started_by } }`.
1188
+ *
1189
+ * 🚨 THIS IS THE ONLY WAY A GUEST LEARNS A RECORDING IS RUNNING. PreJoin
1190
+ * promises every joiner an indicator, and `anon` holds no SELECT grant on
1191
+ * `meet_recordings` (MRI-A10 settled that widening it is forbidden), so
1192
+ * without this packet the promise was a sentence with nothing behind it: on
1193
+ * production 2026-09-09 the guest lane showed no indicator at all while the
1194
+ * host recorded (MRI-C8).
1195
+ */
1196
+ readonly recording: "matrx.meet.recording";
1197
+ /**
1198
+ * A chat message to everyone was SAVED (Meet wave 2). Payload
1199
+ * `{ message: { id, meeting_id, sender_identity, sender_name, body, created_at } }`.
1200
+ */
1201
+ readonly chat: "matrx.meet.chat";
1202
+ /**
1203
+ * A host or co-host acted (Meet wave 2). Payload `{ meeting_id, kind, … }`:
1204
+ * `muted {source, by}` · `ask-unmute {by}` · `removed {by}` — addressed to the
1205
+ * person concerned; `lower-hands {identities: [...] | "*"}` · `role {identity,
1206
+ * role}` — to everyone.
1207
+ */
1208
+ readonly moderation: "matrx.meet.moderation";
1209
+ /**
1210
+ * A meeting ROOM ITEM changed (Meet wave 5): a poll, a Q&A question, a whiteboard
1211
+ * stroke or the breakout plan. Payload `{ meeting_id, item }` · `{ removed: [ids] }` ·
1212
+ * `{ hidden: [ids] }` · `{ cleared: "whiteboard" }` · `{ broadcast }` · `{ item, move }`.
1213
+ * A host's fuller copy of a poll or question carries `audience: "hosts"`.
1214
+ */
1215
+ readonly collab: "matrx.meet.collab";
1216
+ };
1217
+ type RoomEventTopic = (typeof ROOM_EVENTS)[keyof typeof ROOM_EVENTS];
1218
+ declare function isRoomEventTopic(topic: string): boolean;
1219
+ /**
1220
+ * What happened to one packet. Returned rather than logged from inside, so the
1221
+ * room engine can report a drop with its own diagnostic channel and a test can
1222
+ * assert on it — a packet that is silently ignored is indistinguishable from
1223
+ * one that never arrived, which is the failure this whole item is about.
1224
+ */
1225
+ interface RoomEventOutcome {
1226
+ readonly handled: boolean;
1227
+ /** Present when `handled` is false. Says what was wrong, in a sentence. */
1228
+ readonly ignored?: string;
1229
+ }
1230
+ interface ApplyRoomEventArgs {
1231
+ readonly store: MeetStore;
1232
+ readonly topic: string;
1233
+ /** The packet's already-parsed JSON body. */
1234
+ readonly payload: unknown;
1235
+ readonly now?: (() => number) | undefined;
1236
+ }
1237
+ /**
1238
+ * Apply one `matrx.meet.*` packet to the store. Never throws.
1239
+ *
1240
+ * A malformed packet is DROPPED with a reason, never partially applied: half a
1241
+ * note on a guest's screen is worse than no note, because the guest cannot tell
1242
+ * which half they are missing.
1243
+ */
1244
+ declare function applyRoomEvent(args: ApplyRoomEventArgs): RoomEventOutcome;
1245
+
1246
+ /**
1247
+ * DEVICE MANAGEMENT — the part users blame the product for.
1248
+ *
1249
+ * Every rule here exists because it is a support ticket somewhere:
1250
+ *
1251
+ * - **Labels are empty until permission is granted.** `enumerateDevices()`
1252
+ * happily returns three cameras with blank labels before the prompt. A picker
1253
+ * rendering "Camera 1 / Camera 2" is the classic symptom; we report
1254
+ * `labelsHidden` so the UI can say *why* instead.
1255
+ * - **A remembered device can vanish.** Unplug a headset between meetings and a
1256
+ * stored `deviceId` produces `OverconstrainedError`. The manager falls back to
1257
+ * the system default AND SAYS SO — a silent fallback is how someone joins on
1258
+ * the laptop mic while believing they are on their headset.
1259
+ * - **Selections persist per KIND, per device.** Injected storage (C10:
1260
+ * persistence-injected — this package's core purpose is not storage), with a
1261
+ * memory fallback that announces itself rather than pretending to be durable.
1262
+ * - **Enumeration is repeated on `devicechange`.** A picker that lists what was
1263
+ * plugged in at mount is wrong within seconds of a headset arriving.
1264
+ */
1265
+ type DeviceKind = "audioinput" | "videoinput" | "audiooutput";
1266
+ interface MeetDevice {
1267
+ readonly deviceId: string;
1268
+ readonly kind: DeviceKind;
1269
+ readonly label: string;
1270
+ }
1271
+ interface DeviceSnapshot {
1272
+ readonly devices: Readonly<Record<DeviceKind, readonly MeetDevice[]>>;
1273
+ readonly selected: Readonly<Record<DeviceKind, string | null>>;
1274
+ /** True when the browser is hiding labels because permission was not granted. */
1275
+ readonly labelsHidden: boolean;
1276
+ readonly permission: "unknown" | "granted" | "denied";
1277
+ /** Set when a remembered device was gone and we fell back. Shown, not swallowed. */
1278
+ readonly fallbackNotice: string | null;
1279
+ }
1280
+ interface DeviceStorage {
1281
+ read(): Record<string, string>;
1282
+ write(value: Record<string, string>): void;
1283
+ readonly durable: boolean;
1284
+ }
1285
+ interface MediaDevicesLike {
1286
+ enumerateDevices(): Promise<readonly {
1287
+ deviceId: string;
1288
+ kind: string;
1289
+ label: string;
1290
+ }[]>;
1291
+ getUserMedia(constraints: {
1292
+ audio?: unknown;
1293
+ video?: unknown;
1294
+ }): Promise<{
1295
+ getTracks(): readonly {
1296
+ stop(): void;
1297
+ }[];
1298
+ }>;
1299
+ addEventListener?: ((type: "devicechange", handler: () => void) => void) | undefined;
1300
+ removeEventListener?: ((type: "devicechange", handler: () => void) => void) | undefined;
1301
+ }
1302
+ interface DeviceManagerOptions {
1303
+ media: MediaDevicesLike;
1304
+ storage?: DeviceStorage | undefined;
1305
+ onChange?: ((snapshot: DeviceSnapshot) => void) | undefined;
1306
+ }
1307
+ interface DeviceManager {
1308
+ /**
1309
+ * 🚨 THE SAME REFERENCE until something actually changes. This is a
1310
+ * `useSyncExternalStore` `getSnapshot` source (see `useDeviceSnapshot`), and
1311
+ * React compares snapshots by IDENTITY: a fresh object literal per call makes
1312
+ * every render report a change, which React reports as *"The result of
1313
+ * getSnapshot should be cached"* and then kills the tree with *"Maximum
1314
+ * update depth exceeded"*. `createMeetStore()` has always held this contract;
1315
+ * 0.2.0 shipped without it here and every room crashed at `<PreJoin>`.
1316
+ */
1317
+ snapshot(): DeviceSnapshot;
1318
+ /** Enumerate without prompting. Labels may be hidden; that is reported. */
1319
+ refresh(): Promise<DeviceSnapshot>;
1320
+ /** Prompt, then enumerate with real labels. The pre-join screen calls this. */
1321
+ requestPermission(kinds: {
1322
+ audio: boolean;
1323
+ video: boolean;
1324
+ }): Promise<DeviceSnapshot>;
1325
+ select(kind: DeviceKind, deviceId: string): DeviceSnapshot;
1326
+ /** The id to actually use — the remembered one, or the default when it is gone. */
1327
+ resolve(kind: DeviceKind): string | null;
1328
+ subscribe(listener: (snapshot: DeviceSnapshot) => void): () => void;
1329
+ dispose(): void;
1330
+ }
1331
+ /** localStorage when it exists; memory otherwise — and it says which. */
1332
+ declare function createWebDeviceStorage(key?: string): DeviceStorage;
1333
+ declare function createMemoryDeviceStorage(): DeviceStorage;
1334
+ declare function createDeviceManager(options: DeviceManagerOptions): DeviceManager;
1335
+
1336
+ /**
1337
+ * PRESENTATION HELPERS specific to a CALL. Pure, tested, and shared by every
1338
+ * surface — so a roster line reads the same in the meeting, in the history
1339
+ * list, and in the notification.
1340
+ *
1341
+ * The GENERIC formatters this file used to define — `formatDuration`,
1342
+ * `getInitials`, `avatarPaletteIndex` — moved to `@ai-matrx/kit/format` on
1343
+ * 2026-09-07 (duplication census H1). They were byte-for-byte duplicated in
1344
+ * `@ai-matrx/messaging`, and because two packages claimed them, ~65 host
1345
+ * twins across six repos had no correct owner to point at: no surface should
1346
+ * take a dependency on the video-calling package to print a duration. Nothing
1347
+ * is re-exported from here — import `@ai-matrx/kit/format` directly.
1348
+ */
1349
+
1350
+ /** "Ana, Ben and 3 others" — the roster line, never a raw array join. */
1351
+ declare function participantSummary(participants: readonly MeetParticipant[]): string;
1352
+ /** What a settled call says in history. One sentence per terminal state. */
1353
+ declare function callStateLabel(state: CallState, outgoing: boolean): string;
1354
+ /** "2 waiting" for the lobby badge — plural handled once, not per surface. */
1355
+ declare function countLabel(count: number, singular: string, plural?: string): string;
1356
+
1357
+ /**
1358
+ * THE MESSAGING SEAM — a call invitation is an ACTIONABLE MESSAGE (D8).
1359
+ *
1360
+ * `@ai-matrx/messaging` deliberately knows nothing about meetings; its action
1361
+ * registry exists so packages like this one can add a kind without messaging
1362
+ * learning about them. This file is that registration, and it is the whole
1363
+ * coupling between the two packages.
1364
+ *
1365
+ * Why an invitation is a message and not only a ring: a ring is EPHEMERAL. If
1366
+ * you were away from your desk, a broadcast that expired is gone, and "someone
1367
+ * tried to reach me" is exactly the information you most needed. The message is
1368
+ * the durable half — it is still in the thread tomorrow, with the outcome
1369
+ * stamped on it.
1370
+ *
1371
+ * 🚨 **THE PAYLOAD IS NOT AUTHORIZATION.** The chip's payload says what was
1372
+ * ASKED, never what is ALLOWED. The handler re-resolves the invite through the
1373
+ * repository (an auth-checked RPC) before it joins anything. Trusting the
1374
+ * payload would turn any message anyone can send into a room grant.
1375
+ *
1376
+ * Idempotence is the registry's, not ours: two tabs and a double tap settle
1377
+ * ONCE and share one receipt.
1378
+ */
1379
+
1380
+ declare const CALL_INVITE_KIND = "meet.call-invite";
1381
+ declare const CALL_INVITE_VERSION = 1;
1382
+ interface CallInvitePayload {
1383
+ readonly invite_id: string;
1384
+ readonly room_name: string;
1385
+ readonly mode: CallMode;
1386
+ readonly caller_name: string;
1387
+ readonly expires_at: string;
1388
+ }
1389
+ /** The `action_data` a caller attaches to the invitation message. */
1390
+ declare function callInviteAction(invite: CallInvite): {
1391
+ readonly kind: string;
1392
+ readonly version: number;
1393
+ readonly payload: CallInvitePayload;
1394
+ };
1395
+ /** The message body. Readable on a surface that cannot render the chips. */
1396
+ declare function callInviteText(invite: CallInvite): string;
1397
+ interface CallInviteHandlerOptions {
1398
+ calls: CallCenter;
1399
+ repository: MeetRepository;
1400
+ /** Called after a successful accept, with the room to enter. */
1401
+ onJoin: (invite: CallInvite) => void | Promise<void>;
1402
+ now?: (() => number) | undefined;
1403
+ }
1404
+ declare function createCallInviteHandler(options: CallInviteHandlerOptions): ActionHandler<CallInvitePayload>;
1405
+ /**
1406
+ * A meeting invitation — the scheduled twin. Deliberately NOT the same kind: a
1407
+ * scheduled meeting's chip must never say "Join call" three days early, and a
1408
+ * receiver that only understands calls must render NOTHING for it rather than a
1409
+ * button it cannot honor.
1410
+ */
1411
+ declare const MEETING_INVITE_KIND = "meet.meeting-invite";
1412
+ declare const MEETING_INVITE_VERSION = 1;
1413
+ interface MeetingInvitePayload {
1414
+ readonly meeting_id: string;
1415
+ readonly slug: string;
1416
+ readonly title: string;
1417
+ readonly scheduled_for: string | null;
1418
+ readonly link: string;
1419
+ }
1420
+ declare function meetingInviteAction(payload: MeetingInvitePayload): {
1421
+ readonly kind: string;
1422
+ readonly version: number;
1423
+ readonly payload: MeetingInvitePayload;
1424
+ };
1425
+ interface MeetingInviteHandlerOptions {
1426
+ onOpen: (payload: MeetingInvitePayload) => void | Promise<void>;
1427
+ now?: (() => number) | undefined;
1428
+ }
1429
+ declare function createMeetingInviteHandler(options: MeetingInviteHandlerOptions): ActionHandler<MeetingInvitePayload>;
1430
+
1431
+ /**
1432
+ * THE STAGE — who is big, who is small, and who is not rendered at all.
1433
+ *
1434
+ * This is pure arithmetic on purpose: layout is the part of a meeting UI that
1435
+ * everyone re-derives inside a component and gets subtly wrong, and it is
1436
+ * trivially testable once it stops living in JSX.
1437
+ *
1438
+ * R5 SCALE HONESTY lives here. A 200-person meeting does not render 200 video
1439
+ * tiles — every browser dies, and LiveKit's adaptive stream cannot save a
1440
+ * layout that asked for 200 subscriptions. The grid PAGES, and the ordering is
1441
+ * deliberate rather than "whatever the roster map iterated":
1442
+ *
1443
+ * 1. a screen share (or an explicit pin) owns the stage;
1444
+ * 2. then the active speakers, most recent first;
1445
+ * 3. then anyone with a hand raised — they are asking to be seen;
1446
+ * 4. then cameras on before cameras off (an avatar tile is cheap);
1447
+ * 5. then stable by join time, so tiles do not shuffle every time someone
1448
+ * breathes. Shuffling tiles is the single most disorienting thing a
1449
+ * meeting UI can do.
1450
+ */
1451
+
1452
+ interface StageArgs {
1453
+ readonly participants: readonly MeetParticipant[];
1454
+ readonly layout: StageLayout;
1455
+ readonly pinned: ParticipantIdentity | null;
1456
+ readonly activeSpeakers: readonly ParticipantIdentity[];
1457
+ /**
1458
+ * The participant a HOST spotlighted for everyone (Meet wave 2). A local pin
1459
+ * outranks it on this screen; a screen share outranks both.
1460
+ */
1461
+ readonly spotlight?: ParticipantIdentity | null | undefined;
1462
+ /** How many tiles this viewport can honestly render. Default 12. */
1463
+ readonly pageSize?: number;
1464
+ readonly page?: number;
1465
+ }
1466
+ interface Stage {
1467
+ /** The large tile, when the layout has one. */
1468
+ readonly feature: MeetParticipant | null;
1469
+ /** What actually renders as a tile, already paged. */
1470
+ readonly tiles: readonly MeetParticipant[];
1471
+ /** Everyone not on this page. Rendered as "+N others", never dropped silently. */
1472
+ readonly overflow: number;
1473
+ readonly page: number;
1474
+ readonly pageCount: number;
1475
+ /** True when a screen share, not a person, is the reason for the big tile. */
1476
+ readonly presenting: boolean;
1477
+ }
1478
+ declare const DEFAULT_PAGE_SIZE = 12;
1479
+ declare function orderParticipants(participants: readonly MeetParticipant[], activeSpeakers: readonly ParticipantIdentity[]): readonly MeetParticipant[];
1480
+ declare function computeStage(args: StageArgs): Stage;
1481
+ /** Column count for a grid of N tiles — the shape that keeps tiles squarest. */
1482
+ declare function gridColumns(count: number): number;
1483
+
1484
+ /**
1485
+ * THE MEDIA SEAM — the ONE interface the room engine talks to.
1486
+ *
1487
+ * D1/D2: LiveKit Cloud is the media infrastructure, hardcoded. This interface
1488
+ * is NOT a provider abstraction and is not here so a second SFU can be swapped
1489
+ * in — it exists for two reasons that are both about correctness:
1490
+ *
1491
+ * 1. **The engine must be provable without a browser.** Everything expensive in
1492
+ * a calling product (ring timeouts, reconnection, admission, escalation,
1493
+ * speaker tracking) is logic, and logic proven only through a real SFU is
1494
+ * logic that is never proven. `testing/fake-driver.ts` drives every one of
1495
+ * those paths in a node test.
1496
+ * 2. **The SDK stays out of the initial chunk.** `livekit-client` is reached
1497
+ * only through a dynamic `import()` inside the default driver, so importing
1498
+ * this package during SSR — or on a page with a call BUTTON but no call —
1499
+ * costs nothing and touches no browser API.
1500
+ *
1501
+ * The default implementation ships (`createLiveKitDriver`) and is what
1502
+ * `<MeetProvider>` uses when a host injects nothing (THE ALL-INCLUSIVE LAW).
1503
+ */
1504
+
1505
+ /** A participant as the media layer sees them, before roles or profiles. */
1506
+ interface DriverParticipant {
1507
+ readonly identity: string;
1508
+ readonly name: string;
1509
+ /** Opaque JSON string the token minted for this participant. */
1510
+ readonly metadata: string | null;
1511
+ /**
1512
+ * The participant's LiveKit attributes. Unlike `metadata` these are set by
1513
+ * the participant itself at run time, which is how the note-taker announces
1514
+ * that a consent notice is owed (D10).
1515
+ */
1516
+ readonly attributes: Readonly<Record<string, string>>;
1517
+ readonly isLocal: boolean;
1518
+ readonly micEnabled: boolean;
1519
+ readonly cameraEnabled: boolean;
1520
+ readonly screenSharing: boolean;
1521
+ }
1522
+ type DriverConnectionState = "connecting" | "connected" | "reconnecting" | "disconnected";
1523
+ type DriverTrackKind = "camera" | "microphone" | "screen";
1524
+ /**
1525
+ * ONE LIVE CAPTION off the `lk.transcription` text stream.
1526
+ *
1527
+ * `speakerIdentity` is `lk.transcribed_participant_identity` — WHO IS BEING
1528
+ * TRANSCRIBED, not who published the stream (the note-taker publishes every
1529
+ * one of them). The SFU-reported publisher identity below is independently
1530
+ * checked against the meeting's note-taker; captions remain hints and durable
1531
+ * segments remain the record (D16).
1532
+ */
1533
+ interface DriverTranscription {
1534
+ /** `lk.segment_id`. Interim and final share it; that is what makes replacement possible. */
1535
+ readonly segmentId: string;
1536
+ readonly speakerIdentity: string;
1537
+ /**
1538
+ * The LiveKit participant that opened this text stream. Unlike
1539
+ * `speakerIdentity`, this comes from the SFU and is the authority for who
1540
+ * published a caption.
1541
+ */
1542
+ readonly publisherIdentity: string;
1543
+ readonly text: string;
1544
+ /** `lk.transcription_final`. */
1545
+ readonly final: boolean;
1546
+ /** `lk.transcribed_track_id`, when the sender supplied it. */
1547
+ readonly trackId: string | null;
1548
+ readonly at: string;
1549
+ }
1550
+ interface RoomDriverHandlers {
1551
+ /** The FULL roster, every time. A delta protocol here drifts; a snapshot cannot. */
1552
+ participants(participants: readonly DriverParticipant[]): void;
1553
+ activeSpeakers(identities: readonly string[]): void;
1554
+ quality(identity: string, quality: ConnectionQuality): void;
1555
+ connectionState(state: DriverConnectionState): void;
1556
+ /** In-room data messages: hand raise, reactions, in-room chat, AI events. */
1557
+ data(topic: string, payload: string, fromIdentity: string | null): void;
1558
+ /** A live caption. Interim and final both arrive here (D16). */
1559
+ transcription(event: DriverTranscription): void;
1560
+ /** The room ended for this client. `reason` is shown to the user verbatim. */
1561
+ disconnected(reason: string | null): void;
1562
+ /**
1563
+ * The ROOM's metadata — set only by the server (`UpdateRoomMetadata`), and
1564
+ * delivered to every joiner ON JOIN, which is why the spotlight rides it: a
1565
+ * data packet reaches only the people already in the room. Called once after
1566
+ * connect and on every change. Optional so an older host driver still works.
1567
+ */
1568
+ roomMetadata?(metadata: string): void;
1569
+ }
1570
+ /** A camera background effect. `image` replaces the background with `imageUrl`. */
1571
+ type VideoEffect = {
1572
+ readonly kind: "none";
1573
+ } | {
1574
+ readonly kind: "blur";
1575
+ readonly radius?: number;
1576
+ } | {
1577
+ readonly kind: "image";
1578
+ readonly imageUrl: string;
1579
+ };
1580
+ /** Microphone processing the browser applies before anything is sent. */
1581
+ interface AudioProcessing {
1582
+ readonly noiseSuppression: boolean;
1583
+ readonly echoCancellation: boolean;
1584
+ readonly autoGainControl: boolean;
1585
+ }
1586
+ interface MeetRoomDriver {
1587
+ connect(serverUrl: string, token: string): Promise<void>;
1588
+ disconnect(): Promise<void>;
1589
+ setMicrophoneEnabled(enabled: boolean): Promise<void>;
1590
+ setCameraEnabled(enabled: boolean): Promise<void>;
1591
+ setScreenShareEnabled(enabled: boolean): Promise<void>;
1592
+ /**
1593
+ * Reliable, topic-addressed data. The in-room control plane. `to` addresses
1594
+ * named identities only (a private chat message); absent = everyone.
1595
+ */
1596
+ publishData(topic: string, payload: string, to?: readonly string[]): Promise<void>;
1597
+ /**
1598
+ * Apply microphone processing to the live track and to every future one.
1599
+ * Optional: a driver without it makes the control ABSENT, never dead.
1600
+ */
1601
+ setAudioProcessing?(processing: AudioProcessing): Promise<void>;
1602
+ /**
1603
+ * Apply a background effect to the live camera and to every future one.
1604
+ * Rejects with a sentence when this browser cannot run it.
1605
+ */
1606
+ setVideoEffect?(effect: VideoEffect): Promise<void>;
1607
+ /** Whether this browser can run background effects at all. */
1608
+ supportsVideoEffects?(): boolean;
1609
+ /** Whether this browser can share a screen at all (iOS cannot). Absent = assume yes. */
1610
+ supportsScreenShare?(): boolean;
1611
+ /**
1612
+ * Attach a participant's track to a media element. Returns the detach
1613
+ * function — returning it (rather than exposing `detach`) is what makes a
1614
+ * leaked attachment impossible from the React side: the effect's cleanup IS
1615
+ * the return value.
1616
+ */
1617
+ attach(identity: string, kind: DriverTrackKind, element: HTMLMediaElement): () => void;
1618
+ /** The device the driver actually selected, so the UI never lies about it. */
1619
+ activeDevices(): Readonly<Record<"audioinput" | "videoinput" | "audiooutput", string | null>>;
1620
+ switchDevice(kind: "audioinput" | "videoinput" | "audiooutput", deviceId: string): Promise<void>;
1621
+ }
1622
+ type RoomDriverFactory = (handlers: RoomDriverHandlers) => MeetRoomDriver | Promise<MeetRoomDriver>;
1623
+ /** Topics for the in-room control plane. One place, so senders cannot drift. */
1624
+ declare const DATA_TOPICS: {
1625
+ readonly hand: "mx.hand";
1626
+ readonly reaction: "mx.reaction";
1627
+ readonly chat: "mx.chat";
1628
+ };
1629
+ /**
1630
+ * 🚨 `mx.ai.note` and `mx.ai.transcript` WERE declared here and emitted by
1631
+ * nothing. They are gone (MRI-A12): the platform's own events are published by
1632
+ * the SERVER under `matrx.meet.*` — see `core/room-events.ts` — because a packet
1633
+ * a participant could publish is a packet a guest could forge, and these carry
1634
+ * durable rows. A participant topic for the same thing would be a second,
1635
+ * unauthenticated way to say it.
1636
+ */
1637
+ type DataTopic = (typeof DATA_TOPICS)[keyof typeof DATA_TOPICS];
1638
+ /**
1639
+ * THE LIVEKIT TRANSCRIPTION CONTRACT — not ours, so it is written down once
1640
+ * rather than spelled inline. The topic and these four attribute names are the
1641
+ * SDK's own (`lk.*`); the worker publishes to exactly them (MRI-A2) and a
1642
+ * rename on either side must break here, loudly, in one place.
1643
+ */
1644
+ declare const TRANSCRIPTION_TOPIC = "lk.transcription";
1645
+ declare const TRANSCRIPTION_ATTRIBUTES: {
1646
+ readonly segmentId: "lk.segment_id";
1647
+ readonly trackId: "lk.transcribed_track_id";
1648
+ readonly final: "lk.transcription_final";
1649
+ /**
1650
+ * WHO IS BEING TRANSCRIBED. The LiveKit JS client reports the PUBLISHER of a
1651
+ * text stream, and the publisher is always the note-taker — so a caption
1652
+ * carries the speaker in this attribute and falls back to the publisher only
1653
+ * when it is absent (which attributes the line to the agent, visibly, rather
1654
+ * than to the wrong person, silently).
1655
+ */
1656
+ readonly speakerIdentity: "lk.transcribed_participant_identity";
1657
+ };
1658
+
1659
+ /**
1660
+ * THE ONE PLACE `livekit-client` IS TOUCHED.
1661
+ *
1662
+ * Everything above this file talks to `MeetRoomDriver`. That containment is
1663
+ * what lets the engine's whole behavior — ring timeouts, admission, escalation,
1664
+ * reconnection — be proven in a node test, and it is why an SDK upgrade is a
1665
+ * single-file review rather than a package-wide audit.
1666
+ *
1667
+ * 🚨 **THE IMPORT IS DYNAMIC, DELIBERATELY.** `import type` is erased, and the
1668
+ * runtime `await import("livekit-client")` happens inside `connect()`. So:
1669
+ * importing `@ai-matrx/meet` during SSR, or on a page that renders a call
1670
+ * BUTTON but never a call, pulls in none of the media SDK and touches no
1671
+ * browser API (independence contract §4). A static import here would put ~400KB
1672
+ * of WebRTC into every consumer's first load and break RSC rendering.
1673
+ *
1674
+ * The mapping helpers below are exported for their own tests: an SDK enum
1675
+ * renamed upstream must fail a named test, not silently become "unknown" on
1676
+ * every tile.
1677
+ */
1678
+
1679
+ /**
1680
+ * LiveKit's quality enum → ours. Mapped explicitly rather than cast: the SDK
1681
+ * has five states and our UI has five sentences, and an unmapped one must
1682
+ * degrade to `unknown` (an honest "we don't know") rather than to `excellent`
1683
+ * (a lie that hides a failing call).
1684
+ */
1685
+ declare function mapQuality(quality: ConnectionQuality$1 | string): ConnectionQuality;
1686
+ declare function mapConnectionState(state: string): DriverConnectionState;
1687
+ /**
1688
+ * A LiveKit participant → our snapshot.
1689
+ *
1690
+ * 🚨 `name` IS REPORTED EXACTLY AS LIVEKIT GAVE IT, empty string and all
1691
+ * (MRI-A18). Through 0.4.12 it fell back to the identity here, which meant a
1692
+ * driver — the thing furthest from any screen — invented a "name" that
1693
+ * `core/names.ts` then had to recognize and refuse. A driver reports; it does
1694
+ * not name. The one fallback lives in `projectParticipant`, and it is the name
1695
+ * book's honest placeholder rather than a token.
1696
+ */
1697
+ declare function snapshotParticipant(participant: Pick<Participant, "identity" | "name" | "metadata" | "isMicrophoneEnabled" | "isCameraEnabled" | "isScreenShareEnabled"> & {
1698
+ attributes?: Readonly<Record<string, string>> | undefined;
1699
+ }, isLocal: boolean): DriverParticipant;
1700
+ interface LiveKitDriverOptions {
1701
+ /** Adaptive stream + dynacast default ON — R5 scale honesty, not a knob. */
1702
+ roomOptions?: RoomOptions | undefined;
1703
+ }
1704
+ declare function createLiveKitDriver(handlers: RoomDriverHandlers, options?: LiveKitDriverOptions): MeetRoomDriver;
1705
+
1706
+ /**
1707
+ * MEETINGS — the lobby, the host powers, and the durable link.
1708
+ *
1709
+ * A meeting is a room plus scheduling, lobby and host metadata (D3). Everything
1710
+ * here is about the moments OUTSIDE the media session, which is exactly why it
1711
+ * cannot ride LiveKit's data plane: a guest waiting in the lobby is not in the
1712
+ * LiveKit room yet, by construction. The lobby therefore lives on the meet-room
1713
+ * realtime channel.
1714
+ *
1715
+ * 🚨 THE THREE THINGS THAT MAKE GUEST JOIN SAFE (D6)
1716
+ *
1717
+ * 1. **The link is an identity, not a session.** A durable slug resolves before,
1718
+ * during, and after the scheduled time, and a recurring meeting keeps ONE
1719
+ * link. Nothing here mints a per-occurrence URL.
1720
+ * 2. **A guest's capability is the TOKEN, and nothing else.** They are scoped to
1721
+ * exactly one room. There is no guest row this client writes that could grant
1722
+ * anything, and no client-side "is this person allowed" branch.
1723
+ * 3. **Admission is a SERVER decision, recorded.** `admit` is an RPC; the
1724
+ * broadcast that tells the knocker is a NOTIFICATION of that decision, never
1725
+ * the decision itself. Two hosts clicking admit is the ordinary case, and the
1726
+ * RPC is what makes it idempotent.
1727
+ *
1728
+ * Host powers are checked against the role the SERVER minted into the token
1729
+ * (see `room.ts`), and the UI hides what an actor cannot do rather than
1730
+ * rendering a dead button (no dead ends).
1731
+ */
1732
+
1733
+ interface MeetingSessionOptions {
1734
+ manager: RealtimeManager;
1735
+ repository: MeetRepository;
1736
+ store: MeetStore;
1737
+ identity: MeetIdentity;
1738
+ /**
1739
+ * The ONE org-aware aidream client (MRI-A16) — `endForEveryone` is a SERVER
1740
+ * decision (MRI-A6), and every request it makes carries the organization.
1741
+ */
1742
+ api: MeetApi;
1743
+ onDiagnostic?: ((event: {
1744
+ level: "info" | "warn" | "error";
1745
+ message: string;
1746
+ remedy?: string;
1747
+ }) => void) | undefined;
1748
+ /**
1749
+ * 🚨 THE HOST ADMITTED *US*. Called exactly once per admission verdict that
1750
+ * names this participant, and only for `admitted`.
1751
+ *
1752
+ * Admission is not a UI state change: a lobby token is minted with
1753
+ * `canPublish: false` and `canSubscribe: false`, so the waiting room can only
1754
+ * become a room by minting a NEW token. Before this existed the waiting room
1755
+ * said "you will join automatically once you are admitted" and then waited
1756
+ * forever — a sentence on screen that was not true. The provider wires this
1757
+ * to `engine.rejoin()`.
1758
+ */
1759
+ onAdmitted?: (() => void) | undefined;
1760
+ /**
1761
+ * 🚨 THE HOST DENIED *US*. Called once per denial verdict that names this
1762
+ * knocker. Without it the waiting room kept promising "you will join
1763
+ * automatically" to a person the host had already turned away (production
1764
+ * 2026-09-27). The provider wires it to `engine.rejoin()`: the server then
1765
+ * refuses the mint with its own sentence ("The host did not admit you…"),
1766
+ * so the words on screen are the server's decision, never the client's guess.
1767
+ */
1768
+ onDenied?: (() => void) | undefined;
1769
+ }
1770
+ interface MeetingSession {
1771
+ /** Subscribe to the room's out-of-band plane. Hosts see knocks; guests see verdicts. */
1772
+ watch(roomName: RoomName): void;
1773
+ /**
1774
+ * Subscribe to the MEETING RECORD's durable feed — transcript segments, notes,
1775
+ * and the meeting row itself (which is where the note-taker's state lives).
1776
+ * Reconnecting IS re-reading: the backfill door re-reads all three, because
1777
+ * realtime has no replay and a socket blip would otherwise leave a transcript
1778
+ * permanently missing the minute nobody noticed.
1779
+ */
1780
+ watchMeeting(meetingId: MeetingId): void;
1781
+ /** A guest (or a late arrival at a locked room) announces themselves. */
1782
+ knock(roomName: RoomName): void;
1783
+ admit(entry: LobbyEntry, admitted: boolean): Promise<void>;
1784
+ admitAll(): Promise<void>;
1785
+ setLock(locked: boolean): Promise<MeetingRecord>;
1786
+ /** End it for everyone, through the server (MRI-A6). */
1787
+ endForEveryone(): Promise<EndMeetingResult>;
1788
+ dispose(): void;
1789
+ }
1790
+ /** Can `actor` moderate `target`? A host outranks a co-host outranks everyone. */
1791
+ declare function canModerateParticipant(actor: MeetParticipant | undefined, target: MeetParticipant | undefined): boolean;
1792
+ declare function roleOf(participants: readonly MeetParticipant[], identity: ParticipantIdentity | null): ParticipantRole;
1793
+ /**
1794
+ * What ending a meeting produced. `served: false` is the honest answer from a
1795
+ * server that has not landed `POST /v1/meet/end` yet (MRI-A6) — the control is
1796
+ * real, the refusal is a sentence, and nothing pretends the meeting ended.
1797
+ */
1798
+ interface EndMeetingResult {
1799
+ readonly endedAt: string | null;
1800
+ readonly served: boolean;
1801
+ }
1802
+ /** The sentence a server without MRI-A6 earns. Pinned so every surface says it. */
1803
+ declare const END_MEETING_NOT_SERVED = "Ending a meeting is not available on this server yet";
1804
+ declare function createMeetingSession(options: MeetingSessionOptions): MeetingSession;
1805
+ declare function meetingSlug(random?: () => number): string;
1806
+ /** The link a host copies. ONE shape, so every surface produces the same URL. */
1807
+ declare function meetingLink(baseUrl: string, slug: string): string;
1808
+
1809
+ /**
1810
+ * RECORDING — LiveKit Egress, host-controlled, consent-enforced (R3/R5).
1811
+ *
1812
+ * 🚨 **CONSENT IS NOT OPTIONAL AND NOT A PROP.** There is no
1813
+ * `showRecordingIndicator={false}`. Recording a person without telling them is
1814
+ * illegal in a two-party-consent jurisdiction and is indefensible everywhere
1815
+ * else, so the indicator and the join-time notice are properties of the
1816
+ * package, not decisions a consumer gets to make. The only knob is the org's
1817
+ * `recordingPolicy` — disabled / host-controlled / always-on — which decides
1818
+ * whether recording may happen at all, never whether people are told.
1819
+ *
1820
+ * 🚨 **THE RECORDING BELONGS TO THE PLATFORM, NOT TO LIVEKIT.** The server
1821
+ * lands the artifact in the platform files system and returns a FILE ID. A
1822
+ * provider URL would be an orphan the day the room is garbage collected — the
1823
+ * reason `RecordingRecord.fileId` is not `url`.
1824
+ *
1825
+ * The client half is small on purpose: start/stop are aidream calls (the API
1826
+ * secret never comes near a browser), and the truth about a recording arrives
1827
+ * from the server's webhook through the room channel. This module never
1828
+ * *guesses* that a recording started.
1829
+ */
1830
+
1831
+ interface RecordingControllerOptions {
1832
+ /** The ONE org-aware aidream client (MRI-A16). Owns base url and headers. */
1833
+ api: MeetApi;
1834
+ store: MeetStore;
1835
+ /** Announce the new state to everyone in the room (the indicator is for all). */
1836
+ announce?: ((state: RecordingState, by: UserId | null) => void) | undefined;
1837
+ /** Persist that this participant saw the notice. Consent is RECORDED (R5). */
1838
+ acknowledgeConsent?: ((meetingId: MeetingId) => Promise<void>) | undefined;
1839
+ }
1840
+ interface RecordingController {
1841
+ /** True when the org policy and the actor's role allow starting a recording. */
1842
+ canStart(meeting: MeetingRecord | null, isHost: boolean): boolean;
1843
+ start(args: {
1844
+ meetingId: MeetingId;
1845
+ roomName: RoomName;
1846
+ by: UserId;
1847
+ }): Promise<void>;
1848
+ stop(args: {
1849
+ meetingId: MeetingId;
1850
+ by: UserId;
1851
+ }): Promise<void>;
1852
+ /** The join-time notice text. One sentence, one place, every surface. */
1853
+ consentNotice(meeting: MeetingRecord | null): string | null;
1854
+ acknowledge(meetingId: MeetingId): Promise<void>;
1855
+ }
1856
+ declare function createRecordingController(options: RecordingControllerOptions): RecordingController;
1857
+ /** The indicator's words. One definition, so no surface invents a softer one. */
1858
+ declare function recordingLabel(state: RecordingState): string | null;
1859
+
1860
+ /**
1861
+ * THE TOKEN FLOW — the ONLY authorization path into a room (R1).
1862
+ *
1863
+ * 🚨 **THE CLIENT NEVER HOLDS A LIVEKIT SECRET.** Not in an env var, not in a
1864
+ * build-time constant, not "just for local dev". A LiveKit API secret in a
1865
+ * browser bundle mints a token for ANY room in the project, including one the
1866
+ * holder was never invited to — it is a full compromise of every meeting in the
1867
+ * organization. This package therefore has no code path that could accept one:
1868
+ * it asks aidream, and aidream mints.
1869
+ *
1870
+ * Four hard parts live here so no host reimplements them (C22):
1871
+ *
1872
+ * 1. **Single flight.** A React strict-mode double effect, a re-render, and a
1873
+ * reconnect all ask at once; they share ONE request. Two tokens for one
1874
+ * identity is not harmless — LiveKit disconnects the older session, which
1875
+ * looks exactly like "my call randomly drops when I open the tab".
1876
+ * 2. **Expiry is honored BEFORE it bites.** A token is refreshed at 80% of its
1877
+ * life, not when a reconnect fails at 100%. A meeting that outlives its
1878
+ * token and cannot reconnect is the classic 2-hour-meeting bug.
1879
+ * 3. **Guests are first-class, not an exception.** A guest has no session and
1880
+ * the request carries a display name instead; the SERVER decides what a
1881
+ * guest may do. There is no client-side "is this person allowed" branch.
1882
+ * 4. **A missing session is a lifecycle moment.** Between sign-in and token
1883
+ * arrival, the fetch retries ONCE after the host's session resolves and then
1884
+ * reports `session-unavailable` as a warning — never a red error, never a
1885
+ * retry storm.
1886
+ */
1887
+
1888
+ interface RoomToken {
1889
+ readonly token: string;
1890
+ readonly serverUrl: string;
1891
+ readonly roomName: RoomName;
1892
+ readonly identity: ParticipantIdentity;
1893
+ readonly displayName: string;
1894
+ /** ISO. The package refreshes at 80% of the remaining life. */
1895
+ readonly expiresAt: string;
1896
+ /** What the SERVER granted. The client renders from this; it never decides. */
1897
+ readonly canPublish: boolean;
1898
+ readonly canSubscribe: boolean;
1899
+ readonly canRecord: boolean;
1900
+ readonly isHost: boolean;
1901
+ /**
1902
+ * 🚨 THE SERVER'S WORD ON WHO THIS JOINER IS (MRI-A11) — `host`,
1903
+ * `participant` or `guest`, minted from `host_user_id` or a real iam grant.
1904
+ * The same string is minted into the LiveKit token's participant metadata and
1905
+ * its `matrx.participant.role` attribute, so the roster and this response
1906
+ * cannot disagree. A server that predates MRI-A11 sends no role, and the
1907
+ * fallback below reads the older `is_host` boolean rather than pretending.
1908
+ */
1909
+ readonly role: ParticipantRole;
1910
+ /** True when the server put this joiner in the lobby (D6). */
1911
+ readonly lobby: boolean;
1912
+ /**
1913
+ * Meet wave 5: the breakout room this pass is for, or null for the main room. The
1914
+ * pass is still keyed by the MEETING's room name — it is this person's pass for the
1915
+ * meeting, and the server accepts a breakout pass wherever the meeting's is asked.
1916
+ */
1917
+ readonly breakoutRoomId?: string | null | undefined;
1918
+ }
1919
+
1920
+ interface TokenClientOptions {
1921
+ /** The ONE org-aware aidream client (MRI-A16). Owns base url and headers. */
1922
+ api: MeetApi;
1923
+ /** Injected clock, so expiry logic is provable without waiting two hours. */
1924
+ now?: (() => number) | undefined;
1925
+ /**
1926
+ * Where a GUEST's pass survives a page close (MRF-1). Absent means passes
1927
+ * live only in memory, which is correct for a member (they have a session to
1928
+ * mint a new one from) and is why this is optional rather than assumed.
1929
+ */
1930
+ storage?: RoomTokenStorage | undefined;
1931
+ }
1932
+ /**
1933
+ * 🚨 **WHY A GUEST'S PASS MUST OUTLIVE THE TAB (MRF-1).**
1934
+ *
1935
+ * A guest has no session, so their only credential for a meeting is the room
1936
+ * token this server minted for them. After the meeting ends, `/token` refuses
1937
+ * to mint another one — correctly: the room is gone and nobody may rejoin it.
1938
+ * So a guest who closes the tab and comes back to read the summary has, in a
1939
+ * purely in-memory client, *no way to prove they were ever there* — and the one
1940
+ * honest thing left to show them is a refusal.
1941
+ *
1942
+ * Persisting the pass is what makes the durable link durable for them. It is
1943
+ * per-device by nature (it is stored beside the device id that minted it) and
1944
+ * it expires on its own: `meet.room_token_ttl_seconds` is the organization's
1945
+ * setting for how long, and an expired pass produces the renew-the-link
1946
+ * sentence, never "you were never here".
1947
+ */
1948
+ interface RoomTokenStorage {
1949
+ read(roomName: RoomName): string | null;
1950
+ write(roomName: RoomName, serialized: string): void;
1951
+ remove(roomName: RoomName): void;
1952
+ }
1953
+ /** `localStorage` when it exists; a no-op that SAYS so otherwise. */
1954
+ declare function createWebRoomTokenStorage(onDiagnostic?: (message: string, remedy: string) => void): RoomTokenStorage;
1955
+ interface TokenRequest {
1956
+ readonly roomName: RoomName;
1957
+ readonly identity: MeetIdentity;
1958
+ /** Present for a meeting join; absent for an ad-hoc call room. */
1959
+ readonly meetingId?: string | undefined;
1960
+ /** The public meeting slug, when joining by link without knowing the id. */
1961
+ readonly slug?: string | undefined;
1962
+ /** Meet wave 5: a breakout room of this meeting; absent = the main room. */
1963
+ readonly breakoutRoomId?: string | null | undefined;
1964
+ }
1965
+ interface TokenClient {
1966
+ /** A currently-valid token, minting or refreshing only when it must. */
1967
+ get(request: TokenRequest): Promise<RoomToken>;
1968
+ /** Drop the cached token for a room (after a `denied`, or on leave). */
1969
+ invalidate(roomName: RoomName): void;
1970
+ /** True when the cached token is inside its refresh window. */
1971
+ needsRefresh(roomName: RoomName): boolean;
1972
+ /**
1973
+ * The pass this DEVICE holds for a room, **including an expired one** (MRF-1).
1974
+ *
1975
+ * 🚨 The expired case is the whole reason this returns the token instead of
1976
+ * `null`: "your pass expired, open the link again" and "you were never in
1977
+ * this meeting" are different sentences, and only the caller that can see the
1978
+ * expiry can say the right one. Never used to join — `get` mints for that.
1979
+ */
1980
+ remembered(roomName: RoomName): RoomToken | null;
1981
+ }
1982
+ /** The aidream routes this package calls. ONE place, so a rename is one edit. */
1983
+ /**
1984
+ * EVERY aidream ROUTE THIS PACKAGE CALLS, in one object.
1985
+ *
1986
+ * This is the contract in both directions: aidream carries a test
1987
+ * (`services/meet/tests/test_meet_service.py`) that reads THIS object out of
1988
+ * this source file and fails if the server stops serving one of these paths.
1989
+ * The package is the spec.
1990
+ *
1991
+ * `meetingEnd` is pinned here BEFORE the server serves it (MRI-A6 owns landing
1992
+ * it). That is deliberate and it is not a lie on screen: the control that calls
1993
+ * it renders, and a 404 from a server that has not landed A6 is reported as the
1994
+ * honest sentence "Ending a meeting is not available on this server yet" —
1995
+ * never a dead button, never a spinner, and never a silent no-op.
1996
+ */
1997
+ declare const MEET_ROUTES: {
1998
+ readonly token: "/api/v1/meet/token";
1999
+ readonly recordingStart: "/api/v1/meet/recording/start";
2000
+ readonly recordingStop: "/api/v1/meet/recording/stop";
2001
+ readonly noteTakerStart: "/api/v1/meet/agent/start";
2002
+ readonly noteTakerStop: "/api/v1/meet/agent/stop";
2003
+ /** In-meeting Q&A. Streams NDJSON; body is `{meeting_id, question}` only. */
2004
+ readonly intelligenceAsk: "/api/v1/meet/intelligence/ask";
2005
+ /** A host ends the meeting for everyone. Served by MRI-A6. */
2006
+ readonly meetingEnd: "/api/v1/meet/end";
2007
+ /**
2008
+ * THE POST-MEETING RECORD FOR A READER WITH NO DATABASE (MRF-1). A `GET`,
2009
+ * authorized by the room token this device already holds, carried in the
2010
+ * `x-meet-room-token` header — never a query parameter, because a bearer
2011
+ * credential in a URL lands in access logs, referrers and history.
2012
+ */
2013
+ readonly record: "/api/v1/meet/record";
2014
+ /** A host chooses what guests may read of ONE meeting (MRF-1). */
2015
+ readonly recordAccess: "/api/v1/meet/record/access";
2016
+ /**
2017
+ * KEEP YOUR NOTES (2026-10-01): a guest who just created an account presents
2018
+ * their kept pass in `x-meet-room-token` with their new session, and the
2019
+ * meeting they attended becomes theirs to read. Idempotent; never a grant.
2020
+ */
2021
+ readonly claim: "/api/v1/meet/claim";
2022
+ /**
2023
+ * Meet wave 1 (0.8): bring every invitee up to date after a write — sends the
2024
+ * invitation / update / cancellation emails with their `.ics` and re-queues
2025
+ * reminders. Idempotent. `{meeting_id}` is substituted by `meetingPath`.
2026
+ */
2027
+ readonly meetingAnnounce: "/api/v1/meet/meetings/{meeting_id}/announce";
2028
+ /** Put a one-off meeting on the caller's own Google Calendar (gated by the server). */
2029
+ readonly meetingGoogleCalendar: "/api/v1/meet/meetings/{meeting_id}/google-calendar";
2030
+ /**
2031
+ * Meet wave 3: the recap email after the meeting. `GET` = the draft (subject,
2032
+ * body from the wrap-up, who it can go to); `POST` = send what the host
2033
+ * reviewed (`dry_run` renders and queues nothing). Never sent automatically.
2034
+ */
2035
+ readonly meetingFollowUp: "/api/v1/meet/meetings/{meeting_id}/follow-up";
2036
+ /**
2037
+ * Meet wave 2: EVERY in-meeting host power (mute, stop video, mute all, ask to
2038
+ * unmute, spotlight, lower hands, co-host, hand over, rename, remove, report).
2039
+ * Decided by the server from the meeting itself; a guest's pass rides
2040
+ * `x-meet-room-token`.
2041
+ */
2042
+ readonly moderate: "/api/v1/meet/moderate";
2043
+ /** Meet wave 2: the SAVED chat — `POST` sends to everyone, `GET ?meeting_id=` reads history. */
2044
+ readonly chat: "/api/v1/meet/chat";
2045
+ /**
2046
+ * Meet wave 5: polls, Q&A, the shared whiteboard and breakout rooms. `POST` acts
2047
+ * (`{meeting_id, action, …}`), `GET ?meeting_id=` reads what this viewer may see.
2048
+ */
2049
+ readonly collab: "/api/v1/meet/collab";
2050
+ /** Meet wave 5: translate finished caption lines into this viewer's language. */
2051
+ readonly captionsTranslate: "/api/v1/meet/captions/translate";
2052
+ };
2053
+ /** A `MEET_ROUTES` template with its `{meeting_id}` filled in, URL-encoded. */
2054
+ declare function meetingPath(template: string, meetingId: string): string;
2055
+ /** The header the guest's pass rides on. Pinned on both sides. */
2056
+ declare const ROOM_TOKEN_HEADER = "x-meet-room-token";
2057
+ declare function parseRoomToken(body: unknown, roomName: RoomName): RoomToken;
2058
+ declare function createTokenClient(options: TokenClientOptions): TokenClient;
2059
+
2060
+ /**
2061
+ * THE MEETING RECORD — what a meeting IS once the room is gone (MRI-D2).
2062
+ *
2063
+ * 🚨 THE DEFECT THIS EXISTS TO KILL. Through 2026-09-08 a meeting produced a
2064
+ * wrap-up summary, decisions, owner-assigned action items, five windows of live
2065
+ * notes, thirty-two transcript segments and a recording — and then had NO
2066
+ * SURFACE AT ALL. `/meet/<slug>` fell back to `<PreJoin>` on an ended meeting,
2067
+ * so the durable link that R3 promises would outlive the meeting resolved to a
2068
+ * screen offering to join a room that no longer exists. Everything the campaign
2069
+ * built after the room closed was reachable only from SQL.
2070
+ *
2071
+ * Three rules hold this file together:
2072
+ *
2073
+ * 1. **Every read is INDEPENDENT and every refusal is NAMED.** The transcript,
2074
+ * the notes, the attendance list and the recording live in four RLS-guarded
2075
+ * tables, and a guest holding the link can read NONE of them (only
2076
+ * `meet_meeting_by_slug` is granted to `anon`). One `Promise.all` would turn
2077
+ * a permission boundary into a blank page. Known guest restrictions never issue private reads; each authenticated read that is refused
2078
+ * becomes a `MeetingRecordRestriction` with a sentence — the reader is told
2079
+ * what exists and why they cannot see it, which is the difference between a
2080
+ * record they can ask for and a product that looks broken.
2081
+ * 2. **A partial record is still a record.** A failed transcript read never
2082
+ * hides the summary somebody CAN read.
2083
+ * 3. **Nothing here computes meaning.** Grouping, ordering and formatting only.
2084
+ * The summary, decisions and action items are durable rows written by the
2085
+ * wrap-up mandate (D5/D18); this module never re-derives them from the
2086
+ * transcript, because two sources for one fact is how they start to disagree.
2087
+ */
2088
+
2089
+ /** One thing the reader could not see, and the honest reason. Never a blank. */
2090
+ interface MeetingRecordRestriction {
2091
+ /** Which part of the record: `transcript`, `notes`, `attendance`, `recording`. */
2092
+ readonly part: "transcript" | "notes" | "attendance" | "recording";
2093
+ readonly message: string;
2094
+ readonly remedy: string;
2095
+ }
2096
+ /** One visible access state, made from restrictions that share one remedy. */
2097
+ interface MeetingRecordRestrictionNotice {
2098
+ readonly parts: readonly MeetingRecordRestriction["part"][];
2099
+ readonly message: string;
2100
+ readonly remedy: string;
2101
+ }
2102
+ /** A run of consecutive lines by one speaker — how a person reads a transcript. */
2103
+ interface TranscriptBlock {
2104
+ readonly id: string;
2105
+ /** Who spoke. Blocks break on THIS, never on the rendered name (MRI-A16). */
2106
+ readonly identity: ParticipantIdentity;
2107
+ readonly speaker: string;
2108
+ /** ISO of the first line in the block. */
2109
+ readonly startedAt: string;
2110
+ readonly lines: readonly TranscriptSegment[];
2111
+ }
2112
+ interface MeetingRecordBundle {
2113
+ readonly meeting: MeetingRecord;
2114
+ readonly attendees: readonly MeetingAttendee[];
2115
+ /** The wrap-up summary: `kind='note'` with `metadata.role='summary'` (D5). */
2116
+ readonly summary: MeetingNote | null;
2117
+ /** D20: the AUTHORITATIVE set — window rows the wrap-up superseded are out. */
2118
+ readonly decisions: readonly MeetingNote[];
2119
+ readonly actionItems: readonly MeetingNote[];
2120
+ /** The live-notes windows, in window order (D3). */
2121
+ readonly liveNotes: readonly MeetingNote[];
2122
+ readonly transcript: readonly TranscriptSegment[];
2123
+ /** The most recent recording attempt, whatever state it reached. */
2124
+ readonly recording: RecordingRecord | null;
2125
+ /** What this reader could NOT see. Rendered; never swallowed. */
2126
+ readonly restrictions: readonly MeetingRecordRestriction[];
2127
+ /**
2128
+ * Identity → name, learned from everything this read COULD see: the durable
2129
+ * attendance rows and the transcript's own `speaker` column. It is what stops
2130
+ * a raw LiveKit token reaching the screen when one of those reads was refused
2131
+ * (MRI-A16).
2132
+ */
2133
+ readonly names: NameBook;
2134
+ }
2135
+ /**
2136
+ * The current wrap-up summary. Repairs preserve the prior durable row for
2137
+ * history, then mark it `superseded_by`; every current-facing projection must
2138
+ * therefore make the same distinction as decisions and action items.
2139
+ */
2140
+ declare function activeWrapUpSummary(notes: readonly MeetingNote[]): MeetingNote | null;
2141
+ /**
2142
+ * The four durable reads behind a record, as the database returned them, plus
2143
+ * what the reader could not see. `assembleMeetingRecord` turns them into the
2144
+ * bundle; a cache that keeps them can apply a realtime row to one part and
2145
+ * assemble again, without reading the other three (0.7.79).
2146
+ */
2147
+ interface MeetingRecordParts {
2148
+ readonly attendees: readonly MeetingAttendee[];
2149
+ /** Every note row (all kinds, superseded included), oldest first. */
2150
+ readonly notes: readonly MeetingNote[];
2151
+ readonly transcript: readonly TranscriptSegment[];
2152
+ /** Newest first; archived recordings are not here. */
2153
+ readonly recordings: readonly RecordingRecord[];
2154
+ readonly restrictions: readonly MeetingRecordRestriction[];
2155
+ }
2156
+ /**
2157
+ * Load the record parts the reader may request; state all restrictions.
2158
+ *
2159
+ * `meeting` is passed in when the caller already resolved it (the slug route
2160
+ * does, through the `anon`-granted RPC) — re-reading it through the table would
2161
+ * fail for exactly the reader who most needs the page to work.
2162
+ */
2163
+ declare function loadMeetingRecord(repository: MeetRepository, meetingId: MeetingId, meeting: MeetingRecord, reader?: Pick<MeetIdentity, "userId">): Promise<MeetingRecordBundle>;
2164
+ /** The four reads, each with its own verdict (see `loadMeetingRecord`). */
2165
+ declare function loadMeetingRecordParts(repository: MeetRepository, meetingId: MeetingId, reader?: Pick<MeetIdentity, "userId">): Promise<MeetingRecordParts>;
2166
+ /** The bundle a reader sees, from the parts. Grouping and ordering only (rule 3). */
2167
+ declare function assembleMeetingRecord(meeting: MeetingRecord, parts: MeetingRecordParts): MeetingRecordBundle;
2168
+ /**
2169
+ * 🚨 **A READ THAT WAS REFUSED IS NOT AN EMPTY RESULT (MRI-A16).**
2170
+ *
2171
+ * On production 2026-09-09 a guest's record view said *"No wrap-up was written
2172
+ * for this meeting"* directly underneath a panel that had just told them, in
2173
+ * the same screen, that the notes are not readable from their account — and
2174
+ * *"Nothing was recorded as a decision"* beside a decision that exists
2175
+ * (MRI-C8). The page asserted an absence it had no way to know, which is worse
2176
+ * than saying nothing: a person who reads "no wrap-up was written" stops
2177
+ * looking for one.
2178
+ *
2179
+ * So no surface writes its own empty sentence any more. It asks here, and gets
2180
+ * either its absence sentence — earned, because the read SUCCEEDED and returned
2181
+ * nothing — or the refusal, said plainly.
2182
+ */
2183
+ declare const REFUSED_ABSENCE = "This record is not shared with guests.";
2184
+ /** Was this part of the record refused to this reader? */
2185
+ declare function wasRefused(bundle: MeetingRecordBundle, part: MeetingRecordRestriction["part"]): boolean;
2186
+ /**
2187
+ * The sentence to show where a part of the record has nothing in it. NEVER an
2188
+ * absence claim over a read that was refused.
2189
+ */
2190
+ declare function absenceSentence(bundle: MeetingRecordBundle, part: MeetingRecordRestriction["part"], absence: string): string;
2191
+ /**
2192
+ * 🚨 **"NO WRAP-UP WAS WRITTEN" IS A CLAIM, AND FOR ~33 SECONDS IT WAS FALSE.**
2193
+ *
2194
+ * On production 2026-09-09 the HOST's record view said *"No wrap-up was written
2195
+ * for this meeting"* between 04:46:42 — the flip from the room to the record —
2196
+ * and 04:47:15, when the wrap-up landed. The read was permitted and genuinely
2197
+ * empty, so this is not the refusal class above; it is the OTHER way a screen
2198
+ * lies, by reading "not here yet" as "never happened". A person who reads *"no
2199
+ * wrap-up was written"* closes the tab.
2200
+ *
2201
+ * The meeting row already knows the difference, and it is not a guess: the
2202
+ * wrap-up is a durable job, `enqueue_wrap_up` stamps
2203
+ * `metadata.note_taker.intelligence.wrap_up` when it queues it, and the graph
2204
+ * action stamps `state: "failed"` when the pass refuses TERMINALLY. So:
2205
+ *
2206
+ * - a summary exists → `written`;
2207
+ * - the notes read was refused → `refused` (never an absence claim);
2208
+ * - the job is marked failed → `failed`, in the server's own words;
2209
+ * - the job is queued or running → `in-progress`, with a refresh affordance;
2210
+ * - nothing ever queued one → `absent`, which is the only EARNED absence.
2211
+ *
2212
+ * There is no timeout that flips `in-progress` into `absent`. A wrap-up that is
2213
+ * genuinely stuck says it is still being written — which is TRUE — rather than
2214
+ * inventing a verdict this browser has no way to reach.
2215
+ */
2216
+ type WrapUpStatus = {
2217
+ readonly state: "written";
2218
+ } | {
2219
+ readonly state: "in-progress";
2220
+ readonly message: string;
2221
+ readonly canRefresh: true;
2222
+ } | {
2223
+ readonly state: "failed";
2224
+ readonly message: string;
2225
+ } | {
2226
+ readonly state: "refused";
2227
+ readonly message: string;
2228
+ } | {
2229
+ readonly state: "absent";
2230
+ readonly message: string;
2231
+ };
2232
+ /** Where the wrap-up actually is. Never "absent" over a job that is running. */
2233
+ declare function wrapUpStatus(bundle: MeetingRecordBundle): WrapUpStatus;
2234
+ /**
2235
+ * GROUP THE TRANSCRIPT THE WAY A PERSON READS IT: consecutive lines by one
2236
+ * speaker become one block with one timestamp. Reading thirty-two rows each
2237
+ * re-stamped with a name and a clock is how a transcript becomes unreadable.
2238
+ */
2239
+ declare function groupTranscript(segments: readonly TranscriptSegment[], names?: NameBook): readonly TranscriptBlock[];
2240
+ /**
2241
+ * WHAT THE RECORDING IS DOING, in a sentence — including the two states people
2242
+ * hit most and understand least. `processing` is not "broken" and `failed` is
2243
+ * never silence: both say what happened and what to do.
2244
+ */
2245
+ declare function recordingNotice(recording: RecordingRecord | null): string | null;
2246
+ /** Only an `available` recording has a file to play. Nothing else pretends to. */
2247
+ declare function playableRecordingFileId(recording: RecordingRecord | null): string | null;
2248
+ /**
2249
+ * A MEETING IS OVER WHEN THE SERVER SAYS SO. `ended_at` is stamped by
2250
+ * `POST /v1/meet/end` or by the auto-end evaluation (MRI-A6) — never by a
2251
+ * client noticing the room is empty.
2252
+ */
2253
+ declare function hasEnded(meeting: MeetingRecord | null): boolean;
2254
+ /**
2255
+ * "Thursday, 8 September 2026, 4:21 PM · 19 minutes". One shape everywhere.
2256
+ *
2257
+ * THE SPAN IS `formatDurationMs`'s (2026-09-12). This body used to divide by
2258
+ * `60_000` and pluralise "minute" itself — a duration formatter living in a
2259
+ * PACKAGE, found the day `aidream/apps/shared` became a scan root. `@ai-matrx/
2260
+ * meet` already depends on `@ai-matrx/kit`, and `style: "long"` is precisely
2261
+ * this voice ("19 minutes", "1 minute"). Three things improve by adopting it:
2262
+ * a 40-second meeting read "0 minutes" and now reads "40 seconds" (the
2263
+ * confident zero the package refuses), a 19-minute-35-second meeting read
2264
+ * "20 minutes" and now reads "19 minutes" (prose asserts at-least, so it
2265
+ * floors), and a 90-minute meeting reads "1 hour" instead of "90 minutes".
2266
+ */
2267
+ declare function meetingWhen(meeting: MeetingRecord, locale?: string): string;
2268
+ /** How much of a finished meeting a GUEST may read. Pinned with the server. */
2269
+ type GuestRecordAccess = "none" | "summary" | "full";
2270
+ /** Widest last. The ORDER is the comparison, exactly as it is on the server. */
2271
+ declare const GUEST_RECORD_ACCESS_LEVELS: readonly GuestRecordAccess[];
2272
+ /**
2273
+ * What a HOST may choose for one meeting. `default` is not a fourth level — it
2274
+ * is the absence of a per-meeting choice, and it is the only way to un-choose.
2275
+ */
2276
+ type GuestRecordAccessSetting = "default" | GuestRecordAccess;
2277
+ declare const GUEST_RECORD_ACCESS_CHOICES: readonly GuestRecordAccessSetting[];
2278
+ /** `meet_meetings.metadata.guest_record_access`. No DDL; a setting on a row. */
2279
+ declare const GUEST_RECORD_ACCESS_METADATA_KEY = "guest_record_access";
2280
+ /** What a host chose for THIS meeting, read from the row they already have. */
2281
+ declare function guestRecordAccessSetting(meeting: MeetingRecord | null): GuestRecordAccessSetting;
2282
+ /** The sentence on the host's control. Plain English, never a token. */
2283
+ declare function guestRecordAccessLabel(setting: GuestRecordAccessSetting): string;
2284
+ /** What each choice actually means for the people who were in the room. */
2285
+ declare function guestRecordAccessExplanation(setting: GuestRecordAccessSetting): string;
2286
+ /** What came back from the server, so a surface never has to infer a level. */
2287
+ interface GuestRecordAccessResolution {
2288
+ readonly level: GuestRecordAccess;
2289
+ /** `meeting` when a host chose for this one; `organization` otherwise. */
2290
+ readonly source: "meeting" | "organization";
2291
+ }
2292
+ /**
2293
+ * Repeated policy refusals are one access state, not four alarming cards.
2294
+ *
2295
+ * The individual rows remain durable data for absence handling in their own
2296
+ * sections. This projection groups only rows with the exact same remedy, so a
2297
+ * distinct transport or integrity failure remains visible on its own.
2298
+ */
2299
+ declare function summarizeMeetingRecordRestrictions(restrictions: readonly MeetingRecordRestriction[]): readonly MeetingRecordRestrictionNotice[];
2300
+ /**
2301
+ * Read the record the server shares with THIS guest.
2302
+ *
2303
+ * `roomToken` is the pass this device kept from the meeting
2304
+ * (`TokenClient.remembered`). It is presented in a header, never appended to
2305
+ * the URL. A guest with no pass, an expired pass, or a level of `none` gets the
2306
+ * server's refusal — as restrictions on a real bundle, so the screen says what
2307
+ * it cannot show instead of collapsing into an error page.
2308
+ */
2309
+ declare function loadGuestMeetingRecord(args: {
2310
+ api: MeetApi;
2311
+ repository: MeetRepository;
2312
+ meetingId: MeetingId;
2313
+ meeting: MeetingRecord;
2314
+ roomToken: RoomToken | null;
2315
+ /**
2316
+ * A SIGNED-IN reader the database refused (from another organization, or a
2317
+ * guest who later created an account). Their session stands in for the
2318
+ * pass: the server reads their `user:<id>` admission or the attendance they
2319
+ * claimed, and shares what guests may read (2026-10-01).
2320
+ */
2321
+ session?: boolean;
2322
+ }): Promise<MeetingRecordBundle>;
2323
+ /**
2324
+ * A HOST chooses what guests may read of THIS meeting.
2325
+ *
2326
+ * Host power is the SERVER's decision and it re-checks it; this function exists
2327
+ * so the control has one call to make, and it returns what the choice RESOLVES
2328
+ * to so the screen never has to guess what "my organization's setting" means
2329
+ * today.
2330
+ */
2331
+ declare function setGuestRecordAccess(args: {
2332
+ api: MeetApi;
2333
+ meetingId: MeetingId;
2334
+ organizationId: OrganizationId;
2335
+ access: GuestRecordAccessSetting;
2336
+ }): Promise<{
2337
+ readonly setting: GuestRecordAccessSetting;
2338
+ readonly effective: GuestRecordAccessResolution;
2339
+ }>;
2340
+ /** What a guest is told when the level shares nothing. Rendered, never hidden. */
2341
+ declare const GUEST_RECORD_NOT_SHARED: Readonly<Record<"recording" | "transcript" | "notes" | "attendance", string>>;
2342
+
2343
+ /**
2344
+ * AFTER THE MEETING (Meet wave 3) — the pure pieces every client needs once the
2345
+ * room is gone: the attendance report, the transcript and notes as documents,
2346
+ * a moment in the transcript as a moment in the recording, search within one
2347
+ * transcript, and the two server calls behind the recap email and the saved
2348
+ * chat.
2349
+ *
2350
+ * Nothing here computes MEANING (same rule as `record.ts`): the summary,
2351
+ * decisions and action items are the wrap-up's durable rows; this file only
2352
+ * arranges, measures and formats them. Every function is framework-free so a
2353
+ * worker, a test and a browser produce byte-identical exports.
2354
+ */
2355
+
2356
+ /**
2357
+ * Who someone was, in the words an attendance report uses. `signed-in guest` is
2358
+ * an account from outside the meeting's organization that followed the link;
2359
+ * `guest` has no account at all.
2360
+ */
2361
+ type AttendanceKind = "host" | "cohost" | "member" | "signed-in guest" | "guest" | "assistant";
2362
+ interface AttendanceRow {
2363
+ readonly id: string;
2364
+ readonly identity: string;
2365
+ readonly name: string;
2366
+ readonly userId: string | null;
2367
+ readonly kind: AttendanceKind;
2368
+ /** First time they were in the room; null when they knocked and never came in. */
2369
+ readonly joinedAt: string | null;
2370
+ /** When they left; null while still in the room or when the leave was never recorded. */
2371
+ readonly leftAt: string | null;
2372
+ /**
2373
+ * Time in the room. A missing `leftAt` is measured to the meeting's end —
2374
+ * the room closed around them — and `null` only when neither is known.
2375
+ */
2376
+ readonly durationMs: number | null;
2377
+ /** True when `durationMs` was measured to the meeting's end, not a recorded leave. */
2378
+ readonly durationToEnd: boolean;
2379
+ readonly admission: MeetingAttendee["admissionState"];
2380
+ /** How many times this person came in (a rejoin or a second device). */
2381
+ readonly joins: number;
2382
+ }
2383
+ /**
2384
+ * The attendance report of ONE meeting, host first then by arrival. People who
2385
+ * knocked and were never let in are kept (with no join time) — "who asked to
2386
+ * come in" is part of the record — but a denied knock has no duration.
2387
+ */
2388
+ /**
2389
+ * WHO a participant row is, as a PERSON (verifier, 2026-09-27: an export listed
2390
+ * the same attendee twice under "attended"). A signed-in person is their
2391
+ * account — every device and every rejoin is one person. A guest without an
2392
+ * account gets a fresh `guest:<random>` identity on every join, so the name
2393
+ * they typed is the only thread between their joins (Zoom's attendee report
2394
+ * merges link guests the same way); two different accountless guests who
2395
+ * typed the identical name read as one, which is the lesser error.
2396
+ */
2397
+ declare function attendeeKey(person: MeetingAttendee, names?: NameBook): string;
2398
+ /**
2399
+ * One row per PERSON, in first-seen order: the row that actually joined (the
2400
+ * earliest) represents them. For a list of names or a head-count; the
2401
+ * attendance REPORT merges times as well (`attendanceReport`).
2402
+ */
2403
+ declare function uniqueAttendees(attendees: readonly MeetingAttendee[], names?: NameBook): readonly MeetingAttendee[];
2404
+ declare function attendanceReport(meeting: MeetingRecord, attendees: readonly MeetingAttendee[], names?: NameBook): readonly AttendanceRow[];
2405
+ /** Headline numbers for the top of a report. The assistant is never a person. */
2406
+ declare function attendanceTotals(rows: readonly AttendanceRow[]): {
2407
+ readonly people: number;
2408
+ readonly guests: number;
2409
+ readonly neverAdmitted: number;
2410
+ readonly averageMs: number | null;
2411
+ };
2412
+ declare const ATTENDANCE_CSV_HEADER: readonly ["Meeting", "Meeting started", "Name", "Attended as", "Joined", "Left", "Minutes in the meeting", "Admission"];
2413
+ /**
2414
+ * CSV for a spreadsheet: one header row, one row per person, ISO timestamps (a
2415
+ * spreadsheet parses them; a formatted local clock loses the zone). Several
2416
+ * meetings — a series — concatenate with one header.
2417
+ */
2418
+ declare function attendanceCsv(reports: readonly {
2419
+ meeting: MeetingRecord;
2420
+ rows: readonly AttendanceRow[];
2421
+ }[]): string;
2422
+ /**
2423
+ * Where an instant of the meeting falls in the RECORDING, in milliseconds from
2424
+ * its start — or `null` when the recording does not cover it (a recording is
2425
+ * often a few minutes of a longer meeting). Never a clamp: seeking a line that
2426
+ * was said before recording began to 0:00 would play the wrong words.
2427
+ */
2428
+ declare function recordingOffsetMs(at: string, recording: RecordingRecord | null): number | null;
2429
+ /** `m:ss` or `h:mm:ss` — a position in a recording or a meeting. */
2430
+ declare function clockPosition(offsetMs: number): string;
2431
+ interface TranscriptMatch {
2432
+ readonly segmentId: string;
2433
+ /** [start, end) character ranges inside the segment's text, in order. */
2434
+ readonly ranges: readonly (readonly [number, number])[];
2435
+ }
2436
+ /**
2437
+ * Case-insensitive search of the transcript text AND the speaker's
2438
+ * name. Returns matches in transcript order. An empty query matches nothing.
2439
+ */
2440
+ declare function searchTranscript(segments: readonly TranscriptSegment[], query: string, names?: NameBook): readonly TranscriptMatch[];
2441
+ type TranscriptFormat = "txt" | "md" | "vtt";
2442
+ interface ExportOptions {
2443
+ readonly names?: NameBook;
2444
+ /** Timestamps as a clock of day (default) or offsets from `zero`. */
2445
+ readonly locale?: string;
2446
+ readonly timeZone?: string;
2447
+ }
2448
+ /**
2449
+ * The transcript as a document. `txt` and `md` group consecutive lines by one
2450
+ * speaker with the clock of day; `vtt` is WebVTT cued from the recording's
2451
+ * start when a recording exists (so it plays as captions over it), otherwise
2452
+ * from the meeting's start — and a cue ends where the next line begins.
2453
+ */
2454
+ declare function transcriptDocument(bundle: Pick<MeetingRecordBundle, "meeting" | "transcript" | "recording" | "names">, format: TranscriptFormat, options?: ExportOptions): string;
2455
+ /**
2456
+ * The meeting's notes as Markdown: when, who attended, the summary, decisions
2457
+ * and action items (with owners), and the link back. What a person pastes into
2458
+ * a doc or an email; also the source for PDF / Word export.
2459
+ */
2460
+ declare function notesDocument(bundle: Pick<MeetingRecordBundle, "meeting" | "summary" | "decisions" | "actionItems" | "attendees" | "names">, options?: {
2461
+ readonly link?: string | null;
2462
+ readonly when?: string | null;
2463
+ }): string;
2464
+ /** How long the meeting ran, in words ("42 minutes"), or null when unknown. */
2465
+ declare function meetingLength(meeting: MeetingRecord): string | null;
2466
+ interface MeetingChatHistory {
2467
+ readonly messages: readonly RoomChatMessage[];
2468
+ /** The server's refusal, in its words, when the history could not be read. */
2469
+ readonly refusal: {
2470
+ readonly message: string;
2471
+ readonly remedy: string;
2472
+ } | null;
2473
+ }
2474
+ /**
2475
+ * The saved chat of a finished meeting (`GET /v1/meet/chat`). Private messages
2476
+ * are never saved, by design, so they are never here. A refusal is returned as
2477
+ * data — the record still renders everything else.
2478
+ */
2479
+ declare function loadMeetingChat(api: MeetApi, meetingId: MeetingId, roomToken?: string | null): Promise<MeetingChatHistory>;
2480
+ /** One person the recap can go to, as the server resolved them. */
2481
+ interface FollowUpRecipient {
2482
+ /** Stable key for the choice: `user:<id>` or `email:<address>`. */
2483
+ readonly key: string;
2484
+ readonly name: string;
2485
+ /** The address, when this reader may see it (the host sees their guests'). */
2486
+ readonly email: string | null;
2487
+ readonly userId: string | null;
2488
+ /** `invitee`, `attendee` or both — where the server found them. */
2489
+ readonly sources: readonly ("invitee" | "attendee")[];
2490
+ /** False when the server has no way to reach them (no account, no address). */
2491
+ readonly reachable: boolean;
2492
+ }
2493
+ /** The draft the host reviews. The text is the wrap-up's own, arranged. */
2494
+ interface FollowUpDraft {
2495
+ readonly subject: string;
2496
+ readonly body: string;
2497
+ readonly recipients: readonly FollowUpRecipient[];
2498
+ /** False while the wrap-up has not been written — the draft says so. */
2499
+ readonly wrapUpWritten: boolean;
2500
+ /** When a recap for this meeting was last sent, and to how many. */
2501
+ readonly lastSent: {
2502
+ readonly at: string;
2503
+ readonly count: number;
2504
+ } | null;
2505
+ }
2506
+ interface FollowUpSendResult {
2507
+ readonly dryRun: boolean;
2508
+ /** Recipients the send queued (or, on a dry run, would queue). */
2509
+ readonly queued: readonly string[];
2510
+ /** Recipient keys skipped, each with the reason. */
2511
+ readonly skipped: readonly {
2512
+ readonly key: string;
2513
+ readonly reason: string;
2514
+ }[];
2515
+ }
2516
+ /** The recap draft for a finished meeting. The host, a co-host or an editor. */
2517
+ declare function loadFollowUpDraft(api: MeetApi, meetingId: MeetingId): Promise<FollowUpDraft>;
2518
+ /**
2519
+ * Send the recap the host reviewed. `dryRun` resolves every recipient and
2520
+ * renders every email but queues nothing — how a test proves the path without
2521
+ * mailing a real inbox. Never called automatically.
2522
+ */
2523
+ declare function sendFollowUp(api: MeetApi, meetingId: MeetingId, input: {
2524
+ readonly subject: string;
2525
+ readonly body: string;
2526
+ readonly recipientKeys: readonly string[];
2527
+ readonly dryRun?: boolean;
2528
+ }): Promise<FollowUpSendResult>;
2529
+
2530
+ /**
2531
+ * MEET WAVE 1 — the two server calls a management screen makes (0.8).
2532
+ *
2533
+ * Every WRITE (schedule, edit, reschedule, cancel, archive, occurrence change,
2534
+ * invite, answer) goes straight to the database through `MeetRepository` — the
2535
+ * database decides who may. What a browser cannot do is send an email with a
2536
+ * calendar object in it, so after any of those writes a screen calls
2537
+ * `announceMeeting`: the server tells each invitee exactly what they have not
2538
+ * been told (invitation, update or cancellation, each with its `.ics`), and
2539
+ * re-queues reminders. It is idempotent — call it after every save.
2540
+ */
2541
+
2542
+ interface AnnounceResult {
2543
+ readonly invitations: number;
2544
+ readonly updates: number;
2545
+ readonly cancellations: number;
2546
+ readonly alreadyCurrent: number;
2547
+ readonly remindersQueued: number;
2548
+ readonly remindersCancelled: number;
2549
+ }
2550
+ /** Bring every invitee's inbox and calendar up to date. The host, a co-host or an editor. */
2551
+ declare function announceMeeting(api: MeetApi, meetingId: MeetingId): Promise<AnnounceResult>;
2552
+ /**
2553
+ * Put a one-off meeting on the caller's own connected Google Calendar. GATED by
2554
+ * the server to Google's review accounts while the Calendar scope is in review —
2555
+ * a refusal is a `forbidden` MeetError carrying the server's sentence. Everyone
2556
+ * already gets the meeting into any calendar through the invitation's `.ics`.
2557
+ */
2558
+ declare function pushMeetingToGoogleCalendar(api: MeetApi, meetingId: MeetingId, target: {
2559
+ connectionId: string;
2560
+ calendarId?: string;
2561
+ }): Promise<Record<string, unknown>>;
2562
+
2563
+ /**
2564
+ * THE ROOM ENGINE — the object a host mounts once and never reasons about.
2565
+ *
2566
+ * It owns every sequence a calling product gets wrong: ask for a token, connect,
2567
+ * project the roster, carry the in-room control plane, keep the phase honest
2568
+ * through a reconnect, and tear everything down exactly once. A consumer's job
2569
+ * is `engine.join(...)` and rendering a snapshot.
2570
+ *
2571
+ * THREE rules are structural here, not documented-and-hoped:
2572
+ *
2573
+ * 1. **The phase never lies.** `connecting` becomes `connected` only when the
2574
+ * driver says so, and any failure becomes `failed` WITH the remedy attached
2575
+ * to the snapshot. There is no path that leaves a spinner running forever —
2576
+ * the single most common complaint about every video product.
2577
+ * 2. **Role comes from the TOKEN, never from the client.** A participant's role
2578
+ * is read out of the server-minted metadata. A client that could decide it
2579
+ * was host could mute anyone; here the claim is unforgeable because the
2580
+ * server signed it.
2581
+ * 3. **Teardown is idempotent and total.** Leaving twice, or unmounting during
2582
+ * a connect, disconnects once and leaves no listener behind. The half-torn
2583
+ * room is what produces "my camera light stays on after the call".
2584
+ */
2585
+
2586
+ interface JoinRequest {
2587
+ readonly roomName: RoomName;
2588
+ readonly meetingId?: string | undefined;
2589
+ readonly slug?: string | undefined;
2590
+ /** `audio` joins with the camera off. Same room, same code path (D3). */
2591
+ readonly mode: CallMode;
2592
+ /**
2593
+ * Whether to ATTEMPT the microphone on join. Defaults to true. `false` is a
2594
+ * deliberate listen-only join — the pre-join screen's "Join with microphone"
2595
+ * toggle. Either way the join itself never depends on the outcome; a refused
2596
+ * device lands in `snapshot.media.microphone.unavailable`, not in a throw.
2597
+ */
2598
+ readonly microphone?: boolean | undefined;
2599
+ /** Meet wave 5: a breakout room of this meeting; absent/null = the main room. */
2600
+ readonly breakoutRoomId?: string | null | undefined;
2601
+ }
2602
+ interface RoomEngineOptions {
2603
+ store: MeetStore;
2604
+ tokens: TokenClient;
2605
+ identity: MeetIdentity;
2606
+ driverFactory: RoomDriverFactory;
2607
+ /** Canonical public metadata read; room packets alone never authorize teardown. */
2608
+ resolveMeeting?: ((slug: string) => Promise<MeetingRecord>) | undefined;
2609
+ onDiagnostic?: ((event: {
2610
+ level: "info" | "warn" | "error";
2611
+ message: string;
2612
+ remedy?: string;
2613
+ }) => void) | undefined;
2614
+ /** Fired when the local participant leaves for any reason, including kicked. */
2615
+ onLeft?: ((reason: string | null) => void) | undefined;
2616
+ now?: (() => number) | undefined;
2617
+ /** Remembered noise suppression / background choice (Meet wave 2). */
2618
+ effectsStorage?: EffectsStorage | undefined;
2619
+ /**
2620
+ * The device the person PICKED for each kind (the pre-join pickers, the
2621
+ * settings panel). Applied before any track is created — through 0.7.x the
2622
+ * pickers stored a choice nothing ever read, so every join used the browser
2623
+ * default whatever the screen said.
2624
+ */
2625
+ resolveDevice?: ((kind: "audioinput" | "videoinput" | "audiooutput") => string | null) | undefined;
2626
+ }
2627
+ interface RoomEngine {
2628
+ join(request: JoinRequest): Promise<void>;
2629
+ /**
2630
+ * 🚨 THE HOST SAID YES — COME IN.
2631
+ *
2632
+ * A lobby token is minted with `canPublish: false` and `canSubscribe: false`
2633
+ * (that is what makes the lobby real rather than a screen over a live
2634
+ * microphone), so admission is not a UI state change: the cached token has to
2635
+ * be thrown away and a NEW one minted with the grants the server now gives.
2636
+ * Without this the waiting room's own sentence — "you will join automatically
2637
+ * once you are admitted" — is a lie, and the guest waits forever.
2638
+ */
2639
+ rejoin(): Promise<void>;
2640
+ /**
2641
+ * Meet wave 5: go to a breakout room (`null` = back to the main room). The server
2642
+ * mints the pass (`/token` with `breakout_room_id`) and refuses a room the plan does
2643
+ * not allow; the move is a reconnect that never reads as leaving the meeting.
2644
+ */
2645
+ moveToBreakout(roomId: string | null): Promise<void>;
2646
+ leave(): Promise<void>;
2647
+ setMicrophoneEnabled(enabled: boolean): Promise<void>;
2648
+ setCameraEnabled(enabled: boolean): Promise<void>;
2649
+ setScreenShareEnabled(enabled: boolean): Promise<void>;
2650
+ raiseHand(raised: boolean): Promise<void>;
2651
+ react(emoji: string): Promise<void>;
2652
+ /**
2653
+ * A PRIVATE message to one person (Meet wave 2). Data plane only, addressed
2654
+ * to that identity, NEVER saved — see `core/chat.ts` for the saved,
2655
+ * everyone-chat, which goes through the server.
2656
+ */
2657
+ sendPrivateChat(to: ParticipantIdentity, text: string): Promise<void>;
2658
+ /**
2659
+ * Change this participant's own media processing — noise suppression and the
2660
+ * camera background — on the live tracks, and remember it in this browser.
2661
+ * A refused effect is a diagnostic and a revert, never a lost camera.
2662
+ */
2663
+ setEffects(next: Partial<MediaEffects>): Promise<void>;
2664
+ /** Whether this browser can run a background effect at all (absent, never dead). */
2665
+ supportsVideoEffects(): boolean;
2666
+ /** Whether this browser can share a screen at all — the control is ABSENT where it cannot. */
2667
+ supportsScreenShare(): boolean;
2668
+ /** Choose the speaker (audio output) for this meeting. */
2669
+ switchDevice(kind: "audioinput" | "videoinput" | "audiooutput", deviceId: string): Promise<void>;
2670
+ pin(identity: ParticipantIdentity | null): void;
2671
+ setLayout(layout: StageLayout): void;
2672
+ attach(identity: ParticipantIdentity, kind: "camera" | "microphone" | "screen", element: HTMLMediaElement): () => void;
2673
+ token(): RoomToken | null;
2674
+ dispose(): Promise<void>;
2675
+ }
2676
+ /** Where this browser keeps a participant's media choices. */
2677
+ interface EffectsStorage {
2678
+ load(): MediaEffects;
2679
+ save(effects: MediaEffects): void;
2680
+ }
2681
+ /**
2682
+ * 🚨 THE SERVER'S WORD, ON TWO CARRIERS (MRI-A11, feedback `7418ef78`).
2683
+ *
2684
+ * aidream mints the role into the token's participant METADATA (a JSON blob)
2685
+ * AND as a LiveKit participant ATTRIBUTE. Both come out of one decision in
2686
+ * `services/meet/service.py`; neither can be written by a client. The attribute
2687
+ * is preferred because LiveKit delivers the attribute map on every roster
2688
+ * update, while a roster event can carry an empty metadata string — which is
2689
+ * precisely how, through 2026-09-08, EVERY participant projected as
2690
+ * "participant" and no host ever saw a host control in a browser.
2691
+ *
2692
+ * These strings are pinned on the server side too: `test_meet_service.py` reads
2693
+ * them out of THIS file and fails if the two sides drift.
2694
+ */
2695
+ declare const PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
2696
+ declare const PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
2697
+ /**
2698
+ * Metadata is SERVER-MINTED and therefore trusted; it is still parsed
2699
+ * defensively, because a malformed blob must degrade a tile to "participant"
2700
+ * rather than throw inside a roster event and freeze the whole grid.
2701
+ */
2702
+ declare function projectParticipant(driver: DriverParticipant, joinedAt: string): MeetParticipant;
2703
+ declare function createRoomEngine(options: RoomEngineOptions): RoomEngine;
2704
+ /** Exported for the host-power check in UI code — one definition, one rank. */
2705
+ declare function canModerate(participant: MeetParticipant | undefined): boolean;
2706
+ declare function hostUserIds(participants: readonly MeetParticipant[]): readonly UserId[];
2707
+
2708
+ /**
2709
+ * A PARTICIPANT'S OWN MEDIA CHOICES, REMEMBERED (Meet wave 2).
2710
+ *
2711
+ * Noise suppression and the camera background are personal and per device —
2712
+ * Zoom and Meet both remember them — so they live in this browser, never on a
2713
+ * server. Blocked storage means the defaults (suppression on, no background),
2714
+ * never a failed join.
2715
+ */
2716
+
2717
+ declare function createWebEffectsStorage(key?: string): EffectsStorage;
2718
+
2719
+ /**
2720
+ * THE IN-ROOM SERVER CALLS (Meet wave 2) — host moderation and the SAVED chat.
2721
+ *
2722
+ * 🚨 **Why these are server calls and not data packets.** Through 0.7.x a host's
2723
+ * "Mute" and "Remove" were published on the room's data plane (`mx.host`) and
2724
+ * nothing anywhere listened: the buttons did nothing. A client cannot be the
2725
+ * authority on host power, and a guest cannot write the database. So both go to
2726
+ * aidream (`POST /v1/meet/moderate`, `POST|GET /v1/meet/chat`), which decides the
2727
+ * actor's role from the meeting itself, performs the act through LiveKit's server
2728
+ * API, writes what is durable, and tells the room on its own `matrx.meet.*` topic.
2729
+ *
2730
+ * A guest proves who they are with the room pass this server signed for them, in
2731
+ * `x-meet-room-token` — a header, never a URL.
2732
+ */
2733
+
2734
+ /** Every host power the server performs. One vocabulary with `services/meet/moderation.py`. */
2735
+ type ModerationAction = "mute" | "stop_video" | "stop_screen" | "mute_all" | "ask_unmute" | "ask_all_unmute" | "spotlight" | "lower_hand" | "lower_all_hands" | "make_cohost" | "revoke_cohost" | "transfer_host" | "rename" | "remove" | "report";
2736
+ declare const MODERATION_ACTIONS: readonly ModerationAction[];
2737
+ interface ModerationRequest {
2738
+ readonly action: ModerationAction;
2739
+ readonly identity?: ParticipantIdentity | null | undefined;
2740
+ readonly name?: string | undefined;
2741
+ readonly reason?: string | undefined;
2742
+ }
2743
+ interface ModerationResult {
2744
+ readonly action: ModerationAction;
2745
+ readonly affected: readonly string[];
2746
+ /** The server's sentence for the actor. */
2747
+ readonly message: string;
2748
+ }
2749
+ interface InRoomClient {
2750
+ moderate(request: ModerationRequest): Promise<ModerationResult>;
2751
+ /** Send a message to EVERYONE. Saved; shows at once as pending, then as saved. */
2752
+ sendChat(text: string): Promise<void>;
2753
+ /** Read the saved chat once per join, so a late joiner sees what was said. */
2754
+ loadChat(): Promise<void>;
2755
+ /**
2756
+ * Meet wave 5: read the polls, questions, whiteboard and breakout plan this viewer
2757
+ * may see, once per join. A refusal is written to `collab.error`, never thrown away.
2758
+ */
2759
+ loadCollab(): Promise<void>;
2760
+ /**
2761
+ * Meet wave 5: one poll / Q&A / whiteboard / breakout action. The server's answer is
2762
+ * folded into the store at once (the room packet that follows is the same item).
2763
+ * Returns the server's sentence; throws a `MeetError` with the server's words.
2764
+ */
2765
+ collab(request: CollabRequest): Promise<CollabResult>;
2766
+ /**
2767
+ * Meet wave 5: translate finished caption lines into `language` for this viewer.
2768
+ * Results land in `collab.captionTranslations`; a refusal in `captionTranslationError`.
2769
+ */
2770
+ translateCaptions(language: string, lines: readonly {
2771
+ id: string;
2772
+ text: string;
2773
+ }[]): Promise<void>;
2774
+ }
2775
+ /** Every room-item action the server performs. One vocabulary with `services/meet/collab.py`. */
2776
+ type CollabAction = "poll_create" | "poll_update" | "poll_launch" | "poll_close" | "poll_share" | "poll_vote" | "poll_delete" | "question_ask" | "question_upvote" | "question_answer" | "question_reopen" | "question_dismiss" | "question_delete" | "stroke_add" | "stroke_erase" | "whiteboard_clear" | "breakout_open" | "breakout_update" | "breakout_broadcast" | "breakout_move" | "breakout_join" | "breakout_close" | "breakout_expire";
2777
+ interface CollabRequest {
2778
+ readonly action: CollabAction;
2779
+ readonly itemId?: string | undefined;
2780
+ readonly question?: string | undefined;
2781
+ readonly options?: readonly string[] | undefined;
2782
+ readonly multiple?: boolean | undefined;
2783
+ readonly anonymous?: boolean | undefined;
2784
+ readonly optionIds?: readonly string[] | undefined;
2785
+ readonly text?: string | undefined;
2786
+ readonly stroke?: {
2787
+ readonly points: readonly (readonly [number, number])[];
2788
+ readonly color: string;
2789
+ readonly width: number;
2790
+ readonly tool: "pen" | "highlighter";
2791
+ } | undefined;
2792
+ readonly rooms?: readonly {
2793
+ readonly name: string;
2794
+ readonly identities: readonly string[];
2795
+ }[] | undefined;
2796
+ readonly allowChoose?: boolean | undefined;
2797
+ readonly minutes?: number | undefined;
2798
+ readonly identity?: string | undefined;
2799
+ /** `null` = the main room (breakout_join / breakout_move). */
2800
+ readonly roomId?: string | null | undefined;
2801
+ }
2802
+ interface CollabResult {
2803
+ readonly action: CollabAction;
2804
+ readonly message: string;
2805
+ readonly removed: readonly string[];
2806
+ /** The id of the item the server returned (a created poll, a new question), or null. */
2807
+ readonly itemId: string | null;
2808
+ }
2809
+ interface InRoomClientOptions {
2810
+ api: MeetApi;
2811
+ store: MeetStore;
2812
+ /** The room pass for THIS meeting, or null (a signed-in member needs none). */
2813
+ resolveRoomToken: () => string | null;
2814
+ now?: (() => number) | undefined;
2815
+ }
2816
+ declare function createInRoomClient(options: InRoomClientOptions): InRoomClient;
2817
+
2818
+ /**
2819
+ * ONE MEETING RECORD PER READER PER TAB — read once, kept current by one
2820
+ * ref-counted channel (0.7.79).
2821
+ *
2822
+ * 🚨 THE DEFECT THIS CLOSES. `useMeetingRecord` held the bundle in its own
2823
+ * component state and read all four parts (`meet_participants`, `meet_notes`,
2824
+ * `meet_transcript_segments`, `meet_recordings`) in a mount effect. A host that
2825
+ * hides a view without unmounting its data — a board tile asleep under React
2826
+ * `<Activity>`, which tears every effect down and runs it again on wake — read
2827
+ * the whole record again on every wake, on a Remove + Undo, and once more for
2828
+ * every second view of the same meeting (the record tab, the meeting's agent
2829
+ * surface, five "meeting notes" tiles). The view owned the data.
2830
+ *
2831
+ * THE SHAPE. One entry per (reader, meeting). The first view reads; every later
2832
+ * view — the same view after a wake or a remount included — renders the entry
2833
+ * and reads nothing. A read already in flight is joined. A forced read (Retry,
2834
+ * a reconnect) supersedes it, and an answer that lands after a newer read was
2835
+ * asked is DROPPED (the late-answer guard), so an old answer can never paint
2836
+ * over a newer one.
2837
+ *
2838
+ * LIVE WHILE HELD. A member's entry (a direct read, kept as its four parts) is
2839
+ * patched row by row from the database's change feed for the four tables, then
2840
+ * assembled again — one row, never four reads. The channel opens on the first
2841
+ * holder and closes `MEETING_RECORD_GRACE_MS` after the last one lets go, so a
2842
+ * wake or an Undo inside the grace costs nothing. Past the grace nothing
2843
+ * watched the meeting, so the entry is marked stale and the next view reads it
2844
+ * once. A guest's record arrives through the server as a finished bundle; it
2845
+ * has no change feed and is kept as it is.
2846
+ *
2847
+ * Framework-free on purpose: `react/hooks.ts` is one reader of it.
2848
+ */
2849
+
2850
+ /** How long a meeting's channel and kept record outlive the last view of it. */
2851
+ declare const MEETING_RECORD_GRACE_MS = 60000;
2852
+ interface MeetingRecordFailure {
2853
+ readonly message: string;
2854
+ readonly remedy: string;
2855
+ }
2856
+ interface MeetingRecordState {
2857
+ readonly loading: boolean;
2858
+ readonly record: MeetingRecordBundle | null;
2859
+ /** A failure that stopped the WHOLE record loading (never kept as an answer). */
2860
+ readonly failure: MeetingRecordFailure | null;
2861
+ }
2862
+ /** What a read produced: the four parts (patchable), or a finished bundle (a guest's). */
2863
+ type MeetingRecordAnswer = {
2864
+ readonly kind: "parts";
2865
+ readonly meeting: MeetingRecord;
2866
+ readonly parts: MeetingRecordParts;
2867
+ } | {
2868
+ readonly kind: "bundle";
2869
+ readonly bundle: MeetingRecordBundle;
2870
+ };
2871
+ /** One changed row, already projected by the repository. `null` + `removedId` = gone. */
2872
+ type MeetingRecordChange = {
2873
+ readonly part: "transcript";
2874
+ readonly row: MeetingRecordParts["transcript"][number] | null;
2875
+ readonly removedId?: string | undefined;
2876
+ } | {
2877
+ readonly part: "notes";
2878
+ readonly row: MeetingRecordParts["notes"][number] | null;
2879
+ readonly removedId?: string | undefined;
2880
+ } | {
2881
+ readonly part: "attendance";
2882
+ readonly row: MeetingRecordParts["attendees"][number] | null;
2883
+ readonly removedId?: string | undefined;
2884
+ } | {
2885
+ readonly part: "recording";
2886
+ readonly row: MeetingRecordParts["recordings"][number] | null;
2887
+ readonly removedId?: string | undefined;
2888
+ };
2889
+ interface MeetingRecordCache {
2890
+ /** The kept state for `key` (stable until it changes). */
2891
+ state(key: string | null): MeetingRecordState;
2892
+ subscribe(listener: () => void): () => void;
2893
+ /**
2894
+ * Read unless the entry is kept and current (or a read is in flight — joined).
2895
+ * `force` reads anyway and supersedes a read in flight. Never rejects: a
2896
+ * failure is in the state.
2897
+ */
2898
+ ensure(key: string, read: () => Promise<MeetingRecordAnswer>, options?: {
2899
+ force?: boolean;
2900
+ }): Promise<void>;
2901
+ /** The meeting row moved (a rename, it ended): assemble again — no read. */
2902
+ setMeeting(key: string, meeting: MeetingRecord): void;
2903
+ /** Apply one changed row from the change feed — no read. */
2904
+ apply(key: string, change: MeetingRecordChange): void;
2905
+ /**
2906
+ * Hold the live half: `open` on the first holder, its close a grace after the
2907
+ * last. `owner` is what the channel runs on (the realtime manager): a new
2908
+ * owner means the old one's channels are gone, so it opens again.
2909
+ */
2910
+ hold(key: string, open: () => () => void, owner?: unknown): () => void;
2911
+ /** Tests only. */
2912
+ reset(): void;
2913
+ }
2914
+ declare function createMeetingRecordCache(options?: {
2915
+ graceMs?: number;
2916
+ }): MeetingRecordCache;
2917
+
2918
+ /**
2919
+ * The reaction ids every skin offers (CORE-DESIGN §5.3). The id IS the emoji
2920
+ * the wire carries; a skin may draw its own art for each, never a different set.
2921
+ */
2922
+ declare const MEET_REACTIONS: readonly string[];
2923
+
2924
+ /**
2925
+ * COLLAB AND CAPTION LOGIC, HEADLESS (CORE-DESIGN §5.3, step S1).
2926
+ *
2927
+ * What used to live inside the React hooks of the in-room activities: which
2928
+ * activities a person has, when the Activities door deserves a dot, the act /
2929
+ * move-to-breakout calls with their sentences, the caption-translation work
2930
+ * queue, and the one-shot panel and language requests on the store. Pure or
2931
+ * store-shaped, so every skin reads the same answers; the `/react` hooks are
2932
+ * thin selectors over these.
2933
+ */
2934
+
2935
+ /** The activities this person has, in the order Meet lists them. */
2936
+ declare function availableActivities(collab: CollabSnapshot | null, isHost: boolean): CollabPanel[];
2937
+ /** Something worth a dot on the Activities button: a live poll unanswered, a new question for a host. */
2938
+ declare function activityAttention(collab: CollabSnapshot | null, isHost: boolean): boolean;
2939
+ interface CollabRuntime {
2940
+ readonly inRoom: {
2941
+ collab(request: CollabRequest): Promise<CollabResult>;
2942
+ };
2943
+ readonly engine: {
2944
+ moveToBreakout(roomId: string | null): Promise<unknown>;
2945
+ };
2946
+ }
2947
+ /** Perform one act; resolves with the server's sentence, or its refusal (never throws). */
2948
+ declare function performCollab(runtime: CollabRuntime | null, request: CollabRequest): Promise<{
2949
+ ok: boolean;
2950
+ message: string;
2951
+ itemId?: string | null;
2952
+ }>;
2953
+ /** Go to a breakout room (`null` = the main room), with the sentence to show. */
2954
+ declare function moveToRoom(runtime: CollabRuntime | null, roomId: string | null): Promise<{
2955
+ ok: boolean;
2956
+ message: string;
2957
+ }>;
2958
+ interface CollabStoreLike {
2959
+ snapshot(): MeetSnapshot;
2960
+ patch(next: Partial<MeetSnapshot>): void;
2961
+ }
2962
+ /** Finished caption lines that still need a translation into `language`. */
2963
+ declare function untranslatedLines(lines: readonly {
2964
+ id: string;
2965
+ text: string;
2966
+ final: boolean;
2967
+ }[], translations: Readonly<Record<string, string>>): {
2968
+ id: string;
2969
+ text: string;
2970
+ }[];
2971
+ /** The next batch to send: never a line already asked for in this language, at most 20. */
2972
+ declare function nextTranslationBatch(pending: readonly {
2973
+ id: string;
2974
+ text: string;
2975
+ }[], language: string, asked: Set<string>): {
2976
+ id: string;
2977
+ text: string;
2978
+ }[];
2979
+ /** The viewer chose a caption language (or `null` = as spoken): start over in it. */
2980
+ declare function setCaptionLanguage(store: CollabStoreLike, next: string | null): void;
2981
+ /** Take (and clear) the one-shot "open this panel" request, if any. */
2982
+ declare function takePanelRequest(store: CollabStoreLike): CollabPanel | null;
2983
+ /** Take (and clear) the one-shot "move to this breakout room" request, if any. */
2984
+ declare function takeMoveRequest(store: CollabStoreLike): {
2985
+ roomId: string | null;
2986
+ } | null;
2987
+
2988
+ /**
2989
+ * THE OBSERVATION CONTRACT, DERIVED FROM TODAY'S STORE (CORE-DESIGN §7 step S0).
2990
+ *
2991
+ * The real-browser scenario harness reads a meeting through a small set of
2992
+ * `data-meet-*` attributes (common-docs `meet/duplicates/HARNESS-CONTRACT.md`,
2993
+ * owned by the harness lane). This module turns the store's `MeetSnapshot`
2994
+ * into exactly those values — pure, framework-free, rendered ONCE by the
2995
+ * package (`useCoreRootProps()` / `<MeetRoot>`), never per skin.
2996
+ *
2997
+ * S0 rule: TODAY'S VALUES ONLY. Every value here is what the person actually
2998
+ * sees on today's screens, including today's defects — a `failed` or `left`
2999
+ * room renders the pre-join screen, so its phase is `prejoin`, and the harness
3000
+ * records that as the defect it is. A value the store does not know (host
3001
+ * presence before joining, autoplay blocking) is OMITTED rather than guessed:
3002
+ * an absent attribute reads as "unknown", a guessed one would lie.
3003
+ */
3004
+
3005
+ /** `data-meet-phase` values this release renders (a subset of the contract's §2). */
3006
+ type ObservedPhase = "resolving" | `refused:${string}` | "prejoin" | "joining" | "waiting:knocking" | "in_call" | "disconnected" | "ended";
3007
+ type ObservedConnection = "stable" | "reconnecting" | "server_unreachable";
3008
+ interface MeetObservation {
3009
+ readonly phase: ObservedPhase;
3010
+ readonly reason: string | null;
3011
+ readonly role: ParticipantRole | "none";
3012
+ readonly connection: ObservedConnection;
3013
+ /** null = this tab does not know (it is not in the room). */
3014
+ readonly hostPresent: boolean | null;
3015
+ readonly lobbyCount: number;
3016
+ readonly recording: boolean;
3017
+ /** null = today's store does not track autoplay blocking. */
3018
+ readonly audioBlocked: boolean | null;
3019
+ /** null outside `joining` / `in_call` — no device is in play there today. */
3020
+ readonly camera: string | null;
3021
+ readonly microphone: string | null;
3022
+ }
3023
+ interface ObserveInput {
3024
+ readonly snapshot: MeetSnapshot | null;
3025
+ /** The row the consumer resolved, when the store has none yet (the guest lane). */
3026
+ readonly meeting?: MeetingRecord | null | undefined;
3027
+ }
3028
+ /** The store's device state → the contract's `camera` / `microphone` value. */
3029
+ declare function observedDevice(track: LocalTrackState): string;
3030
+ /**
3031
+ * The phase the person is looking at, by the same branches `<MeetingRoom>`
3032
+ * renders on: record → pre-join (idle/failed/left) → waiting room (lobby) →
3033
+ * the room (everything else).
3034
+ */
3035
+ declare function observedPhase(input: ObserveInput): ObservedPhase;
3036
+ declare function observe(input: ObserveInput): MeetObservation;
3037
+ /** A phase the consumer renders itself (link resolution, the guest-name step, a dead link). */
3038
+ declare function staticObservation(phase: ObservedPhase, reason?: string | null): MeetObservation;
3039
+ /** The root element's attributes. Unknown values are omitted, never guessed. */
3040
+ declare function rootAttributes(o: MeetObservation): Record<string, string>;
3041
+ /** The contract's per-person connection vocabulary. `unknown` is not a reading. */
3042
+ declare function observedQuality(quality: ConnectionQuality): string | null;
3043
+ /** `[data-meet-participant]` attributes for one person's tile or roster row. */
3044
+ declare function participantAttributes(p: MeetParticipant): Record<string, string>;
3045
+ /** The listen-only notice's contract code, from why the microphone is off. */
3046
+ declare function microphoneNoticeCode(track: LocalTrackState): "mic-blocked" | "device-lost" | null;
3047
+
3048
+ export { ATTENDANCE_CSV_HEADER, AdmissionState, type AnnounceResult, type ApplyRoomEventArgs, type AskArgs, type AttendanceKind, type AttendanceRow, type AudioProcessing, CALL_INVITE_KIND, CALL_INVITE_VERSION, CONSENT_NOTICE_ATTRIBUTE, type CallCenter, type CallCenterOptions, CallId, CallInvite, type CallInviteHandlerOptions, type CallInvitePayload, CallMode, CallState, type CollabAction, CollabPanel, type CollabRequest, type CollabResult, type CollabRuntime, CollabSnapshot, type CollabStoreLike, ConnectionQuality, type CreateCallInviteInput, type CreateMeetingInput, DATA_TOPICS, DEFAULT_EFFECTS, DEFAULT_PAGE_SIZE, type DataTopic, type DeviceKind, type DeviceManager, type DeviceManagerOptions, type DeviceSnapshot, type DeviceStorage, type DriverConnectionState, type DriverParticipant, type DriverTrackKind, type DriverTranscription, END_MEETING_NOT_SERVED, type EffectsStorage, type EndMeetingResult, type FollowUpDraft, type FollowUpRecipient, type FollowUpSendResult, GUEST_RECORD_ACCESS_CHOICES, GUEST_RECORD_ACCESS_LEVELS, GUEST_RECORD_ACCESS_METADATA_KEY, GUEST_RECORD_NOT_SHARED, type GuestRecordAccess, type GuestRecordAccessResolution, type GuestRecordAccessSetting, IDLE_MEDIA, IDLE_TRACK, type InRoomClient, type InRoomClientOptions, InviteeInput, InviteeRole, type JoinRequest, LISTEN_ONLY_NOTICE, type LibraryRecording, type LiveKitDriverOptions, LobbyEntry, type LocalMediaState, type LocalTrackState, MEETING_INVITE_KIND, MEETING_INVITE_VERSION, MEETING_RECORD_GRACE_MS, MEET_ANSWER_CONTEXT_EVENT, MEET_ANSWER_EVENTS, MEET_EVENTS, MEET_REACTIONS, MEET_ROUTES, MEET_SCHEMA, MODERATION_ACTIONS, type MediaDevicesLike, type MediaEffects, type MediaUnavailable, type MeetAi, type MeetAiAnswer, type MeetAiOptions, type MeetAnswerCoverage, MeetApi, type MeetDevice, MeetError, type MeetErrorCode, type MeetEvent, type MeetGrants, MeetIdentity, type MeetObservation, MeetParticipant, type MeetRepository, type MeetRoomDriver, type MeetSnapshot, type MeetStore, MeetingAttendee, MeetingChanges, type MeetingChatHistory, MeetingId, type MeetingInviteHandlerOptions, type MeetingInvitePayload, MeetingInvitee, MeetingKind, MeetingNote, MeetingOccurrence, MeetingRecord, type MeetingRecordAnswer, type MeetingRecordBundle, type MeetingRecordCache, type MeetingRecordChange, type MeetingRecordFailure, type MeetingRecordParts, type MeetingRecordRestriction, type MeetingRecordRestrictionNotice, type MeetingRecordState, type MeetingSearchHit, type MeetingSearchSource, type MeetingSession, type MeetingSessionOptions, MeetingSettings, type ModerationAction, type ModerationNotice, type ModerationRequest, type ModerationResult, NOTE_TAKER_IDENTITY_PREFIX, NOTE_TAKER_METADATA_KEY, NO_GRANTS, type NameBook, type NoteTakerDispatch, type NoteTakerRefusal, NoteTakerState, type NoteTakerStatus, type ObserveInput, type ObservedConnection, type ObservedPhase, OccurrenceOverride, OrganizationId, PARTICIPANT_CAN_RECORD_ATTRIBUTE, PARTICIPANT_ROLE_ATTRIBUTE, ParticipantIdentity, ParticipantRole, REACTION_TTL_MS, REFUSED_ABSENCE, ROOM_EVENTS, ROOM_EVENT_PREFIX, ROOM_TOKEN_HEADER, RPCS, type RecordingController, type RecordingControllerOptions, RecordingPolicy, RecordingRecord, RecordingState, type RepositoryOptions, type RoomChatMessage, type RoomDriverFactory, type RoomDriverHandlers, type RoomEngine, type RoomEngineOptions, type RoomEventOutcome, type RoomEventTopic, RoomName, RoomPhase, type RoomReaction, type RoomToken, type RoomTokenStorage, RsvpAnswer, RsvpInvitation, type ScheduleMeetingInput, type Stage, type StageArgs, StageLayout, TABLES, TRANSCRIPTION_ATTRIBUTES, TRANSCRIPTION_TOPIC, type TokenClient, type TokenClientOptions, type TokenRequest, type TranscriptBlock, type TranscriptFormat, type TranscriptMatch, TranscriptSegment, UpcomingOccurrence, UserId, type VideoEffect, WrapUpAnnouncement, type WrapUpStatus, absenceSentence, activeWrapUpSummary, activityAttention, announceMeeting, applyRoomEvent, assembleMeetingRecord, attendanceCsv, attendanceReport, attendanceTotals, attendeeKey, availableActivities, callInviteAction, callInviteText, callStateLabel, canModerate, canModerateParticipant, canTransition, captionSegment, changesPatch, clockPosition, computeStage, countLabel, createCallCenter, createCallInviteHandler, createDeviceManager, createInRoomClient, createLiveKitDriver, createMeetAi, createMeetRepository, createMeetStore, createMeetingInviteHandler, createMeetingRecordCache, createMeetingSession, createMemoryDeviceStorage, createRecordingController, createRoomEngine, createTokenClient, createWebDeviceStorage, createWebEffectsStorage, createWebRoomTokenStorage, displayNameFor, gridColumns, groupTranscript, guestRecordAccessExplanation, guestRecordAccessLabel, guestRecordAccessSetting, hasEnded, hostUserIds, invalidResponse, isGuestIdentity, isListenOnly, isNoteTaker, isRealName, isRoomEventTopic, learnName, learnNames, listenOnlyNotice, loadFollowUpDraft, loadGuestMeetingRecord, loadMeetingChat, loadMeetingRecord, loadMeetingRecordParts, mapConnectionState, mapQuality, meetingInviteAction, meetingLength, meetingLink, meetingPath, meetingSlug, meetingTopic, meetingWhen, mergeCaption, mergeRecord, microphoneNoticeCode, moveToRoom, nextTranslationBatch, normalizeDeviceError, normalizeMeetError, normalizeSpoken, noteTakerIdentity, noteTakerLabel, noteTakerParticipant, noteTakerStatus, notesDocument, observe as observeMeeting, observedDevice, observedPhase, observedQuality, orderParticipants, parseRoomToken, participantAttributes, participantSummary, performCollab, playableRecordingFileId, projectInvitee, projectLibraryRecording, projectNote, projectOccurrence, projectOverride, projectParticipant, projectRsvpInvitation, projectSearchHit, projectSegment, projectUpcoming, pushMeetingToGoogleCalendar, readServerRefusal, readableTranscript, recordingLabel, recordingNotice, recordingOffsetMs, roleOf, roomTopic, rootAttributes, searchTranscript, sendFollowUp, setCaptionLanguage, setGuestRecordAccess, settingsPatch, snapshotParticipant, snippetRuns, staticObservation, summarizeMeetingRecordRestrictions, takeMoveRequest, takePanelRequest, transcriptDocument, unavailableFrom, uniqueAttendees, untranslatedLines, userTopic, wasRefused, wrapUpStatus };