@jmtrin/opencode-kevin 1.2.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 (141) hide show
  1. package/dist/plugin/index.d.ts +4 -3
  2. package/dist/plugin/index.js +122 -113
  3. package/dist/plugin/native.d.ts +2 -2
  4. package/dist/plugin/native.js +2 -2
  5. package/package.json +56 -68
  6. package/LICENSE +0 -21
  7. package/README.md +0 -605
  8. package/dist/migrations/001_initial.sql +0 -92
  9. package/dist/migrations/002_indexes.sql +0 -14
  10. package/dist/migrations/003_v02_signal.sql +0 -58
  11. package/dist/migrations/004_v03_knowledge.sql +0 -138
  12. package/dist/migrations/005_v04_signal.sql +0 -57
  13. package/dist/migrations/006_v05_glassbox.sql +0 -118
  14. package/dist/migrations/007_v06_pull.sql +0 -145
  15. package/dist/migrations/008_v07_truth.sql +0 -124
  16. package/dist/migrations/009_v08_team.sql +0 -100
  17. package/dist/migrations/010_v09_native.sql +0 -78
  18. package/dist/migrations/011_v10_proven.sql +0 -69
  19. package/dist/migrations/012_v11_drift.sql +0 -24
  20. package/dist/plugin/Archiver.d.ts +0 -42
  21. package/dist/plugin/Archiver.js +0 -83
  22. package/dist/plugin/ArtifactWriter.d.ts +0 -93
  23. package/dist/plugin/ArtifactWriter.js +0 -291
  24. package/dist/plugin/CausalChain.d.ts +0 -22
  25. package/dist/plugin/CausalChain.js +0 -197
  26. package/dist/plugin/ChatBridge.d.ts +0 -41
  27. package/dist/plugin/ChatBridge.js +0 -103
  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/DashboardHtml.d.ts +0 -5
  37. package/dist/plugin/DashboardHtml.js +0 -180
  38. package/dist/plugin/Feedback.d.ts +0 -67
  39. package/dist/plugin/Feedback.js +0 -120
  40. package/dist/plugin/HookLiveness.d.ts +0 -88
  41. package/dist/plugin/HookLiveness.js +0 -322
  42. package/dist/plugin/InjectionLedger.d.ts +0 -98
  43. package/dist/plugin/InjectionLedger.js +0 -295
  44. package/dist/plugin/LessonFixer.d.ts +0 -44
  45. package/dist/plugin/LessonFixer.js +0 -45
  46. package/dist/plugin/Materializer.d.ts +0 -83
  47. package/dist/plugin/Materializer.js +0 -181
  48. package/dist/plugin/MemoryService.d.ts +0 -360
  49. package/dist/plugin/MemoryService.js +0 -1242
  50. package/dist/plugin/Migrate.d.ts +0 -17
  51. package/dist/plugin/Migrate.js +0 -271
  52. package/dist/plugin/PatternMiner.d.ts +0 -49
  53. package/dist/plugin/PatternMiner.js +0 -132
  54. package/dist/plugin/QualityGate.d.ts +0 -124
  55. package/dist/plugin/QualityGate.js +0 -119
  56. package/dist/plugin/Reflector.d.ts +0 -91
  57. package/dist/plugin/Reflector.js +0 -307
  58. package/dist/plugin/RepoIdentity.d.ts +0 -124
  59. package/dist/plugin/RepoIdentity.js +0 -300
  60. package/dist/plugin/RepoTruth.d.ts +0 -80
  61. package/dist/plugin/RepoTruth.js +0 -599
  62. package/dist/plugin/Retrospective.d.ts +0 -17
  63. package/dist/plugin/Retrospective.js +0 -216
  64. package/dist/plugin/SharedLayer.d.ts +0 -159
  65. package/dist/plugin/SharedLayer.js +0 -462
  66. package/dist/plugin/Store.d.ts +0 -29
  67. package/dist/plugin/Store.js +0 -50
  68. package/dist/plugin/ToolCallObserver.d.ts +0 -32
  69. package/dist/plugin/ToolCallObserver.js +0 -201
  70. package/dist/plugin/TuiActions.d.ts +0 -43
  71. package/dist/plugin/TuiActions.js +0 -181
  72. package/dist/plugin/TuiSnapshots.d.ts +0 -24
  73. package/dist/plugin/TuiSnapshots.js +0 -158
  74. package/dist/plugin/columns.d.ts +0 -11
  75. package/dist/plugin/columns.js +0 -54
  76. package/dist/plugin/confidence.d.ts +0 -8
  77. package/dist/plugin/confidence.js +0 -34
  78. package/dist/plugin/contract.d.ts +0 -54
  79. package/dist/plugin/contract.js +0 -386
  80. package/dist/plugin/diff.d.ts +0 -8
  81. package/dist/plugin/diff.js +0 -182
  82. package/dist/plugin/escape.d.ts +0 -39
  83. package/dist/plugin/escape.js +0 -60
  84. package/dist/plugin/fingerprint.d.ts +0 -27
  85. package/dist/plugin/fingerprint.js +0 -73
  86. package/dist/plugin/inferability.d.ts +0 -32
  87. package/dist/plugin/inferability.js +0 -88
  88. package/dist/plugin/kevin_approve.d.ts +0 -34
  89. package/dist/plugin/kevin_approve.js +0 -52
  90. package/dist/plugin/kevin_audit.d.ts +0 -229
  91. package/dist/plugin/kevin_audit.js +0 -631
  92. package/dist/plugin/kevin_bench.d.ts +0 -10
  93. package/dist/plugin/kevin_bench.js +0 -82
  94. package/dist/plugin/kevin_conflicts.d.ts +0 -9
  95. package/dist/plugin/kevin_conflicts.js +0 -50
  96. package/dist/plugin/kevin_contract.d.ts +0 -8
  97. package/dist/plugin/kevin_contract.js +0 -59
  98. package/dist/plugin/kevin_doctor.d.ts +0 -62
  99. package/dist/plugin/kevin_doctor.js +0 -196
  100. package/dist/plugin/kevin_facts.d.ts +0 -42
  101. package/dist/plugin/kevin_facts.js +0 -36
  102. package/dist/plugin/kevin_forget.d.ts +0 -33
  103. package/dist/plugin/kevin_forget.js +0 -260
  104. package/dist/plugin/kevin_native.d.ts +0 -29
  105. package/dist/plugin/kevin_native.js +0 -79
  106. package/dist/plugin/kevin_propose.d.ts +0 -23
  107. package/dist/plugin/kevin_propose.js +0 -14
  108. package/dist/plugin/kevin_publish.d.ts +0 -38
  109. package/dist/plugin/kevin_publish.js +0 -18
  110. package/dist/plugin/kevin_why.d.ts +0 -23
  111. package/dist/plugin/kevin_why.js +0 -111
  112. package/dist/plugin/memory-format.d.ts +0 -20
  113. package/dist/plugin/memory-format.js +0 -59
  114. package/dist/plugin/metrics.d.ts +0 -97
  115. package/dist/plugin/metrics.js +0 -288
  116. package/dist/plugin/okf-export.d.ts +0 -3
  117. package/dist/plugin/okf-export.js +0 -134
  118. package/dist/plugin/okf-import.d.ts +0 -76
  119. package/dist/plugin/okf-import.js +0 -271
  120. package/dist/plugin/okf.d.ts +0 -107
  121. package/dist/plugin/okf.js +0 -303
  122. package/dist/plugin/perf.d.ts +0 -35
  123. package/dist/plugin/perf.js +0 -174
  124. package/dist/plugin/query-tokenizer.d.ts +0 -13
  125. package/dist/plugin/query-tokenizer.js +0 -133
  126. package/dist/plugin/redact.d.ts +0 -2
  127. package/dist/plugin/redact.js +0 -18
  128. package/dist/plugin/replay-types.d.ts +0 -69
  129. package/dist/plugin/replay-types.js +0 -156
  130. package/dist/plugin/replay.d.ts +0 -36
  131. package/dist/plugin/replay.js +0 -202
  132. package/dist/plugin/sqlite-adapter.d.ts +0 -12
  133. package/dist/plugin/sqlite-adapter.js +0 -81
  134. package/dist/plugin/time-ms.d.ts +0 -1
  135. package/dist/plugin/time-ms.js +0 -16
  136. package/dist/plugin/tui-types.d.ts +0 -59
  137. package/dist/plugin/tui-types.js +0 -4
  138. package/dist/plugin/tui.d.ts +0 -18
  139. package/dist/plugin/tui.js +0 -198
  140. package/dist/plugin/uuid.d.ts +0 -1
  141. package/dist/plugin/uuid.js +0 -50
@@ -1,533 +0,0 @@
1
- import { QualityGate, } from "./QualityGate.js";
2
- import { formatMemories, formatMemorySnippets } from "./memory-format.js";
3
- import { estimateTokens } from "./metrics.js";
4
- import { STOP_WORDS } from "./query-tokenizer.js";
5
- export const QUALITY_GATE_SETTING = "quality_gate_enabled";
6
- export const SNIPPET_INJECTION_SETTING = "lesson_snippet_injection";
7
- /**
8
- * v0.6.0 (K6-023 / plan §5.8) — the effective pre-prompt budget shared by
9
- * `ContextInjector.prePromptCap()` and `kevin_audit`'s channels block
10
- * (budget_tokens). Single source of truth for the K6-021 clamp:
11
- * default 400 when the raw value is missing/non-numeric, clamped to
12
- * [0, 4000]; 0 means "push off".
13
- */
14
- export function effectivePrePromptCap(raw) {
15
- const n = Number(raw);
16
- if (!Number.isFinite(n))
17
- return 400;
18
- return Math.min(4000, Math.max(0, Math.round(n)));
19
- }
20
- // v0.4.0 default pre-prompt budget. v0.5.0 (K5-017 / plan §8.11, D5-11):
21
- // kept as the compile-time fallback, but the EFFECTIVE cap is read at call
22
- // time from the `pre_prompt_budget_tokens` setting. v0.6.0 (K6-021 / plan
23
- // §5.8): the setting's default becomes "400" and the lower clamp bound
24
- // drops to 0 (off) — the confound fix in K5-005 showed a large share of
25
- // injections are `inconclusive`, and the roadmap's kill criterion K1
26
- // requires "off" to be a reachable, supported configuration.
27
- const SYSTEM_TRANSFORM_TOKENS = 900;
28
- const COMPACTING_TOKENS = 2000;
29
- // v0.5.0 (K5-007 / plan §8.10, D5-04) — every rejection reason maps 1:1 to
30
- // an `injections_blocked_*` counter (principle 16: a rejection you did not
31
- // count did not happen). Single lookup + null-check at the call site; the
32
- // `ok` reason admits and must never increment anything.
33
- const BLOCKED_METRIC = {
34
- ok: null,
35
- // v0.6.0 (K6-022 / plan §5.8) — sixth reason, sixth counter.
36
- low_confidence: "injections_blocked_confidence",
37
- seen_this_session: "injections_blocked_seen",
38
- ignored: "injections_blocked_ignored",
39
- not_active: "injections_blocked_stale",
40
- recurrence: "injections_blocked_recurrence",
41
- weak: "injections_blocked_weak",
42
- };
43
- function isWordChar(ch) {
44
- return /[a-z0-9áéíóúüñ]/i.test(ch);
45
- }
46
- /**
47
- * BUG-005 — extract the lesson's `Suggestion:` text (first line after the
48
- * marker) when present. Returns null for agent-saved notes that carry no
49
- * suggestion payload. Also matches the truncated snippet-style lesson
50
- * bodies used by older tests.
51
- */
52
- function extractSuggestionText(content) {
53
- const m = content.match(/\nSuggestion:\s*([^\n]+)/);
54
- return m ? m[1].trim() : null;
55
- }
56
- /**
57
- * BUG-005 — extract the `fails with X` slot of a lesson. For lessons
58
- * without a dispatched code this equals the coarse errorType that
59
- * `QualityGate.evaluate` needs for the strength classification.
60
- */
61
- function extractFailsWithErrorType(content) {
62
- const m = content.match(/\bfails with ([^:]+):/i);
63
- return m ? m[1].trim() : null;
64
- }
65
- export class ContextInjector {
66
- memoryService;
67
- metrics;
68
- ledger;
69
- lastRecurrenceCount = 0;
70
- /** v0.4.0 (K4-016) — session that produced the last recurrence set. */
71
- lastRecurredSession = null;
72
- /** v0.4.0 (K4-017) — per-session seen-set (plan §5.1 rule 3). */
73
- seenBySession = new Map();
74
- constructor(memoryService, metrics = null, ledger = null) {
75
- this.memoryService = memoryService;
76
- this.metrics = metrics;
77
- this.ledger = ledger;
78
- }
79
- /**
80
- * v0.4.0 (K4-017) — reset the per-session seen-set when a session is
81
- * created (plan §5.1 rule 3). Wired from the `session.created` event.
82
- */
83
- onSessionCreated(sessionId) {
84
- this.seenBySession.delete(sessionId);
85
- }
86
- /**
87
- * v0.3.0 (K3-020) — notify the injector that the negative feedback half
88
- * fired N times in the last session.idle. The next system.transform or
89
- * compacting hook will prepend a HITL suggestion block.
90
- * v0.4.0 (K4-016) — the session id enables the concrete suggestion:
91
- * the most-recurred fingerprint's pattern + its fix_args.
92
- */
93
- setRecurrences(count, sessionId) {
94
- this.lastRecurrenceCount = count;
95
- this.lastRecurredSession = sessionId ?? null;
96
- }
97
- /**
98
- * v0.4.0 (K4-016) — generate a CONCRETE HITL suggestion block when
99
- * recurrences occurred (plan §5.5): names the most-recurred pattern,
100
- * its exact recurrence count, observed fix_args and confidence. The
101
- * AGENTS.md draft line derives from the lesson's `Suggestion:` text —
102
- * never a canned string, and never the anonymous "the same error
103
- * pattern" without naming the pattern and count.
104
- *
105
- * BUG-012 — emits AT MOST ONCE per session: calling it resets the
106
- * pending recurrence signal, so whichever hook (system.transform or
107
- * compacting) runs first in a session consumes the block. Documented
108
- * behavior; wire index.ts to match.
109
- */
110
- generateSuggestion() {
111
- const count = this.lastRecurrenceCount;
112
- const sessionId = this.lastRecurredSession;
113
- this.lastRecurrenceCount = 0;
114
- this.lastRecurredSession = null;
115
- if (count === 0)
116
- return "";
117
- const recurrences = this.ledger?.recurrencesFor(sessionId ?? "") ?? new Map();
118
- let topFp = null;
119
- let topCount = 0;
120
- for (const [fp, n] of recurrences) {
121
- if (n > topCount) {
122
- topFp = fp;
123
- topCount = n;
124
- }
125
- }
126
- const pattern = topFp
127
- ? this.memoryService.getByFingerprint(topFp, "pattern")
128
- : null;
129
- // No pattern memory for the recurred fingerprint (or no ledger): a
130
- // short fallback that still names the count.
131
- if (!pattern) {
132
- return `<kevin-suggestion>
133
- An error pattern recurred ${count} time(s) this session.
134
- Consider adding a convention to AGENTS.md.
135
- </kevin-suggestion>`;
136
- }
137
- const summary = pattern.content
138
- .split("\n")[0]
139
- .replace(/^Causal pattern:\s*/, "")
140
- .trim();
141
- const pct = pattern.confidence != null ? Math.round(pattern.confidence * 100) : 0;
142
- const evidence = pattern.evidenceCount ?? 0;
143
- const fixLine = pattern.fixArgs
144
- ? `Observed fix: ${pattern.fixArgs} (${evidence} confirmed fix${evidence === 1 ? "" : "es"}, confidence ${pct}%).`
145
- : "";
146
- return `<kevin-suggestion>
147
- The error pattern "${summary}" recurred ${topCount} time(s) this session.
148
- ${fixLine}
149
- Consider adding this convention to AGENTS.md:
150
- - ${this.agentsDraftLine(pattern)}
151
- </kevin-suggestion>`;
152
- }
153
- /**
154
- * v0.4.0 (K4-016) — the AGENTS.md draft line derives from the lesson's
155
- * `Suggestion:` text (kept verbatim in the pattern's `Original:` block).
156
- */
157
- agentsDraftLine(pattern) {
158
- const m = pattern.content.match(/\nSuggestion: ([^\n]+)/);
159
- if (m)
160
- return m[1].trim();
161
- return `${pattern.content
162
- .split("\n")[0]
163
- .replace(/^Causal pattern:\s*/, "")
164
- .trim()} recurred — document the fix in AGENTS.md`;
165
- }
166
- /**
167
- * v0.2.0 (K2-024): origin-aware ranking at injection time is delegated
168
- * to `MemoryService.getRelevant()` (K2-023, D2-13). The injector does
169
- * NOT re-rank on its own — it consumes the already-ranked slice and
170
- * applies the conditional-budget guard for opt-out (`protect: false`)
171
- * callers. Plan §B6.5: "apply the same multiplier as
172
- * MemoryService.recall so reflector lessons outrank agent-saved notes
173
- * at injection time" — satisfied transitively via the getRelevant call.
174
- *
175
- * v0.4.0 (K4-017): after retrieval, every memory is filtered through
176
- * `QualityGate.canInject` (session seen-set + recurrence + strength),
177
- * and each admitted memory is recorded in the `InjectionLedger`
178
- * (plan §5.2 — one row per injected memory).
179
- *
180
- * v0.5.0 (K5-014 / plan §8.10) — the pipeline is decomposed into
181
- * `fetchSlice` (ranked retrieval + budget overflow) + `evaluate`
182
- * (pure gate verdicts) + this orchestrator, so the read-only `plan()`
183
- * can mirror it without any side effect (D5-08).
184
- */
185
- inject(query, tag, cap, metricKey, sessionId,
186
- // v0.5.0 (K5-007 / plan §8.10, D5-08) — dry-run mode (kevin_trace)
187
- // must never move a counter, not even the injections_blocked_* ones.
188
- dryRun = false) {
189
- const memories = this.fetchSlice(query, cap, tag);
190
- if (memories.length === 0)
191
- return "";
192
- const admitted = this.admit(memories, sessionId, dryRun);
193
- if (admitted.length === 0)
194
- return "";
195
- const block = this.format(admitted, tag);
196
- this.recordInjections(admitted, sessionId, tag, block);
197
- this.metrics?.incr(metricKey, estimateTokens(block));
198
- return block;
199
- }
200
- /**
201
- * v0.5.0 (K5-014 / plan §8.10) — the ranked-retrieval stage shared by
202
- * `inject` and `plan`. With `dry = true` it is a strict read:
203
- *
204
- * - the probe fetch never bumps (BUG-016), like the live path;
205
- * - the overflow retry ALSO fetches with `bump: false` (the live path
206
- * lets the retry fetch bump once — that is the only difference);
207
- * - the no-retry bump is skipped.
208
- *
209
- * This is what lets `plan()` predict the EXACT slice the live path
210
- * would inject without mutating a single relevance score.
211
- */
212
- fetchSlice(query, cap, tag, dry = false) {
213
- let memories = this.memoryService.getRelevant({
214
- query,
215
- maxTokens: cap,
216
- bump: false,
217
- });
218
- if (memories.length === 0)
219
- return [];
220
- const firstBlock = this.format(memories, tag);
221
- const aggregateTokens = estimateTokens(firstBlock);
222
- const firstRowProtect = memories[0]
223
- ?.protect;
224
- const noProtectAboveTheFold = firstRowProtect === false;
225
- if (aggregateTokens > 0.8 * cap && noProtectAboveTheFold) {
226
- // Retry = a single fetch with the adjusted budget, ranked by
227
- // the original scores (the probe never mutated them) — the
228
- // equivalent of one getRelevant call with maxTokens=lowerCap.
229
- const lowerCap = Math.max(1, Math.round(0.8 * cap));
230
- memories = this.memoryService.getRelevant({
231
- query,
232
- maxTokens: lowerCap,
233
- bump: dry ? false : undefined,
234
- });
235
- }
236
- else if (!dry) {
237
- // No retry: the probe slice IS the injected slice — bump it
238
- // exactly once here.
239
- this.memoryService.bumpRelevance(memories.map((m) => m.id));
240
- }
241
- return memories;
242
- }
243
- /**
244
- * v0.5.0 (K5-014 / plan §8.10, D5-08) — PUBLIC read-only prediction of
245
- * what `inject` WOULD do for a query: same retrieval, same gate, zero
246
- * side effects. Never moves a counter, never writes the seen-set, never
247
- * bumps relevance, never records ledger rows. `kevin_trace` (K5-015)
248
- * surfaces this to the agent; tests freeze the clock + settings around
249
- * it.
250
- */
251
- plan(query, options = {}) {
252
- const tag = options.tag ?? "context";
253
- const cap = options.cap ??
254
- (tag === "memory" ? COMPACTING_TOKENS : this.prePromptCap());
255
- const sessionId = options.sessionId ?? "";
256
- const memories = this.fetchSlice(query, cap, tag, true);
257
- const { verdicts } = this.evaluate(memories, sessionId);
258
- const admitted = [];
259
- const blocked = [];
260
- const admittedMemories = [];
261
- for (const v of verdicts) {
262
- const base = {
263
- id: v.memory.id,
264
- type: v.memory.type,
265
- tokens: estimateTokens(v.memory.content),
266
- };
267
- if (v.allowed) {
268
- admitted.push({ ...base, decision: "admitted" });
269
- admittedMemories.push(v.memory);
270
- }
271
- else {
272
- blocked.push({
273
- ...base,
274
- decision: "blocked",
275
- reason: v.reason,
276
- });
277
- }
278
- }
279
- const wouldInject = admitted.length > 0;
280
- const totalTokens = wouldInject
281
- ? estimateTokens(this.format(admittedMemories, tag))
282
- : 0;
283
- return {
284
- query,
285
- tag,
286
- cap,
287
- would_inject: wouldInject,
288
- total_tokens: totalTokens,
289
- admitted,
290
- blocked,
291
- };
292
- }
293
- /**
294
- * v0.5.0 (K5-014 / plan §8.10) — PURE gate evaluation shared by `admit`
295
- * (live path) and `plan` (read-only path): returns the verdict for every
296
- * candidate plus the seen-set as it WOULD look afterwards. Never writes
297
- * state — the caller decides whether to persist.
298
- */
299
- evaluate(memories, sessionId) {
300
- const qualityGateEnabled = this.memoryService.getSetting(QUALITY_GATE_SETTING, "1") === "1";
301
- // v0.6.0 (K6-022 / plan §5.8) — the floor is read ONCE per
302
- // plan/inject call (this method is the shared gate evaluation),
303
- // never per memory. A non-numeric setting degrades to undefined,
304
- // which disables the branch for every memory this call.
305
- const rawFloor = this.memoryService.getSetting("injection_confidence_floor", "0.6");
306
- const floor = Number(rawFloor);
307
- const confidenceFloor = Number.isFinite(floor) ? floor : undefined;
308
- const seen = new Set(this.seenBySession.get(sessionId) ?? []);
309
- const recurrences = this.ledger?.postInjectionRecurrencesFor(sessionId) ??
310
- new Map();
311
- const verdicts = [];
312
- for (const m of memories) {
313
- const q = this.lessonQuality(m);
314
- const verdict = QualityGate.canInjectVerdict({
315
- id: m.id,
316
- status: m.status ?? undefined,
317
- strength: q.strength,
318
- isActionable: q.isActionable,
319
- // v0.5.0 (K5-009) — the `ignored` flag is now a first-class
320
- // Memory field via mapRow (D5-07).
321
- ignored: m.ignored === true,
322
- // v0.6.0 (K6-022) — the memory's computed confidence
323
- // (may be null for rows without evidence).
324
- confidence: m.confidence ?? undefined,
325
- }, {
326
- seenThisSession: seen,
327
- // v0.4.0 (K4-025) — plan §5.1 rule 4: a causal pattern
328
- // re-admits a lesson that the stale error row cannot.
329
- // The recurrence ban (QualityGate rule 3) is scoped to
330
- // error lessons — a pattern is the FIXED form of the
331
- // fingerprint and is exactly what D4-06 wants back in
332
- // the prompt.
333
- recurrenceCount: m.type === "pattern"
334
- ? 0
335
- : m.fingerprint
336
- ? (recurrences.get(m.fingerprint) ?? 0)
337
- : 0,
338
- confidenceFloor,
339
- }, qualityGateEnabled);
340
- verdicts.push({ ...verdict, memory: m });
341
- }
342
- return { verdicts, seen };
343
- }
344
- /**
345
- * v0.5.0 (K5-017 / plan §8.11, D5-11) — the effective pre-prompt cap,
346
- * read at call time from `pre_prompt_budget_tokens` (seeded "900" by
347
- * migration 006). Clamped to [100, 4000]; a non-numeric value falls
348
- * back to 900. `kevin_trace` reports the value used via `plan().cap`.
349
- *
350
- * v0.6.0 (K6-021 / plan §5.8) — default becomes "400" and the lower
351
- * clamp bound drops to **0**: the roadmap's kill criterion K1 prescribes
352
- * cutting the push budget to zero when coverage is poor, and v0.5's
353
- * [100, 4000] clamp made that response unimplementable. A non-numeric
354
- * value falls back to 400. `onSystemTransform` treats 0 as off and
355
- * returns before any retrieval (see K6-021 acceptance).
356
- */
357
- prePromptCap() {
358
- return effectivePrePromptCap(this.memoryService.getSetting("pre_prompt_budget_tokens", "400"));
359
- }
360
- /**
361
- * v0.4.0 (K4-017) — QualityGate admission: filters the ranked slice to
362
- * memories that may be injected this session, updating the session
363
- * seen-set.
364
- *
365
- * v0.5.0 (K5-007 / plan §5.2, D5-04) — uses `canInjectVerdict` and
366
- * increments the matching `injections_blocked_*` counter for every
367
- * rejection, so gate policy becomes measurable. When `dryRun === true`
368
- * the counters stay untouched (D5-08) — and so does the seen-set
369
- * (a dry run is a strict read, K5-014).
370
- *
371
- * BUG-005 — strength/actionability now go through the REAL
372
- * `QualityGate.evaluate` semantics (plan §5.1 rules 1-2), which this
373
- * class had only re-derived from `metadata.dispatch`:
374
- * - dispatched code → strong + actionable (rescued errorType);
375
- * - generic fallback suggestion + no code → weak, NOT actionable
376
- * (the generic-suggestion ban is now enforced even for legacy
377
- * lessons without dispatch metadata, as long as the `Suggestion:`
378
- * text is available);
379
- * - specific suggestion without code → strong + actionable;
380
- * - no `Suggestion:` text at all (agent-saved note) → strong +
381
- * actionable — the agent explicitly asked to remember it;
382
- * - causal patterns (type='pattern') → always strong + actionable:
383
- * they are the FIXED form of a fingerprint (K4-025).
384
- */
385
- admit(memories, sessionId, dryRun = false) {
386
- const { verdicts, seen } = this.evaluate(memories, sessionId);
387
- const admitted = [];
388
- for (const v of verdicts) {
389
- if (v.allowed) {
390
- admitted.push(v.memory);
391
- seen.add(v.memory.id);
392
- }
393
- else if (!dryRun) {
394
- const key = BLOCKED_METRIC[v.reason];
395
- if (key)
396
- this.metrics?.incr(key, 1);
397
- }
398
- }
399
- if (!dryRun)
400
- this.seenBySession.set(sessionId, seen);
401
- return admitted;
402
- }
403
- /**
404
- * BUG-005 — the single source of truth for a memory's lesson quality,
405
- * shared by `admit` (the gate) and `format` (the K4-023 `(low
406
- * confidence)` marker). Routes lessons through `QualityGate.evaluate`
407
- * — the production call site that was previously missing.
408
- */
409
- lessonQuality(m) {
410
- // K4-025: a causal pattern is the fixed form of the fingerprint —
411
- // never gated by suggestion text.
412
- if (m.type === "pattern") {
413
- return { strength: "strong", isActionable: true, weak: false };
414
- }
415
- const meta = (m.metadata ?? null);
416
- const dispatch = meta?.dispatch ?? null;
417
- const suggestion = extractSuggestionText(m.content);
418
- if (suggestion === null) {
419
- // Agent-saved note without a `Suggestion:` line: the agent
420
- // explicitly asked to remember it → strong + actionable.
421
- return { strength: "strong", isActionable: true, weak: false };
422
- }
423
- // The lesson's `fails with X` slot holds the displayed errorType;
424
- // when no code was dispatched it equals the coarse errorType that
425
- // `evaluate` needs for the strength classification.
426
- const errorType = extractFailsWithErrorType(m.content);
427
- const q = QualityGate.evaluate({ errorType: errorType ?? "unknown", suggestion }, dispatch, errorType ?? "unknown");
428
- return {
429
- strength: q.strength,
430
- isActionable: q.isActionable,
431
- weak: q.strength === "weak",
432
- };
433
- }
434
- /**
435
- * v0.4.0 (K4-017) — one ledger row per admitted memory (plan §5.2).
436
- * Token attribution uses the memory's share of the final block.
437
- *
438
- * v0.8.0 (K8-024 / plan §5.7) — shared projections are recorded even
439
- * though they carry NO fingerprint by design (K8-017 — it is a
440
- * different identity dimension): the memory id is their identity in
441
- * the ledger, and recording them is what makes `injections_from_shared`
442
- * observable at all. No tool call can ever match a memory id, so
443
- * settle() marks the row inconclusive — excluded from the precision
444
- * denominator (K5-005) — and the BUG-015 skip is unchanged for local
445
- * notes without a fingerprint.
446
- */
447
- recordInjections(admitted, sessionId, tag, block) {
448
- if (this.ledger === null)
449
- return;
450
- const blockTokens = estimateTokens(block);
451
- const perMemory = Math.max(1, Math.round(blockTokens / admitted.length));
452
- const hook = tag === "context" ? "pre_prompt" : "compacting";
453
- for (const m of admitted) {
454
- if (!m.fingerprint && m.layer !== "shared")
455
- continue;
456
- this.ledger.record({
457
- memoryId: m.id,
458
- fingerprint: m.fingerprint ?? m.id,
459
- sessionId,
460
- hook,
461
- tokens: perMemory,
462
- layer: m.layer ?? "local",
463
- });
464
- }
465
- }
466
- /**
467
- * v0.4.0 (K4-012) — snippet injection payload (plan §5.1 rule 5,
468
- * D4-05): rows show `id:` + first 2 non-empty lines + `<protect>`
469
- * instead of the full body. Gated by the `lesson_snippet_injection`
470
- * setting (default `'1'`); when `'0'`, full content is restored.
471
- * `escapeInjectedText` is applied to snippet content by the formatter.
472
- * v0.4.0 (K4-023) — weak lessons admitted in debug mode are flagged so
473
- * the formatter renders the `(low confidence)` marker.
474
- */
475
- format(memories, tag) {
476
- const snippetsOn = this.memoryService.getSetting(SNIPPET_INJECTION_SETTING, "1") === "1";
477
- const items = memories.map((m) => ({
478
- ...m,
479
- // BUG-005 — the weak marker must mirror the admission decision
480
- // (K4-023 debug mode), not re-derive from dispatch alone.
481
- weak: this.lessonQuality(m).weak,
482
- }));
483
- return snippetsOn
484
- ? formatMemorySnippets(items, tag)
485
- : formatMemories(items, tag);
486
- }
487
- deriveQuery(messages) {
488
- let lastUserContent = "";
489
- for (let i = messages.length - 1; i >= 0; i--) {
490
- if (messages[i].role === "user") {
491
- lastUserContent = messages[i].content;
492
- break;
493
- }
494
- }
495
- if (!lastUserContent)
496
- return "";
497
- const tokens = lastUserContent
498
- .toLowerCase()
499
- .split(/\s+/)
500
- .map((t) => {
501
- let out = "";
502
- for (const ch of t) {
503
- if (isWordChar(ch))
504
- out += ch;
505
- }
506
- return out;
507
- })
508
- .filter((t) => t.length > 0 && !STOP_WORDS.has(t));
509
- return tokens.join(" ");
510
- }
511
- onSystemTransform(input, output) {
512
- // v0.6.0 (K6-021 / plan §5.8) — a cap of 0 means off: return before
513
- // any retrieval, gate evaluation or metric write. "Off" must not
514
- // run a hidden query and throw away the result.
515
- const cap = this.prePromptCap();
516
- if (cap === 0)
517
- return;
518
- const query = this.deriveQuery(input.messages);
519
- if (!query)
520
- return;
521
- const block = this.inject(query, "context", cap, "tokens_injected_pre_prompt", input.sessionID ?? "");
522
- if (block)
523
- output.system.push(block);
524
- }
525
- onCompacting(input, output) {
526
- const query = this.deriveQuery(input.messages);
527
- if (!query)
528
- return;
529
- const block = this.inject(query, "memory", COMPACTING_TOKENS, "tokens_injected_compacting", input.sessionID);
530
- if (block)
531
- output.context.push(block);
532
- }
533
- }
@@ -1,35 +0,0 @@
1
- import type { MemoryService } from "./MemoryService.js";
2
- import type { Store } from "./Store.js";
3
- import type { Metrics } from "./metrics.js";
4
- export interface MinedConvention {
5
- /** Caller-supplied, derived from the normalized statement (D7-11). */
6
- readonly fingerprint: string;
7
- /** Human-readable rule statement. */
8
- readonly statement: string;
9
- /** Distinct sessions in which the pattern held. */
10
- readonly support: number;
11
- readonly kind: "sequence" | "co_edit";
12
- }
13
- export declare class ConventionMiner {
14
- private readonly store;
15
- private readonly memoryService;
16
- private readonly projectId;
17
- private readonly metrics;
18
- constructor(store: Store, memoryService: MemoryService, projectId: string, metrics?: Metrics | null);
19
- /** v0.7.0 (K7-010) — the `sequence` miner over successful tool_calls. */
20
- mineSequence(minSupport?: number): MinedConvention[];
21
- /** v0.7.0 (K7-011) — the `co_edit` miner over same-session file writes. */
22
- mineCoEdit(minSupport?: number): MinedConvention[];
23
- /** v0.7.0 (K7-012) — mine both kinds in one call. */
24
- mine(minSupport?: number): MinedConvention[];
25
- /**
26
- * v0.7.0 (K7-012 / plan §5.4, D7-11) — emit mined(conventions) as `rule`
27
- * memories. Returns the number of memories created or refreshed and
28
- * increments `conventions_mined`. Runs on session.idle only, behind
29
- * convention_mining_enabled. The fingerprint derives from the statement,
30
- * so a re-mine of an unchanged convention collides and supersedes.
31
- */
32
- emit(conventions: MinedConvention[]): number;
33
- private materialize;
34
- private fetchToolCalls;
35
- }