@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/CHANGELOG.md CHANGED
@@ -1,8 +1,45 @@
1
1
  # Changelog — `@ai-matrx/meet`
2
2
 
3
+ ## 0.7.8
4
+ ### (next patch)
5
+
6
+ - **Fix — a refusal never reaches a person as a Python class name** (verifier, 2026-09-27): a
7
+ host's toast read "Internal server error: UnsupportedClientFeatureError". `readServerRefusal`
8
+ now skips raw server text (`Internal server error: …`, a bare `…Error` class, a snake_case
9
+ code) and falls through to the body's `user_message`, or to `SERVER_FAILED_SENTENCE`. The
10
+ three hand-copied refusal builders (`in-room.ts`, `management.ts`, `after.ts`) collapse into
11
+ ONE exported `refusalFromResponse(response, operation, fallbackRemedy)`, whose `message` is
12
+ the server's sentence — no `meet.x:` operation prefix — and whose `remedy` is the server's.
13
+ - **Fix — a refused Remove / Make host keeps its pane open WITH the sentence** : the confirm
14
+ pane now shows the server's refusal inline (`role="alert"`) with its buttons live; before it
15
+ stayed up silently.
16
+ - **Fix — every per-person menu offers "Message privately"**: a tile's menu (Room stage and a
17
+ host layout's strip, e.g. the Board) now opens the private chat through the store
18
+ (`snapshot.privateChatRequest`, answered by the one `ControlBar`); before only the People
19
+ panel could, so two menus for one person disagreed.
20
+ - **Fix — attendance is per PERSON, never per join**: an export listed the same attendee twice
21
+ under "attended". `attendanceReport` merges a person's joins (account = one person across
22
+ devices and rejoins; an accountless guest's rejoin, which gets a fresh `guest:<random>`
23
+ identity, is matched by the typed name) into one row — first arrival, last departure, total
24
+ time with overlaps counted once, and a new `joins` count. `notesDocument`'s "Attended" line
25
+ and the record view's "Who attended" use the new exported `uniqueAttendees` / `attendeeKey`.
26
+ - **Consumer action:** none (a consumer counting `bundle.attendees` itself should count
27
+ `uniqueAttendees(...)` instead).
28
+
3
29
  ## 0.7.7
4
30
  ### Meet wave 3: after the meeting
5
31
 
32
+ - **Fix — the room header on a phone** (2026-09-27, 390px): the header wraps into rows and
33
+ each host control stays one line; before, the title read "Hollo…" and "End meeting for
34
+ everyone" stacked into four underlined words.
35
+ - **Fix — a removed participant reads the server's sentence, not "invalid token: revoked"**
36
+ (2026-09-27, live room): a server-side disconnect now invalidates the cached room token, so
37
+ the next Join mints again and shows "A host removed you from this meeting."
38
+ - **Fix — a host's per-person menu opens beside its button** (2026-09-27, live room): the
39
+ popover was hidden but IN FLOW on its first frame, so the anchor it measured was button +
40
+ menu and it opened a full menu-height lower — Make host, Report and Remove from meeting were
41
+ below the fold. It is now fixed-positioned from the first frame, re-placed when the layout
42
+ moves, and pinned + scrollable when neither side of the button holds it.
6
43
  - **Fix — the waiting room for people from another organization and for denied guests**
7
44
  (2026-09-27, verified in a real room): a signed-in link-follower now knocks as `user:<id>`, the
8
45
  identity the server's lobby row carries — before, the host saw two entries and admitting one
package/dist/index.cjs CHANGED
@@ -84,6 +84,7 @@ __export(src_exports, {
84
84
  attendanceCsv: () => attendanceCsv,
85
85
  attendanceReport: () => attendanceReport,
86
86
  attendanceTotals: () => attendanceTotals,
87
+ attendeeKey: () => attendeeKey,
87
88
  callInviteAction: () => callInviteAction,
88
89
  callInviteText: () => callInviteText,
89
90
  callStateLabel: () => callStateLabel,
@@ -187,6 +188,7 @@ __export(src_exports, {
187
188
  summarizeMeetingRecordRestrictions: () => summarizeMeetingRecordRestrictions,
188
189
  transcriptDocument: () => transcriptDocument,
189
190
  unavailableFrom: () => unavailableFrom,
191
+ uniqueAttendees: () => uniqueAttendees,
190
192
  userTopic: () => userTopic,
191
193
  wasRefused: () => wasRefused,
192
194
  wrapUpStatus: () => wrapUpStatus
@@ -677,6 +679,29 @@ function readText(data) {
677
679
  }
678
680
  return "";
679
681
  }
682
+ var SERVER_FAILED_SENTENCE = "The server hit an unexpected problem and nothing was changed.";
683
+ function isRawServerText(value) {
684
+ const text3 = value.trim();
685
+ 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);
686
+ }
687
+ async function refusalFromResponse(response, operation, fallbackRemedy) {
688
+ let body = null;
689
+ try {
690
+ body = await response.json();
691
+ } catch {
692
+ body = null;
693
+ }
694
+ const said = readServerRefusal(body);
695
+ const code = response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport";
696
+ const error = new MeetError(
697
+ code,
698
+ said?.reason ?? SERVER_FAILED_SENTENCE,
699
+ said !== null && said.remedy.length > 0 ? said.remedy : fallbackRemedy
700
+ );
701
+ error.operation = operation;
702
+ error.status = response.status;
703
+ return error;
704
+ }
680
705
  function readServerRefusal(body) {
681
706
  if (typeof body === "string") {
682
707
  return body.trim().length === 0 ? null : { reason: body.trim(), remedy: "" };
@@ -695,7 +720,8 @@ function readServerRefusal(body) {
695
720
  return messages.length === 0 ? null : { reason: messages.join("; "), remedy: "" };
696
721
  }
697
722
  if (typeof detail === "object" && detail !== null) return readServerRefusal(detail);
698
- 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;
723
+ const humanOrNull = (value) => typeof value === "string" && value.trim().length > 0 && !isRawServerText(value) ? value.trim() : null;
724
+ 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);
699
725
  if (reason === null) return null;
700
726
  const remedy = typeof row["remedy"] === "string" ? row["remedy"] : "";
701
727
  if (remedy.length > 0) return { reason, remedy };
@@ -3378,6 +3404,7 @@ var EMPTY = {
3378
3404
  spotlight: null,
3379
3405
  moderationNotice: null,
3380
3406
  actionNotice: null,
3407
+ privateChatRequest: null,
3381
3408
  effects: DEFAULT_EFFECTS,
3382
3409
  transcript: [],
3383
3410
  notes: [],
@@ -4068,9 +4095,55 @@ function ms(iso) {
4068
4095
  const at = new Date(iso).getTime();
4069
4096
  return Number.isNaN(at) ? null : at;
4070
4097
  }
4098
+ function attendeeKey(person, names = {}) {
4099
+ if (person.userId !== null) return `user:${person.userId}`;
4100
+ if (person.isAgent) return `agent:${person.identity}`;
4101
+ const name = displayNameFor(names, person.identity, person.displayName).trim().toLowerCase();
4102
+ return name.length > 0 ? `guest-name:${name}` : `identity:${person.identity}`;
4103
+ }
4104
+ function uniqueAttendees(attendees, names = {}) {
4105
+ const byKey = /* @__PURE__ */ new Map();
4106
+ for (const person of attendees) {
4107
+ const key = attendeeKey(person, names);
4108
+ const kept = byKey.get(key);
4109
+ if (kept === void 0) {
4110
+ byKey.set(key, person);
4111
+ continue;
4112
+ }
4113
+ const keptJoin = ms(kept.joinedAt);
4114
+ const join = ms(person.joinedAt);
4115
+ if (join !== null && (keptJoin === null || join < keptJoin)) byKey.set(key, person);
4116
+ }
4117
+ return [...byKey.values()];
4118
+ }
4119
+ var KIND_RANK = {
4120
+ host: 0,
4121
+ cohost: 1,
4122
+ member: 2,
4123
+ "signed-in guest": 3,
4124
+ guest: 4,
4125
+ assistant: 5
4126
+ };
4127
+ function coveredMs(intervals) {
4128
+ const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
4129
+ let total = 0;
4130
+ let from = -Infinity;
4131
+ let to = -Infinity;
4132
+ for (const [start, end] of sorted) {
4133
+ if (start > to) {
4134
+ if (to > from) total += to - from;
4135
+ from = start;
4136
+ to = end;
4137
+ } else if (end > to) {
4138
+ to = end;
4139
+ }
4140
+ }
4141
+ if (to > from) total += to - from;
4142
+ return total;
4143
+ }
4071
4144
  function attendanceReport(meeting, attendees, names = {}) {
4072
4145
  const end = ms(meeting.endedAt);
4073
- const rows = attendees.map((person) => {
4146
+ const perJoin = attendees.map((person) => {
4074
4147
  const joined = ms(person.joinedAt);
4075
4148
  const left = ms(person.leftAt);
4076
4149
  const until = left ?? end;
@@ -4085,17 +4158,45 @@ function attendanceReport(meeting, attendees, names = {}) {
4085
4158
  leftAt: person.leftAt,
4086
4159
  durationMs: duration,
4087
4160
  durationToEnd: duration !== null && left === null,
4088
- admission: person.admissionState
4161
+ admission: person.admissionState,
4162
+ joins: 1
4089
4163
  };
4090
4164
  });
4091
- const rank = {
4092
- host: 0,
4093
- cohost: 1,
4094
- member: 2,
4095
- "signed-in guest": 3,
4096
- guest: 4,
4097
- assistant: 5
4098
- };
4165
+ const groups = /* @__PURE__ */ new Map();
4166
+ attendees.forEach((person, index) => {
4167
+ const key = attendeeKey(person, names);
4168
+ const group = groups.get(key) ?? { rows: [], people: [] };
4169
+ group.rows.push(perJoin[index]);
4170
+ group.people.push(person);
4171
+ groups.set(key, group);
4172
+ });
4173
+ const rows = [...groups.values()].map(({ rows: joins }) => {
4174
+ if (joins.length === 1) return joins[0];
4175
+ const came = joins.filter((row) => row.joinedAt !== null);
4176
+ const first = [...came].sort((a, b) => (ms(a.joinedAt) ?? 0) - (ms(b.joinedAt) ?? 0))[0] ?? joins[0];
4177
+ const kind = joins.reduce(
4178
+ (best, row) => KIND_RANK[row.kind] < KIND_RANK[best] ? row.kind : best,
4179
+ first.kind
4180
+ );
4181
+ const open = came.some((row) => row.leftAt === null);
4182
+ const lastLeave = came.reduce((latest, row) => {
4183
+ const left = ms(row.leftAt);
4184
+ return left !== null && (latest === null || left > latest) ? left : latest;
4185
+ }, null);
4186
+ const intervals = came.flatMap((row) => {
4187
+ const joined = ms(row.joinedAt);
4188
+ return joined !== null && row.durationMs !== null ? [[joined, joined + row.durationMs]] : [];
4189
+ });
4190
+ return {
4191
+ ...first,
4192
+ kind,
4193
+ leftAt: open ? null : lastLeave === null ? null : new Date(lastLeave).toISOString(),
4194
+ durationMs: intervals.length === 0 ? null : coveredMs(intervals),
4195
+ durationToEnd: came.some((row) => row.durationToEnd),
4196
+ joins: came.length === 0 ? joins.length : came.length
4197
+ };
4198
+ });
4199
+ const rank = KIND_RANK;
4099
4200
  return rows.sort((a, b) => {
4100
4201
  if (rank[a.kind] !== rank[b.kind]) return rank[a.kind] - rank[b.kind];
4101
4202
  return (ms(a.joinedAt) ?? Number.MAX_SAFE_INTEGER) - (ms(b.joinedAt) ?? Number.MAX_SAFE_INTEGER);
@@ -4253,7 +4354,10 @@ function bullet(note) {
4253
4354
  function notesDocument(bundle, options = {}) {
4254
4355
  const out = [`# ${bundle.meeting.title}`, ""];
4255
4356
  if (options.when) out.push(`_${options.when}_`, "");
4256
- const people = bundle.attendees.filter((person) => !person.isAgent && person.joinedAt !== null).map((person) => displayNameFor(bundle.names, person.identity, person.displayName));
4357
+ const people = uniqueAttendees(
4358
+ bundle.attendees.filter((person) => !person.isAgent && person.joinedAt !== null),
4359
+ bundle.names
4360
+ ).map((person) => displayNameFor(bundle.names, person.identity, person.displayName));
4257
4361
  if (people.length > 0) out.push(`**Attended:** ${people.join(", ")}`, "");
4258
4362
  out.push("## Summary", "", bundle.summary?.text.trim() || "No summary was written.", "");
4259
4363
  out.push("## Decisions", "");
@@ -4317,19 +4421,8 @@ async function loadMeetingChat(api, meetingId, roomToken = null) {
4317
4421
  refusal: null
4318
4422
  };
4319
4423
  }
4320
- async function refusalOf(response, operation) {
4321
- let body = null;
4322
- try {
4323
- body = await response.json();
4324
- } catch {
4325
- body = null;
4326
- }
4327
- const said = readServerRefusal(body);
4328
- return new MeetError(
4329
- response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
4330
- `${operation}: ${said?.reason ?? `the server answered ${response.status}`}`,
4331
- said?.remedy || "Nothing was sent. Try again."
4332
- );
4424
+ function refusalOf(response, operation) {
4425
+ return refusalFromResponse(response, operation, "Nothing was sent. Try again.");
4333
4426
  }
4334
4427
  function asRecipient(row) {
4335
4428
  const sources = Array.isArray(row["sources"]) ? row["sources"] : [];
@@ -4390,18 +4483,11 @@ async function sendFollowUp(api, meetingId, input) {
4390
4483
  }
4391
4484
 
4392
4485
  // src/core/management.ts
4393
- async function refusal(response, operation) {
4394
- let body = null;
4395
- try {
4396
- body = await response.json();
4397
- } catch {
4398
- body = null;
4399
- }
4400
- const said = readServerRefusal(body);
4401
- return new MeetError(
4402
- response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
4403
- `${operation}: ${said?.reason ?? `the server answered ${response.status}`}`,
4404
- said?.remedy || "The meeting itself is saved \u2014 only the telling failed. Try again; the call is idempotent."
4486
+ function refusal(response, operation) {
4487
+ return refusalFromResponse(
4488
+ response,
4489
+ operation,
4490
+ "The meeting itself is saved \u2014 only the telling failed. Try again; the call is idempotent."
4405
4491
  );
4406
4492
  }
4407
4493
  function count(body, key) {
@@ -4727,6 +4813,10 @@ function createRoomEngine(options) {
4727
4813
  }
4728
4814
  },
4729
4815
  disconnected(reason) {
4816
+ if (current !== null) {
4817
+ tokens.invalidate(current.roomName);
4818
+ current = null;
4819
+ }
4730
4820
  store.patch({ phase: "left" });
4731
4821
  options.onLeft?.(reason);
4732
4822
  }
@@ -5029,19 +5119,8 @@ function createInRoomClient(options) {
5029
5119
  const token = options.resolveRoomToken();
5030
5120
  return token === null ? void 0 : { [ROOM_TOKEN_HEADER]: token };
5031
5121
  }
5032
- async function refusal2(response, operation, remedy) {
5033
- let body = null;
5034
- try {
5035
- body = await response.json();
5036
- } catch {
5037
- body = null;
5038
- }
5039
- const said = readServerRefusal(body);
5040
- return new MeetError(
5041
- response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
5042
- said?.reason ?? `${operation}: the server answered ${response.status}`,
5043
- said?.remedy !== void 0 && said.remedy.length > 0 ? said.remedy : remedy
5044
- );
5122
+ function refusal2(response, operation, remedy) {
5123
+ return refusalFromResponse(response, operation, remedy);
5045
5124
  }
5046
5125
  return {
5047
5126
  async moderate(request) {