@zooid/transport-matrix 0.7.0 → 0.7.2

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.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { TransportContextProvider, HistoryOptions, HistoryPage, ThreadOverviewPage, Member, ChannelInfo, AcpRegistry, ApprovalCorrelator } from '@zooid/core';
1
+ import { TransportContextProvider, HistoryOptions, HistoryPage, ThreadOverviewPage, Member, ChannelInfo, RoomBinding, AcpRegistry, ApprovalCorrelator } from '@zooid/core';
2
2
  import * as hono_types from 'hono/types';
3
3
  import { Hono } from 'hono';
4
4
 
@@ -53,6 +53,20 @@ declare class MatrixClient {
53
53
  /** Optional `m.room.name`. When set, sent in the createRoom body so the
54
54
  * room has a display name from the moment it exists. */
55
55
  name?: string;
56
+ /** When set, the room is created with a `restricted` join rule whose allow
57
+ * condition references this space room ID — i.e. joinable by space members
58
+ * only, rather than the whole homeserver. */
59
+ restrictedToSpaceId?: string;
60
+ /** Explicit room version. Restricted join rules require v8+; omit to use the
61
+ * homeserver default (modern Tuwunel defaults to v10/v11). */
62
+ roomVersion?: string;
63
+ /**
64
+ * Seeds `m.room.power_levels.users` at creation via
65
+ * `power_level_content_override.users`. The caller owns the full map —
66
+ * typically the AS bot at 100, plus operator and any agents with
67
+ * declared PLs. Empty/absent → no override (the preset's defaults apply).
68
+ */
69
+ userPowerLevels?: Record<string, number>;
56
70
  }): Promise<string>;
57
71
  createRoomRaw(opts: {
58
72
  asUserId: string;
@@ -67,6 +81,17 @@ declare class MatrixClient {
67
81
  }): Promise<{
68
82
  event_id: string;
69
83
  }>;
84
+ /**
85
+ * Invite a user to a room. Sent as the inviter (`asUserId`) — that user
86
+ * needs invite power in the room. Tolerates the "already in room /
87
+ * already invited" responses idempotently so bootstrap can run on a
88
+ * fresh AND a populated homeserver without branching.
89
+ */
90
+ invite(opts: {
91
+ roomId: string;
92
+ asUserId: string;
93
+ targetUserId: string;
94
+ }): Promise<void>;
70
95
  joinRoom(roomIdOrAlias: string, asUserId: string): Promise<void>;
71
96
  sendMessage(input: SendMessageInput): Promise<{
72
97
  event_id: string;
@@ -188,7 +213,13 @@ interface AgentBinding {
188
213
  userId: string;
189
214
  /** Optional human-readable display name. Falls back to the user_id localpart. */
190
215
  displayName?: string;
191
- rooms: string[];
216
+ /**
217
+ * Rooms this agent is bound to. Each entry's `alias` starts out as the
218
+ * configured `#alias` (or `!id`) and is rewritten to the canonical room
219
+ * ID by `BotPool.bootstrap`. Optional `powerLevel` is seeded into the
220
+ * room's `m.room.power_levels.users` at room creation only.
221
+ */
222
+ rooms: RoomBinding[];
192
223
  trigger: 'mention' | 'any';
193
224
  }
194
225
  interface ThreadState {
@@ -219,11 +250,17 @@ interface BootstrapOpts {
219
250
  spaceRoomId?: string;
220
251
  /** AS bot user ID. Required when spaceRoomId is set; sender of the m.space.child write. */
221
252
  asUserId?: string;
253
+ /**
254
+ * Operator MXIDs seeded at PL 100 in every agent room this pool creates.
255
+ * Applied via `power_level_content_override.users` at room creation only —
256
+ * never reconciled. Empty/absent = no operator entries.
257
+ */
258
+ adminUserIds?: string[];
222
259
  }
223
260
  declare class BotPool {
224
261
  private readonly client;
225
262
  private readonly agents;
226
- constructor(client: Pick<MatrixClient, 'registerBot' | 'joinRoom' | 'resolveAlias' | 'createRoom' | 'sendStateEvent' | 'setDisplayName'>, agents: AgentBinding[]);
263
+ constructor(client: Pick<MatrixClient, 'registerBot' | 'invite' | 'joinRoom' | 'resolveAlias' | 'createRoom' | 'sendStateEvent' | 'setDisplayName'>, agents: AgentBinding[]);
227
264
  bootstrap(opts?: BootstrapOpts): Promise<void>;
228
265
  findByUserId(userId: string): AgentBinding | undefined;
229
266
  findByName(name: string): AgentBinding | undefined;
@@ -237,12 +274,19 @@ interface CreateMatrixTransportOptions {
237
274
  hsToken: string;
238
275
  /** Admin Matrix user ID. When set, BotPool.bootstrap invites this user into rooms it creates. */
239
276
  adminUserId?: string;
277
+ /** Post-turn drain: keep collecting trailing `agent_message_chunk`s until the
278
+ * buffer is quiet for this long before flushing. Defaults to `DRAIN_QUIET_MS`.
279
+ * Set to 0 to disable the drain (e.g. in tests). */
280
+ drainQuietMs?: number;
281
+ /** Hard cap on the post-turn drain. Defaults to `DRAIN_MAX_MS`. */
282
+ drainMaxMs?: number;
240
283
  }
241
284
  declare function createMatrixTransport(opts: CreateMatrixTransportOptions): {
242
285
  app: Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
243
286
  bootstrap: (bootstrapOpts?: {
244
287
  spaceRoomId?: string;
245
288
  asUserId?: string;
289
+ adminUserIds?: string[];
246
290
  }) => Promise<void>;
247
291
  pool: BotPool;
248
292
  };
@@ -253,8 +297,38 @@ interface EnsureSpaceOpts {
253
297
  serverName: string;
254
298
  spaceLocalpart: string;
255
299
  preset: 'public_chat' | 'private_chat';
300
+ /**
301
+ * Operator MXIDs to seed at PL 100 in the space's `m.room.power_levels`
302
+ * at creation. The AS bot is always included. Empty/absent → no override
303
+ * (the preset's PL defaults apply). Only consulted on first creation —
304
+ * if the alias already resolves we return the existing room untouched.
305
+ */
306
+ admins?: string[];
256
307
  }
257
308
  declare function ensureWorkforceSpace(opts: EnsureSpaceOpts): Promise<string>;
309
+ interface EnsureDefaultChannelOpts {
310
+ client: MatrixClient;
311
+ asUserId: string;
312
+ serverName: string;
313
+ spaceId: string;
314
+ /** Localpart of the default channel; defaults to `general`. */
315
+ channelLocalpart?: string;
316
+ /**
317
+ * Operator MXIDs to seed at PL 100 in the channel's `m.room.power_levels`
318
+ * at creation. The AS bot is always included. Empty/absent → no override
319
+ * (the preset's PL defaults apply). Only consulted on first creation —
320
+ * if the alias already resolves we return the existing room untouched.
321
+ */
322
+ admins?: string[];
323
+ }
324
+ /**
325
+ * Ensure a space has a default channel (`#general` by default), restricted to
326
+ * the space's members and attached as an `m.space.child`. Idempotent: returns
327
+ * the existing room if the alias already resolves. Has no agent — it's the
328
+ * human landing room, so it is created here at provisioning time rather than
329
+ * via the agent-room path.
330
+ */
331
+ declare function ensureDefaultChannel(opts: EnsureDefaultChannelOpts): Promise<string>;
258
332
  declare function serverNameFromMxid(mxid: string): string;
259
333
 
260
334
  interface WorkforceRoster {
@@ -285,4 +359,4 @@ interface StartOpts {
285
359
  }
286
360
  declare function startWorkforcePublisher(opts: StartOpts): Promise<PublisherHandle>;
287
361
 
288
- export { type AgentBinding, BotPool, type CreateMatrixTransportOptions, type EnsureSpaceOpts, MatrixClient, type MatrixClientOptions, MatrixContextProvider, type MatrixContextProviderOpts, type MatrixTransportConfig, type MaybeMessage, type PublishOpts, type PublisherHandle, type RouteMatch, type SendCustomEventInput, type SendMessageInput, type StartOpts as StartWorkforcePublisherOpts, type WorkforceRoster, buildWorkforceRoster, createMatrixTransport, ensureWorkforceSpace, extractMentions, publishWorkforce, renderRegistration, route, serverNameFromMxid, startWorkforcePublisher };
362
+ export { type AgentBinding, BotPool, type CreateMatrixTransportOptions, type EnsureDefaultChannelOpts, type EnsureSpaceOpts, MatrixClient, type MatrixClientOptions, MatrixContextProvider, type MatrixContextProviderOpts, type MatrixTransportConfig, type MaybeMessage, type PublishOpts, type PublisherHandle, type RouteMatch, type SendCustomEventInput, type SendMessageInput, type StartOpts as StartWorkforcePublisherOpts, type WorkforceRoster, buildWorkforceRoster, createMatrixTransport, ensureDefaultChannel, ensureWorkforceSpace, extractMentions, publishWorkforce, renderRegistration, route, serverNameFromMxid, startWorkforcePublisher };
package/dist/index.js CHANGED
@@ -39,6 +39,22 @@ var MatrixClient = class {
39
39
  preset: opts.preset ?? "public_chat"
40
40
  };
41
41
  if (opts.name !== void 0) body.name = opts.name;
42
+ if (opts.roomVersion !== void 0) body.room_version = opts.roomVersion;
43
+ if (opts.restrictedToSpaceId !== void 0) {
44
+ body.initial_state = [
45
+ {
46
+ type: "m.room.join_rules",
47
+ state_key: "",
48
+ content: {
49
+ join_rule: "restricted",
50
+ allow: [{ type: "m.room_membership", room_id: opts.restrictedToSpaceId }]
51
+ }
52
+ }
53
+ ];
54
+ }
55
+ if (opts.userPowerLevels && Object.keys(opts.userPowerLevels).length > 0) {
56
+ body.power_level_content_override = { users: opts.userPowerLevels };
57
+ }
42
58
  const r = await this.fetch(
43
59
  `${this.homeserver}/_matrix/client/v3/createRoom?user_id=${encodeURIComponent(opts.senderUserId)}`,
44
60
  {
@@ -84,6 +100,30 @@ var MatrixClient = class {
84
100
  if (!r.ok) throw new Error(`sendStateEvent ${opts.eventType} failed: ${r.status}`);
85
101
  return await r.json();
86
102
  }
103
+ /**
104
+ * Invite a user to a room. Sent as the inviter (`asUserId`) — that user
105
+ * needs invite power in the room. Tolerates the "already in room /
106
+ * already invited" responses idempotently so bootstrap can run on a
107
+ * fresh AND a populated homeserver without branching.
108
+ */
109
+ async invite(opts) {
110
+ const url = `${this.homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(opts.roomId)}/invite?user_id=${encodeURIComponent(opts.asUserId)}`;
111
+ const r = await this.fetch(url, {
112
+ method: "POST",
113
+ headers: {
114
+ Authorization: `Bearer ${this.asToken}`,
115
+ "content-type": "application/json"
116
+ },
117
+ body: JSON.stringify({ user_id: opts.targetUserId })
118
+ });
119
+ if (r.ok) return;
120
+ if (r.status === 403) {
121
+ const body = await r.text();
122
+ if (/already (in the room|invited|a member|joined)/i.test(body)) return;
123
+ throw new Error(`invite(${opts.targetUserId}) failed: 403 ${body}`);
124
+ }
125
+ throw new Error(`invite(${opts.targetUserId}) failed: ${r.status}`);
126
+ }
87
127
  async joinRoom(roomIdOrAlias, asUserId) {
88
128
  const url = `${this.homeserver}/_matrix/client/v3/join/${encodeURIComponent(roomIdOrAlias)}?user_id=${encodeURIComponent(asUserId)}`;
89
129
  const r = await this.fetch(url, {
@@ -412,7 +452,7 @@ function route(event, agents, threadStates) {
412
452
  const threadState = threadRoot ? threadStates?.get(threadRoot) : void 0;
413
453
  for (const a of agents) {
414
454
  if (event.sender === a.userId) continue;
415
- if (!a.rooms.includes(event.room_id ?? "")) continue;
455
+ if (!a.rooms.some((r) => r.alias === event.room_id)) continue;
416
456
  if (a.trigger === "any") {
417
457
  matches.push(a);
418
458
  continue;
@@ -439,15 +479,50 @@ async function ensureWorkforceSpace(opts) {
439
479
  const existing = await opts.client.resolveAlias(alias);
440
480
  if (existing) return existing;
441
481
  const display = opts.spaceLocalpart.charAt(0).toUpperCase() + opts.spaceLocalpart.slice(1);
442
- return opts.client.createRoomRaw({
482
+ const body = {
483
+ room_alias_name: opts.spaceLocalpart,
484
+ name: display,
485
+ preset: opts.preset,
486
+ creation_content: { type: "m.space" },
487
+ // A workspace is joined by invitation, not self-service. Pin the space's
488
+ // join rule to invite regardless of preset so it can't be walked into
489
+ // (which would otherwise satisfy every restricted child room's allow).
490
+ initial_state: [{ type: "m.room.join_rules", state_key: "", content: { join_rule: "invite" } }]
491
+ };
492
+ if (opts.admins && opts.admins.length > 0) {
493
+ body.invite = opts.admins;
494
+ const users = { [opts.asUserId]: 100 };
495
+ for (const a of opts.admins) users[a] = 100;
496
+ body.power_level_content_override = { users };
497
+ }
498
+ return opts.client.createRoomRaw({ asUserId: opts.asUserId, body });
499
+ }
500
+ async function ensureDefaultChannel(opts) {
501
+ const localpart2 = opts.channelLocalpart ?? "general";
502
+ const alias = `#${localpart2}:${opts.serverName}`;
503
+ const existing = await opts.client.resolveAlias(alias);
504
+ if (existing) return existing;
505
+ let userPowerLevels;
506
+ if (opts.admins && opts.admins.length > 0) {
507
+ userPowerLevels = { [opts.asUserId]: 100 };
508
+ for (const a of opts.admins) userPowerLevels[a] = 100;
509
+ }
510
+ const roomId = await opts.client.createRoom({
511
+ roomAliasName: localpart2,
512
+ invite: [],
513
+ senderUserId: opts.asUserId,
514
+ name: localpart2.charAt(0).toUpperCase() + localpart2.slice(1),
515
+ restrictedToSpaceId: opts.spaceId,
516
+ ...userPowerLevels ? { userPowerLevels } : {}
517
+ });
518
+ await opts.client.sendStateEvent({
519
+ roomId: opts.spaceId,
443
520
  asUserId: opts.asUserId,
444
- body: {
445
- room_alias_name: opts.spaceLocalpart,
446
- name: display,
447
- preset: opts.preset,
448
- creation_content: { type: "m.space" }
449
- }
521
+ eventType: "m.space.child",
522
+ stateKey: roomId,
523
+ content: { via: [opts.serverName] }
450
524
  });
525
+ return roomId;
451
526
  }
452
527
  function serverNameFromMxid(mxid) {
453
528
  const colon = mxid.indexOf(":");
@@ -478,8 +553,23 @@ var BotPool = class {
478
553
  } catch (err) {
479
554
  console.warn(`[matrix] setDisplayName(${a.userId}) failed: ${err.message}`);
480
555
  }
556
+ if (opts.spaceRoomId && opts.asUserId) {
557
+ try {
558
+ await this.client.invite({
559
+ roomId: opts.spaceRoomId,
560
+ asUserId: opts.asUserId,
561
+ targetUserId: a.userId
562
+ });
563
+ await this.client.joinRoom(opts.spaceRoomId, a.userId);
564
+ } catch (err) {
565
+ console.warn(
566
+ `[matrix] space membership for ${a.userId} failed: ${err.message}`
567
+ );
568
+ }
569
+ }
481
570
  for (let i = 0; i < a.rooms.length; i++) {
482
- const room = a.rooms[i];
571
+ const binding = a.rooms[i];
572
+ const room = binding.alias;
483
573
  try {
484
574
  let resolved = room;
485
575
  if (room.startsWith("#")) {
@@ -494,17 +584,25 @@ var BotPool = class {
494
584
  const colon = room.indexOf(":");
495
585
  const aliasLocalpart = colon > 1 ? room.slice(1, colon) : room.slice(1);
496
586
  const sender = opts.adminUserId ?? a.userId;
587
+ const userPowerLevels = buildUserPowerLevels(
588
+ opts.asUserId,
589
+ opts.adminUserIds,
590
+ this.agents,
591
+ room
592
+ );
497
593
  resolved = await this.client.createRoom({
498
594
  roomAliasName: aliasLocalpart,
499
595
  invite: opts.adminUserId ? [opts.adminUserId] : [],
500
596
  senderUserId: sender,
501
- name: aliasLocalpart
597
+ name: aliasLocalpart,
598
+ ...opts.spaceRoomId ? { restrictedToSpaceId: opts.spaceRoomId } : {},
599
+ ...userPowerLevels ? { userPowerLevels } : {}
502
600
  });
503
601
  }
504
602
  aliasToId.set(room, resolved);
505
603
  }
506
604
  }
507
- a.rooms[i] = resolved;
605
+ binding.alias = resolved;
508
606
  await this.client.joinRoom(resolved, a.userId);
509
607
  if (opts.spaceRoomId && opts.asUserId && !attachedToSpace.has(resolved)) {
510
608
  attachedToSpace.add(resolved);
@@ -543,6 +641,18 @@ function localpart(userId) {
543
641
  if (!m) throw new Error(`bad user id: ${userId}`);
544
642
  return m[1];
545
643
  }
644
+ function buildUserPowerLevels(asUserId, admins, agents, roomAlias) {
645
+ const users = {};
646
+ if (asUserId) users[asUserId] = 100;
647
+ if (admins) for (const a of admins) users[a] = 100;
648
+ for (const a of agents) {
649
+ for (const r of a.rooms) {
650
+ if (r.alias !== roomAlias) continue;
651
+ if (r.powerLevel !== void 0) users[a.userId] = r.powerLevel;
652
+ }
653
+ }
654
+ return Object.keys(users).length > 0 ? users : void 0;
655
+ }
546
656
 
547
657
  // src/transport.ts
548
658
  import { Hono } from "hono";
@@ -596,6 +706,33 @@ function toPlanBody(evt) {
596
706
  entries: evt.entries
597
707
  };
598
708
  }
709
+ var RECOVERY_URLS = {
710
+ auth_missing: "https://zooid.dev/docs/guides/run-in-container#authentication-that-carries-over",
711
+ auth_invalid: "https://zooid.dev/docs/guides/run-in-container#authentication-that-carries-over",
712
+ mount_failed: "https://zooid.dev/docs/guides/run-in-container#what-you-get-for-free",
713
+ image_pull_failed: "https://zooid.dev/docs/guides/run-in-container#skipping-the-image-prepull"
714
+ };
715
+ function toErrorBody(evt, threadRoot) {
716
+ const msg = evt.message.slice(0, 250);
717
+ const out = {
718
+ msgtype: "m.notice",
719
+ body: `\u26A0 [${evt.code}] ${msg}`,
720
+ code: evt.code,
721
+ message: msg,
722
+ transient: evt.transient,
723
+ "m.relates_to": { rel_type: "m.thread", event_id: threadRoot }
724
+ };
725
+ if (evt.sessionId) out.session_id = evt.sessionId;
726
+ if (evt.turnId) out.turn_id = evt.turnId;
727
+ if (evt.detail) out.detail = evt.detail.slice(0, 2e3);
728
+ if (evt.acp_error) out.acp_error = evt.acp_error;
729
+ const recovery = RECOVERY_URLS[evt.code];
730
+ if (recovery) out.recovery = recovery;
731
+ return out;
732
+ }
733
+
734
+ // src/transport.ts
735
+ import { classify } from "@zooid/acp-client";
599
736
 
600
737
  // src/markdown-to-matrix-html.ts
601
738
  import { marked } from "marked";
@@ -669,12 +806,17 @@ function toMatrixHtml(markdown) {
669
806
  // src/transport.ts
670
807
  var STARTUP_GRACE_MS = 5e3;
671
808
  var SEEN_EVENT_CAP = 5e3;
809
+ var DRAIN_QUIET_MS = 300;
810
+ var DRAIN_MAX_MS = 3e3;
811
+ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
672
812
  function inboundThreadRoot2(evt) {
673
813
  const r = evt.content?.["m.relates_to"];
674
814
  return r?.rel_type === "m.thread" && r.event_id ? r.event_id : void 0;
675
815
  }
676
816
  function createMatrixTransport(opts) {
677
817
  const { agents, approvals, client, bindings, hsToken, adminUserId } = opts;
818
+ const drainQuietMs = opts.drainQuietMs ?? DRAIN_QUIET_MS;
819
+ const drainMaxMs = opts.drainMaxMs ?? DRAIN_MAX_MS;
678
820
  const pool = new BotPool(client, bindings);
679
821
  const sessions = /* @__PURE__ */ new Map();
680
822
  const buffers = /* @__PURE__ */ new Map();
@@ -691,7 +833,9 @@ function createMatrixTransport(opts) {
691
833
  if (event.type === "agent_message_chunk") {
692
834
  const block = event.content;
693
835
  if (block.type === "text" && typeof block.text === "string") {
694
- buffers.set(event.sessionId, (buffers.get(event.sessionId) ?? "") + block.text);
836
+ const current = buffers.get(event.sessionId) ?? "";
837
+ const prefix = block.text === "" && current.length > 0 ? "\n\n" : "";
838
+ buffers.set(event.sessionId, current + prefix + block.text);
695
839
  } else {
696
840
  console.warn(`[matrix:${name}] dropped chunk block type=${block.type}`, block);
697
841
  }
@@ -879,6 +1023,29 @@ function createMatrixTransport(opts) {
879
1023
  if (st.participants.at(-1) !== a.name) st.participants.push(a.name);
880
1024
  }).catch((err) => {
881
1025
  console.error(`[matrix] runTurn failed for ${a.name}:`, err);
1026
+ const c2 = classify(err);
1027
+ const threadRoot = inboundThreadRoot2(evt) ?? evt.event_id;
1028
+ if (!threadRoot || !evt.room_id) return;
1029
+ const body2 = toErrorBody(
1030
+ {
1031
+ kind: "error",
1032
+ agentId: a.name,
1033
+ sessionId: null,
1034
+ turnId: null,
1035
+ code: c2.code,
1036
+ message: err instanceof Error ? err.message : String(err),
1037
+ detail: err instanceof Error && err.stack ? err.stack.slice(0, 2e3) : void 0,
1038
+ transient: c2.transient,
1039
+ acp_error: c2.acp_error
1040
+ },
1041
+ threadRoot
1042
+ );
1043
+ void client.sendCustomEvent({
1044
+ roomId: evt.room_id,
1045
+ asUserId: a.userId,
1046
+ eventType: "eco.zoon.error",
1047
+ content: body2
1048
+ }).catch((e) => console.warn(`[matrix:${a.name}] eco.zoon.error send failed:`, e));
882
1049
  });
883
1050
  }
884
1051
  }
@@ -931,6 +1098,14 @@ function createMatrixTransport(opts) {
931
1098
  channelId: evt.room_id,
932
1099
  content: [{ type: "text", text: promptText }]
933
1100
  });
1101
+ const drainStart = Date.now();
1102
+ let drained = buffers.get(sessionId) ?? "";
1103
+ while (drainQuietMs > 0 && Date.now() - drainStart < drainMaxMs) {
1104
+ await delay(drainQuietMs);
1105
+ const next = buffers.get(sessionId) ?? "";
1106
+ if (next === drained) break;
1107
+ drained = next;
1108
+ }
934
1109
  const text = buffers.get(sessionId) ?? "";
935
1110
  if (text.length > 0) {
936
1111
  const html = toMatrixHtml(text);
@@ -982,7 +1157,7 @@ function createMatrixTransport(opts) {
982
1157
  }
983
1158
  async function rebuildThreadState(client, roomId, rootEventId, bindings) {
984
1159
  const state = { participants: [], rootMentions: [] };
985
- const asUser = (bindings.find((b) => b.rooms.includes(roomId)) ?? bindings[0])?.userId;
1160
+ const asUser = (bindings.find((b) => b.rooms.some((r) => r.alias === roomId)) ?? bindings[0])?.userId;
986
1161
  if (!asUser) return state;
987
1162
  const root = await client.fetchEvent(roomId, rootEventId, asUser);
988
1163
  if (root) {
@@ -1037,7 +1212,11 @@ function truncate(s, n) {
1037
1212
  function buildWorkforceRoster(agents) {
1038
1213
  return {
1039
1214
  version: 1,
1040
- agents: agents.map((a) => ({ user_id: a.userId, name: a.name, rooms: a.rooms }))
1215
+ agents: agents.map((a) => ({
1216
+ user_id: a.userId,
1217
+ name: a.name,
1218
+ rooms: a.rooms.map((r) => r.alias)
1219
+ }))
1041
1220
  };
1042
1221
  }
1043
1222
  async function publishWorkforce(opts) {
@@ -1065,6 +1244,7 @@ export {
1065
1244
  MatrixContextProvider,
1066
1245
  buildWorkforceRoster,
1067
1246
  createMatrixTransport,
1247
+ ensureDefaultChannel,
1068
1248
  ensureWorkforceSpace,
1069
1249
  extractMentions,
1070
1250
  publishWorkforce,