clawnexus 0.2.8 → 0.3.1

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.
@@ -10,6 +10,7 @@ exports.registerAgentRoutes = registerAgentRoutes;
10
10
  exports.registerRegistryRoutes = registerRegistryRoutes;
11
11
  exports.registerInstanceRoutes = registerInstanceRoutes;
12
12
  exports.registerDiagnosticsRoutes = registerDiagnosticsRoutes;
13
+ exports.registerA2aRoutes = registerA2aRoutes;
13
14
  exports.startDaemon = startDaemon;
14
15
  const fastify_1 = __importDefault(require("fastify"));
15
16
  const store_js_1 = require("../registry/store.js");
@@ -21,15 +22,20 @@ const probe_js_1 = require("../local/probe.js");
21
22
  const engine_js_1 = require("../agent/engine.js");
22
23
  const tasks_js_1 = require("../agent/tasks.js");
23
24
  const router_js_1 = require("../agent/router.js");
25
+ const executor_js_1 = require("../agent/executor.js");
24
26
  const broadcast_js_1 = require("../discovery/broadcast.js");
25
27
  const keys_js_1 = require("../crypto/keys.js");
26
28
  const client_js_1 = require("../registry/client.js");
27
29
  const auto_register_js_1 = require("../registry/auto-register.js");
28
30
  const discovery_js_1 = require("../registry/discovery.js");
29
31
  const connector_js_1 = require("../relay/connector.js");
32
+ const card_js_1 = require("../a2a/card.js");
33
+ const services_js_1 = require("../agent/services.js");
34
+ const node_fs_1 = require("node:fs");
35
+ const node_path_1 = require("node:path");
30
36
  const PORT = parseInt(process.env.CLAWNEXUS_PORT ?? "17890", 10);
31
37
  const HOST = process.env.CLAWNEXUS_HOST ?? "127.0.0.1";
32
- function registerRelayRoutes(app, getConnector) {
38
+ function registerRelayRoutes(app, getConnector, getTokenRefresher) {
33
39
  app.post("/relay/connect", async (request, reply) => {
34
40
  const connector = getConnector();
35
41
  if (!connector) {
@@ -43,6 +49,17 @@ function registerRelayRoutes(app, getConnector) {
43
49
  error: "Missing target_claw_id",
44
50
  });
45
51
  }
52
+ // Refresh auth token before JOIN (relay JWTs expire after 5 min)
53
+ const refresher = getTokenRefresher?.();
54
+ if (refresher) {
55
+ try {
56
+ const freshToken = await refresher();
57
+ connector.updateAuthToken(freshToken);
58
+ }
59
+ catch (err) {
60
+ app.log.warn(`Token refresh before JOIN failed: ${err}`);
61
+ }
62
+ }
46
63
  connector.join(target_claw_id);
47
64
  return { status: "connecting", target: target_claw_id };
48
65
  });
@@ -67,7 +84,33 @@ function registerRelayRoutes(app, getConnector) {
67
84
  });
68
85
  }
69
86
  function registerAgentRoutes(app, deps) {
70
- const { engine, tasks, getRouter } = deps;
87
+ const { engine, tasks, getRouter, getExecutor, skillsRegistry } = deps;
88
+ // --- Skills ---
89
+ app.get("/agent/skills", async () => {
90
+ if (!skillsRegistry) {
91
+ return { skills: [], status: { source: "not_initialized" } };
92
+ }
93
+ return { skills: skillsRegistry.getSkills(), status: skillsRegistry.getStatus() };
94
+ });
95
+ app.post("/agent/skills/refresh", async () => {
96
+ if (!skillsRegistry) {
97
+ return { status: "error", message: "Skills registry not initialized" };
98
+ }
99
+ const ok = await skillsRegistry.refresh();
100
+ return {
101
+ status: ok ? "ok" : "error",
102
+ skills: skillsRegistry.getSkills(),
103
+ ...skillsRegistry.getStatus(),
104
+ };
105
+ });
106
+ // --- Executor status ---
107
+ app.get("/agent/executor/status", async () => {
108
+ const executor = getExecutor();
109
+ if (!executor) {
110
+ return { gw_state: "not_initialized", queue_length: 0, executing: [], max_concurrent: 0 };
111
+ }
112
+ return executor.getStatus();
113
+ });
71
114
  // --- Policy ---
72
115
  app.get("/agent/policy", async () => engine.getConfig());
73
116
  app.put("/agent/policy", async (request) => {
@@ -301,6 +344,31 @@ function registerDiagnosticsRoutes(app, deps) {
301
344
  };
302
345
  });
303
346
  }
347
+ function registerA2aRoutes(app, store, daemonVersion, skillsRegistry) {
348
+ // A2A standard well-known endpoint — returns card for the local (is_self) instance
349
+ app.get("/.well-known/agent-card.json", async (_request, reply) => {
350
+ const self = store.getAll().find((i) => i.is_self);
351
+ if (!self) {
352
+ return reply.status(404).send({ error: "No local instance discovered" });
353
+ }
354
+ return (0, card_js_1.buildAgentCard)(self, daemonVersion, skillsRegistry?.getSkills());
355
+ });
356
+ // All instances as Agent Cards
357
+ app.get("/a2a/cards", async () => {
358
+ const instances = store.getAll();
359
+ const skills = skillsRegistry?.getSkills();
360
+ const cards = instances.map((i) => (0, card_js_1.buildAgentCard)(i, daemonVersion, skills));
361
+ return { count: cards.length, cards };
362
+ });
363
+ // Single instance Agent Card by name
364
+ app.get("/a2a/cards/:name", async (request, reply) => {
365
+ const inst = store.resolve(request.params.name);
366
+ if (!inst) {
367
+ return reply.status(404).send({ error: "Instance not found" });
368
+ }
369
+ return (0, card_js_1.buildAgentCard)(inst, daemonVersion, skillsRegistry?.getSkills());
370
+ });
371
+ }
304
372
  async function startDaemon(options = {}) {
305
373
  const port = options.port ?? PORT;
306
374
  const host = options.host ?? HOST;
@@ -327,6 +395,13 @@ async function startDaemon(options = {}) {
327
395
  await engine.init();
328
396
  const taskManager = new tasks_js_1.TaskManager();
329
397
  await taskManager.init();
398
+ // 6b. Create TaskExecutor
399
+ const taskExecutor = new executor_js_1.TaskExecutor({
400
+ tasks: taskManager,
401
+ maxConcurrent: engine.getConfig().max_concurrent_tasks,
402
+ });
403
+ // 6c. Create SkillsRegistry
404
+ const skillsRegistry = new services_js_1.SkillsRegistry();
330
405
  // 7. Detect WireGuard interfaces
331
406
  const wgInfo = await (0, wireguard_js_1.detectWireGuard)();
332
407
  // 8. Create and configure Fastify app
@@ -350,7 +425,7 @@ async function startDaemon(options = {}) {
350
425
  app.get("/health", async () => ({
351
426
  status: "ok",
352
427
  service: "clawnexus-daemon",
353
- version: "0.4.0",
428
+ version: "0.3.1",
354
429
  timestamp: new Date().toISOString(),
355
430
  components: {
356
431
  registry: { instances: store.size },
@@ -373,12 +448,23 @@ async function startDaemon(options = {}) {
373
448
  // Instance management routes
374
449
  registerInstanceRoutes(app, store, scanner);
375
450
  // Relay routes
376
- registerRelayRoutes(app, () => connector);
451
+ registerRelayRoutes(app, () => connector, () => {
452
+ const rc = registryClient;
453
+ const clawName = autoRegister?.clawName;
454
+ if (!rc || !clawName)
455
+ return null;
456
+ return async () => {
457
+ const result = await rc.getToken(clawName);
458
+ return result.token;
459
+ };
460
+ });
377
461
  // Agent routes (Layer B)
378
462
  registerAgentRoutes(app, {
379
463
  engine,
380
464
  tasks: taskManager,
381
465
  getRouter: () => agentRouter,
466
+ getExecutor: () => taskExecutor,
467
+ skillsRegistry,
382
468
  });
383
469
  // Diagnostics routes
384
470
  registerDiagnosticsRoutes(app, {
@@ -390,6 +476,9 @@ async function startDaemon(options = {}) {
390
476
  getAutoRegister: () => autoRegister,
391
477
  unreachable,
392
478
  });
479
+ // A2A Agent Card routes
480
+ const daemonPkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, "../../package.json"), "utf-8"));
481
+ registerA2aRoutes(app, store, daemonPkg.version, skillsRegistry);
393
482
  // 9. Initialize Registry integration (non-fatal — LAN must work without it)
394
483
  let identityKeys = null;
395
484
  let registryClient = null;
@@ -400,6 +489,8 @@ async function startDaemon(options = {}) {
400
489
  localProbe.on("local:discovered", (instance) => {
401
490
  app.log.info({ agent_id: instance.agent_id }, "Local OpenClaw instance discovered");
402
491
  broadcast.sendAnnounce();
492
+ // Start skills registry once we know the local Gateway is available
493
+ skillsRegistry.start();
403
494
  });
404
495
  localProbe.on("local:unavailable", () => {
405
496
  app.log.info("No local OpenClaw instance on :18789");
@@ -416,10 +507,12 @@ async function startDaemon(options = {}) {
416
507
  registryClient,
417
508
  identityKeys,
418
509
  });
510
+ let relayInitializing = false;
419
511
  autoRegister.on("registered", async (info) => {
420
512
  console.log(`[clawnexus] [Registry] Registered as ${info.claw_name} (${info.action})`);
421
- // Initialize relay connector after successful registration
422
- if (!connector && registryClient && info.claw_name) {
513
+ // Initialize relay connector after successful registration (once only)
514
+ if (!connector && !relayInitializing && registryClient && info.claw_name) {
515
+ relayInitializing = true;
423
516
  try {
424
517
  const tokenResult = await registryClient.getToken(info.claw_name);
425
518
  console.log(`[clawnexus] [Relay] Got auth token, relay_hint: ${tokenResult.relay_hint}`);
@@ -447,7 +540,8 @@ async function startDaemon(options = {}) {
447
540
  });
448
541
  newConnector.connect();
449
542
  setConnector(newConnector);
450
- // Start token refresh — every 4 minutes (token expires in 5 min)
543
+ // Start token refresh — every 55 minutes (relay keeps WebSocket alive after auth,
544
+ // only need to refresh for reconnection scenarios)
451
545
  if (tokenRefreshTimer)
452
546
  clearInterval(tokenRefreshTimer);
453
547
  tokenRefreshTimer = setInterval(async () => {
@@ -455,8 +549,16 @@ async function startDaemon(options = {}) {
455
549
  return;
456
550
  try {
457
551
  const fresh = await registryClient.getToken(autoRegister.clawName);
458
- // Reconnect with fresh token
459
- connector?.disconnect();
552
+ // Save peer claw_ids from existing rooms before reconnecting
553
+ const previousPeers = [];
554
+ const oldConnector = connector;
555
+ if (oldConnector) {
556
+ for (const room of oldConnector.getStatus().rooms) {
557
+ if (room.peer_claw_id)
558
+ previousPeers.push(room.peer_claw_id);
559
+ }
560
+ }
561
+ // Connect new first, then disconnect old (relay server handles replacement)
460
562
  const refreshed = new connector_js_1.RelayConnector({
461
563
  relayUrl: process.env.CLAWNEXUS_RELAY_URL ?? `wss://${fresh.relay_hint}/relay`,
462
564
  clawId: autoRegister.clawName,
@@ -465,6 +567,15 @@ async function startDaemon(options = {}) {
465
567
  });
466
568
  refreshed.on("registered", (clawId) => {
467
569
  console.log(`[clawnexus] [Relay] Reconnected (token refresh) as ${clawId}`);
570
+ // Old connector is now replaced on server side — disconnect it locally
571
+ if (oldConnector && oldConnector !== refreshed) {
572
+ oldConnector.disconnect();
573
+ }
574
+ // Re-join rooms with previous peers
575
+ for (const peerId of previousPeers) {
576
+ console.log(`[clawnexus] [Relay] Re-joining peer ${peerId} after token refresh`);
577
+ refreshed.join(peerId);
578
+ }
468
579
  });
469
580
  refreshed.on("relay_error", (code, message) => {
470
581
  console.log(`[clawnexus] [Relay] Error: ${code} — ${message}`);
@@ -475,7 +586,7 @@ async function startDaemon(options = {}) {
475
586
  catch (err) {
476
587
  console.log(`[clawnexus] [Relay] Token refresh failed (non-fatal): ${err}`);
477
588
  }
478
- }, 4 * 60 * 1000);
589
+ }, 55 * 60 * 1000);
479
590
  }
480
591
  catch (err) {
481
592
  console.log(`[clawnexus] [Relay] Failed to initialize (non-fatal): ${err}`);
@@ -521,6 +632,8 @@ async function startDaemon(options = {}) {
521
632
  app.addHook("onClose", async () => {
522
633
  if (tokenRefreshTimer)
523
634
  clearInterval(tokenRefreshTimer);
635
+ skillsRegistry.stop();
636
+ await taskExecutor.close();
524
637
  autoRegister?.stop();
525
638
  agentRouter?.stop();
526
639
  taskManager.close();
@@ -532,6 +645,8 @@ async function startDaemon(options = {}) {
532
645
  await store.close();
533
646
  });
534
647
  await app.listen({ port, host });
648
+ // Start task executor (independent of relay — connects to local OpenClaw Gateway)
649
+ taskExecutor.start();
535
650
  const setConnector = (c) => {
536
651
  connector = c;
537
652
  // When relay connector is set, create and start AgentRouter
@@ -541,9 +656,17 @@ async function startDaemon(options = {}) {
541
656
  engine,
542
657
  tasks: taskManager,
543
658
  localClawId: c.getStatus().claw_id ?? "",
659
+ skillsRegistry,
544
660
  });
545
661
  agentRouter.start();
546
662
  app.log.info("Layer B agent router started");
663
+ // Give executor the router reference so it can send reports
664
+ taskExecutor.setRouter(agentRouter);
665
+ }
666
+ else {
667
+ // Update existing router with new connector (e.g. after token refresh)
668
+ agentRouter.setConnector(c);
669
+ app.log.info("Layer B agent router connector updated");
547
670
  }
548
671
  };
549
672
  return {
@@ -559,6 +682,7 @@ async function startDaemon(options = {}) {
559
682
  engine,
560
683
  tasks: taskManager,
561
684
  getRouter: () => agentRouter,
685
+ skillsRegistry,
562
686
  registryClient,
563
687
  autoRegister,
564
688
  remoteDiscovery,
@@ -46,7 +46,6 @@ const CDP_PORT = 17891;
46
46
  const TCP_PROBE_TIMEOUT = 2_000;
47
47
  const ANNOUNCE_INTERVAL_BASE = 60_000;
48
48
  const ANNOUNCE_JITTER = 10_000;
49
- const CONFIG_PATH = "/__openclaw/control-ui-config.json";
50
49
  class BroadcastDiscovery extends node_events_1.EventEmitter {
51
50
  store;
52
51
  getLocalInstance;
@@ -274,12 +273,14 @@ class BroadcastDiscovery extends node_events_1.EventEmitter {
274
273
  });
275
274
  }
276
275
  async _tcpProbe(address, port) {
277
- const url = `http://${address}:${port}${CONFIG_PATH}`;
276
+ // Quick HTTP liveness check — just confirm the port is alive.
277
+ // Framework identification is left to HealthChecker / subsequent scans.
278
278
  try {
279
- const res = await fetch(url, {
279
+ const res = await fetch(`http://${address}:${port}/`, {
280
280
  signal: AbortSignal.timeout(TCP_PROBE_TIMEOUT),
281
281
  });
282
- return res.ok;
282
+ // Any HTTP response (including 404, 500) means the port is alive
283
+ return res.status > 0;
283
284
  }
284
285
  catch {
285
286
  return false;
@@ -11,4 +11,8 @@ export declare class HealthChecker extends EventEmitter {
11
11
  stop(): void;
12
12
  checkAll(): Promise<void>;
13
13
  private checkOne;
14
+ /** OpenClaw-specific health check with name updates */
15
+ private checkOpenClaw;
16
+ /** Fallback health check for instances without a known adapter */
17
+ private genericHttpCheck;
14
18
  }
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  // Health Checker — periodically pings known instances and updates status
3
3
  // Enhanced with dual-channel connectivity detection
4
+ // Adapter-aware: uses framework-specific health checks for non-OpenClaw instances
4
5
  Object.defineProperty(exports, "__esModule", { value: true });
5
6
  exports.HealthChecker = void 0;
6
7
  const node_events_1 = require("node:events");
8
+ const index_js_1 = require("../adapter/index.js");
7
9
  const CHECK_INTERVAL = 30_000;
8
10
  const PING_TIMEOUT = 5_000;
9
11
  const CONFIG_PATH = "/__openclaw/control-ui-config.json";
@@ -41,36 +43,51 @@ class HealthChecker extends node_events_1.EventEmitter {
41
43
  if (inst.is_self)
42
44
  return;
43
45
  const networkKey = this.store.networkKey(inst.address, inst.gateway_port);
44
- const protocol = inst.tls ? "https" : "http";
45
- const url = `${protocol}://${inst.address}:${inst.gateway_port}${CONFIG_PATH}`;
46
+ const impl = inst.implementation ?? "openclaw";
46
47
  const now = new Date().toISOString();
47
48
  let lanOk = false;
48
49
  let lanLatency;
49
50
  let unreachableReason;
50
- try {
51
- const start = performance.now();
52
- const res = await fetch(url, {
53
- signal: AbortSignal.timeout(PING_TIMEOUT),
54
- });
55
- lanLatency = Math.round(performance.now() - start);
56
- if (res.ok) {
57
- lanOk = true;
58
- const config = (await res.json());
59
- inst.last_seen = now;
60
- if (config.assistantName) {
61
- inst.assistant_name = config.assistantName;
51
+ if (impl === "openclaw" || impl === "goclaw") {
52
+ // OpenClaw path: detailed check with name updates (original behavior)
53
+ const result = await this.checkOpenClaw(inst, now);
54
+ lanOk = result.ok;
55
+ lanLatency = result.latency;
56
+ unreachableReason = result.reason;
57
+ }
58
+ else {
59
+ // Non-OpenClaw: use adapter healthCheck
60
+ const adapter = (0, index_js_1.getAdapter)(impl);
61
+ if (adapter) {
62
+ try {
63
+ const start = performance.now();
64
+ // Use healthCheckLocal for port-0 instances (no HTTP server)
65
+ if (inst.gateway_port === 0 && adapter.healthCheckLocal) {
66
+ lanOk = await adapter.healthCheckLocal();
67
+ }
68
+ else {
69
+ lanOk = await adapter.healthCheck(inst.address, inst.gateway_port);
70
+ }
71
+ lanLatency = Math.round(performance.now() - start);
72
+ if (lanOk) {
73
+ inst.last_seen = now;
74
+ }
75
+ else {
76
+ unreachableReason = "Health check failed";
77
+ }
62
78
  }
63
- if (config.displayName) {
64
- inst.display_name = config.displayName;
79
+ catch (err) {
80
+ unreachableReason = err instanceof Error ? err.message : "Connection failed";
65
81
  }
66
82
  }
67
83
  else {
68
- unreachableReason = `HTTP ${res.status}`;
84
+ // Unknown implementation: generic HTTP check
85
+ const result = await this.genericHttpCheck(inst, now);
86
+ lanOk = result.ok;
87
+ lanLatency = result.latency;
88
+ unreachableReason = result.reason;
69
89
  }
70
90
  }
71
- catch (err) {
72
- unreachableReason = err instanceof Error ? err.message : "Connection failed";
73
- }
74
91
  // Check relay availability
75
92
  const relayAvailable = this.relayChecker?.(inst.agent_id) ?? false;
76
93
  // Update connectivity
@@ -104,5 +121,52 @@ class HealthChecker extends node_events_1.EventEmitter {
104
121
  });
105
122
  }
106
123
  }
124
+ /** OpenClaw-specific health check with name updates */
125
+ async checkOpenClaw(inst, now) {
126
+ const protocol = inst.tls ? "https" : "http";
127
+ const url = `${protocol}://${inst.address}:${inst.gateway_port}${CONFIG_PATH}`;
128
+ try {
129
+ const start = performance.now();
130
+ const res = await fetch(url, {
131
+ signal: AbortSignal.timeout(PING_TIMEOUT),
132
+ });
133
+ const latency = Math.round(performance.now() - start);
134
+ if (res.ok) {
135
+ const config = (await res.json());
136
+ inst.last_seen = now;
137
+ if (config.assistantName) {
138
+ inst.assistant_name = config.assistantName;
139
+ }
140
+ if (config.displayName) {
141
+ inst.display_name = config.displayName;
142
+ }
143
+ return { ok: true, latency };
144
+ }
145
+ return { ok: false, latency, reason: `HTTP ${res.status}` };
146
+ }
147
+ catch (err) {
148
+ return { ok: false, reason: err instanceof Error ? err.message : "Connection failed" };
149
+ }
150
+ }
151
+ /** Fallback health check for instances without a known adapter */
152
+ async genericHttpCheck(inst, now) {
153
+ const protocol = inst.tls ? "https" : "http";
154
+ const url = `${protocol}://${inst.address}:${inst.gateway_port}/`;
155
+ try {
156
+ const start = performance.now();
157
+ const res = await fetch(url, {
158
+ signal: AbortSignal.timeout(PING_TIMEOUT),
159
+ });
160
+ const latency = Math.round(performance.now() - start);
161
+ if (res.ok) {
162
+ inst.last_seen = now;
163
+ return { ok: true, latency };
164
+ }
165
+ return { ok: false, latency, reason: `HTTP ${res.status}` };
166
+ }
167
+ catch (err) {
168
+ return { ok: false, reason: err instanceof Error ? err.message : "Connection failed" };
169
+ }
170
+ }
107
171
  }
108
172
  exports.HealthChecker = HealthChecker;
@@ -11,5 +11,9 @@ export declare class LocalProbe extends EventEmitter {
11
11
  start(): Promise<void>;
12
12
  stop(): void;
13
13
  private _markOffline;
14
+ /** Mark offline any previously-known self instances that were not rediscovered this cycle */
15
+ private _markOfflineStaleSelf;
14
16
  probe(): Promise<ClawInstance | null>;
17
+ /** Original OpenClaw probe logic (kept as primary path for backward compatibility) */
18
+ private probeOpenClaw;
15
19
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
- // LocalProbe — detects OpenClaw instance on localhost and registers it
2
+ // LocalProbe — detects AI instances on localhost and registers them
3
3
  // Runs on daemon startup, then periodically re-checks
4
+ // Adapter-aware: tries all registered adapters on their default ports
4
5
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
6
  if (k2 === undefined) k2 = k;
6
7
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -38,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
38
39
  exports.LocalProbe = void 0;
39
40
  const node_events_1 = require("node:events");
40
41
  const os = __importStar(require("node:os"));
42
+ const index_js_1 = require("../adapter/index.js");
41
43
  const LOCAL_HOST = "127.0.0.1";
42
44
  const DEFAULT_PORT = 18789;
43
45
  const CONFIG_PATH = "/__openclaw/control-ui-config.json";
@@ -71,8 +73,8 @@ class LocalProbe extends node_events_1.EventEmitter {
71
73
  this.timer = null;
72
74
  }
73
75
  }
74
- _markOffline() {
75
- const existing = this.store.getByNetworkKey(LOCAL_HOST, this.port);
76
+ _markOffline(port) {
77
+ const existing = this.store.getByNetworkKey(LOCAL_HOST, port);
76
78
  if (existing && existing.status !== "offline") {
77
79
  this.store.upsert({
78
80
  ...existing,
@@ -81,21 +83,135 @@ class LocalProbe extends node_events_1.EventEmitter {
81
83
  });
82
84
  }
83
85
  }
86
+ /** Mark offline any previously-known self instances that were not rediscovered this cycle */
87
+ _markOfflineStaleSelf(staleKeys) {
88
+ for (const key of staleKeys) {
89
+ const [address, portStr] = key.split(":");
90
+ this._markOffline(Number(portStr));
91
+ }
92
+ }
84
93
  async probe() {
85
- const url = `http://${LOCAL_HOST}:${this.port}${CONFIG_PATH}`;
94
+ // Snapshot all current is_self instances so we can mark stale ones offline later
95
+ const previousSelfKeys = new Set(this.store
96
+ .getAll()
97
+ .filter((inst) => inst.is_self)
98
+ .map((inst) => `${inst.address}:${inst.gateway_port}`));
99
+ // 1. Try OpenClaw on the configured port (backward-compatible primary path)
100
+ const openClawResult = await this.probeOpenClaw(this.port);
101
+ if (openClawResult) {
102
+ previousSelfKeys.delete(`${LOCAL_HOST}:${this.port}`);
103
+ this._markOfflineStaleSelf(previousSelfKeys);
104
+ return openClawResult;
105
+ }
106
+ // 2. Try all adapters on their default ports (skip OpenClaw on this.port, already tried)
107
+ let found = null;
108
+ for (const adapter of index_js_1.ADAPTERS) {
109
+ if (adapter.name === "openclaw")
110
+ continue; // already tried above
111
+ // Try probeLocal first (for frameworks without HTTP servers, e.g. NanoClaw)
112
+ if (adapter.probeLocal) {
113
+ const probeResult = await adapter.probeLocal();
114
+ if (probeResult) {
115
+ const now = new Date().toISOString();
116
+ const partial = adapter.toClawInstance(LOCAL_HOST, 0, probeResult);
117
+ const instance = {
118
+ agent_id: partial.agent_id ?? `${adapter.name}@localhost`,
119
+ auto_name: "",
120
+ assistant_name: partial.assistant_name ?? "",
121
+ display_name: partial.display_name ?? adapter.name,
122
+ lan_host: os.hostname(),
123
+ address: LOCAL_HOST,
124
+ gateway_port: partial.gateway_port ?? 0,
125
+ tls: false,
126
+ discovery_source: "local",
127
+ network_scope: "local",
128
+ status: "online",
129
+ last_seen: now,
130
+ discovered_at: now,
131
+ implementation: partial.implementation,
132
+ connectivity: {
133
+ lan_reachable: false,
134
+ relay_available: false,
135
+ preferred_channel: "local",
136
+ last_lan_check: now,
137
+ },
138
+ is_self: true,
139
+ labels: partial.labels,
140
+ };
141
+ this.store.upsert(instance);
142
+ this.emit("local:discovered", instance);
143
+ previousSelfKeys.delete(`${LOCAL_HOST}:${instance.gateway_port}`);
144
+ found = instance;
145
+ break;
146
+ }
147
+ }
148
+ // Then try HTTP probe on default ports
149
+ if (!found) {
150
+ for (const port of adapter.defaultPorts) {
151
+ const probeResult = await adapter.probe(LOCAL_HOST, port);
152
+ if (probeResult) {
153
+ const now = new Date().toISOString();
154
+ const partial = adapter.toClawInstance(LOCAL_HOST, port, probeResult);
155
+ const instance = {
156
+ agent_id: partial.agent_id ?? `${adapter.name}@localhost`,
157
+ auto_name: "",
158
+ assistant_name: partial.assistant_name ?? "",
159
+ display_name: partial.display_name ?? adapter.name,
160
+ lan_host: os.hostname(),
161
+ address: LOCAL_HOST,
162
+ gateway_port: port,
163
+ tls: false,
164
+ discovery_source: "local",
165
+ network_scope: "local",
166
+ status: "online",
167
+ last_seen: now,
168
+ discovered_at: now,
169
+ implementation: partial.implementation,
170
+ connectivity: {
171
+ lan_reachable: true,
172
+ relay_available: false,
173
+ preferred_channel: "local",
174
+ last_lan_check: now,
175
+ },
176
+ is_self: true,
177
+ };
178
+ this.store.upsert(instance);
179
+ this.emit("local:discovered", instance);
180
+ previousSelfKeys.delete(`${LOCAL_HOST}:${port}`);
181
+ found = instance;
182
+ break;
183
+ }
184
+ }
185
+ }
186
+ if (found)
187
+ break;
188
+ }
189
+ if (found) {
190
+ this._markOfflineStaleSelf(previousSelfKeys);
191
+ return found;
192
+ }
193
+ // Nothing found
194
+ this.localAgentId = null;
195
+ this._markOfflineStaleSelf(previousSelfKeys);
196
+ this.emit("local:unavailable");
197
+ return null;
198
+ }
199
+ /** Original OpenClaw probe logic (kept as primary path for backward compatibility) */
200
+ async probeOpenClaw(port) {
201
+ const url = `http://${LOCAL_HOST}:${port}${CONFIG_PATH}`;
86
202
  try {
87
203
  const res = await fetch(url, {
88
204
  signal: AbortSignal.timeout(PROBE_TIMEOUT),
89
205
  });
90
206
  if (!res.ok) {
91
- this._markOffline();
92
- this.emit("local:unreachable", { reason: `HTTP ${res.status}` });
207
+ // Port responded but not valid OpenClaw — don't mark offline or emit unreachable,
208
+ // because another adapter may be occupying this port. Let the adapter loop try.
93
209
  return null;
94
210
  }
95
211
  const config = (await res.json());
96
212
  if (!config.assistantAgentId) {
97
- this._markOffline();
98
- this.emit("local:unreachable", { reason: "missing assistantAgentId" });
213
+ // Has an HTTP server but no assistantAgentId — not a valid OpenClaw instance.
214
+ // Don't mark offline; another adapter may match this endpoint.
99
215
  return null;
100
216
  }
101
217
  this.localAgentId = config.assistantAgentId;
@@ -107,7 +223,7 @@ class LocalProbe extends node_events_1.EventEmitter {
107
223
  display_name: config.displayName ?? config.assistantName ?? "",
108
224
  lan_host: os.hostname(),
109
225
  address: LOCAL_HOST,
110
- gateway_port: this.port,
226
+ gateway_port: port,
111
227
  tls: false,
112
228
  discovery_source: "local",
113
229
  network_scope: "local",
@@ -128,9 +244,7 @@ class LocalProbe extends node_events_1.EventEmitter {
128
244
  return instance;
129
245
  }
130
246
  catch {
131
- this.localAgentId = null;
132
- this._markOffline();
133
- this.emit("local:unavailable");
247
+ // OpenClaw not running on this port — don't emit yet, try adapters first
134
248
  return null;
135
249
  }
136
250
  }