infinicode 2.8.56 → 2.8.58
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/cli.js +1 -0
- package/dist/commands/serve.d.ts +1 -0
- package/dist/commands/serve.js +1 -0
- package/dist/kernel/config-schema.d.ts +2 -0
- package/dist/kernel/federation/federation.d.ts +5 -0
- package/dist/kernel/federation/federation.js +41 -2
- package/dist/kernel/federation/peer-mesh.d.ts +3 -1
- package/dist/kernel/federation/peer-mesh.js +16 -0
- package/dist/kernel/federation/self-update.js +16 -1
- package/dist/robopark/robot-runtime.js +60 -4
- package/package.json +1 -2
package/dist/cli.js
CHANGED
|
@@ -305,6 +305,7 @@ program
|
|
|
305
305
|
.option('--token <token>', 'shared bearer token peers must present')
|
|
306
306
|
.option('--seed <url...>', 'peer base URL(s) to connect to on start')
|
|
307
307
|
.option('--auto-update', 'accept auto-updates announced by a hub/relay')
|
|
308
|
+
.option('--update-poll-ms <ms>', 'hub/relay npm-release polling interval (default 60000)')
|
|
308
309
|
.option('--supervised', 'this node runs under a supervisor (systemd/pm2/Task Scheduler) — let /update restart it to activate a new version')
|
|
309
310
|
.option('--tailscale', 'auto-discover + connect peers over Tailscale')
|
|
310
311
|
.option('--lan', 'auto-discover + connect peers on the same LAN via UDP broadcast (no seed/Tailscale needed)')
|
package/dist/commands/serve.d.ts
CHANGED
package/dist/commands/serve.js
CHANGED
|
@@ -156,6 +156,7 @@ export async function serve(config, opts) {
|
|
|
156
156
|
token,
|
|
157
157
|
heartbeatMs: (fed.heartbeatSec ?? 30) * 1000,
|
|
158
158
|
autoUpdate: opts.autoUpdate ?? fed.autoUpdate,
|
|
159
|
+
updatePollMs: opts.updatePollMs ? Number(opts.updatePollMs) : fed.updatePollMs,
|
|
159
160
|
applyConfig,
|
|
160
161
|
autoPullConfig: role === 'satellite',
|
|
161
162
|
dashboard: opts.dashboard,
|
|
@@ -43,6 +43,8 @@ export interface FederationConfig {
|
|
|
43
43
|
heartbeatSec?: number;
|
|
44
44
|
/** Accept + apply auto-updates announced by a hub/relay. */
|
|
45
45
|
autoUpdate?: boolean;
|
|
46
|
+
/** Hub/relay npm-release polling interval in milliseconds (default 60000). */
|
|
47
|
+
updatePollMs?: number;
|
|
46
48
|
/** Node runs under a process supervisor — allow `/update` to exit-for-restart. */
|
|
47
49
|
supervised?: boolean;
|
|
48
50
|
/** Zero-config LAN discovery: broadcast + auto-connect peers on this subnet. */
|
|
@@ -54,6 +54,8 @@ export interface FederationOptions {
|
|
|
54
54
|
/** Base URLs of peers to connect to on start (static seeds). */
|
|
55
55
|
seeds?: string[];
|
|
56
56
|
autoUpdate?: boolean;
|
|
57
|
+
/** Hub/relay npm-release polling interval. Defaults to one minute. */
|
|
58
|
+
updatePollMs?: number;
|
|
57
59
|
/** Satellite: how to apply an incoming shared config (register providers etc.). */
|
|
58
60
|
applyConfig?: (cfg: SharedConfig) => void | Promise<void>;
|
|
59
61
|
/** Satellite: auto-pull shared config from any hub/relay we connect to. */
|
|
@@ -133,6 +135,7 @@ export declare class Federation {
|
|
|
133
135
|
/** nodeIds we're currently dialing back after an inbound heartbeat/hello. */
|
|
134
136
|
private inboundConnecting;
|
|
135
137
|
private configPullTimer?;
|
|
138
|
+
private updatePollTimer?;
|
|
136
139
|
readonly configSync: ConfigSync;
|
|
137
140
|
readonly autoUpdate: AutoUpdate;
|
|
138
141
|
readonly compute: ComputeRouter;
|
|
@@ -259,6 +262,8 @@ export declare class Federation {
|
|
|
259
262
|
private maybeAutoPull;
|
|
260
263
|
/** Satellite: re-pull from the first connected hub/relay (periodic refresh). */
|
|
261
264
|
private autoPullFromHub;
|
|
265
|
+
/** Fetch npm's latest tag, announce it to satellites, then update this hub. */
|
|
266
|
+
private pollNpmRelease;
|
|
262
267
|
/** Auto-discover peers over Tailscale and connect to each (skips self/known). */
|
|
263
268
|
discoverAndConnect(tag?: string): Promise<number>;
|
|
264
269
|
/**
|
|
@@ -3,7 +3,7 @@ import { DASHBOARD_HTML } from './dashboard-html.js';
|
|
|
3
3
|
import { PeerMesh } from './peer-mesh.js';
|
|
4
4
|
import { EventBridge } from './event-bridge.js';
|
|
5
5
|
import { ConfigSync } from './config-sync.js';
|
|
6
|
-
import { AutoUpdate } from './auto-update.js';
|
|
6
|
+
import { AutoUpdate, compareVersions } from './auto-update.js';
|
|
7
7
|
import { performSelfUpdate } from './self-update.js';
|
|
8
8
|
import { ComputeRouter } from './compute-router.js';
|
|
9
9
|
import { TelemetryCollector, loadFromSnapshot, selfNodeStatus, nodeStatusFromPeer } from './telemetry.js';
|
|
@@ -32,6 +32,7 @@ export class Federation {
|
|
|
32
32
|
/** nodeIds we're currently dialing back after an inbound heartbeat/hello. */
|
|
33
33
|
inboundConnecting = new Set();
|
|
34
34
|
configPullTimer;
|
|
35
|
+
updatePollTimer;
|
|
35
36
|
configSync;
|
|
36
37
|
autoUpdate;
|
|
37
38
|
compute = new ComputeRouter();
|
|
@@ -134,6 +135,15 @@ export class Federation {
|
|
|
134
135
|
if (options.autoPullConfig && options.configPullMs !== 0) {
|
|
135
136
|
this.configPullTimer = setInterval(() => void this.autoPullFromHub(), options.configPullMs ?? 30_000);
|
|
136
137
|
}
|
|
138
|
+
// The hub owns release discovery. Satellites never poll npm: they only
|
|
139
|
+
// trust signed mesh announcements from a hub/relay. This makes npm publish
|
|
140
|
+
// -> fleet update a real loop, while retaining the --auto-update opt-in.
|
|
141
|
+
if (options.autoUpdate && (identity.role === 'hub' || identity.role === 'relay')) {
|
|
142
|
+
const pollMs = Math.max(30_000, options.updatePollMs ?? 60_000);
|
|
143
|
+
void this.pollNpmRelease();
|
|
144
|
+
this.updatePollTimer = setInterval(() => void this.pollNpmRelease(), pollMs);
|
|
145
|
+
logger.info(`[federation] npm release watcher enabled (every ${Math.round(pollMs / 1000)}s)`);
|
|
146
|
+
}
|
|
137
147
|
logger.info(`[federation] node ${identity.displayName} (${identity.role}) online on :${port}`);
|
|
138
148
|
}
|
|
139
149
|
// ── Inbound RPC ────────────────────────────────────────────────────────────
|
|
@@ -172,7 +182,10 @@ export class Federation {
|
|
|
172
182
|
return reply(env, 'ack', self);
|
|
173
183
|
case 'update.announce': {
|
|
174
184
|
const peer = this.mesh?.get(env.from);
|
|
175
|
-
|
|
185
|
+
// Acknowledge immediately. npm can take minutes on a Pi, while the
|
|
186
|
+
// sender's RPC timeout is intentionally short; the update continues
|
|
187
|
+
// in the background and exits the supervised mesh process on success.
|
|
188
|
+
void this.autoUpdate.onAnnounce(env.data, peer?.role ?? 'peer');
|
|
176
189
|
return reply(env, 'ack', self);
|
|
177
190
|
}
|
|
178
191
|
case 'role.get':
|
|
@@ -625,6 +638,30 @@ export class Federation {
|
|
|
625
638
|
if (hub)
|
|
626
639
|
await this.maybeAutoPull(hub);
|
|
627
640
|
}
|
|
641
|
+
/** Fetch npm's latest tag, announce it to satellites, then update this hub. */
|
|
642
|
+
async pollNpmRelease() {
|
|
643
|
+
try {
|
|
644
|
+
const response = await fetch('https://registry.npmjs.org/infinicode/latest', {
|
|
645
|
+
signal: AbortSignal.timeout(10_000),
|
|
646
|
+
});
|
|
647
|
+
if (!response.ok)
|
|
648
|
+
throw new Error(`npm registry ${response.status}`);
|
|
649
|
+
const payload = await response.json();
|
|
650
|
+
const version = typeof payload.version === 'string' ? payload.version.trim() : '';
|
|
651
|
+
if (!/^\d+(?:\.\d+){1,3}(?:[-+][A-Za-z0-9.-]+)?$/.test(version)) {
|
|
652
|
+
throw new Error('npm registry returned an invalid version');
|
|
653
|
+
}
|
|
654
|
+
await this.mesh?.announceUpdate({ version, channel: 'npm' });
|
|
655
|
+
const current = this.autoUpdate.currentVersion();
|
|
656
|
+
if (compareVersions(version, current) > 0) {
|
|
657
|
+
this.deps.logger.info(`[federation] npm release ${version} announced to fleet; updating ${current} -> ${version}`);
|
|
658
|
+
await this.autoUpdate.onAnnounce({ version, channel: 'npm' }, this.deps.identity.role);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
catch (err) {
|
|
662
|
+
this.deps.logger.warn(`[federation] npm release check failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
628
665
|
/** Auto-discover peers over Tailscale and connect to each (skips self/known). */
|
|
629
666
|
async discoverAndConnect(tag) {
|
|
630
667
|
const port = this.deps.options.port ?? DEFAULT_MESH_PORT;
|
|
@@ -752,6 +789,8 @@ export class Federation {
|
|
|
752
789
|
this.lan?.stop();
|
|
753
790
|
if (this.configPullTimer)
|
|
754
791
|
clearInterval(this.configPullTimer);
|
|
792
|
+
if (this.updatePollTimer)
|
|
793
|
+
clearInterval(this.updatePollTimer);
|
|
755
794
|
if (this.telemetryTimer)
|
|
756
795
|
clearInterval(this.telemetryTimer);
|
|
757
796
|
this.mesh?.stop();
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* manage (satellites are accept-only). A satellite never dials us.
|
|
11
11
|
*/
|
|
12
12
|
import type { Logger } from '../types.js';
|
|
13
|
-
import type { FederationEnvelope, NodeIdentity, PeerInfo, StreamFrame } from './types.js';
|
|
13
|
+
import type { FederationEnvelope, NodeIdentity, PeerInfo, StreamFrame, UpdateAnnounce } from './types.js';
|
|
14
14
|
import { MeshClient } from './transport-http.js';
|
|
15
15
|
export interface PeerMeshOptions {
|
|
16
16
|
self: NodeIdentity;
|
|
@@ -38,6 +38,8 @@ export declare class PeerMesh {
|
|
|
38
38
|
connected(): PeerInfo[];
|
|
39
39
|
/** Discover + attach a peer at a base URL (manifest probe + open its stream). */
|
|
40
40
|
connect(url: string): Promise<PeerInfo | null>;
|
|
41
|
+
/** Send a hub/relay version announcement to every currently connected peer. */
|
|
42
|
+
announceUpdate(announce: UpdateAnnounce): Promise<void>;
|
|
41
43
|
private openStream;
|
|
42
44
|
start(): void;
|
|
43
45
|
private tick;
|
|
@@ -22,6 +22,11 @@ export class PeerMesh {
|
|
|
22
22
|
async connect(url) {
|
|
23
23
|
try {
|
|
24
24
|
const manifest = await this.opts.client.fetchManifest(url);
|
|
25
|
+
// A hub may discover or seed its own advertised address. Never add self
|
|
26
|
+
// as a peer: it duplicates the hub in fleet status and can create a
|
|
27
|
+
// pointless self-stream/update loop.
|
|
28
|
+
if (manifest.nodeId === this.opts.self.nodeId)
|
|
29
|
+
return null;
|
|
25
30
|
const peer = {
|
|
26
31
|
nodeId: manifest.nodeId,
|
|
27
32
|
displayName: manifest.displayName,
|
|
@@ -48,6 +53,17 @@ export class PeerMesh {
|
|
|
48
53
|
return null;
|
|
49
54
|
}
|
|
50
55
|
}
|
|
56
|
+
/** Send a hub/relay version announcement to every currently connected peer. */
|
|
57
|
+
async announceUpdate(announce) {
|
|
58
|
+
await Promise.all(this.connected().map(async (peer) => {
|
|
59
|
+
try {
|
|
60
|
+
await this.opts.client.rpc(peer.url, envelope('update.announce', this.opts.self.nodeId, announce, { to: peer.nodeId }));
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
this.opts.logger.warn(`[federation] update announce to ${peer.displayName} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
64
|
+
}
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
51
67
|
openStream(peer) {
|
|
52
68
|
this.streams.get(peer.nodeId)?.(); // close any prior
|
|
53
69
|
const stop = this.opts.client.openStream(peer.url, f => {
|
|
@@ -35,7 +35,22 @@ export async function performSelfUpdate(opts = {}) {
|
|
|
35
35
|
try {
|
|
36
36
|
// Use npm.cmd on Windows so we never need a shell (no injection surface).
|
|
37
37
|
const bin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
38
|
-
const
|
|
38
|
+
const args = ['install', '-g', spec];
|
|
39
|
+
let res;
|
|
40
|
+
try {
|
|
41
|
+
res = await execa(bin, args, { all: true, timeout: 300_000 });
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
const message = String(err.all ?? err.message ?? err);
|
|
45
|
+
const unprivileged = process.platform !== 'win32' && process.getuid?.() !== 0;
|
|
46
|
+
if (!unprivileged || !/\bEACCES\b|permission denied/i.test(message))
|
|
47
|
+
throw err;
|
|
48
|
+
// RoboPark systemd services normally run as root. This fallback also
|
|
49
|
+
// supports an operator-launched satellite when sudo is intentionally
|
|
50
|
+
// configured non-interactively; it never prompts or shells out.
|
|
51
|
+
info('[update] global npm directory is root-owned; retrying with sudo -n…');
|
|
52
|
+
res = await execa('sudo', ['-n', bin, ...args], { all: true, timeout: 300_000 });
|
|
53
|
+
}
|
|
39
54
|
const tail = String(res.all ?? res.stdout ?? '').split('\n').filter(Boolean).slice(-2).join(' ').trim();
|
|
40
55
|
info(`[update] installed ${spec}${tail ? ' — ' + tail : ''}`);
|
|
41
56
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* it exits unexpectedly.
|
|
9
9
|
*/
|
|
10
10
|
import { spawn } from 'node:child_process';
|
|
11
|
-
import { existsSync,
|
|
11
|
+
import { existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import { homedir } from 'node:os';
|
|
13
13
|
import { dirname, join } from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
@@ -50,8 +50,10 @@ function runtimeLog(label, foreground) {
|
|
|
50
50
|
mkdirSync(logDir, { recursive: true });
|
|
51
51
|
return [
|
|
52
52
|
'ignore',
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
// spawn() accepts file descriptors, but not WriteStreams before their
|
|
54
|
+
// asynchronous open event. openSync makes detached startup reliable.
|
|
55
|
+
openSync(join(logDir, `${label}.log`), 'a'),
|
|
56
|
+
openSync(join(logDir, `${label}.err.log`), 'a'),
|
|
55
57
|
];
|
|
56
58
|
}
|
|
57
59
|
async function waitForVision() {
|
|
@@ -76,6 +78,7 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
76
78
|
const cli = roboparkCliPath();
|
|
77
79
|
const children = [];
|
|
78
80
|
let stopping = false;
|
|
81
|
+
let recycling = false;
|
|
79
82
|
const infinicode = await resolveInfinicodeBin();
|
|
80
83
|
if (!infinicode)
|
|
81
84
|
throw new Error('could not resolve infinicode CLI');
|
|
@@ -84,7 +87,7 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
84
87
|
command: infinicode.node,
|
|
85
88
|
args: [infinicode.script, 'serve', '--role', 'satellite', '--name', opts.name,
|
|
86
89
|
'--port', port, '--token', opts.token, '--seed', opts.hubUrl,
|
|
87
|
-
network === 'tailscale' ? '--tailscale' : '--lan', '--auto-update'],
|
|
90
|
+
network === 'tailscale' ? '--tailscale' : '--lan', '--auto-update', '--supervised'],
|
|
88
91
|
});
|
|
89
92
|
if (opts.vision !== false) {
|
|
90
93
|
children.push({
|
|
@@ -114,11 +117,64 @@ export async function roboparkRobotRuntime(opts) {
|
|
|
114
117
|
entry.child = undefined;
|
|
115
118
|
if (stopping)
|
|
116
119
|
return;
|
|
120
|
+
// A clean mesh exit is how /update signals that npm replaced this
|
|
121
|
+
// package. Reload every child so the robot runs one coherent release.
|
|
122
|
+
if (entry.label === 'mesh' && code === 0) {
|
|
123
|
+
void recycleAfterMeshUpdate();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (recycling)
|
|
127
|
+
return;
|
|
117
128
|
console.error(chalk.yellow(` ${entry.label} exited (${signal ?? code ?? 'unknown'}); restarting in 5s`));
|
|
118
129
|
setTimeout(() => { if (!stopping)
|
|
119
130
|
start(entry); }, RESTART_DELAY_MS);
|
|
120
131
|
});
|
|
121
132
|
};
|
|
133
|
+
const stopChild = async (entry) => {
|
|
134
|
+
const child = entry.child;
|
|
135
|
+
if (!child)
|
|
136
|
+
return;
|
|
137
|
+
await new Promise(resolve => {
|
|
138
|
+
const timer = setTimeout(resolve, 5_000);
|
|
139
|
+
child.once('exit', () => {
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
resolve();
|
|
142
|
+
});
|
|
143
|
+
try {
|
|
144
|
+
child.kill('SIGTERM');
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
resolve();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
const recycleAfterMeshUpdate = async () => {
|
|
153
|
+
if (stopping || recycling)
|
|
154
|
+
return;
|
|
155
|
+
recycling = true;
|
|
156
|
+
console.log(chalk.dim(' mesh updated; restarting RoboVision and preview on the new package...'));
|
|
157
|
+
await Promise.all([...children.filter(entry => entry.label !== 'mesh'), preview].map(stopChild));
|
|
158
|
+
if (stopping)
|
|
159
|
+
return;
|
|
160
|
+
start(children[0]);
|
|
161
|
+
if (opts.vision !== false) {
|
|
162
|
+
start(children[1]);
|
|
163
|
+
if (!await waitForVision()) {
|
|
164
|
+
console.error(chalk.red(' RoboVision did not recover after the mesh update; retrying in 5s'));
|
|
165
|
+
// Stop the partially restarted stack before retrying. Otherwise a
|
|
166
|
+
// second mesh child would collide with the existing listener.
|
|
167
|
+
await Promise.all([...children, preview].map(stopChild));
|
|
168
|
+
recycling = false;
|
|
169
|
+
setTimeout(() => { if (!stopping)
|
|
170
|
+
void recycleAfterMeshUpdate(); }, RESTART_DELAY_MS);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
start(preview);
|
|
175
|
+
recycling = false;
|
|
176
|
+
console.log(chalk.green(' robot runtime updated and healthy'));
|
|
177
|
+
};
|
|
122
178
|
console.log(chalk.bold('\n robopark robot up'));
|
|
123
179
|
console.log(chalk.dim(' ' + '─'.repeat(52)));
|
|
124
180
|
console.log(` robot: ${chalk.cyan(opts.name)}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infinicode",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.58",
|
|
4
4
|
"description": "OpenKernel — provider-agnostic AI execution kernel. Native coding agent + mission-driven execution runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/kernel/index.js",
|
|
@@ -74,7 +74,6 @@
|
|
|
74
74
|
"node": ">=20"
|
|
75
75
|
},
|
|
76
76
|
"dependencies": {
|
|
77
|
-
"@anthropic-ai/claude-code": "^2.1.207",
|
|
78
77
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
79
78
|
"chalk": "^5.3.0",
|
|
80
79
|
"commander": "^12.1.0",
|