sandoichi 0.4.2 → 0.5.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/README.md +5 -1
- package/index.mjs +5 -0
- package/package.json +1 -1
- package/src/accounting-cli.mjs +1 -1
- package/src/artifact-lifecycle.mjs +67 -0
- package/src/artifact-recovery.mjs +5 -0
- package/src/artifact-store.mjs +2 -1
- package/src/cache-attribution.mjs +13 -3
- package/src/context-transform.mjs +129 -17
- package/src/core.mjs +283 -34
- package/src/history-archive.mjs +80 -0
- package/src/hook-cli.mjs +17 -1
- package/src/lazy-mcp-gateway.mjs +10 -6
- package/src/mcp-server.mjs +47 -12
- package/src/metrics.mjs +4 -3
- package/src/provider-usage.mjs +103 -23
- package/src/proxy.mjs +12 -5
- package/src/result-disclosure.mjs +8 -2
- package/src/slice.mjs +419 -0
- package/src/statusline.mjs +5 -9
- package/src/telemetry.mjs +101 -19
package/README.md
CHANGED
|
@@ -26,7 +26,11 @@ Built-ins stay enabled. Profiles are declarative and local to the current projec
|
|
|
26
26
|
|
|
27
27
|
The library requires Node.js `>=22.22.0 <23` and has no runtime dependencies. Installing it does not install or enable the plugin. The plugin remains the supported host surface; this package exports the bounded output/disclosure runtime, context footprint audit, F1/F3/F4 evidence APIs, provider usage report, paired accounting, and explicit proxy API. Host hooks and MCP registration remain outside the package API.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
The provider proxy is pass-through unless request transformation is explicitly enabled.
|
|
30
|
+
|
|
31
|
+
The recoverable-history strategy is opt-in and keeps eligible results inline when they are below 3,072 bytes by default. Provider-boundary replay measurements are diagnostic paired evidence; they do not predict end-to-end agent behavior or provider billing across workloads.
|
|
32
|
+
|
|
33
|
+
`computeWeightedUsage` and `summarizePairedSessions` keep mechanical reduction, weighted cost units, and paired-session evidence separate from the provider-usage report's reported cost provenance and coverage. Host-reported list estimates are not billed-cost records. The library does not install hooks, register MCP servers, or make routing/backoff decisions for a host.
|
|
30
34
|
|
|
31
35
|
For plugin installation, see the [main project README](https://github.com/yuzushi-dev/Sando#readme).
|
|
32
36
|
|
package/index.mjs
CHANGED
|
@@ -90,6 +90,11 @@ export {
|
|
|
90
90
|
recoverArtifactFromWorkspace,
|
|
91
91
|
} from './src/artifact-recovery.mjs';
|
|
92
92
|
export { runArtifactCli } from './src/artifact-cli.mjs';
|
|
93
|
+
export {
|
|
94
|
+
DEFAULT_ARTIFACT_MAX_BYTES,
|
|
95
|
+
DEFAULT_ARTIFACT_TTL_MS,
|
|
96
|
+
cleanupArtifacts,
|
|
97
|
+
} from './src/artifact-lifecycle.mjs';
|
|
93
98
|
export { buildF1TelemetryEvent, publishF1Telemetry } from './src/f1-telemetry.mjs';
|
|
94
99
|
export {
|
|
95
100
|
F4_EVENT_SCHEMA,
|
package/package.json
CHANGED
package/src/accounting-cli.mjs
CHANGED
|
@@ -21,7 +21,7 @@ export function formatAccountingReport(report) {
|
|
|
21
21
|
`reasoning: ${report.reasoningOutputTokens}`,
|
|
22
22
|
`turns: ${report.turnCount}`,
|
|
23
23
|
`weighted estimate: ${report.weightedCost.costUnits} cost units`,
|
|
24
|
-
`
|
|
24
|
+
`reported cost: ${report.cost.totalCostUsd === null ? report.cost.status : `$${report.cost.totalCostUsd.toFixed(6)} (${report.cost.status})`}`,
|
|
25
25
|
];
|
|
26
26
|
if (report.cost.effectiveRateUsdPerMillionTokens !== null) {
|
|
27
27
|
lines.push(`blended effective rate: $${report.cost.effectiveRateUsdPerMillionTokens.toFixed(2)}/M tokens`);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_ARTIFACT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
5
|
+
export const DEFAULT_ARTIFACT_MAX_BYTES = 64 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
function validNumber(value, name) {
|
|
8
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new TypeError(`${name} is invalid`);
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function safeArtifact(directory, name) {
|
|
13
|
+
if (!/^[a-f0-9]{64}\.txt$/.test(name)) return null;
|
|
14
|
+
const target = path.join(directory, name);
|
|
15
|
+
let link;
|
|
16
|
+
try { link = fs.lstatSync(target); } catch { return null; }
|
|
17
|
+
if (!link.isFile() || link.isSymbolicLink()) return null;
|
|
18
|
+
let resolved;
|
|
19
|
+
try { resolved = fs.realpathSync(target); } catch { return null; }
|
|
20
|
+
if (resolved !== target) return null;
|
|
21
|
+
let stat;
|
|
22
|
+
try { stat = fs.statSync(target); } catch { return null; }
|
|
23
|
+
return { target, name, bytes: stat.size, mtimeMs: stat.mtimeMs };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function cleanupArtifacts(
|
|
27
|
+
directory,
|
|
28
|
+
{
|
|
29
|
+
now = Date.now(), ttlMs = DEFAULT_ARTIFACT_TTL_MS,
|
|
30
|
+
maxBytes = DEFAULT_ARTIFACT_MAX_BYTES, preserveName = null,
|
|
31
|
+
} = {},
|
|
32
|
+
) {
|
|
33
|
+
if (typeof directory !== 'string' || !path.isAbsolute(directory)) throw new TypeError('artifact directory is invalid');
|
|
34
|
+
validNumber(now, 'now');
|
|
35
|
+
validNumber(ttlMs, 'ttlMs');
|
|
36
|
+
validNumber(maxBytes, 'maxBytes');
|
|
37
|
+
if (preserveName !== null && !/^[a-f0-9]{64}\.txt$/.test(preserveName)) {
|
|
38
|
+
throw new TypeError('preserveName is invalid');
|
|
39
|
+
}
|
|
40
|
+
const directoryStat = fs.lstatSync(directory, { throwIfNoEntry: false });
|
|
41
|
+
if (!directoryStat?.isDirectory() || directoryStat.isSymbolicLink()) throw new Error('artifact directory is unsafe');
|
|
42
|
+
|
|
43
|
+
const entries = fs.readdirSync(directory).map((name) => safeArtifact(directory, name)).filter(Boolean);
|
|
44
|
+
const expired = entries.filter((entry) => now - entry.mtimeMs >= ttlMs);
|
|
45
|
+
const keep = entries.filter((entry) => !expired.includes(entry));
|
|
46
|
+
let totalBytes = keep.reduce((total, entry) => total + entry.bytes, 0);
|
|
47
|
+
const removals = [...expired, ...keep.sort((left, right) => {
|
|
48
|
+
if (left.name === preserveName) return 1;
|
|
49
|
+
if (right.name === preserveName) return -1;
|
|
50
|
+
return left.mtimeMs - right.mtimeMs || left.name.localeCompare(right.name);
|
|
51
|
+
})];
|
|
52
|
+
let removed = 0;
|
|
53
|
+
let removedBytes = 0;
|
|
54
|
+
for (const entry of removals) {
|
|
55
|
+
if (expired.includes(entry) || totalBytes > maxBytes) {
|
|
56
|
+
try {
|
|
57
|
+
const current = safeArtifact(directory, entry.name);
|
|
58
|
+
if (!current || current.target !== entry.target) continue;
|
|
59
|
+
fs.rmSync(current.target);
|
|
60
|
+
removed += 1;
|
|
61
|
+
removedBytes += current.bytes;
|
|
62
|
+
if (!expired.includes(entry)) totalBytes -= current.bytes;
|
|
63
|
+
} catch { /* cleanup is best-effort and never follows unresolved targets */ }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { removed, removedBytes, retainedBytes: Math.max(0, totalBytes) };
|
|
67
|
+
}
|
|
@@ -17,6 +17,11 @@ function handleDigest(ref) {
|
|
|
17
17
|
return match[1];
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export function validateArtifactHandle(ref) {
|
|
21
|
+
handleDigest(ref);
|
|
22
|
+
return ref;
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
function integer(value, name, { positive = false } = {}) {
|
|
21
26
|
if (!Number.isSafeInteger(value) || value < (positive ? 1 : 0)) throw new TypeError(`${name} is invalid`);
|
|
22
27
|
return value;
|
package/src/artifact-store.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { recoverArtifactContent } from './artifact-recovery.mjs';
|
|
1
|
+
import { recoverArtifactContent, validateArtifactHandle } from './artifact-recovery.mjs';
|
|
2
2
|
|
|
3
3
|
const MAX_ARTIFACTS = 128;
|
|
4
4
|
const MAX_STORED_BYTES = 64 * 1024 * 1024;
|
|
@@ -28,6 +28,7 @@ export function rememberArtifact(artifact) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export function recoverStoredArtifact(options = {}) {
|
|
31
|
+
validateArtifactHandle(options.ref);
|
|
31
32
|
const entry = store.get(options.ref);
|
|
32
33
|
if (!entry) throw new Error('artifact handle is unavailable in this MCP session');
|
|
33
34
|
store.delete(options.ref);
|
|
@@ -45,6 +45,10 @@ function counter(value) {
|
|
|
45
45
|
return Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function validCounter(value) {
|
|
49
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
48
52
|
/** Stable digest of a JSON-serializable value. Order-sensitive by design: a reordered
|
|
49
53
|
* tool array is a different byte prefix to the provider even if the set is equal. */
|
|
50
54
|
export function shapeDigest(value) {
|
|
@@ -88,7 +92,9 @@ export function hasBreakpoint(body) {
|
|
|
88
92
|
*
|
|
89
93
|
* `current` / `previous` are `{ at, usage, tools, system, messages, body }`, where
|
|
90
94
|
* `usage` is `{ cachedInputTokens, cacheWriteInputTokens, inputTokens }` as recorded
|
|
91
|
-
* in the provider ledger. `
|
|
95
|
+
* in the provider ledger. `inputTokens` is the complete prompt, including cache
|
|
96
|
+
* reads and writes; `promptTokens` is accepted as its clearer alias. `previous` is
|
|
97
|
+
* null on the first turn of a session.
|
|
92
98
|
*/
|
|
93
99
|
export function attributeTurn({
|
|
94
100
|
current,
|
|
@@ -101,8 +107,10 @@ export function attributeTurn({
|
|
|
101
107
|
const usage = object(current.usage) ? current.usage : {};
|
|
102
108
|
const cacheReadTokens = counter(usage.cachedInputTokens);
|
|
103
109
|
const cacheWriteTokens = counter(usage.cacheWriteInputTokens);
|
|
104
|
-
const
|
|
105
|
-
const totalPromptTokens =
|
|
110
|
+
const promptTokens = validCounter(usage.promptTokens) ?? counter(usage.inputTokens);
|
|
111
|
+
const totalPromptTokens = promptTokens;
|
|
112
|
+
const effectiveInputTokens = Math.max(0, promptTokens - cacheReadTokens);
|
|
113
|
+
const freshInputTokens = Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens);
|
|
106
114
|
const hit = cacheReadTokens > 0;
|
|
107
115
|
|
|
108
116
|
const currentDigests = messageDigests(current.messages);
|
|
@@ -112,6 +120,8 @@ export function attributeTurn({
|
|
|
112
120
|
const detail = {
|
|
113
121
|
cacheReadTokens,
|
|
114
122
|
cacheWriteTokens,
|
|
123
|
+
promptTokens,
|
|
124
|
+
effectiveInputTokens,
|
|
115
125
|
freshInputTokens,
|
|
116
126
|
totalPromptTokens,
|
|
117
127
|
divergedAtMessage: divergedAt,
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
1
3
|
import { estimateTokens } from './core.mjs';
|
|
2
4
|
import { dedupeHistory } from './history-dedupe.mjs';
|
|
3
5
|
import { selectHistoryCandidates, validateMaxHistoryTokens } from './history-budget.mjs';
|
|
4
6
|
import { shakeHistoricalResult } from './history-shake.mjs';
|
|
5
7
|
import { compactHistoricalStructure } from './history-structure.mjs';
|
|
6
8
|
import { buildHistoryDisclosure } from './history-disclosure.mjs';
|
|
9
|
+
import { prepareHistoryArtifact, persistHistoryArtifact } from './history-archive.mjs';
|
|
10
|
+
import { createRedactionProfile } from './redaction-profile.mjs';
|
|
7
11
|
|
|
8
12
|
const SUPERSEDED = '[sando superseded by newer read]';
|
|
9
13
|
const USELESS = '[sando elided useless success]';
|
|
@@ -12,6 +16,11 @@ const USELESS_SUCCESSES = new Set([
|
|
|
12
16
|
'no output.',
|
|
13
17
|
'(no output)',
|
|
14
18
|
]);
|
|
19
|
+
const DEFAULT_REDACTION_PROFILE = createRedactionProfile();
|
|
20
|
+
const DEFAULT_HISTORY_ARCHIVE_RETAIN_RESULTS = 3;
|
|
21
|
+
// A marker is useful only when the historical observation is materially larger
|
|
22
|
+
// than the recovery instructions it replaces. Small results stay inline.
|
|
23
|
+
const DEFAULT_HISTORY_ARCHIVE_MIN_BYTES = 3072;
|
|
15
24
|
|
|
16
25
|
function object(value) {
|
|
17
26
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -76,10 +85,35 @@ function resultError(item, text) {
|
|
|
76
85
|
return /^\s*(?:error|failed|failure)\b[:\s-]*/i.test(text);
|
|
77
86
|
}
|
|
78
87
|
|
|
88
|
+
function resultSuccess(item, provider, text) {
|
|
89
|
+
if (resultError(item, text)) return false;
|
|
90
|
+
if (provider === 'anthropic') return item.is_error !== true;
|
|
91
|
+
return item.ok === true || ['completed', 'success', 'succeeded'].includes(item.status);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function nativeCodexExecSafety(provider, call, record) {
|
|
95
|
+
const item = record.entry.item;
|
|
96
|
+
const output = item[record.entry.key];
|
|
97
|
+
if (provider !== 'openai-responses' || call?.providerType !== 'function_call'
|
|
98
|
+
|| call.name !== 'exec_command' || item.type !== 'function_call_output') return null;
|
|
99
|
+
if (typeof output !== 'string') return false;
|
|
100
|
+
if (item.error !== undefined || item.is_error === true || item.ok === false
|
|
101
|
+
|| (item.status !== undefined && !['completed', 'success', 'succeeded'].includes(item.status))) return false;
|
|
102
|
+
const lines = output.split(/\r?\n/);
|
|
103
|
+
const outputLine = lines.indexOf('Output:');
|
|
104
|
+
if (outputLine < 0) return false;
|
|
105
|
+
const exits = lines.slice(0, outputLine).filter((line) => line.startsWith('Process exited with code '));
|
|
106
|
+
return exits.length === 1 && exits[0] === 'Process exited with code 0';
|
|
107
|
+
}
|
|
108
|
+
|
|
79
109
|
function useless(text) {
|
|
80
110
|
return USELESS_SUCCESSES.has(text.trim().toLowerCase());
|
|
81
111
|
}
|
|
82
112
|
|
|
113
|
+
function historyArchiveMarker(text) {
|
|
114
|
+
return /^\[sando archived result sando:sha256:[a-f0-9]{64}; /.test(text) && text.endsWith(']');
|
|
115
|
+
}
|
|
116
|
+
|
|
83
117
|
function resultText(value) {
|
|
84
118
|
if (typeof value === 'string') return value;
|
|
85
119
|
if (Array.isArray(value) && value.length > 0
|
|
@@ -168,7 +202,7 @@ function collectResponses(body) {
|
|
|
168
202
|
if (['function_call', 'custom_tool_call'].includes(item?.type)) {
|
|
169
203
|
entries.push({
|
|
170
204
|
kind: 'call', id: item.call_id, name: item.name,
|
|
171
|
-
input: parseArguments(item.arguments ?? item.input), position,
|
|
205
|
+
input: parseArguments(item.arguments ?? item.input), position, providerType: item.type,
|
|
172
206
|
});
|
|
173
207
|
} else if (['function_call_output', 'custom_tool_call_output'].includes(item?.type)) {
|
|
174
208
|
entries.push({
|
|
@@ -186,13 +220,14 @@ const COLLECTORS = {
|
|
|
186
220
|
'openai-responses': collectResponses,
|
|
187
221
|
};
|
|
188
222
|
|
|
189
|
-
function historyRecords(entries, calls) {
|
|
223
|
+
function historyRecords(entries, calls, provider) {
|
|
190
224
|
return entries.flatMap((entry) => {
|
|
191
225
|
if (entry.kind !== 'result') return [];
|
|
192
226
|
const call = calls.get(entry.id);
|
|
193
227
|
const text = resultText(entry.item[entry.key]);
|
|
194
228
|
if (!call || text === null) return [];
|
|
195
229
|
const isError = resultError(entry.item, text);
|
|
230
|
+
const safe = resultSuccess(entry.item, provider, text);
|
|
196
231
|
return [{
|
|
197
232
|
id: entry.id,
|
|
198
233
|
toolName: call.name,
|
|
@@ -200,7 +235,7 @@ function historyRecords(entries, calls) {
|
|
|
200
235
|
output: entry.item[entry.key],
|
|
201
236
|
current: entry.current,
|
|
202
237
|
historical: !entry.current,
|
|
203
|
-
safe
|
|
238
|
+
safe,
|
|
204
239
|
isError,
|
|
205
240
|
position: entry.position,
|
|
206
241
|
estimatedTokens: estimateTokens(text),
|
|
@@ -225,7 +260,29 @@ function collectHistoryRecords(provider, body) {
|
|
|
225
260
|
calls.delete(id);
|
|
226
261
|
results.delete(id);
|
|
227
262
|
}
|
|
228
|
-
return historyRecords(entries, calls);
|
|
263
|
+
return historyRecords(entries, calls, provider);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const DEFAULT_STRATEGIES = Object.freeze({
|
|
267
|
+
supersededRead: true,
|
|
268
|
+
producerUseless: true,
|
|
269
|
+
exactDuplicate: true,
|
|
270
|
+
repeatedLines: true,
|
|
271
|
+
historyShake: true,
|
|
272
|
+
recoverableArchive: false,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
function strategyPolicy(policy) {
|
|
276
|
+
if (!object(policy) || !Object.hasOwn(policy, 'strategies')) return DEFAULT_STRATEGIES;
|
|
277
|
+
if (!object(policy.strategies)) throw new TypeError('strategies must be an object');
|
|
278
|
+
const strategies = { ...DEFAULT_STRATEGIES };
|
|
279
|
+
for (const [name, enabled] of Object.entries(policy.strategies)) {
|
|
280
|
+
if (!Object.hasOwn(strategies, name) || typeof enabled !== 'boolean') {
|
|
281
|
+
throw new TypeError(`invalid context strategy: ${name}`);
|
|
282
|
+
}
|
|
283
|
+
strategies[name] = enabled;
|
|
284
|
+
}
|
|
285
|
+
return strategies;
|
|
229
286
|
}
|
|
230
287
|
|
|
231
288
|
export function detectProviderBody(body, headers = {}) {
|
|
@@ -322,16 +379,21 @@ function suffixTokensByPosition(body) {
|
|
|
322
379
|
return suffix;
|
|
323
380
|
}
|
|
324
381
|
|
|
325
|
-
export function transformProviderRequest({ provider, body, policy, idleMs, redactionProfile } = {}) {
|
|
382
|
+
export function transformProviderRequest({ provider, body, policy, idleMs, redactionProfile, historyArchiveRoot } = {}) {
|
|
326
383
|
const clone = structuredClone(body);
|
|
327
384
|
const estimatedInputTokens = estimate(body);
|
|
328
385
|
const selectedProvider = provider ?? detectProviderBody(body);
|
|
386
|
+
const strategies = strategyPolicy(policy);
|
|
329
387
|
const collector = COLLECTORS[selectedProvider];
|
|
330
388
|
let supersededReads = 0;
|
|
331
389
|
let elidedUselessSuccesses = 0;
|
|
332
390
|
let deduplicatedResults = 0;
|
|
333
391
|
let compactedStructures = 0;
|
|
334
392
|
let shakenResults = 0;
|
|
393
|
+
let archivedResults = 0;
|
|
394
|
+
let archiveRedactionSkips = 0;
|
|
395
|
+
let archiveNoSavingsSkips = 0;
|
|
396
|
+
let archiveSizeSkips = 0;
|
|
335
397
|
const disclosures = [];
|
|
336
398
|
const disclose = (record, reason, originalText, visibleText, recovery = 'rerun-tool') => {
|
|
337
399
|
if (typeof originalText !== 'string' || typeof visibleText !== 'string') return;
|
|
@@ -342,6 +404,22 @@ export function transformProviderRequest({ provider, body, policy, idleMs, redac
|
|
|
342
404
|
const maxHistoryTokens = object(policy) && Object.hasOwn(policy, 'maxHistoryTokens')
|
|
343
405
|
? validateMaxHistoryTokens(policy.maxHistoryTokens)
|
|
344
406
|
: null;
|
|
407
|
+
const historyArchiveRetainResults = object(policy) && Object.hasOwn(policy, 'historyArchiveRetainResults')
|
|
408
|
+
? policy.historyArchiveRetainResults
|
|
409
|
+
: DEFAULT_HISTORY_ARCHIVE_RETAIN_RESULTS;
|
|
410
|
+
if (!Number.isSafeInteger(historyArchiveRetainResults) || historyArchiveRetainResults < 0) {
|
|
411
|
+
throw new TypeError('historyArchiveRetainResults must be a non-negative safe integer');
|
|
412
|
+
}
|
|
413
|
+
const historyArchiveMinBytes = object(policy) && Object.hasOwn(policy, 'historyArchiveMinBytes')
|
|
414
|
+
? policy.historyArchiveMinBytes
|
|
415
|
+
: DEFAULT_HISTORY_ARCHIVE_MIN_BYTES;
|
|
416
|
+
if (!Number.isSafeInteger(historyArchiveMinBytes) || historyArchiveMinBytes < 0) {
|
|
417
|
+
throw new TypeError('historyArchiveMinBytes must be a non-negative safe integer');
|
|
418
|
+
}
|
|
419
|
+
if (strategies.recoverableArchive
|
|
420
|
+
&& (typeof historyArchiveRoot !== 'string' || !path.isAbsolute(historyArchiveRoot))) {
|
|
421
|
+
throw new TypeError('historyArchiveRoot must be an absolute path');
|
|
422
|
+
}
|
|
345
423
|
const budgetTriggered = maxHistoryTokens !== null
|
|
346
424
|
&& BigInt(estimatedInputTokens) * 5n > BigInt(maxHistoryTokens) * 4n;
|
|
347
425
|
// Don't rewrite warm cached history unless the rewrite reclaims enough of the suffix
|
|
@@ -391,18 +469,46 @@ export function transformProviderRequest({ provider, body, policy, idleMs, redac
|
|
|
391
469
|
results.delete(id);
|
|
392
470
|
}
|
|
393
471
|
|
|
472
|
+
const archiveRecords = historyRecords(entries, calls, selectedProvider);
|
|
473
|
+
const resultEntries = entries.filter((entry) => entry.kind === 'result');
|
|
474
|
+
const recentEntries = new Set(historyArchiveRetainResults === 0
|
|
475
|
+
? []
|
|
476
|
+
: resultEntries.slice(-historyArchiveRetainResults));
|
|
477
|
+
const archivedIds = new Set();
|
|
478
|
+
if (strategies.recoverableArchive) {
|
|
479
|
+
const profile = redactionProfile ?? DEFAULT_REDACTION_PROFILE;
|
|
480
|
+
if (!profile || typeof profile.redact !== 'function') throw new TypeError('history archive redaction profile is invalid');
|
|
481
|
+
for (const record of archiveRecords) {
|
|
482
|
+
const nativeSafety = nativeCodexExecSafety(selectedProvider, calls.get(record.id), record);
|
|
483
|
+
const safe = nativeSafety ?? record.safe;
|
|
484
|
+
if (!safe || !record.historical || recentEntries.has(record.entry)) continue;
|
|
485
|
+
const text = resultText(record.output);
|
|
486
|
+
if (text === null || historyArchiveMarker(text)) continue;
|
|
487
|
+
if (Buffer.byteLength(text, 'utf8') < historyArchiveMinBytes) { archiveSizeSkips += 1; continue; }
|
|
488
|
+
if (profile.redact(text).text !== text) { archiveRedactionSkips += 1; continue; }
|
|
489
|
+
const artifact = prepareHistoryArtifact({ root: historyArchiveRoot, content: text });
|
|
490
|
+
const reclaimed = reclaimedTokens(text, artifact.marker);
|
|
491
|
+
if (reclaimed === 0) { archiveNoSavingsSkips += 1; continue; }
|
|
492
|
+
if (cacheProtected(record.entry, reclaimed)) { cacheProtectedSkips += 1; continue; }
|
|
493
|
+
persistHistoryArtifact(artifact);
|
|
494
|
+
replaceResult(record.entry.item, record.entry.key, artifact.marker);
|
|
495
|
+
archivedIds.add(record.id);
|
|
496
|
+
archivedResults += 1;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
394
500
|
const reads = [];
|
|
395
501
|
for (const call of calls.values()) {
|
|
396
502
|
const result = results.get(call.id);
|
|
397
503
|
const identity = readIdentity(call.name, call.input);
|
|
398
504
|
const text = result && resultText(result.item[result.key]);
|
|
399
505
|
if (!result || !identity || text === null) continue;
|
|
400
|
-
if (
|
|
506
|
+
if (resultSuccess(result.item, selectedProvider, text)) reads.push({ call, result, identity, text });
|
|
401
507
|
}
|
|
402
508
|
reads.sort((a, b) => a.call.position - b.call.position);
|
|
403
|
-
for (let index = 0; index < reads.length; index += 1) {
|
|
509
|
+
for (let index = 0; strategies.supersededRead && index < reads.length; index += 1) {
|
|
404
510
|
const old = reads[index];
|
|
405
|
-
if (old.result.current) continue;
|
|
511
|
+
if (old.result.current || archivedIds.has(old.call.id)) continue;
|
|
406
512
|
const newer = reads.slice(index + 1).find((candidate) =>
|
|
407
513
|
covers(candidate.identity, old.identity) && !useless(candidate.text));
|
|
408
514
|
if (!newer) continue;
|
|
@@ -412,23 +518,24 @@ export function transformProviderRequest({ provider, body, policy, idleMs, redac
|
|
|
412
518
|
supersededReads += 1;
|
|
413
519
|
}
|
|
414
520
|
|
|
415
|
-
for (const [id, result] of results) {
|
|
416
|
-
if (!calls.has(id) || result.current) continue;
|
|
521
|
+
for (const [id, result] of strategies.producerUseless ? results : []) {
|
|
522
|
+
if (!calls.has(id) || result.current || archivedIds.has(id)) continue;
|
|
417
523
|
const text = resultText(result.item[result.key]);
|
|
418
524
|
if (text === null) continue;
|
|
419
|
-
if (text === SUPERSEDED ||
|
|
525
|
+
if (text === SUPERSEDED || !resultSuccess(result.item, selectedProvider, text) || !useless(text)) continue;
|
|
420
526
|
if (cacheProtected(result, reclaimedTokens(text, USELESS))) { cacheProtectedSkips += 1; continue; }
|
|
421
527
|
replaceResult(result.item, result.key, USELESS);
|
|
422
528
|
disclose({ toolName: calls.get(id).name }, 'useless-success', text, USELESS);
|
|
423
529
|
elidedUselessSuccesses += 1;
|
|
424
530
|
}
|
|
425
531
|
|
|
426
|
-
const records = historyRecords(entries, calls);
|
|
532
|
+
const records = historyRecords(entries, calls, selectedProvider);
|
|
427
533
|
const candidates = maxHistoryTokens === null
|
|
428
|
-
? records.filter((record) => record.safe && record.historical)
|
|
429
|
-
: selectHistoryCandidates({ bodyTokens: estimatedInputTokens, maxHistoryTokens, candidates: records })
|
|
534
|
+
? records.filter((record) => record.safe && record.historical && !archivedIds.has(record.id))
|
|
535
|
+
: selectHistoryCandidates({ bodyTokens: estimatedInputTokens, maxHistoryTokens, candidates: records })
|
|
536
|
+
.filter((record) => !archivedIds.has(record.id));
|
|
430
537
|
const candidateIds = new Set(candidates.map((candidate) => candidate.id));
|
|
431
|
-
const reductions = dedupeHistory(records);
|
|
538
|
+
const reductions = strategies.exactDuplicate ? dedupeHistory(records) : { entries: [] };
|
|
432
539
|
const recordsById = new Map(records.map((record) => [record.id, record]));
|
|
433
540
|
for (const reduced of reductions.entries) {
|
|
434
541
|
if (!candidateIds.has(reduced.id)) continue;
|
|
@@ -441,7 +548,7 @@ export function transformProviderRequest({ provider, body, policy, idleMs, redac
|
|
|
441
548
|
deduplicatedResults += 1;
|
|
442
549
|
}
|
|
443
550
|
|
|
444
|
-
for (const record of records) {
|
|
551
|
+
for (const record of strategies.repeatedLines ? records : []) {
|
|
445
552
|
if (!candidateIds.has(record.id)) continue;
|
|
446
553
|
const text = resultText(record.entry.item[record.entry.key]);
|
|
447
554
|
if (text === null) continue;
|
|
@@ -458,7 +565,7 @@ export function transformProviderRequest({ provider, body, policy, idleMs, redac
|
|
|
458
565
|
compactedStructures += 1;
|
|
459
566
|
}
|
|
460
567
|
|
|
461
|
-
if (maxHistoryTokens !== null && budgetTriggered) {
|
|
568
|
+
if (strategies.historyShake && maxHistoryTokens !== null && budgetTriggered) {
|
|
462
569
|
for (const record of records) {
|
|
463
570
|
if (!candidateIds.has(record.id)) continue;
|
|
464
571
|
const text = resultText(record.entry.item[record.entry.key]);
|
|
@@ -484,6 +591,7 @@ export function transformProviderRequest({ provider, body, policy, idleMs, redac
|
|
|
484
591
|
if (deduplicatedResults > 0) reasons.push('duplicate-history');
|
|
485
592
|
if (compactedStructures > 0) reasons.push('repeated-lines');
|
|
486
593
|
if (shakenResults > 0) reasons.push('history-shake');
|
|
594
|
+
if (archivedResults > 0) reasons.unshift('recoverable-archive');
|
|
487
595
|
return {
|
|
488
596
|
body: clone,
|
|
489
597
|
changed: reasons.length > 0,
|
|
@@ -497,6 +605,10 @@ export function transformProviderRequest({ provider, body, policy, idleMs, redac
|
|
|
497
605
|
deduplicatedResults,
|
|
498
606
|
compactedStructures,
|
|
499
607
|
shakenResults,
|
|
608
|
+
archivedResults,
|
|
609
|
+
archiveRedactionSkips,
|
|
610
|
+
archiveNoSavingsSkips,
|
|
611
|
+
archiveSizeSkips,
|
|
500
612
|
historyDisclosureCount: disclosures.length,
|
|
501
613
|
historyDisclosureOriginalBytes: disclosures.reduce((total, item) => total + item.bytes.original, 0),
|
|
502
614
|
historyDisclosureVisibleBytes: disclosures.reduce((total, item) => total + item.bytes.visible, 0),
|