clawnexus 0.2.7 → 0.3.0

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.
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ // Framework adapter interface — pluggable probe logic for non-OpenClaw variants
3
+ // that don't share port 18789 or serve /__openclaw/control-ui-config.json
4
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -39,6 +39,7 @@ export interface DiagnosticsDeps {
39
39
  unreachable: UnreachableInstance[];
40
40
  }
41
41
  export declare function registerDiagnosticsRoutes(app: FastifyInstance, deps: DiagnosticsDeps): void;
42
+ export declare function registerA2aRoutes(app: FastifyInstance, store: RegistryStore, daemonVersion: string): void;
42
43
  export interface DaemonOptions {
43
44
  port?: number;
44
45
  host?: string;
@@ -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");
@@ -27,6 +28,9 @@ const client_js_1 = require("../registry/client.js");
27
28
  const auto_register_js_1 = require("../registry/auto-register.js");
28
29
  const discovery_js_1 = require("../registry/discovery.js");
29
30
  const connector_js_1 = require("../relay/connector.js");
31
+ const card_js_1 = require("../a2a/card.js");
32
+ const node_fs_1 = require("node:fs");
33
+ const node_path_1 = require("node:path");
30
34
  const PORT = parseInt(process.env.CLAWNEXUS_PORT ?? "17890", 10);
31
35
  const HOST = process.env.CLAWNEXUS_HOST ?? "127.0.0.1";
32
36
  function registerRelayRoutes(app, getConnector) {
@@ -301,6 +305,30 @@ function registerDiagnosticsRoutes(app, deps) {
301
305
  };
302
306
  });
303
307
  }
308
+ function registerA2aRoutes(app, store, daemonVersion) {
309
+ // A2A standard well-known endpoint — returns card for the local (is_self) instance
310
+ app.get("/.well-known/agent-card.json", async (_request, reply) => {
311
+ const self = store.getAll().find((i) => i.is_self);
312
+ if (!self) {
313
+ return reply.status(404).send({ error: "No local instance discovered" });
314
+ }
315
+ return (0, card_js_1.buildAgentCard)(self, daemonVersion);
316
+ });
317
+ // All instances as Agent Cards
318
+ app.get("/a2a/cards", async () => {
319
+ const instances = store.getAll();
320
+ const cards = instances.map((i) => (0, card_js_1.buildAgentCard)(i, daemonVersion));
321
+ return { count: cards.length, cards };
322
+ });
323
+ // Single instance Agent Card by name
324
+ app.get("/a2a/cards/:name", async (request, reply) => {
325
+ const inst = store.resolve(request.params.name);
326
+ if (!inst) {
327
+ return reply.status(404).send({ error: "Instance not found" });
328
+ }
329
+ return (0, card_js_1.buildAgentCard)(inst, daemonVersion);
330
+ });
331
+ }
304
332
  async function startDaemon(options = {}) {
305
333
  const port = options.port ?? PORT;
306
334
  const host = options.host ?? HOST;
@@ -390,6 +418,9 @@ async function startDaemon(options = {}) {
390
418
  getAutoRegister: () => autoRegister,
391
419
  unreachable,
392
420
  });
421
+ // A2A Agent Card routes
422
+ const daemonPkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, "../../package.json"), "utf-8"));
423
+ registerA2aRoutes(app, store, daemonPkg.version);
393
424
  // 9. Initialize Registry integration (non-fatal — LAN must work without it)
394
425
  let identityKeys = null;
395
426
  let registryClient = null;
package/dist/cli/index.js CHANGED
@@ -156,6 +156,7 @@ function printTable(instances) {
156
156
  }
157
157
  const header = {
158
158
  name: "NAME",
159
+ impl: "IMPL",
159
160
  address: "ADDRESS",
160
161
  status: "STATUS",
161
162
  channel: "CHANNEL",
@@ -166,6 +167,7 @@ function printTable(instances) {
166
167
  const baseName = i.alias ?? i.auto_name;
167
168
  return {
168
169
  name: i.is_self ? `${baseName} (self)` : baseName,
170
+ impl: i.implementation ?? "-",
169
171
  address: `${i.address}:${i.gateway_port}`,
170
172
  status: i.status,
171
173
  channel: getChannel(i),
@@ -175,15 +177,16 @@ function printTable(instances) {
175
177
  });
176
178
  const colWidths = {
177
179
  name: Math.max(header.name.length, ...rows.map((r) => r.name.length)),
180
+ impl: Math.max(header.impl.length, ...rows.map((r) => r.impl.length)),
178
181
  address: Math.max(header.address.length, ...rows.map((r) => r.address.length)),
179
182
  status: Math.max(header.status.length, ...rows.map((r) => r.status.length)),
180
183
  channel: Math.max(header.channel.length, ...rows.map((r) => r.channel.length)),
181
184
  source: Math.max(header.source.length, ...rows.map((r) => r.source.length)),
182
185
  lastSeen: Math.max(header.lastSeen.length, ...rows.map((r) => r.lastSeen.length)),
183
186
  };
184
- const line = (r) => `${r.name.padEnd(colWidths.name)} ${r.address.padEnd(colWidths.address)} ${r.status.padEnd(colWidths.status)} ${r.channel.padEnd(colWidths.channel)} ${r.source.padEnd(colWidths.source)} ${r.lastSeen}`;
187
+ const line = (r) => `${r.name.padEnd(colWidths.name)} ${r.impl.padEnd(colWidths.impl)} ${r.address.padEnd(colWidths.address)} ${r.status.padEnd(colWidths.status)} ${r.channel.padEnd(colWidths.channel)} ${r.source.padEnd(colWidths.source)} ${r.lastSeen}`;
185
188
  console.log(line(header));
186
- console.log("-".repeat(colWidths.name + colWidths.address + colWidths.status + colWidths.channel + colWidths.source + colWidths.lastSeen + 10));
189
+ console.log("-".repeat(colWidths.name + colWidths.impl + colWidths.address + colWidths.status + colWidths.channel + colWidths.source + colWidths.lastSeen + 12));
187
190
  for (const row of rows) {
188
191
  console.log(line(row));
189
192
  }
@@ -404,6 +407,8 @@ async function cmdInfo(args) {
404
407
  const inst = data;
405
408
  console.log(`Auto Name: ${inst.auto_name}${inst.is_self ? " (self)" : ""}`);
406
409
  console.log(`Agent ID: ${inst.agent_id}`);
410
+ if (inst.implementation)
411
+ console.log(`Implementation: ${inst.implementation}`);
407
412
  console.log(`Display Name: ${inst.display_name}`);
408
413
  console.log(`Assistant: ${inst.assistant_name}`);
409
414
  if (inst.alias)
@@ -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;
package/dist/index.d.ts CHANGED
@@ -6,6 +6,10 @@ export { HealthChecker } from './health/checker.js';
6
6
  export { LocalProbe } from './local/probe.js';
7
7
  export { ActiveScanner } from './scanner/active.js';
8
8
  export type { ScanOptions } from './scanner/active.js';
9
+ export { ADAPTERS, getAdapter, getAllAdapterPorts } from './adapter/index.js';
10
+ export { NanoClawAdapter } from './adapter/nanoclaw.js';
11
+ export { NanoBotAdapter } from './adapter/nanobot.js';
12
+ export type { FrameworkAdapter, ProbeResult } from './adapter/types.js';
9
13
  export { BroadcastDiscovery } from './discovery/broadcast.js';
10
14
  export { registerRelayRoutes, registerInstanceRoutes, registerAgentRoutes, registerDiagnosticsRoutes, registerRegistryRoutes, startDaemon } from './api/server.js';
11
15
  export type { DaemonOptions, DaemonHandle, AgentDeps, DiagnosticsDeps, RegistryDeps } from './api/server.js';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ProtocolError = exports.isExpired = exports.validatePayload = exports.parseEnvelope = exports.createEnvelope = exports.AgentRouter = exports.TaskManager = exports.PolicyEngine = exports.RelayConnector = exports.RemoteDiscovery = exports.AutoRegister = exports.RegistryError = exports.RegistryClient = exports.getPublicKeyString = exports.sign = exports.loadOrCreateKeys = exports.startDaemon = exports.registerRegistryRoutes = exports.registerDiagnosticsRoutes = exports.registerAgentRoutes = exports.registerInstanceRoutes = exports.registerRelayRoutes = exports.BroadcastDiscovery = exports.ActiveScanner = exports.LocalProbe = exports.HealthChecker = exports.MdnsListener = exports.NotFoundError = exports.AliasConflictError = exports.AliasError = exports.RegistryStore = void 0;
3
+ exports.ProtocolError = exports.isExpired = exports.validatePayload = exports.parseEnvelope = exports.createEnvelope = exports.AgentRouter = exports.TaskManager = exports.PolicyEngine = exports.RelayConnector = exports.RemoteDiscovery = exports.AutoRegister = exports.RegistryError = exports.RegistryClient = exports.getPublicKeyString = exports.sign = exports.loadOrCreateKeys = exports.startDaemon = exports.registerRegistryRoutes = exports.registerDiagnosticsRoutes = exports.registerAgentRoutes = exports.registerInstanceRoutes = exports.registerRelayRoutes = exports.BroadcastDiscovery = exports.NanoBotAdapter = exports.NanoClawAdapter = exports.getAllAdapterPorts = exports.getAdapter = exports.ADAPTERS = exports.ActiveScanner = exports.LocalProbe = exports.HealthChecker = exports.MdnsListener = exports.NotFoundError = exports.AliasConflictError = exports.AliasError = exports.RegistryStore = void 0;
4
4
  var store_js_1 = require("./registry/store.js");
5
5
  Object.defineProperty(exports, "RegistryStore", { enumerable: true, get: function () { return store_js_1.RegistryStore; } });
6
6
  Object.defineProperty(exports, "AliasError", { enumerable: true, get: function () { return store_js_1.AliasError; } });
@@ -14,6 +14,14 @@ var probe_js_1 = require("./local/probe.js");
14
14
  Object.defineProperty(exports, "LocalProbe", { enumerable: true, get: function () { return probe_js_1.LocalProbe; } });
15
15
  var active_js_1 = require("./scanner/active.js");
16
16
  Object.defineProperty(exports, "ActiveScanner", { enumerable: true, get: function () { return active_js_1.ActiveScanner; } });
17
+ var index_js_1 = require("./adapter/index.js");
18
+ Object.defineProperty(exports, "ADAPTERS", { enumerable: true, get: function () { return index_js_1.ADAPTERS; } });
19
+ Object.defineProperty(exports, "getAdapter", { enumerable: true, get: function () { return index_js_1.getAdapter; } });
20
+ Object.defineProperty(exports, "getAllAdapterPorts", { enumerable: true, get: function () { return index_js_1.getAllAdapterPorts; } });
21
+ var nanoclaw_js_1 = require("./adapter/nanoclaw.js");
22
+ Object.defineProperty(exports, "NanoClawAdapter", { enumerable: true, get: function () { return nanoclaw_js_1.NanoClawAdapter; } });
23
+ var nanobot_js_1 = require("./adapter/nanobot.js");
24
+ Object.defineProperty(exports, "NanoBotAdapter", { enumerable: true, get: function () { return nanobot_js_1.NanoBotAdapter; } });
17
25
  var broadcast_js_1 = require("./discovery/broadcast.js");
18
26
  Object.defineProperty(exports, "BroadcastDiscovery", { enumerable: true, get: function () { return broadcast_js_1.BroadcastDiscovery; } });
19
27
  var server_js_1 = require("./api/server.js");
@@ -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,13 +223,14 @@ 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",
114
230
  status: "online",
115
231
  last_seen: now,
116
232
  discovered_at: now,
233
+ implementation: "openclaw",
117
234
  connectivity: {
118
235
  lan_reachable: true,
119
236
  relay_available: false,
@@ -127,9 +244,7 @@ class LocalProbe extends node_events_1.EventEmitter {
127
244
  return instance;
128
245
  }
129
246
  catch {
130
- this.localAgentId = null;
131
- this._markOffline();
132
- this.emit("local:unavailable");
247
+ // OpenClaw not running on this port — don't emit yet, try adapters first
133
248
  return null;
134
249
  }
135
250
  }
@@ -201,6 +201,8 @@ class RegistryStore extends node_events_1.EventEmitter {
201
201
  // Preserve registry fields
202
202
  instance.claw_name = instance.claw_name ?? existing.claw_name;
203
203
  instance.owner_pubkey = instance.owner_pubkey ?? existing.owner_pubkey;
204
+ // Preserve implementation (prefer new value if provided)
205
+ instance.implementation = instance.implementation ?? existing.implementation;
204
206
  this.instances.set(key, instance);
205
207
  this.scheduleDirtyFlush();
206
208
  this.emit("upsert", instance);
@@ -315,6 +317,7 @@ class RegistryStore extends node_events_1.EventEmitter {
315
317
  labels: existing.labels ?? incoming.labels,
316
318
  connectivity: incoming.connectivity ?? existing.connectivity,
317
319
  is_self: existing.is_self || incoming.is_self,
320
+ implementation: incoming.implementation ?? existing.implementation,
318
321
  };
319
322
  }
320
323
  /** Numeric priority for network scope: local > vpn > public */
@@ -18,6 +18,7 @@ export declare class ActiveScanner extends EventEmitter {
18
18
  private generateIPs;
19
19
  private probeAll;
20
20
  private probeHost;
21
+ private probeConfigEndpoint;
21
22
  /**
22
23
  * Resolve a stable lan_host for scanned instances to enable multi-NIC dedup.
23
24
  *