import { M as MeetStore, C as CallCenter, c as MeetRepository, bb as RoomDriverHandlers, aI as MeetRoomDriver, Z as DriverConnectionState, _ as DriverParticipant, aR as NameBook, be as RoomToken, ba as RoomChatMessage, a2 as EffectsStorage, p as CollabRequest, w as CollabResult, g as MeetSnapshot, l as MeetPhase, q as ConnectionRegion, al as LocalTrackState, a_ as PermissionReading } from './in-room-B9pch08I.js'; export { A as AskArgs, r as AudioProcessing, B as BrowserFamily, s as CONSENT_NOTICE_ATTRIBUTE, t as CallCenterOptions, u as CallOutcomePhase, v as CollabAction, x as ConnectionPolicy, y as ConnectionSupervisor, z as ConnectionSupervisorOptions, E as CreateCallInviteInput, F as CreateMeetingInput, G as DATA_TOPICS, H as DEFAULT_CONNECTION_POLICY, K as DEFAULT_EFFECTS, O as DEFAULT_LEAVE_POLICY, P as DataTopic, Q as DeviceKind, D as DeviceManager, U as DeviceManagerOptions, V as DeviceNotice, W as DeviceOffer, f as DeviceSnapshot, X as DeviceStorage, Y as DeviceUnavailableReason, $ as DriverTrackKind, a0 as DriverTranscription, a1 as END_MEETING_NOT_SERVED, a3 as EndMeetingResult, a4 as ExitInput, a5 as ExitPhase, a6 as ExitResolution, a7 as ExitServerWord, a8 as Ghost, a9 as HostLeavePrompt, aa as IDLE_MEDIA, ab as IDLE_MEDIA_REGION, ac as IDLE_PHASES, ad as IDLE_TRACK, I as InRoomClient, ae as InRoomClientOptions, af as JOIN_REFUSAL_REASONS, ag as JoinRefusalReason, J as JoinRequest, ah as LISTEN_ONLY_NOTICE, ai as LIVEKIT_BASE_DELAYS_MS, aj as LIVE_PHASES, L as LeaveChoice, m as LeavePolicy, ak as LibraryRecording, n as LocalMediaState, am as MEETING_PASS_HEADER, an as MEET_ANSWER_CONTEXT_EVENT, ao as MEET_ANSWER_EVENTS, ap as MEET_ROUTES, aq as MEET_RPCS, ar as MEET_SCHEMA, as as MEET_TABLES, at as MODERATION_ACTIONS, au as MediaDevicesLike, j as MediaEffects, av as MediaKind, aw as MediaPlatform, ax as MediaRegionState, ay as MediaSupervisor, az as MediaSupervisorOptions, aA as MediaUnavailable, d as MeetAi, aB as MeetAiAnswer, aC as MeetAiOptions, k as MeetAnswerCoverage, aD as MeetDevice, aE as MeetError, aF as MeetErrorCode, aG as MeetGrants, aH as MeetPatch, aJ as MeetingSearchHit, aK as MeetingSearchSource, a as MeetingSession, aL as MeetingSessionOptions, aM as ModerationAction, o as ModerationNotice, h as ModerationRequest, i as ModerationResult, aN as NOTE_TAKER_IDENTITY_PREFIX, aO as NOTE_TAKER_METADATA_KEY, aP as NO_GRANTS, aQ as NO_SHARE, aS as NetworkPort, aT as NoteTakerDispatch, aU as NoteTakerRefusal, N as NoteTakerStatus, aV as OsFamily, aW as PARTICIPANT_CAN_RECORD_ATTRIBUTE, aX as PARTICIPANT_HAND_ATTRIBUTE, aY as PARTICIPANT_ROLE_ATTRIBUTE, aZ as PARTICIPANT_STANDING_ATTRIBUTE, a$ as PhaseEvent, b0 as PhaseEventType, b1 as PlatformDevice, b2 as REACTION_TTL_MS, b3 as RESUME_WINDOW_MS, b4 as ReconnectPolicyLike, b5 as ReconnectPolicyOptions, b as RecordingController, b6 as RecordingControllerOptions, b7 as RepositoryOptions, b8 as ResumeMarker, b9 as ResumeStorage, e as RoomDriverFactory, R as RoomEngine, bc as RoomEngineOptions, bd as RoomReaction, bf as RoomTokenStorage, bg as ScheduleMeetingInput, S as ShareRegion, bh as TRANSCRIPTION_ATTRIBUTES, bi as TRANSCRIPTION_TOPIC, T as TokenClient, bj as TokenClientOptions, bk as TokenRequest, bl as VideoEffect, bm as WAITING_PHASES, bn as admissionPhase, bo as autoRejoinDelayMs, bp as canModerate, bq as canModerateParticipant, br as canTransition, bs as changesPatch, bt as classifyDeviceFailure, bu as connectionPolicyFrom, bv as createCallCenter, bw as createConnectionSupervisor, bx as createDeviceManager, by as createInRoomClient, bz as createMediaSupervisor, bA as createMeetAi, bB as createMeetRepository, bC as createMeetStore, bD as createMeetingSession, bE as createMemoryDeviceStorage, bF as createReachabilityProbe, bG as createReconnectPolicy, bH as createRecordingController, bI as createRoomEngine, bJ as createTokenClient, bK as createWebDeviceStorage, bL as createWebNetworkPort, bM as createWebResumeStorage, bN as createWebRoomTokenStorage, bO as detectBrowser, bP as detectOs, bQ as deviceChipLabel, bR as deviceFixSteps, bS as deviceNoticeCode, bT as deviceUnavailableReason, bU as displayNameFor, bV as hostUserIds, bW as httpUrlOf, bX as invalidMeetResponse, bY as isGuestIdentity, bZ as isJoinRefusalReason, b_ as isListenOnly, b$ as isLivePhase, c0 as isNoteTaker, c1 as isRealName, c2 as isRefusedPhase, c3 as isUnannouncedDeparture, c4 as isWaitingPhase, c5 as learnName, c6 as learnNames, c7 as leaveChoices, c8 as leavePolicyFrom, c9 as listenOnlyNotice, ca as meetingLink, cb as meetingPassHeaders, cc as meetingPath, cd as meetingSlug, ce as mergeGhosts, cf as nextPhase, cg as normalizeDeviceError, ch as normalizeMeetError, ci as noteTakerIdentity, cj as noteTakerLabel, ck as noteTakerParticipant, cl as noteTakerStatus, cm as parseRoomToken, cn as projectInvitee, co as projectLibraryRecording, cp as projectNote, cq as projectOccurrence, cr as projectOverride, cs as projectParticipant, ct as projectRsvpInvitation, cu as projectSearchHit, cv as projectSegment, cw as projectUpcoming, cx as readRefusalBody, cy as reasonOf, cz as recordingLabel, cA as refusalPhase, cB as rememberedMeetingPass, cC as resolveExit, cD as roleOf, cE as settingsPatch, cF as snippetRuns, cG as unavailableFrom, cH as webMediaPlatform } from './in-room-B9pch08I.js'; import { ae as TranscriptSegment, c as ParticipantIdentity, h as MeetingId, R as RoomName, U as UserId, n as CallState, d as MeetParticipant, C as CallInvite, f as CallMode, S as StageLayout, p as ConnectionQuality$1, V as MeetingAttendee, g as MeetingNote, a9 as RecordingRecord, b as MeetingRecord, M as MeetApi, a as MeetIdentity, O as OrganizationId, j as CollabSnapshot, e as CollabPanel } from './collab-B85s8sQ8.js'; export { A as AccessTokenSource, k as AdmissionState, l as CAPTION_LANGUAGES, m as CallId, o as CallablePerson, E as EMPTY_COLLAB, F as FILE_SESSION_PATH, I as InviteeInput, q as InviteeRole, J as JsonObject, r as JsonValue, L as LobbyEntry, s as MeetApiOptions, t as MeetApiRequest, u as MeetAppFocus, v as MeetBreakoutBroadcast, w as MeetBreakoutPlan, x as MeetBreakoutRoom, y as MeetCollabSettings, z as MeetFileMedia, B as MeetPoll, D as MeetPollOption, G as MeetPollState, H as MeetQuestion, K as MeetQuestionState, N as MeetStroke, Q as MeetingActivities, i as MeetingAppPanel, X as MeetingChanges, Y as MeetingInvitee, Z as MeetingKind, _ as MeetingNoteKind, $ as MeetingOccurrence, a0 as MeetingSettings, a1 as NoteTakerState, a2 as ORGANIZATION_HEADER, a3 as ObserversVisibleTo, a4 as OccurrenceOverride, a5 as OccurrenceState, P as ParticipantRole, a6 as ParticipantStanding, a7 as ROLE_RANK, a8 as RecordingPolicy, aa as RecordingState, ab as RsvpAnswer, ac as RsvpInvitation, ad as RsvpState, T as TranscriptSource, af as UpcomingOccurrence, W as WrapUpAnnouncement, ag as appPanelOf, ah as applyCollabPacket, ai as asCallId, aj as asMeetingId, ak as asOrganizationId, al as asParticipantIdentity, am as asRoomName, an as asUserId, ao as assignedRoom, ap as autoAssign, aq as breakoutClosingMs, ar as breakoutRemainingMs, as as captureHeldOf, at as collabFromState, au as createMeetApi, av as defaultCaptionLanguage, aw as fileDownloadPath, ax as focusFromRow, ay as isGuest, az as isHostRole, aA as isLinkFollower, aB as isObserverRole, aC as loadMeetingActivities, aD as scheduleSettingsPatch, aE as withItem } from './collab-B85s8sQ8.js'; import { ActionHandler } from '@ai-matrx/messaging'; import { RoomOptions, ConnectionQuality, Participant } from 'livekit-client'; import '@ai-matrx/data/realtime'; import '@ai-matrx/browser-audio/core'; /** * CAPTIONS ARE HINTS; SEGMENTS ARE TRUTH (D16). * * Two independent streams describe the same speech and they arrive out of order * with different identities: * * 1. **Captions** ride the LiveKit `lk.transcription` text stream, keyed by * `lk.segment_id`, interim-then-final, milliseconds behind the voice. They * exist so a person can READ the room live. Their `sender_identity` is * supplied by the publishing client and LiveKit does not validate it, so * nothing downstream — notes, Q&A, the wrap-up — may ever read them. * 2. **Records** are `communication.meet_transcript_segments` rows, written * server-side by the note-taker with a deterministic id (D6) and delivered * here by Postgres Changes. They are the meeting's transcript. * * The reconciliation below is what keeps the screen from saying everything * twice. A record REPLACES the caption it supersedes — matched by the caption * id the worker carries into the row, and failing that by speaker + text — and * a caption never overwrites a record, however late it lands. * * Everything here is a pure function over a list so the rules are provable * without a store, a socket, or a browser. */ /** Comparable text: case, punctuation and spacing are not differences. */ declare function normalizeSpoken(text: string): string; /** * A caption arrived. Interim replaces interim in place; a final never loses to a * late interim; a record is never touched. */ declare function mergeCaption(list: readonly TranscriptSegment[], caption: TranscriptSegment): readonly TranscriptSegment[]; /** * A durable row arrived. It replaces the caption it supersedes — in place, so * the line does not jump down the panel — or lands as a new line if no caption * ever showed it (a surface that joined late, or captions that never came). */ declare function mergeRecord(list: readonly TranscriptSegment[], record: TranscriptSegment): readonly TranscriptSegment[]; /** The transcript a person reads: chronological, and never an empty line. */ declare function readableTranscript(list: readonly TranscriptSegment[]): readonly TranscriptSegment[]; /** Build a caption line from the media plane. */ declare function captionSegment(args: { captionId: string; meetingId: TranscriptSegment["meetingId"]; identity: ParticipantIdentity; speaker: string; text: string; final: boolean; startedAt: string; }): TranscriptSegment; /** * THE MEET CHANNEL NAMESPACES. * * Zero hand-rolled `.channel(` in this package (the realtime doctrine). Every * topic is declared through `@ai-matrx/data/realtime`'s registry, which refuses a * colliding re-declaration. * * THREE channels, each because it has a different AUDIENCE: * * - `meet-user` (per user): where a call REACHES you — the ring, the cancel, * the "answered on another device". You are subscribed to this everywhere the * provider is mounted, whether or not you are in a meeting. * - `meet-room` (per room): the pre-media room — who is knocking, who was * admitted, whether recording started. It carries the moments that happen * BEFORE and AFTER media, which LiveKit's own data plane cannot: a guest in * the lobby is not in the LiveKit room yet, by construction (D6). * - `meet-meeting` (per meeting): the durable record's feed — transcript * segments, notes, the recording row going `available` — so a surface that is * NOT in the call (a meeting page, an admin console) stays live. * * 🚨 The in-MEDIA control plane (hand raise, reactions, in-room chat) rides * LiveKit data messages, NOT these channels — see `DATA_TOPICS`. Splitting them * this way is deliberate: anything that must reach a guest with no Supabase * session travels on the media plane, where their room token is the authority. */ declare function userTopic(userId: UserId): string; declare function roomTopic(roomName: RoomName): string; declare function meetingTopic(meetingId: MeetingId): string; /** Broadcast event names. One place, so a sender and a receiver cannot drift. */ declare const MEET_EVENTS: { /** A call is ringing you. Carries the whole invite; no follow-up read. */ readonly ring: "mx.call.ring"; /** The caller hung up before you answered. */ readonly cancel: "mx.call.cancel"; /** You answered (or declined) on another device. Stop ringing here. */ readonly settled: "mx.call.settled"; /** * 🚨 S4 (CORE-DESIGN §2.6): the ONE room-channel event — payload-free "something changed, * read again". Nothing a person must see rides it: a waiting person learns admitted / * denied / expired / ended by READING their row (`GET /v1/meet/state`) on a timer and on * every hint. The four payload broadcasts (`mx.lobby.knock`, `mx.lobby.admission`, * `mx.room.lock`, `mx.room.recording`) are gone with their senders and receivers. The * server publishes the same event on the same topic (aidream `admission.room_topic`). */ readonly stateChanged: "mx.state.changed"; }; /** * 🚨 `mx.meeting.transcript` and `mx.meeting.note` WERE declared here and * emitted and subscribed by nothing — a broadcast fallback that existed only as * a name (MRI-A10 found it; feedback `b25db4d4`). They are gone. The thing they * described is real now and lives somewhere better: the platform publishes the * durable rows into the LIVEKIT ROOM under `matrx.meet.*` (`core/room-events.ts`, * MRI-A12), where a session-less guest's room token is the authority. Sending * them over a Supabase broadcast channel would have required the guest to hold a * Supabase connection, which is the exact thing they do not have. */ type MeetEvent = (typeof MEET_EVENTS)[keyof typeof MEET_EVENTS]; /** * THE PLATFORM'S OWN VOICE IN THE ROOM — and the ONLY live feed a guest has. * * 🚨 **Why this file exists.** Everything durable a meeting produces — a live * note, a decision, an action item, the note-taker's state, the ending — lands * in `communication.meet_*`, and a signed-in member learns about it over * Postgres Changes. A GUEST cannot: `anon` holds no SELECT grant on * `meet_notes`, `meet_transcript_segments` or `meet_participants`, and MRI-A10 * settled that widening it is forbidden, because those are org-scoped tables and * a meeting link is not org membership. A guest's authority is the ROOM TOKEN. * So the platform publishes the same rows into the room, where the token is the * authorization, and this file turns those packets into exactly the same store * calls the database path makes. * * Three rules hold it together: * * 1. **Same row, same projection, same reducer.** A `matrx.meet.note` packet * carries a `meet_notes` row and is projected by `projectNote` — the very * function the Postgres Changes handler uses — then handed to * `store.upsertNote`. A guest and a member therefore converge on one code * path rather than on two implementations that agree until they don't. * 2. **Keyed by the row id, so both copies are one thing.** A member receives * every note TWICE. They render once because the id is the row's id on both * transports, and where the two could disagree the database wins * (`store.upsertNote`'s precedence rule, D16). * 3. **`matrx.meet.*` is the server's namespace and nothing else's.** LiveKit * does not validate a data packet's sender, so any participant could publish * any bytes under any topic. What makes these trustworthy is that publishing * them requires a `roomAdmin` grant, which only the server's own control * token carries — a participant token cannot claim it. The in-room control * plane participants DO publish (hand, reaction, chat) lives under `mx.*` * and is handled separately, precisely so the two can never be confused. */ /** * The server's namespace. Pinned against `aidream/services/meet/livekit.py`'s * `ROOM_EVENT_PREFIX` — a rename on either side must break a named test here. */ declare const ROOM_EVENT_PREFIX = "matrx.meet."; declare const ROOM_EVENTS: { /** One durable `meet_notes` row was written. Payload `{ note: }`. */ readonly note: "matrx.meet.note"; /** `meet_meetings.metadata.note_taker` changed. Payload `{ meeting_id, note_taker }`. */ readonly noteTaker: "matrx.meet.note_taker"; /** A wrap-up pass finished. Payload `{ meeting_id, note_ids, segment_count }`. */ readonly wrapUp: "matrx.meet.wrap_up"; /** The meeting ended for everyone. Payload `{ meeting_id, ended_at, trigger }`. */ readonly ended: "matrx.meet.ended"; /** * A chat message to everyone was SAVED (Meet wave 2). Payload * `{ message: { id, meeting_id, sender_identity, sender_name, body, created_at } }`. */ readonly chat: "matrx.meet.chat"; /** * A host or co-host acted (Meet wave 2). Payload `{ meeting_id, kind, … }`: * `muted {source, by}` · `ask-unmute {by}` · `removed {by}` — addressed to the * person concerned; `role {identity, role}` — to everyone. */ readonly moderation: "matrx.meet.moderation"; /** * A meeting ROOM ITEM changed (Meet wave 5): a poll, a Q&A question, a whiteboard * stroke or the breakout plan. Payload `{ meeting_id, item }` · `{ removed: [ids] }` · * `{ hidden: [ids] }` · `{ cleared: "whiteboard" }` · `{ broadcast }` · `{ item, move }`. * A host's fuller copy of a poll or question carries `audience: "hosts"`. */ readonly collab: "matrx.meet.collab"; }; type RoomEventTopic = (typeof ROOM_EVENTS)[keyof typeof ROOM_EVENTS]; declare function isRoomEventTopic(topic: string): boolean; /** * What happened to one packet. Returned rather than logged from inside, so the * room engine can report a drop with its own diagnostic channel and a test can * assert on it — a packet that is silently ignored is indistinguishable from * one that never arrived, which is the failure this whole item is about. */ interface RoomEventOutcome { readonly handled: boolean; /** Present when `handled` is false. Says what was wrong, in a sentence. */ readonly ignored?: string; } interface ApplyRoomEventArgs { readonly store: MeetStore; readonly topic: string; /** The packet's already-parsed JSON body. */ readonly payload: unknown; readonly now?: (() => number) | undefined; } /** * Apply one `matrx.meet.*` packet to the store. Never throws. * * A malformed packet is DROPPED with a reason, never partially applied: half a * note on a guest's screen is worse than no note, because the guest cannot tell * which half they are missing. */ declare function applyRoomEvent(args: ApplyRoomEventArgs): RoomEventOutcome; /** * PRESENTATION HELPERS specific to a CALL. Pure, tested, and shared by every * surface — so a roster line reads the same in the meeting, in the history * list, and in the notification. * * The GENERIC formatters this file used to define — `formatDuration`, * `getInitials`, `avatarPaletteIndex` — moved to `@ai-matrx/kit/format` on * 2026-09-07 (duplication census H1). They were byte-for-byte duplicated in * `@ai-matrx/messaging`, and because two packages claimed them, ~65 host * twins across six repos had no correct owner to point at: no surface should * take a dependency on the video-calling package to print a duration. Nothing * is re-exported from here — import `@ai-matrx/kit/format` directly. */ /** "Ana, Ben and 3 others" — the roster line, never a raw array join. */ declare function participantSummary(participants: readonly MeetParticipant[]): string; /** What a settled call says in history. One sentence per terminal state. */ declare function callStateLabel(state: CallState, outgoing: boolean): string; /** "2 waiting" for the lobby badge — plural handled once, not per surface. */ declare function countLabel(count: number, singular: string, plural?: string): string; /** * THE MESSAGING SEAM — a call invitation is an ACTIONABLE MESSAGE (D8). * * `@ai-matrx/messaging` deliberately knows nothing about meetings; its action * registry exists so packages like this one can add a kind without messaging * learning about them. This file is that registration, and it is the whole * coupling between the two packages. * * Why an invitation is a message and not only a ring: a ring is EPHEMERAL. If * you were away from your desk, a broadcast that expired is gone, and "someone * tried to reach me" is exactly the information you most needed. The message is * the durable half — it is still in the thread tomorrow, with the outcome * stamped on it. * * 🚨 **THE PAYLOAD IS NOT AUTHORIZATION.** The chip's payload says what was * ASKED, never what is ALLOWED. The handler re-resolves the invite through the * repository (an auth-checked RPC) before it joins anything. Trusting the * payload would turn any message anyone can send into a room grant. * * Idempotence is the registry's, not ours: two tabs and a double tap settle * ONCE and share one receipt. */ declare const CALL_INVITE_KIND = "meet.call-invite"; declare const CALL_INVITE_VERSION = 1; interface CallInvitePayload { readonly invite_id: string; readonly room_name: string; readonly mode: CallMode; readonly caller_name: string; readonly expires_at: string; } /** The `action_data` a caller attaches to the invitation message. */ declare function callInviteAction(invite: CallInvite): { readonly kind: string; readonly version: number; readonly payload: CallInvitePayload; }; /** The message body. Readable on a surface that cannot render the chips. */ declare function callInviteText(invite: CallInvite): string; interface CallInviteHandlerOptions { calls: CallCenter; repository: MeetRepository; /** Called after a successful accept, with the room to enter. */ onJoin: (invite: CallInvite) => void | Promise; now?: (() => number) | undefined; } declare function createCallInviteHandler(options: CallInviteHandlerOptions): ActionHandler; /** * A meeting invitation — the scheduled twin. Deliberately NOT the same kind: a * scheduled meeting's chip must never say "Join call" three days early, and a * receiver that only understands calls must render NOTHING for it rather than a * button it cannot honor. */ declare const MEETING_INVITE_KIND = "meet.meeting-invite"; declare const MEETING_INVITE_VERSION = 1; interface MeetingInvitePayload { readonly meeting_id: string; readonly slug: string; readonly title: string; readonly scheduled_for: string | null; readonly link: string; } declare function meetingInviteAction(payload: MeetingInvitePayload): { readonly kind: string; readonly version: number; readonly payload: MeetingInvitePayload; }; interface MeetingInviteHandlerOptions { onOpen: (payload: MeetingInvitePayload) => void | Promise; now?: (() => number) | undefined; } declare function createMeetingInviteHandler(options: MeetingInviteHandlerOptions): ActionHandler; /** * THE STAGE — who is big, who is small, and who is not rendered at all. * * This is pure arithmetic on purpose: layout is the part of a meeting UI that * everyone re-derives inside a component and gets subtly wrong, and it is * trivially testable once it stops living in JSX. * * R5 SCALE HONESTY lives here. A 200-person meeting does not render 200 video * tiles — every browser dies, and LiveKit's adaptive stream cannot save a * layout that asked for 200 subscriptions. The grid PAGES, and the ordering is * deliberate rather than "whatever the roster map iterated": * * 1. a screen share (or an explicit pin) owns the stage; * 2. then the active speakers, most recent first; * 3. then anyone with a hand raised — they are asking to be seen; * 4. then cameras on before cameras off (an avatar tile is cheap); * 5. then stable by join time, so tiles do not shuffle every time someone * breathes. Shuffling tiles is the single most disorienting thing a * meeting UI can do. */ interface StageArgs { readonly participants: readonly MeetParticipant[]; readonly layout: StageLayout; readonly pinned: ParticipantIdentity | null; readonly activeSpeakers: readonly ParticipantIdentity[]; /** * The participant a HOST spotlighted for everyone (Meet wave 2). A local pin * outranks it on this screen; a screen share outranks both. */ readonly spotlight?: ParticipantIdentity | null | undefined; /** How many tiles this viewport can honestly render. Default 12. */ readonly pageSize?: number; readonly page?: number; } interface Stage { /** The large tile, when the layout has one. */ readonly feature: MeetParticipant | null; /** What actually renders as a tile, already paged. */ readonly tiles: readonly MeetParticipant[]; /** Everyone not on this page. Rendered as "+N others", never dropped silently. */ readonly overflow: number; readonly page: number; readonly pageCount: number; /** True when a screen share, not a person, is the reason for the big tile. */ readonly presenting: boolean; } declare const DEFAULT_PAGE_SIZE = 12; declare function orderParticipants(participants: readonly MeetParticipant[], activeSpeakers: readonly ParticipantIdentity[]): readonly MeetParticipant[]; declare function computeStage(args: StageArgs): Stage; /** Column count for a grid of N tiles — the shape that keeps tiles squarest. */ declare function gridColumns(count: number): number; /** * THE ONE PLACE `livekit-client` IS TOUCHED. * * Everything above this file talks to `MeetRoomDriver`. That containment is * what lets the engine's whole behavior — ring timeouts, admission, escalation, * reconnection — be proven in a node test, and it is why an SDK upgrade is a * single-file review rather than a package-wide audit. * * 🚨 **THE IMPORT IS DYNAMIC, DELIBERATELY.** `import type` is erased, and the * runtime `await import("livekit-client")` happens inside `connect()`. So: * importing `@ai-matrx/meet` during SSR, or on a page that renders a call * BUTTON but never a call, pulls in none of the media SDK and touches no * browser API (independence contract §4). A static import here would put ~400KB * of WebRTC into every consumer's first load and break RSC rendering. * * The mapping helpers below are exported for their own tests: an SDK enum * renamed upstream must fail a named test, not silently become "unknown" on * every tile. */ /** * LiveKit's quality enum → ours. Mapped explicitly rather than cast: the SDK * has five states and our UI has five sentences, and an unmapped one must * degrade to `unknown` (an honest "we don't know") rather than to `excellent` * (a lie that hides a failing call). */ declare function mapQuality(quality: ConnectionQuality | string): ConnectionQuality$1; declare function mapConnectionState(state: string): DriverConnectionState; /** * A LiveKit participant → our snapshot. * * 🚨 `name` IS REPORTED EXACTLY AS LIVEKIT GAVE IT, empty string and all * (MRI-A18). Through 0.4.12 it fell back to the identity here, which meant a * driver — the thing furthest from any screen — invented a "name" that * `core/names.ts` then had to recognize and refuse. A driver reports; it does * not name. The one fallback lives in `projectParticipant`, and it is the name * book's honest placeholder rather than a token. */ declare function snapshotParticipant(participant: Pick & { attributes?: Readonly> | undefined; }, isLocal: boolean): DriverParticipant; interface LiveKitDriverOptions { /** Adaptive stream + dynacast default ON — R5 scale honesty, not a knob. */ roomOptions?: RoomOptions | undefined; } declare function createLiveKitDriver(handlers: RoomDriverHandlers, options?: LiveKitDriverOptions): MeetRoomDriver; /** * THE MEETING RECORD — what a meeting IS once the room is gone (MRI-D2). * * 🚨 THE DEFECT THIS EXISTS TO KILL. Through 2026-09-08 a meeting produced a * wrap-up summary, decisions, owner-assigned action items, five windows of live * notes, thirty-two transcript segments and a recording — and then had NO * SURFACE AT ALL. `/meet/` fell back to `` on an ended meeting, * so the durable link that R3 promises would outlive the meeting resolved to a * screen offering to join a room that no longer exists. Everything the campaign * built after the room closed was reachable only from SQL. * * Three rules hold this file together: * * 1. **Every read is INDEPENDENT and every refusal is NAMED.** The transcript, * the notes, the attendance list and the recording live in four RLS-guarded * tables, and a guest holding the link can read NONE of them (only * `meet_meeting_by_slug` is granted to `anon`). One `Promise.all` would turn * a permission boundary into a blank page. Known guest restrictions never issue private reads; each authenticated read that is refused * becomes a `MeetingRecordRestriction` with a sentence — the reader is told * what exists and why they cannot see it, which is the difference between a * record they can ask for and a product that looks broken. * 2. **A partial record is still a record.** A failed transcript read never * hides the summary somebody CAN read. * 3. **Nothing here computes meaning.** Grouping, ordering and formatting only. * The summary, decisions and action items are durable rows written by the * wrap-up mandate (D5/D18); this module never re-derives them from the * transcript, because two sources for one fact is how they start to disagree. */ /** One thing the reader could not see, and the honest reason. Never a blank. */ interface MeetingRecordRestriction { /** Which part of the record: `transcript`, `notes`, `attendance`, `recording`. */ readonly part: "transcript" | "notes" | "attendance" | "recording"; readonly message: string; readonly remedy: string; } /** One visible access state, made from restrictions that share one remedy. */ interface MeetingRecordRestrictionNotice { readonly parts: readonly MeetingRecordRestriction["part"][]; readonly message: string; readonly remedy: string; } /** A run of consecutive lines by one speaker — how a person reads a transcript. */ interface TranscriptBlock { readonly id: string; /** Who spoke. Blocks break on THIS, never on the rendered name (MRI-A16). */ readonly identity: ParticipantIdentity; readonly speaker: string; /** ISO of the first line in the block. */ readonly startedAt: string; readonly lines: readonly TranscriptSegment[]; } interface MeetingRecordBundle { readonly meeting: MeetingRecord; readonly attendees: readonly MeetingAttendee[]; /** The wrap-up summary: `kind='note'` with `metadata.role='summary'` (D5). */ readonly summary: MeetingNote | null; /** D20: the AUTHORITATIVE set — window rows the wrap-up superseded are out. */ readonly decisions: readonly MeetingNote[]; readonly actionItems: readonly MeetingNote[]; /** The live-notes windows, in window order (D3). */ readonly liveNotes: readonly MeetingNote[]; readonly transcript: readonly TranscriptSegment[]; /** The most recent recording attempt, whatever state it reached. */ readonly recording: RecordingRecord | null; /** What this reader could NOT see. Rendered; never swallowed. */ readonly restrictions: readonly MeetingRecordRestriction[]; /** * Identity → name, learned from everything this read COULD see: the durable * attendance rows and the transcript's own `speaker` column. It is what stops * a raw LiveKit token reaching the screen when one of those reads was refused * (MRI-A16). */ readonly names: NameBook; } /** * The current wrap-up summary. Repairs preserve the prior durable row for * history, then mark it `superseded_by`; every current-facing projection must * therefore make the same distinction as decisions and action items. */ declare function activeWrapUpSummary(notes: readonly MeetingNote[]): MeetingNote | null; /** * The four durable reads behind a record, as the database returned them, plus * what the reader could not see. `assembleMeetingRecord` turns them into the * bundle; a cache that keeps them can apply a realtime row to one part and * assemble again, without reading the other three (0.7.79). */ interface MeetingRecordParts { readonly attendees: readonly MeetingAttendee[]; /** Every note row (all kinds, superseded included), oldest first. */ readonly notes: readonly MeetingNote[]; readonly transcript: readonly TranscriptSegment[]; /** Newest first; archived recordings are not here. */ readonly recordings: readonly RecordingRecord[]; readonly restrictions: readonly MeetingRecordRestriction[]; } /** * Load the record parts the reader may request; state all restrictions. * * `meeting` is passed in when the caller already resolved it (the slug route * does, through the `anon`-granted RPC) — re-reading it through the table would * fail for exactly the reader who most needs the page to work. */ declare function loadMeetingRecord(repository: MeetRepository, meetingId: MeetingId, meeting: MeetingRecord, reader?: Pick): Promise; /** The four reads, each with its own verdict (see `loadMeetingRecord`). */ declare function loadMeetingRecordParts(repository: MeetRepository, meetingId: MeetingId, reader?: Pick): Promise; /** The bundle a reader sees, from the parts. Grouping and ordering only (rule 3). */ declare function assembleMeetingRecord(meeting: MeetingRecord, parts: MeetingRecordParts): MeetingRecordBundle; /** * 🚨 **A READ THAT WAS REFUSED IS NOT AN EMPTY RESULT (MRI-A16).** * * On production 2026-09-09 a guest's record view said *"No wrap-up was written * for this meeting"* directly underneath a panel that had just told them, in * the same screen, that the notes are not readable from their account — and * *"Nothing was recorded as a decision"* beside a decision that exists * (MRI-C8). The page asserted an absence it had no way to know, which is worse * than saying nothing: a person who reads "no wrap-up was written" stops * looking for one. * * So no surface writes its own empty sentence any more. It asks here, and gets * either its absence sentence — earned, because the read SUCCEEDED and returned * nothing — or the refusal, said plainly. */ declare const REFUSED_ABSENCE = "This record is not shared with guests."; /** Was this part of the record refused to this reader? */ declare function wasRefused(bundle: MeetingRecordBundle, part: MeetingRecordRestriction["part"]): boolean; /** * The sentence to show where a part of the record has nothing in it. NEVER an * absence claim over a read that was refused. */ declare function absenceSentence(bundle: MeetingRecordBundle, part: MeetingRecordRestriction["part"], absence: string): string; /** * 🚨 **"NO WRAP-UP WAS WRITTEN" IS A CLAIM, AND FOR ~33 SECONDS IT WAS FALSE.** * * On production 2026-09-09 the HOST's record view said *"No wrap-up was written * for this meeting"* between 04:46:42 — the flip from the room to the record — * and 04:47:15, when the wrap-up landed. The read was permitted and genuinely * empty, so this is not the refusal class above; it is the OTHER way a screen * lies, by reading "not here yet" as "never happened". A person who reads *"no * wrap-up was written"* closes the tab. * * The meeting row already knows the difference, and it is not a guess: the * wrap-up is a durable job, `enqueue_wrap_up` stamps * `metadata.note_taker.intelligence.wrap_up` when it queues it, and the graph * action stamps `state: "failed"` when the pass refuses TERMINALLY. So: * * - a summary exists → `written`; * - the notes read was refused → `refused` (never an absence claim); * - the job is marked failed → `failed`, in the server's own words; * - the job is queued or running → `in-progress`, with a refresh affordance; * - nothing ever queued one → `absent`, which is the only EARNED absence. * * There is no timeout that flips `in-progress` into `absent`. A wrap-up that is * genuinely stuck says it is still being written — which is TRUE — rather than * inventing a verdict this browser has no way to reach. */ type WrapUpStatus = { readonly state: "written"; } | { readonly state: "in-progress"; readonly message: string; readonly canRefresh: true; } | { readonly state: "failed"; readonly message: string; } /** Nobody spoke: a normal outcome, said plainly (not an alert). */ | { readonly state: "empty"; readonly message: string; } | { readonly state: "refused"; readonly message: string; } | { readonly state: "absent"; readonly message: string; }; /** Where the wrap-up actually is. Never "absent" over a job that is running. */ declare function wrapUpStatus(bundle: MeetingRecordBundle): WrapUpStatus; /** * GROUP THE TRANSCRIPT THE WAY A PERSON READS IT: consecutive lines by one * speaker become one block with one timestamp. Reading thirty-two rows each * re-stamped with a name and a clock is how a transcript becomes unreadable. */ declare function groupTranscript(segments: readonly TranscriptSegment[], names?: NameBook): readonly TranscriptBlock[]; /** * WHAT THE RECORDING IS DOING, in a sentence — including the two states people * hit most and understand least. `processing` is not "broken" and `failed` is * never silence: both say what happened and what to do. */ declare function recordingNotice(recording: RecordingRecord | null): string | null; /** Only an `available` recording has a file to play. Nothing else pretends to. */ declare function playableRecordingFileId(recording: RecordingRecord | null): string | null; /** * A MEETING IS OVER WHEN THE SERVER SAYS SO. `ended_at` is stamped by * `POST /v1/meet/end` or by the auto-end evaluation (MRI-A6) — never by a * client noticing the room is empty. */ declare function hasEnded(meeting: MeetingRecord | null): boolean; /** * "Thursday, 8 September 2026, 4:21 PM · 19 minutes". One shape everywhere. * * THE SPAN IS `formatDurationMs`'s (2026-09-12). This body used to divide by * `60_000` and pluralise "minute" itself — a duration formatter living in a * PACKAGE, found the day `aidream/apps/shared` became a scan root. `@ai-matrx/ * meet` already depends on `@ai-matrx/kit`, and `style: "long"` is precisely * this voice ("19 minutes", "1 minute"). Three things improve by adopting it: * a 40-second meeting read "0 minutes" and now reads "40 seconds" (the * confident zero the package refuses), a 19-minute-35-second meeting read * "20 minutes" and now reads "19 minutes" (prose asserts at-least, so it * floors), and a 90-minute meeting reads "1 hour" instead of "90 minutes". */ declare function meetingWhen(meeting: MeetingRecord, locale?: string): string; /** How much of a finished meeting a GUEST may read. Pinned with the server. */ type GuestRecordAccess = "none" | "summary" | "full"; /** Widest last. The ORDER is the comparison, exactly as it is on the server. */ declare const GUEST_RECORD_ACCESS_LEVELS: readonly GuestRecordAccess[]; /** * What a HOST may choose for one meeting. `default` is not a fourth level — it * is the absence of a per-meeting choice, and it is the only way to un-choose. */ type GuestRecordAccessSetting = "default" | GuestRecordAccess; declare const GUEST_RECORD_ACCESS_CHOICES: readonly GuestRecordAccessSetting[]; /** `meet_meetings.metadata.guest_record_access`. No DDL; a setting on a row. */ declare const GUEST_RECORD_ACCESS_METADATA_KEY = "guest_record_access"; /** What a host chose for THIS meeting, read from the row they already have. */ declare function guestRecordAccessSetting(meeting: MeetingRecord | null): GuestRecordAccessSetting; /** The sentence on the host's control. Plain English, never a token. */ declare function guestRecordAccessLabel(setting: GuestRecordAccessSetting): string; /** What each choice actually means for the people who were in the room. */ declare function guestRecordAccessExplanation(setting: GuestRecordAccessSetting): string; /** What came back from the server, so a surface never has to infer a level. */ interface GuestRecordAccessResolution { readonly level: GuestRecordAccess; /** `meeting` when a host chose for this one; `organization` otherwise. */ readonly source: "meeting" | "organization"; } /** * Repeated policy refusals are one access state, not four alarming cards. * * The individual rows remain durable data for absence handling in their own * sections. This projection groups only rows with the exact same remedy, so a * distinct transport or integrity failure remains visible on its own. */ declare function summarizeMeetingRecordRestrictions(restrictions: readonly MeetingRecordRestriction[]): readonly MeetingRecordRestrictionNotice[]; /** * Read the record the server shares with THIS guest. * * `roomToken` is the pass this device kept from the meeting * (`TokenClient.remembered`). It is presented in a header, never appended to * the URL. A guest with no pass, an expired pass, or a level of `none` gets the * server's refusal — as restrictions on a real bundle, so the screen says what * it cannot show instead of collapsing into an error page. */ declare function loadGuestMeetingRecord(args: { api: MeetApi; repository: MeetRepository; meetingId: MeetingId; meeting: MeetingRecord; roomToken: RoomToken | null; /** * A SIGNED-IN reader the database refused (from another organization, or a * guest who later created an account). Their session stands in for the * pass: the server reads their `user:` admission or the attendance they * claimed, and shares what guests may read (2026-10-01). */ session?: boolean; /** * THE SESSION-AWARE RECORD (CORE-DESIGN §2.3): one run of a series. Omitted = the server's * default (a one-off meeting's whole record; a series' latest ended run). */ sessionId?: string | null; }): Promise; /** * A HOST chooses what guests may read of THIS meeting. * * Host power is the SERVER's decision and it re-checks it; this function exists * so the control has one call to make, and it returns what the choice RESOLVES * to so the screen never has to guess what "my organization's setting" means * today. */ declare function setGuestRecordAccess(args: { api: MeetApi; meetingId: MeetingId; organizationId: OrganizationId; access: GuestRecordAccessSetting; }): Promise<{ readonly setting: GuestRecordAccessSetting; readonly effective: GuestRecordAccessResolution; }>; /** What a guest is told when the level shares nothing. Rendered, never hidden. */ declare const GUEST_RECORD_NOT_SHARED: Readonly>; /** * AFTER THE MEETING (Meet wave 3) — the pure pieces every client needs once the * room is gone: the attendance report, the transcript and notes as documents, * a moment in the transcript as a moment in the recording, search within one * transcript, and the two server calls behind the recap email and the saved * chat. * * Nothing here computes MEANING (same rule as `record.ts`): the summary, * decisions and action items are the wrap-up's durable rows; this file only * arranges, measures and formats them. Every function is framework-free so a * worker, a test and a browser produce byte-identical exports. */ /** * Who someone was, in the words an attendance report uses. `signed-in guest` is * an account from outside the meeting's organization that followed the link; * `guest` has no account at all. */ type AttendanceKind = "host" | "cohost" | "member" | "signed-in guest" | "guest" | "assistant"; interface AttendanceRow { readonly id: string; readonly identity: string; readonly name: string; readonly userId: string | null; readonly kind: AttendanceKind; /** First time they were in the room; null when they knocked and never came in. */ readonly joinedAt: string | null; /** When they left; null while still in the room or when the leave was never recorded. */ readonly leftAt: string | null; /** * Time in the room. A missing `leftAt` is measured to the meeting's end — * the room closed around them — and `null` only when neither is known. */ readonly durationMs: number | null; /** True when `durationMs` was measured to the meeting's end, not a recorded leave. */ readonly durationToEnd: boolean; readonly admission: MeetingAttendee["admissionState"]; /** How many times this person came in (a rejoin or a second device). */ readonly joins: number; } /** * The attendance report of ONE meeting, host first then by arrival. People who * knocked and were never let in are kept (with no join time) — "who asked to * come in" is part of the record — but a denied knock has no duration. */ /** * WHO a participant row is, as a PERSON (verifier, 2026-09-27: an export listed * the same attendee twice under "attended"). A signed-in person is their * account — every device and every rejoin is one person. A guest without an * account gets a fresh `guest:` identity on every join, so the name * they typed is the only thread between their joins (Zoom's attendee report * merges link guests the same way); two different accountless guests who * typed the identical name read as one, which is the lesser error. */ declare function attendeeKey(person: MeetingAttendee, names?: NameBook): string; /** * One row per PERSON, in first-seen order: the row that actually joined (the * earliest) represents them. For a list of names or a head-count; the * attendance REPORT merges times as well (`attendanceReport`). */ declare function uniqueAttendees(attendees: readonly MeetingAttendee[], names?: NameBook): readonly MeetingAttendee[]; declare function attendanceReport(meeting: MeetingRecord, attendees: readonly MeetingAttendee[], names?: NameBook): readonly AttendanceRow[]; /** Headline numbers for the top of a report. The assistant is never a person. */ declare function attendanceTotals(rows: readonly AttendanceRow[]): { readonly people: number; readonly guests: number; readonly neverAdmitted: number; readonly averageMs: number | null; }; declare const ATTENDANCE_CSV_HEADER: readonly ["Meeting", "Meeting started", "Name", "Attended as", "Joined", "Left", "Minutes in the meeting", "Admission"]; /** * CSV for a spreadsheet: one header row, one row per person, ISO timestamps (a * spreadsheet parses them; a formatted local clock loses the zone). Several * meetings — a series — concatenate with one header. */ declare function attendanceCsv(reports: readonly { meeting: MeetingRecord; rows: readonly AttendanceRow[]; }[]): string; /** * Where an instant of the meeting falls in the RECORDING, in milliseconds from * its start — or `null` when the recording does not cover it (a recording is * often a few minutes of a longer meeting). Never a clamp: seeking a line that * was said before recording began to 0:00 would play the wrong words. */ declare function recordingOffsetMs(at: string, recording: RecordingRecord | null): number | null; /** `m:ss` or `h:mm:ss` — a position in a recording or a meeting. */ declare function clockPosition(offsetMs: number): string; interface TranscriptMatch { readonly segmentId: string; /** [start, end) character ranges inside the segment's text, in order. */ readonly ranges: readonly (readonly [number, number])[]; } /** * Case-insensitive search of the transcript text AND the speaker's * name. Returns matches in transcript order. An empty query matches nothing. */ declare function searchTranscript(segments: readonly TranscriptSegment[], query: string, names?: NameBook): readonly TranscriptMatch[]; type TranscriptFormat = "txt" | "md" | "vtt"; interface ExportOptions { readonly names?: NameBook; /** Timestamps as a clock of day (default) or offsets from `zero`. */ readonly locale?: string; readonly timeZone?: string; } /** * The transcript as a document. `txt` and `md` group consecutive lines by one * speaker with the clock of day; `vtt` is WebVTT cued from the recording's * start when a recording exists (so it plays as captions over it), otherwise * from the meeting's start — and a cue ends where the next line begins. */ declare function transcriptDocument(bundle: Pick, format: TranscriptFormat, options?: ExportOptions): string; /** * The meeting's notes as Markdown: when, who attended, the summary, decisions * and action items (with owners), and the link back. What a person pastes into * a doc or an email; also the source for PDF / Word export. */ declare function notesDocument(bundle: Pick, options?: { readonly link?: string | null; readonly when?: string | null; }): string; /** How long the meeting ran, in words ("42 minutes"), or null when unknown. */ declare function meetingLength(meeting: MeetingRecord): string | null; interface MeetingChatHistory { readonly messages: readonly RoomChatMessage[]; /** The server's refusal, in its words, when the history could not be read. */ readonly refusal: { readonly message: string; readonly remedy: string; } | null; } /** * The saved chat of a finished meeting (`GET /v1/meet/chat`). Private messages * are never saved, by design, so they are never here. A refusal is returned as * data — the record still renders everything else. */ declare function loadMeetingChat(api: MeetApi, meetingId: MeetingId, meetingPass?: string | null): Promise; /** One person the recap can go to, as the server resolved them. */ interface FollowUpRecipient { /** Stable key for the choice: `user:` or `email:
`. */ readonly key: string; readonly name: string; /** The address, when this reader may see it (the host sees their guests'). */ readonly email: string | null; readonly userId: string | null; /** `invitee`, `attendee` or both — where the server found them. */ readonly sources: readonly ("invitee" | "attendee")[]; /** False when the server has no way to reach them (no account, no address). */ readonly reachable: boolean; } /** The draft the host reviews. The text is the wrap-up's own, arranged. */ interface FollowUpDraft { readonly subject: string; readonly body: string; readonly recipients: readonly FollowUpRecipient[]; /** False while the wrap-up has not been written — the draft says so. */ readonly wrapUpWritten: boolean; /** When a recap for this meeting was last sent, and to how many. */ readonly lastSent: { readonly at: string; readonly count: number; } | null; } interface FollowUpSendResult { readonly dryRun: boolean; /** Recipients the send queued (or, on a dry run, would queue). */ readonly queued: readonly string[]; /** Recipient keys skipped, each with the reason. */ readonly skipped: readonly { readonly key: string; readonly reason: string; }[]; } /** The recap draft for a finished meeting. The host, a co-host or an editor. */ declare function loadFollowUpDraft(api: MeetApi, meetingId: MeetingId): Promise; /** * Send the recap the host reviewed. `dryRun` resolves every recipient and * renders every email but queues nothing — how a test proves the path without * mailing a real inbox. Never called automatically. */ declare function sendFollowUp(api: MeetApi, meetingId: MeetingId, input: { readonly subject: string; readonly body: string; readonly recipientKeys: readonly string[]; readonly dryRun?: boolean; }): Promise; /** * MEET WAVE 1 — the two server calls a management screen makes (0.8). * * Every WRITE (schedule, edit, reschedule, cancel, archive, occurrence change, * invite, answer) goes straight to the database through `MeetRepository` — the * database decides who may. What a browser cannot do is send an email with a * calendar object in it, so after any of those writes a screen calls * `announceMeeting`: the server tells each invitee exactly what they have not * been told (invitation, update or cancellation, each with its `.ics`), and * re-queues reminders. It is idempotent — call it after every save. */ interface AnnounceResult { readonly invitations: number; readonly updates: number; readonly cancellations: number; readonly alreadyCurrent: number; readonly remindersQueued: number; readonly remindersCancelled: number; } /** Bring every invitee's inbox and calendar up to date. The host, a co-host or an editor. */ declare function announceMeeting(api: MeetApi, meetingId: MeetingId): Promise; /** * Put a one-off meeting on the caller's own connected Google Calendar. GATED by * the server to Google's review accounts while the Calendar scope is in review — * a refusal is a `forbidden` MeetError carrying the server's sentence. Everyone * already gets the meeting into any calendar through the invitation's `.ics`. */ declare function pushMeetingToGoogleCalendar(api: MeetApi, meetingId: MeetingId, target: { connectionId: string; calendarId?: string; }): Promise>; /** * A PARTICIPANT'S OWN MEDIA CHOICES, REMEMBERED (Meet wave 2). * * Noise suppression and the camera background are personal and per device — * Zoom and Meet both remember them — so they live in this browser, never on a * server. Blocked storage means the defaults (suppression on, no background), * never a failed join. */ declare function createWebEffectsStorage(key?: string): EffectsStorage; /** * ONE MEETING RECORD PER READER PER TAB — read once, kept current by one * ref-counted channel (0.7.79). * * 🚨 THE DEFECT THIS CLOSES. `useMeetingRecord` held the bundle in its own * component state and read all four parts (`meet_participants`, `meet_notes`, * `meet_transcript_segments`, `meet_recordings`) in a mount effect. A host that * hides a view without unmounting its data — a board tile asleep under React * ``, which tears every effect down and runs it again on wake — read * the whole record again on every wake, on a Remove + Undo, and once more for * every second view of the same meeting (the record tab, the meeting's agent * surface, five "meeting notes" tiles). The view owned the data. * * THE SHAPE. One entry per (reader, meeting). The first view reads; every later * view — the same view after a wake or a remount included — renders the entry * and reads nothing. A read already in flight is joined. A forced read (Retry, * a reconnect) supersedes it, and an answer that lands after a newer read was * asked is DROPPED (the late-answer guard), so an old answer can never paint * over a newer one. * * LIVE WHILE HELD. A member's entry (a direct read, kept as its four parts) is * patched row by row from the database's change feed for the four tables, then * assembled again — one row, never four reads. The channel opens on the first * holder and closes `MEETING_RECORD_GRACE_MS` after the last one lets go, so a * wake or an Undo inside the grace costs nothing. Past the grace nothing * watched the meeting, so the entry is marked stale and the next view reads it * once. A guest's record arrives through the server as a finished bundle; it * has no change feed and is kept as it is. * * Framework-free on purpose: `react/hooks.ts` is one reader of it. */ /** How long a meeting's channel and kept record outlive the last view of it. */ declare const MEETING_RECORD_GRACE_MS = 60000; interface MeetingRecordFailure { readonly message: string; readonly remedy: string; } interface MeetingRecordState { readonly loading: boolean; readonly record: MeetingRecordBundle | null; /** A failure that stopped the WHOLE record loading (never kept as an answer). */ readonly failure: MeetingRecordFailure | null; } /** What a read produced: the four parts (patchable), or a finished bundle (a guest's). */ type MeetingRecordAnswer = { readonly kind: "parts"; readonly meeting: MeetingRecord; readonly parts: MeetingRecordParts; } | { readonly kind: "bundle"; readonly bundle: MeetingRecordBundle; }; /** One changed row, already projected by the repository. `null` + `removedId` = gone. */ type MeetingRecordChange = { readonly part: "transcript"; readonly row: MeetingRecordParts["transcript"][number] | null; readonly removedId?: string | undefined; } | { readonly part: "notes"; readonly row: MeetingRecordParts["notes"][number] | null; readonly removedId?: string | undefined; } | { readonly part: "attendance"; readonly row: MeetingRecordParts["attendees"][number] | null; readonly removedId?: string | undefined; } | { readonly part: "recording"; readonly row: MeetingRecordParts["recordings"][number] | null; readonly removedId?: string | undefined; }; interface MeetingRecordCache { /** The kept state for `key` (stable until it changes). */ state(key: string | null): MeetingRecordState; subscribe(listener: () => void): () => void; /** * Read unless the entry is kept and current (or a read is in flight — joined). * `force` reads anyway and supersedes a read in flight. Never rejects: a * failure is in the state. */ ensure(key: string, read: () => Promise, options?: { force?: boolean; }): Promise; /** The meeting row moved (a rename, it ended): assemble again — no read. */ setMeeting(key: string, meeting: MeetingRecord): void; /** Apply one changed row from the change feed — no read. */ apply(key: string, change: MeetingRecordChange): void; /** * Hold the live half: `open` on the first holder, its close a grace after the * last. `owner` is what the channel runs on (the realtime manager): a new * owner means the old one's channels are gone, so it opens again. */ hold(key: string, open: () => () => void, owner?: unknown): () => void; /** Tests only. */ reset(): void; } declare function createMeetingRecordCache(options?: { graceMs?: number; }): MeetingRecordCache; /** * The reaction ids every skin offers (CORE-DESIGN §5.3). The id IS the emoji * the wire carries; a skin may draw its own art for each, never a different set. */ declare const MEET_REACTIONS: readonly string[]; /** * COLLAB AND CAPTION LOGIC, HEADLESS (CORE-DESIGN §5.3, step S1). * * What used to live inside the React hooks of the in-room activities: which * activities a person has, when the Activities door deserves a dot, the act / * move-to-breakout calls with their sentences, the caption-translation work * queue, and the one-shot panel and language requests on the store. Pure or * store-shaped, so every skin reads the same answers; the `/react` hooks are * thin selectors over these. */ /** The activities this person has, in the order Meet lists them. */ declare function availableActivities(collab: CollabSnapshot | null, isHost: boolean): CollabPanel[]; /** Something worth a dot on the Activities button: a live poll unanswered, a new question for a host. */ declare function activityAttention(collab: CollabSnapshot | null, isHost: boolean): boolean; interface CollabRuntime { readonly inRoom: { collab(request: CollabRequest): Promise; loadCollab?(): Promise; }; readonly engine: { moveToBreakout(roomId: string | null): Promise; }; } /** Perform one act; resolves with the server's sentence, or its refusal (never throws). */ declare function performCollab(runtime: CollabRuntime | null, request: CollabRequest): Promise<{ ok: boolean; message: string; itemId?: string | null; }>; /** Go to a breakout room (`null` = the main room), with the sentence to show. */ declare function moveToRoom(runtime: CollabRuntime | null, roomId: string | null): Promise<{ ok: boolean; message: string; }>; interface CollabStoreLike { snapshot(): MeetSnapshot; patch(next: Partial): void; } /** Finished caption lines that still need a translation into `language`. */ declare function untranslatedLines(lines: readonly { id: string; text: string; final: boolean; }[], translations: Readonly>): { id: string; text: string; }[]; /** The next batch to send: never a line already asked for in this language, at most 20. */ declare function nextTranslationBatch(pending: readonly { id: string; text: string; }[], language: string, asked: Set): { id: string; text: string; }[]; /** The viewer chose a caption language (or `null` = as spoken): start over in it. */ declare function setCaptionLanguage(store: CollabStoreLike, next: string | null): void; /** Take (and clear) the one-shot "open this panel" request, if any. */ declare function takePanelRequest(store: CollabStoreLike): CollabPanel | null; /** Take (and clear) the one-shot "move to this breakout room" request, if any. */ declare function takeMoveRequest(store: CollabStoreLike): { roomId: string | null; } | null; /** * THE OBSERVATION CONTRACT, DERIVED FROM TODAY'S STORE (CORE-DESIGN §7 step S0). * * The real-browser scenario harness reads a meeting through a small set of * `data-meet-*` attributes (common-docs `meet/duplicates/HARNESS-CONTRACT.md`, * owned by the harness lane). This module turns the store's `MeetSnapshot` * into exactly those values — pure, framework-free, rendered ONCE by the * package (`useCoreRootProps()` / ``), never per skin. * * S0 rule: TODAY'S VALUES ONLY. Every value here is what the person actually * sees on today's screens, including today's defects — a `failed` or `left` * room renders the pre-join screen, so its phase is `prejoin`, and the harness * records that as the defect it is. A value the store does not know (host * presence before joining, autoplay blocking) is OMITTED rather than guessed: * an absent attribute reads as "unknown", a guessed one would lie. */ /** * `data-meet-role` is the HARNESS CONTRACT's vocabulary (host · cohost · participant · guest): * a link follower whose role is `participant` renders `guest` (CORE-DESIGN §2.1); `presenter` / * `viewer` render `participant` until the contract adopts them (§3.7 C3). */ type ObservedRole = "host" | "cohost" | "presenter" | "participant" | "guest"; /** `data-meet-phase` — the core's phase IS the contract value (S6a: no translation layer). */ type ObservedPhase = MeetPhase; type ObservedConnection = ConnectionRegion; interface MeetObservation { readonly phase: ObservedPhase; readonly reason: string | null; readonly role: ObservedRole | "none"; readonly connection: ObservedConnection; /** null = this tab does not know (it is not in the room). */ readonly hostPresent: boolean | null; readonly lobbyCount: number; readonly recording: boolean; /** null = today's store does not track autoplay blocking. */ readonly audioBlocked: boolean | null; /** null outside `joining` / `in_call` — no device is in play there today. */ readonly camera: string | null; readonly microphone: string | null; } interface ObserveInput { readonly snapshot: MeetSnapshot | null; /** The row the consumer resolved, when the store has none yet (the guest lane). */ readonly meeting?: MeetingRecord | null | undefined; } /** * The store's device state → the contract's `camera` / `microphone` value (CORE-DESIGN §3.2). * The reason comes from the media supervisor (S6c), which reads the browser's permission store: a * kind the browser denies is `unavailable:denied` even when nothing tried to open it. */ declare function observedDevice(track: LocalTrackState, permission?: PermissionReading, held?: "locked" | "muted_by_host" | null): string; /** * The phase the person is looking at: the store's phase, verbatim (CORE-DESIGN §3.1). The one * derivation left is the ended meeting: a row that says it is over renders `ended` for anyone not * in a live connection, whichever carrier delivered the ending first. */ declare function observedPhase(input: ObserveInput): ObservedPhase; declare function observe(input: ObserveInput): MeetObservation; /** A phase the consumer renders itself (link resolution, the guest-name step, a dead link). */ declare function staticObservation(phase: ObservedPhase, reason?: string | null): MeetObservation; /** The root element's attributes. Unknown values are omitted, never guessed. */ declare function rootAttributes(o: MeetObservation): Record; /** The contract's per-person connection vocabulary. `unknown` is not a reading. */ declare function observedQuality(quality: ConnectionQuality$1): string | null; /** `[data-meet-participant]` attributes for one person's tile or roster row. */ declare function participantAttributes(p: MeetParticipant): Record; /** The listen-only notice's contract code, from why the microphone is off. */ declare function microphoneNoticeCode(track: LocalTrackState, permission?: PermissionReading): "mic-blocked" | "device-lost" | null; /** * CG — WHAT THIS BROWSER CAN DO, probed before anything else (CORE-DESIGN §11 `browser-unsupported`, * `share-unsupported-mobile`). Pure over an injected scope so the rules are the tests; the web * reading is `detectCapabilities()` with no argument. Never touches LiveKit. */ type UnsupportedReason = "no_webrtc"; interface MeetCapabilities { /** False only when a call cannot run here at all (no WebRTC: an in-app webview, an old browser). */ readonly canJoin: boolean; readonly reason: UnsupportedReason | null; /** The in-app browser this looks like (Instagram, Facebook, LinkedIn…), or null. */ readonly inAppBrowser: string | null; /** A phone or tablet: no screen sharing from the browser there. */ readonly mobile: boolean; /** `getDisplayMedia` exists AND this is not a phone/tablet. */ readonly canShareScreen: boolean; } interface ScopeLike { readonly RTCPeerConnection?: unknown; readonly webkitRTCPeerConnection?: unknown; readonly navigator?: { readonly userAgent?: string; readonly maxTouchPoints?: number; readonly mediaDevices?: { readonly getDisplayMedia?: unknown; }; }; } declare function isMobileUserAgent(userAgent: string, maxTouchPoints?: number): boolean; declare function detectCapabilities(scope?: ScopeLike | null): MeetCapabilities; /** The words for a browser that cannot join: what to open instead. Short lines (layout, not prose). */ declare function unsupportedWords(capabilities: MeetCapabilities): { readonly title: string; readonly body: string; }; /** * MS — SPEAKING WHILE MUTED (CORE-DESIGN §11 `speaking-while-muted`). A pure detector fed the local * microphone level while the person muted themselves: sound that lasts `holdMs` (gaps shorter than * `gapMs` do not break it) fires ONCE, then stays quiet for `cooldownMs` so the nudge never nags. * The level source is the platform's (a browser analyzer in the web skin); the rule lives here. */ interface SpeakingWhileMutedOptions { /** Peak level 0..1 that counts as speech. */ readonly threshold?: number; /** How long speech must last before the nudge. */ readonly holdMs?: number; /** A pause shorter than this does not end the speech. */ readonly gapMs?: number; /** After a nudge, no second one for this long. */ readonly cooldownMs?: number; } interface SpeakingWhileMutedDetector { /** One level reading at time `at` (ms). True exactly when the nudge should show now. */ feed(level: number, at: number): boolean; /** The person unmuted (or left): start over, cooldown included. */ reset(): void; } declare function createSpeakingWhileMutedDetector(options?: SpeakingWhileMutedOptions): SpeakingWhileMutedDetector; export { ATTENDANCE_CSV_HEADER, type AnnounceResult, type ApplyRoomEventArgs, type AttendanceKind, type AttendanceRow, CALL_INVITE_KIND, CALL_INVITE_VERSION, CallCenter, CallInvite, type CallInviteHandlerOptions, type CallInvitePayload, CallMode, CallState, CollabPanel, CollabRequest, CollabResult, type CollabRuntime, CollabSnapshot, type CollabStoreLike, ConnectionQuality$1 as ConnectionQuality, ConnectionRegion, DEFAULT_PAGE_SIZE, DriverConnectionState, DriverParticipant, EffectsStorage, type FollowUpDraft, type FollowUpRecipient, type FollowUpSendResult, GUEST_RECORD_ACCESS_CHOICES, GUEST_RECORD_ACCESS_LEVELS, GUEST_RECORD_ACCESS_METADATA_KEY, GUEST_RECORD_NOT_SHARED, type GuestRecordAccess, type GuestRecordAccessResolution, type GuestRecordAccessSetting, type LiveKitDriverOptions, LocalTrackState, MEETING_INVITE_KIND, MEETING_INVITE_VERSION, MEETING_RECORD_GRACE_MS, MEET_EVENTS, MEET_REACTIONS, MeetApi, type MeetCapabilities, type MeetEvent, MeetIdentity, type MeetObservation, MeetParticipant, MeetPhase, MeetRepository, MeetRoomDriver, MeetSnapshot, MeetStore, MeetingAttendee, type MeetingChatHistory, MeetingId, type MeetingInviteHandlerOptions, type MeetingInvitePayload, MeetingNote, MeetingRecord, type MeetingRecordAnswer, type MeetingRecordBundle, type MeetingRecordCache, type MeetingRecordChange, type MeetingRecordFailure, type MeetingRecordParts, type MeetingRecordRestriction, type MeetingRecordRestrictionNotice, type MeetingRecordState, NameBook, type ObserveInput, type ObservedConnection, type ObservedPhase, OrganizationId, ParticipantIdentity, PermissionReading, REFUSED_ABSENCE, ROOM_EVENTS, ROOM_EVENT_PREFIX, RecordingRecord, RoomChatMessage, RoomDriverHandlers, type RoomEventOutcome, type RoomEventTopic, RoomName, RoomToken, type SpeakingWhileMutedDetector, type SpeakingWhileMutedOptions, type Stage, type StageArgs, StageLayout, type TranscriptBlock, type TranscriptFormat, type TranscriptMatch, TranscriptSegment, type UnsupportedReason, UserId, type WrapUpStatus, absenceSentence, activeWrapUpSummary, activityAttention, announceMeeting, applyRoomEvent, assembleMeetingRecord, attendanceCsv, attendanceReport, attendanceTotals, attendeeKey, availableActivities, callInviteAction, callInviteText, callStateLabel, captionSegment, clockPosition, computeStage, countLabel, createCallInviteHandler, createLiveKitDriver, createMeetingInviteHandler, createMeetingRecordCache, createSpeakingWhileMutedDetector, createWebEffectsStorage, detectCapabilities, gridColumns, groupTranscript, guestRecordAccessExplanation, guestRecordAccessLabel, guestRecordAccessSetting, hasEnded, isMobileUserAgent, isRoomEventTopic, loadFollowUpDraft, loadGuestMeetingRecord, loadMeetingChat, loadMeetingRecord, loadMeetingRecordParts, mapConnectionState, mapQuality, meetingInviteAction, meetingLength, meetingTopic, meetingWhen, mergeCaption, mergeRecord, microphoneNoticeCode, moveToRoom, nextTranslationBatch, normalizeSpoken, notesDocument, observe as observeMeeting, observedDevice, observedPhase, observedQuality, orderParticipants, participantAttributes, participantSummary, performCollab, playableRecordingFileId, pushMeetingToGoogleCalendar, readableTranscript, recordingNotice, recordingOffsetMs, roomTopic, rootAttributes, searchTranscript, sendFollowUp, setCaptionLanguage, setGuestRecordAccess, snapshotParticipant, staticObservation, summarizeMeetingRecordRestrictions, takeMoveRequest, takePanelRequest, transcriptDocument, uniqueAttendees, unsupportedWords, untranslatedLines, userTopic, wasRefused, wrapUpStatus };