infinicode 2.8.26 → 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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.8.26",
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",
@@ -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