@zooid/transport-matrix 0.8.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.d.ts +84 -1
- package/dist/index.js +366 -194
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/bot-pool.test.ts +7 -1
- package/src/bot-pool.ts +8 -2
- package/src/context-provider.ts +1 -1
- package/src/event-encoders.test.ts +22 -0
- package/src/event-encoders.ts +13 -0
- 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 +58 -2
- package/src/matrix-client.ts +51 -0
- package/src/registration.test.ts +20 -0
- package/src/sync-loop.test.ts +113 -0
- package/src/sync-loop.ts +74 -0
- package/src/transport.test.ts +161 -32
- package/src/transport.ts +423 -280
- package/src/workforce-publisher.test.ts +2 -2
- package/src/workforce-publisher.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -125,6 +125,18 @@ var MatrixClient = class {
|
|
|
125
125
|
}
|
|
126
126
|
throw new Error(`invite(${opts.targetUserId}) failed: ${r.status}`);
|
|
127
127
|
}
|
|
128
|
+
async leaveRoom(roomId, asUserId, opts) {
|
|
129
|
+
const url = `${this.homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/leave?user_id=${encodeURIComponent(asUserId)}`;
|
|
130
|
+
const r = await this.fetch(url, {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: {
|
|
133
|
+
Authorization: `Bearer ${this.asToken}`,
|
|
134
|
+
"content-type": "application/json"
|
|
135
|
+
},
|
|
136
|
+
body: JSON.stringify(opts?.reason ? { reason: opts.reason } : {})
|
|
137
|
+
});
|
|
138
|
+
if (!r.ok) throw new Error(`leaveRoom(${roomId}, ${asUserId}) failed: ${r.status}`);
|
|
139
|
+
}
|
|
128
140
|
async joinRoom(roomIdOrAlias, asUserId) {
|
|
129
141
|
const url = `${this.homeserver}/_matrix/client/v3/join/${encodeURIComponent(roomIdOrAlias)}?user_id=${encodeURIComponent(asUserId)}`;
|
|
130
142
|
const r = await this.fetch(url, {
|
|
@@ -243,6 +255,19 @@ var MatrixClient = class {
|
|
|
243
255
|
if (!r.ok) throw new Error(`getJoinedMembers(${roomId}) failed: ${r.status}`);
|
|
244
256
|
return await r.json();
|
|
245
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
|
+
}
|
|
246
271
|
async fetchRoomName(roomId, asUserId) {
|
|
247
272
|
const url = `${this.homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/state/m.room.name/?user_id=${encodeURIComponent(asUserId)}`;
|
|
248
273
|
const r = await this.fetch(url, {
|
|
@@ -272,6 +297,7 @@ var MatrixContextProvider = class {
|
|
|
272
297
|
constructor(opts) {
|
|
273
298
|
this.opts = opts;
|
|
274
299
|
}
|
|
300
|
+
opts;
|
|
275
301
|
async getRoomHistory(channelId, hopts) {
|
|
276
302
|
const { chunk, end } = await this.opts.client.fetchRoomMessages({
|
|
277
303
|
roomId: channelId,
|
|
@@ -431,6 +457,29 @@ function renderRegistration(c) {
|
|
|
431
457
|
);
|
|
432
458
|
}
|
|
433
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
|
+
|
|
434
483
|
// src/mentions.ts
|
|
435
484
|
var MATRIX_TO_RE = /https:\/\/matrix\.to\/#\/(@[^"<>\s]+)/g;
|
|
436
485
|
var RAW_USER_RE = /(@[A-Za-z0-9._\-=/+]+:[A-Za-z0-9.\-]+)/g;
|
|
@@ -561,6 +610,8 @@ var BotPool = class {
|
|
|
561
610
|
this.client = client;
|
|
562
611
|
this.agents = agents;
|
|
563
612
|
}
|
|
613
|
+
client;
|
|
614
|
+
agents;
|
|
564
615
|
async bootstrap(opts = {}) {
|
|
565
616
|
const aliasToId = /* @__PURE__ */ new Map();
|
|
566
617
|
const attachedToSpace = /* @__PURE__ */ new Set();
|
|
@@ -606,7 +657,7 @@ var BotPool = class {
|
|
|
606
657
|
} else {
|
|
607
658
|
const colon = room.indexOf(":");
|
|
608
659
|
const aliasLocalpart = colon > 1 ? room.slice(1, colon) : room.slice(1);
|
|
609
|
-
const sender = opts.adminUserId ?? a.userId;
|
|
660
|
+
const sender = opts.asUserId ?? opts.adminUserId ?? a.userId;
|
|
610
661
|
const userPowerLevels = buildUserPowerLevels(
|
|
611
662
|
opts.asUserId,
|
|
612
663
|
opts.adminUserIds,
|
|
@@ -729,6 +780,15 @@ function toPlanBody(evt) {
|
|
|
729
780
|
entries: evt.entries
|
|
730
781
|
};
|
|
731
782
|
}
|
|
783
|
+
function toAvailableCommandsBody(evt) {
|
|
784
|
+
return {
|
|
785
|
+
session_id: evt.sessionId,
|
|
786
|
+
available_commands: evt.commands.map((c) => ({
|
|
787
|
+
name: c.name,
|
|
788
|
+
description: c.description
|
|
789
|
+
}))
|
|
790
|
+
};
|
|
791
|
+
}
|
|
732
792
|
var RECOVERY_URLS = {
|
|
733
793
|
auth_missing: "https://zooid.dev/docs/guides/run-in-container#authentication-that-carries-over",
|
|
734
794
|
auth_invalid: "https://zooid.dev/docs/guides/run-in-container#authentication-that-carries-over",
|
|
@@ -916,6 +976,44 @@ function writeAttachment(input) {
|
|
|
916
976
|
return { hostPath, agentPath };
|
|
917
977
|
}
|
|
918
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
|
+
|
|
919
1017
|
// src/transport.ts
|
|
920
1018
|
var STARTUP_GRACE_MS = 5e3;
|
|
921
1019
|
async function buildMediaBlocks(items, opts) {
|
|
@@ -985,7 +1083,7 @@ async function sendMediaError(ctx, _err, message, client) {
|
|
|
985
1083
|
await client.sendCustomEvent({
|
|
986
1084
|
roomId: ctx.roomId,
|
|
987
1085
|
asUserId: ctx.agent.userId,
|
|
988
|
-
eventType: "
|
|
1086
|
+
eventType: "dev.zooid.error",
|
|
989
1087
|
content: toErrorBody(
|
|
990
1088
|
{
|
|
991
1089
|
kind: "error",
|
|
@@ -998,7 +1096,7 @@ async function sendMediaError(ctx, _err, message, client) {
|
|
|
998
1096
|
},
|
|
999
1097
|
ctx.threadRoot
|
|
1000
1098
|
)
|
|
1001
|
-
}).catch((e) => console.warn(`[matrix:${ctx.agent.name}]
|
|
1099
|
+
}).catch((e) => console.warn(`[matrix:${ctx.agent.name}] dev.zooid.error send failed:`, e));
|
|
1002
1100
|
}
|
|
1003
1101
|
var SEEN_EVENT_CAP = 5e3;
|
|
1004
1102
|
var DRAIN_QUIET_MS = 300;
|
|
@@ -1009,37 +1107,87 @@ function inboundThreadRoot2(evt) {
|
|
|
1009
1107
|
return r?.rel_type === "m.thread" && r.event_id ? r.event_id : void 0;
|
|
1010
1108
|
}
|
|
1011
1109
|
function createMatrixTransport(opts) {
|
|
1012
|
-
const { agents, approvals, client, bindings, hsToken, adminUserId } = opts;
|
|
1110
|
+
const { agents, approvals, client, bindings, hsToken, adminUserId, botUserId, mode = "appservice" } = opts;
|
|
1013
1111
|
const drainQuietMs = opts.drainQuietMs ?? DRAIN_QUIET_MS;
|
|
1014
1112
|
const drainMaxMs = opts.drainMaxMs ?? DRAIN_MAX_MS;
|
|
1015
1113
|
const mediaClient = opts.media;
|
|
1016
1114
|
const writeAttachmentFn = opts.writeAttachmentFn ?? writeAttachment;
|
|
1017
1115
|
const pendingMedia = new PendingMediaStore();
|
|
1018
1116
|
const pool = new BotPool(client, bindings);
|
|
1117
|
+
const ourBotUserIds = /* @__PURE__ */ new Set([
|
|
1118
|
+
...botUserId ? [botUserId] : [],
|
|
1119
|
+
...bindings.map((b) => b.userId)
|
|
1120
|
+
]);
|
|
1121
|
+
const DECLINE_REASON = "Bots are placed in rooms only by the zooid daemon (workforce-as-code). Ad-hoc invites are declined \u2014 add the bot to the room in zooid.yaml.";
|
|
1019
1122
|
const sessions = /* @__PURE__ */ new Map();
|
|
1020
1123
|
const buffers = /* @__PURE__ */ new Map();
|
|
1021
1124
|
const bufferMessageIds = /* @__PURE__ */ new Map();
|
|
1022
1125
|
const sendQueue = /* @__PURE__ */ new Map();
|
|
1023
1126
|
const threadStates = /* @__PURE__ */ new Map();
|
|
1024
|
-
const cutoffTs = Date.now() - STARTUP_GRACE_MS;
|
|
1127
|
+
const cutoffTs = mode === "client" ? Number.NEGATIVE_INFINITY : Date.now() - STARTUP_GRACE_MS;
|
|
1025
1128
|
const seenEventIds = /* @__PURE__ */ new Set();
|
|
1129
|
+
const flushedCounts = /* @__PURE__ */ new Map();
|
|
1130
|
+
const pendingCommands = /* @__PURE__ */ new Map();
|
|
1131
|
+
const buildTextContent = (text) => {
|
|
1132
|
+
const content = {
|
|
1133
|
+
msgtype: "m.text",
|
|
1134
|
+
body: text
|
|
1135
|
+
};
|
|
1136
|
+
const html = toMatrixHtml(text);
|
|
1137
|
+
if (html) {
|
|
1138
|
+
const escapedPlain = "<p>" + text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") + "</p>";
|
|
1139
|
+
const norm = (s) => s.replace(/\s+/g, " ").trim();
|
|
1140
|
+
if (norm(html) !== norm(escapedPlain)) {
|
|
1141
|
+
content.format = "org.matrix.custom.html";
|
|
1142
|
+
content.formatted_body = html;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return content;
|
|
1146
|
+
};
|
|
1147
|
+
const flushBuffer = (sessionId) => {
|
|
1148
|
+
const ctx = sessions.get(sessionId);
|
|
1149
|
+
const text = buffers.get(sessionId) ?? "";
|
|
1150
|
+
if (!ctx || text.length === 0) return false;
|
|
1151
|
+
buffers.set(sessionId, "");
|
|
1152
|
+
flushedCounts.set(sessionId, (flushedCounts.get(sessionId) ?? 0) + 1);
|
|
1153
|
+
const content = buildTextContent(text);
|
|
1154
|
+
const tail = (sendQueue.get(sessionId) ?? Promise.resolve()).then(async () => {
|
|
1155
|
+
try {
|
|
1156
|
+
await client.sendMessage({
|
|
1157
|
+
roomId: ctx.roomId,
|
|
1158
|
+
asUserId: ctx.agent.userId,
|
|
1159
|
+
content,
|
|
1160
|
+
threadRoot: ctx.threadRoot
|
|
1161
|
+
});
|
|
1162
|
+
} catch (err) {
|
|
1163
|
+
console.warn(`[matrix:${ctx.agent.name}] sendMessage flush failed:`, err);
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
sendQueue.set(sessionId, tail);
|
|
1167
|
+
return true;
|
|
1168
|
+
};
|
|
1026
1169
|
agents.onEvent = async (name, event) => {
|
|
1027
1170
|
const ctx = sessions.get(event.sessionId);
|
|
1028
1171
|
if (!ctx) {
|
|
1029
|
-
|
|
1172
|
+
if (event.type === "available_commands") {
|
|
1173
|
+
pendingCommands.set(event.sessionId, event);
|
|
1174
|
+
} else {
|
|
1175
|
+
console.warn(`[matrix:${name}] no session ctx for ${event.sessionId}`);
|
|
1176
|
+
}
|
|
1030
1177
|
return;
|
|
1031
1178
|
}
|
|
1032
1179
|
if (event.type === "agent_message_chunk") {
|
|
1033
1180
|
const block = event.content;
|
|
1034
1181
|
if (block.type === "text" && typeof block.text === "string") {
|
|
1035
|
-
const current = buffers.get(event.sessionId) ?? "";
|
|
1036
1182
|
const prevMessageId = bufferMessageIds.get(event.sessionId);
|
|
1037
1183
|
const messageChanged = event.messageId !== void 0 && prevMessageId !== void 0 && event.messageId !== prevMessageId;
|
|
1038
|
-
const needsBreak = current.length > 0 && (block.text === "" || messageChanged);
|
|
1039
|
-
const prefix = needsBreak ? "\n\n" : "";
|
|
1040
|
-
buffers.set(event.sessionId, current + prefix + block.text);
|
|
1041
1184
|
if (event.messageId !== void 0)
|
|
1042
1185
|
bufferMessageIds.set(event.sessionId, event.messageId);
|
|
1186
|
+
if (messageChanged) flushBuffer(event.sessionId);
|
|
1187
|
+
const current = buffers.get(event.sessionId) ?? "";
|
|
1188
|
+
const needsBreak = current.length > 0 && block.text === "";
|
|
1189
|
+
const prefix = needsBreak ? "\n\n" : "";
|
|
1190
|
+
buffers.set(event.sessionId, current + prefix + block.text);
|
|
1043
1191
|
} else if (block.type === "image" && typeof block.data === "string" && typeof block.mimeType === "string" && mediaClient) {
|
|
1044
1192
|
const ctx2 = sessions.get(event.sessionId);
|
|
1045
1193
|
if (ctx2) {
|
|
@@ -1068,8 +1216,9 @@ function createMatrixTransport(opts) {
|
|
|
1068
1216
|
}
|
|
1069
1217
|
return;
|
|
1070
1218
|
}
|
|
1071
|
-
|
|
1072
|
-
const
|
|
1219
|
+
flushBuffer(event.sessionId);
|
|
1220
|
+
const eventType = event.type === "tool_call" ? "dev.zooid.tool_call" : event.type === "tool_call_update" ? "dev.zooid.tool_call_update" : event.type === "available_commands" ? "dev.zooid.available_commands_update" : "dev.zooid.plan";
|
|
1221
|
+
const body = event.type === "tool_call" ? toToolCallBody(event) : event.type === "tool_call_update" ? toUpdateBody(event) : event.type === "available_commands" ? toAvailableCommandsBody(event) : toPlanBody(event);
|
|
1073
1222
|
body["m.relates_to"] = { rel_type: "m.thread", event_id: ctx.threadRoot };
|
|
1074
1223
|
const tail = (sendQueue.get(event.sessionId) ?? Promise.resolve()).then(async () => {
|
|
1075
1224
|
try {
|
|
@@ -1108,185 +1257,201 @@ function createMatrixTransport(opts) {
|
|
|
1108
1257
|
void client.sendCustomEvent({
|
|
1109
1258
|
roomId: ctx.roomId,
|
|
1110
1259
|
asUserId: ctx.agent.userId,
|
|
1111
|
-
eventType: "
|
|
1260
|
+
eventType: "dev.zooid.approval_request",
|
|
1112
1261
|
content
|
|
1113
1262
|
});
|
|
1114
1263
|
});
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
const got = h.slice(7);
|
|
1120
|
-
if (got.length !== hsToken.length) return false;
|
|
1121
|
-
return timingSafeEqual(Buffer.from(got), Buffer.from(hsToken));
|
|
1122
|
-
}
|
|
1123
|
-
app.put("/_matrix/app/v1/transactions/:txnId", async (c) => {
|
|
1124
|
-
if (!authOk(c.req.header("authorization"))) {
|
|
1125
|
-
return c.json({ errcode: "M_FORBIDDEN" }, 403);
|
|
1126
|
-
}
|
|
1127
|
-
const body = await c.req.json().catch(() => ({}));
|
|
1128
|
-
for (const evt of body.events ?? []) {
|
|
1129
|
-
if (evt.event_id) {
|
|
1130
|
-
if (seenEventIds.has(evt.event_id)) {
|
|
1131
|
-
continue;
|
|
1132
|
-
}
|
|
1133
|
-
seenEventIds.add(evt.event_id);
|
|
1134
|
-
if (seenEventIds.size > SEEN_EVENT_CAP) {
|
|
1135
|
-
const first = seenEventIds.values().next().value;
|
|
1136
|
-
if (first !== void 0) seenEventIds.delete(first);
|
|
1137
|
-
}
|
|
1264
|
+
async function handleInboundEvent(evt) {
|
|
1265
|
+
if (evt.event_id) {
|
|
1266
|
+
if (seenEventIds.has(evt.event_id)) {
|
|
1267
|
+
return;
|
|
1138
1268
|
}
|
|
1139
|
-
|
|
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);
|
|
1273
|
+
}
|
|
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))) {
|
|
1140
1285
|
console.log(
|
|
1141
|
-
`[matrix]
|
|
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)
|
|
1142
1290
|
);
|
|
1143
|
-
continue;
|
|
1144
1291
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
for (const a of bindings) {
|
|
1154
|
-
agents.endSession(a.name, threadRoot);
|
|
1155
|
-
}
|
|
1156
|
-
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;
|
|
1157
1300
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
);
|
|
1173
|
-
await agents.cancelSession(t.agent, t.sessionId).catch((err) => {
|
|
1174
|
-
console.error(`[matrix] cancelSession(${t.agent}, ${t.sessionId}) failed:`, err);
|
|
1175
|
-
});
|
|
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);
|
|
1304
|
+
}
|
|
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 });
|
|
1176
1316
|
}
|
|
1177
|
-
continue;
|
|
1178
|
-
}
|
|
1179
|
-
if (!content.session_id) {
|
|
1180
|
-
console.warn(`[matrix] eco.zoon.interrupt missing session_id (event_id=${evt.event_id})`);
|
|
1181
|
-
continue;
|
|
1182
1317
|
}
|
|
1183
|
-
const
|
|
1184
|
-
|
|
1185
|
-
|
|
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
|
+
});
|
|
1186
1325
|
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
});
|
|
1193
|
-
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;
|
|
1194
1331
|
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
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(",")}`
|
|
1203
1377
|
);
|
|
1204
|
-
|
|
1205
|
-
|
|
1378
|
+
} catch (err) {
|
|
1379
|
+
console.warn(`[matrix] failed to rebuild threadState for ${inboundRel}:`, err);
|
|
1206
1380
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
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);
|
|
1219
1394
|
}
|
|
1220
|
-
const
|
|
1221
|
-
const
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
const rebuilt = await rebuildThreadState(client, evt.room_id, inboundRel, bindings);
|
|
1225
|
-
threadStates.set(inboundRel, rebuilt);
|
|
1226
|
-
console.log(
|
|
1227
|
-
`[matrix] rebuilt threadState for ${inboundRel}: participants=${rebuilt.participants.join(",")} rootMentions=${rebuilt.rootMentions.join(",")}`
|
|
1228
|
-
);
|
|
1229
|
-
} catch (err) {
|
|
1230
|
-
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);
|
|
1231
1399
|
}
|
|
1232
1400
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
);
|
|
1239
|
-
}
|
|
1240
|
-
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;
|
|
1241
1406
|
let st = threadStates.get(promotedRoot);
|
|
1242
1407
|
if (!st) {
|
|
1243
1408
|
st = { participants: [], rootMentions: [] };
|
|
1244
1409
|
threadStates.set(promotedRoot, st);
|
|
1245
1410
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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);
|
|
1290
1455
|
}
|
|
1291
1456
|
return c.json({});
|
|
1292
1457
|
});
|
|
@@ -1318,6 +1483,12 @@ function createMatrixTransport(opts) {
|
|
|
1318
1483
|
sessions.set(sessionId, { agent, roomId: evt.room_id, threadRoot });
|
|
1319
1484
|
buffers.set(sessionId, "");
|
|
1320
1485
|
bufferMessageIds.delete(sessionId);
|
|
1486
|
+
flushedCounts.set(sessionId, 0);
|
|
1487
|
+
const stashedCommands = pendingCommands.get(sessionId);
|
|
1488
|
+
if (stashedCommands) {
|
|
1489
|
+
pendingCommands.delete(sessionId);
|
|
1490
|
+
void agents.onEvent?.(agent.name, stashedCommands);
|
|
1491
|
+
}
|
|
1321
1492
|
const roomId = evt.room_id;
|
|
1322
1493
|
const TYPING_TTL_MS = 3e4;
|
|
1323
1494
|
const TYPING_REFRESH_MS = 25e3;
|
|
@@ -1363,32 +1534,13 @@ function createMatrixTransport(opts) {
|
|
|
1363
1534
|
while (drainQuietMs > 0 && Date.now() - drainStart < drainMaxMs) {
|
|
1364
1535
|
await delay(drainQuietMs);
|
|
1365
1536
|
const next = buffers.get(sessionId) ?? "";
|
|
1366
|
-
if (next === drained && next.length > 0)
|
|
1537
|
+
if (next === drained && (next.length > 0 || (flushedCounts.get(sessionId) ?? 0) > 0))
|
|
1538
|
+
break;
|
|
1367
1539
|
drained = next;
|
|
1368
1540
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
const content = {
|
|
1373
|
-
msgtype: "m.text",
|
|
1374
|
-
body: text
|
|
1375
|
-
};
|
|
1376
|
-
if (html) {
|
|
1377
|
-
const escapedPlain = "<p>" + text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") + "</p>";
|
|
1378
|
-
const norm = (s) => s.replace(/\s+/g, " ").trim();
|
|
1379
|
-
if (norm(html) !== norm(escapedPlain)) {
|
|
1380
|
-
content.format = "org.matrix.custom.html";
|
|
1381
|
-
content.formatted_body = html;
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
await client.sendMessage({
|
|
1385
|
-
roomId: evt.room_id,
|
|
1386
|
-
asUserId: agent.userId,
|
|
1387
|
-
content,
|
|
1388
|
-
threadRoot
|
|
1389
|
-
// every reply threads, full stop
|
|
1390
|
-
});
|
|
1391
|
-
} else {
|
|
1541
|
+
flushBuffer(sessionId);
|
|
1542
|
+
await (sendQueue.get(sessionId) ?? Promise.resolve());
|
|
1543
|
+
if ((flushedCounts.get(sessionId) ?? 0) === 0) {
|
|
1392
1544
|
console.warn(
|
|
1393
1545
|
`[matrix:${agent.name}] turn finished with empty buffer (session=${sessionId}); nothing sent to ${evt.room_id}`
|
|
1394
1546
|
);
|
|
@@ -1399,10 +1551,22 @@ function createMatrixTransport(opts) {
|
|
|
1399
1551
|
await safePresence("online");
|
|
1400
1552
|
buffers.delete(sessionId);
|
|
1401
1553
|
bufferMessageIds.delete(sessionId);
|
|
1554
|
+
flushedCounts.delete(sessionId);
|
|
1555
|
+
sendQueue.delete(sessionId);
|
|
1402
1556
|
}
|
|
1403
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;
|
|
1404
1567
|
return {
|
|
1405
1568
|
app,
|
|
1569
|
+
syncLoops,
|
|
1406
1570
|
bootstrap: async (bootstrapOpts = {}) => {
|
|
1407
1571
|
await pool.bootstrap({ adminUserId, ...bootstrapOpts });
|
|
1408
1572
|
await Promise.allSettled(
|
|
@@ -1484,7 +1648,7 @@ async function publishWorkforce(opts) {
|
|
|
1484
1648
|
await opts.client.sendStateEvent({
|
|
1485
1649
|
roomId: opts.spaceRoomId,
|
|
1486
1650
|
asUserId: opts.asUserId,
|
|
1487
|
-
eventType: "
|
|
1651
|
+
eventType: "dev.zooid.workforce",
|
|
1488
1652
|
stateKey: "",
|
|
1489
1653
|
content: buildWorkforceRoster(opts.agents)
|
|
1490
1654
|
});
|
|
@@ -1500,6 +1664,7 @@ async function startWorkforcePublisher(opts) {
|
|
|
1500
1664
|
};
|
|
1501
1665
|
}
|
|
1502
1666
|
export {
|
|
1667
|
+
AGENT_KEY_RE,
|
|
1503
1668
|
BotPool,
|
|
1504
1669
|
INLINE_IMAGE_MIMES,
|
|
1505
1670
|
MAX_DOWNLOAD_BYTES,
|
|
@@ -1510,18 +1675,25 @@ export {
|
|
|
1510
1675
|
MatrixContextProvider,
|
|
1511
1676
|
MediaClient,
|
|
1512
1677
|
PendingMediaStore,
|
|
1678
|
+
SLUG_RE,
|
|
1679
|
+
SyncLoop,
|
|
1680
|
+
agentMxid,
|
|
1513
1681
|
buildWorkforceRoster,
|
|
1514
1682
|
createMatrixTransport,
|
|
1515
1683
|
ensureDefaultChannel,
|
|
1516
1684
|
ensureWorkforceSpace,
|
|
1517
1685
|
extractMentions,
|
|
1518
1686
|
isMediaMsgtype,
|
|
1687
|
+
isValidAgentKey,
|
|
1688
|
+
isValidWorkstation,
|
|
1519
1689
|
parseMxcUri,
|
|
1520
1690
|
publishWorkforce,
|
|
1521
1691
|
renderRegistration,
|
|
1522
1692
|
route,
|
|
1523
1693
|
serverNameFromMxid,
|
|
1694
|
+
splitAgentLocalpart,
|
|
1524
1695
|
startWorkforcePublisher,
|
|
1696
|
+
workstationUserNamespace,
|
|
1525
1697
|
writeAttachment
|
|
1526
1698
|
};
|
|
1527
1699
|
//# sourceMappingURL=index.js.map
|