multiclaws 0.4.40 → 0.4.41

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.
@@ -16,16 +16,16 @@ export type A2AAdapterOptions = {
16
16
  };
17
17
  };
18
18
  /**
19
- * Bridges the A2A protocol to OpenClaw's sessions_spawn gateway tool.
19
+ * Bridges the A2A protocol to OpenClaw's session injection mechanism.
20
20
  *
21
21
  * When a remote agent sends a task via A2A `message/send`,
22
22
  * this executor:
23
23
  * 1. Classifies the task risk (safe vs risky)
24
- * 2. Notifies the local human owner
25
- * 3. For risky tasks: waits for explicit human approval
26
- * For safe tasks: executes immediately
27
- * 4. Calls OpenClaw's `sessions_spawn` (run mode) to start execution
28
- * 5. Waits for the sub-agent to call back via `multiclaws_a2a_callback`
24
+ * 2. For risky tasks: pushes approval request to the user's active session and waits
25
+ * For safe tasks: proceeds immediately
26
+ * 3. Finds the target session (where user last sent a message, or main session)
27
+ * 4. Injects the task into that session via sessions_send — no isolated sub-session created
28
+ * 5. Waits for the session AI to call back via `multiclaws_a2a_callback`
29
29
  * 6. Returns the final result as a Message
30
30
  */
31
31
  export declare class OpenClawAgentExecutor implements AgentExecutor {
@@ -61,6 +61,13 @@ export declare class OpenClawAgentExecutor implements AgentExecutor {
61
61
  * or rejects on timeout or cancellation.
62
62
  */
63
63
  private createApprovalCallback;
64
+ /**
65
+ * Find the best target session for task injection:
66
+ * 1. Prefer the session where the user most recently sent a message (role === "user")
67
+ * 2. Fall back to the first non-internal active session (typically the main webchat session)
68
+ * Never returns internal sessions (delegate-*, a2a-*).
69
+ */
70
+ private findTargetSession;
64
71
  /**
65
72
  * Discover the most recently active non-internal session via sessions_list.
66
73
  * Used as fallback when no notification targets have been registered yet
@@ -58,16 +58,16 @@ function extractTextFromMessage(message) {
58
58
  .join("\n");
59
59
  }
60
60
  /**
61
- * Bridges the A2A protocol to OpenClaw's sessions_spawn gateway tool.
61
+ * Bridges the A2A protocol to OpenClaw's session injection mechanism.
62
62
  *
63
63
  * When a remote agent sends a task via A2A `message/send`,
64
64
  * this executor:
65
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`
66
+ * 2. For risky tasks: pushes approval request to the user's active session and waits
67
+ * For safe tasks: proceeds immediately
68
+ * 3. Finds the target session (where user last sent a message, or main session)
69
+ * 4. Injects the task into that session via sessions_send — no isolated sub-session created
70
+ * 5. Waits for the session AI to call back via `multiclaws_a2a_callback`
71
71
  * 6. Returns the final result as a Message
72
72
  */
73
73
  class OpenClawAgentExecutor {
@@ -114,24 +114,25 @@ class OpenClawAgentExecutor {
114
114
  eventBus.finished();
115
115
  return;
116
116
  }
117
- // Classify risk and gate accordingly
117
+ // ── Step 1: Risk classification ──
118
118
  const risk = classifyTaskRisk(taskText);
119
- this.logger.info(`[a2a-adapter] task ${taskId} risk=${risk}`);
119
+ this.logger.info(`[a2a-adapter] task ${taskId} [step:risk-classify] risk=${risk}, text="${taskText.slice(0, 60)}"`);
120
120
  if (risk === "risky") {
121
- // Notify with approval request and wait
121
+ // ── Step 2a: Approval gate (risky tasks only) ──
122
122
  const approvalTimeoutMs = 5 * 60 * 1000; // 5 minutes
123
123
  const approvalPromise = this.createApprovalCallback(taskId, approvalTimeoutMs);
124
- this.logger.info(`[a2a-adapter] task ${taskId} requesting human approval (timeout=${approvalTimeoutMs / 1000}s)`);
124
+ this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-request] sending approval request to user (timeout=${approvalTimeoutMs / 1000}s)`);
125
125
  void this.notifyUser(buildApprovalRequest(taskId, fromAgentName, taskText));
126
126
  let approved;
127
127
  try {
128
128
  approved = await approvalPromise;
129
+ this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-result] user responded: approved=${approved}`);
129
130
  }
130
131
  catch (err) {
132
+ const errMsg = err instanceof Error ? err.message : String(err);
131
133
  const isCanceled = err instanceof Error && err.message === "canceled";
132
134
  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.logger.info(`[a2a-adapter] task ${taskId} [step:approval-result] caught "canceled" error aborting task`);
135
136
  this.taskTracker.update(taskId, { status: "failed", error: "canceled" });
136
137
  this.publishMessage(eventBus, "Task was canceled.");
137
138
  eventBus.finished();
@@ -139,61 +140,66 @@ class OpenClawAgentExecutor {
139
140
  }
140
141
  // Approval timed out → auto-reject
141
142
  approved = false;
142
- this.logger.warn(`[a2a-adapter] task ${taskId} approval timed out auto-rejected`);
143
+ this.logger.warn(`[a2a-adapter] task ${taskId} [step:approval-result] caught error: ${errMsg} → treating as auto-reject`);
143
144
  }
144
145
  if (!approved) {
145
146
  const reason = "用户拒绝或未在超时时间内授权。";
146
- this.logger.info(`[a2a-adapter] task ${taskId} rejected`);
147
+ this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-rejected] → aborting task, reason: ${reason}`);
147
148
  this.taskTracker.update(taskId, { status: "failed", error: reason });
148
149
  this.publishMessage(eventBus, `任务已被拒绝:${reason}`);
149
150
  eventBus.finished();
150
151
  return;
151
152
  }
152
- this.logger.info(`[a2a-adapter] task ${taskId} approved by user`);
153
- void this.notifyUser(`✅ 已授权,开始执行来自 **${fromAgentName}** 的任务…`);
153
+ this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-passed] proceeding to find target session`);
154
154
  }
155
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)}`);
156
+ this.logger.info(`[a2a-adapter] task ${taskId} [step:auto-execute] safe query, skipping approval → proceeding to find target session`);
159
157
  }
158
+ // ── Step 3: Find target session ──
159
+ this.logger.info(`[a2a-adapter] task ${taskId} [step:find-session] calling findTargetSession()`);
160
+ const targetSessionKey = await this.findTargetSession();
161
+ if (!targetSessionKey) {
162
+ const errMsg = "无法找到用户活跃 session,任务未执行。请确保至少有一个活跃的对话 session。";
163
+ this.logger.error(`[a2a-adapter] task ${taskId} [step:find-session] ✗ no target session found → aborting task`);
164
+ this.taskTracker.update(taskId, { status: "failed", error: errMsg });
165
+ this.publishMessage(eventBus, errMsg);
166
+ eventBus.finished();
167
+ return;
168
+ }
169
+ this.logger.info(`[a2a-adapter] task ${taskId} [step:find-session] ✓ target session = ${targetSessionKey}`);
160
170
  try {
161
- // Create a promise that resolves when sub-agent calls multiclaws_a2a_callback
171
+ // ── Step 4: Register callback ──
162
172
  const timeoutMs = 180_000;
163
173
  const resultPromise = this.createCallback(taskId, timeoutMs);
164
- this.logger.info(`[a2a-adapter] task ${taskId} callback registered (timeout=${timeoutMs / 1000}s)`);
165
- // Spawn the subagent with instructions to call back when done
166
- const prompt = buildA2ASubagentPrompt(taskId, taskText);
167
- this.logger.info(`[a2a-adapter] task ${taskId} spawning sub-agent via sessions_spawn (cwd=${this.cwd}, sessionKey=a2a-${taskId})`);
168
- const spawnResult = await (0, gateway_client_1.invokeGatewayTool)({
174
+ this.logger.info(`[a2a-adapter] task ${taskId} [step:register-callback] callback registered (timeout=${timeoutMs / 1000}s, pending total=${this.pendingCallbacks.size})`);
175
+ // ── Step 5: Inject task into target session ──
176
+ const prompt = buildA2AMainSessionPrompt(taskId, fromAgentName, taskText);
177
+ this.logger.info(`[a2a-adapter] task ${taskId} [step:inject-task] calling sessions_send(sessionKey=${targetSessionKey}, promptLen=${prompt.length})`);
178
+ await (0, gateway_client_1.invokeGatewayTool)({
169
179
  gateway: this.gatewayConfig,
170
- tool: "sessions_spawn",
171
- args: {
172
- task: prompt,
173
- mode: "run",
174
- cwd: this.cwd,
175
- },
176
- sessionKey: `a2a-${taskId}`,
180
+ tool: "sessions_send",
181
+ args: { sessionKey: targetSessionKey, message: prompt },
177
182
  timeoutMs: 15_000,
178
183
  });
179
- this.logger.info(`[a2a-adapter] task ${taskId} sub-agent spawned result=${JSON.stringify(spawnResult).slice(0, 200)}`);
180
- this.logger.info(`[a2a-adapter] task ${taskId} waiting for callback from sub-agent...`);
181
- // Wait for the sub-agent to call back
184
+ this.logger.info(`[a2a-adapter] task ${taskId} [step:inject-task] sessions_send succeeded → waiting for callback...`);
185
+ // ── Step 6: Wait for callback ──
182
186
  const output = await resultPromise;
183
- // Return result and notify user
187
+ // ── Step 7: Return result ──
184
188
  this.taskTracker.update(taskId, { status: "completed", result: output });
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)}`);
189
+ this.logger.info(`[a2a-adapter] task ${taskId} [step:completed] resultLen=${output.length}, preview="${output.slice(0, 120)}"`);
187
190
  this.publishMessage(eventBus, output || "Task completed with no output.");
188
191
  }
189
192
  catch (err) {
190
193
  const errorMsg = err instanceof Error ? err.message : String(err);
191
- this.logger.error(`[a2a-adapter] task ${taskId} failed: ${errorMsg}`);
194
+ const isCanceled = err instanceof Error && err.message === "canceled";
195
+ const isTimeout = errorMsg.includes("timed out");
196
+ const errorType = isCanceled ? "canceled" : isTimeout ? "timeout" : "error";
197
+ this.logger.error(`[a2a-adapter] task ${taskId} [step:catch] ✗ type=${errorType}, reason: ${errorMsg} → marking failed, notifying user`);
192
198
  this.taskTracker.update(taskId, { status: "failed", error: errorMsg });
193
199
  void this.notifyUser(`❌ 来自 **${fromAgentName}** 的任务执行失败:${errorMsg}`);
194
200
  this.publishMessage(eventBus, `Error: ${errorMsg}`);
195
201
  }
196
- this.logger.info(`[a2a-adapter] task ${taskId} eventBus.finished()`);
202
+ this.logger.info(`[a2a-adapter] task ${taskId} [step:finished] eventBus.finished()`);
197
203
  eventBus.finished();
198
204
  }
199
205
  /**
@@ -281,15 +287,81 @@ class OpenClawAgentExecutor {
281
287
  this.pendingApprovals.set(taskId, { resolve, reject, timer });
282
288
  });
283
289
  }
290
+ /**
291
+ * Find the best target session for task injection:
292
+ * 1. Prefer the session where the user most recently sent a message (role === "user")
293
+ * 2. Fall back to the first non-internal active session (typically the main webchat session)
294
+ * Never returns internal sessions (delegate-*, a2a-*).
295
+ */
296
+ async findTargetSession() {
297
+ if (!this.gatewayConfig) {
298
+ this.logger.warn(`[a2a-adapter] findTargetSession: skipped — no gateway config`);
299
+ return null;
300
+ }
301
+ try {
302
+ this.logger.info(`[a2a-adapter] findTargetSession: calling sessions_list (limit=20, activeMinutes=1440)`);
303
+ const raw = await (0, gateway_client_1.invokeGatewayTool)({
304
+ gateway: this.gatewayConfig,
305
+ tool: "sessions_list",
306
+ args: { limit: 20, activeMinutes: 1440, messageLimit: 3 },
307
+ timeoutMs: 5_000,
308
+ });
309
+ this.logger.info(`[a2a-adapter] findTargetSession: raw result = ${JSON.stringify(raw).slice(0, 500)}`);
310
+ // Unwrap gateway tool standard response: { content: [{ type: "text", text: "..." }] }
311
+ let parsed = raw;
312
+ if (raw?.content?.[0]?.type === "text") {
313
+ try {
314
+ parsed = JSON.parse(raw.content[0].text);
315
+ this.logger.info(`[a2a-adapter] findTargetSession: unwrapped gateway response successfully`);
316
+ }
317
+ catch (parseErr) {
318
+ this.logger.warn(`[a2a-adapter] findTargetSession: failed to parse content[0].text as JSON — ${parseErr instanceof Error ? parseErr.message : String(parseErr)}, using raw object`);
319
+ }
320
+ }
321
+ const INTERNAL_PREFIXES = ["delegate-", "a2a-"];
322
+ const sessions = parsed?.sessions ?? [];
323
+ this.logger.info(`[a2a-adapter] findTargetSession: ${sessions.length} total sessions from gateway`);
324
+ const filtered = sessions.filter((s) => {
325
+ const k = (s.key ?? s.sessionKey);
326
+ return k && !INTERNAL_PREFIXES.some((p) => k.startsWith(p));
327
+ });
328
+ this.logger.info(`[a2a-adapter] findTargetSession: ${filtered.length} non-internal sessions after filtering`);
329
+ // Prefer sessions that have at least one user-originated message
330
+ const withUserMsg = filtered.filter((s) => Array.isArray(s.messages) && s.messages.some((m) => m.role === "user"));
331
+ // Fall back to any non-internal session (likely the main webchat session)
332
+ const target = withUserMsg[0] ?? filtered[0];
333
+ const targetKey = (target?.key ?? target?.sessionKey);
334
+ if (targetKey) {
335
+ const source = withUserMsg.length > 0 ? "user-message session" : "fallback non-internal session";
336
+ this.logger.info(`[a2a-adapter] findTargetSession: ✓ matched ${targetKey} (${source}, ${withUserMsg.length} with user msgs, ${filtered.length} total)`);
337
+ }
338
+ else {
339
+ this.logger.warn(`[a2a-adapter] findTargetSession: ✗ no target found (${sessions.length} raw, ${filtered.length} after filter, ${withUserMsg.length} with user msgs)`);
340
+ sessions.forEach((s, i) => {
341
+ const k = (s.key ?? s.sessionKey) ?? "(no key)";
342
+ const msgCount = Array.isArray(s.messages) ? s.messages.length : 0;
343
+ this.logger.info(`[a2a-adapter] findTargetSession: session[${i}]: key=${k}, messages=${msgCount}`);
344
+ });
345
+ }
346
+ return targetKey ?? null;
347
+ }
348
+ catch (err) {
349
+ this.logger.error(`[a2a-adapter] findTargetSession: ✗ caught error — ${err instanceof Error ? err.message : String(err)}, returning null`);
350
+ return null;
351
+ }
352
+ }
284
353
  /**
285
354
  * Discover the most recently active non-internal session via sessions_list.
286
355
  * Used as fallback when no notification targets have been registered yet
287
356
  * (e.g. right after a gateway restart before the user sends their first message).
288
357
  */
289
358
  async discoverActiveSession() {
290
- if (!this.gatewayConfig)
359
+ if (!this.gatewayConfig) {
360
+ this.logger.warn(`[a2a-adapter] discoverActiveSession: skipped — no gateway config`);
291
361
  return null;
362
+ }
292
363
  try {
364
+ this.logger.info(`[a2a-adapter] discoverActiveSession: calling sessions_list (limit=10, activeMinutes=120)`);
293
365
  const raw = await (0, gateway_client_1.invokeGatewayTool)({
294
366
  gateway: this.gatewayConfig,
295
367
  tool: "sessions_list",
@@ -302,8 +374,11 @@ class OpenClawAgentExecutor {
302
374
  if (raw?.content?.[0]?.type === "text") {
303
375
  try {
304
376
  parsed = JSON.parse(raw.content[0].text);
377
+ this.logger.info(`[a2a-adapter] discoverActiveSession: unwrapped gateway response successfully`);
378
+ }
379
+ catch (parseErr) {
380
+ this.logger.warn(`[a2a-adapter] discoverActiveSession: failed to parse content[0].text as JSON — ${parseErr instanceof Error ? parseErr.message : String(parseErr)}, using raw object`);
305
381
  }
306
- catch { /* use raw */ }
307
382
  }
308
383
  const sessions = parsed?.sessions ?? [];
309
384
  this.logger.info(`[a2a-adapter] discoverActiveSession: found ${sessions.length} sessions`);
@@ -315,33 +390,34 @@ class OpenClawAgentExecutor {
315
390
  });
316
391
  const matchedKey = (session?.key ?? session?.sessionKey);
317
392
  if (matchedKey) {
318
- this.logger.info(`[a2a-adapter] discoverActiveSession: matched session ${matchedKey}`);
393
+ this.logger.info(`[a2a-adapter] discoverActiveSession: matched session ${matchedKey}`);
319
394
  }
320
395
  else {
321
- this.logger.warn(`[a2a-adapter] discoverActiveSession: all ${sessions.length} sessions filtered or empty`);
322
- sessions.forEach((s) => this.logger.info(`[a2a-adapter] session: ${(s.key ?? s.sessionKey) ?? "(no key)"}`));
396
+ this.logger.warn(`[a2a-adapter] discoverActiveSession: all ${sessions.length} sessions filtered or empty`);
397
+ sessions.forEach((s, i) => this.logger.info(`[a2a-adapter] discoverActiveSession: session[${i}]: key=${(s.key ?? s.sessionKey) ?? "(no key)"}`));
323
398
  }
324
399
  return matchedKey ?? null;
325
400
  }
326
401
  catch (err) {
327
- this.logger.warn(`[a2a-adapter] discoverActiveSession failed: ${err instanceof Error ? err.message : String(err)}`);
402
+ this.logger.error(`[a2a-adapter] discoverActiveSession: ✗ caught error — ${err instanceof Error ? err.message : String(err)}, returning null`);
328
403
  return null;
329
404
  }
330
405
  }
331
406
  /** Send a notification to all known targets. Individual failures are silently ignored. */
332
407
  async notifyUser(message) {
333
408
  const targets = this.getNotificationTargets();
409
+ this.logger.info(`[a2a-adapter] notifyUser: targets=${targets.size}, msgLen=${message.length}, preview="${message.slice(0, 80)}"`);
334
410
  if (!this.gatewayConfig) {
335
- this.logger.info(`[a2a-adapter] notifyUser: skipped (no gateway config)`);
411
+ this.logger.warn(`[a2a-adapter] notifyUser: skipped no gateway config, message lost`);
336
412
  return;
337
413
  }
338
414
  // Fallback: no registered targets yet (e.g. right after gateway restart).
339
415
  // Discover the active session and send directly via sessions_send.
340
416
  if (targets.size === 0) {
341
- this.logger.info(`[a2a-adapter] notifyUser: no registered targets attempting session discovery`);
342
- const sessionKey = await this.discoverActiveSession();
417
+ this.logger.info(`[a2a-adapter] notifyUser: no registered targets falling back to findTargetSession()`);
418
+ const sessionKey = await this.findTargetSession();
343
419
  if (sessionKey) {
344
- this.logger.info(`[a2a-adapter] notifyUser: discovered session ${sessionKey}, sending via sessions_send`);
420
+ this.logger.info(`[a2a-adapter] notifyUser: fallback discovered session ${sessionKey} calling sessions_send`);
345
421
  try {
346
422
  await (0, gateway_client_1.invokeGatewayTool)({
347
423
  gateway: this.gatewayConfig,
@@ -349,22 +425,25 @@ class OpenClawAgentExecutor {
349
425
  args: { sessionKey, message },
350
426
  timeoutMs: 5_000,
351
427
  });
428
+ this.logger.info(`[a2a-adapter] notifyUser: ✓ fallback sessions_send to ${sessionKey} succeeded`);
352
429
  // Also register this session for future notifications
353
430
  if (this.registerDiscoveredTarget) {
354
431
  this.registerDiscoveredTarget(sessionKey);
432
+ this.logger.info(`[a2a-adapter] notifyUser: registered ${sessionKey} as notification target for future use`);
355
433
  }
356
434
  }
357
435
  catch (err) {
358
- this.logger.warn(`[a2a-adapter] notifyUser: sessions_send to ${sessionKey} failed: ${err instanceof Error ? err.message : String(err)}`);
436
+ this.logger.error(`[a2a-adapter] notifyUser: ✗ fallback sessions_send to ${sessionKey} failed: ${err instanceof Error ? err.message : String(err)}`);
359
437
  }
360
438
  }
361
439
  else {
362
- this.logger.warn(`[a2a-adapter] notifyUser: no active session found, message lost`);
440
+ this.logger.warn(`[a2a-adapter] notifyUser: ✗ findTargetSession returned null — no active session found, message lost`);
363
441
  }
364
442
  return;
365
443
  }
444
+ this.logger.info(`[a2a-adapter] notifyUser: sending to ${targets.size} registered target(s): [${[...targets.keys()].join(", ")}]`);
366
445
  const results = await Promise.allSettled([...targets.entries()].map(async ([key, target]) => {
367
- this.logger.info(`[a2a-adapter] notifyUser: sending to ${key} (type=${target.type})`);
446
+ this.logger.info(`[a2a-adapter] notifyUser: ${key} (type=${target.type})`);
368
447
  try {
369
448
  await (target.type === "channel"
370
449
  ? (0, gateway_client_1.invokeGatewayTool)({
@@ -374,23 +453,26 @@ class OpenClawAgentExecutor {
374
453
  timeoutMs: 5_000,
375
454
  })
376
455
  : (0, gateway_client_1.invokeGatewayTool)({
377
- // sessions_send injects a message into the session so the AI
378
- // can relay it to the human (correct tool; was "chat.send" before)
379
456
  gateway: this.gatewayConfig,
380
457
  tool: "sessions_send",
381
458
  args: { sessionKey: target.sessionKey, message },
382
459
  timeoutMs: 5_000,
383
460
  }));
384
- this.logger.info(`[a2a-adapter] notifyUser: sent to ${key} ✓`);
461
+ this.logger.info(`[a2a-adapter] notifyUser: ${key} (${target.type}) succeeded`);
385
462
  }
386
463
  catch (err) {
387
- this.logger.warn(`[a2a-adapter] notifyUser: failed to send to ${key}: ${err instanceof Error ? err.message : String(err)}`);
464
+ this.logger.error(`[a2a-adapter] notifyUser: ${key} (${target.type}) failed: ${err instanceof Error ? err.message : String(err)}`);
388
465
  throw err;
389
466
  }
390
467
  }));
391
468
  const ok = results.filter((r) => r.status === "fulfilled").length;
392
469
  const fail = results.filter((r) => r.status === "rejected").length;
393
- this.logger.info(`[a2a-adapter] notifyUser: done (${ok} ok, ${fail} failed)`);
470
+ if (fail === 0) {
471
+ this.logger.info(`[a2a-adapter] notifyUser: ✓ all ${ok} targets succeeded`);
472
+ }
473
+ else {
474
+ this.logger.error(`[a2a-adapter] notifyUser: done — ${ok} ok, ${fail} FAILED out of ${ok + fail} targets`);
475
+ }
394
476
  }
395
477
  publishMessage(eventBus, text) {
396
478
  const message = {
@@ -426,27 +508,18 @@ ${preview}
426
508
  授权等待时间:5 分钟,超时自动拒绝。`;
427
509
  }
428
510
  /**
429
- * Build the prompt for the sub-agent that handles an incoming A2A task.
430
- * The sub-agent must call `multiclaws_a2a_callback` to report its result.
511
+ * Build the prompt injected into the user's active main session for an incoming A2A task.
512
+ * The AI in that session processes the task naturally and must call multiclaws_a2a_callback.
431
513
  */
432
- function buildA2ASubagentPrompt(taskId, taskText) {
433
- return `你收到了一个来自远端智能体的委派任务。请完成任务并汇报结果。
434
-
435
- ## 任务内容
514
+ function buildA2AMainSessionPrompt(taskId, fromAgentName, taskText) {
515
+ return `[MultiClaws 委派任务] 来自 **${fromAgentName}**:
436
516
 
437
517
  ${taskText}
438
518
 
439
- ## 完成后必做
440
-
441
- 完成任务后,你**必须**调用 \`multiclaws_a2a_callback\` 工具汇报结果:
442
-
443
- \`\`\`
444
- multiclaws_a2a_callback(taskId="${taskId}", result="你的完整回复内容")
445
- \`\`\`
519
+ ---
520
+ 完成后请调用 \`multiclaws_a2a_callback\` 汇报结果:
521
+ - taskId: "${taskId}"
522
+ - result: 你的完整回复内容
446
523
 
447
- **重要**:
448
- - 无论任务成功还是失败,都必须调用 \`multiclaws_a2a_callback\`
449
- - result 参数填写你的完整回复文本
450
- - 如果任务失败,在 result 中说明失败原因
451
- - 这是唯一的结果回传方式,不调用则结果会丢失`;
524
+ 无论成功还是失败都必须调用,这是结果回传给委派方的唯一方式。`;
452
525
  }
@@ -143,7 +143,6 @@ export declare class MulticlawsService extends EventEmitter {
143
143
  addNotificationTarget(key: string, target: NotificationTarget): void;
144
144
  /** Consistent name for this agent: AgentCard.name or fallback. */
145
145
  private getFormattedName;
146
- /** Send a notification to all known targets with detailed logging. */
147
146
  /** Discover the most recently active non-internal session via sessions_list. */
148
147
  private discoverActiveSession;
149
148
  notifyUser(message: string): Promise<void>;
@@ -256,31 +256,41 @@ class MulticlawsService extends node_events_1.EventEmitter {
256
256
  /* ---------------------------------------------------------------- */
257
257
  async delegateTask(params) {
258
258
  this.log("info", `[delegate] ▶ delegateTask(agentUrl=${params.agentUrl}, taskLen=${params.task.length})`);
259
- this.log("info", `[delegate] task preview: ${params.task.slice(0, 120)}`);
260
- await this.requireCompleteProfile();
259
+ this.log("info", `[delegate] task preview: "${params.task.slice(0, 120)}"`);
260
+ // Step 1: Check profile
261
+ this.log("info", `[delegate] [step:profile-check] verifying profile completeness`);
262
+ try {
263
+ await this.requireCompleteProfile();
264
+ }
265
+ catch (err) {
266
+ this.log("error", `[delegate] [step:profile-check] ✗ profile incomplete: ${err instanceof Error ? err.message : String(err)}`);
267
+ return { status: "failed", error: err instanceof Error ? err.message : String(err) };
268
+ }
269
+ // Step 2: Look up agent
270
+ this.log("info", `[delegate] [step:agent-lookup] looking up agent: ${params.agentUrl}`);
261
271
  const agentRecord = await this.agentRegistry.get(params.agentUrl);
262
272
  if (!agentRecord) {
263
- this.log("warn", `[delegate] ✗ unknown agent: ${params.agentUrl}`);
273
+ this.log("warn", `[delegate] [step:agent-lookup] ✗ unknown agent: ${params.agentUrl} → aborting`);
264
274
  return { status: "failed", error: `unknown agent: ${params.agentUrl}` };
265
275
  }
266
- this.log("info", `[delegate] agent found: ${agentRecord.name} (${agentRecord.url})`);
276
+ this.log("info", `[delegate] [step:agent-lookup] found: ${agentRecord.name} (${agentRecord.url})`);
277
+ // Step 3: Track task
267
278
  const track = this.taskTracker.create({
268
279
  fromPeerId: "local",
269
280
  toPeerId: params.agentUrl,
270
281
  task: params.task,
271
282
  });
272
283
  this.taskTracker.update(track.taskId, { status: "running" });
273
- this.log("info", `[delegate] task tracked: ${track.taskId}, status=running`);
284
+ this.log("info", `[delegate] [step:track] taskId=${track.taskId}, status=running`);
274
285
  try {
275
- this.log("info", `[delegate] ${track.taskId} creating A2A client for ${agentRecord.url}`);
286
+ // Step 4: Create A2A client
287
+ this.log("info", `[delegate] ${track.taskId} [step:create-client] creating A2A client for ${agentRecord.url}`);
276
288
  const client = await this.createA2AClient(agentRecord);
277
- this.log("info", `[delegate] ${track.taskId} A2A client created, starting fire-and-forget send`);
278
- // Fire-and-forget execution: keep running in the background so that
279
- // the gateway call can return quickly and the task can outlive
280
- // the gateway's HTTP timeout.
289
+ this.log("info", `[delegate] ${track.taskId} [step:create-client] client created starting fire-and-forget send`);
290
+ // Step 5: Fire-and-forget execution
281
291
  void (async () => {
282
292
  try {
283
- this.log("info", `[delegate] ${track.taskId} sending A2A message (background)...`);
293
+ this.log("info", `[delegate] ${track.taskId} [step:background-send] sending A2A message to ${agentRecord.name}...`);
284
294
  const result = await client.sendMessage({
285
295
  message: {
286
296
  kind: "message",
@@ -289,24 +299,22 @@ class MulticlawsService extends node_events_1.EventEmitter {
289
299
  messageId: track.taskId,
290
300
  },
291
301
  });
292
- this.log("info", `[delegate] ${track.taskId} A2A response received (background)`);
302
+ this.log("info", `[delegate] ${track.taskId} [step:background-send] ✓ A2A response received → processing result`);
293
303
  this.processTaskResult(track.taskId, result);
294
304
  }
295
305
  catch (err) {
296
306
  const errorMsg = err instanceof Error ? err.message : String(err);
297
307
  this.taskTracker.update(track.taskId, { status: "failed", error: errorMsg });
298
- this.log("error", `[delegate] ${track.taskId} background send failed: ${errorMsg}`);
308
+ this.log("error", `[delegate] ${track.taskId} [step:background-send] ✗ caught error: ${errorMsg} → task marked failed`);
299
309
  }
300
310
  })();
301
- // Return immediately so that gateway tool invocations are fast and
302
- // do not depend on the remote agent's total execution time.
303
- this.log("info", `[delegate] ${track.taskId} returned immediately (fire-and-forget)`);
311
+ this.log("info", `[delegate] ${track.taskId} [step:return] returned immediately (fire-and-forget), background send in progress`);
304
312
  return { taskId: track.taskId, status: "running" };
305
313
  }
306
314
  catch (err) {
307
315
  const errorMsg = err instanceof Error ? err.message : String(err);
308
316
  this.taskTracker.update(track.taskId, { status: "failed", error: errorMsg });
309
- this.log("error", `[delegate] ${track.taskId} failed: ${errorMsg}`);
317
+ this.log("error", `[delegate] ${track.taskId} [step:catch] ✗ caught error during client creation: ${errorMsg} → task marked failed`);
310
318
  return { taskId: track.taskId, status: "failed", error: errorMsg };
311
319
  }
312
320
  }
@@ -316,25 +324,38 @@ class MulticlawsService extends node_events_1.EventEmitter {
316
324
  */
317
325
  async delegateTaskSync(params) {
318
326
  this.log("info", `[delegate-sync] ▶ delegateTaskSync(agentUrl=${params.agentUrl}, taskLen=${params.task.length})`);
319
- this.log("info", `[delegate-sync] task preview: ${params.task.slice(0, 120)}`);
320
- await this.requireCompleteProfile();
327
+ this.log("info", `[delegate-sync] task preview: "${params.task.slice(0, 120)}"`);
328
+ // Step 1: Check profile
329
+ this.log("info", `[delegate-sync] [step:profile-check] verifying profile completeness`);
330
+ try {
331
+ await this.requireCompleteProfile();
332
+ }
333
+ catch (err) {
334
+ this.log("error", `[delegate-sync] [step:profile-check] ✗ profile incomplete: ${err instanceof Error ? err.message : String(err)}`);
335
+ return { status: "failed", error: err instanceof Error ? err.message : String(err) };
336
+ }
337
+ // Step 2: Look up agent
338
+ this.log("info", `[delegate-sync] [step:agent-lookup] looking up agent: ${params.agentUrl}`);
321
339
  const agentRecord = await this.agentRegistry.get(params.agentUrl);
322
340
  if (!agentRecord) {
323
- this.log("warn", `[delegate-sync] ✗ unknown agent: ${params.agentUrl}`);
341
+ this.log("warn", `[delegate-sync] [step:agent-lookup] ✗ unknown agent: ${params.agentUrl} → aborting`);
324
342
  return { status: "failed", error: `unknown agent: ${params.agentUrl}` };
325
343
  }
326
- this.log("info", `[delegate-sync] agent found: ${agentRecord.name} (${agentRecord.url})`);
344
+ this.log("info", `[delegate-sync] [step:agent-lookup] found: ${agentRecord.name} (${agentRecord.url})`);
345
+ // Step 3: Track task
327
346
  const track = this.taskTracker.create({
328
347
  fromPeerId: "local",
329
348
  toPeerId: params.agentUrl,
330
349
  task: params.task,
331
350
  });
332
351
  this.taskTracker.update(track.taskId, { status: "running" });
333
- this.log("info", `[delegate-sync] task tracked: ${track.taskId}, status=running`);
352
+ this.log("info", `[delegate-sync] [step:track] taskId=${track.taskId}, status=running`);
334
353
  try {
335
- this.log("info", `[delegate-sync] ${track.taskId} creating A2A client for ${agentRecord.url}`);
354
+ // Step 4: Create A2A client
355
+ this.log("info", `[delegate-sync] ${track.taskId} [step:create-client] creating A2A client for ${agentRecord.url}`);
336
356
  const client = await this.createA2AClient(agentRecord);
337
- this.log("info", `[delegate-sync] ${track.taskId} sending A2A message (sync, with metadata: selfUrl=${this.selfUrl}, selfName=${this.agentCard?.name ?? "unknown"})...`);
357
+ // Step 5: Send A2A message (synchronous blocks until response)
358
+ this.log("info", `[delegate-sync] ${track.taskId} [step:send] sending A2A message (sync, metadata: selfUrl=${this.selfUrl}, selfName=${this.agentCard?.name ?? "unknown"})...`);
338
359
  const result = await client.sendMessage({
339
360
  message: {
340
361
  kind: "message",
@@ -347,15 +368,16 @@ class MulticlawsService extends node_events_1.EventEmitter {
347
368
  },
348
369
  },
349
370
  });
350
- this.log("info", `[delegate-sync] ${track.taskId} A2A response received`);
371
+ this.log("info", `[delegate-sync] ${track.taskId} [step:send] ✓ A2A response received → processing result`);
372
+ // Step 6: Process result
351
373
  const taskResult = this.processTaskResult(track.taskId, result);
352
- this.log("info", `[delegate-sync] ${track.taskId} completed status=${taskResult.status}, outputLen=${taskResult.output?.length ?? 0}`);
374
+ this.log("info", `[delegate-sync] ${track.taskId} [step:completed] status=${taskResult.status}, outputLen=${taskResult.output?.length ?? 0}, preview="${(taskResult.output ?? "").slice(0, 120)}"`);
353
375
  return taskResult;
354
376
  }
355
377
  catch (err) {
356
378
  const errorMsg = err instanceof Error ? err.message : String(err);
357
379
  this.taskTracker.update(track.taskId, { status: "failed", error: errorMsg });
358
- this.log("error", `[delegate-sync] ${track.taskId} failed: ${errorMsg}`);
380
+ this.log("error", `[delegate-sync] ${track.taskId} [step:catch] ✗ caught error: ${errorMsg} → task marked failed`);
359
381
  return { taskId: track.taskId, status: "failed", error: errorMsg };
360
382
  }
361
383
  }
@@ -366,30 +388,48 @@ class MulticlawsService extends node_events_1.EventEmitter {
366
388
  */
367
389
  async spawnDelegation(params) {
368
390
  this.log("info", `[spawn-delegate] ▶ spawnDelegation(agentUrl=${params.agentUrl}, taskLen=${params.task.length})`);
369
- this.log("info", `[spawn-delegate] task preview: ${params.task.slice(0, 120)}`);
370
- await this.requireCompleteProfile();
391
+ this.log("info", `[spawn-delegate] task preview: "${params.task.slice(0, 120)}"`);
392
+ // Step 1: Check profile
393
+ this.log("info", `[spawn-delegate] [step:profile-check] verifying profile completeness`);
394
+ try {
395
+ await this.requireCompleteProfile();
396
+ }
397
+ catch (err) {
398
+ this.log("error", `[spawn-delegate] [step:profile-check] ✗ profile incomplete: ${err instanceof Error ? err.message : String(err)}`);
399
+ throw err;
400
+ }
401
+ // Step 2: Look up agent
402
+ this.log("info", `[spawn-delegate] [step:agent-lookup] looking up agent: ${params.agentUrl}`);
371
403
  const agent = await this.agentRegistry.get(params.agentUrl);
372
404
  if (!agent) {
373
- this.log("warn", `[spawn-delegate] ✗ unknown agent: ${params.agentUrl}`);
405
+ this.log("warn", `[spawn-delegate] [step:agent-lookup] ✗ unknown agent: ${params.agentUrl} → aborting`);
374
406
  throw new Error(`unknown agent: ${params.agentUrl}`);
375
407
  }
376
- this.log("info", `[spawn-delegate] agent found: ${agent.name} (${agent.url})`);
408
+ this.log("info", `[spawn-delegate] [step:agent-lookup] found: ${agent.name} (${agent.url})`);
409
+ // Step 3: Check gateway config
377
410
  if (!this.gatewayConfig) {
378
- this.log("error", `[spawn-delegate] ✗ gateway config not available`);
411
+ this.log("error", `[spawn-delegate] [step:gateway-check] ✗ gateway config not available → aborting`);
379
412
  throw new Error("gateway config not available — cannot spawn sub-agent");
380
413
  }
414
+ // Step 4: Spawn sub-agent
381
415
  const prompt = buildDelegationPrompt(agent, params.task);
382
416
  const sessionKey = `delegate-${Date.now()}`;
383
- this.log("info", `[spawn-delegate] spawning sub-agent via sessions_spawn (cwd=${this.resolvedCwd}, sessionKey=${sessionKey}, promptLen=${prompt.length})`);
384
- const spawnResult = await (0, gateway_client_1.invokeGatewayTool)({
385
- gateway: this.gatewayConfig,
386
- tool: "sessions_spawn",
387
- args: { task: prompt, mode: "run", cwd: this.resolvedCwd },
388
- sessionKey,
389
- timeoutMs: 15_000,
390
- });
391
- this.log("info", `[spawn-delegate] ✓ sub-agent spawned for ${agent.name} — result=${JSON.stringify(spawnResult).slice(0, 200)}`);
392
- return { message: `已启动子 agent ${agent.name} 委派任务` };
417
+ this.log("info", `[spawn-delegate] [step:spawn] calling sessions_spawn (cwd=${this.resolvedCwd}, sessionKey=${sessionKey}, promptLen=${prompt.length})`);
418
+ try {
419
+ const spawnResult = await (0, gateway_client_1.invokeGatewayTool)({
420
+ gateway: this.gatewayConfig,
421
+ tool: "sessions_spawn",
422
+ args: { task: prompt, mode: "run", cwd: this.resolvedCwd },
423
+ sessionKey,
424
+ timeoutMs: 15_000,
425
+ });
426
+ this.log("info", `[spawn-delegate] [step:spawn] sub-agent spawned for ${agent.name} result=${JSON.stringify(spawnResult).slice(0, 200)}`);
427
+ return { message: `已启动子 agent 向 ${agent.name} 委派任务` };
428
+ }
429
+ catch (err) {
430
+ this.log("error", `[spawn-delegate] [step:spawn] ✗ sessions_spawn failed: ${err instanceof Error ? err.message : String(err)} → aborting`);
431
+ throw err;
432
+ }
393
433
  }
394
434
  getTaskStatus(taskId) {
395
435
  return this.taskTracker.get(taskId);
@@ -871,7 +911,16 @@ class MulticlawsService extends node_events_1.EventEmitter {
871
911
  }
872
912
  }
873
913
  async createA2AClient(agent) {
874
- return await this.clientFactory.createFromUrl(agent.url);
914
+ this.log("info", `[a2a-client] creating client for ${agent.name} (${agent.url})`);
915
+ try {
916
+ const client = await this.clientFactory.createFromUrl(agent.url);
917
+ this.log("info", `[a2a-client] ✓ client created for ${agent.name} (${agent.url})`);
918
+ return client;
919
+ }
920
+ catch (err) {
921
+ this.log("error", `[a2a-client] ✗ failed to create client for ${agent.name} (${agent.url}): ${err instanceof Error ? err.message : String(err)}`);
922
+ throw err;
923
+ }
875
924
  }
876
925
  /**
877
926
  * Send a message using A2A streaming to minimize latency.
@@ -979,12 +1028,14 @@ class MulticlawsService extends node_events_1.EventEmitter {
979
1028
  getFormattedName() {
980
1029
  return this.agentCard?.name ?? "OpenClaw Agent";
981
1030
  }
982
- /** Send a notification to all known targets with detailed logging. */
983
1031
  /** Discover the most recently active non-internal session via sessions_list. */
984
1032
  async discoverActiveSession() {
985
- if (!this.gatewayConfig)
1033
+ if (!this.gatewayConfig) {
1034
+ this.log("warn", `discoverActiveSession: skipped — no gateway config`);
986
1035
  return null;
1036
+ }
987
1037
  try {
1038
+ this.log("info", `discoverActiveSession: calling sessions_list (limit=10, activeMinutes=120)`);
988
1039
  const raw = await (0, gateway_client_1.invokeGatewayTool)({
989
1040
  gateway: this.gatewayConfig,
990
1041
  tool: "sessions_list",
@@ -997,11 +1048,14 @@ class MulticlawsService extends node_events_1.EventEmitter {
997
1048
  if (raw?.content?.[0]?.type === "text") {
998
1049
  try {
999
1050
  parsed = JSON.parse(raw.content[0].text);
1051
+ this.log("info", `discoverActiveSession: unwrapped gateway response successfully`);
1052
+ }
1053
+ catch (parseErr) {
1054
+ this.log("warn", `discoverActiveSession: failed to parse content[0].text as JSON — ${parseErr instanceof Error ? parseErr.message : String(parseErr)}, using raw object`);
1000
1055
  }
1001
- catch { /* use raw */ }
1002
1056
  }
1003
1057
  const sessions = parsed?.sessions ?? [];
1004
- this.log("info", `discoverActiveSession: found ${sessions.length} sessions`);
1058
+ this.log("info", `discoverActiveSession: found ${sessions.length} sessions from gateway`);
1005
1059
  const INTERNAL_PREFIXES = ["delegate-", "a2a-"];
1006
1060
  // sessions_list returns "key" not "sessionKey"
1007
1061
  const session = sessions.find((s) => {
@@ -1010,31 +1064,31 @@ class MulticlawsService extends node_events_1.EventEmitter {
1010
1064
  });
1011
1065
  const matchedKey = (session?.key ?? session?.sessionKey);
1012
1066
  if (matchedKey) {
1013
- this.log("info", `discoverActiveSession: matched session ${matchedKey}`);
1067
+ this.log("info", `discoverActiveSession: matched session ${matchedKey}`);
1014
1068
  }
1015
1069
  else {
1016
- this.log("warn", `discoverActiveSession: all ${sessions.length} sessions filtered or empty`);
1017
- sessions.forEach((s) => this.log("info", ` session: ${(s.key ?? s.sessionKey) ?? "(no key)"}`));
1070
+ this.log("warn", `discoverActiveSession: all ${sessions.length} sessions filtered or empty`);
1071
+ sessions.forEach((s, i) => this.log("info", `discoverActiveSession: session[${i}]: key=${(s.key ?? s.sessionKey) ?? "(no key)"}`));
1018
1072
  }
1019
1073
  return matchedKey ?? null;
1020
1074
  }
1021
1075
  catch (err) {
1022
- this.log("warn", `discoverActiveSession failed: ${err instanceof Error ? err.message : String(err)}`);
1076
+ this.log("error", `discoverActiveSession: ✗ caught error — ${err instanceof Error ? err.message : String(err)}, returning null`);
1023
1077
  return null;
1024
1078
  }
1025
1079
  }
1026
1080
  async notifyUser(message) {
1027
- this.log("info", `notifyUser: targets=${this.notificationTargets.size}, msg=${message.slice(0, 80)}`);
1081
+ this.log("info", `notifyUser: targets=${this.notificationTargets.size}, msgLen=${message.length}, preview="${message.slice(0, 80)}"`);
1028
1082
  if (!this.gatewayConfig) {
1029
- this.log("warn", "notifyUser: skipped — no gatewayConfig");
1083
+ this.log("warn", "notifyUser: skipped — no gatewayConfig, message lost");
1030
1084
  return;
1031
1085
  }
1032
1086
  // Fallback: no registered targets yet (e.g. right after gateway restart)
1033
1087
  if (this.notificationTargets.size === 0) {
1034
- this.log("warn", "notifyUser: no registered targets attempting session discovery");
1088
+ this.log("info", "notifyUser: no registered targets falling back to discoverActiveSession()");
1035
1089
  const sessionKey = await this.discoverActiveSession();
1036
1090
  if (sessionKey) {
1037
- this.log("info", `notifyUser: discovered session ${sessionKey}`);
1091
+ this.log("info", `notifyUser: fallback discovered session ${sessionKey} → calling sessions_send`);
1038
1092
  try {
1039
1093
  await (0, gateway_client_1.invokeGatewayTool)({
1040
1094
  gateway: this.gatewayConfig,
@@ -1042,20 +1096,23 @@ class MulticlawsService extends node_events_1.EventEmitter {
1042
1096
  args: { sessionKey, message },
1043
1097
  timeoutMs: 5_000,
1044
1098
  });
1099
+ this.log("info", `notifyUser: ✓ fallback sessions_send to ${sessionKey} succeeded`);
1045
1100
  this.addNotificationTarget(`web:${sessionKey}`, { type: "web", sessionKey });
1101
+ this.log("info", `notifyUser: registered ${sessionKey} as notification target for future use`);
1046
1102
  }
1047
1103
  catch (err) {
1048
- this.log("warn", `notifyUser: sessions_send to ${sessionKey} failed: ${err instanceof Error ? err.message : String(err)}`);
1104
+ this.log("error", `notifyUser: ✗ fallback sessions_send to ${sessionKey} failed: ${err instanceof Error ? err.message : String(err)}`);
1049
1105
  }
1050
1106
  }
1051
1107
  else {
1052
- this.log("warn", "notifyUser: no active session found, message lost");
1108
+ this.log("warn", "notifyUser: ✗ discoverActiveSession returned null — no active session found, message lost");
1053
1109
  }
1054
1110
  return;
1055
1111
  }
1056
1112
  const entries = [...this.notificationTargets.entries()];
1113
+ this.log("info", `notifyUser: sending to ${entries.length} registered target(s): [${entries.map(([k]) => k).join(", ")}]`);
1057
1114
  const results = await Promise.allSettled(entries.map(async ([key, target]) => {
1058
- this.log("info", `notifyUser: sending to ${key} (type=${target.type})`);
1115
+ this.log("info", `notifyUser: ${key} (type=${target.type})`);
1059
1116
  try {
1060
1117
  await (target.type === "channel"
1061
1118
  ? (0, gateway_client_1.invokeGatewayTool)({
@@ -1065,26 +1122,28 @@ class MulticlawsService extends node_events_1.EventEmitter {
1065
1122
  timeoutMs: 5_000,
1066
1123
  })
1067
1124
  : (0, gateway_client_1.invokeGatewayTool)({
1068
- // sessions_send injects a message into the session so the AI
1069
- // can relay it to the human (correct tool; was "chat.send" before)
1070
1125
  gateway: this.gatewayConfig,
1071
1126
  tool: "sessions_send",
1072
1127
  args: { sessionKey: target.sessionKey, message },
1073
1128
  timeoutMs: 5_000,
1074
1129
  }));
1075
- this.log("info", `notifyUser: ${key} (${target.type}) succeeded`);
1130
+ this.log("info", `notifyUser: ${key} (${target.type}) succeeded`);
1076
1131
  }
1077
1132
  catch (err) {
1078
- this.log("warn", `notifyUser: ${key} (${target.type}) failed: ${err instanceof Error ? err.message : String(err)}`);
1133
+ this.log("error", `notifyUser: ${key} (${target.type}) failed: ${err instanceof Error ? err.message : String(err)}`);
1079
1134
  throw err;
1080
1135
  }
1081
1136
  }));
1137
+ const okCount = results.filter((r) => r.status === "fulfilled").length;
1082
1138
  const failCount = results.filter((r) => r.status === "rejected").length;
1083
- if (failCount === entries.length) {
1084
- this.log("error", `notifyUser: ALL ${failCount} targets failed`);
1139
+ if (failCount === 0) {
1140
+ this.log("info", `notifyUser: all ${okCount} targets succeeded`);
1141
+ }
1142
+ else if (failCount === entries.length) {
1143
+ this.log("error", `notifyUser: ✗ ALL ${failCount} targets failed`);
1085
1144
  }
1086
- else if (failCount > 0) {
1087
- this.log("warn", `notifyUser: ${failCount}/${entries.length} targets failed`);
1145
+ else {
1146
+ this.log("warn", `notifyUser: ${okCount} ok, ${failCount} FAILED out of ${entries.length} targets`);
1088
1147
  }
1089
1148
  }
1090
1149
  log(level, message) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multiclaws",
3
- "version": "0.4.40",
3
+ "version": "0.4.41",
4
4
  "description": "MultiClaws plugin for OpenClaw collaboration via A2A protocol",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",