@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.
- package/dist/plugin/index.d.ts +4 -3
- package/dist/plugin/index.js +122 -113
- package/dist/plugin/native.d.ts +2 -2
- package/dist/plugin/native.js +2 -2
- package/package.json +56 -68
- package/LICENSE +0 -21
- package/README.md +0 -605
- package/dist/migrations/001_initial.sql +0 -92
- package/dist/migrations/002_indexes.sql +0 -14
- package/dist/migrations/003_v02_signal.sql +0 -58
- package/dist/migrations/004_v03_knowledge.sql +0 -138
- package/dist/migrations/005_v04_signal.sql +0 -57
- package/dist/migrations/006_v05_glassbox.sql +0 -118
- package/dist/migrations/007_v06_pull.sql +0 -145
- package/dist/migrations/008_v07_truth.sql +0 -124
- package/dist/migrations/009_v08_team.sql +0 -100
- package/dist/migrations/010_v09_native.sql +0 -78
- package/dist/migrations/011_v10_proven.sql +0 -69
- package/dist/migrations/012_v11_drift.sql +0 -24
- package/dist/plugin/Archiver.d.ts +0 -42
- package/dist/plugin/Archiver.js +0 -83
- package/dist/plugin/ArtifactWriter.d.ts +0 -93
- package/dist/plugin/ArtifactWriter.js +0 -291
- package/dist/plugin/CausalChain.d.ts +0 -22
- package/dist/plugin/CausalChain.js +0 -197
- package/dist/plugin/ChatBridge.d.ts +0 -41
- package/dist/plugin/ChatBridge.js +0 -103
- package/dist/plugin/ConflictDetector.d.ts +0 -35
- package/dist/plugin/ConflictDetector.js +0 -260
- package/dist/plugin/ContextInjector.d.ts +0 -222
- package/dist/plugin/ContextInjector.js +0 -533
- package/dist/plugin/ConventionMiner.d.ts +0 -35
- package/dist/plugin/ConventionMiner.js +0 -242
- package/dist/plugin/Curator.d.ts +0 -116
- package/dist/plugin/Curator.js +0 -324
- package/dist/plugin/DashboardHtml.d.ts +0 -5
- package/dist/plugin/DashboardHtml.js +0 -180
- package/dist/plugin/Feedback.d.ts +0 -67
- package/dist/plugin/Feedback.js +0 -120
- package/dist/plugin/HookLiveness.d.ts +0 -88
- package/dist/plugin/HookLiveness.js +0 -322
- package/dist/plugin/InjectionLedger.d.ts +0 -98
- package/dist/plugin/InjectionLedger.js +0 -295
- package/dist/plugin/LessonFixer.d.ts +0 -44
- package/dist/plugin/LessonFixer.js +0 -45
- package/dist/plugin/Materializer.d.ts +0 -83
- package/dist/plugin/Materializer.js +0 -181
- package/dist/plugin/MemoryService.d.ts +0 -360
- package/dist/plugin/MemoryService.js +0 -1242
- package/dist/plugin/Migrate.d.ts +0 -17
- package/dist/plugin/Migrate.js +0 -271
- package/dist/plugin/PatternMiner.d.ts +0 -49
- package/dist/plugin/PatternMiner.js +0 -132
- package/dist/plugin/QualityGate.d.ts +0 -124
- package/dist/plugin/QualityGate.js +0 -119
- package/dist/plugin/Reflector.d.ts +0 -91
- package/dist/plugin/Reflector.js +0 -307
- package/dist/plugin/RepoIdentity.d.ts +0 -124
- package/dist/plugin/RepoIdentity.js +0 -300
- package/dist/plugin/RepoTruth.d.ts +0 -80
- package/dist/plugin/RepoTruth.js +0 -599
- package/dist/plugin/Retrospective.d.ts +0 -17
- package/dist/plugin/Retrospective.js +0 -216
- package/dist/plugin/SharedLayer.d.ts +0 -159
- package/dist/plugin/SharedLayer.js +0 -462
- package/dist/plugin/Store.d.ts +0 -29
- package/dist/plugin/Store.js +0 -50
- package/dist/plugin/ToolCallObserver.d.ts +0 -32
- package/dist/plugin/ToolCallObserver.js +0 -201
- package/dist/plugin/TuiActions.d.ts +0 -43
- package/dist/plugin/TuiActions.js +0 -181
- package/dist/plugin/TuiSnapshots.d.ts +0 -24
- package/dist/plugin/TuiSnapshots.js +0 -158
- package/dist/plugin/columns.d.ts +0 -11
- package/dist/plugin/columns.js +0 -54
- package/dist/plugin/confidence.d.ts +0 -8
- package/dist/plugin/confidence.js +0 -34
- package/dist/plugin/contract.d.ts +0 -54
- package/dist/plugin/contract.js +0 -386
- package/dist/plugin/diff.d.ts +0 -8
- package/dist/plugin/diff.js +0 -182
- package/dist/plugin/escape.d.ts +0 -39
- package/dist/plugin/escape.js +0 -60
- package/dist/plugin/fingerprint.d.ts +0 -27
- package/dist/plugin/fingerprint.js +0 -73
- package/dist/plugin/inferability.d.ts +0 -32
- package/dist/plugin/inferability.js +0 -88
- package/dist/plugin/kevin_approve.d.ts +0 -34
- package/dist/plugin/kevin_approve.js +0 -52
- package/dist/plugin/kevin_audit.d.ts +0 -229
- package/dist/plugin/kevin_audit.js +0 -631
- package/dist/plugin/kevin_bench.d.ts +0 -10
- package/dist/plugin/kevin_bench.js +0 -82
- package/dist/plugin/kevin_conflicts.d.ts +0 -9
- package/dist/plugin/kevin_conflicts.js +0 -50
- package/dist/plugin/kevin_contract.d.ts +0 -8
- package/dist/plugin/kevin_contract.js +0 -59
- package/dist/plugin/kevin_doctor.d.ts +0 -62
- package/dist/plugin/kevin_doctor.js +0 -196
- package/dist/plugin/kevin_facts.d.ts +0 -42
- package/dist/plugin/kevin_facts.js +0 -36
- package/dist/plugin/kevin_forget.d.ts +0 -33
- package/dist/plugin/kevin_forget.js +0 -260
- package/dist/plugin/kevin_native.d.ts +0 -29
- package/dist/plugin/kevin_native.js +0 -79
- package/dist/plugin/kevin_propose.d.ts +0 -23
- package/dist/plugin/kevin_propose.js +0 -14
- package/dist/plugin/kevin_publish.d.ts +0 -38
- package/dist/plugin/kevin_publish.js +0 -18
- package/dist/plugin/kevin_why.d.ts +0 -23
- package/dist/plugin/kevin_why.js +0 -111
- package/dist/plugin/memory-format.d.ts +0 -20
- package/dist/plugin/memory-format.js +0 -59
- package/dist/plugin/metrics.d.ts +0 -97
- package/dist/plugin/metrics.js +0 -288
- package/dist/plugin/okf-export.d.ts +0 -3
- package/dist/plugin/okf-export.js +0 -134
- package/dist/plugin/okf-import.d.ts +0 -76
- package/dist/plugin/okf-import.js +0 -271
- package/dist/plugin/okf.d.ts +0 -107
- package/dist/plugin/okf.js +0 -303
- package/dist/plugin/perf.d.ts +0 -35
- package/dist/plugin/perf.js +0 -174
- package/dist/plugin/query-tokenizer.d.ts +0 -13
- package/dist/plugin/query-tokenizer.js +0 -133
- package/dist/plugin/redact.d.ts +0 -2
- package/dist/plugin/redact.js +0 -18
- package/dist/plugin/replay-types.d.ts +0 -69
- package/dist/plugin/replay-types.js +0 -156
- package/dist/plugin/replay.d.ts +0 -36
- package/dist/plugin/replay.js +0 -202
- package/dist/plugin/sqlite-adapter.d.ts +0 -12
- package/dist/plugin/sqlite-adapter.js +0 -81
- package/dist/plugin/time-ms.d.ts +0 -1
- package/dist/plugin/time-ms.js +0 -16
- package/dist/plugin/tui-types.d.ts +0 -59
- package/dist/plugin/tui-types.js +0 -4
- package/dist/plugin/tui.d.ts +0 -18
- package/dist/plugin/tui.js +0 -198
- package/dist/plugin/uuid.d.ts +0 -1
- package/dist/plugin/uuid.js +0 -50
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
import { readOriginCallId } from "./MemoryService.js";
|
|
2
|
-
import { hasColumn } from "./columns.js";
|
|
3
|
-
import { toMs } from "./time-ms.js";
|
|
4
|
-
import { uuidv7 } from "./uuid.js";
|
|
5
|
-
export class InjectionLedger {
|
|
6
|
-
store;
|
|
7
|
-
metrics;
|
|
8
|
-
constructor(store, metrics) {
|
|
9
|
-
this.store = store;
|
|
10
|
-
this.metrics = metrics ?? null;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Records one injected memory. Idempotent at the row level (UUID PK);
|
|
14
|
-
* duplicates are expected only via the caller's per-session seen-set.
|
|
15
|
-
*/
|
|
16
|
-
record(input) {
|
|
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
|
-
}
|
|
32
|
-
this.metrics?.incr("injections_total", 1);
|
|
33
|
-
// v0.8.0 (K8-024 / plan §5.7) — shared-layer consumption is counted
|
|
34
|
-
// separately so the audit can tell how much of the push channel
|
|
35
|
-
// came from teammates' entries.
|
|
36
|
-
if (input.layer === "shared") {
|
|
37
|
-
this.metrics?.incr("injections_from_shared", 1);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Settles every unmeasured injection of the session: a fingerprint that
|
|
42
|
-
* failed again (as a failing tool_call) after the injection is
|
|
43
|
-
* `ineffective`, otherwise `effective`. Idempotent — only
|
|
44
|
-
* `outcome = 'unmeasured'` rows are flipped, and the recurrence charge
|
|
45
|
-
* is `MAX(recurrence_count, n)` where n = all failing calls of the
|
|
46
|
-
* fingerprint after `injected_at` (a later idle re-computes n and
|
|
47
|
-
* catches up — plan §5.1 rule 4: 3 recurrences → stale).
|
|
48
|
-
*
|
|
49
|
-
* An ineffective injection also bumps the target memory's
|
|
50
|
-
* `recurrence_count` (negative evidence, plan §5.3) and stamps
|
|
51
|
-
* `last_injected_at` (monotonically — an older row can never
|
|
52
|
-
* regress a newer injection's timestamp; the column means "most
|
|
53
|
-
* recent injection").
|
|
54
|
-
*/
|
|
55
|
-
settle(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);
|
|
69
|
-
for (const inj of injections) {
|
|
70
|
-
// Same identity dimension CausalChain uses: the failing call's
|
|
71
|
-
// `error_fingerprint` (stamped by Reflector) or the legacy
|
|
72
|
-
// `fingerprint` hash.
|
|
73
|
-
// v1.1.0 — time comparison uses toMs helper (prefers _ms).
|
|
74
|
-
//
|
|
75
|
-
// BUG-003 — the exemption is now bounded to the lesson's OWN
|
|
76
|
-
// creating call (memories.metadata.origin_call_id, stamped by
|
|
77
|
-
// Reflector). The old code excluded the session's FIRST failing
|
|
78
|
-
// call of the fingerprint, which is only the creating call when
|
|
79
|
-
// the lesson was born in THIS session; a lesson created in an
|
|
80
|
-
// earlier session had its first in-session failure (a genuine
|
|
81
|
-
// post-injection recurrence) wrongly exempted, inflating
|
|
82
|
-
// precision. Memories without a tracked creating call (agent-
|
|
83
|
-
// saved, test fixtures) get no exemption: only the `ts >=
|
|
84
|
-
// injected_at` bound applies.
|
|
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
|
-
}
|
|
129
|
-
// v0.5.0 (K5-005 / plan §5.1, D5-01) — three-way settlement:
|
|
130
|
-
// recurrences >= 1 → ineffective (existing side effects unchanged)
|
|
131
|
-
// else fixes >= 1 → effective (a linked fix was OBSERVED)
|
|
132
|
-
// else → inconclusive (new majority bucket; excluded
|
|
133
|
-
// from the precision denominator)
|
|
134
|
-
if (n >= 1) {
|
|
135
|
-
if (inj.outcome === "unmeasured") {
|
|
136
|
-
this.store
|
|
137
|
-
.prepare(`UPDATE kevin_injections SET outcome = 'ineffective'
|
|
138
|
-
WHERE id = ?`)
|
|
139
|
-
.run(inj.id);
|
|
140
|
-
this.metrics?.incr("injections_ineffective", 1);
|
|
141
|
-
}
|
|
142
|
-
this.store
|
|
143
|
-
.prepare(`UPDATE memories
|
|
144
|
-
SET recurrence_count = MAX(recurrence_count, ?),
|
|
145
|
-
last_injected_at = CASE
|
|
146
|
-
WHEN last_injected_at IS NULL
|
|
147
|
-
OR ? > last_injected_at
|
|
148
|
-
THEN ? ELSE last_injected_at END
|
|
149
|
-
WHERE fingerprint = ? AND id = ?`)
|
|
150
|
-
.run(n, inj.injected_at, inj.injected_at, inj.fingerprint, inj.memory_id);
|
|
151
|
-
// v0.4.0 (K4-025 / plan §5.1 rule 4, D4-06) — recurrence
|
|
152
|
-
// expels: a fingerprint at `recurrence_count >= 3` is
|
|
153
|
-
// demoted to `status='stale'` and never injected again
|
|
154
|
-
// (only a new causal pattern — from a linked fix —
|
|
155
|
-
// re-admits the lesson, not the stale error row).
|
|
156
|
-
this.store
|
|
157
|
-
.prepare(`UPDATE memories SET status = 'stale'
|
|
158
|
-
WHERE id = ? AND recurrence_count >= 3`)
|
|
159
|
-
.run(inj.memory_id);
|
|
160
|
-
}
|
|
161
|
-
else if (inj.outcome === "unmeasured") {
|
|
162
|
-
// Mirror of the recurrence predicate with the success flag
|
|
163
|
-
// inverted and the fingerprint matched on
|
|
164
|
-
// `fix_for_fingerprint` (populated by CausalChain.onSuccess,
|
|
165
|
-
// indexed by idx_tool_calls_fix_fp since migration 004). The
|
|
166
|
-
// `ts >= injected_at` bound and `session_id = ?` filter are
|
|
167
|
-
// kept; there is no `origin_call_id` exemption for fixes —
|
|
168
|
-
// a fix is not the creating call.
|
|
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) {
|
|
194
|
-
this.store
|
|
195
|
-
.prepare(`UPDATE kevin_injections SET outcome = 'effective'
|
|
196
|
-
WHERE id = ?`)
|
|
197
|
-
.run(inj.id);
|
|
198
|
-
this.metrics?.incr("injections_effective", 1);
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
this.store
|
|
202
|
-
.prepare(`UPDATE kevin_injections SET outcome = 'inconclusive'
|
|
203
|
-
WHERE id = ?`)
|
|
204
|
-
.run(inj.id);
|
|
205
|
-
this.metrics?.incr("injections_inconclusive", 1);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Per-fingerprint failing tool-call counts for the session. Feeds
|
|
212
|
-
* QualityGate.canInject and the HITL suggestion block.
|
|
213
|
-
*/
|
|
214
|
-
recurrencesFor(sessionId) {
|
|
215
|
-
const rows = this.store
|
|
216
|
-
.prepare(`SELECT COALESCE(error_fingerprint, fingerprint) AS fp
|
|
217
|
-
FROM tool_calls
|
|
218
|
-
WHERE session_id = ? AND success = 0
|
|
219
|
-
AND (error_fingerprint IS NOT NULL OR fingerprint IS NOT NULL)`)
|
|
220
|
-
.all(sessionId);
|
|
221
|
-
const out = new Map();
|
|
222
|
-
for (const r of rows) {
|
|
223
|
-
if (!r.fp)
|
|
224
|
-
continue;
|
|
225
|
-
out.set(r.fp, (out.get(r.fp) ?? 0) + 1);
|
|
226
|
-
}
|
|
227
|
-
return out;
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* v0.4.0 (K4-017) — recurrence counts for the QualityGate at
|
|
231
|
-
* injection time: only failing calls that happened AFTER the
|
|
232
|
-
* fingerprint was already injected this session count. The failure
|
|
233
|
-
* that *created* a lesson is not a recurrence — it precedes any
|
|
234
|
-
* injection (plan §5.1 rule 4, same `ts >= injected_at` semantics
|
|
235
|
-
* `settle` uses).
|
|
236
|
-
*/
|
|
237
|
-
postInjectionRecurrencesFor(sessionId) {
|
|
238
|
-
const rows = this.store
|
|
239
|
-
.prepare(`SELECT COALESCE(t.error_fingerprint, t.fingerprint) AS fp
|
|
240
|
-
FROM tool_calls t
|
|
241
|
-
WHERE t.session_id = ? AND t.success = 0
|
|
242
|
-
AND (t.error_fingerprint IS NOT NULL OR t.fingerprint IS NOT NULL)
|
|
243
|
-
AND t.ts >= COALESCE(
|
|
244
|
-
(SELECT MAX(injected_at) FROM kevin_injections
|
|
245
|
-
WHERE session_id = t.session_id
|
|
246
|
-
AND fingerprint = COALESCE(t.error_fingerprint, t.fingerprint)),
|
|
247
|
-
'9999-12-31')`)
|
|
248
|
-
.all(sessionId);
|
|
249
|
-
const out = new Map();
|
|
250
|
-
for (const r of rows) {
|
|
251
|
-
if (!r.fp)
|
|
252
|
-
continue;
|
|
253
|
-
out.set(r.fp, (out.get(r.fp) ?? 0) + 1);
|
|
254
|
-
}
|
|
255
|
-
return out;
|
|
256
|
-
}
|
|
257
|
-
/** Number of rows for the session not yet settled (drives tests and settle). */
|
|
258
|
-
unsettledForSession(sessionId) {
|
|
259
|
-
const row = this.store
|
|
260
|
-
.prepare(`SELECT COUNT(*) AS n FROM kevin_injections
|
|
261
|
-
WHERE session_id = ? AND outcome = 'unmeasured'`)
|
|
262
|
-
.get(sessionId);
|
|
263
|
-
return row.n;
|
|
264
|
-
}
|
|
265
|
-
/** Latest ledger rows for a session, newest first (used by tests/tools). */
|
|
266
|
-
rowsForSession(sessionId) {
|
|
267
|
-
return this.store
|
|
268
|
-
.prepare(`SELECT id, memory_id, fingerprint, session_id, hook, tokens,
|
|
269
|
-
injected_at, outcome
|
|
270
|
-
FROM kevin_injections
|
|
271
|
-
WHERE session_id = ?
|
|
272
|
-
ORDER BY injected_at ASC, id ASC`)
|
|
273
|
-
.all(sessionId);
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* v0.5.0 (K5-005 / plan §8.4) — one grouped rollup over the whole
|
|
277
|
-
* ledger, zero-filled for every outcome. Consumed by `kevin_audit`.
|
|
278
|
-
*/
|
|
279
|
-
outcomeCounts() {
|
|
280
|
-
const rows = this.store
|
|
281
|
-
.prepare("SELECT outcome, COUNT(*) AS n FROM kevin_injections GROUP BY outcome")
|
|
282
|
-
.all();
|
|
283
|
-
const out = {
|
|
284
|
-
unmeasured: 0,
|
|
285
|
-
effective: 0,
|
|
286
|
-
ineffective: 0,
|
|
287
|
-
inconclusive: 0,
|
|
288
|
-
};
|
|
289
|
-
for (const r of rows) {
|
|
290
|
-
out[r.outcome] = r.n;
|
|
291
|
-
}
|
|
292
|
-
return out;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
// v1.1.0 — readOriginCallId deduplicated: imported from MemoryService (K11-003/K11-013)
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* v0.4.0 (K4-014) — Deterministic fix capture (plan §5.4 / D4-07).
|
|
3
|
-
*
|
|
4
|
-
* Zero-cost "Fixed by:" raw material from local data: when CausalChain
|
|
5
|
-
* links a success to a failing fingerprint, the linked tool_call's
|
|
6
|
-
* `args_summary` becomes a deterministic fix string stored in
|
|
7
|
-
* `memories.fix_args`. The default promotion path is pure local data —
|
|
8
|
-
* no LLM calls (principle 14).
|
|
9
|
-
*/
|
|
10
|
-
/** Max length of the `args_summary` embedded in a fix_args string. */
|
|
11
|
-
export declare const FIX_ARGS_TRUNCATE = 120;
|
|
12
|
-
export interface LinkedToolCall {
|
|
13
|
-
tool: string;
|
|
14
|
-
args_summary: string | null;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* `"bash" with args "npm i -g rg"` style. Returns null when the linked
|
|
18
|
-
* call carries no args_summary (nothing to say).
|
|
19
|
-
*/
|
|
20
|
-
export declare function extractFixArgs(call: LinkedToolCall): string | null;
|
|
21
|
-
export interface EnrichInput {
|
|
22
|
-
lesson: string;
|
|
23
|
-
fixArgs: string | null;
|
|
24
|
-
originalError: string | null;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Opt-in LLM phrasing hook (K4-015): receives the lesson, the
|
|
28
|
-
* deterministic fix_args and the original error; returns a one-line
|
|
29
|
-
* `Fix:` phrasing, or null to fall back to the deterministic text.
|
|
30
|
-
* May be async; never runs on the failure hot path.
|
|
31
|
-
*/
|
|
32
|
-
export type EnrichFn = (input: EnrichInput) => Promise<string | null>;
|
|
33
|
-
export interface PatternLike {
|
|
34
|
-
content: string;
|
|
35
|
-
fixArgs: string | null;
|
|
36
|
-
}
|
|
37
|
-
/** Deterministic `Fixed by: {fix_args}` line (or "" when there is none). */
|
|
38
|
-
export declare function deterministicFixLine(pattern: PatternLike): string;
|
|
39
|
-
/**
|
|
40
|
-
* Promotion-time fix phrasing. Default path returns the deterministic
|
|
41
|
-
* `Fixed by: {fix_args}` line (or "" when there is none). When an
|
|
42
|
-
* `enrichFn` is supplied and returns a phrase, that phrase wins.
|
|
43
|
-
*/
|
|
44
|
-
export declare function enrichAtPromotion(pattern: PatternLike, enrichFn?: EnrichFn): Promise<string>;
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* v0.4.0 (K4-014) — Deterministic fix capture (plan §5.4 / D4-07).
|
|
3
|
-
*
|
|
4
|
-
* Zero-cost "Fixed by:" raw material from local data: when CausalChain
|
|
5
|
-
* links a success to a failing fingerprint, the linked tool_call's
|
|
6
|
-
* `args_summary` becomes a deterministic fix string stored in
|
|
7
|
-
* `memories.fix_args`. The default promotion path is pure local data —
|
|
8
|
-
* no LLM calls (principle 14).
|
|
9
|
-
*/
|
|
10
|
-
/** Max length of the `args_summary` embedded in a fix_args string. */
|
|
11
|
-
export const FIX_ARGS_TRUNCATE = 120;
|
|
12
|
-
/**
|
|
13
|
-
* `"bash" with args "npm i -g rg"` style. Returns null when the linked
|
|
14
|
-
* call carries no args_summary (nothing to say).
|
|
15
|
-
*/
|
|
16
|
-
export function extractFixArgs(call) {
|
|
17
|
-
const raw = call.args_summary?.trim();
|
|
18
|
-
if (!raw)
|
|
19
|
-
return null;
|
|
20
|
-
const truncated = raw.length > FIX_ARGS_TRUNCATE
|
|
21
|
-
? `${raw.slice(0, FIX_ARGS_TRUNCATE)}…`
|
|
22
|
-
: raw;
|
|
23
|
-
return `${call.tool} with args "${truncated}"`;
|
|
24
|
-
}
|
|
25
|
-
/** Deterministic `Fixed by: {fix_args}` line (or "" when there is none). */
|
|
26
|
-
export function deterministicFixLine(pattern) {
|
|
27
|
-
return pattern.fixArgs ? `Fixed by: ${pattern.fixArgs}` : "";
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Promotion-time fix phrasing. Default path returns the deterministic
|
|
31
|
-
* `Fixed by: {fix_args}` line (or "" when there is none). When an
|
|
32
|
-
* `enrichFn` is supplied and returns a phrase, that phrase wins.
|
|
33
|
-
*/
|
|
34
|
-
export async function enrichAtPromotion(pattern, enrichFn) {
|
|
35
|
-
if (enrichFn) {
|
|
36
|
-
const phrased = await enrichFn({
|
|
37
|
-
lesson: pattern.content,
|
|
38
|
-
fixArgs: pattern.fixArgs,
|
|
39
|
-
originalError: null,
|
|
40
|
-
});
|
|
41
|
-
if (phrased)
|
|
42
|
-
return phrased;
|
|
43
|
-
}
|
|
44
|
-
return deterministicFixLine(pattern);
|
|
45
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import type { ArtifactWriter, WriteOutcome } from "./ArtifactWriter.js";
|
|
2
|
-
import type { Store } from "./Store.js";
|
|
3
|
-
/**
|
|
4
|
-
* K6-017 — v0.6.0 pull — topic bundles for the pull channels (plan §5.6).
|
|
5
|
-
*
|
|
6
|
-
* Writes `~/.opencode-kevin/refs/<topic>.md` (one file per topic) and
|
|
7
|
-
* `~/.opencode-kevin/skills/project-knowledge.md` (one file). Both go
|
|
8
|
-
* through the `ArtifactWriter` — marker-scoped, atomic, hash-audited.
|
|
9
|
-
* There is no direct raw file-write anywhere in this module (D6-01).
|
|
10
|
-
*
|
|
11
|
-
* Topic derivation is deterministic and semantic (D6-14): a topic is
|
|
12
|
-
* `<type>-<dominant token>`, where the dominant token is the
|
|
13
|
-
* highest-frequency non-stop-word token of the fingerprint-normalized
|
|
14
|
-
* content across the memories in the group, ties broken lexicographically.
|
|
15
|
-
* Topics are NEVER derived from a fingerprint prefix, and hash-like tokens
|
|
16
|
-
* (hex-only, length >= 8) are excluded from the race entirely.
|
|
17
|
-
*
|
|
18
|
-
* Output ordering within each bundle is by memory id, matching §5.4.
|
|
19
|
-
* Regeneration with unchanged inputs is a `noop` at the ArtifactWriter
|
|
20
|
-
* level, so the "run twice, compare bytes" criterion holds for every
|
|
21
|
-
* bundle.
|
|
22
|
-
*/
|
|
23
|
-
export interface MaterializedBundle {
|
|
24
|
-
readonly topic: string;
|
|
25
|
-
readonly path: string;
|
|
26
|
-
readonly outcome: WriteOutcome;
|
|
27
|
-
}
|
|
28
|
-
export interface BundleTarget {
|
|
29
|
-
readonly topic: string;
|
|
30
|
-
readonly path: string;
|
|
31
|
-
}
|
|
32
|
-
export declare const SKILL_TOPIC = "project-knowledge";
|
|
33
|
-
/**
|
|
34
|
-
* v0.9.0 (K9-016 / plan §5.4, D9-10) — the native registration surfaces.
|
|
35
|
-
* Registration replaces emission; they are never both active.
|
|
36
|
-
*/
|
|
37
|
-
export type NativeSurface = "skill" | "reference";
|
|
38
|
-
export declare class Materializer {
|
|
39
|
-
private readonly store;
|
|
40
|
-
/** `root` is injectable for tests; production defaults to ~/.opencode-kevin. */
|
|
41
|
-
private readonly root;
|
|
42
|
-
/**
|
|
43
|
-
* v0.9.0 (K9-016 / plan §5.4, D9-10) — the mutual-exclusion guard.
|
|
44
|
-
* A surface `attachNative()` registered is skipped by `materialize()`
|
|
45
|
-
* for that surface only; the other surface keeps emitting.
|
|
46
|
-
*/
|
|
47
|
-
private readonly nativeRegistered;
|
|
48
|
-
constructor(store: Store, options?: {
|
|
49
|
-
root?: string;
|
|
50
|
-
});
|
|
51
|
-
/**
|
|
52
|
-
* v0.9.0 (K9-016 / plan §5.4, D9-10) — record whether the surface was
|
|
53
|
-
* registered natively. Called at attach time (index.ts wiring, K9-018)
|
|
54
|
-
* with each `NativeRegistration.registered` flag.
|
|
55
|
-
*/
|
|
56
|
-
markNativeRegistered(surface: NativeSurface, registered: boolean): void;
|
|
57
|
-
/** v0.9.0 (K9-016 / plan §5.4, D9-10) — is the surface registered natively? */
|
|
58
|
-
hasNativeRegistration(surface: NativeSurface): boolean;
|
|
59
|
-
/** The curated, active memories — the knowledge the pull channels publish. */
|
|
60
|
-
private curatedRows;
|
|
61
|
-
/** The rendered skill body over all curated memories; "" when empty. */
|
|
62
|
-
skillBody(): string;
|
|
63
|
-
/** Group rows by type, deriving each type's topic from its own content. */
|
|
64
|
-
private groupByTopic;
|
|
65
|
-
/**
|
|
66
|
-
* The pull-channel targets: the skill file plus one ref file per topic.
|
|
67
|
-
* The order is deterministic (skill first, refs by topic).
|
|
68
|
-
*/
|
|
69
|
-
bundleTargets(): BundleTarget[];
|
|
70
|
-
/**
|
|
71
|
-
* Regenerate every bundle through the ArtifactWriter (write). This is
|
|
72
|
-
* a call site of `write()` — the single write FUNCTION with two
|
|
73
|
-
* constrained targets: `kevin_approve` reaches `agents_md_path`, this
|
|
74
|
-
* module reaches `~/.opencode-kevin` only (D6-07; enforced by K6-020).
|
|
75
|
-
*
|
|
76
|
-
* v0.9.0 (K9-016 / plan §5.4, D9-10) — the guard: a surface registered
|
|
77
|
-
* natively by `attachNative()` is skipped here for that surface only.
|
|
78
|
-
* Both active would put the curated skill in front of the model twice
|
|
79
|
-
* from two sources that can disagree; neither active would silently
|
|
80
|
-
* remove the feature on hosts that gained the subpath.
|
|
81
|
-
*/
|
|
82
|
-
materialize(writer: ArtifactWriter): MaterializedBundle[];
|
|
83
|
-
}
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { firstSentence } from "./Curator.js";
|
|
4
|
-
import { normalize } from "./fingerprint.js";
|
|
5
|
-
import { STOP_WORDS } from "./query-tokenizer.js";
|
|
6
|
-
export const SKILL_TOPIC = "project-knowledge";
|
|
7
|
-
/** Hash-like noise: FNV-1a output is 16 lowercase hex chars; an 8+ hex
|
|
8
|
-
* token is indistinguishable from a fingerprint prefix and must never
|
|
9
|
-
* become a topic (D6-14). */
|
|
10
|
-
const HEX_LIKE_RE = /^[0-9a-f]{8,}$/;
|
|
11
|
-
const TOKEN_RE = /[^a-z0-9]+/;
|
|
12
|
-
/** Sanitize a memory type into a filesystem-safe topic prefix. */
|
|
13
|
-
function sanitizeType(type) {
|
|
14
|
-
const cleaned = type
|
|
15
|
-
.toLowerCase()
|
|
16
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
17
|
-
.replace(/-+/g, "-")
|
|
18
|
-
.replace(/^-|-$/g, "");
|
|
19
|
-
return cleaned === "" ? "memory" : cleaned;
|
|
20
|
-
}
|
|
21
|
-
function tokensOf(content) {
|
|
22
|
-
return normalize(content)
|
|
23
|
-
.split(TOKEN_RE)
|
|
24
|
-
.filter((t) => t !== "" && !STOP_WORDS.has(t) && !HEX_LIKE_RE.test(t));
|
|
25
|
-
}
|
|
26
|
-
/** Highest-frequency non-stop-word token, ties broken lexicographically
|
|
27
|
-
* (smallest wins) so the result is stable. "" when every token is a
|
|
28
|
-
* stop-word or hash-like noise. */
|
|
29
|
-
function dominantToken(rows) {
|
|
30
|
-
const counts = new Map();
|
|
31
|
-
for (const row of rows) {
|
|
32
|
-
for (const token of tokensOf(row.content)) {
|
|
33
|
-
counts.set(token, (counts.get(token) ?? 0) + 1);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
let best = "";
|
|
37
|
-
let bestCount = 0;
|
|
38
|
-
for (const [token, count] of counts) {
|
|
39
|
-
if (count > bestCount || (count === bestCount && token < best)) {
|
|
40
|
-
best = token;
|
|
41
|
-
bestCount = count;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return best;
|
|
45
|
-
}
|
|
46
|
-
function renderRows(rows) {
|
|
47
|
-
const sorted = [...rows].sort((a, b) => a.id.localeCompare(b.id));
|
|
48
|
-
const lines = sorted
|
|
49
|
-
.map((row) => `- ${firstSentence(row.content)}`)
|
|
50
|
-
.filter((line) => line !== "- ");
|
|
51
|
-
return lines.length === 0 ? "" : `${lines.join("\n")}\n`;
|
|
52
|
-
}
|
|
53
|
-
export class Materializer {
|
|
54
|
-
store;
|
|
55
|
-
/** `root` is injectable for tests; production defaults to ~/.opencode-kevin. */
|
|
56
|
-
root;
|
|
57
|
-
/**
|
|
58
|
-
* v0.9.0 (K9-016 / plan §5.4, D9-10) — the mutual-exclusion guard.
|
|
59
|
-
* A surface `attachNative()` registered is skipped by `materialize()`
|
|
60
|
-
* for that surface only; the other surface keeps emitting.
|
|
61
|
-
*/
|
|
62
|
-
nativeRegistered = new Set();
|
|
63
|
-
constructor(store, options = {}) {
|
|
64
|
-
this.store = store;
|
|
65
|
-
this.root = options.root ?? join(homedir(), ".opencode-kevin");
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* v0.9.0 (K9-016 / plan §5.4, D9-10) — record whether the surface was
|
|
69
|
-
* registered natively. Called at attach time (index.ts wiring, K9-018)
|
|
70
|
-
* with each `NativeRegistration.registered` flag.
|
|
71
|
-
*/
|
|
72
|
-
markNativeRegistered(surface, registered) {
|
|
73
|
-
if (registered)
|
|
74
|
-
this.nativeRegistered.add(surface);
|
|
75
|
-
else
|
|
76
|
-
this.nativeRegistered.delete(surface);
|
|
77
|
-
}
|
|
78
|
-
/** v0.9.0 (K9-016 / plan §5.4, D9-10) — is the surface registered natively? */
|
|
79
|
-
hasNativeRegistration(surface) {
|
|
80
|
-
return this.nativeRegistered.has(surface);
|
|
81
|
-
}
|
|
82
|
-
/** The curated, active memories — the knowledge the pull channels publish. */
|
|
83
|
-
curatedRows() {
|
|
84
|
-
return this.store
|
|
85
|
-
.prepare(`SELECT id, type, content FROM memories
|
|
86
|
-
WHERE status = 'active' AND curated = 1`)
|
|
87
|
-
.all();
|
|
88
|
-
}
|
|
89
|
-
/** The rendered skill body over all curated memories; "" when empty. */
|
|
90
|
-
skillBody() {
|
|
91
|
-
return renderRows(this.curatedRows());
|
|
92
|
-
}
|
|
93
|
-
/** Group rows by type, deriving each type's topic from its own content. */
|
|
94
|
-
groupByTopic(rows) {
|
|
95
|
-
const byType = new Map();
|
|
96
|
-
for (const row of rows) {
|
|
97
|
-
const list = byType.get(row.type) ?? [];
|
|
98
|
-
list.push(row);
|
|
99
|
-
byType.set(row.type, list);
|
|
100
|
-
}
|
|
101
|
-
const groups = [];
|
|
102
|
-
for (const [type, typeRows] of byType) {
|
|
103
|
-
const token = dominantToken(typeRows);
|
|
104
|
-
if (token === "")
|
|
105
|
-
continue;
|
|
106
|
-
groups.push({
|
|
107
|
-
topic: `${sanitizeType(type)}-${token}`,
|
|
108
|
-
rows: typeRows,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
return groups.sort((a, b) => a.topic.localeCompare(b.topic));
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* The pull-channel targets: the skill file plus one ref file per topic.
|
|
115
|
-
* The order is deterministic (skill first, refs by topic).
|
|
116
|
-
*/
|
|
117
|
-
bundleTargets() {
|
|
118
|
-
const rows = this.curatedRows();
|
|
119
|
-
const targets = [];
|
|
120
|
-
if (rows.length > 0 && renderRows(rows) !== "") {
|
|
121
|
-
targets.push({
|
|
122
|
-
topic: SKILL_TOPIC,
|
|
123
|
-
path: join(this.root, "skills", "project-knowledge.md"),
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
for (const group of this.groupByTopic(rows)) {
|
|
127
|
-
if (renderRows(group.rows) === "")
|
|
128
|
-
continue;
|
|
129
|
-
targets.push({
|
|
130
|
-
topic: group.topic,
|
|
131
|
-
path: join(this.root, "refs", `${group.topic}.md`),
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
return targets;
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Regenerate every bundle through the ArtifactWriter (write). This is
|
|
138
|
-
* a call site of `write()` — the single write FUNCTION with two
|
|
139
|
-
* constrained targets: `kevin_approve` reaches `agents_md_path`, this
|
|
140
|
-
* module reaches `~/.opencode-kevin` only (D6-07; enforced by K6-020).
|
|
141
|
-
*
|
|
142
|
-
* v0.9.0 (K9-016 / plan §5.4, D9-10) — the guard: a surface registered
|
|
143
|
-
* natively by `attachNative()` is skipped here for that surface only.
|
|
144
|
-
* Both active would put the curated skill in front of the model twice
|
|
145
|
-
* from two sources that can disagree; neither active would silently
|
|
146
|
-
* remove the feature on hosts that gained the subpath.
|
|
147
|
-
*/
|
|
148
|
-
materialize(writer) {
|
|
149
|
-
const pending = [];
|
|
150
|
-
const rows = this.curatedRows();
|
|
151
|
-
const skillBody = renderRows(rows);
|
|
152
|
-
if (skillBody !== "" && !this.nativeRegistered.has("skill")) {
|
|
153
|
-
pending.push({
|
|
154
|
-
topic: SKILL_TOPIC,
|
|
155
|
-
path: join(this.root, "skills", "project-knowledge.md"),
|
|
156
|
-
body: skillBody,
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
if (!this.nativeRegistered.has("reference")) {
|
|
160
|
-
for (const group of this.groupByTopic(rows)) {
|
|
161
|
-
const body = renderRows(group.rows);
|
|
162
|
-
if (body === "")
|
|
163
|
-
continue;
|
|
164
|
-
pending.push({
|
|
165
|
-
topic: group.topic,
|
|
166
|
-
path: join(this.root, "refs", `${group.topic}.md`),
|
|
167
|
-
body,
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return pending.map((bundle) => ({
|
|
172
|
-
topic: bundle.topic,
|
|
173
|
-
path: bundle.path,
|
|
174
|
-
outcome: writer.write({
|
|
175
|
-
path: bundle.path,
|
|
176
|
-
mode: "markers",
|
|
177
|
-
content: bundle.body,
|
|
178
|
-
}),
|
|
179
|
-
}));
|
|
180
|
-
}
|
|
181
|
-
}
|