@jmtrin/opencode-kevin 0.3.0 → 0.5.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.
Files changed (72) hide show
  1. package/README.md +117 -9
  2. package/dist/migrations/005_v04_signal.sql +57 -0
  3. package/dist/migrations/006_v05_glassbox.sql +118 -0
  4. package/dist/plugin/Archiver.d.ts +42 -0
  5. package/dist/plugin/Archiver.js +98 -0
  6. package/dist/plugin/Archiver.js.map +1 -0
  7. package/dist/plugin/CausalChain.d.ts +13 -2
  8. package/dist/plugin/CausalChain.js +83 -13
  9. package/dist/plugin/CausalChain.js.map +1 -1
  10. package/dist/plugin/ContextInjector.d.ts +152 -4
  11. package/dist/plugin/ContextInjector.js +400 -93
  12. package/dist/plugin/ContextInjector.js.map +1 -1
  13. package/dist/plugin/Feedback.d.ts +67 -0
  14. package/dist/plugin/Feedback.js +137 -0
  15. package/dist/plugin/Feedback.js.map +1 -0
  16. package/dist/plugin/InjectionLedger.d.ts +92 -0
  17. package/dist/plugin/InjectionLedger.js +243 -0
  18. package/dist/plugin/InjectionLedger.js.map +1 -0
  19. package/dist/plugin/LessonFixer.d.ts +44 -0
  20. package/dist/plugin/LessonFixer.js +46 -0
  21. package/dist/plugin/LessonFixer.js.map +1 -0
  22. package/dist/plugin/MemoryService.d.ts +81 -1
  23. package/dist/plugin/MemoryService.js +321 -54
  24. package/dist/plugin/MemoryService.js.map +1 -1
  25. package/dist/plugin/Migrate.js +31 -0
  26. package/dist/plugin/Migrate.js.map +1 -1
  27. package/dist/plugin/QualityGate.d.ts +110 -0
  28. package/dist/plugin/QualityGate.js +108 -0
  29. package/dist/plugin/QualityGate.js.map +1 -0
  30. package/dist/plugin/Reflector.d.ts +17 -0
  31. package/dist/plugin/Reflector.js +81 -26
  32. package/dist/plugin/Reflector.js.map +1 -1
  33. package/dist/plugin/Retrospective.js +22 -1
  34. package/dist/plugin/Retrospective.js.map +1 -1
  35. package/dist/plugin/ToolCallObserver.d.ts +1 -0
  36. package/dist/plugin/ToolCallObserver.js +15 -8
  37. package/dist/plugin/ToolCallObserver.js.map +1 -1
  38. package/dist/plugin/confidence.d.ts +8 -0
  39. package/dist/plugin/confidence.js +35 -0
  40. package/dist/plugin/confidence.js.map +1 -0
  41. package/dist/plugin/index.d.ts +5 -0
  42. package/dist/plugin/index.js +368 -44
  43. package/dist/plugin/index.js.map +1 -1
  44. package/dist/plugin/kevin_audit.d.ts +49 -0
  45. package/dist/plugin/kevin_audit.js +141 -0
  46. package/dist/plugin/kevin_audit.js.map +1 -0
  47. package/dist/plugin/kevin_why.d.ts +4 -0
  48. package/dist/plugin/kevin_why.js +72 -35
  49. package/dist/plugin/kevin_why.js.map +1 -1
  50. package/dist/plugin/memory-format.d.ts +12 -0
  51. package/dist/plugin/memory-format.js +45 -5
  52. package/dist/plugin/memory-format.js.map +1 -1
  53. package/dist/plugin/metrics.d.ts +27 -1
  54. package/dist/plugin/metrics.js +61 -0
  55. package/dist/plugin/metrics.js.map +1 -1
  56. package/dist/plugin/okf-export.js +63 -22
  57. package/dist/plugin/okf-export.js.map +1 -1
  58. package/dist/plugin/okf-import.d.ts +4 -1
  59. package/dist/plugin/okf-import.js +45 -12
  60. package/dist/plugin/okf-import.js.map +1 -1
  61. package/dist/plugin/query-tokenizer.d.ts +13 -0
  62. package/dist/plugin/query-tokenizer.js +86 -0
  63. package/dist/plugin/query-tokenizer.js.map +1 -0
  64. package/dist/plugin/replay-types.d.ts +327 -0
  65. package/dist/plugin/replay-types.js +65 -0
  66. package/dist/plugin/replay-types.js.map +1 -0
  67. package/dist/plugin/replay.d.ts +36 -0
  68. package/dist/plugin/replay.js +193 -0
  69. package/dist/plugin/replay.js.map +1 -0
  70. package/migrations/005_v04_signal.sql +57 -0
  71. package/migrations/006_v05_glassbox.sql +118 -0
  72. package/package.json +3 -2
package/README.md CHANGED
@@ -7,6 +7,9 @@ Kevin is an [OpenCode](https://opencode.ai) plugin that **observes** every agent
7
7
  - **Local-first**: SQLite + FTS5, no external services, no network calls.
8
8
  - **Global memory**: a single `~/.opencode-kevin/kevin.db` shared across all your projects (WAL mode → safe for concurrent sessions). No per-project folders.
9
9
  - **Knowledge + Causality (v0.3.0)**: causal failure→fix chains, `kevin_why` explanations, OKF export/import, a supersede model, and human-in-the-loop AGENTS.md suggestions.
10
+ - **Signal over Noise (v0.4.0)**: a quality gate that stores weak lessons without injecting them, an injection ledger with honest `precision_rate`, two-sided confidence, and a fixed compacting hook.
11
+ - **Glass Box (v0.5.0)**: honest measurement replaces estimates — three-way injection settlement (`effective` / `ineffective` / `inconclusive`), human feedback that actually moves confidence, a strict dry-run `kevin_trace`, a read-only `kevin_audit`, memory lifecycle completion (`superseded_by`, `ignored`, `archived`), and a hermetic replay harness. See [How Kevin measures itself](#how-kevin-measures-itself).
12
+ - **Audited**: the v0.4.0 bug catalog (`docs/Kevin_v0.4.0_Bugs.md`) is fully closed — 16/16 bugs fixed and regression-tested (evidence in `kevin_query`/`kevin_get`, OKF round-trip fidelity, causal refresh guard, redaction precision, cross-session isolation).
10
13
  - **Standalone**: works without any other plugin. With the ecosystem, it learns more richly.
11
14
 
12
15
  ---
@@ -31,7 +34,7 @@ For a **single project**, put the same `plugin` array in `./opencode.json` or `.
31
34
 
32
35
  ### 2. Restart OpenCode
33
36
 
34
- Config is loaded once at startup and is **not hot-reloaded** — quit and reopen OpenCode after editing. On start, OpenCode resolves the npm spec, caches the plugin in `~/.cache/opencode/packages/@jmtrin/opencode-kevin/`, and exposes nine tools: `kevin_save`, `kevin_query`, `kevin_get`, `kevin_recall`, `kevin_status`, `kevin_retrospective`, `kevin_why`, `kevin_export`, `kevin_import`.
37
+ Config is loaded once at startup and is **not hot-reloaded** — quit and reopen OpenCode after editing. On start, OpenCode resolves the npm spec, caches the plugin in `~/.cache/opencode/packages/@jmtrin/opencode-kevin/`, and exposes thirteen tools: `kevin_save`, `kevin_query`, `kevin_get`, `kevin_recall`, `kevin_status`, `kevin_retrospective`, `kevin_why`, `kevin_export`, `kevin_import`, `kevin_config`, `kevin_feedback`, `kevin_trace`, `kevin_audit` (v0.5.0 adds the last three).
35
38
 
36
39
  ### 3. Where data lives
37
40
 
@@ -114,7 +117,7 @@ Use `:memory:` for `dbPath` in tests.
114
117
  ┌─────────────────┐ │
115
118
  │ SHARE │◄──────────────┘
116
119
  │ ContextInjector │ injects relevant lessons pre-prompt
117
- │ │ (1500 tokens) + on compacting (2000)
120
+ │ │ (900 tokens, configurable via pre_prompt_budget_tokens) + on compacting (2000)
118
121
  │ │ + <protect> + id: line (v0.2.0)
119
122
  │ │ + origin-aware rank (v0.2.0)
120
123
  │ │ + <kevin-suggestion> after negative
@@ -135,7 +138,7 @@ Use `:memory:` for `dbPath` in tests.
135
138
 
136
139
  ## Tools
137
140
 
138
- Kevin exposes 9 tools callable by the agent:
141
+ Kevin exposes 13 tools callable by the agent:
139
142
 
140
143
  ### `kevin_save`
141
144
 
@@ -171,7 +174,8 @@ kevin_get({ id: "0195a3b2-..." })
171
174
  // → { "id": "...", "type": "error", "content": "...", "scope": "project",
172
175
  // "relevanceScore": 0.55, "origin": "reflector", "fingerprint": "cbf29ce484222325",
173
176
  // "projectId": null, "metadata": null,
174
- // "evidenceCount": 0, "lastVerifiedAt": null, "status": "active" }
177
+ // "evidenceCount": 2, "recurrenceCount": 1, "lastVerifiedAt": "2026-08-01 10:00:00",
178
+ // "status": "active", "confidence": 0.55, "fixArgs": "npm i -g rg" }
175
179
  ```
176
180
 
177
181
  ### `kevin_recall`
@@ -185,7 +189,7 @@ kevin_recall({ query: "auth", limit: 3 })
185
189
 
186
190
  ### `kevin_status`
187
191
 
188
- Global counts and metrics. v0.2.0 adds `memories_reflector`, `memories_agent`, `memories_pattern` and a `metrics` object; v0.3.0 adds `memories_causal` and 3 more seeded counters (`patterns_causal`, `causal_links`, `memories_superseded`).
192
+ Global counts and metrics. v0.2.0 adds `memories_reflector`, `memories_agent`, `memories_pattern` and a `metrics` object; v0.3.0 adds `memories_causal` and 3 more seeded counters (`patterns_causal`, `causal_links`, `memories_superseded`); v0.4.0 adds the precision block: `injections_total`, `injections_effective`, `injections_ineffective`, `precision_rate`, `patterns_promoted_new`, and per-origin `recurrence_by_origin`. v0.5.0 adds `injections_inconclusive`, `coverage_rate`, `blocked`, `memories_ignored`, `memories_archived` and `feedback { positive, negative }`.
189
193
 
190
194
  ```
191
195
  kevin_status({})
@@ -194,7 +198,14 @@ kevin_status({})
194
198
  // "metrics": { "tokens_injected_pre_prompt": 51, "tokens_injected_compacting": 0,
195
199
  // "reflections_throttled": 3, "duplicate_suppressions": 2,
196
200
  // "tool_calls_deduped": 0, "patterns_mined": 0,
197
- // "patterns_causal": 1, "causal_links": 2, "memories_superseded": 0 } }
201
+ // "patterns_causal": 1, "causal_links": 2, "memories_superseded": 0,
202
+ // "injections_inconclusive": 9, "injections_blocked_seen": 1, ... },
203
+ // "injections_total": 14, "injections_effective": 2, "injections_ineffective": 3,
204
+ // "injections_inconclusive": 9, "precision_rate": 0.40, "coverage_rate": 0.36,
205
+ // "blocked": { "seen": 1, "weak": 0, "recurrence": 2, "stale": 0, "ignored": 1 },
206
+ // "memories_ignored": 1, "memories_archived": 4,
207
+ // "feedback": { "positive": 2, "negative": 1 },
208
+ // "patterns_promoted_new": 2, "recurrence_by_origin": { "reflector": 3, "causal": 1 } }
198
209
  ```
199
210
 
200
211
  ### `kevin_retrospective`
@@ -221,12 +232,90 @@ kevin_why({ query: "TS2304 cannot find name" })
221
232
 
222
233
  ### `kevin_export` (v0.3.0)
223
234
 
224
- Exports knowledge for sharing: `decision`/`rule`/`pattern` memories (active only, no raw errors) as YAML-frontmatter blocks (`format: "okf"`) or markdown (`format: "markdown"`). Includes `id`, `type`, `confidence`, `evidence_count`, `last_verified_at`, `fingerprint`.
235
+ Exports knowledge for sharing: `decision`/`rule`/`pattern` memories (active only, no raw errors) as YAML-frontmatter blocks (`format: "okf"`) or markdown (`format: "markdown"`). Includes `id`, `type`, `confidence` (two-sided v0.4.0 formula), `evidence_count`, `recurrence_count`, `last_verified_at`, `fingerprint`. Timestamps are treated as UTC — a re-import reproduces the exact source values.
225
236
 
226
237
  ### `kevin_import` (v0.3.0)
227
238
 
228
239
  Ingests an exported bundle. Each entry becomes a `context` memory with `origin='imported'`; a fingerprint collision with an existing `decision`/`rule` supersedes the old row. Returns `{ imported, superseded }`.
229
240
 
241
+ ### `kevin_config` (v0.4.0)
242
+
243
+ Reads/writes `kevin_settings` without SQL. `action: "list"` returns every setting; `action: "set"` upserts a value (default `"1"` when omitted) and rejects unknown keys unless `strict: false`.
244
+
245
+ ```
246
+ kevin_config({ action: "list" })
247
+ // → { "quality_gate_enabled": "1", "lesson_snippet_injection": "1", "llm_reflection_enabled": "0", ... }
248
+
249
+ kevin_config({ action: "set", key: "quality_gate_enabled", value: "0" })
250
+ // → { "ok": true }
251
+ ```
252
+
253
+ Known keys: `quality_gate_enabled`, `lesson_snippet_injection`, `llm_reflection_enabled`, `cross_project_enabled`, `patternminer_enabled`, `tool_calls_dedup_enabled` (v0.4.0), `deterministic_retrieval`, `pre_prompt_budget_tokens`, `archive_after_days` (v0.5.0).
254
+
255
+ ### `kevin_feedback` (v0.5.0)
256
+
257
+ Rates an injected memory and makes the rating count. `verdict` is `useful` | `wrong` | `outdated` | `ignore`. The first three are stored in `memory_feedback` and move `kevin_why`'s confidence (`+0.05` / `-0.1` per count); **`ignore` is a hard action** — the memory is stamped `ignored = 1` and excluded from retrieval, queries and injection.
258
+
259
+ ```
260
+ kevin_feedback({ memory_id: "0195a3b2-...", verdict: "wrong", note: "the fix was wrong" })
261
+ // → { "ok": true, "verdict": "wrong" }
262
+ ```
263
+
264
+ ### `kevin_trace` (v0.5.0)
265
+
266
+ Strict dry-run: predicts exactly which memories `onSystemTransform` WOULD inject for a query (optionally `session_id`, `tag` and `cap`), with **zero side effects** — no counters, no ledger rows, no seen-set writes, no relevance bumps. Rejected items carry their `GateReason` (`seen_this_session` | `weak` | `recurrence` | `stale` | `ignored`).
267
+
268
+ ```
269
+ kevin_trace({ query: "tsc error" })
270
+ // → { "query": "tsc error", "tag": "context", "cap": 900, "would_inject": true,
271
+ // "total_tokens": 82,
272
+ // "admitted": [ { "id": "...", "type": "error", "decision": "admitted", "tokens": 62 } ],
273
+ // "blocked": [ { "id": "...", "type": "error", "decision": "blocked", "reason": "seen_this_session", "tokens": 20 } ] }
274
+ ```
275
+
276
+ ### `kevin_audit` (v0.5.0)
277
+
278
+ Read-only report of the whole system state: memories by `status`/`origin`/`type`, injection outcomes with `precision_rate`/`coverage_rate`, the five `blocked` counters, feedback by verdict, tokens injected. `verbose: true` adds the settings block. No writes, no LLM; on a pre-006 database it returns what it can with `"partial": true`.
279
+
280
+ ```
281
+ kevin_audit({})
282
+ // → { "memories": { "total": 42, "by_status": { "active": 37, "stale": 1, "archived": 4 },
283
+ // "by_origin": { "reflector": 12, "agent": 30 }, "by_type": { "error": 20, ... },
284
+ // "ignored": 1, "archived": 4, "with_feedback": 3, "superseded_with_target": 2 },
285
+ // "injections": { "total": 14, "effective": 2, "ineffective": 3, "inconclusive": 9,
286
+ // "unmeasured": 0, "precision_rate": 0.40, "coverage_rate": 0.36 },
287
+ // "blocked": { "seen": 1, "weak": 0, "recurrence": 2, "stale": 0, "ignored": 1 },
288
+ // "feedback": { "positive": 2, "negative": 1, "by_verdict": { "useful": 2, "wrong": 1 } },
289
+ // "tokens": { "pre_prompt": 51, "compacting": 0 }, "partial": false }
290
+ ```
291
+
292
+ ---
293
+
294
+ ## Precision (v0.4.0)
295
+
296
+ Weak lessons — errors the reflector cannot dispatch to a deterministic rule — are **stored but never injected** while `quality_gate_enabled = '1'` (default). Injection now goes through a ledger: every pre-prompt/compacting injection is recorded and settled as effective or ineffective at session idle, so `kevin_status` reports the honest picture (`injections_total`, `injections_effective/ineffective`, `precision_rate`, `patterns_promoted_new`) instead of raw "lessons shared" counts. Recurrences demote lessons (`recurrence_count` → `stale`) and lower confidence. Debug mode: `kevin_config({ action: "set", key: "quality_gate_enabled", value: "0" })` re-injects weak lessons with a `(low confidence)` marker.
297
+
298
+ ## How Kevin measures itself (v0.5.0)
299
+
300
+ Every injection is settled at `session.idle` into one of **four outcomes**:
301
+
302
+ | Outcome | Meaning | Counts toward precision? |
303
+ |---|---|---|
304
+ | `effective` | A linked fix was observed after the injection | yes (numerator) |
305
+ | `ineffective` | The same error recurred after the injection | yes (denominator) |
306
+ | `inconclusive` | Neither — the error did not recur, but no fix was seen either | no |
307
+ | `unmeasured` | Session went idle before settlement could run | no |
308
+
309
+ - **`precision_rate`** = `effective / (effective + ineffective)`. The v0.4.0 definition counted "did not recur" as success — measuring absence of recurrence, not effect. **Your precision rate will look lower on v0.5.0. That is the honest number.** Existing `effective` rows were remapped to `inconclusive` by migration 006.
310
+ - **`coverage_rate`** = `(effective + ineffective) / total` — the share of injections that were actually measured. Reported alongside precision so a low measurable fraction stays visible instead of hiding behind a large total.
311
+ - **`blocked`** counts every gate rejection by reason (`seen_this_session`, `weak`, `recurrence`, `stale`, `ignored`) — a rejection you did not count did not happen.
312
+
313
+ `kevin_trace` shows you the plan *before* it happens (dry run, zero side effects); `kevin_audit` reads the whole picture after; `kevin_feedback` lets a human correct it — and the correction moves the confidence number `kevin_why` reports.
314
+
315
+ ## Replay harness (v0.5.0)
316
+
317
+ `npm run replay` runs every transcript in `tests/replay/fixtures/` through the plugin against an in-memory database with a frozen clock and prints one table row per transcript (memories created, injection outcomes, `precision_rate`, `coverage_rate`, tokens). Record your own session as a JSON array of typed events (`session.created`, `chat.message`, `tool.before`, `tool.after`, `system.transform`, `compacting`, `session.idle`) with ISO-8601 `at` timestamps, drop it into `tests/replay/fixtures/`, and re-run. The `at` timestamps are the only source of time during replay.
318
+
230
319
  ---
231
320
 
232
321
  ## Hooks
@@ -237,7 +326,7 @@ Kevin subscribes to 6 OpenCode hooks:
237
326
  |---|---|
238
327
  | `tool.execute.before` | Records tool call start (callID + redacted args) |
239
328
  | `tool.execute.after` | Records result (id = callID); on failure → Reflector.invoke async (throttled); on success → CausalChain.onSuccess links the fix (v0.3.0) |
240
- | `experimental.chat.system.transform` | Injects relevant lessons in `<kevin-context>` (1500 tokens) + optional `<kevin-suggestion>` (v0.3.0) |
329
+ | `experimental.chat.system.transform` | Injects relevant lessons in `<kevin-context>` (900 tokens by default, configurable) + optional `<kevin-suggestion>` (v0.3.0) |
241
330
  | `experimental.session.compacting` | Re-injects lessons in `<kevin-memory>` after compacting (2000 tokens) + optional `<kevin-suggestion>` |
242
331
  | `event` (`session.created`) | Captures current `sessionID` |
243
332
  | `event` (`session.idle`) | Generates retrospective.md; boosts positive lessons (v0.2.0); penalizes recurring failures (v0.3.0); promotes causal patterns + mines patterns (opt-in); flushes metrics |
@@ -266,6 +355,18 @@ KevinPlugin(input, {
266
355
  });
267
356
  ```
268
357
 
358
+ ### Settings (v0.5.0)
359
+
360
+ Read/write via `kevin_config({ action: "list" | "set", ... })`. v0.5.0 adds:
361
+
362
+ | Setting | Default | Effect |
363
+ |---|---|---|
364
+ | `deterministic_retrieval` | `"0"` | Freezes Kevin's internal clock (recency factor 1.0, no relevance bumps) — for hermetic tests and the replay harness |
365
+ | `pre_prompt_budget_tokens` | `"900"` | Pre-prompt injection cap, clamped to `[100, 4000]` (was a hard-coded 1500) |
366
+ | `archive_after_days` | `"30"` | Age at which stale non-pattern memories are retired to `archived` on `session.idle` |
367
+
368
+ All settings are stored as TEXT in `kevin_settings`; values are compared against `"1"` for booleans.
369
+
269
370
  ---
270
371
 
271
372
  ## Development
@@ -276,8 +377,9 @@ cd opencode-kevin
276
377
  npm install
277
378
  npm run typecheck # tsc --noEmit (strict)
278
379
  npm run lint # biome check .
279
- npm test # vitest run (unit + integration + e2e)
380
+ npm test # vitest run (unit + integration + e2e + replay)
280
381
  npm run verify # post-install verification
382
+ npm run replay # replay report over tests/replay/fixtures (v0.5.0)
281
383
  ```
282
384
 
283
385
  ### Publishing (maintainer)
@@ -301,13 +403,18 @@ plugin/
301
403
  Reflector.ts # Heuristic lessons + per-fingerprint throttle + lesson v2 + LLM enrich
302
404
  CausalChain.ts # v0.3.0 — links fixes to failures + promotes causal patterns
303
405
  ContextInjector.ts # deriveQuery + pre-prompt/compacting injection + <kevin-suggestion>
406
+ QualityGate.ts # v0.4.0 — weak-lesson gate (stored, not injected by default)
407
+ InjectionLedger.ts # v0.4.0 — injection ledger + settle → precision_rate
304
408
  Retrospective.ts # Generates retrospective.md + FP recap + metrics snapshot
409
+ LessonFixer.ts # v0.4.0 — deterministic fix_args capture + promotion enrichment
410
+ confidence.ts # v0.4.0 — two-sided computeConfidence (evidence + recurrence)
305
411
  fingerprint.ts # FNV-1a 64-bit (in-house, no node:crypto)
306
412
  metrics.ts # In-memory counters + debounced flush to kevin_metrics
307
413
  PatternMiner.ts # Opt-in deterministic 2-gram/3-gram miner
308
414
  kevin_why.ts # v0.3.0 — kevin_why tool: failure→fix traces + related rules
309
415
  okf-export.ts # v0.3.0 — kevin_export: OKF/markdown export
310
416
  okf-import.ts # v0.3.0 — kevin_import: bundle parser + import
417
+ query-tokenizer.ts # v0.4.0 — FTS5 tokenizer for query sanitization
311
418
  memory-format.ts # escapeInjectedText, formatMemories, <protect> + id: line wrappers
312
419
  redact.ts # redactPaths + stripPrivate
313
420
  uuid.ts # UUIDv7
@@ -316,6 +423,7 @@ migrations/
316
423
  002_indexes.sql # FTS5 + indexes
317
424
  003_v02_signal.sql # v0.2.0 Signal Quality: fingerprint, origin, metrics, dedup indexes
318
425
  004_v03_knowledge.sql # v0.3.0 Knowledge + Causality: evidence/status/supersede, error_fingerprint
426
+ 005_v04_signal.sql # v0.4.0 Signal over Noise: recurrence_count, fix_args, last_injected_at
319
427
  tests/{unit,integration,e2e}/
320
428
  scripts/
321
429
  copy-migrations.mjs # build step: copies *.sql to dist/migrations
@@ -0,0 +1,57 @@
1
+ -- ============================================================
2
+ -- Kevin 0.4.0 — Migration 005: Signal over Noise (additive)
3
+ -- ============================================================
4
+ -- Backward-compatible, additive only. All new columns are
5
+ -- nullable or carry a NOT NULL DEFAULT so legacy rows keep
6
+ -- working without a destructive rebuild.
7
+ -- ============================================================
8
+
9
+ -- 1. memories: positive/negative evidence split (D4-03).
10
+ -- recurrence_count — how many times this fingerprint recurred AFTER
11
+ -- injection (negative evidence; lowers confidence).
12
+ -- fix_args — deterministic capture of the linked success call's
13
+ -- args_summary ("Fixed by:" raw material, D4-07).
14
+ -- last_injected_at — timestamp of the most recent injection of this memory.
15
+ ALTER TABLE memories ADD COLUMN recurrence_count INTEGER NOT NULL DEFAULT 0;
16
+ ALTER TABLE memories ADD COLUMN fix_args TEXT;
17
+ ALTER TABLE memories ADD COLUMN last_injected_at TEXT;
18
+
19
+ -- 2. kevin_injections: the injection ledger (D4-04). One row per injected
20
+ -- memory per prompt/compaction, settled at session.idle.
21
+ CREATE TABLE IF NOT EXISTS kevin_injections (
22
+ id TEXT PRIMARY KEY,
23
+ memory_id TEXT NOT NULL,
24
+ fingerprint TEXT NOT NULL,
25
+ session_id TEXT NOT NULL,
26
+ hook TEXT NOT NULL CHECK (hook IN ('pre_prompt', 'compacting')),
27
+ tokens INTEGER NOT NULL,
28
+ injected_at TEXT NOT NULL DEFAULT (datetime('now')),
29
+ outcome TEXT CHECK (outcome IN ('unmeasured', 'effective', 'ineffective'))
30
+ NOT NULL DEFAULT 'unmeasured'
31
+ );
32
+
33
+ -- 2b. Indexes: settlement by session, recurrence lookups by fingerprint,
34
+ -- and outcome rollups for precision_rate.
35
+ CREATE INDEX IF NOT EXISTS idx_injections_fp
36
+ ON kevin_injections(fingerprint);
37
+ CREATE INDEX IF NOT EXISTS idx_injections_session
38
+ ON kevin_injections(session_id);
39
+ CREATE INDEX IF NOT EXISTS idx_injections_outcome
40
+ ON kevin_injections(outcome);
41
+
42
+ -- 3. kevin_metrics: seed new v0.4 counters.
43
+ -- patterns_promoted_new replaces patterns_causal (which was inflated by
44
+ -- idempotent refreshes); the latter stays for compat but is frozen.
45
+ INSERT OR IGNORE INTO kevin_metrics (key, value) VALUES
46
+ ('injections_total', 0),
47
+ ('injections_effective', 0),
48
+ ('injections_ineffective', 0),
49
+ ('patterns_promoted_new', 0);
50
+
51
+ -- 4. kevin_settings: seed new v0.4 flags.
52
+ INSERT OR IGNORE INTO kevin_settings (key, value) VALUES
53
+ ('quality_gate_enabled', '1'),
54
+ ('lesson_snippet_injection','1');
55
+
56
+ -- 5. Seed version 005.
57
+ INSERT OR IGNORE INTO schema_version (version) VALUES ('005');
@@ -0,0 +1,118 @@
1
+ -- ============================================================================
2
+ -- 006_v05_glassbox.sql — v0.5.0 "Glass Box"
3
+ --
4
+ -- Honest measurement, human feedback, lifecycle completion.
5
+ --
6
+ -- Section 1: rebuild kevin_injections to admit a fourth outcome.
7
+ -- Section 2: human feedback storage.
8
+ -- Section 3: memory lifecycle columns.
9
+ -- Section 4: metric seeds.
10
+ -- Section 5: setting seeds.
11
+ -- Section 6: schema_version.
12
+ -- ============================================================================
13
+
14
+ -- ---------------------------------------------------------------------------
15
+ -- 1. kevin_injections: add 'inconclusive'.
16
+ --
17
+ -- SQLite cannot ALTER a CHECK constraint, so the table must be rebuilt.
18
+ -- Migration 004 set this precedent. kevin_injections has no FTS5 triggers,
19
+ -- so unlike 004 this is a straight four-step rebuild.
20
+ --
21
+ -- Existing rows with outcome='effective' are remapped to 'inconclusive'.
22
+ -- This is not data loss: v0.4's 'effective' meant "the error did not recur",
23
+ -- which is the exact definition of the new 'inconclusive' bucket. Rows that
24
+ -- genuinely earned the new 'effective' will be re-settled naturally, and the
25
+ -- post-apply hook re-derives the counters from the table.
26
+ -- ---------------------------------------------------------------------------
27
+ CREATE TABLE IF NOT EXISTS kevin_injections_new (
28
+ id TEXT PRIMARY KEY,
29
+ memory_id TEXT NOT NULL,
30
+ fingerprint TEXT NOT NULL,
31
+ session_id TEXT NOT NULL,
32
+ hook TEXT NOT NULL CHECK (hook IN ('pre_prompt','compacting')),
33
+ tokens INTEGER NOT NULL,
34
+ injected_at TEXT NOT NULL DEFAULT (datetime('now')),
35
+ outcome TEXT NOT NULL DEFAULT 'unmeasured'
36
+ CHECK (outcome IN ('unmeasured','effective','ineffective','inconclusive'))
37
+ );
38
+
39
+ INSERT INTO kevin_injections_new
40
+ (id, memory_id, fingerprint, session_id, hook, tokens, injected_at, outcome)
41
+ SELECT
42
+ id, memory_id, fingerprint, session_id, hook, tokens, injected_at,
43
+ CASE WHEN outcome = 'effective' THEN 'inconclusive' ELSE outcome END
44
+ FROM kevin_injections;
45
+
46
+ DROP TABLE kevin_injections;
47
+ ALTER TABLE kevin_injections_new RENAME TO kevin_injections;
48
+
49
+ CREATE INDEX IF NOT EXISTS idx_injections_fp ON kevin_injections(fingerprint);
50
+ CREATE INDEX IF NOT EXISTS idx_injections_session ON kevin_injections(session_id);
51
+ CREATE INDEX IF NOT EXISTS idx_injections_outcome ON kevin_injections(outcome);
52
+
53
+ -- ---------------------------------------------------------------------------
54
+ -- 2. Human feedback. Append-only audit trail; the hot path reads the
55
+ -- denormalized counters on `memories` (section 3), never this table.
56
+ -- ---------------------------------------------------------------------------
57
+ CREATE TABLE IF NOT EXISTS memory_feedback (
58
+ id TEXT PRIMARY KEY,
59
+ memory_id TEXT NOT NULL,
60
+ verdict TEXT NOT NULL CHECK (verdict IN ('useful','wrong','outdated','ignore')),
61
+ session_id TEXT,
62
+ note TEXT,
63
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
64
+ );
65
+
66
+ CREATE INDEX IF NOT EXISTS idx_feedback_memory ON memory_feedback(memory_id);
67
+ CREATE INDEX IF NOT EXISTS idx_feedback_created ON memory_feedback(created_at);
68
+
69
+ -- ---------------------------------------------------------------------------
70
+ -- 3. Memory lifecycle and feedback columns.
71
+ --
72
+ -- feedback_positive / feedback_negative are SEPARATE from evidence_count
73
+ -- and recurrence_count by design: human judgement is evidence about the
74
+ -- memory, causal counters are evidence about the world. Mixing them was
75
+ -- the confidence-poisoning defect closed in v0.4.0.
76
+ --
77
+ -- superseded_by has no REFERENCES clause on purpose. Store enables
78
+ -- PRAGMA foreign_keys=ON, and a hard FK would block deletion of a memory
79
+ -- that superseded another.
80
+ -- ---------------------------------------------------------------------------
81
+ ALTER TABLE memories ADD COLUMN feedback_positive INTEGER NOT NULL DEFAULT 0;
82
+ ALTER TABLE memories ADD COLUMN feedback_negative INTEGER NOT NULL DEFAULT 0;
83
+ ALTER TABLE memories ADD COLUMN ignored INTEGER NOT NULL DEFAULT 0;
84
+ ALTER TABLE memories ADD COLUMN superseded_by TEXT;
85
+ ALTER TABLE memories ADD COLUMN archived_at TEXT;
86
+
87
+ CREATE INDEX IF NOT EXISTS idx_memories_ignored ON memories(ignored);
88
+ CREATE INDEX IF NOT EXISTS idx_memories_archived ON memories(archived_at);
89
+
90
+ -- ---------------------------------------------------------------------------
91
+ -- 4. Metric seeds. Order matches the additions to METRIC_KEYS in metrics.ts.
92
+ -- ---------------------------------------------------------------------------
93
+ INSERT OR IGNORE INTO kevin_metrics (key, value) VALUES
94
+ ('injections_inconclusive', 0),
95
+ ('injections_blocked_seen', 0),
96
+ ('injections_blocked_weak', 0),
97
+ ('injections_blocked_recurrence',0),
98
+ ('injections_blocked_stale', 0),
99
+ ('injections_blocked_ignored', 0),
100
+ ('feedback_positive_total', 0),
101
+ ('feedback_negative_total', 0),
102
+ ('memories_archived', 0);
103
+
104
+ -- ---------------------------------------------------------------------------
105
+ -- 5. Setting seeds. Values are TEXT, always. Read them with an explicit
106
+ -- string comparison or an explicit Number() parse — never `=== 1`.
107
+ -- (That exact mistake kept cross_project_enabled unreachable for the
108
+ -- whole of v0.3.0.)
109
+ -- ---------------------------------------------------------------------------
110
+ INSERT OR IGNORE INTO kevin_settings (key, value) VALUES
111
+ ('deterministic_retrieval', '0'),
112
+ ('pre_prompt_budget_tokens', '900'),
113
+ ('archive_after_days', '30');
114
+
115
+ -- ---------------------------------------------------------------------------
116
+ -- 6. Version marker.
117
+ -- ---------------------------------------------------------------------------
118
+ INSERT OR IGNORE INTO schema_version (version) VALUES ('006');
@@ -0,0 +1,42 @@
1
+ import type { MemoryService } from "./MemoryService.js";
2
+ import type { Store } from "./Store.js";
3
+ import type { Metrics } from "./metrics.js";
4
+ /**
5
+ * v0.5.0 Archiver (K5-012 / plan §5.4, D5-05).
6
+ *
7
+ * Lifecycle tail of the memory loop: reflectors mark a lesson `stale` when
8
+ * its fingerprint recurred enough times (penalizeRecurringReflectors,
9
+ * migration 003), but nothing ever retires those rows — they keep
10
+ * circulating through status filters and inflating counts. The Archiver
11
+ * runs on `session.idle` and retires stale memories whose last activity
12
+ * (`updated_at`, falling back to `last_verified_at`) is older than
13
+ * `archive_after_days` (kevin_settings, seeded '30' in migration 006).
14
+ *
15
+ * Rules:
16
+ * - Only `status = 'stale'` rows with `archived_at IS NULL`.
17
+ * - `type != 'pattern'`: a pattern is the FIXED form of a fingerprint
18
+ * (K4-025); archiving it would break kevin_why and the recurrence
19
+ * re-admission path (QualityGate rule 4).
20
+ * - Clock: the injection clock (K5-008) — tests freeze it; production
21
+ * defaults to `new Date()`. Never `Date.now()` inside.
22
+ * - `memories_archived` is bumped by the batch size, once per run.
23
+ * - Pre-006 DBs (no `archived_at`) degrade to a no-op.
24
+ *
25
+ * Archived rows remain queryable with `includeSuperseded: true`-style
26
+ * reads (status filter is exclusive, not destructive).
27
+ */
28
+ export declare class Archiver {
29
+ private readonly store;
30
+ private readonly memoryService;
31
+ private readonly metrics;
32
+ private readonly now;
33
+ constructor(store: Store, memoryService: MemoryService, metrics?: Metrics | null, now?: () => Date);
34
+ /**
35
+ * Archives every eligible stale memory. Returns the number of rows
36
+ * retired. Idempotent: archived rows are excluded by `archived_at IS
37
+ * NULL`, so a second run archives nothing.
38
+ */
39
+ run(): number;
40
+ private archiveAfterDays;
41
+ private hasArchivedColumn;
42
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * v0.5.0 Archiver (K5-012 / plan §5.4, D5-05).
3
+ *
4
+ * Lifecycle tail of the memory loop: reflectors mark a lesson `stale` when
5
+ * its fingerprint recurred enough times (penalizeRecurringReflectors,
6
+ * migration 003), but nothing ever retires those rows — they keep
7
+ * circulating through status filters and inflating counts. The Archiver
8
+ * runs on `session.idle` and retires stale memories whose last activity
9
+ * (`updated_at`, falling back to `last_verified_at`) is older than
10
+ * `archive_after_days` (kevin_settings, seeded '30' in migration 006).
11
+ *
12
+ * Rules:
13
+ * - Only `status = 'stale'` rows with `archived_at IS NULL`.
14
+ * - `type != 'pattern'`: a pattern is the FIXED form of a fingerprint
15
+ * (K4-025); archiving it would break kevin_why and the recurrence
16
+ * re-admission path (QualityGate rule 4).
17
+ * - Clock: the injection clock (K5-008) — tests freeze it; production
18
+ * defaults to `new Date()`. Never `Date.now()` inside.
19
+ * - `memories_archived` is bumped by the batch size, once per run.
20
+ * - Pre-006 DBs (no `archived_at`) degrade to a no-op.
21
+ *
22
+ * Archived rows remain queryable with `includeSuperseded: true`-style
23
+ * reads (status filter is exclusive, not destructive).
24
+ */
25
+ export class Archiver {
26
+ store;
27
+ memoryService;
28
+ metrics;
29
+ now;
30
+ constructor(store, memoryService, metrics, now = () => new Date()) {
31
+ this.store = store;
32
+ this.memoryService = memoryService;
33
+ this.metrics = metrics ?? null;
34
+ this.now = now;
35
+ }
36
+ /**
37
+ * Archives every eligible stale memory. Returns the number of rows
38
+ * retired. Idempotent: archived rows are excluded by `archived_at IS
39
+ * NULL`, so a second run archives nothing.
40
+ */
41
+ run() {
42
+ if (!this.hasArchivedColumn())
43
+ return 0;
44
+ const days = this.archiveAfterDays();
45
+ if (days <= 0)
46
+ return 0;
47
+ const now = this.now();
48
+ const cutoff = formatSqliteUtc(new Date(now.getTime() - days * 86_400_000));
49
+ this.store
50
+ .prepare(`UPDATE memories
51
+ SET status = 'archived', archived_at = ?, updated_at = ?
52
+ WHERE status = 'stale'
53
+ AND type != 'pattern'
54
+ AND archived_at IS NULL
55
+ AND updated_at < ?`)
56
+ .run(formatSqliteUtc(now), formatSqliteUtc(now), cutoff);
57
+ const changed = this.store.prepare("SELECT changes() AS n").get();
58
+ const n = changed.n;
59
+ if (n > 0)
60
+ this.metrics?.incr("memories_archived", n);
61
+ return n;
62
+ }
63
+ archiveAfterDays() {
64
+ try {
65
+ const raw = this.memoryService.getSetting("archive_after_days", "30");
66
+ const n = Number(raw);
67
+ return Number.isFinite(n) ? n : 30;
68
+ }
69
+ catch {
70
+ return 30;
71
+ }
72
+ }
73
+ hasArchivedColumn() {
74
+ return hasArchivedColumnCached(this.store);
75
+ }
76
+ }
77
+ const archivedColumnCache = new WeakMap();
78
+ function hasArchivedColumnCached(store) {
79
+ const cached = archivedColumnCache.get(store);
80
+ if (cached !== undefined)
81
+ return cached;
82
+ try {
83
+ store.prepare("SELECT archived_at FROM memories LIMIT 1").get();
84
+ archivedColumnCache.set(store, true);
85
+ return true;
86
+ }
87
+ catch {
88
+ archivedColumnCache.set(store, false);
89
+ return false;
90
+ }
91
+ }
92
+ /** 'YYYY-MM-DD HH:MM:SS' UTC — the format SQLite `datetime('now')` uses,
93
+ * so lexicographic comparisons against `updated_at` are valid. */
94
+ function formatSqliteUtc(d) {
95
+ const pad = (n) => String(n).padStart(2, "0");
96
+ return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
97
+ }
98
+ //# sourceMappingURL=Archiver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Archiver.js","sourceRoot":"","sources":["../../plugin/Archiver.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { type EnrichFn } from "./LessonFixer.js";
1
2
  import type { MemoryService } from "./MemoryService.js";
2
3
  import type { Store } from "./Store.js";
3
4
  import type { Metrics } from "./metrics.js";
@@ -5,7 +6,17 @@ export declare class CausalChain {
5
6
  private store;
6
7
  private memoryService;
7
8
  private metrics;
8
- constructor(store: Store, memoryService: MemoryService, metrics: Metrics | null);
9
+ private enrichFn?;
10
+ constructor(store: Store, memoryService: MemoryService, metrics: Metrics | null, enrichFn?: EnrichFn | undefined);
9
11
  onSuccess(_tool: string, _args: Record<string, unknown>, _projectId: string | null, sessionId: string): void;
10
- onSessionIdle(sessionId: string): number;
12
+ onSessionIdle(sessionId: string): Promise<number>;
13
+ /**
14
+ * v0.4.0 (K4-015) — fire the opt-in enrich hook at most once per
15
+ * promoted pattern. The hook's one-line phrase replaces the
16
+ * deterministic `Fixed by:` line; null keeps it. A call (phrase or
17
+ * not) stamps `metadata.enriched` so repeated idle cycles stay at
18
+ * one LLM call per pattern.
19
+ */
20
+ private enrichIfEnabled;
21
+ private isLlmReflectionEnabled;
11
22
  }