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.
@@ -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 port = env.data?.port;
581
- const ip = normalizeIp(remote);
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.connect(url)
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.connect(url)
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;
@@ -352,6 +352,9 @@ export class MeshClient {
352
352
  constructor(opts = {}) {
353
353
  this.opts = opts;
354
354
  }
355
+ get token() {
356
+ return this.opts.token;
357
+ }
355
358
  headers() {
356
359
  const h = { 'content-type': 'application/json' };
357
360
  if (this.opts.token)
@@ -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) {
@@ -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.25",
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
- new_status = "offline"
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