openzoo 0.48.54 → 0.48.56

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/brief.js CHANGED
@@ -54,8 +54,21 @@ export function injectBrief(body, selfUrl = null) {
54
54
  if (msgs.some((m) => typeof m?.content === 'string' && m.content.includes('connected through an openzoo proxy'))) return null;
55
55
 
56
56
  const brief = { role: 'system', content: briefFor(selfUrl) };
57
- const lastSystem = msgs.reduce((acc, m, i) => (m?.role === 'system' ? i : acc), -1);
57
+ // THE LEADING SYSTEM RUN ONLY NOT THE LAST SYSTEM ANYWHERE.
58
+ //
59
+ // This used to reduce over the WHOLE array for the last `role === 'system'`,
60
+ // which is the same thing only while every system message sits at the front.
61
+ // The moment anything injects one later, the brief is spliced in AFTER the
62
+ // user's turn, and the conversation the model receives ends with operator
63
+ // notes instead of a question.
64
+ //
65
+ // OBSERVED live: forwarded tail `s a t a t a u s a u s s` — two system
66
+ // messages after the last user message. The agent replied "I don't see an
67
+ // explicit question or task for this turn", answered the PREVIOUS turn, and
68
+ // read as one message behind for an entire session.
69
+ let lead = -1;
70
+ while (lead + 1 < msgs.length && msgs[lead + 1]?.role === 'system') lead += 1;
58
71
  const out = [...msgs];
59
- out.splice(lastSystem + 1, 0, brief); // after any leading system block, before the user turns
72
+ out.splice(lead + 1, 0, brief); // after the leading system block, before any turn
60
73
  return { ...body, messages: out };
61
74
  }
package/lib/info.js CHANGED
@@ -38,11 +38,23 @@ async function quotedPrices() {
38
38
  try {
39
39
  // Imported here, not at module scope, matching affordableUsd below — this
40
40
  // file is loaded by `openzoo address`, which must work with no network.
41
- const { withNamespace } = await import('./namespace.js');
42
- const r = await fetch(`${config.apiBase}/v1/credits/topup`, {
41
+ // PRICE OFF THE CHAT 402, NOT THE CREDIT 402.
42
+ //
43
+ // /v1/credits/topup sells a USD-denominated product, so every rail in its
44
+ // challenge quotes tokenUsd = 1 — a dollar of credit costs a dollar,
45
+ // whichever asset pays for it. Reading unit prices from there valued every
46
+ // holding at $1: MEASURED, a wallet of 776,302 TOKEN (actually worth ~$178 at the
47
+ // chat 402's 0.00022906) printed "$776302.53", and 1,985 ROBINHOODS worth
48
+ // ~$0.01 printed "$1985.78". Total "≈ $778288.41" for roughly $180 of
49
+ // assets — and `openzoo topup all` then tried to buy $758,819 of credit off
50
+ // that number.
51
+ //
52
+ // The chat challenge prices each asset at its real spot (DexScreener), and
53
+ // needs no namespace signature, so it is both correct and simpler.
54
+ const r = await fetch(`${config.apiBase}/v1/chat/completions`, {
43
55
  method: 'POST',
44
- headers: withNamespace({ 'content-type': 'application/json' }),
45
- body: JSON.stringify({ usd: 1 }),
56
+ headers: { 'content-type': 'application/json' },
57
+ body: JSON.stringify({ model: config.defaultModel || 'anthropic/claude-sonnet-5', max_tokens: 1, messages: [{ role: 'user', content: 'x' }] }),
46
58
  });
47
59
  if (r.status !== 402) return out;
48
60
  const ch = await r.json().catch(() => ({}));
@@ -180,15 +192,32 @@ export async function affordableUsd() {
180
192
  export async function topUp(usdArg) {
181
193
  // "all" spends everything the wallet can cover, minus a small margin so a
182
194
  // price tick between quote and settle does not fail the payment outright.
195
+ // A BARE `openzoo topup` IS NOT `all`.
196
+ //
197
+ // It used to be, and the result was alarming: with a TOKEN-heavy wallet the
198
+ // no-arg form printed "wallet covers ~$782288.39 — buying $758819.73" and only
199
+ // THEN hit the 1-500 clamp and threw. Nothing was ever spent, but a user
200
+ // reading their terminal has every reason to think a three-quarter-million
201
+ // dollar purchase just started. A command with no argument prints usage and
202
+ // touches no wallet.
203
+ if (usdArg === undefined || usdArg === null || String(usdArg).trim() === '') {
204
+ throw new Error('usage: openzoo topup <usd|all> (1-500)');
205
+ }
206
+ const MAX_TOPUP = 500;
183
207
  let usd = Number(usdArg);
184
- if (String(usdArg).toLowerCase() === 'all' || usdArg === undefined) {
208
+ if (String(usdArg).toLowerCase() === 'all') {
185
209
  const max = await affordableUsd();
186
- usd = Math.floor(max * 0.97 * 100) / 100;
210
+ // CLAMP TO THE CEILING THE VALIDATOR ENFORCES. Without this, "all" on any
211
+ // wallet worth more than ~$515 computes a number the very next line
212
+ // rejects, so the feature was unusable for exactly the wallets it was for.
213
+ usd = Math.min(Math.floor(max * 0.97 * 100) / 100, MAX_TOPUP);
187
214
  if (!(usd >= 1)) throw new Error(`wallet covers only $${max.toFixed(4)} of credit — fund it first (openzoo balance)`);
188
- console.log(`wallet covers ~$${max.toFixed(2)} buying $${usd.toFixed(2)}`);
215
+ console.log(max > MAX_TOPUP
216
+ ? `wallet covers ~$${max.toFixed(2)} — buying $${usd.toFixed(2)} (per-topup max is $${MAX_TOPUP}; run it again for more)`
217
+ : `wallet covers ~$${max.toFixed(2)} — buying $${usd.toFixed(2)}`);
189
218
  }
190
- if (!Number.isFinite(usd) || usd < 1 || usd > 500) {
191
- throw new Error('usage: openzoo topup <usd|all> (1-500)');
219
+ if (!Number.isFinite(usd) || usd < 1 || usd > MAX_TOPUP) {
220
+ throw new Error(`usage: openzoo topup <usd|all> (1-${MAX_TOPUP})`);
192
221
  }
193
222
  const { PayClient } = await import('./pay.js');
194
223
  const client = new PayClient();
package/lib/proxy.js CHANGED
@@ -510,6 +510,13 @@ async function spillTranscript(body, log, req) {
510
510
  // NAME THE KEY. A memo keyed on the wrong thing fails silently — it just
511
511
  // re-binds forever and collides sessions — so the log says which key was used.
512
512
  const keyKind = sessionId ? `sid ${String(sessionId).slice(0, 8)}` : 'content-anchor';
513
+ // WHAT ACTUALLY REACHES THE MODEL. Inference about this cut has been wrong
514
+ // twice; the roles of the forwarded tail settle it in one line.
515
+ if (process.env.OPENZOO_LOG_TAIL === '1') {
516
+ const roles = msgs.slice(cut).map((m) => (m.role || '?')[0]).join('');
517
+ const lastU = msgs.slice(cut).some((m) => m.role === 'user' && msgText(m).trim());
518
+ log(` tail roles=${roles} firstSpillable=${firstSpillable} cut=${cut} hasUserText=${lastU}`);
519
+ }
513
520
  log(bind.reused
514
521
  ? `transcript prefix already bound (${bind.contextId}, ${keyKind}) — sending ${sent}/${msgs.length} turns`
515
522
  : `transcript prefix bound (${mb(bind.bytes)}MB → ${bind.contextId}, ${keyKind}) — sending ${sent}/${msgs.length} turns`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.48.54",
3
+ "version": "0.48.56",
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",