sandoichi 0.4.2 → 0.6.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 +18 -1
- 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 +169 -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 +83 -7
- package/src/result-disclosure.mjs +8 -2
- package/src/semantic-gate.mjs +69 -0
- package/src/semantic-judge.mjs +261 -0
- 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
|
@@ -7,7 +7,13 @@ export {
|
|
|
7
7
|
} from './src/core.mjs';
|
|
8
8
|
export { createRedactionProfile } from './src/redaction-profile.mjs';
|
|
9
9
|
export { loadProjectRedactionProfile } from './src/redaction-config.mjs';
|
|
10
|
-
export {
|
|
10
|
+
export {
|
|
11
|
+
detectProviderBody,
|
|
12
|
+
listSemanticCandidates,
|
|
13
|
+
listSemanticJudgmentCandidates,
|
|
14
|
+
restoreSemanticJudgmentCandidates,
|
|
15
|
+
transformProviderRequest,
|
|
16
|
+
} from './src/context-transform.mjs';
|
|
11
17
|
export {
|
|
12
18
|
DEFAULT_ACCOUNTING_WEIGHTS,
|
|
13
19
|
PAIRED_ARMS,
|
|
@@ -24,6 +30,12 @@ export {
|
|
|
24
30
|
SEMANTIC_SUMMARY_SCHEMA,
|
|
25
31
|
validateSemanticSummary,
|
|
26
32
|
} from './src/semantic-compactor.mjs';
|
|
33
|
+
export {
|
|
34
|
+
buildSemanticJudgeRequest,
|
|
35
|
+
createSemanticJudge,
|
|
36
|
+
SEMANTIC_JUDGMENT_SCHEMA,
|
|
37
|
+
} from './src/semantic-judge.mjs';
|
|
38
|
+
export { createSemanticGate } from './src/semantic-gate.mjs';
|
|
27
39
|
export { createProviderProxy } from './src/proxy.mjs';
|
|
28
40
|
export { shakeHistoricalResult } from './src/history-shake.mjs';
|
|
29
41
|
export {
|
|
@@ -90,6 +102,11 @@ export {
|
|
|
90
102
|
recoverArtifactFromWorkspace,
|
|
91
103
|
} from './src/artifact-recovery.mjs';
|
|
92
104
|
export { runArtifactCli } from './src/artifact-cli.mjs';
|
|
105
|
+
export {
|
|
106
|
+
DEFAULT_ARTIFACT_MAX_BYTES,
|
|
107
|
+
DEFAULT_ARTIFACT_TTL_MS,
|
|
108
|
+
cleanupArtifacts,
|
|
109
|
+
} from './src/artifact-lifecycle.mjs';
|
|
93
110
|
export { buildF1TelemetryEvent, publishF1Telemetry } from './src/f1-telemetry.mjs';
|
|
94
111
|
export {
|
|
95
112
|
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 = {}) {
|
|
@@ -263,6 +320,46 @@ export function listSemanticCandidates({ provider, body, model } = {}) {
|
|
|
263
320
|
}));
|
|
264
321
|
}
|
|
265
322
|
|
|
323
|
+
export function listSemanticJudgmentCandidates({ provider, originalBody, transformedBody, model } = {}) {
|
|
324
|
+
const originalRecords = collectHistoryRecords(provider, originalBody);
|
|
325
|
+
const originalById = new Map(originalRecords.map((record) => [record.id, record]));
|
|
326
|
+
return collectHistoryRecords(provider, transformedBody)
|
|
327
|
+
.filter((record) => record.safe && record.historical)
|
|
328
|
+
.flatMap((preview) => {
|
|
329
|
+
const original = originalById.get(preview.id);
|
|
330
|
+
const originalText = resultText(original?.output);
|
|
331
|
+
const previewText = resultText(preview.output);
|
|
332
|
+
if (!original || originalText === null || previewText === null || originalText === previewText) return [];
|
|
333
|
+
return [{
|
|
334
|
+
id: preview.id,
|
|
335
|
+
provider,
|
|
336
|
+
model: model ?? null,
|
|
337
|
+
toolName: preview.toolName,
|
|
338
|
+
originalText,
|
|
339
|
+
previewText,
|
|
340
|
+
historical: preview.historical,
|
|
341
|
+
isError: preview.isError,
|
|
342
|
+
recoverable: historyArchiveMarker(previewText),
|
|
343
|
+
estimatedTokens: original.estimatedTokens,
|
|
344
|
+
previewTokens: preview.estimatedTokens,
|
|
345
|
+
}];
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function restoreSemanticJudgmentCandidates({ provider, originalBody, transformedBody, ids = [] } = {}) {
|
|
350
|
+
const restoreIds = new Set(ids);
|
|
351
|
+
const body = structuredClone(transformedBody);
|
|
352
|
+
if (restoreIds.size === 0) return body;
|
|
353
|
+
const originalById = new Map(collectHistoryRecords(provider, originalBody).map((record) => [record.id, record]));
|
|
354
|
+
for (const preview of collectHistoryRecords(provider, body)) {
|
|
355
|
+
if (!restoreIds.has(preview.id) || !preview.historical || !preview.safe) continue;
|
|
356
|
+
const original = originalById.get(preview.id);
|
|
357
|
+
if (!original?.safe || resultText(original.output) === null) continue;
|
|
358
|
+
replaceResult(preview.entry.item, preview.entry.key, structuredClone(original.output));
|
|
359
|
+
}
|
|
360
|
+
return body;
|
|
361
|
+
}
|
|
362
|
+
|
|
266
363
|
function carriesBreakpoint(value) {
|
|
267
364
|
if (Array.isArray(value)) return value.some(carriesBreakpoint);
|
|
268
365
|
if (!object(value)) return false;
|
|
@@ -322,16 +419,21 @@ function suffixTokensByPosition(body) {
|
|
|
322
419
|
return suffix;
|
|
323
420
|
}
|
|
324
421
|
|
|
325
|
-
export function transformProviderRequest({ provider, body, policy, idleMs, redactionProfile } = {}) {
|
|
422
|
+
export function transformProviderRequest({ provider, body, policy, idleMs, redactionProfile, historyArchiveRoot } = {}) {
|
|
326
423
|
const clone = structuredClone(body);
|
|
327
424
|
const estimatedInputTokens = estimate(body);
|
|
328
425
|
const selectedProvider = provider ?? detectProviderBody(body);
|
|
426
|
+
const strategies = strategyPolicy(policy);
|
|
329
427
|
const collector = COLLECTORS[selectedProvider];
|
|
330
428
|
let supersededReads = 0;
|
|
331
429
|
let elidedUselessSuccesses = 0;
|
|
332
430
|
let deduplicatedResults = 0;
|
|
333
431
|
let compactedStructures = 0;
|
|
334
432
|
let shakenResults = 0;
|
|
433
|
+
let archivedResults = 0;
|
|
434
|
+
let archiveRedactionSkips = 0;
|
|
435
|
+
let archiveNoSavingsSkips = 0;
|
|
436
|
+
let archiveSizeSkips = 0;
|
|
335
437
|
const disclosures = [];
|
|
336
438
|
const disclose = (record, reason, originalText, visibleText, recovery = 'rerun-tool') => {
|
|
337
439
|
if (typeof originalText !== 'string' || typeof visibleText !== 'string') return;
|
|
@@ -342,6 +444,22 @@ export function transformProviderRequest({ provider, body, policy, idleMs, redac
|
|
|
342
444
|
const maxHistoryTokens = object(policy) && Object.hasOwn(policy, 'maxHistoryTokens')
|
|
343
445
|
? validateMaxHistoryTokens(policy.maxHistoryTokens)
|
|
344
446
|
: null;
|
|
447
|
+
const historyArchiveRetainResults = object(policy) && Object.hasOwn(policy, 'historyArchiveRetainResults')
|
|
448
|
+
? policy.historyArchiveRetainResults
|
|
449
|
+
: DEFAULT_HISTORY_ARCHIVE_RETAIN_RESULTS;
|
|
450
|
+
if (!Number.isSafeInteger(historyArchiveRetainResults) || historyArchiveRetainResults < 0) {
|
|
451
|
+
throw new TypeError('historyArchiveRetainResults must be a non-negative safe integer');
|
|
452
|
+
}
|
|
453
|
+
const historyArchiveMinBytes = object(policy) && Object.hasOwn(policy, 'historyArchiveMinBytes')
|
|
454
|
+
? policy.historyArchiveMinBytes
|
|
455
|
+
: DEFAULT_HISTORY_ARCHIVE_MIN_BYTES;
|
|
456
|
+
if (!Number.isSafeInteger(historyArchiveMinBytes) || historyArchiveMinBytes < 0) {
|
|
457
|
+
throw new TypeError('historyArchiveMinBytes must be a non-negative safe integer');
|
|
458
|
+
}
|
|
459
|
+
if (strategies.recoverableArchive
|
|
460
|
+
&& (typeof historyArchiveRoot !== 'string' || !path.isAbsolute(historyArchiveRoot))) {
|
|
461
|
+
throw new TypeError('historyArchiveRoot must be an absolute path');
|
|
462
|
+
}
|
|
345
463
|
const budgetTriggered = maxHistoryTokens !== null
|
|
346
464
|
&& BigInt(estimatedInputTokens) * 5n > BigInt(maxHistoryTokens) * 4n;
|
|
347
465
|
// Don't rewrite warm cached history unless the rewrite reclaims enough of the suffix
|
|
@@ -391,18 +509,46 @@ export function transformProviderRequest({ provider, body, policy, idleMs, redac
|
|
|
391
509
|
results.delete(id);
|
|
392
510
|
}
|
|
393
511
|
|
|
512
|
+
const archiveRecords = historyRecords(entries, calls, selectedProvider);
|
|
513
|
+
const resultEntries = entries.filter((entry) => entry.kind === 'result');
|
|
514
|
+
const recentEntries = new Set(historyArchiveRetainResults === 0
|
|
515
|
+
? []
|
|
516
|
+
: resultEntries.slice(-historyArchiveRetainResults));
|
|
517
|
+
const archivedIds = new Set();
|
|
518
|
+
if (strategies.recoverableArchive) {
|
|
519
|
+
const profile = redactionProfile ?? DEFAULT_REDACTION_PROFILE;
|
|
520
|
+
if (!profile || typeof profile.redact !== 'function') throw new TypeError('history archive redaction profile is invalid');
|
|
521
|
+
for (const record of archiveRecords) {
|
|
522
|
+
const nativeSafety = nativeCodexExecSafety(selectedProvider, calls.get(record.id), record);
|
|
523
|
+
const safe = nativeSafety ?? record.safe;
|
|
524
|
+
if (!safe || !record.historical || recentEntries.has(record.entry)) continue;
|
|
525
|
+
const text = resultText(record.output);
|
|
526
|
+
if (text === null || historyArchiveMarker(text)) continue;
|
|
527
|
+
if (Buffer.byteLength(text, 'utf8') < historyArchiveMinBytes) { archiveSizeSkips += 1; continue; }
|
|
528
|
+
if (profile.redact(text).text !== text) { archiveRedactionSkips += 1; continue; }
|
|
529
|
+
const artifact = prepareHistoryArtifact({ root: historyArchiveRoot, content: text });
|
|
530
|
+
const reclaimed = reclaimedTokens(text, artifact.marker);
|
|
531
|
+
if (reclaimed === 0) { archiveNoSavingsSkips += 1; continue; }
|
|
532
|
+
if (cacheProtected(record.entry, reclaimed)) { cacheProtectedSkips += 1; continue; }
|
|
533
|
+
persistHistoryArtifact(artifact);
|
|
534
|
+
replaceResult(record.entry.item, record.entry.key, artifact.marker);
|
|
535
|
+
archivedIds.add(record.id);
|
|
536
|
+
archivedResults += 1;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
394
540
|
const reads = [];
|
|
395
541
|
for (const call of calls.values()) {
|
|
396
542
|
const result = results.get(call.id);
|
|
397
543
|
const identity = readIdentity(call.name, call.input);
|
|
398
544
|
const text = result && resultText(result.item[result.key]);
|
|
399
545
|
if (!result || !identity || text === null) continue;
|
|
400
|
-
if (
|
|
546
|
+
if (resultSuccess(result.item, selectedProvider, text)) reads.push({ call, result, identity, text });
|
|
401
547
|
}
|
|
402
548
|
reads.sort((a, b) => a.call.position - b.call.position);
|
|
403
|
-
for (let index = 0; index < reads.length; index += 1) {
|
|
549
|
+
for (let index = 0; strategies.supersededRead && index < reads.length; index += 1) {
|
|
404
550
|
const old = reads[index];
|
|
405
|
-
if (old.result.current) continue;
|
|
551
|
+
if (old.result.current || archivedIds.has(old.call.id)) continue;
|
|
406
552
|
const newer = reads.slice(index + 1).find((candidate) =>
|
|
407
553
|
covers(candidate.identity, old.identity) && !useless(candidate.text));
|
|
408
554
|
if (!newer) continue;
|
|
@@ -412,23 +558,24 @@ export function transformProviderRequest({ provider, body, policy, idleMs, redac
|
|
|
412
558
|
supersededReads += 1;
|
|
413
559
|
}
|
|
414
560
|
|
|
415
|
-
for (const [id, result] of results) {
|
|
416
|
-
if (!calls.has(id) || result.current) continue;
|
|
561
|
+
for (const [id, result] of strategies.producerUseless ? results : []) {
|
|
562
|
+
if (!calls.has(id) || result.current || archivedIds.has(id)) continue;
|
|
417
563
|
const text = resultText(result.item[result.key]);
|
|
418
564
|
if (text === null) continue;
|
|
419
|
-
if (text === SUPERSEDED ||
|
|
565
|
+
if (text === SUPERSEDED || !resultSuccess(result.item, selectedProvider, text) || !useless(text)) continue;
|
|
420
566
|
if (cacheProtected(result, reclaimedTokens(text, USELESS))) { cacheProtectedSkips += 1; continue; }
|
|
421
567
|
replaceResult(result.item, result.key, USELESS);
|
|
422
568
|
disclose({ toolName: calls.get(id).name }, 'useless-success', text, USELESS);
|
|
423
569
|
elidedUselessSuccesses += 1;
|
|
424
570
|
}
|
|
425
571
|
|
|
426
|
-
const records = historyRecords(entries, calls);
|
|
572
|
+
const records = historyRecords(entries, calls, selectedProvider);
|
|
427
573
|
const candidates = maxHistoryTokens === null
|
|
428
|
-
? records.filter((record) => record.safe && record.historical)
|
|
429
|
-
: selectHistoryCandidates({ bodyTokens: estimatedInputTokens, maxHistoryTokens, candidates: records })
|
|
574
|
+
? records.filter((record) => record.safe && record.historical && !archivedIds.has(record.id))
|
|
575
|
+
: selectHistoryCandidates({ bodyTokens: estimatedInputTokens, maxHistoryTokens, candidates: records })
|
|
576
|
+
.filter((record) => !archivedIds.has(record.id));
|
|
430
577
|
const candidateIds = new Set(candidates.map((candidate) => candidate.id));
|
|
431
|
-
const reductions = dedupeHistory(records);
|
|
578
|
+
const reductions = strategies.exactDuplicate ? dedupeHistory(records) : { entries: [] };
|
|
432
579
|
const recordsById = new Map(records.map((record) => [record.id, record]));
|
|
433
580
|
for (const reduced of reductions.entries) {
|
|
434
581
|
if (!candidateIds.has(reduced.id)) continue;
|
|
@@ -441,7 +588,7 @@ export function transformProviderRequest({ provider, body, policy, idleMs, redac
|
|
|
441
588
|
deduplicatedResults += 1;
|
|
442
589
|
}
|
|
443
590
|
|
|
444
|
-
for (const record of records) {
|
|
591
|
+
for (const record of strategies.repeatedLines ? records : []) {
|
|
445
592
|
if (!candidateIds.has(record.id)) continue;
|
|
446
593
|
const text = resultText(record.entry.item[record.entry.key]);
|
|
447
594
|
if (text === null) continue;
|
|
@@ -458,7 +605,7 @@ export function transformProviderRequest({ provider, body, policy, idleMs, redac
|
|
|
458
605
|
compactedStructures += 1;
|
|
459
606
|
}
|
|
460
607
|
|
|
461
|
-
if (maxHistoryTokens !== null && budgetTriggered) {
|
|
608
|
+
if (strategies.historyShake && maxHistoryTokens !== null && budgetTriggered) {
|
|
462
609
|
for (const record of records) {
|
|
463
610
|
if (!candidateIds.has(record.id)) continue;
|
|
464
611
|
const text = resultText(record.entry.item[record.entry.key]);
|
|
@@ -484,6 +631,7 @@ export function transformProviderRequest({ provider, body, policy, idleMs, redac
|
|
|
484
631
|
if (deduplicatedResults > 0) reasons.push('duplicate-history');
|
|
485
632
|
if (compactedStructures > 0) reasons.push('repeated-lines');
|
|
486
633
|
if (shakenResults > 0) reasons.push('history-shake');
|
|
634
|
+
if (archivedResults > 0) reasons.unshift('recoverable-archive');
|
|
487
635
|
return {
|
|
488
636
|
body: clone,
|
|
489
637
|
changed: reasons.length > 0,
|
|
@@ -497,6 +645,10 @@ export function transformProviderRequest({ provider, body, policy, idleMs, redac
|
|
|
497
645
|
deduplicatedResults,
|
|
498
646
|
compactedStructures,
|
|
499
647
|
shakenResults,
|
|
648
|
+
archivedResults,
|
|
649
|
+
archiveRedactionSkips,
|
|
650
|
+
archiveNoSavingsSkips,
|
|
651
|
+
archiveSizeSkips,
|
|
500
652
|
historyDisclosureCount: disclosures.length,
|
|
501
653
|
historyDisclosureOriginalBytes: disclosures.reduce((total, item) => total + item.bytes.original, 0),
|
|
502
654
|
historyDisclosureVisibleBytes: disclosures.reduce((total, item) => total + item.bytes.visible, 0),
|