openzoo 0.48.76 → 0.48.78

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/spill.js +71 -7
  2. package/package.json +2 -2
package/lib/spill.js CHANGED
@@ -495,6 +495,58 @@ export function looksLikeFileView(command) {
495
495
  /** Tool result larger than this is "fat" — stub it in the forwarded tail. */
496
496
  export const FAT_TOOL_CHARS = 400;
497
497
 
498
+ /**
499
+ * Bound-file matching used to stub any size, including a 461-byte Read the
500
+ * model had just fetched. Stay under this floor unless we are over budget
501
+ * *and* the result is not this turn's current tool round.
502
+ */
503
+ export const KEEP_TOOL_CHARS = 4096;
504
+
505
+ function toolCallIds(m) {
506
+ if (!m || typeof m !== 'object') return [];
507
+ const ids = [];
508
+ if (Array.isArray(m.tool_calls)) {
509
+ for (const c of m.tool_calls) {
510
+ const id = c?.id || c?.tool_call_id;
511
+ if (id) ids.push(id);
512
+ }
513
+ }
514
+ if (m.function_call) {
515
+ const id = m.function_call.id || m.function_call.tool_call_id;
516
+ if (id) ids.push(id);
517
+ }
518
+ if (Array.isArray(m.content)) {
519
+ for (const b of m.content) {
520
+ if (b?.type === 'tool_use' && b.id) ids.push(b.id);
521
+ }
522
+ }
523
+ return ids;
524
+ }
525
+
526
+ /**
527
+ * Last assistant that issued tool_calls / tool_use, plus those ids.
528
+ *
529
+ * Claude Code / OpenAI put *this turn's* Read/Bash/WebSearch results in
530
+ * `role:tool` (or tool_result blocks) AFTER the last user ask. Those are
531
+ * in-flight — the model's current eyes — and must never be stubbed, even
532
+ * when large. A completed round (tools, then a follow-up ask) may still
533
+ * be stubbed; that is the 0.48.76 storm savings. The last round's small
534
+ * bodies still use KEEP_TOOL_CHARS so a 461-byte Read survives.
535
+ */
536
+ export function currentToolRound(msgs, { lastUser = -1 } = {}) {
537
+ let index = -1;
538
+ const ids = new Set();
539
+ if (!Array.isArray(msgs)) return { index, ids, inFlight: false };
540
+ for (let i = 0; i < msgs.length; i++) {
541
+ const here = toolCallIds(msgs[i]);
542
+ if (!here.length) continue;
543
+ index = i;
544
+ ids.clear();
545
+ for (const id of here) ids.add(id);
546
+ }
547
+ return { index, ids, inFlight: index >= 0 && index > lastUser };
548
+ }
549
+
498
550
  export function fileBoundStub(paths, n) {
499
551
  const list = [...new Set((paths || []).filter(Boolean))].join(' ');
500
552
  const mark = Number.isFinite(n) && n > 0 ? `[bound, ${n} chars]` : '[bound]';
@@ -586,7 +638,12 @@ function resolveBoundPath(raw, cwd, boundAbs) {
586
638
  *
587
639
  * `fromIndex` limits the rewrite to the forwarded tail so the spilled prefix
588
640
  * that becomes the conversation corpus is unchanged. The last user ask is
589
- * never rewritten.
641
+ * never rewritten. The current tool round — last assistant with tool_calls
642
+ * / tool_use and every matching role:tool / tool_result — is never rewritten
643
+ * when that assistant sits after the last user ask (this turn's eyes).
644
+ * Bodies under KEEP_TOOL_CHARS on that last round also stay, even when the
645
+ * ask follows the results, so a 461-byte Read is not replaced with
646
+ * `[bound, 461 chars]`.
590
647
  */
591
648
  export function stubBoundFileResults(msgs, {
592
649
  boundFiles,
@@ -659,15 +716,19 @@ export function stubBoundFileResults(msgs, {
659
716
  const firstSpillable = firstSpillableIndex(msgs);
660
717
  let lastUser = lastUserAskIndex(msgs, firstSpillable);
661
718
  const fatLimit = Number.isFinite(Number(fatChars)) ? Number(fatChars) : FAT_TOOL_CHARS;
719
+ const keepFloor = KEEP_TOOL_CHARS;
662
720
  const slimArgs = aggressive || wantBudget;
721
+ let round = currentToolRound(msgs, { lastUser });
663
722
 
664
723
  const stubFor = (id, n) => {
665
724
  const paths = idPaths.get(id);
666
725
  return paths?.length ? fileBoundStub(paths, n) : toolResultStub(n);
667
726
  };
668
727
 
669
- const shouldStubBody = (id, n) => {
728
+ const shouldStubBody = (id, n, { overBudget = false } = {}) => {
670
729
  if (!n || isStubText(typeof n === 'number' ? '' : n)) return false;
730
+ if (id && round.inFlight && round.ids.has(id)) return false;
731
+ if (n < keepFloor && (!overBudget || (id && round.ids.has(id)))) return false;
671
732
  if (stubIds.has(id)) return true;
672
733
  if (slimArgs && n >= fatLimit) return true;
673
734
  return false;
@@ -686,8 +747,11 @@ export function stubBoundFileResults(msgs, {
686
747
  firstSpillable,
687
748
  });
688
749
  lastUser = lastUserAskIndex(messages, firstSpillableIndex(messages));
750
+ round = currentToolRound(messages, { lastUser });
689
751
  }
690
752
 
753
+ const skipSlimCalls = (i) => round.inFlight && i === round.index;
754
+
691
755
  messages = messages.map((m, i) => {
692
756
  if (i < fromIndex || !m || i === lastUser) return m;
693
757
  if (m.role === 'tool') {
@@ -698,7 +762,7 @@ export function stubBoundFileResults(msgs, {
698
762
  return { ...m, content: stubFor(m.tool_call_id, n) };
699
763
  }
700
764
  let next = m;
701
- if (Array.isArray(m.tool_calls) && slimArgs) {
765
+ if (Array.isArray(m.tool_calls) && slimArgs && !skipSlimCalls(i)) {
702
766
  let changed = false;
703
767
  const calls = m.tool_calls.map((tc) => {
704
768
  const n = toolCallArgLength(tc);
@@ -711,7 +775,7 @@ export function stubBoundFileResults(msgs, {
711
775
  });
712
776
  if (changed) next = { ...next, tool_calls: calls };
713
777
  }
714
- if (next.function_call && slimArgs) {
778
+ if (next.function_call && slimArgs && !skipSlimCalls(i)) {
715
779
  const n = toolCallArgLength(next.function_call);
716
780
  const raw = next.function_call.arguments;
717
781
  if (n >= fatLimit && !isStubText(typeof raw === 'string' ? raw : '')) {
@@ -751,7 +815,7 @@ export function stubBoundFileResults(msgs, {
751
815
  if (m.role === 'tool') {
752
816
  if (isStubText(m.content)) continue;
753
817
  const n = toolContentLength(m.content);
754
- if (!n) continue;
818
+ if (!shouldStubBody(m.tool_call_id, n, { overBudget: true })) continue;
755
819
  const stub = stubFor(m.tool_call_id, n);
756
820
  if (stub.length >= n) continue;
757
821
  used = used - n + stub.length;
@@ -760,7 +824,7 @@ export function stubBoundFileResults(msgs, {
760
824
  dropped += n;
761
825
  continue;
762
826
  }
763
- if (Array.isArray(m.tool_calls)) {
827
+ if (Array.isArray(m.tool_calls) && !skipSlimCalls(i)) {
764
828
  let changed = false;
765
829
  const calls = m.tool_calls.map((tc) => {
766
830
  if (used <= cap) return tc;
@@ -784,7 +848,7 @@ export function stubBoundFileResults(msgs, {
784
848
  const blocks = cur.content.map((b) => {
785
849
  if (b?.type !== 'tool_result' || used <= cap || isStubText(b.content)) return b;
786
850
  const n = toolContentLength(b.content);
787
- if (!n) return b;
851
+ if (!shouldStubBody(b.tool_use_id, n, { overBudget: true })) return b;
788
852
  const stub = stubFor(b.tool_use_id, n);
789
853
  if (stub.length >= n) return b;
790
854
  used = used - n + stub.length;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.48.76",
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.",
3
+ "version": "0.48.78",
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",
7
7
  "bin": {