openzoo 0.48.9 → 0.48.11

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.
Files changed (2) hide show
  1. package/lib/grokui.mjs +55 -6
  2. package/package.json +1 -1
package/lib/grokui.mjs CHANGED
@@ -9,7 +9,7 @@ import { exec } from 'node:child_process';
9
9
  import http from 'node:http';
10
10
  import { randomUUID } from 'node:crypto';
11
11
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
12
- import { homedir } from 'node:os';
12
+ import { cpus, homedir } from 'node:os';
13
13
  import path from 'node:path';
14
14
  import { brain, brainStream, MODEL, PROXY } from './podagent.mjs';
15
15
 
@@ -29,7 +29,17 @@ const STORE_FILE = path.join(STORE_DIR, 'grokui-threads.json');
29
29
  // chat. safeResolveIn rejects any path that would escape that thread's root
30
30
  // (../, absolute paths, symlink tricks via normalize) — access is real, but
31
31
  // always contained to whatever root was explicitly chosen for that thread.
32
- const WORKSPACE_DIR = path.join(homedir(), '.openzoo', 'grokui-workspace');
32
+ // Where a thread's WRITE/READ/RUN/LS/GLOB/GREP are scoped by default.
33
+ //
34
+ // Overridable because a BOX puts uploaded files somewhere else: box-server
35
+ // unpacks them into /workspace, while this defaulted to ~/.openzoo/grokui-
36
+ // workspace. So a user uploaded a 670-part archive, asked a bot to find it,
37
+ // and got "GLOB **/prooffront: no matches" — the bot was searching an empty
38
+ // directory and looked broken while the files sat one path away. The site had
39
+ // resorted to printing "grokui: /dir /workspace" as a hint for the user to
40
+ // fix it by hand every time.
41
+ const WORKSPACE_DIR = process.env.OZ_WORKSPACE_DIR
42
+ || path.join(homedir(), '.openzoo', 'grokui-workspace');
33
43
  mkdirSync(WORKSPACE_DIR, { recursive: true });
34
44
  function expandHome(p) { return p.startsWith('~') ? path.join(homedir(), p.slice(1)) : p; }
35
45
  function dirFor(threadId) { return threads.get(threadId)?.dir || WORKSPACE_DIR; }
@@ -339,7 +349,15 @@ const AUTO_MAX_STEPS = Number(process.env.OZ_AUTO_MAX_STEPS || 8);
339
349
  // Ceiling on subagents per thread. Spawning is fire-and-forget and each child
340
350
  // can spawn too, so without a count it is unbounded — MEASURED as 15+ threads
341
351
  // all named tetris-contract, every one of them a live agent making paid calls.
342
- const SPAWN_MAX_CHILDREN = Number(process.env.OZ_SPAWN_MAX_CHILDREN || 12);
352
+ //
353
+ // Scaled to the box rather than a magic number: a 2-core container and a
354
+ // 32-core one should not get the same allowance. Agents are network-bound, not
355
+ // CPU-bound — they spend their time waiting on the model — so the multiplier
356
+ // is generous, and cores are a proxy for "how big is this machine" rather than
357
+ // a real parallelism limit. The honest limit is money, which is why the
358
+ // refusal message says so.
359
+ const SPAWN_MAX_CHILDREN = Number(process.env.OZ_SPAWN_MAX_CHILDREN)
360
+ || Math.max(8, (cpus()?.length || 2) * 4);
343
361
 
344
362
  // Injected fresh on every AUTO turn, never persisted into the thread.
345
363
  //
@@ -1219,10 +1237,18 @@ async function runTurn(threadId, userText, onEvent, images) {
1219
1237
  }
1220
1238
  if (t.runMode === 'auto') extras.push({ role: 'system', content: AUTO_DIRECTIVE });
1221
1239
  const callMsgs = extras.length ? [...t.messages, ...extras] : t.messages;
1240
+ const ask = async () => (onEvent
1241
+ ? (await brainStream(callMsgs, (delta) => onEvent({ type: 'delta', name: t.name, color: t.color, delta }), t.contextId, t.model)).trim()
1242
+ : (await brain(callMsgs, t.contextId, t.model)).trim());
1222
1243
  try {
1223
- reply = onEvent
1224
- ? (await brainStream(callMsgs, (delta) => onEvent({ type: 'delta', name: t.name, color: t.color, delta }), t.contextId, t.model)).trim()
1225
- : (await brain(callMsgs, t.contextId, t.model)).trim();
1244
+ reply = await ask();
1245
+ // An EMPTY completion is transient far more often than it is meaningful
1246
+ // it showed up repeatedly as a dead "(no response)" bubble that cost the
1247
+ // user a turn and told them nothing. Retry once before giving up, and if
1248
+ // it is still empty, say what actually happened instead of "(no
1249
+ // response)", which reads like the harness broke.
1250
+ if (!reply) reply = await ask();
1251
+ if (!reply) reply = '(the model returned an empty completion — usually a transient provider hiccup or a stop-sequence firing early. Say "continue" to retry.)';
1226
1252
  } catch (e) {
1227
1253
  reply = `error: ${e.message}`;
1228
1254
  }
@@ -1275,6 +1301,29 @@ async function runTurn(threadId, userText, onEvent, images) {
1275
1301
  t.status = 'idle';
1276
1302
  t.lastActivityAt = Date.now();
1277
1303
  saveThreads();
1304
+
1305
+ // AUTO CONTINUES AFTER *ANY* DIRECTIVE, not just RUN.
1306
+ //
1307
+ // Only the RUN branch above fed its output back and looped, so a turn that
1308
+ // used SPAWN / WRITE / EDIT / MCP / GLOB executed one directive, posted its
1309
+ // ack, and stopped dead — the user had to type "continue" to get each
1310
+ // subsequent step, on every single turn, which is not what auto means. The
1311
+ // model never saw its own directive's result either, so it could not react
1312
+ // to "no matches" or "already exists".
1313
+ //
1314
+ // Same budget as RUN (shared t.autoSteps, reset when the user speaks), so
1315
+ // this cannot spend more than a chained RUN loop already could.
1316
+ if (t.runMode === 'auto' && ack !== null && ack !== undefined) {
1317
+ t.autoSteps = (t.autoSteps || 0) + 1;
1318
+ if (t.autoSteps < AUTO_MAX_STEPS) {
1319
+ runTurn(threadId, `(directive result)\n${ack}`, onEvent).catch(() => {});
1320
+ return;
1321
+ }
1322
+ const note = `(auto stopped after ${AUTO_MAX_STEPS} steps — say "continue" to keep going)`;
1323
+ t.history.push({ who: 'bot', text: note });
1324
+ onEvent?.({ type: 'final', name: t.name, color: t.color, text: note });
1325
+ saveThreads();
1326
+ }
1278
1327
  bindThread(t).catch(() => {});
1279
1328
  }
1280
1329
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.48.9",
3
+ "version": "0.48.11",
4
4
  "description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
5
5
  "license": "MIT",
6
6
  "type": "module",