zooid 0.14.1 → 0.16.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/README.md CHANGED
@@ -42,7 +42,7 @@ zooid dev
42
42
 
43
43
  > **Prerequisite:** `zooid dev` runs Tuwunel inside a container, so you need a container engine installed and running first — either **Docker** (with the daemon started) or **Podman** aliased as `docker`.
44
44
 
45
- `zooid dev` starts a Tuwunel Matrix homeserver in a container, generates the Application Service registration, registers an `admin:admin` user, runs the daemon, and serves the [Zooid web client](https://github.com/zooid-ai/clients).
45
+ `zooid dev` starts a Tuwunel Matrix homeserver in a container, generates the Application Service registration, registers an `admin:admin` user, runs the daemon, and serves the [Zooid web client](https://github.com/zooid-ai/clients). The client's runtime settings come from a generated `/config.json`; its `workforce_space` field is the `matrix.space` alias localpart from `zooid.yaml` (default `dev`). A static deployment sets `workforce_space` in the `config.json` it serves beside the bundle — there is no build-time variable.
46
46
 
47
47
  Open `http://localhost:5173`, log in as `admin` / `admin`, join `#welcome`, and `@`-mention your agent.
48
48
 
package/dist/bin.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  MediaClient,
6
6
  SpawnRegistry,
7
7
  buildAcpRegistry,
8
+ buildMentionContent,
8
9
  contextEligibleAgents,
9
10
  createMatrixTransport,
10
11
  ensureDefaultChannel,
@@ -19,7 +20,7 @@ import {
19
20
  renderTemplate,
20
21
  startAgentSocketServers,
21
22
  startWorkforcePublisher
22
- } from "./chunk-R5S26T7B.js";
23
+ } from "./chunk-HE7DVDJA.js";
23
24
 
24
25
  // src/bin.ts
25
26
  import { resolve as resolve8 } from "path";
@@ -697,9 +698,15 @@ function shouldBindHttpListener(mode) {
697
698
  import { Cron } from "croner";
698
699
 
699
700
  // src/daemon/trigger-runner.ts
701
+ var warnedTargets = /* @__PURE__ */ new Set();
700
702
  async function fireTrigger(deps) {
701
- const { name, as, message, agentUserId, resolveRoom, ensureBot, sendMessage } = deps;
703
+ const { name, as, message, agentUserIds, resolveRoom, ensureBot, sendMessage, getJoinedMembers } = deps;
702
704
  try {
705
+ const target = message.mention.startsWith("@") ? message.mention : agentUserIds[message.mention];
706
+ if (!target) {
707
+ console.warn(`[trigger:${name}] unknown agent "${message.mention}" \u2014 skipping`);
708
+ return;
709
+ }
703
710
  const roomId = await resolveRoom(message.room);
704
711
  if (!roomId) {
705
712
  console.warn(`[trigger:${name}] cannot resolve room ${message.room} \u2014 skipping`);
@@ -710,13 +717,24 @@ async function fireTrigger(deps) {
710
717
  roomId,
711
718
  asUserId: as,
712
719
  content: {
713
- msgtype: "m.text",
714
- body: message.text,
715
- // Structural mention: routes deterministically AND disarms the raw-body
716
- // fallback in extractMentions, which only fires when nothing matched.
717
- "m.mentions": { user_ids: [agentUserId] }
720
+ ...buildMentionContent({ userId: target, text: message.text, msgtype: "m.text" }),
721
+ "dev.zooid.trigger": {
722
+ name,
723
+ fired_at: Date.now(),
724
+ ...message.ttlMs !== void 0 ? { ttl_ms: message.ttlMs } : {}
725
+ }
718
726
  }
719
727
  });
728
+ if (getJoinedMembers) {
729
+ const { joined } = await getJoinedMembers(roomId, as);
730
+ const key = `${name}|${target}`;
731
+ if (!(target in joined) && !warnedTargets.has(key)) {
732
+ warnedTargets.add(key);
733
+ console.warn(
734
+ `[trigger:${name}] ${target} is not in ${message.room} \u2014 nothing will handle this unless that agent joins before the message expires`
735
+ );
736
+ }
737
+ }
720
738
  } catch (err) {
721
739
  console.warn(`[trigger:${name}] failed:`, err.message);
722
740
  }
@@ -731,18 +749,22 @@ function validateCron(name, expr) {
731
749
  }
732
750
  }
733
751
  function startTriggerScheduler(deps) {
734
- const { triggers, agentUserIds, resolveRoom, ensureBot, sendMessage } = deps;
752
+ const { triggers, agentUserIds, resolveRoom, ensureBot, sendMessage, getJoinedMembers } = deps;
735
753
  const jobs = [];
736
754
  for (const [name, trigger] of Object.entries(triggers)) {
737
755
  if (!trigger.schedule) continue;
738
756
  const job = new Cron(trigger.schedule, () => {
739
757
  for (const message of trigger.messages) {
740
- const agentUserId = agentUserIds[message.mention];
741
- if (!agentUserId) {
742
- console.warn(`[trigger:${name}] unknown agent "${message.mention}" \u2014 skipping`);
743
- continue;
744
- }
745
- void fireTrigger({ name, as: trigger.as, message, agentUserId, resolveRoom, ensureBot, sendMessage });
758
+ void fireTrigger({
759
+ name,
760
+ as: trigger.as,
761
+ message,
762
+ agentUserIds,
763
+ resolveRoom,
764
+ ensureBot,
765
+ sendMessage,
766
+ getJoinedMembers
767
+ });
746
768
  }
747
769
  });
748
770
  jobs.push(job);
@@ -955,19 +977,15 @@ async function handleDelivery(deps, name, trigger, raw, headers, cache, customDe
955
977
  };
956
978
  for (const message of trigger.messages) {
957
979
  if (message.match !== void 0 && !evaluateMatch(message.match, ctx)) continue;
958
- const agentUserId = deps.agentUserIds[message.mention];
959
- if (!agentUserId) {
960
- console.warn(`[webhook:${name}] unknown agent "${message.mention}" \u2014 skipping`);
961
- continue;
962
- }
963
980
  await fireTrigger({
964
981
  name,
965
982
  as: trigger.as,
966
983
  message: { ...message, text: renderTemplate(message.text, ctx) },
967
- agentUserId,
984
+ agentUserIds: deps.agentUserIds,
968
985
  resolveRoom: deps.resolveRoom,
969
986
  ensureBot: deps.ensureBot,
970
- sendMessage: deps.sendMessage
987
+ sendMessage: deps.sendMessage,
988
+ getJoinedMembers: deps.getJoinedMembers
971
989
  });
972
990
  }
973
991
  } catch (err) {
@@ -1095,6 +1113,7 @@ async function startDaemon(opts = {}) {
1095
1113
  const registry = buildAcpRegistry(config, {
1096
1114
  approvals,
1097
1115
  onTap: opts.onTap,
1116
+ onLifecycle: opts.onLifecycle,
1098
1117
  agentsDir: opts.agentsDir,
1099
1118
  contextSpawnRegistry,
1100
1119
  daemonSockPaths: contextSockets.paths,
@@ -1237,7 +1256,8 @@ async function startDaemon(opts = {}) {
1237
1256
  agentUserIds,
1238
1257
  resolveRoom,
1239
1258
  ensureBot,
1240
- sendMessage: (m) => client.sendMessage(m)
1259
+ sendMessage: (m) => client.sendMessage(m),
1260
+ getJoinedMembers: (roomId, asUserId2) => client.getJoinedMembers(roomId, asUserId2)
1241
1261
  });
1242
1262
  for (const [name] of webhookTriggers) {
1243
1263
  console.log(`[webhook] POST ${WEBHOOK_ROUTE_PREFIX}/${name}`);
@@ -1281,7 +1301,8 @@ async function startDaemon(opts = {}) {
1281
1301
  agentUserIds,
1282
1302
  resolveRoom,
1283
1303
  ensureBot,
1284
- sendMessage: (m) => client.sendMessage(m)
1304
+ sendMessage: (m) => client.sendMessage(m),
1305
+ getJoinedMembers: (roomId, asUserId2) => client.getJoinedMembers(roomId, asUserId2)
1285
1306
  });
1286
1307
  const scheduled = Object.values(config.triggers).filter((t) => t.schedule).length;
1287
1308
  if (scheduled > 0) console.log(`[trigger] scheduled ${scheduled} trigger(s)`);
@@ -1317,7 +1338,8 @@ async function startDaemon(opts = {}) {
1317
1338
  client,
1318
1339
  spaceRoomId,
1319
1340
  asUserId,
1320
- getAgents: () => bindings
1341
+ getAgents: () => bindings,
1342
+ stateKey: matrix.transport.workstation ?? ""
1321
1343
  });
1322
1344
  console.log(`[matrix] published dev.zooid.workforce (${bindings.length} agents)`);
1323
1345
  void publisher;
@@ -1637,6 +1659,7 @@ function webStatic(opts) {
1637
1659
  "/config.json",
1638
1660
  (c) => c.json({
1639
1661
  homeserver_url: opts.homeserverUrl,
1662
+ ...opts.workforceSpace ? { workforce_space: opts.workforceSpace } : {},
1640
1663
  ...opts.pushGatewayUrl ? { push_gateway_url: opts.pushGatewayUrl } : {},
1641
1664
  ...opts.vapidPublicKey ? { vapid_public_key: opts.vapidPublicKey } : {}
1642
1665
  })
@@ -1873,6 +1896,12 @@ function wireAgentCapture(opts) {
1873
1896
  const sink = new JsonlSink(opts.paths.agentTap(opts.agent));
1874
1897
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
1875
1898
  const pending = [];
1899
+ const counters = {
1900
+ closeAttempts: 0,
1901
+ closeSuccesses: 0,
1902
+ sessionsReclaimed: 0,
1903
+ recoveries: { cached: 0, resume: 0, load: 0, new: 0 }
1904
+ };
1876
1905
  const onTap = (event) => {
1877
1906
  if (event.kind === "session_update") {
1878
1907
  const variant = event.update.sessionUpdate ?? "unknown";
@@ -1894,6 +1923,25 @@ function wireAgentCapture(opts) {
1894
1923
  };
1895
1924
  return {
1896
1925
  onTap,
1926
+ onLifecycle: (event) => {
1927
+ if (event.reason && event.outcome !== "unsupported") counters.closeAttempts++;
1928
+ if (event.reason && event.outcome === "closed") {
1929
+ counters.closeSuccesses++;
1930
+ counters.sessionsReclaimed++;
1931
+ }
1932
+ if (event.outcome === "recovered" && event.recoveryMethod) counters.recoveries[event.recoveryMethod]++;
1933
+ pending.push(sink.write({
1934
+ ts: now().toISOString(),
1935
+ agent: opts.agent,
1936
+ kind: "session_lifecycle",
1937
+ session_key: event.sessionKey,
1938
+ session_id: event.sessionId,
1939
+ reason: event.reason,
1940
+ outcome: event.outcome,
1941
+ recovery_method: event.recoveryMethod
1942
+ }));
1943
+ },
1944
+ readLifecycleCounters: () => ({ ...counters, recoveries: { ...counters.recoveries } }),
1897
1945
  close: async () => {
1898
1946
  await Promise.all(pending);
1899
1947
  await sink.close();
@@ -2059,6 +2107,7 @@ async function runDev(flags) {
2059
2107
  // the web client without needing an invite.
2060
2108
  publicWorkforceSpace: true,
2061
2109
  onTap: (agentName, event) => captures[agentName]?.onTap(event),
2110
+ onLifecycle: (agentName, event) => captures[agentName]?.onLifecycle(event),
2062
2111
  prepullLog: (line) => {
2063
2112
  t.output = line.replace(/^\[zooid\]\s+/, "").trim();
2064
2113
  }
@@ -2095,6 +2144,8 @@ Pass an explicit path: --watch-web=/path/to/zooid-clients/packages/web`
2095
2144
  const app = webStatic({
2096
2145
  webRoot,
2097
2146
  homeserverUrl: homeserver,
2147
+ // Same default as the daemon's space provisioning (start-daemon.ts).
2148
+ workforceSpace: matrix.transport.space ?? "dev",
2098
2149
  ...ctx.daemon?.vapidPublicKey ? {
2099
2150
  // Tuwunel runs in a container; `localhost` here would
2100
2151
  // resolve to the container, not the daemon. Same shorthand
@@ -2757,6 +2808,22 @@ async function dumpByTurn(dayDir, turnId, writer) {
2757
2808
  // src/commands/start.ts
2758
2809
  import { randomBytes as randomBytes2 } from "crypto";
2759
2810
  import { resolve as resolve6 } from "path";
2811
+
2812
+ // src/observability/lifecycle-log.ts
2813
+ function formatLifecycleLine(agentName, event) {
2814
+ if (event.outcome === "unsupported") return null;
2815
+ if (event.recoveryMethod === "cached") return null;
2816
+ const what = event.outcome === "recovered" ? `recovered (${event.recoveryMethod ?? "unknown"})` : `${event.reason ?? "close"} \u2192 ${event.outcome}`;
2817
+ return `[lifecycle] ${agentName} key=${event.sessionKey} session=${event.sessionId} ${what}`;
2818
+ }
2819
+ function logLifecycle(agentName, event, out = console) {
2820
+ const line = formatLifecycleLine(agentName, event);
2821
+ if (!line) return;
2822
+ if (event.outcome === "failed") out.error(line);
2823
+ else out.log(line);
2824
+ }
2825
+
2826
+ // src/commands/start.ts
2760
2827
  async function runStart(flags) {
2761
2828
  if (flags.printToken) {
2762
2829
  process.stdout.write(`${randomBytes2(32).toString("hex")}
@@ -2768,7 +2835,8 @@ async function runStart(flags) {
2768
2835
  const handle = await startDaemon({
2769
2836
  cliFlags: flags,
2770
2837
  installSignalHandlers: true,
2771
- agentsDir: layout.agentsDir
2838
+ agentsDir: layout.agentsDir,
2839
+ onLifecycle: (agentName, event) => logLifecycle(agentName, event)
2772
2840
  });
2773
2841
  console.log(`zooid listening on http://localhost:${handle.port}`);
2774
2842
  for (const name of handle.agentNames) console.log(` agent: ${name}`);