openzoo 0.49.5 → 0.49.7
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 +7 -7
- package/lib/boxes.js +1 -1
- package/lib/demo.js +4 -1
- package/lib/grokui.mjs +177 -5
- package/lib/gui.html +4 -1
- package/lib/mcp.js +3 -1
- package/lib/models.js +27 -5
- package/lib/pay.js +3 -0
- package/lib/proxy.js +168 -118
- package/lib/racesettle.js +22 -8
- package/lib/spill.js +445 -25
- package/lib/x402.js +13 -4
- package/package.json +3 -3
package/lib/spill.js
CHANGED
|
@@ -118,6 +118,31 @@ export function corpusCharsForSend(boundChars, contextId, thisTurn) {
|
|
|
118
118
|
return Math.max(boundChars.get(contextId) || 0, thisTurn || 0);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Pricing / unspilled basis: the unspilled size, never the shrunken sent
|
|
123
|
+
* (tokensAfter) size. Same unit on both args — tokens or chars, not mixed.
|
|
124
|
+
*/
|
|
125
|
+
export function unspilledBasis({ tokensBefore, corpus } = {}) {
|
|
126
|
+
return Math.max(Number(tokensBefore) || 0, Number(corpus) || 0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Gateway counterfactual the tell-line must print. Missing / non-finite /
|
|
131
|
+
* non-positive → null (`basis ?`). Never reads lecore.corpusTokens.
|
|
132
|
+
*/
|
|
133
|
+
export function spillPricedTellBasis(x) {
|
|
134
|
+
const n = Number(x?.counterfactualTokensUsed);
|
|
135
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Exact `spill priced:` / `spill priced (streamed):` line the proxy logs. */
|
|
139
|
+
export function spillPricedLine(x, { streamed = false } = {}) {
|
|
140
|
+
const lc = x?.lecore || {};
|
|
141
|
+
const basis = spillPricedTellBasis(x);
|
|
142
|
+
const prefix = streamed ? 'spill priced (streamed):' : 'spill priced:';
|
|
143
|
+
return `${prefix} ${x?.pricing} · basis ${basis ?? '?'} tok vs sent ${lc.tokensBefore ?? '?'} -> ${lc.tokensAfter ?? '?'} · billed ${(x?.billedUsd ?? 0).toFixed(5)} direct ${(x?.directUsd ?? 0).toFixed(5)}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
121
146
|
/** Expand ~ and resolve relative paths against cwd. Returns null if unusable. */
|
|
122
147
|
export function resolveReadablePath(p, cwd = process.cwd()) {
|
|
123
148
|
if (typeof p !== 'string') return null;
|
|
@@ -527,6 +552,15 @@ export function hudDollarX({
|
|
|
527
552
|
/** Tool result larger than this is "fat" — stub it in the forwarded tail. */
|
|
528
553
|
export const FAT_TOOL_CHARS = 400;
|
|
529
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
|
+
|
|
530
564
|
/**
|
|
531
565
|
* Last-round bodies under this floor stay even after a follow-up ask
|
|
532
566
|
* (0.48.77: a 461-byte bound Read must remain visible). Older rounds use
|
|
@@ -642,6 +676,84 @@ function toolContentLength(content) {
|
|
|
642
676
|
return 0;
|
|
643
677
|
}
|
|
644
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
|
+
|
|
645
757
|
function resolveBoundPath(raw, cwd, boundAbs) {
|
|
646
758
|
if (!boundAbs?.size) return null;
|
|
647
759
|
const t = typeof raw === 'string' ? raw.trim() : '';
|
|
@@ -654,10 +766,17 @@ function resolveBoundPath(raw, cwd, boundAbs) {
|
|
|
654
766
|
|
|
655
767
|
/**
|
|
656
768
|
* After a file is bound, drop its tool_result / file body from the forwarded
|
|
657
|
-
* tail. Keep the path and a short marker.
|
|
658
|
-
*
|
|
659
|
-
*
|
|
660
|
-
*
|
|
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).
|
|
661
780
|
*
|
|
662
781
|
* Cheap rewrite — no disk I/O. First-read results (not yet in boundAbs) and
|
|
663
782
|
* non-file tool output stay verbatim UNLESS `aggressive` / `stubMore` or a
|
|
@@ -703,6 +822,9 @@ export function stubBoundFileResults(msgs, {
|
|
|
703
822
|
// assistant(tool_calls)+results chain. Ignored when the tail is severable.
|
|
704
823
|
keepTail = null,
|
|
705
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,
|
|
706
828
|
} = {}) {
|
|
707
829
|
const absSet = boundAbs || boundAbsFromKeys(boundFiles);
|
|
708
830
|
const wantBudget = budget != null && Number.isFinite(Number(budget));
|
|
@@ -791,6 +913,29 @@ export function stubBoundFileResults(msgs, {
|
|
|
791
913
|
return false;
|
|
792
914
|
};
|
|
793
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
|
+
|
|
794
939
|
let stubbed = 0;
|
|
795
940
|
let dropped = 0;
|
|
796
941
|
let messages = msgs;
|
|
@@ -824,6 +969,7 @@ export function stubBoundFileResults(msgs, {
|
|
|
824
969
|
if (m.role === 'tool') {
|
|
825
970
|
const n = toolContentLength(m.content);
|
|
826
971
|
if (!shouldStubBody(m.tool_call_id, n) || isStubText(m.content)) return m;
|
|
972
|
+
if (!allowBoundStub(m.tool_call_id, m.content)) return m;
|
|
827
973
|
dropped += n;
|
|
828
974
|
stubbed += 1;
|
|
829
975
|
return { ...m, content: stubFor(m.tool_call_id, n) };
|
|
@@ -857,6 +1003,7 @@ export function stubBoundFileResults(msgs, {
|
|
|
857
1003
|
if (b?.type !== 'tool_result') return b;
|
|
858
1004
|
const n = toolContentLength(b.content);
|
|
859
1005
|
if (!shouldStubBody(b.tool_use_id, n) || isStubText(b.content)) return b;
|
|
1006
|
+
if (!allowBoundStub(b.tool_use_id, b.content)) return b;
|
|
860
1007
|
dropped += n;
|
|
861
1008
|
stubbed += 1;
|
|
862
1009
|
changed = true;
|
|
@@ -865,11 +1012,12 @@ export function stubBoundFileResults(msgs, {
|
|
|
865
1012
|
return changed ? { ...next, content: blocks } : next;
|
|
866
1013
|
});
|
|
867
1014
|
|
|
868
|
-
//
|
|
869
|
-
// past assistant(tool_calls) / role:tool (pairing 400s the
|
|
870
|
-
// 300-result storm used to ride in at ~728k after file-only
|
|
871
|
-
// bodies first, then fat tool_call JSON; if still over, drop
|
|
872
|
-
// 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.
|
|
873
1021
|
if (wantBudget) {
|
|
874
1022
|
const cap = Number(budget);
|
|
875
1023
|
let used = sliceChars(messages, fromIndex);
|
|
@@ -883,6 +1031,7 @@ export function stubBoundFileResults(msgs, {
|
|
|
883
1031
|
if (isStubText(m.content)) continue;
|
|
884
1032
|
const n = toolContentLength(m.content);
|
|
885
1033
|
if (!shouldStubBody(m.tool_call_id, n, { overBudget: true })) continue;
|
|
1034
|
+
if (!allowBoundStub(m.tool_call_id, m.content, { overBudget: true })) continue;
|
|
886
1035
|
const stub = stubFor(m.tool_call_id, n);
|
|
887
1036
|
if (stub.length >= n) continue;
|
|
888
1037
|
used = used - n + stub.length;
|
|
@@ -916,6 +1065,7 @@ export function stubBoundFileResults(msgs, {
|
|
|
916
1065
|
if (b?.type !== 'tool_result' || used <= cap || isStubText(b.content)) return b;
|
|
917
1066
|
const n = toolContentLength(b.content);
|
|
918
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;
|
|
919
1069
|
const stub = stubFor(b.tool_use_id, n);
|
|
920
1070
|
if (stub.length >= n) return b;
|
|
921
1071
|
used = used - n + stub.length;
|
|
@@ -959,7 +1109,7 @@ export const LAST_SEND_TIGHTEN = 24;
|
|
|
959
1109
|
export const KNOB_DEFAULTS = Object.freeze({
|
|
960
1110
|
keepTail: 8,
|
|
961
1111
|
minTurns: 6,
|
|
962
|
-
budget:
|
|
1112
|
+
budget: SHRINK_OVER,
|
|
963
1113
|
stubMore: false,
|
|
964
1114
|
});
|
|
965
1115
|
|
|
@@ -1256,9 +1406,9 @@ function lastUserAskIndex(msgs, firstSpillable) {
|
|
|
1256
1406
|
return -1;
|
|
1257
1407
|
}
|
|
1258
1408
|
|
|
1259
|
-
function countRealTurns(msgs, from) {
|
|
1409
|
+
function countRealTurns(msgs, from, to = msgs.length) {
|
|
1260
1410
|
let n = 0;
|
|
1261
|
-
for (let i = from; i <
|
|
1411
|
+
for (let i = from; i < to; i++) {
|
|
1262
1412
|
const r = msgs[i]?.role;
|
|
1263
1413
|
if (r === 'user' || r === 'assistant') n += 1;
|
|
1264
1414
|
}
|
|
@@ -1356,7 +1506,9 @@ export function trimUnseverablePairs(msgs, {
|
|
|
1356
1506
|
|
|
1357
1507
|
/**
|
|
1358
1508
|
* Pick a severable cut: keep a recent tail, honour the byte budget, floor
|
|
1359
|
-
* at minTurns of user/assistant, and never drop the last
|
|
1509
|
+
* at minTurns of user/assistant on a LONG thread, and never drop the last
|
|
1510
|
+
* user ask. minTurns must not empty the bind prefix — a short AUTO thread
|
|
1511
|
+
* binds early turns and may forward a tail with fewer than minTurns.
|
|
1360
1512
|
*/
|
|
1361
1513
|
export function cutTranscript(msgs, knobs = {}) {
|
|
1362
1514
|
const k = sanitizeKnobs({ ...envKnobs(), ...knobs });
|
|
@@ -1370,6 +1522,8 @@ export function cutTranscript(msgs, knobs = {}) {
|
|
|
1370
1522
|
const minTurns = Math.max(2, k.minTurns);
|
|
1371
1523
|
const budget = k.budget;
|
|
1372
1524
|
|
|
1525
|
+
const lastUser = lastUserAskIndex(msgs, firstSpillable);
|
|
1526
|
+
|
|
1373
1527
|
let cut = -1;
|
|
1374
1528
|
for (let i = msgs.length - keepTail; i > firstSpillable; i--) {
|
|
1375
1529
|
if (isSeverable(msgs, i, firstSpillable)) { cut = i; break; }
|
|
@@ -1380,7 +1534,7 @@ export function cutTranscript(msgs, knobs = {}) {
|
|
|
1380
1534
|
}
|
|
1381
1535
|
}
|
|
1382
1536
|
if (cut <= firstSpillable) {
|
|
1383
|
-
return { cut: -1, firstSpillable, lastUser
|
|
1537
|
+
return { cut: -1, firstSpillable, lastUser, knobs: k };
|
|
1384
1538
|
}
|
|
1385
1539
|
|
|
1386
1540
|
// Only moves the cut at a severable index. A current-turn tool storm
|
|
@@ -1388,31 +1542,67 @@ export function cutTranscript(msgs, knobs = {}) {
|
|
|
1388
1542
|
// index inside the chain, so this walk is a no-op — the byte budget is
|
|
1389
1543
|
// applied by stubbing bodies in stubBoundFileResults, not by orphaning
|
|
1390
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.
|
|
1391
1551
|
let tailStart = cut;
|
|
1392
1552
|
{
|
|
1393
1553
|
let used = 0;
|
|
1394
1554
|
for (let i = msgs.length - 1; i >= cut; i--) {
|
|
1395
1555
|
used += msgText(msgs[i]).length;
|
|
1396
|
-
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; }
|
|
1397
1569
|
}
|
|
1398
1570
|
}
|
|
1399
1571
|
if (tailStart > cut) cut = tailStart;
|
|
1400
1572
|
|
|
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.
|
|
1401
1578
|
if (countRealTurns(msgs, cut) < minTurns) {
|
|
1579
|
+
let moved = false;
|
|
1402
1580
|
for (let i = cut - 1; i > firstSpillable; i--) {
|
|
1403
|
-
if (isSeverable(msgs, i, firstSpillable) && countRealTurns(msgs, i) >= minTurns) {
|
|
1404
|
-
|
|
1581
|
+
if (isSeverable(msgs, i, firstSpillable) && countRealTurns(msgs, i) >= minTurns) {
|
|
1582
|
+
if (sliceChars(msgs, i) > budget) break;
|
|
1583
|
+
cut = i;
|
|
1584
|
+
moved = true;
|
|
1585
|
+
break;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
if (!moved && lastUser > firstSpillable && isSeverable(msgs, lastUser, firstSpillable)) {
|
|
1589
|
+
cut = lastUser;
|
|
1405
1590
|
}
|
|
1406
1591
|
}
|
|
1407
1592
|
|
|
1408
|
-
const lastUser = lastUserAskIndex(msgs, firstSpillable);
|
|
1409
1593
|
if (lastUser > firstSpillable && cut > lastUser) cut = lastUser;
|
|
1410
1594
|
|
|
1411
|
-
//
|
|
1412
|
-
//
|
|
1595
|
+
// 2-real-turn floor protects the last user ask: never drop it, and expand
|
|
1596
|
+
// the tail to two turns on a LONG thread. Do not steal the first
|
|
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).
|
|
1413
1599
|
if (lastUser > firstSpillable && countRealTurns(msgs, cut) < 2) {
|
|
1414
1600
|
for (let i = cut - 1; i > firstSpillable; i--) {
|
|
1415
|
-
if (isSeverable(msgs, i, firstSpillable)
|
|
1601
|
+
if (!isSeverable(msgs, i, firstSpillable) || countRealTurns(msgs, i) < 2) continue;
|
|
1602
|
+
if (countRealTurns(msgs, firstSpillable, i) < 2) continue;
|
|
1603
|
+
if (sliceChars(msgs, i) > budget) break;
|
|
1604
|
+
cut = i;
|
|
1605
|
+
break;
|
|
1416
1606
|
}
|
|
1417
1607
|
if (cut > lastUser) cut = lastUser;
|
|
1418
1608
|
}
|
|
@@ -1420,6 +1610,121 @@ export function cutTranscript(msgs, knobs = {}) {
|
|
|
1420
1610
|
return { cut, firstSpillable, lastUser, knobs: k };
|
|
1421
1611
|
}
|
|
1422
1612
|
|
|
1613
|
+
/** Opening slice used as a content-anchor when no session header is sent. */
|
|
1614
|
+
export const SPILL_CONTENT_ANCHOR_CHARS = 2048;
|
|
1615
|
+
|
|
1616
|
+
/**
|
|
1617
|
+
* Find a memoized / ledger bind for this request. Prefers an explicit
|
|
1618
|
+
* session key; otherwise matches a stored prefix so a growing content-anchor
|
|
1619
|
+
* (grokui AUTO sends no session header) still recalls the same contextId.
|
|
1620
|
+
*/
|
|
1621
|
+
export function lookupSpillMemo(spillMemo, sessionLedger, { sessionKey, corpus } = {}) {
|
|
1622
|
+
if (sessionKey && spillMemo?.has(sessionKey)) {
|
|
1623
|
+
return { key: sessionKey, source: 'memo', ...spillMemo.get(sessionKey) };
|
|
1624
|
+
}
|
|
1625
|
+
if (sessionKey && sessionLedger?.has(sessionKey)) {
|
|
1626
|
+
const led = sessionLedger.get(sessionKey);
|
|
1627
|
+
if (led?.contextId) return { key: sessionKey, source: 'ledger', restored: !led.corpus, ...led };
|
|
1628
|
+
}
|
|
1629
|
+
if (corpus && spillMemo) {
|
|
1630
|
+
for (const [k, v] of spillMemo) {
|
|
1631
|
+
if (typeof v?.corpus === 'string' && v.corpus.length && corpus.startsWith(v.corpus)) {
|
|
1632
|
+
return { key: k, source: 'memo-prefix', ...v };
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
const opening = corpus.slice(0, SPILL_CONTENT_ANCHOR_CHARS);
|
|
1636
|
+
for (const [k, v] of spillMemo) {
|
|
1637
|
+
if (typeof k !== 'string' || k.startsWith('sid:')) continue;
|
|
1638
|
+
if (opening.startsWith(k) || (k && k.startsWith(opening))) {
|
|
1639
|
+
return { key: k, source: 'memo-anchor', ...v };
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
return null;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
export function rememberSpillMemo(spillMemo, key, entry, { max = 32 } = {}) {
|
|
1647
|
+
if (!spillMemo || !key) return;
|
|
1648
|
+
spillMemo.set(key, entry);
|
|
1649
|
+
while (spillMemo.size > max) spillMemo.delete(spillMemo.keys().next().value);
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
/**
|
|
1653
|
+
* Decide first-bind vs later-turn recall. No I/O — bindCorpus is injected
|
|
1654
|
+
* by the caller (or mocked in tests).
|
|
1655
|
+
*
|
|
1656
|
+
* cold-bind — fire-and-forget first bind; this turn may go unspilled
|
|
1657
|
+
* await-pending — later turn while that bind is in flight; await, then tail
|
|
1658
|
+
* recall — contextId known; send tail (+ optional delta append)
|
|
1659
|
+
*/
|
|
1660
|
+
export function planConversationBind({ sessionKey, corpus, spillMemo, sessionLedger } = {}) {
|
|
1661
|
+
const key = sessionKey || (corpus ? corpus.slice(0, SPILL_CONTENT_ANCHOR_CHARS) : null);
|
|
1662
|
+
const prior = lookupSpillMemo(spillMemo, sessionLedger, { sessionKey: key, corpus });
|
|
1663
|
+
if (!prior) {
|
|
1664
|
+
return { action: 'cold-bind', send: 'full', key, corpus };
|
|
1665
|
+
}
|
|
1666
|
+
if (prior.pending && !prior.contextId) {
|
|
1667
|
+
return {
|
|
1668
|
+
action: 'await-pending',
|
|
1669
|
+
send: 'tail',
|
|
1670
|
+
key: prior.key || key,
|
|
1671
|
+
corpus,
|
|
1672
|
+
ready: prior.ready,
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
if (prior.contextId) {
|
|
1676
|
+
let delta = '';
|
|
1677
|
+
let append = false;
|
|
1678
|
+
if (prior.restored) {
|
|
1679
|
+
append = true;
|
|
1680
|
+
} else if (typeof prior.corpus === 'string' && corpus.startsWith(prior.corpus) && corpus.length > prior.corpus.length) {
|
|
1681
|
+
delta = corpus.slice(prior.corpus.length);
|
|
1682
|
+
append = true;
|
|
1683
|
+
}
|
|
1684
|
+
return {
|
|
1685
|
+
action: 'recall',
|
|
1686
|
+
send: 'tail',
|
|
1687
|
+
key: prior.key || key,
|
|
1688
|
+
corpus,
|
|
1689
|
+
contextId: prior.contextId,
|
|
1690
|
+
hash: prior.hash,
|
|
1691
|
+
reused: true,
|
|
1692
|
+
append,
|
|
1693
|
+
delta,
|
|
1694
|
+
restored: Boolean(prior.restored),
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
return { action: 'cold-bind', send: 'full', key, corpus };
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/**
|
|
1701
|
+
* Cut the transcript and plan the conversation bind. Tests use this instead
|
|
1702
|
+
* of standing up the proxy or a live gateway.
|
|
1703
|
+
*/
|
|
1704
|
+
export function planTranscriptSpill(msgs, {
|
|
1705
|
+
knobs,
|
|
1706
|
+
sessionKey,
|
|
1707
|
+
spillMemo,
|
|
1708
|
+
sessionLedger,
|
|
1709
|
+
corpusChars = 0,
|
|
1710
|
+
adapt = false,
|
|
1711
|
+
persist = false,
|
|
1712
|
+
...cutOpts
|
|
1713
|
+
} = {}) {
|
|
1714
|
+
const adapted = applySpillCut(msgs, {
|
|
1715
|
+
knobs,
|
|
1716
|
+
corpusChars,
|
|
1717
|
+
adapt,
|
|
1718
|
+
persist,
|
|
1719
|
+
...cutOpts,
|
|
1720
|
+
});
|
|
1721
|
+
const empty = { ...adapted, corpus: '', bindPlan: null };
|
|
1722
|
+
if (adapted.cut <= adapted.firstSpillable) return empty;
|
|
1723
|
+
const corpus = msgs.slice(adapted.firstSpillable, adapted.cut).map(msgText).filter(Boolean).join('\n\n');
|
|
1724
|
+
const bindPlan = planConversationBind({ sessionKey, corpus, spillMemo, sessionLedger });
|
|
1725
|
+
return { ...adapted, corpus, bindPlan };
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1423
1728
|
function stubForCut(msgs, cut, opts) {
|
|
1424
1729
|
return stubBoundFileResults(msgs, {
|
|
1425
1730
|
boundFiles: opts.boundFiles,
|
|
@@ -1429,6 +1734,7 @@ function stubForCut(msgs, cut, opts) {
|
|
|
1429
1734
|
aggressive: Boolean(opts.aggressive),
|
|
1430
1735
|
budget: opts.budget,
|
|
1431
1736
|
keepTail: opts.keepTail,
|
|
1737
|
+
recall: opts.recall,
|
|
1432
1738
|
});
|
|
1433
1739
|
}
|
|
1434
1740
|
|
|
@@ -1452,6 +1758,7 @@ export function applySpillCut(msgs, {
|
|
|
1452
1758
|
home,
|
|
1453
1759
|
dollarX,
|
|
1454
1760
|
lastSend,
|
|
1761
|
+
recall,
|
|
1455
1762
|
} = {}) {
|
|
1456
1763
|
const persistOpts = { persist, file, home };
|
|
1457
1764
|
let k = sanitizeKnobs(knobs || getLiveKnobs(persistOpts));
|
|
@@ -1472,7 +1779,7 @@ export function applySpillCut(msgs, {
|
|
|
1472
1779
|
// 300-result storm is not forwarded at full size.
|
|
1473
1780
|
const stubFrom = plan.firstSpillable >= 0 ? plan.firstSpillable : 0;
|
|
1474
1781
|
let stubbed = stubForCut(msgs, stubFrom, {
|
|
1475
|
-
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,
|
|
1476
1783
|
});
|
|
1477
1784
|
let sentChars = sliceChars(stubbed.messages, stubFrom);
|
|
1478
1785
|
const corpus = Math.max(Number(corpusChars) || 0, sentChars);
|
|
@@ -1492,7 +1799,7 @@ export function applySpillCut(msgs, {
|
|
|
1492
1799
|
action = decision.action;
|
|
1493
1800
|
if (decision.recut) {
|
|
1494
1801
|
stubbed = stubForCut(msgs, stubFrom, {
|
|
1495
|
-
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,
|
|
1496
1803
|
});
|
|
1497
1804
|
sentChars = sliceChars(stubbed.messages, stubFrom);
|
|
1498
1805
|
ratio = spillRatio(corpus, sentChars);
|
|
@@ -1518,7 +1825,7 @@ export function applySpillCut(msgs, {
|
|
|
1518
1825
|
};
|
|
1519
1826
|
|
|
1520
1827
|
let stubbed = stubForCut(msgs, plan.cut, {
|
|
1521
|
-
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,
|
|
1522
1829
|
});
|
|
1523
1830
|
let stats = measure(plan.cut, stubbed, k);
|
|
1524
1831
|
let action = 'hold';
|
|
@@ -1539,7 +1846,7 @@ export function applySpillCut(msgs, {
|
|
|
1539
1846
|
plan = cutTranscript(msgs, k);
|
|
1540
1847
|
if (plan.cut > plan.firstSpillable) {
|
|
1541
1848
|
stubbed = stubForCut(msgs, plan.cut, {
|
|
1542
|
-
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,
|
|
1543
1850
|
});
|
|
1544
1851
|
stats = measure(plan.cut, stubbed, k);
|
|
1545
1852
|
}
|
|
@@ -1562,6 +1869,119 @@ export function applySpillCut(msgs, {
|
|
|
1562
1869
|
};
|
|
1563
1870
|
}
|
|
1564
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
|
+
|
|
1565
1985
|
/** Session counters the HUD reads off /v1/info. */
|
|
1566
1986
|
export function createSpillStats() {
|
|
1567
1987
|
return {
|
package/lib/x402.js
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
* payTo: "<wallet>", resource, description, maxTimeoutSeconds,
|
|
23
23
|
* extra: { facilitator, feePayer, symbol, billedUsd, tokenUsd,
|
|
24
24
|
* pricedAt, pricing: "markup"|"counterfactual",
|
|
25
|
-
*
|
|
25
|
+
* billedUsd, directUsd?, savedUsd?, savesVsDirect?,
|
|
26
26
|
* acquire?: { method: "spl-token-wrap", steps: WRAP_ACQUIRE_STEPS } } } ],
|
|
27
27
|
* error: "payment required",
|
|
28
28
|
* help: "Wrap ix has 9 accounts. Program CPIs the deposit — do not send a separate TransferChecked. 0x6a = NotEnoughAccounts (old 5-account wrap is dead)." }
|
|
@@ -232,12 +232,21 @@ export function receiptLine(accept, settle) {
|
|
|
232
232
|
// reaches the spill threshold, so there is nothing to compress and nothing
|
|
233
233
|
// to save — which is worth saying out loud, because the fix on the caller's
|
|
234
234
|
// side is to BIND a corpus, not to change models.
|
|
235
|
-
const
|
|
235
|
+
const billed = Number(x.billedUsd);
|
|
236
|
+
const direct = x.directUsd != null ? Number(x.directUsd) : null;
|
|
237
|
+
const saved = x.savedUsd != null ? Number(x.savedUsd)
|
|
238
|
+
: (Number.isFinite(billed) && Number.isFinite(direct) ? Math.max(0, direct - billed) : null);
|
|
239
|
+
const ratio = x.savesVsDirect != null ? Number(x.savesVsDirect)
|
|
240
|
+
: (Number.isFinite(billed) && billed > 0 && Number.isFinite(direct) ? direct / billed : null);
|
|
241
|
+
// Wallet path: OpenRouter price, plus 33% of savings vs direct when any.
|
|
242
|
+
// Never print extra.markup — that field is leftover 3× and is not the quote.
|
|
236
243
|
const saves = ratio != null
|
|
237
244
|
? (ratio >= 1.05
|
|
238
245
|
? ` (${ratio.toFixed(1)}× cheaper than direct)`
|
|
239
|
-
: ' (at
|
|
240
|
-
: (
|
|
246
|
+
: ' (at OpenRouter price — nothing to compress; bind a corpus to save)')
|
|
247
|
+
: (Number.isFinite(saved) && saved > 0
|
|
248
|
+
? ` ($${saved.toFixed(4)} saved vs direct)`
|
|
249
|
+
: ' (at OpenRouter price — short body)');
|
|
241
250
|
const tx = settle?.transaction || settle?.txHash || settle?.signature;
|
|
242
251
|
const rail = railOf(accept);
|
|
243
252
|
return `paid ${usd}${saves}${rail ? ` · rail ${rail}` : ''}${tx ? ` · tx ${tx}` : ''}`;
|