multiclaws 0.4.29 → 0.4.31

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.js CHANGED
@@ -200,6 +200,40 @@ function createTools(getService, logger) {
200
200
  }
201
201
  },
202
202
  };
203
+ const multiclawsA2ACallback = {
204
+ name: "multiclaws_a2a_callback",
205
+ description: "Report the result of an incoming A2A delegated task. " +
206
+ "Called by sub-agents spawned to handle remote tasks. " +
207
+ "Do NOT call this directly.",
208
+ parameters: {
209
+ type: "object",
210
+ additionalProperties: false,
211
+ properties: {
212
+ taskId: { type: "string" },
213
+ result: { type: "string" },
214
+ },
215
+ required: ["taskId", "result"],
216
+ },
217
+ execute: async (_toolCallId, args) => {
218
+ const taskId = typeof args.taskId === "string" ? args.taskId.trim() : "";
219
+ const result = typeof args.result === "string" ? args.result : "";
220
+ log("info", `tool:multiclaws_a2a_callback(taskId=${taskId})`);
221
+ try {
222
+ const service = requireService(getService());
223
+ if (!taskId || !result)
224
+ throw new Error("taskId and result are required");
225
+ const resolved = service.resolveA2ACallback(taskId, result);
226
+ if (!resolved) {
227
+ return textResult(`No pending callback for task ${taskId}. It may have timed out.`);
228
+ }
229
+ return textResult(`Task ${taskId} result reported successfully.`);
230
+ }
231
+ catch (err) {
232
+ log("error", `tool:multiclaws_a2a_callback failed: ${err instanceof Error ? err.message : String(err)}`);
233
+ throw err;
234
+ }
235
+ },
236
+ };
203
237
  const multiclawsTaskStatus = {
204
238
  name: "multiclaws_task_status",
205
239
  description: "Check the status of a delegated task.",
@@ -436,6 +470,7 @@ function createTools(getService, logger) {
436
470
  multiclawsRemoveAgent,
437
471
  multiclawsDelegate,
438
472
  multiclawsDelegateSend,
473
+ multiclawsA2ACallback,
439
474
  multiclawsTaskStatus,
440
475
  multiclawsTeamCreate,
441
476
  multiclawsTeamJoin,
@@ -458,16 +493,34 @@ const plugin = {
458
493
  let service = null;
459
494
  // Ensure required tools are in gateway.tools.allow at registration time
460
495
  // so the gateway starts with them already present (no restart needed).
496
+ //
497
+ // Two categories:
498
+ // 1. Adapter-internal tools: sessions_spawn, sessions_history, message
499
+ // — needed by a2a-adapter itself to spawn/poll/notify.
500
+ // 2. A2A execution tools: exec, read, write, glob, grep
501
+ // — needed by spawned sub-agents to actually perform delegated tasks.
502
+ // Without these the sub-agent session hits "permission denied" because
503
+ // gateway.tools.allow restricts which tools the session can invoke.
504
+ //
505
+ // Users can override the execution tool list via plugin config:
506
+ // plugins.multiclaws.a2aAllowedTools: ["exec", "read", "write", ...]
461
507
  if (api.config) {
462
508
  const gw = api.config.gateway;
463
509
  if (gw) {
464
510
  const tools = (gw.tools ?? {});
465
511
  const allow = Array.isArray(tools.allow) ? tools.allow : [];
466
- const required = ["sessions_spawn", "sessions_history", "message"];
512
+ const adapterRequired = ["sessions_spawn", "sessions_history", "message"];
513
+ const defaultA2AExecutionTools = ["exec", "read", "write", "edit", "process"];
514
+ const pluginConf = api.pluginConfig ?? {};
515
+ const a2aExecTools = Array.isArray(pluginConf.a2aAllowedTools)
516
+ ? pluginConf.a2aAllowedTools
517
+ : defaultA2AExecutionTools;
518
+ const required = [...new Set([...adapterRequired, ...a2aExecTools])];
467
519
  const missing = required.filter((t) => !allow.includes(t));
468
520
  if (missing.length > 0) {
469
521
  tools.allow = [...allow, ...missing];
470
522
  gw.tools = tools;
523
+ structured.logger.info(`auto-added gateway tools: ${missing.join(", ")}`);
471
524
  }
472
525
  }
473
526
  }
@@ -542,10 +595,10 @@ const plugin = {
542
595
  api.on("gateway_stop", () => {
543
596
  structured.logger.info("[multiclaws] gateway_stop observed");
544
597
  });
545
- // Track the most recently active channel for notifications
598
+ // Collect all channel IDs for broadcasting notifications
546
599
  api.on("message_received", (_event, ctx) => {
547
600
  if (service && ctx.channelId) {
548
- service.setActiveChannelId(ctx.channelId);
601
+ service.addChannelId(ctx.channelId);
549
602
  }
550
603
  });
551
604
  // Inject onboarding prompt when profile is pending first-run setup
@@ -5,7 +5,7 @@ export type A2AAdapterOptions = {
5
5
  gatewayConfig: GatewayConfig | null;
6
6
  taskTracker: TaskTracker;
7
7
  cwd?: string;
8
- getActiveChannelId?: () => string | null;
8
+ getChannelIds?: () => ReadonlySet<string>;
9
9
  logger: {
10
10
  info: (msg: string) => void;
11
11
  warn: (msg: string) => void;
@@ -19,35 +19,31 @@ export type A2AAdapterOptions = {
19
19
  * this executor:
20
20
  * 1. Records the task via TaskTracker
21
21
  * 2. Calls OpenClaw's `sessions_spawn` (run mode) to start execution
22
- * 3. Polls `sessions_history` until the subagent completes
22
+ * 3. Waits for the sub-agent to call back via `multiclaws_a2a_callback`
23
23
  * 4. Returns the final result as a Message
24
24
  */
25
25
  export declare class OpenClawAgentExecutor implements AgentExecutor {
26
26
  private gatewayConfig;
27
27
  private readonly taskTracker;
28
- private readonly getActiveChannelId;
28
+ private readonly getChannelIds;
29
29
  private readonly logger;
30
30
  private readonly cwd;
31
+ private readonly pendingCallbacks;
31
32
  constructor(options: A2AAdapterOptions);
32
33
  execute(context: RequestContext, eventBus: ExecutionEventBus): Promise<void>;
33
34
  /**
34
- * Poll sessions_history until the subagent session completes.
35
- * Collects ALL assistant text messages and returns them joined.
35
+ * Called by the `multiclaws_a2a_callback` tool when a sub-agent reports its result.
36
+ * Returns true if a pending callback was found and resolved.
36
37
  */
37
- private waitForCompletion;
38
- /**
39
- * Extract all assistant text from session history once the session is complete.
40
- * Returns null if the session is still running.
41
- * Returns all assistant text messages joined (not just the last one).
42
- *
43
- * Gateway /tools/invoke returns: { content: [...], details: { messages: [...], isComplete?: boolean } }
44
- */
45
- private extractCompletedResult;
46
- /** Extract text content from a single history message. */
47
- private extractTextFromHistoryMessage;
38
+ resolveCallback(taskId: string, result: string): boolean;
48
39
  cancelTask(taskId: string, eventBus: ExecutionEventBus): Promise<void>;
49
40
  updateGatewayConfig(config: GatewayConfig): void;
50
- /** Send a notification to the local user via the gateway message tool. */
41
+ /**
42
+ * Create a pending callback that resolves when the sub-agent reports back,
43
+ * or rejects on timeout.
44
+ */
45
+ private createCallback;
46
+ /** Send a notification to all known channels. Individual failures are silently ignored. */
51
47
  private notifyUser;
52
48
  private publishMessage;
53
49
  }
@@ -10,24 +10,6 @@ function extractTextFromMessage(message) {
10
10
  .map((p) => p.text)
11
11
  .join("\n");
12
12
  }
13
- function sleep(ms) {
14
- return new Promise((resolve) => setTimeout(resolve, ms));
15
- }
16
- /**
17
- * Extract the details object from a gateway /tools/invoke result.
18
- * The result shape is: { content: [...], details: { ...actual data... } }
19
- */
20
- function extractDetails(result) {
21
- if (!result || typeof result !== "object")
22
- return null;
23
- const r = result;
24
- // Direct details from /tools/invoke
25
- if (r.details && typeof r.details === "object") {
26
- return r.details;
27
- }
28
- // Fallback: result itself might be the details
29
- return r;
30
- }
31
13
  /**
32
14
  * Bridges the A2A protocol to OpenClaw's sessions_spawn gateway tool.
33
15
  *
@@ -35,19 +17,20 @@ function extractDetails(result) {
35
17
  * this executor:
36
18
  * 1. Records the task via TaskTracker
37
19
  * 2. Calls OpenClaw's `sessions_spawn` (run mode) to start execution
38
- * 3. Polls `sessions_history` until the subagent completes
20
+ * 3. Waits for the sub-agent to call back via `multiclaws_a2a_callback`
39
21
  * 4. Returns the final result as a Message
40
22
  */
41
23
  class OpenClawAgentExecutor {
42
24
  gatewayConfig;
43
25
  taskTracker;
44
- getActiveChannelId;
26
+ getChannelIds;
45
27
  logger;
46
28
  cwd;
29
+ pendingCallbacks = new Map();
47
30
  constructor(options) {
48
31
  this.gatewayConfig = options.gatewayConfig;
49
32
  this.taskTracker = options.taskTracker;
50
- this.getActiveChannelId = options.getActiveChannelId ?? (() => null);
33
+ this.getChannelIds = options.getChannelIds ?? (() => new Set());
51
34
  this.logger = options.logger;
52
35
  this.cwd = options.cwd || process.cwd();
53
36
  }
@@ -76,34 +59,27 @@ class OpenClawAgentExecutor {
76
59
  void this.notifyUser(`📨 收到来自 **${fromAgent}** 的委派任务:${taskText.slice(0, 200)}`);
77
60
  try {
78
61
  this.logger.info(`[a2a-adapter] executing task ${taskId}: ${taskText.slice(0, 100)}`);
79
- // 1. Spawn the subagent
80
- // Use a dedicated session key to avoid inheriting the main session's
81
- // thinking blocks, which would cause "thinking blocks cannot be modified"
82
- // errors from the Claude API.
83
- const spawnResult = await (0, gateway_client_1.invokeGatewayTool)({
62
+ // Create a promise that resolves when sub-agent calls multiclaws_a2a_callback
63
+ const resultPromise = this.createCallback(taskId, 180_000);
64
+ // Spawn the subagent with instructions to call back when done
65
+ const prompt = buildA2ASubagentPrompt(taskId, taskText);
66
+ await (0, gateway_client_1.invokeGatewayTool)({
84
67
  gateway: this.gatewayConfig,
85
68
  tool: "sessions_spawn",
86
69
  args: {
87
- task: taskText,
70
+ task: prompt,
88
71
  mode: "run",
89
72
  cwd: this.cwd,
90
73
  },
91
74
  sessionKey: `a2a-${taskId}`,
92
75
  timeoutMs: 15_000,
93
76
  });
94
- // Extract details from gateway response: { content: [...], details: { childSessionKey, ... } }
95
- const details = extractDetails(spawnResult);
96
- const childSessionKey = details?.childSessionKey;
97
- if (!childSessionKey) {
98
- throw new Error("sessions_spawn did not return a childSessionKey");
99
- }
100
- // 2. Poll for completion
101
- const gatewaySessionKey = `a2a-${taskId}`;
102
- this.logger.info(`[a2a-adapter] task ${taskId} spawned as ${childSessionKey}, waiting for result...`);
103
- const output = await this.waitForCompletion(childSessionKey, 180_000, gatewaySessionKey);
104
- // 3. Return result
77
+ this.logger.info(`[a2a-adapter] task ${taskId} spawned, waiting for callback...`);
78
+ // Wait for the sub-agent to call back
79
+ const output = await resultPromise;
80
+ // Return result
105
81
  this.taskTracker.update(taskId, { status: "completed", result: output });
106
- this.logger.info(`[a2a-adapter] task ${taskId} completed`);
82
+ this.logger.info(`[a2a-adapter] task ${taskId} completed, resultLen=${output.length}`);
107
83
  this.publishMessage(eventBus, output || "Task completed with no output.");
108
84
  }
109
85
  catch (err) {
@@ -115,137 +91,27 @@ class OpenClawAgentExecutor {
115
91
  eventBus.finished();
116
92
  }
117
93
  /**
118
- * Poll sessions_history until the subagent session completes.
119
- * Collects ALL assistant text messages and returns them joined.
120
- */
121
- async waitForCompletion(sessionKey, timeoutMs, gatewaySessionKey) {
122
- this.logger.info(`[a2a-adapter] waitForCompletion(sessionKey=${sessionKey}, timeoutMs=${timeoutMs})`);
123
- const gateway = this.gatewayConfig;
124
- const startTime = Date.now();
125
- let attempt = 0;
126
- const pollDelays = [100, 200, 300, 500];
127
- while (Date.now() - startTime < timeoutMs) {
128
- const delay = pollDelays[Math.min(attempt, pollDelays.length - 1)];
129
- await sleep(delay);
130
- attempt++;
131
- try {
132
- const histResult = await (0, gateway_client_1.invokeGatewayTool)({
133
- gateway,
134
- tool: "sessions_history",
135
- args: {
136
- sessionKey,
137
- limit: 50,
138
- includeTools: false,
139
- },
140
- sessionKey: gatewaySessionKey,
141
- timeoutMs: 8_000,
142
- });
143
- const result = this.extractCompletedResult(histResult);
144
- if (result !== null) {
145
- return result;
146
- }
147
- // Log details every 50 attempts to help diagnose stuck sessions
148
- if (attempt % 50 === 0) {
149
- const details = extractDetails(histResult);
150
- const messages = (details?.messages ?? []);
151
- const lastMsg = messages[messages.length - 1];
152
- this.logger.info(`[a2a-adapter] poll attempt ${attempt}: session ${sessionKey} still running. ` +
153
- `isComplete=${details?.isComplete}, status=${details?.status}, ` +
154
- `msgCount=${messages.length}, lastRole=${lastMsg?.role}, ` +
155
- `lastContentTypes=${JSON.stringify(Array.isArray(lastMsg?.content)
156
- ? lastMsg.content.map((c) => c?.type)
157
- : typeof lastMsg?.content)}`);
158
- }
159
- else {
160
- this.logger.info(`[a2a-adapter] poll attempt ${attempt}: session ${sessionKey} still running...`);
161
- }
162
- }
163
- catch (err) {
164
- this.logger.warn(`[a2a-adapter] poll attempt ${attempt} error: ${err instanceof Error ? err.message : err}`);
165
- }
166
- }
167
- throw new Error(`task timed out after ${Math.round(timeoutMs / 1000)}s waiting for subagent`);
168
- }
169
- /**
170
- * Extract all assistant text from session history once the session is complete.
171
- * Returns null if the session is still running.
172
- * Returns all assistant text messages joined (not just the last one).
173
- *
174
- * Gateway /tools/invoke returns: { content: [...], details: { messages: [...], isComplete?: boolean } }
94
+ * Called by the `multiclaws_a2a_callback` tool when a sub-agent reports its result.
95
+ * Returns true if a pending callback was found and resolved.
175
96
  */
176
- extractCompletedResult(histResult) {
177
- const details = extractDetails(histResult);
178
- if (!details)
179
- return null;
180
- // Check for session-level error/status from gateway
181
- const sessionError = details.error;
182
- const sessionStatus = details.status;
183
- const messages = (details.messages ?? []);
184
- if (messages.length === 0 && !details.isComplete)
185
- return null;
186
- // If session is not explicitly complete, use heuristic: check if the session is still executing
187
- if (details.isComplete !== true) {
188
- if (messages.length === 0)
189
- return null;
190
- const lastMsg = messages[messages.length - 1];
191
- if (lastMsg && Array.isArray(lastMsg.content)) {
192
- const content = lastMsg.content;
193
- const hasToolCalls = content.some((c) => c?.type === "toolCall" || c?.type === "tool_use");
194
- // If the last message only has tool calls (no text), still running
195
- const hasText = content.some((c) => c?.type === "text" && typeof c.text === "string" && c.text.trim());
196
- if (hasToolCalls && !hasText)
197
- return null;
198
- }
199
- // If the last message is a user message, the agent hasn't responded yet
200
- if (lastMsg?.role === "user")
201
- return null;
202
- }
203
- // Session is complete — collect ALL assistant text messages in order
204
- const allTexts = [];
205
- for (const msg of messages) {
206
- if (msg.role !== "assistant")
207
- continue;
208
- const text = this.extractTextFromHistoryMessage(msg);
209
- if (text)
210
- allTexts.push(text);
211
- }
212
- // If we have assistant text, return it (even if there's also an error)
213
- if (allTexts.length > 0) {
214
- // Append error info if present so the delegating agent sees both
215
- if (sessionError) {
216
- allTexts.push(`[session error: ${sessionError}]`);
217
- }
218
- return allTexts.join("\n\n");
219
- }
220
- // No assistant text — check if the session reported an error
221
- if (sessionError) {
222
- return `Error: ${sessionError}`;
223
- }
224
- if (sessionStatus === "failed" || sessionStatus === "error") {
225
- return `Error: session ended with status "${sessionStatus}"`;
226
- }
227
- // Session truly completed with no output at all
228
- return "(task completed with no text output)";
229
- }
230
- /** Extract text content from a single history message. */
231
- extractTextFromHistoryMessage(msg) {
232
- const content = msg.content;
233
- if (typeof content === "string" && content.trim()) {
234
- return content;
235
- }
236
- if (Array.isArray(content)) {
237
- const parts = content;
238
- const textParts = parts
239
- .filter((c) => c?.type === "text" && typeof c.text === "string" && c.text.trim())
240
- .map((c) => c.text);
241
- if (textParts.length > 0) {
242
- return textParts.join("\n");
243
- }
244
- }
245
- return null;
97
+ resolveCallback(taskId, result) {
98
+ const pending = this.pendingCallbacks.get(taskId);
99
+ if (!pending)
100
+ return false;
101
+ clearTimeout(pending.timer);
102
+ this.pendingCallbacks.delete(taskId);
103
+ pending.resolve(result);
104
+ return true;
246
105
  }
247
106
  async cancelTask(taskId, eventBus) {
248
107
  this.logger.info(`[a2a-adapter] cancelTask(taskId=${taskId})`);
108
+ // Reject pending callback if any
109
+ const pending = this.pendingCallbacks.get(taskId);
110
+ if (pending) {
111
+ clearTimeout(pending.timer);
112
+ this.pendingCallbacks.delete(taskId);
113
+ pending.reject(new Error("canceled"));
114
+ }
249
115
  this.taskTracker.update(taskId, { status: "failed", error: "canceled" });
250
116
  this.publishMessage(eventBus, "Task was canceled.");
251
117
  eventBus.finished();
@@ -253,22 +119,30 @@ class OpenClawAgentExecutor {
253
119
  updateGatewayConfig(config) {
254
120
  this.gatewayConfig = config;
255
121
  }
256
- /** Send a notification to the local user via the gateway message tool. */
122
+ /**
123
+ * Create a pending callback that resolves when the sub-agent reports back,
124
+ * or rejects on timeout.
125
+ */
126
+ createCallback(taskId, timeoutMs) {
127
+ return new Promise((resolve, reject) => {
128
+ const timer = setTimeout(() => {
129
+ this.pendingCallbacks.delete(taskId);
130
+ reject(new Error(`task timed out after ${timeoutMs / 1000}s waiting for sub-agent callback`));
131
+ }, timeoutMs);
132
+ this.pendingCallbacks.set(taskId, { resolve, reject, timer });
133
+ });
134
+ }
135
+ /** Send a notification to all known channels. Individual failures are silently ignored. */
257
136
  async notifyUser(message) {
258
- const target = this.getActiveChannelId();
259
- if (!this.gatewayConfig || !target)
137
+ const channels = this.getChannelIds();
138
+ if (!this.gatewayConfig || channels.size === 0)
260
139
  return;
261
- try {
262
- await (0, gateway_client_1.invokeGatewayTool)({
263
- gateway: this.gatewayConfig,
264
- tool: "message",
265
- args: { action: "send", target, message },
266
- timeoutMs: 5_000,
267
- });
268
- }
269
- catch {
270
- this.logger.warn(`[a2a-adapter] notifyUser failed: ${message.slice(0, 80)}`);
271
- }
140
+ await Promise.allSettled([...channels].map((target) => (0, gateway_client_1.invokeGatewayTool)({
141
+ gateway: this.gatewayConfig,
142
+ tool: "message",
143
+ args: { action: "send", target, message },
144
+ timeoutMs: 5_000,
145
+ })));
272
146
  }
273
147
  publishMessage(eventBus, text) {
274
148
  const message = {
@@ -281,3 +155,28 @@ class OpenClawAgentExecutor {
281
155
  }
282
156
  }
283
157
  exports.OpenClawAgentExecutor = OpenClawAgentExecutor;
158
+ /**
159
+ * Build the prompt for the sub-agent that handles an incoming A2A task.
160
+ * The sub-agent must call `multiclaws_a2a_callback` to report its result.
161
+ */
162
+ function buildA2ASubagentPrompt(taskId, taskText) {
163
+ return `你收到了一个来自远端智能体的委派任务。请完成任务并汇报结果。
164
+
165
+ ## 任务内容
166
+
167
+ ${taskText}
168
+
169
+ ## 完成后必做
170
+
171
+ 完成任务后,你**必须**调用 \`multiclaws_a2a_callback\` 工具汇报结果:
172
+
173
+ \`\`\`
174
+ multiclaws_a2a_callback(taskId="${taskId}", result="你的完整回复内容")
175
+ \`\`\`
176
+
177
+ **重要**:
178
+ - 无论任务成功还是失败,都必须调用 \`multiclaws_a2a_callback\`
179
+ - result 参数填写你的完整回复文本
180
+ - 如果任务失败,在 result 中说明失败原因
181
+ - 这是唯一的结果回传方式,不调用则结果会丢失`;
182
+ }
@@ -45,7 +45,7 @@ export declare class MulticlawsService extends EventEmitter {
45
45
  private profileDescription;
46
46
  private readonly gatewayConfig;
47
47
  private readonly resolvedCwd;
48
- private activeChannelId;
48
+ private readonly knownChannelIds;
49
49
  constructor(options: MulticlawsServiceOptions);
50
50
  start(): Promise<void>;
51
51
  stop(): Promise<void>;
@@ -126,9 +126,10 @@ export declare class MulticlawsService extends EventEmitter {
126
126
  private extractArtifactText;
127
127
  /** Fetch with up to 2 retries and exponential backoff. */
128
128
  private fetchWithRetry;
129
- /** Update the most recently active channel for notifications. */
130
- setActiveChannelId(channelId: string): void;
131
- /** Send a notification to the most recently active channel. */
129
+ /** Register a channel ID for notifications. */
130
+ resolveA2ACallback(taskId: string, result: string): boolean;
131
+ addChannelId(channelId: string): void;
132
+ /** Send a notification to all known channels. Individual failures are silently ignored. */
132
133
  private notifyUser;
133
134
  private log;
134
135
  }
@@ -68,7 +68,7 @@ class MulticlawsService extends node_events_1.EventEmitter {
68
68
  profileDescription = "OpenClaw agent";
69
69
  gatewayConfig;
70
70
  resolvedCwd;
71
- activeChannelId = null;
71
+ knownChannelIds = new Set();
72
72
  constructor(options) {
73
73
  super();
74
74
  this.options = options;
@@ -123,7 +123,7 @@ class MulticlawsService extends node_events_1.EventEmitter {
123
123
  gatewayConfig: this.options.gatewayConfig ?? null,
124
124
  taskTracker: this.taskTracker,
125
125
  cwd: this.resolvedCwd,
126
- getActiveChannelId: () => this.activeChannelId,
126
+ getChannelIds: () => this.knownChannelIds,
127
127
  logger,
128
128
  });
129
129
  this.agentCard = {
@@ -919,26 +919,28 @@ class MulticlawsService extends node_events_1.EventEmitter {
919
919
  }
920
920
  throw lastError;
921
921
  }
922
- /** Update the most recently active channel for notifications. */
923
- setActiveChannelId(channelId) {
924
- this.activeChannelId = channelId;
925
- this.log("debug", `activeChannelId set to: ${channelId}`);
922
+ /** Register a channel ID for notifications. */
923
+ resolveA2ACallback(taskId, result) {
924
+ if (!this.agentExecutor)
925
+ return false;
926
+ return this.agentExecutor.resolveCallback(taskId, result);
926
927
  }
927
- /** Send a notification to the most recently active channel. */
928
+ addChannelId(channelId) {
929
+ if (!this.knownChannelIds.has(channelId)) {
930
+ this.knownChannelIds.add(channelId);
931
+ this.log("debug", `channel registered: ${channelId} (total: ${this.knownChannelIds.size})`);
932
+ }
933
+ }
934
+ /** Send a notification to all known channels. Individual failures are silently ignored. */
928
935
  async notifyUser(message) {
929
- if (!this.gatewayConfig || !this.activeChannelId)
936
+ if (!this.gatewayConfig || this.knownChannelIds.size === 0)
930
937
  return;
931
- try {
932
- await (0, gateway_client_1.invokeGatewayTool)({
933
- gateway: this.gatewayConfig,
934
- tool: "message",
935
- args: { action: "send", target: this.activeChannelId, message },
936
- timeoutMs: 5_000,
937
- });
938
- }
939
- catch (err) {
940
- this.log("warn", `notifyUser failed: ${err instanceof Error ? err.message : String(err)} | msg=${message.slice(0, 80)}`);
941
- }
938
+ await Promise.allSettled([...this.knownChannelIds].map((target) => (0, gateway_client_1.invokeGatewayTool)({
939
+ gateway: this.gatewayConfig,
940
+ tool: "message",
941
+ args: { action: "send", target, message },
942
+ timeoutMs: 5_000,
943
+ })));
942
944
  }
943
945
  log(level, message) {
944
946
  this.options.logger?.[level]?.(`[multiclaws] ${message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multiclaws",
3
- "version": "0.4.29",
3
+ "version": "0.4.31",
4
4
  "description": "MultiClaws plugin for OpenClaw collaboration via A2A protocol",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -15,6 +15,12 @@
15
15
  "openclaw.plugin.json",
16
16
  "README.md"
17
17
  ],
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "clean": "rm -rf dist"
23
+ },
18
24
  "keywords": [
19
25
  "openclaw",
20
26
  "plugin",
@@ -46,11 +52,5 @@
46
52
  "@types/proper-lockfile": "^4.1.4",
47
53
  "typescript": "^5.9.2",
48
54
  "vitest": "^3.2.4"
49
- },
50
- "scripts": {
51
- "build": "tsc -p tsconfig.json",
52
- "test": "vitest run",
53
- "test:watch": "vitest",
54
- "clean": "rm -rf dist"
55
55
  }
56
- }
56
+ }