zooid 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.
@@ -23383,6 +23383,17 @@ function acpUpdateToAgentEvent(notif) {
23383
23383
  };
23384
23384
  case "plan":
23385
23385
  return { type: "plan", sessionId, entries: update.entries };
23386
+ case "available_commands_update": {
23387
+ const u = update;
23388
+ return {
23389
+ type: "available_commands",
23390
+ sessionId,
23391
+ commands: (u.availableCommands ?? []).map((c) => ({
23392
+ name: c.name,
23393
+ description: c.description
23394
+ }))
23395
+ };
23396
+ }
23386
23397
  default:
23387
23398
  return null;
23388
23399
  }
@@ -24900,6 +24911,18 @@ var MatrixClient = class {
24900
24911
  }
24901
24912
  throw new Error(`invite(${opts.targetUserId}) failed: ${r.status}`);
24902
24913
  }
24914
+ async leaveRoom(roomId, asUserId, opts) {
24915
+ const url2 = `${this.homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/leave?user_id=${encodeURIComponent(asUserId)}`;
24916
+ const r = await this.fetch(url2, {
24917
+ method: "POST",
24918
+ headers: {
24919
+ Authorization: `Bearer ${this.asToken}`,
24920
+ "content-type": "application/json"
24921
+ },
24922
+ body: JSON.stringify(opts?.reason ? { reason: opts.reason } : {})
24923
+ });
24924
+ if (!r.ok) throw new Error(`leaveRoom(${roomId}, ${asUserId}) failed: ${r.status}`);
24925
+ }
24903
24926
  async joinRoom(roomIdOrAlias, asUserId) {
24904
24927
  const url2 = `${this.homeserver}/_matrix/client/v3/join/${encodeURIComponent(roomIdOrAlias)}?user_id=${encodeURIComponent(asUserId)}`;
24905
24928
  const r = await this.fetch(url2, {
@@ -25134,14 +25157,29 @@ var MatrixContextProvider = class {
25134
25157
  }
25135
25158
  toMessage(ev) {
25136
25159
  if (ev.type !== "m.room.message") return null;
25137
- if (ev.content?.msgtype !== "m.text" || typeof ev.content.body !== "string") return null;
25160
+ const msgtype = ev.content?.msgtype;
25161
+ const body = ev.content?.body;
25138
25162
  const agent = this.opts.agentBots.get(ev.sender);
25139
- const relatesTo = ev.content["m.relates_to"];
25163
+ const relatesTo = ev.content?.["m.relates_to"];
25140
25164
  const threadId = relatesTo?.rel_type === "m.thread" && relatesTo.event_id ? relatesTo.event_id : void 0;
25165
+ if (msgtype === "m.image" || msgtype === "m.file" || msgtype === "m.video" || msgtype === "m.audio") {
25166
+ const kind = msgtype.slice(2);
25167
+ const name = typeof body === "string" && body ? body : "untitled";
25168
+ return {
25169
+ id: ev.event_id,
25170
+ sender: ev.sender,
25171
+ text: `[${kind}: ${name}]`,
25172
+ timestamp: new Date(ev.origin_server_ts).toISOString(),
25173
+ is_agent: agent !== void 0,
25174
+ ...agent !== void 0 ? { agent_name: agent } : {},
25175
+ ...threadId !== void 0 ? { thread_id: threadId } : {}
25176
+ };
25177
+ }
25178
+ if (msgtype !== "m.text" || typeof body !== "string") return null;
25141
25179
  return {
25142
25180
  id: ev.event_id,
25143
25181
  sender: ev.sender,
25144
- text: ev.content.body,
25182
+ text: body,
25145
25183
  timestamp: new Date(ev.origin_server_ts).toISOString(),
25146
25184
  is_agent: agent !== void 0,
25147
25185
  ...agent !== void 0 ? { agent_name: agent } : {},
@@ -25215,6 +25253,10 @@ function stripMention(body, userId) {
25215
25253
  }
25216
25254
 
25217
25255
  // ../transport-matrix/src/router.ts
25256
+ var MEDIA_MSGTYPES = /* @__PURE__ */ new Set(["m.image", "m.file", "m.video", "m.audio"]);
25257
+ function isMediaMsgtype(t) {
25258
+ return t !== void 0 && MEDIA_MSGTYPES.has(t);
25259
+ }
25218
25260
  function inboundThreadRoot(event) {
25219
25261
  const r = event.content?.["m.relates_to"];
25220
25262
  return r?.rel_type === "m.thread" && r.event_id ? r.event_id : void 0;
@@ -25222,6 +25264,7 @@ function inboundThreadRoot(event) {
25222
25264
  function route(event, agents, threadStates) {
25223
25265
  if (event.type !== "m.room.message") return [];
25224
25266
  if (!event.content?.msgtype) return [];
25267
+ if (isMediaMsgtype(event.content.msgtype)) return [];
25225
25268
  const mentions = new Set(extractMentions(event));
25226
25269
  const matches = [];
25227
25270
  const threadRoot = inboundThreadRoot(event);
@@ -25260,10 +25303,12 @@ async function ensureWorkforceSpace(opts) {
25260
25303
  name: display,
25261
25304
  preset: opts.preset,
25262
25305
  creation_content: { type: "m.space" },
25263
- // A workspace is joined by invitation, not self-service. Pin the space's
25264
- // join rule to invite regardless of preset so it can't be walked into
25265
- // (which would otherwise satisfy every restricted child room's allow).
25266
- initial_state: [{ type: "m.room.join_rules", state_key: "", content: { join_rule: "invite" } }]
25306
+ // Pin the join rule regardless of preset. Defaults to invite so the space
25307
+ // can't be walked into (which would otherwise satisfy every restricted
25308
+ // child room's allow); `zooid dev` opts into `public` for local-only use.
25309
+ initial_state: [
25310
+ { type: "m.room.join_rules", state_key: "", content: { join_rule: opts.joinRule ?? "invite" } }
25311
+ ]
25267
25312
  };
25268
25313
  if (opts.admins && opts.admins.length > 0) {
25269
25314
  body.invite = opts.admins;
@@ -25482,6 +25527,15 @@ function toPlanBody(evt) {
25482
25527
  entries: evt.entries
25483
25528
  };
25484
25529
  }
25530
+ function toAvailableCommandsBody(evt) {
25531
+ return {
25532
+ session_id: evt.sessionId,
25533
+ available_commands: evt.commands.map((c) => ({
25534
+ name: c.name,
25535
+ description: c.description
25536
+ }))
25537
+ };
25538
+ }
25485
25539
  var RECOVERY_URLS = {
25486
25540
  auth_missing: "https://zooid.dev/docs/guides/run-in-container#authentication-that-carries-over",
25487
25541
  auth_invalid: "https://zooid.dev/docs/guides/run-in-container#authentication-that-carries-over",
@@ -25576,8 +25630,180 @@ function toMatrixHtml(markdown) {
25576
25630
  });
25577
25631
  }
25578
25632
 
25633
+ // ../transport-matrix/src/pending-media.ts
25634
+ var MAX_MEDIA_PER_TURN = 8;
25635
+ var PendingMediaStore = class {
25636
+ queues = /* @__PURE__ */ new Map();
25637
+ key(roomId, threadKey) {
25638
+ return `${roomId} ${threadKey ?? "main"}`;
25639
+ }
25640
+ add(roomId, threadKey, item) {
25641
+ const k = this.key(roomId, threadKey);
25642
+ const q = this.queues.get(k) ?? [];
25643
+ q.push(item);
25644
+ while (q.length > MAX_MEDIA_PER_TURN) q.shift();
25645
+ this.queues.set(k, q);
25646
+ }
25647
+ drain(roomId, threadKey, sender) {
25648
+ const k = this.key(roomId, threadKey);
25649
+ const q = this.queues.get(k) ?? [];
25650
+ const mine = q.filter((i) => i.sender === sender);
25651
+ const rest = q.filter((i) => i.sender !== sender);
25652
+ if (rest.length) this.queues.set(k, rest);
25653
+ else this.queues.delete(k);
25654
+ return mine;
25655
+ }
25656
+ };
25657
+
25658
+ // ../transport-matrix/src/media-client.ts
25659
+ var MAX_INLINE_IMAGE_BYTES = 524288;
25660
+ var INLINE_IMAGE_MIMES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
25661
+ var MAX_DOWNLOAD_BYTES = 33554432;
25662
+ function parseMxcUri(uri) {
25663
+ const m = /^mxc:\/\/([^/]+)\/(.+)$/.exec(uri);
25664
+ return m ? { serverName: m[1], mediaId: m[2] } : null;
25665
+ }
25666
+ var MediaClient = class {
25667
+ homeserver;
25668
+ asToken;
25669
+ fetch;
25670
+ constructor(opts) {
25671
+ this.homeserver = opts.homeserver.replace(/\/$/, "");
25672
+ this.asToken = opts.asToken;
25673
+ this.fetch = opts.fetch ?? globalThis.fetch;
25674
+ }
25675
+ async download(input) {
25676
+ const parsed = parseMxcUri(input.mxcUri);
25677
+ if (!parsed) throw new Error(`not an mxc uri: ${input.mxcUri}`);
25678
+ const url2 = `${this.homeserver}/_matrix/client/v1/media/download/${encodeURIComponent(parsed.serverName)}/${encodeURIComponent(parsed.mediaId)}?user_id=${encodeURIComponent(input.asUserId)}`;
25679
+ const r = await this.fetch(url2, {
25680
+ headers: { Authorization: `Bearer ${this.asToken}` }
25681
+ });
25682
+ if (!r.ok) throw new Error(`media download failed: ${r.status}`);
25683
+ const buf = new Uint8Array(await r.arrayBuffer());
25684
+ const max = input.maxBytes ?? MAX_DOWNLOAD_BYTES;
25685
+ if (buf.byteLength > max) {
25686
+ throw new Error(`media too large: ${buf.byteLength} > ${max}`);
25687
+ }
25688
+ return { data: buf, contentType: r.headers.get("content-type") ?? "application/octet-stream" };
25689
+ }
25690
+ async upload(input) {
25691
+ const params = new URLSearchParams();
25692
+ if (input.filename) params.set("filename", input.filename);
25693
+ params.set("user_id", input.asUserId);
25694
+ const r = await this.fetch(`${this.homeserver}/_matrix/media/v3/upload?${params}`, {
25695
+ method: "POST",
25696
+ headers: { Authorization: `Bearer ${this.asToken}`, "Content-Type": input.contentType },
25697
+ body: input.data
25698
+ });
25699
+ if (!r.ok) throw new Error(`media upload failed: ${r.status}`);
25700
+ return await r.json();
25701
+ }
25702
+ };
25703
+
25704
+ // ../transport-matrix/src/attachments.ts
25705
+ import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
25706
+ import { join as join4 } from "path";
25707
+ import { posix } from "path";
25708
+ function sanitize(s, fallback) {
25709
+ const cleaned = s.replace(/[^A-Za-z0-9._-]/g, "").replace(/^\.+/, "");
25710
+ return cleaned || fallback;
25711
+ }
25712
+ function writeAttachment(input) {
25713
+ const dir = sanitize(input.eventId, "event");
25714
+ const name = sanitize(input.filename, "file");
25715
+ const hostDir = join4(input.workspaceDir, ".zooid", "attachments", dir);
25716
+ mkdirSync2(hostDir, { recursive: true });
25717
+ const hostPath = join4(hostDir, name);
25718
+ writeFileSync(hostPath, input.data);
25719
+ const agentPath = posix.join(input.agentWorkspacePath, ".zooid", "attachments", dir, name);
25720
+ return { hostPath, agentPath };
25721
+ }
25722
+
25579
25723
  // ../transport-matrix/src/transport.ts
25580
25724
  var STARTUP_GRACE_MS = 5e3;
25725
+ async function buildMediaBlocks(items, opts) {
25726
+ const blocks = [];
25727
+ const pathLines = [];
25728
+ if (!opts.media || items.length === 0) return { blocks, pathLines };
25729
+ for (const item of items) {
25730
+ try {
25731
+ 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);
25732
+ if (isInlineCandidate) {
25733
+ const { data, contentType } = await opts.media.download({
25734
+ mxcUri: item.url,
25735
+ asUserId: opts.agent.userId
25736
+ });
25737
+ if (data.byteLength <= MAX_INLINE_IMAGE_BYTES) {
25738
+ blocks.push({
25739
+ type: "image",
25740
+ data: Buffer.from(data).toString("base64"),
25741
+ mimeType: contentType
25742
+ });
25743
+ continue;
25744
+ }
25745
+ if (opts.agent.workspaceDir) {
25746
+ const paths = opts.writeAttachmentFn({
25747
+ workspaceDir: opts.agent.workspaceDir,
25748
+ agentWorkspacePath: opts.agent.agentWorkspacePath ?? opts.agent.workspaceDir,
25749
+ eventId: item.eventId,
25750
+ filename: item.filename ?? item.body,
25751
+ data
25752
+ });
25753
+ blocks.push({
25754
+ type: "resource_link",
25755
+ uri: `file://${paths.agentPath}`,
25756
+ name: item.filename ?? item.body
25757
+ });
25758
+ pathLines.push(`Attached file: ${paths.agentPath}`);
25759
+ }
25760
+ } else {
25761
+ if (!opts.agent.workspaceDir) continue;
25762
+ const { data } = await opts.media.download({
25763
+ mxcUri: item.url,
25764
+ asUserId: opts.agent.userId
25765
+ });
25766
+ const paths = opts.writeAttachmentFn({
25767
+ workspaceDir: opts.agent.workspaceDir,
25768
+ agentWorkspacePath: opts.agent.agentWorkspacePath ?? opts.agent.workspaceDir,
25769
+ eventId: item.eventId,
25770
+ filename: item.filename ?? item.body,
25771
+ data
25772
+ });
25773
+ blocks.push({
25774
+ type: "resource_link",
25775
+ uri: `file://${paths.agentPath}`,
25776
+ name: item.filename ?? item.body,
25777
+ mimeType: item.info?.mimetype,
25778
+ size: item.info?.size
25779
+ });
25780
+ pathLines.push(`Attached file: ${paths.agentPath}`);
25781
+ }
25782
+ } catch (err) {
25783
+ opts.onError(item, err);
25784
+ }
25785
+ }
25786
+ return { blocks, pathLines };
25787
+ }
25788
+ async function sendMediaError(ctx, _err, message, client) {
25789
+ await client.sendCustomEvent({
25790
+ roomId: ctx.roomId,
25791
+ asUserId: ctx.agent.userId,
25792
+ eventType: "dev.zooid.error",
25793
+ content: toErrorBody(
25794
+ {
25795
+ kind: "error",
25796
+ agentId: ctx.agent.name,
25797
+ sessionId: null,
25798
+ turnId: null,
25799
+ code: "media_failed",
25800
+ message: message.slice(0, 250),
25801
+ transient: false
25802
+ },
25803
+ ctx.threadRoot
25804
+ )
25805
+ }).catch((e) => console.warn(`[matrix:${ctx.agent.name}] dev.zooid.error send failed:`, e));
25806
+ }
25581
25807
  var SEEN_EVENT_CAP = 5e3;
25582
25808
  var DRAIN_QUIET_MS = 300;
25583
25809
  var DRAIN_MAX_MS = 3e4;
@@ -25587,10 +25813,18 @@ function inboundThreadRoot2(evt) {
25587
25813
  return r?.rel_type === "m.thread" && r.event_id ? r.event_id : void 0;
25588
25814
  }
25589
25815
  function createMatrixTransport(opts) {
25590
- const { agents, approvals, client, bindings, hsToken, adminUserId } = opts;
25816
+ const { agents, approvals, client, bindings, hsToken, adminUserId, botUserId } = opts;
25591
25817
  const drainQuietMs = opts.drainQuietMs ?? DRAIN_QUIET_MS;
25592
25818
  const drainMaxMs = opts.drainMaxMs ?? DRAIN_MAX_MS;
25819
+ const mediaClient = opts.media;
25820
+ const writeAttachmentFn = opts.writeAttachmentFn ?? writeAttachment;
25821
+ const pendingMedia = new PendingMediaStore();
25593
25822
  const pool = new BotPool(client, bindings);
25823
+ const ourBotUserIds = /* @__PURE__ */ new Set([
25824
+ ...botUserId ? [botUserId] : [],
25825
+ ...bindings.map((b) => b.userId)
25826
+ ]);
25827
+ 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.";
25594
25828
  const sessions = /* @__PURE__ */ new Map();
25595
25829
  const buffers = /* @__PURE__ */ new Map();
25596
25830
  const bufferMessageIds = /* @__PURE__ */ new Map();
@@ -25598,30 +25832,99 @@ function createMatrixTransport(opts) {
25598
25832
  const threadStates = /* @__PURE__ */ new Map();
25599
25833
  const cutoffTs = Date.now() - STARTUP_GRACE_MS;
25600
25834
  const seenEventIds = /* @__PURE__ */ new Set();
25835
+ const flushedCounts = /* @__PURE__ */ new Map();
25836
+ const pendingCommands = /* @__PURE__ */ new Map();
25837
+ const buildTextContent = (text) => {
25838
+ const content = {
25839
+ msgtype: "m.text",
25840
+ body: text
25841
+ };
25842
+ const html = toMatrixHtml(text);
25843
+ if (html) {
25844
+ const escapedPlain = "<p>" + text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;") + "</p>";
25845
+ const norm = (s) => s.replace(/\s+/g, " ").trim();
25846
+ if (norm(html) !== norm(escapedPlain)) {
25847
+ content.format = "org.matrix.custom.html";
25848
+ content.formatted_body = html;
25849
+ }
25850
+ }
25851
+ return content;
25852
+ };
25853
+ const flushBuffer = (sessionId) => {
25854
+ const ctx = sessions.get(sessionId);
25855
+ const text = buffers.get(sessionId) ?? "";
25856
+ if (!ctx || text.length === 0) return false;
25857
+ buffers.set(sessionId, "");
25858
+ flushedCounts.set(sessionId, (flushedCounts.get(sessionId) ?? 0) + 1);
25859
+ const content = buildTextContent(text);
25860
+ const tail = (sendQueue.get(sessionId) ?? Promise.resolve()).then(async () => {
25861
+ try {
25862
+ await client.sendMessage({
25863
+ roomId: ctx.roomId,
25864
+ asUserId: ctx.agent.userId,
25865
+ content,
25866
+ threadRoot: ctx.threadRoot
25867
+ });
25868
+ } catch (err) {
25869
+ console.warn(`[matrix:${ctx.agent.name}] sendMessage flush failed:`, err);
25870
+ }
25871
+ });
25872
+ sendQueue.set(sessionId, tail);
25873
+ return true;
25874
+ };
25601
25875
  agents.onEvent = async (name, event) => {
25602
25876
  const ctx = sessions.get(event.sessionId);
25603
25877
  if (!ctx) {
25604
- console.warn(`[matrix:${name}] no session ctx for ${event.sessionId}`);
25878
+ if (event.type === "available_commands") {
25879
+ pendingCommands.set(event.sessionId, event);
25880
+ } else {
25881
+ console.warn(`[matrix:${name}] no session ctx for ${event.sessionId}`);
25882
+ }
25605
25883
  return;
25606
25884
  }
25607
25885
  if (event.type === "agent_message_chunk") {
25608
25886
  const block = event.content;
25609
25887
  if (block.type === "text" && typeof block.text === "string") {
25610
- const current = buffers.get(event.sessionId) ?? "";
25611
25888
  const prevMessageId = bufferMessageIds.get(event.sessionId);
25612
25889
  const messageChanged = event.messageId !== void 0 && prevMessageId !== void 0 && event.messageId !== prevMessageId;
25613
- const needsBreak = current.length > 0 && (block.text === "" || messageChanged);
25614
- const prefix = needsBreak ? "\n\n" : "";
25615
- buffers.set(event.sessionId, current + prefix + block.text);
25616
25890
  if (event.messageId !== void 0)
25617
25891
  bufferMessageIds.set(event.sessionId, event.messageId);
25892
+ if (messageChanged) flushBuffer(event.sessionId);
25893
+ const current = buffers.get(event.sessionId) ?? "";
25894
+ const needsBreak = current.length > 0 && block.text === "";
25895
+ const prefix = needsBreak ? "\n\n" : "";
25896
+ buffers.set(event.sessionId, current + prefix + block.text);
25897
+ } else if (block.type === "image" && typeof block.data === "string" && typeof block.mimeType === "string" && mediaClient) {
25898
+ const ctx2 = sessions.get(event.sessionId);
25899
+ if (ctx2) {
25900
+ const bytes = Buffer.from(block.data, "base64");
25901
+ const ext = (block.mimeType.split("/")[1] ?? "png").replace(/[^a-z0-9]/gi, "");
25902
+ const filename = `image.${ext}`;
25903
+ void mediaClient.upload({ data: bytes, contentType: block.mimeType, filename, asUserId: ctx2.agent.userId }).then(
25904
+ ({ content_uri }) => client.sendMessage({
25905
+ roomId: ctx2.roomId,
25906
+ asUserId: ctx2.agent.userId,
25907
+ threadRoot: ctx2.threadRoot,
25908
+ content: {
25909
+ msgtype: "m.image",
25910
+ body: filename,
25911
+ url: content_uri,
25912
+ info: { mimetype: block.mimeType, size: bytes.length }
25913
+ }
25914
+ })
25915
+ ).catch((err) => {
25916
+ console.warn(`[matrix:${name}] outbound image upload failed:`, err);
25917
+ void sendMediaError(ctx2, err, "agent image upload failed", client);
25918
+ });
25919
+ }
25618
25920
  } else {
25619
25921
  console.warn(`[matrix:${name}] dropped chunk block type=${block.type}`, block);
25620
25922
  }
25621
25923
  return;
25622
25924
  }
25623
- const eventType = event.type === "tool_call" ? "eco.zoon.tool_call" : event.type === "tool_call_update" ? "eco.zoon.tool_call_update" : "eco.zoon.plan";
25624
- const body = event.type === "tool_call" ? toToolCallBody(event) : event.type === "tool_call_update" ? toUpdateBody(event) : toPlanBody(event);
25925
+ flushBuffer(event.sessionId);
25926
+ 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";
25927
+ const body = event.type === "tool_call" ? toToolCallBody(event) : event.type === "tool_call_update" ? toUpdateBody(event) : event.type === "available_commands" ? toAvailableCommandsBody(event) : toPlanBody(event);
25625
25928
  body["m.relates_to"] = { rel_type: "m.thread", event_id: ctx.threadRoot };
25626
25929
  const tail = (sendQueue.get(event.sessionId) ?? Promise.resolve()).then(async () => {
25627
25930
  try {
@@ -25660,7 +25963,7 @@ function createMatrixTransport(opts) {
25660
25963
  void client.sendCustomEvent({
25661
25964
  roomId: ctx.roomId,
25662
25965
  asUserId: ctx.agent.userId,
25663
- eventType: "eco.zoon.approval_request",
25966
+ eventType: "dev.zooid.approval_request",
25664
25967
  content
25665
25968
  });
25666
25969
  });
@@ -25694,20 +25997,33 @@ function createMatrixTransport(opts) {
25694
25997
  );
25695
25998
  continue;
25696
25999
  }
25697
- if (evt.type === "eco.zoon.session_reset") {
26000
+ if (evt.type === "m.room.member" && evt.content?.membership === "invite") {
26001
+ const target = evt.state_key;
26002
+ const inviter = evt.sender;
26003
+ if (target && evt.room_id && ourBotUserIds.has(target) && (!inviter || !ourBotUserIds.has(inviter))) {
26004
+ console.log(
26005
+ `[matrix] declining ad-hoc invite for ${target} in ${evt.room_id} from ${inviter ?? "unknown"}`
26006
+ );
26007
+ await client.leaveRoom(evt.room_id, target, { reason: DECLINE_REASON }).catch(
26008
+ (err) => console.warn(`[matrix] leaveRoom(${evt.room_id}, ${target}) failed:`, err)
26009
+ );
26010
+ }
26011
+ continue;
26012
+ }
26013
+ if (evt.type === "dev.zooid.session_reset") {
25698
26014
  const relates = evt.content?.["m.relates_to"];
25699
26015
  const threadRoot = relates?.rel_type === "m.thread" && relates.event_id ? relates.event_id : void 0;
25700
26016
  if (!threadRoot) {
25701
- console.log("[matrix] dropping eco.zoon.session_reset without thread relation");
26017
+ console.log("[matrix] dropping dev.zooid.session_reset without thread relation");
25702
26018
  continue;
25703
26019
  }
25704
- console.log(`[matrix] inbound eco.zoon.session_reset in ${evt.room_id} thread=${threadRoot}`);
26020
+ console.log(`[matrix] inbound dev.zooid.session_reset in ${evt.room_id} thread=${threadRoot}`);
25705
26021
  for (const a of bindings) {
25706
26022
  agents.endSession(a.name, threadRoot);
25707
26023
  }
25708
26024
  continue;
25709
26025
  }
25710
- if (evt.type === "eco.zoon.interrupt") {
26026
+ if (evt.type === "dev.zooid.interrupt") {
25711
26027
  const content = evt.content ?? {};
25712
26028
  const relates = evt.content?.["m.relates_to"];
25713
26029
  const threadRoot = relates?.rel_type === "m.thread" && relates.event_id ? relates.event_id : void 0;
@@ -25729,7 +26045,7 @@ function createMatrixTransport(opts) {
25729
26045
  continue;
25730
26046
  }
25731
26047
  if (!content.session_id) {
25732
- console.warn(`[matrix] eco.zoon.interrupt missing session_id (event_id=${evt.event_id})`);
26048
+ console.warn(`[matrix] dev.zooid.interrupt missing session_id (event_id=${evt.event_id})`);
25733
26049
  continue;
25734
26050
  }
25735
26051
  const ctx = sessions.get(content.session_id);
@@ -25744,7 +26060,7 @@ function createMatrixTransport(opts) {
25744
26060
  });
25745
26061
  continue;
25746
26062
  }
25747
- if (evt.type === "eco.zoon.approval_response") {
26063
+ if (evt.type === "dev.zooid.approval_response") {
25748
26064
  const content = evt.content ?? {};
25749
26065
  if (!content.session_id || !content.approval_id || !content.decision) continue;
25750
26066
  const decision = content.option_id ? { decision: content.decision, optionId: content.option_id } : { decision: content.decision };
@@ -25757,6 +26073,18 @@ function createMatrixTransport(opts) {
25757
26073
  continue;
25758
26074
  }
25759
26075
  logInbound(evt);
26076
+ 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)) {
26077
+ pendingMedia.add(evt.room_id, inboundThreadRoot2(evt), {
26078
+ eventId: evt.event_id,
26079
+ sender: evt.sender,
26080
+ msgtype: evt.content.msgtype,
26081
+ body: evt.content.body ?? "",
26082
+ filename: evt.content.filename,
26083
+ url: evt.content.url,
26084
+ info: evt.content.info
26085
+ });
26086
+ continue;
26087
+ }
25760
26088
  const promotedRoot = inboundThreadRoot2(evt) ?? evt.event_id;
25761
26089
  const inboundRel = inboundThreadRoot2(evt);
25762
26090
  if (evt.type === "m.room.message" && inboundRel && !threadStates.has(inboundRel) && evt.room_id) {
@@ -25822,9 +26150,9 @@ function createMatrixTransport(opts) {
25822
26150
  void client.sendCustomEvent({
25823
26151
  roomId: evt.room_id,
25824
26152
  asUserId: a.userId,
25825
- eventType: "eco.zoon.error",
26153
+ eventType: "dev.zooid.error",
25826
26154
  content: body2
25827
- }).catch((e) => console.warn(`[matrix:${a.name}] eco.zoon.error send failed:`, e));
26155
+ }).catch((e) => console.warn(`[matrix:${a.name}] dev.zooid.error send failed:`, e));
25828
26156
  });
25829
26157
  }
25830
26158
  }
@@ -25858,6 +26186,12 @@ function createMatrixTransport(opts) {
25858
26186
  sessions.set(sessionId, { agent, roomId: evt.room_id, threadRoot });
25859
26187
  buffers.set(sessionId, "");
25860
26188
  bufferMessageIds.delete(sessionId);
26189
+ flushedCounts.set(sessionId, 0);
26190
+ const stashedCommands = pendingCommands.get(sessionId);
26191
+ if (stashedCommands) {
26192
+ pendingCommands.delete(sessionId);
26193
+ void agents.onEvent?.(agent.name, stashedCommands);
26194
+ }
25861
26195
  const roomId = evt.room_id;
25862
26196
  const TYPING_TTL_MS = 3e4;
25863
26197
  const TYPING_REFRESH_MS = 25e3;
@@ -25873,42 +26207,43 @@ function createMatrixTransport(opts) {
25873
26207
  try {
25874
26208
  const rawBody = evt.content?.body ?? "";
25875
26209
  const promptText = stripMention(rawBody, agent.userId);
26210
+ const pendingItems = pendingMedia.drain(
26211
+ evt.room_id,
26212
+ inboundThreadRoot2(evt),
26213
+ evt.sender ?? ""
26214
+ );
26215
+ const { blocks, pathLines } = await buildMediaBlocks(pendingItems, {
26216
+ agent,
26217
+ media: mediaClient,
26218
+ writeAttachmentFn,
26219
+ onError: (item, err) => {
26220
+ console.warn(`[matrix:${agent.name}] media_failed for ${item.body}:`, err);
26221
+ void sendMediaError(
26222
+ { agent, roomId: evt.room_id, threadRoot },
26223
+ err,
26224
+ `Could not process attachment: ${item.body}`,
26225
+ client
26226
+ );
26227
+ }
26228
+ });
26229
+ const fullPromptText = [promptText, ...pathLines].filter(Boolean).join("\n");
25876
26230
  await agents.prompt(agent.name, {
25877
26231
  threadId: sessionKey,
25878
26232
  channelId: evt.room_id,
25879
- content: [{ type: "text", text: promptText }]
26233
+ content: [...blocks, { type: "text", text: fullPromptText }]
25880
26234
  });
25881
26235
  const drainStart = Date.now();
25882
26236
  let drained = buffers.get(sessionId) ?? "";
25883
26237
  while (drainQuietMs > 0 && Date.now() - drainStart < drainMaxMs) {
25884
26238
  await delay(drainQuietMs);
25885
26239
  const next = buffers.get(sessionId) ?? "";
25886
- if (next === drained && next.length > 0) break;
26240
+ if (next === drained && (next.length > 0 || (flushedCounts.get(sessionId) ?? 0) > 0))
26241
+ break;
25887
26242
  drained = next;
25888
26243
  }
25889
- const text = buffers.get(sessionId) ?? "";
25890
- if (text.length > 0) {
25891
- const html = toMatrixHtml(text);
25892
- const content = {
25893
- msgtype: "m.text",
25894
- body: text
25895
- };
25896
- if (html) {
25897
- const escapedPlain = "<p>" + text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;") + "</p>";
25898
- const norm = (s) => s.replace(/\s+/g, " ").trim();
25899
- if (norm(html) !== norm(escapedPlain)) {
25900
- content.format = "org.matrix.custom.html";
25901
- content.formatted_body = html;
25902
- }
25903
- }
25904
- await client.sendMessage({
25905
- roomId: evt.room_id,
25906
- asUserId: agent.userId,
25907
- content,
25908
- threadRoot
25909
- // every reply threads, full stop
25910
- });
25911
- } else {
26244
+ flushBuffer(sessionId);
26245
+ await (sendQueue.get(sessionId) ?? Promise.resolve());
26246
+ if ((flushedCounts.get(sessionId) ?? 0) === 0) {
25912
26247
  console.warn(
25913
26248
  `[matrix:${agent.name}] turn finished with empty buffer (session=${sessionId}); nothing sent to ${evt.room_id}`
25914
26249
  );
@@ -25919,6 +26254,8 @@ function createMatrixTransport(opts) {
25919
26254
  await safePresence("online");
25920
26255
  buffers.delete(sessionId);
25921
26256
  bufferMessageIds.delete(sessionId);
26257
+ flushedCounts.delete(sessionId);
26258
+ sendQueue.delete(sessionId);
25922
26259
  }
25923
26260
  }
25924
26261
  return {
@@ -26004,7 +26341,7 @@ async function publishWorkforce(opts) {
26004
26341
  await opts.client.sendStateEvent({
26005
26342
  roomId: opts.spaceRoomId,
26006
26343
  asUserId: opts.asUserId,
26007
- eventType: "eco.zoon.workforce",
26344
+ eventType: "dev.zooid.workforce",
26008
26345
  stateKey: "",
26009
26346
  content: buildWorkforceRoster(opts.agents)
26010
26347
  });
@@ -31591,10 +31928,11 @@ export {
31591
31928
  renderRegistration,
31592
31929
  ensureWorkforceSpace,
31593
31930
  ensureDefaultChannel,
31931
+ MediaClient,
31594
31932
  createMatrixTransport,
31595
31933
  startWorkforcePublisher,
31596
31934
  SpawnRegistry,
31597
31935
  startDaemonSocketServer,
31598
31936
  buildAcpRegistry
31599
31937
  };
31600
- //# sourceMappingURL=chunk-OKGNOROJ.js.map
31938
+ //# sourceMappingURL=chunk-LOILUENL.js.map