@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.
@@ -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
- if (openclawDiscoveryConfigEnabled(cfg)) {
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(cfg);
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 (!prev || priority[item.source] > priority[prev.source] || hasMoreAuth(item, prev)) {
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 `openclaw-${base.replace(/[^A-Za-z0-9_-]+/g, "-")}`;
517
+ return `${prefix}-${base.replace(/[^A-Za-z0-9_-]+/g, "-")}`;
507
518
  } catch {
508
- return "openclaw-local";
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: { openclawGateways?: any[] } | undefined;
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;