openzoo 0.48.8 → 0.48.10

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 +37 -2
  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
 
@@ -336,6 +336,18 @@ function newGroupThread(names) {
336
336
  const BIND_CHUNK_BYTES = 512 * 1024;
337
337
  // Chained auto-run commands per user message. Each hop is a paid call.
338
338
  const AUTO_MAX_STEPS = Number(process.env.OZ_AUTO_MAX_STEPS || 8);
339
+ // Ceiling on subagents per thread. Spawning is fire-and-forget and each child
340
+ // can spawn too, so without a count it is unbounded — MEASURED as 15+ threads
341
+ // all named tetris-contract, every one of them a live agent making paid calls.
342
+ //
343
+ // Scaled to the box rather than a magic number: a 2-core container and a
344
+ // 32-core one should not get the same allowance. Agents are network-bound, not
345
+ // CPU-bound — they spend their time waiting on the model — so the multiplier
346
+ // is generous, and cores are a proxy for "how big is this machine" rather than
347
+ // a real parallelism limit. The honest limit is money, which is why the
348
+ // refusal message says so.
349
+ const SPAWN_MAX_CHILDREN = Number(process.env.OZ_SPAWN_MAX_CHILDREN)
350
+ || Math.max(8, (cpus()?.length || 2) * 4);
339
351
 
340
352
  // Injected fresh on every AUTO turn, never persisted into the thread.
341
353
  //
@@ -575,7 +587,10 @@ async function handleSlash(task, t) {
575
587
  + ' FETCH: <url> read a page’s text\n'
576
588
  + ' MCP: <url> [| tool | {json}] list or call MCP tools\n'
577
589
  + ' TODO: <lines> visible checklist\n'
578
- + ' SPAWN / SEND / PING / PEEK other bots\n\n'
590
+ + ' SPAWN: <name> | <task> a NEW subagent (names are unique —\n'
591
+ + ' spawning an existing one sends to it)\n'
592
+ + ' SEND: <name> | <msg> more work for an EXISTING subagent\n'
593
+ + ' PING / PEEK reach or inspect another bot\n\n'
579
594
  + 'READ, LS, GLOB, GREP, FETCH, PEEK and MCP run CONCURRENTLY when several\n'
580
595
  + 'appear in one reply — four files cost one round trip, not four.';
581
596
  }
@@ -776,6 +791,26 @@ async function tryDirective(reply, originId) {
776
791
  if (spawn) {
777
792
  const name = spawn[1].trim();
778
793
  const task = spawn[2].trim();
794
+ // NAMES ARE UNIQUE. This used to call newThread() unconditionally, so a
795
+ // model asked to "keep spawning" produced FIFTEEN threads all called
796
+ // tetris-contract, each one a live agent burning paid calls, and the
797
+ // sidebar became an unusable wall of identical rows. A repeat SPAWN is
798
+ // almost always the model re-issuing work for the same worker, not asking
799
+ // for a second identical one — so route it to the existing thread, which
800
+ // is what SEND already does.
801
+ const existing = findByName(name);
802
+ if (existing) {
803
+ runTurn(existing.id, task).catch(() => {});
804
+ return `${name} already exists — sent it the task instead of spawning a duplicate.`;
805
+ }
806
+ // Storm guard. Fire-and-forget spawning is unbounded by construction: each
807
+ // child can spawn, and nothing above it is counting.
808
+ const siblings = [...threads.values()].filter((x) => x.parent === originId).length;
809
+ if (siblings >= SPAWN_MAX_CHILDREN) {
810
+ return `Not spawning "${name}": this thread already has ${siblings} subagents `
811
+ + `(limit ${SPAWN_MAX_CHILDREN}). Reuse one with SEND: <name> | <task> — `
812
+ + `every live subagent costs paid calls.`;
813
+ }
779
814
  const sub = newThread(name, originId);
780
815
  runTurn(sub.id, task).catch(() => {}); // fire and forget — runs independently
781
816
  return `Spawned ${name} — working on it.`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.48.8",
3
+ "version": "0.48.10",
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",