@jmtrin/opencode-kevin 1.1.0 → 1.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 (131) hide show
  1. package/dist/plugin/capabilities.d.ts +2 -0
  2. package/dist/plugin/capabilities.js +3 -0
  3. package/dist/plugin/index.d.ts +5 -4
  4. package/dist/plugin/index.js +401 -108
  5. package/dist/plugin/native.d.ts +2 -2
  6. package/dist/plugin/native.js +2 -2
  7. package/package.json +56 -63
  8. package/LICENSE +0 -21
  9. package/README.md +0 -576
  10. package/dist/migrations/001_initial.sql +0 -92
  11. package/dist/migrations/002_indexes.sql +0 -14
  12. package/dist/migrations/003_v02_signal.sql +0 -58
  13. package/dist/migrations/004_v03_knowledge.sql +0 -138
  14. package/dist/migrations/005_v04_signal.sql +0 -57
  15. package/dist/migrations/006_v05_glassbox.sql +0 -118
  16. package/dist/migrations/007_v06_pull.sql +0 -145
  17. package/dist/migrations/008_v07_truth.sql +0 -124
  18. package/dist/migrations/009_v08_team.sql +0 -100
  19. package/dist/migrations/010_v09_native.sql +0 -78
  20. package/dist/migrations/011_v10_proven.sql +0 -69
  21. package/dist/migrations/012_v11_drift.sql +0 -24
  22. package/dist/plugin/Archiver.d.ts +0 -42
  23. package/dist/plugin/Archiver.js +0 -83
  24. package/dist/plugin/ArtifactWriter.d.ts +0 -93
  25. package/dist/plugin/ArtifactWriter.js +0 -291
  26. package/dist/plugin/CausalChain.d.ts +0 -22
  27. package/dist/plugin/CausalChain.js +0 -197
  28. package/dist/plugin/ConflictDetector.d.ts +0 -35
  29. package/dist/plugin/ConflictDetector.js +0 -260
  30. package/dist/plugin/ContextInjector.d.ts +0 -222
  31. package/dist/plugin/ContextInjector.js +0 -533
  32. package/dist/plugin/ConventionMiner.d.ts +0 -35
  33. package/dist/plugin/ConventionMiner.js +0 -242
  34. package/dist/plugin/Curator.d.ts +0 -116
  35. package/dist/plugin/Curator.js +0 -324
  36. package/dist/plugin/Feedback.d.ts +0 -67
  37. package/dist/plugin/Feedback.js +0 -120
  38. package/dist/plugin/HookLiveness.d.ts +0 -88
  39. package/dist/plugin/HookLiveness.js +0 -322
  40. package/dist/plugin/InjectionLedger.d.ts +0 -98
  41. package/dist/plugin/InjectionLedger.js +0 -295
  42. package/dist/plugin/LessonFixer.d.ts +0 -44
  43. package/dist/plugin/LessonFixer.js +0 -45
  44. package/dist/plugin/Materializer.d.ts +0 -83
  45. package/dist/plugin/Materializer.js +0 -181
  46. package/dist/plugin/MemoryService.d.ts +0 -360
  47. package/dist/plugin/MemoryService.js +0 -1242
  48. package/dist/plugin/Migrate.d.ts +0 -17
  49. package/dist/plugin/Migrate.js +0 -271
  50. package/dist/plugin/PatternMiner.d.ts +0 -49
  51. package/dist/plugin/PatternMiner.js +0 -132
  52. package/dist/plugin/QualityGate.d.ts +0 -124
  53. package/dist/plugin/QualityGate.js +0 -119
  54. package/dist/plugin/Reflector.d.ts +0 -91
  55. package/dist/plugin/Reflector.js +0 -307
  56. package/dist/plugin/RepoIdentity.d.ts +0 -124
  57. package/dist/plugin/RepoIdentity.js +0 -300
  58. package/dist/plugin/RepoTruth.d.ts +0 -80
  59. package/dist/plugin/RepoTruth.js +0 -599
  60. package/dist/plugin/Retrospective.d.ts +0 -17
  61. package/dist/plugin/Retrospective.js +0 -213
  62. package/dist/plugin/SharedLayer.d.ts +0 -159
  63. package/dist/plugin/SharedLayer.js +0 -462
  64. package/dist/plugin/Store.d.ts +0 -29
  65. package/dist/plugin/Store.js +0 -50
  66. package/dist/plugin/ToolCallObserver.d.ts +0 -32
  67. package/dist/plugin/ToolCallObserver.js +0 -201
  68. package/dist/plugin/columns.d.ts +0 -11
  69. package/dist/plugin/columns.js +0 -54
  70. package/dist/plugin/confidence.d.ts +0 -8
  71. package/dist/plugin/confidence.js +0 -34
  72. package/dist/plugin/contract.d.ts +0 -54
  73. package/dist/plugin/contract.js +0 -383
  74. package/dist/plugin/diff.d.ts +0 -8
  75. package/dist/plugin/diff.js +0 -182
  76. package/dist/plugin/escape.d.ts +0 -39
  77. package/dist/plugin/escape.js +0 -60
  78. package/dist/plugin/fingerprint.d.ts +0 -27
  79. package/dist/plugin/fingerprint.js +0 -73
  80. package/dist/plugin/inferability.d.ts +0 -32
  81. package/dist/plugin/inferability.js +0 -88
  82. package/dist/plugin/kevin_approve.d.ts +0 -34
  83. package/dist/plugin/kevin_approve.js +0 -52
  84. package/dist/plugin/kevin_audit.d.ts +0 -213
  85. package/dist/plugin/kevin_audit.js +0 -563
  86. package/dist/plugin/kevin_bench.d.ts +0 -10
  87. package/dist/plugin/kevin_bench.js +0 -82
  88. package/dist/plugin/kevin_conflicts.d.ts +0 -9
  89. package/dist/plugin/kevin_conflicts.js +0 -50
  90. package/dist/plugin/kevin_contract.d.ts +0 -8
  91. package/dist/plugin/kevin_contract.js +0 -59
  92. package/dist/plugin/kevin_doctor.d.ts +0 -62
  93. package/dist/plugin/kevin_doctor.js +0 -196
  94. package/dist/plugin/kevin_facts.d.ts +0 -42
  95. package/dist/plugin/kevin_facts.js +0 -36
  96. package/dist/plugin/kevin_forget.d.ts +0 -33
  97. package/dist/plugin/kevin_forget.js +0 -260
  98. package/dist/plugin/kevin_native.d.ts +0 -29
  99. package/dist/plugin/kevin_native.js +0 -79
  100. package/dist/plugin/kevin_propose.d.ts +0 -23
  101. package/dist/plugin/kevin_propose.js +0 -14
  102. package/dist/plugin/kevin_publish.d.ts +0 -38
  103. package/dist/plugin/kevin_publish.js +0 -18
  104. package/dist/plugin/kevin_why.d.ts +0 -23
  105. package/dist/plugin/kevin_why.js +0 -111
  106. package/dist/plugin/memory-format.d.ts +0 -20
  107. package/dist/plugin/memory-format.js +0 -59
  108. package/dist/plugin/metrics.d.ts +0 -97
  109. package/dist/plugin/metrics.js +0 -284
  110. package/dist/plugin/okf-export.d.ts +0 -3
  111. package/dist/plugin/okf-export.js +0 -134
  112. package/dist/plugin/okf-import.d.ts +0 -76
  113. package/dist/plugin/okf-import.js +0 -271
  114. package/dist/plugin/okf.d.ts +0 -107
  115. package/dist/plugin/okf.js +0 -303
  116. package/dist/plugin/perf.d.ts +0 -35
  117. package/dist/plugin/perf.js +0 -174
  118. package/dist/plugin/query-tokenizer.d.ts +0 -13
  119. package/dist/plugin/query-tokenizer.js +0 -133
  120. package/dist/plugin/redact.d.ts +0 -2
  121. package/dist/plugin/redact.js +0 -18
  122. package/dist/plugin/replay-types.d.ts +0 -69
  123. package/dist/plugin/replay-types.js +0 -156
  124. package/dist/plugin/replay.d.ts +0 -36
  125. package/dist/plugin/replay.js +0 -202
  126. package/dist/plugin/sqlite-adapter.d.ts +0 -12
  127. package/dist/plugin/sqlite-adapter.js +0 -81
  128. package/dist/plugin/time-ms.d.ts +0 -1
  129. package/dist/plugin/time-ms.js +0 -16
  130. package/dist/plugin/uuid.d.ts +0 -1
  131. package/dist/plugin/uuid.js +0 -50
@@ -1,291 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { closeSync, fsyncSync, openSync, readFileSync, renameSync, unlinkSync, writeSync, } from "node:fs";
3
- import { unifiedDiff } from "./diff.js";
4
- import { escapeForFence, escapeForMarkerBlock, escapeForOkfLine, } from "./escape.js";
5
- import { uuidv7 } from "./uuid.js";
6
- // v0.6.0 (K6-005 / plan §5.1, D6-02) — the frozen marker contract. The exact
7
- // byte sequences are load-bearing: README, v1.0.0 plan C-01 and the round-trip
8
- // test all depend on them.
9
- export const MARKER_BEGIN = "<!-- kevin:begin — curated by opencode-kevin, safe to edit -->";
10
- export const MARKER_END = "<!-- kevin:end -->";
11
- function sha256(text) {
12
- return createHash("sha256").update(text, "utf8").digest("hex");
13
- }
14
- /**
15
- * Rule 5 (K6-008 / plan §5.1, D6-02) — the line-ending style comes from the
16
- * FIRST line ending in the existing file: CRLF if it is CRLF, otherwise LF.
17
- * A CRLF file whose last line lacks a terminator, and a mixed-ending file,
18
- * are both resolved by the same first-ending rule, deterministically.
19
- */
20
- function detectEol(text) {
21
- const nl = text.indexOf("\n");
22
- if (nl === -1)
23
- return "\n";
24
- return nl > 0 && text[nl - 1] === "\r" ? "\r\n" : "\n";
25
- }
26
- /**
27
- * Rule 5 (K6-008) — normalize the generated body into the file's line-ending
28
- * style so a CRLF file stays CRLF everywhere, including inside the block.
29
- */
30
- function normalizeEol(body, eol) {
31
- return body.replace(/\r\n/g, "\n").replace(/\n/g, eol);
32
- }
33
- /**
34
- * Rule 9, layer (a) — superseded in v1.0.0 by `escapeForMarkerBlock`
35
- * in `plugin/escape.ts` (K10-027), which carries the same idempotent
36
- * entity discipline.
37
- */
38
- /**
39
- * v1.0.0 (K10-027 / plan §5.7) — the boundary functions now live in
40
- * `plugin/escape.ts`; this composer applies them in fence-then-marker
41
- * order. Layer (b) — strip any line containing `kevin:begin` or
42
- * `kevin:end`, in any casing — is a filter rather than an escape and
43
- * stays here. Without layer (b), a memory containing a literal
44
- * `<!-- kevin:end -->` line would close the marker comment early and
45
- * let subsequent content escape the curated region on the next
46
- * regeneration — a marker-injection variant of the v0.1.5
47
- * prompt-injection defect (plan §3.5). The trailing `-->` strip is kept
48
- * as defence in depth although `escapeForMarkerBlock` already escapes
49
- * every `>` that could form one.
50
- */
51
- export function sanitizeArtifactBody(body) {
52
- const escaped = escapeForMarkerBlock(escapeForFence(body));
53
- const kept = escaped
54
- .split("\n")
55
- .filter((line) => !/kevin:begin|kevin:end/i.test(line))
56
- .join("\n");
57
- return kept.replace(/-->/g, "");
58
- }
59
- /**
60
- * v1.0.0 (K10-027 / plan §5.7, rule 2) — whole-file writes escape by
61
- * container. Only `.okf` files are line-oriented JSON: each line gets
62
- * `escapeForOkfLine`, which is the identity on a well-formed OKF line
63
- * (canonical JSON never contains raw control characters) and therefore
64
- * preserves the re-render `noop`. Kevin-owned markdown paths keep their
65
- * bytes — fences there are legitimate content.
66
- */
67
- export function escapeForContainer(path, content) {
68
- if (!path.endsWith(".okf"))
69
- return content;
70
- return content
71
- .split("\n")
72
- .map((line) => {
73
- // A CRLF file (healHeader preserves the original EOL) leaves a
74
- // trailing \r on each line after the split — it is line-ending
75
- // bytes, not statement content, so it must survive untouched.
76
- if (line.endsWith("\r")) {
77
- return `${escapeForOkfLine(line.slice(0, -1))}\r`;
78
- }
79
- return escapeForOkfLine(line);
80
- })
81
- .join("\n");
82
- }
83
- /**
84
- * v0.6.0 (K6-005 / plan §5.1) — the single write path to disk (D6-01).
85
- *
86
- * `plan()` is pure: it reads the target file, locates the marker pair, splices
87
- * the body between the markers and returns a {@link WritePlan}. It performs no
88
- * writes — rule 1. `apply()` is implemented by K6-007; until then it is a stub
89
- * that throws, so no caller can accidentally write before the audit trail
90
- * exists.
91
- *
92
- * `projectId` is a constructor argument rather than a per-call argument so that
93
- * every audit row is attributed without the call site having to remember.
94
- */
95
- export class ArtifactWriter {
96
- store;
97
- projectId;
98
- metrics;
99
- constructor(store, projectId, metrics) {
100
- this.store = store;
101
- this.projectId = projectId;
102
- this.metrics = metrics ?? null;
103
- }
104
- plan(pathOrRequest, body) {
105
- const request = typeof pathOrRequest === "string"
106
- ? { path: pathOrRequest, mode: "markers", content: body ?? "" }
107
- : pathOrRequest;
108
- const { path, mode, content } = request;
109
- let before;
110
- try {
111
- // Read as Buffer, not as utf8 text: readFileSync's text decoding
112
- // strips a leading BOM, which would silently drop it on the next
113
- // write. Buffer.toString keeps \uFEFF as a character of `before`.
114
- before = readFileSync(path).toString("utf8");
115
- }
116
- catch (err) {
117
- if (err instanceof Error && "code" in err && err.code === "ENOENT") {
118
- before = "";
119
- }
120
- else {
121
- throw err;
122
- }
123
- }
124
- if (mode === "whole") {
125
- // K8-019 (D8-08) — the whole-file path. The file is Kevin-owned,
126
- // so there are no markers, no sanitization and no EOL
127
- // normalization: the rendered bytes are written as-is, which is
128
- // what makes a re-render of the same content a `noop`. A
129
- // caller-side refusal leaves the file untouched and is audited
130
- // like any other refusal: after = before, both hashes recorded.
131
- const refusal = request.refusal;
132
- const refused = refusal !== undefined;
133
- // K10-027 — the container boundary: OKF lines are escaped at the
134
- // single write path, never by callers.
135
- const after = refused ? before : escapeForContainer(path, content);
136
- return {
137
- path,
138
- before,
139
- after,
140
- diff: unifiedDiff(path, before, after),
141
- outcome: refused ? "refused" : after === before ? "noop" : "written",
142
- ...(refusal !== undefined ? { reason: refusal } : {}),
143
- hashBefore: sha256(before),
144
- hashAfter: sha256(after),
145
- };
146
- }
147
- const eol = detectEol(before);
148
- // Rule 9 — sanitation happens in plan(), before hashing, so the hashes
149
- // describe what was actually written.
150
- const bodyEol = normalizeEol(sanitizeArtifactBody(content), eol);
151
- const firstBegin = before.indexOf(MARKER_BEGIN);
152
- const firstEnd = before.indexOf(MARKER_END);
153
- let after;
154
- let outcome = "written";
155
- let reason;
156
- if (firstBegin === -1 && firstEnd === -1) {
157
- // Rule 2 — create: the block is appended at the end of the content,
158
- // preceded by a blank line. For an empty file (missing file treated
159
- // as "") the result is exactly: blank line, MARKER_BEGIN, body,
160
- // MARKER_END, trailing newline.
161
- const separator = before === "" ? eol : before.endsWith(eol) ? eol : eol + eol;
162
- after =
163
- before +
164
- separator +
165
- MARKER_BEGIN +
166
- eol +
167
- bodyEol +
168
- eol +
169
- MARKER_END +
170
- eol;
171
- }
172
- else if (firstBegin === -1 || firstEnd === -1) {
173
- // Rule 3 — exactly one marker present.
174
- after = before;
175
- outcome = "refused";
176
- reason =
177
- firstBegin === -1
178
- ? "kevin:end marker present without kevin:begin"
179
- : "kevin:begin marker present without kevin:end";
180
- }
181
- else if (firstEnd < firstBegin) {
182
- // Rule 3 — MARKER_END precedes MARKER_BEGIN.
183
- after = before;
184
- outcome = "refused";
185
- reason = "kevin:end appears before kevin:begin";
186
- }
187
- else if (before.indexOf(MARKER_BEGIN, firstBegin + MARKER_BEGIN.length) !== -1 ||
188
- before.indexOf(MARKER_END, firstEnd + MARKER_END.length) !== -1) {
189
- // Rule 3 — more than one pair (any additional marker occurrence).
190
- after = before;
191
- outcome = "refused";
192
- reason = "more than one marker pair present";
193
- }
194
- else {
195
- // Rule 4 — bytes outside the marker pair are byte-identical. The
196
- // block between the markers (begin marker, old block, end marker)
197
- // is replaced by the regenerated block.
198
- const blockEnd = firstEnd + MARKER_END.length;
199
- after =
200
- before.slice(0, firstBegin) +
201
- MARKER_BEGIN +
202
- eol +
203
- bodyEol +
204
- eol +
205
- MARKER_END +
206
- before.slice(blockEnd);
207
- // Rule 6 — unchanged content is a noop, never a write.
208
- outcome = after === before ? "noop" : "written";
209
- }
210
- return {
211
- path,
212
- before,
213
- after,
214
- // v0.6.0 (K6-006 / plan §5.2, D6-05) — approval prompts show bytes,
215
- // never prose. Identical inputs yield "".
216
- diff: unifiedDiff(path, before, after),
217
- outcome,
218
- ...(reason !== undefined ? { reason } : {}),
219
- hashBefore: sha256(before),
220
- hashAfter: sha256(after),
221
- };
222
- }
223
- /**
224
- * K8-019 (D8-08) — the single write funnel: every file Kevin writes
225
- * goes through this method, which is the ONLY call site of `apply()`
226
- * in the plugin (asserted by tests/unit/single_write_path.test.ts).
227
- */
228
- write(request, proposalId) {
229
- return this.apply(this.plan(request), proposalId);
230
- }
231
- // v0.6.0 (K6-007 / plan §5.1, rules 7–8) — atomic write + audit row.
232
- apply(plan, proposalId) {
233
- // Rule 8 — refusals and noops still leave an audit trail; a refusal
234
- // that leaves no trace is indistinguishable from a write that never
235
- // happened. Rule 6 — a noop creates no temp file and writes nothing.
236
- if (plan.outcome === "noop" || plan.outcome === "refused") {
237
- if (plan.outcome === "noop") {
238
- this.metrics?.incr("artifact_writes_noop", 1);
239
- }
240
- this.audit(plan, proposalId);
241
- return plan.outcome;
242
- }
243
- // Rule 7 — write to `<path>.kevin.tmp` in the same directory (same
244
- // filesystem, so rename is atomic), fsync, close, then rename over the
245
- // target. Never write the target path directly, never truncate-then-write.
246
- const tmpPath = `${plan.path}.kevin.tmp`;
247
- let fd;
248
- try {
249
- fd = openSync(tmpPath, "w");
250
- writeSync(fd, plan.after, null, "utf8");
251
- fsyncSync(fd);
252
- closeSync(fd);
253
- fd = undefined;
254
- this.renameTemp(tmpPath, plan.path);
255
- }
256
- catch (err) {
257
- // Never leave .kevin.tmp litter next to the user's file.
258
- if (fd !== undefined) {
259
- try {
260
- closeSync(fd);
261
- }
262
- catch {
263
- // already closed or unusable; unlink below is the cleanup
264
- }
265
- }
266
- try {
267
- unlinkSync(tmpPath);
268
- }
269
- catch {
270
- // nothing to clean up
271
- }
272
- throw err;
273
- }
274
- this.metrics?.incr("artifact_writes_total", 1);
275
- this.audit(plan, proposalId);
276
- return "written";
277
- }
278
- // Fault-injection seam for the atomicity test: the rename is the point
279
- // where a failure must leave the target untouched and the temp file gone.
280
- renameTemp(tmpPath, target) {
281
- renameSync(tmpPath, target);
282
- }
283
- audit(plan, proposalId) {
284
- this.store
285
- .prepare(`INSERT INTO artifact_writes
286
- (id, proposal_id, project_id, path, bytes_before, bytes_after,
287
- hash_before, hash_after, outcome, reason)
288
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
289
- .run(uuidv7(), proposalId ?? null, this.projectId, plan.path, Buffer.byteLength(plan.before, "utf8"), Buffer.byteLength(plan.after, "utf8"), plan.hashBefore, plan.hashAfter, plan.outcome, plan.reason ?? null);
290
- }
291
- }
@@ -1,22 +0,0 @@
1
- import { type EnrichFn } from "./LessonFixer.js";
2
- import type { MemoryService } from "./MemoryService.js";
3
- import type { Store } from "./Store.js";
4
- import type { Metrics } from "./metrics.js";
5
- export declare class CausalChain {
6
- private store;
7
- private memoryService;
8
- private metrics;
9
- private enrichFn?;
10
- constructor(store: Store, memoryService: MemoryService, metrics: Metrics | null, enrichFn?: EnrichFn | undefined);
11
- onSuccess(_tool: string, _args: Record<string, unknown>, _projectId: string | null, sessionId: string): void;
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;
22
- }
@@ -1,197 +0,0 @@
1
- import { extractFixArgs, } from "./LessonFixer.js";
2
- import { hasColumn } from "./columns.js";
3
- import { toMs } from "./time-ms.js";
4
- /** K3-007 — a success only links to a failure within this many calls. */
5
- const MAX_LINK_DISTANCE = 10;
6
- export class CausalChain {
7
- store;
8
- memoryService;
9
- metrics;
10
- enrichFn;
11
- constructor(store, memoryService, metrics,
12
- // v0.4.0 (K4-015) — opt-in promotion-time LLM phrasing. Absent in
13
- // production (zero network calls by default); injected by tests or
14
- // by a future settings-driven wiring.
15
- enrichFn) {
16
- this.store = store;
17
- this.memoryService = memoryService;
18
- this.metrics = metrics;
19
- this.enrichFn = enrichFn;
20
- }
21
- // K3-007: link a success to the failing fingerprint only when it
22
- // occurred within MAX_LINK_DISTANCE tool calls of the failure (plan
23
- // §K3-007 acceptance: "within 24h and within 10 tool calls").
24
- // v0.3.0 fix (bug #3) — the old code linked the most recent session
25
- // success to the most recent unlinked error memory regardless of
26
- // distance, so an unrelated success (e.g. an `ls` run after a
27
- // typecheck failure) was stamped as the fix for that error.
28
- onSuccess(_tool, _args, _projectId, sessionId) {
29
- // v1.1.0 (K11-004 / plan §5.2, D11-01/D11-07) — ms-aware window: prefer _ms
30
- const hasTsMs = hasColumn(this.store, "tool_calls", "ts_ms");
31
- const successRow = (hasTsMs
32
- ? this.store.prepare(`SELECT rowid, tool, args_summary, ts, ts_ms FROM tool_calls
33
- WHERE session_id = ? AND success = 1
34
- ORDER BY rowid DESC LIMIT 1`)
35
- : this.store.prepare(`SELECT rowid, tool, args_summary, ts FROM tool_calls
36
- WHERE session_id = ? AND success = 1
37
- ORDER BY rowid DESC LIMIT 1`)).get(sessionId);
38
- if (!successRow)
39
- return;
40
- const linkedFps = new Set(this.store
41
- .prepare(`SELECT DISTINCT fix_for_fingerprint FROM tool_calls
42
- WHERE session_id = ? AND fix_for_fingerprint IS NOT NULL`)
43
- .all(sessionId).map((r) => r.fix_for_fingerprint));
44
- // Most recent failing calls in this session, newest first. The
45
- // failing call's `error_fingerprint` (stamped by Reflector via
46
- // onLinkError) is the SAME identity dimension the error memory
47
- // uses; `fingerprint` is the legacy tool|args|success hash and
48
- // simply never matches a reflector error memory.
49
- const failRows = (hasTsMs
50
- ? this.store.prepare(`SELECT rowid, COALESCE(error_fingerprint, fingerprint) AS fp, ts, ts_ms
51
- FROM tool_calls
52
- WHERE session_id = ?
53
- AND success = 0
54
- AND (error_fingerprint IS NOT NULL OR fingerprint IS NOT NULL)
55
- ORDER BY rowid DESC LIMIT ?`)
56
- : this.store.prepare(`SELECT rowid, COALESCE(error_fingerprint, fingerprint) AS fp, ts
57
- FROM tool_calls
58
- WHERE session_id = ?
59
- AND success = 0
60
- AND (error_fingerprint IS NOT NULL OR fingerprint IS NOT NULL)
61
- ORDER BY rowid DESC LIMIT ?`)).all(sessionId, MAX_LINK_DISTANCE);
62
- for (const fail of failRows) {
63
- if (!fail.fp || linkedFps.has(fail.fp))
64
- continue;
65
- const dist = successRow.rowid - fail.rowid;
66
- if (dist <= 0 || dist > MAX_LINK_DISTANCE)
67
- continue;
68
- // v1.1.0 — ≤24h window uses ms when available
69
- const successMs = toMs(successRow.ts, successRow.ts_ms ?? null);
70
- const failMs = toMs(fail.ts, fail.ts_ms ?? null);
71
- if (successMs !== null && failMs !== null) {
72
- const diff = successMs - failMs;
73
- if (diff < 0 || diff > 86_400_000)
74
- continue;
75
- }
76
- const mem = this.store
77
- .prepare(`SELECT 1 FROM memories
78
- WHERE fingerprint = ? AND type = 'error'
79
- AND origin = 'reflector' AND status IN ('active', 'stale')
80
- AND created_at > datetime('now', '-24 hours')
81
- LIMIT 1`)
82
- .get(fail.fp);
83
- if (!mem)
84
- continue;
85
- this.store
86
- .prepare("UPDATE tool_calls SET fix_for_fingerprint = ? WHERE rowid = ?")
87
- .run(fail.fp, successRow.rowid);
88
- // v0.4.0 (K4-014) — deterministic "Fixed by:" raw material
89
- // (plan §5.4 / D4-07): copy the linked success call's
90
- // args_summary into memories.fix_args for every active row of
91
- // that fingerprint (error + pattern), zero LLM cost.
92
- const fixArgs = extractFixArgs({
93
- tool: successRow.tool,
94
- args_summary: successRow.args_summary ?? null,
95
- });
96
- if (fixArgs) {
97
- this.store
98
- .prepare(`UPDATE memories SET fix_args = ?
99
- WHERE fingerprint = ? AND status IN ('active', 'stale')`)
100
- .run(fixArgs, fail.fp);
101
- }
102
- this.metrics?.incr("causal_links", 1);
103
- return;
104
- }
105
- }
106
- async onSessionIdle(sessionId) {
107
- const linkedErrors = this.store
108
- .prepare(`SELECT m.id, m.fingerprint, m.recurrence_count,
109
- (SELECT COUNT(*)
110
- FROM tool_calls tc_all
111
- WHERE tc_all.fix_for_fingerprint = m.fingerprint) as evidence_count
112
- FROM memories m
113
- WHERE m.type = 'error'
114
- AND m.origin = 'reflector'
115
- AND m.fingerprint IN (
116
- SELECT DISTINCT fix_for_fingerprint
117
- FROM tool_calls
118
- WHERE session_id = ? AND fix_for_fingerprint IS NOT NULL
119
- )
120
- AND m.status IN ('active', 'stale')
121
- GROUP BY m.fingerprint
122
- HAVING (
123
- SELECT MAX(tc.ts) FROM tool_calls tc
124
- WHERE tc.fix_for_fingerprint = m.fingerprint
125
- ) >= COALESCE(
126
- (SELECT MAX(m2.updated_at) FROM memories m2
127
- WHERE m2.fingerprint = m.fingerprint
128
- AND m2.type = 'pattern'
129
- AND m2.origin = 'causal'),
130
- '1970-01-01'
131
- )`)
132
- .all(sessionId);
133
- let promoted = 0;
134
- for (const err of linkedErrors) {
135
- try {
136
- const result = this.memoryService.promoteToPattern(err.id, err.evidence_count, err.recurrence_count ?? 0);
137
- if (result) {
138
- promoted++;
139
- // v0.4.0 (K4-009) — only a NEW pattern row counts as a
140
- // promotion; the idempotent refresh path no longer
141
- // inflates the metric. `patterns_causal` is deprecated
142
- // (key kept for compat, never incremented).
143
- if (result.created) {
144
- this.metrics?.incr("patterns_promoted_new", 1);
145
- // v0.4.0 (K4-015) — promotion-time LLM enrichment:
146
- // at most one call per NEW pattern, gated on
147
- // `kevin_settings.llm_reflection_enabled` and the
148
- // per-pattern `metadata.enriched` marker. Never on
149
- // the failure hot path.
150
- await this.enrichIfEnabled(err.id, result.id);
151
- }
152
- }
153
- }
154
- catch {
155
- // promoteToPattern may fail if the error memory was removed
156
- }
157
- }
158
- return promoted;
159
- }
160
- /**
161
- * v0.4.0 (K4-015) — fire the opt-in enrich hook at most once per
162
- * promoted pattern. The hook's one-line phrase replaces the
163
- * deterministic `Fixed by:` line; null keeps it. A call (phrase or
164
- * not) stamps `metadata.enriched` so repeated idle cycles stay at
165
- * one LLM call per pattern.
166
- */
167
- async enrichIfEnabled(errorId, patternId) {
168
- if (!this.enrichFn || !this.isLlmReflectionEnabled())
169
- return;
170
- const pattern = this.memoryService.getById(patternId);
171
- if (!pattern)
172
- return;
173
- const meta = (pattern.metadata ?? {});
174
- if (meta.enriched === true)
175
- return;
176
- const phrase = await this.enrichFn({
177
- lesson: pattern.content,
178
- fixArgs: pattern.fixArgs ?? null,
179
- originalError: this.memoryService.getById(errorId)?.content ?? null,
180
- });
181
- const content = phrase
182
- ? pattern.content.includes("\nFixed by: ")
183
- ? pattern.content.replace(/\nFixed by: .+$/s, `\n${phrase}`)
184
- : `${pattern.content}\n${phrase}`
185
- : pattern.content;
186
- this.memoryService.update(patternId, {
187
- content,
188
- metadata: { ...meta, enriched: true },
189
- });
190
- }
191
- isLlmReflectionEnabled() {
192
- const row = this.store
193
- .prepare("SELECT value FROM kevin_settings WHERE key = ?")
194
- .get("llm_reflection_enabled");
195
- return row?.value === "1";
196
- }
197
- }
@@ -1,35 +0,0 @@
1
- import type { MemoryService } from "./MemoryService.js";
2
- import type { RepoTruth } from "./RepoTruth.js";
3
- import type { Store } from "./Store.js";
4
- import type { Metrics } from "./metrics.js";
5
- export type ConflictKind = "repo_truth" | "decision_pair" | "temporal";
6
- export interface Conflict {
7
- readonly id: string;
8
- readonly kind: ConflictKind;
9
- readonly memoryA: string;
10
- readonly memoryB?: string;
11
- readonly factId?: string;
12
- readonly detail: string;
13
- }
14
- export interface RepoTruthConflictInput {
15
- readonly memoryId: string;
16
- readonly factId?: string | null;
17
- readonly reasons: readonly string[];
18
- }
19
- export declare class ConflictDetector {
20
- private readonly store;
21
- private readonly projectId;
22
- private readonly repoTruth?;
23
- private readonly memoryService?;
24
- private readonly metrics;
25
- constructor(store: Store, projectId: string, metrics?: Metrics | null, repoTruth?: RepoTruth | undefined, memoryService?: MemoryService | undefined);
26
- detect(repoTruthInput?: readonly RepoTruthConflictInput[]): Conflict[];
27
- acknowledge(id: string): void;
28
- /** Human-only operation. It resolves the conflict row, never a memory. */
29
- resolve(id: string, keepMemoryId: string): void;
30
- private detectRepoTruth;
31
- private repoTruthInputs;
32
- private detectDecisionPairs;
33
- private detectTemporal;
34
- private insertIfNew;
35
- }