@alfe.ai/openclaw-chat 0.3.0 → 0.3.2

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.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as createAlfeChannelPlugin, c as AlfeResolvedAccount, o as AlfeChannelConfig, r as plugin, s as AlfePluginConfig } from "./plugin.cjs";
1
+ import { a as plugin, c as AlfeChannelConfig, l as AlfePluginConfig, s as createAlfeChannelPlugin, u as AlfeResolvedAccount } from "./plugin.cjs";
2
2
 
3
3
  //#region src/session-store.d.ts
4
4
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as createAlfeChannelPlugin, c as AlfeResolvedAccount, o as AlfeChannelConfig, r as plugin, s as AlfePluginConfig } from "./plugin.js";
1
+ import { a as plugin, c as AlfeChannelConfig, l as AlfePluginConfig, s as createAlfeChannelPlugin, u as AlfeResolvedAccount } from "./plugin.js";
2
2
 
3
3
  //#region src/session-store.d.ts
4
4
 
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as createAlfeChannelPlugin, r as plugin } from "./plugin2.js";
1
+ import { i as plugin, o as createAlfeChannelPlugin } from "./plugin2.js";
2
2
  export { createAlfeChannelPlugin, plugin as default };
package/dist/plugin.cjs CHANGED
@@ -3,6 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_plugin = require("./plugin2.cjs");
6
+ exports.admitAgentEvent = require_plugin.admitAgentEvent;
6
7
  exports.buildA2ACompletePayload = require_plugin.buildA2ACompletePayload;
7
8
  exports.buildToolActivity = require_plugin.buildToolActivity;
8
9
  exports.default = require_plugin.plugin;
package/dist/plugin.d.cts CHANGED
@@ -268,6 +268,40 @@ interface AgentEventPayload {
268
268
  data: Record<string, unknown>;
269
269
  sessionKey?: string;
270
270
  }
271
+ interface RunEventGate {
272
+ runId: string | null;
273
+ sessionKey: string | null;
274
+ }
275
+ /**
276
+ * Decide whether an agent-event belongs to THIS turn's run and should be
277
+ * forwarded as a delta / activity frame.
278
+ *
279
+ * Why gate on `runId`, not `sessionKey`:
280
+ * On OpenClaw 2026.x the daemon registers this ('alfe') channel's main run
281
+ * with `isControlUiVisible: false` — only the built-in "webchat" surface is
282
+ * treated as control-UI-visible (`isInternalMessageChannel`). The shared
283
+ * agent-event bus then NULLS `sessionKey` on every event of a
284
+ * non-control-UI run (`agent-events`: `sessionKey = isControlUiVisible ? … :
285
+ * undefined`). So main-run assistant/thinking/tool events arrive with
286
+ * `sessionKey: undefined`, and the old `!evt.sessionKey` + key-equality gate
287
+ * silently dropped ALL activity — replies still landed because they come via
288
+ * the `deliver()` callback, not the event bus (the reported "replies arrive,
289
+ * zero activity" symptom). Verified against openclaw@2026.6.10 and
290
+ * back-compatible with 2026.4.2 (identical emit semantics). `runId` is always
291
+ * present on every emitted event.
292
+ *
293
+ * Isolation is preserved: turns to a daemon are serialised
294
+ * (`enqueueAgentTurn`) and the listener is torn down before the next turn
295
+ * dispatches, so the first relevant event locks onto THIS turn's main run.
296
+ * Concurrent heartbeat/cron runs and subagent / side-question runs carry a
297
+ * different `runId` and are excluded — exactly the leakage the gate exists to
298
+ * prevent. When a run IS control-UI-visible and both keys are present, a
299
+ * mismatched `sessionKey` is still rejected as a secondary defence.
300
+ *
301
+ * Exported for unit tests — this gate is the fix's load-bearing logic and is
302
+ * pinned against real 2026.6.10 event shapes.
303
+ */
304
+ declare function admitAgentEvent(gate: RunEventGate, evt: Pick<AgentEventPayload, 'runId' | 'stream' | 'sessionKey'>): boolean;
271
305
  interface PluginServiceContext {
272
306
  config: Record<string, unknown>;
273
307
  workspaceDir?: string;
@@ -362,4 +396,4 @@ declare const plugin: {
362
396
  deactivate(api: PluginApi): Promise<void>;
363
397
  };
364
398
  //#endregion
365
- export { createAlfeChannelPlugin as a, AlfeResolvedAccount as c, stripLeakedToolSummary as i, buildToolActivity as n, AlfeChannelConfig as o, plugin as r, AlfePluginConfig as s, buildA2ACompletePayload as t };
399
+ export { plugin as a, AlfeChannelConfig as c, buildToolActivity as i, AlfePluginConfig as l, admitAgentEvent as n, stripLeakedToolSummary as o, buildA2ACompletePayload as r, createAlfeChannelPlugin as s, RunEventGate as t, AlfeResolvedAccount as u };
package/dist/plugin.d.ts CHANGED
@@ -268,6 +268,40 @@ interface AgentEventPayload {
268
268
  data: Record<string, unknown>;
269
269
  sessionKey?: string;
270
270
  }
271
+ interface RunEventGate {
272
+ runId: string | null;
273
+ sessionKey: string | null;
274
+ }
275
+ /**
276
+ * Decide whether an agent-event belongs to THIS turn's run and should be
277
+ * forwarded as a delta / activity frame.
278
+ *
279
+ * Why gate on `runId`, not `sessionKey`:
280
+ * On OpenClaw 2026.x the daemon registers this ('alfe') channel's main run
281
+ * with `isControlUiVisible: false` — only the built-in "webchat" surface is
282
+ * treated as control-UI-visible (`isInternalMessageChannel`). The shared
283
+ * agent-event bus then NULLS `sessionKey` on every event of a
284
+ * non-control-UI run (`agent-events`: `sessionKey = isControlUiVisible ? … :
285
+ * undefined`). So main-run assistant/thinking/tool events arrive with
286
+ * `sessionKey: undefined`, and the old `!evt.sessionKey` + key-equality gate
287
+ * silently dropped ALL activity — replies still landed because they come via
288
+ * the `deliver()` callback, not the event bus (the reported "replies arrive,
289
+ * zero activity" symptom). Verified against openclaw@2026.6.10 and
290
+ * back-compatible with 2026.4.2 (identical emit semantics). `runId` is always
291
+ * present on every emitted event.
292
+ *
293
+ * Isolation is preserved: turns to a daemon are serialised
294
+ * (`enqueueAgentTurn`) and the listener is torn down before the next turn
295
+ * dispatches, so the first relevant event locks onto THIS turn's main run.
296
+ * Concurrent heartbeat/cron runs and subagent / side-question runs carry a
297
+ * different `runId` and are excluded — exactly the leakage the gate exists to
298
+ * prevent. When a run IS control-UI-visible and both keys are present, a
299
+ * mismatched `sessionKey` is still rejected as a secondary defence.
300
+ *
301
+ * Exported for unit tests — this gate is the fix's load-bearing logic and is
302
+ * pinned against real 2026.6.10 event shapes.
303
+ */
304
+ declare function admitAgentEvent(gate: RunEventGate, evt: Pick<AgentEventPayload, 'runId' | 'stream' | 'sessionKey'>): boolean;
271
305
  interface PluginServiceContext {
272
306
  config: Record<string, unknown>;
273
307
  workspaceDir?: string;
@@ -362,4 +396,4 @@ declare const plugin: {
362
396
  deactivate(api: PluginApi): Promise<void>;
363
397
  };
364
398
  //#endregion
365
- export { createAlfeChannelPlugin as a, AlfeResolvedAccount as c, stripLeakedToolSummary as i, buildToolActivity as n, AlfeChannelConfig as o, plugin as r, AlfePluginConfig as s, buildA2ACompletePayload as t };
399
+ export { plugin as a, AlfeChannelConfig as c, buildToolActivity as i, AlfePluginConfig as l, admitAgentEvent as n, stripLeakedToolSummary as o, buildA2ACompletePayload as r, createAlfeChannelPlugin as s, RunEventGate as t, AlfeResolvedAccount as u };
package/dist/plugin.js CHANGED
@@ -1,2 +1,2 @@
1
- import { i as stripLeakedToolSummary, n as buildToolActivity, r as plugin, t as buildA2ACompletePayload } from "./plugin2.js";
2
- export { buildA2ACompletePayload, buildToolActivity, plugin as default, stripLeakedToolSummary };
1
+ import { a as stripLeakedToolSummary, i as plugin, n as buildA2ACompletePayload, r as buildToolActivity, t as admitAgentEvent } from "./plugin2.js";
2
+ export { admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, stripLeakedToolSummary };
package/dist/plugin2.cjs CHANGED
@@ -864,6 +864,45 @@ function extractResultText(result) {
864
864
  */
865
865
  const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
866
866
  const pkg = require$1("../package.json");
867
+ /**
868
+ * Decide whether an agent-event belongs to THIS turn's run and should be
869
+ * forwarded as a delta / activity frame.
870
+ *
871
+ * Why gate on `runId`, not `sessionKey`:
872
+ * On OpenClaw 2026.x the daemon registers this ('alfe') channel's main run
873
+ * with `isControlUiVisible: false` — only the built-in "webchat" surface is
874
+ * treated as control-UI-visible (`isInternalMessageChannel`). The shared
875
+ * agent-event bus then NULLS `sessionKey` on every event of a
876
+ * non-control-UI run (`agent-events`: `sessionKey = isControlUiVisible ? … :
877
+ * undefined`). So main-run assistant/thinking/tool events arrive with
878
+ * `sessionKey: undefined`, and the old `!evt.sessionKey` + key-equality gate
879
+ * silently dropped ALL activity — replies still landed because they come via
880
+ * the `deliver()` callback, not the event bus (the reported "replies arrive,
881
+ * zero activity" symptom). Verified against openclaw@2026.6.10 and
882
+ * back-compatible with 2026.4.2 (identical emit semantics). `runId` is always
883
+ * present on every emitted event.
884
+ *
885
+ * Isolation is preserved: turns to a daemon are serialised
886
+ * (`enqueueAgentTurn`) and the listener is torn down before the next turn
887
+ * dispatches, so the first relevant event locks onto THIS turn's main run.
888
+ * Concurrent heartbeat/cron runs and subagent / side-question runs carry a
889
+ * different `runId` and are excluded — exactly the leakage the gate exists to
890
+ * prevent. When a run IS control-UI-visible and both keys are present, a
891
+ * mismatched `sessionKey` is still rejected as a secondary defence.
892
+ *
893
+ * Exported for unit tests — this gate is the fix's load-bearing logic and is
894
+ * pinned against real 2026.6.10 event shapes.
895
+ */
896
+ function admitAgentEvent(gate, evt) {
897
+ if (!(evt.stream === "assistant" || evt.stream === "thinking" || evt.stream === "tool")) return false;
898
+ if (gate.runId === null) {
899
+ gate.runId = evt.runId;
900
+ if (evt.sessionKey) gate.sessionKey ??= evt.sessionKey;
901
+ }
902
+ if (evt.runId !== gate.runId) return false;
903
+ if (gate.sessionKey && evt.sessionKey && evt.sessionKey !== gate.sessionKey) return false;
904
+ return true;
905
+ }
867
906
  function asString(v) {
868
907
  return typeof v === "string" && v.length > 0 ? v : void 0;
869
908
  }
@@ -1128,7 +1167,10 @@ async function handleAgentRequest(request, log) {
1128
1167
  const sessionId = conversationId ?? legacySessionKey;
1129
1168
  if (!await getSession(sessionId)) await createSession(sessionId, "", "alfe", tenantId, userId);
1130
1169
  await addMessage(sessionId, "user", message, userId ?? senderId, displayName ?? senderId);
1131
- let resolvedOpenClawKey = null;
1170
+ const runGate = {
1171
+ runId: null,
1172
+ sessionKey: null
1173
+ };
1132
1174
  const UPDATE_THROTTLE_MS = 250;
1133
1175
  const pendingUpdates = /* @__PURE__ */ new Map();
1134
1176
  const flushToolUpdate = (toolCallId) => {
@@ -1143,9 +1185,7 @@ async function handleAgentRequest(request, log) {
1143
1185
  pendingUpdates.clear();
1144
1186
  };
1145
1187
  const unsubscribe = runtime.events.onAgentEvent((evt) => {
1146
- if (!evt.sessionKey) return;
1147
- if (evt.stream === "assistant" || evt.stream === "thinking" || evt.stream === "tool") resolvedOpenClawKey ??= evt.sessionKey;
1148
- if (evt.sessionKey !== resolvedOpenClawKey) return;
1188
+ if (!admitAgentEvent(runGate, evt)) return;
1149
1189
  if (evt.stream === "assistant") {
1150
1190
  chatClient?.sendEvent("chat", {
1151
1191
  runId: evt.runId,
@@ -1223,7 +1263,7 @@ async function handleAgentRequest(request, log) {
1223
1263
  const conversationLabel = conversationType === "group" ? shortConvId ? `[${channelLabel}] Group (${shortConvId})` : `[${channelLabel}] Group` : shortConvId ? `[${channelLabel}] ${userLabel} (${shortConvId})` : `[${channelLabel}] ${userLabel}`;
1224
1264
  let a2aResponseBuffer = "";
1225
1265
  if (isA2A) resetA2AEndSignal();
1226
- resolvedOpenClawKey = (await dispatchInbound({
1266
+ runGate.sessionKey = (await dispatchInbound({
1227
1267
  cfg,
1228
1268
  runtime: { channel: runtime.channel },
1229
1269
  channel: "alfe",
@@ -1276,7 +1316,7 @@ async function handleAgentRequest(request, log) {
1276
1316
  chatClient?.notify("agent-message", {
1277
1317
  conversationId: conversationId ?? legacySessionKey,
1278
1318
  text: responseText,
1279
- sessionKey: resolvedOpenClawKey ?? legacySessionKey,
1319
+ sessionKey: runGate.sessionKey ?? legacySessionKey,
1280
1320
  messageId: chatMessageId ?? request.id,
1281
1321
  ...mediaUrls.length ? { mediaUrls } : {}
1282
1322
  });
@@ -1297,8 +1337,8 @@ async function handleAgentRequest(request, log) {
1297
1337
  resolved: a2aResolved
1298
1338
  }));
1299
1339
  }
1300
- chatClient?.sendResponse(request.id, true, { sessionKey: resolvedOpenClawKey });
1301
- log.info(`Agent dispatch complete: sessionKey=${resolvedOpenClawKey}`);
1340
+ chatClient?.sendResponse(request.id, true, { sessionKey: runGate.sessionKey });
1341
+ log.info(`Agent dispatch complete: sessionKey=${runGate.sessionKey}`);
1302
1342
  } catch (err) {
1303
1343
  const errMsg = err instanceof Error ? err.message : String(err);
1304
1344
  log.error(`Agent dispatch failed: ${errMsg}`);
@@ -1534,6 +1574,12 @@ const plugin = {
1534
1574
  }
1535
1575
  };
1536
1576
  //#endregion
1577
+ Object.defineProperty(exports, "admitAgentEvent", {
1578
+ enumerable: true,
1579
+ get: function() {
1580
+ return admitAgentEvent;
1581
+ }
1582
+ });
1537
1583
  Object.defineProperty(exports, "buildA2ACompletePayload", {
1538
1584
  enumerable: true,
1539
1585
  get: function() {
@@ -1,2 +1,2 @@
1
- import { i as stripLeakedToolSummary, n as buildToolActivity, r as plugin, t as buildA2ACompletePayload } from "./plugin.cjs";
2
- export { buildA2ACompletePayload, buildToolActivity, plugin as default, stripLeakedToolSummary };
1
+ import { a as plugin, i as buildToolActivity, n as admitAgentEvent, o as stripLeakedToolSummary, r as buildA2ACompletePayload, t as RunEventGate } from "./plugin.cjs";
2
+ export { RunEventGate, admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, stripLeakedToolSummary };
package/dist/plugin2.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { i as stripLeakedToolSummary, n as buildToolActivity, r as plugin, t as buildA2ACompletePayload } from "./plugin.js";
2
- export { buildA2ACompletePayload, buildToolActivity, plugin as default, stripLeakedToolSummary };
1
+ import { a as plugin, i as buildToolActivity, n as admitAgentEvent, o as stripLeakedToolSummary, r as buildA2ACompletePayload, t as RunEventGate } from "./plugin.js";
2
+ export { RunEventGate, admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, stripLeakedToolSummary };
package/dist/plugin2.js CHANGED
@@ -864,6 +864,45 @@ function extractResultText(result) {
864
864
  */
865
865
  const require = createRequire(import.meta.url);
866
866
  const pkg = require("../package.json");
867
+ /**
868
+ * Decide whether an agent-event belongs to THIS turn's run and should be
869
+ * forwarded as a delta / activity frame.
870
+ *
871
+ * Why gate on `runId`, not `sessionKey`:
872
+ * On OpenClaw 2026.x the daemon registers this ('alfe') channel's main run
873
+ * with `isControlUiVisible: false` — only the built-in "webchat" surface is
874
+ * treated as control-UI-visible (`isInternalMessageChannel`). The shared
875
+ * agent-event bus then NULLS `sessionKey` on every event of a
876
+ * non-control-UI run (`agent-events`: `sessionKey = isControlUiVisible ? … :
877
+ * undefined`). So main-run assistant/thinking/tool events arrive with
878
+ * `sessionKey: undefined`, and the old `!evt.sessionKey` + key-equality gate
879
+ * silently dropped ALL activity — replies still landed because they come via
880
+ * the `deliver()` callback, not the event bus (the reported "replies arrive,
881
+ * zero activity" symptom). Verified against openclaw@2026.6.10 and
882
+ * back-compatible with 2026.4.2 (identical emit semantics). `runId` is always
883
+ * present on every emitted event.
884
+ *
885
+ * Isolation is preserved: turns to a daemon are serialised
886
+ * (`enqueueAgentTurn`) and the listener is torn down before the next turn
887
+ * dispatches, so the first relevant event locks onto THIS turn's main run.
888
+ * Concurrent heartbeat/cron runs and subagent / side-question runs carry a
889
+ * different `runId` and are excluded — exactly the leakage the gate exists to
890
+ * prevent. When a run IS control-UI-visible and both keys are present, a
891
+ * mismatched `sessionKey` is still rejected as a secondary defence.
892
+ *
893
+ * Exported for unit tests — this gate is the fix's load-bearing logic and is
894
+ * pinned against real 2026.6.10 event shapes.
895
+ */
896
+ function admitAgentEvent(gate, evt) {
897
+ if (!(evt.stream === "assistant" || evt.stream === "thinking" || evt.stream === "tool")) return false;
898
+ if (gate.runId === null) {
899
+ gate.runId = evt.runId;
900
+ if (evt.sessionKey) gate.sessionKey ??= evt.sessionKey;
901
+ }
902
+ if (evt.runId !== gate.runId) return false;
903
+ if (gate.sessionKey && evt.sessionKey && evt.sessionKey !== gate.sessionKey) return false;
904
+ return true;
905
+ }
867
906
  function asString(v) {
868
907
  return typeof v === "string" && v.length > 0 ? v : void 0;
869
908
  }
@@ -1128,7 +1167,10 @@ async function handleAgentRequest(request, log) {
1128
1167
  const sessionId = conversationId ?? legacySessionKey;
1129
1168
  if (!await getSession(sessionId)) await createSession(sessionId, "", "alfe", tenantId, userId);
1130
1169
  await addMessage(sessionId, "user", message, userId ?? senderId, displayName ?? senderId);
1131
- let resolvedOpenClawKey = null;
1170
+ const runGate = {
1171
+ runId: null,
1172
+ sessionKey: null
1173
+ };
1132
1174
  const UPDATE_THROTTLE_MS = 250;
1133
1175
  const pendingUpdates = /* @__PURE__ */ new Map();
1134
1176
  const flushToolUpdate = (toolCallId) => {
@@ -1143,9 +1185,7 @@ async function handleAgentRequest(request, log) {
1143
1185
  pendingUpdates.clear();
1144
1186
  };
1145
1187
  const unsubscribe = runtime.events.onAgentEvent((evt) => {
1146
- if (!evt.sessionKey) return;
1147
- if (evt.stream === "assistant" || evt.stream === "thinking" || evt.stream === "tool") resolvedOpenClawKey ??= evt.sessionKey;
1148
- if (evt.sessionKey !== resolvedOpenClawKey) return;
1188
+ if (!admitAgentEvent(runGate, evt)) return;
1149
1189
  if (evt.stream === "assistant") {
1150
1190
  chatClient?.sendEvent("chat", {
1151
1191
  runId: evt.runId,
@@ -1223,7 +1263,7 @@ async function handleAgentRequest(request, log) {
1223
1263
  const conversationLabel = conversationType === "group" ? shortConvId ? `[${channelLabel}] Group (${shortConvId})` : `[${channelLabel}] Group` : shortConvId ? `[${channelLabel}] ${userLabel} (${shortConvId})` : `[${channelLabel}] ${userLabel}`;
1224
1264
  let a2aResponseBuffer = "";
1225
1265
  if (isA2A) resetA2AEndSignal();
1226
- resolvedOpenClawKey = (await dispatchInbound({
1266
+ runGate.sessionKey = (await dispatchInbound({
1227
1267
  cfg,
1228
1268
  runtime: { channel: runtime.channel },
1229
1269
  channel: "alfe",
@@ -1276,7 +1316,7 @@ async function handleAgentRequest(request, log) {
1276
1316
  chatClient?.notify("agent-message", {
1277
1317
  conversationId: conversationId ?? legacySessionKey,
1278
1318
  text: responseText,
1279
- sessionKey: resolvedOpenClawKey ?? legacySessionKey,
1319
+ sessionKey: runGate.sessionKey ?? legacySessionKey,
1280
1320
  messageId: chatMessageId ?? request.id,
1281
1321
  ...mediaUrls.length ? { mediaUrls } : {}
1282
1322
  });
@@ -1297,8 +1337,8 @@ async function handleAgentRequest(request, log) {
1297
1337
  resolved: a2aResolved
1298
1338
  }));
1299
1339
  }
1300
- chatClient?.sendResponse(request.id, true, { sessionKey: resolvedOpenClawKey });
1301
- log.info(`Agent dispatch complete: sessionKey=${resolvedOpenClawKey}`);
1340
+ chatClient?.sendResponse(request.id, true, { sessionKey: runGate.sessionKey });
1341
+ log.info(`Agent dispatch complete: sessionKey=${runGate.sessionKey}`);
1302
1342
  } catch (err) {
1303
1343
  const errMsg = err instanceof Error ? err.message : String(err);
1304
1344
  log.error(`Agent dispatch failed: ${errMsg}`);
@@ -1534,4 +1574,4 @@ const plugin = {
1534
1574
  }
1535
1575
  };
1536
1576
  //#endregion
1537
- export { createAlfeChannelPlugin as a, stripLeakedToolSummary as i, buildToolActivity as n, plugin as r, buildA2ACompletePayload as t };
1577
+ export { stripLeakedToolSummary as a, plugin as i, buildA2ACompletePayload as n, createAlfeChannelPlugin as o, buildToolActivity as r, admitAgentEvent as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/openclaw-chat",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "OpenClaw chat plugin for Alfe — web widget and mobile app channels",
5
5
  "type": "module",
6
6
  "main": "./dist/plugin.js",
@@ -28,8 +28,8 @@
28
28
  ],
29
29
  "dependencies": {
30
30
  "@alfe.ai/agent-api-client": "^0.4.0",
31
- "@alfe.ai/chat": "^0.0.13",
32
- "@alfe.ai/config": "^0.2.0"
31
+ "@alfe.ai/chat": "^0.0.14",
32
+ "@alfe.ai/config": "^0.3.0"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "openclaw": ">=2026.3.0"