import { c as ParticipantIdentity, P as ParticipantRole, k as AdmissionState, a1 as NoteTakerState, d as MeetParticipant, b as MeetingRecord, R as RoomName, C as CallInvite, a6 as ParticipantStanding, a3 as ObserversVisibleTo, S as StageLayout, L as LobbyEntry, aa as RecordingState, U as UserId, aF as RecordingSource, ae as TranscriptSegment, g as MeetingNote, W as WrapUpAnnouncement, j as CollabSnapshot, p as ConnectionQuality, h as MeetingId, M as MeetApi, f as CallMode, O as OrganizationId, m as CallId, n as CallState, Z as MeetingKind, a8 as RecordingPolicy, V as MeetingAttendee, a9 as RecordingRecord, a0 as MeetingSettings, X as MeetingChanges, a4 as OccurrenceOverride, $ as MeetingOccurrence, af as UpcomingOccurrence, Y as MeetingInvitee, I as InviteeInput, q as InviteeRole, ab as RsvpAnswer, ac as RsvpInvitation, a as MeetIdentity } from './collab-B85s8sQ8.js'; import { RealtimeManager } from '@ai-matrx/data/realtime'; import { MediaDevicesLike as MediaDevicesLike$1 } from '@ai-matrx/browser-audio/core'; import { MessagingSupabaseClient } from '@ai-matrx/messaging'; /** * ONE error classification for the whole package (C22). * * The banned alternative is a host catching a LiveKit or PostgREST error and * deciding for itself whether the camera is busy, the token expired, or the * meeting is locked — which is how the same five-branch `if` gets copy-pasted * into every consumer and drifts. Everything this package throws is a * `MeetError` with a stable `code` and a `remedy` SENTENCE. Nothing fails * silently, and nothing fails without saying what to do about it. * * The device codes exist because "it doesn't work" in a video product is almost * always one of four specific, differently-remediable things: permission * denied, no such device, device already in use by another app, or insecure * context (http, where `getUserMedia` does not exist at all). A single * "media-error" would make the UI say the wrong sentence three times out of * four. */ 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" /** Meet wave 1: the meeting was cancelled — its link resolves, its room never opens. */ | "cancelled" | "unknown"; declare class MeetError extends Error { readonly code: MeetErrorCode; /** What to DO. Rendered to the user for the device codes. */ readonly remedy: string; readonly cause?: unknown; /** * The join gate's reason code (CORE-DESIGN §2.5) when the server named one: `not_found`, * `cancelled`, `ended`, `locked`, `removed`, `lobby_rejects`, `not_invited`… The phase * renders `refused:` (or `ended`) from it — never from the HTTP status. */ readonly reason: string | null; /** * A refusal that lifts by itself says when: whole seconds from the moment the server answered * (a denial's cooldown, `reknock_cooldown_seconds`). Null for every refusal that does not. */ readonly retryAfterSeconds: number | null; constructor(code: MeetErrorCode, message: string, remedy: string, cause?: unknown, reason?: string | null, retryAfterSeconds?: number | null); /** True when retrying after the host re-establishes a session is the fix. */ get isRetryable(): boolean; /** True when the user, not the code, must act — the UI shows `remedy`. */ get isUserActionable(): boolean; } /** * THE ONE PLACE a raw database/transport error becomes a typed one. Called at * every repository boundary; nothing else in the package inspects a raw error, * and no consumer should ever have to. */ declare function normalizeMeetError(error: unknown, operation: string): MeetError; /** * THE DEVICE ERROR TRANSLATOR — the second-most-copied block in every video app. * * `getUserMedia` rejects with DOMException names, and every one of them means * something a user could fix if we told them. This is the whole reason the * device layer lives in the package (C22 test 3: less-than-perfect unless done * perfectly). */ declare function normalizeDeviceError(error: unknown, operation: string): MeetError; /** A response whose SHAPE is wrong — the ingress boundary, not the wire. */ declare function invalidMeetResponse(operation: string, detail: string): MeetError; /** * WHO SAID THAT — one answer, every surface, both lanes (MRI-A16). * * 🚨 **A RAW LIVEKIT IDENTITY IS NEVER A PERSON'S NAME.** On production * 2026-09-09 the host's caption rail read *"Ada Lovelace: I planted a marigold * in the courtyard"* and the guest's — the same meeting, the same second — * read *"guest:c8-spk-1"*, because the host resolved the name from the durable * rows it can read and the guest had no fallback at all and printed the token * (MRI-C8). Two surfaces disagreeing about who is speaking is worse than either * one being ugly: a person reading the transcript cannot tell whether those are * one speaker or two. * * The cure is that nothing resolves a name for itself. Every source that * happens to KNOW a name teaches this book — the LiveKit roster, a durable * `meet_transcript_segments` row, a durable `meet_participants` row, a chat * message, our own identity — and every surface asks it. Three rules: * * 1. **A name is learned, never unlearned.** Someone who leaves the room keeps * the name they had; a caption that lands after them still says who spoke. * This is the "last known name" rule, and it is why the book is a store * field rather than a derivation over the current roster. * 2. **An identity is not a name.** `learn` refuses a "name" that is the * identity itself — which is exactly what the LiveKit roster hands back for * a participant whose token carried no name, and exactly how the token got * onto the screen in the first place. * 3. **When nobody knows, SAY nobody knows.** `displayNameFor` answers with an * honest placeholder — "Unnamed guest" / "Unnamed participant" — never a * token, and never an invented name. The placeholder is deliberately not * unique, so surfaces that must keep two unnamed people apart group by * IDENTITY rather than by the rendered name. */ /** Identity → the best display name anything has ever known for it. */ type NameBook = Readonly>; /** A guest's identity is minted `guest:` by the token route. */ declare function isGuestIdentity(identity: ParticipantIdentity | string): boolean; /** * Would this string be a real name for this identity? A name equal to the * identity is the roster's way of saying it has none. */ declare function isRealName(identity: ParticipantIdentity | string, name: string | null | undefined): boolean; /** * Teach the book one identity's name. Returns the SAME book when nothing was * learned, so a store commit can be skipped rather than waking every subscriber * on every caption. */ declare function learnName(book: NameBook, identity: ParticipantIdentity | string, name: string | null | undefined): NameBook; /** Teach the book several at once — a roster replace, or a durable read. */ declare function learnNames(book: NameBook, entries: readonly { identity: ParticipantIdentity | string; displayName?: string | null | undefined; }[]): NameBook; /** * The name a surface renders for `identity`. Never a raw token, never invented. * * `preferred` is what the arriving thing itself claimed (a caption's roster * lookup, a durable row's `speaker` column); it wins when it is a real name, * because it is the freshest, and teaches the book on its way past. */ declare function displayNameFor(book: NameBook, identity: ParticipantIdentity | string, preferred?: string | null | undefined): string; /** * S8 — LEAVE INTENTS (CORE-DESIGN §2.7 `/v1/meet/leave`, §4.2 `host_leave_prompt` · `end_confirm`, * §11 `leave-or-end-choice`, `host-leave-assign`). * * What the host's Leave OFFERS is the meeting's behavior profile, resolved by the server and * carried on the token response (`leave_policy`). What each choice DOES is the server's: the * `/leave` door decides from the pass and the run row whether this person holds the run's host * powers. The skin owns only the words. */ type HostLeavePrompt = "leave_or_end" | "must_assign_or_end" | "leave_only"; interface LeavePolicy { readonly hostLeavePrompt: HostLeavePrompt; /** Ask "End for everyone?" once more before ending (Teams). */ readonly endConfirm: boolean; } /** The Meet profile's values — used only when the server sent none (an ad-hoc call). */ declare const DEFAULT_LEAVE_POLICY: LeavePolicy; declare function leavePolicyFrom(raw: unknown): LeavePolicy; /** One thing the Leave control can do. */ type LeaveChoice = "leave" | "assign_and_leave" | "end_for_all"; /** * The choices Leave offers, in order. Not the host → just leave (no menu). The host: * `leave_or_end` (Meet) → leave · end for everyone; `must_assign_or_end` (Zoom) → assign a new * host (when someone else is here) · end for everyone, with a plain leave only when nobody is * left to hand it to; `leave_only` (Teams) → leave · end meeting. */ declare function leaveChoices(args: { readonly policy: LeavePolicy; readonly role: ParticipantRole | null; readonly othersPresent: boolean; }): readonly LeaveChoice[]; /** * THE RULES A SCREEN REDRAWS FROM WHILE THE CALL IS RUNNING (P1: chat-restrictions, * mod-attendee-av-disabled, time-limit-warning, rec-consent-gate). * * The host can change a meeting's rules mid-call through the meeting's own rules door, which * never talks to the room. The server's state read answers the live values (`live_policy`), and * the room reads it on a short timer while the person is in the call — so a rule changed at * second 40 is on every screen within one poll. Pure functions here; the timer lives in * `meetings.ts`. */ type ChatMode = "everyone" | "hosts_only" | "off"; type AvLock = "off" | "mic" | "camera" | "both"; type RecordingConsent = "notice" | "ok_or_leave" | "view_only_until_agree"; /** Who may share a screen (`share_who_can`) and how many may at once (`share_concurrency`). */ type ShareWhoCan = "everyone" | "host_only" | "org" | "presenters"; type ShareConcurrency = "one_takeover" | "one_replace" | "multiple"; /** What remains when no host or co-host is in the room (`headless_powers`, CORE-DESIGN §4.2). */ type HeadlessPowers = "members_admit" | "none" | "cohosts_only"; interface LivePolicy { readonly headlessPowers: HeadlessPowers; readonly chatMode: ChatMode; readonly avLock: AvLock; /** 0 = no limit. */ readonly timeLimitMinutes: number; readonly timeLimitWarningMinutes: number; readonly recordingConsent: RecordingConsent; readonly shareWhoCan: ShareWhoCan; readonly shareConcurrency: ShareConcurrency; } /** * LISTEN-ONLY ATTENDANCE — being IN the room is never contingent on a device. * * 0.3.1 and every version before it enabled the microphone as the last step of * `connect()`, inside the same `try` as the join. So a denied, absent, or busy * microphone did not produce a muted participant — it threw, the join was * failed with a device remedy, and the user was dropped back to `` * having never entered the meeting. The whole in-room surface (tiles, captions, * live notes, Q&A, the note-taker consent notice) was unreachable to anyone * without a working mic, which is ordinary: a locked-down laptop, a conference * room dial-in, a phone on a call, a browser profile that denies capture by * policy, or a person who simply wants to listen. Every competitor allows it. * * THE CLASS, and how it is closed here: * * 1. **Connecting and publishing are two different operations.** The engine * reaches `connected` the moment the transport is up. Media is attempted * AFTER that, per track, and a rejection never unwinds the join. * 2. **A track that could not be acquired is STATE, not an exception.** The * reason and its remedy — the browser's own, translated by * `normalizeDeviceError` — ride in the snapshot so the UI says WHY the mic * is off instead of showing a mute button that silently does nothing. * 3. **Enabling later runs the SAME code path.** `setMicrophoneEnabled(true)` * from the control bar records exactly what the join attempt records, so a * person who plugs in a headset mid-meeting is one click from speaking and * a person whose second attempt also fails gets the same honest sentence. * A separate "retry" path is how the two drift. * 4. **The notice is one sentence, defined once.** Both the package's own room * chrome and any host that renders its own read `listenOnlyNotice()`, so * there is no second wording to keep in sync. */ /** * WHY a device is unusable, in the contract's words (CORE-DESIGN §3.2, the `mic` / `camera` * region's `unavailable:`). The media supervisor (`media-supervisor.ts`) decides it from * the browser's error AND the Permissions API, because the error alone cannot tell a person's * Block from an operating-system refusal or a stale device id. */ type DeviceUnavailableReason = "denied" | "blocked_site" | "blocked_os" | "none" | "busy" | "failed" | "revoked"; /** Why a local track is not publishing, in the browser's terms plus a remedy. */ interface MediaUnavailable { readonly code: string; readonly message: string; readonly remedy: string; /** The region reason (S6c). Absent on states recorded before the supervisor classified them. */ readonly reason?: DeviceUnavailableReason | undefined; } /** What the browser's permission store says for one device kind. `unknown` = no Permissions API. */ type PermissionReading = "granted" | "denied" | "prompt" | "unknown"; /** A device change the person is told about (S6c): unplugged → switched, or nothing left. */ interface DeviceNotice { readonly id: number; readonly kind: "microphone" | "camera"; /** `switched` = the meeting moved to another device; `lost` = none of this kind is left. */ readonly outcome: "switched" | "lost"; /** The device that went away, by label (empty when the browser hid labels). */ readonly from: string; /** The device that went away, by id, so Undo can go back to it if it is plugged in again. */ readonly fromId?: string | undefined; /** The device now in use (`switched` only). */ readonly to: string | null; } /** A device that just appeared — offered, never switched to silently (catalog `device-default-changed`). */ interface DeviceOffer { readonly kind: "microphone" | "camera"; readonly deviceId: string; readonly label: string; } /** * THE MEDIA REGIONS THE SUPERVISOR OWNS (CORE-DESIGN §3.2 `audio_output`, and the inputs the * `mic` / `camera` regions are derived from). None of it can change the call phase. */ interface MediaRegionState { readonly permission: { readonly microphone: PermissionReading; readonly camera: PermissionReading; }; /** `blocked` = the browser's autoplay policy is holding remote audio until a gesture. */ readonly audioOutput: "ok" | "blocked"; readonly notice: DeviceNotice | null; readonly offer: DeviceOffer | null; /** * THE PRE-JOIN CHECK per kind (P1 `perm-prompt-pending`, `perm-blocked-site`, `device-none-found`, * `device-busy`): what the preview found BEFORE the room exists. `prompting` = the browser was * asked and has not answered (Join stays usable); `unavailable` carries the classified refusal. */ readonly preview: { readonly microphone: DevicePreview; readonly camera: DevicePreview; }; } /** One kind's pre-join check. `idle` = nothing was tried (or the person turned it off before). */ interface DevicePreview { readonly state: "idle" | "prompting" | "on" | "off" | "unavailable"; readonly unavailable: MediaUnavailable | null; } declare const IDLE_MEDIA_REGION: MediaRegionState; interface LocalTrackState { /** True only when the track is actually published. Never optimistic. */ readonly enabled: boolean; /** * Set when acquisition was ATTEMPTED and refused. `null` covers both "on" * and "deliberately off" — a user who unchecked the mic in pre-join is not * looking at an error, they are looking at a choice they made. */ readonly unavailable: MediaUnavailable | null; } interface LocalMediaState { readonly microphone: LocalTrackState; readonly camera: LocalTrackState; } declare const IDLE_TRACK: LocalTrackState; declare const IDLE_MEDIA: LocalMediaState; declare function unavailableFrom(error: MeetError, reason?: DeviceUnavailableReason): MediaUnavailable; /** * THE SENTENCE. One definition, so the room chrome, a host's own chrome and the * tests cannot say three different things about the same fact. */ declare const LISTEN_ONLY_NOTICE = "You're attending without a microphone \u2014 others can't hear you"; /** * True when this participant is in the room with NO USABLE microphone — the * device was refused or is missing. A person who simply muted themselves has a * working microphone and is not "attending without" one, so a mute never * counts: `unavailable` is the only signal. */ declare function isListenOnly(media: LocalMediaState): boolean; /** * The notice to render while in the room, or `null` when the mic is live. * Returns the plain sentence; the reason and remedy, when there is one, are * rendered beside it from `media.microphone.unavailable`. */ declare function listenOnlyNotice(media: LocalMediaState): string | null; /** * THE JOIN GATE'S REASON CODES — ONE VOCABULARY (CORE-DESIGN §2.5). * * aidream's `services/meet/errors.py` (`JOIN_REFUSAL_REASONS`) emits exactly these codes and a * server test fails if the two lists differ; the consuming app decides every refusal screen by * one of them — never by the HTTP status and never by matching message text. Skins own the * words; the phase renders `refused:`, except `ended`, which renders the `ended` phase. */ declare const JOIN_REFUSAL_REASONS: readonly ["not_found", "cancelled", "ended", "expired", "too_early", "sign_in_required", "wrong_account", "not_invited", "locked", "removed", "lobby_rejects", "meeting_full", "platform_capacity", "verification_required", "registration_required"]; type JoinRefusalReason = (typeof JOIN_REFUSAL_REASONS)[number]; /** Is `value` one of the codes the join gate speaks? */ declare function isJoinRefusalReason(value: unknown): value is JoinRefusalReason; /** * THE PHASE MACHINE — the one place a meeting tab's top-level phase changes (CORE-DESIGN §3.1, * step S6a). * * The values ARE the observation contract's `data-meet-phase` column (HARNESS-CONTRACT §2), so * the core renders its own state with no translation layer. Every change goes through * `nextPhase(from, event)`: an explicit table of which event may move which phase where. An * event the table does not allow from the current phase is REFUSED (the store keeps the phase * and reports it) — never applied. * * 🚨 THE INVARIANT (guard: `phase.test.ts`, `in-call-phase-guard.test.ts`): nothing enters * `prejoin` except `resolving` (the link resolved) or the person's own click. In-call outcomes — * share, devices, recording, captions, chat, collab, moderation — live in REGIONS and have no * event here at all, so the MD-01 class ("a canceled share picker threw a live meeting back to * Ready to join?") cannot recur: from `in_call` the only ways out are the exit-reason * resolution (§3.4), the person's own Leave, and the meeting ending. * * 1:1 calls run on this same machine (§3.6): ringing is a pre-room phase set, and an answered * call goes `joining` → `in_call` exactly like a meeting. */ /** Every gate reason except `ended`, which renders the `ended` phase (contract §2). */ type RefusedReason = Exclude; /** Where the person is (HARNESS-CONTRACT §2, verbatim). */ type MeetPhase = "resolving" | "unsupported" | `refused:${RefusedReason}` | "guest_name" | "prejoin" | "joining" | "waiting:host_not_started" | "waiting:knocking" | "denied" | "knock_expired" | "in_call" | "disconnected" | "left" | "removed" | "superseded" | "inactive" | "ended" | "record" | "ringing_out" | "ringing_in" | "call_declined" | "call_missed" | "call_busy" | "answered_elsewhere"; /** The phases a §3.4 exit-reason resolution can land on. */ type ExitPhase = "removed" | "superseded" | "inactive" | "ended" | "disconnected" | "waiting:knocking"; /** How a 1:1 call ended before it connected (§3.6). */ type CallOutcomePhase = "call_declined" | "call_missed" | "call_busy" | "answered_elsewhere"; /** * Every event that can move the phase. `click: true` marks the ones only the person's own * press may send — the guard test reads it. */ type PhaseEvent = /** The link resolved on page load / mount: `resolving` → `prejoin` (first entry). */ { readonly type: "resolved"; } /** Capability gate failed: this browser cannot join. */ | { readonly type: "unsupported"; } /** * The link's door said, BEFORE any Join, that this run cannot be joined (`ended`, `cancelled`): * `resolving` → that screen, never the pre-join with a Join button that will be refused. */ | { readonly type: "link_state"; readonly reason: string | null; } /** The person pressed Join / Rejoin / Ask again. */ | { readonly type: "join"; readonly click: true; } /** * S6b: the connection supervisor rejoins by itself from `disconnected` once the network and our * servers answer (`auto_rejoin`). Not a click — and it can only ever lead to `joining`. */ | { readonly type: "auto_rejoin"; } /** * S6b: the page reloaded while this tab was in (or dropped from) the call — Chrome's offline * page reloading, a refresh — so `resolving` goes straight back to `joining`, never pre-join. */ | { readonly type: "resumed"; } /** The person chose to go back to the device check from `left` / `disconnected`. */ | { readonly type: "back_to_prejoin"; readonly click: true; } /** The person pressed Try again on a refusal: look the link up again. */ | { readonly type: "retry"; readonly click: true; } /** The server said wait (or decided a wait): the admission row's own word. */ | { readonly type: "admission"; readonly admission: AdmissionState; } /** Admitted from the waiting room: the core mints the real pass and joins. */ | { readonly type: "admitted"; } /** LiveKit said connected. */ | { readonly type: "connected"; } /** * S7: the server answered the join with a QUESTION — this person is already in the meeting on * another tab or device — so `joining` goes back to `prejoin`, which offers the choice. */ | { readonly type: "second_device"; } /** The join itself failed: a gate reason (→ `refused:*` / `ended`) or none (→ `disconnected`). */ | { readonly type: "join_failed"; readonly reason: string | null; } /** §3.4: the exit-reason resolution decided where an in-call person went. */ | { readonly type: "exit"; readonly to: ExitPhase; } /** My Leave (in the room, joining, or withdrawing from the waiting room). */ | { readonly type: "leave"; } /** The meeting (or this run of it) is over, by any carrier. */ | { readonly type: "ended"; } /** §3.6: I placed a call / a call rings me while nothing else is on screen. */ | { readonly type: "ring_out"; } | { readonly type: "ring_in"; } /** §3.6: an answered call (by me, or my callee) — join its room. */ | { readonly type: "call_answered"; } /** §3.6: the call ended before it connected. */ | { readonly type: "call_outcome"; readonly outcome: CallOutcomePhase; } /** * Back to the start: a different meeting mounted or the call surface was dismissed (refused * from a live phase — leave first). `teardown: true` is the engine's own dispose: it has * already closed the connection, so nothing live remains to protect and the reset applies from * any phase. */ | { readonly type: "reset"; readonly teardown?: true; }; type PhaseEventType = PhaseEvent["type"]; /** Phases in which this tab holds (or is getting) a live media connection. */ declare const LIVE_PHASES: ReadonlySet; /** The waiting-room phase set (the core reads the server's state while in one of these). */ declare const WAITING_PHASES: ReadonlySet; /** Nothing live, nothing pending: a call may take the screen from here. */ declare const IDLE_PHASES: ReadonlySet; declare function isLivePhase(phase: MeetPhase): boolean; declare function isWaitingPhase(phase: MeetPhase): boolean; declare function isRefusedPhase(phase: MeetPhase): phase is `refused:${RefusedReason}`; /** The reason a refused / terminal phase carries (`data-meet-reason`). */ declare function reasonOf(phase: MeetPhase): string | null; /** A gate reason → the phase it renders. `ended` is the ended phase, never `refused:ended`. */ declare function refusalPhase(reason: string | null): MeetPhase; /** The server's admission word → the waiting phase it renders (§3.1, S4). */ declare function admissionPhase(admission: AdmissionState): MeetPhase | null; /** * THE TABLE. Returns the next phase, or null when `event` may not move `from` (refused). * Pure — the store applies it, and the guard tests enumerate it. */ declare function nextPhase(from: MeetPhase, event: PhaseEvent): MeetPhase | null; /** What the server's state read (`GET /v1/meet/state?connection=`) says about this exit. */ interface ExitServerWord { /** `meet_connections.exit_reason` of THIS connection, if the server recorded one. */ readonly exitReason: string | null; /** The run's phase: `ended` means the meeting (this run) is over. */ readonly sessionPhase: string | null; readonly meetingPhase?: string | null; } interface ExitInput { /** The server's word; `null` = the state read failed (offline / unreachable); undefined = no ST for this room (a call). */ readonly server: ExitServerWord | null | undefined; /** LiveKit's `DisconnectReason` name (`PARTICIPANT_REMOVED`, `DUPLICATE_IDENTITY`, …), or null. */ readonly hint: string | null; /** The page was frozen (Page Lifecycle) when the connection dropped. */ readonly frozen?: boolean; } interface ExitResolution { readonly to: ExitPhase; /** `data-meet-reason` for the exit (null when the phase says it all). */ readonly reason: string | null; /** `data-meet-connection` to show beside `disconnected`. */ readonly connection: "stable" | "offline" | "server_unreachable"; /** A notice code the skin renders with it (`sent-to-waiting`), when one applies. */ readonly notice: string | null; } /** §3.4, row by row. Pure: the engine gathers the inputs, this decides. */ declare function resolveExit(input: ExitInput): ExitResolution; /** * THE NOTE-TAKER, AS A SURFACE MUST RENDER IT. * * 🚨 **THE PACKAGE NEVER GUESSES WHAT THE AGENT IS DOING.** Two facts decide * everything here and both come from outside this client: the meeting row's * `metadata.note_taker` (written by the server with a CAS, MRI-A2/D9) and * whether the agent participant is actually in the LiveKit room. A spinner — * "starting…" held open by a client-side timer — is the exact lie this file * exists to prevent: it survives a worker that never came, a dispatch nobody * claimed, and a job that died on a release. * * 🚨 **A REFUSAL IS RENDERED VERBATIM.** `/agent/start` answers a 503 whose body * carries `reason` and `remedy` written by the readiness gate (D7) — "no worker * is registered under matrx-note-taker", "every worker is at capacity", * "the fleet is draining". Those sentences are the truth about the system and * they reach the screen unedited. This package adds no sentence of its own on * top of one the server already wrote. * * The agent's identity is DETERMINISTIC — `agent:note_taker:` (D9) — * which is what makes "is it in the room" answerable without a round trip, and * what makes a second note-taker impossible to mistake for the first. */ /** The one spelling of the agent's participant identity (D9). */ declare const NOTE_TAKER_IDENTITY_PREFIX = "agent:note_taker:"; declare function noteTakerIdentity(meetingId: string): string; /** * The participant attribute the note-taker sets on itself so EVERY client — * including one that cannot read the token metadata — knows a notice is owed * (D10). */ declare const CONSENT_NOTICE_ATTRIBUTE = "matrx.agent.consent_notice"; /** Where the state lives on the meeting row. One spelling, one file. */ declare const NOTE_TAKER_METADATA_KEY = "note_taker"; /** The server's refusal, as it wrote it. Never paraphrased. */ interface NoteTakerRefusal { readonly reason: string; readonly remedy: string; readonly status: number; } interface NoteTakerStatus { readonly state: NoteTakerState; /** The server's own words for why it is in this state, when it gave any. */ readonly detail: string | null; /** True when the agent participant is in the room RIGHT NOW. */ readonly present: boolean; /** The last dispatch refusal, rendered verbatim. */ readonly refusal: NoteTakerRefusal | null; } /** Is this participant the note-taker (or any agent that owes a notice, D10)? */ declare function isNoteTaker(participant: MeetParticipant): boolean; /** The agent participant in the room, if any. */ declare function noteTakerParticipant(participants: readonly MeetParticipant[]): MeetParticipant | null; /** * THE STATE, from the two facts that decide it. * * The durable row is the authority for work. Exact room presence corroborates * only a durable `transcribing` state; an arbitrary agent, a consent marker, * or a stale note-taker participant never proves speech capture. */ declare function noteTakerStatus(args: { meeting: MeetingRecord | null; participants: readonly MeetParticipant[]; refusal?: NoteTakerRefusal | null; }): NoteTakerStatus; /** * THE SENTENCE FOR EACH STATE. One place, so the room, the panel and any future * surface say the same true thing — and so a state can never render as a bare * enum name. */ declare function noteTakerLabel(status: NoteTakerStatus): string; /** * THE ROOM SNAPSHOT — one immutable object the UI renders and nothing else. * * Deliberately NOT Redux, NOT a context full of `useState`, and NOT a bag of * refs: one store with `getSnapshot`/`subscribe` is exactly what * `useSyncExternalStore` wants, which is what makes tearing impossible when a * roster event and a speaker event land in the same tick. * * The rules encoded here: * * - **The roster is replaced wholesale, never patched.** The driver hands us * the full truth on every media event (see `room-driver.ts`); merging deltas * from six event sources is how a tile for someone who left stays on screen. * - **Ephemera EXPIRE.** Reactions and speaking flags are time-boxed in the * snapshot, so a dropped "stop" event cannot leave a permanent 🎉 or a * permanently-speaking ring. (Nothing here relies on receiving an end event.) * - **An interim transcript line is REPLACED in place by its final.** Appending * both is how a transcript ends up saying everything twice. */ interface RoomChatMessage { readonly id: string; readonly identity: ParticipantIdentity; readonly displayName: string; readonly text: string; readonly at: string; /** * Set for a PRIVATE message: the one identity it was addressed to (from the * sender's side) or this participant (from the receiver's). Private messages * travel the data plane only and are never saved (Meet wave 2). */ readonly to?: ParticipantIdentity | null | undefined; /** True once the server has SAVED it — a message everyone sees, late joiners too. */ readonly saved?: boolean | undefined; /** A send the server refused, with its sentence. The message stays visible, marked. */ readonly failed?: string | undefined; } /** * A HOST ACTED ON *ME* (Meet wave 2) — the server's own `matrx.meet.moderation` * packet, addressed to this participant. Rendered once, dismissible, in words. */ interface ModerationNotice { readonly kind: "muted" | "ask-unmute" | "removed" | "role" | "locked" | "capability"; readonly text: string; readonly at: number; } /** What this participant chose for their own media. Remembered per browser. */ interface MediaEffects { readonly noiseSuppression: boolean; readonly background: "none" | "blur" | "image"; readonly backgroundImage: string | null; } declare const DEFAULT_EFFECTS: MediaEffects; interface RoomReaction { readonly id: string; readonly identity: ParticipantIdentity; readonly emoji: string; readonly at: number; } /** * 🚨 WHAT THE SERVER GRANTED THIS JOINER, straight off the token response * (MRI-A11). It is placed in the snapshot the instant the token lands — before * the LiveKit roster exists at all — so a host's controls are correct on the * first frame of the room rather than one roster event later. The roster entry * for the local participant carries the SAME decision as a LiveKit attribute; * the two agree because one server statement produced both, and the UI reads * whichever has arrived. */ interface MeetGrants { readonly role: ParticipantRole; /** Where this joiner stands with the meeting (server-minted, beside the role). */ readonly standing?: ParticipantStanding | null; readonly canRecord: boolean; readonly canPublish: boolean; readonly canSubscribe: boolean; /** HR-360 wave 2 (MD-16): who sees the "Observing" line in People (knob). */ readonly observersVisibleTo?: ObserversVisibleTo | undefined; } /** No token yet grants nothing. Never "assume host until told otherwise". */ declare const NO_GRANTS: MeetGrants; /** * S4 — THIS PERSON'S WAIT, as the server's state read (`GET /v1/meet/state`) last said it * (CORE-DESIGN §2.6). Set while the phase is a waiting one (`waiting:*`, `denied`, * `knock_expired`); the waiting screens read it, never a broadcast. `admission` is the row's own word: `waiting_host` · `knocking` · `denied` · * `expired` · `withdrawn` · `ended` (`admitted` is never held here — it is a join). */ interface MeetWaiting { readonly admission: AdmissionState; /** Is anyone who may admit in the room? null until the first read. */ readonly admittersPresent: boolean | null; /** May a denied person ask again (`reknock_after_deny`)? */ readonly reknockAllowed: boolean; readonly knockExpiresAt: string | null; /** While a denial's cooldown runs: the epoch-ms moment the person may ask again; else null. */ readonly reknockAt?: number | null; /** How often the core reads while waiting (`waiting_poll_seconds`). */ readonly pollSeconds: number; /** The run's `live_version` at the last read. */ readonly liveVersion: number; } /** The core's `connection` region (CORE-DESIGN §3.2) — never a phase. */ interface LocalNetwork { readonly path: "direct" | "restricted" | "unknown"; readonly quality: "good" | "poor" | "unknown"; readonly videoPaused: boolean; } type ConnectionRegion = "stable" | "signal_reconnecting" | "reconnecting" | "offline" | "server_unreachable"; /** * The core's `share` region (CORE-DESIGN §3.2/§3.3 SA). A refused share is a situation the person * is in — a notice (`share-unavailable`), never a phase. */ interface ShareRegion { readonly state: "none" | "picking" | "presenting" | "refused" | "ended"; /** * Why a share was refused (this browser cannot share, the system blocks it, or it failed) or, for * `ended`, why the server took it down: someone else presented instead, or the host's rule no longer lets this person share. */ readonly reason: "unsupported" | "blocked" | "failed" | "replaced" | "not_allowed" | null; /** `ended` / `replaced`: who presents now. */ readonly by?: string; /** The fix, in the core's words (system setting, administrator policy, …), or null. */ readonly remedy: string | null; readonly at: number; /** While presenting: whether the share carries sound (`share-audio`). Absent = not known. */ readonly audio?: boolean; } declare const NO_SHARE: ShareRegion; interface MeetSnapshot { /** * WHERE THIS TAB IS — the contract's `data-meet-phase` value, verbatim (CORE-DESIGN §3.1). * Written ONLY by `store.dispatch(event)` through the phase machine (`core/phase.ts`); * `patch` cannot touch it. */ readonly phase: MeetPhase; /** `data-meet-reason` for a refused / terminal phase; null otherwise. */ readonly reason: string | null; /** A notice that came WITH the phase (`sent-to-waiting`), or null. */ readonly phaseNotice: string | null; /** The room this tab last joined (or is joining) — a different meeting mounting resets. */ readonly roomName: RoomName | null; /** The `connection` region: reconnecting is here, never a phase. */ readonly connection: ConnectionRegion; /** * Whether the reconnect banner shows: the region is honest at once, the banner only after the * drop outlasts `reconnect_banner_delay_ms` (the connection supervisor sets it, §3.3 CS). */ readonly connectionBanner: boolean; /** * P1 connection-network — THIS tab's own path, as the core judged it from its WebRTC stats * (`network-health.ts`): `restricted` = relayed or TCP-only (`net-udp-blocked`), `quality` = * the self-assessment (`net-quality-self`), `videoPaused` = the camera was paused by the * network to keep the audio (`net-video-auto-off`). The room engine is the one writer. */ readonly localNetwork: LocalNetwork; /** The `share` region. */ readonly share: ShareRegion; /** * The 1:1 call this tab is placing or answering (CORE-DESIGN §3.6) — the core `calls` slice. * The ringing phases (`ringing_out` / `ringing_in` / `call_*`) describe it; null = no call. */ readonly call: { readonly invite: CallInvite; readonly direction: "outgoing" | "incoming"; } | null; /** S4: this person's wait (see `MeetWaiting`); null outside the waiting room. */ readonly waiting: MeetWaiting | null; readonly meeting: MeetingRecord | null; readonly localIdentity: ParticipantIdentity | null; /** * S7: this person is already in this meeting on another tab or device, and the pre-join offers * the meeting's choice — `rule` is the `second_device` knob (`ask_switch_or_join` · Meet: Switch * here / Join here too; `ask_transfer_or_add` · Teams: Transfer / Add). Null otherwise. */ readonly secondDevice: { readonly rule: string; } | null; /** The server's word on what THIS joiner may do. See `MeetGrants`. */ readonly grants: MeetGrants; readonly participants: readonly MeetParticipant[]; readonly activeSpeakers: readonly ParticipantIdentity[]; readonly pinned: ParticipantIdentity | null; readonly layout: StageLayout; readonly lobby: readonly LobbyEntry[]; readonly locked: boolean; /** * 🚨 THE DURABLE `meet_recordings.state`, not a client's hope. Through 0.4.7 * this only ever held what THIS browser optimistically patched — `starting` * on the click, `stopping` on the stop — so the indicator read "Starting * recording…" for the whole meeting while a perfectly healthy egress ran, and * "Stopping recording…" forever after (MRI-C8). The server's transitions * arrive on two transports now: the `meet_recordings` row for a member, and * the room's replicated state for a guest, who has no database. */ readonly recording: RecordingState; readonly recordingStartedBy: UserId | null; /** * WHICH transport last spoke, so the two cannot undo each other (D16). The * database wins: `"record"` is never overwritten by `"room"` or `"local"`. */ readonly recordingSource: RecordingSource; /** * WHICH recording this state is about. It is what tells a LATE packet for a * finished recording apart from the FIRST packet of the next one — without * it, "the database wins" either blocks a second recording forever or lets a * stale packet drag a finished one back to `recording`. */ readonly recordingId: string | null; readonly chat: readonly RoomChatMessage[]; /** Whether the SAVED chat has been read for this meeting (late joiners see history). */ readonly chatLoaded: boolean; /** Why the saved chat could not be read on join, in words — or null. */ readonly chatHistoryError: string | null; readonly reactions: readonly RoomReaction[]; /** * The participant a HOST spotlighted for everyone, from the room's metadata * (server-set, delivered on join). A local pin outranks it on this screen only. */ readonly spotlight: ParticipantIdentity | null; /** * S8 REPLICATED ROOM STATE (CORE-DESIGN §2.6 RS), from the room's metadata — the server's * truth, delivered on join and after every reconnect: who holds the run's host powers * (`actingHost`), who is presenting, and how many people wait to be let in. */ readonly actingHost: ParticipantIdentity | null; readonly presenter: ParticipantIdentity | null; readonly waitingCount: number; /** S8: what the host's Leave offers in this meeting (from the token's `leave_policy`). */ readonly leavePolicy: LeavePolicy; /** The meeting's rules as they are RIGHT NOW (chat mode, attendee lock, time limit), read live while in the call. */ readonly livePolicy: LivePolicy; /** host-reclaim: this person is the meeting's owner and may take the host powers back (Zoom rules). */ readonly canReclaim: boolean; /** rec-consent-gate: this person agreed to the recording that is running. */ readonly recordingConsentGiven: boolean; /** When this run started (epoch ms), so every screen counts a time limit down together. */ readonly sessionStartedAt: number | null; /** True from the moment a host muted THIS microphone until the person turns it on themselves. */ readonly mutedByHost: boolean; /** The last thing a host did to THIS participant, in words. Dismissible. */ readonly moderationNotice: ModerationNotice | null; /** * The server's answer to the last host action THIS participant took ("Muted 3 * people", or the refusal). One place, so whichever surface acted — a tile's * menu, the People panel, More options — the sentence survives the menu closing. */ readonly actionNotice: { readonly text: string; readonly failed: boolean; readonly at: number; } | null; /** * "Message privately" asked for from ANY per-person menu — a tile's, the * People panel's — and answered by the one `ControlBar` that owns the chat * panel (0.7.8). Through 0.7.7 only the People panel could open a private * chat, so the Board's tile menu (and the Room's) had no such item while the * People panel did: two menus for one person that disagreed. */ readonly privateChatRequest: { readonly to: ParticipantIdentity; readonly at: number; } | null; readonly effects: MediaEffects; /** * Captions AND durable rows, reconciled into one list (D16). A record * replaces the caption it supersedes; a caption never overwrites a record. */ readonly transcript: readonly TranscriptSegment[]; readonly notes: readonly MeetingNote[]; /** * 🚨 IDENTITY → THE BEST NAME ANYTHING HAS EVER KNOWN (MRI-A16). Learned from * every source that has one — the roster, a durable segment, a durable * attendance row, a chat message — and never unlearned, so a caption that * lands after somebody leaves still says who spoke. It exists because the * host and the guest rendered DIFFERENT names for the same speaker in the * same second, one of them a raw LiveKit token (MRI-C8). See `core/names.ts`. */ readonly names: NameBook; /** * The wrap-up pass ANNOUNCED itself (MRI-A12). `null` means it has not been * announced in this room — which is not the same as "no summary exists", and * no surface may render it as such. */ readonly wrapUp: WrapUpAnnouncement | null; /** * The server's last refusal to dispatch the note-taker, in ITS words (D7). * Rendered verbatim; never replaced by a spinner or a house sentence. */ readonly noteTakerRefusal: NoteTakerRefusal | null; /** `captions-toggle`: THIS viewer wants captions. Local to the viewer; never sent to the room. */ readonly captionsOn: boolean; /** Why asking for captions failed, in the server's words (`captions-unavailable`). */ readonly captionsRefusal: NoteTakerRefusal | null; /** `captions_start_timeout_seconds` from the token's knobs; the "Starting captions" ceiling. */ readonly captionsStartTimeoutSeconds: number; /** * What the LOCAL participant is actually publishing, and why not when it is * not. Attendance never depends on it (see `attendance.ts`): a refused * microphone is a state with a remedy, not a failed join. */ readonly media: LocalMediaState; /** * S6c — the media supervisor's regions: permission per kind, autoplay (`audio_output`), the * last device change and an offered new device. Written only by `media-supervisor.ts`. */ readonly mediaRegion: MediaRegionState; /** The last failure, with its remedy. Never null-and-hoping. */ readonly failure: { readonly message: string; readonly remedy: string; /** The join gate's reason code, when the server named one (CORE-DESIGN §2.5). */ readonly reason?: string | null; /** * When a refusal lifts by itself (a denial's cooldown): the epoch-ms moment the person may * ask again. Absent for every refusal that does not lift (CORE-DESIGN §11 `wr-denied-reknock`). */ readonly retryAt?: number | null; /** * `join` = the person never got into the call this time (`net-cannot-connect`): a network or * server failure on the way IN is "can't connect", not "lost your connection". */ readonly stage?: "join"; } | null; /** * Meet wave 5: polls, Q&A, the shared whiteboard and the breakout plan — what the * server said, per viewer (`core/collab.ts`). Read once per join, then packets. */ readonly collab: CollabSnapshot; /** The breakout room THIS participant is in, or null for the main room. */ readonly breakoutRoomId: string | null; } /** Reactions live this long. A dropped end event cannot outlive it. */ declare const REACTION_TTL_MS = 5000; /** What `patch` accepts: the snapshot minus the machine-owned fields. */ type MeetPatch = Partial>; interface MeetStore { snapshot(): MeetSnapshot; subscribe(listener: () => void): () => void; /** Everything EXCEPT the phase. The phase moves only through `dispatch`. */ patch(next: MeetPatch): void; /** * THE ONE WRITER OF THE PHASE (CORE-DESIGN §3.1). Applies `event` through the machine's table; * returns false — and changes nothing — when the table does not allow it from the current * phase. `reason` overrides the phase's own reason (an exit's `removed`, `displaced`, …). */ dispatch(event: PhaseEvent, detail?: { readonly reason?: string | null; readonly notice?: string | null; }): boolean; setParticipants(participants: readonly MeetParticipant[]): void; setSpeaking(identities: readonly ParticipantIdentity[]): void; setQuality(identity: ParticipantIdentity, quality: ConnectionQuality): void; addChat(message: RoomChatMessage): void; /** The saved chat, read once per join. Merged with anything that arrived live. */ loadChat(messages: readonly RoomChatMessage[]): void; /** Replace a pending message (by id) with what the server saved, or mark it failed. */ settleChat(localId: string, next: RoomChatMessage | { failed: string; }): void; addReaction(reaction: RoomReaction): void; /** Drops reactions past their TTL. Called by the UI's animation frame. */ expireReactions(now: number): void; /** * The recording reached a state, and WHO says so. `"local"` is this browser's * optimistic `starting`/`stopping`; `"room"` is the server's packet on the * media plane; `"record"` is the durable row. Precedence is D16's: the * database's word stands until the database changes it. */ setRecording(state: RecordingState, by: UserId | null, source: RecordingSource, recordingId?: string | null): void; upsertLobby(entry: LobbyEntry): void; setAdmission(identity: ParticipantIdentity, state: AdmissionState, by: UserId | null): void; /** * Teach the name book. Every source that knows a name calls this, and only * a REAL name is learned — an identity handed back as its own name is not one. */ learn(entries: readonly { identity: ParticipantIdentity; displayName?: string | null; }[]): void; /** A live caption from the media plane. A HINT (D16). */ ingestCaption(segment: TranscriptSegment): void; /** A durable `meet_transcript_segments` row. THE TRANSCRIPT (D16). */ ingestRecord(segment: TranscriptSegment): void; /** Durable notes are re-written in place — a re-run rewrites the same ids. */ upsertNote(note: MeetingNote): void; /** The wrap-up pass finished. Announced on the media plane only (MRI-A12). */ announceWrapUp(announcement: WrapUpAnnouncement): void; /** Record what a local track ACTUALLY did — published, or refused and why. */ setLocalTrack(kind: "microphone" | "camera", state: LocalTrackState): void; reset(): void; } declare function createMeetStore(initial?: Partial): MeetStore; /** * AI-NATIVE, ON BY DEFAULT — and SERVER-OWNED as of 0.3.0 (R4, D2, D16). * * 🚨 **THE PACKAGE NO LONGER KNOWS WHO ANSWERS.** Through 0.2.1 a host injected * `MeetAgents` — four database row ids — and this module ran the agent itself, * shipping the transcript up as `variables`. Both halves of that are gone: * * 1. **No agent id crosses this boundary, and none is injected.** Reasoning runs * behind Mandates whose Holder is resolved from the database at run time * (`meet.live_notes`, `meet.live_intelligence`, `meet.wrap_up`), so an * organization rebinds the job in the Mandate admin with no release of this * package and no change in the host. A host that had to supply an id was a * host that could supply the WRONG id, and a capability that silently * disappeared when nobody had filled a config line. * 2. **No transcript crosses this boundary either.** `POST /v1/meet/intelligence/ask` * takes `{meeting_id, question}` and NOTHING else; the answer is built * server-side from durable rows. Captions are hints and this client holds * only hints (D16) — sending them would be sending the model our guesses. * * What is left here is small and honest: ask a question and render the answer * as it streams, summon or dismiss the note-taker, end the meeting for * everyone, and report the server's refusal in the server's own words. * * 🚨 **THE USER-INPUT LAW still holds** and is now the server's to keep: * `question` is the one genuine human utterance in this module, and it is the * only free text this package sends anywhere near a model. */ interface MeetAiOptions { /** The ONE org-aware aidream client (MRI-A16). Owns base url and headers. */ api: MeetApi; store: MeetStore; /** * Resolves the current meeting's room pass for a guest Q&A request. The * provider owns this because it alone has both the active room and its * persisted guest pass. `null` leaves the header absent for signed-in record * Q&A, where the session remains the credential. */ resolveRoomToken?: (() => string | null) | undefined; } /** What the answer was built from. The server measures it; we never guess it. */ interface MeetAnswerCoverage { readonly segmentsUsed: number; readonly segmentsTotal: number; readonly truncated: boolean; readonly speakers: readonly string[]; } interface MeetAiAnswer { readonly text: string; readonly coverage: MeetAnswerCoverage | null; } interface AskArgs { readonly meetingId: MeetingId; readonly question: string; readonly signal?: AbortSignal | undefined; /** Called with the answer so far, every time it grows. Rendering is live. */ readonly onText?: ((text: string) => void) | undefined; /** The server's phase, verbatim, so a slow answer is never a bare spinner. */ readonly onPhase?: ((phase: string) => void) | undefined; } interface NoteTakerDispatch { readonly dispatched: boolean; /** The server's sentence. Never empty on a refusal. */ readonly detail: string; } interface MeetAi { /** In-meeting Q&A. The ONE human utterance this package sends (D16). */ ask(args: AskArgs): Promise; /** Ask aidream to dispatch the note-taker INTO the room. Host-only. */ startNoteTaker(args: { meetingId: MeetingId; roomName: RoomName; }): Promise; /** * Make sure the note-taker (the speech-to-text) is in the room because THIS viewer turned * captions on. Open to everyone in the meeting, guests included. Never throws: a refusal * is stored as `captionsRefusal` in the server's own words. */ requestCaptions(args: { meetingId: MeetingId; }): Promise; /** Dismiss it. Nothing automatic brings a dismissed note-taker back (D9). */ stopNoteTaker(args: { meetingId: MeetingId; }): Promise; } /** The event vocabulary `/intelligence/ask` emits, pinned by the server's FEATURE.md. */ declare const MEET_ANSWER_EVENTS: { readonly chunk: "chunk"; readonly info: "info"; readonly phase: "phase"; readonly error: "error"; readonly end: "end"; }; /** The `info` event carrying what the answer was built from. */ declare const MEET_ANSWER_CONTEXT_EVENT = "meet_intelligence_context"; interface ServerRefusal { readonly reason: string; readonly remedy: string; } /** * THE SERVER'S REFUSAL, READ RATHER THAN RE-WRITTEN. * * aidream answers a refused dispatch with a body carrying its own `reason` and * `remedy` — "no worker is registered under matrx-note-taker", and what to do * about it. FastAPI nests a raised `HTTPException` under `detail`, and a * validation failure produces a list. All three shapes are read here so the * screen shows the system's own words instead of "something went wrong". */ declare function readRefusalBody(body: unknown): ServerRefusal | null; declare function createMeetAi(options: MeetAiOptions): MeetAi; /** * THE DATA CONTRACT — the ONE place a meet table or RPC name exists. * * 🚨 **STATE OF THE SCHEMA (read this before believing a green test).** The * `communication.meet_*` tables below are the canonical schema DESIGNED by this * package per R8; they are **not deployed to Matrx Main yet**. They were placed * by inspecting the live database on 2026-08-31: `communication` is the schema * that already holds `dm_*` (messaging) and `sms_*` (telephony), so meet takes * the `meet_*` prefix in the same schema rather than inventing a new one. Until * the migration lands, every read here fails LOUDLY with `not-found` and a * remedy naming this file — never silently, and never with a fabricated row. * The migration and the aidream server half are one deliverable; see FEATURE.md * § Deliberately NOT in this package. * * Four rules hold this file together — the same four that hold messaging's: * * 1. **Org is explicit on every write (R5).** Not defaulted by a trigger we * hope fires — passed, and refused in-package when absent. * 2. **Authorization is the database's job.** There is no permission branch in * this file. An RPC that says no is a `forbidden` MeetError, never a * client-side re-decision. Guest capability is scoped by the room TOKEN, not * by a row this client could write. * 3. **Writes go through SECURITY DEFINER RPCs; reads are RLS-backed table * reads** (C17's hybrid ruling). Every write here is an RPC, which is also * what keeps admission and settle-a-call ATOMIC — two hosts clicking "admit" * and a callee accepting on two devices are the ordinary case, not the edge. * 4. **Pagination is keyset, terminated by a unique column.** `(created_at, * id)` — never a timestamp alone, which duplicates or skips rows whenever * two share a millisecond. * * The Supabase client is accepted STRUCTURALLY, through the shape * `@ai-matrx/messaging` already owns. One definition of "a Supabase client" for * the whole communications family; this package adds no second copy. * * 🚨 AND THE PUBLIC BOUNDARY IS THE SHALLOW ONE (0.2.1). `RepositoryOptions` * and `` take `MessagingSupabaseClient` — method names and * arity, results as `unknown` — never the rich `SupabaseLike`. Checking a * host's real `SupabaseClient` (built from generated types) against * the rich shape makes tsc chase PostgREST's schema machinery until it bails * with `TS2589: Type instantiation is excessively deep and possibly infinite`, * so the only way to pass the client a host ALREADY HAS was a cast — which is * what the first real consumer had to write (register MRI-D1). Messaging 0.9.0 * learned this and wrote it down; this package did not apply the lesson until * now. The rich shape stays the INTERNAL contract, and we narrow to it ONCE, * here, in-package. */ /** The schema. Meet is never in `public`. */ declare const MEET_SCHEMA = "communication"; declare const MEET_TABLES: { readonly meetings: "meet_meetings"; readonly participants: "meet_participants"; readonly invites: "meet_call_invites"; readonly recordings: "meet_recordings"; readonly transcripts: "meet_transcript_segments"; readonly notes: "meet_notes"; /** Wave 1 (0.8): who a meeting was sent to, and what they answered. */ readonly invitees: "meet_invitees"; /** Wave 1 (0.8): one occurrence of a series canceled or moved. */ readonly occurrenceOverrides: "meet_occurrence_overrides"; /** S5: one row per RUN of a meeting; carries that run's note-taker state. */ readonly sessions: "meet_sessions"; }; declare const MEET_RPCS: { /** Atomic: create the invite row and stamp its expiry in one statement. */ readonly createCallInvite: "meet_create_call_invite"; /** Atomic settle. Refuses a transition out of a terminal state. */ readonly settleCallInvite: "meet_settle_call_invite"; /** Every invite still ringing this user — the reconnect backfill door. */ readonly pendingCallInvites: "meet_pending_call_invites"; /** Instant meeting, or the durable one behind a recurring link. */ readonly getOrCreateMeeting: "meet_get_or_create_meeting"; /** Resolve a durable public link, before/during/after its scheduled time. */ readonly meetingBySlug: "meet_meeting_by_slug"; /** The current run of a durable link (open / not started / ended + next start), no pass needed. */ readonly linkState: "meet_link_state"; readonly admit: "meet_admit"; readonly deny: "meet_deny"; readonly undeny: "meet_undeny"; readonly admitAll: "meet_admit_all"; readonly setLock: "meet_set_lock"; readonly recordConsent: "meet_record_consent"; /** One meeting rule as the gate obeys it (the knob ladder, this meeting's rung included). */ readonly meetingRule: "meet_policy"; /** A host sets one rule for THIS meeting (the `meet_meeting` rung); refusals come back as `{ok:false}`. */ readonly setMeetingRule: "meet_policy_set"; /** Schedule with every setting at create; unset settings come from the knobs. */ readonly scheduleMeeting: "meet_schedule_meeting"; /** Edit / reschedule — a JSON patch with a version compare-and-swap. */ readonly updateMeeting: "meet_update_meeting"; /** Cancel is a state (`cancelled_at`); the link keeps resolving. */ readonly cancelMeeting: "meet_cancel_meeting"; /** Archive (soft delete) — the invitations and exceptions cascade. */ readonly archiveMeeting: "meet_archive_meeting"; readonly restoreMeeting: "meet_restore_meeting"; /** Cancel, move or restore ONE occurrence of a series by its original start. */ readonly setOccurrence: "meet_set_occurrence"; readonly addInvitees: "meet_add_invitees"; readonly setInviteeRole: "meet_set_invitee_role"; readonly removeInvitee: "meet_remove_invitee"; /** A signed-in invitee answers for themselves. */ readonly respond: "meet_respond"; /** The emailed RSVP link (no account): read and answer. */ readonly invitationByToken: "meet_invitation_by_token"; readonly respondByToken: "meet_respond_by_token"; /** One meeting's occurrences between two instants (INVOKER — the reader's RLS). */ readonly meetingOccurrences: "meet_meeting_occurrences"; /** Every occurrence the reader can see between two instants — the Upcoming list. */ readonly occurrencesBetween: "meet_occurrences_between"; /** The recordings library. */ readonly myRecordings: "meet_my_recordings"; /** Titles, agendas, current notes and final transcript lines; snippets mark hits with ⟦ ⟧. */ readonly search: "meet_search"; }; type Row = Record; interface CreateCallInviteInput { readonly roomName: RoomName; readonly mode: CallMode; readonly callerUserId: UserId; readonly callerName: string; readonly callerAvatarUrl: string | null; readonly calleeUserIds: readonly UserId[]; readonly organizationId: OrganizationId; readonly conversationId: string | null; readonly expiresAt: string; } interface CreateMeetingInput { readonly organizationId: OrganizationId; readonly hostUserId: UserId; readonly title: string; readonly kind: MeetingKind; readonly scheduledFor?: string | null; readonly scheduledDurationMinutes?: number | null; readonly lobbyEnabled?: boolean; readonly recordingPolicy?: RecordingPolicy; readonly aiEnabled?: boolean; /** Reuse a durable slug (a recurring meeting keeps ONE link — R3). */ readonly slug?: string | undefined; } interface MeetRepository { readonly schema: string; readonly tables: typeof MEET_TABLES; projectInvite(row: Row): CallInvite; projectMeeting(row: Row): MeetingRecord; createCallInvite(input: CreateCallInviteInput): Promise; settleCallInvite(id: CallId, state: CallState, declineMessage: string | null): Promise; pendingInvites(userId: UserId): Promise; getOrCreateMeeting(input: CreateMeetingInput): Promise; meetingBySlug(slug: string): Promise; meeting(id: MeetingId): Promise; /** * Keyset paged, `(scheduled_for, id)`. Every meeting the person can access, in * ALL their organizations — `organizationId` is an OPTIONAL page filter * (absent/null = all organizations), never the active organization * (active-org-is-never-a-list-filter.md). Access is decided by RLS. */ listMeetings(args: { organizationId?: OrganizationId | null; limit?: number; before?: { scheduledFor: string; id: MeetingId; } | null; }): Promise<{ readonly items: readonly MeetingRecord[]; readonly hasMore: boolean; }>; /** S4: one decision on one waiting person; returns the row as it now stands. */ decideAdmission(args: { meetingId: MeetingId; identity: string; decision: "admit" | "deny" | "undeny"; byUserId: UserId; }): Promise; /** S4: admit everyone still waiting (not expired); returns the admitted rows. */ admitAll(meetingId: MeetingId, byUserId: UserId): Promise; setLock(meetingId: MeetingId, locked: boolean, byUserId: UserId): Promise; /** `wr-policy-who-waits`: read one rule of this meeting (`lobby_bypass`, `access_type`…). */ meetingRule(meetingId: MeetingId, key: string): Promise; /** A host sets one rule for this meeting; a refusal throws with the server's sentence. */ setMeetingRule(meetingId: MeetingId, key: string, value: string | number | boolean): Promise; projectSegment(row: Row): TranscriptSegment; projectNote(row: Row): MeetingNote; /** One `meet_recordings` row, however it arrived (row or room packet). */ projectRecording(row: Row): RecordingRecord; /** One durable `meet_participants` row — attendance, and the lobby queue. */ projectAttendee(row: Row): MeetingAttendee; projectAttendee(row: Row): MeetingAttendee; /** * WHO ATTENDED, oldest arrival first. Everyone the meeting ever admitted — * including the note-taker, which is a labeled participant and never a hidden * listener (R4) — so the record can say who was in the room AND that it was * transcribed. */ attendees(meetingId: MeetingId): Promise; /** * THE DURABLE TRANSCRIPT (D16). Final rows only — the table holds nothing * else — oldest first, capped so a four-hour meeting cannot hand a surface * fifty thousand rows in one read. */ transcript(meetingId: MeetingId, limit?: number): Promise; /** Live notes, decisions, action items and the wrap-up summary, oldest first. */ notes(meetingId: MeetingId): Promise; /** R5: consent is RECORDED, not assumed. One row per participant per recording. */ recordConsent(args: { meetingId: MeetingId; identity: string; acknowledgedAt: string; }): Promise; recordings(meetingId: MeetingId): Promise; /** Meet wave 3: every recording of the reader's own meetings, newest first. */ myRecordings(args?: { includeArchived?: boolean; limit?: number; }): Promise; /** Meet wave 3: search the reader's own meetings. */ searchMeetings(query: string, limit?: number): Promise; /** Meet wave 3: name a recording (`null` = back to the meeting's title and date). */ renameRecording(recordingId: string, title: string | null): Promise; /** Meet wave 3: archive (soft delete) or restore a recording. Never a purge. */ setRecordingArchived(recordingId: string, archived: boolean): Promise; scheduleMeeting(input: ScheduleMeetingInput): Promise; /** `expectedVersion` from the row you edited; a stale one is a `conflict`. */ updateMeeting(meetingId: MeetingId, changes: MeetingChanges, options?: { expectedVersion?: number | null; byUserId?: UserId | null; }): Promise; cancelMeeting(meetingId: MeetingId, reason: string | null, byUserId?: UserId | null): Promise; archiveMeeting(meetingId: MeetingId, byUserId?: UserId | null): Promise; restoreMeeting(meetingId: MeetingId, byUserId?: UserId | null): Promise; setOccurrence(args: { meetingId: MeetingId; originalStart: string; action: "cancel" | "move" | "restore"; newStart?: string | null; newDurationMinutes?: number | null; reason?: string | null; byUserId?: UserId | null; }): Promise; meetingOccurrences(meetingId: MeetingId, range: { from: string; to: string; limit?: number; }): Promise; /** The Upcoming list: every occurrence the reader may see. `organizationId` FILTERS; RLS decides. */ occurrencesBetween(range: { from: string; to: string; organizationId?: OrganizationId | null; limit?: number; }): Promise; invitees(meetingId: MeetingId): Promise; addInvitees(meetingId: MeetingId, people: readonly InviteeInput[], byUserId?: UserId | null): Promise; setInviteeRole(inviteeId: string, role: InviteeRole, byUserId?: UserId | null): Promise; removeInvitee(inviteeId: string, byUserId?: UserId | null): Promise; respond(meetingId: MeetingId, answer: RsvpAnswer, note?: string | null): Promise; invitationByToken(secret: string): Promise; respondByToken(secret: string, answer: RsvpAnswer, note?: string | null): Promise; projectInvitee(row: Row): MeetingInvitee; } interface ScheduleMeetingInput { readonly organizationId: OrganizationId; readonly hostUserId: UserId; readonly title: string; readonly scheduledFor: string | null; /** IANA zone, e.g. `America/Los_Angeles`. The wall-clock time a series keeps across DST. */ readonly timeZone: string; /** Omit to use the person's `meet.default_duration_minutes`. */ readonly durationMinutes?: number | null | undefined; readonly agenda?: string | null | undefined; /** RRULE subset: FREQ=DAILY|WEEKLY|MONTHLY, INTERVAL, BYDAY, BYMONTHDAY, COUNT, UNTIL. */ readonly recurrenceRule?: string | null | undefined; readonly settings?: MeetingSettings | undefined; /** HR-360 wave 2 (MD-15): the registered app panel the call opens beside it. */ readonly appPanel?: { readonly key: string; readonly recordId: string; } | undefined; /** HR-360 wave 2 (MD-16): user ids the host names as silent observers. */ readonly observers?: readonly string[] | undefined; } interface RepositoryOptions { /** * Any Supabase client. Deliberately the SHALLOW public shape — pass the * `SupabaseClient` you already have, with no cast. See the header. */ client: MessagingSupabaseClient; /** Resolve a session; used to retry ONCE across the sign-in gap. */ session?: (() => Promise) | undefined; } /** * 🚨 THE PROJECTIONS ARE MODULE-LEVEL AND EXPORTED, DELIBERATELY (MRI-A12). * * A durable Meet row reaches a client on one of two transports: as a * `meet_notes` / `meet_transcript_segments` row over Postgres Changes, which * needs a Supabase session and an org grant, or as a `matrx.meet.*` packet on * the LiveKit data plane, which needs only the room token — and is therefore the * ONLY one a session-less guest can receive (MRI-A10 settled that `anon` never * gets a grant on an org-scoped table). * * Both carry the same row. If each transport projected it with its own function, * a guest and a member would render the same meeting differently the first time * the two drifted. So there is one function per row shape, it takes no client, * and `core/room-events.ts` calls exactly these. */ declare function projectSegment(row: Row): TranscriptSegment; declare function projectNote(row: Row): MeetingNote; declare function projectInvitee(row: Row): MeetingInvitee; declare function projectOccurrence(row: Row): MeetingOccurrence; declare function projectUpcoming(row: Row): UpcomingOccurrence; declare function projectOverride(row: Row): OccurrenceOverride; /** The jsonb the RSVP doors return, typed. A body that is neither shape is refused loudly. */ declare function projectRsvpInvitation(body: unknown): RsvpInvitation; /** Settings → the door's snake_case object, carrying ONLY what was set. */ declare function settingsPatch(settings: MeetingSettings): Record; /** * A partial edit → the `meet_update_meeting` patch. `undefined` = leave alone, * `null` = clear — the difference a JSON patch exists to carry. */ declare function changesPatch(changes: MeetingChanges): Record; declare function createMeetRepository(options: RepositoryOptions): MeetRepository; /** One row of the recordings library (`meet_my_recordings`). */ interface LibraryRecording { readonly id: string; readonly meetingId: MeetingId; readonly meetingTitle: string; readonly meetingSlug: string; /** The reader's strongest role on the meeting. */ readonly role: "host" | "cohost" | "invitee" | "attendee"; readonly state: RecordingState; /** A name a host gave it; null = show the meeting title. */ readonly title: string | null; readonly fileId: string | null; readonly startedAt: string | null; readonly endedAt: string | null; readonly durationMs: number | null; readonly sizeBytes: number | null; readonly failureReason: string | null; readonly archivedAt: string | null; readonly organizationId: OrganizationId; } declare function projectLibraryRecording(row: Row): LibraryRecording; /** Where a search hit was found. */ type MeetingSearchSource = "title" | "summary" | "decision" | "action_item" | "note" | "transcript"; /** One hit from `meet_search`. `snippet` marks matched words with ⟦ ⟧. */ interface MeetingSearchHit { readonly meetingId: MeetingId; readonly meetingTitle: string; readonly meetingSlug: string; readonly meetingAt: string | null; readonly source: MeetingSearchSource; /** The note or transcript segment id (the meeting id for a title hit). */ readonly refId: string; readonly speaker: string | null; readonly snippet: string; /** When it was said / written. */ readonly at: string | null; readonly rank: number; } declare function projectSearchHit(row: Row): MeetingSearchHit; /** A snippet split into plain and matched runs, for highlighting without HTML. */ declare function snippetRuns(snippet: string): readonly { readonly text: string; readonly hit: boolean; }[]; /** * THE CALL CENTER — ringing, answering, and the six ways a call ends badly. * * A call reaches you on whatever surface you are on (R2), which is why this is * mounted by the provider and not by a call page. It is also why delivery is * DUAL by design, not by belt-and-braces nervousness: * * - **The durable leg** is the invite ROW plus its Postgres Changes event. It * survives a reload, a late-joining tab, and a dropped socket, and it is what * makes a missed call a fact rather than a lost packet. * - **The instant leg** is a broadcast on the callee's user channel, which * arrives without a database round trip. * * Both are deduplicated on the invite id, so a call rings ONCE however it * arrives. Neither is a fallback for the other: removing the durable leg loses * calls, and removing the instant leg makes ringing feel slow. * * 🚨 THE FOUR RULES THAT MAKE RINGING SURVIVABLE * * 1. **A ring always ends.** Every outgoing call arms a timeout that writes * `missed`. A ring with no terminal state is a phone that never stops. * 2. **Answering on one device stops every other.** The `settled` signal on * your OWN user channel silences the other tabs. Without it, answering on a * laptop leaves a phone ringing in the room. * 3. **Busy is answered, not ignored.** Being in an active call auto-declines * with `busy`, so the caller learns something instead of ringing out. * 4. **The state machine is a MACHINE.** Transitions are validated: a * `declined` call cannot become `active` because a late accept arrived. */ declare function canTransition(from: CallState, to: CallState): boolean; interface CallCenterOptions { manager: RealtimeManager; repository: MeetRepository; store: MeetStore; identity: MeetIdentity; /** Ring for this long before the call is `missed`. Org knob, not a constant. */ ringTimeoutMs?: number | undefined; onIncoming?: ((invite: CallInvite) => void) | undefined; onSettled?: ((invite: CallInvite) => void) | undefined; /** * §3.6: MY outgoing call was answered — join its room on the same engine and machine * (`ringing_out` → `joining` → `in_call`). The provider wires this to `engine.join`. */ onAnswered?: ((invite: CallInvite) => void) | undefined; onDiagnostic?: ((event: { level: "info" | "warn" | "error"; message: string; remedy?: string; }) => void) | undefined; timers?: { setTimeout: (run: () => void, ms: number) => unknown; clearTimeout: (handle: unknown) => void; now: () => number; }; } interface CallCenter { /** Subscribe to this user's calling channel. Call once, from the provider. */ start(): Promise; /** Ring someone. Returns the invite the UI shows as "calling…". */ call(args: { people: readonly UserId[]; mode: CallMode; conversationId?: string | undefined; }): Promise; accept(id: CallId, mode: CallMode): Promise; decline(id: CallId, message?: string): Promise; cancel(id: CallId): Promise; /** Every invite currently ringing this device. */ incoming(): readonly CallInvite[]; outgoing(): CallInvite | null; /** Feed a ring that arrived through another path (a messaging action chip). */ ingest(invite: CallInvite): void; dispose(): void; } declare function createCallCenter(options: CallCenterOptions): CallCenter; /** * DEVICE MANAGEMENT — the part users blame the product for. * * Every rule here exists because it is a support ticket somewhere: * * - **Labels are empty until permission is granted.** `enumerateDevices()` * happily returns three cameras with blank labels before the prompt. A picker * rendering "Camera 1 / Camera 2" is the classic symptom; we report * `labelsHidden` so the UI can say *why* instead. * - **A remembered device can vanish.** Unplug a headset between meetings and a * stored `deviceId` produces `OverconstrainedError`. The manager falls back to * the system default AND SAYS SO — a silent fallback is how someone joins on * the laptop mic while believing they are on their headset. * - **Selections persist per KIND, per device.** Injected storage (C10: * persistence-injected — this package's core purpose is not storage), with a * memory fallback that announces itself rather than pretending to be durable. * - **Enumeration is repeated on `devicechange`.** A picker that lists what was * plugged in at mount is wrong within seconds of a headset arriving. */ type DeviceKind = "audioinput" | "videoinput" | "audiooutput"; interface MeetDevice { readonly deviceId: string; readonly kind: DeviceKind; readonly label: string; } interface DeviceSnapshot { readonly devices: Readonly>; readonly selected: Readonly>; /** True when the browser is hiding labels because permission was not granted. */ readonly labelsHidden: boolean; readonly permission: "unknown" | "granted" | "denied"; /** Set when a remembered device was gone and we fell back. Shown, not swallowed. */ readonly fallbackNotice: string | null; } interface DeviceStorage { read(): Record; write(value: Record): void; readonly durable: boolean; } interface MediaDevicesLike extends MediaDevicesLike$1 { getUserMedia(constraints: { audio?: unknown; video?: unknown; }): Promise<{ getTracks(): readonly { stop(): void; }[]; }>; } interface DeviceManagerOptions { media: MediaDevicesLike; storage?: DeviceStorage | undefined; onChange?: ((snapshot: DeviceSnapshot) => void) | undefined; } interface DeviceManager { /** * 🚨 THE SAME REFERENCE until something actually changes. This is a * `useSyncExternalStore` `getSnapshot` source (see `useDeviceSnapshot`), and * React compares snapshots by IDENTITY: a fresh object literal per call makes * every render report a change, which React reports as *"The result of * getSnapshot should be cached"* and then kills the tree with *"Maximum * update depth exceeded"*. `createMeetStore()` has always held this contract; * 0.2.0 shipped without it here and every room crashed at ``. */ snapshot(): DeviceSnapshot; /** Enumerate without prompting. Labels may be hidden; that is reported. */ refresh(): Promise; /** Prompt, then enumerate with real labels. The pre-join screen calls this. */ requestPermission(kinds: { audio: boolean; video: boolean; }): Promise; select(kind: DeviceKind, deviceId: string): DeviceSnapshot; /** The id to actually use — the remembered one, or the default when it is gone. */ resolve(kind: DeviceKind): string | null; subscribe(listener: (snapshot: DeviceSnapshot) => void): () => void; dispose(): void; } /** localStorage when it exists; memory otherwise — and it says which. */ declare function createWebDeviceStorage(key?: string): DeviceStorage; declare function createMemoryDeviceStorage(): DeviceStorage; declare function createDeviceManager(options: DeviceManagerOptions): DeviceManager; /** * NETWORK HEALTH (CORE-DESIGN §3.3 CS, P1 connection-network) — the pure rules behind what each * person sees when a network degrades. No socket, no timer, no browser global: the driver hands * in readings, the engine hands in the clock, and every rule here is provable in a node test. * * Why the core judges quality itself and does not only repeat LiveKit's `ConnectionQuality`: * LiveKit scores a TCP/TURN path with 800 ms of round trip as "good" (no packets are lost on * TCP — they are just late), and it keeps a participant whose cable was pulled "good" for 25 s * and more. Google Meet documents media quality degrading at RTT ≥ 300 ms; that is the bar here. * * Four rules: * 1. **Self-assessment** (`createSelfAssessor`): this tab's own path, from its WebRTC stats — * round trip, loss — with hysteresis so the reading does not flap. * 2. **The path** (`pathOf`): media relayed over TURN or carried over TCP is a restricted network * (`net-udp-blocked`): the call works, quality may be reduced. * 3. **Liveness** (`createLivenessTracker`): every client says "alive" on a light data heartbeat * that carries its own reading and whether its video is paused. A person who goes silent past * `staleAfterMs` is shown `reconnecting` to everyone else (`net-others-see-drop`) — long before * the media server gives up on them. A client that never sent a heartbeat (an agent, an older * client) is never judged by it. * 4. **Video auto-off** (`createVideoAutoOff`): a poor reading that lasts * `video_auto_off_after_poor_seconds` pauses this person's camera so their audio survives * (`net-video-auto-off`); a person who turns it back on is not paused again until the path has * recovered once. */ /** One sample of this tab's WebRTC path, as the driver read it from `getStats()`. */ interface NetworkReading { /** Selected candidate pair's current round trip (ms), or null when not known. */ readonly rttMs: number | null; /** Fraction of packets lost since the previous sample (0..1), or null. */ readonly lossRatio: number | null; /** The selected local candidate is a TURN relay. */ readonly relayed: boolean | null; /** The selected local candidate carries media over TCP. */ readonly tcp: boolean | null; } /** * THE MEDIA SEAM — the ONE interface the room engine talks to. * * D1/D2: LiveKit Cloud is the media infrastructure, hardcoded. This interface * is NOT a provider abstraction and is not here so a second SFU can be swapped * in — it exists for two reasons that are both about correctness: * * 1. **The engine must be provable without a browser.** Everything expensive in * a calling product (ring timeouts, reconnection, admission, escalation, * speaker tracking) is logic, and logic proven only through a real SFU is * logic that is never proven. `testing/fake-driver.ts` drives every one of * those paths in a node test. * 2. **The SDK stays out of the initial chunk.** `livekit-client` is reached * only through a dynamic `import()` inside the default driver, so importing * this package during SSR — or on a page with a call BUTTON but no call — * costs nothing and touches no browser API. * * The default implementation ships (`createLiveKitDriver`) and is what * `` uses when a host injects nothing (THE ALL-INCLUSIVE LAW). */ /** A participant as the media layer sees them, before roles or profiles. */ interface DriverParticipant { readonly identity: string; readonly name: string; /** Opaque JSON string the token minted for this participant. */ readonly metadata: string | null; /** * The participant's LiveKit attributes. Unlike `metadata` these are set by * the participant itself at run time, which is how the note-taker announces * that a consent notice is owed (D10). */ readonly attributes: Readonly>; readonly isLocal: boolean; readonly micEnabled: boolean; readonly cameraEnabled: boolean; readonly screenSharing: boolean; } type DriverConnectionState = "connecting" | "connected" /** Only the signal connection is being re-established; media may still flow. */ | "signal_reconnecting" | "reconnecting" | "disconnected"; type DriverTrackKind = "camera" | "microphone" | "screen"; /** * ONE LIVE CAPTION off the `lk.transcription` text stream. * * `speakerIdentity` is `lk.transcribed_participant_identity` — WHO IS BEING * TRANSCRIBED, not who published the stream (the note-taker publishes every * one of them). The SFU-reported publisher identity below is independently * checked against the meeting's note-taker; captions remain hints and durable * segments remain the record (D16). */ interface DriverTranscription { /** `lk.segment_id`. Interim and final share it; that is what makes replacement possible. */ readonly segmentId: string; readonly speakerIdentity: string; /** * The LiveKit participant that opened this text stream. Unlike * `speakerIdentity`, this comes from the SFU and is the authority for who * published a caption. */ readonly publisherIdentity: string; readonly text: string; /** `lk.transcription_final`. */ readonly final: boolean; /** `lk.transcribed_track_id`, when the sender supplied it. */ readonly trackId: string | null; readonly at: string; } interface RoomDriverHandlers { /** The FULL roster, every time. A delta protocol here drifts; a snapshot cannot. */ participants(participants: readonly DriverParticipant[]): void; activeSpeakers(identities: readonly string[]): void; quality(identity: string, quality: ConnectionQuality): void; connectionState(state: DriverConnectionState): void; /** In-room data messages: hand raise, reactions, in-room chat, AI events. */ data(topic: string, payload: string, fromIdentity: string | null): void; /** A live caption. Interim and final both arrive here (D16). */ transcription(event: DriverTranscription): void; /** * A remote participant left the room, with LiveKit's reason by NAME (`CLIENT_INITIATED`, …) or * null when none was given. Fired BEFORE the roster update that drops them, so an unannounced * departure can be held as a `reconnecting` tile (S6b). Optional for older host drivers. */ participantLeft?(identity: string, reason: string | null): void; /** The room ended for this client. `reason` is shown to the user verbatim. */ disconnected(reason: string | null): void; /** * The ROOM's metadata — set only by the server (`UpdateRoomMetadata`), and * delivered to every joiner ON JOIN, which is why the spotlight rides it: a * data packet reaches only the people already in the room. Called once after * connect and on every change. Optional so an older host driver still works. */ roomMetadata?(metadata: string): void; /** * S6c — MEDIA. Can remote audio play (`false` = the browser's autoplay policy is holding it * until a gesture)? Fired on every change and once right after connect. Optional for older drivers. */ audioPlayback?(canPlay: boolean): void; /** S6c — the device list changed (plugged / unplugged), AFTER the SDK re-picked defaults. */ devicesChanged?(): void; /** S6c — the SDK could not acquire or keep a local device. `kind` when the SDK knows it. */ mediaDeviceError?(error: unknown, kind: "audioinput" | "videoinput" | null): void; /** * P1 connection-network — one sample of THIS tab's WebRTC path (round trip, loss, relay/TCP), * read from the peer connections' stats every couple of seconds while connected. The rules * that turn samples into what people see are `network-health.ts`. Optional for older drivers. */ networkReading?(reading: NetworkReading): void; } /** A camera background effect. `image` replaces the background with `imageUrl`. */ type VideoEffect = { readonly kind: "none"; } | { readonly kind: "blur"; readonly radius?: number; } | { readonly kind: "image"; readonly imageUrl: string; }; /** Microphone processing the browser applies before anything is sent. */ interface AudioProcessing { readonly noiseSuppression: boolean; readonly echoCancellation: boolean; readonly autoGainControl: boolean; } interface MeetRoomDriver { connect(serverUrl: string, token: string): Promise; disconnect(): Promise; setMicrophoneEnabled(enabled: boolean): Promise; setCameraEnabled(enabled: boolean): Promise; setScreenShareEnabled(enabled: boolean): Promise; /** * Reliable, topic-addressed data. The in-room control plane. `to` addresses * named identities only (a private chat message); absent = everyone. */ publishData(topic: string, payload: string, to?: readonly string[]): Promise; /** * Apply microphone processing to the live track and to every future one. * Optional: a driver without it makes the control ABSENT, never dead. */ setAudioProcessing?(processing: AudioProcessing): Promise; /** * Apply a background effect to the live camera and to every future one. * Rejects with a sentence when this browser cannot run it. */ setVideoEffect?(effect: VideoEffect): Promise; /** Whether this browser can run background effects at all. */ supportsVideoEffects?(): boolean; /** Whether this browser can share a screen at all (iOS cannot). Absent = assume yes. */ supportsScreenShare?(): boolean; /** The screen share being published carries sound (`share-audio`). */ screenShareHasAudio?(): boolean; /** * Attach a participant's track to a media element. Returns the detach * function — returning it (rather than exposing `detach`) is what makes a * leaked attachment impossible from the React side: the effect's cleanup IS * the return value. */ attach(identity: string, kind: DriverTrackKind, element: HTMLMediaElement): () => void; /** The device the driver actually selected, so the UI never lies about it. */ activeDevices(): Readonly>; switchDevice(kind: "audioinput" | "videoinput" | "audiooutput", deviceId: string): Promise; /** * S6c — resume remote audio the autoplay policy blocked. MUST run inside the person's click * (the gesture is what the browser checks). Optional: a driver without it never blocks. */ startAudio?(): Promise; /** * S7 — a "join here too" companion plays no remote audio (the person's other device does; two * speakers in one room echo). `false` removes every attached remote audio element and keeps new * ones from attaching. Optional: a driver without it always plays. */ setRemoteAudioPlayback?(enabled: boolean): void; } type RoomDriverFactory = (handlers: RoomDriverHandlers) => MeetRoomDriver | Promise; /** Topics for the in-room control plane. One place, so senders cannot drift. */ declare const DATA_TOPICS: { readonly reaction: "mx.reaction"; readonly chat: "mx.chat"; }; /** * 🚨 `mx.ai.note` and `mx.ai.transcript` WERE declared here and emitted by * nothing. They are gone (MRI-A12): the platform's own events are published by * the SERVER under `matrx.meet.*` — see `core/room-events.ts` — because a packet * a participant could publish is a packet a guest could forge, and these carry * durable rows. A participant topic for the same thing would be a second, * unauthenticated way to say it. */ type DataTopic = (typeof DATA_TOPICS)[keyof typeof DATA_TOPICS]; /** * THE LIVEKIT TRANSCRIPTION CONTRACT — not ours, so it is written down once * rather than spelled inline. The topic and these four attribute names are the * SDK's own (`lk.*`); the worker publishes to exactly them (MRI-A2) and a * rename on either side must break here, loudly, in one place. */ declare const TRANSCRIPTION_TOPIC = "lk.transcription"; declare const TRANSCRIPTION_ATTRIBUTES: { readonly segmentId: "lk.segment_id"; readonly trackId: "lk.transcribed_track_id"; readonly final: "lk.transcription_final"; /** * WHO IS BEING TRANSCRIBED. The LiveKit JS client reports the PUBLISHER of a * text stream, and the publisher is always the note-taker — so a caption * carries the speaker in this attribute and falls back to the publisher only * when it is absent (which attributes the line to the agent, visibly, rather * than to the wrong person, silently). */ readonly speakerIdentity: "lk.transcribed_participant_identity"; }; /** * MEETINGS — the lobby, the host powers, and the durable link. * * A meeting is a room plus scheduling, lobby and host metadata (D3). Everything * here is about the moments OUTSIDE the media session, which is exactly why it * cannot ride LiveKit's data plane: a guest waiting in the lobby is not in the * LiveKit room yet, by construction. The lobby therefore lives on the meet-room * realtime channel. * * 🚨 THE THREE THINGS THAT MAKE GUEST JOIN SAFE (D6) * * 1. **The link is an identity, not a session.** A durable slug resolves before, * during, and after the scheduled time, and a recurring meeting keeps ONE * link. Nothing here mints a per-occurrence URL. * 2. **A guest's capability is the TOKEN, and nothing else.** They are scoped to * exactly one room. There is no guest row this client writes that could grant * anything, and no client-side "is this person allowed" branch. * 3. **Admission is a SERVER decision, recorded.** `admit` is an RPC; the * broadcast that tells the knocker is a NOTIFICATION of that decision, never * the decision itself. Two hosts clicking admit is the ordinary case, and the * RPC is what makes it idempotent. * * Host powers are checked against the role the SERVER minted into the token * (see `room.ts`), and the UI hides what an actor cannot do rather than * rendering a dead button (no dead ends). */ interface MeetingSessionOptions { manager: RealtimeManager; repository: MeetRepository; store: MeetStore; identity: MeetIdentity; /** * The ONE org-aware aidream client (MRI-A16) — `endForEveryone` is a SERVER * decision (MRI-A6), and every request it makes carries the organization. */ api: MeetApi; onDiagnostic?: ((event: { level: "info" | "warn" | "error"; message: string; remedy?: string; }) => void) | undefined; /** * 🚨 THE HOST ADMITTED *US*. Called exactly once per admission verdict that * names this participant, and only for `admitted`. * * Admission is not a UI state change: a lobby token is minted with * `canPublish: false` and `canSubscribe: false`, so the waiting room can only * become a room by minting a NEW token. Before this existed the waiting room * said "you will join automatically once you are admitted" and then waited * forever — a sentence on screen that was not true. The provider wires this * to `engine.rejoin()`. */ onAdmitted?: (() => void) | undefined; /** * S4: the meeting pass this device holds (`x-meet-pass`) — what the state read and * `/leave` are authorized by. Null when no pass is held yet. */ resolvePass?: (() => string | null) | undefined; /** S4: mint again (Ask to join again / Rejoin from the waiting screens). */ rejoin?: (() => Promise) | undefined; } interface MeetingSession { /** Subscribe to the room's out-of-band plane. Hosts see knocks; guests see verdicts. */ watch(roomName: RoomName): void; /** * Subscribe to the MEETING RECORD's durable feed — transcript segments, notes, * and the meeting row itself (which is where the note-taker's state lives). * Reconnecting IS re-reading: the backfill door re-reads all three, because * realtime has no replay and a socket blip would otherwise leave a transcript * permanently missing the minute nobody noticed. */ watchMeeting(meetingId: MeetingId): void; admit(entry: LobbyEntry, admitted: boolean): Promise; /** S4: lift a denial so the person may knock again (`meet_undeny`). */ undeny(entry: LobbyEntry): Promise; admitAll(): Promise; /** * 🚨 S4 — THE WAIT READS SERVER TRUTH (CORE-DESIGN §2.6). While this tab waits, the state * read (`GET /v1/meet/state`) runs every `waiting_poll_seconds`, on every * `mx.state.changed` hint, on `online` and on visibility return. Admitted → a fresh mint * (`rejoin`); denied / expired / ended are what the row says, never a broadcast's guess. */ startWaiting(): void; stopWaiting(): void; /** One state read now (idempotent while one is in flight). */ readState(): Promise; /** One read of the meeting's rules now (chat mode, attendee lock, time limit); resolves once they are on the store. */ readLivePolicy(): Promise; /** rec-consent-gate: tell the SERVER this person agreed to being recorded (a row fact; it gives publish back). */ acknowledgeConsent(): Promise; /** * §3.4 ER: what the server recorded for THIS connection's exit, and the run's phase. null = * the read failed (offline / unreachable); undefined = no meeting or no pass to ask with. */ readExit(connection: string, pass?: string | null): Promise; /** Stop waiting: the row becomes `withdrawn` (`POST /v1/meet/leave {intent: "withdraw"}`). */ withdraw(): Promise; /** Ask again after a denial or an expired knock (only offered when allowed). */ askAgain(): Promise; /** * S8: the HOST's leave intents through `POST /v1/meet/leave` — end the meeting for everyone, * or hand the run's host powers to `assignTo` (a person key) before leaving. The server * decides from the pass whether this person holds the run's host powers. */ hostLeave(intent: "end_for_all" | { readonly assignTo: string; }): Promise; setLock(locked: boolean): Promise; /** `wr-policy-who-waits`: one rule of the meeting on screen (null when unreadable). */ meetingRule(key: string): Promise; /** A host changes one rule for this meeting; waiting people are told to read again. */ setMeetingRule(key: string, value: string | number | boolean): Promise; /** End it for everyone, through the server (MRI-A6). */ endForEveryone(): Promise; dispose(): void; } /** Can `actor` moderate `target`? A host outranks a co-host outranks everyone. */ declare function canModerateParticipant(actor: MeetParticipant | undefined, target: MeetParticipant | undefined): boolean; declare function roleOf(participants: readonly MeetParticipant[], identity: ParticipantIdentity | null): ParticipantRole; /** * What ending a meeting produced. `served: false` is the honest answer from a * server that has not landed `POST /v1/meet/end` yet (MRI-A6) — the control is * real, the refusal is a sentence, and nothing pretends the meeting ended. */ interface EndMeetingResult { readonly endedAt: string | null; readonly served: boolean; } /** The sentence a server without MRI-A6 earns. Pinned so every surface says it. */ declare const END_MEETING_NOT_SERVED = "Ending a meeting is not available on this server yet"; declare function createMeetingSession(options: MeetingSessionOptions): MeetingSession; declare function meetingSlug(random?: () => number): string; /** The link a host copies. ONE shape, so every surface produces the same URL. */ declare function meetingLink(baseUrl: string, slug: string): string; /** * RECORDING — LiveKit Egress, host-controlled, consent-enforced (R3/R5). * * 🚨 **CONSENT IS NOT OPTIONAL AND NOT A PROP.** There is no * `showRecordingIndicator={false}`. Recording a person without telling them is * illegal in a two-party-consent jurisdiction and is indefensible everywhere * else, so the indicator and the join-time notice are properties of the * package, not decisions a consumer gets to make. The only knob is the org's * `recordingPolicy` — disabled / host-controlled / always-on — which decides * whether recording may happen at all, never whether people are told. * * 🚨 **THE RECORDING BELONGS TO THE PLATFORM, NOT TO LIVEKIT.** The server * lands the artifact in the platform files system and returns a FILE ID. A * provider URL would be an orphan the day the room is garbage collected — the * reason `RecordingRecord.fileId` is not `url`. * * The client half is small on purpose: start/stop are aidream calls (the API * secret never comes near a browser), and the truth about a recording arrives * from the server's webhook through the room channel. This module never * *guesses* that a recording started. */ interface RecordingControllerOptions { /** The ONE org-aware aidream client (MRI-A16). Owns base url and headers. */ api: MeetApi; store: MeetStore; /** Announce the new state to everyone in the room (the indicator is for all). */ announce?: ((state: RecordingState, by: UserId | null) => void) | undefined; /** Persist that this participant saw the notice. Consent is RECORDED (R5). */ acknowledgeConsent?: ((meetingId: MeetingId) => Promise) | undefined; } interface RecordingController { /** True when the org policy and the actor's role allow starting a recording. */ canStart(meeting: MeetingRecord | null, isHost: boolean): boolean; start(args: { meetingId: MeetingId; roomName: RoomName; by: UserId; }): Promise; stop(args: { meetingId: MeetingId; by: UserId; }): Promise; /** The join-time notice text. One sentence, one place, every surface. */ consentNotice(meeting: MeetingRecord | null): string | null; acknowledge(meetingId: MeetingId): Promise; } declare function createRecordingController(options: RecordingControllerOptions): RecordingController; /** The indicator's words. One definition, so no surface invents a softer one. */ declare function recordingLabel(state: RecordingState): string | null; /** * MS — THE MEDIA SUPERVISOR (CORE-DESIGN §3.3, step S6c). * * Owns the media regions of §3.2 that are about THIS person's devices and speakers: * * - **Permission per kind.** The browser's error alone cannot tell a person's Block from an * operating-system refusal or a stale device id (`NotAllowedError` is all three in Chrome), so * every refusal is classified against the Permissions API: `denied` there is the person's (or * the site's) Block; a refusal while the browser says `granted` is the operating system. A kind * whose permission is `denied` reads `unavailable:denied` even when nothing tried to open it — * a camera the browser will not hand over is not "off by choice". * - **Live device switching.** LiveKit falls back to the default for a lost MICROPHONE by itself * but MUTES a lost camera (client-sdk-js #2035). On every device change the supervisor checks * the devices in use against the devices present: a vanished camera moves to the next camera; * a vanished microphone is reported with the device LiveKit moved to; nothing left of a kind is * `unavailable:none`. Each is a notice — never a silent switch. A NEW device is offered, never * taken (a person mid-sentence does not want their mic to jump to a headset being charged). * - **Autoplay.** `audio_output` = `blocked` while the browser holds remote audio until a gesture; * `enableAudio()` runs inside the person's click; the check is repeated when the tab comes back. * * 🚨 NOTHING HERE CHANGES THE CALL PHASE (§3.1 invariant). Every outcome is region state. * 🚨 NO BROWSER GLOBAL IS TOUCHED HERE: the browser half is the `MediaPlatform` port (§3.5) — * `webMediaPlatform()` below is the web adapter; a React Native host passes its own. */ type MediaKind = "microphone" | "camera"; type BrowserFamily = "chrome" | "edge" | "firefox" | "safari" | "other"; type OsFamily = "mac" | "windows" | "ios" | "android" | "linux" | "other"; interface PlatformDevice { readonly deviceId: string; readonly kind: string; readonly label: string; } /** The slice of §3.5's `MeetPlatform` the media supervisor needs. */ interface MediaPlatform { readonly browser: BrowserFamily; readonly os: OsFamily; /** The Permissions API reading, or `unknown` where the browser has none for this kind. */ queryPermission(kind: MediaKind): Promise; /** Call `onChange` whenever the permission for `kind` changes. Returns the unsubscribe. */ watchPermission(kind: MediaKind, onChange: (reading: PermissionReading) => void): () => void; listDevices(): Promise; /** The tab became visible again (autoplay is re-checked then). Returns the unsubscribe. */ onVisible(callback: () => void): () => void; } /** * The region reason for a refused device, from the error AND the permission store. * Pure — the rules are the tests. */ declare function classifyDeviceFailure(error: { readonly code: string; readonly message: string; }, permission: PermissionReading): DeviceUnavailableReason; /** The contract notice code for an unusable device (HARNESS-CONTRACT §4). */ declare function deviceNoticeCode(kind: MediaKind, reason: DeviceUnavailableReason): "mic-blocked" | "camera-blocked" | "device-lost"; /** * The region reason this track is in, or null when the device is usable (on, or off by choice). * A kind the browser's permission store denies is `denied` even when nothing tried to open it. */ declare function deviceUnavailableReason(track: LocalTrackState, permission: PermissionReading): DeviceUnavailableReason | null; /** The short chip text — the skin's words may differ; the reason is the fact. */ declare function deviceChipLabel(kind: MediaKind, reason: DeviceUnavailableReason): string; /** * THE REAL REMEDY, per browser and operating system: the steps a person actually follows. Short * lines (interface text is layout); the last step is always the Try again the screen offers. */ declare function deviceFixSteps(kind: MediaKind, reason: DeviceUnavailableReason, where: { readonly browser: BrowserFamily; readonly os: OsFamily; }): readonly string[]; declare function detectBrowser(userAgent: string): BrowserFamily; declare function detectOs(userAgent: string, maxTouchPoints?: number): OsFamily; /** The browser adapter, or null outside a browser (SSR, a test, React Native without one). */ declare function webMediaPlatform(): MediaPlatform | null; interface MediaSupervisorOptions { readonly store: MeetStore; readonly platform: MediaPlatform | null; /** The live room's driver, or null between rooms. */ readonly driver: () => MeetRoomDriver | null; readonly report: (level: "info" | "warn" | "error", message: string, remedy?: string) => void; } interface MediaSupervisor { /** Begin watching permissions and visibility (idempotent). */ start(): void; stop(): void; /** The `MediaUnavailable` for a refused device: the browser's words + the region reason. */ unavailable(error: unknown, kind: MediaKind, operation: string): Promise; /** Driver handlers (wired by the engine). */ onAudioPlayback(canPlay: boolean): void; onDevicesChanged(): void; onDeviceError(error: unknown, kind: "audioinput" | "videoinput" | null): void; /** The engine connected a room: remember which devices are in use now. */ onConnected(): void; /** The room is gone: autoplay and device notices belong to the room. */ onDisconnected(): void; /** "Enable sound" — call from inside the person's click. */ enableAudio(): Promise; /** * The pre-join preview's outcome for one kind: `prompting` when it asks the browser, `on` / `off` * when it settles or is turned off, or the error it was refused with (classified here, against * the permission store, exactly as an in-room refusal is). */ preview(kind: MediaKind, outcome: "prompting" | "on" | "off" | { readonly error: unknown; }): Promise; /** Use the offered new device. */ acceptOffer(): Promise; dismissOffer(): void; dismissNotice(): void; /** Undo on a "switched" notice: go back to the device that went away, when it is available again. */ undoNotice(): Promise; } declare function createMediaSupervisor(options: MediaSupervisorOptions): MediaSupervisor; interface TabClaim { /** This page's claimed tab id (resolves once the claim settles). */ readonly tabId: Promise; /** Announce that this tab is in `room` as `person` (until `left`); answers presence questions. */ inRoom(room: string, person: string, rule?: string | null): void; left(): void; /** Is `person` in `room` on ANOTHER tab of this browser? Answers within `TAB_ANSWER_MS`. */ presentElsewhere(room: string, person: string): Promise; /** * The same question, with the meeting's `second_device` rule the live tab holds — so the second * tab offers the PROFILE's choice (Meet: Switch here / Join here too; Teams: Transfer here / Add * this device) before anybody presses Join. Null when no other tab has the person live. */ presenceElsewhere?(room: string, person: string): Promise<{ readonly rule: string | null; } | null>; dispose(): void; } /** * THE ROOM ENGINE — the object a host mounts once and never reasons about. * * It owns every sequence a calling product gets wrong: ask for a token, connect, * project the roster, carry the in-room control plane, keep the phase honest * through a reconnect, and tear everything down exactly once. A consumer's job * is `engine.join(...)` and rendering a snapshot. * * THREE rules are structural here, not documented-and-hoped: * * 1. **The phase never lies.** `connecting` becomes `connected` only when the * driver says so, and any failure becomes `failed` WITH the remedy attached * to the snapshot. There is no path that leaves a spinner running forever — * the single most common complaint about every video product. * 2. **Role comes from the TOKEN, never from the client.** A participant's role * is read out of the server-minted metadata. A client that could decide it * was host could mute anyone; here the claim is unforgeable because the * server signed it. * 3. **Teardown is idempotent and total.** Leaving twice, or unmounting during * a connect, disconnects once and leaves no listener behind. The half-torn * room is what produces "my camera light stays on after the call". */ interface JoinRequest { readonly roomName: RoomName; readonly meetingId?: string | undefined; readonly slug?: string | undefined; /** `audio` joins with the camera off. Same room, same code path (D3). */ readonly mode: CallMode; /** * Whether to ATTEMPT the microphone on join. Defaults to true. `false` is a * deliberate listen-only join — the pre-join screen's "Join with microphone" * toggle. Either way the join itself never depends on the outcome; a refused * device lands in `snapshot.media.microphone.unavailable`, not in a throw. */ readonly microphone?: boolean | undefined; /** Meet wave 5: a breakout room of this meeting; absent/null = the main room. */ readonly breakoutRoomId?: string | null | undefined; /** HR-360 wave 2 (MD-16): ask to join as a silent observer. The server decides. */ readonly observe?: boolean | undefined; /** * S7: the person's answer when they are already in this meeting on another tab or device * (`snapshot.secondDevice`): `switch_here` moves the call here, `join_too` adds this tab as a * companion (no microphone, speaker off). */ readonly secondDevice?: SecondDeviceAnswer | undefined; } interface RoomEngineOptions { store: MeetStore; tokens: TokenClient; identity: MeetIdentity; driverFactory: RoomDriverFactory; /** S7: this page's tab claim (`core/tab-claim.ts`); absent = one connection per person. */ tabClaim?: TabClaim | undefined; /** Canonical public metadata read; room packets alone never authorize teardown. */ resolveMeeting?: ((slug: string) => Promise) | undefined; onDiagnostic?: ((event: { level: "info" | "warn" | "error"; message: string; remedy?: string; }) => void) | undefined; /** Fired when the local participant leaves for any reason, including kicked. */ onLeft?: ((reason: string | null) => void) | undefined; now?: (() => number) | undefined; /** Remembered noise suppression / background choice (Meet wave 2). */ effectsStorage?: EffectsStorage | undefined; /** * The device the person PICKED for each kind (the pre-join pickers, the * settings panel). Applied before any track is created — through 0.7.x the * pickers stored a choice nothing ever read, so every join used the browser * default whatever the screen said. */ resolveDevice?: ((kind: "audioinput" | "videoinput" | "audiooutput") => string | null) | undefined; /** * §3.4 EXIT-REASON RESOLUTION: on every LiveKit disconnect the engine asks the server's state * read what happened to THIS connection (`GET /v1/meet/state?connection=`). Resolves null when * the read failed (offline / unreachable), undefined when this room has no state read (a call). */ readExit?: ((connection: string, pass?: string | null) => Promise) | undefined; /** Was the page frozen when the connection dropped (Page Lifecycle)? → `inactive`. */ wasFrozen?: (() => boolean) | undefined; /** * S6b — the connection supervisor's ports. `network` = does the device have a network * (default: `navigator.onLine`); `probe` = do our API and the media server answer (default: * assume yes); `resume` = per-tab storage for the reload-after-offline marker (default: none). */ /** * S6c — the media supervisor's platform port (permissions, device list, visibility). Default: * the browser's (`webMediaPlatform()`); `null` turns the supervisor's browser reads off. */ mediaPlatform?: MediaPlatform | null | undefined; connection?: { readonly network?: NetworkPort | undefined; readonly probe?: ((serverUrl: string | null) => Promise) | undefined; readonly resume?: ResumeStorage | undefined; } | undefined; } interface RoomEngine { join(request: JoinRequest): Promise; /** * 🚨 THE HOST SAID YES — COME IN. * * A lobby token is minted with `canPublish: false` and `canSubscribe: false` * (that is what makes the lobby real rather than a screen over a live * microphone), so admission is not a UI state change: the cached token has to * be thrown away and a NEW one minted with the grants the server now gives. * Without this the waiting room's own sentence — "you will join automatically * once you are admitted" — is a lie, and the guest waits forever. */ rejoin(): Promise; /** * S6b: rejoin BY ITSELF from `disconnected` (the connection supervisor calls this once the * network and our servers answer). A fresh mint into the same identity: the server restores * role, hand and admission from the person row. */ autoRejoin(): Promise; /** * S6b: this tab was in (or dropped from) the call before the page reloaded — Chrome's offline * page reloading, a refresh. Returns true when it went straight back to joining (from * `resolving`), false when there is nothing fresh to resume (the caller resolves as usual). */ resume(roomName: RoomName): boolean; /** * Meet wave 5: go to a breakout room (`null` = back to the main room). The server * mints the pass (`/token` with `breakout_room_id`) and refuses a room the plan does * not allow; the move is a reconnect that never reads as leaving the meeting. */ moveToBreakout(roomId: string | null): Promise; leave(): Promise; /** The person's own "back to the device check" press from `left` / `disconnected`. */ backToPrejoin(): void; /** * S7: BEFORE Join — is this person already live in `roomName` on another tab of THIS browser * (`TabClaim.presentElsewhere`)? When yes the pre-join offers Switch here / Join here too up * front (the same choice the server would otherwise ask after the press). Returns whether it did. * A guest has no stable person key before the pass, so the server's own answer covers them. */ checkElsewhere(roomName: RoomName): Promise; /** Clear a refused-share notice (the `share` region) the person dismissed. */ dismissShareNotice(): void; setMicrophoneEnabled(enabled: boolean): Promise; setCameraEnabled(enabled: boolean): Promise; setScreenShareEnabled(enabled: boolean): Promise; react(emoji: string): Promise; /** * A PRIVATE message to one person (Meet wave 2). Data plane only, addressed * to that identity, NEVER saved — see `core/chat.ts` for the saved, * everyone-chat, which goes through the server. */ sendPrivateChat(to: ParticipantIdentity, text: string): Promise; /** * Change this participant's own media processing — noise suppression and the * camera background — on the live tracks, and remember it in this browser. * A refused effect is a diagnostic and a revert, never a lost camera. */ setEffects(next: Partial): Promise; /** Whether this browser can run a background effect at all (absent, never dead). */ supportsVideoEffects(): boolean; /** Whether this browser can share a screen at all — the control is ABSENT where it cannot. */ supportsScreenShare(): boolean; /** S6c: "Enable sound" — resume remote audio the autoplay policy holds. Call inside the click. */ enableAudio(): Promise; /** S6c: switch to the device the supervisor offered (a new one was plugged in). */ /** The pre-join preview's outcome for one kind (the media supervisor classifies a refusal). */ previewDevice(kind: "microphone" | "camera", outcome: "prompting" | "on" | "off" | { readonly error: unknown; }): Promise; acceptDeviceOffer(): Promise; dismissDeviceOffer(): void; dismissDeviceNotice(): void; undoDeviceNotice(): Promise; /** Choose the speaker (audio output) for this meeting. */ switchDevice(kind: "audioinput" | "videoinput" | "audiooutput", deviceId: string): Promise; pin(identity: ParticipantIdentity | null): void; setLayout(layout: StageLayout): void; attach(identity: ParticipantIdentity, kind: "camera" | "microphone" | "screen", element: HTMLMediaElement): () => void; token(): RoomToken | null; dispose(): Promise; } /** Where this browser keeps a participant's media choices. */ interface EffectsStorage { load(): MediaEffects; save(effects: MediaEffects): void; } /** * 🚨 THE SERVER'S WORD, ON TWO CARRIERS (MRI-A11, feedback `7418ef78`). * * aidream mints the role into the token's participant METADATA (a JSON blob) * AND as a LiveKit participant ATTRIBUTE. Both come out of one decision in * `services/meet/service.py`; neither can be written by a client. The attribute * is preferred because LiveKit delivers the attribute map on every roster * update, while a roster event can carry an empty metadata string — which is * precisely how, through 2026-09-08, EVERY participant projected as * "participant" and no host ever saw a host control in a browser. * * These strings are pinned on the server side too: `test_meet_service.py` reads * them out of THIS file and fails if the two sides drift. */ declare const PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role"; /** S8: the server's raise time of a hand (epoch ms), "" when down. Pinned with aidream's `room_state.ATTRIBUTE_HAND`. */ declare const PARTICIPANT_HAND_ATTRIBUTE = "matrx.hand"; declare const PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record"; /** Standing travels beside the role (CORE-DESIGN §2.1). Pinned by aidream's tests too. */ declare const PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing"; /** * Metadata is SERVER-MINTED and therefore trusted; it is still parsed * defensively, because a malformed blob must degrade a tile to "participant" * rather than throw inside a roster event and freeze the whole grid. */ declare function projectParticipant(driver: DriverParticipant, joinedAt: string): MeetParticipant; declare function createRoomEngine(options: RoomEngineOptions): RoomEngine; /** Exported for the host-power check in UI code — one definition, one rank. */ declare function canModerate(participant: MeetParticipant | undefined): boolean; declare function hostUserIds(participants: readonly MeetParticipant[]): readonly UserId[]; /** * CS — THE CONNECTION SUPERVISOR (CORE-DESIGN §3.3, step S6b). * * The one owner of "is this tab still connected, and if not, why, and what happens next". It * never touches a browser global itself — the network signal and the reachability probe arrive * as ports (`NetworkPort`, `probe`), with web implementations below for the provider to hand in — * and it never moves the phase except through `store.dispatch` (via the engine's `autoRejoin`). * * What it decides: * * 1. **The `connection` region** (§3.2): `stable · signal_reconnecting · reconnecting · offline · * server_unreachable`. `offline` = the device has no network (`navigator.onLine` false); * `server_unreachable` = online, but our API or the media server does not answer the probe. * The two get different screens because they have different fixes. * 2. **The banner debounce** (`reconnect_banner_delay_ms`): the region is honest at once (the * contract attribute), the banner appears only if the blip outlasts the delay (`net-blip`). * 3. **Auto-rejoin after the SDK gives up** (`auto_rejoin`, `auto_rejoin_max_attempts`): from * `disconnected`, when the network is back AND the probe answers, rejoin with a fresh mint into * the same identity — the server restores role / hand / admission from the person row. Attempts * are only spent while reachable; a long offline spell costs none. Manual Rejoin always works. * 4. **The reload-after-offline marker**: while in the call (or dropped from it) the tab keeps a * per-tab marker, so a page that Chrome replaced with its own offline page and then reloaded * comes back INTO the call instead of to the pre-join (`resume`). A deliberate exit clears it. * 5. **The jittered reconnect policy** handed to LiveKit (`createReconnectPolicy`): jitter from * attempt 0 (the SDK's default has none on its first two attempts — 650 people reconnecting * on the same millisecond is the thundering-herd issue), with the give-up limits as options. * * Others' drops (`net-others-see-drop`) are the pure ghost helpers at the bottom: an unannounced * departure keeps a `reconnecting` tile for `reconnect_grace_seconds`. */ /** The server-delivered connection knobs (CORE-DESIGN §4.2/§4.3), resolved per meeting. */ interface ConnectionPolicy { /** `reconnect_banner_delay_ms`: how long a drop must last before the banner shows. */ readonly bannerDelayMs: number; /** `auto_rejoin`: rejoin by itself after the SDK gives up. */ readonly autoRejoin: boolean; /** `auto_rejoin_max_attempts`: rejoin attempts (while reachable) before only Rejoin is left. */ readonly autoRejoinMaxAttempts: number; /** `reconnect_grace_seconds`: how long others keep a dropped person's `reconnecting` tile. */ readonly reconnectGraceSeconds: number; /** * `video_auto_off_after_poor_seconds`: a poor path that lasts this long pauses the person's * camera so their audio survives (`net-video-auto-off`); 0 = never. */ readonly videoAutoOffAfterPoorSeconds: number; /** * `captions_start_timeout_seconds`: how long "Starting captions" may last before the person is * told, honestly, that captions are unavailable (and offered Try again). */ readonly captionsStartTimeoutSeconds: number; } /** The knob defaults (§4.3) — what an older server that sends nothing gets. */ declare const DEFAULT_CONNECTION_POLICY: ConnectionPolicy; /** The token's `connection_policy` object → the typed policy; anything missing is the default. */ declare function connectionPolicyFrom(raw: unknown): ConnectionPolicy; /** LiveKit's `ReconnectPolicy` shape, restated so this module never imports the SDK. */ interface ReconnectPolicyLike { nextRetryDelayInMs(context: { readonly retryCount: number; readonly elapsedMs: number; }): number | null; } interface ReconnectPolicyOptions { /** Base delay per attempt (ms); the last one repeats. Default: LiveKit's own schedule. */ readonly delaysMs?: readonly number[]; /** Stop after this long since the drop (ms). Default 45 000 — about the SDK's own ~44 s. */ readonly giveUpMs?: number; /** Stop after this many attempts. Default 12. */ readonly maxAttempts?: number; readonly random?: () => number; } /** livekit-client 2.22 `DEFAULT_RETRY_DELAYS_IN_MS` (verified in the SDK source, 2026-10-08). */ declare const LIVEKIT_BASE_DELAYS_MS: readonly number[]; /** * THE JITTERED RECONNECT POLICY. Every attempt — attempt 0 included — gets a random spread of at * least 500 ms (half the base delay, capped at 2 s), so a room whose SFU blinks does not hear * every client come back on the same millisecond. */ declare function createReconnectPolicy(options?: ReconnectPolicyOptions): ReconnectPolicyLike; /** Whether the device has a network, and when that changes (the platform port's `network`). */ interface NetworkPort { online(): boolean; subscribe(listener: (online: boolean) => void): () => void; } /** `navigator.onLine` + `online`/`offline` events; outside a browser it is always online. */ declare function createWebNetworkPort(): NetworkPort; /** * THE REACHABILITY PROBE: does each URL answer at all? An opaque (`no-cors`) GET resolves for any * HTTP answer and rejects only when the network path is down — exactly the question, with no * CORS dependency. All URLs must answer; a timeout is a "no". */ declare function createReachabilityProbe(urls: () => readonly string[], timeoutMs?: number): () => Promise; /** `wss://x.livekit.cloud` → `https://x.livekit.cloud` (what the probe can GET). */ declare function httpUrlOf(serverUrl: string): string; /** Per-tab storage for the reload-after-offline marker (session storage on the web). */ interface ResumeStorage { load(roomName: string): ResumeMarker | null; save(roomName: string, marker: ResumeMarker): void; clear(roomName: string): void; } interface ResumeMarker { readonly request: JoinRequest; /** Epoch ms of the last time this tab was in (or dropped from) the call. */ readonly at: number; } /** How long after the last in-call moment a reload still goes straight back in. */ declare const RESUME_WINDOW_MS: number; declare function createWebResumeStorage(): ResumeStorage | undefined; interface ConnectionSupervisorOptions { readonly store: MeetStore; readonly network: NetworkPort; /** Do our API and the media server answer? */ readonly probe: () => Promise; readonly policy: () => ConnectionPolicy; /** Rejoin from `disconnected` with a fresh mint (the engine's `autoRejoin`). */ readonly autoRejoin: () => Promise; /** The request to resume with (current media state folded in), or null. */ readonly currentRequest: () => JoinRequest | null; readonly resume?: ResumeStorage | undefined; readonly now?: () => number; readonly random?: () => number; /** How often to re-probe while not stable (ms). Default 3000. */ readonly probeEveryMs?: number; readonly report?: (level: "info" | "warn", message: string) => void; } interface ConnectionSupervisor { /** LiveKit's connection state, as the driver reports it. */ driverState(state: DriverConnectionState): void; /** The region to show beside `disconnected` when the server's state read failed. */ exitConnection(): Exclude; /** The resume marker for `roomName` if it is fresh, else null (and a stale one is cleared). */ resumable(roomName: string): ResumeMarker | null; dispose(): void; } /** Backoff before auto-rejoin attempt `n` (0-based): 1 s, 2 s, 4 s … capped at 15 s, jittered ±50%. */ declare function autoRejoinDelayMs(attempt: number, random?: () => number): number; declare function createConnectionSupervisor(options: ConnectionSupervisorOptions): ConnectionSupervisor; declare function isUnannouncedDeparture(reason: string | null): boolean; interface Ghost { readonly participant: DriverParticipant; readonly until: number; } /** * The roster with the live ghosts merged in. A ghost whose identity is back in the live roster is * dropped (the person returned — one tile, never a copy); an expired one is dropped. */ declare function mergeGhosts(live: readonly DriverParticipant[], ghosts: ReadonlyMap, at: number): { readonly roster: readonly (DriverParticipant & { readonly reconnecting?: boolean; })[]; readonly kept: Map; }; /** * THE TOKEN FLOW — the ONLY authorization path into a room (R1). * * 🚨 **THE CLIENT NEVER HOLDS A LIVEKIT SECRET.** Not in an env var, not in a * build-time constant, not "just for local dev". A LiveKit API secret in a * browser bundle mints a token for ANY room in the project, including one the * holder was never invited to — it is a full compromise of every meeting in the * organization. This package therefore has no code path that could accept one: * it asks aidream, and aidream mints. * * Four hard parts live here so no host reimplements them (C22): * * 1. **Single flight.** A React strict-mode double effect, a re-render, and a * reconnect all ask at once; they share ONE request. Two tokens for one * identity is not harmless — LiveKit disconnects the older session, which * looks exactly like "my call randomly drops when I open the tab". * 2. **Expiry is honored BEFORE it bites.** A token is refreshed at 80% of its * life, not when a reconnect fails at 100%. A meeting that outlives its * token and cannot reconnect is the classic 2-hour-meeting bug. * 3. **Guests are first-class, not an exception.** A guest has no session and * the request carries a display name instead; the SERVER decides what a * guest may do. There is no client-side "is this person allowed" branch. * 4. **A missing session is a lifecycle moment.** Between sign-in and token * arrival, the fetch retries ONCE after the host's session resolves and then * reports `session-unavailable` as a warning — never a red error, never a * retry storm. */ interface RoomToken { readonly token: string; readonly serverUrl: string; readonly roomName: RoomName; /** S7: the PERSON key (`user:` · `guest:`) — what every roster tile and row uses. */ readonly identity: ParticipantIdentity; /** S7: this tab's LiveKit identity, `~` (the person key from an older server). */ readonly connectionIdentity?: string | undefined; /** S7: `companion` for a "join here too" connection (no audio publish, speaker off). */ readonly connectionKind?: "main" | "companion" | undefined; /** * S7: set when the person is ALREADY in the room on another tab or device and has not chosen * yet — the meeting's `second_device` rule (`ask_switch_or_join` · `ask_transfer_or_add`). No * LiveKit token was issued (`token` is empty); the pre-join offers the choice. */ readonly secondDeviceChoice?: string | null | undefined; /** S7: the meeting's `second_device` rule, always — told to a second tab of this browser. */ readonly secondDeviceRule?: string | null | undefined; readonly displayName: string; /** ISO. The package refreshes at 80% of the remaining life. */ readonly expiresAt: string; /** What the SERVER granted. The client renders from this; it never decides. */ readonly canPublish: boolean; readonly canSubscribe: boolean; readonly canRecord: boolean; readonly isHost: boolean; /** * 🚨 THE SERVER'S WORD ON WHO THIS JOINER IS (MRI-A11) — `host · cohost · presenter · * participant · viewer` (CORE-DESIGN §2.1). The same string is minted into the LiveKit * token's participant metadata and its `matrx.participant.role` attribute. */ readonly role: ParticipantRole; /** Where this joiner stands with the meeting — beside the role, never inside it. */ readonly standing: ParticipantStanding | null; /** * 🚨 THE MEETING PASS (CORE-DESIGN §2.5) — the credential for every aidream meet route, * sent as `x-meet-pass`. It is NOT the LiveKit token (`token`), which is for the media * connection only. Null for an ad-hoc call room (no meeting) or an older server. */ readonly meetingPass: string | null; readonly meetingPassExpiresAt: string | null; /** The run (`meet_sessions.id`) this join belongs to; null while waiting before any run. */ readonly sessionId: string | null; /** True when the server put this joiner in the lobby (D6). */ readonly lobby: boolean; /** * S4: what the mint wrote on the person row — `admitted` · `knocking` · `waiting_host`. * A waiting person then reads `GET /v1/meet/state` every `pollSeconds`. */ readonly admissionState?: AdmissionState | undefined; readonly pollSeconds?: number | undefined; /** * Meet wave 5: the breakout room this pass is for, or null for the main room. The * pass is still keyed by the MEETING's room name — it is this person's pass for the * meeting, and the server accepts a breakout pass wherever the meeting's is asked. */ readonly breakoutRoomId?: string | null | undefined; /** HR-360 wave 2 (MD-16): who sees the "Observing" line in People (knob). */ readonly observersVisibleTo?: ObserversVisibleTo | undefined; /** * S6b: the meeting's connection knobs (`reconnect_banner_delay_ms`, `auto_rejoin`, * `auto_rejoin_max_attempts`, `reconnect_grace_seconds`), resolved by the server's policy * ladder. Absent from an older server → the knob defaults. */ readonly connectionPolicy?: ConnectionPolicy | undefined; /** S8: what the host's Leave offers for this meeting (`host_leave_prompt`, `end_confirm`). */ readonly leavePolicy?: LeavePolicy | undefined; /** host-reclaim (`host_reclaim = prompt`): the returning owner may take the host powers back. */ readonly canReclaim?: boolean | undefined; } interface TokenClientOptions { /** The ONE org-aware aidream client (MRI-A16). Owns base url and headers. */ api: MeetApi; /** Injected clock, so expiry logic is provable without waiting two hours. */ now?: (() => number) | undefined; /** * Where a GUEST's pass survives a page close (MRF-1). Absent means passes * live only in memory, which is correct for a member (they have a session to * mint a new one from) and is why this is optional rather than assumed. */ storage?: RoomTokenStorage | undefined; } /** * 🚨 **WHY A GUEST'S PASS MUST OUTLIVE THE TAB (MRF-1).** * * A guest has no session, so their only credential for a meeting is the room * token this server minted for them. After the meeting ends, `/token` refuses * to mint another one — correctly: the room is gone and nobody may rejoin it. * So a guest who closes the tab and comes back to read the summary has, in a * purely in-memory client, *no way to prove they were ever there* — and the one * honest thing left to show them is a refusal. * * Persisting the pass is what makes the durable link durable for them. It is * per-device by nature (it is stored beside the device id that minted it) and * it expires on its own: `meet.room_token_ttl_seconds` is the organization's * setting for how long, and an expired pass produces the renew-the-link * sentence, never "you were never here". */ interface RoomTokenStorage { read(roomName: RoomName): string | null; write(roomName: RoomName, serialized: string): void; remove(roomName: RoomName): void; } /** `localStorage` when it exists; a no-op that SAYS so otherwise. */ declare function createWebRoomTokenStorage(onDiagnostic?: (message: string, remedy: string) => void): RoomTokenStorage; /** * The meeting pass a GUEST's browser kept for a room (MRF-1), or null — read from the same * storage `createWebRoomTokenStorage` writes. Used by "keep your notes" after sign-up. */ declare function rememberedMeetingPass(roomName: RoomName, storage?: RoomTokenStorage): string | null; interface TokenRequest { readonly roomName: RoomName; readonly identity: MeetIdentity; /** Present for a meeting join; absent for an ad-hoc call room. */ readonly meetingId?: string | undefined; /** The public meeting slug, when joining by link without knowing the id. */ readonly slug?: string | undefined; /** Meet wave 5: a breakout room of this meeting; absent = the main room. */ readonly breakoutRoomId?: string | null | undefined; /** HR-360 wave 2 (MD-16): ask to join as a silent observer. The server decides. */ readonly observe?: boolean | undefined; /** S7: this tab's claimed id (`core/tab-claim.ts`) — the LiveKit identity is `~`. */ readonly tabId?: string | undefined; /** S7: the person's answer when they are already in this meeting on another tab or device. */ readonly secondDevice?: SecondDeviceAnswer | undefined; } /** S7: what a person already in the meeting elsewhere chose (CORE-DESIGN §2.1). */ type SecondDeviceAnswer = "switch_here" | "join_too"; interface TokenClient { /** A currently-valid token, minting or refreshing only when it must. */ get(request: TokenRequest): Promise; /** Drop the cached token for a room (after a `denied`, or on leave). */ invalidate(roomName: RoomName): void; /** True when the cached token is inside its refresh window. */ needsRefresh(roomName: RoomName): boolean; /** * The pass this DEVICE holds for a room, **including an expired one** (MRF-1). * * 🚨 The expired case is the whole reason this returns the token instead of * `null`: "your pass expired, open the link again" and "you were never in * this meeting" are different sentences, and only the caller that can see the * expiry can say the right one. Never used to join — `get` mints for that. */ remembered(roomName: RoomName): RoomToken | null; } /** The aidream routes this package calls. ONE place, so a rename is one edit. */ /** * EVERY aidream ROUTE THIS PACKAGE CALLS, in one object. * * This is the contract in both directions: aidream carries a test * (`services/meet/tests/test_meet_service.py`) that reads THIS object out of * this source file and fails if the server stops serving one of these paths. * The package is the spec. * * `meetingEnd` is pinned here BEFORE the server serves it (MRI-A6 owns landing * it). That is deliberate and it is not a lie on screen: the control that calls * it renders, and a 404 from a server that has not landed A6 is reported as the * honest sentence "Ending a meeting is not available on this server yet" — * never a dead button, never a spinner, and never a silent no-op. */ declare const MEET_ROUTES: { readonly token: "/api/v1/meet/token"; readonly recordingStart: "/api/v1/meet/recording/start"; readonly recordingStop: "/api/v1/meet/recording/stop"; readonly noteTakerStart: "/api/v1/meet/agent/start"; readonly noteTakerStop: "/api/v1/meet/agent/stop"; /** Anyone in the meeting (guests with the room pass included) asks for captions for themselves. */ readonly noteTakerCaptions: "/api/v1/meet/agent/captions"; /** In-meeting Q&A. Streams NDJSON; body is `{meeting_id, question}` only. */ readonly intelligenceAsk: "/api/v1/meet/intelligence/ask"; /** A host ends the meeting for everyone. Served by MRI-A6. */ readonly meetingEnd: "/api/v1/meet/end"; /** * THE POST-MEETING RECORD FOR A READER WITH NO DATABASE (MRF-1). A `GET`, * authorized by the room token this device already holds, carried in the * `x-meet-pass` header — never a query parameter, because a bearer * credential in a URL lands in access logs, referrers and history. */ readonly record: "/api/v1/meet/record"; /** A host chooses what guests may read of ONE meeting (MRF-1). */ readonly recordAccess: "/api/v1/meet/record/access"; /** * KEEP YOUR NOTES (2026-10-01): a guest who just created an account presents * their kept pass in `x-meet-pass` with their new session, and the * meeting they attended becomes theirs to read. Idempotent; never a grant. */ readonly claim: "/api/v1/meet/claim"; /** * Meet wave 1 (0.8): bring every invitee up to date after a write — sends the * invitation / update / cancellation emails with their `.ics` and re-queues * reminders. Idempotent. `{meeting_id}` is substituted by `meetingPath`. */ readonly meetingAnnounce: "/api/v1/meet/meetings/{meeting_id}/announce"; /** Put a one-off meeting on the caller's own Google Calendar (gated by the server). */ readonly meetingGoogleCalendar: "/api/v1/meet/meetings/{meeting_id}/google-calendar"; /** * Meet wave 3: the recap email after the meeting. `GET` = the draft (subject, * body from the wrap-up, who it can go to); `POST` = send what the host * reviewed (`dry_run` renders and queues nothing). Never sent automatically. */ readonly meetingFollowUp: "/api/v1/meet/meetings/{meeting_id}/follow-up"; /** * Meet wave 2: EVERY in-meeting host power (mute, stop video, mute all, ask to * unmute, spotlight, lower hands, co-host, hand over, rename, remove, report). * Decided by the server from the meeting itself; a guest's pass rides * `x-meet-pass`. */ readonly moderate: "/api/v1/meet/moderate"; /** Meet wave 2: the SAVED chat — `POST` sends to everyone, `GET ?meeting_id=` reads history. */ readonly chat: "/api/v1/meet/chat"; /** * Meet wave 5: polls, Q&A, the shared whiteboard and breakout rooms. `POST` acts * (`{meeting_id, action, …}`), `GET ?meeting_id=` reads what this viewer may see. */ readonly collab: "/api/v1/meet/collab"; /** Meet wave 5: translate finished caption lines into this viewer's language. */ readonly captionsTranslate: "/api/v1/meet/captions/translate"; /** * S4 — THE STATE READ (CORE-DESIGN §2.6): `GET ?meeting_id=` with `x-meet-pass`. This * person's admission (lazy expiry applied), whether anyone who can admit is here, the run's * phase. Read every `waiting_poll_seconds` while waiting and on every `mx.state.changed`. */ readonly state: "/api/v1/meet/state"; /** S4: `withdraw` (stop waiting) or `leave` (close this tab); the pass may ride in the body. */ readonly leave: "/api/v1/meet/leave"; /** * S8: a host's browser just called an admission DOOR (`meet_admit` · `meet_deny` · * `meet_undeny` · `meet_admit_all`). Payload-free: the server republishes the room's * replicated state (the People badge's waiting count) from the database truth. */ readonly admissionChanged: "/api/v1/meet/admission/changed"; /** S4: a pass holder (guests included) saw the recording notice, for this run. */ readonly consent: "/api/v1/meet/consent"; }; /** A `MEET_ROUTES` template with its `{meeting_id}` filled in, URL-encoded. */ declare function meetingPath(template: string, meetingId: string): string; /** The header the meeting pass rides on (CORE-DESIGN §2.5). Pinned on both sides. */ declare const MEETING_PASS_HEADER = "x-meet-pass"; /** The `x-meet-pass` header for a held token, or no header at all. */ declare function meetingPassHeaders(token: RoomToken | null | undefined): Record; declare function parseRoomToken(body: unknown, roomName: RoomName): RoomToken; declare function createTokenClient(options: TokenClientOptions): TokenClient; /** * THE IN-ROOM SERVER CALLS (Meet wave 2) — host moderation and the SAVED chat. * * 🚨 **Why these are server calls and not data packets.** Through 0.7.x a host's * "Mute" and "Remove" were published on the room's data plane (`mx.host`) and * nothing anywhere listened: the buttons did nothing. A client cannot be the * authority on host power, and a guest cannot write the database. So both go to * aidream (`POST /v1/meet/moderate`, `POST|GET /v1/meet/chat`), which decides the * actor's role from the meeting itself, performs the act through LiveKit's server * API, writes what is durable, and tells the room on its own `matrx.meet.*` topic. * * A guest proves who they are with the meeting pass this server signed for them, in * `x-meet-pass` — a header, never a URL. */ /** Every host power the server performs. One vocabulary with `services/meet/moderation.py`. */ type ModerationAction = "mute" | "stop_video" | "stop_screen" | "mute_all" | "ask_unmute" | "ask_all_unmute" | "spotlight" | "sync_presenter" | "raise_hand" | "lower_hand" | "lower_all_hands" | "make_cohost" | "make_presenter" | "revoke_presenter" | "set_av_lock" | "revoke_cohost" | "transfer_host" | "reclaim_host" | "rename" | "remove" | "report"; declare const MODERATION_ACTIONS: readonly ModerationAction[]; interface ModerationRequest { readonly action: ModerationAction; readonly identity?: ParticipantIdentity | null | undefined; readonly name?: string | undefined; readonly value?: string | undefined; readonly reason?: string | undefined; } interface ModerationResult { readonly action: ModerationAction; readonly affected: readonly string[]; /** The server's sentence for the actor. */ readonly message: string; } interface InRoomClient { moderate(request: ModerationRequest): Promise; /** Send a message to EVERYONE. Saved; shows at once as pending, then as saved. */ sendChat(text: string): Promise; /** Read the saved chat once per join, so a late joiner sees what was said. */ loadChat(): Promise; /** * Meet wave 5: read the polls, questions, whiteboard and breakout plan this viewer * may see, once per join. A refusal is written to `collab.error`, never thrown away. */ loadCollab(): Promise; /** * Meet wave 5: one poll / Q&A / whiteboard / breakout action. The server's answer is * folded into the store at once (the room packet that follows is the same item). * Returns the server's sentence; throws a `MeetError` with the server's words. */ collab(request: CollabRequest): Promise; /** * Meet wave 5: translate finished caption lines into `language` for this viewer. * Results land in `collab.captionTranslations`; a refusal in `captionTranslationError`. */ translateCaptions(language: string, lines: readonly { id: string; text: string; }[]): Promise; } /** Every room-item action the server performs. One vocabulary with `services/meet/collab.py`. */ 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" /** HR-360 wave 2 (MD-15): move everyone's app panel to one section. */ | "focus_set"; interface CollabRequest { readonly action: CollabAction; readonly itemId?: string | undefined; readonly question?: string | undefined; readonly options?: readonly string[] | undefined; readonly multiple?: boolean | undefined; readonly anonymous?: boolean | undefined; readonly optionIds?: readonly string[] | undefined; readonly text?: string | undefined; readonly stroke?: { readonly points: readonly (readonly [number, number])[]; readonly color: string; readonly width: number; readonly tool: "pen" | "highlighter"; } | undefined; readonly rooms?: readonly { readonly name: string; readonly identities: readonly string[]; }[] | undefined; readonly allowChoose?: boolean | undefined; readonly minutes?: number | undefined; readonly identity?: string | undefined; /** `null` = the main room (breakout_join / breakout_move). */ readonly roomId?: string | null | undefined; /** focus_set: the app panel's key and the section everyone looks at. */ readonly panel?: string | undefined; readonly section?: string | undefined; } interface CollabResult { readonly action: CollabAction; readonly message: string; readonly removed: readonly string[]; /** The id of the item the server returned (a created poll, a new question), or null. */ readonly itemId: string | null; } interface InRoomClientOptions { api: MeetApi; store: MeetStore; /** The room pass for THIS meeting, or null (a signed-in member needs none). */ resolveRoomToken: () => string | null; now?: (() => number) | undefined; } declare function createInRoomClient(options: InRoomClientOptions): InRoomClient; export { type DriverTrackKind as $, type AskArgs as A, type BrowserFamily as B, type CallCenter as C, type DeviceManager as D, type CreateCallInviteInput as E, type CreateMeetingInput as F, DATA_TOPICS as G, DEFAULT_CONNECTION_POLICY as H, type InRoomClient as I, type JoinRequest as J, DEFAULT_EFFECTS as K, type LeaveChoice as L, type MeetStore as M, type NoteTakerStatus as N, DEFAULT_LEAVE_POLICY as O, type DataTopic as P, type DeviceKind as Q, type RoomEngine as R, type ShareRegion as S, type TokenClient as T, type DeviceManagerOptions as U, type DeviceNotice as V, type DeviceOffer as W, type DeviceStorage as X, type DeviceUnavailableReason as Y, type DriverConnectionState as Z, type DriverParticipant as _, type MeetingSession as a, type PhaseEvent as a$, type DriverTranscription as a0, END_MEETING_NOT_SERVED as a1, type EffectsStorage as a2, type EndMeetingResult as a3, type ExitInput as a4, type ExitPhase as a5, type ExitResolution as a6, type ExitServerWord as a7, type Ghost as a8, type HostLeavePrompt as a9, type MediaUnavailable as aA, type MeetAiAnswer as aB, type MeetAiOptions as aC, type MeetDevice as aD, MeetError as aE, type MeetErrorCode as aF, type MeetGrants as aG, type MeetPatch as aH, type MeetRoomDriver as aI, type MeetingSearchHit as aJ, type MeetingSearchSource as aK, type MeetingSessionOptions as aL, type ModerationAction as aM, NOTE_TAKER_IDENTITY_PREFIX as aN, NOTE_TAKER_METADATA_KEY as aO, NO_GRANTS as aP, NO_SHARE as aQ, type NameBook as aR, type NetworkPort as aS, type NoteTakerDispatch as aT, type NoteTakerRefusal as aU, type OsFamily as aV, PARTICIPANT_CAN_RECORD_ATTRIBUTE as aW, PARTICIPANT_HAND_ATTRIBUTE as aX, PARTICIPANT_ROLE_ATTRIBUTE as aY, PARTICIPANT_STANDING_ATTRIBUTE as aZ, type PermissionReading as a_, IDLE_MEDIA as aa, IDLE_MEDIA_REGION as ab, IDLE_PHASES as ac, IDLE_TRACK as ad, type InRoomClientOptions as ae, JOIN_REFUSAL_REASONS as af, type JoinRefusalReason as ag, LISTEN_ONLY_NOTICE as ah, LIVEKIT_BASE_DELAYS_MS as ai, LIVE_PHASES as aj, type LibraryRecording as ak, type LocalTrackState as al, MEETING_PASS_HEADER as am, MEET_ANSWER_CONTEXT_EVENT as an, MEET_ANSWER_EVENTS as ao, MEET_ROUTES as ap, MEET_RPCS as aq, MEET_SCHEMA as ar, MEET_TABLES as as, MODERATION_ACTIONS as at, type MediaDevicesLike as au, type MediaKind as av, type MediaPlatform as aw, type MediaRegionState as ax, type MediaSupervisor as ay, type MediaSupervisorOptions as az, type RecordingController as b, isLivePhase as b$, type PhaseEventType as b0, type PlatformDevice as b1, REACTION_TTL_MS as b2, RESUME_WINDOW_MS as b3, type ReconnectPolicyLike as b4, type ReconnectPolicyOptions as b5, type RecordingControllerOptions as b6, type RepositoryOptions as b7, type ResumeMarker as b8, type ResumeStorage as b9, createMeetAi as bA, createMeetRepository as bB, createMeetStore as bC, createMeetingSession as bD, createMemoryDeviceStorage as bE, createReachabilityProbe as bF, createReconnectPolicy as bG, createRecordingController as bH, createRoomEngine as bI, createTokenClient as bJ, createWebDeviceStorage as bK, createWebNetworkPort as bL, createWebResumeStorage as bM, createWebRoomTokenStorage as bN, detectBrowser as bO, detectOs as bP, deviceChipLabel as bQ, deviceFixSteps as bR, deviceNoticeCode as bS, deviceUnavailableReason as bT, displayNameFor as bU, hostUserIds as bV, httpUrlOf as bW, invalidMeetResponse as bX, isGuestIdentity as bY, isJoinRefusalReason as bZ, isListenOnly as b_, type RoomChatMessage as ba, type RoomDriverHandlers as bb, type RoomEngineOptions as bc, type RoomReaction as bd, type RoomToken as be, type RoomTokenStorage as bf, type ScheduleMeetingInput as bg, TRANSCRIPTION_ATTRIBUTES as bh, TRANSCRIPTION_TOPIC as bi, type TokenClientOptions as bj, type TokenRequest as bk, type VideoEffect as bl, WAITING_PHASES as bm, admissionPhase as bn, autoRejoinDelayMs as bo, canModerate as bp, canModerateParticipant as bq, canTransition as br, changesPatch as bs, classifyDeviceFailure as bt, connectionPolicyFrom as bu, createCallCenter as bv, createConnectionSupervisor as bw, createDeviceManager as bx, createInRoomClient as by, createMediaSupervisor as bz, type MeetRepository as c, isNoteTaker as c0, isRealName as c1, isRefusedPhase as c2, isUnannouncedDeparture as c3, isWaitingPhase as c4, learnName as c5, learnNames as c6, leaveChoices as c7, leavePolicyFrom as c8, listenOnlyNotice as c9, refusalPhase as cA, rememberedMeetingPass as cB, resolveExit as cC, roleOf as cD, settingsPatch as cE, snippetRuns as cF, unavailableFrom as cG, webMediaPlatform as cH, meetingLink as ca, meetingPassHeaders as cb, meetingPath as cc, meetingSlug as cd, mergeGhosts as ce, nextPhase as cf, normalizeDeviceError as cg, normalizeMeetError as ch, noteTakerIdentity as ci, noteTakerLabel as cj, noteTakerParticipant as ck, noteTakerStatus as cl, parseRoomToken as cm, projectInvitee as cn, projectLibraryRecording as co, projectNote as cp, projectOccurrence as cq, projectOverride as cr, projectParticipant as cs, projectRsvpInvitation as ct, projectSearchHit as cu, projectSegment as cv, projectUpcoming as cw, readRefusalBody as cx, reasonOf as cy, recordingLabel as cz, type MeetAi as d, type RoomDriverFactory as e, type DeviceSnapshot as f, type MeetSnapshot as g, type ModerationRequest as h, type ModerationResult as i, type MediaEffects as j, type MeetAnswerCoverage as k, type MeetPhase as l, type LeavePolicy as m, type LocalMediaState as n, type ModerationNotice as o, type CollabRequest as p, type ConnectionRegion as q, type AudioProcessing as r, CONSENT_NOTICE_ATTRIBUTE as s, type CallCenterOptions as t, type CallOutcomePhase as u, type CollabAction as v, type CollabResult as w, type ConnectionPolicy as x, type ConnectionSupervisor as y, type ConnectionSupervisorOptions as z };