agent-cache-optimizer 0.5.3 → 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/CHANGELOG.md +85 -22
- package/README.md +55 -33
- package/bin/aco +61 -6
- package/docs/superpowers/plans/2026-06-25-cross-agent-cache-sharing.md +549 -0
- package/docs/superpowers/specs/2026-06-25-cross-agent-cache-hit-design.md +102 -0
- package/package.json +1 -1
- package/src/__tests__/heuristics-splitting.test.ts +287 -0
- package/src/__tests__/plugin.test.ts +620 -0
- package/src/heuristics.ts +43 -6
- package/src/index.ts +724 -48
- package/src/splitting.ts +155 -15
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { tmpdir } from "node:os"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest"
|
|
5
|
+
import { hashContent } from "../core"
|
|
6
|
+
|
|
7
|
+
const stateDir = (cacheRoot: string) => join(cacheRoot, "opencode", "agent-cache-optimizer")
|
|
8
|
+
|
|
9
|
+
const model = (providerID: string, id = "deepseek-chat") =>
|
|
10
|
+
({
|
|
11
|
+
id,
|
|
12
|
+
providerID,
|
|
13
|
+
name: id,
|
|
14
|
+
}) as any
|
|
15
|
+
|
|
16
|
+
async function withPlugin<T>(fn: (hooks: any, cacheRoot: string) => Promise<T>): Promise<T> {
|
|
17
|
+
const originalCacheHome = process.env.XDG_CACHE_HOME
|
|
18
|
+
const cacheRoot = mkdtempSync(join(tmpdir(), "aco-test-"))
|
|
19
|
+
process.env.XDG_CACHE_HOME = cacheRoot
|
|
20
|
+
vi.resetModules()
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const { CacheOptimizerPlugin } = await import("../index")
|
|
24
|
+
const hooks = await CacheOptimizerPlugin({} as any)
|
|
25
|
+
return await fn(hooks, cacheRoot)
|
|
26
|
+
} finally {
|
|
27
|
+
if (originalCacheHome === undefined) delete process.env.XDG_CACHE_HOME
|
|
28
|
+
else process.env.XDG_CACHE_HOME = originalCacheHome
|
|
29
|
+
rmSync(cacheRoot, { recursive: true, force: true })
|
|
30
|
+
vi.resetModules()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("CacheOptimizerPlugin provider/model scope", () => {
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
vi.restoreAllMocks()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("tracks the same model id separately for different providers", async () => {
|
|
40
|
+
await withPlugin(async (hooks, cacheRoot) => {
|
|
41
|
+
const system = [
|
|
42
|
+
"currentDate: 2026-06-25",
|
|
43
|
+
"You are a provider scoped cache optimizer. ".repeat(8),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
await hooks["experimental.chat.system.transform"](
|
|
47
|
+
{ sessionID: "s1", model: model("deepseek") },
|
|
48
|
+
{ system: [...system] },
|
|
49
|
+
)
|
|
50
|
+
await hooks["experimental.chat.system.transform"](
|
|
51
|
+
{ sessionID: "s2", model: model("openrouter") },
|
|
52
|
+
{ system: [...system] },
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const files = readdirSync(stateDir(cacheRoot))
|
|
56
|
+
.filter((file) => file.startsWith("stability-"))
|
|
57
|
+
.sort()
|
|
58
|
+
expect(files).toEqual([
|
|
59
|
+
"stability-deepseek__deepseek-chat.json",
|
|
60
|
+
"stability-openrouter__deepseek-chat.json",
|
|
61
|
+
])
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it("writes loaded diagnostics once per provider/model scope", async () => {
|
|
66
|
+
await withPlugin(async (hooks, cacheRoot) => {
|
|
67
|
+
await hooks["chat.params"]({ agent: "build", model: model("deepseek") }, {})
|
|
68
|
+
await hooks["chat.params"]({ agent: "build", model: model("openrouter") }, {})
|
|
69
|
+
|
|
70
|
+
const log = readFileSync(join(stateDir(cacheRoot), "diag.log"), "utf-8")
|
|
71
|
+
expect(log).toContain("[deepseek__deepseek-chat__build] v0.6.0 loaded")
|
|
72
|
+
expect(log).toContain("[openrouter__deepseek-chat__build] v0.6.0 loaded")
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it("does not multiply cumulative savings by the observation count twice", async () => {
|
|
77
|
+
await withPlugin(async (hooks, cacheRoot) => {
|
|
78
|
+
const stable = "You are a stable cacheable system prompt block. ".repeat(8)
|
|
79
|
+
const dynamic = "currentDate: 2026-06-25"
|
|
80
|
+
|
|
81
|
+
await hooks["experimental.chat.system.transform"](
|
|
82
|
+
{ sessionID: "s1", model: model("openrouter") },
|
|
83
|
+
{ system: [dynamic, stable] },
|
|
84
|
+
)
|
|
85
|
+
await hooks["experimental.chat.system.transform"](
|
|
86
|
+
{ sessionID: "s2", model: model("openrouter") },
|
|
87
|
+
{ system: [dynamic, stable] },
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
const savings = JSON.parse(readFileSync(join(stateDir(cacheRoot), "savings.json"), "utf-8"))
|
|
91
|
+
const expected = (Math.round(stable.length * 2 * 0.25) / 1_000_000) * 0.431
|
|
92
|
+
expect(savings.totalStableBytes).toBe(stable.length * 2)
|
|
93
|
+
expect(savings.totalObservations).toBe(2)
|
|
94
|
+
expect(savings.estimatedSavingsUSD).toBeCloseTo(expected, 12)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it("uses session agent context when system transform only has provider/model", async () => {
|
|
99
|
+
await withPlugin(async (hooks, cacheRoot) => {
|
|
100
|
+
const system = [
|
|
101
|
+
"currentDate: 2026-06-25",
|
|
102
|
+
"You are a stable cacheable system prompt block. ".repeat(8),
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
await hooks["chat.params"](
|
|
106
|
+
{ sessionID: "s-build", agent: "build", model: model("deepseek") },
|
|
107
|
+
{},
|
|
108
|
+
)
|
|
109
|
+
await hooks["chat.params"](
|
|
110
|
+
{ sessionID: "s-review", agent: "review", model: model("deepseek") },
|
|
111
|
+
{},
|
|
112
|
+
)
|
|
113
|
+
await hooks["experimental.chat.system.transform"](
|
|
114
|
+
{ sessionID: "s-build", model: model("deepseek") },
|
|
115
|
+
{ system: [...system] },
|
|
116
|
+
)
|
|
117
|
+
await hooks["experimental.chat.system.transform"](
|
|
118
|
+
{ sessionID: "s-review", model: model("deepseek") },
|
|
119
|
+
{ system: [...system] },
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
const files = readdirSync(stateDir(cacheRoot))
|
|
123
|
+
.filter((file) => file.startsWith("stability-"))
|
|
124
|
+
.sort()
|
|
125
|
+
expect(files).toContain("stability-deepseek__deepseek-chat__build.json")
|
|
126
|
+
expect(files).toContain("stability-deepseek__deepseek-chat__review.json")
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it("writes a provider-model family DB alongside agent-scoped DBs", async () => {
|
|
131
|
+
await withPlugin(async (hooks, cacheRoot) => {
|
|
132
|
+
const stable = "Stable shared prompt content for the model family. ".repeat(20)
|
|
133
|
+
const dynamic = "currentDate: 2026-06-25\nsession id: family-db"
|
|
134
|
+
|
|
135
|
+
await hooks["chat.params"](
|
|
136
|
+
{ sessionID: "s-build-family", agent: "build", model: model("deepseek") },
|
|
137
|
+
{},
|
|
138
|
+
)
|
|
139
|
+
await hooks["experimental.chat.system.transform"](
|
|
140
|
+
{ sessionID: "s-build-family", model: model("deepseek") },
|
|
141
|
+
{ system: [dynamic, stable] },
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
const files = readdirSync(stateDir(cacheRoot))
|
|
145
|
+
.filter((file) => file.startsWith("stability-"))
|
|
146
|
+
.sort()
|
|
147
|
+
|
|
148
|
+
expect(files).toContain("stability-deepseek__deepseek-chat.json")
|
|
149
|
+
expect(files).toContain("stability-deepseek__deepseek-chat__build.json")
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it("records provider cache metrics from message events without double-counting updates", async () => {
|
|
154
|
+
await withPlugin(async (hooks, cacheRoot) => {
|
|
155
|
+
await hooks["chat.params"](
|
|
156
|
+
{ sessionID: "s-build", agent: "build", model: model("openrouter") },
|
|
157
|
+
{},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
await hooks.event({
|
|
161
|
+
event: {
|
|
162
|
+
type: "message.updated",
|
|
163
|
+
properties: {
|
|
164
|
+
info: {
|
|
165
|
+
id: "assistant-1",
|
|
166
|
+
sessionID: "s-build",
|
|
167
|
+
role: "assistant",
|
|
168
|
+
providerID: "openrouter",
|
|
169
|
+
modelID: "deepseek-chat",
|
|
170
|
+
agent: "build",
|
|
171
|
+
cost: 0.01,
|
|
172
|
+
tokens: {
|
|
173
|
+
input: 100,
|
|
174
|
+
output: 20,
|
|
175
|
+
reasoning: 0,
|
|
176
|
+
cache: { read: 40, write: 10 },
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
await hooks.event({
|
|
183
|
+
event: {
|
|
184
|
+
type: "message.updated",
|
|
185
|
+
properties: {
|
|
186
|
+
info: {
|
|
187
|
+
id: "assistant-1",
|
|
188
|
+
sessionID: "s-build",
|
|
189
|
+
role: "assistant",
|
|
190
|
+
providerID: "openrouter",
|
|
191
|
+
modelID: "deepseek-chat",
|
|
192
|
+
agent: "build",
|
|
193
|
+
cost: 0.015,
|
|
194
|
+
tokens: {
|
|
195
|
+
input: 150,
|
|
196
|
+
output: 25,
|
|
197
|
+
reasoning: 0,
|
|
198
|
+
cache: { read: 70, write: 10 },
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
const metrics = JSON.parse(
|
|
206
|
+
readFileSync(join(stateDir(cacheRoot), "cache-metrics.json"), "utf-8"),
|
|
207
|
+
)
|
|
208
|
+
expect(metrics.total.inputTokens).toBe(150)
|
|
209
|
+
expect(metrics.total.outputTokens).toBe(25)
|
|
210
|
+
expect(metrics.total.cacheReadTokens).toBe(70)
|
|
211
|
+
expect(metrics.total.cacheWriteTokens).toBe(10)
|
|
212
|
+
expect(metrics.total.costUSD).toBeCloseTo(0.015, 12)
|
|
213
|
+
expect(metrics.scopes["openrouter__deepseek-chat__build"].cacheHitRate).toBeCloseTo(
|
|
214
|
+
70 / (150 + 70),
|
|
215
|
+
12,
|
|
216
|
+
)
|
|
217
|
+
const snapshotKeys = Object.keys(metrics.snapshots)
|
|
218
|
+
expect(snapshotKeys).toEqual([
|
|
219
|
+
`message:${hashContent("s-build")}:${hashContent("assistant-1")}`,
|
|
220
|
+
])
|
|
221
|
+
expect(snapshotKeys[0]).not.toContain("s-build")
|
|
222
|
+
expect(snapshotKeys[0]).not.toContain("assistant-1")
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it("skips duplicate zero-delta provider cache metric events", async () => {
|
|
227
|
+
await withPlugin(async (hooks, cacheRoot) => {
|
|
228
|
+
const update = {
|
|
229
|
+
event: {
|
|
230
|
+
type: "message.updated",
|
|
231
|
+
properties: {
|
|
232
|
+
info: {
|
|
233
|
+
id: "assistant-1",
|
|
234
|
+
sessionID: "s-build",
|
|
235
|
+
role: "assistant",
|
|
236
|
+
providerID: "openrouter",
|
|
237
|
+
modelID: "deepseek-chat",
|
|
238
|
+
agent: "build",
|
|
239
|
+
cost: 0.01,
|
|
240
|
+
tokens: {
|
|
241
|
+
input: 100,
|
|
242
|
+
output: 20,
|
|
243
|
+
reasoning: 0,
|
|
244
|
+
cache: { read: 40, write: 10 },
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
await hooks["chat.params"](
|
|
252
|
+
{ sessionID: "s-build", agent: "build", model: model("openrouter") },
|
|
253
|
+
{},
|
|
254
|
+
)
|
|
255
|
+
await hooks.event(update)
|
|
256
|
+
await hooks.event(update)
|
|
257
|
+
|
|
258
|
+
const raw = readFileSync(join(stateDir(cacheRoot), "events.jsonl"), "utf-8")
|
|
259
|
+
const events = raw
|
|
260
|
+
.trim()
|
|
261
|
+
.split("\n")
|
|
262
|
+
.map((line) => JSON.parse(line))
|
|
263
|
+
const metricsEvents = events.filter((event) => event.type === "metrics")
|
|
264
|
+
const metrics = JSON.parse(
|
|
265
|
+
readFileSync(join(stateDir(cacheRoot), "cache-metrics.json"), "utf-8"),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
expect(metricsEvents).toHaveLength(1)
|
|
269
|
+
expect(metrics.total.events).toBe(1)
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it("migrates raw cache metric snapshot keys before applying deltas", async () => {
|
|
274
|
+
await withPlugin(async (hooks, cacheRoot) => {
|
|
275
|
+
const metricsDir = stateDir(cacheRoot)
|
|
276
|
+
mkdirSync(metricsDir, { recursive: true })
|
|
277
|
+
const existingTotals = {
|
|
278
|
+
events: 1,
|
|
279
|
+
inputTokens: 100,
|
|
280
|
+
outputTokens: 20,
|
|
281
|
+
cacheReadTokens: 40,
|
|
282
|
+
cacheWriteTokens: 10,
|
|
283
|
+
costUSD: 0.01,
|
|
284
|
+
cacheHitRate: 0.4,
|
|
285
|
+
}
|
|
286
|
+
writeFileSync(
|
|
287
|
+
join(metricsDir, "cache-metrics.json"),
|
|
288
|
+
JSON.stringify(
|
|
289
|
+
{
|
|
290
|
+
total: { ...existingTotals },
|
|
291
|
+
scopes: {
|
|
292
|
+
"openrouter__deepseek-chat__build": { ...existingTotals },
|
|
293
|
+
},
|
|
294
|
+
snapshots: {
|
|
295
|
+
"message:s-build:assistant-1": {
|
|
296
|
+
inputTokens: 100,
|
|
297
|
+
outputTokens: 20,
|
|
298
|
+
cacheReadTokens: 40,
|
|
299
|
+
cacheWriteTokens: 10,
|
|
300
|
+
costUSD: 0.01,
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
updated: 1,
|
|
304
|
+
},
|
|
305
|
+
null,
|
|
306
|
+
2,
|
|
307
|
+
),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
await hooks["chat.params"](
|
|
311
|
+
{ sessionID: "s-build", agent: "build", model: model("openrouter") },
|
|
312
|
+
{},
|
|
313
|
+
)
|
|
314
|
+
await hooks.event({
|
|
315
|
+
event: {
|
|
316
|
+
type: "message.updated",
|
|
317
|
+
properties: {
|
|
318
|
+
info: {
|
|
319
|
+
id: "assistant-1",
|
|
320
|
+
sessionID: "s-build",
|
|
321
|
+
role: "assistant",
|
|
322
|
+
providerID: "openrouter",
|
|
323
|
+
modelID: "deepseek-chat",
|
|
324
|
+
agent: "build",
|
|
325
|
+
cost: 0.015,
|
|
326
|
+
tokens: {
|
|
327
|
+
input: 150,
|
|
328
|
+
output: 25,
|
|
329
|
+
reasoning: 0,
|
|
330
|
+
cache: { read: 70, write: 10 },
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
const metrics = JSON.parse(
|
|
338
|
+
readFileSync(join(stateDir(cacheRoot), "cache-metrics.json"), "utf-8"),
|
|
339
|
+
)
|
|
340
|
+
expect(metrics.total.inputTokens).toBe(150)
|
|
341
|
+
expect(metrics.total.outputTokens).toBe(25)
|
|
342
|
+
expect(metrics.total.cacheReadTokens).toBe(70)
|
|
343
|
+
expect(metrics.total.cacheWriteTokens).toBe(10)
|
|
344
|
+
expect(metrics.total.costUSD).toBeCloseTo(0.015, 12)
|
|
345
|
+
expect(Object.keys(metrics.snapshots)).toEqual([
|
|
346
|
+
`message:${hashContent("s-build")}:${hashContent("assistant-1")}`,
|
|
347
|
+
])
|
|
348
|
+
})
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it("computes cache hit rate from cached plus uncached input tokens", async () => {
|
|
352
|
+
await withPlugin(async (hooks, cacheRoot) => {
|
|
353
|
+
await hooks["chat.params"](
|
|
354
|
+
{ sessionID: "s-build", agent: "build", model: model("openrouter") },
|
|
355
|
+
{},
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
await hooks.event({
|
|
359
|
+
event: {
|
|
360
|
+
type: "message.updated",
|
|
361
|
+
properties: {
|
|
362
|
+
info: {
|
|
363
|
+
id: "assistant-1",
|
|
364
|
+
sessionID: "s-build",
|
|
365
|
+
role: "assistant",
|
|
366
|
+
providerID: "openrouter",
|
|
367
|
+
modelID: "deepseek-chat",
|
|
368
|
+
agent: "build",
|
|
369
|
+
cost: 0,
|
|
370
|
+
tokens: {
|
|
371
|
+
input: 109,
|
|
372
|
+
output: 4,
|
|
373
|
+
reasoning: 15,
|
|
374
|
+
cache: { read: 29952, write: 0 },
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
const metrics = JSON.parse(
|
|
382
|
+
readFileSync(join(stateDir(cacheRoot), "cache-metrics.json"), "utf-8"),
|
|
383
|
+
)
|
|
384
|
+
const hitRate = metrics.scopes["openrouter__deepseek-chat__build"].cacheHitRate
|
|
385
|
+
expect(hitRate).toBeCloseTo(29952 / (109 + 29952), 12)
|
|
386
|
+
expect(hitRate).toBeLessThanOrEqual(1)
|
|
387
|
+
})
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
it("persists scoped warm cache and promotes hashes to global after multiple scopes", async () => {
|
|
391
|
+
await withPlugin(async (hooks, cacheRoot) => {
|
|
392
|
+
const stable = "You are a stable cacheable system prompt block. ".repeat(8)
|
|
393
|
+
const dynamic = "currentDate: 2026-06-25"
|
|
394
|
+
|
|
395
|
+
await hooks["chat.params"](
|
|
396
|
+
{ sessionID: "s-build", agent: "build", model: model("deepseek") },
|
|
397
|
+
{},
|
|
398
|
+
)
|
|
399
|
+
await hooks["chat.params"](
|
|
400
|
+
{ sessionID: "s-review", agent: "review", model: model("deepseek") },
|
|
401
|
+
{},
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
for (let i = 0; i < 10; i++) {
|
|
405
|
+
await hooks["experimental.chat.system.transform"](
|
|
406
|
+
{ sessionID: "s-build", model: model("deepseek") },
|
|
407
|
+
{ system: [`${dynamic}-${i}`, stable] },
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
for (let i = 0; i < 10; i++) {
|
|
411
|
+
await hooks["experimental.chat.system.transform"](
|
|
412
|
+
{ sessionID: "s-review", model: model("deepseek") },
|
|
413
|
+
{ system: [`${dynamic}-${i}`, stable] },
|
|
414
|
+
)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const warm = JSON.parse(readFileSync(join(stateDir(cacheRoot), "warm-cache.json"), "utf-8"))
|
|
418
|
+
const stableHash = hashContent(stable)
|
|
419
|
+
expect(warm.version).toBe(2)
|
|
420
|
+
expect(warm.scopes["deepseek__deepseek-chat__build"]).toContain(stableHash)
|
|
421
|
+
expect(warm.scopes["deepseek__deepseek-chat__review"]).toContain(stableHash)
|
|
422
|
+
expect(warm.global).toContain(stableHash)
|
|
423
|
+
})
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
it("orders family-stable shared blocks before agent-specific stable blocks after agent switches", async () => {
|
|
427
|
+
await withPlugin(async (hooks, cacheRoot) => {
|
|
428
|
+
const sharedTools =
|
|
429
|
+
"Shared tool and project instructions stay identical across agents. ".repeat(30)
|
|
430
|
+
const buildPrompt = "You are the build agent with build-only instructions. ".repeat(30)
|
|
431
|
+
const reviewPrompt = "You are the review agent with review-only instructions. ".repeat(30)
|
|
432
|
+
|
|
433
|
+
await hooks["chat.params"](
|
|
434
|
+
{ sessionID: "s-build", agent: "build", model: model("deepseek") },
|
|
435
|
+
{},
|
|
436
|
+
)
|
|
437
|
+
for (let i = 0; i < 3; i++) {
|
|
438
|
+
await hooks["experimental.chat.system.transform"](
|
|
439
|
+
{ sessionID: "s-build", model: model("deepseek") },
|
|
440
|
+
{ system: [`currentDate: 2026-06-25\nsession id: build-${i}`, buildPrompt, sharedTools] },
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
await hooks["chat.params"](
|
|
445
|
+
{ sessionID: "s-review", agent: "review", model: model("deepseek") },
|
|
446
|
+
{},
|
|
447
|
+
)
|
|
448
|
+
for (let i = 0; i < 3; i++) {
|
|
449
|
+
await hooks["experimental.chat.system.transform"](
|
|
450
|
+
{ sessionID: "s-review", model: model("deepseek") },
|
|
451
|
+
{
|
|
452
|
+
system: [`currentDate: 2026-06-25\nsession id: review-${i}`, reviewPrompt, sharedTools],
|
|
453
|
+
},
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const output = {
|
|
458
|
+
system: ["currentDate: 2026-06-25\nsession id: review-final", reviewPrompt, sharedTools],
|
|
459
|
+
}
|
|
460
|
+
await hooks["experimental.chat.system.transform"](
|
|
461
|
+
{ sessionID: "s-review", model: model("deepseek") },
|
|
462
|
+
output,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
expect(output.system[0]).toBe(sharedTools)
|
|
466
|
+
expect(output.system[1]).toBe(reviewPrompt)
|
|
467
|
+
expect(output.system[2]).toContain("session id: review-final")
|
|
468
|
+
|
|
469
|
+
const raw = readFileSync(join(stateDir(cacheRoot), "events.jsonl"), "utf-8")
|
|
470
|
+
const events = raw
|
|
471
|
+
.trim()
|
|
472
|
+
.split("\n")
|
|
473
|
+
.map((line) => JSON.parse(line))
|
|
474
|
+
const transform = events.filter((event) => event.type === "transform").at(-1)
|
|
475
|
+
|
|
476
|
+
expect(transform.ranking).toMatchObject({
|
|
477
|
+
sharedStable: 1,
|
|
478
|
+
scopedStable: 1,
|
|
479
|
+
coldStable: 0,
|
|
480
|
+
})
|
|
481
|
+
expect(transform.ranking.sharedPrefixBytes).toBe(sharedTools.length)
|
|
482
|
+
})
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
it("writes structured events for statistics and debugging without raw ids", async () => {
|
|
486
|
+
await withPlugin(async (hooks, cacheRoot) => {
|
|
487
|
+
const stable = "You are a stable cacheable system prompt block. ".repeat(8)
|
|
488
|
+
const dynamic = "currentDate: 2026-06-25"
|
|
489
|
+
|
|
490
|
+
await hooks["chat.params"](
|
|
491
|
+
{ sessionID: "s-sensitive-build", agent: "build", model: model("openrouter") },
|
|
492
|
+
{},
|
|
493
|
+
)
|
|
494
|
+
for (let i = 0; i < 10; i++) {
|
|
495
|
+
await hooks["experimental.chat.system.transform"](
|
|
496
|
+
{ sessionID: "s-sensitive-build", model: model("openrouter") },
|
|
497
|
+
{ system: [`${dynamic}-${i}`, stable] },
|
|
498
|
+
)
|
|
499
|
+
}
|
|
500
|
+
await hooks.event({
|
|
501
|
+
event: {
|
|
502
|
+
type: "message.updated",
|
|
503
|
+
properties: {
|
|
504
|
+
info: {
|
|
505
|
+
id: "msg-sensitive-1",
|
|
506
|
+
sessionID: "s-sensitive-build",
|
|
507
|
+
role: "assistant",
|
|
508
|
+
providerID: "openrouter",
|
|
509
|
+
modelID: "deepseek-chat",
|
|
510
|
+
agent: "build",
|
|
511
|
+
cost: 0.015,
|
|
512
|
+
tokens: {
|
|
513
|
+
input: 150,
|
|
514
|
+
output: 25,
|
|
515
|
+
reasoning: 0,
|
|
516
|
+
cache: { read: 70, write: 10 },
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
const raw = readFileSync(join(stateDir(cacheRoot), "events.jsonl"), "utf-8")
|
|
524
|
+
const events = raw
|
|
525
|
+
.trim()
|
|
526
|
+
.split("\n")
|
|
527
|
+
.map((line) => JSON.parse(line))
|
|
528
|
+
const types = events.map((event) => event.type)
|
|
529
|
+
|
|
530
|
+
expect(types).toContain("loaded")
|
|
531
|
+
expect(types).toContain("transform_seen")
|
|
532
|
+
expect(types).toContain("transform")
|
|
533
|
+
expect(types).toContain("warm_cache_update")
|
|
534
|
+
expect(types).toContain("metrics")
|
|
535
|
+
expect(raw).not.toContain("s-sensitive-build")
|
|
536
|
+
expect(raw).not.toContain("msg-sensitive-1")
|
|
537
|
+
|
|
538
|
+
const transform = events.find((event) => event.type === "transform")
|
|
539
|
+
expect(transform.sessionHash).toMatch(/^[a-f0-9]{16}$/)
|
|
540
|
+
expect(transform.counts).toMatchObject({ stable: 1, dynamic: 1, total: 2 })
|
|
541
|
+
expect(transform.classifier).toMatchObject({ unknown: 0 })
|
|
542
|
+
|
|
543
|
+
const seen = events.find((event) => event.type === "transform_seen")
|
|
544
|
+
expect(seen.sessionHash).toMatch(/^[a-f0-9]{16}$/)
|
|
545
|
+
expect(seen.rawBlockCount).toBe(2)
|
|
546
|
+
expect(seen.status).toBe("received")
|
|
547
|
+
|
|
548
|
+
const metrics = events.find((event) => event.type === "metrics")
|
|
549
|
+
expect(metrics.messageHash).toMatch(/^[a-f0-9]{16}$/)
|
|
550
|
+
expect(metrics.delta.cacheReadTokens).toBe(70)
|
|
551
|
+
expect(metrics.totals.cacheHitRate).toBeCloseTo(70 / (150 + 70), 12)
|
|
552
|
+
|
|
553
|
+
const warmUpdate = events.find((event) => event.type === "warm_cache_update")
|
|
554
|
+
expect(warmUpdate.scopedHashCount).toBeGreaterThan(0)
|
|
555
|
+
expect(warmUpdate.globalHashCount).toBeGreaterThanOrEqual(0)
|
|
556
|
+
})
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
it("records transform entry events for no-op system transforms", async () => {
|
|
560
|
+
await withPlugin(async (hooks, cacheRoot) => {
|
|
561
|
+
await hooks["chat.params"](
|
|
562
|
+
{ sessionID: "s-sensitive-noop", agent: "build", model: model("openrouter") },
|
|
563
|
+
{},
|
|
564
|
+
)
|
|
565
|
+
await hooks["experimental.chat.system.transform"](
|
|
566
|
+
{ sessionID: "s-sensitive-noop", model: model("openrouter") },
|
|
567
|
+
{ system: ["single block"] },
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
const raw = readFileSync(join(stateDir(cacheRoot), "events.jsonl"), "utf-8")
|
|
571
|
+
const events = raw
|
|
572
|
+
.trim()
|
|
573
|
+
.split("\n")
|
|
574
|
+
.map((line) => JSON.parse(line))
|
|
575
|
+
const seen = events.find((event) => event.type === "transform_seen")
|
|
576
|
+
|
|
577
|
+
expect(seen.scope).toBe("openrouter__deepseek-chat__build")
|
|
578
|
+
expect(seen.sessionHash).toMatch(/^[a-f0-9]{16}$/)
|
|
579
|
+
expect(seen.rawBlockCount).toBe(1)
|
|
580
|
+
expect(seen.status).toBe("skipped")
|
|
581
|
+
expect(seen.reason).toBe("insufficient_system_blocks")
|
|
582
|
+
expect(raw).not.toContain("s-sensitive-noop")
|
|
583
|
+
expect(events.some((event) => event.type === "transform")).toBe(false)
|
|
584
|
+
})
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
it("splits a single long system block before deciding whether transform is a no-op", async () => {
|
|
588
|
+
await withPlugin(async (hooks, cacheRoot) => {
|
|
589
|
+
const dynamic = "currentDate: 2026-06-25"
|
|
590
|
+
const stableA = "You are stable cacheable instructions A. ".repeat(90)
|
|
591
|
+
const stableB = "You are stable cacheable instructions B. ".repeat(90)
|
|
592
|
+
const systemBlock = [dynamic, stableA, stableB].join("\n\n")
|
|
593
|
+
const output = { system: [systemBlock] }
|
|
594
|
+
|
|
595
|
+
await hooks["chat.params"](
|
|
596
|
+
{ sessionID: "s-single-long", agent: "build", model: model("openrouter") },
|
|
597
|
+
{},
|
|
598
|
+
)
|
|
599
|
+
await hooks["experimental.chat.system.transform"](
|
|
600
|
+
{ sessionID: "s-single-long", model: model("openrouter") },
|
|
601
|
+
output,
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
expect(output.system).toEqual([stableA, stableB, dynamic])
|
|
605
|
+
|
|
606
|
+
const raw = readFileSync(join(stateDir(cacheRoot), "events.jsonl"), "utf-8")
|
|
607
|
+
const events = raw
|
|
608
|
+
.trim()
|
|
609
|
+
.split("\n")
|
|
610
|
+
.map((line) => JSON.parse(line))
|
|
611
|
+
const seen = events.find((event) => event.type === "transform_seen")
|
|
612
|
+
const transform = events.find((event) => event.type === "transform")
|
|
613
|
+
|
|
614
|
+
expect(seen.rawBlockCount).toBe(1)
|
|
615
|
+
expect(seen.splitBlockCount).toBe(3)
|
|
616
|
+
expect(seen.status).toBe("received")
|
|
617
|
+
expect(transform.counts).toMatchObject({ stable: 2, dynamic: 1, total: 3 })
|
|
618
|
+
})
|
|
619
|
+
})
|
|
620
|
+
})
|
package/src/heuristics.ts
CHANGED
|
@@ -38,9 +38,39 @@ export function coldStartScore(block: string, index: number, total: number): num
|
|
|
38
38
|
const avgLineLen = block.length / Math.max(1, lines.length)
|
|
39
39
|
if (lines.length > 15 && avgLineLen < 30) score = Math.min(score, 0.3)
|
|
40
40
|
|
|
41
|
+
const cap = volatileMetadataCap(block)
|
|
42
|
+
if (cap !== null) score = Math.min(score, cap)
|
|
43
|
+
|
|
41
44
|
return score
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Dynamic meta-info / structural patterns.
|
|
49
|
+
*
|
|
50
|
+
* Run this as a final cap so structured boosts cannot move volatile metadata
|
|
51
|
+
* back into the stable prefix.
|
|
52
|
+
*/
|
|
53
|
+
function volatileMetadataCap(block: string): number | null {
|
|
54
|
+
const dynamicPatterns = [
|
|
55
|
+
{ re: /(^|\n)\s*["']?(currentDate|current date)["']?\s*[:=]/i, cap: 0.15 },
|
|
56
|
+
{ re: /["'](currentDate|current date)["']\s*[:=]/i, cap: 0.15 },
|
|
57
|
+
{ re: /(^|\n)\s*today is\b/i, cap: 0.15 },
|
|
58
|
+
{
|
|
59
|
+
re: /(^|\n)\s*["']?(session\s*id|session|timestamp|last updated|iso timestamp)["']?\s*[:=]/i,
|
|
60
|
+
cap: 0.25,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
re: /["'](session\s*id|session|timestamp|last updated|iso timestamp)["']\s*[:=]/i,
|
|
64
|
+
cap: 0.25,
|
|
65
|
+
},
|
|
66
|
+
]
|
|
67
|
+
let cap: number | null = null
|
|
68
|
+
for (const { re, cap: nextCap } of dynamicPatterns) {
|
|
69
|
+
if (re.test(block)) cap = cap === null ? nextCap : Math.min(cap, nextCap)
|
|
70
|
+
}
|
|
71
|
+
return cap
|
|
72
|
+
}
|
|
73
|
+
|
|
44
74
|
// ── Classification ───────────────────────────────────────────────────
|
|
45
75
|
|
|
46
76
|
/**
|
|
@@ -79,11 +109,18 @@ export function classify(
|
|
|
79
109
|
// Priority 2: content-addressed score (primary)
|
|
80
110
|
const contentScore = lookupContentScore(db, hash)
|
|
81
111
|
if (contentScore !== null && db.contentObservations >= 2) {
|
|
82
|
-
if (contentScore >= 0.7) {
|
|
83
|
-
|
|
112
|
+
if (contentScore >= 0.7) {
|
|
113
|
+
result.stable.push(item)
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
if (contentScore <= 0.2) {
|
|
117
|
+
result.dynamic.push(item)
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
// Middle range (0.2–0.7): fall through to cold-start for tiered classification
|
|
84
121
|
}
|
|
85
122
|
|
|
86
|
-
// Priority 3: position-based score (fallback)
|
|
123
|
+
// Priority 3: position-based score (fallback) or cold-start heuristic
|
|
87
124
|
const known = lookupScore(db, hash)
|
|
88
125
|
let score: number
|
|
89
126
|
if (known !== null && warm) {
|
|
@@ -92,9 +129,9 @@ export function classify(
|
|
|
92
129
|
score = coldStartScore(item, i, total)
|
|
93
130
|
}
|
|
94
131
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
else result.
|
|
132
|
+
// Tiered classification: 0.5 threshold reduces unknown to near-empty
|
|
133
|
+
if (score >= 0.5) result.stable.push(item)
|
|
134
|
+
else result.dynamic.push(item)
|
|
98
135
|
}
|
|
99
136
|
|
|
100
137
|
return result
|