agent-cache-optimizer 0.5.2 → 0.5.4
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/package.json +1 -1
- package/src/index.ts +57 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-cache-optimizer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "Content-agnostic KV cache optimizer for LLM CLI agents — boosts prompt cache hit rate by 40-88% through automatic stability tracking and block reordering",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kv-cache",
|
package/src/index.ts
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* that stable content comes FIRST and dynamic content comes LAST,
|
|
6
6
|
* maximizing prefix-match cache reuse across sessions.
|
|
7
7
|
*
|
|
8
|
-
* v0.4: cache warming, savings estimates, conversation log awareness
|
|
9
|
-
*
|
|
10
8
|
* @license MIT
|
|
11
9
|
*/
|
|
12
10
|
|
|
11
|
+
const VERSION = "0.5.4"
|
|
12
|
+
|
|
13
13
|
import type { Plugin } from "@opencode-ai/plugin"
|
|
14
14
|
import { join } from "node:path"
|
|
15
15
|
import { homedir } from "node:os"
|
|
@@ -40,26 +40,12 @@ function loadDB(agent: string): StabilityDB {
|
|
|
40
40
|
const raw = readFileSync(dbPath(agent), "utf-8")
|
|
41
41
|
const db = JSON.parse(raw) as StabilityDB
|
|
42
42
|
// Migrate from pre-0.5.0: rebuild contentIndex from position data
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
((!db.contentIndex || Object.keys(db.contentIndex).length === 0) ||
|
|
47
|
-
db.contentObservations === undefined)
|
|
48
|
-
) {
|
|
43
|
+
// Migrate from pre-0.5.x: ensure contentObservations exists
|
|
44
|
+
if (db.contentObservations === undefined || db.contentObservations === null) {
|
|
45
|
+
// Reset contentIndex — old position-based counts don't map cleanly
|
|
49
46
|
db.contentIndex = {}
|
|
50
|
-
for (const fps of Object.values(db.positions)) {
|
|
51
|
-
for (const fp of fps) {
|
|
52
|
-
const existing = db.contentIndex[fp.hash]
|
|
53
|
-
if (existing) {
|
|
54
|
-
existing.count = Math.max(existing.count, fp.count)
|
|
55
|
-
if (fp.lastSeen > existing.lastSeen) existing.lastSeen = fp.lastSeen
|
|
56
|
-
} else {
|
|
57
|
-
db.contentIndex[fp.hash] = { ...fp }
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
47
|
db.contentScores = {}
|
|
62
|
-
db.contentObservations = 0
|
|
48
|
+
db.contentObservations = 0
|
|
63
49
|
saveDB(agent, db)
|
|
64
50
|
}
|
|
65
51
|
return db
|
|
@@ -141,6 +127,11 @@ function saveSavings(data: SavingsData): void {
|
|
|
141
127
|
|
|
142
128
|
// ── Diagnostics ──────────────────────────────────────────────────────
|
|
143
129
|
|
|
130
|
+
const MAX_DIAG_LINES = 1000
|
|
131
|
+
const MAX_DIAG_BYTES = 50 * 1024 // 50KB
|
|
132
|
+
const DB_PRUNE_INTERVAL = 100 // prune every N observations
|
|
133
|
+
const DB_STALE_DAYS = 7
|
|
134
|
+
|
|
144
135
|
let firstCallLogged = false
|
|
145
136
|
|
|
146
137
|
function diag(agent: string, msg: string): void {
|
|
@@ -153,6 +144,44 @@ function diag(agent: string, msg: string): void {
|
|
|
153
144
|
}
|
|
154
145
|
}
|
|
155
146
|
|
|
147
|
+
// ── Disk management ──────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
function rotateDiagLog(): void {
|
|
150
|
+
try {
|
|
151
|
+
const path = join(STATE_DIR, "diag.log")
|
|
152
|
+
if (!existsSync(path)) return
|
|
153
|
+
const stat = existsSync(path) ? readFileSync(path, "utf-8").length : 0
|
|
154
|
+
if (stat < MAX_DIAG_BYTES) return
|
|
155
|
+
const lines = readFileSync(path, "utf-8").split("\n").filter(Boolean)
|
|
156
|
+
if (lines.length <= MAX_DIAG_LINES) return
|
|
157
|
+
writeFileSync(path, lines.slice(-MAX_DIAG_LINES).join("\n") + "\n")
|
|
158
|
+
} catch {
|
|
159
|
+
/* best-effort */
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function pruneStaleHashes(db: StabilityDB): void {
|
|
164
|
+
const now = Date.now()
|
|
165
|
+
const staleMs = DB_STALE_DAYS * 24 * 60 * 60 * 1000
|
|
166
|
+
// Prune contentIndex: remove hashes not seen in STALE_DAYS with low count
|
|
167
|
+
for (const [hash, fp] of Object.entries(db.contentIndex)) {
|
|
168
|
+
if (now - fp.lastSeen > staleMs && fp.count <= 2) {
|
|
169
|
+
delete db.contentIndex[hash]
|
|
170
|
+
delete db.contentScores[hash]
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Prune position hashes similarly
|
|
174
|
+
for (const fps of Object.values(db.positions)) {
|
|
175
|
+
for (let i = fps.length - 1; i >= 0; i--) {
|
|
176
|
+
const fp = fps[i]!
|
|
177
|
+
if (now - fp.lastSeen > staleMs && fp.count <= 2) {
|
|
178
|
+
delete db.scores[fp.hash]
|
|
179
|
+
fps.splice(i, 1)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
156
185
|
// ── Plugin ───────────────────────────────────────────────────────────
|
|
157
186
|
|
|
158
187
|
export const CacheOptimizerPlugin: Plugin = async () => {
|
|
@@ -178,7 +207,14 @@ export const CacheOptimizerPlugin: Plugin = async () => {
|
|
|
178
207
|
// Persist position-based + content-addressed
|
|
179
208
|
updateDB(db, output.system)
|
|
180
209
|
updateContentDB(db, output.system)
|
|
210
|
+
|
|
211
|
+
// Periodic maintenance
|
|
212
|
+
if (db.observations % DB_PRUNE_INTERVAL === 0) {
|
|
213
|
+
pruneStaleHashes(db)
|
|
214
|
+
}
|
|
215
|
+
|
|
181
216
|
saveDB(agent, db)
|
|
217
|
+
rotateDiagLog()
|
|
182
218
|
|
|
183
219
|
// Update warm cache every 10 observations
|
|
184
220
|
if (db.observations % 10 === 0) {
|
|
@@ -215,7 +251,7 @@ export const CacheOptimizerPlugin: Plugin = async () => {
|
|
|
215
251
|
const warmCount = warmHashes?.size ?? 0
|
|
216
252
|
diag(
|
|
217
253
|
agent,
|
|
218
|
-
`
|
|
254
|
+
`v${VERSION} loaded agent=${agent} model=${input.model?.id ?? "?"} ` +
|
|
219
255
|
`warm=${warmCount}`,
|
|
220
256
|
)
|
|
221
257
|
}
|