openzoo 0.49.6 → 0.49.8
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/README.md +20 -1
- package/lib/boxes.js +1 -1
- package/lib/grokui.mjs +29 -2
- package/lib/launch.js +31 -68
- package/lib/livestatus.js +2 -2
- package/lib/models.js +113 -5
- package/lib/pay.js +67 -3
- package/lib/podagent.mjs +11 -1
- package/lib/proxy.js +96 -35
- package/lib/racesettle.js +127 -0
- package/lib/spill.js +285 -24
- package/package.json +1 -1
package/lib/spill.js
CHANGED
|
@@ -552,6 +552,15 @@ export function hudDollarX({
|
|
|
552
552
|
/** Tool result larger than this is "fat" — stub it in the forwarded tail. */
|
|
553
553
|
export const FAT_TOOL_CHARS = 400;
|
|
554
554
|
|
|
555
|
+
/**
|
|
556
|
+
* Over-budget forwarded-tail shrink (SHRINK_OVER). When the tail exceeds
|
|
557
|
+
* this many chars (or the caller `budget`), oldest tool_results are
|
|
558
|
+
* considered for `[bound]` stubs. A stub is emitted only if HRR recall
|
|
559
|
+
* actually returns overlapping bytes for that item — `boundFiles` /
|
|
560
|
+
* `boundAbs` is not proof.
|
|
561
|
+
*/
|
|
562
|
+
export const SHRINK_OVER = 6000;
|
|
563
|
+
|
|
555
564
|
/**
|
|
556
565
|
* Last-round bodies under this floor stay even after a follow-up ask
|
|
557
566
|
* (0.48.77: a 461-byte bound Read must remain visible). Older rounds use
|
|
@@ -667,6 +676,84 @@ function toolContentLength(content) {
|
|
|
667
676
|
return 0;
|
|
668
677
|
}
|
|
669
678
|
|
|
679
|
+
function toolContentText(content) {
|
|
680
|
+
if (typeof content === 'string') return content;
|
|
681
|
+
if (Array.isArray(content)) {
|
|
682
|
+
return content.map((b) => (typeof b === 'string' ? b : String(b?.text ?? b?.content ?? ''))).join('');
|
|
683
|
+
}
|
|
684
|
+
if (content && typeof content === 'object') return JSON.stringify(content);
|
|
685
|
+
return content == null ? '' : String(content);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/** Normalize a recall hook result to text. Empty means a miss. */
|
|
689
|
+
export function recallText(got) {
|
|
690
|
+
if (got == null) return '';
|
|
691
|
+
if (typeof got === 'string') return got;
|
|
692
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(got)) return got.toString('utf8');
|
|
693
|
+
if (typeof got === 'object') {
|
|
694
|
+
if (typeof got.text === 'string') return got.text;
|
|
695
|
+
if (typeof got.content === 'string') return got.content;
|
|
696
|
+
if (typeof got.bytes === 'string') return got.bytes;
|
|
697
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(got.bytes)) return got.bytes.toString('utf8');
|
|
698
|
+
}
|
|
699
|
+
return '';
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/** True when recalled text is non-empty and overlaps the original body. */
|
|
703
|
+
export function recallOverlapsBody(recalled, body) {
|
|
704
|
+
const a = recallText(recalled);
|
|
705
|
+
const b = toolContentText(body);
|
|
706
|
+
if (!a || !b) return false;
|
|
707
|
+
if (b.includes(a) || a.includes(b)) return true;
|
|
708
|
+
const n = Math.min(32, a.length, b.length);
|
|
709
|
+
if (n < 1) return false;
|
|
710
|
+
for (let i = 0; i + n <= a.length; i += Math.max(1, Math.floor(n / 2))) {
|
|
711
|
+
if (b.includes(a.slice(i, i + n))) return true;
|
|
712
|
+
}
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Probe `recall` for this tool_result / file. True only when the hook
|
|
718
|
+
* actually produced overlapping bytes — not when we merely think the
|
|
719
|
+
* path is bound.
|
|
720
|
+
*/
|
|
721
|
+
export function recallReturnedBytes(recall, item = {}) {
|
|
722
|
+
if (typeof recall !== 'function') return false;
|
|
723
|
+
let got;
|
|
724
|
+
try {
|
|
725
|
+
const content = toolContentText(item.content);
|
|
726
|
+
got = recall({
|
|
727
|
+
id: item.id,
|
|
728
|
+
paths: item.paths || [],
|
|
729
|
+
content,
|
|
730
|
+
query: item.query || (Array.isArray(item.paths) && item.paths[0]) || content.slice(0, 80),
|
|
731
|
+
});
|
|
732
|
+
} catch {
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
return recallOverlapsBody(got, item.content);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Local recall over already-bound corpus text. Hard-proof stand-in when a
|
|
740
|
+
* live HRR probe is not available: the exact bytes must be in the corpus
|
|
741
|
+
* AND the probe must return a non-empty overlapping slice.
|
|
742
|
+
*/
|
|
743
|
+
export function corpusRecall(corpus) {
|
|
744
|
+
const text = typeof corpus === 'string' ? corpus : '';
|
|
745
|
+
return (item = {}) => {
|
|
746
|
+
if (!text) return '';
|
|
747
|
+
const body = toolContentText(item.content);
|
|
748
|
+
const snippet = body.length >= 16 ? body.slice(0, 64) : body;
|
|
749
|
+
if (snippet && text.includes(snippet)) {
|
|
750
|
+
const i = text.indexOf(snippet);
|
|
751
|
+
return text.slice(i, i + Math.min(Math.max(body.length, snippet.length), 8192));
|
|
752
|
+
}
|
|
753
|
+
return '';
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
670
757
|
function resolveBoundPath(raw, cwd, boundAbs) {
|
|
671
758
|
if (!boundAbs?.size) return null;
|
|
672
759
|
const t = typeof raw === 'string' ? raw.trim() : '';
|
|
@@ -679,10 +766,17 @@ function resolveBoundPath(raw, cwd, boundAbs) {
|
|
|
679
766
|
|
|
680
767
|
/**
|
|
681
768
|
* After a file is bound, drop its tool_result / file body from the forwarded
|
|
682
|
-
* tail. Keep the path and a short marker.
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
769
|
+
* tail. Keep the path and a short marker. Shipping those bytes again makes
|
|
770
|
+
* sent ≈ corpus and the gateway's `counterfactualTokens > promptTokens`
|
|
771
|
+
* gate barely fires (live: 5MB filebind, lastSend 13/107, savingX 1.22
|
|
772
|
+
* instead of ~7x).
|
|
773
|
+
*
|
|
774
|
+
* A `[bound]` marker is a promise that HRR recall can return those bytes.
|
|
775
|
+
* `boundAbs` / `boundFiles` only means we think we bound the path — that
|
|
776
|
+
* is not proof. On the SHRINK_OVER path (tail over `budget`), and whenever
|
|
777
|
+
* a `recall` hook is installed on a budgeted tail, stub only if recall
|
|
778
|
+
* actually returns non-empty overlapping content. Prefer the original body
|
|
779
|
+
* over a lying stub (context missing, file not in corpus, recall miss).
|
|
686
780
|
*
|
|
687
781
|
* Cheap rewrite — no disk I/O. First-read results (not yet in boundAbs) and
|
|
688
782
|
* non-file tool output stay verbatim UNLESS `aggressive` / `stubMore` or a
|
|
@@ -728,6 +822,9 @@ export function stubBoundFileResults(msgs, {
|
|
|
728
822
|
// assistant(tool_calls)+results chain. Ignored when the tail is severable.
|
|
729
823
|
keepTail = null,
|
|
730
824
|
fatChars = FAT_TOOL_CHARS,
|
|
825
|
+
// (query) => bytes. SHRINK_OVER [bound] stubs require a non-empty
|
|
826
|
+
// overlapping hit. boundFiles alone is not enough.
|
|
827
|
+
recall = null,
|
|
731
828
|
} = {}) {
|
|
732
829
|
const absSet = boundAbs || boundAbsFromKeys(boundFiles);
|
|
733
830
|
const wantBudget = budget != null && Number.isFinite(Number(budget));
|
|
@@ -816,6 +913,29 @@ export function stubBoundFileResults(msgs, {
|
|
|
816
913
|
return false;
|
|
817
914
|
};
|
|
818
915
|
|
|
916
|
+
const recallCache = new Map();
|
|
917
|
+
const probeRecall = (id, content) => {
|
|
918
|
+
const key = id || toolContentText(content);
|
|
919
|
+
if (recallCache.has(key)) return recallCache.get(key);
|
|
920
|
+
const ok = recallReturnedBytes(recall, {
|
|
921
|
+
id,
|
|
922
|
+
content,
|
|
923
|
+
paths: idPaths.get(id) || [],
|
|
924
|
+
});
|
|
925
|
+
recallCache.set(key, ok);
|
|
926
|
+
return ok;
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
const allowBoundStub = (id, content, { overBudget = false } = {}) => {
|
|
930
|
+
// SHRINK_OVER leftovers always need real recall bytes (fail closed
|
|
931
|
+
// when no hook is installed). When a recall hook is installed on a
|
|
932
|
+
// budgeted tail, every [bound] claim uses the same gate.
|
|
933
|
+
if (overBudget || (wantBudget && typeof recall === 'function')) {
|
|
934
|
+
return probeRecall(id, content);
|
|
935
|
+
}
|
|
936
|
+
return true;
|
|
937
|
+
};
|
|
938
|
+
|
|
819
939
|
let stubbed = 0;
|
|
820
940
|
let dropped = 0;
|
|
821
941
|
let messages = msgs;
|
|
@@ -849,6 +969,7 @@ export function stubBoundFileResults(msgs, {
|
|
|
849
969
|
if (m.role === 'tool') {
|
|
850
970
|
const n = toolContentLength(m.content);
|
|
851
971
|
if (!shouldStubBody(m.tool_call_id, n) || isStubText(m.content)) return m;
|
|
972
|
+
if (!allowBoundStub(m.tool_call_id, m.content)) return m;
|
|
852
973
|
dropped += n;
|
|
853
974
|
stubbed += 1;
|
|
854
975
|
return { ...m, content: stubFor(m.tool_call_id, n) };
|
|
@@ -882,6 +1003,7 @@ export function stubBoundFileResults(msgs, {
|
|
|
882
1003
|
if (b?.type !== 'tool_result') return b;
|
|
883
1004
|
const n = toolContentLength(b.content);
|
|
884
1005
|
if (!shouldStubBody(b.tool_use_id, n) || isStubText(b.content)) return b;
|
|
1006
|
+
if (!allowBoundStub(b.tool_use_id, b.content)) return b;
|
|
885
1007
|
dropped += n;
|
|
886
1008
|
stubbed += 1;
|
|
887
1009
|
changed = true;
|
|
@@ -890,11 +1012,12 @@ export function stubBoundFileResults(msgs, {
|
|
|
890
1012
|
return changed ? { ...next, content: blocks } : next;
|
|
891
1013
|
});
|
|
892
1014
|
|
|
893
|
-
//
|
|
894
|
-
// past assistant(tool_calls) / role:tool (pairing 400s the
|
|
895
|
-
// 300-result storm used to ride in at ~728k after file-only
|
|
896
|
-
// bodies first, then fat tool_call JSON; if still over, drop
|
|
897
|
-
// Never drop the ask. Never orphan a remaining tool_result.
|
|
1015
|
+
// SHRINK_OVER: byte budget wins inside a tool chain. cutTranscript cannot
|
|
1016
|
+
// move tailStart past assistant(tool_calls) / role:tool (pairing 400s the
|
|
1017
|
+
// provider), so a 300-result storm used to ride in at ~728k after file-only
|
|
1018
|
+
// stubs. Stub bodies first, then fat tool_call JSON; if still over, drop
|
|
1019
|
+
// older pairs. Never drop the ask. Never orphan a remaining tool_result.
|
|
1020
|
+
// Never write `[bound]` unless recall returned those bytes.
|
|
898
1021
|
if (wantBudget) {
|
|
899
1022
|
const cap = Number(budget);
|
|
900
1023
|
let used = sliceChars(messages, fromIndex);
|
|
@@ -908,6 +1031,7 @@ export function stubBoundFileResults(msgs, {
|
|
|
908
1031
|
if (isStubText(m.content)) continue;
|
|
909
1032
|
const n = toolContentLength(m.content);
|
|
910
1033
|
if (!shouldStubBody(m.tool_call_id, n, { overBudget: true })) continue;
|
|
1034
|
+
if (!allowBoundStub(m.tool_call_id, m.content, { overBudget: true })) continue;
|
|
911
1035
|
const stub = stubFor(m.tool_call_id, n);
|
|
912
1036
|
if (stub.length >= n) continue;
|
|
913
1037
|
used = used - n + stub.length;
|
|
@@ -941,6 +1065,7 @@ export function stubBoundFileResults(msgs, {
|
|
|
941
1065
|
if (b?.type !== 'tool_result' || used <= cap || isStubText(b.content)) return b;
|
|
942
1066
|
const n = toolContentLength(b.content);
|
|
943
1067
|
if (!shouldStubBody(b.tool_use_id, n, { overBudget: true })) return b;
|
|
1068
|
+
if (!allowBoundStub(b.tool_use_id, b.content, { overBudget: true })) return b;
|
|
944
1069
|
const stub = stubFor(b.tool_use_id, n);
|
|
945
1070
|
if (stub.length >= n) return b;
|
|
946
1071
|
used = used - n + stub.length;
|
|
@@ -984,7 +1109,7 @@ export const LAST_SEND_TIGHTEN = 24;
|
|
|
984
1109
|
export const KNOB_DEFAULTS = Object.freeze({
|
|
985
1110
|
keepTail: 8,
|
|
986
1111
|
minTurns: 6,
|
|
987
|
-
budget:
|
|
1112
|
+
budget: SHRINK_OVER,
|
|
988
1113
|
stubMore: false,
|
|
989
1114
|
});
|
|
990
1115
|
|
|
@@ -1397,6 +1522,8 @@ export function cutTranscript(msgs, knobs = {}) {
|
|
|
1397
1522
|
const minTurns = Math.max(2, k.minTurns);
|
|
1398
1523
|
const budget = k.budget;
|
|
1399
1524
|
|
|
1525
|
+
const lastUser = lastUserAskIndex(msgs, firstSpillable);
|
|
1526
|
+
|
|
1400
1527
|
let cut = -1;
|
|
1401
1528
|
for (let i = msgs.length - keepTail; i > firstSpillable; i--) {
|
|
1402
1529
|
if (isSeverable(msgs, i, firstSpillable)) { cut = i; break; }
|
|
@@ -1407,7 +1534,7 @@ export function cutTranscript(msgs, knobs = {}) {
|
|
|
1407
1534
|
}
|
|
1408
1535
|
}
|
|
1409
1536
|
if (cut <= firstSpillable) {
|
|
1410
|
-
return { cut: -1, firstSpillable, lastUser
|
|
1537
|
+
return { cut: -1, firstSpillable, lastUser, knobs: k };
|
|
1411
1538
|
}
|
|
1412
1539
|
|
|
1413
1540
|
// Only moves the cut at a severable index. A current-turn tool storm
|
|
@@ -1415,27 +1542,44 @@ export function cutTranscript(msgs, knobs = {}) {
|
|
|
1415
1542
|
// index inside the chain, so this walk is a no-op — the byte budget is
|
|
1416
1543
|
// applied by stubbing bodies in stubBoundFileResults, not by orphaning
|
|
1417
1544
|
// a tool_result.
|
|
1545
|
+
//
|
|
1546
|
+
// When the overflow IS severable (grokui text hops: a fat previous
|
|
1547
|
+
// command output sitting just before the current ask), start the tail
|
|
1548
|
+
// AFTER that message so the live window is actually bounded. Starting
|
|
1549
|
+
// AT the overflowing index left the fat hop in the forwarded body —
|
|
1550
|
+
// spent≈direct on 1-model AUTO even after a cut existed.
|
|
1418
1551
|
let tailStart = cut;
|
|
1419
1552
|
{
|
|
1420
1553
|
let used = 0;
|
|
1421
1554
|
for (let i = msgs.length - 1; i >= cut; i--) {
|
|
1422
1555
|
used += msgText(msgs[i]).length;
|
|
1423
|
-
if (used
|
|
1556
|
+
if (used <= budget) continue;
|
|
1557
|
+
const after = i + 1;
|
|
1558
|
+
// Plain user/assistant hops only. Jumping after a tool_result would
|
|
1559
|
+
// drop the current tool chain from the tail (orphans / lost pairing);
|
|
1560
|
+
// those storms stay in-window and get stubbed.
|
|
1561
|
+
const overflow = msgs[i];
|
|
1562
|
+
const plainHop = overflow?.role === 'user' || (overflow?.role === 'assistant' && !toolCallIds(overflow).length);
|
|
1563
|
+
if (plainHop && lastUser >= 0 && after <= lastUser && after > firstSpillable
|
|
1564
|
+
&& isSeverable(msgs, after, firstSpillable)) {
|
|
1565
|
+
tailStart = after;
|
|
1566
|
+
break;
|
|
1567
|
+
}
|
|
1568
|
+
if (isSeverable(msgs, i, firstSpillable)) { tailStart = i; break; }
|
|
1424
1569
|
}
|
|
1425
1570
|
}
|
|
1426
1571
|
if (tailStart > cut) cut = tailStart;
|
|
1427
1572
|
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
//
|
|
1431
|
-
//
|
|
1432
|
-
//
|
|
1433
|
-
// thread that cannot satisfy minTurns at all, pin the last ask so early
|
|
1434
|
-
// user/assistant turns bind instead of riding in the forwarded tail.
|
|
1573
|
+
// minTurns may still size the tail on a long thread, but never past the
|
|
1574
|
+
// byte budget (that pulled fat 1-model hops back so sent≈unspilled).
|
|
1575
|
+
// If walking earlier would empty the bind prefix, keep a non-empty prefix
|
|
1576
|
+
// and allow fewer than minTurns. On a short thread that cannot satisfy
|
|
1577
|
+
// minTurns at all, pin the last ask so early turns bind.
|
|
1435
1578
|
if (countRealTurns(msgs, cut) < minTurns) {
|
|
1436
1579
|
let moved = false;
|
|
1437
1580
|
for (let i = cut - 1; i > firstSpillable; i--) {
|
|
1438
1581
|
if (isSeverable(msgs, i, firstSpillable) && countRealTurns(msgs, i) >= minTurns) {
|
|
1582
|
+
if (sliceChars(msgs, i) > budget) break;
|
|
1439
1583
|
cut = i;
|
|
1440
1584
|
moved = true;
|
|
1441
1585
|
break;
|
|
@@ -1450,11 +1594,13 @@ export function cutTranscript(msgs, knobs = {}) {
|
|
|
1450
1594
|
|
|
1451
1595
|
// 2-real-turn floor protects the last user ask: never drop it, and expand
|
|
1452
1596
|
// the tail to two turns on a LONG thread. Do not steal the first
|
|
1453
|
-
// user+assistant pair from the prefix just to pad a short tail
|
|
1597
|
+
// user+assistant pair from the prefix just to pad a short tail, and do
|
|
1598
|
+
// not expand past the byte budget (that re-imported fat 1-model hops).
|
|
1454
1599
|
if (lastUser > firstSpillable && countRealTurns(msgs, cut) < 2) {
|
|
1455
1600
|
for (let i = cut - 1; i > firstSpillable; i--) {
|
|
1456
1601
|
if (!isSeverable(msgs, i, firstSpillable) || countRealTurns(msgs, i) < 2) continue;
|
|
1457
1602
|
if (countRealTurns(msgs, firstSpillable, i) < 2) continue;
|
|
1603
|
+
if (sliceChars(msgs, i) > budget) break;
|
|
1458
1604
|
cut = i;
|
|
1459
1605
|
break;
|
|
1460
1606
|
}
|
|
@@ -1588,6 +1734,7 @@ function stubForCut(msgs, cut, opts) {
|
|
|
1588
1734
|
aggressive: Boolean(opts.aggressive),
|
|
1589
1735
|
budget: opts.budget,
|
|
1590
1736
|
keepTail: opts.keepTail,
|
|
1737
|
+
recall: opts.recall,
|
|
1591
1738
|
});
|
|
1592
1739
|
}
|
|
1593
1740
|
|
|
@@ -1611,6 +1758,7 @@ export function applySpillCut(msgs, {
|
|
|
1611
1758
|
home,
|
|
1612
1759
|
dollarX,
|
|
1613
1760
|
lastSend,
|
|
1761
|
+
recall,
|
|
1614
1762
|
} = {}) {
|
|
1615
1763
|
const persistOpts = { persist, file, home };
|
|
1616
1764
|
let k = sanitizeKnobs(knobs || getLiveKnobs(persistOpts));
|
|
@@ -1631,7 +1779,7 @@ export function applySpillCut(msgs, {
|
|
|
1631
1779
|
// 300-result storm is not forwarded at full size.
|
|
1632
1780
|
const stubFrom = plan.firstSpillable >= 0 ? plan.firstSpillable : 0;
|
|
1633
1781
|
let stubbed = stubForCut(msgs, stubFrom, {
|
|
1634
|
-
boundFiles, boundAbs, cwd, aggressive: k.stubMore, budget: k.budget, keepTail: k.keepTail,
|
|
1782
|
+
boundFiles, boundAbs, cwd, aggressive: k.stubMore, budget: k.budget, keepTail: k.keepTail, recall,
|
|
1635
1783
|
});
|
|
1636
1784
|
let sentChars = sliceChars(stubbed.messages, stubFrom);
|
|
1637
1785
|
const corpus = Math.max(Number(corpusChars) || 0, sentChars);
|
|
@@ -1651,7 +1799,7 @@ export function applySpillCut(msgs, {
|
|
|
1651
1799
|
action = decision.action;
|
|
1652
1800
|
if (decision.recut) {
|
|
1653
1801
|
stubbed = stubForCut(msgs, stubFrom, {
|
|
1654
|
-
boundFiles, boundAbs, cwd, aggressive: k.stubMore, budget: k.budget, keepTail: k.keepTail,
|
|
1802
|
+
boundFiles, boundAbs, cwd, aggressive: k.stubMore, budget: k.budget, keepTail: k.keepTail, recall,
|
|
1655
1803
|
});
|
|
1656
1804
|
sentChars = sliceChars(stubbed.messages, stubFrom);
|
|
1657
1805
|
ratio = spillRatio(corpus, sentChars);
|
|
@@ -1677,7 +1825,7 @@ export function applySpillCut(msgs, {
|
|
|
1677
1825
|
};
|
|
1678
1826
|
|
|
1679
1827
|
let stubbed = stubForCut(msgs, plan.cut, {
|
|
1680
|
-
boundFiles, boundAbs, cwd, aggressive: k.stubMore, budget: k.budget, keepTail: k.keepTail,
|
|
1828
|
+
boundFiles, boundAbs, cwd, aggressive: k.stubMore, budget: k.budget, keepTail: k.keepTail, recall,
|
|
1681
1829
|
});
|
|
1682
1830
|
let stats = measure(plan.cut, stubbed, k);
|
|
1683
1831
|
let action = 'hold';
|
|
@@ -1698,7 +1846,7 @@ export function applySpillCut(msgs, {
|
|
|
1698
1846
|
plan = cutTranscript(msgs, k);
|
|
1699
1847
|
if (plan.cut > plan.firstSpillable) {
|
|
1700
1848
|
stubbed = stubForCut(msgs, plan.cut, {
|
|
1701
|
-
boundFiles, boundAbs, cwd, aggressive: k.stubMore, budget: k.budget, keepTail: k.keepTail,
|
|
1849
|
+
boundFiles, boundAbs, cwd, aggressive: k.stubMore, budget: k.budget, keepTail: k.keepTail, recall,
|
|
1702
1850
|
});
|
|
1703
1851
|
stats = measure(plan.cut, stubbed, k);
|
|
1704
1852
|
}
|
|
@@ -1721,6 +1869,119 @@ export function applySpillCut(msgs, {
|
|
|
1721
1869
|
};
|
|
1722
1870
|
}
|
|
1723
1871
|
|
|
1872
|
+
/**
|
|
1873
|
+
* Same threshold hrr.js BIND_MIN_CHARS uses. Kept here so the 1-model /
|
|
1874
|
+
* raced AUTO decision can be tested without standing up the bind client.
|
|
1875
|
+
*/
|
|
1876
|
+
export const SPILL_MIN_PREFIX_CHARS = Number(process.env.OPENZOO_CONTEXT_MIN_CHARS || 16384);
|
|
1877
|
+
|
|
1878
|
+
/**
|
|
1879
|
+
* zoo_ask / chat-surface shape: one huge final string ending in `\n\n<ask>`.
|
|
1880
|
+
* Agent transcripts (Claude Code, grokui) never look like this.
|
|
1881
|
+
*/
|
|
1882
|
+
export function isOneShotCorpusAsk(msgs, minChars = SPILL_MIN_PREFIX_CHARS) {
|
|
1883
|
+
if (!Array.isArray(msgs) || !msgs.length) return false;
|
|
1884
|
+
const last = msgs[msgs.length - 1];
|
|
1885
|
+
if (typeof last?.content !== 'string') return false;
|
|
1886
|
+
if (last.content.length <= minChars) return false;
|
|
1887
|
+
if (last.content.lastIndexOf('\n\n') < minChars) return false;
|
|
1888
|
+
const ask = last.content.slice(last.content.lastIndexOf('\n\n') + 2).trim();
|
|
1889
|
+
return Boolean(ask && ask.length <= 8000);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
function emptySpillDecision(msgs, reason) {
|
|
1893
|
+
return {
|
|
1894
|
+
mode: 'passthrough',
|
|
1895
|
+
reason,
|
|
1896
|
+
setHrrContext: false,
|
|
1897
|
+
forwarded: msgs,
|
|
1898
|
+
prefix: '',
|
|
1899
|
+
head: [],
|
|
1900
|
+
tail: msgs,
|
|
1901
|
+
sentChars: sliceChars(msgs),
|
|
1902
|
+
unspilledChars: sliceChars(msgs),
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
/**
|
|
1907
|
+
* THE spill/bind/forward gate. 1-model grokui and raced grokui AUTO both
|
|
1908
|
+
* POST /chat/completions through maybeCacheCorpus → this decision. `race`,
|
|
1909
|
+
* `race_need`, and `tier` on the body are ignored — the cut is a property
|
|
1910
|
+
* of messages[], not of how many models will read them.
|
|
1911
|
+
*
|
|
1912
|
+
* Oversized: bind the old prefix, forward system + bounded tail, and the
|
|
1913
|
+
* sidecar must set x-hrr-context (setHrrContext). Small: passthrough,
|
|
1914
|
+
* no bind, no header. Does not talk to the network.
|
|
1915
|
+
*/
|
|
1916
|
+
export function decideChatSpill(body, opts = {}) {
|
|
1917
|
+
const msgs = Array.isArray(body?.messages) ? body.messages : null;
|
|
1918
|
+
if (!msgs?.length) return emptySpillDecision(msgs, 'no-messages');
|
|
1919
|
+
const minChars = opts.minPrefixChars ?? SPILL_MIN_PREFIX_CHARS;
|
|
1920
|
+
|
|
1921
|
+
if (isOneShotCorpusAsk(msgs, minChars)) {
|
|
1922
|
+
const last = msgs[msgs.length - 1];
|
|
1923
|
+
const at = last.content.lastIndexOf('\n\n');
|
|
1924
|
+
const prefix = last.content.slice(0, at);
|
|
1925
|
+
const ask = last.content.slice(at + 2).trim();
|
|
1926
|
+
const forwarded = [...msgs.slice(0, -1), { ...last, content: ask }];
|
|
1927
|
+
return {
|
|
1928
|
+
mode: 'oneshot',
|
|
1929
|
+
reason: 'corpus-question',
|
|
1930
|
+
setHrrContext: true,
|
|
1931
|
+
prefix,
|
|
1932
|
+
ask,
|
|
1933
|
+
forwarded,
|
|
1934
|
+
head: msgs.slice(0, -1),
|
|
1935
|
+
tail: [{ ...last, content: ask }],
|
|
1936
|
+
sentChars: sliceChars(forwarded),
|
|
1937
|
+
unspilledChars: sliceChars(msgs),
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
const adapted = applySpillCut(msgs, opts);
|
|
1942
|
+
if (adapted.cut <= adapted.firstSpillable) {
|
|
1943
|
+
if (adapted.stubbed?.dropped || adapted.stubbed?.stubbed) {
|
|
1944
|
+
return {
|
|
1945
|
+
mode: 'stub-only',
|
|
1946
|
+
reason: 'no-cut-stubbed',
|
|
1947
|
+
setHrrContext: false,
|
|
1948
|
+
adapted,
|
|
1949
|
+
forwarded: adapted.stubbed.messages,
|
|
1950
|
+
prefix: '',
|
|
1951
|
+
head: [],
|
|
1952
|
+
tail: adapted.stubbed.messages,
|
|
1953
|
+
sentChars: sliceChars(adapted.stubbed.messages),
|
|
1954
|
+
unspilledChars: sliceChars(msgs),
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
return { ...emptySpillDecision(msgs, 'no-cut'), adapted };
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
const prefix = msgs.slice(adapted.firstSpillable, adapted.cut)
|
|
1961
|
+
.map(msgText).filter(Boolean).join('\n\n');
|
|
1962
|
+
if (prefix.length <= minChars) {
|
|
1963
|
+
return { ...emptySpillDecision(msgs, 'prefix-under-threshold'), adapted, prefix };
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
const head = msgs.slice(0, adapted.firstSpillable);
|
|
1967
|
+
const tail = adapted.stubbed.messages.slice(adapted.cut);
|
|
1968
|
+
const forwarded = [...head, ...tail];
|
|
1969
|
+
return {
|
|
1970
|
+
mode: 'spill',
|
|
1971
|
+
reason: 'oversized-prefix',
|
|
1972
|
+
setHrrContext: true,
|
|
1973
|
+
adapted,
|
|
1974
|
+
prefix,
|
|
1975
|
+
head,
|
|
1976
|
+
tail,
|
|
1977
|
+
forwarded,
|
|
1978
|
+
cut: adapted.cut,
|
|
1979
|
+
firstSpillable: adapted.firstSpillable,
|
|
1980
|
+
sentChars: sliceChars(forwarded),
|
|
1981
|
+
unspilledChars: sliceChars(msgs),
|
|
1982
|
+
};
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1724
1985
|
/** Session counters the HUD reads off /v1/info. */
|
|
1725
1986
|
export function createSpillStats() {
|
|
1726
1987
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.49.
|
|
3
|
+
"version": "0.49.8",
|
|
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",
|