@jmtrin/kevin-core 1.3.0 → 1.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/LICENSE +21 -0
- package/README.md +667 -0
- package/dist/ArtifactWriter.js +1 -1
- package/dist/InjectionLedger.d.ts +5 -2
- package/dist/InjectionLedger.js +132 -113
- package/dist/Materializer.d.ts +5 -0
- package/dist/Materializer.js +11 -0
- package/dist/MemoryService.js +93 -91
- package/dist/RepoIdentity.js +1 -1
- package/dist/Retrospective.js +10 -0
- package/dist/Store.d.ts +1 -0
- package/dist/Store.js +15 -2
- package/dist/contract.d.ts +8 -0
- package/dist/contract.js +31 -2
- package/dist/import-host.d.ts +41 -0
- package/dist/import-host.js +286 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +12 -1
- package/dist/kevin_audit.d.ts +31 -0
- package/dist/kevin_audit.js +94 -0
- package/dist/metrics.d.ts +1 -1
- package/dist/metrics.js +10 -0
- package/dist/mif.d.ts +32 -0
- package/dist/mif.js +112 -0
- package/dist/migrations/013_v14_bridge.sql +46 -0
- package/dist/perf.d.ts +1 -1
- package/dist/perf.js +2 -0
- package/dist/skills-emit.d.ts +38 -0
- package/dist/skills-emit.js +421 -0
- package/dist/skills-validate.d.ts +7 -0
- package/dist/skills-validate.js +236 -0
- package/dist/sqlite-adapter.js +1 -1
- package/package.json +1 -1
|
@@ -21,7 +21,8 @@ import type { Metrics } from "./metrics.js";
|
|
|
21
21
|
* them `effective` forever and inflate `precision_rate`. The caller
|
|
22
22
|
* (ContextInjector.recordInjections) skips them.
|
|
23
23
|
*/
|
|
24
|
-
export type InjectionHook = "pre_prompt" | "compacting";
|
|
24
|
+
export type InjectionHook = "pre_prompt" | "compacting" | "pull_mcp";
|
|
25
|
+
export type InjectionChannel = "plugin" | "mcp";
|
|
25
26
|
export type InjectionOutcome = "unmeasured" | "effective" | "ineffective" | "inconclusive";
|
|
26
27
|
export interface InjectionRecordInput {
|
|
27
28
|
memoryId: string;
|
|
@@ -53,8 +54,10 @@ export declare class InjectionLedger {
|
|
|
53
54
|
/**
|
|
54
55
|
* Records one injected memory. Idempotent at the row level (UUID PK);
|
|
55
56
|
* duplicates are expected only via the caller's per-session seen-set.
|
|
57
|
+
* v1.4.0 (K14-004 / plan §4.3, D14-04/D14-05) — optional channel, defaults to
|
|
58
|
+
* "plugin"; written only when the column exists so pre-013 DBs keep working.
|
|
56
59
|
*/
|
|
57
|
-
record(input: InjectionRecordInput): void;
|
|
60
|
+
record(input: InjectionRecordInput, channel?: InjectionChannel): void;
|
|
58
61
|
/**
|
|
59
62
|
* Settles every unmeasured injection of the session: a fingerprint that
|
|
60
63
|
* failed again (as a failing tool_call) after the injection is
|
package/dist/InjectionLedger.js
CHANGED
|
@@ -12,16 +12,33 @@ export class InjectionLedger {
|
|
|
12
12
|
/**
|
|
13
13
|
* Records one injected memory. Idempotent at the row level (UUID PK);
|
|
14
14
|
* duplicates are expected only via the caller's per-session seen-set.
|
|
15
|
+
* v1.4.0 (K14-004 / plan §4.3, D14-04/D14-05) — optional channel, defaults to
|
|
16
|
+
* "plugin"; written only when the column exists so pre-013 DBs keep working.
|
|
15
17
|
*/
|
|
16
|
-
record(input) {
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
record(input, channel = "plugin") {
|
|
19
|
+
const hasMs = hasColumn(this.store, "kevin_injections", "injected_at_ms");
|
|
20
|
+
const hasChannel = hasColumn(this.store, "kevin_injections", "channel");
|
|
21
|
+
if (hasMs && hasChannel) {
|
|
22
|
+
this.store
|
|
23
|
+
.prepare(`INSERT INTO kevin_injections
|
|
24
|
+
(id, memory_id, fingerprint, session_id, hook, tokens, outcome, injected_at_ms, channel)
|
|
25
|
+
VALUES (?, ?, ?, ?, ?, ?, 'unmeasured', ?, ?)`)
|
|
26
|
+
.run(uuidv7(), input.memoryId, input.fingerprint, input.sessionId, input.hook, input.tokens, Date.now(), channel);
|
|
27
|
+
}
|
|
28
|
+
else if (hasMs) {
|
|
19
29
|
this.store
|
|
20
30
|
.prepare(`INSERT INTO kevin_injections
|
|
21
31
|
(id, memory_id, fingerprint, session_id, hook, tokens, outcome, injected_at_ms)
|
|
22
32
|
VALUES (?, ?, ?, ?, ?, ?, 'unmeasured', ?)`)
|
|
23
33
|
.run(uuidv7(), input.memoryId, input.fingerprint, input.sessionId, input.hook, input.tokens, Date.now());
|
|
24
34
|
}
|
|
35
|
+
else if (hasChannel) {
|
|
36
|
+
this.store
|
|
37
|
+
.prepare(`INSERT INTO kevin_injections
|
|
38
|
+
(id, memory_id, fingerprint, session_id, hook, tokens, outcome, channel)
|
|
39
|
+
VALUES (?, ?, ?, ?, ?, ?, 'unmeasured', ?)`)
|
|
40
|
+
.run(uuidv7(), input.memoryId, input.fingerprint, input.sessionId, input.hook, input.tokens, channel);
|
|
41
|
+
}
|
|
25
42
|
else {
|
|
26
43
|
this.store
|
|
27
44
|
.prepare(`INSERT INTO kevin_injections
|
|
@@ -66,146 +83,148 @@ export class InjectionLedger {
|
|
|
66
83
|
FROM kevin_injections
|
|
67
84
|
WHERE session_id = ?
|
|
68
85
|
ORDER BY injected_at ASC, id ASC`)).all(sessionId);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
86
|
+
this.store.transaction(() => {
|
|
87
|
+
for (const inj of injections) {
|
|
88
|
+
// Same identity dimension CausalChain uses: the failing call's
|
|
89
|
+
// `error_fingerprint` (stamped by Reflector) or the legacy
|
|
90
|
+
// `fingerprint` hash.
|
|
91
|
+
// v1.1.0 — time comparison uses toMs helper (prefers _ms).
|
|
92
|
+
//
|
|
93
|
+
// BUG-003 — the exemption is now bounded to the lesson's OWN
|
|
94
|
+
// creating call (memories.metadata.origin_call_id, stamped by
|
|
95
|
+
// Reflector). The old code excluded the session's FIRST failing
|
|
96
|
+
// call of the fingerprint, which is only the creating call when
|
|
97
|
+
// the lesson was born in THIS session; a lesson created in an
|
|
98
|
+
// earlier session had its first in-session failure (a genuine
|
|
99
|
+
// post-injection recurrence) wrongly exempted, inflating
|
|
100
|
+
// precision. Memories without a tracked creating call (agent-
|
|
101
|
+
// saved, test fixtures) get no exemption: only the `ts >=
|
|
102
|
+
// injected_at` bound applies.
|
|
103
|
+
const metaRow = this.store
|
|
104
|
+
.prepare("SELECT metadata FROM memories WHERE id = ?")
|
|
105
|
+
.get(inj.memory_id);
|
|
106
|
+
const originCallId = readOriginCallId(metaRow?.metadata ?? null);
|
|
107
|
+
// v1.1.0 — heuristic: when legacy string and _ms diverge by >2s
|
|
108
|
+
// (manual UPDATE of injected_at in tests), trust the string
|
|
109
|
+
// because the ms reflects wall time at record, not the pinned
|
|
110
|
+
// fixture time. Real rows differ by <1s (second truncation).
|
|
111
|
+
const rawInjMs = inj.injected_at_ms ?? null;
|
|
112
|
+
const stringMs = inj.injected_at
|
|
113
|
+
? Date.parse(`${inj.injected_at.replace(" ", "T")}Z`)
|
|
114
|
+
: null;
|
|
115
|
+
const injectedMs = rawInjMs !== null &&
|
|
116
|
+
stringMs !== null &&
|
|
117
|
+
!Number.isNaN(stringMs) &&
|
|
118
|
+
Math.abs(rawInjMs - stringMs) > 2000
|
|
119
|
+
? stringMs
|
|
120
|
+
: toMs(inj.injected_at, rawInjMs);
|
|
121
|
+
// Fetch candidate failing calls for this fingerprint and filter by ms
|
|
122
|
+
const failRows = (hasToolMs
|
|
123
|
+
? this.store.prepare(`SELECT id, ts, ts_ms FROM tool_calls
|
|
106
124
|
WHERE session_id = ?
|
|
107
125
|
AND success = 0
|
|
108
126
|
AND COALESCE(error_fingerprint, fingerprint) = ?`)
|
|
109
|
-
|
|
127
|
+
: this.store.prepare(`SELECT id, ts FROM tool_calls
|
|
110
128
|
WHERE session_id = ?
|
|
111
129
|
AND success = 0
|
|
112
130
|
AND COALESCE(error_fingerprint, fingerprint) = ?`)).all(sessionId, inj.fingerprint);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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)
|
|
131
|
+
let n = 0;
|
|
132
|
+
for (const r of failRows) {
|
|
133
|
+
if (originCallId !== null && r.id === originCallId)
|
|
125
134
|
continue;
|
|
135
|
+
const tsMs = toMs(r.ts, r.ts_ms ?? null);
|
|
136
|
+
if (injectedMs !== null && tsMs !== null) {
|
|
137
|
+
if (tsMs < injectedMs)
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
// fallback to string comparison when either side missing (legacy)
|
|
142
|
+
if (r.ts < inj.injected_at)
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
n++;
|
|
126
146
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
this.store
|
|
137
|
-
.prepare(`UPDATE kevin_injections SET outcome = 'ineffective'
|
|
147
|
+
// v0.5.0 (K5-005 / plan §5.1, D5-01) — three-way settlement:
|
|
148
|
+
// recurrences >= 1 → ineffective (existing side effects unchanged)
|
|
149
|
+
// else fixes >= 1 → effective (a linked fix was OBSERVED)
|
|
150
|
+
// else → inconclusive (new majority bucket; excluded
|
|
151
|
+
// from the precision denominator)
|
|
152
|
+
if (n >= 1) {
|
|
153
|
+
if (inj.outcome === "unmeasured") {
|
|
154
|
+
this.store
|
|
155
|
+
.prepare(`UPDATE kevin_injections SET outcome = 'ineffective'
|
|
138
156
|
WHERE id = ?`)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
157
|
+
.run(inj.id);
|
|
158
|
+
this.metrics?.incr("injections_ineffective", 1);
|
|
159
|
+
}
|
|
160
|
+
this.store
|
|
161
|
+
.prepare(`UPDATE memories
|
|
144
162
|
SET recurrence_count = MAX(recurrence_count, ?),
|
|
145
163
|
last_injected_at = CASE
|
|
146
164
|
WHEN last_injected_at IS NULL
|
|
147
165
|
OR ? > last_injected_at
|
|
148
166
|
THEN ? ELSE last_injected_at END
|
|
149
167
|
WHERE fingerprint = ? AND id = ?`)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
168
|
+
.run(n, inj.injected_at, inj.injected_at, inj.fingerprint, inj.memory_id);
|
|
169
|
+
// v0.4.0 (K4-025 / plan §5.1 rule 4, D4-06) — recurrence
|
|
170
|
+
// expels: a fingerprint at `recurrence_count >= 3` is
|
|
171
|
+
// demoted to `status='stale'` and never injected again
|
|
172
|
+
// (only a new causal pattern — from a linked fix —
|
|
173
|
+
// re-admits the lesson, not the stale error row).
|
|
174
|
+
this.store
|
|
175
|
+
.prepare(`UPDATE memories SET status = 'stale'
|
|
158
176
|
WHERE id = ? AND recurrence_count >= 3`)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
177
|
+
.run(inj.memory_id);
|
|
178
|
+
}
|
|
179
|
+
else if (inj.outcome === "unmeasured") {
|
|
180
|
+
// Mirror of the recurrence predicate with the success flag
|
|
181
|
+
// inverted and the fingerprint matched on
|
|
182
|
+
// `fix_for_fingerprint` (populated by CausalChain.onSuccess,
|
|
183
|
+
// indexed by idx_tool_calls_fix_fp since migration 004). The
|
|
184
|
+
// `ts >= injected_at` bound and `session_id = ?` filter are
|
|
185
|
+
// kept; there is no `origin_call_id` exemption for fixes —
|
|
186
|
+
// a fix is not the creating call.
|
|
187
|
+
// v1.1.0 — ms-aware: fetch and filter via toMs.
|
|
188
|
+
const fixCandidates = (hasToolMs
|
|
189
|
+
? this.store.prepare(`SELECT ts, ts_ms FROM tool_calls
|
|
172
190
|
WHERE session_id = ?
|
|
173
191
|
AND success = 1
|
|
174
192
|
AND fix_for_fingerprint = ?`)
|
|
175
|
-
|
|
193
|
+
: this.store.prepare(`SELECT ts FROM tool_calls
|
|
176
194
|
WHERE session_id = ?
|
|
177
195
|
AND success = 1
|
|
178
196
|
AND fix_for_fingerprint = ?`)).all(sessionId, inj.fingerprint);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
197
|
+
let hasFix = false;
|
|
198
|
+
for (const fr of fixCandidates) {
|
|
199
|
+
const tsMs = toMs(fr.ts, fr.ts_ms ?? null);
|
|
200
|
+
if (injectedMs !== null && tsMs !== null) {
|
|
201
|
+
if (tsMs >= injectedMs) {
|
|
202
|
+
hasFix = true;
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else if (fr.ts >= inj.injected_at) {
|
|
184
207
|
hasFix = true;
|
|
185
208
|
break;
|
|
186
209
|
}
|
|
187
210
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
if (hasFix) {
|
|
194
|
-
this.store
|
|
195
|
-
.prepare(`UPDATE kevin_injections SET outcome = 'effective'
|
|
211
|
+
if (hasFix) {
|
|
212
|
+
this.store
|
|
213
|
+
.prepare(`UPDATE kevin_injections SET outcome = 'effective'
|
|
196
214
|
WHERE id = ?`)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
215
|
+
.run(inj.id);
|
|
216
|
+
this.metrics?.incr("injections_effective", 1);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
this.store
|
|
220
|
+
.prepare(`UPDATE kevin_injections SET outcome = 'inconclusive'
|
|
203
221
|
WHERE id = ?`)
|
|
204
|
-
|
|
205
|
-
|
|
222
|
+
.run(inj.id);
|
|
223
|
+
this.metrics?.incr("injections_inconclusive", 1);
|
|
224
|
+
}
|
|
206
225
|
}
|
|
207
226
|
}
|
|
208
|
-
}
|
|
227
|
+
});
|
|
209
228
|
}
|
|
210
229
|
/**
|
|
211
230
|
* Per-fingerprint failing tool-call counts for the session. Feeds
|
package/dist/Materializer.d.ts
CHANGED
|
@@ -59,6 +59,11 @@ export declare class Materializer {
|
|
|
59
59
|
hasNativeRegistration(surface: NativeSurface): boolean;
|
|
60
60
|
/** The curated, active memories — the knowledge the pull channels publish. */
|
|
61
61
|
private curatedRows;
|
|
62
|
+
/** K15-003 — topic bundles for skill emit (deterministic) */
|
|
63
|
+
topicBundles(): Array<{
|
|
64
|
+
topic: string;
|
|
65
|
+
content: string;
|
|
66
|
+
}>;
|
|
62
67
|
/** The rendered skill body over all curated memories; "" when empty. */
|
|
63
68
|
skillBody(): string;
|
|
64
69
|
/** Group rows by type, deriving each type's topic from its own content. */
|
package/dist/Materializer.js
CHANGED
|
@@ -86,6 +86,17 @@ export class Materializer {
|
|
|
86
86
|
WHERE status = 'active' AND curated = 1`)
|
|
87
87
|
.all();
|
|
88
88
|
}
|
|
89
|
+
/** K15-003 — topic bundles for skill emit (deterministic) */
|
|
90
|
+
topicBundles() {
|
|
91
|
+
const rows = this.curatedRows();
|
|
92
|
+
const bundles = [];
|
|
93
|
+
for (const g of this.groupByTopic(rows)) {
|
|
94
|
+
const body = renderRows(g.rows);
|
|
95
|
+
if (body !== "")
|
|
96
|
+
bundles.push({ topic: g.topic, content: body });
|
|
97
|
+
}
|
|
98
|
+
return bundles;
|
|
99
|
+
}
|
|
89
100
|
/** The rendered skill body over all curated memories; "" when empty. */
|
|
90
101
|
skillBody() {
|
|
91
102
|
return renderRows(this.curatedRows());
|
package/dist/MemoryService.js
CHANGED
|
@@ -301,99 +301,101 @@ export class MemoryService {
|
|
|
301
301
|
// supersession a navigable audit trail. Guarded: pre-006 DBs lack
|
|
302
302
|
// the column (same migration as `ignored`).
|
|
303
303
|
const supersedableTypes = ["decision", "rule"];
|
|
304
|
-
if (fp !== null && supersedableTypes.includes(input.type)) {
|
|
305
|
-
const withSupersededBy = this.hasIgnoredColumn();
|
|
306
|
-
const setClause = withSupersededBy
|
|
307
|
-
? "SET status = 'superseded', superseded_by = ?, updated_at = datetime('now')"
|
|
308
|
-
: "SET status = 'superseded', updated_at = datetime('now')";
|
|
309
|
-
// v0.8.0 (K8-007 / plan §5.7) — once the 009 column exists and an
|
|
310
|
-
// identity is resolved, supersession is scoped on repo_id (NULL
|
|
311
|
-
// rows are global); project_id stays as the pre-009 scope.
|
|
312
|
-
const scopedOnRepoId = this.hasRepoIdColumn() && this.repoId !== null;
|
|
313
|
-
const whereScope = scopedOnRepoId
|
|
314
|
-
? "AND (repo_id IS ? OR repo_id IS NULL)"
|
|
315
|
-
: "AND (project_id IS ? OR (project_id IS NULL AND ? IS NULL))";
|
|
316
|
-
const scopeParams = scopedOnRepoId
|
|
317
|
-
? [this.repoId]
|
|
318
|
-
: [projectId, projectId];
|
|
319
|
-
this.store
|
|
320
|
-
.prepare(`UPDATE memories
|
|
321
|
-
${setClause}
|
|
322
|
-
WHERE fingerprint = ?
|
|
323
|
-
AND type = ?
|
|
324
|
-
AND status = 'active'
|
|
325
|
-
${whereScope}`)
|
|
326
|
-
.run(...(withSupersededBy
|
|
327
|
-
? [id, fp, input.type, ...scopeParams]
|
|
328
|
-
: [fp, input.type, ...scopeParams]));
|
|
329
|
-
const after = this.store.prepare("SELECT changes() AS n").get();
|
|
330
|
-
if (after.n > 0) {
|
|
331
|
-
this.metrics?.incr("memories_superseded", 1);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
304
|
try {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
305
|
+
this.store.transaction(() => {
|
|
306
|
+
if (fp !== null && supersedableTypes.includes(input.type)) {
|
|
307
|
+
const withSupersededBy = this.hasIgnoredColumn();
|
|
308
|
+
const setClause = withSupersededBy
|
|
309
|
+
? "SET status = 'superseded', superseded_by = ?, updated_at = datetime('now')"
|
|
310
|
+
: "SET status = 'superseded', updated_at = datetime('now')";
|
|
311
|
+
// v0.8.0 (K8-007 / plan §5.7) — once the 009 column exists and an
|
|
312
|
+
// identity is resolved, supersession is scoped on repo_id (NULL
|
|
313
|
+
// rows are global); project_id stays as the pre-009 scope.
|
|
314
|
+
const scopedOnRepoId = this.hasRepoIdColumn() && this.repoId !== null;
|
|
315
|
+
const whereScope = scopedOnRepoId
|
|
316
|
+
? "AND (repo_id IS ? OR repo_id IS NULL)"
|
|
317
|
+
: "AND (project_id IS ? OR (project_id IS NULL AND ? IS NULL))";
|
|
318
|
+
const scopeParams = scopedOnRepoId
|
|
319
|
+
? [this.repoId]
|
|
320
|
+
: [projectId, projectId];
|
|
321
|
+
this.store
|
|
322
|
+
.prepare(`UPDATE memories
|
|
323
|
+
${setClause}
|
|
324
|
+
WHERE fingerprint = ?
|
|
325
|
+
AND type = ?
|
|
326
|
+
AND status = 'active'
|
|
327
|
+
${whereScope}`)
|
|
328
|
+
.run(...(withSupersededBy
|
|
329
|
+
? [id, fp, input.type, ...scopeParams]
|
|
330
|
+
: [fp, input.type, ...scopeParams]));
|
|
331
|
+
const after = this.store.prepare("SELECT changes() AS n").get();
|
|
332
|
+
if (after.n > 0) {
|
|
333
|
+
this.metrics?.incr("memories_superseded", 1);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// v0.4.0 (BUG-008) — recurrence_count is only persisted when the
|
|
337
|
+
// column exists (migration 005); pre-005 DBs get the legacy shape.
|
|
338
|
+
// v0.6.0 (K6-011 / plan §5.3) — the inferable verdict is persisted
|
|
339
|
+
// on insert when the column exists (migration 007). The column
|
|
340
|
+
// list is assembled so every migration level gets exactly its own
|
|
341
|
+
// shape: 005+ gains recurrence_count, 007+ gains inferable.
|
|
342
|
+
const withRecurrence = this.hasRecurrenceColumn();
|
|
343
|
+
const withCurated = this.hasCuratedColumn();
|
|
344
|
+
const columns = [
|
|
345
|
+
"id",
|
|
346
|
+
"type",
|
|
347
|
+
"content",
|
|
348
|
+
"scope",
|
|
349
|
+
"relevance_score",
|
|
350
|
+
"source_tool",
|
|
351
|
+
"source_session",
|
|
352
|
+
"metadata",
|
|
353
|
+
"expires_at",
|
|
354
|
+
"project_id",
|
|
355
|
+
"fingerprint",
|
|
356
|
+
"origin",
|
|
357
|
+
"evidence_count",
|
|
358
|
+
"last_verified_at",
|
|
359
|
+
"status",
|
|
360
|
+
];
|
|
361
|
+
const params = [
|
|
362
|
+
id,
|
|
363
|
+
input.type,
|
|
364
|
+
input.content,
|
|
365
|
+
scope,
|
|
366
|
+
relevanceScore,
|
|
367
|
+
input.sourceTool ?? null,
|
|
368
|
+
input.sourceSession ?? null,
|
|
369
|
+
metadata,
|
|
370
|
+
expiresAt,
|
|
371
|
+
projectId,
|
|
372
|
+
fp,
|
|
373
|
+
origin,
|
|
374
|
+
input.evidenceCount ?? 0,
|
|
375
|
+
input.lastVerifiedAt ?? null,
|
|
376
|
+
status,
|
|
377
|
+
];
|
|
378
|
+
if (withRecurrence) {
|
|
379
|
+
columns.push("recurrence_count");
|
|
380
|
+
params.push(input.recurrenceCount ?? 0);
|
|
381
|
+
}
|
|
382
|
+
if (withCurated) {
|
|
383
|
+
columns.push("inferable");
|
|
384
|
+
params.push(persistInferable(input.type, input.content, input.metadata));
|
|
385
|
+
}
|
|
386
|
+
// v0.8.0 (K8-007 / plan §5.7) — repo_id is persisted on every new
|
|
387
|
+
// memory (009 column). A NULL projectId stays NULL-scoped — the
|
|
388
|
+
// global rows PatternMiner's nullPid convention relies on — and a
|
|
389
|
+
// NULL repo_id row matches every scope. project_id remains written
|
|
390
|
+
// above: it is provenance now, not scope (D8-02).
|
|
391
|
+
if (this.hasRepoIdColumn()) {
|
|
392
|
+
columns.push("repo_id");
|
|
393
|
+
params.push(projectId !== null ? this.repoId : null);
|
|
394
|
+
}
|
|
395
|
+
const insert = `INSERT INTO memories (${columns.join(", ")})
|
|
395
396
|
VALUES (${params.map(() => "?").join(", ")})`;
|
|
396
|
-
|
|
397
|
+
this.store.prepare(insert).run(...params);
|
|
398
|
+
});
|
|
397
399
|
return id;
|
|
398
400
|
}
|
|
399
401
|
catch (err) {
|
package/dist/RepoIdentity.js
CHANGED
|
@@ -63,7 +63,7 @@ export function parseGitConfigRemote(text, name = "origin") {
|
|
|
63
63
|
continue;
|
|
64
64
|
const eq = line.indexOf("=");
|
|
65
65
|
if (eq === -1)
|
|
66
|
-
|
|
66
|
+
continue;
|
|
67
67
|
const key = line.slice(0, eq).trim();
|
|
68
68
|
const value = line.slice(eq + 1).trim();
|
|
69
69
|
if (key !== "url")
|
package/dist/Retrospective.js
CHANGED
|
@@ -81,6 +81,16 @@ export const METRIC_KEY_LABELS = {
|
|
|
81
81
|
// v1.2.0 (K12-001 / plan §4) — surface metrics; labels required by BUG-014 regression.
|
|
82
82
|
tui_snapshots_flushed: "Snapshots TUI generados",
|
|
83
83
|
tui_actions_invoked: "Acciones TUI invocadas",
|
|
84
|
+
// v1.4.0 (K14-003 / plan §4.3) — MCP bridge metrics; labels required by BUG-014.
|
|
85
|
+
mcp_requests_total: "Solicitudes MCP totales",
|
|
86
|
+
mcp_reads_served: "Lecturas MCP servidas",
|
|
87
|
+
mcp_writes_accepted: "Escrituras MCP aceptadas",
|
|
88
|
+
mcp_writes_refused: "Escrituras MCP rechazadas",
|
|
89
|
+
mcp_errors_total: "Errores MCP totales",
|
|
90
|
+
// v1.5.0 (K15-001 / plan §4) — Diaspora metrics; labels required by BUG-014.
|
|
91
|
+
skills_emitted_total: "Skills emitidos (total)",
|
|
92
|
+
mif_exports_total: "Exportaciones MIF totales",
|
|
93
|
+
mif_imports_total: "Importaciones MIF totales",
|
|
84
94
|
};
|
|
85
95
|
function originLabel(origin) {
|
|
86
96
|
if (origin === "reflector")
|
package/dist/Store.d.ts
CHANGED
package/dist/Store.js
CHANGED
|
@@ -17,10 +17,12 @@ import { createDatabase } from "./sqlite-adapter.js";
|
|
|
17
17
|
export class Store {
|
|
18
18
|
db;
|
|
19
19
|
closed = false;
|
|
20
|
+
txDepth = 0;
|
|
20
21
|
constructor(options) {
|
|
21
22
|
this.db = createDatabase(options.path);
|
|
22
23
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
23
24
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
25
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
24
26
|
}
|
|
25
27
|
prepare(sql) {
|
|
26
28
|
if (this.closed)
|
|
@@ -30,8 +32,19 @@ export class Store {
|
|
|
30
32
|
transaction(fn) {
|
|
31
33
|
if (this.closed)
|
|
32
34
|
throw new Error("Store is closed");
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
if (this.txDepth > 0)
|
|
36
|
+
return fn();
|
|
37
|
+
this.txDepth++;
|
|
38
|
+
try {
|
|
39
|
+
const tx = this.db.transaction(fn);
|
|
40
|
+
const result = tx();
|
|
41
|
+
this.txDepth--;
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
this.txDepth--;
|
|
46
|
+
throw e;
|
|
47
|
+
}
|
|
35
48
|
}
|
|
36
49
|
exec(sql) {
|
|
37
50
|
if (this.closed)
|