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
|
|
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.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* 4.
|
|
28
|
-
* 5. Waits for the
|
|
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
|
|
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.
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* 4.
|
|
70
|
-
* 5. Waits for the
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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}
|
|
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
|
-
|
|
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
|
|
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}
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
166
|
-
const prompt =
|
|
167
|
-
this.logger.info(`[a2a-adapter] task ${taskId}
|
|
168
|
-
|
|
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: "
|
|
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}
|
|
180
|
-
|
|
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
|
|
187
|
+
// ── Step 7: Return result ──
|
|
184
188
|
this.taskTracker.update(taskId, { status: "completed", result: output });
|
|
185
|
-
this.logger.info(`[a2a-adapter]
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
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
|
|
342
|
-
const sessionKey = await this.
|
|
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}
|
|
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.
|
|
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:
|
|
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:
|
|
461
|
+
this.logger.info(`[a2a-adapter] notifyUser: ✓ ${key} (${target.type}) succeeded`);
|
|
385
462
|
}
|
|
386
463
|
catch (err) {
|
|
387
|
-
this.logger.
|
|
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
|
-
|
|
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
|
|
430
|
-
* The
|
|
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
|
|
433
|
-
return
|
|
434
|
-
|
|
435
|
-
## 任务内容
|
|
514
|
+
function buildA2AMainSessionPrompt(taskId, fromAgentName, taskText) {
|
|
515
|
+
return `[MultiClaws 委派任务] 来自 **${fromAgentName}**:
|
|
436
516
|
|
|
437
517
|
${taskText}
|
|
438
518
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
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]
|
|
284
|
+
this.log("info", `[delegate] [step:track] taskId=${track.taskId}, status=running`);
|
|
274
285
|
try {
|
|
275
|
-
|
|
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}
|
|
278
|
-
// Fire-and-forget execution
|
|
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
|
|
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
|
|
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]
|
|
308
|
+
this.log("error", `[delegate] ${track.taskId} [step:background-send] ✗ caught error: ${errorMsg} → task marked failed`);
|
|
299
309
|
}
|
|
300
310
|
})();
|
|
301
|
-
|
|
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]
|
|
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
|
-
|
|
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]
|
|
352
|
+
this.log("info", `[delegate-sync] [step:track] taskId=${track.taskId}, status=running`);
|
|
334
353
|
try {
|
|
335
|
-
|
|
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
|
-
|
|
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]
|
|
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]
|
|
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
|
-
|
|
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]
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
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", `
|
|
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("
|
|
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},
|
|
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("
|
|
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("
|
|
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:
|
|
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("
|
|
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 ===
|
|
1084
|
-
this.log("
|
|
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
|
|
1087
|
-
this.log("warn", `notifyUser: ${failCount}
|
|
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) {
|