@zooid/transport-matrix 0.9.0 → 0.9.1

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
@@ -255,6 +255,19 @@ var MatrixClient = class {
255
255
  if (!r.ok) throw new Error(`getJoinedMembers(${roomId}) failed: ${r.status}`);
256
256
  return await r.json();
257
257
  }
258
+ async sync(opts) {
259
+ const params = new URLSearchParams({
260
+ user_id: opts.asUserId,
261
+ timeout: String(opts.timeoutMs ?? 3e4)
262
+ });
263
+ if (opts.since) params.set("since", opts.since);
264
+ const url = `${this.homeserver}/_matrix/client/v3/sync?${params.toString()}`;
265
+ const r = await this.fetch(url, {
266
+ headers: { Authorization: `Bearer ${this.asToken}` }
267
+ });
268
+ if (!r.ok) throw new Error(`sync(${opts.asUserId}) failed: ${r.status}`);
269
+ return r.json();
270
+ }
258
271
  async fetchRoomName(roomId, asUserId) {
259
272
  const url = `${this.homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/state/m.room.name/?user_id=${encodeURIComponent(asUserId)}`;
260
273
  const r = await this.fetch(url, {
@@ -284,6 +297,7 @@ var MatrixContextProvider = class {
284
297
  constructor(opts) {
285
298
  this.opts = opts;
286
299
  }
300
+ opts;
287
301
  async getRoomHistory(channelId, hopts) {
288
302
  const { chunk, end } = await this.opts.client.fetchRoomMessages({
289
303
  roomId: channelId,
@@ -443,6 +457,29 @@ function renderRegistration(c) {
443
457
  );
444
458
  }
445
459
 
460
+ // src/identity.ts
461
+ var SLUG_RE = /^[a-z0-9-]+$/;
462
+ var AGENT_KEY_RE = /^[a-z0-9-]+$/;
463
+ var isValidWorkstation = (s) => SLUG_RE.test(s);
464
+ var isValidAgentKey = (s) => AGENT_KEY_RE.test(s);
465
+ function agentMxid(workstation, agent, serverName) {
466
+ if (!isValidWorkstation(workstation))
467
+ throw new Error(`invalid workstation: ${JSON.stringify(workstation)}`);
468
+ if (!isValidAgentKey(agent)) throw new Error(`invalid agent key: ${JSON.stringify(agent)}`);
469
+ return `@${workstation}.${agent}:${serverName}`;
470
+ }
471
+ function splitAgentLocalpart(localpart2) {
472
+ const i = localpart2.indexOf(".");
473
+ if (i <= 0 || i === localpart2.length - 1)
474
+ throw new Error(`not a workstation.agent localpart: ${localpart2}`);
475
+ return { workstation: localpart2.slice(0, i), agent: localpart2.slice(i + 1) };
476
+ }
477
+ function workstationUserNamespace(workstation, serverName) {
478
+ if (!isValidWorkstation(workstation))
479
+ throw new Error(`invalid workstation: ${JSON.stringify(workstation)}`);
480
+ return `@${workstation}\\..*:${serverName}`;
481
+ }
482
+
446
483
  // src/mentions.ts
447
484
  var MATRIX_TO_RE = /https:\/\/matrix\.to\/#\/(@[^"<>\s]+)/g;
448
485
  var RAW_USER_RE = /(@[A-Za-z0-9._\-=/+]+:[A-Za-z0-9.\-]+)/g;
@@ -573,6 +610,8 @@ var BotPool = class {
573
610
  this.client = client;
574
611
  this.agents = agents;
575
612
  }
613
+ client;
614
+ agents;
576
615
  async bootstrap(opts = {}) {
577
616
  const aliasToId = /* @__PURE__ */ new Map();
578
617
  const attachedToSpace = /* @__PURE__ */ new Set();
@@ -618,7 +657,7 @@ var BotPool = class {
618
657
  } else {
619
658
  const colon = room.indexOf(":");
620
659
  const aliasLocalpart = colon > 1 ? room.slice(1, colon) : room.slice(1);
621
- const sender = opts.adminUserId ?? a.userId;
660
+ const sender = opts.asUserId ?? opts.adminUserId ?? a.userId;
622
661
  const userPowerLevels = buildUserPowerLevels(
623
662
  opts.asUserId,
624
663
  opts.adminUserIds,
@@ -937,6 +976,44 @@ function writeAttachment(input) {
937
976
  return { hostPath, agentPath };
938
977
  }
939
978
 
979
+ // src/sync-loop.ts
980
+ var SyncLoop = class {
981
+ opts;
982
+ running = false;
983
+ constructor(opts) {
984
+ this.opts = opts;
985
+ }
986
+ async tick() {
987
+ const since = this.opts.loadSince();
988
+ const res = await this.opts.client.sync({
989
+ asUserId: this.opts.asUserId,
990
+ since,
991
+ timeoutMs: this.opts.timeoutMs ?? 3e4
992
+ });
993
+ for (const [roomId, roomState] of Object.entries(res.rooms?.join ?? {})) {
994
+ for (const baseEvt of roomState.timeline?.events ?? []) {
995
+ await this.opts.onEvent({ ...baseEvt, room_id: roomId });
996
+ }
997
+ }
998
+ this.opts.saveSince(res.next_batch);
999
+ }
1000
+ async run() {
1001
+ this.running = true;
1002
+ while (this.running) {
1003
+ try {
1004
+ await this.tick();
1005
+ } catch (err) {
1006
+ if (!this.running) break;
1007
+ console.warn(`[sync-loop] ${this.opts.asUserId} tick failed, retrying:`, err);
1008
+ await new Promise((r) => setTimeout(r, this.opts.retryDelayMs ?? 5e3));
1009
+ }
1010
+ }
1011
+ }
1012
+ stop() {
1013
+ this.running = false;
1014
+ }
1015
+ };
1016
+
940
1017
  // src/transport.ts
941
1018
  var STARTUP_GRACE_MS = 5e3;
942
1019
  async function buildMediaBlocks(items, opts) {
@@ -1030,7 +1107,7 @@ function inboundThreadRoot2(evt) {
1030
1107
  return r?.rel_type === "m.thread" && r.event_id ? r.event_id : void 0;
1031
1108
  }
1032
1109
  function createMatrixTransport(opts) {
1033
- const { agents, approvals, client, bindings, hsToken, adminUserId, botUserId } = opts;
1110
+ const { agents, approvals, client, bindings, hsToken, adminUserId, botUserId, mode = "appservice" } = opts;
1034
1111
  const drainQuietMs = opts.drainQuietMs ?? DRAIN_QUIET_MS;
1035
1112
  const drainMaxMs = opts.drainMaxMs ?? DRAIN_MAX_MS;
1036
1113
  const mediaClient = opts.media;
@@ -1047,7 +1124,7 @@ function createMatrixTransport(opts) {
1047
1124
  const bufferMessageIds = /* @__PURE__ */ new Map();
1048
1125
  const sendQueue = /* @__PURE__ */ new Map();
1049
1126
  const threadStates = /* @__PURE__ */ new Map();
1050
- const cutoffTs = Date.now() - STARTUP_GRACE_MS;
1127
+ const cutoffTs = mode === "client" ? Number.NEGATIVE_INFINITY : Date.now() - STARTUP_GRACE_MS;
1051
1128
  const seenEventIds = /* @__PURE__ */ new Set();
1052
1129
  const flushedCounts = /* @__PURE__ */ new Map();
1053
1130
  const pendingCommands = /* @__PURE__ */ new Map();
@@ -1184,194 +1261,197 @@ function createMatrixTransport(opts) {
1184
1261
  content
1185
1262
  });
1186
1263
  });
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
- }
1264
+ async function handleInboundEvent(evt) {
1265
+ if (evt.event_id) {
1266
+ if (seenEventIds.has(evt.event_id)) {
1267
+ return;
1268
+ }
1269
+ seenEventIds.add(evt.event_id);
1270
+ if (seenEventIds.size > SEEN_EVENT_CAP) {
1271
+ const first = seenEventIds.values().next().value;
1272
+ if (first !== void 0) seenEventIds.delete(first);
1210
1273
  }
1211
- if (evt.origin_server_ts !== void 0 && evt.origin_server_ts < cutoffTs && evt.type === "m.room.message") {
1274
+ }
1275
+ if (evt.origin_server_ts !== void 0 && evt.origin_server_ts < cutoffTs && evt.type === "m.room.message") {
1276
+ console.log(
1277
+ `[matrix] dropping stale message event ${evt.event_id} (ts=${evt.origin_server_ts}, daemon started at ${cutoffTs + STARTUP_GRACE_MS})`
1278
+ );
1279
+ return;
1280
+ }
1281
+ if (evt.type === "m.room.member" && evt.content?.membership === "invite") {
1282
+ const target = evt.state_key;
1283
+ const inviter = evt.sender;
1284
+ if (target && evt.room_id && ourBotUserIds.has(target) && (!inviter || !ourBotUserIds.has(inviter))) {
1212
1285
  console.log(
1213
- `[matrix] dropping stale message event ${evt.event_id} (ts=${evt.origin_server_ts}, daemon started at ${cutoffTs + STARTUP_GRACE_MS})`
1286
+ `[matrix] declining ad-hoc invite for ${target} in ${evt.room_id} from ${inviter ?? "unknown"}`
1287
+ );
1288
+ await client.leaveRoom(evt.room_id, target, { reason: DECLINE_REASON }).catch(
1289
+ (err) => console.warn(`[matrix] leaveRoom(${evt.room_id}, ${target}) failed:`, err)
1214
1290
  );
1215
- continue;
1216
1291
  }
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;
1292
+ return;
1293
+ }
1294
+ if (evt.type === "dev.zooid.session_reset") {
1295
+ const relates = evt.content?.["m.relates_to"];
1296
+ const threadRoot = relates?.rel_type === "m.thread" && relates.event_id ? relates.event_id : void 0;
1297
+ if (!threadRoot) {
1298
+ console.log("[matrix] dropping dev.zooid.session_reset without thread relation");
1299
+ return;
1229
1300
  }
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;
1301
+ console.log(`[matrix] inbound dev.zooid.session_reset in ${evt.room_id} thread=${threadRoot}`);
1302
+ for (const a of bindings) {
1303
+ agents.endSession(a.name, threadRoot);
1242
1304
  }
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
- });
1305
+ return;
1306
+ }
1307
+ if (evt.type === "dev.zooid.interrupt") {
1308
+ const content = evt.content ?? {};
1309
+ const relates = evt.content?.["m.relates_to"];
1310
+ const threadRoot = relates?.rel_type === "m.thread" && relates.event_id ? relates.event_id : void 0;
1311
+ if (threadRoot) {
1312
+ const targets = [];
1313
+ for (const [sessionId, ctx2] of sessions) {
1314
+ if (ctx2.threadRoot === threadRoot) {
1315
+ targets.push({ sessionId, agent: ctx2.agent.name });
1261
1316
  }
1262
- continue;
1263
1317
  }
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;
1318
+ for (const t of targets) {
1319
+ console.log(
1320
+ `[matrix] interrupt session=${t.sessionId} agent=${t.agent} thread=${threadRoot}` + (content.reason ? ` reason=${content.reason}` : "")
1321
+ );
1322
+ await agents.cancelSession(t.agent, t.sessionId).catch((err) => {
1323
+ console.error(`[matrix] cancelSession(${t.agent}, ${t.sessionId}) failed:`, err);
1324
+ });
1271
1325
  }
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;
1326
+ return;
1327
+ }
1328
+ if (!content.session_id) {
1329
+ console.warn(`[matrix] dev.zooid.interrupt missing session_id (event_id=${evt.event_id})`);
1330
+ return;
1279
1331
  }
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
1332
+ const ctx = sessions.get(content.session_id);
1333
+ if (!ctx) {
1334
+ return;
1335
+ }
1336
+ console.log(
1337
+ `[matrix] interrupt session=${content.session_id} agent=${ctx.agent.name}` + (content.reason ? ` reason=${content.reason}` : "")
1338
+ );
1339
+ await agents.cancelSession(ctx.agent.name, content.session_id).catch((err) => {
1340
+ console.error(`[matrix] cancelSession(${ctx.agent.name}, ${content.session_id}) failed:`, err);
1341
+ });
1342
+ return;
1343
+ }
1344
+ if (evt.type === "dev.zooid.approval_response") {
1345
+ const content = evt.content ?? {};
1346
+ if (!content.session_id || !content.approval_id || !content.decision) return;
1347
+ const decision = content.option_id ? { decision: content.decision, optionId: content.option_id } : { decision: content.decision };
1348
+ const ok = approvals.resolve(
1349
+ content.session_id,
1350
+ content.approval_id,
1351
+ decision
1352
+ );
1353
+ if (!ok) console.warn(`[matrix] unknown approval ${content.approval_id}`);
1354
+ return;
1355
+ }
1356
+ logInbound(evt);
1357
+ 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)) {
1358
+ pendingMedia.add(evt.room_id, inboundThreadRoot2(evt), {
1359
+ eventId: evt.event_id,
1360
+ sender: evt.sender,
1361
+ msgtype: evt.content.msgtype,
1362
+ body: evt.content.body ?? "",
1363
+ filename: evt.content.filename,
1364
+ url: evt.content.url,
1365
+ info: evt.content.info
1366
+ });
1367
+ return;
1368
+ }
1369
+ const promotedRoot = inboundThreadRoot2(evt) ?? evt.event_id;
1370
+ const inboundRel = inboundThreadRoot2(evt);
1371
+ if (evt.type === "m.room.message" && inboundRel && !threadStates.has(inboundRel) && evt.room_id) {
1372
+ try {
1373
+ const rebuilt = await rebuildThreadState(client, evt.room_id, inboundRel, bindings);
1374
+ threadStates.set(inboundRel, rebuilt);
1375
+ console.log(
1376
+ `[matrix] rebuilt threadState for ${inboundRel}: participants=${rebuilt.participants.join(",")} rootMentions=${rebuilt.rootMentions.join(",")}`
1288
1377
  );
1289
- if (!ok) console.warn(`[matrix] unknown approval ${content.approval_id}`);
1290
- continue;
1378
+ } catch (err) {
1379
+ console.warn(`[matrix] failed to rebuild threadState for ${inboundRel}:`, err);
1291
1380
  }
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;
1381
+ }
1382
+ const matches = route(evt, bindings, threadStates);
1383
+ const senderIsBot = bindings.some((b) => b.userId === evt.sender);
1384
+ if (evt.type === "m.room.message" && matches.length === 0 && !senderIsBot) {
1385
+ console.warn(
1386
+ `[matrix] no agent matched message in ${evt.room_id} from ${evt.sender} (bindings: ${bindings.map((b) => `${b.name}@${b.userId}[${b.trigger}]`).join(", ")})`
1387
+ );
1388
+ }
1389
+ if (matches.length > 0 && promotedRoot) {
1390
+ let st = threadStates.get(promotedRoot);
1391
+ if (!st) {
1392
+ st = { participants: [], rootMentions: [] };
1393
+ threadStates.set(promotedRoot, st);
1304
1394
  }
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);
1311
- console.log(
1312
- `[matrix] rebuilt threadState for ${inboundRel}: participants=${rebuilt.participants.join(",")} rootMentions=${rebuilt.rootMentions.join(",")}`
1313
- );
1314
- } catch (err) {
1315
- console.warn(`[matrix] failed to rebuild threadState for ${inboundRel}:`, err);
1395
+ const msgMentions = new Set(extractMentions(evt));
1396
+ for (const a of bindings) {
1397
+ if (msgMentions.has(a.userId) && !st.rootMentions.includes(a.name)) {
1398
+ st.rootMentions.push(a.name);
1316
1399
  }
1317
1400
  }
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(", ")})`
1323
- );
1324
- }
1325
- if (matches.length > 0 && promotedRoot) {
1401
+ }
1402
+ for (const a of matches) {
1403
+ console.log(`[matrix] \u2192 ${a.name} (${a.userId})`);
1404
+ void runTurn(a, evt).then(() => {
1405
+ if (!promotedRoot) return;
1326
1406
  let st = threadStates.get(promotedRoot);
1327
1407
  if (!st) {
1328
1408
  st = { participants: [], rootMentions: [] };
1329
1409
  threadStates.set(promotedRoot, st);
1330
1410
  }
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
- }
1411
+ if (st.participants.at(-1) !== a.name) st.participants.push(a.name);
1412
+ }).catch((err) => {
1413
+ console.error(`[matrix] runTurn failed for ${a.name}:`, err);
1414
+ const c = classify(err);
1415
+ const threadRoot = inboundThreadRoot2(evt) ?? evt.event_id;
1416
+ if (!threadRoot || !evt.room_id) return;
1417
+ const body = toErrorBody(
1418
+ {
1419
+ kind: "error",
1420
+ agentId: a.name,
1421
+ sessionId: null,
1422
+ turnId: null,
1423
+ code: c.code,
1424
+ message: err instanceof Error ? err.message : String(err),
1425
+ detail: err instanceof Error && err.stack ? err.stack.slice(0, 2e3) : void 0,
1426
+ transient: c.transient,
1427
+ acp_error: c.acp_error
1428
+ },
1429
+ threadRoot
1430
+ );
1431
+ void client.sendCustomEvent({
1432
+ roomId: evt.room_id,
1433
+ asUserId: a.userId,
1434
+ eventType: "dev.zooid.error",
1435
+ content: body
1436
+ }).catch((e) => console.warn(`[matrix:${a.name}] dev.zooid.error send failed:`, e));
1437
+ });
1438
+ }
1439
+ }
1440
+ const app = new Hono();
1441
+ function authOk(authHeader) {
1442
+ const h = authHeader ?? "";
1443
+ if (!h.startsWith("Bearer ")) return false;
1444
+ const got = h.slice(7);
1445
+ if (got.length !== hsToken.length) return false;
1446
+ return timingSafeEqual(Buffer.from(got), Buffer.from(hsToken));
1447
+ }
1448
+ app.put("/_matrix/app/v1/transactions/:txnId", async (c) => {
1449
+ if (!authOk(c.req.header("authorization"))) {
1450
+ return c.json({ errcode: "M_FORBIDDEN" }, 403);
1451
+ }
1452
+ const body = await c.req.json().catch(() => ({}));
1453
+ for (const evt of body.events ?? []) {
1454
+ await handleInboundEvent(evt);
1375
1455
  }
1376
1456
  return c.json({});
1377
1457
  });
@@ -1475,8 +1555,18 @@ function createMatrixTransport(opts) {
1475
1555
  sendQueue.delete(sessionId);
1476
1556
  }
1477
1557
  }
1558
+ const syncLoops = mode === "client" ? bindings.map(
1559
+ (b) => new SyncLoop({
1560
+ client,
1561
+ asUserId: b.userId,
1562
+ loadSince: () => opts.loadSince?.(b.userId) ?? null,
1563
+ saveSince: (since) => opts.saveSince?.(b.userId, since),
1564
+ onEvent: (evt) => handleInboundEvent(evt)
1565
+ })
1566
+ ) : void 0;
1478
1567
  return {
1479
1568
  app,
1569
+ syncLoops,
1480
1570
  bootstrap: async (bootstrapOpts = {}) => {
1481
1571
  await pool.bootstrap({ adminUserId, ...bootstrapOpts });
1482
1572
  await Promise.allSettled(
@@ -1574,6 +1664,7 @@ async function startWorkforcePublisher(opts) {
1574
1664
  };
1575
1665
  }
1576
1666
  export {
1667
+ AGENT_KEY_RE,
1577
1668
  BotPool,
1578
1669
  INLINE_IMAGE_MIMES,
1579
1670
  MAX_DOWNLOAD_BYTES,
@@ -1584,18 +1675,25 @@ export {
1584
1675
  MatrixContextProvider,
1585
1676
  MediaClient,
1586
1677
  PendingMediaStore,
1678
+ SLUG_RE,
1679
+ SyncLoop,
1680
+ agentMxid,
1587
1681
  buildWorkforceRoster,
1588
1682
  createMatrixTransport,
1589
1683
  ensureDefaultChannel,
1590
1684
  ensureWorkforceSpace,
1591
1685
  extractMentions,
1592
1686
  isMediaMsgtype,
1687
+ isValidAgentKey,
1688
+ isValidWorkstation,
1593
1689
  parseMxcUri,
1594
1690
  publishWorkforce,
1595
1691
  renderRegistration,
1596
1692
  route,
1597
1693
  serverNameFromMxid,
1694
+ splitAgentLocalpart,
1598
1695
  startWorkforcePublisher,
1696
+ workstationUserNamespace,
1599
1697
  writeAttachment
1600
1698
  };
1601
1699
  //# sourceMappingURL=index.js.map