openzoo 0.48.11 → 0.48.13

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/lib/grokui.mjs CHANGED
@@ -236,9 +236,23 @@ function loadThreads() {
236
236
 
237
237
  function newThread(name, parent, members) {
238
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;
239
250
  const t = { id, name, color: members ? members[0].color : colorFor(name), parent: parent || null,
240
251
  messages: members ? null : [{ role: 'system', content: SYSTEM }],
241
- 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 } : {}) };
242
256
  threads.set(id, t);
243
257
  saveThreads();
244
258
  return t;
@@ -546,6 +560,27 @@ const SLASH_COMMANDS = [
546
560
 
547
561
  const usd = (n) => (n >= 0.01 || n === 0 ? '$' + n.toFixed(2) : '$' + n.toFixed(5));
548
562
 
563
+ // BIND, DON'T PASTE — the whole point of the thing this runs on.
564
+ //
565
+ // Directive results get fed back to the model as a user message, verbatim. A
566
+ // GLOB over a 670-part upload, an LS of a big tree or an MCP tool dump is
567
+ // thousands of tokens, and in an auto chain it is re-sent on EVERY subsequent
568
+ // hop as conversation history: the window grows quadratically and the user
569
+ // pays for it each time. Meanwhile the full text is already going into
570
+ // t.history, which bindThread pushes into the thread's leCore context — so
571
+ // the model can recall any of it on demand.
572
+ //
573
+ // So: feed back the head and tail (where the answer nearly always is) and say
574
+ // plainly that the middle is recallable rather than lost.
575
+ const FEEDBACK_MAX = Number(process.env.OZ_FEEDBACK_MAX || 3000);
576
+ function condense(label, text) {
577
+ const s = String(text ?? '');
578
+ if (s.length <= FEEDBACK_MAX) return `${label}\n${s}`;
579
+ const head = s.slice(0, Math.floor(FEEDBACK_MAX * 0.7));
580
+ const tail = s.slice(-Math.floor(FEEDBACK_MAX * 0.3));
581
+ return `${label}\n${head}\n\n…[${s.length - head.length - tail.length} chars elided — the FULL output is bound to this thread's holographic context; ask for any part of it and it will be recalled]…\n\n${tail}`;
582
+ }
583
+
549
584
  // threadId -> open SSE responses. A Set because the same thread can be open in
550
585
  // two tabs, and both should see the same tokens.
551
586
  const streamListeners = new Map();
@@ -1274,12 +1309,19 @@ async function runTurn(threadId, userText, onEvent, images) {
1274
1309
  t.lastActivityAt = Date.now();
1275
1310
  saveThreads();
1276
1311
  if (t.autoSteps < AUTO_MAX_STEPS) {
1277
- runTurn(threadId, `(command output)\n${output}`, onEvent).catch(() => {});
1312
+ // BIND BEFORE CHAINING. bindThread only ran at the end of a normal
1313
+ // turn, and both auto paths return before reaching it — so in auto
1314
+ // mode nothing was ever bound, exactly when the agent produces the
1315
+ // most material (command output, GLOB results, MCP tool lists). The
1316
+ // holographic context stopped growing precisely when it mattered.
1317
+ bindThread(t).catch(() => {});
1318
+ runTurn(threadId, condense('(command output)', output), onEvent).catch(() => {});
1278
1319
  } else {
1279
1320
  const note = `(auto-run stopped after ${AUTO_MAX_STEPS} chained commands — say "continue" to keep going)`;
1280
1321
  t.history.push({ who: 'bot', text: note });
1281
1322
  onEvent?.({ type: 'final', name: t.name, color: t.color, text: note });
1282
1323
  saveThreads();
1324
+ bindThread(t).catch(() => {});
1283
1325
  }
1284
1326
  return;
1285
1327
  }
@@ -1316,7 +1358,8 @@ async function runTurn(threadId, userText, onEvent, images) {
1316
1358
  if (t.runMode === 'auto' && ack !== null && ack !== undefined) {
1317
1359
  t.autoSteps = (t.autoSteps || 0) + 1;
1318
1360
  if (t.autoSteps < AUTO_MAX_STEPS) {
1319
- runTurn(threadId, `(directive result)\n${ack}`, onEvent).catch(() => {});
1361
+ bindThread(t).catch(() => {}); // bind every hop, not just the last one
1362
+ runTurn(threadId, condense('(directive result)', ack), onEvent).catch(() => {});
1320
1363
  return;
1321
1364
  }
1322
1365
  const note = `(auto stopped after ${AUTO_MAX_STEPS} steps — say "continue" to keep going)`;
package/lib/podagent.mjs CHANGED
@@ -184,6 +184,10 @@ function execFrame(command, cwd = '/tmp') {
184
184
  // on a text-only model silently ignoring pasted images, any message with
185
185
  // multimodal (image_url) content routes to a model KNOWN to support vision.
186
186
  const VISION_MODEL = process.env.OZ_VISION_MODEL || 'anthropic/claude-sonnet-5';
187
+ // Output budget per turn. Reasoning models spend this on their chain of
188
+ // thought BEFORE emitting any content, so a budget that is merely "enough for
189
+ // the answer" produces no answer at all on a hard prompt.
190
+ const MAX_TOKENS = Number(process.env.OZ_MAX_TOKENS || 4096);
187
191
  const msgHasImage = (m) => Array.isArray(m.content) && m.content.some((c) => c?.type === 'image_url');
188
192
 
189
193
  // How far back an image still counts as "being discussed". Beyond this the
@@ -305,12 +309,13 @@ export async function brain(messages, contextId, modelOverride) {
305
309
  /** Same call, but streamed — invokes onDelta(text) as tokens arrive (for a
306
310
  * live-typing UI) and resolves with the full accumulated text at the end, so
307
311
  * callers that need to parse a directive out of the complete reply still can. */
308
- export async function brainStream(messages, onDelta, contextId, modelOverride) {
312
+ export async function brainStream(messages, onDelta, contextId, modelOverride, maxTokens) {
309
313
  const vision = hasImages(messages);
310
314
  const model = vision ? VISION_MODEL : (modelOverride || MODEL);
311
315
  messages = vision ? messages : stripImages(messages);
316
+ const budget = maxTokens || MAX_TOKENS;
312
317
  const r = await postChat(
313
- { model, max_tokens: 4096, messages: withModelId(messages, model), plugins: [{ id: 'web' }], stream: true },
318
+ { model, max_tokens: budget, messages: withModelId(messages, model), plugins: [{ id: 'web' }], stream: true },
314
319
  contextId,
315
320
  );
316
321
  if (!r.ok || !r.body) {
@@ -322,7 +327,7 @@ export async function brainStream(messages, onDelta, contextId, modelOverride) {
322
327
  }
323
328
  const reader = r.body.getReader();
324
329
  const decoder = new TextDecoder();
325
- let buf = '', full = '';
330
+ let buf = '', full = '', reasonedChars = 0;
326
331
  for (;;) {
327
332
  const { value, done } = await reader.read();
328
333
  if (done) break;
@@ -335,11 +340,24 @@ export async function brainStream(messages, onDelta, contextId, modelOverride) {
335
340
  const payload = s.slice(5).trim();
336
341
  if (payload === '[DONE]') continue;
337
342
  try {
338
- const delta = JSON.parse(payload)?.choices?.[0]?.delta?.content;
339
- if (delta) { full += delta; onDelta(delta); }
343
+ const d = JSON.parse(payload)?.choices?.[0]?.delta;
344
+ if (d?.content) { full += d.content; onDelta(d.content); }
345
+ // Reasoning models emit their chain of thought on a SEPARATE field and
346
+ // only then start producing content. Count it — not to show it, but to
347
+ // tell "the model said nothing" apart from "the model spent its whole
348
+ // budget thinking and got cut off".
349
+ else if (d?.reasoning || d?.reasoning_content) reasonedChars += (d.reasoning || d.reasoning_content).length;
340
350
  } catch { /* keep-alive line or partial JSON — ignore */ }
341
351
  }
342
352
  }
353
+
354
+ // EMPTY CONTENT AFTER HEAVY REASONING is a truncation, not an answer. It
355
+ // surfaced in the UI as a bare "(no response)" bubble that cost the user a
356
+ // turn and explained nothing — on exactly the long, complex prompts where a
357
+ // reasoning model thinks the most. Retry ONCE with a bigger budget.
358
+ if (!full && reasonedChars > 0 && !maxTokens) {
359
+ return brainStream(messages, onDelta, contextId, modelOverride, budget * 4);
360
+ }
343
361
  return full;
344
362
  }
345
363
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.48.11",
3
+ "version": "0.48.13",
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",