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.
- package/dist/a2a/card.d.ts +29 -0
- package/dist/a2a/card.js +30 -0
- package/dist/adapter/index.js +4 -0
- package/dist/adapter/nanobot.d.ts +1 -0
- package/dist/adapter/nanobot.js +17 -2
- package/dist/adapter/nanoclaw.d.ts +21 -4
- package/dist/adapter/nanoclaw.js +229 -44
- package/dist/adapter/openclaw.d.ts +9 -0
- package/dist/adapter/openclaw.js +62 -0
- package/dist/adapter/openfang.d.ts +11 -0
- package/dist/adapter/openfang.js +88 -0
- package/dist/adapter/types.d.ts +3 -0
- package/dist/agent/engine.js +9 -2
- package/dist/agent/executor.d.ts +48 -0
- package/dist/agent/executor.js +374 -0
- package/dist/agent/gateway.d.ts +26 -0
- package/dist/agent/gateway.js +298 -0
- package/dist/agent/protocol.js +2 -0
- package/dist/agent/router.d.ts +10 -1
- package/dist/agent/router.js +31 -2
- package/dist/agent/services.d.ts +36 -0
- package/dist/agent/services.js +153 -0
- package/dist/agent/tasks.js +4 -2
- package/dist/api/server.d.ts +7 -1
- package/dist/api/server.js +134 -10
- package/dist/discovery/broadcast.js +5 -4
- package/dist/health/checker.d.ts +4 -0
- package/dist/health/checker.js +84 -20
- package/dist/local/probe.d.ts +4 -0
- package/dist/local/probe.js +126 -12
- package/dist/registry/auto-register.js +32 -20
- package/dist/relay/connector.d.ts +5 -1
- package/dist/relay/connector.js +13 -2
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
package/dist/api/server.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
//
|
|
459
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
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(
|
|
279
|
+
const res = await fetch(`http://${address}:${port}/`, {
|
|
280
280
|
signal: AbortSignal.timeout(TCP_PROBE_TIMEOUT),
|
|
281
281
|
});
|
|
282
|
-
|
|
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;
|
package/dist/health/checker.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/health/checker.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
79
|
+
catch (err) {
|
|
80
|
+
unreachableReason = err instanceof Error ? err.message : "Connection failed";
|
|
65
81
|
}
|
|
66
82
|
}
|
|
67
83
|
else {
|
|
68
|
-
|
|
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/local/probe.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/local/probe.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// LocalProbe — detects
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
this.
|
|
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
|
-
|
|
98
|
-
|
|
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:
|
|
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
|
|
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
|
}
|