@zooid/transport-matrix 0.12.0 → 0.13.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
@@ -177,7 +177,18 @@ declare class MatrixClient {
177
177
 
178
178
  interface MatrixContextProviderOpts {
179
179
  client: MatrixClient;
180
- /** AS sender_localpart user (read access). */
180
+ /**
181
+ * The **agent's own** Matrix user (`@{workstation}.{name}:server`), which
182
+ * every read is impersonated as via `?user_id=`. Not the AS bot: that would
183
+ * read every room on the homeserver and make this class the only thing
184
+ * standing between an agent and someone else's conversation.
185
+ *
186
+ * This is load-bearing. It is what makes the homeserver — not our own
187
+ * bookkeeping — the authorization boundary for context reads, so a room or
188
+ * thread the agent is not in fails at Matrix with 403/404. Anything that
189
+ * widens who can name a room (a CLI, a new tool parameter) is safe only
190
+ * while this holds.
191
+ */
181
192
  asUserId: string;
182
193
  /** Map of Matrix user IDs → agent names, for is_agent / agent_name flags. */
183
194
  agentBots: Map<string, string>;
package/dist/index.js CHANGED
@@ -331,7 +331,8 @@ var MatrixContextProvider = class {
331
331
  const threads = [];
332
332
  for (const ev of chunk) {
333
333
  if (ev.type !== "m.room.message") continue;
334
- if (ev.content?.msgtype !== "m.text" || typeof ev.content.body !== "string") continue;
334
+ if (ev.content?.msgtype !== "m.text" && ev.content?.msgtype !== "m.notice" || typeof ev.content.body !== "string")
335
+ continue;
335
336
  const relatesTo = ev.content["m.relates_to"];
336
337
  if (relatesTo?.rel_type === "m.thread") continue;
337
338
  const agent = this.opts.agentBots.get(ev.sender);
@@ -405,7 +406,7 @@ var MatrixContextProvider = class {
405
406
  ...threadId !== void 0 ? { thread_id: threadId } : {}
406
407
  };
407
408
  }
408
- if (msgtype !== "m.text" || typeof body !== "string") return null;
409
+ if (msgtype !== "m.text" && msgtype !== "m.notice" || typeof body !== "string") return null;
409
410
  return {
410
411
  id: ev.event_id,
411
412
  sender: ev.sender,
@@ -821,7 +822,11 @@ var RECOVERY_URLS = {
821
822
  function toErrorBody(evt, threadRoot) {
822
823
  const msg = evt.message.slice(0, 250);
823
824
  const out = {
824
- msgtype: "m.notice",
825
+ // No msgtype: dev.zooid.error is not m.room.message, so the field is
826
+ // meaningless here — it was a vestige of copying the message-body shape.
827
+ // Its presence used to force careful push-rule `before` positioning
828
+ // (ZNC025 §10); that positioning is kept regardless, since it also
829
+ // protects rules for event types that never carried the field.
825
830
  body: `\u26A0 [${evt.code}] ${msg}`,
826
831
  code: evt.code,
827
832
  message: msg,
@@ -836,6 +841,25 @@ function toErrorBody(evt, threadRoot) {
836
841
  if (recovery) out.recovery = recovery;
837
842
  return out;
838
843
  }
844
+ var PREVIEW_MAX = 140;
845
+ function toTurnEndBody(evt, threadRoot) {
846
+ const preview = evt.lastMessage?.trim().replace(/\s+/g, " ");
847
+ return {
848
+ // `body` stays the turn-boundary summary: it is what a generic Matrix
849
+ // client renders for this event, and the prose is already its own message
850
+ // in the timeline. The preview below exists only for the push, which
851
+ // cannot see that message — agent prose is `m.notice`, deliberately
852
+ // silenced by `.m.rule.suppress_notices` so a chatty turn doesn't fire one
853
+ // push per chunk ([[ZNC025]] §10). Without it the only notification the
854
+ // user gets says an agent finished and nothing about what it said.
855
+ body: evt.producedOutput ? `${evt.agentId} finished` : `${evt.agentId} finished without output`,
856
+ ...preview ? { last_message: preview.slice(0, PREVIEW_MAX) } : {},
857
+ agent_id: evt.agentId,
858
+ session_id: evt.sessionId,
859
+ produced_output: evt.producedOutput,
860
+ "m.relates_to": { rel_type: "m.thread", event_id: threadRoot }
861
+ };
862
+ }
839
863
 
840
864
  // src/transport.ts
841
865
  import { classify } from "@zooid/acp-client";
@@ -1153,7 +1177,11 @@ function createMatrixTransport(opts) {
1153
1177
  const pendingCommands = /* @__PURE__ */ new Map();
1154
1178
  const buildTextContent = (text) => {
1155
1179
  const content = {
1156
- msgtype: "m.text",
1180
+ // m.notice, not m.text: .m.rule.suppress_notices silences the
1181
+ // chunk-storm of agent prose server-side (ZNC025 §10) instead of every
1182
+ // client having to filter it. dev.zooid.error carries the same tweak
1183
+ // for the same reason.
1184
+ msgtype: "m.notice",
1157
1185
  body: text
1158
1186
  };
1159
1187
  const html = toMatrixHtml(text);
@@ -1167,11 +1195,13 @@ function createMatrixTransport(opts) {
1167
1195
  }
1168
1196
  return content;
1169
1197
  };
1198
+ const lastFlushed = /* @__PURE__ */ new Map();
1170
1199
  const flushBuffer = (sessionId) => {
1171
1200
  const ctx = sessions.get(sessionId);
1172
1201
  const text = buffers.get(sessionId) ?? "";
1173
1202
  if (!ctx || text.length === 0) return false;
1174
1203
  buffers.set(sessionId, "");
1204
+ lastFlushed.set(sessionId, text);
1175
1205
  flushedCounts.set(sessionId, (flushedCounts.get(sessionId) ?? 0) + 1);
1176
1206
  const content = buildTextContent(text);
1177
1207
  const tail = (sendQueue.get(sessionId) ?? Promise.resolve()).then(async () => {
@@ -1584,19 +1614,30 @@ function createMatrixTransport(opts) {
1584
1614
  drained = next;
1585
1615
  }
1586
1616
  flushBuffer(sessionId);
1617
+ } finally {
1618
+ clearInterval(refresh);
1619
+ await safeTyping(false);
1620
+ await safePresence("online");
1587
1621
  await (sendQueue.get(sessionId) ?? Promise.resolve());
1588
- if ((flushedCounts.get(sessionId) ?? 0) === 0) {
1622
+ const producedOutput = (flushedCounts.get(sessionId) ?? 0) > 0;
1623
+ if (!producedOutput) {
1589
1624
  console.warn(
1590
1625
  `[matrix:${agent.name}] turn finished with empty buffer (session=${sessionId}); nothing sent to ${evt.room_id}`
1591
1626
  );
1592
1627
  }
1593
- } finally {
1594
- clearInterval(refresh);
1595
- await safeTyping(false);
1596
- await safePresence("online");
1628
+ await client.sendCustomEvent({
1629
+ roomId: evt.room_id,
1630
+ asUserId: agent.userId,
1631
+ eventType: "dev.zooid.turn.end",
1632
+ content: toTurnEndBody(
1633
+ { agentId: agent.name, sessionId, producedOutput, lastMessage: lastFlushed.get(sessionId) },
1634
+ threadRoot
1635
+ )
1636
+ }).catch((e) => console.warn(`[matrix:${agent.name}] turn.end send failed:`, e));
1597
1637
  buffers.delete(sessionId);
1598
1638
  bufferMessageIds.delete(sessionId);
1599
1639
  flushedCounts.delete(sessionId);
1640
+ lastFlushed.delete(sessionId);
1600
1641
  sendQueue.delete(sessionId);
1601
1642
  }
1602
1643
  }