agent-cache-optimizer 0.5.4 → 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 +681 -55
- package/src/splitting.ts +155 -15
package/src/index.ts
CHANGED
|
@@ -8,14 +8,23 @@
|
|
|
8
8
|
* @license MIT
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
const VERSION = "0.
|
|
11
|
+
const VERSION = "0.6.0"
|
|
12
12
|
|
|
13
13
|
import type { Plugin } from "@opencode-ai/plugin"
|
|
14
14
|
import { join } from "node:path"
|
|
15
15
|
import { homedir } from "node:os"
|
|
16
16
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs"
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
emptyDB,
|
|
19
|
+
updateDB,
|
|
20
|
+
updateContentDB,
|
|
21
|
+
extractWarmHashes,
|
|
22
|
+
estimateSavings,
|
|
23
|
+
hashContent,
|
|
24
|
+
lookupContentScore,
|
|
25
|
+
} from "./core"
|
|
18
26
|
import { classify } from "./heuristics"
|
|
27
|
+
import { splitAll } from "./splitting"
|
|
19
28
|
import type { StabilityDB } from "./types"
|
|
20
29
|
|
|
21
30
|
// ── Persistence ──────────────────────────────────────────────────────
|
|
@@ -26,8 +35,28 @@ const STATE_DIR = join(
|
|
|
26
35
|
"agent-cache-optimizer",
|
|
27
36
|
)
|
|
28
37
|
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
interface ModelIdentity {
|
|
39
|
+
id?: string
|
|
40
|
+
modelID?: string
|
|
41
|
+
providerID?: string
|
|
42
|
+
name?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function scopePart(value: string | undefined, fallback: string): string {
|
|
46
|
+
const trimmed = value?.trim()
|
|
47
|
+
return trimmed && trimmed.length > 0 ? trimmed : fallback
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function modelScope(model: ModelIdentity | undefined, agent?: string): string {
|
|
51
|
+
const provider = scopePart(model?.providerID, "unknown-provider")
|
|
52
|
+
const modelID = scopePart(model?.id ?? model?.modelID ?? model?.name, "unknown-model")
|
|
53
|
+
if (provider === "unknown-provider" && modelID === "unknown-model") return "default"
|
|
54
|
+
const normalizedAgent = agent?.trim()
|
|
55
|
+
return normalizedAgent ? `${provider}__${modelID}__${normalizedAgent}` : `${provider}__${modelID}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function dbPath(scope: string): string {
|
|
59
|
+
const safe = scope.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64) || "default"
|
|
31
60
|
return join(STATE_DIR, `stability-${safe}.json`)
|
|
32
61
|
}
|
|
33
62
|
|
|
@@ -35,9 +64,17 @@ function warmCachePath(): string {
|
|
|
35
64
|
return join(STATE_DIR, "warm-cache.json")
|
|
36
65
|
}
|
|
37
66
|
|
|
38
|
-
function
|
|
67
|
+
function cacheMetricsPath(): string {
|
|
68
|
+
return join(STATE_DIR, "cache-metrics.json")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function eventsPath(): string {
|
|
72
|
+
return join(STATE_DIR, "events.jsonl")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function loadDB(scope: string): StabilityDB {
|
|
39
76
|
try {
|
|
40
|
-
const raw = readFileSync(dbPath(
|
|
77
|
+
const raw = readFileSync(dbPath(scope), "utf-8")
|
|
41
78
|
const db = JSON.parse(raw) as StabilityDB
|
|
42
79
|
// Migrate from pre-0.5.0: rebuild contentIndex from position data
|
|
43
80
|
// Migrate from pre-0.5.x: ensure contentObservations exists
|
|
@@ -46,7 +83,7 @@ function loadDB(agent: string): StabilityDB {
|
|
|
46
83
|
db.contentIndex = {}
|
|
47
84
|
db.contentScores = {}
|
|
48
85
|
db.contentObservations = 0
|
|
49
|
-
saveDB(
|
|
86
|
+
saveDB(scope, db)
|
|
50
87
|
}
|
|
51
88
|
return db
|
|
52
89
|
} catch {
|
|
@@ -54,43 +91,143 @@ function loadDB(agent: string): StabilityDB {
|
|
|
54
91
|
}
|
|
55
92
|
}
|
|
56
93
|
|
|
57
|
-
function saveDB(
|
|
94
|
+
function saveDB(scope: string, db: StabilityDB): void {
|
|
58
95
|
try {
|
|
59
96
|
if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true })
|
|
60
97
|
db.updated = Date.now()
|
|
61
|
-
writeFileSync(dbPath(
|
|
62
|
-
} catch {
|
|
63
|
-
|
|
98
|
+
writeFileSync(dbPath(scope), JSON.stringify(db, null, 2))
|
|
99
|
+
} catch (error) {
|
|
100
|
+
logError(scope, "save_db", error)
|
|
64
101
|
}
|
|
65
102
|
}
|
|
66
103
|
|
|
104
|
+
// ── Session scope tracking ───────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
interface ScopeContext {
|
|
107
|
+
scope: string
|
|
108
|
+
familyScope: string
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const sessionScopes = new Map<string, ScopeContext>()
|
|
112
|
+
|
|
113
|
+
function familyScope(model: ModelIdentity | undefined): string {
|
|
114
|
+
return modelScope(model)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function scopeContext(model: ModelIdentity | undefined, agent?: string): ScopeContext {
|
|
118
|
+
const scope = modelScope(model, agent)
|
|
119
|
+
const modelFamily = familyScope(model)
|
|
120
|
+
return {
|
|
121
|
+
scope,
|
|
122
|
+
familyScope: modelFamily === "default" ? scope : modelFamily,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function rememberSessionScope(
|
|
127
|
+
sessionID: string | undefined,
|
|
128
|
+
model: ModelIdentity | undefined,
|
|
129
|
+
agent?: string,
|
|
130
|
+
): string {
|
|
131
|
+
const context = scopeContext(model, agent)
|
|
132
|
+
if (sessionID) sessionScopes.set(sessionID, context)
|
|
133
|
+
return context.scope
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function scopeForSession(sessionID: string | undefined, model: ModelIdentity | undefined): string {
|
|
137
|
+
if (sessionID) {
|
|
138
|
+
const known = sessionScopes.get(sessionID)
|
|
139
|
+
if (known) return known.scope
|
|
140
|
+
}
|
|
141
|
+
return scopeContext(model).scope
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function familyScopeForSession(
|
|
145
|
+
sessionID: string | undefined,
|
|
146
|
+
model: ModelIdentity | undefined,
|
|
147
|
+
): string {
|
|
148
|
+
if (sessionID) {
|
|
149
|
+
const known = sessionScopes.get(sessionID)
|
|
150
|
+
if (known) return known.familyScope
|
|
151
|
+
}
|
|
152
|
+
return scopeContext(model).familyScope
|
|
153
|
+
}
|
|
154
|
+
|
|
67
155
|
// ── Cache warming ────────────────────────────────────────────────────
|
|
68
156
|
|
|
69
|
-
|
|
157
|
+
interface WarmCacheStore {
|
|
158
|
+
version: 2
|
|
159
|
+
global: string[]
|
|
160
|
+
scopes: Record<string, string[]>
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
interface WarmCacheMemory {
|
|
164
|
+
global: Set<string>
|
|
165
|
+
scopes: Map<string, Set<string>>
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let warmCache: WarmCacheMemory = { global: new Set(), scopes: new Map() }
|
|
70
169
|
let warmHashesLoaded = false
|
|
71
170
|
|
|
72
|
-
function loadWarmCache():
|
|
73
|
-
if (warmHashesLoaded) return
|
|
171
|
+
function loadWarmCache(): WarmCacheMemory {
|
|
172
|
+
if (warmHashesLoaded) return warmCache
|
|
74
173
|
warmHashesLoaded = true
|
|
75
174
|
try {
|
|
76
175
|
const raw = readFileSync(warmCachePath(), "utf-8")
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
176
|
+
const parsed = JSON.parse(raw) as string[] | WarmCacheStore
|
|
177
|
+
if (Array.isArray(parsed)) {
|
|
178
|
+
warmCache = { global: new Set(parsed), scopes: new Map() }
|
|
179
|
+
} else {
|
|
180
|
+
warmCache = {
|
|
181
|
+
global: new Set(parsed.global ?? []),
|
|
182
|
+
scopes: new Map(
|
|
183
|
+
Object.entries(parsed.scopes ?? {}).map(([scope, hashes]) => [scope, new Set(hashes)]),
|
|
184
|
+
),
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return warmCache
|
|
80
188
|
} catch {
|
|
81
|
-
return
|
|
189
|
+
return warmCache
|
|
82
190
|
}
|
|
83
191
|
}
|
|
84
192
|
|
|
85
|
-
function
|
|
193
|
+
function warmHashesForScope(scope: string): Set<string> | undefined {
|
|
194
|
+
const scoped = warmCache.scopes.get(scope)
|
|
195
|
+
const hashes = new Set<string>(warmCache.global)
|
|
196
|
+
for (const hash of scoped ?? []) hashes.add(hash)
|
|
197
|
+
return hashes.size > 0 ? hashes : undefined
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function saveWarmCache(scope: string, db: StabilityDB): void {
|
|
86
201
|
try {
|
|
87
202
|
if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true })
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
203
|
+
warmCache.scopes.set(scope, extractWarmHashes(db))
|
|
204
|
+
|
|
205
|
+
const counts = new Map<string, number>()
|
|
206
|
+
for (const hashes of warmCache.scopes.values()) {
|
|
207
|
+
for (const hash of hashes) counts.set(hash, (counts.get(hash) ?? 0) + 1)
|
|
91
208
|
}
|
|
92
|
-
|
|
93
|
-
|
|
209
|
+
warmCache.global = new Set(
|
|
210
|
+
[...counts.entries()].filter(([, count]) => count >= 2).map(([hash]) => hash),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
const store: WarmCacheStore = {
|
|
214
|
+
version: 2,
|
|
215
|
+
global: [...warmCache.global].sort(),
|
|
216
|
+
scopes: Object.fromEntries(
|
|
217
|
+
[...warmCache.scopes.entries()]
|
|
218
|
+
.filter(([, hashes]) => hashes.size > 0)
|
|
219
|
+
.map(([scopeName, hashes]) => [scopeName, [...hashes].sort()]),
|
|
220
|
+
),
|
|
221
|
+
}
|
|
222
|
+
writeFileSync(warmCachePath(), JSON.stringify(store, null, 2))
|
|
223
|
+
eventLog("warm_cache_update", scope, {
|
|
224
|
+
scopedHashCount: store.scopes[scope]?.length ?? 0,
|
|
225
|
+
globalHashCount: store.global.length,
|
|
226
|
+
scopeCount: Object.keys(store.scopes).length,
|
|
227
|
+
observations: db.observations,
|
|
228
|
+
})
|
|
229
|
+
} catch (error) {
|
|
230
|
+
logError(scope, "save_warm_cache", error)
|
|
94
231
|
}
|
|
95
232
|
}
|
|
96
233
|
|
|
@@ -120,46 +257,364 @@ function saveSavings(data: SavingsData): void {
|
|
|
120
257
|
if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true })
|
|
121
258
|
data.updated = Date.now()
|
|
122
259
|
writeFileSync(savingsPath(), JSON.stringify(data, null, 2))
|
|
260
|
+
} catch (error) {
|
|
261
|
+
logError("global", "save_savings", error)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ── Provider cache metrics ───────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
interface CacheMetricSnapshot {
|
|
268
|
+
inputTokens: number
|
|
269
|
+
outputTokens: number
|
|
270
|
+
cacheReadTokens: number
|
|
271
|
+
cacheWriteTokens: number
|
|
272
|
+
costUSD: number
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
interface CacheMetricTotals extends CacheMetricSnapshot {
|
|
276
|
+
events: number
|
|
277
|
+
cacheHitRate: number
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
interface CacheMetricsData {
|
|
281
|
+
total: CacheMetricTotals
|
|
282
|
+
scopes: Record<string, CacheMetricTotals>
|
|
283
|
+
snapshots: Record<string, CacheMetricSnapshot>
|
|
284
|
+
updated: number
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function isMetricSnapshotIDSafe(value: string): boolean {
|
|
288
|
+
return value === "unknown" || /^[a-f0-9]{16}$/.test(value)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function metricSnapshotPart(value: unknown): string {
|
|
292
|
+
return hashID(value) ?? "unknown"
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function metricSnapshotKey(
|
|
296
|
+
source: "message" | "step",
|
|
297
|
+
sessionID: unknown,
|
|
298
|
+
itemID: unknown,
|
|
299
|
+
): string {
|
|
300
|
+
return `${source}:${metricSnapshotPart(sessionID)}:${metricSnapshotPart(itemID)}`
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function normalizeMetricSnapshotKey(key: string): string {
|
|
304
|
+
const match = /^(message|step):([^:]+):([^:]+)$/.exec(key)
|
|
305
|
+
if (!match) return key
|
|
306
|
+
const source = match[1] as "message" | "step"
|
|
307
|
+
const sessionID = match[2]!
|
|
308
|
+
const itemID = match[3]!
|
|
309
|
+
const safeSessionID = isMetricSnapshotIDSafe(sessionID) ? sessionID : hashContent(sessionID)
|
|
310
|
+
const safeItemID = isMetricSnapshotIDSafe(itemID) ? itemID : hashContent(itemID)
|
|
311
|
+
return `${source}:${safeSessionID}:${safeItemID}`
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function normalizeMetricSnapshots(
|
|
315
|
+
snapshots: Record<string, CacheMetricSnapshot> | undefined,
|
|
316
|
+
): Record<string, CacheMetricSnapshot> {
|
|
317
|
+
const normalized: Record<string, CacheMetricSnapshot> = {}
|
|
318
|
+
for (const [key, snapshot] of Object.entries(snapshots ?? {})) {
|
|
319
|
+
normalized[normalizeMetricSnapshotKey(key)] = snapshot
|
|
320
|
+
}
|
|
321
|
+
return normalized
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function sameKeys(
|
|
325
|
+
left: Record<string, CacheMetricSnapshot> | undefined,
|
|
326
|
+
right: Record<string, CacheMetricSnapshot>,
|
|
327
|
+
): boolean {
|
|
328
|
+
const leftKeys = Object.keys(left ?? {}).sort()
|
|
329
|
+
const rightKeys = Object.keys(right).sort()
|
|
330
|
+
return (
|
|
331
|
+
leftKeys.length === rightKeys.length && leftKeys.every((key, index) => key === rightKeys[index])
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function emptyMetricTotals(): CacheMetricTotals {
|
|
336
|
+
return {
|
|
337
|
+
events: 0,
|
|
338
|
+
inputTokens: 0,
|
|
339
|
+
outputTokens: 0,
|
|
340
|
+
cacheReadTokens: 0,
|
|
341
|
+
cacheWriteTokens: 0,
|
|
342
|
+
costUSD: 0,
|
|
343
|
+
cacheHitRate: 0,
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function computeCacheHitRate(inputTokens: number, cacheReadTokens: number): number {
|
|
348
|
+
const promptTokens = inputTokens + cacheReadTokens
|
|
349
|
+
return promptTokens > 0 ? cacheReadTokens / promptTokens : 0
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function refreshCacheHitRate(total: CacheMetricTotals): boolean {
|
|
353
|
+
const next = computeCacheHitRate(total.inputTokens, total.cacheReadTokens)
|
|
354
|
+
const changed = total.cacheHitRate !== next
|
|
355
|
+
total.cacheHitRate = next
|
|
356
|
+
return changed
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function loadCacheMetrics(): CacheMetricsData {
|
|
360
|
+
try {
|
|
361
|
+
const parsed = JSON.parse(readFileSync(cacheMetricsPath(), "utf-8")) as CacheMetricsData
|
|
362
|
+
const snapshots = normalizeMetricSnapshots(parsed.snapshots)
|
|
363
|
+
const data: CacheMetricsData = {
|
|
364
|
+
total: parsed.total ?? emptyMetricTotals(),
|
|
365
|
+
scopes: parsed.scopes ?? {},
|
|
366
|
+
snapshots,
|
|
367
|
+
updated: parsed.updated ?? 0,
|
|
368
|
+
}
|
|
369
|
+
let changed = !sameKeys(parsed.snapshots, snapshots)
|
|
370
|
+
changed = refreshCacheHitRate(data.total) || changed
|
|
371
|
+
for (const total of Object.values(data.scopes)) {
|
|
372
|
+
changed = refreshCacheHitRate(total) || changed
|
|
373
|
+
}
|
|
374
|
+
if (changed) {
|
|
375
|
+
data.updated = Date.now()
|
|
376
|
+
writeFileSync(cacheMetricsPath(), JSON.stringify(data, null, 2))
|
|
377
|
+
}
|
|
378
|
+
return data
|
|
123
379
|
} catch {
|
|
124
|
-
|
|
380
|
+
return { total: emptyMetricTotals(), scopes: {}, snapshots: {}, updated: 0 }
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function applyMetricDelta(total: CacheMetricTotals, delta: CacheMetricSnapshot): void {
|
|
385
|
+
total.events++
|
|
386
|
+
total.inputTokens += delta.inputTokens
|
|
387
|
+
total.outputTokens += delta.outputTokens
|
|
388
|
+
total.cacheReadTokens += delta.cacheReadTokens
|
|
389
|
+
total.cacheWriteTokens += delta.cacheWriteTokens
|
|
390
|
+
total.costUSD += delta.costUSD
|
|
391
|
+
refreshCacheHitRate(total)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function positiveDelta(current: number, previous: number | undefined): number {
|
|
395
|
+
return Math.max(0, current - (previous ?? 0))
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function saveCacheMetrics(
|
|
399
|
+
scope: string,
|
|
400
|
+
key: string,
|
|
401
|
+
current: CacheMetricSnapshot,
|
|
402
|
+
): CacheMetricsData {
|
|
403
|
+
const data = loadCacheMetrics()
|
|
404
|
+
const previous = data.snapshots[key]
|
|
405
|
+
const delta: CacheMetricSnapshot = {
|
|
406
|
+
inputTokens: positiveDelta(current.inputTokens, previous?.inputTokens),
|
|
407
|
+
outputTokens: positiveDelta(current.outputTokens, previous?.outputTokens),
|
|
408
|
+
cacheReadTokens: positiveDelta(current.cacheReadTokens, previous?.cacheReadTokens),
|
|
409
|
+
cacheWriteTokens: positiveDelta(current.cacheWriteTokens, previous?.cacheWriteTokens),
|
|
410
|
+
costUSD: positiveDelta(current.costUSD, previous?.costUSD),
|
|
125
411
|
}
|
|
412
|
+
|
|
413
|
+
if (
|
|
414
|
+
delta.inputTokens === 0 &&
|
|
415
|
+
delta.outputTokens === 0 &&
|
|
416
|
+
delta.cacheReadTokens === 0 &&
|
|
417
|
+
delta.cacheWriteTokens === 0 &&
|
|
418
|
+
delta.costUSD === 0
|
|
419
|
+
) {
|
|
420
|
+
return data
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
data.snapshots[key] = current
|
|
424
|
+
applyMetricDelta(data.total, delta)
|
|
425
|
+
data.scopes[scope] ??= emptyMetricTotals()
|
|
426
|
+
applyMetricDelta(data.scopes[scope], delta)
|
|
427
|
+
data.updated = Date.now()
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true })
|
|
431
|
+
writeFileSync(cacheMetricsPath(), JSON.stringify(data, null, 2))
|
|
432
|
+
} catch (error) {
|
|
433
|
+
logError(scope, "save_cache_metrics", error)
|
|
434
|
+
}
|
|
435
|
+
return data
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function metricSnapshotFromTokens(tokens: any, cost: unknown): CacheMetricSnapshot | null {
|
|
439
|
+
if (!tokens || typeof tokens !== "object") return null
|
|
440
|
+
const inputTokens = Number(tokens.input ?? 0)
|
|
441
|
+
const outputTokens = Number(tokens.output ?? 0)
|
|
442
|
+
const cacheReadTokens = Number(tokens.cache?.read ?? 0)
|
|
443
|
+
const cacheWriteTokens = Number(tokens.cache?.write ?? 0)
|
|
444
|
+
const costUSD = Number(cost ?? 0)
|
|
445
|
+
if (
|
|
446
|
+
inputTokens === 0 &&
|
|
447
|
+
outputTokens === 0 &&
|
|
448
|
+
cacheReadTokens === 0 &&
|
|
449
|
+
cacheWriteTokens === 0 &&
|
|
450
|
+
costUSD === 0
|
|
451
|
+
) {
|
|
452
|
+
return null
|
|
453
|
+
}
|
|
454
|
+
return { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, costUSD }
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function recordCacheMetricFromEvent(event: any): void {
|
|
458
|
+
if (event?.type === "session.next.step.started") {
|
|
459
|
+
const props = event.properties
|
|
460
|
+
rememberSessionScope(props?.sessionID, props?.model, props?.agent)
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (event?.type === "message.updated") {
|
|
465
|
+
const info = event.properties?.info
|
|
466
|
+
if (info?.role !== "assistant") return
|
|
467
|
+
const snapshot = metricSnapshotFromTokens(info.tokens, info.cost)
|
|
468
|
+
if (!snapshot) return
|
|
469
|
+
const scope =
|
|
470
|
+
info.agent && info.providerID && info.modelID
|
|
471
|
+
? modelScope({ providerID: info.providerID, modelID: info.modelID }, info.agent)
|
|
472
|
+
: scopeForSession(info.sessionID, { providerID: info.providerID, modelID: info.modelID })
|
|
473
|
+
const key = metricSnapshotKey("message", info.sessionID, info.id)
|
|
474
|
+
const previous = loadCacheMetrics().snapshots[key]
|
|
475
|
+
const data = saveCacheMetrics(scope, key, snapshot)
|
|
476
|
+
const delta = metricDelta(snapshot, previous)
|
|
477
|
+
if (isZeroMetricDelta(delta)) return
|
|
478
|
+
diag(
|
|
479
|
+
scope,
|
|
480
|
+
`metrics input:${data.scopes[scope]?.inputTokens ?? 0} ` +
|
|
481
|
+
`cacheRead:${data.scopes[scope]?.cacheReadTokens ?? 0} ` +
|
|
482
|
+
`cacheWrite:${data.scopes[scope]?.cacheWriteTokens ?? 0} ` +
|
|
483
|
+
`hitRate:${((data.scopes[scope]?.cacheHitRate ?? 0) * 100).toFixed(1)}%`,
|
|
484
|
+
)
|
|
485
|
+
eventLog("metrics", scope, {
|
|
486
|
+
sessionHash: hashID(info.sessionID),
|
|
487
|
+
messageHash: hashID(info.id),
|
|
488
|
+
source: "message.updated",
|
|
489
|
+
delta,
|
|
490
|
+
totals: data.scopes[scope] ?? emptyMetricTotals(),
|
|
491
|
+
})
|
|
492
|
+
return
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (event?.type === "session.next.step.ended") {
|
|
496
|
+
const props = event.properties
|
|
497
|
+
const snapshot = metricSnapshotFromTokens(props?.tokens, props?.cost)
|
|
498
|
+
if (!snapshot) return
|
|
499
|
+
const scope = scopeForSession(props?.sessionID, undefined)
|
|
500
|
+
const key = metricSnapshotKey("step", props?.sessionID, props?.assistantMessageID ?? event.id)
|
|
501
|
+
const previous = loadCacheMetrics().snapshots[key]
|
|
502
|
+
const data = saveCacheMetrics(scope, key, snapshot)
|
|
503
|
+
const delta = metricDelta(snapshot, previous)
|
|
504
|
+
if (isZeroMetricDelta(delta)) return
|
|
505
|
+
diag(
|
|
506
|
+
scope,
|
|
507
|
+
`metrics input:${data.scopes[scope]?.inputTokens ?? 0} ` +
|
|
508
|
+
`cacheRead:${data.scopes[scope]?.cacheReadTokens ?? 0} ` +
|
|
509
|
+
`cacheWrite:${data.scopes[scope]?.cacheWriteTokens ?? 0} ` +
|
|
510
|
+
`hitRate:${((data.scopes[scope]?.cacheHitRate ?? 0) * 100).toFixed(1)}%`,
|
|
511
|
+
)
|
|
512
|
+
eventLog("metrics", scope, {
|
|
513
|
+
sessionHash: hashID(props?.sessionID),
|
|
514
|
+
messageHash: hashID(props?.assistantMessageID ?? event.id),
|
|
515
|
+
source: "session.next.step.ended",
|
|
516
|
+
delta,
|
|
517
|
+
totals: data.scopes[scope] ?? emptyMetricTotals(),
|
|
518
|
+
})
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function metricDelta(
|
|
523
|
+
current: CacheMetricSnapshot,
|
|
524
|
+
previous: CacheMetricSnapshot | undefined,
|
|
525
|
+
): CacheMetricSnapshot {
|
|
526
|
+
return {
|
|
527
|
+
inputTokens: positiveDelta(current.inputTokens, previous?.inputTokens),
|
|
528
|
+
outputTokens: positiveDelta(current.outputTokens, previous?.outputTokens),
|
|
529
|
+
cacheReadTokens: positiveDelta(current.cacheReadTokens, previous?.cacheReadTokens),
|
|
530
|
+
cacheWriteTokens: positiveDelta(current.cacheWriteTokens, previous?.cacheWriteTokens),
|
|
531
|
+
costUSD: positiveDelta(current.costUSD, previous?.costUSD),
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function isZeroMetricDelta(delta: CacheMetricSnapshot): boolean {
|
|
536
|
+
return (
|
|
537
|
+
delta.inputTokens === 0 &&
|
|
538
|
+
delta.outputTokens === 0 &&
|
|
539
|
+
delta.cacheReadTokens === 0 &&
|
|
540
|
+
delta.cacheWriteTokens === 0 &&
|
|
541
|
+
delta.costUSD === 0
|
|
542
|
+
)
|
|
126
543
|
}
|
|
127
544
|
|
|
128
545
|
// ── Diagnostics ──────────────────────────────────────────────────────
|
|
129
546
|
|
|
130
547
|
const MAX_DIAG_LINES = 1000
|
|
131
548
|
const MAX_DIAG_BYTES = 50 * 1024 // 50KB
|
|
549
|
+
const MAX_EVENT_LINES = 5000
|
|
550
|
+
const MAX_EVENT_BYTES = 512 * 1024 // 512KB
|
|
132
551
|
const DB_PRUNE_INTERVAL = 100 // prune every N observations
|
|
133
552
|
const DB_STALE_DAYS = 7
|
|
134
553
|
|
|
135
|
-
|
|
554
|
+
const loadedScopes = new Set<string>()
|
|
136
555
|
|
|
137
|
-
function diag(
|
|
556
|
+
function diag(scope: string, msg: string): void {
|
|
138
557
|
try {
|
|
139
558
|
if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true })
|
|
140
559
|
const ts = new Date().toISOString()
|
|
141
|
-
writeFileSync(join(STATE_DIR, "diag.log"), `[${ts}] [${
|
|
560
|
+
writeFileSync(join(STATE_DIR, "diag.log"), `[${ts}] [${scope}] ${msg}\n`, { flag: "a" })
|
|
142
561
|
} catch {
|
|
143
562
|
/* silent */
|
|
144
563
|
}
|
|
145
564
|
}
|
|
146
565
|
|
|
566
|
+
function errorMessage(error: unknown): string {
|
|
567
|
+
return error instanceof Error ? error.message : String(error)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function logError(scope: string, operation: string, error: unknown): void {
|
|
571
|
+
eventLog("error", scope, {
|
|
572
|
+
operation,
|
|
573
|
+
message: errorMessage(error),
|
|
574
|
+
})
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function hashID(value: unknown): string | undefined {
|
|
578
|
+
if (typeof value !== "string" || value.length === 0) return undefined
|
|
579
|
+
return hashContent(value)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function eventLog(type: string, scope: string, data: Record<string, unknown> = {}): void {
|
|
583
|
+
try {
|
|
584
|
+
if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true })
|
|
585
|
+
rotateLineLog(eventsPath(), MAX_EVENT_BYTES, MAX_EVENT_LINES)
|
|
586
|
+
const event = {
|
|
587
|
+
ts: new Date().toISOString(),
|
|
588
|
+
version: VERSION,
|
|
589
|
+
type,
|
|
590
|
+
scope,
|
|
591
|
+
...data,
|
|
592
|
+
}
|
|
593
|
+
writeFileSync(eventsPath(), `${JSON.stringify(event)}\n`, { flag: "a" })
|
|
594
|
+
} catch {
|
|
595
|
+
/* best-effort */
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
147
599
|
// ── Disk management ──────────────────────────────────────────────────
|
|
148
600
|
|
|
149
|
-
function
|
|
601
|
+
function rotateLineLog(path: string, maxBytes: number, maxLines: number): void {
|
|
150
602
|
try {
|
|
151
|
-
const path = join(STATE_DIR, "diag.log")
|
|
152
603
|
if (!existsSync(path)) return
|
|
153
|
-
const
|
|
154
|
-
if (
|
|
155
|
-
const lines =
|
|
156
|
-
if (lines.length <=
|
|
157
|
-
writeFileSync(path, lines.slice(-
|
|
604
|
+
const content = readFileSync(path, "utf-8")
|
|
605
|
+
if (content.length < maxBytes) return
|
|
606
|
+
const lines = content.split("\n").filter(Boolean)
|
|
607
|
+
if (lines.length <= maxLines) return
|
|
608
|
+
writeFileSync(path, lines.slice(-maxLines).join("\n") + "\n")
|
|
158
609
|
} catch {
|
|
159
610
|
/* best-effort */
|
|
160
611
|
}
|
|
161
612
|
}
|
|
162
613
|
|
|
614
|
+
function rotateDiagLog(): void {
|
|
615
|
+
rotateLineLog(join(STATE_DIR, "diag.log"), MAX_DIAG_BYTES, MAX_DIAG_LINES)
|
|
616
|
+
}
|
|
617
|
+
|
|
163
618
|
function pruneStaleHashes(db: StabilityDB): void {
|
|
164
619
|
const now = Date.now()
|
|
165
620
|
const staleMs = DB_STALE_DAYS * 24 * 60 * 60 * 1000
|
|
@@ -182,6 +637,77 @@ function pruneStaleHashes(db: StabilityDB): void {
|
|
|
182
637
|
}
|
|
183
638
|
}
|
|
184
639
|
|
|
640
|
+
// ── Cross-agent stable prefix ranking ────────────────────────────────
|
|
641
|
+
|
|
642
|
+
interface WarmHashMembership {
|
|
643
|
+
global: Set<string>
|
|
644
|
+
scoped: Set<string>
|
|
645
|
+
family: Set<string>
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
interface StableRanking {
|
|
649
|
+
sharedStable: string[]
|
|
650
|
+
scopedStable: string[]
|
|
651
|
+
coldStable: string[]
|
|
652
|
+
dynamic: string[]
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function classificationWarmHashes(membership: WarmHashMembership): Set<string> {
|
|
656
|
+
const hashes = new Set<string>(membership.global)
|
|
657
|
+
for (const hash of membership.scoped) hashes.add(hash)
|
|
658
|
+
return hashes
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function warmMembershipForScope(scope: string, familyDB: StabilityDB): WarmHashMembership {
|
|
662
|
+
const cache = loadWarmCache()
|
|
663
|
+
return {
|
|
664
|
+
global: cache.global,
|
|
665
|
+
scoped: cache.scopes.get(scope) ?? new Set(),
|
|
666
|
+
family: extractWarmHashes(familyDB),
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function hasStableContentScore(db: StabilityDB, hash: string): boolean {
|
|
671
|
+
const score = lookupContentScore(db, hash)
|
|
672
|
+
return db.contentObservations >= 2 && score !== null && score >= 0.7
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function rankStableBlocks(
|
|
676
|
+
stableBlocks: string[],
|
|
677
|
+
dynamicBlocks: string[],
|
|
678
|
+
scopeDB: StabilityDB,
|
|
679
|
+
familyDB: StabilityDB,
|
|
680
|
+
warmMembership: WarmHashMembership,
|
|
681
|
+
): StableRanking {
|
|
682
|
+
const ranking: StableRanking = {
|
|
683
|
+
sharedStable: [],
|
|
684
|
+
scopedStable: [],
|
|
685
|
+
coldStable: [],
|
|
686
|
+
dynamic: dynamicBlocks,
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
for (const block of stableBlocks) {
|
|
690
|
+
const hash = hashContent(block)
|
|
691
|
+
if (
|
|
692
|
+
warmMembership.global.has(hash) ||
|
|
693
|
+
warmMembership.family.has(hash) ||
|
|
694
|
+
hasStableContentScore(familyDB, hash)
|
|
695
|
+
) {
|
|
696
|
+
ranking.sharedStable.push(block)
|
|
697
|
+
continue
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (warmMembership.scoped.has(hash) || hasStableContentScore(scopeDB, hash)) {
|
|
701
|
+
ranking.scopedStable.push(block)
|
|
702
|
+
continue
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
ranking.coldStable.push(block)
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return ranking
|
|
709
|
+
}
|
|
710
|
+
|
|
185
711
|
// ── Plugin ───────────────────────────────────────────────────────────
|
|
186
712
|
|
|
187
713
|
export const CacheOptimizerPlugin: Plugin = async () => {
|
|
@@ -193,32 +719,75 @@ export const CacheOptimizerPlugin: Plugin = async () => {
|
|
|
193
719
|
|
|
194
720
|
"experimental.chat.system.transform": async (input, output) => {
|
|
195
721
|
const rawBlocks = output.system
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
722
|
+
const rawBlockCount = rawBlocks?.length ?? 0
|
|
723
|
+
const scope = scopeForSession(input.sessionID, input.model)
|
|
724
|
+
const splitBlocks = rawBlocks ? splitAll(rawBlocks) : []
|
|
725
|
+
const splitBlockCount = splitBlocks.length
|
|
726
|
+
if (splitBlockCount <= 1) {
|
|
727
|
+
eventLog("transform_seen", scope, {
|
|
728
|
+
sessionHash: hashID(input.sessionID),
|
|
729
|
+
rawBlockCount,
|
|
730
|
+
splitBlockCount,
|
|
731
|
+
status: "skipped",
|
|
732
|
+
reason: "insufficient_system_blocks",
|
|
733
|
+
})
|
|
734
|
+
return
|
|
735
|
+
}
|
|
736
|
+
eventLog("transform_seen", scope, {
|
|
737
|
+
sessionHash: hashID(input.sessionID),
|
|
738
|
+
rawBlockCount,
|
|
739
|
+
splitBlockCount,
|
|
740
|
+
status: "received",
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
const family = familyScopeForSession(input.sessionID, input.model)
|
|
744
|
+
const db = loadDB(scope)
|
|
745
|
+
const familyDB = family === scope ? db : loadDB(family)
|
|
746
|
+
const warmMembership = warmMembershipForScope(scope, familyDB)
|
|
747
|
+
|
|
748
|
+
const classified = classify(splitBlocks, db, {
|
|
749
|
+
splitThreshold: Number.MAX_SAFE_INTEGER,
|
|
750
|
+
warmHashes: classificationWarmHashes(warmMembership),
|
|
751
|
+
})
|
|
752
|
+
|
|
753
|
+
const ranked = rankStableBlocks(
|
|
754
|
+
classified.stable,
|
|
755
|
+
[...classified.unknown, ...classified.dynamic],
|
|
756
|
+
db,
|
|
757
|
+
familyDB,
|
|
758
|
+
warmMembership,
|
|
759
|
+
)
|
|
203
760
|
|
|
204
|
-
|
|
205
|
-
|
|
761
|
+
output.system = [
|
|
762
|
+
...ranked.sharedStable,
|
|
763
|
+
...ranked.scopedStable,
|
|
764
|
+
...ranked.coldStable,
|
|
765
|
+
...ranked.dynamic,
|
|
766
|
+
]
|
|
206
767
|
|
|
207
768
|
// Persist position-based + content-addressed
|
|
208
769
|
updateDB(db, output.system)
|
|
209
770
|
updateContentDB(db, output.system)
|
|
771
|
+
if (family !== scope) {
|
|
772
|
+
updateDB(familyDB, output.system)
|
|
773
|
+
updateContentDB(familyDB, output.system)
|
|
774
|
+
}
|
|
210
775
|
|
|
211
776
|
// Periodic maintenance
|
|
212
777
|
if (db.observations % DB_PRUNE_INTERVAL === 0) {
|
|
213
778
|
pruneStaleHashes(db)
|
|
214
779
|
}
|
|
780
|
+
if (family !== scope && familyDB.observations % DB_PRUNE_INTERVAL === 0) {
|
|
781
|
+
pruneStaleHashes(familyDB)
|
|
782
|
+
}
|
|
215
783
|
|
|
216
|
-
saveDB(
|
|
784
|
+
saveDB(scope, db)
|
|
785
|
+
if (family !== scope) saveDB(family, familyDB)
|
|
217
786
|
rotateDiagLog()
|
|
218
787
|
|
|
219
788
|
// Update warm cache every 10 observations
|
|
220
789
|
if (db.observations % 10 === 0) {
|
|
221
|
-
saveWarmCache(db)
|
|
790
|
+
saveWarmCache(scope, db)
|
|
222
791
|
}
|
|
223
792
|
|
|
224
793
|
// Track savings
|
|
@@ -226,37 +795,83 @@ export const CacheOptimizerPlugin: Plugin = async () => {
|
|
|
226
795
|
const savings = loadSavings()
|
|
227
796
|
savings.totalStableBytes += stableBytes
|
|
228
797
|
savings.totalObservations++
|
|
229
|
-
savings.estimatedSavingsUSD = estimateSavings(savings.totalStableBytes,
|
|
798
|
+
savings.estimatedSavingsUSD = estimateSavings(savings.totalStableBytes, 1)
|
|
230
799
|
saveSavings(savings)
|
|
800
|
+
const sharedPrefixBytes = ranked.sharedStable.reduce((s, b) => s + b.length, 0)
|
|
231
801
|
|
|
232
802
|
// Diagnostic log with savings
|
|
233
803
|
const estCallSaving = estimateSavings(stableBytes, 1)
|
|
804
|
+
const warmCount = warmHashesForScope(scope)?.size ?? 0
|
|
234
805
|
diag(
|
|
235
|
-
|
|
806
|
+
scope,
|
|
236
807
|
`S:${classified.stable.length} U:${classified.unknown.length} ` +
|
|
237
808
|
`D:${classified.dynamic.length} T:${output.system.length} ` +
|
|
809
|
+
`SH:${ranked.sharedStable.length} SC:${ranked.scopedStable.length} ` +
|
|
810
|
+
`CS:${ranked.coldStable.length} ` +
|
|
238
811
|
`obs:${db.observations} ` +
|
|
239
812
|
`stableKB:${(stableBytes / 1024).toFixed(1)} ` +
|
|
813
|
+
`sharedKB:${(sharedPrefixBytes / 1024).toFixed(1)} ` +
|
|
240
814
|
`saved:$${estCallSaving.toFixed(6)} ` +
|
|
241
815
|
`total:$${savings.estimatedSavingsUSD.toFixed(4)}`,
|
|
242
816
|
)
|
|
817
|
+
eventLog("transform", scope, {
|
|
818
|
+
sessionHash: hashID(input.sessionID),
|
|
819
|
+
family,
|
|
820
|
+
counts: {
|
|
821
|
+
stable: classified.stable.length,
|
|
822
|
+
unknown: classified.unknown.length,
|
|
823
|
+
dynamic: classified.dynamic.length,
|
|
824
|
+
total: output.system.length,
|
|
825
|
+
},
|
|
826
|
+
classifier: {
|
|
827
|
+
unknown: classified.unknown.length,
|
|
828
|
+
warmHashes: warmCount,
|
|
829
|
+
},
|
|
830
|
+
ranking: {
|
|
831
|
+
sharedStable: ranked.sharedStable.length,
|
|
832
|
+
scopedStable: ranked.scopedStable.length,
|
|
833
|
+
coldStable: ranked.coldStable.length,
|
|
834
|
+
dynamic: ranked.dynamic.length,
|
|
835
|
+
sharedPrefixBytes,
|
|
836
|
+
},
|
|
837
|
+
stableBytes,
|
|
838
|
+
estimatedCallSavingsUSD: estCallSaving,
|
|
839
|
+
totalEstimatedSavingsUSD: savings.estimatedSavingsUSD,
|
|
840
|
+
observations: db.observations,
|
|
841
|
+
})
|
|
243
842
|
},
|
|
244
843
|
|
|
245
844
|
// ── Diagnostic: chat.params (confirms plugin loaded) ──────────
|
|
246
845
|
|
|
247
846
|
"chat.params": async (input, _output) => {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const warmCount =
|
|
847
|
+
const scope = rememberSessionScope(input.sessionID, input.model, input.agent)
|
|
848
|
+
if (!loadedScopes.has(scope)) {
|
|
849
|
+
loadedScopes.add(scope)
|
|
850
|
+
const warmCount = warmHashesForScope(scope)?.size ?? 0
|
|
252
851
|
diag(
|
|
253
|
-
|
|
254
|
-
`v${VERSION} loaded agent=${
|
|
852
|
+
scope,
|
|
853
|
+
`v${VERSION} loaded agent=${input.agent ?? "unknown"} ` +
|
|
854
|
+
`provider=${input.model?.providerID ?? "?"} model=${input.model?.id ?? "?"} ` +
|
|
255
855
|
`warm=${warmCount}`,
|
|
256
856
|
)
|
|
857
|
+
eventLog("loaded", scope, {
|
|
858
|
+
sessionHash: hashID(input.sessionID),
|
|
859
|
+
provider: input.model?.providerID ?? "unknown-provider",
|
|
860
|
+
model: input.model?.id ?? "unknown-model",
|
|
861
|
+
agent: input.agent ?? "unknown",
|
|
862
|
+
warmCount,
|
|
863
|
+
})
|
|
257
864
|
}
|
|
258
865
|
},
|
|
259
866
|
|
|
867
|
+
"chat.message": async (input, _output) => {
|
|
868
|
+
rememberSessionScope(input.sessionID, input.model, input.agent)
|
|
869
|
+
},
|
|
870
|
+
|
|
871
|
+
event: async (input) => {
|
|
872
|
+
recordCacheMetricFromEvent(input.event)
|
|
873
|
+
},
|
|
874
|
+
|
|
260
875
|
// ── Provider cache headers ────────────────────────────────────
|
|
261
876
|
|
|
262
877
|
"chat.headers": async (input, output) => {
|
|
@@ -270,7 +885,18 @@ export const CacheOptimizerPlugin: Plugin = async () => {
|
|
|
270
885
|
}
|
|
271
886
|
|
|
272
887
|
// Re-exports
|
|
273
|
-
export {
|
|
888
|
+
export {
|
|
889
|
+
emptyDB,
|
|
890
|
+
updateDB,
|
|
891
|
+
updateContentDB,
|
|
892
|
+
hashContent,
|
|
893
|
+
lookupScore,
|
|
894
|
+
lookupContentScore,
|
|
895
|
+
isWarm,
|
|
896
|
+
extractWarmHashes,
|
|
897
|
+
isWarmHash,
|
|
898
|
+
estimateSavings,
|
|
899
|
+
} from "./core"
|
|
274
900
|
export { coldStartScore, classify } from "./heuristics"
|
|
275
901
|
export { splitBlock, splitAll } from "./splitting"
|
|
276
902
|
export type { StabilityDB, Classified, BlockFingerprint, CacheOptimizerOptions } from "./types"
|