@zooid/transport-matrix 0.9.0 → 0.10.0

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.js CHANGED
@@ -53,7 +53,9 @@ var MatrixClient = class {
53
53
  ];
54
54
  }
55
55
  if (opts.userPowerLevels && Object.keys(opts.userPowerLevels).length > 0) {
56
- body.power_level_content_override = { users: opts.userPowerLevels };
56
+ const users = { ...opts.userPowerLevels };
57
+ if (users[opts.senderUserId] === void 0) users[opts.senderUserId] = 100;
58
+ body.power_level_content_override = { users };
57
59
  }
58
60
  const r = await this.fetch(
59
61
  `${this.homeserver}/_matrix/client/v3/createRoom?user_id=${encodeURIComponent(opts.senderUserId)}`,
@@ -255,6 +257,19 @@ var MatrixClient = class {
255
257
  if (!r.ok) throw new Error(`getJoinedMembers(${roomId}) failed: ${r.status}`);
256
258
  return await r.json();
257
259
  }
260
+ async sync(opts) {
261
+ const params = new URLSearchParams({
262
+ user_id: opts.asUserId,
263
+ timeout: String(opts.timeoutMs ?? 3e4)
264
+ });
265
+ if (opts.since) params.set("since", opts.since);
266
+ const url = `${this.homeserver}/_matrix/client/v3/sync?${params.toString()}`;
267
+ const r = await this.fetch(url, {
268
+ headers: { Authorization: `Bearer ${this.asToken}` }
269
+ });
270
+ if (!r.ok) throw new Error(`sync(${opts.asUserId}) failed: ${r.status}`);
271
+ return r.json();
272
+ }
258
273
  async fetchRoomName(roomId, asUserId) {
259
274
  const url = `${this.homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/state/m.room.name/?user_id=${encodeURIComponent(asUserId)}`;
260
275
  const r = await this.fetch(url, {
@@ -284,6 +299,7 @@ var MatrixContextProvider = class {
284
299
  constructor(opts) {
285
300
  this.opts = opts;
286
301
  }
302
+ opts;
287
303
  async getRoomHistory(channelId, hopts) {
288
304
  const { chunk, end } = await this.opts.client.fetchRoomMessages({
289
305
  roomId: channelId,
@@ -443,6 +459,29 @@ function renderRegistration(c) {
443
459
  );
444
460
  }
445
461
 
462
+ // src/identity.ts
463
+ var SLUG_RE = /^[a-z0-9-]+$/;
464
+ var AGENT_KEY_RE = /^[a-z0-9-]+$/;
465
+ var isValidWorkstation = (s) => SLUG_RE.test(s);
466
+ var isValidAgentKey = (s) => AGENT_KEY_RE.test(s);
467
+ function agentMxid(workstation, agent, serverName) {
468
+ if (!isValidWorkstation(workstation))
469
+ throw new Error(`invalid workstation: ${JSON.stringify(workstation)}`);
470
+ if (!isValidAgentKey(agent)) throw new Error(`invalid agent key: ${JSON.stringify(agent)}`);
471
+ return `@${workstation}.${agent}:${serverName}`;
472
+ }
473
+ function splitAgentLocalpart(localpart2) {
474
+ const i = localpart2.indexOf(".");
475
+ if (i <= 0 || i === localpart2.length - 1)
476
+ throw new Error(`not a workstation.agent localpart: ${localpart2}`);
477
+ return { workstation: localpart2.slice(0, i), agent: localpart2.slice(i + 1) };
478
+ }
479
+ function workstationUserNamespace(workstation, serverName) {
480
+ if (!isValidWorkstation(workstation))
481
+ throw new Error(`invalid workstation: ${JSON.stringify(workstation)}`);
482
+ return `@${workstation}\\..*:${serverName}`;
483
+ }
484
+
446
485
  // src/mentions.ts
447
486
  var MATRIX_TO_RE = /https:\/\/matrix\.to\/#\/(@[^"<>\s]+)/g;
448
487
  var RAW_USER_RE = /(@[A-Za-z0-9._\-=/+]+:[A-Za-z0-9.\-]+)/g;
@@ -495,11 +534,16 @@ function route(event, agents, threadStates) {
495
534
  continue;
496
535
  }
497
536
  if (threadState) {
498
- const lastPoster = threadState.participants.at(-1);
499
- if (lastPoster) {
500
- if (lastPoster === a.name) matches.push(a);
501
- } else if (threadState.rootMentions.includes(a.name)) {
502
- matches.push(a);
537
+ const senderAgent = agents.find((x) => x.userId === event.sender);
538
+ if (senderAgent) {
539
+ if (threadState.callers[senderAgent.name] === a.name) matches.push(a);
540
+ } else {
541
+ const lastPoster = threadState.participants.at(-1);
542
+ if (lastPoster) {
543
+ if (lastPoster === a.name) matches.push(a);
544
+ } else if (threadState.rootMentions.includes(a.name)) {
545
+ matches.push(a);
546
+ }
503
547
  }
504
548
  }
505
549
  }
@@ -573,6 +617,8 @@ var BotPool = class {
573
617
  this.client = client;
574
618
  this.agents = agents;
575
619
  }
620
+ client;
621
+ agents;
576
622
  async bootstrap(opts = {}) {
577
623
  const aliasToId = /* @__PURE__ */ new Map();
578
624
  const attachedToSpace = /* @__PURE__ */ new Set();
@@ -618,21 +664,27 @@ var BotPool = class {
618
664
  } else {
619
665
  const colon = room.indexOf(":");
620
666
  const aliasLocalpart = colon > 1 ? room.slice(1, colon) : room.slice(1);
621
- const sender = opts.adminUserId ?? a.userId;
667
+ const sender = opts.asUserId ?? opts.adminUserId ?? a.userId;
622
668
  const userPowerLevels = buildUserPowerLevels(
623
669
  opts.asUserId,
624
670
  opts.adminUserIds,
625
671
  this.agents,
626
672
  room
627
673
  );
628
- resolved = await this.client.createRoom({
629
- roomAliasName: aliasLocalpart,
630
- invite: opts.adminUserId ? [opts.adminUserId] : [],
631
- senderUserId: sender,
632
- name: aliasLocalpart,
633
- ...opts.spaceRoomId ? { restrictedToSpaceId: opts.spaceRoomId } : {},
634
- ...userPowerLevels ? { userPowerLevels } : {}
635
- });
674
+ try {
675
+ resolved = await this.client.createRoom({
676
+ roomAliasName: aliasLocalpart,
677
+ invite: opts.adminUserId ? [opts.adminUserId] : [],
678
+ senderUserId: sender,
679
+ name: aliasLocalpart,
680
+ ...opts.spaceRoomId ? { restrictedToSpaceId: opts.spaceRoomId } : {},
681
+ ...userPowerLevels ? { userPowerLevels } : {}
682
+ });
683
+ } catch (err) {
684
+ const raced = await this.client.resolveAlias(room);
685
+ if (!raced) throw err;
686
+ resolved = raced;
687
+ }
636
688
  }
637
689
  aliasToId.set(room, resolved);
638
690
  }
@@ -937,6 +989,44 @@ function writeAttachment(input) {
937
989
  return { hostPath, agentPath };
938
990
  }
939
991
 
992
+ // src/sync-loop.ts
993
+ var SyncLoop = class {
994
+ opts;
995
+ running = false;
996
+ constructor(opts) {
997
+ this.opts = opts;
998
+ }
999
+ async tick() {
1000
+ const since = this.opts.loadSince();
1001
+ const res = await this.opts.client.sync({
1002
+ asUserId: this.opts.asUserId,
1003
+ since,
1004
+ timeoutMs: this.opts.timeoutMs ?? 3e4
1005
+ });
1006
+ for (const [roomId, roomState] of Object.entries(res.rooms?.join ?? {})) {
1007
+ for (const baseEvt of roomState.timeline?.events ?? []) {
1008
+ await this.opts.onEvent({ ...baseEvt, room_id: roomId });
1009
+ }
1010
+ }
1011
+ this.opts.saveSince(res.next_batch);
1012
+ }
1013
+ async run() {
1014
+ this.running = true;
1015
+ while (this.running) {
1016
+ try {
1017
+ await this.tick();
1018
+ } catch (err) {
1019
+ if (!this.running) break;
1020
+ console.warn(`[sync-loop] ${this.opts.asUserId} tick failed, retrying:`, err);
1021
+ await new Promise((r) => setTimeout(r, this.opts.retryDelayMs ?? 5e3));
1022
+ }
1023
+ }
1024
+ }
1025
+ stop() {
1026
+ this.running = false;
1027
+ }
1028
+ };
1029
+
940
1030
  // src/transport.ts
941
1031
  var STARTUP_GRACE_MS = 5e3;
942
1032
  async function buildMediaBlocks(items, opts) {
@@ -1030,7 +1120,7 @@ function inboundThreadRoot2(evt) {
1030
1120
  return r?.rel_type === "m.thread" && r.event_id ? r.event_id : void 0;
1031
1121
  }
1032
1122
  function createMatrixTransport(opts) {
1033
- const { agents, approvals, client, bindings, hsToken, adminUserId, botUserId } = opts;
1123
+ const { agents, approvals, client, bindings, hsToken, adminUserId, botUserId, mode = "appservice" } = opts;
1034
1124
  const drainQuietMs = opts.drainQuietMs ?? DRAIN_QUIET_MS;
1035
1125
  const drainMaxMs = opts.drainMaxMs ?? DRAIN_MAX_MS;
1036
1126
  const mediaClient = opts.media;
@@ -1047,7 +1137,7 @@ function createMatrixTransport(opts) {
1047
1137
  const bufferMessageIds = /* @__PURE__ */ new Map();
1048
1138
  const sendQueue = /* @__PURE__ */ new Map();
1049
1139
  const threadStates = /* @__PURE__ */ new Map();
1050
- const cutoffTs = Date.now() - STARTUP_GRACE_MS;
1140
+ const cutoffTs = mode === "client" ? Number.NEGATIVE_INFINITY : Date.now() - STARTUP_GRACE_MS;
1051
1141
  const seenEventIds = /* @__PURE__ */ new Set();
1052
1142
  const flushedCounts = /* @__PURE__ */ new Map();
1053
1143
  const pendingCommands = /* @__PURE__ */ new Map();
@@ -1184,194 +1274,198 @@ function createMatrixTransport(opts) {
1184
1274
  content
1185
1275
  });
1186
1276
  });
1187
- const app = new Hono();
1188
- function authOk(authHeader) {
1189
- const h = authHeader ?? "";
1190
- if (!h.startsWith("Bearer ")) return false;
1191
- const got = h.slice(7);
1192
- if (got.length !== hsToken.length) return false;
1193
- return timingSafeEqual(Buffer.from(got), Buffer.from(hsToken));
1194
- }
1195
- app.put("/_matrix/app/v1/transactions/:txnId", async (c) => {
1196
- if (!authOk(c.req.header("authorization"))) {
1197
- return c.json({ errcode: "M_FORBIDDEN" }, 403);
1198
- }
1199
- const body = await c.req.json().catch(() => ({}));
1200
- for (const evt of body.events ?? []) {
1201
- if (evt.event_id) {
1202
- if (seenEventIds.has(evt.event_id)) {
1203
- continue;
1204
- }
1205
- seenEventIds.add(evt.event_id);
1206
- if (seenEventIds.size > SEEN_EVENT_CAP) {
1207
- const first = seenEventIds.values().next().value;
1208
- if (first !== void 0) seenEventIds.delete(first);
1209
- }
1277
+ async function handleInboundEvent(evt) {
1278
+ if (evt.event_id) {
1279
+ if (seenEventIds.has(evt.event_id)) {
1280
+ return;
1210
1281
  }
1211
- if (evt.origin_server_ts !== void 0 && evt.origin_server_ts < cutoffTs && evt.type === "m.room.message") {
1282
+ seenEventIds.add(evt.event_id);
1283
+ if (seenEventIds.size > SEEN_EVENT_CAP) {
1284
+ const first = seenEventIds.values().next().value;
1285
+ if (first !== void 0) seenEventIds.delete(first);
1286
+ }
1287
+ }
1288
+ if (evt.origin_server_ts !== void 0 && evt.origin_server_ts < cutoffTs && evt.type === "m.room.message") {
1289
+ console.log(
1290
+ `[matrix] dropping stale message event ${evt.event_id} (ts=${evt.origin_server_ts}, daemon started at ${cutoffTs + STARTUP_GRACE_MS})`
1291
+ );
1292
+ return;
1293
+ }
1294
+ if (evt.type === "m.room.member" && evt.content?.membership === "invite") {
1295
+ const target = evt.state_key;
1296
+ const inviter = evt.sender;
1297
+ if (target && evt.room_id && ourBotUserIds.has(target) && (!inviter || !ourBotUserIds.has(inviter))) {
1212
1298
  console.log(
1213
- `[matrix] dropping stale message event ${evt.event_id} (ts=${evt.origin_server_ts}, daemon started at ${cutoffTs + STARTUP_GRACE_MS})`
1299
+ `[matrix] declining ad-hoc invite for ${target} in ${evt.room_id} from ${inviter ?? "unknown"}`
1300
+ );
1301
+ await client.leaveRoom(evt.room_id, target, { reason: DECLINE_REASON }).catch(
1302
+ (err) => console.warn(`[matrix] leaveRoom(${evt.room_id}, ${target}) failed:`, err)
1214
1303
  );
1215
- continue;
1216
1304
  }
1217
- if (evt.type === "m.room.member" && evt.content?.membership === "invite") {
1218
- const target = evt.state_key;
1219
- const inviter = evt.sender;
1220
- if (target && evt.room_id && ourBotUserIds.has(target) && (!inviter || !ourBotUserIds.has(inviter))) {
1221
- console.log(
1222
- `[matrix] declining ad-hoc invite for ${target} in ${evt.room_id} from ${inviter ?? "unknown"}`
1223
- );
1224
- await client.leaveRoom(evt.room_id, target, { reason: DECLINE_REASON }).catch(
1225
- (err) => console.warn(`[matrix] leaveRoom(${evt.room_id}, ${target}) failed:`, err)
1226
- );
1227
- }
1228
- continue;
1305
+ return;
1306
+ }
1307
+ if (evt.type === "dev.zooid.session_reset") {
1308
+ const relates = evt.content?.["m.relates_to"];
1309
+ const threadRoot = relates?.rel_type === "m.thread" && relates.event_id ? relates.event_id : void 0;
1310
+ if (!threadRoot) {
1311
+ console.log("[matrix] dropping dev.zooid.session_reset without thread relation");
1312
+ return;
1229
1313
  }
1230
- if (evt.type === "dev.zooid.session_reset") {
1231
- const relates = evt.content?.["m.relates_to"];
1232
- const threadRoot = relates?.rel_type === "m.thread" && relates.event_id ? relates.event_id : void 0;
1233
- if (!threadRoot) {
1234
- console.log("[matrix] dropping dev.zooid.session_reset without thread relation");
1235
- continue;
1236
- }
1237
- console.log(`[matrix] inbound dev.zooid.session_reset in ${evt.room_id} thread=${threadRoot}`);
1238
- for (const a of bindings) {
1239
- agents.endSession(a.name, threadRoot);
1240
- }
1241
- continue;
1314
+ console.log(`[matrix] inbound dev.zooid.session_reset in ${evt.room_id} thread=${threadRoot}`);
1315
+ for (const a of bindings) {
1316
+ agents.endSession(a.name, threadRoot);
1242
1317
  }
1243
- if (evt.type === "dev.zooid.interrupt") {
1244
- const content = evt.content ?? {};
1245
- const relates = evt.content?.["m.relates_to"];
1246
- const threadRoot = relates?.rel_type === "m.thread" && relates.event_id ? relates.event_id : void 0;
1247
- if (threadRoot) {
1248
- const targets = [];
1249
- for (const [sessionId, ctx2] of sessions) {
1250
- if (ctx2.threadRoot === threadRoot) {
1251
- targets.push({ sessionId, agent: ctx2.agent.name });
1252
- }
1253
- }
1254
- for (const t of targets) {
1255
- console.log(
1256
- `[matrix] interrupt session=${t.sessionId} agent=${t.agent} thread=${threadRoot}` + (content.reason ? ` reason=${content.reason}` : "")
1257
- );
1258
- await agents.cancelSession(t.agent, t.sessionId).catch((err) => {
1259
- console.error(`[matrix] cancelSession(${t.agent}, ${t.sessionId}) failed:`, err);
1260
- });
1318
+ return;
1319
+ }
1320
+ if (evt.type === "dev.zooid.interrupt") {
1321
+ const content = evt.content ?? {};
1322
+ const relates = evt.content?.["m.relates_to"];
1323
+ const threadRoot = relates?.rel_type === "m.thread" && relates.event_id ? relates.event_id : void 0;
1324
+ if (threadRoot) {
1325
+ const targets = [];
1326
+ for (const [sessionId, ctx2] of sessions) {
1327
+ if (ctx2.threadRoot === threadRoot) {
1328
+ targets.push({ sessionId, agent: ctx2.agent.name });
1261
1329
  }
1262
- continue;
1263
1330
  }
1264
- if (!content.session_id) {
1265
- console.warn(`[matrix] dev.zooid.interrupt missing session_id (event_id=${evt.event_id})`);
1266
- continue;
1267
- }
1268
- const ctx = sessions.get(content.session_id);
1269
- if (!ctx) {
1270
- continue;
1271
- }
1272
- console.log(
1273
- `[matrix] interrupt session=${content.session_id} agent=${ctx.agent.name}` + (content.reason ? ` reason=${content.reason}` : "")
1274
- );
1275
- await agents.cancelSession(ctx.agent.name, content.session_id).catch((err) => {
1276
- console.error(`[matrix] cancelSession(${ctx.agent.name}, ${content.session_id}) failed:`, err);
1277
- });
1278
- continue;
1279
- }
1280
- if (evt.type === "dev.zooid.approval_response") {
1281
- const content = evt.content ?? {};
1282
- if (!content.session_id || !content.approval_id || !content.decision) continue;
1283
- const decision = content.option_id ? { decision: content.decision, optionId: content.option_id } : { decision: content.decision };
1284
- const ok = approvals.resolve(
1285
- content.session_id,
1286
- content.approval_id,
1287
- decision
1288
- );
1289
- if (!ok) console.warn(`[matrix] unknown approval ${content.approval_id}`);
1290
- continue;
1291
- }
1292
- logInbound(evt);
1293
- if (evt.type === "m.room.message" && isMediaMsgtype(evt.content?.msgtype) && evt.room_id && evt.event_id && evt.sender && evt.content?.url && !bindings.some((b) => b.userId === evt.sender)) {
1294
- pendingMedia.add(evt.room_id, inboundThreadRoot2(evt), {
1295
- eventId: evt.event_id,
1296
- sender: evt.sender,
1297
- msgtype: evt.content.msgtype,
1298
- body: evt.content.body ?? "",
1299
- filename: evt.content.filename,
1300
- url: evt.content.url,
1301
- info: evt.content.info
1302
- });
1303
- continue;
1304
- }
1305
- const promotedRoot = inboundThreadRoot2(evt) ?? evt.event_id;
1306
- const inboundRel = inboundThreadRoot2(evt);
1307
- if (evt.type === "m.room.message" && inboundRel && !threadStates.has(inboundRel) && evt.room_id) {
1308
- try {
1309
- const rebuilt = await rebuildThreadState(client, evt.room_id, inboundRel, bindings);
1310
- threadStates.set(inboundRel, rebuilt);
1331
+ for (const t of targets) {
1311
1332
  console.log(
1312
- `[matrix] rebuilt threadState for ${inboundRel}: participants=${rebuilt.participants.join(",")} rootMentions=${rebuilt.rootMentions.join(",")}`
1333
+ `[matrix] interrupt session=${t.sessionId} agent=${t.agent} thread=${threadRoot}` + (content.reason ? ` reason=${content.reason}` : "")
1313
1334
  );
1314
- } catch (err) {
1315
- console.warn(`[matrix] failed to rebuild threadState for ${inboundRel}:`, err);
1335
+ await agents.cancelSession(t.agent, t.sessionId).catch((err) => {
1336
+ console.error(`[matrix] cancelSession(${t.agent}, ${t.sessionId}) failed:`, err);
1337
+ });
1316
1338
  }
1339
+ return;
1317
1340
  }
1318
- const matches = route(evt, bindings, threadStates);
1319
- const senderIsBot = bindings.some((b) => b.userId === evt.sender);
1320
- if (evt.type === "m.room.message" && matches.length === 0 && !senderIsBot) {
1321
- console.warn(
1322
- `[matrix] no agent matched message in ${evt.room_id} from ${evt.sender} (bindings: ${bindings.map((b) => `${b.name}@${b.userId}[${b.trigger}]`).join(", ")})`
1341
+ if (!content.session_id) {
1342
+ console.warn(`[matrix] dev.zooid.interrupt missing session_id (event_id=${evt.event_id})`);
1343
+ return;
1344
+ }
1345
+ const ctx = sessions.get(content.session_id);
1346
+ if (!ctx) {
1347
+ return;
1348
+ }
1349
+ console.log(
1350
+ `[matrix] interrupt session=${content.session_id} agent=${ctx.agent.name}` + (content.reason ? ` reason=${content.reason}` : "")
1351
+ );
1352
+ await agents.cancelSession(ctx.agent.name, content.session_id).catch((err) => {
1353
+ console.error(`[matrix] cancelSession(${ctx.agent.name}, ${content.session_id}) failed:`, err);
1354
+ });
1355
+ return;
1356
+ }
1357
+ if (evt.type === "dev.zooid.approval_response") {
1358
+ const content = evt.content ?? {};
1359
+ if (!content.session_id || !content.approval_id || !content.decision) return;
1360
+ const decision = content.option_id ? { decision: content.decision, optionId: content.option_id } : { decision: content.decision };
1361
+ const ok = approvals.resolve(
1362
+ content.session_id,
1363
+ content.approval_id,
1364
+ decision
1365
+ );
1366
+ if (!ok) console.warn(`[matrix] unknown approval ${content.approval_id}`);
1367
+ return;
1368
+ }
1369
+ logInbound(evt);
1370
+ if (evt.type === "m.room.message" && isMediaMsgtype(evt.content?.msgtype) && evt.room_id && evt.event_id && evt.sender && evt.content?.url && !bindings.some((b) => b.userId === evt.sender)) {
1371
+ pendingMedia.add(evt.room_id, inboundThreadRoot2(evt), {
1372
+ eventId: evt.event_id,
1373
+ sender: evt.sender,
1374
+ msgtype: evt.content.msgtype,
1375
+ body: evt.content.body ?? "",
1376
+ filename: evt.content.filename,
1377
+ url: evt.content.url,
1378
+ info: evt.content.info
1379
+ });
1380
+ return;
1381
+ }
1382
+ const promotedRoot = inboundThreadRoot2(evt) ?? evt.event_id;
1383
+ const inboundRel = inboundThreadRoot2(evt);
1384
+ if (evt.type === "m.room.message" && inboundRel && !threadStates.has(inboundRel) && evt.room_id) {
1385
+ try {
1386
+ const rebuilt = await rebuildThreadState(client, evt.room_id, inboundRel, bindings);
1387
+ threadStates.set(inboundRel, rebuilt);
1388
+ console.log(
1389
+ `[matrix] rebuilt threadState for ${inboundRel}: participants=${rebuilt.participants.join(",")} rootMentions=${rebuilt.rootMentions.join(",")}`
1323
1390
  );
1391
+ } catch (err) {
1392
+ console.warn(`[matrix] failed to rebuild threadState for ${inboundRel}:`, err);
1324
1393
  }
1325
- if (matches.length > 0 && promotedRoot) {
1394
+ }
1395
+ const matches = route(evt, bindings, threadStates);
1396
+ const senderIsBot = bindings.some((b) => b.userId === evt.sender);
1397
+ if (evt.type === "m.room.message" && matches.length === 0 && !senderIsBot) {
1398
+ console.warn(
1399
+ `[matrix] no agent matched message in ${evt.room_id} from ${evt.sender} (bindings: ${bindings.map((b) => `${b.name}@${b.userId}[${b.trigger}]`).join(", ")})`
1400
+ );
1401
+ }
1402
+ if (matches.length > 0 && promotedRoot) {
1403
+ let st = threadStates.get(promotedRoot);
1404
+ if (!st) {
1405
+ st = { participants: [], rootMentions: [], callers: {} };
1406
+ threadStates.set(promotedRoot, st);
1407
+ }
1408
+ const msgMentions = new Set(extractMentions(evt));
1409
+ const senderAgent = bindings.find((b) => b.userId === evt.sender);
1410
+ for (const a of bindings) {
1411
+ if (!msgMentions.has(a.userId)) continue;
1412
+ if (!st.rootMentions.includes(a.name)) st.rootMentions.push(a.name);
1413
+ if (senderAgent && a.name !== senderAgent.name) st.callers[a.name] = senderAgent.name;
1414
+ }
1415
+ }
1416
+ for (const a of matches) {
1417
+ console.log(`[matrix] \u2192 ${a.name} (${a.userId})`);
1418
+ void runTurn(a, evt).then(() => {
1419
+ if (!promotedRoot) return;
1326
1420
  let st = threadStates.get(promotedRoot);
1327
1421
  if (!st) {
1328
- st = { participants: [], rootMentions: [] };
1422
+ st = { participants: [], rootMentions: [], callers: {} };
1329
1423
  threadStates.set(promotedRoot, st);
1330
1424
  }
1331
- const msgMentions = new Set(extractMentions(evt));
1332
- for (const a of bindings) {
1333
- if (msgMentions.has(a.userId) && !st.rootMentions.includes(a.name)) {
1334
- st.rootMentions.push(a.name);
1335
- }
1336
- }
1337
- }
1338
- for (const a of matches) {
1339
- console.log(`[matrix] \u2192 ${a.name} (${a.userId})`);
1340
- void runTurn(a, evt).then(() => {
1341
- if (!promotedRoot) return;
1342
- let st = threadStates.get(promotedRoot);
1343
- if (!st) {
1344
- st = { participants: [], rootMentions: [] };
1345
- threadStates.set(promotedRoot, st);
1346
- }
1347
- if (st.participants.at(-1) !== a.name) st.participants.push(a.name);
1348
- }).catch((err) => {
1349
- console.error(`[matrix] runTurn failed for ${a.name}:`, err);
1350
- const c2 = classify(err);
1351
- const threadRoot = inboundThreadRoot2(evt) ?? evt.event_id;
1352
- if (!threadRoot || !evt.room_id) return;
1353
- const body2 = toErrorBody(
1354
- {
1355
- kind: "error",
1356
- agentId: a.name,
1357
- sessionId: null,
1358
- turnId: null,
1359
- code: c2.code,
1360
- message: err instanceof Error ? err.message : String(err),
1361
- detail: err instanceof Error && err.stack ? err.stack.slice(0, 2e3) : void 0,
1362
- transient: c2.transient,
1363
- acp_error: c2.acp_error
1364
- },
1365
- threadRoot
1366
- );
1367
- void client.sendCustomEvent({
1368
- roomId: evt.room_id,
1369
- asUserId: a.userId,
1370
- eventType: "dev.zooid.error",
1371
- content: body2
1372
- }).catch((e) => console.warn(`[matrix:${a.name}] dev.zooid.error send failed:`, e));
1373
- });
1374
- }
1425
+ if (st.participants.at(-1) !== a.name) st.participants.push(a.name);
1426
+ }).catch((err) => {
1427
+ console.error(`[matrix] runTurn failed for ${a.name}:`, err);
1428
+ const c = classify(err);
1429
+ const threadRoot = inboundThreadRoot2(evt) ?? evt.event_id;
1430
+ if (!threadRoot || !evt.room_id) return;
1431
+ const body = toErrorBody(
1432
+ {
1433
+ kind: "error",
1434
+ agentId: a.name,
1435
+ sessionId: null,
1436
+ turnId: null,
1437
+ code: c.code,
1438
+ message: err instanceof Error ? err.message : String(err),
1439
+ detail: err instanceof Error && err.stack ? err.stack.slice(0, 2e3) : void 0,
1440
+ transient: c.transient,
1441
+ acp_error: c.acp_error
1442
+ },
1443
+ threadRoot
1444
+ );
1445
+ void client.sendCustomEvent({
1446
+ roomId: evt.room_id,
1447
+ asUserId: a.userId,
1448
+ eventType: "dev.zooid.error",
1449
+ content: body
1450
+ }).catch((e) => console.warn(`[matrix:${a.name}] dev.zooid.error send failed:`, e));
1451
+ });
1452
+ }
1453
+ }
1454
+ const app = new Hono();
1455
+ function authOk(authHeader) {
1456
+ const h = authHeader ?? "";
1457
+ if (!h.startsWith("Bearer ")) return false;
1458
+ const got = h.slice(7);
1459
+ if (got.length !== hsToken.length) return false;
1460
+ return timingSafeEqual(Buffer.from(got), Buffer.from(hsToken));
1461
+ }
1462
+ app.put("/_matrix/app/v1/transactions/:txnId", async (c) => {
1463
+ if (!authOk(c.req.header("authorization"))) {
1464
+ return c.json({ errcode: "M_FORBIDDEN" }, 403);
1465
+ }
1466
+ const body = await c.req.json().catch(() => ({}));
1467
+ for (const evt of body.events ?? []) {
1468
+ await handleInboundEvent(evt);
1375
1469
  }
1376
1470
  return c.json({});
1377
1471
  });
@@ -1475,8 +1569,18 @@ function createMatrixTransport(opts) {
1475
1569
  sendQueue.delete(sessionId);
1476
1570
  }
1477
1571
  }
1572
+ const syncLoops = mode === "client" ? bindings.map(
1573
+ (b) => new SyncLoop({
1574
+ client,
1575
+ asUserId: b.userId,
1576
+ loadSince: () => opts.loadSince?.(b.userId) ?? null,
1577
+ saveSince: (since) => opts.saveSince?.(b.userId, since),
1578
+ onEvent: (evt) => handleInboundEvent(evt)
1579
+ })
1580
+ ) : void 0;
1478
1581
  return {
1479
1582
  app,
1583
+ syncLoops,
1480
1584
  bootstrap: async (bootstrapOpts = {}) => {
1481
1585
  await pool.bootstrap({ adminUserId, ...bootstrapOpts });
1482
1586
  await Promise.allSettled(
@@ -1491,16 +1595,18 @@ function createMatrixTransport(opts) {
1491
1595
  };
1492
1596
  }
1493
1597
  async function rebuildThreadState(client, roomId, rootEventId, bindings) {
1494
- const state = { participants: [], rootMentions: [] };
1598
+ const state = { participants: [], rootMentions: [], callers: {} };
1495
1599
  const asUser = (bindings.find((b) => b.rooms.some((r) => r.alias === roomId)) ?? bindings[0])?.userId;
1496
1600
  if (!asUser) return state;
1497
1601
  const root = await client.fetchEvent(roomId, rootEventId, asUser);
1498
1602
  if (root) {
1499
1603
  const rootMentions = new Set(extractMentions(root));
1604
+ const rootSender = root.sender;
1605
+ const rootSenderAgent = rootSender ? bindings.find((b) => b.userId === rootSender) : void 0;
1500
1606
  for (const a of bindings) {
1501
- if (rootMentions.has(a.userId) && !state.rootMentions.includes(a.name)) {
1502
- state.rootMentions.push(a.name);
1503
- }
1607
+ if (!rootMentions.has(a.userId)) continue;
1608
+ if (!state.rootMentions.includes(a.name)) state.rootMentions.push(a.name);
1609
+ if (rootSenderAgent && a.name !== rootSenderAgent.name) state.callers[a.name] = rootSenderAgent.name;
1504
1610
  }
1505
1611
  }
1506
1612
  const { chunk: thread } = await client.fetchThreadRelations({
@@ -1510,15 +1616,16 @@ async function rebuildThreadState(client, roomId, rootEventId, bindings) {
1510
1616
  });
1511
1617
  for (const ev of thread) {
1512
1618
  const mentions = new Set(extractMentions(ev));
1619
+ const evSender = ev.sender;
1620
+ const evSenderAgent = evSender ? bindings.find((b) => b.userId === evSender) : void 0;
1513
1621
  for (const a of bindings) {
1514
- if (mentions.has(a.userId) && !state.rootMentions.includes(a.name)) {
1515
- state.rootMentions.push(a.name);
1516
- }
1622
+ if (!mentions.has(a.userId)) continue;
1623
+ if (!state.rootMentions.includes(a.name)) state.rootMentions.push(a.name);
1624
+ if (evSenderAgent && a.name !== evSenderAgent.name) state.callers[a.name] = evSenderAgent.name;
1517
1625
  }
1518
- const sender = ev.sender;
1519
1626
  const type = ev.type;
1520
- if (type === "m.room.message" && sender) {
1521
- const a = bindings.find((b) => b.userId === sender);
1627
+ if (type === "m.room.message" && evSender) {
1628
+ const a = bindings.find((b) => b.userId === evSender);
1522
1629
  if (a && state.participants.at(-1) !== a.name) state.participants.push(a.name);
1523
1630
  }
1524
1631
  }
@@ -1574,6 +1681,7 @@ async function startWorkforcePublisher(opts) {
1574
1681
  };
1575
1682
  }
1576
1683
  export {
1684
+ AGENT_KEY_RE,
1577
1685
  BotPool,
1578
1686
  INLINE_IMAGE_MIMES,
1579
1687
  MAX_DOWNLOAD_BYTES,
@@ -1584,18 +1692,25 @@ export {
1584
1692
  MatrixContextProvider,
1585
1693
  MediaClient,
1586
1694
  PendingMediaStore,
1695
+ SLUG_RE,
1696
+ SyncLoop,
1697
+ agentMxid,
1587
1698
  buildWorkforceRoster,
1588
1699
  createMatrixTransport,
1589
1700
  ensureDefaultChannel,
1590
1701
  ensureWorkforceSpace,
1591
1702
  extractMentions,
1592
1703
  isMediaMsgtype,
1704
+ isValidAgentKey,
1705
+ isValidWorkstation,
1593
1706
  parseMxcUri,
1594
1707
  publishWorkforce,
1595
1708
  renderRegistration,
1596
1709
  route,
1597
1710
  serverNameFromMxid,
1711
+ splitAgentLocalpart,
1598
1712
  startWorkforcePublisher,
1713
+ workstationUserNamespace,
1599
1714
  writeAttachment
1600
1715
  };
1601
1716
  //# sourceMappingURL=index.js.map