@jmtrin/opencode-kevin 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -9
- package/dist/migrations/005_v04_signal.sql +57 -0
- package/dist/migrations/006_v05_glassbox.sql +118 -0
- package/dist/plugin/Archiver.d.ts +42 -0
- package/dist/plugin/Archiver.js +98 -0
- package/dist/plugin/Archiver.js.map +1 -0
- package/dist/plugin/CausalChain.d.ts +13 -2
- package/dist/plugin/CausalChain.js +83 -13
- package/dist/plugin/CausalChain.js.map +1 -1
- package/dist/plugin/ContextInjector.d.ts +152 -4
- package/dist/plugin/ContextInjector.js +400 -93
- package/dist/plugin/ContextInjector.js.map +1 -1
- package/dist/plugin/Feedback.d.ts +67 -0
- package/dist/plugin/Feedback.js +137 -0
- package/dist/plugin/Feedback.js.map +1 -0
- package/dist/plugin/InjectionLedger.d.ts +92 -0
- package/dist/plugin/InjectionLedger.js +243 -0
- package/dist/plugin/InjectionLedger.js.map +1 -0
- package/dist/plugin/LessonFixer.d.ts +44 -0
- package/dist/plugin/LessonFixer.js +46 -0
- package/dist/plugin/LessonFixer.js.map +1 -0
- package/dist/plugin/MemoryService.d.ts +81 -1
- package/dist/plugin/MemoryService.js +321 -54
- package/dist/plugin/MemoryService.js.map +1 -1
- package/dist/plugin/Migrate.js +31 -0
- package/dist/plugin/Migrate.js.map +1 -1
- package/dist/plugin/QualityGate.d.ts +110 -0
- package/dist/plugin/QualityGate.js +108 -0
- package/dist/plugin/QualityGate.js.map +1 -0
- package/dist/plugin/Reflector.d.ts +17 -0
- package/dist/plugin/Reflector.js +81 -26
- package/dist/plugin/Reflector.js.map +1 -1
- package/dist/plugin/Retrospective.js +22 -1
- package/dist/plugin/Retrospective.js.map +1 -1
- package/dist/plugin/ToolCallObserver.d.ts +1 -0
- package/dist/plugin/ToolCallObserver.js +15 -8
- package/dist/plugin/ToolCallObserver.js.map +1 -1
- package/dist/plugin/confidence.d.ts +8 -0
- package/dist/plugin/confidence.js +35 -0
- package/dist/plugin/confidence.js.map +1 -0
- package/dist/plugin/index.d.ts +5 -0
- package/dist/plugin/index.js +368 -44
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/kevin_audit.d.ts +49 -0
- package/dist/plugin/kevin_audit.js +141 -0
- package/dist/plugin/kevin_audit.js.map +1 -0
- package/dist/plugin/kevin_why.d.ts +4 -0
- package/dist/plugin/kevin_why.js +72 -35
- package/dist/plugin/kevin_why.js.map +1 -1
- package/dist/plugin/memory-format.d.ts +12 -0
- package/dist/plugin/memory-format.js +45 -5
- package/dist/plugin/memory-format.js.map +1 -1
- package/dist/plugin/metrics.d.ts +27 -1
- package/dist/plugin/metrics.js +61 -0
- package/dist/plugin/metrics.js.map +1 -1
- package/dist/plugin/okf-export.js +63 -22
- package/dist/plugin/okf-export.js.map +1 -1
- package/dist/plugin/okf-import.d.ts +4 -1
- package/dist/plugin/okf-import.js +45 -12
- package/dist/plugin/okf-import.js.map +1 -1
- package/dist/plugin/query-tokenizer.d.ts +13 -0
- package/dist/plugin/query-tokenizer.js +86 -0
- package/dist/plugin/query-tokenizer.js.map +1 -0
- package/dist/plugin/replay-types.d.ts +327 -0
- package/dist/plugin/replay-types.js +65 -0
- package/dist/plugin/replay-types.js.map +1 -0
- package/dist/plugin/replay.d.ts +36 -0
- package/dist/plugin/replay.js +193 -0
- package/dist/plugin/replay.js.map +1 -0
- package/migrations/005_v04_signal.sql +57 -0
- package/migrations/006_v05_glassbox.sql +118 -0
- package/package.json +3 -2
|
@@ -1,15 +1,28 @@
|
|
|
1
|
+
import { deterministicFixLine } from "./LessonFixer.js";
|
|
2
|
+
import { computeConfidence } from "./confidence.js";
|
|
1
3
|
import { fingerprint as computeFingerprint } from "./fingerprint.js";
|
|
4
|
+
import { toMatchClause, tokenizeQuery } from "./query-tokenizer.js";
|
|
2
5
|
import { uuidv7 } from "./uuid.js";
|
|
3
6
|
const MAX_SNIPPET_CHARS = 200;
|
|
4
|
-
function toSlim(mem) {
|
|
5
|
-
const
|
|
6
|
-
return {
|
|
7
|
+
function toSlim(mem, evidence = false) {
|
|
8
|
+
const base = {
|
|
7
9
|
id: mem.id,
|
|
8
10
|
type: mem.type,
|
|
9
11
|
scope: mem.scope,
|
|
10
|
-
score: typeof
|
|
12
|
+
score: typeof mem.metadata?.score ===
|
|
13
|
+
"number"
|
|
14
|
+
? mem.metadata.score
|
|
15
|
+
: mem.relevanceScore,
|
|
11
16
|
snippet: mem.content.slice(0, MAX_SNIPPET_CHARS),
|
|
12
17
|
};
|
|
18
|
+
if (!evidence)
|
|
19
|
+
return base;
|
|
20
|
+
return {
|
|
21
|
+
...base,
|
|
22
|
+
confidence: mem.confidence ?? null,
|
|
23
|
+
evidence_count: mem.evidenceCount ?? null,
|
|
24
|
+
last_verified_at: mem.lastVerifiedAt ?? null,
|
|
25
|
+
};
|
|
13
26
|
}
|
|
14
27
|
const TYPE_PRIORITY = {
|
|
15
28
|
error: 0,
|
|
@@ -31,6 +44,10 @@ const ORIGIN_BOOST_PATTERN = 1.5;
|
|
|
31
44
|
const ORIGIN_BOOST_CASUAL = 2;
|
|
32
45
|
const ORIGIN_BOOST_AGENT = 1;
|
|
33
46
|
const RECENCY_DECAY_PER_DAY = 0.95; // newer = closer to 1 (less penalty)
|
|
47
|
+
// v0.5.0 (K5-008 / plan §5.6, D5-10) — DATE_NOW sentinel: deterministic
|
|
48
|
+
// retrieval reads this fixed future instant instead of the wall clock,
|
|
49
|
+
// making ordering a pure function of database content. Export for tests.
|
|
50
|
+
export const DATE_NOW = "2099-01-01T00:00:00.000Z";
|
|
34
51
|
function sqliteUtcToMs(createdAt) {
|
|
35
52
|
// SQLite `datetime('now')` returns 'YYYY-MM-DD HH:MM:SS' in UTC.
|
|
36
53
|
// JS Date can parse ISO 8601 with 'T' and 'Z'.
|
|
@@ -48,6 +65,41 @@ function sqliteUtcNowPlusHours(hours) {
|
|
|
48
65
|
const pad = (n) => String(n).padStart(2, "0");
|
|
49
66
|
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
|
|
50
67
|
}
|
|
68
|
+
/** Shared column list for row reads (must stay in sync with MemoryRow). */
|
|
69
|
+
const MEMORY_ROW_SELECT = `id, type, content, scope, relevance_score, source_tool, source_session,
|
|
70
|
+
metadata, created_at, updated_at, expires_at,
|
|
71
|
+
project_id, fingerprint, origin,
|
|
72
|
+
evidence_count, recurrence_count, last_verified_at, status, fix_args`;
|
|
73
|
+
/**
|
|
74
|
+
* v0.5.0 (K5-008/009 / plan §5.6) — cached per-store probe for the 006-only
|
|
75
|
+
* `ignored` column (pre-006 DBs must not reference it). Shared by the
|
|
76
|
+
* retrieval filter, the row SELECT, and the save() ignored stamp.
|
|
77
|
+
*/
|
|
78
|
+
const ignoredColumnCache = new WeakMap();
|
|
79
|
+
function hasIgnoredColumn(store) {
|
|
80
|
+
const cached = ignoredColumnCache.get(store);
|
|
81
|
+
if (cached !== undefined)
|
|
82
|
+
return cached;
|
|
83
|
+
try {
|
|
84
|
+
store.prepare("SELECT ignored FROM memories LIMIT 1").get();
|
|
85
|
+
ignoredColumnCache.set(store, true);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
ignoredColumnCache.set(store, false);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* v0.5.0 (K5-009 / plan §5.3) — the 006-only columns are appended when the
|
|
95
|
+
* migration has run (same probe as the `ignored = 0` retrieval filter).
|
|
96
|
+
*/
|
|
97
|
+
function rowSelect(store) {
|
|
98
|
+
return hasIgnoredColumn(store)
|
|
99
|
+
? `${MEMORY_ROW_SELECT}, ignored, superseded_by,
|
|
100
|
+
feedback_positive, feedback_negative`
|
|
101
|
+
: MEMORY_ROW_SELECT;
|
|
102
|
+
}
|
|
51
103
|
function mapRow(row, score) {
|
|
52
104
|
const mem = {
|
|
53
105
|
id: row.id,
|
|
@@ -67,11 +119,20 @@ function mapRow(row, score) {
|
|
|
67
119
|
fingerprint: row.fingerprint ?? null,
|
|
68
120
|
origin: row.origin ?? null,
|
|
69
121
|
confidence: typeof row.evidence_count === "number"
|
|
70
|
-
?
|
|
122
|
+
? computeConfidence(row.evidence_count ?? 0, row.recurrence_count ?? 0, row.feedback_positive ?? 0, row.feedback_negative ?? 0)
|
|
71
123
|
: null,
|
|
72
124
|
evidenceCount: row.evidence_count ?? null,
|
|
73
125
|
lastVerifiedAt: row.last_verified_at ?? null,
|
|
74
126
|
status: row.status ?? "active",
|
|
127
|
+
fixArgs: row.fix_args ?? null,
|
|
128
|
+
recurrenceCount: row.recurrence_count ?? null,
|
|
129
|
+
// v0.5.0 (K5-009 / plan §5.3, D5-07) — the human-verdict and
|
|
130
|
+
// supersession fields; absent on pre-006 rows.
|
|
131
|
+
ignored: row.ignored === undefined ? undefined : Boolean(row.ignored),
|
|
132
|
+
supersedes: row.superseded_by ?? null,
|
|
133
|
+
// v0.5.0 (K5-010 / plan §5.3) — human judgement counters.
|
|
134
|
+
feedbackPositive: row.feedback_positive ?? 0,
|
|
135
|
+
feedbackNegative: row.feedback_negative ?? 0,
|
|
75
136
|
};
|
|
76
137
|
if (score !== undefined) {
|
|
77
138
|
if (!mem.metadata)
|
|
@@ -106,6 +167,29 @@ export class MemoryService {
|
|
|
106
167
|
// so that Metrics can be added without changing the parameter order callers
|
|
107
168
|
// have been using since v0.1.0.
|
|
108
169
|
store;
|
|
170
|
+
// v0.4.0 (BUG-008) — cached column probe for pre-005 DBs (which lack
|
|
171
|
+
// `recurrence_count`); save() must not reference the column there.
|
|
172
|
+
hasRecurrenceColumn() {
|
|
173
|
+
if (this._hasRecurrenceColumn === undefined) {
|
|
174
|
+
try {
|
|
175
|
+
this.store
|
|
176
|
+
.prepare("SELECT recurrence_count FROM memories LIMIT 1")
|
|
177
|
+
.get();
|
|
178
|
+
this._hasRecurrenceColumn = true;
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
this._hasRecurrenceColumn = false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return this._hasRecurrenceColumn;
|
|
185
|
+
}
|
|
186
|
+
_hasRecurrenceColumn;
|
|
187
|
+
// v0.5.0 (K5-008 / plan §5.6) — cached column probe for pre-006 DBs
|
|
188
|
+
// (which lack `ignored`); the retrieval filter must not reference the
|
|
189
|
+
// column there.
|
|
190
|
+
hasIgnoredColumn() {
|
|
191
|
+
return hasIgnoredColumn(this.store);
|
|
192
|
+
}
|
|
109
193
|
save(input) {
|
|
110
194
|
const scope = input.scope ?? "project";
|
|
111
195
|
const relevanceScore = input.relevanceScore ?? 0.5;
|
|
@@ -131,29 +215,56 @@ export class MemoryService {
|
|
|
131
215
|
if (scope === "session" && !input.expiresAt) {
|
|
132
216
|
expiresAt = sqliteUtcNowPlusHours(SESSION_DEFAULT_TTL_HOURS);
|
|
133
217
|
}
|
|
134
|
-
const id = uuidv7();
|
|
218
|
+
const id = input.id ?? uuidv7();
|
|
135
219
|
// v0.3.0 (K3-014) — supersede model: when saving a decision/rule with
|
|
136
220
|
// the same fingerprint as an existing active row, mark the old as
|
|
137
221
|
// superseded and insert the fresh version as active.
|
|
222
|
+
// v0.4.0 (K4-011) — `memories_superseded` is only counted here,
|
|
223
|
+
// where a row is truly replaced; penalization of recurring
|
|
224
|
+
// reflectors no longer increments it.
|
|
225
|
+
// v0.5.0 (K5-013 / plan §5.5, D5-06) — the old row also records WHO
|
|
226
|
+
// superseded it (`superseded_by = <new id>`), giving status-based
|
|
227
|
+
// supersession a navigable audit trail. Guarded: pre-006 DBs lack
|
|
228
|
+
// the column (same migration as `ignored`).
|
|
138
229
|
const supersedableTypes = ["decision", "rule"];
|
|
139
230
|
if (fp !== null && supersedableTypes.includes(input.type)) {
|
|
231
|
+
const withSupersededBy = this.hasIgnoredColumn();
|
|
232
|
+
const setClause = withSupersededBy
|
|
233
|
+
? "SET status = 'superseded', superseded_by = ?, updated_at = datetime('now')"
|
|
234
|
+
: "SET status = 'superseded', updated_at = datetime('now')";
|
|
140
235
|
this.store
|
|
141
236
|
.prepare(`UPDATE memories
|
|
142
|
-
|
|
237
|
+
${setClause}
|
|
143
238
|
WHERE fingerprint = ?
|
|
144
239
|
AND type = ?
|
|
145
240
|
AND status = 'active'
|
|
146
241
|
AND (project_id IS ? OR (project_id IS NULL AND ? IS NULL))`)
|
|
147
|
-
.run(
|
|
242
|
+
.run(...(withSupersededBy
|
|
243
|
+
? [id, fp, input.type, projectId, projectId]
|
|
244
|
+
: [fp, input.type, projectId, projectId]));
|
|
245
|
+
const after = this.store.prepare("SELECT changes() AS n").get();
|
|
246
|
+
if (after.n > 0) {
|
|
247
|
+
this.metrics?.incr("memories_superseded", 1);
|
|
248
|
+
}
|
|
148
249
|
}
|
|
149
250
|
try {
|
|
150
|
-
|
|
151
|
-
|
|
251
|
+
// v0.4.0 (BUG-008) — recurrence_count is only persisted when the
|
|
252
|
+
// column exists (migration 005); pre-005 DBs get the legacy shape.
|
|
253
|
+
const withRecurrence = this.hasRecurrenceColumn();
|
|
254
|
+
const insert = withRecurrence
|
|
255
|
+
? `INSERT INTO memories
|
|
256
|
+
(id, type, content, scope, relevance_score, source_tool, source_session,
|
|
257
|
+
metadata, expires_at, project_id, fingerprint, origin,
|
|
258
|
+
evidence_count, last_verified_at, status, recurrence_count)
|
|
259
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
260
|
+
: `INSERT INTO memories
|
|
152
261
|
(id, type, content, scope, relevance_score, source_tool, source_session,
|
|
153
262
|
metadata, expires_at, project_id, fingerprint, origin,
|
|
154
263
|
evidence_count, last_verified_at, status)
|
|
155
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
156
|
-
|
|
264
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
|
265
|
+
this.store
|
|
266
|
+
.prepare(insert)
|
|
267
|
+
.run(id, input.type, input.content, scope, relevanceScore, input.sourceTool ?? null, input.sourceSession ?? null, metadata, expiresAt, projectId, fp, origin, input.evidenceCount ?? 0, input.lastVerifiedAt ?? null, status, ...(withRecurrence ? [input.recurrenceCount ?? 0] : []));
|
|
157
268
|
return id;
|
|
158
269
|
}
|
|
159
270
|
catch (err) {
|
|
@@ -185,14 +296,26 @@ export class MemoryService {
|
|
|
185
296
|
}
|
|
186
297
|
getById(id) {
|
|
187
298
|
const row = this.store
|
|
188
|
-
.prepare(`SELECT
|
|
189
|
-
metadata, created_at, updated_at, expires_at,
|
|
190
|
-
project_id, fingerprint, origin,
|
|
191
|
-
evidence_count, last_verified_at, status
|
|
299
|
+
.prepare(`SELECT ${rowSelect(this.store)}
|
|
192
300
|
FROM memories WHERE id = ?`)
|
|
193
301
|
.get(id);
|
|
194
302
|
return row ? mapRow(row) : null;
|
|
195
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* v0.4.0 (K4-016) — most recent ACTIVE memory for a fingerprint,
|
|
306
|
+
* optionally filtered by type. Feeds the HITL suggestion lookup
|
|
307
|
+
* (most-recurred fingerprint → its pattern memory).
|
|
308
|
+
*/
|
|
309
|
+
getByFingerprint(fingerprint, type) {
|
|
310
|
+
const row = this.store
|
|
311
|
+
.prepare(`SELECT ${rowSelect(this.store)}
|
|
312
|
+
FROM memories
|
|
313
|
+
WHERE fingerprint = ? AND status = 'active'
|
|
314
|
+
${type ? "AND type = ?" : ""}
|
|
315
|
+
ORDER BY created_at DESC LIMIT 1`)
|
|
316
|
+
.get(...(type ? [fingerprint, type] : [fingerprint]));
|
|
317
|
+
return row ? mapRow(row) : null;
|
|
318
|
+
}
|
|
196
319
|
update(id, fields) {
|
|
197
320
|
const cols = [];
|
|
198
321
|
const vals = [];
|
|
@@ -232,6 +355,13 @@ export class MemoryService {
|
|
|
232
355
|
cols.push("status = ?");
|
|
233
356
|
vals.push(fields.status);
|
|
234
357
|
}
|
|
358
|
+
// v0.4.0 (BUG-008) — recurrence_count is writable so okf-import can
|
|
359
|
+
// restore negative evidence across a round-trip. Guarded by the
|
|
360
|
+
// caller (pre-005 DBs lack the column; update() throws).
|
|
361
|
+
if (fields.recurrenceCount !== undefined) {
|
|
362
|
+
cols.push("recurrence_count = ?");
|
|
363
|
+
vals.push(fields.recurrenceCount);
|
|
364
|
+
}
|
|
235
365
|
if (cols.length === 0)
|
|
236
366
|
return;
|
|
237
367
|
cols.push("updated_at = datetime('now')");
|
|
@@ -261,6 +391,11 @@ export class MemoryService {
|
|
|
261
391
|
JOIN memories m ON m.rowid = memories_fts.rowid
|
|
262
392
|
WHERE memories_fts MATCH ?
|
|
263
393
|
AND (m.expires_at IS NULL OR m.expires_at > datetime('now'))`;
|
|
394
|
+
// v0.5.0 (K5-011 / plan §5.6, D5-07) — ignored memories are hidden
|
|
395
|
+
// from kevin_query too (guarded for pre-006 DBs).
|
|
396
|
+
if (this.hasIgnoredColumn()) {
|
|
397
|
+
sql += "\n AND m.ignored = 0";
|
|
398
|
+
}
|
|
264
399
|
const params = [match];
|
|
265
400
|
if (!input.includeSuperseded) {
|
|
266
401
|
sql += "\n AND m.status = 'active'";
|
|
@@ -284,27 +419,57 @@ export class MemoryService {
|
|
|
284
419
|
.map((r) => mapRow(r, r.score))
|
|
285
420
|
.filter((m) => !isNotSearchable(m))
|
|
286
421
|
.filter((m) => crossProjectOn || m.projectId !== null || m.origin !== "imported");
|
|
287
|
-
return input.full === true
|
|
422
|
+
return input.full === true
|
|
423
|
+
? memories
|
|
424
|
+
: memories.map((m) => toSlim(m, input.evidence === true));
|
|
288
425
|
}
|
|
289
426
|
isCrossProjectEnabled() {
|
|
290
427
|
try {
|
|
291
428
|
const row = this.store
|
|
292
429
|
.prepare("SELECT value FROM kevin_settings WHERE key = 'cross_project_enabled'")
|
|
293
430
|
.get();
|
|
294
|
-
|
|
431
|
+
// BUG-002 — the column stores TEXT ('0'/'1'); the old numeric
|
|
432
|
+
// comparison `=== 1` could never match '1'.
|
|
433
|
+
return (row?.value ?? "0") === "1";
|
|
295
434
|
}
|
|
296
435
|
catch {
|
|
297
436
|
return false;
|
|
298
437
|
}
|
|
299
438
|
}
|
|
439
|
+
/**
|
|
440
|
+
* v0.4.0 (K4-012) — read a kevin_settings flag by key. Falls back to
|
|
441
|
+
* the caller-provided default when the key is missing or the table is
|
|
442
|
+
* unavailable (legacy DB without the settings table).
|
|
443
|
+
*/
|
|
444
|
+
getSetting(key, fallback = "0") {
|
|
445
|
+
try {
|
|
446
|
+
const row = this.store
|
|
447
|
+
.prepare("SELECT value FROM kevin_settings WHERE key = ?")
|
|
448
|
+
.get(key);
|
|
449
|
+
return row?.value ?? fallback;
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
return fallback;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
300
455
|
loadAll(scope, includeSuperseded = false) {
|
|
301
456
|
let sql = `
|
|
302
457
|
SELECT id, type, content, scope, relevance_score, source_tool, source_session,
|
|
303
458
|
metadata, created_at, updated_at, expires_at,
|
|
304
459
|
project_id, fingerprint, origin,
|
|
305
|
-
evidence_count, last_verified_at, status
|
|
460
|
+
evidence_count, last_verified_at, status`;
|
|
461
|
+
// v0.5.0 (K5-009/010) — 006-only columns, appended when present.
|
|
462
|
+
if (this.hasIgnoredColumn()) {
|
|
463
|
+
sql += ", ignored, superseded_by, feedback_positive, feedback_negative";
|
|
464
|
+
}
|
|
465
|
+
sql += `
|
|
306
466
|
FROM memories
|
|
307
467
|
WHERE (expires_at IS NULL OR expires_at > datetime('now'))`;
|
|
468
|
+
// v0.5.0 (K5-008 / plan §5.6) — ignored memories are excluded from
|
|
469
|
+
// retrieval (human verdict, D5-07). Guarded for pre-006 DBs.
|
|
470
|
+
if (this.hasIgnoredColumn()) {
|
|
471
|
+
sql += "\n AND ignored = 0";
|
|
472
|
+
}
|
|
308
473
|
if (!includeSuperseded) {
|
|
309
474
|
sql += "\n AND status = 'active'";
|
|
310
475
|
}
|
|
@@ -317,24 +482,32 @@ export class MemoryService {
|
|
|
317
482
|
return this.store.prepare(sql).all(...params);
|
|
318
483
|
}
|
|
319
484
|
queryRelevant(text, scope, includeSuperseded = false) {
|
|
320
|
-
const tokens = stripUnbalancedQuotes(text
|
|
321
|
-
.split(/\s+/)
|
|
322
|
-
.filter((t) => t.length > 0)
|
|
323
|
-
.map((t) => `"${t.replace(/"/g, '""')}"`);
|
|
485
|
+
const tokens = tokenizeQuery(stripUnbalancedQuotes(text));
|
|
324
486
|
if (tokens.length === 0)
|
|
325
487
|
return [];
|
|
326
|
-
const match = tokens
|
|
488
|
+
const match = toMatchClause(tokens, " OR ");
|
|
327
489
|
let sql = `
|
|
328
490
|
SELECT m.id, m.type, m.content, m.scope, m.relevance_score,
|
|
329
491
|
m.source_tool, m.source_session, m.metadata,
|
|
330
492
|
m.created_at, m.updated_at, m.expires_at,
|
|
331
493
|
m.project_id, m.fingerprint, m.origin,
|
|
332
|
-
m.evidence_count, m.last_verified_at, m.status
|
|
494
|
+
m.evidence_count, m.last_verified_at, m.status`;
|
|
495
|
+
// v0.5.0 (K5-009/010) — 006-only columns, appended when present.
|
|
496
|
+
if (this.hasIgnoredColumn()) {
|
|
497
|
+
sql +=
|
|
498
|
+
", m.ignored, m.superseded_by, m.feedback_positive, m.feedback_negative";
|
|
499
|
+
}
|
|
500
|
+
sql += `,
|
|
333
501
|
bm25(memories_fts) AS score
|
|
334
502
|
FROM memories_fts
|
|
335
503
|
JOIN memories m ON m.rowid = memories_fts.rowid
|
|
336
504
|
WHERE memories_fts MATCH ?
|
|
337
505
|
AND (m.expires_at IS NULL OR m.expires_at > datetime('now'))`;
|
|
506
|
+
// v0.5.0 (K5-008 / plan §5.6) — ignored memories are excluded from
|
|
507
|
+
// retrieval (human verdict, D5-07). Guarded for pre-006 DBs.
|
|
508
|
+
if (this.hasIgnoredColumn()) {
|
|
509
|
+
sql += "\n AND m.ignored = 0";
|
|
510
|
+
}
|
|
338
511
|
if (!includeSuperseded) {
|
|
339
512
|
sql += "\n AND m.status = 'active'";
|
|
340
513
|
}
|
|
@@ -354,6 +527,20 @@ export class MemoryService {
|
|
|
354
527
|
const charBudget = maxTokens * 4;
|
|
355
528
|
const scope = input.scope ?? "project";
|
|
356
529
|
const includeSuperseded = input.includeSuperseded === true;
|
|
530
|
+
// v0.5.0 (K5-008 / plan §5.6, D5-10) — one clock per call and one
|
|
531
|
+
// read of the opt-in determinism flag. Retrieval then becomes a
|
|
532
|
+
// pure function of database state: recency decay is frozen at 1.0
|
|
533
|
+
// and the relevance bump is skipped regardless of the `bump`
|
|
534
|
+
// argument. The column is TEXT; compare against the string.
|
|
535
|
+
const now = input.now ?? new Date();
|
|
536
|
+
const deterministic = this.getSetting("deterministic_retrieval", "0") === "1";
|
|
537
|
+
// v0.5.0 (K5-008 / plan §5.6, D5-10) — DATE_NOW sentinel: in
|
|
538
|
+
// deterministic mode the wall clock is never read; every query sees
|
|
539
|
+
// the same fixed future instant, so ordering is a pure function of
|
|
540
|
+
// database content.
|
|
541
|
+
const clockMs = deterministic
|
|
542
|
+
? new Date(DATE_NOW).getTime()
|
|
543
|
+
: now.getTime();
|
|
357
544
|
let candidates;
|
|
358
545
|
if (input.query && input.query.trim().length > 0) {
|
|
359
546
|
candidates = this.queryRelevant(input.query, scope, includeSuperseded);
|
|
@@ -376,7 +563,7 @@ export class MemoryService {
|
|
|
376
563
|
// v0.2.0 (K2-023) origin-aware rank: BM25 × origin-boost × recency-decay.
|
|
377
564
|
// Tie-breakers preserve the v0.1.x spirit (errors/patterns before
|
|
378
565
|
// context; newer before older when nothing else decides).
|
|
379
|
-
candidates.sort((a, b) => rankCompare(a, b));
|
|
566
|
+
candidates.sort((a, b) => rankCompare(a, b, clockMs, deterministic));
|
|
380
567
|
const result = [];
|
|
381
568
|
let used = 0;
|
|
382
569
|
for (const mem of candidates) {
|
|
@@ -386,7 +573,11 @@ export class MemoryService {
|
|
|
386
573
|
result.push(mem);
|
|
387
574
|
used += len;
|
|
388
575
|
}
|
|
389
|
-
|
|
576
|
+
// v0.5.0 (K5-008 / D5-10) — the bump is part of the non-determinism
|
|
577
|
+
// this release makes optional: in deterministic mode it is skipped
|
|
578
|
+
// entirely so repeated queries return identical ranks and leave
|
|
579
|
+
// every relevance_score untouched.
|
|
580
|
+
if (result.length > 0 && !deterministic && input.bump !== false) {
|
|
390
581
|
const bump = this.store.prepare("UPDATE memories SET relevance_score = MIN(?, relevance_score + ?) WHERE id = ?");
|
|
391
582
|
this.store.transaction(() => {
|
|
392
583
|
for (const m of result)
|
|
@@ -395,6 +586,20 @@ export class MemoryService {
|
|
|
395
586
|
}
|
|
396
587
|
return result;
|
|
397
588
|
}
|
|
589
|
+
/**
|
|
590
|
+
* v0.4.0 (BUG-016) — apply the K2-023 relevance bump to a fixed slice
|
|
591
|
+
* of ids, exactly once. Lets ContextInjector probe without mutating
|
|
592
|
+
* and still bump the slice it actually injects.
|
|
593
|
+
*/
|
|
594
|
+
bumpRelevance(ids) {
|
|
595
|
+
if (ids.length === 0)
|
|
596
|
+
return;
|
|
597
|
+
const bump = this.store.prepare("UPDATE memories SET relevance_score = MIN(?, relevance_score + ?) WHERE id = ?");
|
|
598
|
+
this.store.transaction(() => {
|
|
599
|
+
for (const id of ids)
|
|
600
|
+
bump.run(RELEVANCE_MAX, RELEVANCE_BUMP, id);
|
|
601
|
+
});
|
|
602
|
+
}
|
|
398
603
|
/**
|
|
399
604
|
* v0.3.0 (K3-004) — Promote an error memory to a causal pattern.
|
|
400
605
|
*
|
|
@@ -404,14 +609,29 @@ export class MemoryService {
|
|
|
404
609
|
* or null when the source error is not eligible (missing fingerprint,
|
|
405
610
|
* wrong type, or already promoted).
|
|
406
611
|
*/
|
|
407
|
-
|
|
612
|
+
/**
|
|
613
|
+
* v0.4.0 (K4-009) — returns `{ id, created }` so callers can tell a
|
|
614
|
+
* newly-created pattern from an idempotent refresh.
|
|
615
|
+
*/
|
|
616
|
+
promoteToPattern(errorId, evidenceCount, recurrenceCount = 0) {
|
|
408
617
|
const error = this.getById(errorId);
|
|
409
618
|
if (!error || error.type !== "error" || !error.fingerprint)
|
|
410
619
|
return null;
|
|
411
|
-
|
|
620
|
+
// v0.4.0 (K4-010) — two-sided confidence: recurrence demotes the
|
|
621
|
+
// pattern's confidence.
|
|
622
|
+
const confidence = computeConfidence(evidenceCount, recurrenceCount);
|
|
412
623
|
const now = new Date().toISOString();
|
|
413
624
|
const summary = error.content.split("\n")[0].slice(0, 200);
|
|
414
|
-
const
|
|
625
|
+
const base = `Causal pattern: ${summary}\n\nEvidence: ${evidenceCount} confirmed fix(es)\nConfidence: ${(confidence * 100).toFixed(0)}%\n\nOriginal: ${error.content.slice(0, 1000)}`;
|
|
626
|
+
// v0.4.0 (K4-014) — deterministic "Fixed by:" from the linked
|
|
627
|
+
// success call's args_summary (D4-07). The opt-in LLM phrasing
|
|
628
|
+
// (K4-015) runs later in CausalChain.onSessionIdle, never here and
|
|
629
|
+
// never on the failure hot path.
|
|
630
|
+
const fixLine = deterministicFixLine({
|
|
631
|
+
content: base,
|
|
632
|
+
fixArgs: error.fixArgs ?? null,
|
|
633
|
+
});
|
|
634
|
+
const content = fixLine ? `${base}\n${fixLine}` : base;
|
|
415
635
|
// v0.3.0 fix (bug #4) — idempotent promotion: the supersede model
|
|
416
636
|
// only covers decision/rule, so the old code inserted a duplicate
|
|
417
637
|
// pattern on every subsequent session.idle with a new fix. When an
|
|
@@ -424,23 +644,34 @@ export class MemoryService {
|
|
|
424
644
|
AND origin = 'causal' AND status = 'active'
|
|
425
645
|
ORDER BY created_at DESC LIMIT 1`)
|
|
426
646
|
.get(error.fingerprint);
|
|
647
|
+
let patternId;
|
|
427
648
|
if (existing) {
|
|
428
649
|
this.update(existing.id, { content, evidenceCount, lastVerifiedAt: now });
|
|
429
|
-
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
650
|
+
patternId = existing.id;
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
patternId = this.save({
|
|
654
|
+
type: "pattern",
|
|
655
|
+
content,
|
|
656
|
+
scope: "project",
|
|
657
|
+
origin: "causal",
|
|
658
|
+
sourceTool: error.sourceTool ?? undefined,
|
|
659
|
+
sourceSession: error.sourceSession ?? undefined,
|
|
660
|
+
fingerprint: error.fingerprint,
|
|
661
|
+
evidenceCount,
|
|
662
|
+
lastVerifiedAt: now,
|
|
663
|
+
status: "active",
|
|
664
|
+
projectId: error.projectId ?? undefined,
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
// v0.4.0 (K4-010) — persist the recurrence count on the pattern row
|
|
668
|
+
// so mapRow (and kevin_why) recompute the SAME demoted confidence.
|
|
669
|
+
// v0.4.0 (K4-014) — persist fix_args too: the pattern's "Fixed by:"
|
|
670
|
+
// raw material travels with the row for kevin_why/HITL (K4-016/020).
|
|
671
|
+
this.store
|
|
672
|
+
.prepare("UPDATE memories SET recurrence_count = ?, fix_args = ? WHERE id = ?")
|
|
673
|
+
.run(recurrenceCount, error.fixArgs ?? null, patternId);
|
|
674
|
+
return { id: patternId, created: !existing };
|
|
444
675
|
}
|
|
445
676
|
/**
|
|
446
677
|
* v0.2.0 (K2-026) — Feedback loop positive half (plan §B6.10 / D2-10).
|
|
@@ -541,9 +772,16 @@ export class MemoryService {
|
|
|
541
772
|
AND (error_fingerprint = ? OR fingerprint = ?)
|
|
542
773
|
AND (project_id IS ? OR (project_id IS NULL AND ? IS NULL))
|
|
543
774
|
AND (? IS NULL OR id <> ?)`);
|
|
775
|
+
const settledCheck = this.store.prepare(`SELECT 1 FROM kevin_injections
|
|
776
|
+
WHERE session_id = ? AND memory_id = ? AND outcome = 'ineffective'
|
|
777
|
+
LIMIT 1`);
|
|
544
778
|
const penalizeOne = this.store.prepare(`UPDATE memories
|
|
545
779
|
SET relevance_score = MAX(0, relevance_score - ?),
|
|
546
|
-
|
|
780
|
+
recurrence_count = recurrence_count + 1,
|
|
781
|
+
last_verified_at = datetime('now')
|
|
782
|
+
WHERE id = ?`);
|
|
783
|
+
const penalizeRelevanceOnly = this.store.prepare(`UPDATE memories
|
|
784
|
+
SET relevance_score = MAX(0, relevance_score - ?),
|
|
547
785
|
last_verified_at = datetime('now')
|
|
548
786
|
WHERE id = ?`);
|
|
549
787
|
let penalized = 0;
|
|
@@ -553,9 +791,35 @@ export class MemoryService {
|
|
|
553
791
|
const row = recurrenceCheck.get(sessionId, l.fingerprint, l.fingerprint, l.project_id, l.project_id, originCallId, originCallId);
|
|
554
792
|
const c = row?.c ?? 0;
|
|
555
793
|
if (c > 0) {
|
|
556
|
-
|
|
794
|
+
// v0.4.0 (K4-025) — no double-charge: when the
|
|
795
|
+
// session's injection of this memory was already
|
|
796
|
+
// settled `ineffective`, `InjectionLedger.settle`
|
|
797
|
+
// charged recurrence_count (K4-007) and this pass
|
|
798
|
+
// only applies the relevance penalty. The +1 charge
|
|
799
|
+
// below is the pre-ledger path (K4-011) for memories
|
|
800
|
+
// that were never injected this session.
|
|
801
|
+
const settled = settledCheck.get(sessionId, l.id);
|
|
802
|
+
if (settled) {
|
|
803
|
+
penalizeRelevanceOnly.run(RELEVANCE_PENALTY, l.id);
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
// v0.4.0 (K4-011) — recurrence is negative evidence:
|
|
807
|
+
// it bumps `recurrence_count`, NOT `evidence_count`
|
|
808
|
+
// (the old code counted recurrence as positive
|
|
809
|
+
// evidence). No `memories_superseded` increment here —
|
|
810
|
+
// supersede is only counted when a decision/rule is
|
|
811
|
+
// truly replaced (see save()).
|
|
812
|
+
penalizeOne.run(RELEVANCE_PENALTY, l.id);
|
|
813
|
+
// v0.4.0 (K4-025 / plan §5.1 rule 4, D4-06) — same
|
|
814
|
+
// recurrence-expels rule the settle enforces: at
|
|
815
|
+
// `recurrence_count >= 3` the error lesson is demoted
|
|
816
|
+
// to `status='stale'`.
|
|
817
|
+
this.store
|
|
818
|
+
.prepare(`UPDATE memories SET status = 'stale'
|
|
819
|
+
WHERE id = ? AND recurrence_count >= 3`)
|
|
820
|
+
.run(l.id);
|
|
821
|
+
}
|
|
557
822
|
penalized += 1;
|
|
558
|
-
this.metrics?.incr("memories_superseded", 1);
|
|
559
823
|
}
|
|
560
824
|
}
|
|
561
825
|
});
|
|
@@ -617,19 +881,22 @@ export function countSupersedeCandidates(store, type, fingerprint, projectId) {
|
|
|
617
881
|
.get(fingerprint, projectId, projectId);
|
|
618
882
|
return row?.c ?? 0;
|
|
619
883
|
}
|
|
620
|
-
function rankScore(mem) {
|
|
884
|
+
function rankScore(mem, nowMs, deterministic) {
|
|
621
885
|
// FTS5 bm25 returns a negative score (more negative = better match).
|
|
622
886
|
// For non-FTS rows (loadAll path), fall back to -relevance_score so
|
|
623
887
|
// higher-relevance memories also come first under the same sign convention.
|
|
624
888
|
const rawScore = mem.metadata?.score;
|
|
625
889
|
const base = typeof rawScore === "number" ? rawScore : -mem.relevanceScore;
|
|
626
|
-
const ageDays = Math.max(0, (
|
|
627
|
-
|
|
890
|
+
const ageDays = Math.max(0, (nowMs - sqliteUtcToMs(mem.createdAt)) / 86_400_000);
|
|
891
|
+
// v0.5.0 (K5-008 / plan §5.6, D5-10) — deterministic retrieval freezes
|
|
892
|
+
// the recency factor at 1.0 so ordering depends only on content
|
|
893
|
+
// relevance and origin boost, never on the wall clock.
|
|
894
|
+
const recencyDecay = deterministic ? 1 : RECENCY_DECAY_PER_DAY ** ageDays;
|
|
628
895
|
return base * originBoost(mem) * recencyDecay;
|
|
629
896
|
}
|
|
630
|
-
function rankCompare(a, b) {
|
|
631
|
-
const ra = rankScore(a);
|
|
632
|
-
const rb = rankScore(b);
|
|
897
|
+
function rankCompare(a, b, nowMs, deterministic) {
|
|
898
|
+
const ra = rankScore(a, nowMs, deterministic);
|
|
899
|
+
const rb = rankScore(b, nowMs, deterministic);
|
|
633
900
|
if (ra !== rb)
|
|
634
901
|
return ra - rb; // ascending: most negative (best) first
|
|
635
902
|
if (TYPE_PRIORITY[a.type] !== TYPE_PRIORITY[b.type]) {
|