openzoo 0.48.2 → 0.48.4

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 +59 -4
  2. package/package.json +1 -1
package/lib/grokui.mjs CHANGED
@@ -301,6 +301,35 @@ function newGroupThread(names) {
301
301
  const BIND_CHUNK_BYTES = 512 * 1024;
302
302
  // Chained auto-run commands per user message. Each hop is a paid call.
303
303
  const AUTO_MAX_STEPS = Number(process.env.OZ_AUTO_MAX_STEPS || 8);
304
+
305
+ // Injected fresh on every AUTO turn, never persisted into the thread.
306
+ //
307
+ // The auto loop only continues while directives keep parsing, so a reply that
308
+ // merely OFFERS ends the run — auto silently degrades to ask the moment the
309
+ // model hedges. Observed live: "If you want, I can rewrite the prompt with
310
+ // these fixes folded in", "Spawned mcp-integration — working on it" with
311
+ // nothing spawned, and a user reduced to answering "no, this... impl all".
312
+ // Models are trained to close on a consent question; in auto that instinct is
313
+ // the bug. The system prompt is frozen into a thread at creation, so an
314
+ // existing thread can only be reached by a per-turn message.
315
+ const AUTO_DIRECTIVE = `AUTO MODE IS ON for this thread.
316
+
317
+ Do the work in this turn. Do not ask whether to proceed, do not offer to do it,
318
+ do not say what you are "about to" do and stop. The user has already consented
319
+ by enabling auto — a question back to them is a dropped turn, and they must
320
+ type "yes" to get what they already asked for.
321
+
322
+ Concretely, NEVER end a turn with any of: "If you want, I can…", "Should I…?",
323
+ "Let me know and I'll…", "Ready to proceed?", or a plan with no directive after
324
+ it. If you catch yourself writing one, emit the RUN/WRITE/READ/SERVE/FETCH line
325
+ instead — that IS the answer.
326
+
327
+ Announcing an action does not perform it. "Spawned X", "working on it" and
328
+ "kicked that off" are false unless the directive line is in this same reply.
329
+ If a task needs several commands, emit the FIRST one now; you get its real
330
+ output back and continue from there. Only stop to ask when the next step is
331
+ genuinely destructive and irreversible, or when you truly cannot proceed
332
+ without a fact only the user has.`;
304
333
  async function bindThread(t) {
305
334
  // Only bind what's NEW since the last successful bind, continuing the
306
335
  // existing context_id — previously this rebuilt and re-sent the WHOLE
@@ -378,8 +407,16 @@ function parseRun(reply) {
378
407
  // use U+FF5C FULLWIDTH VERTICAL LINE (|) — matching only `|` parsed the
379
408
  // pretty-printed form in tests while missing what the model actually emits,
380
409
  // which is exactly how this survived one round of "fixed".
410
+ // The PARAMETER NAME is not fixed either. Models emit name="command",
411
+ // name="cmd", name="shell_command" and name="script" for the same thing —
412
+ // MEASURED live emitting `name="cmd"` inside an invoke named exec_command,
413
+ // against a parser that demanded name="command". One attribute apart, and
414
+ // the whole envelope was dropped in silence: the bot then explained what it
415
+ // was "about to run" forever, never running anything. Match the shape of the
416
+ // envelope, not one vendor's spelling of it.
381
417
  const SEP = '[||\\s]*';
382
- const dsml = new RegExp(`<${SEP}DSML[^>]*\\bparameter\\b[^>]*name="command"[^>]*>([\\s\\S]*?)<\\/${SEP}DSML`, 'i').exec(reply);
418
+ const NAME = '(?:command|cmd|shell_command|script)';
419
+ const dsml = new RegExp(`<${SEP}DSML[^>]*\\bparameter\\b[^>]*\\bname="${NAME}"[^>]*>([\\s\\S]*?)<\\/${SEP}DSML`, 'i').exec(reply);
383
420
  if (dsml) return sanitizeRunCommand(dsml[1]);
384
421
 
385
422
  const m = /^[ \t>*-]*RUN:[ \t]*([\s\S]+)/m.exec(reply);
@@ -391,9 +428,21 @@ function parseRun(reply) {
391
428
  return sanitizeRunCommand(cmd);
392
429
  }
393
430
 
431
+ // RUN through BASH, not /bin/sh. node's exec() defaults to /bin/sh, which on
432
+ // Debian is dash — so every bash-ism a model writes (`for … do`, `[[ ]]`,
433
+ // arrays, process substitution) dies as
434
+ // /bin/sh: 40: Syntax error: "do" unexpected
435
+ // which reads as the model writing bad code when it wrote perfectly good bash.
436
+ // Models overwhelmingly emit bash; give them bash. Windows is left alone so
437
+ // node picks cmd.exe, and a box without /bin/bash falls back to the default.
438
+ const RUN_SHELL = process.platform !== 'win32' && existsSync('/bin/bash') ? '/bin/bash' : undefined;
439
+ // Installing a toolchain (apt-get, pip, cargo) routinely outruns two minutes,
440
+ // and a killed install leaves a half-configured box that fails confusingly.
441
+ const RUN_TIMEOUT_MS = Number(process.env.OZ_RUN_TIMEOUT_MS || 600000);
442
+
394
443
  function execCommand(command, cwd) {
395
444
  return new Promise((resolve) => {
396
- exec(command, { cwd, timeout: 120000, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
445
+ exec(command, { cwd, shell: RUN_SHELL, timeout: RUN_TIMEOUT_MS, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
397
446
  let out = (stdout || '') + (stderr ? '\n' + stderr : '');
398
447
  if (err) out += `\n(exit ${err.code ?? 1})`;
399
448
  resolve(out.slice(0, 6000) || '(no output)');
@@ -555,10 +604,16 @@ async function runTurn(threadId, userText, onEvent, images) {
555
604
  t.status = 'thinking';
556
605
  let reply = '';
557
606
  onEvent?.({ type: 'start', name: t.name, color: t.color });
607
+ // Transient: the nudge is appended for THIS call only and never pushed into
608
+ // t.messages, so it can't accumulate across a chained auto run or get bound
609
+ // into the thread's context.
610
+ const callMsgs = t.runMode === 'auto'
611
+ ? [...t.messages, { role: 'system', content: AUTO_DIRECTIVE }]
612
+ : t.messages;
558
613
  try {
559
614
  reply = onEvent
560
- ? (await brainStream(t.messages, (delta) => onEvent({ type: 'delta', name: t.name, color: t.color, delta }), t.contextId)).trim()
561
- : (await brain(t.messages, t.contextId)).trim();
615
+ ? (await brainStream(callMsgs, (delta) => onEvent({ type: 'delta', name: t.name, color: t.color, delta }), t.contextId)).trim()
616
+ : (await brain(callMsgs, t.contextId)).trim();
562
617
  } catch (e) {
563
618
  reply = `error: ${e.message}`;
564
619
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.48.2",
3
+ "version": "0.48.4",
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",