zooid 0.14.0 → 0.15.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.
@@ -24368,14 +24368,13 @@ var AcpClient = class {
24368
24368
  );
24369
24369
  const persisted = this.store?.get(threadId);
24370
24370
  if (persisted && this.agentCapabilities.loadSession) {
24371
+ let loaded;
24371
24372
  try {
24372
- await this.connection.loadSession({
24373
+ loaded = await this.connection.loadSession({
24373
24374
  sessionId: persisted,
24374
24375
  cwd: pathResolve(this.options.agent.cwd ?? process.cwd()),
24375
24376
  mcpServers
24376
24377
  });
24377
- this.sessions.set(key, { sessionId: persisted, startedAt: Date.now() });
24378
- return persisted;
24379
24378
  } catch (err) {
24380
24379
  console.warn(
24381
24380
  `[acp-client:${this.options.agent.id}] loadSession(${persisted}) failed; falling back to newSession:`,
@@ -24383,15 +24382,43 @@ var AcpClient = class {
24383
24382
  );
24384
24383
  await this.store?.delete(threadId);
24385
24384
  }
24385
+ if (loaded) {
24386
+ await this.applyMode(persisted, loaded.modes);
24387
+ this.sessions.set(key, { sessionId: persisted, startedAt: Date.now() });
24388
+ return persisted;
24389
+ }
24386
24390
  }
24387
- const { sessionId } = await this.connection.newSession({
24391
+ const { sessionId, modes } = await this.connection.newSession({
24388
24392
  cwd: pathResolve(this.options.agent.cwd ?? process.cwd()),
24389
24393
  mcpServers
24390
24394
  });
24395
+ await this.applyMode(sessionId, modes);
24391
24396
  this.sessions.set(key, { sessionId, startedAt: Date.now() });
24392
24397
  await this.store?.set(threadId, sessionId);
24393
24398
  return sessionId;
24394
24399
  }
24400
+ /**
24401
+ * Put a freshly created or loaded session into the agent's configured mode.
24402
+ * Mode ids are adapter-defined, so an id the adapter doesn't list is a
24403
+ * config error: fail the session rather than run it in a mode nobody chose.
24404
+ */
24405
+ async applyMode(sessionId, modes) {
24406
+ const wanted = this.options.agent.mode;
24407
+ if (!wanted || !this.connection) return;
24408
+ if (!modes) {
24409
+ throw new Error(
24410
+ `agents.${this.options.agent.id}.acp.mode "${wanted}": this agent does not offer session modes`
24411
+ );
24412
+ }
24413
+ if (!modes.availableModes.some((m) => m.id === wanted)) {
24414
+ const offered = modes.availableModes.map((m) => m.id).join(", ");
24415
+ throw new Error(
24416
+ `agents.${this.options.agent.id}.acp.mode "${wanted}": not offered by this agent (offers: ${offered})`
24417
+ );
24418
+ }
24419
+ if (modes.currentModeId === wanted) return;
24420
+ await this.connection.setSessionMode({ sessionId, modeId: wanted });
24421
+ }
24395
24422
  async ensureStoreLoaded() {
24396
24423
  if (!this.store) {
24397
24424
  if (!this.options.agentDataDir) {
@@ -43471,6 +43498,13 @@ function parseAcpBlock(name, raw) {
43471
43498
  `agents.${name}.acp: must specify either preset or command`
43472
43499
  );
43473
43500
  }
43501
+ let mode;
43502
+ if (a.mode !== void 0) {
43503
+ if (typeof a.mode !== "string" || a.mode.trim().length === 0) {
43504
+ throw new Error(`agents.${name}.acp.mode: must be a non-empty string`);
43505
+ }
43506
+ mode = a.mode.trim();
43507
+ }
43474
43508
  if (hasPreset) {
43475
43509
  if (typeof a.preset !== "string" || a.preset.length === 0) {
43476
43510
  throw new Error(`agents.${name}.acp.preset: must be a non-empty string`);
@@ -43487,6 +43521,7 @@ function parseAcpBlock(name, raw) {
43487
43521
  }
43488
43522
  out.model = a.model.trim();
43489
43523
  }
43524
+ if (mode !== void 0) out.mode = mode;
43490
43525
  return out;
43491
43526
  }
43492
43527
  if (typeof a.command !== "string" || a.command.length === 0) {
@@ -43504,7 +43539,7 @@ function parseAcpBlock(name, raw) {
43504
43539
  args.push(v);
43505
43540
  }
43506
43541
  }
43507
- return { command: a.command, args };
43542
+ return mode === void 0 ? { command: a.command, args } : { command: a.command, args, mode };
43508
43543
  }
43509
43544
  function parseApprovalTimeout(name, raw) {
43510
43545
  if (raw === void 0) return 0;
@@ -44188,8 +44223,17 @@ function parseTriggers(raw, agents, transports, validateCron, processEnv, config
44188
44223
  }
44189
44224
  rawMessages = t.messages;
44190
44225
  }
44226
+ let ttlMs;
44227
+ if (t.expires_after !== void 0) {
44228
+ if (typeof t.expires_after !== "string" || t.expires_after.length === 0) {
44229
+ throw new Error(`triggers.${name}.expires_after: must be a non-empty duration string`);
44230
+ }
44231
+ ttlMs = parseDurationMs(`triggers.${name}.expires_after`, t.expires_after);
44232
+ } else {
44233
+ ttlMs = webhook ? void 0 : 6 * 36e5;
44234
+ }
44191
44235
  const messages = rawMessages.map(
44192
- (m, i) => parseTriggerMessage(name, i, m, agents, as, !!webhook)
44236
+ (m, i) => parseTriggerMessage(name, i, m, agents, transports, as, !!webhook, ttlMs)
44193
44237
  );
44194
44238
  const entry = { as, messages };
44195
44239
  if (schedule !== void 0) entry.schedule = schedule;
@@ -44198,7 +44242,34 @@ function parseTriggers(raw, agents, transports, validateCron, processEnv, config
44198
44242
  }
44199
44243
  return result;
44200
44244
  }
44201
- function parseTriggerMessage(name, index, raw, agents, as, hasWebhook) {
44245
+ function parseDurationMs(label, raw) {
44246
+ const m = /^(\d+)(s|m|h)$/.exec(raw);
44247
+ if (!m) {
44248
+ throw new Error(`${label}: "${raw}" is not a valid duration (use "<n>s", "<n>m", or "<n>h")`);
44249
+ }
44250
+ const n = Number(m[1]);
44251
+ switch (m[2]) {
44252
+ case "s":
44253
+ return n * 1e3;
44254
+ case "m":
44255
+ return n * 6e4;
44256
+ case "h":
44257
+ return n * 60 * 6e4;
44258
+ }
44259
+ throw new Error("unreachable");
44260
+ }
44261
+ function soleMatrixTransport(transports) {
44262
+ const matrixTransports = Object.values(transports).filter(
44263
+ (t) => t.type === "matrix"
44264
+ );
44265
+ if (matrixTransports.length !== 1) {
44266
+ throw new Error(
44267
+ `triggers: a full-MXID mention requires exactly one matrix transport to validate its homeserver against (found ${matrixTransports.length})`
44268
+ );
44269
+ }
44270
+ return matrixTransports[0];
44271
+ }
44272
+ function parseTriggerMessage(name, index, raw, agents, transports, as, hasWebhook, ttlMs) {
44202
44273
  const label = `triggers.${name}.messages[${index}]`;
44203
44274
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
44204
44275
  throw new Error(`${label}: must be a mapping`);
@@ -44213,24 +44284,43 @@ function parseTriggerMessage(name, index, raw, agents, as, hasWebhook) {
44213
44284
  if (typeof m.mention !== "string" || m.mention.length === 0) {
44214
44285
  throw new Error(`${label}.mention: must be a non-empty string`);
44215
44286
  }
44216
- const mentionedAgent = agents[m.mention];
44217
- if (!mentionedAgent) {
44218
- throw new Error(`${label}.mention: unknown agent "${m.mention}"`);
44219
- }
44220
- if (!mentionedAgent.matrix) {
44221
- throw new Error(
44222
- `${label}.mention: agent "${m.mention}" has no matrix: binding \u2014 a trigger posts through Matrix, so the mentioned agent must be matrix-bound.`
44223
- );
44224
- }
44225
- if (as === mentionedAgent.matrix.user_id) {
44226
- throw new Error(
44227
- `triggers.${name}.as: must not equal the mentioned agent's own MXID (${as}) \u2014 a message an agent sends never routes back to itself, so this trigger would silently never wake "${m.mention}". Post as a different identity (a dedicated bot, or another agent's).`
44228
- );
44287
+ if (m.mention.startsWith("@")) {
44288
+ if (!MATRIX_USER_ID_RE.test(m.mention)) {
44289
+ throw new Error(`${label}.mention: must be a full MXID (got ${JSON.stringify(m.mention)})`);
44290
+ }
44291
+ const server = m.mention.slice(m.mention.indexOf(":") + 1);
44292
+ const local = deriveServerName(soleMatrixTransport(transports).user_namespace);
44293
+ if (server !== local) {
44294
+ throw new Error(
44295
+ `${label}.mention: "${m.mention}" is on a different homeserver than this daemon (${local}). A trigger addresses a user on its own homeserver \u2014 see [[ZOD081]] \xA7Non-goals.`
44296
+ );
44297
+ }
44298
+ if (as === m.mention) {
44299
+ throw new Error(
44300
+ `triggers.${name}.as: must not equal the mentioned agent's own MXID (${as}) \u2014 a message an agent sends never routes back to itself, so this trigger would silently never wake "${m.mention}". Post as a different identity (a dedicated bot, or another agent's).`
44301
+ );
44302
+ }
44303
+ } else {
44304
+ const mentionedAgent = agents[m.mention];
44305
+ if (!mentionedAgent) {
44306
+ throw new Error(`${label}.mention: unknown agent "${m.mention}"`);
44307
+ }
44308
+ if (!mentionedAgent.matrix) {
44309
+ throw new Error(
44310
+ `${label}.mention: agent "${m.mention}" has no matrix: binding \u2014 a trigger posts through Matrix, so the mentioned agent must be matrix-bound.`
44311
+ );
44312
+ }
44313
+ if (as === mentionedAgent.matrix.user_id) {
44314
+ throw new Error(
44315
+ `triggers.${name}.as: must not equal the mentioned agent's own MXID (${as}) \u2014 a message an agent sends never routes back to itself, so this trigger would silently never wake "${m.mention}". Post as a different identity (a dedicated bot, or another agent's).`
44316
+ );
44317
+ }
44229
44318
  }
44230
44319
  if (typeof m.text !== "string" || m.text.length === 0) {
44231
44320
  throw new Error(`${label}.text: must be a non-empty string`);
44232
44321
  }
44233
44322
  const message = { room: m.room, mention: m.mention, text: m.text };
44323
+ if (ttlMs !== void 0) message.ttlMs = ttlMs;
44234
44324
  if (m.match !== void 0) {
44235
44325
  if (typeof m.match !== "string" || m.match.length === 0) {
44236
44326
  throw new Error(`${label}.match: must be a non-empty string`);
@@ -44552,6 +44642,7 @@ var AcpAgentRegistry = class {
44552
44642
  id: name,
44553
44643
  command: spawn4.command,
44554
44644
  args: spawn4.args,
44645
+ mode: cfg.acp.mode,
44555
44646
  env: this.opts.env?.[name],
44556
44647
  cwd: this.resolveSpawnCwd(name),
44557
44648
  image: this.opts.image?.[name],
@@ -44960,6 +45051,15 @@ var MatrixClient = class {
44960
45051
  if (!r.ok) throw new Error(`sync(${opts.asUserId}) failed: ${r.status}`);
44961
45052
  return r.json();
44962
45053
  }
45054
+ async fetchRoomState(roomId, asUserId) {
45055
+ const url2 = `${this.homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/state?user_id=${encodeURIComponent(asUserId)}`;
45056
+ const r = await this.fetch(url2, {
45057
+ method: "GET",
45058
+ headers: { Authorization: `Bearer ${this.asToken}` }
45059
+ });
45060
+ if (!r.ok) throw new Error(`fetchRoomState(${roomId}) failed: ${r.status}`);
45061
+ return await r.json();
45062
+ }
44963
45063
  async fetchRoomName(roomId, asUserId) {
44964
45064
  const url2 = `${this.homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/state/m.room.name/?user_id=${encodeURIComponent(asUserId)}`;
44965
45065
  const r = await this.fetch(url2, {
@@ -45201,6 +45301,24 @@ function stripMention(body, userId) {
45201
45301
  return body.replace(re, " ").replace(/\s+/g, " ").trim();
45202
45302
  }
45203
45303
 
45304
+ // ../transport-matrix/src/mention-content.ts
45305
+ function buildMentionContent(input) {
45306
+ return {
45307
+ msgtype: input.msgtype,
45308
+ body: `${input.userId} ${input.text}`.trim(),
45309
+ "m.mentions": { user_ids: [input.userId] }
45310
+ };
45311
+ }
45312
+
45313
+ // ../transport-matrix/src/trigger-freshness.ts
45314
+ function isExpiredTrigger(evt, now) {
45315
+ const stamp = evt.content?.["dev.zooid.trigger"];
45316
+ if (!stamp || typeof stamp !== "object") return false;
45317
+ const { fired_at: firedAt, ttl_ms: ttlMs } = stamp;
45318
+ if (typeof firedAt !== "number" || typeof ttlMs !== "number") return false;
45319
+ return now - firedAt > ttlMs;
45320
+ }
45321
+
45204
45322
  // ../transport-matrix/src/router.ts
45205
45323
  var MEDIA_MSGTYPES = /* @__PURE__ */ new Set(["m.image", "m.file", "m.video", "m.audio"]);
45206
45324
  function isMediaMsgtype(t) {
@@ -45210,14 +45328,26 @@ function inboundThreadRoot(event) {
45210
45328
  const r = event.content?.["m.relates_to"];
45211
45329
  return r?.rel_type === "m.thread" && r.event_id ? r.event_id : void 0;
45212
45330
  }
45213
- function route(event, agents, threadStates, task) {
45331
+ function route(event, agents, threadStates, task, knownAgentIds) {
45214
45332
  if (event.type !== "m.room.message") return [];
45215
45333
  if (!event.content?.msgtype) return [];
45216
45334
  if (isMediaMsgtype(event.content.msgtype)) return [];
45335
+ if (isExpiredTrigger(event, Date.now())) {
45336
+ const stamp = event.content["dev.zooid.trigger"];
45337
+ const ageMs = stamp?.fired_at !== void 0 ? Date.now() - stamp.fired_at : void 0;
45338
+ console.info(
45339
+ `[router] dropping expired trigger "${stamp?.name ?? "unknown"}"` + (ageMs !== void 0 ? ` fired ${ageMs}ms ago` : "")
45340
+ );
45341
+ return [];
45342
+ }
45217
45343
  const mentions = new Set(extractMentions(event));
45218
45344
  const matches2 = [];
45219
45345
  const threadRoot = inboundThreadRoot(event);
45220
45346
  const threadState = threadRoot ? threadStates?.get(threadRoot) : void 0;
45347
+ const senderIsAgent = agents.some((x) => x.userId === event.sender) || event.sender !== void 0 && knownAgentIds?.has(event.sender) === true || event.content.msgtype === "m.notice";
45348
+ const addressesAgent = [...mentions].some(
45349
+ (id) => id !== event.sender && (knownAgentIds?.has(id) === true || agents.some((x) => x.userId === id && x.rooms.some((r) => r.alias === event.room_id)))
45350
+ );
45221
45351
  for (const a of agents) {
45222
45352
  if (!a.rooms.some((r) => r.alias === event.room_id)) continue;
45223
45353
  if (task?.isRoot) {
@@ -45230,10 +45360,9 @@ function route(event, agents, threadStates, task) {
45230
45360
  matches2.push(a);
45231
45361
  continue;
45232
45362
  }
45233
- const senderAgent = agents.find((x) => x.userId === event.sender);
45234
- if (senderAgent) {
45363
+ if (senderIsAgent) {
45235
45364
  continue;
45236
- } else if (a.name === task.assignee) {
45365
+ } else if (a.name === task.assignee && !addressesAgent) {
45237
45366
  matches2.push(a);
45238
45367
  }
45239
45368
  continue;
@@ -45247,10 +45376,9 @@ function route(event, agents, threadStates, task) {
45247
45376
  continue;
45248
45377
  }
45249
45378
  if (threadState) {
45250
- const senderAgent = agents.find((x) => x.userId === event.sender);
45251
- if (senderAgent) {
45379
+ if (senderIsAgent) {
45252
45380
  if (isReturnRoute(event, a, agents, threadState)) matches2.push(a);
45253
- } else {
45381
+ } else if (!addressesAgent) {
45254
45382
  const lastPoster = threadState.participants.at(-1);
45255
45383
  if (lastPoster) {
45256
45384
  if (lastPoster === a.name) matches2.push(a);
@@ -45484,6 +45612,57 @@ function sessionKeyFor(agentName, threadRoot, state) {
45484
45612
  return arc ? composeHandoffKey(threadRoot, arc) : threadRoot;
45485
45613
  }
45486
45614
 
45615
+ // ../transport-matrix/src/workforce-publisher.ts
45616
+ function buildWorkforceRoster(agents) {
45617
+ return {
45618
+ version: 1,
45619
+ agents: agents.map((a) => ({
45620
+ user_id: a.userId,
45621
+ name: a.name,
45622
+ rooms: a.rooms.map((r) => r.alias)
45623
+ }))
45624
+ };
45625
+ }
45626
+ async function publishWorkforce(opts) {
45627
+ await opts.client.sendStateEvent({
45628
+ roomId: opts.spaceRoomId,
45629
+ asUserId: opts.asUserId,
45630
+ eventType: "dev.zooid.workforce",
45631
+ stateKey: opts.stateKey ?? "",
45632
+ content: buildWorkforceRoster(opts.agents)
45633
+ });
45634
+ }
45635
+ async function startWorkforcePublisher(opts) {
45636
+ await publishWorkforce({ ...opts, agents: opts.getAgents() });
45637
+ return {
45638
+ async reload() {
45639
+ await publishWorkforce({ ...opts, agents: opts.getAgents() });
45640
+ },
45641
+ async stop() {
45642
+ }
45643
+ };
45644
+ }
45645
+ var WorkforceDirectory = class {
45646
+ rosters = /* @__PURE__ */ new Map();
45647
+ agentIds = /* @__PURE__ */ new Set();
45648
+ /** Seed from a space's full state (`GET /rooms/{id}/state`). */
45649
+ load(events) {
45650
+ for (const ev of events) {
45651
+ if (ev.type === "dev.zooid.workforce" && ev.state_key !== void 0)
45652
+ this.apply(ev.state_key, ev.content);
45653
+ }
45654
+ }
45655
+ /** Replace one workstation's roster; empty content removes it. */
45656
+ apply(stateKey, content) {
45657
+ const agents = content?.agents;
45658
+ const ids = Array.isArray(agents) ? agents.map((a) => a?.user_id).filter((id) => typeof id === "string") : [];
45659
+ if (ids.length > 0) this.rosters.set(stateKey, ids);
45660
+ else this.rosters.delete(stateKey);
45661
+ this.agentIds.clear();
45662
+ for (const list of this.rosters.values()) for (const id of list) this.agentIds.add(id);
45663
+ }
45664
+ };
45665
+
45487
45666
  // ../transport-matrix/src/event-encoders.ts
45488
45667
  var RAW_INPUT_STR_MAX = 250;
45489
45668
  function toToolCallBody(evt) {
@@ -45763,7 +45942,33 @@ var SyncLoop = class {
45763
45942
  timeoutMs: this.opts.timeoutMs ?? 3e4
45764
45943
  });
45765
45944
  for (const [roomId, roomState] of Object.entries(res.rooms?.join ?? {})) {
45766
- for (const baseEvt of roomState.timeline?.events ?? []) {
45945
+ const tl = roomState.timeline;
45946
+ if (tl?.limited && tl.prev_batch && since) {
45947
+ const recovered = [];
45948
+ let from = tl.prev_batch;
45949
+ let pages = 0;
45950
+ const max = this.opts.maxGapPages ?? 10;
45951
+ while (from && from !== since && pages < max) {
45952
+ const page = await this.opts.client.fetchRoomMessages({
45953
+ roomId,
45954
+ asUserId: this.opts.asUserId,
45955
+ from,
45956
+ limit: 100
45957
+ });
45958
+ recovered.push(...page.chunk);
45959
+ from = page.end;
45960
+ pages++;
45961
+ }
45962
+ if (pages >= max && from !== since) {
45963
+ console.warn(`[sync-loop] gap in ${roomId} exceeded ${max} pages \u2014 older events skipped`, {
45964
+ asUserId: this.opts.asUserId
45965
+ });
45966
+ }
45967
+ for (const evt of recovered.reverse()) {
45968
+ await this.opts.onEvent({ ...evt, room_id: roomId });
45969
+ }
45970
+ }
45971
+ for (const baseEvt of tl?.events ?? []) {
45767
45972
  await this.opts.onEvent({ ...baseEvt, room_id: roomId });
45768
45973
  }
45769
45974
  }
@@ -46466,6 +46671,8 @@ function createMatrixTransport(opts) {
46466
46671
  });
46467
46672
  return chained;
46468
46673
  }
46674
+ const workforce = new WorkforceDirectory();
46675
+ let workforceSpaceId;
46469
46676
  async function handleInboundEvent(evt) {
46470
46677
  if (evt.event_id) {
46471
46678
  if (seenEventIds.has(evt.event_id)) {
@@ -46483,6 +46690,11 @@ function createMatrixTransport(opts) {
46483
46690
  );
46484
46691
  return;
46485
46692
  }
46693
+ if (evt.type === "dev.zooid.workforce") {
46694
+ if (evt.room_id === workforceSpaceId && evt.state_key !== void 0)
46695
+ workforce.apply(evt.state_key, evt.content);
46696
+ return;
46697
+ }
46486
46698
  if (evt.type === "m.room.member" && evt.content?.membership === "invite") {
46487
46699
  const target = evt.state_key;
46488
46700
  const inviter = evt.sender;
@@ -46610,7 +46822,13 @@ function createMatrixTransport(opts) {
46610
46822
  const inboundRel = inboundThreadRoot2(evt);
46611
46823
  if (evt.type === "m.room.message" && inboundRel && !threadStates.has(inboundRel) && evt.room_id) {
46612
46824
  try {
46613
- const rebuilt = await rebuildThreadState(client, evt.room_id, inboundRel, bindings);
46825
+ const rebuilt = await rebuildThreadState(
46826
+ client,
46827
+ evt.room_id,
46828
+ inboundRel,
46829
+ bindings,
46830
+ workforce.agentIds
46831
+ );
46614
46832
  threadStates.set(inboundRel, rebuilt);
46615
46833
  console.log(
46616
46834
  `[matrix] rebuilt threadState for ${inboundRel}: participants=${rebuilt.participants.join(",")} rootMentions=${rebuilt.rootMentions.join(",")}`
@@ -46628,7 +46846,10 @@ function createMatrixTransport(opts) {
46628
46846
  assignee: taskRec.assignee,
46629
46847
  isRoot: !inboundRel && evt.event_id === taskRec.threadRoot
46630
46848
  } : void 0;
46631
- let matches2 = route(evt, bindings, threadStates, taskCtx);
46849
+ const inboundState = inboundRel ? threadStates.get(inboundRel) : void 0;
46850
+ if (inboundState && evt.type === "m.room.message" && evt.sender && isRemoteAgent(evt.sender, evt.content?.msgtype, bindings, workforce.agentIds) && inboundState.participants.at(-1) !== evt.sender)
46851
+ inboundState.participants.push(evt.sender);
46852
+ let matches2 = route(evt, bindings, threadStates, taskCtx, workforce.agentIds);
46632
46853
  if (taskCtx && !taskCtx.isRoot && evt.event_id && bindings.some((b) => b.userId === evt.sender)) {
46633
46854
  const invocation = invocations.byCallEvent(evt.event_id);
46634
46855
  matches2 = invocation ? matches2.filter((match) => match.name === invocation.calleeAgent) : [];
@@ -47099,6 +47320,15 @@ function createMatrixTransport(opts) {
47099
47320
  syncLoops,
47100
47321
  bootstrap: async (bootstrapOpts = {}) => {
47101
47322
  await pool.bootstrap({ adminUserId, ...bootstrapOpts });
47323
+ const { spaceRoomId, asUserId } = bootstrapOpts;
47324
+ if (spaceRoomId && asUserId) {
47325
+ workforceSpaceId = spaceRoomId;
47326
+ try {
47327
+ workforce.load(await client.fetchRoomState(spaceRoomId, asUserId));
47328
+ } catch (err) {
47329
+ console.warn("[matrix] workforce roster load failed:", err);
47330
+ }
47331
+ }
47102
47332
  await Promise.allSettled(
47103
47333
  bindings.map(
47104
47334
  (b) => client.setPresence({ asUserId: b.userId, presence: "online" }).catch((err) => {
@@ -47110,7 +47340,7 @@ function createMatrixTransport(opts) {
47110
47340
  pool
47111
47341
  };
47112
47342
  }
47113
- async function rebuildThreadState(client, roomId, rootEventId, bindings) {
47343
+ async function rebuildThreadState(client, roomId, rootEventId, bindings, knownAgentIds) {
47114
47344
  const state = {
47115
47345
  participants: [],
47116
47346
  rootMentions: [],
@@ -47158,11 +47388,18 @@ async function rebuildThreadState(client, roomId, rootEventId, bindings) {
47158
47388
  const type = ev.type;
47159
47389
  if (type === "m.room.message" && evSender) {
47160
47390
  const a = bindings.find((b) => b.userId === evSender);
47161
- if (a && state.participants.at(-1) !== a.name) state.participants.push(a.name);
47391
+ const msgtype = ev.content?.msgtype;
47392
+ const participant = a ? a.name : isRemoteAgent(evSender, msgtype, bindings, knownAgentIds) ? evSender : void 0;
47393
+ if (participant && state.participants.at(-1) !== participant)
47394
+ state.participants.push(participant);
47162
47395
  }
47163
47396
  }
47164
47397
  return state;
47165
47398
  }
47399
+ function isRemoteAgent(sender, msgtype, bindings, knownAgentIds) {
47400
+ if (bindings.some((b) => b.userId === sender)) return false;
47401
+ return knownAgentIds?.has(sender) === true || msgtype === "m.notice";
47402
+ }
47166
47403
  function logInbound(evt) {
47167
47404
  const sender = evt.sender ?? "?";
47168
47405
  const room = evt.room_id ?? "?";
@@ -47182,37 +47419,6 @@ function truncate(s, n) {
47182
47419
  return s.length > n ? s.slice(0, n) + "\u2026" : s;
47183
47420
  }
47184
47421
 
47185
- // ../transport-matrix/src/workforce-publisher.ts
47186
- function buildWorkforceRoster(agents) {
47187
- return {
47188
- version: 1,
47189
- agents: agents.map((a) => ({
47190
- user_id: a.userId,
47191
- name: a.name,
47192
- rooms: a.rooms.map((r) => r.alias)
47193
- }))
47194
- };
47195
- }
47196
- async function publishWorkforce(opts) {
47197
- await opts.client.sendStateEvent({
47198
- roomId: opts.spaceRoomId,
47199
- asUserId: opts.asUserId,
47200
- eventType: "dev.zooid.workforce",
47201
- stateKey: "",
47202
- content: buildWorkforceRoster(opts.agents)
47203
- });
47204
- }
47205
- async function startWorkforcePublisher(opts) {
47206
- await publishWorkforce({ ...opts, agents: opts.getAgents() });
47207
- return {
47208
- async reload() {
47209
- await publishWorkforce({ ...opts, agents: opts.getAgents() });
47210
- },
47211
- async stop() {
47212
- }
47213
- };
47214
- }
47215
-
47216
47422
  // ../context-mcp/src/spawn-registry.ts
47217
47423
  import { randomUUID as randomUUID5 } from "crypto";
47218
47424
  var SpawnRegistry = class {
@@ -52963,14 +53169,15 @@ export {
52963
53169
  ApprovalCorrelator,
52964
53170
  MatrixClient,
52965
53171
  renderRegistration,
53172
+ buildMentionContent,
52966
53173
  ensureWorkforceSpace,
52967
53174
  ensureDefaultChannel,
53175
+ startWorkforcePublisher,
52968
53176
  MediaClient,
52969
53177
  createMatrixTransport,
52970
- startWorkforcePublisher,
52971
53178
  SpawnRegistry,
52972
53179
  startAgentSocketServers,
52973
53180
  buildAcpRegistry,
52974
53181
  contextEligibleAgents
52975
53182
  };
52976
- //# sourceMappingURL=chunk-R5S26T7B.js.map
53183
+ //# sourceMappingURL=chunk-35IKQPC5.js.map