@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.d.ts +85 -1
- package/dist/index.js +312 -197
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/bot-pool.test.ts +130 -1
- package/src/bot-pool.ts +27 -9
- package/src/identity.test.ts +52 -0
- package/src/identity.ts +27 -0
- package/src/index.ts +11 -0
- package/src/matrix-client.test.ts +66 -0
- package/src/matrix-client.ts +40 -1
- package/src/registration.test.ts +20 -0
- package/src/router.test.ts +117 -1
- package/src/router.ts +25 -7
- package/src/sync-loop.test.ts +113 -0
- package/src/sync-loop.ts +74 -0
- package/src/transport.test.ts +107 -1
- package/src/transport.ts +290 -251
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
|
-
|
|
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
|
|
499
|
-
if (
|
|
500
|
-
if (
|
|
501
|
-
} else
|
|
502
|
-
|
|
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
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
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
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
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
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
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
|
-
|
|
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]
|
|
1333
|
+
`[matrix] interrupt session=${t.sessionId} agent=${t.agent} thread=${threadRoot}` + (content.reason ? ` reason=${content.reason}` : "")
|
|
1313
1334
|
);
|
|
1314
|
-
|
|
1315
|
-
|
|
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
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
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)
|
|
1502
|
-
|
|
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)
|
|
1515
|
-
|
|
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" &&
|
|
1521
|
-
const a = bindings.find((b) => b.userId ===
|
|
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
|