@zooid/transport-matrix 0.8.0 → 0.9.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 CHANGED
@@ -92,6 +92,9 @@ declare class MatrixClient {
92
92
  asUserId: string;
93
93
  targetUserId: string;
94
94
  }): Promise<void>;
95
+ leaveRoom(roomId: string, asUserId: string, opts?: {
96
+ reason?: string;
97
+ }): Promise<void>;
95
98
  joinRoom(roomIdOrAlias: string, asUserId: string): Promise<void>;
96
99
  sendMessage(input: SendMessageInput): Promise<{
97
100
  event_id: string;
@@ -320,6 +323,10 @@ interface CreateMatrixTransportOptions {
320
323
  media?: MediaClientLike;
321
324
  /** Injected attachment writer (defaults to the real writeAttachment). */
322
325
  writeAttachmentFn?: typeof writeAttachment;
326
+ /** AS sender-bot MXID (@<sender_localpart>:<server>). Together with the agent
327
+ * bindings this forms the set of "our bot users" whose ad-hoc invites are
328
+ * declined. */
329
+ botUserId?: string;
323
330
  }
324
331
  declare function createMatrixTransport(opts: CreateMatrixTransportOptions): {
325
332
  app: Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
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, {
@@ -729,6 +741,15 @@ function toPlanBody(evt) {
729
741
  entries: evt.entries
730
742
  };
731
743
  }
744
+ function toAvailableCommandsBody(evt) {
745
+ return {
746
+ session_id: evt.sessionId,
747
+ available_commands: evt.commands.map((c) => ({
748
+ name: c.name,
749
+ description: c.description
750
+ }))
751
+ };
752
+ }
732
753
  var RECOVERY_URLS = {
733
754
  auth_missing: "https://zooid.dev/docs/guides/run-in-container#authentication-that-carries-over",
734
755
  auth_invalid: "https://zooid.dev/docs/guides/run-in-container#authentication-that-carries-over",
@@ -985,7 +1006,7 @@ async function sendMediaError(ctx, _err, message, client) {
985
1006
  await client.sendCustomEvent({
986
1007
  roomId: ctx.roomId,
987
1008
  asUserId: ctx.agent.userId,
988
- eventType: "eco.zoon.error",
1009
+ eventType: "dev.zooid.error",
989
1010
  content: toErrorBody(
990
1011
  {
991
1012
  kind: "error",
@@ -998,7 +1019,7 @@ async function sendMediaError(ctx, _err, message, client) {
998
1019
  },
999
1020
  ctx.threadRoot
1000
1021
  )
1001
- }).catch((e) => console.warn(`[matrix:${ctx.agent.name}] eco.zoon.error send failed:`, e));
1022
+ }).catch((e) => console.warn(`[matrix:${ctx.agent.name}] dev.zooid.error send failed:`, e));
1002
1023
  }
1003
1024
  var SEEN_EVENT_CAP = 5e3;
1004
1025
  var DRAIN_QUIET_MS = 300;
@@ -1009,13 +1030,18 @@ function inboundThreadRoot2(evt) {
1009
1030
  return r?.rel_type === "m.thread" && r.event_id ? r.event_id : void 0;
1010
1031
  }
1011
1032
  function createMatrixTransport(opts) {
1012
- const { agents, approvals, client, bindings, hsToken, adminUserId } = opts;
1033
+ const { agents, approvals, client, bindings, hsToken, adminUserId, botUserId } = opts;
1013
1034
  const drainQuietMs = opts.drainQuietMs ?? DRAIN_QUIET_MS;
1014
1035
  const drainMaxMs = opts.drainMaxMs ?? DRAIN_MAX_MS;
1015
1036
  const mediaClient = opts.media;
1016
1037
  const writeAttachmentFn = opts.writeAttachmentFn ?? writeAttachment;
1017
1038
  const pendingMedia = new PendingMediaStore();
1018
1039
  const pool = new BotPool(client, bindings);
1040
+ const ourBotUserIds = /* @__PURE__ */ new Set([
1041
+ ...botUserId ? [botUserId] : [],
1042
+ ...bindings.map((b) => b.userId)
1043
+ ]);
1044
+ 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
1045
  const sessions = /* @__PURE__ */ new Map();
1020
1046
  const buffers = /* @__PURE__ */ new Map();
1021
1047
  const bufferMessageIds = /* @__PURE__ */ new Map();
@@ -1023,23 +1049,68 @@ function createMatrixTransport(opts) {
1023
1049
  const threadStates = /* @__PURE__ */ new Map();
1024
1050
  const cutoffTs = Date.now() - STARTUP_GRACE_MS;
1025
1051
  const seenEventIds = /* @__PURE__ */ new Set();
1052
+ const flushedCounts = /* @__PURE__ */ new Map();
1053
+ const pendingCommands = /* @__PURE__ */ new Map();
1054
+ const buildTextContent = (text) => {
1055
+ const content = {
1056
+ msgtype: "m.text",
1057
+ body: text
1058
+ };
1059
+ const html = toMatrixHtml(text);
1060
+ if (html) {
1061
+ const escapedPlain = "<p>" + text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;") + "</p>";
1062
+ const norm = (s) => s.replace(/\s+/g, " ").trim();
1063
+ if (norm(html) !== norm(escapedPlain)) {
1064
+ content.format = "org.matrix.custom.html";
1065
+ content.formatted_body = html;
1066
+ }
1067
+ }
1068
+ return content;
1069
+ };
1070
+ const flushBuffer = (sessionId) => {
1071
+ const ctx = sessions.get(sessionId);
1072
+ const text = buffers.get(sessionId) ?? "";
1073
+ if (!ctx || text.length === 0) return false;
1074
+ buffers.set(sessionId, "");
1075
+ flushedCounts.set(sessionId, (flushedCounts.get(sessionId) ?? 0) + 1);
1076
+ const content = buildTextContent(text);
1077
+ const tail = (sendQueue.get(sessionId) ?? Promise.resolve()).then(async () => {
1078
+ try {
1079
+ await client.sendMessage({
1080
+ roomId: ctx.roomId,
1081
+ asUserId: ctx.agent.userId,
1082
+ content,
1083
+ threadRoot: ctx.threadRoot
1084
+ });
1085
+ } catch (err) {
1086
+ console.warn(`[matrix:${ctx.agent.name}] sendMessage flush failed:`, err);
1087
+ }
1088
+ });
1089
+ sendQueue.set(sessionId, tail);
1090
+ return true;
1091
+ };
1026
1092
  agents.onEvent = async (name, event) => {
1027
1093
  const ctx = sessions.get(event.sessionId);
1028
1094
  if (!ctx) {
1029
- console.warn(`[matrix:${name}] no session ctx for ${event.sessionId}`);
1095
+ if (event.type === "available_commands") {
1096
+ pendingCommands.set(event.sessionId, event);
1097
+ } else {
1098
+ console.warn(`[matrix:${name}] no session ctx for ${event.sessionId}`);
1099
+ }
1030
1100
  return;
1031
1101
  }
1032
1102
  if (event.type === "agent_message_chunk") {
1033
1103
  const block = event.content;
1034
1104
  if (block.type === "text" && typeof block.text === "string") {
1035
- const current = buffers.get(event.sessionId) ?? "";
1036
1105
  const prevMessageId = bufferMessageIds.get(event.sessionId);
1037
1106
  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
1107
  if (event.messageId !== void 0)
1042
1108
  bufferMessageIds.set(event.sessionId, event.messageId);
1109
+ if (messageChanged) flushBuffer(event.sessionId);
1110
+ const current = buffers.get(event.sessionId) ?? "";
1111
+ const needsBreak = current.length > 0 && block.text === "";
1112
+ const prefix = needsBreak ? "\n\n" : "";
1113
+ buffers.set(event.sessionId, current + prefix + block.text);
1043
1114
  } else if (block.type === "image" && typeof block.data === "string" && typeof block.mimeType === "string" && mediaClient) {
1044
1115
  const ctx2 = sessions.get(event.sessionId);
1045
1116
  if (ctx2) {
@@ -1068,8 +1139,9 @@ function createMatrixTransport(opts) {
1068
1139
  }
1069
1140
  return;
1070
1141
  }
1071
- const eventType = event.type === "tool_call" ? "eco.zoon.tool_call" : event.type === "tool_call_update" ? "eco.zoon.tool_call_update" : "eco.zoon.plan";
1072
- const body = event.type === "tool_call" ? toToolCallBody(event) : event.type === "tool_call_update" ? toUpdateBody(event) : toPlanBody(event);
1142
+ flushBuffer(event.sessionId);
1143
+ 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";
1144
+ const body = event.type === "tool_call" ? toToolCallBody(event) : event.type === "tool_call_update" ? toUpdateBody(event) : event.type === "available_commands" ? toAvailableCommandsBody(event) : toPlanBody(event);
1073
1145
  body["m.relates_to"] = { rel_type: "m.thread", event_id: ctx.threadRoot };
1074
1146
  const tail = (sendQueue.get(event.sessionId) ?? Promise.resolve()).then(async () => {
1075
1147
  try {
@@ -1108,7 +1180,7 @@ function createMatrixTransport(opts) {
1108
1180
  void client.sendCustomEvent({
1109
1181
  roomId: ctx.roomId,
1110
1182
  asUserId: ctx.agent.userId,
1111
- eventType: "eco.zoon.approval_request",
1183
+ eventType: "dev.zooid.approval_request",
1112
1184
  content
1113
1185
  });
1114
1186
  });
@@ -1142,20 +1214,33 @@ function createMatrixTransport(opts) {
1142
1214
  );
1143
1215
  continue;
1144
1216
  }
1145
- if (evt.type === "eco.zoon.session_reset") {
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;
1229
+ }
1230
+ if (evt.type === "dev.zooid.session_reset") {
1146
1231
  const relates = evt.content?.["m.relates_to"];
1147
1232
  const threadRoot = relates?.rel_type === "m.thread" && relates.event_id ? relates.event_id : void 0;
1148
1233
  if (!threadRoot) {
1149
- console.log("[matrix] dropping eco.zoon.session_reset without thread relation");
1234
+ console.log("[matrix] dropping dev.zooid.session_reset without thread relation");
1150
1235
  continue;
1151
1236
  }
1152
- console.log(`[matrix] inbound eco.zoon.session_reset in ${evt.room_id} thread=${threadRoot}`);
1237
+ console.log(`[matrix] inbound dev.zooid.session_reset in ${evt.room_id} thread=${threadRoot}`);
1153
1238
  for (const a of bindings) {
1154
1239
  agents.endSession(a.name, threadRoot);
1155
1240
  }
1156
1241
  continue;
1157
1242
  }
1158
- if (evt.type === "eco.zoon.interrupt") {
1243
+ if (evt.type === "dev.zooid.interrupt") {
1159
1244
  const content = evt.content ?? {};
1160
1245
  const relates = evt.content?.["m.relates_to"];
1161
1246
  const threadRoot = relates?.rel_type === "m.thread" && relates.event_id ? relates.event_id : void 0;
@@ -1177,7 +1262,7 @@ function createMatrixTransport(opts) {
1177
1262
  continue;
1178
1263
  }
1179
1264
  if (!content.session_id) {
1180
- console.warn(`[matrix] eco.zoon.interrupt missing session_id (event_id=${evt.event_id})`);
1265
+ console.warn(`[matrix] dev.zooid.interrupt missing session_id (event_id=${evt.event_id})`);
1181
1266
  continue;
1182
1267
  }
1183
1268
  const ctx = sessions.get(content.session_id);
@@ -1192,7 +1277,7 @@ function createMatrixTransport(opts) {
1192
1277
  });
1193
1278
  continue;
1194
1279
  }
1195
- if (evt.type === "eco.zoon.approval_response") {
1280
+ if (evt.type === "dev.zooid.approval_response") {
1196
1281
  const content = evt.content ?? {};
1197
1282
  if (!content.session_id || !content.approval_id || !content.decision) continue;
1198
1283
  const decision = content.option_id ? { decision: content.decision, optionId: content.option_id } : { decision: content.decision };
@@ -1282,9 +1367,9 @@ function createMatrixTransport(opts) {
1282
1367
  void client.sendCustomEvent({
1283
1368
  roomId: evt.room_id,
1284
1369
  asUserId: a.userId,
1285
- eventType: "eco.zoon.error",
1370
+ eventType: "dev.zooid.error",
1286
1371
  content: body2
1287
- }).catch((e) => console.warn(`[matrix:${a.name}] eco.zoon.error send failed:`, e));
1372
+ }).catch((e) => console.warn(`[matrix:${a.name}] dev.zooid.error send failed:`, e));
1288
1373
  });
1289
1374
  }
1290
1375
  }
@@ -1318,6 +1403,12 @@ function createMatrixTransport(opts) {
1318
1403
  sessions.set(sessionId, { agent, roomId: evt.room_id, threadRoot });
1319
1404
  buffers.set(sessionId, "");
1320
1405
  bufferMessageIds.delete(sessionId);
1406
+ flushedCounts.set(sessionId, 0);
1407
+ const stashedCommands = pendingCommands.get(sessionId);
1408
+ if (stashedCommands) {
1409
+ pendingCommands.delete(sessionId);
1410
+ void agents.onEvent?.(agent.name, stashedCommands);
1411
+ }
1321
1412
  const roomId = evt.room_id;
1322
1413
  const TYPING_TTL_MS = 3e4;
1323
1414
  const TYPING_REFRESH_MS = 25e3;
@@ -1363,32 +1454,13 @@ function createMatrixTransport(opts) {
1363
1454
  while (drainQuietMs > 0 && Date.now() - drainStart < drainMaxMs) {
1364
1455
  await delay(drainQuietMs);
1365
1456
  const next = buffers.get(sessionId) ?? "";
1366
- if (next === drained && next.length > 0) break;
1457
+ if (next === drained && (next.length > 0 || (flushedCounts.get(sessionId) ?? 0) > 0))
1458
+ break;
1367
1459
  drained = next;
1368
1460
  }
1369
- const text = buffers.get(sessionId) ?? "";
1370
- if (text.length > 0) {
1371
- const html = toMatrixHtml(text);
1372
- const content = {
1373
- msgtype: "m.text",
1374
- body: text
1375
- };
1376
- if (html) {
1377
- const escapedPlain = "<p>" + text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;") + "</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 {
1461
+ flushBuffer(sessionId);
1462
+ await (sendQueue.get(sessionId) ?? Promise.resolve());
1463
+ if ((flushedCounts.get(sessionId) ?? 0) === 0) {
1392
1464
  console.warn(
1393
1465
  `[matrix:${agent.name}] turn finished with empty buffer (session=${sessionId}); nothing sent to ${evt.room_id}`
1394
1466
  );
@@ -1399,6 +1471,8 @@ function createMatrixTransport(opts) {
1399
1471
  await safePresence("online");
1400
1472
  buffers.delete(sessionId);
1401
1473
  bufferMessageIds.delete(sessionId);
1474
+ flushedCounts.delete(sessionId);
1475
+ sendQueue.delete(sessionId);
1402
1476
  }
1403
1477
  }
1404
1478
  return {
@@ -1484,7 +1558,7 @@ async function publishWorkforce(opts) {
1484
1558
  await opts.client.sendStateEvent({
1485
1559
  roomId: opts.spaceRoomId,
1486
1560
  asUserId: opts.asUserId,
1487
- eventType: "eco.zoon.workforce",
1561
+ eventType: "dev.zooid.workforce",
1488
1562
  stateKey: "",
1489
1563
  content: buildWorkforceRoster(opts.agents)
1490
1564
  });