openzoo 0.50.60 → 0.50.62

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.
@@ -2718,6 +2718,8 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
2718
2718
  } catch (e) {
2719
2719
  lastErr = e;
2720
2720
  log(`cursor-backend: zoo POST attempt=${attempt} ${e.message}`);
2721
+ // A superseded turn must not spend four more retries on a dead signal.
2722
+ if (opts.signal?.aborted || isSupersededError(e)) throw e;
2721
2723
  if (attempt === tries) throw e;
2722
2724
  const abortish = /abort|timeout/i.test(String(e?.name || '') + String(e?.message || ''));
2723
2725
  await new Promise((ok) => setTimeout(ok, (abortish ? 2000 : 800) * attempt));
@@ -2942,6 +2944,30 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
2942
2944
  }
2943
2945
  if (!path0.startsWith('/api/')) return false;
2944
2946
  const name = path0.slice('/api/'.length);
2947
+ // Revive: the minutely cron (scripts/revive-bots.sh) calls this after it has
2948
+ // made sure the hijack + app are alive. Every non-group bot gets a wakeup
2949
+ // timer if it lost one; nothing is spawned, nothing already armed is touched.
2950
+ if (name === 'ozRevive') {
2951
+ let parsed = {};
2952
+ try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
2953
+ const every = String(parsed.every || '5m');
2954
+ const wakes = readWakeups(HOME);
2955
+ const list = (cachedAgentList() || []).filter((a) => a?.id && !a.isGroup);
2956
+ const armed = [];
2957
+ const already = [];
2958
+ for (const a of list) {
2959
+ if (wakes[a.id]) { already.push(a.id); continue; }
2960
+ try {
2961
+ scheduleAgentWakeup(a.id, { every, prompt: parsed.prompt });
2962
+ armed.push({ id: a.id, name: a.name });
2963
+ } catch (e) {
2964
+ log(`cursor-backend: ozRevive ${a.id} ${e.message}`);
2965
+ }
2966
+ }
2967
+ log(`cursor-backend: ozRevive total=${list.length} armed=${armed.length} already=${already.length} every=${every}`);
2968
+ jsonSend(res, { ok: true, total: list.length, armed, already: already.length, every });
2969
+ return true;
2970
+ }
2945
2971
  if (name === 'getHostStatus') {
2946
2972
  jsonSend(res, { status: 'ready', ready: true, state: 'ready', hostStatus: 'ready' });
2947
2973
  log('cursor-backend: -> getHostStatus ready');
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @openzoobot public receipt. X collapses single newlines into one
3
+ * paragraph; join with a blank line so PAID / WOULDA / tx stay stacked.
4
+ * `(this call) openzoo.fun` stays on the PAID line — never split.
5
+ */
6
+
7
+ const HARD = '\n\n';
8
+
9
+ export function receiptLines({
10
+ paid = '0.004',
11
+ woulda = '0.03',
12
+ wouldaLabel = 'grok.com / xAI API',
13
+ tx = '',
14
+ model = 'grok-4.6 @ openzoo',
15
+ } = {}) {
16
+ const paidStr = String(paid).replace(/^\$/, '');
17
+ const wouldaStr = String(woulda).replace(/^\$/, '');
18
+ const lines = [
19
+ model,
20
+ `**PAID** $${paidStr} x402 (this call) openzoo.fun`,
21
+ `**WOULDA** $${wouldaStr} ${wouldaLabel}`,
22
+ ];
23
+ if (tx) lines.push(`tx ${tx}`);
24
+ return lines;
25
+ }
26
+
27
+ export function receiptFooter(opts = {}) {
28
+ return receiptLines(opts).join(HARD);
29
+ }
30
+
31
+ /** Same body without markdown, for the X compose box. */
32
+ export function xTweetReceipt(opts = {}) {
33
+ return receiptLines(opts)
34
+ .map((line) => line.replace(/\*\*/g, ''))
35
+ .join(HARD);
36
+ }
37
+
38
+ /** What X does when it eats extra blank lines but keeps single \n. */
39
+ export function afterXCollapse(text) {
40
+ return String(text).replace(/\n{2,}/g, '\n').trim();
41
+ }
package/lib/proxy.js CHANGED
@@ -304,6 +304,18 @@ function replayPut(key, data, settle) {
304
304
  */
305
305
  export async function startProxy({ silent = false, requireToken = null, sessionMaxUsd = null, autoTunnel = false } = {}) {
306
306
  const client = new PayClient();
307
+ // HOUSE OUTAGE GATE. Measured 2026-09-01: TOKEN payments settled on chain
308
+ // (tx 3rwy5z…) and the gateway still answered 402 because ITS upstream
309
+ // (OpenRouter) was out of credits. Paying again buys nothing, and the
310
+ // "wallet underfunded" copy blamed the user's burner. When the paid 402 says
311
+ // upstream credits, stop paying for a minute and say what is actually wrong.
312
+ let upstreamOutageUntil = 0;
313
+ let upstreamOutageMsg = '';
314
+ const upstreamOutage = (body) => {
315
+ const m = String(body?.error?.message || body?.message || '');
316
+ const src = String(body?.error?.metadata?.limit_source || '');
317
+ return /openrouter_credits/i.test(src) || /insufficient credits/i.test(m);
318
+ };
307
319
  const log = silent ? () => {} : (...a) => console.log(...a);
308
320
  // ALWAYS-ON, BUT NEVER INTO A HARNESS'S TERMINAL. `silent` means another
309
321
  // process (openzoo claude, the editor launcher) owns stdio — printing request
@@ -684,6 +696,11 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
684
696
  }
685
697
 
686
698
  try {
699
+ if (Date.now() < upstreamOutageUntil) {
700
+ log('upstream outage: not paying (gate)');
701
+ jsonErr(res, 503, upstreamOutageMsg);
702
+ return;
703
+ }
687
704
  const result = await client.fetch(url, init);
688
705
  const { response, paid, receipt, accept } = result;
689
706
  if (paid && receipt) {
@@ -786,8 +803,22 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
786
803
  if (response.status === 402) {
787
804
  let quoted = '';
788
805
  let usd;
806
+ let q402 = null;
807
+ try { q402 = await response.clone().json(); } catch { q402 = null; }
808
+ if (paid && upstreamOutage(q402)) {
809
+ const settle = q402?.x402?.settle;
810
+ const tx = settle?.transaction || result.receipt?.tx || '';
811
+ upstreamOutageMsg = 'openzoo gateway upstream is out of credits (OpenRouter: "Insufficient credits"). '
812
+ + `Your payment settled${tx ? ` (tx ${tx})` : ''} — this is NOT your wallet. `
813
+ + 'The gateway operator must top up OpenRouter or route this model to another upstream. '
814
+ + 'Pausing paid retries for 60s.';
815
+ upstreamOutageUntil = Date.now() + 60_000;
816
+ log(`upstream outage: ${upstreamOutageMsg.slice(0, 120)}`);
817
+ jsonErr(res, 503, upstreamOutageMsg);
818
+ return;
819
+ }
789
820
  try {
790
- const q = await response.clone().json();
821
+ const q = q402;
791
822
  usd = Number(q?.accepts?.[0]?.extra?.billedUsd);
792
823
  if (Number.isFinite(usd)) quoted = ` This call needs ≈$${usd.toFixed(4)}.`;
793
824
  } catch { /* body was not the quote after all */ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.50.60",
3
+ "version": "0.50.62",
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",