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.
- package/lib/cursorbackend.js +26 -0
- package/lib/openzoobotReceipt.js +41 -0
- package/lib/proxy.js +32 -1
- package/package.json +1 -1
package/lib/cursorbackend.js
CHANGED
|
@@ -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 =
|
|
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.
|
|
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",
|