openclaw-node-harness 2.0.2 → 2.0.3
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/bin/fleet-deploy.js +1 -1
- package/bin/mesh-agent.js +3 -3
- package/bin/mesh-deploy-listener.js +1 -1
- package/bin/mesh-deploy.js +3 -3
- package/bin/mesh-health-publisher.js +1 -1
- package/bin/mesh.js +155 -22
- package/bin/openclaw-node-init.js +147 -3
- package/lib/mesh-tasks.js +6 -7
- package/package.json +1 -1
package/bin/fleet-deploy.js
CHANGED
|
@@ -23,7 +23,7 @@ const os = require('os');
|
|
|
23
23
|
|
|
24
24
|
const { NATS_URL, natsConnectOpts } = require('../lib/nats-resolve');
|
|
25
25
|
const sc = StringCodec();
|
|
26
|
-
const REPO_DIR = process.env.OPENCLAW_REPO_DIR || path.join(os.homedir(), 'openclaw
|
|
26
|
+
const REPO_DIR = process.env.OPENCLAW_REPO_DIR || path.join(os.homedir(), 'openclaw');
|
|
27
27
|
const NODE_ID = process.env.OPENCLAW_NODE_ID ||
|
|
28
28
|
os.hostname().toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
29
29
|
|
package/bin/mesh-agent.js
CHANGED
|
@@ -117,7 +117,7 @@ function buildInitialPrompt(task) {
|
|
|
117
117
|
parts.push('');
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
if (task.success_criteria.length > 0) {
|
|
120
|
+
if (task.success_criteria && task.success_criteria.length > 0) {
|
|
121
121
|
parts.push('## Success Criteria');
|
|
122
122
|
for (const c of task.success_criteria) {
|
|
123
123
|
parts.push(`- ${c}`);
|
|
@@ -132,7 +132,7 @@ function buildInitialPrompt(task) {
|
|
|
132
132
|
parts.push('');
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
if (task.scope.length > 0) {
|
|
135
|
+
if (task.scope && task.scope.length > 0) {
|
|
136
136
|
parts.push('## Scope');
|
|
137
137
|
parts.push('Only modify these files/paths:');
|
|
138
138
|
for (const s of task.scope) {
|
|
@@ -192,7 +192,7 @@ function buildRetryPrompt(task, previousAttempts, attemptNumber) {
|
|
|
192
192
|
parts.push('');
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
if (task.scope.length > 0) {
|
|
195
|
+
if (task.scope && task.scope.length > 0) {
|
|
196
196
|
parts.push('## Scope');
|
|
197
197
|
for (const s of task.scope) {
|
|
198
198
|
parts.push(`- ${s}`);
|
|
@@ -26,7 +26,7 @@ const os = require('os');
|
|
|
26
26
|
const NODE_ID = process.env.OPENCLAW_NODE_ID ||
|
|
27
27
|
os.hostname().toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
28
28
|
const REPO_DIR = process.env.OPENCLAW_REPO_DIR ||
|
|
29
|
-
path.join(os.homedir(), 'openclaw
|
|
29
|
+
path.join(os.homedir(), 'openclaw');
|
|
30
30
|
const DEPLOY_SCRIPT = path.join(REPO_DIR, 'bin', 'mesh-deploy.js');
|
|
31
31
|
|
|
32
32
|
const { NATS_URL, natsConnectOpts } = require('../lib/nats-resolve');
|
package/bin/mesh-deploy.js
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
*
|
|
33
33
|
* ENVIRONMENT:
|
|
34
34
|
* OPENCLAW_DEPLOY_BRANCH — git branch (default: main)
|
|
35
|
-
* OPENCLAW_REPO_DIR — repo location (default: ~/openclaw
|
|
35
|
+
* OPENCLAW_REPO_DIR — repo location (default: ~/openclaw)
|
|
36
36
|
* OPENCLAW_NATS — NATS server URL (from env or openclaw.env)
|
|
37
37
|
*/
|
|
38
38
|
|
|
@@ -47,7 +47,7 @@ const crypto = require('crypto');
|
|
|
47
47
|
const IS_MAC = os.platform() === 'darwin';
|
|
48
48
|
const HOME = os.homedir();
|
|
49
49
|
const DEPLOY_BRANCH = process.env.OPENCLAW_DEPLOY_BRANCH || 'main';
|
|
50
|
-
const REPO_DIR = process.env.OPENCLAW_REPO_DIR || path.join(HOME, 'openclaw
|
|
50
|
+
const REPO_DIR = process.env.OPENCLAW_REPO_DIR || path.join(HOME, 'openclaw');
|
|
51
51
|
|
|
52
52
|
// Standard directory layout
|
|
53
53
|
const DIRS = {
|
|
@@ -884,7 +884,7 @@ async function main() {
|
|
|
884
884
|
|
|
885
885
|
if (!fs.existsSync(REPO_DIR)) {
|
|
886
886
|
fail(`Repo not found at ${REPO_DIR}`);
|
|
887
|
-
console.log(` Clone it: git clone https://github.com/moltyguibros-design/openclaw-node.git ${REPO_DIR}`);
|
|
887
|
+
console.log(` Clone it: git clone https://github.com/moltyguibros-design/openclaw-node.git "${REPO_DIR}"`);
|
|
888
888
|
process.exit(1);
|
|
889
889
|
}
|
|
890
890
|
|
|
@@ -29,7 +29,7 @@ const HEALTH_BUCKET = "MESH_NODE_HEALTH";
|
|
|
29
29
|
const KV_TTL_MS = 120_000; // entries expire after 2 minutes if node dies
|
|
30
30
|
|
|
31
31
|
const REPO_DIR = process.env.OPENCLAW_REPO_DIR ||
|
|
32
|
-
path.join(os.homedir(), 'openclaw
|
|
32
|
+
path.join(os.homedir(), 'openclaw');
|
|
33
33
|
|
|
34
34
|
const sc = StringCodec();
|
|
35
35
|
const IS_MAC = os.platform() === "darwin";
|
package/bin/mesh.js
CHANGED
|
@@ -29,23 +29,44 @@ const path = require('path');
|
|
|
29
29
|
const os = require('os');
|
|
30
30
|
|
|
31
31
|
// ─── Config ──────────────────────────────────────────
|
|
32
|
-
// NATS URL
|
|
33
|
-
const
|
|
32
|
+
// ── NATS URL resolution: env var → ~/.openclaw/openclaw.env → fallback IP ──
|
|
33
|
+
const NATS_FALLBACK = 'nats://100.91.131.61:4222';
|
|
34
|
+
function resolveNatsUrl() {
|
|
35
|
+
if (process.env.OPENCLAW_NATS) return process.env.OPENCLAW_NATS;
|
|
36
|
+
try {
|
|
37
|
+
const envFile = path.join(os.homedir(), '.openclaw', 'openclaw.env');
|
|
38
|
+
if (fs.existsSync(envFile)) {
|
|
39
|
+
const content = fs.readFileSync(envFile, 'utf8');
|
|
40
|
+
const match = content.match(/^\s*OPENCLAW_NATS\s*=\s*(.+)/m);
|
|
41
|
+
if (match && match[1].trim()) return match[1].trim();
|
|
42
|
+
}
|
|
43
|
+
} catch {}
|
|
44
|
+
return NATS_FALLBACK;
|
|
45
|
+
}
|
|
46
|
+
const NATS_URL = resolveNatsUrl();
|
|
34
47
|
const SHARED_DIR = path.join(os.homedir(), 'openclaw', 'shared');
|
|
35
48
|
const LOCAL_NODE = os.hostname().toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
36
49
|
const sc = StringCodec();
|
|
37
50
|
|
|
38
51
|
// ─── Known nodes (for --node shortcuts) ──────────────
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
const NODE_ALIASES_DEFAULTS = {
|
|
53
|
+
'ubuntu': 'calos-vmware-virtual-platform',
|
|
54
|
+
'linux': 'calos-vmware-virtual-platform',
|
|
55
|
+
'mac': 'moltymacs-virtual-machine-local',
|
|
56
|
+
'macos': 'moltymacs-virtual-machine-local',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function loadNodeAliases() {
|
|
60
|
+
const aliasPath = path.join(os.homedir(), '.openclaw', 'mesh-aliases.json');
|
|
61
|
+
try {
|
|
62
|
+
if (fs.existsSync(aliasPath)) {
|
|
63
|
+
const custom = JSON.parse(fs.readFileSync(aliasPath, 'utf8'));
|
|
64
|
+
return { ...NODE_ALIASES_DEFAULTS, ...custom };
|
|
65
|
+
}
|
|
66
|
+
} catch {}
|
|
67
|
+
return NODE_ALIASES_DEFAULTS;
|
|
48
68
|
}
|
|
69
|
+
const NODE_ALIASES = loadNodeAliases();
|
|
49
70
|
|
|
50
71
|
/**
|
|
51
72
|
* Resolve a node name — accepts aliases, full IDs, or "self"/"local"
|
|
@@ -98,7 +119,7 @@ function checkExecSafety(command) {
|
|
|
98
119
|
*/
|
|
99
120
|
async function natsConnect() {
|
|
100
121
|
try {
|
|
101
|
-
return await connect(
|
|
122
|
+
return await connect({ servers: NATS_URL, timeout: 5000 });
|
|
102
123
|
} catch (err) {
|
|
103
124
|
console.error(`Error: Cannot connect to NATS at ${NATS_URL}`);
|
|
104
125
|
console.error(`Is the NATS server running? Is Tailscale connected?`);
|
|
@@ -140,21 +161,15 @@ async function collectHeartbeats(nc, waitMs = 3000) {
|
|
|
140
161
|
uptime: os.uptime(),
|
|
141
162
|
};
|
|
142
163
|
|
|
143
|
-
// Force-unsubscribe after deadline to prevent hanging if no messages arrive
|
|
144
|
-
const timer = setTimeout(() => sub.unsubscribe(), waitMs);
|
|
145
|
-
|
|
146
164
|
// Listen for heartbeats for a few seconds
|
|
147
165
|
const deadline = Date.now() + waitMs;
|
|
148
166
|
for await (const msg of sub) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
} catch {}
|
|
167
|
+
const s = JSON.parse(sc.decode(msg.data));
|
|
168
|
+
if (s.node !== LOCAL_NODE) {
|
|
169
|
+
nodes[s.node] = s;
|
|
170
|
+
}
|
|
155
171
|
if (Date.now() >= deadline) break;
|
|
156
172
|
}
|
|
157
|
-
clearTimeout(timer);
|
|
158
173
|
sub.unsubscribe();
|
|
159
174
|
return nodes;
|
|
160
175
|
}
|
|
@@ -575,6 +590,119 @@ async function cmdRepair(args) {
|
|
|
575
590
|
}
|
|
576
591
|
}
|
|
577
592
|
|
|
593
|
+
/**
|
|
594
|
+
* mesh deploy [--force] [--component <name>] [--node <name>] — trigger fleet deploy.
|
|
595
|
+
*
|
|
596
|
+
* Publishes mesh.deploy.trigger to NATS. All nodes with mesh-deploy-listener
|
|
597
|
+
* will pull from git and self-deploy. Polls MESH_DEPLOY_RESULTS for status.
|
|
598
|
+
*/
|
|
599
|
+
async function cmdDeploy(args) {
|
|
600
|
+
const { execSync } = require('child_process');
|
|
601
|
+
const repoDir = process.env.OPENCLAW_REPO_DIR || path.join(os.homedir(), 'openclaw');
|
|
602
|
+
const force = args.includes('--force');
|
|
603
|
+
|
|
604
|
+
// Parse --component flags
|
|
605
|
+
const components = [];
|
|
606
|
+
for (let i = 0; i < args.length; i++) {
|
|
607
|
+
if (args[i] === '--component' && args[i + 1]) {
|
|
608
|
+
components.push(args[i + 1]);
|
|
609
|
+
i++;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Parse --node flags (target specific nodes, default: all)
|
|
614
|
+
const targetNodes = [];
|
|
615
|
+
for (let i = 0; i < args.length; i++) {
|
|
616
|
+
if (args[i] === '--node' && args[i + 1]) {
|
|
617
|
+
targetNodes.push(resolveNode(args[i + 1]));
|
|
618
|
+
i++;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Get current SHA and branch
|
|
623
|
+
let sha, branch;
|
|
624
|
+
try {
|
|
625
|
+
sha = execSync('git rev-parse --short HEAD', { cwd: repoDir, encoding: 'utf8' }).trim();
|
|
626
|
+
branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: repoDir, encoding: 'utf8' }).trim();
|
|
627
|
+
} catch {
|
|
628
|
+
console.error(`Error: Cannot read git state from ${repoDir}`);
|
|
629
|
+
process.exit(1);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
console.log(`Deploying ${sha} (${branch})${force ? ' [FORCE]' : ''}`);
|
|
633
|
+
if (components.length > 0) console.log(` Components: ${components.join(', ')}`);
|
|
634
|
+
if (targetNodes.length > 0) console.log(` Targets: ${targetNodes.join(', ')}`);
|
|
635
|
+
else console.log(' Targets: all nodes');
|
|
636
|
+
|
|
637
|
+
const nc = await natsConnect();
|
|
638
|
+
|
|
639
|
+
const trigger = {
|
|
640
|
+
sha,
|
|
641
|
+
branch,
|
|
642
|
+
components: components.length > 0 ? components : ['all'],
|
|
643
|
+
nodes: targetNodes.length > 0 ? targetNodes : ['all'],
|
|
644
|
+
force,
|
|
645
|
+
initiator: LOCAL_NODE,
|
|
646
|
+
timestamp: new Date().toISOString(),
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
// Write "latest" marker so offline nodes can catch up
|
|
650
|
+
try {
|
|
651
|
+
const js = nc.jetstream();
|
|
652
|
+
const resultsKv = await js.views.kv('MESH_DEPLOY_RESULTS', { history: 5, ttl: 7 * 24 * 60 * 60 * 1000 });
|
|
653
|
+
await resultsKv.put('latest', sc.encode(JSON.stringify({ sha, branch })));
|
|
654
|
+
} catch {}
|
|
655
|
+
|
|
656
|
+
// Publish trigger
|
|
657
|
+
nc.publish('mesh.deploy.trigger', sc.encode(JSON.stringify(trigger)));
|
|
658
|
+
await nc.flush();
|
|
659
|
+
console.log('Deploy trigger sent.\n');
|
|
660
|
+
|
|
661
|
+
// Poll for results (15s timeout)
|
|
662
|
+
console.log('Waiting for node responses...');
|
|
663
|
+
const deadline = Date.now() + 15000;
|
|
664
|
+
const seen = new Set();
|
|
665
|
+
|
|
666
|
+
try {
|
|
667
|
+
const js = nc.jetstream();
|
|
668
|
+
const resultsKv = await js.views.kv('MESH_DEPLOY_RESULTS');
|
|
669
|
+
|
|
670
|
+
while (Date.now() < deadline) {
|
|
671
|
+
const allAliasNodes = [...new Set(Object.values(NODE_ALIASES))];
|
|
672
|
+
const checkNodes = targetNodes.length > 0 ? targetNodes : allAliasNodes;
|
|
673
|
+
|
|
674
|
+
for (const nodeId of checkNodes) {
|
|
675
|
+
if (seen.has(nodeId)) continue;
|
|
676
|
+
const key = `${sha}-${nodeId}`;
|
|
677
|
+
try {
|
|
678
|
+
const entry = await resultsKv.get(key);
|
|
679
|
+
if (entry && entry.value) {
|
|
680
|
+
const result = JSON.parse(sc.decode(entry.value));
|
|
681
|
+
if (result.status === 'success' || result.status === 'failed' || result.status === 'skipped') {
|
|
682
|
+
const icon = result.status === 'success' ? '\x1b[32m✓\x1b[0m' : result.status === 'skipped' ? '\x1b[33m-\x1b[0m' : '\x1b[31m✗\x1b[0m';
|
|
683
|
+
console.log(` ${icon} ${nodeId}: ${result.status} (${result.durationSeconds || 0}s)`);
|
|
684
|
+
if (result.errors && result.errors.length > 0) {
|
|
685
|
+
for (const e of result.errors) console.log(` Error: ${e}`);
|
|
686
|
+
}
|
|
687
|
+
seen.add(nodeId);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
} catch {}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (seen.size >= checkNodes.length) break;
|
|
694
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
695
|
+
}
|
|
696
|
+
} catch {}
|
|
697
|
+
|
|
698
|
+
if (seen.size === 0) {
|
|
699
|
+
console.log(' (no responses yet — nodes may still be deploying)');
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
console.log('');
|
|
703
|
+
await nc.close();
|
|
704
|
+
}
|
|
705
|
+
|
|
578
706
|
/**
|
|
579
707
|
* mesh help — show usage.
|
|
580
708
|
*/
|
|
@@ -602,6 +730,10 @@ function cmdHelp() {
|
|
|
602
730
|
' mesh health --json Health check (JSON output)',
|
|
603
731
|
' mesh repair Self-repair this node',
|
|
604
732
|
' mesh repair --all Self-repair ALL nodes',
|
|
733
|
+
' mesh deploy Deploy to all nodes',
|
|
734
|
+
' mesh deploy --force Force deploy (even if up to date)',
|
|
735
|
+
' mesh deploy --component <name> Deploy specific component',
|
|
736
|
+
' mesh deploy --node <name> Deploy to specific node',
|
|
605
737
|
'',
|
|
606
738
|
'NODE ALIASES:',
|
|
607
739
|
' ubuntu, linux = Ubuntu VM (calos-vmware-virtual-platform)',
|
|
@@ -632,6 +764,7 @@ async function main() {
|
|
|
632
764
|
case 'tasks': return cmdTasks(args);
|
|
633
765
|
case 'health': return cmdHealth(args);
|
|
634
766
|
case 'repair': return cmdRepair(args);
|
|
767
|
+
case 'deploy': return cmdDeploy(args);
|
|
635
768
|
case 'help':
|
|
636
769
|
case '--help':
|
|
637
770
|
case '-h': return cmdHelp();
|
|
@@ -460,14 +460,60 @@ function installLaunchdService(meshDir, nodeBin, nodeId, provider, natsUrl) {
|
|
|
460
460
|
return;
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
+
// Deploy listener plist
|
|
464
|
+
const deployPlistPath = path.join(plistDir, 'ai.openclaw.deploy-listener.plist');
|
|
465
|
+
const deployPlist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
466
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
467
|
+
<plist version="1.0">
|
|
468
|
+
<dict>
|
|
469
|
+
<key>Label</key>
|
|
470
|
+
<string>ai.openclaw.deploy-listener</string>
|
|
471
|
+
<key>ProgramArguments</key>
|
|
472
|
+
<array>
|
|
473
|
+
<string>${nodeBin}</string>
|
|
474
|
+
<string>${meshDir}/bin/mesh-deploy-listener.js</string>
|
|
475
|
+
</array>
|
|
476
|
+
<key>KeepAlive</key>
|
|
477
|
+
<true/>
|
|
478
|
+
<key>RunAtLoad</key>
|
|
479
|
+
<true/>
|
|
480
|
+
<key>StandardOutPath</key>
|
|
481
|
+
<string>${os.homedir()}/.openclaw/workspace/.tmp/mesh-deploy-listener.log</string>
|
|
482
|
+
<key>StandardErrorPath</key>
|
|
483
|
+
<string>${os.homedir()}/.openclaw/workspace/.tmp/mesh-deploy-listener.err</string>
|
|
484
|
+
<key>EnvironmentVariables</key>
|
|
485
|
+
<dict>
|
|
486
|
+
<key>OPENCLAW_NATS</key>
|
|
487
|
+
<string>${natsUrl}</string>
|
|
488
|
+
<key>OPENCLAW_NODE_ID</key>
|
|
489
|
+
<string>${nodeId}</string>
|
|
490
|
+
<key>OPENCLAW_NODE_ROLE</key>
|
|
491
|
+
<string>worker</string>
|
|
492
|
+
<key>OPENCLAW_REPO_DIR</key>
|
|
493
|
+
<string>${meshDir}</string>
|
|
494
|
+
<key>PATH</key>
|
|
495
|
+
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:${os.homedir()}/.npm-global/bin</string>
|
|
496
|
+
<key>NODE_PATH</key>
|
|
497
|
+
<string>${meshDir}/node_modules:${meshDir}/lib</string>
|
|
498
|
+
</dict>
|
|
499
|
+
<key>ThrottleInterval</key>
|
|
500
|
+
<integer>30</integer>
|
|
501
|
+
</dict>
|
|
502
|
+
</plist>`;
|
|
503
|
+
|
|
463
504
|
fs.mkdirSync(plistDir, { recursive: true });
|
|
464
505
|
fs.writeFileSync(plistPath, plist);
|
|
465
|
-
ok(`
|
|
506
|
+
ok(`Mesh agent service written: ${plistPath}`);
|
|
507
|
+
fs.writeFileSync(deployPlistPath, deployPlist);
|
|
508
|
+
ok(`Deploy listener service written: ${deployPlistPath}`);
|
|
466
509
|
|
|
467
510
|
try {
|
|
468
511
|
execSync(`launchctl unload "${plistPath}" 2>/dev/null || true`, { stdio: 'pipe' });
|
|
469
512
|
execSync(`launchctl load "${plistPath}"`, { stdio: 'pipe' });
|
|
470
|
-
ok('
|
|
513
|
+
ok('Mesh agent loaded and started');
|
|
514
|
+
execSync(`launchctl unload "${deployPlistPath}" 2>/dev/null || true`, { stdio: 'pipe' });
|
|
515
|
+
execSync(`launchctl load "${deployPlistPath}"`, { stdio: 'pipe' });
|
|
516
|
+
ok('Deploy listener loaded and started');
|
|
471
517
|
} catch (e) {
|
|
472
518
|
warn(`Service load warning: ${e.message}`);
|
|
473
519
|
}
|
|
@@ -508,11 +554,40 @@ WantedBy=default.target
|
|
|
508
554
|
fs.writeFileSync(servicePath, service);
|
|
509
555
|
ok(`Systemd service written: ${servicePath}`);
|
|
510
556
|
|
|
557
|
+
// Deploy listener service
|
|
558
|
+
const deployServicePath = path.join(serviceDir, 'openclaw-deploy-listener.service');
|
|
559
|
+
const deployService = `[Unit]
|
|
560
|
+
Description=OpenClaw Deploy Listener
|
|
561
|
+
After=network-online.target
|
|
562
|
+
Wants=network-online.target
|
|
563
|
+
|
|
564
|
+
[Service]
|
|
565
|
+
Type=simple
|
|
566
|
+
ExecStart=${nodeBin} ${meshDir}/bin/mesh-deploy-listener.js
|
|
567
|
+
Restart=always
|
|
568
|
+
RestartSec=30
|
|
569
|
+
Environment=OPENCLAW_NATS=${natsUrl}
|
|
570
|
+
Environment=OPENCLAW_NODE_ID=${nodeId}
|
|
571
|
+
Environment=OPENCLAW_NODE_ROLE=worker
|
|
572
|
+
Environment=OPENCLAW_REPO_DIR=${meshDir}
|
|
573
|
+
Environment=NODE_PATH=${meshDir}/node_modules:${meshDir}/lib
|
|
574
|
+
Environment=PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin:${os.homedir()}/.local/bin:${os.homedir()}/.npm-global/bin
|
|
575
|
+
WorkingDirectory=${meshDir}
|
|
576
|
+
|
|
577
|
+
[Install]
|
|
578
|
+
WantedBy=default.target
|
|
579
|
+
`;
|
|
580
|
+
fs.writeFileSync(deployServicePath, deployService);
|
|
581
|
+
ok(`Deploy listener service written: ${deployServicePath}`);
|
|
582
|
+
|
|
511
583
|
try {
|
|
512
584
|
execSync('systemctl --user daemon-reload', { stdio: 'pipe' });
|
|
513
585
|
execSync('systemctl --user enable openclaw-mesh-agent', { stdio: 'pipe' });
|
|
514
586
|
execSync('systemctl --user start openclaw-mesh-agent', { stdio: 'pipe' });
|
|
515
|
-
ok('
|
|
587
|
+
ok('Mesh agent enabled and started');
|
|
588
|
+
execSync('systemctl --user enable openclaw-deploy-listener', { stdio: 'pipe' });
|
|
589
|
+
execSync('systemctl --user start openclaw-deploy-listener', { stdio: 'pipe' });
|
|
590
|
+
ok('Deploy listener enabled and started');
|
|
516
591
|
} catch (e) {
|
|
517
592
|
warn(`Service start warning: ${e.message}`);
|
|
518
593
|
warn('Try manually: systemctl --user start openclaw-mesh-agent');
|
|
@@ -630,6 +705,71 @@ async function verifyNatsHealth(natsUrl, nodeId) {
|
|
|
630
705
|
}
|
|
631
706
|
}
|
|
632
707
|
|
|
708
|
+
// ── Mesh Topology Discovery ──────────────────────────
|
|
709
|
+
|
|
710
|
+
async function discoverTopology(natsUrl, localNodeId) {
|
|
711
|
+
log('Discovering mesh topology...');
|
|
712
|
+
|
|
713
|
+
if (DRY_RUN) {
|
|
714
|
+
warn('[DRY RUN] Would query MESH_NODE_HEALTH and write mesh-aliases.json');
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
try {
|
|
719
|
+
const nats = require('nats');
|
|
720
|
+
const nc = await nats.connect({ servers: natsUrl, timeout: 10000 });
|
|
721
|
+
const sc = nats.StringCodec();
|
|
722
|
+
const js = nc.jetstream();
|
|
723
|
+
|
|
724
|
+
const aliases = {};
|
|
725
|
+
|
|
726
|
+
// Query MESH_NODE_HEALTH for all known nodes
|
|
727
|
+
try {
|
|
728
|
+
const kv = await js.views.kv('MESH_NODE_HEALTH');
|
|
729
|
+
const keys = await kv.keys();
|
|
730
|
+
for await (const key of keys) {
|
|
731
|
+
const entry = await kv.get(key);
|
|
732
|
+
if (entry && entry.value) {
|
|
733
|
+
const health = JSON.parse(sc.decode(entry.value));
|
|
734
|
+
const nodeId = health.nodeId || key;
|
|
735
|
+
// Create short alias from node ID (strip common suffixes)
|
|
736
|
+
const short = nodeId
|
|
737
|
+
.replace(/-virtual-machine.*$/i, '')
|
|
738
|
+
.replace(/-vmware.*$/i, '')
|
|
739
|
+
.replace(/-local$/, '');
|
|
740
|
+
aliases[short] = nodeId;
|
|
741
|
+
if (health.role === 'lead') aliases['lead'] = nodeId;
|
|
742
|
+
ok(`Peer: ${nodeId} (${health.role || 'worker'}, ${health.tailscaleIp || 'unknown'})`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
} catch {
|
|
746
|
+
warn('MESH_NODE_HEALTH bucket not available — skipping topology');
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Also add self
|
|
750
|
+
const selfShort = localNodeId
|
|
751
|
+
.replace(/-virtual-machine.*$/i, '')
|
|
752
|
+
.replace(/-vmware.*$/i, '')
|
|
753
|
+
.replace(/-local$/, '');
|
|
754
|
+
aliases[selfShort] = localNodeId;
|
|
755
|
+
aliases['self'] = localNodeId;
|
|
756
|
+
|
|
757
|
+
await nc.drain();
|
|
758
|
+
|
|
759
|
+
if (Object.keys(aliases).length > 1) {
|
|
760
|
+
const aliasPath = path.join(os.homedir(), '.openclaw', 'mesh-aliases.json');
|
|
761
|
+
fs.writeFileSync(aliasPath, JSON.stringify(aliases, null, 2) + '\n', { mode: 0o644 });
|
|
762
|
+
ok(`Mesh aliases written: ${aliasPath} (${Object.keys(aliases).length} entries)`);
|
|
763
|
+
} else {
|
|
764
|
+
warn('No peers found in MESH_NODE_HEALTH — mesh-aliases.json will only have self');
|
|
765
|
+
const aliasPath = path.join(os.homedir(), '.openclaw', 'mesh-aliases.json');
|
|
766
|
+
fs.writeFileSync(aliasPath, JSON.stringify(aliases, null, 2) + '\n', { mode: 0o644 });
|
|
767
|
+
}
|
|
768
|
+
} catch (e) {
|
|
769
|
+
warn(`Topology discovery failed: ${e.message} (non-fatal)`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
633
773
|
// ── Main ──────────────────────────────────────────────
|
|
634
774
|
|
|
635
775
|
async function main() {
|
|
@@ -699,6 +839,10 @@ async function main() {
|
|
|
699
839
|
const natsHealthy = await verifyNatsHealth(config.nats, nodeId);
|
|
700
840
|
const healthy = serviceAlive && natsHealthy;
|
|
701
841
|
|
|
842
|
+
// ── Step 9: Discover mesh topology ──
|
|
843
|
+
step(9, 'Discovering mesh topology...');
|
|
844
|
+
await discoverTopology(config.nats, nodeId);
|
|
845
|
+
|
|
702
846
|
// ── Done ──
|
|
703
847
|
console.log(`\n${BOLD}${GREEN}═══════════════════════════════════════${RESET}`);
|
|
704
848
|
if (healthy) {
|
package/lib/mesh-tasks.js
CHANGED
|
@@ -140,15 +140,15 @@ class TaskStore {
|
|
|
140
140
|
// Apply filters
|
|
141
141
|
if (filter.status && task.status !== filter.status) continue;
|
|
142
142
|
if (filter.owner && task.owner !== filter.owner) continue;
|
|
143
|
-
if (filter.tag && !task.tags.includes(filter.tag)) continue;
|
|
143
|
+
if (filter.tag && (!task.tags || !task.tags.includes(filter.tag))) continue;
|
|
144
144
|
|
|
145
145
|
tasks.push(task);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
// Sort by priority (higher first), then created_at (older first)
|
|
149
149
|
tasks.sort((a, b) => {
|
|
150
|
-
if (b.priority !== a.priority) return b.priority - a.priority;
|
|
151
|
-
return new Date(a.created_at) - new Date(b.created_at);
|
|
150
|
+
if ((b.priority || 0) !== (a.priority || 0)) return (b.priority || 0) - (a.priority || 0);
|
|
151
|
+
return (new Date(a.created_at || 0)) - (new Date(b.created_at || 0));
|
|
152
152
|
});
|
|
153
153
|
|
|
154
154
|
return tasks;
|
|
@@ -169,7 +169,7 @@ class TaskStore {
|
|
|
169
169
|
if (task.exclude_nodes && task.exclude_nodes.includes(nodeId)) continue;
|
|
170
170
|
|
|
171
171
|
// Respect dependencies
|
|
172
|
-
if (task.depends_on.length > 0) {
|
|
172
|
+
if (task.depends_on && task.depends_on.length > 0) {
|
|
173
173
|
const depsReady = await this._checkDeps(task.depends_on);
|
|
174
174
|
if (!depsReady) continue;
|
|
175
175
|
}
|
|
@@ -192,9 +192,8 @@ class TaskStore {
|
|
|
192
192
|
task.status = TASK_STATUS.CLAIMED;
|
|
193
193
|
task.owner = nodeId;
|
|
194
194
|
task.claimed_at = new Date().toISOString();
|
|
195
|
-
task.
|
|
196
|
-
|
|
197
|
-
).toISOString();
|
|
195
|
+
const budgetMs = (task.budget_minutes || 30) * 60 * 1000;
|
|
196
|
+
task.budget_deadline = new Date(Date.now() + budgetMs).toISOString();
|
|
198
197
|
|
|
199
198
|
await this.put(task);
|
|
200
199
|
return task;
|
package/package.json
CHANGED