@ai-matrx/meet 0.7.7 → 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
@@ -2898,12 +2909,30 @@ interface AttendanceRow {
2898
2909
  /** True when `durationMs` was measured to the meeting's end, not a recorded leave. */
2899
2910
  readonly durationToEnd: boolean;
2900
2911
  readonly admission: MeetingAttendee["admissionState"];
2912
+ /** How many times this person came in (a rejoin or a second device). */
2913
+ readonly joins: number;
2901
2914
  }
2902
2915
  /**
2903
2916
  * The attendance report of ONE meeting, host first then by arrival. People who
2904
2917
  * knocked and were never let in are kept (with no join time) — "who asked to
2905
2918
  * come in" is part of the record — but a denied knock has no duration.
2906
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[];
2907
2936
  declare function attendanceReport(meeting: MeetingRecord, attendees: readonly MeetingAttendee[], names?: NameBook): readonly AttendanceRow[];
2908
2937
  /** Headline numbers for the top of a report. The assistant is never a person. */
2909
2938
  declare function attendanceTotals(rows: readonly AttendanceRow[]): {
@@ -3255,4 +3284,4 @@ interface InRoomClientOptions {
3255
3284
  }
3256
3285
  declare function createInRoomClient(options: InRoomClientOptions): InRoomClient;
3257
3286
 
3258
- 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, 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, userTopic, wasRefused, wrapUpStatus };
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 };
package/dist/index.d.ts 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
@@ -2898,12 +2909,30 @@ interface AttendanceRow {
2898
2909
  /** True when `durationMs` was measured to the meeting's end, not a recorded leave. */
2899
2910
  readonly durationToEnd: boolean;
2900
2911
  readonly admission: MeetingAttendee["admissionState"];
2912
+ /** How many times this person came in (a rejoin or a second device). */
2913
+ readonly joins: number;
2901
2914
  }
2902
2915
  /**
2903
2916
  * The attendance report of ONE meeting, host first then by arrival. People who
2904
2917
  * knocked and were never let in are kept (with no join time) — "who asked to
2905
2918
  * come in" is part of the record — but a denied knock has no duration.
2906
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[];
2907
2936
  declare function attendanceReport(meeting: MeetingRecord, attendees: readonly MeetingAttendee[], names?: NameBook): readonly AttendanceRow[];
2908
2937
  /** Headline numbers for the top of a report. The assistant is never a person. */
2909
2938
  declare function attendanceTotals(rows: readonly AttendanceRow[]): {
@@ -3255,4 +3284,4 @@ interface InRoomClientOptions {
3255
3284
  }
3256
3285
  declare function createInRoomClient(options: InRoomClientOptions): InRoomClient;
3257
3286
 
3258
- 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, 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, userTopic, wasRefused, wrapUpStatus };
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 };
package/dist/index.js CHANGED
@@ -482,6 +482,29 @@ function readText(data) {
482
482
  }
483
483
  return "";
484
484
  }
485
+ var SERVER_FAILED_SENTENCE = "The server hit an unexpected problem and nothing was changed.";
486
+ function isRawServerText(value) {
487
+ const text3 = value.trim();
488
+ return /^internal server error\b/i.test(text3) || /^[A-Z][A-Za-z0-9]*(Error|Exception)$/.test(text3) || /^[a-z][a-z0-9]*(_[a-z0-9]+)+$/.test(text3);
489
+ }
490
+ async function refusalFromResponse(response, operation, fallbackRemedy) {
491
+ let body = null;
492
+ try {
493
+ body = await response.json();
494
+ } catch {
495
+ body = null;
496
+ }
497
+ const said = readServerRefusal(body);
498
+ const code = response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport";
499
+ const error = new MeetError(
500
+ code,
501
+ said?.reason ?? SERVER_FAILED_SENTENCE,
502
+ said !== null && said.remedy.length > 0 ? said.remedy : fallbackRemedy
503
+ );
504
+ error.operation = operation;
505
+ error.status = response.status;
506
+ return error;
507
+ }
485
508
  function readServerRefusal(body) {
486
509
  if (typeof body === "string") {
487
510
  return body.trim().length === 0 ? null : { reason: body.trim(), remedy: "" };
@@ -500,7 +523,8 @@ function readServerRefusal(body) {
500
523
  return messages.length === 0 ? null : { reason: messages.join("; "), remedy: "" };
501
524
  }
502
525
  if (typeof detail === "object" && detail !== null) return readServerRefusal(detail);
503
- const reason = typeof row["reason"] === "string" ? row["reason"] : typeof row["message"] === "string" ? row["message"] : typeof detail === "string" ? detail : typeof row["error"] === "string" ? row["error"] : null;
526
+ const humanOrNull = (value) => typeof value === "string" && value.trim().length > 0 && !isRawServerText(value) ? value.trim() : null;
527
+ const reason = humanOrNull(row["reason"]) ?? humanOrNull(row["message"]) ?? humanOrNull(row["user_message"]) ?? (typeof detail === "string" ? humanOrNull(detail) : null) ?? (typeof row["message"] === "string" || typeof row["error"] === "string" || typeof detail === "string" ? SERVER_FAILED_SENTENCE : null);
504
528
  if (reason === null) return null;
505
529
  const remedy = typeof row["remedy"] === "string" ? row["remedy"] : "";
506
530
  if (remedy.length > 0) return { reason, remedy };
@@ -3183,6 +3207,7 @@ var EMPTY = {
3183
3207
  spotlight: null,
3184
3208
  moderationNotice: null,
3185
3209
  actionNotice: null,
3210
+ privateChatRequest: null,
3186
3211
  effects: DEFAULT_EFFECTS,
3187
3212
  transcript: [],
3188
3213
  notes: [],
@@ -3873,9 +3898,55 @@ function ms(iso) {
3873
3898
  const at = new Date(iso).getTime();
3874
3899
  return Number.isNaN(at) ? null : at;
3875
3900
  }
3901
+ function attendeeKey(person, names = {}) {
3902
+ if (person.userId !== null) return `user:${person.userId}`;
3903
+ if (person.isAgent) return `agent:${person.identity}`;
3904
+ const name = displayNameFor(names, person.identity, person.displayName).trim().toLowerCase();
3905
+ return name.length > 0 ? `guest-name:${name}` : `identity:${person.identity}`;
3906
+ }
3907
+ function uniqueAttendees(attendees, names = {}) {
3908
+ const byKey = /* @__PURE__ */ new Map();
3909
+ for (const person of attendees) {
3910
+ const key = attendeeKey(person, names);
3911
+ const kept = byKey.get(key);
3912
+ if (kept === void 0) {
3913
+ byKey.set(key, person);
3914
+ continue;
3915
+ }
3916
+ const keptJoin = ms(kept.joinedAt);
3917
+ const join = ms(person.joinedAt);
3918
+ if (join !== null && (keptJoin === null || join < keptJoin)) byKey.set(key, person);
3919
+ }
3920
+ return [...byKey.values()];
3921
+ }
3922
+ var KIND_RANK = {
3923
+ host: 0,
3924
+ cohost: 1,
3925
+ member: 2,
3926
+ "signed-in guest": 3,
3927
+ guest: 4,
3928
+ assistant: 5
3929
+ };
3930
+ function coveredMs(intervals) {
3931
+ const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
3932
+ let total = 0;
3933
+ let from = -Infinity;
3934
+ let to = -Infinity;
3935
+ for (const [start, end] of sorted) {
3936
+ if (start > to) {
3937
+ if (to > from) total += to - from;
3938
+ from = start;
3939
+ to = end;
3940
+ } else if (end > to) {
3941
+ to = end;
3942
+ }
3943
+ }
3944
+ if (to > from) total += to - from;
3945
+ return total;
3946
+ }
3876
3947
  function attendanceReport(meeting, attendees, names = {}) {
3877
3948
  const end = ms(meeting.endedAt);
3878
- const rows = attendees.map((person) => {
3949
+ const perJoin = attendees.map((person) => {
3879
3950
  const joined = ms(person.joinedAt);
3880
3951
  const left = ms(person.leftAt);
3881
3952
  const until = left ?? end;
@@ -3890,17 +3961,45 @@ function attendanceReport(meeting, attendees, names = {}) {
3890
3961
  leftAt: person.leftAt,
3891
3962
  durationMs: duration,
3892
3963
  durationToEnd: duration !== null && left === null,
3893
- admission: person.admissionState
3964
+ admission: person.admissionState,
3965
+ joins: 1
3894
3966
  };
3895
3967
  });
3896
- const rank = {
3897
- host: 0,
3898
- cohost: 1,
3899
- member: 2,
3900
- "signed-in guest": 3,
3901
- guest: 4,
3902
- assistant: 5
3903
- };
3968
+ const groups = /* @__PURE__ */ new Map();
3969
+ attendees.forEach((person, index) => {
3970
+ const key = attendeeKey(person, names);
3971
+ const group = groups.get(key) ?? { rows: [], people: [] };
3972
+ group.rows.push(perJoin[index]);
3973
+ group.people.push(person);
3974
+ groups.set(key, group);
3975
+ });
3976
+ const rows = [...groups.values()].map(({ rows: joins }) => {
3977
+ if (joins.length === 1) return joins[0];
3978
+ const came = joins.filter((row) => row.joinedAt !== null);
3979
+ const first = [...came].sort((a, b) => (ms(a.joinedAt) ?? 0) - (ms(b.joinedAt) ?? 0))[0] ?? joins[0];
3980
+ const kind = joins.reduce(
3981
+ (best, row) => KIND_RANK[row.kind] < KIND_RANK[best] ? row.kind : best,
3982
+ first.kind
3983
+ );
3984
+ const open = came.some((row) => row.leftAt === null);
3985
+ const lastLeave = came.reduce((latest, row) => {
3986
+ const left = ms(row.leftAt);
3987
+ return left !== null && (latest === null || left > latest) ? left : latest;
3988
+ }, null);
3989
+ const intervals = came.flatMap((row) => {
3990
+ const joined = ms(row.joinedAt);
3991
+ return joined !== null && row.durationMs !== null ? [[joined, joined + row.durationMs]] : [];
3992
+ });
3993
+ return {
3994
+ ...first,
3995
+ kind,
3996
+ leftAt: open ? null : lastLeave === null ? null : new Date(lastLeave).toISOString(),
3997
+ durationMs: intervals.length === 0 ? null : coveredMs(intervals),
3998
+ durationToEnd: came.some((row) => row.durationToEnd),
3999
+ joins: came.length === 0 ? joins.length : came.length
4000
+ };
4001
+ });
4002
+ const rank = KIND_RANK;
3904
4003
  return rows.sort((a, b) => {
3905
4004
  if (rank[a.kind] !== rank[b.kind]) return rank[a.kind] - rank[b.kind];
3906
4005
  return (ms(a.joinedAt) ?? Number.MAX_SAFE_INTEGER) - (ms(b.joinedAt) ?? Number.MAX_SAFE_INTEGER);
@@ -4058,7 +4157,10 @@ function bullet(note) {
4058
4157
  function notesDocument(bundle, options = {}) {
4059
4158
  const out = [`# ${bundle.meeting.title}`, ""];
4060
4159
  if (options.when) out.push(`_${options.when}_`, "");
4061
- const people = bundle.attendees.filter((person) => !person.isAgent && person.joinedAt !== null).map((person) => displayNameFor(bundle.names, person.identity, person.displayName));
4160
+ const people = uniqueAttendees(
4161
+ bundle.attendees.filter((person) => !person.isAgent && person.joinedAt !== null),
4162
+ bundle.names
4163
+ ).map((person) => displayNameFor(bundle.names, person.identity, person.displayName));
4062
4164
  if (people.length > 0) out.push(`**Attended:** ${people.join(", ")}`, "");
4063
4165
  out.push("## Summary", "", bundle.summary?.text.trim() || "No summary was written.", "");
4064
4166
  out.push("## Decisions", "");
@@ -4122,19 +4224,8 @@ async function loadMeetingChat(api, meetingId, roomToken = null) {
4122
4224
  refusal: null
4123
4225
  };
4124
4226
  }
4125
- async function refusalOf(response, operation) {
4126
- let body = null;
4127
- try {
4128
- body = await response.json();
4129
- } catch {
4130
- body = null;
4131
- }
4132
- const said = readServerRefusal(body);
4133
- return new MeetError(
4134
- response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
4135
- `${operation}: ${said?.reason ?? `the server answered ${response.status}`}`,
4136
- said?.remedy || "Nothing was sent. Try again."
4137
- );
4227
+ function refusalOf(response, operation) {
4228
+ return refusalFromResponse(response, operation, "Nothing was sent. Try again.");
4138
4229
  }
4139
4230
  function asRecipient(row) {
4140
4231
  const sources = Array.isArray(row["sources"]) ? row["sources"] : [];
@@ -4195,18 +4286,11 @@ async function sendFollowUp(api, meetingId, input) {
4195
4286
  }
4196
4287
 
4197
4288
  // src/core/management.ts
4198
- async function refusal(response, operation) {
4199
- let body = null;
4200
- try {
4201
- body = await response.json();
4202
- } catch {
4203
- body = null;
4204
- }
4205
- const said = readServerRefusal(body);
4206
- return new MeetError(
4207
- response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
4208
- `${operation}: ${said?.reason ?? `the server answered ${response.status}`}`,
4209
- said?.remedy || "The meeting itself is saved \u2014 only the telling failed. Try again; the call is idempotent."
4289
+ function refusal(response, operation) {
4290
+ return refusalFromResponse(
4291
+ response,
4292
+ operation,
4293
+ "The meeting itself is saved \u2014 only the telling failed. Try again; the call is idempotent."
4210
4294
  );
4211
4295
  }
4212
4296
  function count(body, key) {
@@ -4532,6 +4616,10 @@ function createRoomEngine(options) {
4532
4616
  }
4533
4617
  },
4534
4618
  disconnected(reason) {
4619
+ if (current !== null) {
4620
+ tokens.invalidate(current.roomName);
4621
+ current = null;
4622
+ }
4535
4623
  store.patch({ phase: "left" });
4536
4624
  options.onLeft?.(reason);
4537
4625
  }
@@ -4834,19 +4922,8 @@ function createInRoomClient(options) {
4834
4922
  const token = options.resolveRoomToken();
4835
4923
  return token === null ? void 0 : { [ROOM_TOKEN_HEADER]: token };
4836
4924
  }
4837
- async function refusal2(response, operation, remedy) {
4838
- let body = null;
4839
- try {
4840
- body = await response.json();
4841
- } catch {
4842
- body = null;
4843
- }
4844
- const said = readServerRefusal(body);
4845
- return new MeetError(
4846
- response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
4847
- said?.reason ?? `${operation}: the server answered ${response.status}`,
4848
- said?.remedy !== void 0 && said.remedy.length > 0 ? said.remedy : remedy
4849
- );
4925
+ function refusal2(response, operation, remedy) {
4926
+ return refusalFromResponse(response, operation, remedy);
4850
4927
  }
4851
4928
  return {
4852
4929
  async moderate(request) {
@@ -5396,6 +5473,7 @@ export {
5396
5473
  attendanceCsv,
5397
5474
  attendanceReport,
5398
5475
  attendanceTotals,
5476
+ attendeeKey,
5399
5477
  callInviteAction,
5400
5478
  callInviteText,
5401
5479
  callStateLabel,
@@ -5499,6 +5577,7 @@ export {
5499
5577
  summarizeMeetingRecordRestrictions,
5500
5578
  transcriptDocument,
5501
5579
  unavailableFrom,
5580
+ uniqueAttendees,
5502
5581
  userTopic,
5503
5582
  wasRefused,
5504
5583
  wrapUpStatus