@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.
@@ -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) => resolve(code ?? 0));
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
- 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;