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
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,25 +257,457 @@ 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),
|
|
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
|
|
125
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
|
|
548
|
+
const MAX_DIAG_BYTES = 50 * 1024 // 50KB
|
|
549
|
+
const MAX_EVENT_LINES = 5000
|
|
550
|
+
const MAX_EVENT_BYTES = 512 * 1024 // 512KB
|
|
551
|
+
const DB_PRUNE_INTERVAL = 100 // prune every N observations
|
|
552
|
+
const DB_STALE_DAYS = 7
|
|
553
|
+
|
|
554
|
+
const loadedScopes = new Set<string>()
|
|
131
555
|
|
|
132
|
-
function diag(
|
|
556
|
+
function diag(scope: string, msg: string): void {
|
|
133
557
|
try {
|
|
134
558
|
if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true })
|
|
135
559
|
const ts = new Date().toISOString()
|
|
136
|
-
writeFileSync(join(STATE_DIR, "diag.log"), `[${ts}] [${
|
|
560
|
+
writeFileSync(join(STATE_DIR, "diag.log"), `[${ts}] [${scope}] ${msg}\n`, { flag: "a" })
|
|
137
561
|
} catch {
|
|
138
562
|
/* silent */
|
|
139
563
|
}
|
|
140
564
|
}
|
|
141
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
|
+
|
|
599
|
+
// ── Disk management ──────────────────────────────────────────────────
|
|
600
|
+
|
|
601
|
+
function rotateLineLog(path: string, maxBytes: number, maxLines: number): void {
|
|
602
|
+
try {
|
|
603
|
+
if (!existsSync(path)) return
|
|
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")
|
|
609
|
+
} catch {
|
|
610
|
+
/* best-effort */
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function rotateDiagLog(): void {
|
|
615
|
+
rotateLineLog(join(STATE_DIR, "diag.log"), MAX_DIAG_BYTES, MAX_DIAG_LINES)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function pruneStaleHashes(db: StabilityDB): void {
|
|
619
|
+
const now = Date.now()
|
|
620
|
+
const staleMs = DB_STALE_DAYS * 24 * 60 * 60 * 1000
|
|
621
|
+
// Prune contentIndex: remove hashes not seen in STALE_DAYS with low count
|
|
622
|
+
for (const [hash, fp] of Object.entries(db.contentIndex)) {
|
|
623
|
+
if (now - fp.lastSeen > staleMs && fp.count <= 2) {
|
|
624
|
+
delete db.contentIndex[hash]
|
|
625
|
+
delete db.contentScores[hash]
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
// Prune position hashes similarly
|
|
629
|
+
for (const fps of Object.values(db.positions)) {
|
|
630
|
+
for (let i = fps.length - 1; i >= 0; i--) {
|
|
631
|
+
const fp = fps[i]!
|
|
632
|
+
if (now - fp.lastSeen > staleMs && fp.count <= 2) {
|
|
633
|
+
delete db.scores[fp.hash]
|
|
634
|
+
fps.splice(i, 1)
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
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
|
+
|
|
142
711
|
// ── Plugin ───────────────────────────────────────────────────────────
|
|
143
712
|
|
|
144
713
|
export const CacheOptimizerPlugin: Plugin = async () => {
|
|
@@ -150,25 +719,75 @@ export const CacheOptimizerPlugin: Plugin = async () => {
|
|
|
150
719
|
|
|
151
720
|
"experimental.chat.system.transform": async (input, output) => {
|
|
152
721
|
const rawBlocks = output.system
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
+
)
|
|
160
760
|
|
|
161
|
-
|
|
162
|
-
|
|
761
|
+
output.system = [
|
|
762
|
+
...ranked.sharedStable,
|
|
763
|
+
...ranked.scopedStable,
|
|
764
|
+
...ranked.coldStable,
|
|
765
|
+
...ranked.dynamic,
|
|
766
|
+
]
|
|
163
767
|
|
|
164
768
|
// Persist position-based + content-addressed
|
|
165
769
|
updateDB(db, output.system)
|
|
166
770
|
updateContentDB(db, output.system)
|
|
167
|
-
|
|
771
|
+
if (family !== scope) {
|
|
772
|
+
updateDB(familyDB, output.system)
|
|
773
|
+
updateContentDB(familyDB, output.system)
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Periodic maintenance
|
|
777
|
+
if (db.observations % DB_PRUNE_INTERVAL === 0) {
|
|
778
|
+
pruneStaleHashes(db)
|
|
779
|
+
}
|
|
780
|
+
if (family !== scope && familyDB.observations % DB_PRUNE_INTERVAL === 0) {
|
|
781
|
+
pruneStaleHashes(familyDB)
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
saveDB(scope, db)
|
|
785
|
+
if (family !== scope) saveDB(family, familyDB)
|
|
786
|
+
rotateDiagLog()
|
|
168
787
|
|
|
169
788
|
// Update warm cache every 10 observations
|
|
170
789
|
if (db.observations % 10 === 0) {
|
|
171
|
-
saveWarmCache(db)
|
|
790
|
+
saveWarmCache(scope, db)
|
|
172
791
|
}
|
|
173
792
|
|
|
174
793
|
// Track savings
|
|
@@ -176,37 +795,83 @@ export const CacheOptimizerPlugin: Plugin = async () => {
|
|
|
176
795
|
const savings = loadSavings()
|
|
177
796
|
savings.totalStableBytes += stableBytes
|
|
178
797
|
savings.totalObservations++
|
|
179
|
-
savings.estimatedSavingsUSD = estimateSavings(savings.totalStableBytes,
|
|
798
|
+
savings.estimatedSavingsUSD = estimateSavings(savings.totalStableBytes, 1)
|
|
180
799
|
saveSavings(savings)
|
|
800
|
+
const sharedPrefixBytes = ranked.sharedStable.reduce((s, b) => s + b.length, 0)
|
|
181
801
|
|
|
182
802
|
// Diagnostic log with savings
|
|
183
803
|
const estCallSaving = estimateSavings(stableBytes, 1)
|
|
804
|
+
const warmCount = warmHashesForScope(scope)?.size ?? 0
|
|
184
805
|
diag(
|
|
185
|
-
|
|
806
|
+
scope,
|
|
186
807
|
`S:${classified.stable.length} U:${classified.unknown.length} ` +
|
|
187
808
|
`D:${classified.dynamic.length} T:${output.system.length} ` +
|
|
809
|
+
`SH:${ranked.sharedStable.length} SC:${ranked.scopedStable.length} ` +
|
|
810
|
+
`CS:${ranked.coldStable.length} ` +
|
|
188
811
|
`obs:${db.observations} ` +
|
|
189
812
|
`stableKB:${(stableBytes / 1024).toFixed(1)} ` +
|
|
813
|
+
`sharedKB:${(sharedPrefixBytes / 1024).toFixed(1)} ` +
|
|
190
814
|
`saved:$${estCallSaving.toFixed(6)} ` +
|
|
191
815
|
`total:$${savings.estimatedSavingsUSD.toFixed(4)}`,
|
|
192
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
|
+
})
|
|
193
842
|
},
|
|
194
843
|
|
|
195
844
|
// ── Diagnostic: chat.params (confirms plugin loaded) ──────────
|
|
196
845
|
|
|
197
846
|
"chat.params": async (input, _output) => {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
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
|
|
202
851
|
diag(
|
|
203
|
-
|
|
204
|
-
`v${VERSION} loaded agent=${
|
|
852
|
+
scope,
|
|
853
|
+
`v${VERSION} loaded agent=${input.agent ?? "unknown"} ` +
|
|
854
|
+
`provider=${input.model?.providerID ?? "?"} model=${input.model?.id ?? "?"} ` +
|
|
205
855
|
`warm=${warmCount}`,
|
|
206
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
|
+
})
|
|
207
864
|
}
|
|
208
865
|
},
|
|
209
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
|
+
|
|
210
875
|
// ── Provider cache headers ────────────────────────────────────
|
|
211
876
|
|
|
212
877
|
"chat.headers": async (input, output) => {
|
|
@@ -220,7 +885,18 @@ export const CacheOptimizerPlugin: Plugin = async () => {
|
|
|
220
885
|
}
|
|
221
886
|
|
|
222
887
|
// Re-exports
|
|
223
|
-
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"
|
|
224
900
|
export { coldStartScore, classify } from "./heuristics"
|
|
225
901
|
export { splitBlock, splitAll } from "./splitting"
|
|
226
902
|
export type { StabilityDB, Classified, BlockFingerprint, CacheOptimizerOptions } from "./types"
|