sandoichi 0.1.0
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/LICENSE +21 -0
- package/README.md +25 -0
- package/index.mjs +54 -0
- package/package.json +27 -0
- package/src/active-session.mjs +136 -0
- package/src/cache-attribution.mjs +182 -0
- package/src/context-transform.mjs +492 -0
- package/src/core.mjs +258 -0
- package/src/history-budget.mjs +36 -0
- package/src/history-dedupe.mjs +98 -0
- package/src/history-shake.mjs +66 -0
- package/src/history-structure.mjs +28 -0
- package/src/hook-cli.mjs +118 -0
- package/src/mcp-server.mjs +45 -0
- package/src/metrics-cli.mjs +34 -0
- package/src/metrics.mjs +390 -0
- package/src/provider-ledger.mjs +138 -0
- package/src/provider-usage.mjs +224 -0
- package/src/proxy-metrics.mjs +34 -0
- package/src/proxy.mjs +212 -0
- package/src/routing.mjs +89 -0
- package/src/secret-redaction.mjs +21 -0
- package/src/semantic-compactor.mjs +218 -0
- package/src/statusline.mjs +100 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { estimateTokens } from './core.mjs';
|
|
2
|
+
import { dedupeHistory } from './history-dedupe.mjs';
|
|
3
|
+
import { selectHistoryCandidates, validateMaxHistoryTokens } from './history-budget.mjs';
|
|
4
|
+
import { shakeHistoricalResult } from './history-shake.mjs';
|
|
5
|
+
import { compactHistoricalStructure } from './history-structure.mjs';
|
|
6
|
+
|
|
7
|
+
const SUPERSEDED = '[sando superseded by newer read]';
|
|
8
|
+
const USELESS = '[sando elided useless success]';
|
|
9
|
+
const USELESS_SUCCESSES = new Set([
|
|
10
|
+
'command completed successfully with no output.',
|
|
11
|
+
'no output.',
|
|
12
|
+
'(no output)',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
function object(value) {
|
|
16
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function estimate(body) {
|
|
20
|
+
const json = JSON.stringify(body);
|
|
21
|
+
if (json === undefined) throw new TypeError('body must be JSON-serializable');
|
|
22
|
+
return estimateTokens(json);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseArguments(value) {
|
|
26
|
+
if (object(value)) return value;
|
|
27
|
+
if (typeof value !== 'string') return null;
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(value);
|
|
30
|
+
return object(parsed) ? parsed : null;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readIdentity(name, input) {
|
|
37
|
+
if (typeof name !== 'string' || name.toLowerCase() !== 'read' || !object(input)) return null;
|
|
38
|
+
const paths = ['file_path', 'path'].filter((key) => Object.hasOwn(input, key));
|
|
39
|
+
if (paths.length !== 1 || typeof input[paths[0]] !== 'string' || input[paths[0]].length === 0) return null;
|
|
40
|
+
|
|
41
|
+
let path = input[paths[0]];
|
|
42
|
+
let range = null;
|
|
43
|
+
const suffix = path.match(/^(.*):(\d+)-(\d+)$/);
|
|
44
|
+
const selectorKeys = ['offset', 'limit', 'line_start', 'line_end', 'start_line', 'end_line']
|
|
45
|
+
.filter((key) => Object.hasOwn(input, key));
|
|
46
|
+
if (suffix && selectorKeys.length === 0) {
|
|
47
|
+
path = suffix[1];
|
|
48
|
+
range = [Number(suffix[2]), Number(suffix[3])];
|
|
49
|
+
} else if (selectorKeys.length > 0) {
|
|
50
|
+
if (selectorKeys.length !== 2) return null;
|
|
51
|
+
if (selectorKeys.includes('offset') && selectorKeys.includes('limit')) {
|
|
52
|
+
const { offset, limit } = input;
|
|
53
|
+
if (!Number.isInteger(offset) || offset < 0 || !Number.isInteger(limit) || limit < 1) return null;
|
|
54
|
+
range = [offset, offset + limit - 1];
|
|
55
|
+
} else {
|
|
56
|
+
const pair = selectorKeys.includes('line_start') ? ['line_start', 'line_end'] : ['start_line', 'end_line'];
|
|
57
|
+
if (!pair.every((key) => selectorKeys.includes(key))) return null;
|
|
58
|
+
const [start, end] = pair.map((key) => input[key]);
|
|
59
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start) return null;
|
|
60
|
+
range = [start, end];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (range && range[1] < range[0]) return null;
|
|
64
|
+
return { path, range };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function covers(newer, older) {
|
|
68
|
+
if (newer.path !== older.path) return false;
|
|
69
|
+
if (newer.range === null) return true;
|
|
70
|
+
return older.range !== null && newer.range[0] <= older.range[0] && newer.range[1] >= older.range[1];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resultError(item, text) {
|
|
74
|
+
if (item.is_error === true || item.error !== undefined || ['error', 'failed'].includes(item.status)) return true;
|
|
75
|
+
return /^\s*(?:error|failed|failure)\b[:\s-]*/i.test(text);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function useless(text) {
|
|
79
|
+
return USELESS_SUCCESSES.has(text.trim().toLowerCase());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resultText(value) {
|
|
83
|
+
if (typeof value === 'string') return value;
|
|
84
|
+
if (Array.isArray(value) && value.length > 0
|
|
85
|
+
&& value.every((item) => ['text', 'input_text'].includes(item?.type) && typeof item.text === 'string')) {
|
|
86
|
+
return value.map((item) => item.text).join('\n');
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Collapsing a run of text blocks into one drops every block after index 0. Any
|
|
92
|
+
// `cache_control` breakpoint the host (e.g. Claude Code) placed on a later block
|
|
93
|
+
// would vanish silently, costing a cache read on every subsequent turn. Carry the
|
|
94
|
+
// deepest marker onto the surviving block: cache semantics cover everything up to
|
|
95
|
+
// and including a marked block, so the collapsed block inherits that boundary.
|
|
96
|
+
function collapseTextBlocks(blocks, value) {
|
|
97
|
+
const collapsed = { ...blocks[0], text: value };
|
|
98
|
+
const marker = blocks.findLast((block) => block?.cache_control !== undefined)?.cache_control;
|
|
99
|
+
if (marker !== undefined) collapsed.cache_control = marker;
|
|
100
|
+
return [collapsed];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function replaceResult(item, key, value) {
|
|
104
|
+
if (typeof item[key] === 'string' && typeof value === 'string') item[key] = value;
|
|
105
|
+
else if (Array.isArray(item[key]) && Array.isArray(value)) item[key] = value;
|
|
106
|
+
else if (Array.isArray(item[key]) && typeof value === 'string'
|
|
107
|
+
&& item[key].every((block) => ['text', 'input_text'].includes(block?.type))) {
|
|
108
|
+
item[key] = collapseTextBlocks(item[key], value);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function collectAnthropic(body) {
|
|
113
|
+
if (!Array.isArray(body.messages)) return [];
|
|
114
|
+
const entries = [];
|
|
115
|
+
body.messages.forEach((message, position) => {
|
|
116
|
+
if (!Array.isArray(message?.content)) return;
|
|
117
|
+
message.content.forEach((item) => {
|
|
118
|
+
if (item?.type === 'tool_use') {
|
|
119
|
+
entries.push({ kind: 'call', id: item.id, name: item.name, input: object(item.input) ? item.input : null, position });
|
|
120
|
+
} else if (item?.type === 'tool_result') {
|
|
121
|
+
entries.push({
|
|
122
|
+
kind: 'result', id: item.tool_use_id, item, key: 'content', position,
|
|
123
|
+
current: position === body.messages.length - 1,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
return entries;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function collectChat(body) {
|
|
132
|
+
if (!Array.isArray(body.messages)) return [];
|
|
133
|
+
const entries = [];
|
|
134
|
+
const lastNonToolPosition = body.messages.reduce(
|
|
135
|
+
(last, message, position) => message?.role === 'tool' ? last : position,
|
|
136
|
+
-1,
|
|
137
|
+
);
|
|
138
|
+
body.messages.forEach((message, position) => {
|
|
139
|
+
if (Array.isArray(message?.tool_calls)) {
|
|
140
|
+
message.tool_calls.forEach((item) => {
|
|
141
|
+
if (item?.type === 'function' && object(item.function)) {
|
|
142
|
+
entries.push({
|
|
143
|
+
kind: 'call', id: item.id, name: item.function.name,
|
|
144
|
+
input: parseArguments(item.function.arguments), position,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (message?.role === 'tool') {
|
|
150
|
+
entries.push({
|
|
151
|
+
kind: 'result', id: message.tool_call_id, item: message, key: 'content', position,
|
|
152
|
+
current: position > lastNonToolPosition,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
return entries;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function collectResponses(body) {
|
|
160
|
+
if (!Array.isArray(body.input)) return [];
|
|
161
|
+
const entries = [];
|
|
162
|
+
const lastCallPosition = body.input.reduce(
|
|
163
|
+
(last, item, position) => ['function_call', 'custom_tool_call'].includes(item?.type) ? position : last,
|
|
164
|
+
-1,
|
|
165
|
+
);
|
|
166
|
+
body.input.forEach((item, position) => {
|
|
167
|
+
if (['function_call', 'custom_tool_call'].includes(item?.type)) {
|
|
168
|
+
entries.push({
|
|
169
|
+
kind: 'call', id: item.call_id, name: item.name,
|
|
170
|
+
input: parseArguments(item.arguments ?? item.input), position,
|
|
171
|
+
});
|
|
172
|
+
} else if (['function_call_output', 'custom_tool_call_output'].includes(item?.type)) {
|
|
173
|
+
entries.push({
|
|
174
|
+
kind: 'result', id: item.call_id, item, key: 'output', position,
|
|
175
|
+
current: position > lastCallPosition,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
return entries;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const COLLECTORS = {
|
|
183
|
+
anthropic: collectAnthropic,
|
|
184
|
+
'openai-chat': collectChat,
|
|
185
|
+
'openai-responses': collectResponses,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
function historyRecords(entries, calls) {
|
|
189
|
+
return entries.flatMap((entry) => {
|
|
190
|
+
if (entry.kind !== 'result') return [];
|
|
191
|
+
const call = calls.get(entry.id);
|
|
192
|
+
const text = resultText(entry.item[entry.key]);
|
|
193
|
+
if (!call || text === null) return [];
|
|
194
|
+
const isError = resultError(entry.item, text);
|
|
195
|
+
return [{
|
|
196
|
+
id: entry.id,
|
|
197
|
+
toolName: call.name,
|
|
198
|
+
input: call.input,
|
|
199
|
+
output: entry.item[entry.key],
|
|
200
|
+
current: entry.current,
|
|
201
|
+
historical: !entry.current,
|
|
202
|
+
safe: !isError,
|
|
203
|
+
isError,
|
|
204
|
+
position: entry.position,
|
|
205
|
+
estimatedTokens: estimateTokens(text),
|
|
206
|
+
entry,
|
|
207
|
+
}];
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function collectHistoryRecords(provider, body) {
|
|
212
|
+
const collector = COLLECTORS[provider];
|
|
213
|
+
if (!collector) return [];
|
|
214
|
+
const entries = collector(body);
|
|
215
|
+
const calls = new Map();
|
|
216
|
+
const results = new Map();
|
|
217
|
+
const ambiguous = new Set();
|
|
218
|
+
for (const entry of entries) {
|
|
219
|
+
const map = entry.kind === 'call' ? calls : results;
|
|
220
|
+
if (typeof entry.id !== 'string' || entry.id.length === 0 || map.has(entry.id)) ambiguous.add(entry.id);
|
|
221
|
+
else map.set(entry.id, entry);
|
|
222
|
+
}
|
|
223
|
+
for (const id of ambiguous) {
|
|
224
|
+
calls.delete(id);
|
|
225
|
+
results.delete(id);
|
|
226
|
+
}
|
|
227
|
+
return historyRecords(entries, calls);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function detectProviderBody(body, headers = {}) {
|
|
231
|
+
if (!object(body)) return null;
|
|
232
|
+
const header = typeof headers.get === 'function'
|
|
233
|
+
? headers.get('anthropic-version')
|
|
234
|
+
: headers['anthropic-version'] ?? headers['Anthropic-Version'];
|
|
235
|
+
if (header && Array.isArray(body.messages)) return 'anthropic';
|
|
236
|
+
|
|
237
|
+
const responses = Array.isArray(body.input) && body.input.some((item) =>
|
|
238
|
+
['function_call', 'function_call_output', 'custom_tool_call', 'custom_tool_call_output'].includes(item?.type));
|
|
239
|
+
if (responses) return 'openai-responses';
|
|
240
|
+
if (!Array.isArray(body.messages)) return null;
|
|
241
|
+
const anthropic = body.messages.some((message) => Array.isArray(message?.content)
|
|
242
|
+
&& message.content.some((item) => ['tool_use', 'tool_result'].includes(item?.type)));
|
|
243
|
+
const chat = body.messages.some((message) => Array.isArray(message?.tool_calls) || message?.role === 'tool');
|
|
244
|
+
if (anthropic === chat) return null;
|
|
245
|
+
return anthropic ? 'anthropic' : 'openai-chat';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function listSemanticCandidates({ provider, body, model } = {}) {
|
|
249
|
+
const selectedProvider = provider ?? detectProviderBody(body);
|
|
250
|
+
const selectedModel = model ?? (typeof body?.model === 'string' ? body.model : null);
|
|
251
|
+
return collectHistoryRecords(selectedProvider, body)
|
|
252
|
+
.filter((record) => record.safe && record.historical)
|
|
253
|
+
.map((record) => ({
|
|
254
|
+
id: record.id,
|
|
255
|
+
model: selectedModel,
|
|
256
|
+
toolName: record.toolName,
|
|
257
|
+
text: resultText(record.output),
|
|
258
|
+
current: record.current,
|
|
259
|
+
historical: record.historical,
|
|
260
|
+
isError: record.isError,
|
|
261
|
+
estimatedTokens: record.estimatedTokens,
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function carriesBreakpoint(value) {
|
|
266
|
+
if (Array.isArray(value)) return value.some(carriesBreakpoint);
|
|
267
|
+
if (!object(value)) return false;
|
|
268
|
+
if (value.cache_control !== undefined) return true;
|
|
269
|
+
return Object.values(value).some(carriesBreakpoint);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Minimum fraction of the re-prefilled suffix a rewrite must reclaim to pay for itself.
|
|
274
|
+
*
|
|
275
|
+
* Derived from Anthropic's published multipliers (cache read 0.1x, 5-minute cache
|
|
276
|
+
* write 1.25x of base input). With `S` tokens reclaimed, `P` tokens of suffix forced
|
|
277
|
+
* back through a cache write, and `K` further turns to amortize over:
|
|
278
|
+
*
|
|
279
|
+
* rewrite: 1.25(P-S) + 0.10(P-S)K leave: 0.10P(K+1)
|
|
280
|
+
* rewrite wins <=> S/P > 1.15 / (1.25 + 0.10K)
|
|
281
|
+
*
|
|
282
|
+
* The threshold is a RATIO and is independent of P — an absolute token budget is the
|
|
283
|
+
* wrong parameterization. K=0 needs 92%, K=10 needs 51%, K=50 needs 18%. Sando cannot
|
|
284
|
+
* know K (how much longer the session runs), so this uses the K=10 point: a rewrite
|
|
285
|
+
* must reclaim over half the suffix it invalidates. Conservative for short sessions,
|
|
286
|
+
* slightly cautious for very long ones.
|
|
287
|
+
*/
|
|
288
|
+
const DEFAULT_CACHE_REWRITE_RATIO = 0.51;
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Below this idle time, the ratio guard above governs. At or beyond it, the host's
|
|
292
|
+
* prompt cache has already expired on its own — Anthropic's longest published ephemeral
|
|
293
|
+
* TTL is 1h (measured on a real Claude Code request: `{"type":"ephemeral","ttl":"1h"}`),
|
|
294
|
+
* so a request idle this long forces a full cache-write regardless of what Sando does.
|
|
295
|
+
* Rewriting costs nothing extra at that point, so the ratio guard is bypassed entirely.
|
|
296
|
+
* Set `policy.cacheIdleFlushMs: null` to disable (ratio guard always governs).
|
|
297
|
+
*/
|
|
298
|
+
const DEFAULT_CACHE_IDLE_FLUSH_MS = 65 * 60_000;
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Token counts of the suffix following each message index.
|
|
302
|
+
*
|
|
303
|
+
* Position relative to a breakpoint is the wrong metric here. A real Claude Code
|
|
304
|
+
* request (measured: 90 tools, 2 system markers, 1 message marker, all ttl 1h) marks
|
|
305
|
+
* the LAST message, moving the breakpoint forward every turn so the growing
|
|
306
|
+
* conversation stays one cached span — the same strategy Cline documents. Protecting
|
|
307
|
+
* everything at or before that marker would protect the entire conversation and
|
|
308
|
+
* disable the transform outright (measured: 17.1% mechanical saving -> 0%).
|
|
309
|
+
*
|
|
310
|
+
* What actually matters is how much has to be re-prefilled, which is the size of the
|
|
311
|
+
* suffix after the rewritten message.
|
|
312
|
+
*/
|
|
313
|
+
function suffixTokensByPosition(body) {
|
|
314
|
+
const messages = Array.isArray(body?.messages) ? body.messages : [];
|
|
315
|
+
const suffix = new Array(messages.length).fill(0);
|
|
316
|
+
let running = 0;
|
|
317
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
318
|
+
suffix[index] = running;
|
|
319
|
+
running += estimateTokens(JSON.stringify(messages[index]) ?? '');
|
|
320
|
+
}
|
|
321
|
+
return suffix;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function transformProviderRequest({ provider, body, policy, idleMs } = {}) {
|
|
325
|
+
const clone = structuredClone(body);
|
|
326
|
+
const estimatedInputTokens = estimate(body);
|
|
327
|
+
const selectedProvider = provider ?? detectProviderBody(body);
|
|
328
|
+
const collector = COLLECTORS[selectedProvider];
|
|
329
|
+
let supersededReads = 0;
|
|
330
|
+
let elidedUselessSuccesses = 0;
|
|
331
|
+
let deduplicatedResults = 0;
|
|
332
|
+
let compactedStructures = 0;
|
|
333
|
+
let shakenResults = 0;
|
|
334
|
+
const maxHistoryTokens = object(policy) && Object.hasOwn(policy, 'maxHistoryTokens')
|
|
335
|
+
? validateMaxHistoryTokens(policy.maxHistoryTokens)
|
|
336
|
+
: null;
|
|
337
|
+
const budgetTriggered = maxHistoryTokens !== null
|
|
338
|
+
&& BigInt(estimatedInputTokens) * 5n > BigInt(maxHistoryTokens) * 4n;
|
|
339
|
+
// Don't rewrite warm cached history unless the rewrite reclaims enough of the suffix
|
|
340
|
+
// it invalidates to pay the cache-write premium. Only applies when the host actually
|
|
341
|
+
// asked for caching — with no breakpoint there is no warm prefix to protect.
|
|
342
|
+
// Set `policy.cacheRewriteRatio: 0` to disable.
|
|
343
|
+
const cacheRewriteRatio = object(policy) && Object.hasOwn(policy, 'cacheRewriteRatio')
|
|
344
|
+
? policy.cacheRewriteRatio
|
|
345
|
+
: DEFAULT_CACHE_REWRITE_RATIO;
|
|
346
|
+
if (typeof cacheRewriteRatio !== 'number' || !(cacheRewriteRatio >= 0) || cacheRewriteRatio > 1) {
|
|
347
|
+
throw new TypeError('cacheRewriteRatio must be a number between 0 and 1');
|
|
348
|
+
}
|
|
349
|
+
const cacheIdleFlushMs = object(policy) && Object.hasOwn(policy, 'cacheIdleFlushMs')
|
|
350
|
+
? policy.cacheIdleFlushMs
|
|
351
|
+
: DEFAULT_CACHE_IDLE_FLUSH_MS;
|
|
352
|
+
if (cacheIdleFlushMs !== null && (typeof cacheIdleFlushMs !== 'number' || !(cacheIdleFlushMs >= 0))) {
|
|
353
|
+
throw new TypeError('cacheIdleFlushMs must be a non-negative number or null');
|
|
354
|
+
}
|
|
355
|
+
const cacheWarm = cacheRewriteRatio > 0 && carriesBreakpoint(clone);
|
|
356
|
+
const suffixTokens = cacheWarm ? suffixTokensByPosition(clone) : null;
|
|
357
|
+
// The host's own cache has already gone cold from inactivity: any rewrite here is
|
|
358
|
+
// free, since the provider will cache-write the whole prefix again regardless.
|
|
359
|
+
const idleCold = cacheIdleFlushMs !== null && typeof idleMs === 'number' && idleMs >= cacheIdleFlushMs;
|
|
360
|
+
// `reclaimed` is how many tokens this particular rewrite removes.
|
|
361
|
+
const cacheProtected = (entry, reclaimed) => {
|
|
362
|
+
if (!cacheWarm || idleCold) return false;
|
|
363
|
+
const suffix = suffixTokens[entry.position] ?? 0;
|
|
364
|
+
if (suffix === 0) return false;
|
|
365
|
+
return reclaimed / suffix < cacheRewriteRatio;
|
|
366
|
+
};
|
|
367
|
+
let cacheProtectedSkips = 0;
|
|
368
|
+
const reclaimedTokens = (before, after) =>
|
|
369
|
+
Math.max(0, estimateTokens(before) - estimateTokens(after));
|
|
370
|
+
|
|
371
|
+
if (collector) {
|
|
372
|
+
const entries = collector(clone);
|
|
373
|
+
const calls = new Map();
|
|
374
|
+
const results = new Map();
|
|
375
|
+
const ambiguous = new Set();
|
|
376
|
+
for (const entry of entries) {
|
|
377
|
+
const map = entry.kind === 'call' ? calls : results;
|
|
378
|
+
if (typeof entry.id !== 'string' || entry.id.length === 0 || map.has(entry.id)) ambiguous.add(entry.id);
|
|
379
|
+
else map.set(entry.id, entry);
|
|
380
|
+
}
|
|
381
|
+
for (const id of ambiguous) {
|
|
382
|
+
calls.delete(id);
|
|
383
|
+
results.delete(id);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const reads = [];
|
|
387
|
+
for (const call of calls.values()) {
|
|
388
|
+
const result = results.get(call.id);
|
|
389
|
+
const identity = readIdentity(call.name, call.input);
|
|
390
|
+
const text = result && resultText(result.item[result.key]);
|
|
391
|
+
if (!result || !identity || text === null) continue;
|
|
392
|
+
if (!resultError(result.item, text)) reads.push({ call, result, identity, text });
|
|
393
|
+
}
|
|
394
|
+
reads.sort((a, b) => a.call.position - b.call.position);
|
|
395
|
+
for (let index = 0; index < reads.length; index += 1) {
|
|
396
|
+
const old = reads[index];
|
|
397
|
+
if (old.result.current) continue;
|
|
398
|
+
const newer = reads.slice(index + 1).find((candidate) =>
|
|
399
|
+
covers(candidate.identity, old.identity) && !useless(candidate.text));
|
|
400
|
+
if (!newer) continue;
|
|
401
|
+
if (cacheProtected(old.result, reclaimedTokens(old.text, SUPERSEDED))) { cacheProtectedSkips += 1; continue; }
|
|
402
|
+
replaceResult(old.result.item, old.result.key, SUPERSEDED);
|
|
403
|
+
supersededReads += 1;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
for (const [id, result] of results) {
|
|
407
|
+
if (!calls.has(id) || result.current) continue;
|
|
408
|
+
const text = resultText(result.item[result.key]);
|
|
409
|
+
if (text === null) continue;
|
|
410
|
+
if (text === SUPERSEDED || resultError(result.item, text) || !useless(text)) continue;
|
|
411
|
+
if (cacheProtected(result, reclaimedTokens(text, USELESS))) { cacheProtectedSkips += 1; continue; }
|
|
412
|
+
replaceResult(result.item, result.key, USELESS);
|
|
413
|
+
elidedUselessSuccesses += 1;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const records = historyRecords(entries, calls);
|
|
417
|
+
const candidates = maxHistoryTokens === null
|
|
418
|
+
? records.filter((record) => record.safe && record.historical)
|
|
419
|
+
: selectHistoryCandidates({ bodyTokens: estimatedInputTokens, maxHistoryTokens, candidates: records });
|
|
420
|
+
const candidateIds = new Set(candidates.map((candidate) => candidate.id));
|
|
421
|
+
const reductions = dedupeHistory(records);
|
|
422
|
+
const recordsById = new Map(records.map((record) => [record.id, record]));
|
|
423
|
+
for (const reduced of reductions.entries) {
|
|
424
|
+
if (!candidateIds.has(reduced.id)) continue;
|
|
425
|
+
const original = recordsById.get(reduced.id);
|
|
426
|
+
if (!original || reduced.output === original.output) continue;
|
|
427
|
+
if (cacheProtected(original.entry, reclaimedTokens(
|
|
428
|
+
resultText(original.output) ?? '', resultText(reduced.output) ?? ''))) { cacheProtectedSkips += 1; continue; }
|
|
429
|
+
replaceResult(original.entry.item, original.entry.key, reduced.output);
|
|
430
|
+
deduplicatedResults += 1;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
for (const record of records) {
|
|
434
|
+
if (!candidateIds.has(record.id)) continue;
|
|
435
|
+
const text = resultText(record.entry.item[record.entry.key]);
|
|
436
|
+
if (text === null) continue;
|
|
437
|
+
const compacted = compactHistoricalStructure({
|
|
438
|
+
toolName: record.toolName,
|
|
439
|
+
text,
|
|
440
|
+
historical: record.historical,
|
|
441
|
+
isError: record.isError,
|
|
442
|
+
});
|
|
443
|
+
if (compacted === text) continue;
|
|
444
|
+
if (cacheProtected(record.entry, reclaimedTokens(text, compacted))) { cacheProtectedSkips += 1; continue; }
|
|
445
|
+
replaceResult(record.entry.item, record.entry.key, compacted);
|
|
446
|
+
compactedStructures += 1;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (maxHistoryTokens !== null && budgetTriggered) {
|
|
450
|
+
for (const record of records) {
|
|
451
|
+
if (!candidateIds.has(record.id)) continue;
|
|
452
|
+
const text = resultText(record.entry.item[record.entry.key]);
|
|
453
|
+
if (text === null) continue;
|
|
454
|
+
const shaken = shakeHistoricalResult({
|
|
455
|
+
toolName: record.toolName,
|
|
456
|
+
text,
|
|
457
|
+
historical: record.historical,
|
|
458
|
+
isError: record.isError,
|
|
459
|
+
});
|
|
460
|
+
if (!shaken.changed) continue;
|
|
461
|
+
if (cacheProtected(record.entry, reclaimedTokens(text, shaken.text))) { cacheProtectedSkips += 1; continue; }
|
|
462
|
+
replaceResult(record.entry.item, record.entry.key, shaken.text);
|
|
463
|
+
shakenResults += 1;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const reasons = [];
|
|
469
|
+
if (supersededReads > 0) reasons.push('superseded-read');
|
|
470
|
+
if (elidedUselessSuccesses > 0) reasons.push('useless-success');
|
|
471
|
+
if (deduplicatedResults > 0) reasons.push('duplicate-history');
|
|
472
|
+
if (compactedStructures > 0) reasons.push('repeated-lines');
|
|
473
|
+
if (shakenResults > 0) reasons.push('history-shake');
|
|
474
|
+
return {
|
|
475
|
+
body: clone,
|
|
476
|
+
changed: reasons.length > 0,
|
|
477
|
+
reasons,
|
|
478
|
+
stats: {
|
|
479
|
+
estimatedInputTokens,
|
|
480
|
+
estimatedOutputTokens: estimate(clone),
|
|
481
|
+
supersededReads,
|
|
482
|
+
elidedUselessSuccesses,
|
|
483
|
+
deduplicatedResults,
|
|
484
|
+
compactedStructures,
|
|
485
|
+
shakenResults,
|
|
486
|
+
budgetTriggered,
|
|
487
|
+
cacheProtectedSkips,
|
|
488
|
+
cacheRewriteRatio: cacheWarm ? cacheRewriteRatio : null,
|
|
489
|
+
cacheIdleFlushed: cacheWarm ? idleCold : false,
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
}
|