@jmtrin/opencode-kevin 0.1.5 → 0.3.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 (56) hide show
  1. package/README.md +108 -33
  2. package/dist/migrations/003_v02_signal.sql +58 -0
  3. package/dist/migrations/004_v03_knowledge.sql +138 -0
  4. package/dist/plugin/CausalChain.d.ts +11 -0
  5. package/dist/plugin/CausalChain.js +109 -0
  6. package/dist/plugin/CausalChain.js.map +1 -0
  7. package/dist/plugin/ContextInjector.d.ts +25 -1
  8. package/dist/plugin/ContextInjector.js +72 -15
  9. package/dist/plugin/ContextInjector.js.map +1 -1
  10. package/dist/plugin/MemoryService.d.ts +110 -3
  11. package/dist/plugin/MemoryService.js +432 -20
  12. package/dist/plugin/MemoryService.js.map +1 -1
  13. package/dist/plugin/Migrate.d.ts +4 -1
  14. package/dist/plugin/Migrate.js +40 -1
  15. package/dist/plugin/Migrate.js.map +1 -1
  16. package/dist/plugin/PatternMiner.d.ts +49 -0
  17. package/dist/plugin/PatternMiner.js +133 -0
  18. package/dist/plugin/PatternMiner.js.map +1 -0
  19. package/dist/plugin/Reflector.d.ts +45 -2
  20. package/dist/plugin/Reflector.js +134 -12
  21. package/dist/plugin/Reflector.js.map +1 -1
  22. package/dist/plugin/Retrospective.d.ts +4 -1
  23. package/dist/plugin/Retrospective.js +72 -5
  24. package/dist/plugin/Retrospective.js.map +1 -1
  25. package/dist/plugin/Store.d.ts +15 -0
  26. package/dist/plugin/Store.js +15 -0
  27. package/dist/plugin/Store.js.map +1 -1
  28. package/dist/plugin/ToolCallObserver.d.ts +4 -1
  29. package/dist/plugin/ToolCallObserver.js +39 -7
  30. package/dist/plugin/ToolCallObserver.js.map +1 -1
  31. package/dist/plugin/fingerprint.d.ts +27 -0
  32. package/dist/plugin/fingerprint.js +74 -0
  33. package/dist/plugin/fingerprint.js.map +1 -0
  34. package/dist/plugin/index.js +213 -15
  35. package/dist/plugin/index.js.map +1 -1
  36. package/dist/plugin/kevin_why.d.ts +19 -0
  37. package/dist/plugin/kevin_why.js +92 -0
  38. package/dist/plugin/kevin_why.js.map +1 -0
  39. package/dist/plugin/memory-format.d.ts +2 -0
  40. package/dist/plugin/memory-format.js +6 -3
  41. package/dist/plugin/memory-format.js.map +1 -1
  42. package/dist/plugin/metrics.d.ts +60 -0
  43. package/dist/plugin/metrics.js +162 -0
  44. package/dist/plugin/metrics.js.map +1 -0
  45. package/dist/plugin/okf-export.d.ts +3 -0
  46. package/dist/plugin/okf-export.js +86 -0
  47. package/dist/plugin/okf-export.js.map +1 -0
  48. package/dist/plugin/okf-import.d.ts +73 -0
  49. package/dist/plugin/okf-import.js +239 -0
  50. package/dist/plugin/okf-import.js.map +1 -0
  51. package/dist/plugin/redact.d.ts +1 -0
  52. package/dist/plugin/redact.js +7 -0
  53. package/dist/plugin/redact.js.map +1 -1
  54. package/migrations/003_v02_signal.sql +58 -0
  55. package/migrations/004_v03_knowledge.sql +138 -0
  56. package/package.json +48 -48
package/README.md CHANGED
@@ -6,6 +6,7 @@ Kevin is an [OpenCode](https://opencode.ai) plugin that **observes** every agent
6
6
 
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
+ - **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.
9
10
  - **Standalone**: works without any other plugin. With the ecosystem, it learns more richly.
10
11
 
11
12
  ---
@@ -30,7 +31,7 @@ For a **single project**, put the same `plugin` array in `./opencode.json` or `.
30
31
 
31
32
  ### 2. Restart OpenCode
32
33
 
33
- 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 five tools: `kevin_save`, `kevin_query`, `kevin_recall`, `kevin_status`, `kevin_retrospective`.
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`.
34
35
 
35
36
  ### 3. Where data lives
36
37
 
@@ -89,32 +90,52 @@ Use `:memory:` for `dbPath` in tests.
89
90
 
90
91
  ┌─────────────────┐
91
92
  │ OBSERVE │ ToolCallObserver records every call
92
- │ ToolCallObserver│ (tool, args redacted, success, duration, error_type)
93
+ │ ToolCallObserver│ (tool, args redacted, success, duration, error_type,
94
+ │ │ id = callID) + stripPrivate + opt-in dedup (v0.2.0)
93
95
  └────────┬────────┘
94
- │ on failure
95
-
96
- ┌─────────────────┐
97
- │ LEARN │ Reflector generates a heuristic lesson
98
- │ Reflector │ redacts paths/secrets, throttled 1/min,
99
- └────────┬────────┘ persists type:error memory
100
-
101
-
102
- ┌─────────────────┐
103
- SHARE │ ContextInjector injects relevant lessons
104
- ContextInjector │ pre-prompt (1500 tokens) and on compacting (2000 tokens)
96
+ │ on failure │ on success
97
+
98
+ ┌─────────────────┐ ┌─────────────────────────┐
99
+ │ LEARN │ CAUSAL CHAIN (v0.3.0) │
100
+ │ Reflector │ CausalChain.onSuccess │
101
+ heuristic lesson │ │ links the fix to the │
102
+ per-error-code │ │ failure within 10 tool │
103
+ │ rule table │ │ calls (error_fingerprint)
104
+ │ (v0.2.0 v2), │ └───────────┬─────────────┘
105
+ per-fingerprint │ │
106
+ throttle BEFORE │ │ session.idle
107
+ │ LLM enrich │ ▼
108
+ │ (opt-in v0.3.0),│ ┌─────────────────────────┐
109
+ │ stamps │ │ CausalChain.onSessionIdle│
110
+ │ error_fingerprint│ │ promotes recurring errors│
111
+ └────────┬────────┘ │ → causal patterns (idempotent,
112
+ │ │ cumulative evidence) │
113
+ ▼ └───────────┬───────────────┘
114
+ ┌─────────────────┐ │
115
+ │ SHARE │◄──────────────┘
116
+ │ ContextInjector │ injects relevant lessons pre-prompt
117
+ │ │ (1500 tokens) + on compacting (2000)
118
+ │ │ + <protect> + id: line (v0.2.0)
119
+ │ │ + origin-aware rank (v0.2.0)
120
+ │ │ + <kevin-suggestion> after negative
121
+ │ │ feedback half (HITL, v0.3.0)
105
122
  └────────┬────────┘
106
123
  │ session.idle
107
124
 
108
125
  ┌─────────────────┐
109
- │ RETROSPECTIVE │ Retrospective generates ~/.opencode-kevin/retrospectives/<session>.md
110
- └─────────────────┘ with summary of failures and lessons
126
+ │ RETROSPECTIVE │ generates ~/.opencode-kevin/retrospectives/<session>.md
127
+ │ │ with origin labels, FP recap, metrics snapshot (v0.2.0)
128
+ │ │ + boostPositiveReflectors (v0.2.0)
129
+ │ │ + penalizeRecurringReflectors (v0.3.0)
130
+ │ │ + PatternMiner.mine (opt-in, v0.2.0)
131
+ └─────────────────┘
111
132
  ```
112
133
 
113
134
  ---
114
135
 
115
136
  ## Tools
116
137
 
117
- Kevin exposes 5 tools callable by the agent:
138
+ Kevin exposes 9 tools callable by the agent:
118
139
 
119
140
  ### `kevin_save`
120
141
 
@@ -125,20 +146,37 @@ kevin_save({ type: "decision", content: "We use vitest for tests", scope: "proje
125
146
  // → { "id": "0195a3b2-..." }
126
147
  ```
127
148
 
128
- `type`: `error` | `pattern` | `decision` | `context`. `scope`: `project` (persists) | `session` (TTL 24h).
149
+ `type`: `error` | `pattern` | `decision` | `context` | `rule` | `solution` (v0.3.0). `scope`: `project` (persists) | `session` (TTL 24h).
150
+
151
+ Saving a `decision` or `rule` with the same `fingerprint` as an existing active row supersedes the old one (v0.3.0 — `status='superseded'`, hidden from default queries).
129
152
 
130
153
  ### `kevin_query`
131
154
 
132
- Searches memories by text (FTS5 + bm25).
155
+ Searches memories by text (FTS5 + bm25). Returns a **slim** payload by default (v0.2.0). Pass `full: true` for the v0.1.x full content body, or `evidence: true` (v0.3.0) to include `confidence`, `evidence_count`, `last_verified_at` in the slim payload.
133
156
 
134
157
  ```
135
158
  kevin_query({ query: "typecheck", type: "error", limit: 5 })
159
+ // → [{ "id": "...", "type": "error", "scope": "project", "score": -0.87, "snippet": "When bash fails with typecheck:..." }, ...]
160
+
161
+ kevin_query({ query: "typecheck", type: "error", limit: 5, full: true })
136
162
  // → [{ "id": "...", "type": "error", "content": "...", "scope": "project" }, ...]
137
163
  ```
138
164
 
165
+ ### `kevin_get`
166
+
167
+ Fetches a **single full memory** by id (v0.2.0 — progressive disclosure). Use when `kevin_query` returns a slim snippet and you need the complete content.
168
+
169
+ ```
170
+ kevin_get({ id: "0195a3b2-..." })
171
+ // → { "id": "...", "type": "error", "content": "...", "scope": "project",
172
+ // "relevanceScore": 0.55, "origin": "reflector", "fingerprint": "cbf29ce484222325",
173
+ // "projectId": null, "metadata": null,
174
+ // "evidenceCount": 0, "lastVerifiedAt": null, "status": "active" }
175
+ ```
176
+
139
177
  ### `kevin_recall`
140
178
 
141
- Retrieves relevant memories (greedy fill by relevance). Without `query`, returns all memories in scope.
179
+ Retrieves relevant memories (greedy fill by relevance). Without `query`, returns all memories in scope. Pass `includeSuperseded: true` to include superseded rows (v0.3.0).
142
180
 
143
181
  ```
144
182
  kevin_recall({ query: "auth", limit: 3 })
@@ -147,11 +185,16 @@ kevin_recall({ query: "auth", limit: 3 })
147
185
 
148
186
  ### `kevin_status`
149
187
 
150
- Global counts.
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`).
151
189
 
152
190
  ```
153
191
  kevin_status({})
154
- // → { "memories": 42, "tool_calls": 318, "retrospectives": 7 }
192
+ // → { "memories": 42, "memories_reflector": 12, "memories_agent": 30, "memories_pattern": 0,
193
+ // "memories_causal": 1, "tool_calls": 318, "retrospectives": 7,
194
+ // "metrics": { "tokens_injected_pre_prompt": 51, "tokens_injected_compacting": 0,
195
+ // "reflections_throttled": 3, "duplicate_suppressions": 2,
196
+ // "tool_calls_deduped": 0, "patterns_mined": 0,
197
+ // "patterns_causal": 1, "causal_links": 2, "memories_superseded": 0 } }
155
198
  ```
156
199
 
157
200
  ### `kevin_retrospective`
@@ -164,6 +207,26 @@ kevin_retrospective({ session_id: "sess-abc" })
164
207
  // or → { "message": "No failures in session sess-abc." }
165
208
  ```
166
209
 
210
+ ### `kevin_why` (v0.3.0)
211
+
212
+ Explains *why* a failure keeps happening: looks up causal patterns for the query and builds a failure → fix trace from memories + tool_calls, including related TypeScript error-code rules.
213
+
214
+ ```
215
+ kevin_why({ query: "TS2304 cannot find name" })
216
+ // → { "summary": "TS2304 recurs because ... Confirmed by 2 fixes.",
217
+ // "confidence": 0.7, "evidence_count": 2, "last_verified": "2026-08-01 10:00:00",
218
+ // "trace": [ { "type": "error", "summary": "..." }, { "type": "fix", "tool": "bash" } ],
219
+ // "related_rules": [ { "code": "TS2304", "suggestion": "import or typo" } ] }
220
+ ```
221
+
222
+ ### `kevin_export` (v0.3.0)
223
+
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`.
225
+
226
+ ### `kevin_import` (v0.3.0)
227
+
228
+ 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
+
167
230
  ---
168
231
 
169
232
  ## Hooks
@@ -173,15 +236,15 @@ Kevin subscribes to 6 OpenCode hooks:
173
236
  | Hook | What Kevin does |
174
237
  |---|---|
175
238
  | `tool.execute.before` | Records tool call start (callID + redacted args) |
176
- | `tool.execute.after` | Records result; on failure → Reflector.invoke async (throttled) |
177
- | `experimental.chat.system.transform` | Injects relevant lessons in `<kevin-context>` (1500 tokens) |
178
- | `experimental.session.compacting` | Re-injects lessons in `<kevin-memory>` after compacting (2000 tokens) |
239
+ | `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) |
241
+ | `experimental.session.compacting` | Re-injects lessons in `<kevin-memory>` after compacting (2000 tokens) + optional `<kevin-suggestion>` |
179
242
  | `event` (`session.created`) | Captures current `sessionID` |
180
- | `event` (`session.idle`) | Generates retrospective.md for the session |
243
+ | `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 |
181
244
 
182
- **Redaction**: absolute paths (`C:\Users\...`, `/home/...`) → `<path>` and secrets (`API_KEY=`, `Bearer`, `token`) → `<redacted>` before persisting anything.
245
+ **Redaction**: absolute paths (`C:\Users\...`, `/home/...`) → `<path>` and secrets (`API_KEY=`, `Bearer`, `token`) → `<redacted>` before persisting anything. v0.2.0 adds `<private>…</private>` block redaction: sweeps tool call args and output before persistence, replaces with `<private: redacted N chars>`.
183
246
 
184
- **Throttle**: Reflector generates at most 1 lesson per minute (configurable via `throttleMs`).
247
+ **Throttle**: Reflector generates at most 1 lesson per minute per unique fingerprint (v0.2.0: per-fingerprint, not global). Configurable via `throttleMs`.
185
248
 
186
249
  **Truncation**: content > 4KB keeps the lesson searchable; only the additional context is truncated (`metadata.truncated = true`).
187
250
 
@@ -232,15 +295,27 @@ npm publish --access public
232
295
  plugin/
233
296
  index.ts # Entry point: KevinPlugin
234
297
  Store.ts # Wrapper SQLite (node:sqlite / bun:sqlite / better-sqlite3 fallback)
235
- Migrate.ts # Idempotent migrations
236
- MemoryService.ts # save/query/getRelevant (FTS5 + bm25)
237
- ToolCallObserver.ts # onBefore/onAfter + redact + inferErrorType
238
- Reflector.ts # Heuristic lessons + throttle + truncation
239
- ContextInjector.ts # deriveQuery + pre-prompt/compacting injection
240
- Retrospective.ts # Generates retrospective.md + table insert
298
+ Migrate.ts # Idempotent migrations + post-apply hooks
299
+ MemoryService.ts # save/query/getRelevant (FTS5 + bm25 + origin-aware rank + supersede)
300
+ ToolCallObserver.ts # onBefore/onAfter + redact + inferErrorType + dedup (opt-in)
301
+ Reflector.ts # Heuristic lessons + per-fingerprint throttle + lesson v2 + LLM enrich
302
+ CausalChain.ts # v0.3.0 — links fixes to failures + promotes causal patterns
303
+ ContextInjector.ts # deriveQuery + pre-prompt/compacting injection + <kevin-suggestion>
304
+ Retrospective.ts # Generates retrospective.md + FP recap + metrics snapshot
305
+ fingerprint.ts # FNV-1a 64-bit (in-house, no node:crypto)
306
+ metrics.ts # In-memory counters + debounced flush to kevin_metrics
307
+ PatternMiner.ts # Opt-in deterministic 2-gram/3-gram miner
308
+ kevin_why.ts # v0.3.0 — kevin_why tool: failure→fix traces + related rules
309
+ okf-export.ts # v0.3.0 — kevin_export: OKF/markdown export
310
+ okf-import.ts # v0.3.0 — kevin_import: bundle parser + import
311
+ memory-format.ts # escapeInjectedText, formatMemories, <protect> + id: line wrappers
312
+ redact.ts # redactPaths + stripPrivate
313
+ uuid.ts # UUIDv7
241
314
  migrations/
242
315
  001_initial.sql # schema: memories, tool_calls, retrospectives
243
316
  002_indexes.sql # FTS5 + indexes
317
+ 003_v02_signal.sql # v0.2.0 Signal Quality: fingerprint, origin, metrics, dedup indexes
318
+ 004_v03_knowledge.sql # v0.3.0 Knowledge + Causality: evidence/status/supersede, error_fingerprint
244
319
  tests/{unit,integration,e2e}/
245
320
  scripts/
246
321
  copy-migrations.mjs # build step: copies *.sql to dist/migrations
@@ -0,0 +1,58 @@
1
+ -- ============================================================
2
+ -- Kevin 0.2.0 — Migration 003: Signal Quality (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. The partial UNIQUE
7
+ -- index excludes NULL fingerprints so legacy error memories
8
+ -- do not collide. The schema_version table (created by 001)
9
+ -- guarantees this file runs at most once per DB.
10
+ -- ============================================================
11
+
12
+ -- 1. memories: Signal Quality columns.
13
+ -- project_id — first-class scoping dimension (D2-11).
14
+ -- fingerprint — stable hash of normalized error content, salted by project_id (D2-14).
15
+ -- origin — distinguishes reflector-sourced vs agent-sourced memories (D2-06),
16
+ -- with a CHECK enum and DEFAULT 'agent' so legacy rows backfill cleanly.
17
+ ALTER TABLE memories ADD COLUMN project_id TEXT;
18
+ ALTER TABLE memories ADD COLUMN fingerprint TEXT;
19
+ ALTER TABLE memories ADD COLUMN origin TEXT NOT NULL DEFAULT 'agent'
20
+ CHECK(origin IN ('reflector', 'agent', 'pattern', 'retrospective'));
21
+
22
+ -- 2. tool_calls: same scoping/fingerprint columns for dedup and PatternMiner.
23
+ ALTER TABLE tool_calls ADD COLUMN project_id TEXT;
24
+ ALTER TABLE tool_calls ADD COLUMN fingerprint TEXT;
25
+
26
+ -- 3. Partial UNIQUE: one reflector-sourced error memory per (project_id, fingerprint).
27
+ -- NULL fingerprints are excluded (no dedup for non-error memories or legacy rows).
28
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_memories_error_fp
29
+ ON memories (project_id, fingerprint)
30
+ WHERE type = 'error' AND fingerprint IS NOT NULL AND origin = 'reflector';
31
+
32
+ -- 4. kevin_metrics: seeded counters surfaced by kevin_status (D2-07 / K2-004).
33
+ CREATE TABLE IF NOT EXISTS kevin_metrics (
34
+ key TEXT PRIMARY KEY,
35
+ value INTEGER NOT NULL DEFAULT 0,
36
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
37
+ );
38
+
39
+ INSERT OR IGNORE INTO kevin_metrics (key, value) VALUES
40
+ ('tokens_injected_pre_prompt', 0),
41
+ ('tokens_injected_compacting', 0),
42
+ ('reflections_throttled', 0),
43
+ ('duplicate_suppressions', 0),
44
+ ('tool_calls_deduped', 0),
45
+ ('patterns_mined', 0);
46
+
47
+ -- 5. kevin_settings: opt-in feature flags (PatternMiner off, tool_calls dedup off).
48
+ CREATE TABLE IF NOT EXISTS kevin_settings (
49
+ key TEXT PRIMARY KEY,
50
+ value TEXT NOT NULL
51
+ );
52
+
53
+ INSERT OR IGNORE INTO kevin_settings (key, value) VALUES
54
+ ('patternminer_enabled', '0'),
55
+ ('tool_calls_dedup_enabled', '0');
56
+
57
+ -- 6. Seed version 003.
58
+ INSERT OR IGNORE INTO schema_version (version) VALUES ('003');
@@ -0,0 +1,138 @@
1
+ -- ============================================================
2
+ -- Kevin 0.3.0 — Migration 004: Knowledge + Causality (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: evidence + lifecycle columns.
10
+ -- evidence_count — how many times this fingerprint was confirmed as fixed.
11
+ -- last_verified_at — timestamp of the most recent causal confirmation.
12
+ -- status — lifecycle state; default 'active'. Superseded rows are hidden.
13
+ ALTER TABLE memories ADD COLUMN evidence_count INTEGER NOT NULL DEFAULT 0;
14
+ ALTER TABLE memories ADD COLUMN last_verified_at TEXT;
15
+ ALTER TABLE memories ADD COLUMN status TEXT NOT NULL DEFAULT 'active'
16
+ CHECK (status IN ('active', 'superseded', 'stale', 'archived'));
17
+
18
+ -- 2. tool_calls: causal link + feedback-loop link columns.
19
+ -- fix_for_fingerprint — set when this successful call resolved a prior failure
20
+ -- with the given fingerprint. NULL for tool calls that were not fixes.
21
+ -- error_fingerprint — set by Reflector (via onLinkError callback) when a call
22
+ -- FAILS, to the stderr-based fingerprint the matching error memory uses.
23
+ -- This fixes the v0.2.0/v0.3.0 feedback-loop fingerprint mismatch bug:
24
+ -- tool_calls.fingerprint is hashed from "tool|args|success" by ToolCallObserver,
25
+ -- while memories.fingerprint is hashed from stderr text by Reflector — they
26
+ -- never agreed, so boost/penalize queries silently mismatched. The new column
27
+ -- stores the SAME identity dimension the error memory uses.
28
+ ALTER TABLE tool_calls ADD COLUMN fix_for_fingerprint TEXT;
29
+ ALTER TABLE tool_calls ADD COLUMN error_fingerprint TEXT;
30
+
31
+ -- 3. Index: causal linkage by fingerprint. Used by CausalChain.onSuccess
32
+ -- and kevin_why to materialize traces.
33
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_fix_fp
34
+ ON tool_calls(fix_for_fingerprint)
35
+ WHERE fix_for_fingerprint IS NOT NULL;
36
+
37
+ -- 3b. Index: feedback-loop linkage by error_fingerprint. Used by
38
+ -- boostPositiveReflectors / penalizeRecurringReflectors to count
39
+ -- recurrences by the same identity dimension the error memory uses.
40
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_error_fp
41
+ ON tool_calls(error_fingerprint)
42
+ WHERE error_fingerprint IS NOT NULL;
43
+
44
+ -- 4. Index: memories by fingerprint for promotion + supersede queries.
45
+ CREATE INDEX IF NOT EXISTS idx_memories_fp
46
+ ON memories(fingerprint)
47
+ WHERE fingerprint IS NOT NULL;
48
+
49
+ -- 5. kevin_metrics: seed new v0.3 counters.
50
+ INSERT OR IGNORE INTO kevin_metrics (key, value) VALUES
51
+ ('patterns_causal', 0),
52
+ ('causal_links', 0),
53
+ ('memories_superseded', 0);
54
+
55
+ -- 6. kevin_settings: seed new opt-in flags.
56
+ INSERT OR IGNORE INTO kevin_settings (key, value) VALUES
57
+ ('llm_reflection_enabled', '0'),
58
+ ('cross_project_enabled', '0');
59
+
60
+ -- 7. Seed version 004.
61
+ INSERT OR IGNORE INTO schema_version (version) VALUES ('004');
62
+
63
+ -- ============================================================
64
+ -- 8. Rebuild memories table with expanded CHECK constraints.
65
+ -- v0.3.0 introduces new types (rule, solution) and origins
66
+ -- (causal, imported). SQLite cannot ALTER a CHECK constraint,
67
+ -- so we rebuild via a temporary table. FTS5 external-content
68
+ -- table references the content table by name, so we drop+recreate
69
+ -- the FTS5 triggers after the rebuild.
70
+ -- ============================================================
71
+
72
+ -- Step 1: Create new table with correct constraints
73
+ CREATE TABLE memories_v04 (
74
+ id TEXT PRIMARY KEY,
75
+ type TEXT NOT NULL CHECK(type IN ('error','pattern','decision','context','rule','solution')),
76
+ content TEXT NOT NULL,
77
+ scope TEXT NOT NULL DEFAULT 'project' CHECK(scope IN ('project','session')),
78
+ relevance_score REAL DEFAULT 0.5,
79
+ source_tool TEXT,
80
+ source_session TEXT,
81
+ metadata TEXT,
82
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
83
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
84
+ expires_at TEXT,
85
+ project_id TEXT,
86
+ fingerprint TEXT,
87
+ origin TEXT NOT NULL DEFAULT 'agent'
88
+ CHECK(origin IN ('reflector','agent','pattern','retrospective','causal','imported')),
89
+ evidence_count INTEGER NOT NULL DEFAULT 0,
90
+ last_verified_at TEXT,
91
+ status TEXT NOT NULL DEFAULT 'active'
92
+ CHECK(status IN ('active','superseded','stale','archived'))
93
+ );
94
+
95
+ -- Step 2: Copy data preserving rowids (FTS5 content sync relies on rowid)
96
+ INSERT INTO memories_v04
97
+ (rowid, id, type, content, scope, relevance_score, source_tool,
98
+ source_session, metadata, created_at, updated_at, expires_at,
99
+ project_id, fingerprint, origin, evidence_count, last_verified_at, status)
100
+ SELECT rowid, id, type, content, scope, relevance_score, source_tool,
101
+ source_session, metadata, created_at, updated_at, expires_at,
102
+ project_id, fingerprint, origin, evidence_count, last_verified_at, status
103
+ FROM memories;
104
+
105
+ -- Step 3: Drop old table and FTS triggers
106
+ DROP TRIGGER IF EXISTS memories_ai;
107
+ DROP TRIGGER IF EXISTS memories_ad;
108
+ DROP TRIGGER IF EXISTS memories_au;
109
+ DROP TABLE memories;
110
+
111
+ -- Step 4: Rename new table
112
+ ALTER TABLE memories_v04 RENAME TO memories;
113
+
114
+ -- Step 5: Recreate FTS triggers (memories_fts is content=external, survives DROP of content table)
115
+ CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
116
+ INSERT INTO memories_fts(rowid, content) VALUES (new.rowid, new.content);
117
+ END;
118
+
119
+ CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
120
+ INSERT INTO memories_fts(memories_fts, rowid, content) VALUES ('delete', old.rowid, old.content);
121
+ END;
122
+
123
+ CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
124
+ INSERT INTO memories_fts(memories_fts, rowid, content) VALUES ('delete', old.rowid, old.content);
125
+ INSERT INTO memories_fts(rowid, content) VALUES (new.rowid, new.content);
126
+ END;
127
+
128
+ -- Step 6: Recreate indexes
129
+ CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
130
+ CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope);
131
+ CREATE INDEX IF NOT EXISTS idx_memories_relevance ON memories(relevance_score DESC);
132
+ CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at);
133
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_memories_error_fp
134
+ ON memories(project_id, fingerprint)
135
+ WHERE type = 'error' AND fingerprint IS NOT NULL AND origin = 'reflector';
136
+ CREATE INDEX IF NOT EXISTS idx_memories_fp
137
+ ON memories(fingerprint)
138
+ WHERE fingerprint IS NOT NULL;
@@ -0,0 +1,11 @@
1
+ import type { MemoryService } from "./MemoryService.js";
2
+ import type { Store } from "./Store.js";
3
+ import type { Metrics } from "./metrics.js";
4
+ export declare class CausalChain {
5
+ private store;
6
+ private memoryService;
7
+ private metrics;
8
+ constructor(store: Store, memoryService: MemoryService, metrics: Metrics | null);
9
+ onSuccess(_tool: string, _args: Record<string, unknown>, _projectId: string | null, sessionId: string): void;
10
+ onSessionIdle(sessionId: string): number;
11
+ }
@@ -0,0 +1,109 @@
1
+ /** K3-007 — a success only links to a failure within this many calls. */
2
+ const MAX_LINK_DISTANCE = 10;
3
+ export class CausalChain {
4
+ store;
5
+ memoryService;
6
+ metrics;
7
+ constructor(store, memoryService, metrics) {
8
+ this.store = store;
9
+ this.memoryService = memoryService;
10
+ this.metrics = metrics;
11
+ }
12
+ // K3-007: link a success to the failing fingerprint only when it
13
+ // occurred within MAX_LINK_DISTANCE tool calls of the failure (plan
14
+ // §K3-007 acceptance: "within 24h and within 10 tool calls").
15
+ // v0.3.0 fix (bug #3) — the old code linked the most recent session
16
+ // success to the most recent unlinked error memory regardless of
17
+ // distance, so an unrelated success (e.g. an `ls` run after a
18
+ // typecheck failure) was stamped as the fix for that error.
19
+ onSuccess(_tool, _args, _projectId, sessionId) {
20
+ const successRow = this.store
21
+ .prepare(`SELECT rowid FROM tool_calls
22
+ WHERE session_id = ? AND success = 1
23
+ ORDER BY rowid DESC LIMIT 1`)
24
+ .get(sessionId);
25
+ if (!successRow)
26
+ return;
27
+ const linkedFps = new Set(this.store
28
+ .prepare(`SELECT DISTINCT fix_for_fingerprint FROM tool_calls
29
+ WHERE session_id = ? AND fix_for_fingerprint IS NOT NULL`)
30
+ .all(sessionId).map((r) => r.fix_for_fingerprint));
31
+ // Most recent failing calls in this session, newest first. The
32
+ // failing call's `error_fingerprint` (stamped by Reflector via
33
+ // onLinkError) is the SAME identity dimension the error memory
34
+ // uses; `fingerprint` is the legacy tool|args|success hash and
35
+ // simply never matches a reflector error memory.
36
+ const failRows = this.store
37
+ .prepare(`SELECT rowid, COALESCE(error_fingerprint, fingerprint) AS fp
38
+ FROM tool_calls
39
+ WHERE session_id = ?
40
+ AND success = 0
41
+ AND (error_fingerprint IS NOT NULL OR fingerprint IS NOT NULL)
42
+ ORDER BY rowid DESC LIMIT ?`)
43
+ .all(sessionId, MAX_LINK_DISTANCE);
44
+ for (const fail of failRows) {
45
+ if (!fail.fp || linkedFps.has(fail.fp))
46
+ continue;
47
+ const dist = successRow.rowid - fail.rowid;
48
+ if (dist <= 0 || dist > MAX_LINK_DISTANCE)
49
+ continue;
50
+ const mem = this.store
51
+ .prepare(`SELECT 1 FROM memories
52
+ WHERE fingerprint = ? AND type = 'error'
53
+ AND origin = 'reflector' AND status = 'active'
54
+ AND created_at > datetime('now', '-24 hours')
55
+ LIMIT 1`)
56
+ .get(fail.fp);
57
+ if (!mem)
58
+ continue;
59
+ this.store
60
+ .prepare("UPDATE tool_calls SET fix_for_fingerprint = ? WHERE rowid = ?")
61
+ .run(fail.fp, successRow.rowid);
62
+ this.metrics?.incr("causal_links", 1);
63
+ return;
64
+ }
65
+ }
66
+ onSessionIdle(sessionId) {
67
+ const linkedErrors = this.store
68
+ .prepare(`SELECT m.id, m.fingerprint,
69
+ (SELECT COUNT(*)
70
+ FROM tool_calls tc_all
71
+ WHERE tc_all.fix_for_fingerprint = m.fingerprint) as evidence_count
72
+ FROM memories m
73
+ WHERE m.type = 'error'
74
+ AND m.origin = 'reflector'
75
+ AND m.fingerprint IN (
76
+ SELECT DISTINCT fix_for_fingerprint
77
+ FROM tool_calls
78
+ WHERE session_id = ? AND fix_for_fingerprint IS NOT NULL
79
+ )
80
+ AND m.status = 'active'
81
+ GROUP BY m.fingerprint
82
+ HAVING (
83
+ SELECT MAX(tc.rowid) FROM tool_calls tc
84
+ WHERE tc.fix_for_fingerprint = m.fingerprint
85
+ ) > COALESCE(
86
+ (SELECT MAX(m2.rowid) FROM memories m2
87
+ WHERE m2.fingerprint = m.fingerprint
88
+ AND m2.type = 'pattern'
89
+ AND m2.origin = 'causal'),
90
+ 0
91
+ )`)
92
+ .all(sessionId);
93
+ let promoted = 0;
94
+ for (const err of linkedErrors) {
95
+ try {
96
+ const id = this.memoryService.promoteToPattern(err.id, err.evidence_count);
97
+ if (id) {
98
+ promoted++;
99
+ this.metrics?.incr("patterns_causal", 1);
100
+ }
101
+ }
102
+ catch {
103
+ // promoteToPattern may fail if the error memory was removed
104
+ }
105
+ }
106
+ return promoted;
107
+ }
108
+ }
109
+ //# sourceMappingURL=CausalChain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CausalChain.js","sourceRoot":"","sources":["../../plugin/CausalChain.ts"],"names":[],"mappings":"AAIA,yEAAyE;AACzE,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAE7B,MAAM,OAAO,WAAW;IAEd;IACA;IACA;IAHT,YACS,KAAY,EACZ,aAA4B,EAC5B,OAAuB;QAFvB,UAAK,GAAL,KAAK,CAAO;QACZ,kBAAa,GAAb,aAAa,CAAe;QAC5B,YAAO,GAAP,OAAO,CAAgB;IAC7B,CAAC;IAEJ,iEAAiE;IACjE,oEAAoE;IACpE,8DAA8D;IAC9D,oEAAoE;IACpE,iEAAiE;IACjE,8DAA8D;IAC9D,4DAA4D;IAC5D,SAAS,CACR,KAAa,EACb,KAA8B,EAC9B,UAAyB,EACzB,SAAiB;QAEjB,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK;aAC3B,OAAO,CACP;;iCAE6B,CAC7B;aACA,GAAG,CAAC,SAAS,CAAkC,CAAC;QAClD,IAAI,CAAC,UAAU;YAAE,OAAO;QAExB,MAAM,SAAS,GAAG,IAAI,GAAG,CAEvB,IAAI,CAAC,KAAK;aACR,OAAO,CACP;gEAC0D,CAC1D;aACA,GAAG,CAAC,SAAS,CACf,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,mBAAmB,CAAC,CACnC,CAAC;QAEF,+DAA+D;QAC/D,+DAA+D;QAC/D,+DAA+D;QAC/D,+DAA+D;QAC/D,iDAAiD;QACjD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK;aACzB,OAAO,CACP;;;;;iCAK6B,CAC7B;aACA,GAAG,CAAC,SAAS,EAAE,iBAAiB,CAG/B,CAAC;QAEJ,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;YAC7B,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBAAE,SAAS;YACjD,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;YAC3C,IAAI,IAAI,IAAI,CAAC,IAAI,IAAI,GAAG,iBAAiB;gBAAE,SAAS;YAEpD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK;iBACpB,OAAO,CACP;;;;cAIS,CACT;iBACA,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACf,IAAI,CAAC,GAAG;gBAAE,SAAS;YAEnB,IAAI,CAAC,KAAK;iBACR,OAAO,CACP,+DAA+D,CAC/D;iBACA,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC;YACjC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC;YACtC,OAAO;QACR,CAAC;IACF,CAAC;IAED,aAAa,CAAC,SAAiB;QAC9B,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK;aAC7B,OAAO,CACP;;;;;;;;;;;;;;;;;;;;;;;OAuBG,CACH;aACA,GAAG,CAAC,SAAS,CAIZ,CAAC;QAEJ,IAAI,QAAQ,GAAG,CAAC,CAAC;QACjB,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;YAChC,IAAI,CAAC;gBACJ,MAAM,EAAE,GAAG,IAAI,CAAC,aAAa,CAAC,gBAAgB,CAC7C,GAAG,CAAC,EAAE,EACN,GAAG,CAAC,cAAc,CAClB,CAAC;gBACF,IAAI,EAAE,EAAE,CAAC;oBACR,QAAQ,EAAE,CAAC;oBACX,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC;gBAC1C,CAAC;YACF,CAAC;YAAC,MAAM,CAAC;gBACR,4DAA4D;YAC7D,CAAC;QACF,CAAC;QAED,OAAO,QAAQ,CAAC;IACjB,CAAC;CACD"}
@@ -1,4 +1,5 @@
1
1
  import type { MemoryService } from "./MemoryService.js";
2
+ import { type Metrics } from "./metrics.js";
2
3
  export interface ChatMessage {
3
4
  role: string;
4
5
  content: string;
@@ -19,7 +20,30 @@ export interface CompactingOutput {
19
20
  }
20
21
  export declare class ContextInjector {
21
22
  private memoryService;
22
- constructor(memoryService: MemoryService);
23
+ private metrics;
24
+ private lastRecurrenceCount;
25
+ constructor(memoryService: MemoryService, metrics?: Metrics | null);
26
+ /**
27
+ * v0.3.0 (K3-020) — notify the injector that the negative feedback half
28
+ * fired N times in the last session.idle. The next system.transform or
29
+ * compacting hook will prepend a HITL suggestion block.
30
+ */
31
+ setRecurrences(count: number): void;
32
+ /**
33
+ * v0.3.0 (K3-020) — generate a HITL suggestion block when recurrences
34
+ * occurred. Returns the block or empty string. Resets the counter.
35
+ */
36
+ generateSuggestion(): string;
37
+ /**
38
+ * v0.2.0 (K2-024): origin-aware ranking at injection time is delegated
39
+ * to `MemoryService.getRelevant()` (K2-023, D2-13). The injector does
40
+ * NOT re-rank on its own — it consumes the already-ranked slice and
41
+ * applies the conditional-budget guard for opt-out (`protect: false`)
42
+ * callers. Plan §B6.5: "apply the same multiplier as
43
+ * MemoryService.recall so reflector lessons outrank agent-saved notes
44
+ * at injection time" — satisfied transitively via the getRelevant call.
45
+ */
46
+ private inject;
23
47
  deriveQuery(messages: ChatMessage[]): string;
24
48
  onSystemTransform(input: SystemTransformInput, output: SystemTransformOutput): void;
25
49
  onCompacting(input: CompactingInput, output: CompactingOutput): void;