openzoo 0.48.10 → 0.48.12

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 +60 -5
  2. package/package.json +1 -1
package/lib/grokui.mjs CHANGED
@@ -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; }
@@ -226,9 +236,23 @@ function loadThreads() {
226
236
 
227
237
  function newThread(name, parent, members) {
228
238
  const id = randomUUID();
239
+ // A subagent INHERITS its parent's run mode, working directory and model.
240
+ //
241
+ // runMode especially: children defaulted to 'ask', so a bot spawned in auto
242
+ // mode emitted a RUN, the harness parked it awaiting approval, and nobody
243
+ // was watching that thread to approve it. The subagent looked "stuck
244
+ // typing…" forever while the parent reported it as working. Spawning from
245
+ // auto and landing in ask is never what the user meant.
246
+ //
247
+ // dir matters just as much — a child that defaults elsewhere cannot see the
248
+ // files the parent was sent to work on.
249
+ const p = parent ? threads.get(parent) : null;
229
250
  const t = { id, name, color: members ? members[0].color : colorFor(name), parent: parent || null,
230
251
  messages: members ? null : [{ role: 'system', content: SYSTEM }],
231
- members: members || null, history: [], status: 'idle', createdAt: Date.now(), lastActivityAt: Date.now() };
252
+ members: members || null, history: [], status: 'idle', createdAt: Date.now(), lastActivityAt: Date.now(),
253
+ ...(p?.runMode ? { runMode: p.runMode } : {}),
254
+ ...(p?.dir ? { dir: p.dir } : {}),
255
+ ...(p?.model ? { model: p.model } : {}) };
232
256
  threads.set(id, t);
233
257
  saveThreads();
234
258
  return t;
@@ -1227,10 +1251,18 @@ async function runTurn(threadId, userText, onEvent, images) {
1227
1251
  }
1228
1252
  if (t.runMode === 'auto') extras.push({ role: 'system', content: AUTO_DIRECTIVE });
1229
1253
  const callMsgs = extras.length ? [...t.messages, ...extras] : t.messages;
1254
+ const ask = async () => (onEvent
1255
+ ? (await brainStream(callMsgs, (delta) => onEvent({ type: 'delta', name: t.name, color: t.color, delta }), t.contextId, t.model)).trim()
1256
+ : (await brain(callMsgs, t.contextId, t.model)).trim());
1230
1257
  try {
1231
- reply = onEvent
1232
- ? (await brainStream(callMsgs, (delta) => onEvent({ type: 'delta', name: t.name, color: t.color, delta }), t.contextId, t.model)).trim()
1233
- : (await brain(callMsgs, t.contextId, t.model)).trim();
1258
+ reply = await ask();
1259
+ // An EMPTY completion is transient far more often than it is meaningful
1260
+ // it showed up repeatedly as a dead "(no response)" bubble that cost the
1261
+ // user a turn and told them nothing. Retry once before giving up, and if
1262
+ // it is still empty, say what actually happened instead of "(no
1263
+ // response)", which reads like the harness broke.
1264
+ if (!reply) reply = await ask();
1265
+ if (!reply) reply = '(the model returned an empty completion — usually a transient provider hiccup or a stop-sequence firing early. Say "continue" to retry.)';
1234
1266
  } catch (e) {
1235
1267
  reply = `error: ${e.message}`;
1236
1268
  }
@@ -1283,6 +1315,29 @@ async function runTurn(threadId, userText, onEvent, images) {
1283
1315
  t.status = 'idle';
1284
1316
  t.lastActivityAt = Date.now();
1285
1317
  saveThreads();
1318
+
1319
+ // AUTO CONTINUES AFTER *ANY* DIRECTIVE, not just RUN.
1320
+ //
1321
+ // Only the RUN branch above fed its output back and looped, so a turn that
1322
+ // used SPAWN / WRITE / EDIT / MCP / GLOB executed one directive, posted its
1323
+ // ack, and stopped dead — the user had to type "continue" to get each
1324
+ // subsequent step, on every single turn, which is not what auto means. The
1325
+ // model never saw its own directive's result either, so it could not react
1326
+ // to "no matches" or "already exists".
1327
+ //
1328
+ // Same budget as RUN (shared t.autoSteps, reset when the user speaks), so
1329
+ // this cannot spend more than a chained RUN loop already could.
1330
+ if (t.runMode === 'auto' && ack !== null && ack !== undefined) {
1331
+ t.autoSteps = (t.autoSteps || 0) + 1;
1332
+ if (t.autoSteps < AUTO_MAX_STEPS) {
1333
+ runTurn(threadId, `(directive result)\n${ack}`, onEvent).catch(() => {});
1334
+ return;
1335
+ }
1336
+ const note = `(auto stopped after ${AUTO_MAX_STEPS} steps — say "continue" to keep going)`;
1337
+ t.history.push({ who: 'bot', text: note });
1338
+ onEvent?.({ type: 'final', name: t.name, color: t.color, text: note });
1339
+ saveThreads();
1340
+ }
1286
1341
  bindThread(t).catch(() => {});
1287
1342
  }
1288
1343
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.48.10",
3
+ "version": "0.48.12",
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",