@botcord/daemon 0.2.61 → 0.2.63
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.d.ts +1 -0
- package/dist/diagnostics.js +163 -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__/diagnostics.test.ts +23 -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 +166 -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 {
|
|
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;
|