@jmtrin/opencode-kevin 1.0.0 → 1.1.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 (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +24 -7
  3. package/dist/migrations/001_initial.sql +91 -91
  4. package/dist/migrations/002_indexes.sql +13 -13
  5. package/dist/migrations/003_v02_signal.sql +57 -57
  6. package/dist/migrations/004_v03_knowledge.sql +138 -138
  7. package/dist/migrations/005_v04_signal.sql +57 -57
  8. package/dist/migrations/006_v05_glassbox.sql +118 -118
  9. package/dist/migrations/007_v06_pull.sql +144 -144
  10. package/dist/migrations/012_v11_drift.sql +24 -0
  11. package/dist/plugin/Archiver.js +2 -17
  12. package/dist/plugin/CausalChain.js +32 -13
  13. package/dist/plugin/ConflictDetector.js +7 -29
  14. package/dist/plugin/Feedback.js +2 -19
  15. package/dist/plugin/HookLiveness.d.ts +1 -0
  16. package/dist/plugin/HookLiveness.js +11 -27
  17. package/dist/plugin/InjectionLedger.js +104 -57
  18. package/dist/plugin/Materializer.js +1 -88
  19. package/dist/plugin/MemoryService.d.ts +59 -1
  20. package/dist/plugin/MemoryService.js +13 -106
  21. package/dist/plugin/Migrate.js +5 -0
  22. package/dist/plugin/Retrospective.js +4 -0
  23. package/dist/plugin/ToolCallObserver.js +18 -5
  24. package/dist/plugin/columns.d.ts +11 -0
  25. package/dist/plugin/columns.js +54 -0
  26. package/dist/plugin/contract.d.ts +8 -0
  27. package/dist/plugin/contract.js +20 -5
  28. package/dist/plugin/index.d.ts +1 -1
  29. package/dist/plugin/index.js +24 -3
  30. package/dist/plugin/kevin_forget.d.ts +33 -0
  31. package/dist/plugin/kevin_forget.js +260 -0
  32. package/dist/plugin/kevin_why.js +1 -18
  33. package/dist/plugin/metrics.d.ts +1 -1
  34. package/dist/plugin/metrics.js +4 -0
  35. package/dist/plugin/query-tokenizer.js +56 -8
  36. package/dist/plugin/time-ms.d.ts +1 -0
  37. package/dist/plugin/time-ms.js +16 -0
  38. package/package.json +2 -1
@@ -1,3 +1,6 @@
1
+ import { readOriginCallId } from "./MemoryService.js";
2
+ import { hasColumn } from "./columns.js";
3
+ import { toMs } from "./time-ms.js";
1
4
  import { uuidv7 } from "./uuid.js";
2
5
  export class InjectionLedger {
3
6
  store;
@@ -11,11 +14,21 @@ export class InjectionLedger {
11
14
  * duplicates are expected only via the caller's per-session seen-set.
12
15
  */
13
16
  record(input) {
14
- this.store
15
- .prepare(`INSERT INTO kevin_injections
16
- (id, memory_id, fingerprint, session_id, hook, tokens, outcome)
17
- VALUES (?, ?, ?, ?, ?, ?, 'unmeasured')`)
18
- .run(uuidv7(), input.memoryId, input.fingerprint, input.sessionId, input.hook, input.tokens);
17
+ // v1.1.0 (K11-003 / plan §5.2, D11-01) — dual-write injected_at_ms when column exists
18
+ if (hasColumn(this.store, "kevin_injections", "injected_at_ms")) {
19
+ this.store
20
+ .prepare(`INSERT INTO kevin_injections
21
+ (id, memory_id, fingerprint, session_id, hook, tokens, outcome, injected_at_ms)
22
+ VALUES (?, ?, ?, ?, ?, ?, 'unmeasured', ?)`)
23
+ .run(uuidv7(), input.memoryId, input.fingerprint, input.sessionId, input.hook, input.tokens, Date.now());
24
+ }
25
+ else {
26
+ this.store
27
+ .prepare(`INSERT INTO kevin_injections
28
+ (id, memory_id, fingerprint, session_id, hook, tokens, outcome)
29
+ VALUES (?, ?, ?, ?, ?, ?, 'unmeasured')`)
30
+ .run(uuidv7(), input.memoryId, input.fingerprint, input.sessionId, input.hook, input.tokens);
31
+ }
19
32
  this.metrics?.incr("injections_total", 1);
20
33
  // v0.8.0 (K8-024 / plan §5.7) — shared-layer consumption is counted
21
34
  // separately so the audit can tell how much of the push channel
@@ -40,20 +53,24 @@ export class InjectionLedger {
40
53
  * recent injection").
41
54
  */
42
55
  settle(sessionId) {
43
- const injections = this.store
44
- .prepare(`SELECT id, memory_id, fingerprint, injected_at, outcome
45
- FROM kevin_injections
46
- WHERE session_id = ?
47
- ORDER BY injected_at ASC, id ASC`)
48
- .all(sessionId);
56
+ // v1.1.0 (K11-003 / plan §5.2, D11-01/D11-07) — ms-aware settle: readers
57
+ // prefer _ms and fall back to legacy string. Column probes are cached.
58
+ const hasInjMs = hasColumn(this.store, "kevin_injections", "injected_at_ms");
59
+ const hasToolMs = hasColumn(this.store, "tool_calls", "ts_ms");
60
+ const injections = (hasInjMs
61
+ ? this.store.prepare(`SELECT id, memory_id, fingerprint, injected_at, injected_at_ms, outcome
62
+ FROM kevin_injections
63
+ WHERE session_id = ?
64
+ ORDER BY injected_at ASC, id ASC`)
65
+ : this.store.prepare(`SELECT id, memory_id, fingerprint, injected_at, outcome
66
+ FROM kevin_injections
67
+ WHERE session_id = ?
68
+ ORDER BY injected_at ASC, id ASC`)).all(sessionId);
49
69
  for (const inj of injections) {
50
70
  // Same identity dimension CausalChain uses: the failing call's
51
71
  // `error_fingerprint` (stamped by Reflector) or the legacy
52
- // `fingerprint` hash. `ts` and `injected_at` are both
53
- // `datetime('now')` text lexicographic comparison is valid.
54
- // COUNT (not LIMIT 1): every failing call after the injection
55
- // is a recurrence — the charge must reach 3 so D4-06 expels
56
- // the lesson.
72
+ // `fingerprint` hash.
73
+ // v1.1.0 time comparison uses toMs helper (prefers _ms).
57
74
  //
58
75
  // BUG-003 — the exemption is now bounded to the lesson's OWN
59
76
  // creating call (memories.metadata.origin_call_id, stamped by
@@ -65,17 +82,50 @@ export class InjectionLedger {
65
82
  // precision. Memories without a tracked creating call (agent-
66
83
  // saved, test fixtures) get no exemption: only the `ts >=
67
84
  // injected_at` bound applies.
68
- const originCallId = readOriginCallId(this.store, inj.memory_id);
69
- const countRow = this.store
70
- .prepare(`SELECT COUNT(*) AS n FROM tool_calls
71
- WHERE session_id = ?
72
- AND success = 0
73
- AND COALESCE(error_fingerprint, fingerprint) = ?
74
- AND ts >= ?
75
- AND (? IS NULL OR id != ?)
76
- LIMIT 1`)
77
- .get(sessionId, inj.fingerprint, inj.injected_at, originCallId, originCallId);
78
- const n = countRow.n;
85
+ const metaRow = this.store
86
+ .prepare("SELECT metadata FROM memories WHERE id = ?")
87
+ .get(inj.memory_id);
88
+ const originCallId = readOriginCallId(metaRow?.metadata ?? null);
89
+ // v1.1.0 heuristic: when legacy string and _ms diverge by >2s
90
+ // (manual UPDATE of injected_at in tests), trust the string
91
+ // because the ms reflects wall time at record, not the pinned
92
+ // fixture time. Real rows differ by <1s (second truncation).
93
+ const rawInjMs = inj.injected_at_ms ?? null;
94
+ const stringMs = inj.injected_at
95
+ ? Date.parse(`${inj.injected_at.replace(" ", "T")}Z`)
96
+ : null;
97
+ const injectedMs = rawInjMs !== null &&
98
+ stringMs !== null &&
99
+ !Number.isNaN(stringMs) &&
100
+ Math.abs(rawInjMs - stringMs) > 2000
101
+ ? stringMs
102
+ : toMs(inj.injected_at, rawInjMs);
103
+ // Fetch candidate failing calls for this fingerprint and filter by ms
104
+ const failRows = (hasToolMs
105
+ ? this.store.prepare(`SELECT id, ts, ts_ms FROM tool_calls
106
+ WHERE session_id = ?
107
+ AND success = 0
108
+ AND COALESCE(error_fingerprint, fingerprint) = ?`)
109
+ : this.store.prepare(`SELECT id, ts FROM tool_calls
110
+ WHERE session_id = ?
111
+ AND success = 0
112
+ AND COALESCE(error_fingerprint, fingerprint) = ?`)).all(sessionId, inj.fingerprint);
113
+ let n = 0;
114
+ for (const r of failRows) {
115
+ if (originCallId !== null && r.id === originCallId)
116
+ continue;
117
+ const tsMs = toMs(r.ts, r.ts_ms ?? null);
118
+ if (injectedMs !== null && tsMs !== null) {
119
+ if (tsMs < injectedMs)
120
+ continue;
121
+ }
122
+ else {
123
+ // fallback to string comparison when either side missing (legacy)
124
+ if (r.ts < inj.injected_at)
125
+ continue;
126
+ }
127
+ n++;
128
+ }
79
129
  // v0.5.0 (K5-005 / plan §5.1, D5-01) — three-way settlement:
80
130
  // recurrences >= 1 → ineffective (existing side effects unchanged)
81
131
  // else fixes >= 1 → effective (a linked fix was OBSERVED)
@@ -97,7 +147,7 @@ export class InjectionLedger {
97
147
  OR ? > last_injected_at
98
148
  THEN ? ELSE last_injected_at END
99
149
  WHERE fingerprint = ? AND id = ?`)
100
- .run(countRow.n, inj.injected_at, inj.injected_at, inj.fingerprint, inj.memory_id);
150
+ .run(n, inj.injected_at, inj.injected_at, inj.fingerprint, inj.memory_id);
101
151
  // v0.4.0 (K4-025 / plan §5.1 rule 4, D4-06) — recurrence
102
152
  // expels: a fingerprint at `recurrence_count >= 3` is
103
153
  // demoted to `status='stale'` and never injected again
@@ -116,15 +166,31 @@ export class InjectionLedger {
116
166
  // `ts >= injected_at` bound and `session_id = ?` filter are
117
167
  // kept; there is no `origin_call_id` exemption for fixes —
118
168
  // a fix is not the creating call.
119
- const fixRow = this.store
120
- .prepare(`SELECT COUNT(*) AS m FROM tool_calls
121
- WHERE session_id = ?
122
- AND success = 1
123
- AND fix_for_fingerprint = ?
124
- AND ts >= ?
125
- LIMIT 1`)
126
- .get(sessionId, inj.fingerprint, inj.injected_at);
127
- if (fixRow.m >= 1) {
169
+ // v1.1.0 ms-aware: fetch and filter via toMs.
170
+ const fixCandidates = (hasToolMs
171
+ ? this.store.prepare(`SELECT ts, ts_ms FROM tool_calls
172
+ WHERE session_id = ?
173
+ AND success = 1
174
+ AND fix_for_fingerprint = ?`)
175
+ : this.store.prepare(`SELECT ts FROM tool_calls
176
+ WHERE session_id = ?
177
+ AND success = 1
178
+ AND fix_for_fingerprint = ?`)).all(sessionId, inj.fingerprint);
179
+ let hasFix = false;
180
+ for (const fr of fixCandidates) {
181
+ const tsMs = toMs(fr.ts, fr.ts_ms ?? null);
182
+ if (injectedMs !== null && tsMs !== null) {
183
+ if (tsMs >= injectedMs) {
184
+ hasFix = true;
185
+ break;
186
+ }
187
+ }
188
+ else if (fr.ts >= inj.injected_at) {
189
+ hasFix = true;
190
+ break;
191
+ }
192
+ }
193
+ if (hasFix) {
128
194
  this.store
129
195
  .prepare(`UPDATE kevin_injections SET outcome = 'effective'
130
196
  WHERE id = ?`)
@@ -226,23 +292,4 @@ export class InjectionLedger {
226
292
  return out;
227
293
  }
228
294
  }
229
- /**
230
- * BUG-003 — read `origin_call_id` (the failing tool_call that CREATED the
231
- * memory) from memories.metadata, mirroring the feedback loop's
232
- * `readOriginCallId` in MemoryService. Returns null when absent/malformed.
233
- */
234
- function readOriginCallId(store, memoryId) {
235
- const row = store
236
- .prepare("SELECT metadata FROM memories WHERE id = ?")
237
- .get(memoryId);
238
- if (!row?.metadata)
239
- return null;
240
- try {
241
- const parsed = JSON.parse(row.metadata);
242
- const id = parsed?.origin_call_id;
243
- return typeof id === "string" && id.length > 0 ? id : null;
244
- }
245
- catch {
246
- return null;
247
- }
248
- }
295
+ // v1.1.0 — readOriginCallId deduplicated: imported from MemoryService (K11-003/K11-013)
@@ -2,100 +2,13 @@ import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
3
  import { firstSentence } from "./Curator.js";
4
4
  import { normalize } from "./fingerprint.js";
5
+ import { STOP_WORDS } from "./query-tokenizer.js";
5
6
  export const SKILL_TOPIC = "project-knowledge";
6
7
  /** Hash-like noise: FNV-1a output is 16 lowercase hex chars; an 8+ hex
7
8
  * token is indistinguishable from a fingerprint prefix and must never
8
9
  * become a topic (D6-14). */
9
10
  const HEX_LIKE_RE = /^[0-9a-f]{8,}$/;
10
11
  const TOKEN_RE = /[^a-z0-9]+/;
11
- /** Conservative English function words; code-adjacent meaning-bearing
12
- * words (npm, ts2304, cargo...) are deliberately NOT stop-words. */
13
- const STOP_WORDS = new Set([
14
- "a",
15
- "about",
16
- "after",
17
- "again",
18
- "all",
19
- "also",
20
- "an",
21
- "and",
22
- "any",
23
- "are",
24
- "as",
25
- "at",
26
- "be",
27
- "been",
28
- "before",
29
- "being",
30
- "but",
31
- "by",
32
- "can",
33
- "could",
34
- "did",
35
- "do",
36
- "does",
37
- "for",
38
- "from",
39
- "had",
40
- "has",
41
- "have",
42
- "he",
43
- "her",
44
- "his",
45
- "if",
46
- "in",
47
- "into",
48
- "is",
49
- "it",
50
- "its",
51
- "may",
52
- "might",
53
- "more",
54
- "most",
55
- "must",
56
- "not",
57
- "of",
58
- "on",
59
- "one",
60
- "or",
61
- "our",
62
- "per",
63
- "shall",
64
- "she",
65
- "so",
66
- "than",
67
- "that",
68
- "the",
69
- "their",
70
- "them",
71
- "then",
72
- "there",
73
- "these",
74
- "they",
75
- "this",
76
- "those",
77
- "through",
78
- "to",
79
- "too",
80
- "under",
81
- "up",
82
- "us",
83
- "via",
84
- "was",
85
- "we",
86
- "were",
87
- "what",
88
- "when",
89
- "where",
90
- "which",
91
- "while",
92
- "who",
93
- "will",
94
- "with",
95
- "would",
96
- "you",
97
- "your",
98
- ]);
99
12
  /** Sanitize a memory type into a filesystem-safe topic prefix. */
100
13
  function sanitizeType(type) {
101
14
  const cleaned = type
@@ -160,8 +160,55 @@ export interface GetRelevantInput {
160
160
  */
161
161
  now?: Date;
162
162
  }
163
+ interface MemoryRow {
164
+ id: string;
165
+ type: MemoryType;
166
+ content: string;
167
+ scope: MemoryScope;
168
+ relevance_score: number;
169
+ source_tool: string | null;
170
+ source_session: string | null;
171
+ metadata: string | null;
172
+ created_at: string;
173
+ updated_at: string;
174
+ expires_at: string | null;
175
+ /** v0.2.0 columns — nullable for rows from pre-003 DBs. */
176
+ project_id?: string | null;
177
+ fingerprint?: string | null;
178
+ origin?: MemoryOrigin | null;
179
+ /** v0.3.0 */
180
+ evidence_count?: number;
181
+ last_verified_at?: string | null;
182
+ status?: string;
183
+ /** v0.4.0 */
184
+ recurrence_count?: number;
185
+ /** v0.4.0 */
186
+ fix_args?: string | null;
187
+ /** v0.5.0 (K5-009) */
188
+ ignored?: number;
189
+ /** v0.5.0 (K5-009) */
190
+ superseded_by?: string | null;
191
+ /** v0.5.0 (K5-010) */
192
+ feedback_positive?: number;
193
+ /** v0.5.0 (K5-010) */
194
+ feedback_negative?: number;
195
+ /** v0.6.0 (K6-011 / migration 007) */
196
+ curated?: number;
197
+ /** v0.6.0 (K6-011 / migration 007) */
198
+ curated_at?: string | null;
199
+ /** v0.6.0 (K6-011 / migration 007) — 1 = inferable, 0 = non_inferable,
200
+ * NULL = unknown. */
201
+ inferable?: number | null;
202
+ /** v0.7.0 (K7-008 / migration 008) — de-ranking penalty in [0, 0.5]. */
203
+ truth_penalty?: number | null;
204
+ /** v0.7.0 (K7-008 / migration 008) — first contradiction timestamp. */
205
+ contradicted_at?: string | null;
206
+ /** v0.8.0 (K8-018 / migration 009) — layer marker on the row. */
207
+ layer?: string | null;
208
+ }
163
209
  export declare const DATE_NOW = "2099-01-01T00:00:00.000Z";
164
210
  export declare function hasRepoIdColumn(store: Store): boolean;
211
+ export declare function mapRow(row: MemoryRow, score?: number): Memory;
165
212
  export declare class MemoryService {
166
213
  private readonly metrics;
167
214
  constructor(store: Store, metrics?: Metrics | null, repoId?: string | null);
@@ -169,7 +216,6 @@ export declare class MemoryService {
169
216
  private repoId;
170
217
  setRepoId(repoId: string | null): void;
171
218
  private hasRecurrenceColumn;
172
- private _hasRecurrenceColumn;
173
219
  private hasIgnoredColumn;
174
220
  private hasCuratedColumn;
175
221
  private hasTruthColumns;
@@ -290,6 +336,17 @@ export declare class MemoryService {
290
336
  countSupersedeCandidates(type: MemoryType, fingerprint: string | null | undefined, projectId: string | null): number;
291
337
  penalizeRecurringReflectors(sessionId: string): number;
292
338
  }
339
+ /**
340
+ * v0.3.0 fix — Extract `origin_call_id` from the memory metadata blob.
341
+ *
342
+ * Reflector stores the failing tool_call id in metadata.origin_call_id
343
+ * (when available) so the feedback loop can exclude the original call
344
+ * from the recurrence count. Returns null when metadata is absent,
345
+ * malformed, or lacks the field.
346
+ * // v1.1.0 (K11-003 / plan §5.5, D11-05) — single source for origin lookup;
347
+ * // InjectionLedger reuses this implementation (K11-013).
348
+ */
349
+ export declare function readOriginCallId(metadata: string | null): string | null;
293
350
  /**
294
351
  * v0.3.0 fix — Count active memories that would be superseded by a new
295
352
  * row with the given (type, fingerprint, projectId) tuple. Used by
@@ -300,3 +357,4 @@ export declare class MemoryService {
300
357
  * any other type.
301
358
  */
302
359
  export declare function countSupersedeCandidates(store: Store, type: MemoryType, fingerprint: string | null | undefined, projectId: string | null): number;
360
+ export {};
@@ -1,4 +1,5 @@
1
1
  import { deterministicFixLine } from "./LessonFixer.js";
2
+ import { hasCuratedColumn as columnsHasCurated, hasIgnoredColumn as columnsHasIgnored, hasLayerColumn as columnsHasLayer, hasRecurrenceColumn as columnsHasRecurrence, hasRepoIdColumn as columnsHasRepoId, hasTruthColumns as columnsHasTruth, } from "./columns.js";
2
3
  import { computeConfidence } from "./confidence.js";
3
4
  import { fingerprint as computeFingerprint } from "./fingerprint.js";
4
5
  import { classify } from "./inferability.js";
@@ -82,106 +83,21 @@ const MEMORY_ROW_SELECT = `id, type, content, scope, relevance_score, source_too
82
83
  metadata, created_at, updated_at, expires_at,
83
84
  project_id, fingerprint, origin,
84
85
  evidence_count, recurrence_count, last_verified_at, status, fix_args`;
85
- /**
86
- * v0.5.0 (K5-008/009 / plan §5.6) — cached per-store probe for the 006-only
87
- * `ignored` column (pre-006 DBs must not reference it). Shared by the
88
- * retrieval filter, the row SELECT, and the save() ignored stamp.
89
- * v0.6.0 (K6-001a) — positive-only caching: a successful probe is cached,
90
- * a failed probe is NOT. A Store migrated in place heals on the next call.
91
- */
92
- const ignoredColumnCache = new WeakMap();
86
+ // v1.1.0 (K11-011 / plan §5.5, D11-06) — probes delegate to columns registry
93
87
  function hasIgnoredColumn(store) {
94
- const cached = ignoredColumnCache.get(store);
95
- if (cached === true)
96
- return true;
97
- try {
98
- store.prepare("SELECT ignored FROM memories LIMIT 1").get();
99
- ignoredColumnCache.set(store, true);
100
- return true;
101
- }
102
- catch {
103
- return false;
104
- }
88
+ return columnsHasIgnored(store);
105
89
  }
106
- /**
107
- * v0.6.0 (K6-011 / plan §5.4) — cached per-store probe for the 007-only
108
- * `curated` column (pre-007 DBs must not reference it). Same positive-only
109
- * caching discipline as `ignored` (K6-001a): a successful probe is cached,
110
- * a failed probe is NOT.
111
- */
112
- const curatedColumnCache = new WeakMap();
113
90
  function hasCuratedColumn(store) {
114
- const cached = curatedColumnCache.get(store);
115
- if (cached === true)
116
- return true;
117
- try {
118
- store.prepare("SELECT curated FROM memories LIMIT 1").get();
119
- curatedColumnCache.set(store, true);
120
- return true;
121
- }
122
- catch {
123
- return false;
124
- }
91
+ return columnsHasCurated(store);
125
92
  }
126
- /**
127
- * v0.7.0 (K7-008 / migration 008) — cached per-store probe for the 008-only
128
- * `truth_penalty` column (pre-008 DBs must not reference it). Same
129
- * positive-only caching discipline as `curated` (K6-001a).
130
- */
131
- const truthColumnsCache = new WeakMap();
132
93
  function hasTruthColumns(store) {
133
- const cached = truthColumnsCache.get(store);
134
- if (cached === true)
135
- return true;
136
- try {
137
- store.prepare("SELECT truth_penalty FROM memories LIMIT 1").get();
138
- truthColumnsCache.set(store, true);
139
- return true;
140
- }
141
- catch {
142
- return false;
143
- }
94
+ return columnsHasTruth(store);
144
95
  }
145
- /**
146
- * v0.8.0 (K8-007 / plan §5.7) — cached per-store probe for the 009-only
147
- * `repo_id` column (pre-009 DBs must not reference it). Same positive-only
148
- * caching discipline as `ignored` (K6-001a): a successful probe is cached,
149
- * a failed probe is NOT. Exported so `kevin_audit`'s rollups scope on the
150
- * same column.
151
- */
152
- const repoIdColumnCache = new WeakMap();
153
96
  export function hasRepoIdColumn(store) {
154
- const cached = repoIdColumnCache.get(store);
155
- if (cached === true)
156
- return true;
157
- try {
158
- store.prepare("SELECT repo_id FROM memories LIMIT 1").get();
159
- repoIdColumnCache.set(store, true);
160
- return true;
161
- }
162
- catch {
163
- return false;
164
- }
97
+ return columnsHasRepoId(store);
165
98
  }
166
- /**
167
- * v0.8.0 (K8-018 / plan §5.2) — cached per-store probe for the 009-only
168
- * `layer` column (pre-009 DBs must not reference it). Same positive-only
169
- * caching discipline: pre-009 databases have no shared layer, so the
170
- * immutability refusal in `update()` is skipped there entirely.
171
- */
172
- const layerColumnCache = new WeakMap();
173
99
  function hasLayerColumn(store) {
174
- const cached = layerColumnCache.get(store);
175
- if (cached === true)
176
- return true;
177
- try {
178
- store.prepare("SELECT layer FROM memories LIMIT 1").get();
179
- layerColumnCache.set(store, true);
180
- return true;
181
- }
182
- catch {
183
- return false;
184
- }
100
+ return columnsHasLayer(store);
185
101
  }
186
102
  /**
187
103
  * v0.5.0 (K5-009 / plan §5.3) — the 006-only columns are appended when the
@@ -204,7 +120,7 @@ function rowSelect(store) {
204
120
  : withCurated;
205
121
  return hasLayerColumn(store) ? `${withTruth}, layer` : withTruth;
206
122
  }
207
- function mapRow(row, score) {
123
+ export function mapRow(row, score) {
208
124
  const mem = {
209
125
  id: row.id,
210
126
  type: row.type,
@@ -320,21 +236,10 @@ export class MemoryService {
320
236
  }
321
237
  // v0.4.0 (BUG-008) — cached column probe for pre-005 DBs (which lack
322
238
  // `recurrence_count`); save() must not reference the column there.
239
+ // v1.1.0 (K11-011) — delegates to columns registry
323
240
  hasRecurrenceColumn() {
324
- if (this._hasRecurrenceColumn === undefined) {
325
- try {
326
- this.store
327
- .prepare("SELECT recurrence_count FROM memories LIMIT 1")
328
- .get();
329
- this._hasRecurrenceColumn = true;
330
- }
331
- catch {
332
- this._hasRecurrenceColumn = false;
333
- }
334
- }
335
- return this._hasRecurrenceColumn;
241
+ return columnsHasRecurrence(this.store);
336
242
  }
337
- _hasRecurrenceColumn;
338
243
  // v0.5.0 (K5-008 / plan §5.6) — cached column probe for pre-006 DBs
339
244
  // (which lack `ignored`); the retrieval filter must not reference the
340
245
  // column there.
@@ -1246,8 +1151,10 @@ function originBoost(mem) {
1246
1151
  * (when available) so the feedback loop can exclude the original call
1247
1152
  * from the recurrence count. Returns null when metadata is absent,
1248
1153
  * malformed, or lacks the field.
1154
+ * // v1.1.0 (K11-003 / plan §5.5, D11-05) — single source for origin lookup;
1155
+ * // InjectionLedger reuses this implementation (K11-013).
1249
1156
  */
1250
- function readOriginCallId(metadata) {
1157
+ export function readOriginCallId(metadata) {
1251
1158
  if (!metadata)
1252
1159
  return null;
1253
1160
  try {
@@ -247,6 +247,11 @@ export class Migrate {
247
247
  applied: pending.map((m) => m.version),
248
248
  };
249
249
  }
250
+ // v1.1.0 (K11-015) — lexicographic ordering is valid through "999" because
251
+ // versions are zero-padded 3-digit strings ("001" … "999"). Any future
252
+ // migration beyond 999 must use a 4-digit prefix and this comparison must
253
+ // become numeric (parseInt). Until then, string > works and keeps the
254
+ // migration idempotency simple (plan §5.5, D11-??).
250
255
  listPending(current) {
251
256
  let files = [];
252
257
  try {
@@ -74,6 +74,10 @@ export const METRIC_KEY_LABELS = {
74
74
  dispose_misses_total: "Misses de dispose",
75
75
  contract_digest_changes: "Cambios de digest de contrato",
76
76
  bench_runs_total: "Ejecuciones de benchmark (totales)",
77
+ // v1.1.0 (K11-001 / plan §4) — drift metrics; labels required by BUG-014 regression.
78
+ bench_regression_failures: "Fallos de regresion de benchmark",
79
+ forget_requests_total: "Solicitudes de olvido totales",
80
+ forget_tombstones_published: "Tombstones publicados por olvido",
77
81
  };
78
82
  function originLabel(origin) {
79
83
  if (origin === "reflector")
@@ -1,3 +1,4 @@
1
+ import { hasColumn } from "./columns.js";
1
2
  import { fingerprint as computeFingerprint } from "./fingerprint.js";
2
3
  import { redactPaths, stripPrivate } from "./redact.js";
3
4
  import { uuidv7 } from "./uuid.js";
@@ -54,11 +55,23 @@ export class ToolCallObserver {
54
55
  return;
55
56
  }
56
57
  }
57
- this.store
58
- .prepare(`INSERT INTO tool_calls
59
- (id, session_id, ts, tool, args_summary, success, duration_ms, agent, error_type, metadata, project_id, fingerprint)
60
- VALUES (?, ?, datetime('now'), ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
61
- .run(input.callID ?? uuidv7(), sessionId, input.tool, argsSummary, success, durationMs, agent, errorType, metadata, projectId, fp);
58
+ // v1.1.0 (K11-002 / plan §5.2, D11-01/D11-07) — dual-write: legacy `ts`
59
+ // stays (`datetime('now')`), new `ts_ms` holds Date.now() when the
60
+ // column exists. Probe is cached via columns registry (K11-011).
61
+ if (hasColumn(this.store, "tool_calls", "ts_ms")) {
62
+ this.store
63
+ .prepare(`INSERT INTO tool_calls
64
+ (id, session_id, ts, ts_ms, tool, args_summary, success, duration_ms, agent, error_type, metadata, project_id, fingerprint)
65
+ VALUES (?, ?, datetime('now'), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
66
+ .run(input.callID ?? uuidv7(), sessionId, Date.now(), input.tool, argsSummary, success, durationMs, agent, errorType, metadata, projectId, fp);
67
+ }
68
+ else {
69
+ this.store
70
+ .prepare(`INSERT INTO tool_calls
71
+ (id, session_id, ts, tool, args_summary, success, duration_ms, agent, error_type, metadata, project_id, fingerprint)
72
+ VALUES (?, ?, datetime('now'), ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
73
+ .run(input.callID ?? uuidv7(), sessionId, input.tool, argsSummary, success, durationMs, agent, errorType, metadata, projectId, fp);
74
+ }
62
75
  }
63
76
  isDedupEnabled() {
64
77
  try {
@@ -0,0 +1,11 @@
1
+ import type { Store } from "./Store.js";
2
+ export declare function hasColumn(store: Store, table: string, column: string): boolean;
3
+ export declare function hasIgnoredColumn(store: Store): boolean;
4
+ export declare function hasCuratedColumn(store: Store): boolean;
5
+ export declare function hasTruthColumns(store: Store): boolean;
6
+ export declare function hasRepoIdColumn(store: Store): boolean;
7
+ export declare function hasLayerColumn(store: Store): boolean;
8
+ export declare function hasRecurrenceColumn(store: Store): boolean;
9
+ export declare function hasArchivedColumn(store: Store): boolean;
10
+ export declare function hasFeedbackColumns(store: Store): boolean;
11
+ export declare function hasFeedbackTable(store: Store): boolean;