@zooid/transport-matrix 0.7.4 → 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 +116 -1
- package/dist/index.js +387 -51
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/attachments.test.ts +58 -0
- package/src/attachments.ts +30 -0
- package/src/bot-pool.ts +1 -1
- package/src/context-provider.test.ts +33 -0
- package/src/context-provider.ts +22 -4
- package/src/event-encoders.test.ts +22 -0
- package/src/event-encoders.ts +13 -0
- package/src/index.ts +8 -2
- package/src/matrix-client.test.ts +35 -2
- package/src/matrix-client.ts +19 -0
- package/src/media-client.test.ts +102 -0
- package/src/media-client.ts +69 -0
- package/src/pending-media.test.ts +51 -0
- package/src/pending-media.ts +37 -0
- package/src/router.test.ts +22 -1
- package/src/router.ts +11 -0
- package/src/space-provisioner.test.ts +26 -1
- package/src/space-provisioner.ts +15 -4
- package/src/transport.test.ts +401 -30
- package/src/transport.ts +402 -70
- 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, {
|
|
@@ -359,14 +371,29 @@ var MatrixContextProvider = class {
|
|
|
359
371
|
}
|
|
360
372
|
toMessage(ev) {
|
|
361
373
|
if (ev.type !== "m.room.message") return null;
|
|
362
|
-
|
|
374
|
+
const msgtype = ev.content?.msgtype;
|
|
375
|
+
const body = ev.content?.body;
|
|
363
376
|
const agent = this.opts.agentBots.get(ev.sender);
|
|
364
|
-
const relatesTo = ev.content["m.relates_to"];
|
|
377
|
+
const relatesTo = ev.content?.["m.relates_to"];
|
|
365
378
|
const threadId = relatesTo?.rel_type === "m.thread" && relatesTo.event_id ? relatesTo.event_id : void 0;
|
|
379
|
+
if (msgtype === "m.image" || msgtype === "m.file" || msgtype === "m.video" || msgtype === "m.audio") {
|
|
380
|
+
const kind = msgtype.slice(2);
|
|
381
|
+
const name = typeof body === "string" && body ? body : "untitled";
|
|
382
|
+
return {
|
|
383
|
+
id: ev.event_id,
|
|
384
|
+
sender: ev.sender,
|
|
385
|
+
text: `[${kind}: ${name}]`,
|
|
386
|
+
timestamp: new Date(ev.origin_server_ts).toISOString(),
|
|
387
|
+
is_agent: agent !== void 0,
|
|
388
|
+
...agent !== void 0 ? { agent_name: agent } : {},
|
|
389
|
+
...threadId !== void 0 ? { thread_id: threadId } : {}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
if (msgtype !== "m.text" || typeof body !== "string") return null;
|
|
366
393
|
return {
|
|
367
394
|
id: ev.event_id,
|
|
368
395
|
sender: ev.sender,
|
|
369
|
-
text:
|
|
396
|
+
text: body,
|
|
370
397
|
timestamp: new Date(ev.origin_server_ts).toISOString(),
|
|
371
398
|
is_agent: agent !== void 0,
|
|
372
399
|
...agent !== void 0 ? { agent_name: agent } : {},
|
|
@@ -440,6 +467,10 @@ function stripMention(body, userId) {
|
|
|
440
467
|
}
|
|
441
468
|
|
|
442
469
|
// src/router.ts
|
|
470
|
+
var MEDIA_MSGTYPES = /* @__PURE__ */ new Set(["m.image", "m.file", "m.video", "m.audio"]);
|
|
471
|
+
function isMediaMsgtype(t) {
|
|
472
|
+
return t !== void 0 && MEDIA_MSGTYPES.has(t);
|
|
473
|
+
}
|
|
443
474
|
function inboundThreadRoot(event) {
|
|
444
475
|
const r = event.content?.["m.relates_to"];
|
|
445
476
|
return r?.rel_type === "m.thread" && r.event_id ? r.event_id : void 0;
|
|
@@ -447,6 +478,7 @@ function inboundThreadRoot(event) {
|
|
|
447
478
|
function route(event, agents, threadStates) {
|
|
448
479
|
if (event.type !== "m.room.message") return [];
|
|
449
480
|
if (!event.content?.msgtype) return [];
|
|
481
|
+
if (isMediaMsgtype(event.content.msgtype)) return [];
|
|
450
482
|
const mentions = new Set(extractMentions(event));
|
|
451
483
|
const matches = [];
|
|
452
484
|
const threadRoot = inboundThreadRoot(event);
|
|
@@ -485,10 +517,12 @@ async function ensureWorkforceSpace(opts) {
|
|
|
485
517
|
name: display,
|
|
486
518
|
preset: opts.preset,
|
|
487
519
|
creation_content: { type: "m.space" },
|
|
488
|
-
//
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
initial_state: [
|
|
520
|
+
// Pin the join rule regardless of preset. Defaults to invite so the space
|
|
521
|
+
// can't be walked into (which would otherwise satisfy every restricted
|
|
522
|
+
// child room's allow); `zooid dev` opts into `public` for local-only use.
|
|
523
|
+
initial_state: [
|
|
524
|
+
{ type: "m.room.join_rules", state_key: "", content: { join_rule: opts.joinRule ?? "invite" } }
|
|
525
|
+
]
|
|
492
526
|
};
|
|
493
527
|
if (opts.admins && opts.admins.length > 0) {
|
|
494
528
|
body.invite = opts.admins;
|
|
@@ -707,6 +741,15 @@ function toPlanBody(evt) {
|
|
|
707
741
|
entries: evt.entries
|
|
708
742
|
};
|
|
709
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
|
+
}
|
|
710
753
|
var RECOVERY_URLS = {
|
|
711
754
|
auth_missing: "https://zooid.dev/docs/guides/run-in-container#authentication-that-carries-over",
|
|
712
755
|
auth_invalid: "https://zooid.dev/docs/guides/run-in-container#authentication-that-carries-over",
|
|
@@ -804,8 +847,180 @@ function toMatrixHtml(markdown) {
|
|
|
804
847
|
});
|
|
805
848
|
}
|
|
806
849
|
|
|
850
|
+
// src/pending-media.ts
|
|
851
|
+
var MAX_MEDIA_PER_TURN = 8;
|
|
852
|
+
var PendingMediaStore = class {
|
|
853
|
+
queues = /* @__PURE__ */ new Map();
|
|
854
|
+
key(roomId, threadKey) {
|
|
855
|
+
return `${roomId} ${threadKey ?? "main"}`;
|
|
856
|
+
}
|
|
857
|
+
add(roomId, threadKey, item) {
|
|
858
|
+
const k = this.key(roomId, threadKey);
|
|
859
|
+
const q = this.queues.get(k) ?? [];
|
|
860
|
+
q.push(item);
|
|
861
|
+
while (q.length > MAX_MEDIA_PER_TURN) q.shift();
|
|
862
|
+
this.queues.set(k, q);
|
|
863
|
+
}
|
|
864
|
+
drain(roomId, threadKey, sender) {
|
|
865
|
+
const k = this.key(roomId, threadKey);
|
|
866
|
+
const q = this.queues.get(k) ?? [];
|
|
867
|
+
const mine = q.filter((i) => i.sender === sender);
|
|
868
|
+
const rest = q.filter((i) => i.sender !== sender);
|
|
869
|
+
if (rest.length) this.queues.set(k, rest);
|
|
870
|
+
else this.queues.delete(k);
|
|
871
|
+
return mine;
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
// src/media-client.ts
|
|
876
|
+
var MAX_INLINE_IMAGE_BYTES = 524288;
|
|
877
|
+
var INLINE_IMAGE_MIMES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
878
|
+
var MAX_DOWNLOAD_BYTES = 33554432;
|
|
879
|
+
function parseMxcUri(uri) {
|
|
880
|
+
const m = /^mxc:\/\/([^/]+)\/(.+)$/.exec(uri);
|
|
881
|
+
return m ? { serverName: m[1], mediaId: m[2] } : null;
|
|
882
|
+
}
|
|
883
|
+
var MediaClient = class {
|
|
884
|
+
homeserver;
|
|
885
|
+
asToken;
|
|
886
|
+
fetch;
|
|
887
|
+
constructor(opts) {
|
|
888
|
+
this.homeserver = opts.homeserver.replace(/\/$/, "");
|
|
889
|
+
this.asToken = opts.asToken;
|
|
890
|
+
this.fetch = opts.fetch ?? globalThis.fetch;
|
|
891
|
+
}
|
|
892
|
+
async download(input) {
|
|
893
|
+
const parsed = parseMxcUri(input.mxcUri);
|
|
894
|
+
if (!parsed) throw new Error(`not an mxc uri: ${input.mxcUri}`);
|
|
895
|
+
const url = `${this.homeserver}/_matrix/client/v1/media/download/${encodeURIComponent(parsed.serverName)}/${encodeURIComponent(parsed.mediaId)}?user_id=${encodeURIComponent(input.asUserId)}`;
|
|
896
|
+
const r = await this.fetch(url, {
|
|
897
|
+
headers: { Authorization: `Bearer ${this.asToken}` }
|
|
898
|
+
});
|
|
899
|
+
if (!r.ok) throw new Error(`media download failed: ${r.status}`);
|
|
900
|
+
const buf = new Uint8Array(await r.arrayBuffer());
|
|
901
|
+
const max = input.maxBytes ?? MAX_DOWNLOAD_BYTES;
|
|
902
|
+
if (buf.byteLength > max) {
|
|
903
|
+
throw new Error(`media too large: ${buf.byteLength} > ${max}`);
|
|
904
|
+
}
|
|
905
|
+
return { data: buf, contentType: r.headers.get("content-type") ?? "application/octet-stream" };
|
|
906
|
+
}
|
|
907
|
+
async upload(input) {
|
|
908
|
+
const params = new URLSearchParams();
|
|
909
|
+
if (input.filename) params.set("filename", input.filename);
|
|
910
|
+
params.set("user_id", input.asUserId);
|
|
911
|
+
const r = await this.fetch(`${this.homeserver}/_matrix/media/v3/upload?${params}`, {
|
|
912
|
+
method: "POST",
|
|
913
|
+
headers: { Authorization: `Bearer ${this.asToken}`, "Content-Type": input.contentType },
|
|
914
|
+
body: input.data
|
|
915
|
+
});
|
|
916
|
+
if (!r.ok) throw new Error(`media upload failed: ${r.status}`);
|
|
917
|
+
return await r.json();
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
// src/attachments.ts
|
|
922
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
923
|
+
import { join } from "path";
|
|
924
|
+
import { posix } from "path";
|
|
925
|
+
function sanitize(s, fallback) {
|
|
926
|
+
const cleaned = s.replace(/[^A-Za-z0-9._-]/g, "").replace(/^\.+/, "");
|
|
927
|
+
return cleaned || fallback;
|
|
928
|
+
}
|
|
929
|
+
function writeAttachment(input) {
|
|
930
|
+
const dir = sanitize(input.eventId, "event");
|
|
931
|
+
const name = sanitize(input.filename, "file");
|
|
932
|
+
const hostDir = join(input.workspaceDir, ".zooid", "attachments", dir);
|
|
933
|
+
mkdirSync(hostDir, { recursive: true });
|
|
934
|
+
const hostPath = join(hostDir, name);
|
|
935
|
+
writeFileSync(hostPath, input.data);
|
|
936
|
+
const agentPath = posix.join(input.agentWorkspacePath, ".zooid", "attachments", dir, name);
|
|
937
|
+
return { hostPath, agentPath };
|
|
938
|
+
}
|
|
939
|
+
|
|
807
940
|
// src/transport.ts
|
|
808
941
|
var STARTUP_GRACE_MS = 5e3;
|
|
942
|
+
async function buildMediaBlocks(items, opts) {
|
|
943
|
+
const blocks = [];
|
|
944
|
+
const pathLines = [];
|
|
945
|
+
if (!opts.media || items.length === 0) return { blocks, pathLines };
|
|
946
|
+
for (const item of items) {
|
|
947
|
+
try {
|
|
948
|
+
const isInlineCandidate = item.msgtype === "m.image" && INLINE_IMAGE_MIMES.includes(item.info?.mimetype ?? "") && (item.info?.size === void 0 || item.info.size <= MAX_INLINE_IMAGE_BYTES);
|
|
949
|
+
if (isInlineCandidate) {
|
|
950
|
+
const { data, contentType } = await opts.media.download({
|
|
951
|
+
mxcUri: item.url,
|
|
952
|
+
asUserId: opts.agent.userId
|
|
953
|
+
});
|
|
954
|
+
if (data.byteLength <= MAX_INLINE_IMAGE_BYTES) {
|
|
955
|
+
blocks.push({
|
|
956
|
+
type: "image",
|
|
957
|
+
data: Buffer.from(data).toString("base64"),
|
|
958
|
+
mimeType: contentType
|
|
959
|
+
});
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
if (opts.agent.workspaceDir) {
|
|
963
|
+
const paths = opts.writeAttachmentFn({
|
|
964
|
+
workspaceDir: opts.agent.workspaceDir,
|
|
965
|
+
agentWorkspacePath: opts.agent.agentWorkspacePath ?? opts.agent.workspaceDir,
|
|
966
|
+
eventId: item.eventId,
|
|
967
|
+
filename: item.filename ?? item.body,
|
|
968
|
+
data
|
|
969
|
+
});
|
|
970
|
+
blocks.push({
|
|
971
|
+
type: "resource_link",
|
|
972
|
+
uri: `file://${paths.agentPath}`,
|
|
973
|
+
name: item.filename ?? item.body
|
|
974
|
+
});
|
|
975
|
+
pathLines.push(`Attached file: ${paths.agentPath}`);
|
|
976
|
+
}
|
|
977
|
+
} else {
|
|
978
|
+
if (!opts.agent.workspaceDir) continue;
|
|
979
|
+
const { data } = await opts.media.download({
|
|
980
|
+
mxcUri: item.url,
|
|
981
|
+
asUserId: opts.agent.userId
|
|
982
|
+
});
|
|
983
|
+
const paths = opts.writeAttachmentFn({
|
|
984
|
+
workspaceDir: opts.agent.workspaceDir,
|
|
985
|
+
agentWorkspacePath: opts.agent.agentWorkspacePath ?? opts.agent.workspaceDir,
|
|
986
|
+
eventId: item.eventId,
|
|
987
|
+
filename: item.filename ?? item.body,
|
|
988
|
+
data
|
|
989
|
+
});
|
|
990
|
+
blocks.push({
|
|
991
|
+
type: "resource_link",
|
|
992
|
+
uri: `file://${paths.agentPath}`,
|
|
993
|
+
name: item.filename ?? item.body,
|
|
994
|
+
mimeType: item.info?.mimetype,
|
|
995
|
+
size: item.info?.size
|
|
996
|
+
});
|
|
997
|
+
pathLines.push(`Attached file: ${paths.agentPath}`);
|
|
998
|
+
}
|
|
999
|
+
} catch (err) {
|
|
1000
|
+
opts.onError(item, err);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return { blocks, pathLines };
|
|
1004
|
+
}
|
|
1005
|
+
async function sendMediaError(ctx, _err, message, client) {
|
|
1006
|
+
await client.sendCustomEvent({
|
|
1007
|
+
roomId: ctx.roomId,
|
|
1008
|
+
asUserId: ctx.agent.userId,
|
|
1009
|
+
eventType: "dev.zooid.error",
|
|
1010
|
+
content: toErrorBody(
|
|
1011
|
+
{
|
|
1012
|
+
kind: "error",
|
|
1013
|
+
agentId: ctx.agent.name,
|
|
1014
|
+
sessionId: null,
|
|
1015
|
+
turnId: null,
|
|
1016
|
+
code: "media_failed",
|
|
1017
|
+
message: message.slice(0, 250),
|
|
1018
|
+
transient: false
|
|
1019
|
+
},
|
|
1020
|
+
ctx.threadRoot
|
|
1021
|
+
)
|
|
1022
|
+
}).catch((e) => console.warn(`[matrix:${ctx.agent.name}] dev.zooid.error send failed:`, e));
|
|
1023
|
+
}
|
|
809
1024
|
var SEEN_EVENT_CAP = 5e3;
|
|
810
1025
|
var DRAIN_QUIET_MS = 300;
|
|
811
1026
|
var DRAIN_MAX_MS = 3e4;
|
|
@@ -815,10 +1030,18 @@ function inboundThreadRoot2(evt) {
|
|
|
815
1030
|
return r?.rel_type === "m.thread" && r.event_id ? r.event_id : void 0;
|
|
816
1031
|
}
|
|
817
1032
|
function createMatrixTransport(opts) {
|
|
818
|
-
const { agents, approvals, client, bindings, hsToken, adminUserId } = opts;
|
|
1033
|
+
const { agents, approvals, client, bindings, hsToken, adminUserId, botUserId } = opts;
|
|
819
1034
|
const drainQuietMs = opts.drainQuietMs ?? DRAIN_QUIET_MS;
|
|
820
1035
|
const drainMaxMs = opts.drainMaxMs ?? DRAIN_MAX_MS;
|
|
1036
|
+
const mediaClient = opts.media;
|
|
1037
|
+
const writeAttachmentFn = opts.writeAttachmentFn ?? writeAttachment;
|
|
1038
|
+
const pendingMedia = new PendingMediaStore();
|
|
821
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.";
|
|
822
1045
|
const sessions = /* @__PURE__ */ new Map();
|
|
823
1046
|
const buffers = /* @__PURE__ */ new Map();
|
|
824
1047
|
const bufferMessageIds = /* @__PURE__ */ new Map();
|
|
@@ -826,30 +1049,99 @@ function createMatrixTransport(opts) {
|
|
|
826
1049
|
const threadStates = /* @__PURE__ */ new Map();
|
|
827
1050
|
const cutoffTs = Date.now() - STARTUP_GRACE_MS;
|
|
828
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, "&").replace(/</g, "<").replace(/>/g, ">") + "</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
|
+
};
|
|
829
1092
|
agents.onEvent = async (name, event) => {
|
|
830
1093
|
const ctx = sessions.get(event.sessionId);
|
|
831
1094
|
if (!ctx) {
|
|
832
|
-
|
|
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
|
+
}
|
|
833
1100
|
return;
|
|
834
1101
|
}
|
|
835
1102
|
if (event.type === "agent_message_chunk") {
|
|
836
1103
|
const block = event.content;
|
|
837
1104
|
if (block.type === "text" && typeof block.text === "string") {
|
|
838
|
-
const current = buffers.get(event.sessionId) ?? "";
|
|
839
1105
|
const prevMessageId = bufferMessageIds.get(event.sessionId);
|
|
840
1106
|
const messageChanged = event.messageId !== void 0 && prevMessageId !== void 0 && event.messageId !== prevMessageId;
|
|
841
|
-
const needsBreak = current.length > 0 && (block.text === "" || messageChanged);
|
|
842
|
-
const prefix = needsBreak ? "\n\n" : "";
|
|
843
|
-
buffers.set(event.sessionId, current + prefix + block.text);
|
|
844
1107
|
if (event.messageId !== void 0)
|
|
845
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);
|
|
1114
|
+
} else if (block.type === "image" && typeof block.data === "string" && typeof block.mimeType === "string" && mediaClient) {
|
|
1115
|
+
const ctx2 = sessions.get(event.sessionId);
|
|
1116
|
+
if (ctx2) {
|
|
1117
|
+
const bytes = Buffer.from(block.data, "base64");
|
|
1118
|
+
const ext = (block.mimeType.split("/")[1] ?? "png").replace(/[^a-z0-9]/gi, "");
|
|
1119
|
+
const filename = `image.${ext}`;
|
|
1120
|
+
void mediaClient.upload({ data: bytes, contentType: block.mimeType, filename, asUserId: ctx2.agent.userId }).then(
|
|
1121
|
+
({ content_uri }) => client.sendMessage({
|
|
1122
|
+
roomId: ctx2.roomId,
|
|
1123
|
+
asUserId: ctx2.agent.userId,
|
|
1124
|
+
threadRoot: ctx2.threadRoot,
|
|
1125
|
+
content: {
|
|
1126
|
+
msgtype: "m.image",
|
|
1127
|
+
body: filename,
|
|
1128
|
+
url: content_uri,
|
|
1129
|
+
info: { mimetype: block.mimeType, size: bytes.length }
|
|
1130
|
+
}
|
|
1131
|
+
})
|
|
1132
|
+
).catch((err) => {
|
|
1133
|
+
console.warn(`[matrix:${name}] outbound image upload failed:`, err);
|
|
1134
|
+
void sendMediaError(ctx2, err, "agent image upload failed", client);
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
846
1137
|
} else {
|
|
847
1138
|
console.warn(`[matrix:${name}] dropped chunk block type=${block.type}`, block);
|
|
848
1139
|
}
|
|
849
1140
|
return;
|
|
850
1141
|
}
|
|
851
|
-
|
|
852
|
-
const
|
|
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);
|
|
853
1145
|
body["m.relates_to"] = { rel_type: "m.thread", event_id: ctx.threadRoot };
|
|
854
1146
|
const tail = (sendQueue.get(event.sessionId) ?? Promise.resolve()).then(async () => {
|
|
855
1147
|
try {
|
|
@@ -888,7 +1180,7 @@ function createMatrixTransport(opts) {
|
|
|
888
1180
|
void client.sendCustomEvent({
|
|
889
1181
|
roomId: ctx.roomId,
|
|
890
1182
|
asUserId: ctx.agent.userId,
|
|
891
|
-
eventType: "
|
|
1183
|
+
eventType: "dev.zooid.approval_request",
|
|
892
1184
|
content
|
|
893
1185
|
});
|
|
894
1186
|
});
|
|
@@ -922,20 +1214,33 @@ function createMatrixTransport(opts) {
|
|
|
922
1214
|
);
|
|
923
1215
|
continue;
|
|
924
1216
|
}
|
|
925
|
-
if (evt.type === "
|
|
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") {
|
|
926
1231
|
const relates = evt.content?.["m.relates_to"];
|
|
927
1232
|
const threadRoot = relates?.rel_type === "m.thread" && relates.event_id ? relates.event_id : void 0;
|
|
928
1233
|
if (!threadRoot) {
|
|
929
|
-
console.log("[matrix] dropping
|
|
1234
|
+
console.log("[matrix] dropping dev.zooid.session_reset without thread relation");
|
|
930
1235
|
continue;
|
|
931
1236
|
}
|
|
932
|
-
console.log(`[matrix] inbound
|
|
1237
|
+
console.log(`[matrix] inbound dev.zooid.session_reset in ${evt.room_id} thread=${threadRoot}`);
|
|
933
1238
|
for (const a of bindings) {
|
|
934
1239
|
agents.endSession(a.name, threadRoot);
|
|
935
1240
|
}
|
|
936
1241
|
continue;
|
|
937
1242
|
}
|
|
938
|
-
if (evt.type === "
|
|
1243
|
+
if (evt.type === "dev.zooid.interrupt") {
|
|
939
1244
|
const content = evt.content ?? {};
|
|
940
1245
|
const relates = evt.content?.["m.relates_to"];
|
|
941
1246
|
const threadRoot = relates?.rel_type === "m.thread" && relates.event_id ? relates.event_id : void 0;
|
|
@@ -957,7 +1262,7 @@ function createMatrixTransport(opts) {
|
|
|
957
1262
|
continue;
|
|
958
1263
|
}
|
|
959
1264
|
if (!content.session_id) {
|
|
960
|
-
console.warn(`[matrix]
|
|
1265
|
+
console.warn(`[matrix] dev.zooid.interrupt missing session_id (event_id=${evt.event_id})`);
|
|
961
1266
|
continue;
|
|
962
1267
|
}
|
|
963
1268
|
const ctx = sessions.get(content.session_id);
|
|
@@ -972,7 +1277,7 @@ function createMatrixTransport(opts) {
|
|
|
972
1277
|
});
|
|
973
1278
|
continue;
|
|
974
1279
|
}
|
|
975
|
-
if (evt.type === "
|
|
1280
|
+
if (evt.type === "dev.zooid.approval_response") {
|
|
976
1281
|
const content = evt.content ?? {};
|
|
977
1282
|
if (!content.session_id || !content.approval_id || !content.decision) continue;
|
|
978
1283
|
const decision = content.option_id ? { decision: content.decision, optionId: content.option_id } : { decision: content.decision };
|
|
@@ -985,6 +1290,18 @@ function createMatrixTransport(opts) {
|
|
|
985
1290
|
continue;
|
|
986
1291
|
}
|
|
987
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
|
+
}
|
|
988
1305
|
const promotedRoot = inboundThreadRoot2(evt) ?? evt.event_id;
|
|
989
1306
|
const inboundRel = inboundThreadRoot2(evt);
|
|
990
1307
|
if (evt.type === "m.room.message" && inboundRel && !threadStates.has(inboundRel) && evt.room_id) {
|
|
@@ -1050,9 +1367,9 @@ function createMatrixTransport(opts) {
|
|
|
1050
1367
|
void client.sendCustomEvent({
|
|
1051
1368
|
roomId: evt.room_id,
|
|
1052
1369
|
asUserId: a.userId,
|
|
1053
|
-
eventType: "
|
|
1370
|
+
eventType: "dev.zooid.error",
|
|
1054
1371
|
content: body2
|
|
1055
|
-
}).catch((e) => console.warn(`[matrix:${a.name}]
|
|
1372
|
+
}).catch((e) => console.warn(`[matrix:${a.name}] dev.zooid.error send failed:`, e));
|
|
1056
1373
|
});
|
|
1057
1374
|
}
|
|
1058
1375
|
}
|
|
@@ -1086,6 +1403,12 @@ function createMatrixTransport(opts) {
|
|
|
1086
1403
|
sessions.set(sessionId, { agent, roomId: evt.room_id, threadRoot });
|
|
1087
1404
|
buffers.set(sessionId, "");
|
|
1088
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
|
+
}
|
|
1089
1412
|
const roomId = evt.room_id;
|
|
1090
1413
|
const TYPING_TTL_MS = 3e4;
|
|
1091
1414
|
const TYPING_REFRESH_MS = 25e3;
|
|
@@ -1101,42 +1424,43 @@ function createMatrixTransport(opts) {
|
|
|
1101
1424
|
try {
|
|
1102
1425
|
const rawBody = evt.content?.body ?? "";
|
|
1103
1426
|
const promptText = stripMention(rawBody, agent.userId);
|
|
1427
|
+
const pendingItems = pendingMedia.drain(
|
|
1428
|
+
evt.room_id,
|
|
1429
|
+
inboundThreadRoot2(evt),
|
|
1430
|
+
evt.sender ?? ""
|
|
1431
|
+
);
|
|
1432
|
+
const { blocks, pathLines } = await buildMediaBlocks(pendingItems, {
|
|
1433
|
+
agent,
|
|
1434
|
+
media: mediaClient,
|
|
1435
|
+
writeAttachmentFn,
|
|
1436
|
+
onError: (item, err) => {
|
|
1437
|
+
console.warn(`[matrix:${agent.name}] media_failed for ${item.body}:`, err);
|
|
1438
|
+
void sendMediaError(
|
|
1439
|
+
{ agent, roomId: evt.room_id, threadRoot },
|
|
1440
|
+
err,
|
|
1441
|
+
`Could not process attachment: ${item.body}`,
|
|
1442
|
+
client
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
});
|
|
1446
|
+
const fullPromptText = [promptText, ...pathLines].filter(Boolean).join("\n");
|
|
1104
1447
|
await agents.prompt(agent.name, {
|
|
1105
1448
|
threadId: sessionKey,
|
|
1106
1449
|
channelId: evt.room_id,
|
|
1107
|
-
content: [{ type: "text", text:
|
|
1450
|
+
content: [...blocks, { type: "text", text: fullPromptText }]
|
|
1108
1451
|
});
|
|
1109
1452
|
const drainStart = Date.now();
|
|
1110
1453
|
let drained = buffers.get(sessionId) ?? "";
|
|
1111
1454
|
while (drainQuietMs > 0 && Date.now() - drainStart < drainMaxMs) {
|
|
1112
1455
|
await delay(drainQuietMs);
|
|
1113
1456
|
const next = buffers.get(sessionId) ?? "";
|
|
1114
|
-
if (next === drained && next.length > 0)
|
|
1457
|
+
if (next === drained && (next.length > 0 || (flushedCounts.get(sessionId) ?? 0) > 0))
|
|
1458
|
+
break;
|
|
1115
1459
|
drained = next;
|
|
1116
1460
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
const content = {
|
|
1121
|
-
msgtype: "m.text",
|
|
1122
|
-
body: text
|
|
1123
|
-
};
|
|
1124
|
-
if (html) {
|
|
1125
|
-
const escapedPlain = "<p>" + text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") + "</p>";
|
|
1126
|
-
const norm = (s) => s.replace(/\s+/g, " ").trim();
|
|
1127
|
-
if (norm(html) !== norm(escapedPlain)) {
|
|
1128
|
-
content.format = "org.matrix.custom.html";
|
|
1129
|
-
content.formatted_body = html;
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
await client.sendMessage({
|
|
1133
|
-
roomId: evt.room_id,
|
|
1134
|
-
asUserId: agent.userId,
|
|
1135
|
-
content,
|
|
1136
|
-
threadRoot
|
|
1137
|
-
// every reply threads, full stop
|
|
1138
|
-
});
|
|
1139
|
-
} else {
|
|
1461
|
+
flushBuffer(sessionId);
|
|
1462
|
+
await (sendQueue.get(sessionId) ?? Promise.resolve());
|
|
1463
|
+
if ((flushedCounts.get(sessionId) ?? 0) === 0) {
|
|
1140
1464
|
console.warn(
|
|
1141
1465
|
`[matrix:${agent.name}] turn finished with empty buffer (session=${sessionId}); nothing sent to ${evt.room_id}`
|
|
1142
1466
|
);
|
|
@@ -1147,6 +1471,8 @@ function createMatrixTransport(opts) {
|
|
|
1147
1471
|
await safePresence("online");
|
|
1148
1472
|
buffers.delete(sessionId);
|
|
1149
1473
|
bufferMessageIds.delete(sessionId);
|
|
1474
|
+
flushedCounts.delete(sessionId);
|
|
1475
|
+
sendQueue.delete(sessionId);
|
|
1150
1476
|
}
|
|
1151
1477
|
}
|
|
1152
1478
|
return {
|
|
@@ -1232,7 +1558,7 @@ async function publishWorkforce(opts) {
|
|
|
1232
1558
|
await opts.client.sendStateEvent({
|
|
1233
1559
|
roomId: opts.spaceRoomId,
|
|
1234
1560
|
asUserId: opts.asUserId,
|
|
1235
|
-
eventType: "
|
|
1561
|
+
eventType: "dev.zooid.workforce",
|
|
1236
1562
|
stateKey: "",
|
|
1237
1563
|
content: buildWorkforceRoster(opts.agents)
|
|
1238
1564
|
});
|
|
@@ -1249,17 +1575,27 @@ async function startWorkforcePublisher(opts) {
|
|
|
1249
1575
|
}
|
|
1250
1576
|
export {
|
|
1251
1577
|
BotPool,
|
|
1578
|
+
INLINE_IMAGE_MIMES,
|
|
1579
|
+
MAX_DOWNLOAD_BYTES,
|
|
1580
|
+
MAX_INLINE_IMAGE_BYTES,
|
|
1581
|
+
MAX_MEDIA_PER_TURN,
|
|
1582
|
+
MEDIA_MSGTYPES,
|
|
1252
1583
|
MatrixClient,
|
|
1253
1584
|
MatrixContextProvider,
|
|
1585
|
+
MediaClient,
|
|
1586
|
+
PendingMediaStore,
|
|
1254
1587
|
buildWorkforceRoster,
|
|
1255
1588
|
createMatrixTransport,
|
|
1256
1589
|
ensureDefaultChannel,
|
|
1257
1590
|
ensureWorkforceSpace,
|
|
1258
1591
|
extractMentions,
|
|
1592
|
+
isMediaMsgtype,
|
|
1593
|
+
parseMxcUri,
|
|
1259
1594
|
publishWorkforce,
|
|
1260
1595
|
renderRegistration,
|
|
1261
1596
|
route,
|
|
1262
1597
|
serverNameFromMxid,
|
|
1263
|
-
startWorkforcePublisher
|
|
1598
|
+
startWorkforcePublisher,
|
|
1599
|
+
writeAttachment
|
|
1264
1600
|
};
|
|
1265
1601
|
//# sourceMappingURL=index.js.map
|