multiclaws 0.4.36 → 0.4.38

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.
@@ -2,6 +2,53 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OpenClawAgentExecutor = void 0;
4
4
  const gateway_client_1 = require("../infra/gateway-client");
5
+ /* ------------------------------------------------------------------ */
6
+ /* Risk classification */
7
+ /* ------------------------------------------------------------------ */
8
+ /**
9
+ * Heuristic risk classifier. Returns "safe" only when the task is
10
+ * clearly a read-only query; defaults to "risky" for anything ambiguous.
11
+ *
12
+ * This drives the permission gate: risky tasks require explicit human
13
+ * approval before a sub-agent is spawned to execute them.
14
+ */
15
+ function classifyTaskRisk(taskText) {
16
+ const text = taskText.toLowerCase();
17
+ // Explicit risky patterns (write / modify / execute / send)
18
+ const riskyPatterns = [
19
+ // English — word-boundary matched to avoid false positives
20
+ /\b(write|creat|delet|remov|modif|edit|updat|install|execut|deploy|push|commit|send|post|drop|format|rename|overwrite|reset|wipe|destroy|kill|terminat|rm|mkdir|touch|mv)\b/i,
21
+ // Chinese — multi-character phrases to avoid single-char false positives
22
+ // e.g. 安 alone would match 安排(schedule) or 安全(safe)
23
+ /写入|写文件|写邮件|写信|创建|新建|删除|移除|修改|更改|编辑|更新|升级|安装|部署|执行|运行命令|发送|发邮件|提交|推送|重命名|覆盖|重置|清空|清除|销毁|终止|停止服务|kill进程/,
24
+ ];
25
+ // Explicitly safe read-only patterns — checked BEFORE risky to avoid false positives
26
+ // (e.g. "查询并发送报告" is risky overall, but "查询" alone should be safe)
27
+ const safePatterns = [
28
+ // English read-only verbs
29
+ /\b(list|show|get|check|view|read|query|find|search|display|fetch|retriev|look|what|which|count|how many|summariz|describ|explain|analyz|report)\b/i,
30
+ // Chinese read-only verbs (multi-char to be specific)
31
+ /查看|查询|获取|搜索|显示|检查|列出|列举|统计|描述|分析|报告|读取|浏览/,
32
+ // Calendar / scheduling queries
33
+ /\b(calendar|schedule|event|meeting|free|busy|availab|appointment)\b/i,
34
+ /日历|日程|会议|空闲|忙碌|可用时间|时间段|什么时候|哪个时间|安排会议|约会|预约/,
35
+ // Process / system info queries
36
+ /\b(process|pid|cpu|memory|disk|uptime|version|status|running|service|log)\b/i,
37
+ /进程|内存|磁盘|系统状态|运行状态|版本信息|日志|监控/,
38
+ ];
39
+ // Safe check first — if clearly a read query, don't let ambiguous chars trigger risky
40
+ if (safePatterns.some((p) => p.test(text))) {
41
+ return "safe";
42
+ }
43
+ if (riskyPatterns.some((p) => p.test(text))) {
44
+ return "risky";
45
+ }
46
+ // Default: treat as risky if uncertain
47
+ return "risky";
48
+ }
49
+ /* ------------------------------------------------------------------ */
50
+ /* Helpers */
51
+ /* ------------------------------------------------------------------ */
5
52
  function extractTextFromMessage(message) {
6
53
  if (!message.parts)
7
54
  return "";
@@ -15,22 +62,28 @@ function extractTextFromMessage(message) {
15
62
  *
16
63
  * When a remote agent sends a task via A2A `message/send`,
17
64
  * this executor:
18
- * 1. Records the task via TaskTracker
19
- * 2. Calls OpenClaw's `sessions_spawn` (run mode) to start execution
20
- * 3. Waits for the sub-agent to call back via `multiclaws_a2a_callback`
21
- * 4. Returns the final result as a Message
65
+ * 1. Classifies the task risk (safe vs risky)
66
+ * 2. Notifies the local human owner
67
+ * 3. For risky tasks: waits for explicit human approval
68
+ * For safe tasks: executes immediately
69
+ * 4. Calls OpenClaw's `sessions_spawn` (run mode) to start execution
70
+ * 5. Waits for the sub-agent to call back via `multiclaws_a2a_callback`
71
+ * 6. Returns the final result as a Message
22
72
  */
23
73
  class OpenClawAgentExecutor {
24
74
  gatewayConfig;
25
75
  taskTracker;
26
76
  getNotificationTargets;
77
+ registerDiscoveredTarget;
27
78
  logger;
28
79
  cwd;
29
80
  pendingCallbacks = new Map();
81
+ pendingApprovals = new Map();
30
82
  constructor(options) {
31
83
  this.gatewayConfig = options.gatewayConfig;
32
84
  this.taskTracker = options.taskTracker;
33
85
  this.getNotificationTargets = options.getNotificationTargets ?? (() => new Map());
86
+ this.registerDiscoveredTarget = options.registerDiscoveredTarget;
34
87
  this.logger = options.logger;
35
88
  this.cwd = options.cwd || process.cwd();
36
89
  }
@@ -61,10 +114,49 @@ class OpenClawAgentExecutor {
61
114
  eventBus.finished();
62
115
  return;
63
116
  }
64
- // Notify local user about incoming task
65
- const notifyTargets = this.getNotificationTargets();
66
- this.logger.info(`[a2a-adapter] task ${taskId} notifying user (${notifyTargets.size} targets)`);
67
- void this.notifyUser(`📨 收到来自 **${fromAgentName}** 的委派任务:${taskText.slice(0, 200)}`);
117
+ // Classify risk and gate accordingly
118
+ const risk = classifyTaskRisk(taskText);
119
+ this.logger.info(`[a2a-adapter] task ${taskId} risk=${risk}`);
120
+ if (risk === "risky") {
121
+ // Notify with approval request and wait
122
+ const approvalTimeoutMs = 5 * 60 * 1000; // 5 minutes
123
+ const approvalPromise = this.createApprovalCallback(taskId, approvalTimeoutMs);
124
+ this.logger.info(`[a2a-adapter] task ${taskId} requesting human approval (timeout=${approvalTimeoutMs / 1000}s)`);
125
+ void this.notifyUser(buildApprovalRequest(taskId, fromAgentName, taskText));
126
+ let approved;
127
+ try {
128
+ approved = await approvalPromise;
129
+ }
130
+ catch (err) {
131
+ const isCanceled = err instanceof Error && err.message === "canceled";
132
+ if (isCanceled) {
133
+ // Task was explicitly canceled — use the canonical "canceled" message
134
+ this.logger.info(`[a2a-adapter] task ${taskId} canceled during approval wait`);
135
+ this.taskTracker.update(taskId, { status: "failed", error: "canceled" });
136
+ this.publishMessage(eventBus, "Task was canceled.");
137
+ eventBus.finished();
138
+ return;
139
+ }
140
+ // Approval timed out → auto-reject
141
+ approved = false;
142
+ this.logger.warn(`[a2a-adapter] task ${taskId} approval timed out — auto-rejected`);
143
+ }
144
+ if (!approved) {
145
+ const reason = "用户拒绝或未在超时时间内授权。";
146
+ this.logger.info(`[a2a-adapter] task ${taskId} rejected`);
147
+ this.taskTracker.update(taskId, { status: "failed", error: reason });
148
+ this.publishMessage(eventBus, `任务已被拒绝:${reason}`);
149
+ eventBus.finished();
150
+ return;
151
+ }
152
+ this.logger.info(`[a2a-adapter] task ${taskId} approved by user`);
153
+ void this.notifyUser(`✅ 已授权,开始执行来自 **${fromAgentName}** 的任务…`);
154
+ }
155
+ else {
156
+ // Safe task: notify but auto-execute
157
+ this.logger.info(`[a2a-adapter] task ${taskId} safe query — auto-executing`);
158
+ void this.notifyUser(`📨 收到来自 **${fromAgentName}** 的查询任务(安全,自动执行):\n\n${taskText.slice(0, 300)}`);
159
+ }
68
160
  try {
69
161
  // Create a promise that resolves when sub-agent calls multiclaws_a2a_callback
70
162
  const timeoutMs = 180_000;
@@ -88,15 +180,17 @@ class OpenClawAgentExecutor {
88
180
  this.logger.info(`[a2a-adapter] task ${taskId} waiting for callback from sub-agent...`);
89
181
  // Wait for the sub-agent to call back
90
182
  const output = await resultPromise;
91
- // Return result
183
+ // Return result and notify user
92
184
  this.taskTracker.update(taskId, { status: "completed", result: output });
93
185
  this.logger.info(`[a2a-adapter] ✓ task ${taskId} completed — resultLen=${output.length}, preview=${output.slice(0, 120)}`);
186
+ void this.notifyUser(`✅ **来自 ${fromAgentName} 的任务已完成**\n\n${output.slice(0, 800)}`);
94
187
  this.publishMessage(eventBus, output || "Task completed with no output.");
95
188
  }
96
189
  catch (err) {
97
190
  const errorMsg = err instanceof Error ? err.message : String(err);
98
191
  this.logger.error(`[a2a-adapter] ✗ task ${taskId} failed: ${errorMsg}`);
99
192
  this.taskTracker.update(taskId, { status: "failed", error: errorMsg });
193
+ void this.notifyUser(`❌ 来自 **${fromAgentName}** 的任务执行失败:${errorMsg}`);
100
194
  this.publishMessage(eventBus, `Error: ${errorMsg}`);
101
195
  }
102
196
  this.logger.info(`[a2a-adapter] task ${taskId} eventBus.finished()`);
@@ -118,8 +212,32 @@ class OpenClawAgentExecutor {
118
212
  pending.resolve(result);
119
213
  return true;
120
214
  }
215
+ /**
216
+ * Called when the local human owner approves or rejects a pending risky task.
217
+ * Returns true if a pending approval was found.
218
+ */
219
+ resolveApproval(taskId, approved) {
220
+ const pending = this.pendingApprovals.get(taskId);
221
+ if (!pending) {
222
+ this.logger.warn(`[a2a-adapter] resolveApproval: no pending approval for taskId=${taskId}`);
223
+ return false;
224
+ }
225
+ clearTimeout(pending.timer);
226
+ this.pendingApprovals.delete(taskId);
227
+ this.logger.info(`[a2a-adapter] resolveApproval: taskId=${taskId} approved=${approved}`);
228
+ pending.resolve(approved);
229
+ return true;
230
+ }
121
231
  async cancelTask(taskId, eventBus) {
122
232
  this.logger.info(`[a2a-adapter] cancelTask(taskId=${taskId})`);
233
+ // Reject pending approval if any — distinct from user-rejection, uses Error("canceled")
234
+ const approval = this.pendingApprovals.get(taskId);
235
+ if (approval) {
236
+ clearTimeout(approval.timer);
237
+ this.pendingApprovals.delete(taskId);
238
+ approval.reject(new Error("canceled"));
239
+ this.logger.info(`[a2a-adapter] cancelTask: pending approval canceled for taskId=${taskId}`);
240
+ }
123
241
  // Reject pending callback if any
124
242
  const pending = this.pendingCallbacks.get(taskId);
125
243
  if (pending) {
@@ -149,11 +267,78 @@ class OpenClawAgentExecutor {
149
267
  this.pendingCallbacks.set(taskId, { resolve, reject, timer });
150
268
  });
151
269
  }
270
+ /**
271
+ * Create a pending approval that resolves when the human owner responds,
272
+ * or rejects on timeout or cancellation.
273
+ */
274
+ createApprovalCallback(taskId, timeoutMs) {
275
+ return new Promise((resolve, reject) => {
276
+ const timer = setTimeout(() => {
277
+ this.pendingApprovals.delete(taskId);
278
+ this.logger.warn(`[a2a-adapter] task ${taskId} approval timed out after ${timeoutMs / 1000}s`);
279
+ reject(new Error(`approval timed out after ${timeoutMs / 1000}s`));
280
+ }, timeoutMs);
281
+ this.pendingApprovals.set(taskId, { resolve, reject, timer });
282
+ });
283
+ }
284
+ /**
285
+ * Discover the most recently active non-internal session via sessions_list.
286
+ * Used as fallback when no notification targets have been registered yet
287
+ * (e.g. right after a gateway restart before the user sends their first message).
288
+ */
289
+ async discoverActiveSession() {
290
+ if (!this.gatewayConfig)
291
+ return null;
292
+ try {
293
+ const result = await (0, gateway_client_1.invokeGatewayTool)({
294
+ gateway: this.gatewayConfig,
295
+ tool: "sessions_list",
296
+ args: { limit: 10, activeMinutes: 120 },
297
+ timeoutMs: 5_000,
298
+ });
299
+ const INTERNAL_PREFIXES = ["delegate-", "a2a-"];
300
+ const session = result?.sessions?.find((s) => s.sessionKey &&
301
+ !INTERNAL_PREFIXES.some((p) => s.sessionKey.startsWith(p)));
302
+ return session?.sessionKey ?? null;
303
+ }
304
+ catch (err) {
305
+ this.logger.warn(`[a2a-adapter] discoverActiveSession failed: ${err instanceof Error ? err.message : String(err)}`);
306
+ return null;
307
+ }
308
+ }
152
309
  /** Send a notification to all known targets. Individual failures are silently ignored. */
153
310
  async notifyUser(message) {
154
311
  const targets = this.getNotificationTargets();
155
- if (!this.gatewayConfig || targets.size === 0) {
156
- this.logger.info(`[a2a-adapter] notifyUser: skipped (gateway=${!!this.gatewayConfig}, targets=${targets.size})`);
312
+ if (!this.gatewayConfig) {
313
+ this.logger.info(`[a2a-adapter] notifyUser: skipped (no gateway config)`);
314
+ return;
315
+ }
316
+ // Fallback: no registered targets yet (e.g. right after gateway restart).
317
+ // Discover the active session and send directly via sessions_send.
318
+ if (targets.size === 0) {
319
+ this.logger.info(`[a2a-adapter] notifyUser: no registered targets — attempting session discovery`);
320
+ const sessionKey = await this.discoverActiveSession();
321
+ if (sessionKey) {
322
+ this.logger.info(`[a2a-adapter] notifyUser: discovered session ${sessionKey}, sending via sessions_send`);
323
+ try {
324
+ await (0, gateway_client_1.invokeGatewayTool)({
325
+ gateway: this.gatewayConfig,
326
+ tool: "sessions_send",
327
+ args: { sessionKey, message },
328
+ timeoutMs: 5_000,
329
+ });
330
+ // Also register this session for future notifications
331
+ if (this.registerDiscoveredTarget) {
332
+ this.registerDiscoveredTarget(sessionKey);
333
+ }
334
+ }
335
+ catch (err) {
336
+ this.logger.warn(`[a2a-adapter] notifyUser: sessions_send to ${sessionKey} failed: ${err instanceof Error ? err.message : String(err)}`);
337
+ }
338
+ }
339
+ else {
340
+ this.logger.warn(`[a2a-adapter] notifyUser: no active session found, message lost`);
341
+ }
157
342
  return;
158
343
  }
159
344
  const results = await Promise.allSettled([...targets.entries()].map(async ([key, target]) => {
@@ -167,8 +352,10 @@ class OpenClawAgentExecutor {
167
352
  timeoutMs: 5_000,
168
353
  })
169
354
  : (0, gateway_client_1.invokeGatewayTool)({
355
+ // sessions_send injects a message into the session so the AI
356
+ // can relay it to the human (correct tool; was "chat.send" before)
170
357
  gateway: this.gatewayConfig,
171
- tool: "chat.send",
358
+ tool: "sessions_send",
172
359
  args: { sessionKey: target.sessionKey, message },
173
360
  timeoutMs: 5_000,
174
361
  }));
@@ -194,28 +381,50 @@ class OpenClawAgentExecutor {
194
381
  }
195
382
  }
196
383
  exports.OpenClawAgentExecutor = OpenClawAgentExecutor;
384
+ /* ------------------------------------------------------------------ */
385
+ /* Prompt builders */
386
+ /* ------------------------------------------------------------------ */
387
+ /**
388
+ * Build the approval request message injected into the human's active session.
389
+ * The AI in that session will relay it and handle the human's approve/reject response.
390
+ */
391
+ function buildApprovalRequest(taskId, fromAgentName, taskText) {
392
+ const preview = taskText.length > 600 ? taskText.slice(0, 600) + "…" : taskText;
393
+ return `[MultiClaws] 收到来自 **${fromAgentName}** 的委派任务,需要授权
394
+
395
+ **任务内容:**
396
+ ${preview}
397
+
398
+ ⚠️ 该任务涉及写操作或高风险操作,需要您授权才能执行。
399
+
400
+ 请询问用户是否同意执行,并根据回复调用对应工具:
401
+ - 同意:\`multiclaws_task_respond(taskId="${taskId}", approved=true)\`
402
+ - 拒绝:\`multiclaws_task_respond(taskId="${taskId}", approved=false)\`
403
+
404
+ 授权等待时间:5 分钟,超时自动拒绝。`;
405
+ }
197
406
  /**
198
407
  * Build the prompt for the sub-agent that handles an incoming A2A task.
199
408
  * The sub-agent must call `multiclaws_a2a_callback` to report its result.
200
409
  */
201
410
  function buildA2ASubagentPrompt(taskId, taskText) {
202
- return `你收到了一个来自远端智能体的委派任务。请完成任务并汇报结果。
203
-
204
- ## 任务内容
205
-
206
- ${taskText}
207
-
208
- ## 完成后必做
209
-
210
- 完成任务后,你**必须**调用 \`multiclaws_a2a_callback\` 工具汇报结果:
211
-
212
- \`\`\`
213
- multiclaws_a2a_callback(taskId="${taskId}", result="你的完整回复内容")
214
- \`\`\`
215
-
216
- **重要**:
217
- - 无论任务成功还是失败,都必须调用 \`multiclaws_a2a_callback\`
218
- - result 参数填写你的完整回复文本
219
- - 如果任务失败,在 result 中说明失败原因
411
+ return `你收到了一个来自远端智能体的委派任务。请完成任务并汇报结果。
412
+
413
+ ## 任务内容
414
+
415
+ ${taskText}
416
+
417
+ ## 完成后必做
418
+
419
+ 完成任务后,你**必须**调用 \`multiclaws_a2a_callback\` 工具汇报结果:
420
+
421
+ \`\`\`
422
+ multiclaws_a2a_callback(taskId="${taskId}", result="你的完整回复内容")
423
+ \`\`\`
424
+
425
+ **重要**:
426
+ - 无论任务成功还是失败,都必须调用 \`multiclaws_a2a_callback\`
427
+ - result 参数填写你的完整回复文本
428
+ - 如果任务失败,在 result 中说明失败原因
220
429
  - 这是唯一的结果回传方式,不调用则结果会丢失`;
221
430
  }
@@ -133,12 +133,19 @@ export declare class MulticlawsService extends EventEmitter {
133
133
  private extractArtifactText;
134
134
  /** Fetch with up to 2 retries and exponential backoff. */
135
135
  private fetchWithRetry;
136
+ /**
137
+ * Called by the `multiclaws_task_respond` tool when the local human
138
+ * approves or rejects a pending risky incoming task.
139
+ */
140
+ respondToTask(taskId: string, approved: boolean): boolean;
136
141
  /** Resolve a pending A2A callback from sub-agent. */
137
142
  resolveA2ACallback(taskId: string, result: string): boolean;
138
143
  addNotificationTarget(key: string, target: NotificationTarget): void;
139
144
  /** Consistent name for this agent: AgentCard.name or fallback. */
140
145
  private getFormattedName;
141
146
  /** Send a notification to all known targets with detailed logging. */
147
+ /** Discover the most recently active non-internal session via sessions_list. */
148
+ private discoverActiveSession;
142
149
  notifyUser(message: string): Promise<void>;
143
150
  private log;
144
151
  }
@@ -15,6 +15,7 @@ const express_1 = __importDefault(require("express"));
15
15
  const server_1 = require("@a2a-js/sdk/server");
16
16
  const express_2 = require("@a2a-js/sdk/server/express");
17
17
  const client_1 = require("@a2a-js/sdk/client");
18
+ const version_1 = require("../infra/version");
18
19
  const a2a_adapter_1 = require("./a2a-adapter");
19
20
  const agent_registry_1 = require("./agent-registry");
20
21
  const agent_profile_1 = require("./agent-profile");
@@ -30,22 +31,22 @@ function buildDelegationPrompt(agent, task) {
30
31
  const bioSnippet = agent.description
31
32
  ? `\n**智能体能力**: ${agent.description.slice(0, 500)}`
32
33
  : "";
33
- return `## 委派任务
34
- 向远端智能体发送任务并汇报结果。
35
-
36
- **目标智能体**: ${agent.name} (${agent.url})${bioSnippet}
37
- **任务内容**: ${task}
38
-
39
- ## 执行步骤
40
- 1. 调用 multiclaws_delegate_send(agentUrl="${agent.url}", task="${task.replace(/"/g, '\\"')}") 发送任务
41
- 2. 收到回复后,调用 multiclaws_notify(message="结果内容") 将结果推送给用户
42
- 3. 如果需要进一步沟通,可再次调用 multiclaws_delegate_send(最多 5 轮)
43
- 4. 每次收到回复后立即调用 multiclaws_notify 推送进展
44
-
45
- ## 规则
46
- - 使用 multiclaws_delegate_send(不是 multiclaws_delegate)发送任务
47
- - 使用 multiclaws_notify(不是 message)将结果推送给用户
48
- - 最多 5 轮沟通
34
+ return `## 委派任务
35
+ 向远端智能体发送任务并汇报结果。
36
+
37
+ **目标智能体**: ${agent.name} (${agent.url})${bioSnippet}
38
+ **任务内容**: ${task}
39
+
40
+ ## 执行步骤
41
+ 1. 调用 multiclaws_delegate_send(agentUrl="${agent.url}", task="${task.replace(/"/g, '\\"')}") 发送任务
42
+ 2. 收到回复后,调用 multiclaws_notify(message="结果内容") 将结果推送给用户
43
+ 3. 如果需要进一步沟通,可再次调用 multiclaws_delegate_send(最多 5 轮)
44
+ 4. 每次收到回复后立即调用 multiclaws_notify 推送进展
45
+
46
+ ## 规则
47
+ - 使用 multiclaws_delegate_send(不是 multiclaws_delegate)发送任务
48
+ - 使用 multiclaws_notify(不是 message)将结果推送给用户
49
+ - 最多 5 轮沟通
49
50
  - 遇到错误时在 multiclaws_notify 中说明失败原因`;
50
51
  }
51
52
  /* ------------------------------------------------------------------ */
@@ -120,13 +121,16 @@ class MulticlawsService extends node_events_1.EventEmitter {
120
121
  taskTracker: this.taskTracker,
121
122
  cwd: this.resolvedCwd,
122
123
  getNotificationTargets: () => this.notificationTargets,
124
+ registerDiscoveredTarget: (sessionKey) => {
125
+ this.addNotificationTarget(`web:${sessionKey}`, { type: "web", sessionKey });
126
+ },
123
127
  logger,
124
128
  });
125
129
  this.agentCard = {
126
130
  name: profile.ownerName?.trim() ? (0, agent_profile_1.formatAgentCardName)(profile.ownerName.trim()) : "OpenClaw Agent",
127
131
  description: this.profileDescription,
128
132
  url: this.selfUrl,
129
- version: "0.3.0",
133
+ version: version_1.PLUGIN_VERSION,
130
134
  protocolVersion: "0.2.2",
131
135
  defaultInputModes: ["text/plain"],
132
136
  defaultOutputModes: ["text/plain"],
@@ -940,6 +944,20 @@ class MulticlawsService extends node_events_1.EventEmitter {
940
944
  }
941
945
  throw lastError;
942
946
  }
947
+ /**
948
+ * Called by the `multiclaws_task_respond` tool when the local human
949
+ * approves or rejects a pending risky incoming task.
950
+ */
951
+ respondToTask(taskId, approved) {
952
+ this.log("info", `respondToTask(taskId=${taskId}, approved=${approved})`);
953
+ if (!this.agentExecutor) {
954
+ this.log("warn", `respondToTask: no agentExecutor available for taskId=${taskId}`);
955
+ return false;
956
+ }
957
+ const resolved = this.agentExecutor.resolveApproval(taskId, approved);
958
+ this.log("info", `respondToTask: ${resolved ? "✓" : "✗"} taskId=${taskId} ${resolved ? "resolved" : "no pending approval found"}`);
959
+ return resolved;
960
+ }
943
961
  /** Resolve a pending A2A callback from sub-agent. */
944
962
  resolveA2ACallback(taskId, result) {
945
963
  this.log("info", `[a2a-callback] resolveA2ACallback(taskId=${taskId}, resultLen=${result.length})`);
@@ -962,10 +980,54 @@ class MulticlawsService extends node_events_1.EventEmitter {
962
980
  return this.agentCard?.name ?? "OpenClaw Agent";
963
981
  }
964
982
  /** Send a notification to all known targets with detailed logging. */
983
+ /** Discover the most recently active non-internal session via sessions_list. */
984
+ async discoverActiveSession() {
985
+ if (!this.gatewayConfig)
986
+ return null;
987
+ try {
988
+ const result = await (0, gateway_client_1.invokeGatewayTool)({
989
+ gateway: this.gatewayConfig,
990
+ tool: "sessions_list",
991
+ args: { limit: 10, activeMinutes: 120 },
992
+ timeoutMs: 5_000,
993
+ });
994
+ const INTERNAL_PREFIXES = ["delegate-", "a2a-"];
995
+ const session = result?.sessions?.find((s) => s.sessionKey && !INTERNAL_PREFIXES.some((p) => s.sessionKey.startsWith(p)));
996
+ return session?.sessionKey ?? null;
997
+ }
998
+ catch (err) {
999
+ this.log("warn", `discoverActiveSession failed: ${err instanceof Error ? err.message : String(err)}`);
1000
+ return null;
1001
+ }
1002
+ }
965
1003
  async notifyUser(message) {
966
1004
  this.log("info", `notifyUser: targets=${this.notificationTargets.size}, msg=${message.slice(0, 80)}`);
967
- if (!this.gatewayConfig || this.notificationTargets.size === 0) {
968
- this.log("warn", "notifyUser: skipped — no gatewayConfig or no targets");
1005
+ if (!this.gatewayConfig) {
1006
+ this.log("warn", "notifyUser: skipped — no gatewayConfig");
1007
+ return;
1008
+ }
1009
+ // Fallback: no registered targets yet (e.g. right after gateway restart)
1010
+ if (this.notificationTargets.size === 0) {
1011
+ this.log("warn", "notifyUser: no registered targets — attempting session discovery");
1012
+ const sessionKey = await this.discoverActiveSession();
1013
+ if (sessionKey) {
1014
+ this.log("info", `notifyUser: discovered session ${sessionKey}`);
1015
+ try {
1016
+ await (0, gateway_client_1.invokeGatewayTool)({
1017
+ gateway: this.gatewayConfig,
1018
+ tool: "sessions_send",
1019
+ args: { sessionKey, message },
1020
+ timeoutMs: 5_000,
1021
+ });
1022
+ this.addNotificationTarget(`web:${sessionKey}`, { type: "web", sessionKey });
1023
+ }
1024
+ catch (err) {
1025
+ this.log("warn", `notifyUser: sessions_send to ${sessionKey} failed: ${err instanceof Error ? err.message : String(err)}`);
1026
+ }
1027
+ }
1028
+ else {
1029
+ this.log("warn", "notifyUser: no active session found, message lost");
1030
+ }
969
1031
  return;
970
1032
  }
971
1033
  const entries = [...this.notificationTargets.entries()];
@@ -980,8 +1042,10 @@ class MulticlawsService extends node_events_1.EventEmitter {
980
1042
  timeoutMs: 5_000,
981
1043
  })
982
1044
  : (0, gateway_client_1.invokeGatewayTool)({
1045
+ // sessions_send injects a message into the session so the AI
1046
+ // can relay it to the human (correct tool; was "chat.send" before)
983
1047
  gateway: this.gatewayConfig,
984
- tool: "chat.send",
1048
+ tool: "sessions_send",
985
1049
  args: { sessionKey: target.sessionKey, message },
986
1050
  timeoutMs: 5_000,
987
1051
  }));
@@ -1,47 +1,47 @@
1
- {
2
- "id": "multiclaws",
3
- "name": "MultiClaws",
4
- "description": "MultiClaws plugin for multi-instance collaboration using A2A protocol",
5
- "version": "0.3.0",
6
- "skills": ["./skills/multiclaws"],
7
- "configSchema": {
8
- "type": "object",
9
- "additionalProperties": false,
10
- "properties": {
11
- "port": {
12
- "type": "integer",
13
- "minimum": 1,
14
- "maximum": 65535,
15
- "default": 3100
16
- },
17
- "displayName": {
18
- "type": "string",
19
- "minLength": 1
20
- },
21
- "selfUrl": {
22
- "type": "string",
23
- "description": "Publicly reachable URL for this agent. If not set, tunnel configuration is required."
24
- },
25
- "tunnel": {
26
- "type": "object",
27
- "description": "FRP tunnel configuration for cross-network connectivity. Required if selfUrl is not set.",
28
- "properties": {
29
- "type": { "type": "string", "enum": ["frp"] },
30
- "serverAddr": { "type": "string", "description": "FRP server address (IP or domain)" },
31
- "serverPort": { "type": "integer", "default": 7000, "description": "FRP server bind port" },
32
- "token": { "type": "string", "description": "FRP authentication token" },
33
- "portRangeStart": { "type": "integer", "description": "Start of available remote port range" },
34
- "portRangeEnd": { "type": "integer", "description": "End of available remote port range (inclusive)" }
35
- },
36
- "required": ["type", "serverAddr", "serverPort", "token", "portRangeStart", "portRangeEnd"]
37
- },
38
- "telemetry": {
39
- "type": "object",
40
- "additionalProperties": false,
41
- "properties": {
42
- "consoleExporter": { "type": "boolean", "default": false }
43
- }
44
- }
45
- }
46
- }
47
- }
1
+ {
2
+ "id": "multiclaws",
3
+ "name": "MultiClaws",
4
+ "description": "MultiClaws plugin for multi-instance collaboration using A2A protocol",
5
+ "version": "0.3.0",
6
+ "skills": ["./skills/multiclaws", "./skills/meeting-scheduler"],
7
+ "configSchema": {
8
+ "type": "object",
9
+ "additionalProperties": false,
10
+ "properties": {
11
+ "port": {
12
+ "type": "integer",
13
+ "minimum": 1,
14
+ "maximum": 65535,
15
+ "default": 3100
16
+ },
17
+ "displayName": {
18
+ "type": "string",
19
+ "minLength": 1
20
+ },
21
+ "selfUrl": {
22
+ "type": "string",
23
+ "description": "Publicly reachable URL for this agent. If not set, tunnel configuration is required."
24
+ },
25
+ "tunnel": {
26
+ "type": "object",
27
+ "description": "FRP tunnel configuration for cross-network connectivity. Required if selfUrl is not set.",
28
+ "properties": {
29
+ "type": { "type": "string", "enum": ["frp"] },
30
+ "serverAddr": { "type": "string", "description": "FRP server address (IP or domain)" },
31
+ "serverPort": { "type": "integer", "default": 7000, "description": "FRP server bind port" },
32
+ "token": { "type": "string", "description": "FRP authentication token" },
33
+ "portRangeStart": { "type": "integer", "description": "Start of available remote port range" },
34
+ "portRangeEnd": { "type": "integer", "description": "End of available remote port range (inclusive)" }
35
+ },
36
+ "required": ["type", "serverAddr", "serverPort", "token", "portRangeStart", "portRangeEnd"]
37
+ },
38
+ "telemetry": {
39
+ "type": "object",
40
+ "additionalProperties": false,
41
+ "properties": {
42
+ "consoleExporter": { "type": "boolean", "default": false }
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }