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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. 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.2",
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
- if (
44
- db.positions &&
45
- Object.keys(db.positions).length > 0 &&
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 // warm from scratch for accurate scores
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
- `v0.5.2 loaded agent=${agent} model=${input.model?.id ?? "?"} ` +
254
+ `v${VERSION} loaded agent=${agent} model=${input.model?.id ?? "?"} ` +
219
255
  `warm=${warmCount}`,
220
256
  )
221
257
  }