openzoo 0.48.78 → 0.48.80

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/proxy.js CHANGED
@@ -610,7 +610,10 @@ async function spillTranscript(body, log, req, stats) {
610
610
  bindFilesInBackground('background', { appendTo: bind.contextId, asAppend: true });
611
611
  spillMemo.set(anchor, { corpus, contextId: bind.contextId, hash: bind.hash });
612
612
  if (spillMemo.size > 32) spillMemo.delete(spillMemo.keys().next().value);
613
- const sent = msgs.length - cut;
613
+ // Count the tail that is actually forwarded after stub/trim � older
614
+ // continue-turn rounds after the ask may have been dropped, so
615
+ // msgs.length - cut would keep lastSend growing with the raw pile.
616
+ const sent = Math.max(0, stubbed.messages.length - cut);
614
617
  // NAME THE KEY. A memo keyed on the wrong thing fails silently — it just
615
618
  // re-binds forever and collides sessions — so the log says which key was used.
616
619
  const keyKind = sessionId ? `sid ${String(sessionId).slice(0, 8)}` : 'content-anchor';
package/lib/spill.js CHANGED
@@ -496,9 +496,10 @@ export function looksLikeFileView(command) {
496
496
  export const FAT_TOOL_CHARS = 400;
497
497
 
498
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.
499
+ * Last-round bodies under this floor stay even after a follow-up ask
500
+ * (0.48.77: a 461-byte bound Read must remain visible). Older rounds use
501
+ * the 0.48.76 rules (bound / fat / over budget) whether or not they sit
502
+ * after the last user ask.
502
503
  */
503
504
  export const KEEP_TOOL_CHARS = 4096;
504
505
 
@@ -524,20 +525,22 @@ function toolCallIds(m) {
524
525
  }
525
526
 
526
527
  /**
527
- * Last assistant that issued tool_calls / tool_use, plus those ids.
528
+ * Most recent assistant tool_calls / Anthropic tool_use in the forwarded
529
+ * tail, plus those ids. Not "everything after the last user ask".
528
530
  *
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.
531
+ * A long continue / ultracode turn parks the last user ask early and then
532
+ * stacks Read/Bash/WebSearch rounds after it. Only the latest batch is
533
+ * this turn's eyes — even a 461-byte Read, even a fat WebSearch. Older
534
+ * batches in that same tail stub/trim under the 0.48.76 rules. When the
535
+ * ask follows the last batch, KEEP_TOOL_CHARS still keeps that last
536
+ * round's small bodies (0.48.77) while fat completed rounds may stub.
535
537
  */
536
- export function currentToolRound(msgs, { lastUser = -1 } = {}) {
538
+ export function currentToolRound(msgs, { lastUser = -1, fromIndex = 0 } = {}) {
537
539
  let index = -1;
538
540
  const ids = new Set();
539
541
  if (!Array.isArray(msgs)) return { index, ids, inFlight: false };
540
- for (let i = 0; i < msgs.length; i++) {
542
+ const start = Math.max(0, Number(fromIndex) || 0);
543
+ for (let i = start; i < msgs.length; i++) {
541
544
  const here = toolCallIds(msgs[i]);
542
545
  if (!here.length) continue;
543
546
  index = i;
@@ -638,12 +641,12 @@ function resolveBoundPath(raw, cwd, boundAbs) {
638
641
  *
639
642
  * `fromIndex` limits the rewrite to the forwarded tail so the spilled prefix
640
643
  * that becomes the conversation corpus is unchanged. The last user ask is
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]`.
644
+ * never rewritten. The current tool round is only the most recent assistant
645
+ * tool_calls / tool_use in that tail, plus tool results for those ids not
646
+ * every Read/Bash/Search after the last user ask. In-flight latest bodies
647
+ * stay even when large. KEEP_TOOL_CHARS applies only to that last round so
648
+ * a 461-byte Read survives a follow-up ask. Older rounds after the same
649
+ * ask stub (bound / fat / over budget) and trim (keepTail) like 0.48.76.
647
650
  */
648
651
  export function stubBoundFileResults(msgs, {
649
652
  boundFiles,
@@ -718,7 +721,7 @@ export function stubBoundFileResults(msgs, {
718
721
  const fatLimit = Number.isFinite(Number(fatChars)) ? Number(fatChars) : FAT_TOOL_CHARS;
719
722
  const keepFloor = KEEP_TOOL_CHARS;
720
723
  const slimArgs = aggressive || wantBudget;
721
- let round = currentToolRound(msgs, { lastUser });
724
+ let round = currentToolRound(msgs, { lastUser, fromIndex });
722
725
 
723
726
  const stubFor = (id, n) => {
724
727
  const paths = idPaths.get(id);
@@ -727,10 +730,13 @@ export function stubBoundFileResults(msgs, {
727
730
 
728
731
  const shouldStubBody = (id, n, { overBudget = false } = {}) => {
729
732
  if (!n || isStubText(typeof n === 'number' ? '' : n)) return false;
733
+ // Latest in-flight batch (after the ask): never stub, even if fat.
730
734
  if (id && round.inFlight && round.ids.has(id)) return false;
731
- if (n < keepFloor && (!overBudget || (id && round.ids.has(id)))) return false;
735
+ // Last completed batch (ask follows): keep small bodies only.
736
+ if (id && round.ids.has(id) && n < keepFloor) return false;
732
737
  if (stubIds.has(id)) return true;
733
738
  if (slimArgs && n >= fatLimit) return true;
739
+ if (overBudget && n > 0) return true;
734
740
  return false;
735
741
  };
736
742
 
@@ -738,16 +744,25 @@ export function stubBoundFileResults(msgs, {
738
744
  let dropped = 0;
739
745
  let messages = msgs;
740
746
 
741
- // keepTail votes here because cutTranscript cannot move inside the chain.
747
+ // keepTail votes here because cutTranscript cannot move inside the chain
748
+ // *or* past the last user ask. A continue turn stacks severable
749
+ // assistant+result rounds after that ask; trim those older rounds first
750
+ // so lastSend cannot grow to hundreds of fully-billed messages.
742
751
  if (wantKeep) {
752
+ messages = trimRoundsAfterAsk(messages, {
753
+ fromIndex,
754
+ keepTail: Number(keepTail),
755
+ lastUser,
756
+ });
757
+ lastUser = lastUserAskIndex(messages, firstSpillableIndex(messages));
743
758
  messages = trimUnseverablePairs(messages, {
744
759
  fromIndex,
745
760
  keepTail: Number(keepTail),
746
761
  lastUser,
747
- firstSpillable,
762
+ firstSpillable: firstSpillableIndex(messages),
748
763
  });
749
764
  lastUser = lastUserAskIndex(messages, firstSpillableIndex(messages));
750
- round = currentToolRound(messages, { lastUser });
765
+ round = currentToolRound(messages, { lastUser, fromIndex });
751
766
  }
752
767
 
753
768
  const skipSlimCalls = (i) => round.inFlight && i === round.index;
@@ -862,6 +877,9 @@ export function stubBoundFileResults(msgs, {
862
877
  messages = next;
863
878
  used = sliceChars(messages, fromIndex);
864
879
  if (used > cap) {
880
+ // Only the un-severable last chain can still be huge (one assistant
881
+ // with hundreds of calls). Do not drop already-stubbed older rounds
882
+ // after the ask — the overage is often the protected latest batch.
865
883
  messages = trimUnseverablePairs(messages, {
866
884
  fromIndex,
867
885
  keepTail: 1,
@@ -1176,6 +1194,35 @@ function isSeverable(msgs, i, firstSpillable) {
1176
1194
  return msgs[i].role !== 'tool';
1177
1195
  }
1178
1196
 
1197
+ /**
1198
+ * cutTranscript pins `cut` at the last user ask, so every later tool round
1199
+ * on a continue / ultracode turn used to ride in the forwarded tail. Keep
1200
+ * the newest `keepTail` complete assistant(tool_calls)+results rounds after
1201
+ * that ask and drop the older ones. Pairing stays valid; the ask stays;
1202
+ * the latest round is never dropped.
1203
+ */
1204
+ export function trimRoundsAfterAsk(msgs, {
1205
+ fromIndex = 0,
1206
+ keepTail = 1,
1207
+ lastUser = -1,
1208
+ } = {}) {
1209
+ if (!Array.isArray(msgs) || lastUser < 0) return msgs;
1210
+ const keep = Math.max(1, Math.round(Number(keepTail) || 1));
1211
+ const start = Math.max(fromIndex, lastUser + 1);
1212
+ const starts = [];
1213
+ for (let i = start; i < msgs.length; i++) {
1214
+ if (toolCallIds(msgs[i]).length) starts.push(i);
1215
+ }
1216
+ if (starts.length <= keep) return msgs;
1217
+ const keepFrom = starts[starts.length - keep];
1218
+ const out = [];
1219
+ for (let i = 0; i < msgs.length; i++) {
1220
+ if (i > lastUser && i < keepFrom) continue;
1221
+ out.push(msgs[i]);
1222
+ }
1223
+ return out;
1224
+ }
1225
+
1179
1226
  /**
1180
1227
  * On an un-severable assistant(tool_calls)+results chain, keepTail cannot
1181
1228
  * move `cut`. Drop older complete pairs (rewrite tool_calls, drop their
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.48.78",
3
+ "version": "0.48.80",
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",