openzoo 0.50.35 → 0.50.37

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.
@@ -1549,6 +1549,11 @@ async function runLocalTool(name, args, log) {
1549
1549
  }
1550
1550
  }
1551
1551
 
1552
+ /** Model parked instead of writing files. Host keeps the tool loop going. */
1553
+ export function looksStoppedReply(s) {
1554
+ return /stopped on research|no (?:new )?app files written|nothing (?:extra )?to open yet|say go again|next message i['’]?ll write|stopped — no new/i.test(String(s || ''));
1555
+ }
1556
+
1552
1557
  function zooTextFromMessage(msg, data) {
1553
1558
  let c = msg?.content;
1554
1559
  if (Array.isArray(c)) {
@@ -1597,6 +1602,7 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
1597
1602
  'Pasted images arrive as attachments — you can see them when present. Do not claim you cannot see images if they are in this turn.',
1598
1603
  'Never claim you lack filesystem access or local-exec. If a tool errors, report the error.',
1599
1604
  'After using tools you MUST still write a normal chat reply: what you did, file paths written, and what to open. Empty content is a bug.',
1605
+ 'Do not stop mid-task. Do not write "Stopped on research", "nothing to open yet", "no app files written this turn", or "say go again". Keep using tools until the files the user asked for exist on disk THIS turn. Summarize only after those writes succeed.',
1600
1606
  'A spend footer is appended after your reply by the host — ignore it.',
1601
1607
  'Prior turns of THIS Grok Bot chat are in the messages below. Do not claim the thread starts blank or that earlier questions did not arrive.',
1602
1608
  ].join(' '),
@@ -1624,10 +1630,12 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
1624
1630
  : userContent,
1625
1631
  });
1626
1632
 
1627
- const maxTok = Number(process.env.OPENZOO_ASK_MAX_TOKENS || 4096);
1633
+ const maxTok = Number(process.env.OPENZOO_ASK_MAX_TOKENS || 8192);
1634
+ const maxSteps = Math.min(64, Math.max(8, Number(process.env.OPENZOO_ASK_TOOL_STEPS || 32)));
1628
1635
  let lastData = {};
1629
1636
  let text = '';
1630
1637
  const usedTools = [];
1638
+ const KEEP_GOING = 'Do not stop. Do not wait for another message. Write the files to disk now with write_file / exec. Keep going until the requested paths exist. A summary is only allowed after the files are written.';
1631
1639
  const zooPost = async (payload) => {
1632
1640
  const ms = Number(process.env.OPENZOO_ASK_TIMEOUT_MS || 10 * 60_000);
1633
1641
  const post = () => fetch('http://127.0.0.1:8402/v1/chat/completions', {
@@ -1638,7 +1646,8 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
1638
1646
  });
1639
1647
  let r;
1640
1648
  let lastErr;
1641
- for (let attempt = 1; attempt <= 3; attempt++) {
1649
+ const tries = 5;
1650
+ for (let attempt = 1; attempt <= tries; attempt++) {
1642
1651
  try {
1643
1652
  r = await post();
1644
1653
  lastErr = null;
@@ -1646,8 +1655,9 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
1646
1655
  } catch (e) {
1647
1656
  lastErr = e;
1648
1657
  log(`cursor-backend: zoo POST attempt=${attempt} ${e.message}`);
1649
- if (attempt === 3) throw e;
1650
- await new Promise((ok) => setTimeout(ok, 800 * attempt));
1658
+ if (attempt === tries) throw e;
1659
+ const abortish = /abort|timeout/i.test(String(e?.name || '') + String(e?.message || ''));
1660
+ await new Promise((ok) => setTimeout(ok, (abortish ? 2000 : 800) * attempt));
1651
1661
  }
1652
1662
  }
1653
1663
  if (!r) throw lastErr || new Error('zoo POST failed');
@@ -1675,7 +1685,8 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
1675
1685
  }
1676
1686
  return { r, data };
1677
1687
  };
1678
- for (let step = 0; step < 8; step++) {
1688
+ let keepGoingNudge = 0;
1689
+ for (let step = 0; step < maxSteps; step++) {
1679
1690
  const { r, data } = await zooPost({
1680
1691
  model,
1681
1692
  messages,
@@ -1709,13 +1720,26 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
1709
1720
  continue;
1710
1721
  }
1711
1722
  text = zooTextFromMessage(msg, data);
1712
- log(`cursor-backend: << zoo ${r.status} ${text.length}c model=${data.model || model} finish=${data.choices?.[0]?.finish_reason || '?'}`);
1723
+ const finish = data.choices?.[0]?.finish_reason || '?';
1724
+ log(`cursor-backend: << zoo ${r.status} ${text.length}c model=${data.model || model} finish=${finish}`);
1725
+ // Model likes to park after research ("Stopped on research", "say go again")
1726
+ // instead of writing files. Measured 2026-08-30 on volume track00r.
1727
+ if ((finish === 'length' || looksStoppedReply(text)) && keepGoingNudge < 6) {
1728
+ keepGoingNudge += 1;
1729
+ log(`cursor-backend: keep-going nudge=${keepGoingNudge} finish=${finish}`);
1730
+ messages.push(msg);
1731
+ messages.push({ role: 'user', content: KEEP_GOING });
1732
+ continue;
1733
+ }
1713
1734
  break;
1714
1735
  }
1715
1736
  if (usedTools.length && !String(text || '').trim()) {
1737
+ const wrote = usedTools.some((t) => t.name === 'write_file');
1716
1738
  messages.push({
1717
1739
  role: 'user',
1718
- content: 'Stop calling tools. Write a short chat reply summarizing what you did: files written (full paths), commands that mattered, and what the user should open. No empty message.',
1740
+ content: wrote
1741
+ ? 'Write a short chat reply: files written (full paths), commands that mattered, and what the user should open. No empty message.'
1742
+ : KEEP_GOING,
1719
1743
  });
1720
1744
  const { r, data } = await zooPost({
1721
1745
  model,
package/lib/pay.js CHANGED
@@ -413,7 +413,27 @@ export class PayClient {
413
413
  if (response.status === 402 && !this._rebuilt) {
414
414
  this._rebuilt = true;
415
415
  try {
416
- const fresh = await this.fetch(url, init, { onStage });
416
+ // TAKE THE GATEWAY'S ADVICE BEFORE RE-QUOTING BLIND. On "payer balance
417
+ // insufficient" the 402 now carries advice.retryWithMaxTokens — the
418
+ // max_tokens whose ceiling the wallet's ACTUAL balance covers. A blind
419
+ // re-quote asks the same price and dies the same death (the gateway
420
+ // ledger shows wallets doing exactly that, hundreds of times a day);
421
+ // shrinking the ask turns the retry into a sale. A shorter answer
422
+ // beats no answer, and the upto rail still bills only actual usage.
423
+ let retryInit = init;
424
+ try {
425
+ const body402 = await response.clone().json();
426
+ const fit = Number(body402?.advice?.retryWithMaxTokens);
427
+ if (Number.isFinite(fit) && fit >= 64 && typeof init.body === 'string') {
428
+ const req = JSON.parse(init.body);
429
+ if (!Number.isFinite(Number(req.max_tokens)) || Number(req.max_tokens) > fit) {
430
+ req.max_tokens = fit;
431
+ retryInit = { ...init, body: JSON.stringify(req) };
432
+ onStage?.('advice', `balance covers ~${body402?.advice?.coversPct ?? '?'}% — retrying at max_tokens=${fit}`);
433
+ }
434
+ }
435
+ } catch { /* no advice or unreadable body: plain re-quote */ }
436
+ const fresh = await this.fetch(url, retryInit, { onStage });
417
437
  return fresh;
418
438
  } finally {
419
439
  this._rebuilt = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.50.35",
3
+ "version": "0.50.37",
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",