@botcord/daemon 0.2.61 → 0.2.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp-logs.d.ts +39 -0
- package/dist/acp-logs.js +333 -0
- package/dist/diagnostics.js +58 -1
- package/dist/gateway/dispatcher.js +32 -7
- package/dist/gateway/runtimes/acp-stream.js +114 -3
- package/dist/gateway/runtimes/openclaw-acp.js +77 -0
- package/dist/index.js +30 -24
- package/dist/openclaw-discovery.js +13 -5
- package/dist/provision.js +29 -0
- package/package.json +1 -1
- package/src/__tests__/acp-logs.test.ts +88 -0
- package/src/__tests__/openclaw-acp.test.ts +39 -0
- package/src/__tests__/openclaw-discovery.test.ts +1 -0
- package/src/acp-logs.ts +382 -0
- package/src/diagnostics.ts +60 -0
- package/src/gateway/__tests__/dispatcher.test.ts +26 -0
- package/src/gateway/__tests__/hermes-agent-adapter.test.ts +27 -0
- package/src/gateway/dispatcher.ts +31 -8
- package/src/gateway/runtimes/acp-stream.ts +112 -1
- package/src/gateway/runtimes/openclaw-acp.ts +76 -0
- package/src/index.ts +31 -23
- package/src/openclaw-discovery.ts +16 -5
- package/src/provision.ts +32 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
|
+
import { createAcpTraceLogger, type AcpTraceLogger } from "../../acp-logs.js";
|
|
2
3
|
import { consoleLogger } from "../log.js";
|
|
3
4
|
import type {
|
|
4
5
|
RuntimeAdapter,
|
|
@@ -33,9 +34,17 @@ const ASSISTANT_TEXT_CAP = 1 * 1024 * 1024;
|
|
|
33
34
|
const KILL_GRACE_MS = 5_000;
|
|
34
35
|
/** Deadline for the initial `initialize` handshake. */
|
|
35
36
|
const INITIALIZE_TIMEOUT_MS = 30_000;
|
|
37
|
+
/** Short drain window for late `session/update` chunks after a prompt RPC error. */
|
|
38
|
+
const PROMPT_ERROR_DRAIN_MS = 750;
|
|
36
39
|
/** ACP protocol version this client targets. */
|
|
37
40
|
export const ACP_PROTOCOL_VERSION = 1;
|
|
38
41
|
|
|
42
|
+
function stringField(obj: unknown, key: string): string | undefined {
|
|
43
|
+
if (!obj || typeof obj !== "object") return undefined;
|
|
44
|
+
const value = (obj as Record<string, unknown>)[key];
|
|
45
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
39
48
|
export interface AcpInitializeResult {
|
|
40
49
|
protocolVersion?: number;
|
|
41
50
|
agentInfo?: { name?: string; version?: string };
|
|
@@ -119,6 +128,7 @@ class AcpConnection {
|
|
|
119
128
|
): Promise<unknown> | unknown;
|
|
120
129
|
},
|
|
121
130
|
private readonly logId: string,
|
|
131
|
+
private readonly trace: AcpTraceLogger | null = null,
|
|
122
132
|
) {
|
|
123
133
|
child.stdout.setEncoding("utf8");
|
|
124
134
|
child.stdout.on("data", (chunk: string) => this.onStdout(chunk));
|
|
@@ -142,9 +152,11 @@ class AcpConnection {
|
|
|
142
152
|
|
|
143
153
|
private dispatchLine(line: string): void {
|
|
144
154
|
let msg: any;
|
|
155
|
+
|
|
145
156
|
try {
|
|
146
157
|
msg = JSON.parse(line);
|
|
147
158
|
} catch {
|
|
159
|
+
this.trace?.write({ stream: "stdout_non_json", chunk: line });
|
|
148
160
|
log.warn(`${this.logId} non-json acp line`, { line: line.slice(0, 200) });
|
|
149
161
|
return;
|
|
150
162
|
}
|
|
@@ -155,11 +167,26 @@ class AcpConnection {
|
|
|
155
167
|
if (!pending) return;
|
|
156
168
|
this.pending.delete(msg.id);
|
|
157
169
|
if (msg.error) {
|
|
170
|
+
this.trace?.write({
|
|
171
|
+
stream: "rpc_in",
|
|
172
|
+
direction: "in",
|
|
173
|
+
id: msg.id,
|
|
174
|
+
status: "error",
|
|
175
|
+
code: typeof msg.error.code === "number" ? msg.error.code : undefined,
|
|
176
|
+
error: msg.error.message ?? "(no message)",
|
|
177
|
+
});
|
|
158
178
|
const err = new Error(
|
|
159
179
|
`acp error ${msg.error.code ?? "?"}: ${msg.error.message ?? "(no message)"}`,
|
|
160
180
|
);
|
|
161
181
|
pending.reject(err);
|
|
162
182
|
} else {
|
|
183
|
+
this.trace?.write({
|
|
184
|
+
stream: "rpc_in",
|
|
185
|
+
direction: "in",
|
|
186
|
+
id: msg.id,
|
|
187
|
+
status: "response",
|
|
188
|
+
result: msg.result ?? null,
|
|
189
|
+
});
|
|
163
190
|
pending.resolve(msg.result ?? null);
|
|
164
191
|
}
|
|
165
192
|
return;
|
|
@@ -167,8 +194,23 @@ class AcpConnection {
|
|
|
167
194
|
if (typeof msg.method === "string") {
|
|
168
195
|
// Server→client request (has `id`) or notification (no `id`)
|
|
169
196
|
if (msg.id !== undefined) {
|
|
197
|
+
this.trace?.write({
|
|
198
|
+
stream: "rpc_in",
|
|
199
|
+
direction: "in",
|
|
200
|
+
id: msg.id,
|
|
201
|
+
method: msg.method,
|
|
202
|
+
status: "request",
|
|
203
|
+
params: msg.params,
|
|
204
|
+
});
|
|
170
205
|
void this.handleServerRequest(msg.id, msg.method, msg.params);
|
|
171
206
|
} else {
|
|
207
|
+
this.trace?.write({
|
|
208
|
+
stream: "rpc_in",
|
|
209
|
+
direction: "in",
|
|
210
|
+
method: msg.method,
|
|
211
|
+
status: "notification",
|
|
212
|
+
params: msg.params,
|
|
213
|
+
});
|
|
172
214
|
try {
|
|
173
215
|
this.handlers.onNotification(msg.method, msg.params);
|
|
174
216
|
} catch (err) {
|
|
@@ -199,6 +241,15 @@ class AcpConnection {
|
|
|
199
241
|
const reply = error
|
|
200
242
|
? { jsonrpc: "2.0", id, error }
|
|
201
243
|
: { jsonrpc: "2.0", id, result: result ?? null };
|
|
244
|
+
this.trace?.write({
|
|
245
|
+
stream: "rpc_out",
|
|
246
|
+
direction: "out",
|
|
247
|
+
id,
|
|
248
|
+
status: error ? "error" : "response",
|
|
249
|
+
code: error?.code,
|
|
250
|
+
error: error?.message,
|
|
251
|
+
result: error ? undefined : result ?? null,
|
|
252
|
+
});
|
|
202
253
|
this.writeMessage(reply);
|
|
203
254
|
}
|
|
204
255
|
|
|
@@ -221,11 +272,26 @@ class AcpConnection {
|
|
|
221
272
|
resolve: (v) => resolve(v as T),
|
|
222
273
|
reject,
|
|
223
274
|
});
|
|
275
|
+
this.trace?.write({
|
|
276
|
+
stream: "rpc_out",
|
|
277
|
+
direction: "out",
|
|
278
|
+
id,
|
|
279
|
+
method,
|
|
280
|
+
status: "request",
|
|
281
|
+
params,
|
|
282
|
+
});
|
|
224
283
|
this.writeMessage({ jsonrpc: "2.0", id, method, params });
|
|
225
284
|
});
|
|
226
285
|
}
|
|
227
286
|
|
|
228
287
|
notify(method: string, params: unknown): void {
|
|
288
|
+
this.trace?.write({
|
|
289
|
+
stream: "rpc_out",
|
|
290
|
+
direction: "out",
|
|
291
|
+
method,
|
|
292
|
+
status: "notification",
|
|
293
|
+
params,
|
|
294
|
+
});
|
|
229
295
|
this.writeMessage({ jsonrpc: "2.0", method, params });
|
|
230
296
|
}
|
|
231
297
|
|
|
@@ -323,6 +389,20 @@ export abstract class AcpRuntimeAdapter implements RuntimeAdapter {
|
|
|
323
389
|
env: this.spawnEnv(opts),
|
|
324
390
|
stdio: ["pipe", "pipe", "pipe"],
|
|
325
391
|
}) as ChildProcessWithoutNullStreams;
|
|
392
|
+
const trace = createAcpTraceLogger({
|
|
393
|
+
runtime: this.id,
|
|
394
|
+
accountId: opts.accountId,
|
|
395
|
+
turnId: stringField(opts.context, "turnId"),
|
|
396
|
+
roomId: stringField(opts.context, "roomId"),
|
|
397
|
+
topicId: stringField(opts.context, "topicId") ?? null,
|
|
398
|
+
hermesProfile: opts.hermesProfile,
|
|
399
|
+
sessionId: opts.sessionId,
|
|
400
|
+
});
|
|
401
|
+
trace?.write({
|
|
402
|
+
stream: "child_start",
|
|
403
|
+
pid: child.pid,
|
|
404
|
+
params: { command: binary, args, cwd: opts.cwd },
|
|
405
|
+
});
|
|
326
406
|
|
|
327
407
|
let killTimer: ReturnType<typeof setTimeout> | null = null;
|
|
328
408
|
const onAbort = () => {
|
|
@@ -351,6 +431,7 @@ export abstract class AcpRuntimeAdapter implements RuntimeAdapter {
|
|
|
351
431
|
child.stderr.setEncoding("utf8");
|
|
352
432
|
child.stderr.on("data", (chunk: string) => {
|
|
353
433
|
stderrTail = (stderrTail + chunk).slice(-STDERR_TAIL_CAP);
|
|
434
|
+
trace?.write({ stream: "stderr", pid: child.pid, chunk });
|
|
354
435
|
});
|
|
355
436
|
|
|
356
437
|
const state: AcpRunState = {
|
|
@@ -414,13 +495,25 @@ export abstract class AcpRuntimeAdapter implements RuntimeAdapter {
|
|
|
414
495
|
},
|
|
415
496
|
},
|
|
416
497
|
this.id,
|
|
498
|
+
trace,
|
|
417
499
|
);
|
|
418
500
|
|
|
419
501
|
const childExit = new Promise<number>((resolve) => {
|
|
420
|
-
child.on("close", (code) =>
|
|
502
|
+
child.on("close", (code, signal) => {
|
|
503
|
+
trace?.write({ stream: "child_exit", pid: child.pid, code, signal });
|
|
504
|
+
resolve(code ?? 0);
|
|
505
|
+
});
|
|
506
|
+
child.on("error", (err) => {
|
|
507
|
+
trace?.write({
|
|
508
|
+
stream: "child_error",
|
|
509
|
+
pid: child.pid,
|
|
510
|
+
error: err instanceof Error ? err.message : String(err),
|
|
511
|
+
});
|
|
512
|
+
});
|
|
421
513
|
});
|
|
422
514
|
|
|
423
515
|
let newSessionId = opts.sessionId ?? "";
|
|
516
|
+
let promptStarted = false;
|
|
424
517
|
|
|
425
518
|
try {
|
|
426
519
|
// 1) initialize
|
|
@@ -471,6 +564,7 @@ export abstract class AcpRuntimeAdapter implements RuntimeAdapter {
|
|
|
471
564
|
newSessionId = sessionId;
|
|
472
565
|
|
|
473
566
|
// 3) session/prompt
|
|
567
|
+
promptStarted = true;
|
|
474
568
|
const promptResult = (await conn.request<unknown>("session/prompt", {
|
|
475
569
|
sessionId,
|
|
476
570
|
prompt: [{ type: "text", text: opts.text }],
|
|
@@ -508,6 +602,9 @@ export abstract class AcpRuntimeAdapter implements RuntimeAdapter {
|
|
|
508
602
|
const tail = stderrTail.slice(-STDERR_ERROR_SNIPPET).trim();
|
|
509
603
|
state.errorText =
|
|
510
604
|
state.errorText ?? (tail ? `${baseMsg}; stderr: ${tail}` : baseMsg);
|
|
605
|
+
if (promptStarted && !opts.signal.aborted) {
|
|
606
|
+
await sleepUnlessAborted(PROMPT_ERROR_DRAIN_MS, opts.signal);
|
|
607
|
+
}
|
|
511
608
|
try {
|
|
512
609
|
child.stdin.end();
|
|
513
610
|
} catch {
|
|
@@ -563,3 +660,17 @@ export abstract class AcpRuntimeAdapter implements RuntimeAdapter {
|
|
|
563
660
|
});
|
|
564
661
|
}
|
|
565
662
|
}
|
|
663
|
+
|
|
664
|
+
function sleepUnlessAborted(ms: number, signal: AbortSignal): Promise<void> {
|
|
665
|
+
if (signal.aborted) return Promise.resolve();
|
|
666
|
+
return new Promise((resolve) => {
|
|
667
|
+
const t = setTimeout(done, ms);
|
|
668
|
+
if (typeof t.unref === "function") t.unref();
|
|
669
|
+
function done(): void {
|
|
670
|
+
signal.removeEventListener("abort", done);
|
|
671
|
+
clearTimeout(t);
|
|
672
|
+
resolve();
|
|
673
|
+
}
|
|
674
|
+
signal.addEventListener("abort", done, { once: true });
|
|
675
|
+
});
|
|
676
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
|
+
import { createAcpTraceLogger, type AcpTraceLogger } from "../../acp-logs.js";
|
|
2
3
|
import {
|
|
3
4
|
readCommandVersion,
|
|
4
5
|
resolveCommandOnPath,
|
|
@@ -51,6 +52,7 @@ interface AcpProcessHandle {
|
|
|
51
52
|
*/
|
|
52
53
|
spawnedUrl: string;
|
|
53
54
|
spawnedToken: string | undefined;
|
|
55
|
+
trace: AcpTraceLogger | null;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
interface PendingCall {
|
|
@@ -358,6 +360,12 @@ export class OpenclawAcpAdapter implements RuntimeAdapter {
|
|
|
358
360
|
|
|
359
361
|
if (!finalText) {
|
|
360
362
|
const stopReason = pickStopReason(promptResult);
|
|
363
|
+
if (!stopReason || stopReason === "end_turn") {
|
|
364
|
+
return {
|
|
365
|
+
text: "",
|
|
366
|
+
newSessionId: acpSessionId,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
361
369
|
const warningTail = handle.nonJsonStdoutTail.slice(-8).join("\n").trim();
|
|
362
370
|
const detail = warningTail ? `; stdout: ${truncateDetail(warningTail, 1000)}` : "";
|
|
363
371
|
const reason = stopReason ? `prompt stopped: ${stopReason}` : "empty assistant response";
|
|
@@ -444,11 +452,23 @@ export class OpenclawAcpAdapter implements RuntimeAdapter {
|
|
|
444
452
|
const command = resolveOpenclawCommand() ?? "openclaw";
|
|
445
453
|
const args = ["acp", "--url", gateway.url];
|
|
446
454
|
if (gateway.token) args.push("--token", gateway.token);
|
|
455
|
+
const accountId = key.split("::", 1)[0];
|
|
456
|
+
const trace = createAcpTraceLogger({
|
|
457
|
+
runtime: acpRuntimeLogName(gateway),
|
|
458
|
+
accountId,
|
|
459
|
+
gatewayName: gateway.name,
|
|
460
|
+
gatewayUrl: gateway.url,
|
|
461
|
+
});
|
|
447
462
|
|
|
448
463
|
const child = this.spawnFn(command, args, {
|
|
449
464
|
stdio: ["pipe", "pipe", "pipe"],
|
|
450
465
|
env: { ...process.env },
|
|
451
466
|
}) as ChildProcessWithoutNullStreams;
|
|
467
|
+
trace?.write({
|
|
468
|
+
stream: "child_start",
|
|
469
|
+
pid: child.pid,
|
|
470
|
+
params: { command, args, gateway: gateway.name },
|
|
471
|
+
});
|
|
452
472
|
|
|
453
473
|
const handle: AcpProcessHandle = {
|
|
454
474
|
child,
|
|
@@ -462,19 +482,27 @@ export class OpenclawAcpAdapter implements RuntimeAdapter {
|
|
|
462
482
|
closed: false,
|
|
463
483
|
spawnedUrl: gateway.url,
|
|
464
484
|
spawnedToken: gateway.token,
|
|
485
|
+
trace,
|
|
465
486
|
};
|
|
466
487
|
|
|
467
488
|
child.stdout.setEncoding("utf8");
|
|
468
489
|
child.stdout.on("data", (chunk: string) => onStdoutChunk(handle, chunk));
|
|
469
490
|
child.stderr.setEncoding("utf8");
|
|
470
491
|
child.stderr.on("data", (chunk: string) => {
|
|
492
|
+
trace?.write({ stream: "stderr", pid: child.pid, chunk });
|
|
471
493
|
log.debug("openclaw-acp.stderr", { key, chunk: chunk.slice(0, 500) });
|
|
472
494
|
});
|
|
473
495
|
child.on("exit", (code, signal) => {
|
|
496
|
+
trace?.write({ stream: "child_exit", pid: child.pid, code, signal });
|
|
474
497
|
shutdownHandle(handle, `exit code=${code ?? "null"} signal=${signal ?? "null"}`);
|
|
475
498
|
ACP_POOL.delete(key);
|
|
476
499
|
});
|
|
477
500
|
child.on("error", (err) => {
|
|
501
|
+
trace?.write({
|
|
502
|
+
stream: "child_error",
|
|
503
|
+
pid: child.pid,
|
|
504
|
+
error: err instanceof Error ? err.message : String(err),
|
|
505
|
+
});
|
|
478
506
|
log.warn("openclaw-acp.child-error", {
|
|
479
507
|
key,
|
|
480
508
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -532,6 +560,16 @@ export class OpenclawAcpAdapter implements RuntimeAdapter {
|
|
|
532
560
|
// JSON-RPC stdio plumbing
|
|
533
561
|
// ---------------------------------------------------------------------------
|
|
534
562
|
|
|
563
|
+
function acpRuntimeLogName(gateway: NonNullable<RuntimeRunOptions["gateway"]>): string {
|
|
564
|
+
if (gateway.name.toLowerCase().includes("qclaw")) return "qclaw-acp";
|
|
565
|
+
try {
|
|
566
|
+
if (new URL(gateway.url).port === "28789") return "qclaw-acp";
|
|
567
|
+
} catch {
|
|
568
|
+
// Fall back to OpenClaw.
|
|
569
|
+
}
|
|
570
|
+
return "openclaw-acp";
|
|
571
|
+
}
|
|
572
|
+
|
|
535
573
|
function onStdoutChunk(handle: AcpProcessHandle, chunk: string): void {
|
|
536
574
|
handle.buffer += chunk;
|
|
537
575
|
let idx: number;
|
|
@@ -551,6 +589,7 @@ function onStdoutChunk(handle: AcpProcessHandle, chunk: string): void {
|
|
|
551
589
|
error: err instanceof Error ? err.message : String(err),
|
|
552
590
|
line: line.slice(0, 200),
|
|
553
591
|
});
|
|
592
|
+
handle.trace?.write({ stream: "stdout_non_json", chunk: line });
|
|
554
593
|
continue;
|
|
555
594
|
}
|
|
556
595
|
routeMessage(handle, msg);
|
|
@@ -565,14 +604,36 @@ function routeMessage(handle: AcpProcessHandle, msg: any): void {
|
|
|
565
604
|
handle.pending.delete(id);
|
|
566
605
|
if (msg.error) {
|
|
567
606
|
const message = formatRpcError(msg.error);
|
|
607
|
+
handle.trace?.write({
|
|
608
|
+
stream: "rpc_in",
|
|
609
|
+
direction: "in",
|
|
610
|
+
id,
|
|
611
|
+
status: "error",
|
|
612
|
+
code: typeof msg.error.code === "number" ? msg.error.code : undefined,
|
|
613
|
+
error: message,
|
|
614
|
+
});
|
|
568
615
|
pending.reject(new Error(message));
|
|
569
616
|
} else {
|
|
617
|
+
handle.trace?.write({
|
|
618
|
+
stream: "rpc_in",
|
|
619
|
+
direction: "in",
|
|
620
|
+
id,
|
|
621
|
+
status: "response",
|
|
622
|
+
result: msg.result ?? null,
|
|
623
|
+
});
|
|
570
624
|
pending.resolve(msg.result);
|
|
571
625
|
}
|
|
572
626
|
return;
|
|
573
627
|
}
|
|
574
628
|
// Notification.
|
|
575
629
|
if (msg?.method && msg?.params) {
|
|
630
|
+
handle.trace?.write({
|
|
631
|
+
stream: "rpc_in",
|
|
632
|
+
direction: "in",
|
|
633
|
+
method: msg.method,
|
|
634
|
+
status: "notification",
|
|
635
|
+
params: msg.params,
|
|
636
|
+
});
|
|
576
637
|
const sid = msg.params?.sessionId;
|
|
577
638
|
if (typeof sid === "string") {
|
|
578
639
|
const sub = handle.subscribers.get(sid);
|
|
@@ -598,6 +659,14 @@ function sendRequest(
|
|
|
598
659
|
return new Promise((resolve, reject) => {
|
|
599
660
|
const id = handle.nextId++;
|
|
600
661
|
handle.pending.set(id, { resolve, reject, method });
|
|
662
|
+
handle.trace?.write({
|
|
663
|
+
stream: "rpc_out",
|
|
664
|
+
direction: "out",
|
|
665
|
+
id,
|
|
666
|
+
method,
|
|
667
|
+
status: "request",
|
|
668
|
+
params,
|
|
669
|
+
});
|
|
601
670
|
const frame = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
|
|
602
671
|
try {
|
|
603
672
|
handle.child.stdin.write(frame);
|
|
@@ -614,6 +683,13 @@ function sendNotification(
|
|
|
614
683
|
params: any,
|
|
615
684
|
): void {
|
|
616
685
|
if (handle.closed) return;
|
|
686
|
+
handle.trace?.write({
|
|
687
|
+
stream: "rpc_out",
|
|
688
|
+
direction: "out",
|
|
689
|
+
method,
|
|
690
|
+
status: "notification",
|
|
691
|
+
params,
|
|
692
|
+
});
|
|
617
693
|
const frame = JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n";
|
|
618
694
|
try {
|
|
619
695
|
handle.child.stdin.write(frame);
|
package/src/index.ts
CHANGED
|
@@ -307,6 +307,34 @@ function loadOrInitConfig(args: ParsedArgs): DaemonConfig {
|
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
+
async function refreshDiscoveredOpenclawGateways(
|
|
311
|
+
cfg: DaemonConfig,
|
|
312
|
+
source: string,
|
|
313
|
+
): Promise<DaemonConfig> {
|
|
314
|
+
if (!openclawDiscoveryConfigEnabled(cfg)) return cfg;
|
|
315
|
+
try {
|
|
316
|
+
const found = await discoverLocalOpenclawGateways({
|
|
317
|
+
searchPaths: cfg.openclawDiscovery?.searchPaths,
|
|
318
|
+
defaultPorts: cfg.openclawDiscovery?.defaultPorts,
|
|
319
|
+
timeoutMs: 500,
|
|
320
|
+
});
|
|
321
|
+
const merged = mergeOpenclawGateways(cfg, found);
|
|
322
|
+
if (!merged.changed) return cfg;
|
|
323
|
+
saveConfig(merged.cfg);
|
|
324
|
+
log.info("openclaw discovery: gateways merged", {
|
|
325
|
+
source,
|
|
326
|
+
added: merged.added.map((g) => ({ name: g.name, url: g.url })),
|
|
327
|
+
});
|
|
328
|
+
return merged.cfg;
|
|
329
|
+
} catch (err) {
|
|
330
|
+
log.warn("openclaw discovery failed; continuing", {
|
|
331
|
+
source,
|
|
332
|
+
error: err instanceof Error ? err.message : String(err),
|
|
333
|
+
});
|
|
334
|
+
return cfg;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
310
338
|
/**
|
|
311
339
|
* Read the current user-auth record without throwing on parse / permission
|
|
312
340
|
* errors — those are returned as `null` so the caller treats them like a
|
|
@@ -566,27 +594,7 @@ async function ensureUserAuthForStart(args: ParsedArgs): Promise<UserAuthRecord
|
|
|
566
594
|
|
|
567
595
|
async function cmdStart(args: ParsedArgs): Promise<void> {
|
|
568
596
|
let cfg = loadOrInitConfig(args);
|
|
569
|
-
|
|
570
|
-
try {
|
|
571
|
-
const found = await discoverLocalOpenclawGateways({
|
|
572
|
-
searchPaths: cfg.openclawDiscovery?.searchPaths,
|
|
573
|
-
defaultPorts: cfg.openclawDiscovery?.defaultPorts,
|
|
574
|
-
timeoutMs: 500,
|
|
575
|
-
});
|
|
576
|
-
const merged = mergeOpenclawGateways(cfg, found);
|
|
577
|
-
if (merged.changed) {
|
|
578
|
-
cfg = merged.cfg;
|
|
579
|
-
saveConfig(cfg);
|
|
580
|
-
log.info("openclaw discovery: gateways merged", {
|
|
581
|
-
added: merged.added.map((g) => ({ name: g.name, url: g.url })),
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
} catch (err) {
|
|
585
|
-
log.warn("openclaw discovery failed; continuing", {
|
|
586
|
-
error: err instanceof Error ? err.message : String(err),
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
}
|
|
597
|
+
cfg = await refreshDiscoveredOpenclawGateways(cfg, "start");
|
|
590
598
|
// Foreground is now the default. --background (alias -d) detaches.
|
|
591
599
|
// --foreground is still accepted (no-op) for backwards compatibility and
|
|
592
600
|
// is also what the detached child re-execs itself with.
|
|
@@ -1373,8 +1381,8 @@ async function cmdDoctor(args: ParsedArgs): Promise<void> {
|
|
|
1373
1381
|
let cfgForEndpoints: import("./config.js").DaemonConfig | null = null;
|
|
1374
1382
|
try {
|
|
1375
1383
|
const cfg = loadConfig();
|
|
1376
|
-
cfgForEndpoints = cfg;
|
|
1377
|
-
channels = channelsFromDaemonConfig(
|
|
1384
|
+
cfgForEndpoints = await refreshDiscoveredOpenclawGateways(cfg, "doctor");
|
|
1385
|
+
channels = channelsFromDaemonConfig(cfgForEndpoints);
|
|
1378
1386
|
} catch {
|
|
1379
1387
|
channels = [];
|
|
1380
1388
|
}
|
|
@@ -346,6 +346,7 @@ export function mergeOpenclawGateways(
|
|
|
346
346
|
|
|
347
347
|
function discoverFromConfigDir(root: string): DiscoveredOpenclawGateway[] {
|
|
348
348
|
const dir = expandHome(root);
|
|
349
|
+
const rootIsQclaw = path.basename(dir) === ".qclaw";
|
|
349
350
|
let names: string[];
|
|
350
351
|
try {
|
|
351
352
|
names = readdirSync(dir);
|
|
@@ -362,8 +363,9 @@ function discoverFromConfigDir(root: string): DiscoveredOpenclawGateway[] {
|
|
|
362
363
|
const raw = readFileSync(file, "utf8");
|
|
363
364
|
const parsed = name.endsWith(".json") ? parseJsonConfig(raw) : parseTomlConfig(raw);
|
|
364
365
|
if (!parsed?.url) continue;
|
|
366
|
+
const namePrefix = rootIsQclaw || name.toLowerCase() === "qclaw.json" ? "qclaw" : "openclaw";
|
|
365
367
|
const item: DiscoveredOpenclawGateway = {
|
|
366
|
-
name: nameFromUrl(parsed.url),
|
|
368
|
+
name: nameFromUrl(parsed.url, namePrefix),
|
|
367
369
|
url: parsed.url,
|
|
368
370
|
source: "config-file",
|
|
369
371
|
};
|
|
@@ -487,25 +489,34 @@ function dedupeDiscovered(items: DiscoveredOpenclawGateway[]): DiscoveredOpencla
|
|
|
487
489
|
for (const item of items) {
|
|
488
490
|
const key = normalizeUrlKey(item.url);
|
|
489
491
|
const prev = byUrl.get(key);
|
|
490
|
-
if (
|
|
492
|
+
if (
|
|
493
|
+
!prev ||
|
|
494
|
+
priority[item.source] > priority[prev.source] ||
|
|
495
|
+
hasMoreAuth(item, prev) ||
|
|
496
|
+
prefersQclawName(item, prev)
|
|
497
|
+
) {
|
|
491
498
|
byUrl.set(key, item);
|
|
492
499
|
}
|
|
493
500
|
}
|
|
494
501
|
return [...byUrl.values()];
|
|
495
502
|
}
|
|
496
503
|
|
|
504
|
+
function prefersQclawName(a: DiscoveredOpenclawGateway, b: DiscoveredOpenclawGateway): boolean {
|
|
505
|
+
return a.name.startsWith("qclaw-") && !b.name.startsWith("qclaw-");
|
|
506
|
+
}
|
|
507
|
+
|
|
497
508
|
function hasMoreAuth(a: DiscoveredOpenclawGateway, b: DiscoveredOpenclawGateway): boolean {
|
|
498
509
|
const score = (x: DiscoveredOpenclawGateway): number => (x.token ? 2 : x.tokenFile ? 1 : 0);
|
|
499
510
|
return score(a) > score(b);
|
|
500
511
|
}
|
|
501
512
|
|
|
502
|
-
function nameFromUrl(raw: string): string {
|
|
513
|
+
function nameFromUrl(raw: string, prefix = "openclaw"): string {
|
|
503
514
|
try {
|
|
504
515
|
const u = new URL(raw);
|
|
505
516
|
const base = `${u.hostname}-${u.port || (u.protocol === "wss:" ? "443" : "80")}`;
|
|
506
|
-
return
|
|
517
|
+
return `${prefix}-${base.replace(/[^A-Za-z0-9_-]+/g, "-")}`;
|
|
507
518
|
} catch {
|
|
508
|
-
return
|
|
519
|
+
return `${prefix}-local`;
|
|
509
520
|
}
|
|
510
521
|
}
|
|
511
522
|
|
package/src/provision.ts
CHANGED
|
@@ -48,6 +48,11 @@ import {
|
|
|
48
48
|
buildManagedRoutes,
|
|
49
49
|
prepareGatewayProfile,
|
|
50
50
|
} from "./daemon-config-map.js";
|
|
51
|
+
import {
|
|
52
|
+
discoverLocalOpenclawGateways,
|
|
53
|
+
mergeOpenclawGateways,
|
|
54
|
+
openclawDiscoveryConfigEnabled,
|
|
55
|
+
} from "./openclaw-discovery.js";
|
|
51
56
|
import {
|
|
52
57
|
agentHomeDir,
|
|
53
58
|
agentStateDir,
|
|
@@ -324,9 +329,10 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
324
329
|
case CONTROL_FRAME_TYPES.LIST_RUNTIMES: {
|
|
325
330
|
// Async path so the openclaw-acp endpoints get probed inline; gateway
|
|
326
331
|
// / WS errors are swallowed inside `collectRuntimeSnapshotAsync`.
|
|
327
|
-
let cfgForProbe:
|
|
332
|
+
let cfgForProbe: DaemonConfig | undefined;
|
|
328
333
|
try {
|
|
329
334
|
cfgForProbe = loadConfig();
|
|
335
|
+
cfgForProbe = await refreshDiscoveredOpenclawGateways(cfgForProbe);
|
|
330
336
|
} catch {
|
|
331
337
|
cfgForProbe = undefined;
|
|
332
338
|
}
|
|
@@ -428,6 +434,31 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
428
434
|
};
|
|
429
435
|
}
|
|
430
436
|
|
|
437
|
+
async function refreshDiscoveredOpenclawGateways(cfg: DaemonConfig): Promise<DaemonConfig> {
|
|
438
|
+
if (!openclawDiscoveryConfigEnabled(cfg)) return cfg;
|
|
439
|
+
try {
|
|
440
|
+
const found = await discoverLocalOpenclawGateways({
|
|
441
|
+
searchPaths: cfg.openclawDiscovery?.searchPaths,
|
|
442
|
+
defaultPorts: cfg.openclawDiscovery?.defaultPorts,
|
|
443
|
+
timeoutMs: 500,
|
|
444
|
+
});
|
|
445
|
+
const merged = mergeOpenclawGateways(cfg, found);
|
|
446
|
+
if (!merged.changed) return cfg;
|
|
447
|
+
saveConfig(merged.cfg);
|
|
448
|
+
daemonLog.info("openclaw discovery: gateways merged", {
|
|
449
|
+
source: "list_runtimes",
|
|
450
|
+
added: merged.added.map((g) => ({ name: g.name, url: g.url })),
|
|
451
|
+
});
|
|
452
|
+
return merged.cfg;
|
|
453
|
+
} catch (err) {
|
|
454
|
+
daemonLog.warn("openclaw discovery failed; continuing", {
|
|
455
|
+
source: "list_runtimes",
|
|
456
|
+
error: err instanceof Error ? err.message : String(err),
|
|
457
|
+
});
|
|
458
|
+
return cfg;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
431
462
|
interface WakeAgentParams {
|
|
432
463
|
agent_id?: string;
|
|
433
464
|
agentId?: string;
|