openzoo 0.48.32 → 0.48.34

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.
Files changed (2) hide show
  1. package/lib/proxy.js +46 -2
  2. package/package.json +1 -1
package/lib/proxy.js CHANGED
@@ -300,9 +300,31 @@ async function spillTranscript(body, log) {
300
300
  const firstSpillable = msgs.findIndex((m) => m?.role !== 'system');
301
301
  if (firstSpillable < 0) return null;
302
302
 
303
+ // A SEVERABLE BOUNDARY IS NOT ONLY A `user` TURN.
304
+ //
305
+ // That was the whole bug: in a Claude Code agent loop the human speaks ONCE
306
+ // and everything after is assistant->tool pairs, so a rule that only cuts at
307
+ // role:"user" finds nothing and the spill returns null on EVERY turn.
308
+ // MEASURED against the live proxy log: 104 real claude-cli requests, zero
309
+ // spills, while every synthetic transcript I tested spilled fine because I
310
+ // had written user turns into it.
311
+ //
312
+ // What actually matters is that no assistant `tool_calls` is left unanswered
313
+ // across the cut. So any message is a legal boundary when the one BEFORE it
314
+ // is not an unanswered tool call — i.e. the previous message is a plain
315
+ // assistant/user/tool with every call already resolved.
316
+ const severable = (i) => {
317
+ if (i <= firstSpillable || i >= msgs.length) return false;
318
+ const prev = msgs[i - 1];
319
+ if (!prev) return false;
320
+ // an assistant that issued tool_calls must be followed by its tool results
321
+ if (prev.role === 'assistant' && Array.isArray(prev.tool_calls) && prev.tool_calls.length) return false;
322
+ // never split a tool_call/tool_result run in the middle
323
+ return msgs[i].role !== 'tool';
324
+ };
303
325
  let cut = -1;
304
326
  for (let i = msgs.length - keepTail; i > firstSpillable; i--) {
305
- if (msgs[i]?.role === 'user') { cut = i; break; }
327
+ if (severable(i)) { cut = i; break; }
306
328
  }
307
329
  // FALL BACK TO THE LAST SEVERABLE TURN. The keepTail window is a preference,
308
330
  // not a requirement: a transcript can be enormous and still have very few
@@ -312,11 +334,33 @@ async function spillTranscript(body, log) {
312
334
  // final turn; anything earlier that is severable is better than not spilling.
313
335
  if (cut <= firstSpillable) {
314
336
  for (let i = msgs.length - 2; i > firstSpillable; i--) {
315
- if (msgs[i]?.role === 'user') { cut = i; break; }
337
+ if (severable(i)) { cut = i; break; }
316
338
  }
317
339
  }
318
340
  if (cut <= firstSpillable) return null; // nothing safely severable
319
341
 
342
+ // TRIM THE TAIL BY BYTES, NOT MESSAGE COUNT.
343
+ //
344
+ // MEASURED on the live session: 9 kept turns of Claude Code tool output made
345
+ // promptTokens swamp the counterfactual and the call scored 1.00x, while the
346
+ // SAME bound context with a one-line ask scored 8.53x. Nine messages is a
347
+ // trivial number and an enormous payload — a single Read or grep result is
348
+ // tens of KB — so counting messages measures the wrong thing entirely.
349
+ //
350
+ // Walk backwards from the newest and stop at a byte budget. The newest turns
351
+ // are the ones the model actually needs verbatim; everything older is already
352
+ // in the bound corpus and comes back through recall.
353
+ let tailStart = cut;
354
+ {
355
+ const budget = Number(process.env.OPENZOO_TAIL_MAX_CHARS || 24000);
356
+ let used = 0;
357
+ for (let i = msgs.length - 1; i >= cut; i--) {
358
+ used += msgText(msgs[i]).length;
359
+ if (used > budget && severable(i)) { tailStart = i; break; }
360
+ }
361
+ }
362
+ if (tailStart > cut) cut = tailStart;
363
+
320
364
  const head = msgs.slice(0, firstSpillable); // system block, always kept
321
365
  const corpus = msgs.slice(firstSpillable, cut).map(msgText).filter(Boolean).join('\n\n');
322
366
  if (corpus.length <= BIND_MIN_CHARS) return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.48.32",
3
+ "version": "0.48.34",
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",