multiclaws 0.4.39 → 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,54 +287,137 @@ 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 {
293
- const result = await (0, gateway_client_1.invokeGatewayTool)({
364
+ this.logger.info(`[a2a-adapter] discoverActiveSession: calling sessions_list (limit=10, activeMinutes=120)`);
365
+ const raw = await (0, gateway_client_1.invokeGatewayTool)({
294
366
  gateway: this.gatewayConfig,
295
367
  tool: "sessions_list",
296
368
  args: { limit: 10, activeMinutes: 120 },
297
369
  timeoutMs: 5_000,
298
370
  });
299
- this.logger.info(`[a2a-adapter] discoverActiveSession: raw result = ${JSON.stringify(result).slice(0, 500)}`);
300
- const sessions = result?.sessions ?? [];
371
+ this.logger.info(`[a2a-adapter] discoverActiveSession: raw result = ${JSON.stringify(raw).slice(0, 500)}`);
372
+ // Unwrap gateway tool standard response: { content: [{ type: "text", text: "..." }] }
373
+ let parsed = raw;
374
+ if (raw?.content?.[0]?.type === "text") {
375
+ try {
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`);
381
+ }
382
+ }
383
+ const sessions = parsed?.sessions ?? [];
301
384
  this.logger.info(`[a2a-adapter] discoverActiveSession: found ${sessions.length} sessions`);
302
385
  const INTERNAL_PREFIXES = ["delegate-", "a2a-"];
303
- const session = sessions.find((s) => s.sessionKey && !INTERNAL_PREFIXES.some((p) => s.sessionKey.startsWith(p)));
304
- if (session) {
305
- this.logger.info(`[a2a-adapter] discoverActiveSession: matched session ${session.sessionKey}`);
386
+ // sessions_list returns "key" not "sessionKey"
387
+ const session = sessions.find((s) => {
388
+ const k = (s.key ?? s.sessionKey);
389
+ return k && !INTERNAL_PREFIXES.some((p) => k.startsWith(p));
390
+ });
391
+ const matchedKey = (session?.key ?? session?.sessionKey);
392
+ if (matchedKey) {
393
+ this.logger.info(`[a2a-adapter] discoverActiveSession: ✓ matched session ${matchedKey}`);
306
394
  }
307
395
  else {
308
- this.logger.warn(`[a2a-adapter] discoverActiveSession: all ${sessions.length} sessions filtered or empty`);
309
- sessions.forEach((s) => this.logger.info(`[a2a-adapter] session: ${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)"}`));
310
398
  }
311
- return session?.sessionKey ?? null;
399
+ return matchedKey ?? null;
312
400
  }
313
401
  catch (err) {
314
- 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`);
315
403
  return null;
316
404
  }
317
405
  }
318
406
  /** Send a notification to all known targets. Individual failures are silently ignored. */
319
407
  async notifyUser(message) {
320
408
  const targets = this.getNotificationTargets();
409
+ this.logger.info(`[a2a-adapter] notifyUser: targets=${targets.size}, msgLen=${message.length}, preview="${message.slice(0, 80)}"`);
321
410
  if (!this.gatewayConfig) {
322
- this.logger.info(`[a2a-adapter] notifyUser: skipped (no gateway config)`);
411
+ this.logger.warn(`[a2a-adapter] notifyUser: skipped no gateway config, message lost`);
323
412
  return;
324
413
  }
325
414
  // Fallback: no registered targets yet (e.g. right after gateway restart).
326
415
  // Discover the active session and send directly via sessions_send.
327
416
  if (targets.size === 0) {
328
- this.logger.info(`[a2a-adapter] notifyUser: no registered targets attempting session discovery`);
329
- 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();
330
419
  if (sessionKey) {
331
- 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`);
332
421
  try {
333
422
  await (0, gateway_client_1.invokeGatewayTool)({
334
423
  gateway: this.gatewayConfig,
@@ -336,22 +425,25 @@ class OpenClawAgentExecutor {
336
425
  args: { sessionKey, message },
337
426
  timeoutMs: 5_000,
338
427
  });
428
+ this.logger.info(`[a2a-adapter] notifyUser: ✓ fallback sessions_send to ${sessionKey} succeeded`);
339
429
  // Also register this session for future notifications
340
430
  if (this.registerDiscoveredTarget) {
341
431
  this.registerDiscoveredTarget(sessionKey);
432
+ this.logger.info(`[a2a-adapter] notifyUser: registered ${sessionKey} as notification target for future use`);
342
433
  }
343
434
  }
344
435
  catch (err) {
345
- 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)}`);
346
437
  }
347
438
  }
348
439
  else {
349
- 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`);
350
441
  }
351
442
  return;
352
443
  }
444
+ this.logger.info(`[a2a-adapter] notifyUser: sending to ${targets.size} registered target(s): [${[...targets.keys()].join(", ")}]`);
353
445
  const results = await Promise.allSettled([...targets.entries()].map(async ([key, target]) => {
354
- this.logger.info(`[a2a-adapter] notifyUser: sending to ${key} (type=${target.type})`);
446
+ this.logger.info(`[a2a-adapter] notifyUser: ${key} (type=${target.type})`);
355
447
  try {
356
448
  await (target.type === "channel"
357
449
  ? (0, gateway_client_1.invokeGatewayTool)({
@@ -361,23 +453,26 @@ class OpenClawAgentExecutor {
361
453
  timeoutMs: 5_000,
362
454
  })
363
455
  : (0, gateway_client_1.invokeGatewayTool)({
364
- // sessions_send injects a message into the session so the AI
365
- // can relay it to the human (correct tool; was "chat.send" before)
366
456
  gateway: this.gatewayConfig,
367
457
  tool: "sessions_send",
368
458
  args: { sessionKey: target.sessionKey, message },
369
459
  timeoutMs: 5_000,
370
460
  }));
371
- this.logger.info(`[a2a-adapter] notifyUser: sent to ${key} ✓`);
461
+ this.logger.info(`[a2a-adapter] notifyUser: ${key} (${target.type}) succeeded`);
372
462
  }
373
463
  catch (err) {
374
- 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)}`);
375
465
  throw err;
376
466
  }
377
467
  }));
378
468
  const ok = results.filter((r) => r.status === "fulfilled").length;
379
469
  const fail = results.filter((r) => r.status === "rejected").length;
380
- 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
+ }
381
476
  }
382
477
  publishMessage(eventBus, text) {
383
478
  const message = {
@@ -413,27 +508,18 @@ ${preview}
413
508
  授权等待时间:5 分钟,超时自动拒绝。`;
414
509
  }
415
510
  /**
416
- * Build the prompt for the sub-agent that handles an incoming A2A task.
417
- * 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.
418
513
  */
419
- function buildA2ASubagentPrompt(taskId, taskText) {
420
- return `你收到了一个来自远端智能体的委派任务。请完成任务并汇报结果。
421
-
422
- ## 任务内容
514
+ function buildA2AMainSessionPrompt(taskId, fromAgentName, taskText) {
515
+ return `[MultiClaws 委派任务] 来自 **${fromAgentName}**:
423
516
 
424
517
  ${taskText}
425
518
 
426
- ## 完成后必做
427
-
428
- 完成任务后,你**必须**调用 \`multiclaws_a2a_callback\` 工具汇报结果:
429
-
430
- \`\`\`
431
- multiclaws_a2a_callback(taskId="${taskId}", result="你的完整回复内容")
432
- \`\`\`
519
+ ---
520
+ 完成后请调用 \`multiclaws_a2a_callback\` 汇报结果:
521
+ - taskId: "${taskId}"
522
+ - result: 你的完整回复内容
433
523
 
434
- **重要**:
435
- - 无论任务成功还是失败,都必须调用 \`multiclaws_a2a_callback\`
436
- - result 参数填写你的完整回复文本
437
- - 如果任务失败,在 result 中说明失败原因
438
- - 这是唯一的结果回传方式,不调用则结果会丢失`;
524
+ 无论成功还是失败都必须调用,这是结果回传给委派方的唯一方式。`;
439
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,49 +1028,67 @@ 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 {
988
- const result = await (0, gateway_client_1.invokeGatewayTool)({
1038
+ this.log("info", `discoverActiveSession: calling sessions_list (limit=10, activeMinutes=120)`);
1039
+ const raw = await (0, gateway_client_1.invokeGatewayTool)({
989
1040
  gateway: this.gatewayConfig,
990
1041
  tool: "sessions_list",
991
1042
  args: { limit: 10, activeMinutes: 120 },
992
1043
  timeoutMs: 5_000,
993
1044
  });
994
- this.log("info", `discoverActiveSession: raw result = ${JSON.stringify(result).slice(0, 500)}`);
995
- const sessions = result?.sessions ?? [];
996
- this.log("info", `discoverActiveSession: found ${sessions.length} sessions`);
1045
+ this.log("info", `discoverActiveSession: raw result = ${JSON.stringify(raw).slice(0, 500)}`);
1046
+ // Unwrap gateway tool standard response: { content: [{ type: "text", text: "..." }] }
1047
+ let parsed = raw;
1048
+ if (raw?.content?.[0]?.type === "text") {
1049
+ try {
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`);
1055
+ }
1056
+ }
1057
+ const sessions = parsed?.sessions ?? [];
1058
+ this.log("info", `discoverActiveSession: found ${sessions.length} sessions from gateway`);
997
1059
  const INTERNAL_PREFIXES = ["delegate-", "a2a-"];
998
- const session = sessions.find((s) => s.sessionKey && !INTERNAL_PREFIXES.some((p) => s.sessionKey.startsWith(p)));
999
- if (session) {
1000
- this.log("info", `discoverActiveSession: matched session ${session.sessionKey}`);
1060
+ // sessions_list returns "key" not "sessionKey"
1061
+ const session = sessions.find((s) => {
1062
+ const k = (s.key ?? s.sessionKey);
1063
+ return k && !INTERNAL_PREFIXES.some((p) => k.startsWith(p));
1064
+ });
1065
+ const matchedKey = (session?.key ?? session?.sessionKey);
1066
+ if (matchedKey) {
1067
+ this.log("info", `discoverActiveSession: ✓ matched session ${matchedKey}`);
1001
1068
  }
1002
1069
  else {
1003
- this.log("warn", `discoverActiveSession: all ${sessions.length} sessions filtered or empty`);
1004
- sessions.forEach((s) => this.log("info", ` session: ${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)"}`));
1005
1072
  }
1006
- return session?.sessionKey ?? null;
1073
+ return matchedKey ?? null;
1007
1074
  }
1008
1075
  catch (err) {
1009
- 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`);
1010
1077
  return null;
1011
1078
  }
1012
1079
  }
1013
1080
  async notifyUser(message) {
1014
- 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)}"`);
1015
1082
  if (!this.gatewayConfig) {
1016
- this.log("warn", "notifyUser: skipped — no gatewayConfig");
1083
+ this.log("warn", "notifyUser: skipped — no gatewayConfig, message lost");
1017
1084
  return;
1018
1085
  }
1019
1086
  // Fallback: no registered targets yet (e.g. right after gateway restart)
1020
1087
  if (this.notificationTargets.size === 0) {
1021
- this.log("warn", "notifyUser: no registered targets attempting session discovery");
1088
+ this.log("info", "notifyUser: no registered targets falling back to discoverActiveSession()");
1022
1089
  const sessionKey = await this.discoverActiveSession();
1023
1090
  if (sessionKey) {
1024
- this.log("info", `notifyUser: discovered session ${sessionKey}`);
1091
+ this.log("info", `notifyUser: fallback discovered session ${sessionKey} → calling sessions_send`);
1025
1092
  try {
1026
1093
  await (0, gateway_client_1.invokeGatewayTool)({
1027
1094
  gateway: this.gatewayConfig,
@@ -1029,20 +1096,23 @@ class MulticlawsService extends node_events_1.EventEmitter {
1029
1096
  args: { sessionKey, message },
1030
1097
  timeoutMs: 5_000,
1031
1098
  });
1099
+ this.log("info", `notifyUser: ✓ fallback sessions_send to ${sessionKey} succeeded`);
1032
1100
  this.addNotificationTarget(`web:${sessionKey}`, { type: "web", sessionKey });
1101
+ this.log("info", `notifyUser: registered ${sessionKey} as notification target for future use`);
1033
1102
  }
1034
1103
  catch (err) {
1035
- 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)}`);
1036
1105
  }
1037
1106
  }
1038
1107
  else {
1039
- this.log("warn", "notifyUser: no active session found, message lost");
1108
+ this.log("warn", "notifyUser: ✗ discoverActiveSession returned null — no active session found, message lost");
1040
1109
  }
1041
1110
  return;
1042
1111
  }
1043
1112
  const entries = [...this.notificationTargets.entries()];
1113
+ this.log("info", `notifyUser: sending to ${entries.length} registered target(s): [${entries.map(([k]) => k).join(", ")}]`);
1044
1114
  const results = await Promise.allSettled(entries.map(async ([key, target]) => {
1045
- this.log("info", `notifyUser: sending to ${key} (type=${target.type})`);
1115
+ this.log("info", `notifyUser: ${key} (type=${target.type})`);
1046
1116
  try {
1047
1117
  await (target.type === "channel"
1048
1118
  ? (0, gateway_client_1.invokeGatewayTool)({
@@ -1052,26 +1122,28 @@ class MulticlawsService extends node_events_1.EventEmitter {
1052
1122
  timeoutMs: 5_000,
1053
1123
  })
1054
1124
  : (0, gateway_client_1.invokeGatewayTool)({
1055
- // sessions_send injects a message into the session so the AI
1056
- // can relay it to the human (correct tool; was "chat.send" before)
1057
1125
  gateway: this.gatewayConfig,
1058
1126
  tool: "sessions_send",
1059
1127
  args: { sessionKey: target.sessionKey, message },
1060
1128
  timeoutMs: 5_000,
1061
1129
  }));
1062
- this.log("info", `notifyUser: ${key} (${target.type}) succeeded`);
1130
+ this.log("info", `notifyUser: ${key} (${target.type}) succeeded`);
1063
1131
  }
1064
1132
  catch (err) {
1065
- 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)}`);
1066
1134
  throw err;
1067
1135
  }
1068
1136
  }));
1137
+ const okCount = results.filter((r) => r.status === "fulfilled").length;
1069
1138
  const failCount = results.filter((r) => r.status === "rejected").length;
1070
- if (failCount === entries.length) {
1071
- 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`);
1072
1144
  }
1073
- else if (failCount > 0) {
1074
- 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`);
1075
1147
  }
1076
1148
  }
1077
1149
  log(level, message) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multiclaws",
3
- "version": "0.4.39",
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",