openzoo 0.49.2 → 0.49.3

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
@@ -691,6 +691,7 @@ const AUTO_RACE_RETRY = 'AUTO is still on — the last model call failed (race/e
691
691
  + 'Do not stop and do not ask the user to type continue. '
692
692
  + 'Emit the next directive now (RUN:/SPAWN:/SEND:/READ:/WRITE:/GLOB:/FETCH:/MCP:/SERVE:), '
693
693
  + 'or DONE: if the job is actually finished.';
694
+ const AUTO_EMPTY_RETRY = 'AUTO_EMPTY_RETRY: the command produced no output, try a different command or a different path, do not stop.';
694
695
  // Said it would, without a directive line. "Spawned X" and "working on it" are
695
696
  // in here because they are FALSE without a SPAWN: in the same reply — the bot
696
697
  // reports success for something the harness never saw.
@@ -709,13 +710,43 @@ function isTransientModelFail(text) {
709
710
  function isPaymentFailed(text) {
710
711
  return /\b(?:payment failed|HTTP 402|wallet is empty|empty wallet)\b/i.test(String(text || ''));
711
712
  }
713
+ // Empty stdout, "(no output)", or a directive that found nothing. That is
714
+ // still a command-output hop today, so AUTO used to chain once and then park
715
+ // as if the job had succeeded. It has not — try another command or path.
716
+ function isEmptyExecOutput(text) {
717
+ const s = String(text ?? '').trim();
718
+ return !s || s === '(no output)';
719
+ }
720
+ function isEmptyDirectiveAck(text) {
721
+ const s = String(text ?? '').trim();
722
+ if (isEmptyExecOutput(s)) return true;
723
+ if (/:\s*\(empty\)$/i.test(s)) return true;
724
+ if (/:\s*no matches\s*$/i.test(s)) return true;
725
+ return false;
726
+ }
727
+ function isEmptyToolResult(text) {
728
+ if (text == null) return false;
729
+ const s = String(text).trim();
730
+ if (isEmptyExecOutput(s)) return true;
731
+ const cmd = /^\(command output\)\s*([\s\S]*)$/.exec(s);
732
+ if (cmd) return isEmptyExecOutput(cmd[1]);
733
+ const dir = /^\(directive result\)\s*([\s\S]*)$/.exec(s);
734
+ if (dir) return isEmptyDirectiveAck(dir[1]);
735
+ return false;
736
+ }
737
+ function isEmptyShownRun(text) {
738
+ const shown = /^\$ [^\n]*\n([\s\S]*)$/.exec(String(text ?? ''));
739
+ return Boolean(shown && isEmptyExecOutput(shown[1]));
740
+ }
712
741
  // Park only: ask mode, pendingRun, DONE:, 402/empty-wallet, or the hard cap.
713
- function shouldKeepAuto(t, reply) {
742
+ // Empty /(no output) exec is not DONE — keep going with AUTO_EMPTY_RETRY.
743
+ function shouldKeepAuto(t, reply, userText) {
714
744
  if (!t || t.runMode !== 'auto') return false;
715
745
  if (t.pendingRun) return false;
716
746
  if ((t.autoSteps || 0) >= AUTO_MAX_STEPS) return false;
717
- if (isDoneReply(reply)) return false;
718
747
  if (isPaymentFailed(reply)) return false;
748
+ if (isEmptyToolResult(userText) || isEmptyShownRun(reply)) return true;
749
+ if (isDoneReply(reply)) return false;
719
750
  return true;
720
751
  }
721
752
  function enqueueAutoHop(t, threadId, userText, onEvent) {
@@ -725,7 +756,8 @@ function enqueueAutoHop(t, threadId, userText, onEvent) {
725
756
  kickTurn(threadId, userText, onEvent).catch(() => {});
726
757
  return true;
727
758
  }
728
- function autoHopText(reply) {
759
+ function autoHopText(reply, userText) {
760
+ if (isEmptyToolResult(userText) || isEmptyShownRun(reply)) return AUTO_EMPTY_RETRY;
729
761
  return isTransientModelFail(reply) ? AUTO_RACE_RETRY : AUTO_CONTINUE;
730
762
  }
731
763
  // PING used to be a read: last-line status, no turn. Idle children stayed idle
@@ -805,7 +837,8 @@ without a fact only the user has.
805
837
 
806
838
  When the job is actually finished, emit DONE: as the first line. A status
807
839
  sentence is not a stop — the harness keeps this thread working until DONE:,
808
- a real blocking question, or the step cap.`;
840
+ a real blocking question, or the step cap. Empty output, "(no output)", and
841
+ GLOB/GREP with no matches are not finished — try a different command or path.`;
809
842
  async function bindThread(t) {
810
843
  // Only bind what's NEW since the last successful bind, continuing the
811
844
  // existing context_id — previously this rebuilt and re-sent the WHOLE
@@ -992,7 +1025,7 @@ function execCommand(command, cwd) {
992
1025
  exec(command, { cwd, shell: RUN_SHELL, timeout: RUN_TIMEOUT_MS, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
993
1026
  let out = (stdout || '') + (stderr ? '\n' + stderr : '');
994
1027
  if (err) out += `\n(exit ${err.code ?? 1})`;
995
- resolve(keepWhole(out) || '(no output)');
1028
+ resolve(keepWhole(out).trim() || '(no output)');
996
1029
  });
997
1030
  });
998
1031
  }
@@ -1411,7 +1444,7 @@ setInterval(() => {
1411
1444
  const lastBot = [...(t.history || [])].reverse().find((h) => h.who === 'bot');
1412
1445
  const lastText = lastBot?.text || '';
1413
1446
  if (shouldKeepAuto(t, lastText)) {
1414
- kickTurn(t.id, isTransientModelFail(lastText) ? AUTO_RACE_RETRY : AUTO_CONTINUE).catch(() => {});
1447
+ kickTurn(t.id, autoHopText(lastText)).catch(() => {});
1415
1448
  } else {
1416
1449
  t.status = 'idle';
1417
1450
  t.liveStatus = '';
@@ -1676,7 +1709,9 @@ function isHarnessUserText(text) {
1676
1709
  return /^\((command output|directive result)\)/.test(String(text || ''))
1677
1710
  || String(text || '') === NUDGE
1678
1711
  || String(text || '') === AUTO_CONTINUE
1679
- || String(text || '') === AUTO_RACE_RETRY;
1712
+ || String(text || '') === AUTO_RACE_RETRY
1713
+ || String(text || '') === AUTO_EMPTY_RETRY
1714
+ || String(text || '').startsWith('AUTO_EMPTY_RETRY:');
1680
1715
  }
1681
1716
 
1682
1717
  function firstUserAsk(t) {
@@ -2502,8 +2537,8 @@ async function runTurn(threadId, userText, onEvent, images) {
2502
2537
  }
2503
2538
  bindThread(t).catch(() => {});
2504
2539
  lastReply = memberReply;
2505
- if (shouldKeepAuto(t, memberReply)) {
2506
- chained = enqueueAutoHop(t, threadId, autoHopText(memberReply), onEvent);
2540
+ if (shouldKeepAuto(t, memberReply, userText)) {
2541
+ chained = enqueueAutoHop(t, threadId, autoHopText(memberReply, userText), onEvent);
2507
2542
  }
2508
2543
  return;
2509
2544
  }
@@ -2638,7 +2673,11 @@ async function runTurn(threadId, userText, onEvent, images) {
2638
2673
  // most material (command output, GLOB results, MCP tool lists). The
2639
2674
  // holographic context stopped growing precisely when it mattered.
2640
2675
  lastReply = shown;
2641
- chained = enqueueAutoHop(t, threadId, condense('(command output)', output), onEvent);
2676
+ chained = enqueueAutoHop(
2677
+ t, threadId,
2678
+ isEmptyExecOutput(output) ? AUTO_EMPTY_RETRY : condense('(command output)', output),
2679
+ onEvent,
2680
+ );
2642
2681
  return;
2643
2682
  }
2644
2683
  {
@@ -2668,8 +2707,12 @@ async function runTurn(threadId, userText, onEvent, images) {
2668
2707
  // Same budget as RUN (shared t.autoSteps, reset when the user speaks), so
2669
2708
  // this cannot spend more than a chained RUN loop already could.
2670
2709
  if (t.runMode === 'auto' && ack !== null && ack !== undefined
2671
- && !isDoneReply(reply)) {
2672
- chained = enqueueAutoHop(t, threadId, condense('(directive result)', ack), onEvent);
2710
+ && (isEmptyDirectiveAck(ack) || !isDoneReply(reply))) {
2711
+ chained = enqueueAutoHop(
2712
+ t, threadId,
2713
+ isEmptyDirectiveAck(ack) ? AUTO_EMPTY_RETRY : condense('(directive result)', ack),
2714
+ onEvent,
2715
+ );
2673
2716
  return;
2674
2717
  }
2675
2718
 
@@ -2699,17 +2742,18 @@ async function runTurn(threadId, userText, onEvent, images) {
2699
2742
 
2700
2743
  // After any auto reply that is not DONE: and not waiting on approval,
2701
2744
  // kick immediately. Race/empty/error uses AUTO_RACE_RETRY.
2702
- if (shouldKeepAuto(t, reply)) {
2703
- chained = enqueueAutoHop(t, threadId, autoHopText(reply), onEvent);
2745
+ if (shouldKeepAuto(t, reply, userText)) {
2746
+ chained = enqueueAutoHop(t, threadId, autoHopText(reply, userText), onEvent);
2704
2747
  return;
2705
2748
  }
2706
2749
  bindThread(t).catch(() => {});
2707
2750
  } finally {
2708
2751
  // Idle only when this hop should not keep AUTO going: DONE:, pendingRun,
2709
- // ask mode, 402/empty-wallet, or the hard cap. Otherwise kick again.
2752
+ // ask mode, 402/empty-wallet, or the hard cap. Empty /(no output) is not
2753
+ // DONE — AUTO_EMPTY_RETRY. Otherwise kick again.
2710
2754
  if (stillMine() && !chained && !parked) {
2711
- if (shouldKeepAuto(t, lastReply)) {
2712
- enqueueAutoHop(t, threadId, autoHopText(lastReply), onEvent);
2755
+ if (shouldKeepAuto(t, lastReply, userText)) {
2756
+ enqueueAutoHop(t, threadId, autoHopText(lastReply, userText), onEvent);
2713
2757
  } else if (!t.pendingRun) {
2714
2758
  t.status = 'idle';
2715
2759
  t.liveStatus = '';
@@ -4973,12 +5017,12 @@ const APP_HTML = `<!doctype html>
4973
5017
  // is shipping the WHOLE corpus), so 2dp would read as noise up there.
4974
5018
  savedEl.textContent = (mult >= 100 ? Math.round(mult) : mult.toFixed(mult >= 10 ? 1 : 2)) + 'x';
4975
5019
  savedEl.className = mult >= 1 ? 'hlime' : 'hember';
4976
- // Session direct/spent (never first-call). Cogs-over-paid is the
4977
- // louder warn unused grant-back should have kept used cogs ≤ billed.
5020
+ // Session direct/spent (never first-call). Ember when cogs > spent —
5021
+ // house losing. Do not treat race_unused as a user refund.
4978
5022
  if (cogsOver) {
4979
5023
  hintEl.className = 'hhint show';
4980
- hintEl.innerHTML = '<b>cogs above paid.</b> our cost exceeded what you were billed '
4981
- + ' this line is used racers, not the N+judge ceiling.';
5024
+ hintEl.innerHTML = '<b>cogs above paid.</b> house is losing — our cost exceeded what you were billed. '
5025
+ + 'you pay for every entrant we actually launched; failures still cost us.';
4982
5026
  } else {
4983
5027
  hintEl.className = mult >= 1 ? 'hhint' : 'hhint show';
4984
5028
  hintEl.innerHTML = '<b>feed it more.</b> you\\'re billed on the slice actually sent, '
@@ -5423,8 +5467,8 @@ export {
5423
5467
  tryDirective, ensureWorkspacePort, isPreviewableRel, previewAck, workspaceFileUrl,
5424
5468
  parseRun, looksLikeMcpAsBash, stripThinkTags, safeResolveIn, inDir, listDir,
5425
5469
  handleSlash, newThread, setRunTurnForTest, setBrainAskForTest, runTurn,
5426
- AUTO_CONTINUE, AUTO_RACE_RETRY, pingWakeText, pingCanWake, shouldKeepAuto,
5427
- isDoneReply, isTransientModelFail, enqueueAutoHop, childKickoff, findByName,
5470
+ AUTO_CONTINUE, AUTO_RACE_RETRY, AUTO_EMPTY_RETRY, pingWakeText, pingCanWake, shouldKeepAuto,
5471
+ isDoneReply, isTransientModelFail, isEmptyToolResult, enqueueAutoHop, childKickoff, findByName,
5428
5472
  attachChildDir, finishChildDir,
5429
5473
  lockWorktree, unlockWorktree, parsePrRef, fetchSpecsForOrigin, agentSlug,
5430
5474
  };
package/lib/podagent.mjs CHANGED
@@ -936,6 +936,7 @@ async function readGatewayRaceStream(r, feed, signal) {
936
936
  *
937
937
  * Every entrant is paid for, including the abandoned one — this trades money
938
938
  * for latency and quality, which is why it is opt-in and capped.
939
+ * grokui does not grant unused or failed racers back to the user.
939
940
  *
940
941
  * `hooks` is for tests: `{ stream, classify, pairwise, minScore }`.
941
942
  */
package/lib/racesettle.js CHANGED
@@ -1,6 +1,9 @@
1
1
  /**
2
- * Fly gateway race: one POST { race, race_need, tier }, unused grant-back,
3
- * and HUD cogs for the racers that actually ran never the N+judge ceiling.
2
+ * Fly gateway race: one POST { race, race_need, tier }.
3
+ * User pays for every entrant we actually launched. Failures still cost us
4
+ * (OpenRouter was paid). race_unused on a receipt is informational — do not
5
+ * treat it as a user refund or shrink HUD cogs to hide a house loss.
6
+ * HUD embers when cogs > spent.
4
7
  */
5
8
 
6
9
  export const FLY_GATEWAY_HOST = 'x402-tokens.fly.dev';
@@ -92,40 +95,23 @@ export function capRaceByCredit(n, { creditUsd, quoteUsd } = {}) {
92
95
  return { n: Math.min(want, maxN), reason: 'shrunk' };
93
96
  }
94
97
 
95
- function unusedGrant(x) {
96
- const u = x?.race_unused ?? x?.raceUnused ?? x?.unused;
97
- if (u == null) return { billed: 0, cogs: 0 };
98
- if (typeof u === 'number' && Number.isFinite(u)) return { billed: u, cogs: u };
99
- if (typeof u !== 'object') return { billed: 0, cogs: 0 };
100
- const billed = Number(u.billedUsd ?? u.refundUsd ?? u.unusedBilledUsd ?? u.usd ?? 0);
101
- const cogs = Number(u.cogsUsd ?? u.unusedCogsUsd ?? u.refundCogsUsd ?? billed);
102
- return {
103
- billed: Number.isFinite(billed) && billed > 0 ? billed : 0,
104
- cogs: Number.isFinite(cogs) && cogs > 0 ? cogs : 0,
105
- };
106
- }
107
-
108
98
  /**
109
- * Actual used-racer cogs after unused grant-back.
110
- * Never the N+judge ceiling. Does not clamp to billed HUD embers when
111
- * used cogs still exceed what was paid.
99
+ * House cost from the receipt. Do not subtract race_unused — unused
100
+ * grant-back is not a user refund, and shrinking cogs would hide house loss.
101
+ * Does not clamp to billed — HUD embers when cogs exceed what was paid.
112
102
  */
113
103
  export function receiptUsedCogs(x, markup = 3) {
114
104
  if (!x || typeof x !== 'object') return 0;
115
105
  const billedRaw = Number(x.billedUsd);
116
106
  const billedOk = Number.isFinite(billedRaw) && billedRaw >= 0;
117
- let cogs = typeof x.cogsUsd === 'number' && Number.isFinite(x.cogsUsd)
118
- ? x.cogsUsd
119
- : (billedOk ? billedRaw / markup : 0);
120
- const grant = unusedGrant(x);
121
- if (grant.cogs > 0) cogs = Math.max(0, cogs - grant.cogs);
122
- return cogs;
107
+ if (typeof x.cogsUsd === 'number' && Number.isFinite(x.cogsUsd)) return x.cogsUsd;
108
+ return billedOk ? billedRaw / markup : 0;
123
109
  }
124
110
 
125
111
  /**
126
- * Session meter. spent/direct are the receipt totals (already net of unused
127
- * when the gateway refunds into billedUsd) — never a first-call rewrite.
128
- * cogs is used racers after unused grant-back.
112
+ * Session meter. spent/direct are the receipt totals as billed never a
113
+ * first-call rewrite, never a race_unused user refund.
114
+ * cogs is the house cost on that receipt (HUD embers when cogs > spent).
129
115
  */
130
116
  export function meterRaceReceipt(x, markup = 3) {
131
117
  const billed = Number(x?.billedUsd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.49.2",
3
+ "version": "0.49.3",
4
4
  "description": "Local x402-paying proxy + MCP server for openzoo.fun \u2014 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",