ucode-agent 1.61.0 → 1.62.0

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/README.md CHANGED
@@ -96,6 +96,9 @@ Served by Google (aistudio.google.com), free with a key.
96
96
 
97
97
  `/model` shows them and switches. `ucode -m gemini-3.5-flash` starts on one.
98
98
 
99
+ When Google is overloaded and Flash-Lite stops answering, ucode carries on with
100
+ 3.5 Flash by itself and goes back to Flash-Lite a few minutes later.
101
+
99
102
  ## What it does
100
103
 
101
104
  **Twenty-one tools.** `create_app`, `read_file`, `read_files`, `write_file`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucode-agent",
3
- "version": "1.61.0",
3
+ "version": "1.62.0",
4
4
  "description": "ucode - a terminal coding agent that reads, edits and runs your code, on Google Gemini models.",
5
5
  "type": "module",
6
6
  "main": "ucode.js",
@@ -12,7 +12,7 @@ import os from 'node:os';
12
12
  import path from 'node:path';
13
13
  import { theme, dim, blue } from '../ui/theme.js';
14
14
  import { VERSION } from './version.js';
15
- import { DEFAULT_MODEL, ENV_FILE, BASE_URL, providerKey } from './provider.js';
15
+ import { ENV_FILE, BASE_URL, providerKey } from './provider.js';
16
16
  import { newer } from './updater.js';
17
17
 
18
18
  const DEADLINE = 6000;
@@ -27,22 +27,11 @@ async function checkKey() {
27
27
  const key = providerKey();
28
28
  if (!key) return { ok: false, name: 'API key', detail: 'not set', fix: `Add GEMINI_API_KEY=... to ${ENV_FILE} (free at aistudio.google.com/apikey)` };
29
29
  try {
30
- const r = await fetch(`${BASE_URL}/chat/completions`, {
31
- method: 'POST',
32
- headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
33
- body: JSON.stringify({ model: DEFAULT_MODEL, messages: [{ role: 'user', content: 'ok' }], max_tokens: 1 }),
34
- signal: timed(),
35
- });
36
- if (r.status === 401 || r.status === 403) return { ok: false, name: 'API key', detail: 'rejected', fix: 'Check the key for typos, or make a new one' };
37
- const body = await r.json().catch(() => ({}));
38
- const meta = body?.error?.metadata?.headers ?? {};
39
- const left = r.headers.get('x-ratelimit-remaining') ?? meta['X-RateLimit-Remaining'];
40
- const reset = Number(r.headers.get('x-ratelimit-reset') ?? meta['X-RateLimit-Reset']);
41
- if (r.status === 429 && /per[- ]day|daily/i.test(`${body?.error?.message} ${body?.error?.metadata?.limit_source}`)) {
42
- const at = Number.isFinite(reset) ? new Date(reset).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : 'tomorrow';
43
- return { ok: false, name: 'Requests today', detail: 'daily free limit used up', fix: `It resets at ${at}; adding credit raises the limit` };
44
- }
45
- return { ok: true, name: 'API key', detail: left != null ? `works · ${left} free requests left today` : 'works' };
30
+ // Listing models checks the key in under a second; a chat request can queue for half a minute.
31
+ const r = await fetch(`${BASE_URL}/models`, { headers: { Authorization: `Bearer ${key}` }, signal: timed() });
32
+ if ([400, 401, 403].includes(r.status)) return { ok: false, name: 'API key', detail: 'rejected', fix: 'Check the key for typos, or make a new one' };
33
+ if (!r.ok) return { ok: false, name: 'API key', detail: `could not check (HTTP ${r.status})`, fix: 'Try again in a minute' };
34
+ return { ok: true, name: 'API key', detail: 'works' };
46
35
  } catch (err) {
47
36
  return { ok: false, name: 'API key', detail: `could not check (${err.name === 'TimeoutError' ? 'no answer' : err.message})`, fix: 'Check your internet connection' };
48
37
  }
package/src/core/loop.js CHANGED
@@ -27,7 +27,7 @@ import { spawn } from 'node:child_process';
27
27
 
28
28
  import {
29
29
  ask, model, setModel, modelName, modelList, contextLimit, rateLimits,
30
- estimateConversation, MODELS, DEFAULT_MODEL, PROVIDER, fallbackFor,
30
+ estimateConversation, MODELS, DEFAULT_MODEL, PROVIDER, fallbackFor, backupFor,
31
31
  } from './provider.js';
32
32
  import {
33
33
  tools, runTool, describe, setRoot, setConfirm, setRequest,
@@ -1946,6 +1946,26 @@ export class Agent {
1946
1946
  .map((f) => ({ abs: path.resolve(this.cwd, f.path), size: f.content.length, path: f.path }));
1947
1947
  }
1948
1948
 
1949
+ /**
1950
+ * A model that has just made todo-app/ sometimes writes app.js beside it
1951
+ * instead of in it: the page never gets its script, and the hand-over, which
1952
+ * watches the app folder, never fires. A bare name the app already has, and
1953
+ * this folder does not, goes into the app.
1954
+ */
1955
+ intoApp(call) {
1956
+ const app = this.apps?.at(-1);
1957
+ if (!app) return;
1958
+ const fix = (p) => {
1959
+ if (typeof p !== 'string' || path.isAbsolute(p) || existsSync(path.resolve(this.cwd, p))) return p;
1960
+ const inside = path.join(app, p);
1961
+ if (path.relative(app, inside).startsWith('..') || !existsSync(inside)) return p;
1962
+ return path.relative(this.cwd, inside).split(path.sep).join('/');
1963
+ };
1964
+ const a = call.args ?? {};
1965
+ if (call.name === 'write_file') a.path = fix(a.path);
1966
+ else if (call.name === 'batch_write') a.files = normaliseFiles(a.files).map((f) => (f && typeof f === 'object' ? { ...f, path: fix(f.path) } : f));
1967
+ }
1968
+
1949
1969
  async dispatch(call, offered = this.offering) {
1950
1970
  // The model emitted arguments that were not valid JSON. Hand the parser's
1951
1971
  // own complaint straight back so it can correct itself next step.
@@ -2053,6 +2073,8 @@ export class Agent {
2053
2073
  }
2054
2074
  }
2055
2075
 
2076
+ if (call.name === 'write_file' || call.name === 'batch_write') this.intoApp(call);
2077
+
2056
2078
  if (call.name === 'load_skill') return this.loadSkill(call.args?.name);
2057
2079
  if (call.name === 'update_plan') return this.updatePlan(call.args?.items);
2058
2080
  if (call.name === 'delegate') return this.delegate(call.args?.tasks);
@@ -2073,6 +2095,8 @@ export class Agent {
2073
2095
  if (++this.failovers > MAX_FAILOVERS) return false;
2074
2096
  const from = model();
2075
2097
  let next = fallbackFor(from, this.tried);
2098
+ const backup = err.kind !== 'rate_limit' && backupFor(from);
2099
+ if (!next && backup && !this.tried?.has(backup)) next = backup;
2076
2100
 
2077
2101
  const why = err.kind === 'rate_limit' ? 'busy' : err.kind === 'timeout' ? 'too slow to answer' : 'not answering';
2078
2102
 
@@ -104,6 +104,15 @@ export function fallbackFor(id, tried = new Set()) {
104
104
  return null;
105
105
  }
106
106
 
107
+ /**
108
+ * Where a Flash-Lite model goes when Google is overloaded (503) or it freezes.
109
+ * Unlike fallbackFor this is on by default: both Lite models go down together
110
+ * at peak hours while Flash keeps answering, and a build that waits on them
111
+ * can wait for hours. UCODE_BACKUP=0 turns it off.
112
+ */
113
+ export const backupFor = (id) =>
114
+ /flash-lite/.test(id) && process.env.UCODE_BACKUP !== '0' ? 'gemini-3.5-flash' : null;
115
+
107
116
  /** Seconds to wait on successive rate limits that come with no retry-after. */
108
117
  const RATE_LIMIT_BACKOFF = [5, 10, 20];
109
118
 
@@ -711,7 +720,7 @@ export async function ask(messages, tools = [], opts = {}) {
711
720
  kind: 'stalled',
712
721
  attempted: `asking ${modelName(id)} for a reply`,
713
722
  failed: `${modelName(id)} froze ${MAX_STALLS} times in a row — it took the request and then sent nothing.`,
714
- fix: 'Its free endpoint is struggling right now. Run /model and pick another one; North Mini Code answers soonest.',
723
+ fix: 'Google is struggling with it right now. Run /model and pick another one.',
715
724
  cause: problem,
716
725
  });
717
726
  }
@@ -741,6 +750,7 @@ export async function ask(messages, tools = [], opts = {}) {
741
750
  if (!worthRetrying || attempt === attempts || opts.signal?.aborted) break;
742
751
  if (printed > 0) break; // half an answer is on screen; do not print it twice
743
752
  if (problem.detail?.handed) break; // its first tool calls are already running
753
+ if (problem.kind === 'timeout' && backupFor(id)) break; // one freeze is enough: the loop moves to the backup
744
754
 
745
755
  // A stalled provider needs longer to come back than a dropped socket
746
756
  // does, and the wait is narrated so a slow turn never looks like a hang.