@ricky-stevens/context-guardian 2.1.0 → 2.2.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CLAUDE.md +11 -1
- package/README.md +26 -39
- package/hooks/session-start.mjs +16 -6
- package/hooks/stop.mjs +34 -50
- package/hooks/submit.mjs +34 -31
- package/lib/checkpoint.mjs +14 -4
- package/lib/config.mjs +37 -10
- package/lib/handoff.mjs +12 -2
- package/lib/statusline.mjs +104 -54
- package/lib/tokens.mjs +2 -16
- package/package.json +1 -1
- package/skills/config/SKILL.md +1 -1
- package/skills/stats/SKILL.md +7 -28
- package/test/checkpoint.test.mjs +2 -2
- package/test/config.test.mjs +39 -0
- package/test/integration.test.mjs +4 -1
- package/test/statusline.test.mjs +116 -6
- package/test/submit.test.mjs +3 -9
- package/test/tokens.test.mjs +2 -40
- package/lib/estimate.mjs +0 -254
- package/test/estimate.test.mjs +0 -262
package/lib/estimate.mjs
DELETED
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fast compaction savings estimator.
|
|
3
|
-
*
|
|
4
|
-
* Performs a single-pass byte categorisation of the transcript to estimate
|
|
5
|
-
* how much context each compaction mode would save — WITHOUT running
|
|
6
|
-
* the full extraction pipeline. Used by the statusline state and /cg:stats
|
|
7
|
-
* to show estimated post-compaction percentages.
|
|
8
|
-
*
|
|
9
|
-
* @module estimate
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import fs from "node:fs";
|
|
13
|
-
import { estimateOverhead } from "./tokens.mjs";
|
|
14
|
-
import { readTranscriptLines } from "./transcript.mjs";
|
|
15
|
-
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
// Constants
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Estimated ratio of a tool_use summary vs the original input size.
|
|
22
|
-
* Edit/Write keep ~80% (diffs preserved), Read/Grep/Glob keep ~5% (note only),
|
|
23
|
-
* average across all tool types is roughly 15%.
|
|
24
|
-
*/
|
|
25
|
-
const TOOL_USE_SUMMARY_RATIO = 0.15;
|
|
26
|
-
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
// Main estimator
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Estimate post-compaction token percentages for Smart Compact and Keep Recent.
|
|
33
|
-
*
|
|
34
|
-
* Does a fast single-pass categorisation of transcript bytes into
|
|
35
|
-
* "keep" vs "remove" buckets, then projects the savings onto the
|
|
36
|
-
* real token count.
|
|
37
|
-
*
|
|
38
|
-
* @param {string} transcriptPath - Path to the JSONL transcript
|
|
39
|
-
* @param {number} currentTokens - Current real token count from API
|
|
40
|
-
* @param {number} maxTokens - Model's max token limit
|
|
41
|
-
* @returns {{ smartPct: number, recentPct: number }} Estimated post-compaction percentages (0-100)
|
|
42
|
-
*/
|
|
43
|
-
export function estimateSavings(
|
|
44
|
-
transcriptPath,
|
|
45
|
-
currentTokens,
|
|
46
|
-
maxTokens,
|
|
47
|
-
baselineOverhead = 0,
|
|
48
|
-
) {
|
|
49
|
-
if (!transcriptPath || !Number.isFinite(maxTokens) || maxTokens <= 0) {
|
|
50
|
-
return { smartPct: 0, recentPct: 0 };
|
|
51
|
-
}
|
|
52
|
-
try {
|
|
53
|
-
fs.statSync(transcriptPath);
|
|
54
|
-
} catch {
|
|
55
|
-
return { smartPct: 0, recentPct: 0 };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
const lines = readTranscriptLines(transcriptPath);
|
|
60
|
-
const scan = categoriseBytes(lines);
|
|
61
|
-
|
|
62
|
-
// Smart Compact: remove noise, keep content
|
|
63
|
-
const totalBytes = scan.keepBytes + scan.removeBytes;
|
|
64
|
-
if (totalBytes === 0) return { smartPct: 0, recentPct: 0 };
|
|
65
|
-
|
|
66
|
-
// Per-session overhead (system prompt, tools, memory, skills).
|
|
67
|
-
// Uses measured baseline from first response when available.
|
|
68
|
-
const overhead = estimateOverhead(
|
|
69
|
-
currentTokens,
|
|
70
|
-
transcriptPath,
|
|
71
|
-
baselineOverhead,
|
|
72
|
-
);
|
|
73
|
-
const conversationTokens = Math.max(0, currentTokens - overhead);
|
|
74
|
-
|
|
75
|
-
const smartKeepRatio = scan.keepBytes / totalBytes;
|
|
76
|
-
const smartTokens =
|
|
77
|
-
Math.round(conversationTokens * smartKeepRatio) + overhead;
|
|
78
|
-
const smartPct = Number(((smartTokens / maxTokens) * 100).toFixed(1));
|
|
79
|
-
|
|
80
|
-
// Keep Recent: last 10 user exchanges, with same noise removal
|
|
81
|
-
const recentRatio =
|
|
82
|
-
scan.userExchanges > 0 ? Math.min(1, 10 / scan.userExchanges) : 1;
|
|
83
|
-
const recentTokens =
|
|
84
|
-
Math.round(conversationTokens * smartKeepRatio * recentRatio) + overhead;
|
|
85
|
-
const recentPct = Number(((recentTokens / maxTokens) * 100).toFixed(1));
|
|
86
|
-
|
|
87
|
-
return { smartPct, recentPct };
|
|
88
|
-
} catch {
|
|
89
|
-
return { smartPct: 0, recentPct: 0 };
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ---------------------------------------------------------------------------
|
|
94
|
-
// Byte categorisation — single-pass scan
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Categorise transcript content bytes into "keep" vs "remove" buckets
|
|
99
|
-
* and count user exchanges.
|
|
100
|
-
*
|
|
101
|
-
* @param {string[]} lines - JSONL transcript lines
|
|
102
|
-
* @returns {{ keepBytes: number, removeBytes: number, userExchanges: number }}
|
|
103
|
-
*/
|
|
104
|
-
function categoriseBytes(lines) {
|
|
105
|
-
let keepBytes = 0;
|
|
106
|
-
let removeBytes = 0;
|
|
107
|
-
let userExchanges = 0;
|
|
108
|
-
|
|
109
|
-
for (const line of lines) {
|
|
110
|
-
let obj;
|
|
111
|
-
try {
|
|
112
|
-
obj = JSON.parse(line);
|
|
113
|
-
} catch {
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// System / progress — entirely removed
|
|
118
|
-
if (obj.type === "system" || obj.type === "progress") {
|
|
119
|
-
removeBytes += Buffer.byteLength(line, "utf8");
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (obj.type === "assistant" && obj.message?.content) {
|
|
124
|
-
const result = categoriseAssistantContent(obj.message.content);
|
|
125
|
-
keepBytes += result.keep;
|
|
126
|
-
removeBytes += result.remove;
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (obj.type === "user" && obj.message?.content) {
|
|
131
|
-
const result = categoriseUserContent(obj.message.content);
|
|
132
|
-
keepBytes += result.keep;
|
|
133
|
-
removeBytes += result.remove;
|
|
134
|
-
if (result.hasUserText) userExchanges++;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return { keepBytes, removeBytes, userExchanges };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// ---------------------------------------------------------------------------
|
|
142
|
-
// Content block categorisation helpers
|
|
143
|
-
// ---------------------------------------------------------------------------
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Categorise bytes from a single assistant content block.
|
|
147
|
-
* @param {object} block - A content block from an assistant message
|
|
148
|
-
* @returns {{ keep: number, remove: number }}
|
|
149
|
-
*/
|
|
150
|
-
function categoriseAssistantBlock(block) {
|
|
151
|
-
if (block.type === "text" && block.text) {
|
|
152
|
-
return { keep: Buffer.byteLength(block.text, "utf8"), remove: 0 };
|
|
153
|
-
}
|
|
154
|
-
if (block.type === "tool_use" && block.input) {
|
|
155
|
-
const inputBytes = Buffer.byteLength(JSON.stringify(block.input), "utf8");
|
|
156
|
-
return {
|
|
157
|
-
keep: Math.round(inputBytes * TOOL_USE_SUMMARY_RATIO),
|
|
158
|
-
remove: Math.round(inputBytes * (1 - TOOL_USE_SUMMARY_RATIO)),
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
if (block.type === "thinking" || block.type === "redacted_thinking") {
|
|
162
|
-
const thinkBytes = block.thinking
|
|
163
|
-
? Buffer.byteLength(block.thinking, "utf8")
|
|
164
|
-
: 100;
|
|
165
|
-
return { keep: 0, remove: thinkBytes };
|
|
166
|
-
}
|
|
167
|
-
return { keep: 0, remove: 0 };
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Categorise bytes from assistant message content (array or string).
|
|
172
|
-
* @param {Array|string} content - The message.content field
|
|
173
|
-
* @returns {{ keep: number, remove: number }}
|
|
174
|
-
*/
|
|
175
|
-
function categoriseAssistantContent(content) {
|
|
176
|
-
if (typeof content === "string") {
|
|
177
|
-
return { keep: Buffer.byteLength(content, "utf8"), remove: 0 };
|
|
178
|
-
}
|
|
179
|
-
if (!Array.isArray(content)) {
|
|
180
|
-
return { keep: 0, remove: 0 };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
let keep = 0;
|
|
184
|
-
let remove = 0;
|
|
185
|
-
for (const block of content) {
|
|
186
|
-
const result = categoriseAssistantBlock(block);
|
|
187
|
-
keep += result.keep;
|
|
188
|
-
remove += result.remove;
|
|
189
|
-
}
|
|
190
|
-
return { keep, remove };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Categorise bytes from a single user content block.
|
|
195
|
-
* @param {object} block - A content block from a user message
|
|
196
|
-
* @returns {{ keep: number, remove: number, isText: boolean }}
|
|
197
|
-
*/
|
|
198
|
-
function categoriseUserBlock(block) {
|
|
199
|
-
if (block.type === "text" && block.text) {
|
|
200
|
-
return {
|
|
201
|
-
keep: Buffer.byteLength(block.text, "utf8"),
|
|
202
|
-
remove: 0,
|
|
203
|
-
isText: true,
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
if (block.type === "tool_result") {
|
|
207
|
-
const resultContent = block.content;
|
|
208
|
-
let resultBytes;
|
|
209
|
-
if (resultContent) {
|
|
210
|
-
const raw =
|
|
211
|
-
typeof resultContent === "string"
|
|
212
|
-
? resultContent
|
|
213
|
-
: JSON.stringify(resultContent);
|
|
214
|
-
resultBytes = Buffer.byteLength(raw, "utf8");
|
|
215
|
-
} else {
|
|
216
|
-
resultBytes = 0;
|
|
217
|
-
}
|
|
218
|
-
return {
|
|
219
|
-
keep: Math.round(resultBytes * 0.1),
|
|
220
|
-
remove: Math.round(resultBytes * 0.9),
|
|
221
|
-
isText: false,
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
return { keep: 0, remove: 0, isText: false };
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Categorise bytes from user message content (array or string).
|
|
229
|
-
* @param {Array|string} content - The message.content field
|
|
230
|
-
* @returns {{ keep: number, remove: number, hasUserText: boolean }}
|
|
231
|
-
*/
|
|
232
|
-
function categoriseUserContent(content) {
|
|
233
|
-
if (typeof content === "string") {
|
|
234
|
-
return {
|
|
235
|
-
keep: Buffer.byteLength(content, "utf8"),
|
|
236
|
-
remove: 0,
|
|
237
|
-
hasUserText: true,
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
if (!Array.isArray(content)) {
|
|
241
|
-
return { keep: 0, remove: 0, hasUserText: false };
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
let keep = 0;
|
|
245
|
-
let remove = 0;
|
|
246
|
-
let hasUserText = false;
|
|
247
|
-
for (const block of content) {
|
|
248
|
-
const result = categoriseUserBlock(block);
|
|
249
|
-
keep += result.keep;
|
|
250
|
-
remove += result.remove;
|
|
251
|
-
if (result.isText) hasUserText = true;
|
|
252
|
-
}
|
|
253
|
-
return { keep, remove, hasUserText };
|
|
254
|
-
}
|
package/test/estimate.test.mjs
DELETED
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
6
|
-
import { estimateSavings } from "../lib/estimate.mjs";
|
|
7
|
-
|
|
8
|
-
let tmpDir;
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "cg-estimate-test-"));
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
afterEach(() => {
|
|
15
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
/** Write a JSONL transcript file from an array of objects. */
|
|
19
|
-
function writeTranscript(name, lines) {
|
|
20
|
-
const p = path.join(tmpDir, name);
|
|
21
|
-
fs.writeFileSync(p, `${lines.map((l) => JSON.stringify(l)).join("\n")}\n`);
|
|
22
|
-
return p;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
// Helpers for building transcript lines
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
function userText(text) {
|
|
30
|
-
return { type: "user", message: { role: "user", content: text } };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function userBlocks(blocks) {
|
|
34
|
-
return { type: "user", message: { role: "user", content: blocks } };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function assistantText(text) {
|
|
38
|
-
return {
|
|
39
|
-
type: "assistant",
|
|
40
|
-
message: {
|
|
41
|
-
role: "assistant",
|
|
42
|
-
content: [{ type: "text", text }],
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function systemMsg(text) {
|
|
48
|
-
return { type: "system", message: { content: text } };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function progressMsg(text) {
|
|
52
|
-
return { type: "progress", message: { content: text } };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
// Tests
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
|
|
59
|
-
describe("estimateSavings", () => {
|
|
60
|
-
it("returns zeros when transcriptPath is null", () => {
|
|
61
|
-
const result = estimateSavings(null, 50000, 200000);
|
|
62
|
-
assert.deepStrictEqual(result, { smartPct: 0, recentPct: 0 });
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("returns zeros when transcriptPath does not exist", () => {
|
|
66
|
-
const result = estimateSavings("/nonexistent/path.jsonl", 50000, 200000);
|
|
67
|
-
assert.deepStrictEqual(result, { smartPct: 0, recentPct: 0 });
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("returns zeros when transcript is empty", () => {
|
|
71
|
-
const p = path.join(tmpDir, "empty.jsonl");
|
|
72
|
-
fs.writeFileSync(p, "");
|
|
73
|
-
const result = estimateSavings(p, 50000, 200000);
|
|
74
|
-
assert.deepStrictEqual(result, { smartPct: 0, recentPct: 0 });
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("system/progress messages are all removable — estimates lower than raw percentage", () => {
|
|
78
|
-
const p = writeTranscript("sys-progress.jsonl", [
|
|
79
|
-
systemMsg("System prompt content here with some length to it"),
|
|
80
|
-
progressMsg("Loading tools..."),
|
|
81
|
-
systemMsg("Another system message with extra content padding"),
|
|
82
|
-
progressMsg("Still loading more tools and resources..."),
|
|
83
|
-
]);
|
|
84
|
-
|
|
85
|
-
const rawPct = (50000 / 200000) * 100; // 25%
|
|
86
|
-
const result = estimateSavings(p, 50000, 200000);
|
|
87
|
-
assert.ok(result.smartPct <= rawPct);
|
|
88
|
-
assert.ok(result.recentPct <= rawPct);
|
|
89
|
-
assert.equal(result.smartPct, result.recentPct);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("user text + assistant text — both kept, estimates reflect kept content", () => {
|
|
93
|
-
const p = writeTranscript("text-only.jsonl", [
|
|
94
|
-
userText("Please help me refactor the auth module"),
|
|
95
|
-
assistantText(
|
|
96
|
-
"I will refactor the auth module by extracting the token validation into a separate function.",
|
|
97
|
-
),
|
|
98
|
-
]);
|
|
99
|
-
|
|
100
|
-
const result = estimateSavings(p, 50000, 200000);
|
|
101
|
-
assert.ok(result.smartPct > 10);
|
|
102
|
-
assert.ok(result.recentPct > 0);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("large tool_result content is mostly removed — low estimate", () => {
|
|
106
|
-
const bigContent = "x".repeat(50000);
|
|
107
|
-
const p = writeTranscript("tool-result.jsonl", [
|
|
108
|
-
userText("Read the config file"),
|
|
109
|
-
assistantText("I will read the config file for you."),
|
|
110
|
-
userBlocks([
|
|
111
|
-
{ type: "text", text: "Here is the result" },
|
|
112
|
-
{
|
|
113
|
-
type: "tool_result",
|
|
114
|
-
tool_use_id: "toolu_123",
|
|
115
|
-
content: bigContent,
|
|
116
|
-
},
|
|
117
|
-
]),
|
|
118
|
-
]);
|
|
119
|
-
|
|
120
|
-
const rawPct = (80000 / 200000) * 100; // 40%
|
|
121
|
-
const result = estimateSavings(p, 80000, 200000);
|
|
122
|
-
assert.ok(result.smartPct < rawPct);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("baselineOverhead parameter affects the result", () => {
|
|
126
|
-
const bigContent = "y".repeat(20000);
|
|
127
|
-
const p = writeTranscript("overhead.jsonl", [
|
|
128
|
-
userText("Analyse the file"),
|
|
129
|
-
assistantText("I will read and analyse the file."),
|
|
130
|
-
userBlocks([
|
|
131
|
-
{ type: "text", text: "Result" },
|
|
132
|
-
{
|
|
133
|
-
type: "tool_result",
|
|
134
|
-
tool_use_id: "toolu_oh1",
|
|
135
|
-
content: bigContent,
|
|
136
|
-
},
|
|
137
|
-
]),
|
|
138
|
-
assistantText("The file looks good."),
|
|
139
|
-
]);
|
|
140
|
-
|
|
141
|
-
const withoutOverhead = estimateSavings(p, 80000, 200000, 0);
|
|
142
|
-
const withOverhead = estimateSavings(p, 80000, 200000, 40000);
|
|
143
|
-
assert.notEqual(withoutOverhead.smartPct, withOverhead.smartPct);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("returns zeros when maxTokens is zero or NaN", () => {
|
|
147
|
-
const p = writeTranscript("zero-max.jsonl", [
|
|
148
|
-
userText("hello"),
|
|
149
|
-
assistantText("hi"),
|
|
150
|
-
]);
|
|
151
|
-
assert.deepStrictEqual(estimateSavings(p, 1000, 0), {
|
|
152
|
-
smartPct: 0,
|
|
153
|
-
recentPct: 0,
|
|
154
|
-
});
|
|
155
|
-
assert.deepStrictEqual(estimateSavings(p, 1000, NaN), {
|
|
156
|
-
smartPct: 0,
|
|
157
|
-
recentPct: 0,
|
|
158
|
-
});
|
|
159
|
-
assert.deepStrictEqual(estimateSavings(p, 1000, -100), {
|
|
160
|
-
smartPct: 0,
|
|
161
|
-
recentPct: 0,
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("thinking blocks are categorised as removable", () => {
|
|
166
|
-
const p = writeTranscript("thinking.jsonl", [
|
|
167
|
-
userText("solve this problem"),
|
|
168
|
-
{
|
|
169
|
-
type: "assistant",
|
|
170
|
-
message: {
|
|
171
|
-
role: "assistant",
|
|
172
|
-
content: [
|
|
173
|
-
{
|
|
174
|
-
type: "thinking",
|
|
175
|
-
thinking: "Let me think about this carefully... ".repeat(100),
|
|
176
|
-
},
|
|
177
|
-
{ type: "text", text: "The answer is 42." },
|
|
178
|
-
],
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
]);
|
|
182
|
-
|
|
183
|
-
const rawPct = (50000 / 200000) * 100;
|
|
184
|
-
const result = estimateSavings(p, 50000, 200000);
|
|
185
|
-
assert.ok(result.smartPct < rawPct);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it("tool_use blocks are partially kept (summary ratio)", () => {
|
|
189
|
-
const p = writeTranscript("tool-use.jsonl", [
|
|
190
|
-
userText("read the file"),
|
|
191
|
-
{
|
|
192
|
-
type: "assistant",
|
|
193
|
-
message: {
|
|
194
|
-
role: "assistant",
|
|
195
|
-
content: [
|
|
196
|
-
{
|
|
197
|
-
type: "tool_use",
|
|
198
|
-
id: "t1",
|
|
199
|
-
name: "Read",
|
|
200
|
-
input: { file_path: "/some/very/long/path/to/file.js" },
|
|
201
|
-
},
|
|
202
|
-
{ type: "text", text: "Here is the file content." },
|
|
203
|
-
],
|
|
204
|
-
},
|
|
205
|
-
},
|
|
206
|
-
]);
|
|
207
|
-
|
|
208
|
-
const result = estimateSavings(p, 50000, 200000);
|
|
209
|
-
assert.ok(result.smartPct > 0);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it("assistant string content (non-array) is categorised as kept", () => {
|
|
213
|
-
const p = writeTranscript("string-content.jsonl", [
|
|
214
|
-
userText("hello"),
|
|
215
|
-
{
|
|
216
|
-
type: "assistant",
|
|
217
|
-
message: { role: "assistant", content: "Simple string response" },
|
|
218
|
-
},
|
|
219
|
-
]);
|
|
220
|
-
|
|
221
|
-
const result = estimateSavings(p, 50000, 200000);
|
|
222
|
-
assert.ok(result.smartPct > 0);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it("redacted_thinking blocks are categorised as removable", () => {
|
|
226
|
-
const p = writeTranscript("redacted.jsonl", [
|
|
227
|
-
userText("think about this"),
|
|
228
|
-
{
|
|
229
|
-
type: "assistant",
|
|
230
|
-
message: {
|
|
231
|
-
role: "assistant",
|
|
232
|
-
content: [
|
|
233
|
-
{ type: "redacted_thinking" },
|
|
234
|
-
{ type: "text", text: "Done thinking." },
|
|
235
|
-
],
|
|
236
|
-
},
|
|
237
|
-
},
|
|
238
|
-
]);
|
|
239
|
-
|
|
240
|
-
const result = estimateSavings(p, 50000, 200000);
|
|
241
|
-
assert.ok(result.smartPct > 0);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it("userExchanges counting — 20 exchanges makes recentPct less than smartPct", () => {
|
|
245
|
-
const padding = "A".repeat(5000);
|
|
246
|
-
const lines = [];
|
|
247
|
-
for (let i = 0; i < 20; i++) {
|
|
248
|
-
lines.push(userText(`User message ${i + 1}: ${padding}`));
|
|
249
|
-
lines.push(assistantText(`Response ${i + 1}: ${padding}`));
|
|
250
|
-
}
|
|
251
|
-
const p = writeTranscript("twenty-exchanges.jsonl", lines);
|
|
252
|
-
|
|
253
|
-
const result = estimateSavings(p, 60000, 200000);
|
|
254
|
-
assert.ok(result.smartPct > 0);
|
|
255
|
-
assert.ok(result.recentPct > 0);
|
|
256
|
-
assert.ok(result.recentPct < result.smartPct);
|
|
257
|
-
|
|
258
|
-
const ratio = result.recentPct / result.smartPct;
|
|
259
|
-
assert.ok(ratio < 0.85);
|
|
260
|
-
assert.ok(ratio > 0.4);
|
|
261
|
-
});
|
|
262
|
-
});
|