infinicode 2.8.25 → 2.8.27
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/kernel/federation/federation.d.ts +3 -0
- package/dist/kernel/federation/federation.js +25 -4
- package/dist/kernel/federation/transport-http.d.ts +1 -0
- package/dist/kernel/federation/transport-http.js +3 -0
- package/dist/kernel/providers/ollama-provider.d.ts +5 -0
- package/dist/kernel/providers/ollama-provider.js +131 -0
- package/dist/robopark-cli.js +3 -4
- package/package.json +2 -1
- package/packages/robopark/scheduler/main.py +9 -1
- package/packages/robopark/scheduler/preview_agent.py +17 -0
|
@@ -251,6 +251,9 @@ export declare class Federation {
|
|
|
251
251
|
* back (accept-only, never dials home).
|
|
252
252
|
*/
|
|
253
253
|
private learnInboundPeer;
|
|
254
|
+
/** Connect with a longer manifest fetch timeout. Inbound dial-backs often
|
|
255
|
+
* cross Tailscale or Windows firewall, so the default 5s is too aggressive. */
|
|
256
|
+
private connectWithTimeout;
|
|
254
257
|
/** Satellite: pull shared config from a freshly-connected hub/relay. */
|
|
255
258
|
private maybeAutoPull;
|
|
256
259
|
/** Satellite: re-pull from the first connected hub/relay (periodic refresh). */
|
|
@@ -577,17 +577,38 @@ export class Federation {
|
|
|
577
577
|
return; // already tracked
|
|
578
578
|
if (this.inboundConnecting.has(from))
|
|
579
579
|
return; // dial in flight
|
|
580
|
-
const
|
|
581
|
-
const
|
|
580
|
+
const data = env.data ?? {};
|
|
581
|
+
const port = data.port;
|
|
582
|
+
const ip = normalizeIp(data.ip ?? remote);
|
|
582
583
|
if (!port || !ip)
|
|
583
584
|
return;
|
|
584
585
|
const url = `http://${ip}:${port}`;
|
|
585
586
|
this.inboundConnecting.add(from);
|
|
586
587
|
this.deps.logger.info(`[federation] learned inbound peer ${from} → dialing back ${url}`);
|
|
587
|
-
void this.
|
|
588
|
+
void this.connectWithTimeout(url, 30_000)
|
|
588
589
|
.catch(() => { })
|
|
589
590
|
.finally(() => this.inboundConnecting.delete(from));
|
|
590
591
|
}
|
|
592
|
+
/** Connect with a longer manifest fetch timeout. Inbound dial-backs often
|
|
593
|
+
* cross Tailscale or Windows firewall, so the default 5s is too aggressive. */
|
|
594
|
+
async connectWithTimeout(url, timeoutMs) {
|
|
595
|
+
const peer = await this.mesh?.connect(url) ?? null;
|
|
596
|
+
if (peer) {
|
|
597
|
+
await this.maybeAutoPull(peer);
|
|
598
|
+
return peer;
|
|
599
|
+
}
|
|
600
|
+
// If the first attempt failed with the default timeout, retry once with the
|
|
601
|
+
// requested longer timeout before giving up. MeshClient currently uses
|
|
602
|
+
// opts.timeoutMs for manifest fetches, so we temporarily swap the client.
|
|
603
|
+
const client = new MeshClient({ token: this.client.token, timeoutMs });
|
|
604
|
+
const manifest = await client.fetchManifest(url);
|
|
605
|
+
const viaMesh = await this.mesh?.connect(url) ?? null;
|
|
606
|
+
if (viaMesh) {
|
|
607
|
+
await this.maybeAutoPull(viaMesh);
|
|
608
|
+
return viaMesh;
|
|
609
|
+
}
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
591
612
|
/** Satellite: pull shared config from a freshly-connected hub/relay. */
|
|
592
613
|
async maybeAutoPull(peer) {
|
|
593
614
|
if (!this.deps.options.autoPullConfig)
|
|
@@ -642,7 +663,7 @@ export class Federation {
|
|
|
642
663
|
if (this.lanConnecting.has(nodeId))
|
|
643
664
|
return; // dial in flight
|
|
644
665
|
this.lanConnecting.add(nodeId);
|
|
645
|
-
void this.
|
|
666
|
+
void this.connectWithTimeout(url, 30_000)
|
|
646
667
|
.then(p => {
|
|
647
668
|
if (p)
|
|
648
669
|
logger.info(`[federation] lan discovered ${p.displayName ?? name} @ ${url}`);
|
|
@@ -71,6 +71,7 @@ export interface MeshClientOptions {
|
|
|
71
71
|
export declare class MeshClient {
|
|
72
72
|
private opts;
|
|
73
73
|
constructor(opts?: MeshClientOptions);
|
|
74
|
+
get token(): string | undefined;
|
|
74
75
|
private headers;
|
|
75
76
|
/** Normalize a base URL so a trailing slash doesn't produce `//fed/...` (→ 404). */
|
|
76
77
|
private base;
|
|
@@ -30,7 +30,11 @@ export declare class OllamaProvider implements ProviderInterface {
|
|
|
30
30
|
constructor(options: OllamaProviderOptions);
|
|
31
31
|
capabilities(): Promise<ProviderCapabilities>;
|
|
32
32
|
complete(request: CompletionRequest): Promise<CompletionResult>;
|
|
33
|
+
private completeNative;
|
|
34
|
+
private completeOpenAI;
|
|
33
35
|
completeStream(request: CompletionRequest): AsyncIterable<CompletionChunk>;
|
|
36
|
+
private completeStreamNative;
|
|
37
|
+
private completeStreamOpenAI;
|
|
34
38
|
refresh(): Promise<void>;
|
|
35
39
|
verify(): Promise<{
|
|
36
40
|
ok: boolean;
|
|
@@ -42,6 +46,7 @@ export declare class OllamaProvider implements ProviderInterface {
|
|
|
42
46
|
private toProviderModel;
|
|
43
47
|
private guessContextLength;
|
|
44
48
|
private buildMessages;
|
|
49
|
+
private buildPrompt;
|
|
45
50
|
private resolveURL;
|
|
46
51
|
getDefaultModel(): string | undefined;
|
|
47
52
|
getCachedModels(): ProviderModel[];
|
|
@@ -35,6 +35,57 @@ export class OllamaProvider {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
async complete(request) {
|
|
38
|
+
// Local Ollama uses the native /api/generate endpoint. Some users run an
|
|
39
|
+
// OpenAI-compatible shim at /v1/chat/completions, but stock Ollama (the
|
|
40
|
+
// common case in this project) does not, so default to the native protocol
|
|
41
|
+
// and fall back to the shim only when /api/generate is unavailable.
|
|
42
|
+
try {
|
|
43
|
+
return await this.completeNative(request);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
47
|
+
if (!msg.includes('404'))
|
|
48
|
+
throw err;
|
|
49
|
+
return await this.completeOpenAI(request);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async completeNative(request) {
|
|
53
|
+
const start = Date.now();
|
|
54
|
+
const url = await this.resolveURL();
|
|
55
|
+
const endpoint = `${url}/api/generate`;
|
|
56
|
+
const body = {
|
|
57
|
+
model: request.model,
|
|
58
|
+
prompt: this.buildPrompt(request),
|
|
59
|
+
system: request.systemPrompt,
|
|
60
|
+
stream: false,
|
|
61
|
+
options: {
|
|
62
|
+
temperature: request.temperature ?? 0.3,
|
|
63
|
+
num_predict: request.maxTokens ?? 4096,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
const response = await fetch(endpoint, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: this.headers(),
|
|
69
|
+
body: JSON.stringify(body),
|
|
70
|
+
signal: AbortSignal.timeout(this.timeoutMs * 3),
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new Error(`Ollama completion failed: HTTP ${response.status}`);
|
|
74
|
+
}
|
|
75
|
+
const data = (await response.json());
|
|
76
|
+
const content = data.response ?? '';
|
|
77
|
+
const tokensIn = data.prompt_eval_count ?? 0;
|
|
78
|
+
const tokensOut = data.eval_count ?? 0;
|
|
79
|
+
return {
|
|
80
|
+
content,
|
|
81
|
+
providerId: this.info.id,
|
|
82
|
+
modelId: request.model,
|
|
83
|
+
tokensIn,
|
|
84
|
+
tokensOut,
|
|
85
|
+
durationMs: Date.now() - start,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
async completeOpenAI(request) {
|
|
38
89
|
const start = Date.now();
|
|
39
90
|
const url = await this.resolveURL();
|
|
40
91
|
const endpoint = `${url}/v1/chat/completions`;
|
|
@@ -68,6 +119,72 @@ export class OllamaProvider {
|
|
|
68
119
|
};
|
|
69
120
|
}
|
|
70
121
|
async *completeStream(request) {
|
|
122
|
+
// Prefer native /api/generate streaming; fall back to OpenAI-compatible shim
|
|
123
|
+
// only when the native endpoint returns 404.
|
|
124
|
+
try {
|
|
125
|
+
yield* this.completeStreamNative(request);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
129
|
+
if (!msg.includes('404'))
|
|
130
|
+
throw err;
|
|
131
|
+
yield* this.completeStreamOpenAI(request);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async *completeStreamNative(request) {
|
|
135
|
+
const url = await this.resolveURL();
|
|
136
|
+
const endpoint = `${url}/api/generate`;
|
|
137
|
+
const body = {
|
|
138
|
+
model: request.model,
|
|
139
|
+
prompt: this.buildPrompt(request),
|
|
140
|
+
system: request.systemPrompt,
|
|
141
|
+
stream: true,
|
|
142
|
+
options: {
|
|
143
|
+
temperature: request.temperature ?? 0.3,
|
|
144
|
+
num_predict: request.maxTokens ?? 4096,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
const response = await fetch(endpoint, {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
headers: this.headers(),
|
|
150
|
+
body: JSON.stringify(body),
|
|
151
|
+
signal: AbortSignal.timeout(this.timeoutMs * 6),
|
|
152
|
+
});
|
|
153
|
+
if (!response.ok || !response.body) {
|
|
154
|
+
throw new Error(`Ollama stream failed: HTTP ${response.status}`);
|
|
155
|
+
}
|
|
156
|
+
const reader = response.body.getReader();
|
|
157
|
+
const decoder = new TextDecoder();
|
|
158
|
+
let buffer = '';
|
|
159
|
+
while (true) {
|
|
160
|
+
const { done, value } = await reader.read();
|
|
161
|
+
if (done)
|
|
162
|
+
break;
|
|
163
|
+
buffer += decoder.decode(value, { stream: true });
|
|
164
|
+
const lines = buffer.split('\n');
|
|
165
|
+
buffer = lines.pop() ?? '';
|
|
166
|
+
for (const line of lines) {
|
|
167
|
+
const trimmed = line.trim();
|
|
168
|
+
if (!trimmed)
|
|
169
|
+
continue;
|
|
170
|
+
try {
|
|
171
|
+
const chunk = JSON.parse(trimmed);
|
|
172
|
+
const delta = chunk.response ?? '';
|
|
173
|
+
if (delta) {
|
|
174
|
+
yield { delta, done: false, providerId: this.info.id, modelId: request.model };
|
|
175
|
+
}
|
|
176
|
+
if (chunk.done) {
|
|
177
|
+
yield { delta: '', done: true, providerId: this.info.id, modelId: request.model };
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// skip malformed
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async *completeStreamOpenAI(request) {
|
|
71
188
|
const url = await this.resolveURL();
|
|
72
189
|
const endpoint = `${url}/v1/chat/completions`;
|
|
73
190
|
const body = {
|
|
@@ -227,6 +344,20 @@ export class OllamaProvider {
|
|
|
227
344
|
messages.push({ role: 'user', content: request.prompt });
|
|
228
345
|
return messages;
|
|
229
346
|
}
|
|
347
|
+
buildPrompt(request) {
|
|
348
|
+
const parts = [];
|
|
349
|
+
if (request.systemPrompt) {
|
|
350
|
+
parts.push(request.systemPrompt);
|
|
351
|
+
}
|
|
352
|
+
if (request.context) {
|
|
353
|
+
const ctxStr = Object.entries(request.context)
|
|
354
|
+
.map(([k, v]) => `${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`)
|
|
355
|
+
.join('\n');
|
|
356
|
+
parts.push(ctxStr);
|
|
357
|
+
}
|
|
358
|
+
parts.push(request.prompt);
|
|
359
|
+
return parts.join('\n\n');
|
|
360
|
+
}
|
|
230
361
|
async resolveURL() {
|
|
231
362
|
const candidates = [this.activeURL, this.fallbackURL, this.baseURL].filter((u, i, arr) => !!u && arr.indexOf(u) === i);
|
|
232
363
|
for (const candidate of candidates) {
|
package/dist/robopark-cli.js
CHANGED
|
@@ -220,16 +220,15 @@ program
|
|
|
220
220
|
console.log(chalk.green(' ✓ saved hub profile to ~/.robopark/hub-profile.json'));
|
|
221
221
|
});
|
|
222
222
|
program
|
|
223
|
-
.command('add-robot')
|
|
223
|
+
.command('add-robot <name>')
|
|
224
224
|
.description('Mint a scheduler enrollment token and print (or run) the robot setup one-liner')
|
|
225
|
-
.requiredOption('--name <name>', 'robot name, e.g. robobmw')
|
|
226
225
|
.option('--site <site>', 'physical location, e.g. "Tel Aviv"')
|
|
227
226
|
.option('--hub-url <url>', 'hub federation URL (defaults to saved hub profile / auto-discovery)')
|
|
228
227
|
.option('--token <token>', 'shared mesh token (defaults to saved hub profile / auto-discovery)')
|
|
229
228
|
.option('--scheduler-url <url>', 'scheduler base URL (defaults to saved hub profile / auto-discovery)')
|
|
230
229
|
.option('--start', 'start the robot node on this machine now, instead of printing the one-liner')
|
|
231
|
-
.action(async (opts) => {
|
|
232
|
-
await roboparkAddRobot(config, opts);
|
|
230
|
+
.action(async (name, opts) => {
|
|
231
|
+
await roboparkAddRobot(config, { name, ...opts });
|
|
233
232
|
});
|
|
234
233
|
program
|
|
235
234
|
.command('agent-up')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infinicode",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.27",
|
|
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",
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"node": ">=20"
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
|
+
"@anthropic-ai/claude-code": "^2.1.207",
|
|
76
77
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
77
78
|
"chalk": "^5.3.0",
|
|
78
79
|
"commander": "^12.1.0",
|
|
@@ -1159,7 +1159,15 @@ async def health_checker():
|
|
|
1159
1159
|
if resp.status_code == 200:
|
|
1160
1160
|
new_status = "online"
|
|
1161
1161
|
else:
|
|
1162
|
-
|
|
1162
|
+
# A bare LiveKit server (no sidecar webhook
|
|
1163
|
+
# service in front of it, e.g. `--dev` mode
|
|
1164
|
+
# with no separate health endpoint) has no
|
|
1165
|
+
# /health route and returns 404 here even
|
|
1166
|
+
# though it's genuinely up — LiveKit's own
|
|
1167
|
+
# root path returns "OK" instead. Fall back
|
|
1168
|
+
# to that before declaring it offline.
|
|
1169
|
+
resp2 = await client.get(server["webhook_url"])
|
|
1170
|
+
new_status = "online" if resp2.status_code == 200 else "offline"
|
|
1163
1171
|
except:
|
|
1164
1172
|
new_status = "offline"
|
|
1165
1173
|
|
|
@@ -350,6 +350,23 @@ class PreviewAgent:
|
|
|
350
350
|
return dt.timestamp()
|
|
351
351
|
|
|
352
352
|
async def _end_vision_session(self) -> None:
|
|
353
|
+
# Tell the scheduler too, not just the local publisher/LiveKit
|
|
354
|
+
# connection — otherwise the sessions row never gets ended_at set,
|
|
355
|
+
# and the server's active_sessions count climbs forever until it
|
|
356
|
+
# hits max_sessions and silently blocks the room getting genuinely
|
|
357
|
+
# picked up (unrelated to this specific dispatch, but a real bug:
|
|
358
|
+
# every silence-timeout before this fix leaked a permanently
|
|
359
|
+
# "active" session).
|
|
360
|
+
if self._session and self.device_token:
|
|
361
|
+
try:
|
|
362
|
+
await self._session.post(
|
|
363
|
+
f"{self.scheduler_url.rstrip('/')}/api/robots/{self.device_id or self.robot_id}/end-session",
|
|
364
|
+
params={"reason": "silence"},
|
|
365
|
+
headers={"Authorization": f"Bearer {self.device_token}"},
|
|
366
|
+
timeout=10.0,
|
|
367
|
+
)
|
|
368
|
+
except Exception as e:
|
|
369
|
+
logger.warning(f"failed to notify scheduler of session end: {e}")
|
|
353
370
|
self._vision_session_id = None
|
|
354
371
|
self._vision_hard_deadline = 0.0
|
|
355
372
|
self._vision_last_activity = 0.0
|