@ai-matrx/meet 0.7.6 → 0.7.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -605,6 +605,55 @@ declare function createMeetApi(options: MeetApiOptions): MeetApi;
605
605
  /** `GET /files/{id}/download?inline=1` — the platform files route (R3). */
606
606
  declare function fileDownloadPath(fileId: string): string;
607
607
 
608
+ /**
609
+ * ONE error classification for the whole package (C22).
610
+ *
611
+ * The banned alternative is a host catching a LiveKit or PostgREST error and
612
+ * deciding for itself whether the camera is busy, the token expired, or the
613
+ * meeting is locked — which is how the same five-branch `if` gets copy-pasted
614
+ * into every consumer and drifts. Everything this package throws is a
615
+ * `MeetError` with a stable `code` and a `remedy` SENTENCE. Nothing fails
616
+ * silently, and nothing fails without saying what to do about it.
617
+ *
618
+ * The device codes exist because "it doesn't work" in a video product is almost
619
+ * always one of four specific, differently-remediable things: permission
620
+ * denied, no such device, device already in use by another app, or insecure
621
+ * context (http, where `getUserMedia` does not exist at all). A single
622
+ * "media-error" would make the UI say the wrong sentence three times out of
623
+ * four.
624
+ */
625
+ 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"
626
+ /** Meet wave 1: the meeting was cancelled — its link resolves, its room never opens. */
627
+ | "cancelled" | "unknown";
628
+ declare class MeetError extends Error {
629
+ readonly code: MeetErrorCode;
630
+ /** What to DO. Rendered to the user for the device codes. */
631
+ readonly remedy: string;
632
+ readonly cause?: unknown;
633
+ constructor(code: MeetErrorCode, message: string, remedy: string, cause?: unknown);
634
+ /** True when retrying after the host re-establishes a session is the fix. */
635
+ get isRetryable(): boolean;
636
+ /** True when the user, not the code, must act — the UI shows `remedy`. */
637
+ get isUserActionable(): boolean;
638
+ }
639
+ /**
640
+ * THE ONE PLACE a raw database/transport error becomes a typed one. Called at
641
+ * every repository boundary; nothing else in the package inspects a raw error,
642
+ * and no consumer should ever have to.
643
+ */
644
+ declare function normalizeMeetError(error: unknown, operation: string): MeetError;
645
+ /**
646
+ * THE DEVICE ERROR TRANSLATOR — the second-most-copied block in every video app.
647
+ *
648
+ * `getUserMedia` rejects with DOMException names, and every one of them means
649
+ * something a user could fix if we told them. This is the whole reason the
650
+ * device layer lives in the package (C22 test 3: less-than-perfect unless done
651
+ * perfectly).
652
+ */
653
+ declare function normalizeDeviceError(error: unknown, operation: string): MeetError;
654
+ /** A response whose SHAPE is wrong — the ingress boundary, not the wire. */
655
+ declare function invalidResponse(operation: string, detail: string): MeetError;
656
+
608
657
  /**
609
658
  * WHO SAID THAT — one answer, every surface, both lanes (MRI-A16).
610
659
  *
@@ -666,55 +715,6 @@ declare function learnNames(book: NameBook, entries: readonly {
666
715
  */
667
716
  declare function displayNameFor(book: NameBook, identity: ParticipantIdentity | string, preferred?: string | null | undefined): string;
668
717
 
669
- /**
670
- * ONE error classification for the whole package (C22).
671
- *
672
- * The banned alternative is a host catching a LiveKit or PostgREST error and
673
- * deciding for itself whether the camera is busy, the token expired, or the
674
- * meeting is locked — which is how the same five-branch `if` gets copy-pasted
675
- * into every consumer and drifts. Everything this package throws is a
676
- * `MeetError` with a stable `code` and a `remedy` SENTENCE. Nothing fails
677
- * silently, and nothing fails without saying what to do about it.
678
- *
679
- * The device codes exist because "it doesn't work" in a video product is almost
680
- * always one of four specific, differently-remediable things: permission
681
- * denied, no such device, device already in use by another app, or insecure
682
- * context (http, where `getUserMedia` does not exist at all). A single
683
- * "media-error" would make the UI say the wrong sentence three times out of
684
- * four.
685
- */
686
- 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"
687
- /** Meet wave 1: the meeting was cancelled — its link resolves, its room never opens. */
688
- | "cancelled" | "unknown";
689
- declare class MeetError extends Error {
690
- readonly code: MeetErrorCode;
691
- /** What to DO. Rendered to the user for the device codes. */
692
- readonly remedy: string;
693
- readonly cause?: unknown;
694
- constructor(code: MeetErrorCode, message: string, remedy: string, cause?: unknown);
695
- /** True when retrying after the host re-establishes a session is the fix. */
696
- get isRetryable(): boolean;
697
- /** True when the user, not the code, must act — the UI shows `remedy`. */
698
- get isUserActionable(): boolean;
699
- }
700
- /**
701
- * THE ONE PLACE a raw database/transport error becomes a typed one. Called at
702
- * every repository boundary; nothing else in the package inspects a raw error,
703
- * and no consumer should ever have to.
704
- */
705
- declare function normalizeMeetError(error: unknown, operation: string): MeetError;
706
- /**
707
- * THE DEVICE ERROR TRANSLATOR — the second-most-copied block in every video app.
708
- *
709
- * `getUserMedia` rejects with DOMException names, and every one of them means
710
- * something a user could fix if we told them. This is the whole reason the
711
- * device layer lives in the package (C22 test 3: less-than-perfect unless done
712
- * perfectly).
713
- */
714
- declare function normalizeDeviceError(error: unknown, operation: string): MeetError;
715
- /** A response whose SHAPE is wrong — the ingress boundary, not the wire. */
716
- declare function invalidResponse(operation: string, detail: string): MeetError;
717
-
718
718
  /**
719
719
  * LISTEN-ONLY ATTENDANCE — being IN the room is never contingent on a device.
720
720
  *
@@ -995,6 +995,17 @@ interface MeetSnapshot {
995
995
  readonly failed: boolean;
996
996
  readonly at: number;
997
997
  } | null;
998
+ /**
999
+ * "Message privately" asked for from ANY per-person menu — a tile's, the
1000
+ * People panel's — and answered by the one `ControlBar` that owns the chat
1001
+ * panel (0.7.8). Through 0.7.7 only the People panel could open a private
1002
+ * chat, so the Board's tile menu (and the Room's) had no such item while the
1003
+ * People panel did: two menus for one person that disagreed.
1004
+ */
1005
+ readonly privateChatRequest: {
1006
+ readonly to: ParticipantIdentity;
1007
+ readonly at: number;
1008
+ } | null;
998
1009
  readonly effects: MediaEffects;
999
1010
  /**
1000
1011
  * Captions AND durable rows, reconciled into one list (D16). A record
@@ -1342,6 +1353,10 @@ declare const RPCS: {
1342
1353
  readonly meetingOccurrences: "meet_meeting_occurrences";
1343
1354
  /** Every occurrence the reader can see between two instants — the Upcoming list. */
1344
1355
  readonly occurrencesBetween: "meet_occurrences_between";
1356
+ /** The recordings library. */
1357
+ readonly myRecordings: "meet_my_recordings";
1358
+ /** Titles, agendas, current notes and final transcript lines; snippets mark hits with ⟦ ⟧. */
1359
+ readonly search: "meet_search";
1345
1360
  };
1346
1361
  type Row = Record<string, unknown>;
1347
1362
  interface CreateCallInviteInput {
@@ -1428,6 +1443,17 @@ interface MeetRepository {
1428
1443
  acknowledgedAt: string;
1429
1444
  }): Promise<void>;
1430
1445
  recordings(meetingId: MeetingId): Promise<readonly RecordingRecord[]>;
1446
+ /** Meet wave 3: every recording of the reader's own meetings, newest first. */
1447
+ myRecordings(args?: {
1448
+ includeArchived?: boolean;
1449
+ limit?: number;
1450
+ }): Promise<readonly LibraryRecording[]>;
1451
+ /** Meet wave 3: search the reader's own meetings. */
1452
+ searchMeetings(query: string, limit?: number): Promise<readonly MeetingSearchHit[]>;
1453
+ /** Meet wave 3: name a recording (`null` = back to the meeting's title and date). */
1454
+ renameRecording(recordingId: string, title: string | null): Promise<void>;
1455
+ /** Meet wave 3: archive (soft delete) or restore a recording. Never a purge. */
1456
+ setRecordingArchived(recordingId: string, archived: boolean): Promise<void>;
1431
1457
  scheduleMeeting(input: ScheduleMeetingInput): Promise<MeetingRecord>;
1432
1458
  /** `expectedVersion` from the row you edited; a stale one is a `conflict`. */
1433
1459
  updateMeeting(meetingId: MeetingId, changes: MeetingChanges, options?: {
@@ -1521,6 +1547,50 @@ declare function settingsPatch(settings: MeetingSettings): Record<string, unknow
1521
1547
  */
1522
1548
  declare function changesPatch(changes: MeetingChanges): Record<string, unknown>;
1523
1549
  declare function createMeetRepository(options: RepositoryOptions): MeetRepository;
1550
+ /** One row of the recordings library (`meet_my_recordings`). */
1551
+ interface LibraryRecording {
1552
+ readonly id: string;
1553
+ readonly meetingId: MeetingId;
1554
+ readonly meetingTitle: string;
1555
+ readonly meetingSlug: string;
1556
+ /** The reader's strongest role on the meeting. */
1557
+ readonly role: "host" | "cohost" | "invitee" | "attendee";
1558
+ readonly state: RecordingState;
1559
+ /** A name a host gave it; null = show the meeting title. */
1560
+ readonly title: string | null;
1561
+ readonly fileId: string | null;
1562
+ readonly startedAt: string | null;
1563
+ readonly endedAt: string | null;
1564
+ readonly durationMs: number | null;
1565
+ readonly sizeBytes: number | null;
1566
+ readonly failureReason: string | null;
1567
+ readonly archivedAt: string | null;
1568
+ readonly organizationId: OrganizationId;
1569
+ }
1570
+ declare function projectLibraryRecording(row: Row): LibraryRecording;
1571
+ /** Where a search hit was found. */
1572
+ type MeetingSearchSource = "title" | "summary" | "decision" | "action_item" | "note" | "transcript";
1573
+ /** One hit from `meet_search`. `snippet` marks matched words with ⟦ ⟧. */
1574
+ interface MeetingSearchHit {
1575
+ readonly meetingId: MeetingId;
1576
+ readonly meetingTitle: string;
1577
+ readonly meetingSlug: string;
1578
+ readonly meetingAt: string | null;
1579
+ readonly source: MeetingSearchSource;
1580
+ /** The note or transcript segment id (the meeting id for a title hit). */
1581
+ readonly refId: string;
1582
+ readonly speaker: string | null;
1583
+ readonly snippet: string;
1584
+ /** When it was said / written. */
1585
+ readonly at: string | null;
1586
+ readonly rank: number;
1587
+ }
1588
+ declare function projectSearchHit(row: Row): MeetingSearchHit;
1589
+ /** A snippet split into plain and matched runs, for highlighting without HTML. */
1590
+ declare function snippetRuns(snippet: string): readonly {
1591
+ readonly text: string;
1592
+ readonly hit: boolean;
1593
+ }[];
1524
1594
 
1525
1595
  /**
1526
1596
  * THE CALL CENTER — ringing, answering, and the six ways a call ends badly.
@@ -2266,6 +2336,15 @@ interface MeetingSessionOptions {
2266
2336
  * to `engine.rejoin()`.
2267
2337
  */
2268
2338
  onAdmitted?: (() => void) | undefined;
2339
+ /**
2340
+ * 🚨 THE HOST DENIED *US*. Called once per denial verdict that names this
2341
+ * knocker. Without it the waiting room kept promising "you will join
2342
+ * automatically" to a person the host had already turned away (production
2343
+ * 2026-09-27). The provider wires it to `engine.rejoin()`: the server then
2344
+ * refuses the mint with its own sentence ("The host did not admit you…"),
2345
+ * so the words on screen are the server's decision, never the client's guess.
2346
+ */
2347
+ onDenied?: (() => void) | undefined;
2269
2348
  }
2270
2349
  interface MeetingSession {
2271
2350
  /** Subscribe to the room's out-of-band plane. Hosts see knocks; guests see verdicts. */
@@ -2357,39 +2436,6 @@ declare function createRecordingController(options: RecordingControllerOptions):
2357
2436
  /** The indicator's words. One definition, so no surface invents a softer one. */
2358
2437
  declare function recordingLabel(state: RecordingState): string | null;
2359
2438
 
2360
- /**
2361
- * MEET WAVE 1 — the two server calls a management screen makes (0.8).
2362
- *
2363
- * Every WRITE (schedule, edit, reschedule, cancel, archive, occurrence change,
2364
- * invite, answer) goes straight to the database through `MeetRepository` — the
2365
- * database decides who may. What a browser cannot do is send an email with a
2366
- * calendar object in it, so after any of those writes a screen calls
2367
- * `announceMeeting`: the server tells each invitee exactly what they have not
2368
- * been told (invitation, update or cancellation, each with its `.ics`), and
2369
- * re-queues reminders. It is idempotent — call it after every save.
2370
- */
2371
-
2372
- interface AnnounceResult {
2373
- readonly invitations: number;
2374
- readonly updates: number;
2375
- readonly cancellations: number;
2376
- readonly alreadyCurrent: number;
2377
- readonly remindersQueued: number;
2378
- readonly remindersCancelled: number;
2379
- }
2380
- /** Bring every invitee's inbox and calendar up to date. The host, a co-host or an editor. */
2381
- declare function announceMeeting(api: MeetApi, meetingId: MeetingId): Promise<AnnounceResult>;
2382
- /**
2383
- * Put a one-off meeting on the caller's own connected Google Calendar. GATED by
2384
- * the server to Google's review accounts while the Calendar scope is in review —
2385
- * a refusal is a `forbidden` MeetError carrying the server's sentence. Everyone
2386
- * already gets the meeting into any calendar through the invitation's `.ics`.
2387
- */
2388
- declare function pushMeetingToGoogleCalendar(api: MeetApi, meetingId: MeetingId, target: {
2389
- connectionId: string;
2390
- calendarId?: string;
2391
- }): Promise<Record<string, unknown>>;
2392
-
2393
2439
  /**
2394
2440
  * THE TOKEN FLOW — the ONLY authorization path into a room (R1).
2395
2441
  *
@@ -2546,6 +2592,12 @@ declare const MEET_ROUTES: {
2546
2592
  readonly meetingAnnounce: "/api/v1/meet/meetings/{meeting_id}/announce";
2547
2593
  /** Put a one-off meeting on the caller's own Google Calendar (gated by the server). */
2548
2594
  readonly meetingGoogleCalendar: "/api/v1/meet/meetings/{meeting_id}/google-calendar";
2595
+ /**
2596
+ * Meet wave 3: the recap email after the meeting. `GET` = the draft (subject,
2597
+ * body from the wrap-up, who it can go to); `POST` = send what the host
2598
+ * reviewed (`dry_run` renders and queues nothing). Never sent automatically.
2599
+ */
2600
+ readonly meetingFollowUp: "/api/v1/meet/meetings/{meeting_id}/follow-up";
2549
2601
  /**
2550
2602
  * Meet wave 2: EVERY in-meeting host power (mute, stop video, mute all, ask to
2551
2603
  * unmute, spotlight, lower hands, co-host, hand over, rename, remove, report).
@@ -2564,56 +2616,533 @@ declare function parseRoomToken(body: unknown, roomName: RoomName): RoomToken;
2564
2616
  declare function createTokenClient(options: TokenClientOptions): TokenClient;
2565
2617
 
2566
2618
  /**
2567
- * THE ROOM ENGINE — the object a host mounts once and never reasons about.
2619
+ * THE MEETING RECORD — what a meeting IS once the room is gone (MRI-D2).
2568
2620
  *
2569
- * It owns every sequence a calling product gets wrong: ask for a token, connect,
2570
- * project the roster, carry the in-room control plane, keep the phase honest
2571
- * through a reconnect, and tear everything down exactly once. A consumer's job
2572
- * is `engine.join(...)` and rendering a snapshot.
2621
+ * 🚨 THE DEFECT THIS EXISTS TO KILL. Through 2026-09-08 a meeting produced a
2622
+ * wrap-up summary, decisions, owner-assigned action items, five windows of live
2623
+ * notes, thirty-two transcript segments and a recording — and then had NO
2624
+ * SURFACE AT ALL. `/meet/<slug>` fell back to `<PreJoin>` on an ended meeting,
2625
+ * so the durable link that R3 promises would outlive the meeting resolved to a
2626
+ * screen offering to join a room that no longer exists. Everything the campaign
2627
+ * built after the room closed was reachable only from SQL.
2573
2628
  *
2574
- * THREE rules are structural here, not documented-and-hoped:
2629
+ * Three rules hold this file together:
2575
2630
  *
2576
- * 1. **The phase never lies.** `connecting` becomes `connected` only when the
2577
- * driver says so, and any failure becomes `failed` WITH the remedy attached
2578
- * to the snapshot. There is no path that leaves a spinner running forever —
2579
- * the single most common complaint about every video product.
2580
- * 2. **Role comes from the TOKEN, never from the client.** A participant's role
2581
- * is read out of the server-minted metadata. A client that could decide it
2582
- * was host could mute anyone; here the claim is unforgeable because the
2583
- * server signed it.
2584
- * 3. **Teardown is idempotent and total.** Leaving twice, or unmounting during
2585
- * a connect, disconnects once and leaves no listener behind. The half-torn
2586
- * room is what produces "my camera light stays on after the call".
2631
+ * 1. **Every read is INDEPENDENT and every refusal is NAMED.** The transcript,
2632
+ * the notes, the attendance list and the recording live in four RLS-guarded
2633
+ * tables, and a guest holding the link can read NONE of them (only
2634
+ * `meet_meeting_by_slug` is granted to `anon`). One `Promise.all` would turn
2635
+ * a permission boundary into a blank page. Known guest restrictions never issue private reads; each authenticated read that is refused
2636
+ * becomes a `MeetingRecordRestriction` with a sentence — the reader is told
2637
+ * what exists and why they cannot see it, which is the difference between a
2638
+ * record they can ask for and a product that looks broken.
2639
+ * 2. **A partial record is still a record.** A failed transcript read never
2640
+ * hides the summary somebody CAN read.
2641
+ * 3. **Nothing here computes meaning.** Grouping, ordering and formatting only.
2642
+ * The summary, decisions and action items are durable rows written by the
2643
+ * wrap-up mandate (D5/D18); this module never re-derives them from the
2644
+ * transcript, because two sources for one fact is how they start to disagree.
2587
2645
  */
2588
2646
 
2589
- interface JoinRequest {
2590
- readonly roomName: RoomName;
2591
- readonly meetingId?: string | undefined;
2592
- readonly slug?: string | undefined;
2593
- /** `audio` joins with the camera off. Same room, same code path (D3). */
2594
- readonly mode: CallMode;
2647
+ /** One thing the reader could not see, and the honest reason. Never a blank. */
2648
+ interface MeetingRecordRestriction {
2649
+ /** Which part of the record: `transcript`, `notes`, `attendance`, `recording`. */
2650
+ readonly part: "transcript" | "notes" | "attendance" | "recording";
2651
+ readonly message: string;
2652
+ readonly remedy: string;
2653
+ }
2654
+ /** One visible access state, made from restrictions that share one remedy. */
2655
+ interface MeetingRecordRestrictionNotice {
2656
+ readonly parts: readonly MeetingRecordRestriction["part"][];
2657
+ readonly message: string;
2658
+ readonly remedy: string;
2659
+ }
2660
+ /** A run of consecutive lines by one speaker — how a person reads a transcript. */
2661
+ interface TranscriptBlock {
2662
+ readonly id: string;
2663
+ /** Who spoke. Blocks break on THIS, never on the rendered name (MRI-A16). */
2664
+ readonly identity: ParticipantIdentity;
2665
+ readonly speaker: string;
2666
+ /** ISO of the first line in the block. */
2667
+ readonly startedAt: string;
2668
+ readonly lines: readonly TranscriptSegment[];
2669
+ }
2670
+ interface MeetingRecordBundle {
2671
+ readonly meeting: MeetingRecord;
2672
+ readonly attendees: readonly MeetingAttendee[];
2673
+ /** The wrap-up summary: `kind='note'` with `metadata.role='summary'` (D5). */
2674
+ readonly summary: MeetingNote | null;
2675
+ /** D20: the AUTHORITATIVE set — window rows the wrap-up superseded are out. */
2676
+ readonly decisions: readonly MeetingNote[];
2677
+ readonly actionItems: readonly MeetingNote[];
2678
+ /** The live-notes windows, in window order (D3). */
2679
+ readonly liveNotes: readonly MeetingNote[];
2680
+ readonly transcript: readonly TranscriptSegment[];
2681
+ /** The most recent recording attempt, whatever state it reached. */
2682
+ readonly recording: RecordingRecord | null;
2683
+ /** What this reader could NOT see. Rendered; never swallowed. */
2684
+ readonly restrictions: readonly MeetingRecordRestriction[];
2595
2685
  /**
2596
- * Whether to ATTEMPT the microphone on join. Defaults to true. `false` is a
2597
- * deliberate listen-only join — the pre-join screen's "Join with microphone"
2598
- * toggle. Either way the join itself never depends on the outcome; a refused
2599
- * device lands in `snapshot.media.microphone.unavailable`, not in a throw.
2686
+ * Identity → name, learned from everything this read COULD see: the durable
2687
+ * attendance rows and the transcript's own `speaker` column. It is what stops
2688
+ * a raw LiveKit token reaching the screen when one of those reads was refused
2689
+ * (MRI-A16).
2600
2690
  */
2601
- readonly microphone?: boolean | undefined;
2691
+ readonly names: NameBook;
2602
2692
  }
2603
- interface RoomEngineOptions {
2604
- store: MeetStore;
2605
- tokens: TokenClient;
2606
- identity: MeetIdentity;
2607
- driverFactory: RoomDriverFactory;
2608
- /** Canonical public metadata read; room packets alone never authorize teardown. */
2609
- resolveMeeting?: ((slug: string) => Promise<MeetingRecord>) | undefined;
2610
- onDiagnostic?: ((event: {
2611
- level: "info" | "warn" | "error";
2612
- message: string;
2613
- remedy?: string;
2614
- }) => void) | undefined;
2615
- /** Fired when the local participant leaves for any reason, including kicked. */
2616
- onLeft?: ((reason: string | null) => void) | undefined;
2693
+ /**
2694
+ * The current wrap-up summary. Repairs preserve the prior durable row for
2695
+ * history, then mark it `superseded_by`; every current-facing projection must
2696
+ * therefore make the same distinction as decisions and action items.
2697
+ */
2698
+ declare function activeWrapUpSummary(notes: readonly MeetingNote[]): MeetingNote | null;
2699
+ /**
2700
+ * Load the record parts the reader may request; state all restrictions.
2701
+ *
2702
+ * `meeting` is passed in when the caller already resolved it (the slug route
2703
+ * does, through the `anon`-granted RPC) — re-reading it through the table would
2704
+ * fail for exactly the reader who most needs the page to work.
2705
+ */
2706
+ declare function loadMeetingRecord(repository: MeetRepository, meetingId: MeetingId, meeting: MeetingRecord, reader?: Pick<MeetIdentity, "userId">): Promise<MeetingRecordBundle>;
2707
+ /**
2708
+ * 🚨 **A READ THAT WAS REFUSED IS NOT AN EMPTY RESULT (MRI-A16).**
2709
+ *
2710
+ * On production 2026-09-09 a guest's record view said *"No wrap-up was written
2711
+ * for this meeting"* directly underneath a panel that had just told them, in
2712
+ * the same screen, that the notes are not readable from their account — and
2713
+ * *"Nothing was recorded as a decision"* beside a decision that exists
2714
+ * (MRI-C8). The page asserted an absence it had no way to know, which is worse
2715
+ * than saying nothing: a person who reads "no wrap-up was written" stops
2716
+ * looking for one.
2717
+ *
2718
+ * So no surface writes its own empty sentence any more. It asks here, and gets
2719
+ * either its absence sentence — earned, because the read SUCCEEDED and returned
2720
+ * nothing — or the refusal, said plainly.
2721
+ */
2722
+ declare const REFUSED_ABSENCE = "This record is not shared with guests.";
2723
+ /** Was this part of the record refused to this reader? */
2724
+ declare function wasRefused(bundle: MeetingRecordBundle, part: MeetingRecordRestriction["part"]): boolean;
2725
+ /**
2726
+ * The sentence to show where a part of the record has nothing in it. NEVER an
2727
+ * absence claim over a read that was refused.
2728
+ */
2729
+ declare function absenceSentence(bundle: MeetingRecordBundle, part: MeetingRecordRestriction["part"], absence: string): string;
2730
+ /**
2731
+ * 🚨 **"NO WRAP-UP WAS WRITTEN" IS A CLAIM, AND FOR ~33 SECONDS IT WAS FALSE.**
2732
+ *
2733
+ * On production 2026-09-09 the HOST's record view said *"No wrap-up was written
2734
+ * for this meeting"* between 04:46:42 — the flip from the room to the record —
2735
+ * and 04:47:15, when the wrap-up landed. The read was permitted and genuinely
2736
+ * empty, so this is not the refusal class above; it is the OTHER way a screen
2737
+ * lies, by reading "not here yet" as "never happened". A person who reads *"no
2738
+ * wrap-up was written"* closes the tab.
2739
+ *
2740
+ * The meeting row already knows the difference, and it is not a guess: the
2741
+ * wrap-up is a durable job, `enqueue_wrap_up` stamps
2742
+ * `metadata.note_taker.intelligence.wrap_up` when it queues it, and the graph
2743
+ * action stamps `state: "failed"` when the pass refuses TERMINALLY. So:
2744
+ *
2745
+ * - a summary exists → `written`;
2746
+ * - the notes read was refused → `refused` (never an absence claim);
2747
+ * - the job is marked failed → `failed`, in the server's own words;
2748
+ * - the job is queued or running → `in-progress`, with a refresh affordance;
2749
+ * - nothing ever queued one → `absent`, which is the only EARNED absence.
2750
+ *
2751
+ * There is no timeout that flips `in-progress` into `absent`. A wrap-up that is
2752
+ * genuinely stuck says it is still being written — which is TRUE — rather than
2753
+ * inventing a verdict this browser has no way to reach.
2754
+ */
2755
+ type WrapUpStatus = {
2756
+ readonly state: "written";
2757
+ } | {
2758
+ readonly state: "in-progress";
2759
+ readonly message: string;
2760
+ readonly canRefresh: true;
2761
+ } | {
2762
+ readonly state: "failed";
2763
+ readonly message: string;
2764
+ } | {
2765
+ readonly state: "refused";
2766
+ readonly message: string;
2767
+ } | {
2768
+ readonly state: "absent";
2769
+ readonly message: string;
2770
+ };
2771
+ /** Where the wrap-up actually is. Never "absent" over a job that is running. */
2772
+ declare function wrapUpStatus(bundle: MeetingRecordBundle): WrapUpStatus;
2773
+ /**
2774
+ * GROUP THE TRANSCRIPT THE WAY A PERSON READS IT: consecutive lines by one
2775
+ * speaker become one block with one timestamp. Reading thirty-two rows each
2776
+ * re-stamped with a name and a clock is how a transcript becomes unreadable.
2777
+ */
2778
+ declare function groupTranscript(segments: readonly TranscriptSegment[], names?: NameBook): readonly TranscriptBlock[];
2779
+ /**
2780
+ * WHAT THE RECORDING IS DOING, in a sentence — including the two states people
2781
+ * hit most and understand least. `processing` is not "broken" and `failed` is
2782
+ * never silence: both say what happened and what to do.
2783
+ */
2784
+ declare function recordingNotice(recording: RecordingRecord | null): string | null;
2785
+ /** Only an `available` recording has a file to play. Nothing else pretends to. */
2786
+ declare function playableRecordingFileId(recording: RecordingRecord | null): string | null;
2787
+ /**
2788
+ * A MEETING IS OVER WHEN THE SERVER SAYS SO. `ended_at` is stamped by
2789
+ * `POST /v1/meet/end` or by the auto-end evaluation (MRI-A6) — never by a
2790
+ * client noticing the room is empty.
2791
+ */
2792
+ declare function hasEnded(meeting: MeetingRecord | null): boolean;
2793
+ /**
2794
+ * "Thursday, 8 September 2026, 4:21 PM · 19 minutes". One shape everywhere.
2795
+ *
2796
+ * THE SPAN IS `formatDurationMs`'s (2026-09-12). This body used to divide by
2797
+ * `60_000` and pluralise "minute" itself — a duration formatter living in a
2798
+ * PACKAGE, found the day `aidream/apps/shared` became a scan root. `@ai-matrx/
2799
+ * meet` already depends on `@ai-matrx/kit`, and `style: "long"` is precisely
2800
+ * this voice ("19 minutes", "1 minute"). Three things improve by adopting it:
2801
+ * a 40-second meeting read "0 minutes" and now reads "40 seconds" (the
2802
+ * confident zero the package refuses), a 19-minute-35-second meeting read
2803
+ * "20 minutes" and now reads "19 minutes" (prose asserts at-least, so it
2804
+ * floors), and a 90-minute meeting reads "1 hour" instead of "90 minutes".
2805
+ */
2806
+ declare function meetingWhen(meeting: MeetingRecord, locale?: string): string;
2807
+ /** How much of a finished meeting a GUEST may read. Pinned with the server. */
2808
+ type GuestRecordAccess = "none" | "summary" | "full";
2809
+ /** Widest last. The ORDER is the comparison, exactly as it is on the server. */
2810
+ declare const GUEST_RECORD_ACCESS_LEVELS: readonly GuestRecordAccess[];
2811
+ /**
2812
+ * What a HOST may choose for one meeting. `default` is not a fourth level — it
2813
+ * is the absence of a per-meeting choice, and it is the only way to un-choose.
2814
+ */
2815
+ type GuestRecordAccessSetting = "default" | GuestRecordAccess;
2816
+ declare const GUEST_RECORD_ACCESS_CHOICES: readonly GuestRecordAccessSetting[];
2817
+ /** `meet_meetings.metadata.guest_record_access`. No DDL; a setting on a row. */
2818
+ declare const GUEST_RECORD_ACCESS_METADATA_KEY = "guest_record_access";
2819
+ /** What a host chose for THIS meeting, read from the row they already have. */
2820
+ declare function guestRecordAccessSetting(meeting: MeetingRecord | null): GuestRecordAccessSetting;
2821
+ /** The sentence on the host's control. Plain English, never a token. */
2822
+ declare function guestRecordAccessLabel(setting: GuestRecordAccessSetting): string;
2823
+ /** What each choice actually means for the people who were in the room. */
2824
+ declare function guestRecordAccessExplanation(setting: GuestRecordAccessSetting): string;
2825
+ /** What came back from the server, so a surface never has to infer a level. */
2826
+ interface GuestRecordAccessResolution {
2827
+ readonly level: GuestRecordAccess;
2828
+ /** `meeting` when a host chose for this one; `organization` otherwise. */
2829
+ readonly source: "meeting" | "organization";
2830
+ }
2831
+ /**
2832
+ * Repeated policy refusals are one access state, not four alarming cards.
2833
+ *
2834
+ * The individual rows remain durable data for absence handling in their own
2835
+ * sections. This projection groups only rows with the exact same remedy, so a
2836
+ * distinct transport or integrity failure remains visible on its own.
2837
+ */
2838
+ declare function summarizeMeetingRecordRestrictions(restrictions: readonly MeetingRecordRestriction[]): readonly MeetingRecordRestrictionNotice[];
2839
+ /**
2840
+ * Read the record the server shares with THIS guest.
2841
+ *
2842
+ * `roomToken` is the pass this device kept from the meeting
2843
+ * (`TokenClient.remembered`). It is presented in a header, never appended to
2844
+ * the URL. A guest with no pass, an expired pass, or a level of `none` gets the
2845
+ * server's refusal — as restrictions on a real bundle, so the screen says what
2846
+ * it cannot show instead of collapsing into an error page.
2847
+ */
2848
+ declare function loadGuestMeetingRecord(args: {
2849
+ api: MeetApi;
2850
+ repository: MeetRepository;
2851
+ meetingId: MeetingId;
2852
+ meeting: MeetingRecord;
2853
+ roomToken: RoomToken | null;
2854
+ }): Promise<MeetingRecordBundle>;
2855
+ /**
2856
+ * A HOST chooses what guests may read of THIS meeting.
2857
+ *
2858
+ * Host power is the SERVER's decision and it re-checks it; this function exists
2859
+ * so the control has one call to make, and it returns what the choice RESOLVES
2860
+ * to so the screen never has to guess what "my organization's setting" means
2861
+ * today.
2862
+ */
2863
+ declare function setGuestRecordAccess(args: {
2864
+ api: MeetApi;
2865
+ meetingId: MeetingId;
2866
+ organizationId: OrganizationId;
2867
+ access: GuestRecordAccessSetting;
2868
+ }): Promise<{
2869
+ readonly setting: GuestRecordAccessSetting;
2870
+ readonly effective: GuestRecordAccessResolution;
2871
+ }>;
2872
+ /** What a guest is told when the level shares nothing. Rendered, never hidden. */
2873
+ declare const GUEST_RECORD_NOT_SHARED: Readonly<Record<"recording" | "transcript" | "notes" | "attendance", string>>;
2874
+
2875
+ /**
2876
+ * AFTER THE MEETING (Meet wave 3) — the pure pieces every client needs once the
2877
+ * room is gone: the attendance report, the transcript and notes as documents,
2878
+ * a moment in the transcript as a moment in the recording, search within one
2879
+ * transcript, and the two server calls behind the recap email and the saved
2880
+ * chat.
2881
+ *
2882
+ * Nothing here computes MEANING (same rule as `record.ts`): the summary,
2883
+ * decisions and action items are the wrap-up's durable rows; this file only
2884
+ * arranges, measures and formats them. Every function is framework-free so a
2885
+ * worker, a test and a browser produce byte-identical exports.
2886
+ */
2887
+
2888
+ /**
2889
+ * Who someone was, in the words an attendance report uses. `signed-in guest` is
2890
+ * an account from outside the meeting's organization that followed the link;
2891
+ * `guest` has no account at all.
2892
+ */
2893
+ type AttendanceKind = "host" | "cohost" | "member" | "signed-in guest" | "guest" | "assistant";
2894
+ interface AttendanceRow {
2895
+ readonly id: string;
2896
+ readonly identity: string;
2897
+ readonly name: string;
2898
+ readonly userId: string | null;
2899
+ readonly kind: AttendanceKind;
2900
+ /** First time they were in the room; null when they knocked and never came in. */
2901
+ readonly joinedAt: string | null;
2902
+ /** When they left; null while still in the room or when the leave was never recorded. */
2903
+ readonly leftAt: string | null;
2904
+ /**
2905
+ * Time in the room. A missing `leftAt` is measured to the meeting's end —
2906
+ * the room closed around them — and `null` only when neither is known.
2907
+ */
2908
+ readonly durationMs: number | null;
2909
+ /** True when `durationMs` was measured to the meeting's end, not a recorded leave. */
2910
+ readonly durationToEnd: boolean;
2911
+ readonly admission: MeetingAttendee["admissionState"];
2912
+ /** How many times this person came in (a rejoin or a second device). */
2913
+ readonly joins: number;
2914
+ }
2915
+ /**
2916
+ * The attendance report of ONE meeting, host first then by arrival. People who
2917
+ * knocked and were never let in are kept (with no join time) — "who asked to
2918
+ * come in" is part of the record — but a denied knock has no duration.
2919
+ */
2920
+ /**
2921
+ * WHO a participant row is, as a PERSON (verifier, 2026-09-27: an export listed
2922
+ * the same attendee twice under "attended"). A signed-in person is their
2923
+ * account — every device and every rejoin is one person. A guest without an
2924
+ * account gets a fresh `guest:<random>` identity on every join, so the name
2925
+ * they typed is the only thread between their joins (Zoom's attendee report
2926
+ * merges link guests the same way); two different accountless guests who
2927
+ * typed the identical name read as one, which is the lesser error.
2928
+ */
2929
+ declare function attendeeKey(person: MeetingAttendee, names?: NameBook): string;
2930
+ /**
2931
+ * One row per PERSON, in first-seen order: the row that actually joined (the
2932
+ * earliest) represents them. For a list of names or a head-count; the
2933
+ * attendance REPORT merges times as well (`attendanceReport`).
2934
+ */
2935
+ declare function uniqueAttendees(attendees: readonly MeetingAttendee[], names?: NameBook): readonly MeetingAttendee[];
2936
+ declare function attendanceReport(meeting: MeetingRecord, attendees: readonly MeetingAttendee[], names?: NameBook): readonly AttendanceRow[];
2937
+ /** Headline numbers for the top of a report. The assistant is never a person. */
2938
+ declare function attendanceTotals(rows: readonly AttendanceRow[]): {
2939
+ readonly people: number;
2940
+ readonly guests: number;
2941
+ readonly neverAdmitted: number;
2942
+ readonly averageMs: number | null;
2943
+ };
2944
+ declare const ATTENDANCE_CSV_HEADER: readonly ["Meeting", "Meeting started", "Name", "Attended as", "Joined", "Left", "Minutes in the meeting", "Admission"];
2945
+ /**
2946
+ * CSV for a spreadsheet: one header row, one row per person, ISO timestamps (a
2947
+ * spreadsheet parses them; a formatted local clock loses the zone). Several
2948
+ * meetings — a series — concatenate with one header.
2949
+ */
2950
+ declare function attendanceCsv(reports: readonly {
2951
+ meeting: MeetingRecord;
2952
+ rows: readonly AttendanceRow[];
2953
+ }[]): string;
2954
+ /**
2955
+ * Where an instant of the meeting falls in the RECORDING, in milliseconds from
2956
+ * its start — or `null` when the recording does not cover it (a recording is
2957
+ * often a few minutes of a longer meeting). Never a clamp: seeking a line that
2958
+ * was said before recording began to 0:00 would play the wrong words.
2959
+ */
2960
+ declare function recordingOffsetMs(at: string, recording: RecordingRecord | null): number | null;
2961
+ /** `m:ss` or `h:mm:ss` — a position in a recording or a meeting. */
2962
+ declare function clockPosition(offsetMs: number): string;
2963
+ interface TranscriptMatch {
2964
+ readonly segmentId: string;
2965
+ /** [start, end) character ranges inside the segment's text, in order. */
2966
+ readonly ranges: readonly (readonly [number, number])[];
2967
+ }
2968
+ /**
2969
+ * Case-insensitive search of the transcript text AND the speaker's
2970
+ * name. Returns matches in transcript order. An empty query matches nothing.
2971
+ */
2972
+ declare function searchTranscript(segments: readonly TranscriptSegment[], query: string, names?: NameBook): readonly TranscriptMatch[];
2973
+ type TranscriptFormat = "txt" | "md" | "vtt";
2974
+ interface ExportOptions {
2975
+ readonly names?: NameBook;
2976
+ /** Timestamps as a clock of day (default) or offsets from `zero`. */
2977
+ readonly locale?: string;
2978
+ readonly timeZone?: string;
2979
+ }
2980
+ /**
2981
+ * The transcript as a document. `txt` and `md` group consecutive lines by one
2982
+ * speaker with the clock of day; `vtt` is WebVTT cued from the recording's
2983
+ * start when a recording exists (so it plays as captions over it), otherwise
2984
+ * from the meeting's start — and a cue ends where the next line begins.
2985
+ */
2986
+ declare function transcriptDocument(bundle: Pick<MeetingRecordBundle, "meeting" | "transcript" | "recording" | "names">, format: TranscriptFormat, options?: ExportOptions): string;
2987
+ /**
2988
+ * The meeting's notes as Markdown: when, who attended, the summary, decisions
2989
+ * and action items (with owners), and the link back. What a person pastes into
2990
+ * a doc or an email; also the source for PDF / Word export.
2991
+ */
2992
+ declare function notesDocument(bundle: Pick<MeetingRecordBundle, "meeting" | "summary" | "decisions" | "actionItems" | "attendees" | "names">, options?: {
2993
+ readonly link?: string | null;
2994
+ readonly when?: string | null;
2995
+ }): string;
2996
+ /** How long the meeting ran, in words ("42 minutes"), or null when unknown. */
2997
+ declare function meetingLength(meeting: MeetingRecord): string | null;
2998
+ interface MeetingChatHistory {
2999
+ readonly messages: readonly RoomChatMessage[];
3000
+ /** The server's refusal, in its words, when the history could not be read. */
3001
+ readonly refusal: {
3002
+ readonly message: string;
3003
+ readonly remedy: string;
3004
+ } | null;
3005
+ }
3006
+ /**
3007
+ * The saved chat of a finished meeting (`GET /v1/meet/chat`). Private messages
3008
+ * are never saved, by design, so they are never here. A refusal is returned as
3009
+ * data — the record still renders everything else.
3010
+ */
3011
+ declare function loadMeetingChat(api: MeetApi, meetingId: MeetingId, roomToken?: string | null): Promise<MeetingChatHistory>;
3012
+ /** One person the recap can go to, as the server resolved them. */
3013
+ interface FollowUpRecipient {
3014
+ /** Stable key for the choice: `user:<id>` or `email:<address>`. */
3015
+ readonly key: string;
3016
+ readonly name: string;
3017
+ /** The address, when this reader may see it (the host sees their guests'). */
3018
+ readonly email: string | null;
3019
+ readonly userId: string | null;
3020
+ /** `invitee`, `attendee` or both — where the server found them. */
3021
+ readonly sources: readonly ("invitee" | "attendee")[];
3022
+ /** False when the server has no way to reach them (no account, no address). */
3023
+ readonly reachable: boolean;
3024
+ }
3025
+ /** The draft the host reviews. The text is the wrap-up's own, arranged. */
3026
+ interface FollowUpDraft {
3027
+ readonly subject: string;
3028
+ readonly body: string;
3029
+ readonly recipients: readonly FollowUpRecipient[];
3030
+ /** False while the wrap-up has not been written — the draft says so. */
3031
+ readonly wrapUpWritten: boolean;
3032
+ /** When a recap for this meeting was last sent, and to how many. */
3033
+ readonly lastSent: {
3034
+ readonly at: string;
3035
+ readonly count: number;
3036
+ } | null;
3037
+ }
3038
+ interface FollowUpSendResult {
3039
+ readonly dryRun: boolean;
3040
+ /** Recipients the send queued (or, on a dry run, would queue). */
3041
+ readonly queued: readonly string[];
3042
+ /** Recipient keys skipped, each with the reason. */
3043
+ readonly skipped: readonly {
3044
+ readonly key: string;
3045
+ readonly reason: string;
3046
+ }[];
3047
+ }
3048
+ /** The recap draft for a finished meeting. The host, a co-host or an editor. */
3049
+ declare function loadFollowUpDraft(api: MeetApi, meetingId: MeetingId): Promise<FollowUpDraft>;
3050
+ /**
3051
+ * Send the recap the host reviewed. `dryRun` resolves every recipient and
3052
+ * renders every email but queues nothing — how a test proves the path without
3053
+ * mailing a real inbox. Never called automatically.
3054
+ */
3055
+ declare function sendFollowUp(api: MeetApi, meetingId: MeetingId, input: {
3056
+ readonly subject: string;
3057
+ readonly body: string;
3058
+ readonly recipientKeys: readonly string[];
3059
+ readonly dryRun?: boolean;
3060
+ }): Promise<FollowUpSendResult>;
3061
+
3062
+ /**
3063
+ * MEET WAVE 1 — the two server calls a management screen makes (0.8).
3064
+ *
3065
+ * Every WRITE (schedule, edit, reschedule, cancel, archive, occurrence change,
3066
+ * invite, answer) goes straight to the database through `MeetRepository` — the
3067
+ * database decides who may. What a browser cannot do is send an email with a
3068
+ * calendar object in it, so after any of those writes a screen calls
3069
+ * `announceMeeting`: the server tells each invitee exactly what they have not
3070
+ * been told (invitation, update or cancellation, each with its `.ics`), and
3071
+ * re-queues reminders. It is idempotent — call it after every save.
3072
+ */
3073
+
3074
+ interface AnnounceResult {
3075
+ readonly invitations: number;
3076
+ readonly updates: number;
3077
+ readonly cancellations: number;
3078
+ readonly alreadyCurrent: number;
3079
+ readonly remindersQueued: number;
3080
+ readonly remindersCancelled: number;
3081
+ }
3082
+ /** Bring every invitee's inbox and calendar up to date. The host, a co-host or an editor. */
3083
+ declare function announceMeeting(api: MeetApi, meetingId: MeetingId): Promise<AnnounceResult>;
3084
+ /**
3085
+ * Put a one-off meeting on the caller's own connected Google Calendar. GATED by
3086
+ * the server to Google's review accounts while the Calendar scope is in review —
3087
+ * a refusal is a `forbidden` MeetError carrying the server's sentence. Everyone
3088
+ * already gets the meeting into any calendar through the invitation's `.ics`.
3089
+ */
3090
+ declare function pushMeetingToGoogleCalendar(api: MeetApi, meetingId: MeetingId, target: {
3091
+ connectionId: string;
3092
+ calendarId?: string;
3093
+ }): Promise<Record<string, unknown>>;
3094
+
3095
+ /**
3096
+ * THE ROOM ENGINE — the object a host mounts once and never reasons about.
3097
+ *
3098
+ * It owns every sequence a calling product gets wrong: ask for a token, connect,
3099
+ * project the roster, carry the in-room control plane, keep the phase honest
3100
+ * through a reconnect, and tear everything down exactly once. A consumer's job
3101
+ * is `engine.join(...)` and rendering a snapshot.
3102
+ *
3103
+ * THREE rules are structural here, not documented-and-hoped:
3104
+ *
3105
+ * 1. **The phase never lies.** `connecting` becomes `connected` only when the
3106
+ * driver says so, and any failure becomes `failed` WITH the remedy attached
3107
+ * to the snapshot. There is no path that leaves a spinner running forever —
3108
+ * the single most common complaint about every video product.
3109
+ * 2. **Role comes from the TOKEN, never from the client.** A participant's role
3110
+ * is read out of the server-minted metadata. A client that could decide it
3111
+ * was host could mute anyone; here the claim is unforgeable because the
3112
+ * server signed it.
3113
+ * 3. **Teardown is idempotent and total.** Leaving twice, or unmounting during
3114
+ * a connect, disconnects once and leaves no listener behind. The half-torn
3115
+ * room is what produces "my camera light stays on after the call".
3116
+ */
3117
+
3118
+ interface JoinRequest {
3119
+ readonly roomName: RoomName;
3120
+ readonly meetingId?: string | undefined;
3121
+ readonly slug?: string | undefined;
3122
+ /** `audio` joins with the camera off. Same room, same code path (D3). */
3123
+ readonly mode: CallMode;
3124
+ /**
3125
+ * Whether to ATTEMPT the microphone on join. Defaults to true. `false` is a
3126
+ * deliberate listen-only join — the pre-join screen's "Join with microphone"
3127
+ * toggle. Either way the join itself never depends on the outcome; a refused
3128
+ * device lands in `snapshot.media.microphone.unavailable`, not in a throw.
3129
+ */
3130
+ readonly microphone?: boolean | undefined;
3131
+ }
3132
+ interface RoomEngineOptions {
3133
+ store: MeetStore;
3134
+ tokens: TokenClient;
3135
+ identity: MeetIdentity;
3136
+ driverFactory: RoomDriverFactory;
3137
+ /** Canonical public metadata read; room packets alone never authorize teardown. */
3138
+ resolveMeeting?: ((slug: string) => Promise<MeetingRecord>) | undefined;
3139
+ onDiagnostic?: ((event: {
3140
+ level: "info" | "warn" | "error";
3141
+ message: string;
3142
+ remedy?: string;
3143
+ }) => void) | undefined;
3144
+ /** Fired when the local participant leaves for any reason, including kicked. */
3145
+ onLeft?: ((reason: string | null) => void) | undefined;
2617
3146
  now?: (() => number) | undefined;
2618
3147
  /** Remembered noise suppression / background choice (Meet wave 2). */
2619
3148
  effectsStorage?: EffectsStorage | undefined;
@@ -2755,198 +3284,4 @@ interface InRoomClientOptions {
2755
3284
  }
2756
3285
  declare function createInRoomClient(options: InRoomClientOptions): InRoomClient;
2757
3286
 
2758
- /**
2759
- * THE MEETING RECORD — what a meeting IS once the room is gone (MRI-D2).
2760
- *
2761
- * 🚨 THE DEFECT THIS EXISTS TO KILL. Through 2026-09-08 a meeting produced a
2762
- * wrap-up summary, decisions, owner-assigned action items, five windows of live
2763
- * notes, thirty-two transcript segments and a recording — and then had NO
2764
- * SURFACE AT ALL. `/meet/<slug>` fell back to `<PreJoin>` on an ended meeting,
2765
- * so the durable link that R3 promises would outlive the meeting resolved to a
2766
- * screen offering to join a room that no longer exists. Everything the campaign
2767
- * built after the room closed was reachable only from SQL.
2768
- *
2769
- * Three rules hold this file together:
2770
- *
2771
- * 1. **Every read is INDEPENDENT and every refusal is NAMED.** The transcript,
2772
- * the notes, the attendance list and the recording live in four RLS-guarded
2773
- * tables, and a guest holding the link can read NONE of them (only
2774
- * `meet_meeting_by_slug` is granted to `anon`). One `Promise.all` would turn
2775
- * a permission boundary into a blank page. Known guest restrictions never issue private reads; each authenticated read that is refused
2776
- * becomes a `MeetingRecordRestriction` with a sentence — the reader is told
2777
- * what exists and why they cannot see it, which is the difference between a
2778
- * record they can ask for and a product that looks broken.
2779
- * 2. **A partial record is still a record.** A failed transcript read never
2780
- * hides the summary somebody CAN read.
2781
- * 3. **Nothing here computes meaning.** Grouping, ordering and formatting only.
2782
- * The summary, decisions and action items are durable rows written by the
2783
- * wrap-up mandate (D5/D18); this module never re-derives them from the
2784
- * transcript, because two sources for one fact is how they start to disagree.
2785
- */
2786
-
2787
- /** One thing the reader could not see, and the honest reason. Never a blank. */
2788
- interface MeetingRecordRestriction {
2789
- /** Which part of the record: `transcript`, `notes`, `attendance`, `recording`. */
2790
- readonly part: "transcript" | "notes" | "attendance" | "recording";
2791
- readonly message: string;
2792
- readonly remedy: string;
2793
- }
2794
- /** A run of consecutive lines by one speaker — how a person reads a transcript. */
2795
- interface TranscriptBlock {
2796
- readonly id: string;
2797
- /** Who spoke. Blocks break on THIS, never on the rendered name (MRI-A16). */
2798
- readonly identity: ParticipantIdentity;
2799
- readonly speaker: string;
2800
- /** ISO of the first line in the block. */
2801
- readonly startedAt: string;
2802
- readonly lines: readonly TranscriptSegment[];
2803
- }
2804
- interface MeetingRecordBundle {
2805
- readonly meeting: MeetingRecord;
2806
- readonly attendees: readonly MeetingAttendee[];
2807
- /** The wrap-up summary: `kind='note'` with `metadata.role='summary'` (D5). */
2808
- readonly summary: MeetingNote | null;
2809
- /** D20: the AUTHORITATIVE set — window rows the wrap-up superseded are out. */
2810
- readonly decisions: readonly MeetingNote[];
2811
- readonly actionItems: readonly MeetingNote[];
2812
- /** The live-notes windows, in window order (D3). */
2813
- readonly liveNotes: readonly MeetingNote[];
2814
- readonly transcript: readonly TranscriptSegment[];
2815
- /** The most recent recording attempt, whatever state it reached. */
2816
- readonly recording: RecordingRecord | null;
2817
- /** What this reader could NOT see. Rendered; never swallowed. */
2818
- readonly restrictions: readonly MeetingRecordRestriction[];
2819
- /**
2820
- * Identity → name, learned from everything this read COULD see: the durable
2821
- * attendance rows and the transcript's own `speaker` column. It is what stops
2822
- * a raw LiveKit token reaching the screen when one of those reads was refused
2823
- * (MRI-A16).
2824
- */
2825
- readonly names: NameBook;
2826
- }
2827
- /**
2828
- * Load the record parts the reader may request; state all restrictions.
2829
- *
2830
- * `meeting` is passed in when the caller already resolved it (the slug route
2831
- * does, through the `anon`-granted RPC) — re-reading it through the table would
2832
- * fail for exactly the reader who most needs the page to work.
2833
- */
2834
- declare function loadMeetingRecord(repository: MeetRepository, meetingId: MeetingId, meeting: MeetingRecord, reader?: Pick<MeetIdentity, "userId">): Promise<MeetingRecordBundle>;
2835
- /**
2836
- * 🚨 **A READ THAT WAS REFUSED IS NOT AN EMPTY RESULT (MRI-A16).**
2837
- *
2838
- * On production 2026-09-09 a guest's record view said *"No wrap-up was written
2839
- * for this meeting"* directly underneath a panel that had just told them, in
2840
- * the same screen, that the notes are not readable from their account — and
2841
- * *"Nothing was recorded as a decision"* beside a decision that exists
2842
- * (MRI-C8). The page asserted an absence it had no way to know, which is worse
2843
- * than saying nothing: a person who reads "no wrap-up was written" stops
2844
- * looking for one.
2845
- *
2846
- * So no surface writes its own empty sentence any more. It asks here, and gets
2847
- * either its absence sentence — earned, because the read SUCCEEDED and returned
2848
- * nothing — or the refusal, said plainly.
2849
- */
2850
- declare const REFUSED_ABSENCE = "This record is not shared with guests.";
2851
- /** Was this part of the record refused to this reader? */
2852
- declare function wasRefused(bundle: MeetingRecordBundle, part: MeetingRecordRestriction["part"]): boolean;
2853
- /**
2854
- * The sentence to show where a part of the record has nothing in it. NEVER an
2855
- * absence claim over a read that was refused.
2856
- */
2857
- declare function absenceSentence(bundle: MeetingRecordBundle, part: MeetingRecordRestriction["part"], absence: string): string;
2858
- /**
2859
- * GROUP THE TRANSCRIPT THE WAY A PERSON READS IT: consecutive lines by one
2860
- * speaker become one block with one timestamp. Reading thirty-two rows each
2861
- * re-stamped with a name and a clock is how a transcript becomes unreadable.
2862
- */
2863
- declare function groupTranscript(segments: readonly TranscriptSegment[], names?: NameBook): readonly TranscriptBlock[];
2864
- /**
2865
- * WHAT THE RECORDING IS DOING, in a sentence — including the two states people
2866
- * hit most and understand least. `processing` is not "broken" and `failed` is
2867
- * never silence: both say what happened and what to do.
2868
- */
2869
- declare function recordingNotice(recording: RecordingRecord | null): string | null;
2870
- /** Only an `available` recording has a file to play. Nothing else pretends to. */
2871
- declare function playableRecordingFileId(recording: RecordingRecord | null): string | null;
2872
- /**
2873
- * A MEETING IS OVER WHEN THE SERVER SAYS SO. `ended_at` is stamped by
2874
- * `POST /v1/meet/end` or by the auto-end evaluation (MRI-A6) — never by a
2875
- * client noticing the room is empty.
2876
- */
2877
- declare function hasEnded(meeting: MeetingRecord | null): boolean;
2878
- /**
2879
- * "Thursday, 8 September 2026, 4:21 PM · 19 minutes". One shape everywhere.
2880
- *
2881
- * THE SPAN IS `formatDurationMs`'s (2026-09-12). This body used to divide by
2882
- * `60_000` and pluralise "minute" itself — a duration formatter living in a
2883
- * PACKAGE, found the day `aidream/apps/shared` became a scan root. `@ai-matrx/
2884
- * meet` already depends on `@ai-matrx/kit`, and `style: "long"` is precisely
2885
- * this voice ("19 minutes", "1 minute"). Three things improve by adopting it:
2886
- * a 40-second meeting read "0 minutes" and now reads "40 seconds" (the
2887
- * confident zero the package refuses), a 19-minute-35-second meeting read
2888
- * "20 minutes" and now reads "19 minutes" (prose asserts at-least, so it
2889
- * floors), and a 90-minute meeting reads "1 hour" instead of "90 minutes".
2890
- */
2891
- declare function meetingWhen(meeting: MeetingRecord, locale?: string): string;
2892
- /** How much of a finished meeting a GUEST may read. Pinned with the server. */
2893
- type GuestRecordAccess = "none" | "summary" | "full";
2894
- /** Widest last. The ORDER is the comparison, exactly as it is on the server. */
2895
- declare const GUEST_RECORD_ACCESS_LEVELS: readonly GuestRecordAccess[];
2896
- /**
2897
- * What a HOST may choose for one meeting. `default` is not a fourth level — it
2898
- * is the absence of a per-meeting choice, and it is the only way to un-choose.
2899
- */
2900
- type GuestRecordAccessSetting = "default" | GuestRecordAccess;
2901
- declare const GUEST_RECORD_ACCESS_CHOICES: readonly GuestRecordAccessSetting[];
2902
- /** `meet_meetings.metadata.guest_record_access`. No DDL; a setting on a row. */
2903
- declare const GUEST_RECORD_ACCESS_METADATA_KEY = "guest_record_access";
2904
- /** What a host chose for THIS meeting, read from the row they already have. */
2905
- declare function guestRecordAccessSetting(meeting: MeetingRecord | null): GuestRecordAccessSetting;
2906
- /** The sentence on the host's control. Plain English, never a token. */
2907
- declare function guestRecordAccessLabel(setting: GuestRecordAccessSetting): string;
2908
- /** What each choice actually means for the people who were in the room. */
2909
- declare function guestRecordAccessExplanation(setting: GuestRecordAccessSetting): string;
2910
- /** What came back from the server, so a surface never has to infer a level. */
2911
- interface GuestRecordAccessResolution {
2912
- readonly level: GuestRecordAccess;
2913
- /** `meeting` when a host chose for this one; `organization` otherwise. */
2914
- readonly source: "meeting" | "organization";
2915
- }
2916
- /**
2917
- * Read the record the server shares with THIS guest.
2918
- *
2919
- * `roomToken` is the pass this device kept from the meeting
2920
- * (`TokenClient.remembered`). It is presented in a header, never appended to
2921
- * the URL. A guest with no pass, an expired pass, or a level of `none` gets the
2922
- * server's refusal — as restrictions on a real bundle, so the screen says what
2923
- * it cannot show instead of collapsing into an error page.
2924
- */
2925
- declare function loadGuestMeetingRecord(args: {
2926
- api: MeetApi;
2927
- repository: MeetRepository;
2928
- meetingId: MeetingId;
2929
- meeting: MeetingRecord;
2930
- roomToken: RoomToken | null;
2931
- }): Promise<MeetingRecordBundle>;
2932
- /**
2933
- * A HOST chooses what guests may read of THIS meeting.
2934
- *
2935
- * Host power is the SERVER's decision and it re-checks it; this function exists
2936
- * so the control has one call to make, and it returns what the choice RESOLVES
2937
- * to so the screen never has to guess what "my organization's setting" means
2938
- * today.
2939
- */
2940
- declare function setGuestRecordAccess(args: {
2941
- api: MeetApi;
2942
- meetingId: MeetingId;
2943
- organizationId: OrganizationId;
2944
- access: GuestRecordAccessSetting;
2945
- }): Promise<{
2946
- readonly setting: GuestRecordAccessSetting;
2947
- readonly effective: GuestRecordAccessResolution;
2948
- }>;
2949
- /** What a guest is told when the level shares nothing. Rendered, never hidden. */
2950
- declare const GUEST_RECORD_NOT_SHARED: Readonly<Record<"recording" | "transcript" | "notes" | "attendance", string>>;
2951
-
2952
- export { type AccessTokenSource, type AdmissionState, type AnnounceResult, type ApplyRoomEventArgs, type AskArgs, type AudioProcessing, CALL_INVITE_KIND, CALL_INVITE_VERSION, CONSENT_NOTICE_ATTRIBUTE, type CallCenter, type CallCenterOptions, type CallId, type CallInvite, type CallInviteHandlerOptions, type CallInvitePayload, type CallMode, type CallState, type CallablePerson, type ConnectionQuality, type CreateCallInviteInput, type CreateMeetingInput, DATA_TOPICS, DEFAULT_EFFECTS, DEFAULT_PAGE_SIZE, type DataTopic, type DeviceKind, type DeviceManager, type DeviceManagerOptions, type DeviceSnapshot, type DeviceStorage, type DriverConnectionState, type DriverParticipant, type DriverTrackKind, type DriverTranscription, END_MEETING_NOT_SERVED, type EffectsStorage, type EndMeetingResult, FILE_SESSION_PATH, GUEST_RECORD_ACCESS_CHOICES, GUEST_RECORD_ACCESS_LEVELS, GUEST_RECORD_ACCESS_METADATA_KEY, GUEST_RECORD_NOT_SHARED, type GuestRecordAccess, type GuestRecordAccessResolution, type GuestRecordAccessSetting, IDLE_MEDIA, IDLE_TRACK, type InRoomClient, type InRoomClientOptions, type InviteeInput, type InviteeRole, type JoinRequest, type JsonObject, type JsonValue, LISTEN_ONLY_NOTICE, type LiveKitDriverOptions, type LobbyEntry, type LocalMediaState, type LocalTrackState, MEETING_INVITE_KIND, MEETING_INVITE_VERSION, MEET_ANSWER_CONTEXT_EVENT, MEET_ANSWER_EVENTS, MEET_EVENTS, MEET_ROUTES, MEET_SCHEMA, MODERATION_ACTIONS, type MediaDevicesLike, type MediaEffects, type MediaUnavailable, type MeetAi, type MeetAiAnswer, type MeetAiOptions, type MeetAnswerCoverage, type MeetApi, type MeetApiOptions, type MeetApiRequest, type MeetDevice, MeetError, type MeetErrorCode, type MeetEvent, type MeetFileMedia, type MeetGrants, type MeetIdentity, type MeetParticipant, type MeetRepository, type MeetRoomDriver, type MeetSnapshot, type MeetStore, type MeetingAttendee, type MeetingChanges, type MeetingId, type MeetingInviteHandlerOptions, type MeetingInvitePayload, type MeetingInvitee, type MeetingKind, type MeetingNote, type MeetingNoteKind, type MeetingOccurrence, type MeetingRecord, type MeetingRecordBundle, type MeetingRecordRestriction, type MeetingSession, type MeetingSessionOptions, type MeetingSettings, type ModerationAction, type ModerationNotice, type ModerationRequest, type ModerationResult, NOTE_TAKER_IDENTITY_PREFIX, NOTE_TAKER_METADATA_KEY, NO_GRANTS, type NameBook, type NoteTakerDispatch, type NoteTakerRefusal, type NoteTakerState, type NoteTakerStatus, ORGANIZATION_HEADER, type OccurrenceOverride, type OccurrenceState, type OrganizationId, PARTICIPANT_CAN_RECORD_ATTRIBUTE, PARTICIPANT_ROLE_ATTRIBUTE, type ParticipantIdentity, type ParticipantRole, REACTION_TTL_MS, REFUSED_ABSENCE, ROLE_RANK, ROOM_EVENTS, ROOM_EVENT_PREFIX, ROOM_TOKEN_HEADER, RPCS, type RecordingController, type RecordingControllerOptions, type RecordingPolicy, type RecordingRecord, type RecordingState, type RepositoryOptions, type RoomChatMessage, type RoomDriverFactory, type RoomDriverHandlers, type RoomEngine, type RoomEngineOptions, type RoomEventOutcome, type RoomEventTopic, type RoomName, type RoomPhase, type RoomReaction, type RoomToken, type RoomTokenStorage, type RsvpAnswer, type RsvpInvitation, type RsvpState, type ScheduleMeetingInput, type Stage, type StageArgs, type StageLayout, TABLES, TRANSCRIPTION_ATTRIBUTES, TRANSCRIPTION_TOPIC, type TokenClient, type TokenClientOptions, type TokenRequest, type TranscriptBlock, type TranscriptSegment, type TranscriptSource, type UpcomingOccurrence, type UserId, type VideoEffect, type WrapUpAnnouncement, absenceSentence, announceMeeting, applyRoomEvent, asCallId, asMeetingId, asOrganizationId, asParticipantIdentity, asRoomName, asUserId, callInviteAction, callInviteText, callStateLabel, canModerate, canModerateParticipant, canTransition, captionSegment, changesPatch, computeStage, countLabel, createCallCenter, createCallInviteHandler, createDeviceManager, createInRoomClient, createLiveKitDriver, createMeetAi, createMeetApi, createMeetRepository, createMeetStore, createMeetingInviteHandler, createMeetingSession, createMemoryDeviceStorage, createRecordingController, createRoomEngine, createTokenClient, createWebDeviceStorage, createWebEffectsStorage, createWebRoomTokenStorage, displayNameFor, fileDownloadPath, gridColumns, groupTranscript, guestRecordAccessExplanation, guestRecordAccessLabel, guestRecordAccessSetting, hasEnded, hostUserIds, invalidResponse, isGuest, isGuestIdentity, isHostRole, isListenOnly, isNoteTaker, isRealName, isRoomEventTopic, learnName, learnNames, listenOnlyNotice, loadGuestMeetingRecord, loadMeetingRecord, mapConnectionState, mapQuality, meetingInviteAction, meetingLink, meetingPath, meetingSlug, meetingTopic, meetingWhen, mergeCaption, mergeRecord, normalizeDeviceError, normalizeMeetError, normalizeSpoken, noteTakerIdentity, noteTakerLabel, noteTakerParticipant, noteTakerStatus, orderParticipants, parseRoomToken, participantSummary, playableRecordingFileId, projectInvitee, projectNote, projectOccurrence, projectOverride, projectParticipant, projectRsvpInvitation, projectSegment, projectUpcoming, pushMeetingToGoogleCalendar, readServerRefusal, readableTranscript, recordingLabel, recordingNotice, roleOf, roomTopic, setGuestRecordAccess, settingsPatch, snapshotParticipant, unavailableFrom, userTopic, wasRefused };
3287
+ export { ATTENDANCE_CSV_HEADER, type AccessTokenSource, type AdmissionState, type AnnounceResult, type ApplyRoomEventArgs, type AskArgs, type AttendanceKind, type AttendanceRow, type AudioProcessing, CALL_INVITE_KIND, CALL_INVITE_VERSION, CONSENT_NOTICE_ATTRIBUTE, type CallCenter, type CallCenterOptions, type CallId, type CallInvite, type CallInviteHandlerOptions, type CallInvitePayload, type CallMode, type CallState, type CallablePerson, type ConnectionQuality, type CreateCallInviteInput, type CreateMeetingInput, DATA_TOPICS, DEFAULT_EFFECTS, DEFAULT_PAGE_SIZE, type DataTopic, type DeviceKind, type DeviceManager, type DeviceManagerOptions, type DeviceSnapshot, type DeviceStorage, type DriverConnectionState, type DriverParticipant, type DriverTrackKind, type DriverTranscription, END_MEETING_NOT_SERVED, type EffectsStorage, type EndMeetingResult, FILE_SESSION_PATH, type FollowUpDraft, type FollowUpRecipient, type FollowUpSendResult, GUEST_RECORD_ACCESS_CHOICES, GUEST_RECORD_ACCESS_LEVELS, GUEST_RECORD_ACCESS_METADATA_KEY, GUEST_RECORD_NOT_SHARED, type GuestRecordAccess, type GuestRecordAccessResolution, type GuestRecordAccessSetting, IDLE_MEDIA, IDLE_TRACK, type InRoomClient, type InRoomClientOptions, type InviteeInput, type InviteeRole, type JoinRequest, type JsonObject, type JsonValue, LISTEN_ONLY_NOTICE, type LibraryRecording, type LiveKitDriverOptions, type LobbyEntry, type LocalMediaState, type LocalTrackState, MEETING_INVITE_KIND, MEETING_INVITE_VERSION, MEET_ANSWER_CONTEXT_EVENT, MEET_ANSWER_EVENTS, MEET_EVENTS, MEET_ROUTES, MEET_SCHEMA, MODERATION_ACTIONS, type MediaDevicesLike, type MediaEffects, type MediaUnavailable, type MeetAi, type MeetAiAnswer, type MeetAiOptions, type MeetAnswerCoverage, type MeetApi, type MeetApiOptions, type MeetApiRequest, type MeetDevice, MeetError, type MeetErrorCode, type MeetEvent, type MeetFileMedia, type MeetGrants, type MeetIdentity, type MeetParticipant, type MeetRepository, type MeetRoomDriver, type MeetSnapshot, type MeetStore, type MeetingAttendee, type MeetingChanges, type MeetingChatHistory, type MeetingId, type MeetingInviteHandlerOptions, type MeetingInvitePayload, type MeetingInvitee, type MeetingKind, type MeetingNote, type MeetingNoteKind, type MeetingOccurrence, type MeetingRecord, type MeetingRecordBundle, type MeetingRecordRestriction, type MeetingRecordRestrictionNotice, type MeetingSearchHit, type MeetingSearchSource, type MeetingSession, type MeetingSessionOptions, type MeetingSettings, type ModerationAction, type ModerationNotice, type ModerationRequest, type ModerationResult, NOTE_TAKER_IDENTITY_PREFIX, NOTE_TAKER_METADATA_KEY, NO_GRANTS, type NameBook, type NoteTakerDispatch, type NoteTakerRefusal, type NoteTakerState, type NoteTakerStatus, ORGANIZATION_HEADER, type OccurrenceOverride, type OccurrenceState, type OrganizationId, PARTICIPANT_CAN_RECORD_ATTRIBUTE, PARTICIPANT_ROLE_ATTRIBUTE, type ParticipantIdentity, type ParticipantRole, REACTION_TTL_MS, REFUSED_ABSENCE, ROLE_RANK, ROOM_EVENTS, ROOM_EVENT_PREFIX, ROOM_TOKEN_HEADER, RPCS, type RecordingController, type RecordingControllerOptions, type RecordingPolicy, type RecordingRecord, type RecordingState, type RepositoryOptions, type RoomChatMessage, type RoomDriverFactory, type RoomDriverHandlers, type RoomEngine, type RoomEngineOptions, type RoomEventOutcome, type RoomEventTopic, type RoomName, type RoomPhase, type RoomReaction, type RoomToken, type RoomTokenStorage, type RsvpAnswer, type RsvpInvitation, type RsvpState, type ScheduleMeetingInput, type Stage, type StageArgs, type StageLayout, TABLES, TRANSCRIPTION_ATTRIBUTES, TRANSCRIPTION_TOPIC, type TokenClient, type TokenClientOptions, type TokenRequest, type TranscriptBlock, type TranscriptFormat, type TranscriptMatch, type TranscriptSegment, type TranscriptSource, type UpcomingOccurrence, type UserId, type VideoEffect, type WrapUpAnnouncement, type WrapUpStatus, absenceSentence, activeWrapUpSummary, announceMeeting, applyRoomEvent, asCallId, asMeetingId, asOrganizationId, asParticipantIdentity, asRoomName, asUserId, attendanceCsv, attendanceReport, attendanceTotals, attendeeKey, callInviteAction, callInviteText, callStateLabel, canModerate, canModerateParticipant, canTransition, captionSegment, changesPatch, clockPosition, computeStage, countLabel, createCallCenter, createCallInviteHandler, createDeviceManager, createInRoomClient, createLiveKitDriver, createMeetAi, createMeetApi, createMeetRepository, createMeetStore, createMeetingInviteHandler, createMeetingSession, createMemoryDeviceStorage, createRecordingController, createRoomEngine, createTokenClient, createWebDeviceStorage, createWebEffectsStorage, createWebRoomTokenStorage, displayNameFor, fileDownloadPath, gridColumns, groupTranscript, guestRecordAccessExplanation, guestRecordAccessLabel, guestRecordAccessSetting, hasEnded, hostUserIds, invalidResponse, isGuest, isGuestIdentity, isHostRole, isListenOnly, isNoteTaker, isRealName, isRoomEventTopic, learnName, learnNames, listenOnlyNotice, loadFollowUpDraft, loadGuestMeetingRecord, loadMeetingChat, loadMeetingRecord, mapConnectionState, mapQuality, meetingInviteAction, meetingLength, meetingLink, meetingPath, meetingSlug, meetingTopic, meetingWhen, mergeCaption, mergeRecord, normalizeDeviceError, normalizeMeetError, normalizeSpoken, noteTakerIdentity, noteTakerLabel, noteTakerParticipant, noteTakerStatus, notesDocument, orderParticipants, parseRoomToken, participantSummary, playableRecordingFileId, projectInvitee, projectLibraryRecording, projectNote, projectOccurrence, projectOverride, projectParticipant, projectRsvpInvitation, projectSearchHit, projectSegment, projectUpcoming, pushMeetingToGoogleCalendar, readServerRefusal, readableTranscript, recordingLabel, recordingNotice, recordingOffsetMs, roleOf, roomTopic, searchTranscript, sendFollowUp, setGuestRecordAccess, settingsPatch, snapshotParticipant, snippetRuns, summarizeMeetingRecordRestrictions, transcriptDocument, unavailableFrom, uniqueAttendees, userTopic, wasRefused, wrapUpStatus };