@jmtrin/opencode-kevin 1.2.0 → 1.4.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 +84 -22
- package/dist/plugin/index.d.ts +5 -4
- package/dist/plugin/index.js +127 -113
- package/dist/plugin/native.d.ts +2 -2
- package/dist/plugin/native.js +2 -2
- package/package.json +56 -68
- 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,322 +0,0 @@
|
|
|
1
|
-
import { HOOK_NAMES } from "./Migrate.js";
|
|
2
|
-
const DEFAULT_THRESHOLD = 3;
|
|
3
|
-
/**
|
|
4
|
-
* v0.9.0 (K9-010 / plan §5.3, D9-09) — parse the TEXT setting
|
|
5
|
-
* `dead_hook_report_threshold` into the [1, 1000] clamp. NaN and empty
|
|
6
|
-
* fall back to 3 (never 0: a zero threshold would report every hook dead
|
|
7
|
-
* before a single session completes a checkpoint).
|
|
8
|
-
*/
|
|
9
|
-
export function parseThreshold(text) {
|
|
10
|
-
if (text === null || text === undefined || text === "")
|
|
11
|
-
return DEFAULT_THRESHOLD;
|
|
12
|
-
const n = Number.parseInt(text, 10);
|
|
13
|
-
// NaN → default; 0 is not a valid threshold (K9-010 AC: 'abc'/''/'0'
|
|
14
|
-
// all clamp to the default of three — never zero).
|
|
15
|
-
if (Number.isNaN(n) || n === 0)
|
|
16
|
-
return DEFAULT_THRESHOLD;
|
|
17
|
-
return Math.min(1000, Math.max(1, n));
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* v0.9.0 (K9-009/K9-010 / plan §5.3) — wraps the plugin's hooks with a
|
|
21
|
-
* success-path recorder, dedups per-session checkpoints, and reports each
|
|
22
|
-
* hook's liveness state. Persistence is machine-scoped: the hook_liveness
|
|
23
|
-
* table carries no project_id or repo_id (D9-08).
|
|
24
|
-
*/
|
|
25
|
-
export class HookLiveness {
|
|
26
|
-
store;
|
|
27
|
-
options;
|
|
28
|
-
// v1.1.0 (K11-015) — debug counter for excess arity; never logged on hot path
|
|
29
|
-
excessArityCount = 0;
|
|
30
|
-
counters;
|
|
31
|
-
seenSessions;
|
|
32
|
-
suppressedSessions = new Set();
|
|
33
|
-
threshold;
|
|
34
|
-
pluginVersion;
|
|
35
|
-
constructor(store, options) {
|
|
36
|
-
this.store = store;
|
|
37
|
-
this.options = options;
|
|
38
|
-
this.threshold = parseThreshold(options.thresholdText);
|
|
39
|
-
this.pluginVersion = options.pluginVersion;
|
|
40
|
-
this.counters = new Map();
|
|
41
|
-
this.seenSessions = new Map();
|
|
42
|
-
for (const hook of HOOK_NAMES) {
|
|
43
|
-
this.counters.set(hook, {
|
|
44
|
-
fireCount: 0,
|
|
45
|
-
errorCount: 0,
|
|
46
|
-
expectedCount: 0,
|
|
47
|
-
firstSeenAt: null,
|
|
48
|
-
lastSeenAt: null,
|
|
49
|
-
deadSince: null,
|
|
50
|
-
});
|
|
51
|
-
this.seenSessions.set(hook, new Set());
|
|
52
|
-
}
|
|
53
|
-
this.loadFromDb();
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Returns a new object with the same keys; function values are replaced
|
|
57
|
-
* by delegating wrappers, non-function values (the tool map) pass by
|
|
58
|
-
* reference. When liveness is disabled, returns the argument unchanged —
|
|
59
|
-
* the same object, not a copy.
|
|
60
|
-
*/
|
|
61
|
-
wrap(hooks) {
|
|
62
|
-
if (!this.options.enabled)
|
|
63
|
-
return hooks;
|
|
64
|
-
const out = {};
|
|
65
|
-
for (const [key, value] of Object.entries(hooks)) {
|
|
66
|
-
if (typeof value === "function") {
|
|
67
|
-
out[key] = this.makeWrapper(key, value);
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
out[key] = value;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return out;
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* v0.9.0 (K9-010 / plan §5.3) — a checkpoint: the session reached a
|
|
77
|
-
* model turn (tool.execute.after fired), so the system prompt was
|
|
78
|
-
* assembled and `experimental.chat.system.transform` MUST have been
|
|
79
|
-
* offered. Deduped per session: 20 tool calls in one session count
|
|
80
|
-
* exactly one expectation.
|
|
81
|
-
*/
|
|
82
|
-
expect(hook, sessionID) {
|
|
83
|
-
const seen = this.seenSessions.get(hook);
|
|
84
|
-
if (!seen || seen.has(sessionID))
|
|
85
|
-
return;
|
|
86
|
-
seen.add(sessionID);
|
|
87
|
-
const counters = this.counters.get(hook);
|
|
88
|
-
if (!counters)
|
|
89
|
-
return;
|
|
90
|
-
counters.expectedCount += 1;
|
|
91
|
-
// The dead flag is materialized here (not only in report()) so that
|
|
92
|
-
// flush() persists dead_since even when no report() was ever called:
|
|
93
|
-
// kevin_doctor's PURE-SQL blocks read dead_since from the table.
|
|
94
|
-
if (counters.fireCount === 0 &&
|
|
95
|
-
counters.expectedCount >= this.threshold &&
|
|
96
|
-
counters.deadSince === null) {
|
|
97
|
-
counters.deadSince = counters.lastSeenAt ?? new Date().toISOString();
|
|
98
|
-
}
|
|
99
|
-
// v0.9.0 (K9-011 / plan §5.3) — when the injection hook is dead, the
|
|
100
|
-
// checkpointed session was suppressed: the host stopped offering the
|
|
101
|
-
// transform, so no injection could happen. Counted once per session
|
|
102
|
-
// (same dedup as expected_count) and persisted by flush(). This is
|
|
103
|
-
// the counter that turns "zero injections" from an ambiguous number
|
|
104
|
-
// into a diagnosis.
|
|
105
|
-
if (hook === "experimental.chat.system.transform" &&
|
|
106
|
-
counters.deadSince !== null &&
|
|
107
|
-
!this.suppressedSessions.has(sessionID)) {
|
|
108
|
-
this.suppressedSessions.add(sessionID);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Per-hook liveness verdicts in canonical HOOK_NAMES order.
|
|
113
|
-
* A hook that fired once is live forever; dead requires both zero fires
|
|
114
|
-
* and `expectedCount >= threshold`; everything else is unknown — and
|
|
115
|
-
* unknown is never rounded to healthy (D9-09).
|
|
116
|
-
*/
|
|
117
|
-
report() {
|
|
118
|
-
const out = [];
|
|
119
|
-
for (const hook of HOOK_NAMES) {
|
|
120
|
-
const c = this.counters.get(hook);
|
|
121
|
-
if (!c)
|
|
122
|
-
continue;
|
|
123
|
-
let state;
|
|
124
|
-
if (c.fireCount > 0) {
|
|
125
|
-
state = "live";
|
|
126
|
-
}
|
|
127
|
-
else if (c.expectedCount >= this.threshold) {
|
|
128
|
-
// dead_since is set ONCE and never cleared: it documents the
|
|
129
|
-
// historical death even if the hook later recovers.
|
|
130
|
-
if (c.deadSince === null) {
|
|
131
|
-
c.deadSince = c.lastSeenAt ?? new Date().toISOString();
|
|
132
|
-
}
|
|
133
|
-
state = "dead";
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
state = "unknown";
|
|
137
|
-
}
|
|
138
|
-
out.push({
|
|
139
|
-
hook,
|
|
140
|
-
experimental: hook.startsWith("experimental."),
|
|
141
|
-
state,
|
|
142
|
-
firstSeenAt: c.firstSeenAt,
|
|
143
|
-
lastSeenAt: c.lastSeenAt,
|
|
144
|
-
fireCount: c.fireCount,
|
|
145
|
-
expectedCount: c.expectedCount,
|
|
146
|
-
deadSince: c.deadSince,
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
return out;
|
|
150
|
-
}
|
|
151
|
-
/** Persists every counter row. Called on the metrics.flush() cadence. */
|
|
152
|
-
flush() {
|
|
153
|
-
const store = this.store;
|
|
154
|
-
store.transaction(() => {
|
|
155
|
-
// Created lazily on first flush, mirroring Metrics.flush(): the
|
|
156
|
-
// migration guarantees the table in production, but unit tests
|
|
157
|
-
// against pre-010 DBs must not explode at dispose.
|
|
158
|
-
store.exec(`CREATE TABLE IF NOT EXISTS hook_liveness (
|
|
159
|
-
hook TEXT PRIMARY KEY,
|
|
160
|
-
experimental INTEGER NOT NULL DEFAULT 0,
|
|
161
|
-
fire_count INTEGER NOT NULL DEFAULT 0,
|
|
162
|
-
error_count INTEGER NOT NULL DEFAULT 0,
|
|
163
|
-
expected_count INTEGER NOT NULL DEFAULT 0,
|
|
164
|
-
first_seen_at TEXT,
|
|
165
|
-
last_seen_at TEXT,
|
|
166
|
-
dead_since TEXT,
|
|
167
|
-
plugin_version TEXT
|
|
168
|
-
)`);
|
|
169
|
-
const upsert = store.prepare(`INSERT INTO hook_liveness (hook, experimental, fire_count, error_count,
|
|
170
|
-
expected_count, first_seen_at, last_seen_at, dead_since, plugin_version)
|
|
171
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
172
|
-
ON CONFLICT(hook) DO UPDATE SET
|
|
173
|
-
experimental = excluded.experimental,
|
|
174
|
-
fire_count = excluded.fire_count,
|
|
175
|
-
error_count = excluded.error_count,
|
|
176
|
-
expected_count = excluded.expected_count,
|
|
177
|
-
first_seen_at = excluded.first_seen_at,
|
|
178
|
-
last_seen_at = excluded.last_seen_at,
|
|
179
|
-
dead_since = excluded.dead_since,
|
|
180
|
-
plugin_version = excluded.plugin_version`);
|
|
181
|
-
for (const [hook, c] of this.counters) {
|
|
182
|
-
upsert.run(hook, hook.startsWith("experimental.") ? 1 : 0, c.fireCount, c.errorCount, c.expectedCount, c.firstSeenAt, c.lastSeenAt, c.deadSince, this.pluginVersion);
|
|
183
|
-
}
|
|
184
|
-
// v0.9.0 (K9-011 / plan §5.3) — the four v0.9.0 counters are
|
|
185
|
-
// re-derived here, on the metrics cadence, exactly like the 009
|
|
186
|
-
// post-apply hook re-derives hooks_dead_total: SUM(fire_count),
|
|
187
|
-
// SUM(error_count), COUNT(dead_since) and the per-session
|
|
188
|
-
// suppression count. No Metrics dependency, no writes in the hot
|
|
189
|
-
// path — flush() is the single persistence point.
|
|
190
|
-
let fireSum = 0;
|
|
191
|
-
let errorSum = 0;
|
|
192
|
-
let deadCount = 0;
|
|
193
|
-
for (const c of this.counters.values()) {
|
|
194
|
-
fireSum += c.fireCount;
|
|
195
|
-
errorSum += c.errorCount;
|
|
196
|
-
if (c.deadSince !== null)
|
|
197
|
-
deadCount += 1;
|
|
198
|
-
}
|
|
199
|
-
store.exec(`CREATE TABLE IF NOT EXISTS kevin_metrics (
|
|
200
|
-
key TEXT PRIMARY KEY,
|
|
201
|
-
value INTEGER NOT NULL DEFAULT 0,
|
|
202
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
203
|
-
)`);
|
|
204
|
-
const derive = store.prepare(`INSERT INTO kevin_metrics (key, value, updated_at)
|
|
205
|
-
VALUES (?, ?, datetime('now'))
|
|
206
|
-
ON CONFLICT(key) DO UPDATE SET
|
|
207
|
-
value = excluded.value,
|
|
208
|
-
updated_at = datetime('now')`);
|
|
209
|
-
derive.run("hook_fires_total", fireSum);
|
|
210
|
-
derive.run("hook_errors_total", errorSum);
|
|
211
|
-
derive.run("hooks_dead_total", deadCount);
|
|
212
|
-
derive.run("injections_suppressed_dead_hook", this.suppressedSessions.size);
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
makeWrapper(key, delegate) {
|
|
216
|
-
const record = () => {
|
|
217
|
-
// v1.0.0 (K10-013) — dispose is recorded inside its own delegate
|
|
218
|
-
// (via recordDispose()) rather than here: the wrapper's post-return
|
|
219
|
-
// record would land after the store is closed and could never be
|
|
220
|
-
// persisted — the event being detected is the process ending.
|
|
221
|
-
if (key === "dispose")
|
|
222
|
-
return;
|
|
223
|
-
if (HOOK_NAMES.includes(key)) {
|
|
224
|
-
this.recordSuccess(key);
|
|
225
|
-
}
|
|
226
|
-
};
|
|
227
|
-
const recordError = (e) => {
|
|
228
|
-
if (HOOK_NAMES.includes(key)) {
|
|
229
|
-
this.recordError(key);
|
|
230
|
-
}
|
|
231
|
-
throw e;
|
|
232
|
-
};
|
|
233
|
-
// v1.1.0 (K11-015) — arity guard: maximum supported arity is 2 (plan §5.5).
|
|
234
|
-
// Excess args are sliced and counted via excessArityCount; never logged.
|
|
235
|
-
return async (...args) => {
|
|
236
|
-
let callArgs = args;
|
|
237
|
-
if (args.length > 2) {
|
|
238
|
-
this.excessArityCount++;
|
|
239
|
-
callArgs = args.slice(0, 2);
|
|
240
|
-
}
|
|
241
|
-
try {
|
|
242
|
-
const result = await delegate(...callArgs);
|
|
243
|
-
record();
|
|
244
|
-
return result;
|
|
245
|
-
}
|
|
246
|
-
catch (e) {
|
|
247
|
-
return recordError(e);
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* v1.0.0 (K10-013 / plan §5.3) — records the `dispose` fire and
|
|
253
|
-
* flushes immediately, from inside the dispose delegate: this is the
|
|
254
|
-
* last write of the process, and nothing after it can persist.
|
|
255
|
-
*/
|
|
256
|
-
recordDispose() {
|
|
257
|
-
this.recordSuccess("dispose");
|
|
258
|
-
this.flush();
|
|
259
|
-
}
|
|
260
|
-
recordSuccess(hook) {
|
|
261
|
-
const c = this.counters.get(hook);
|
|
262
|
-
if (!c)
|
|
263
|
-
return;
|
|
264
|
-
c.fireCount += 1;
|
|
265
|
-
const now = new Date().toISOString();
|
|
266
|
-
if (c.firstSeenAt === null)
|
|
267
|
-
c.firstSeenAt = now;
|
|
268
|
-
c.lastSeenAt = now;
|
|
269
|
-
}
|
|
270
|
-
recordError(hook) {
|
|
271
|
-
const c = this.counters.get(hook);
|
|
272
|
-
if (!c)
|
|
273
|
-
return;
|
|
274
|
-
c.errorCount += 1;
|
|
275
|
-
}
|
|
276
|
-
loadFromDb() {
|
|
277
|
-
let rows = [];
|
|
278
|
-
try {
|
|
279
|
-
rows = this.store
|
|
280
|
-
.prepare("SELECT hook, fire_count, error_count, expected_count, first_seen_at, last_seen_at, dead_since FROM hook_liveness")
|
|
281
|
-
.all();
|
|
282
|
-
}
|
|
283
|
-
catch {
|
|
284
|
-
// Graceful: pre-010 DBs have no hook_liveness table; the seeded
|
|
285
|
-
// zeros remain until the first flush creates rows.
|
|
286
|
-
rows = [];
|
|
287
|
-
}
|
|
288
|
-
for (const row of rows) {
|
|
289
|
-
if (!HOOK_NAMES.includes(row.hook))
|
|
290
|
-
continue;
|
|
291
|
-
const c = this.counters.get(row.hook);
|
|
292
|
-
if (!c)
|
|
293
|
-
continue;
|
|
294
|
-
c.fireCount = row.fire_count;
|
|
295
|
-
c.errorCount = row.error_count;
|
|
296
|
-
c.expectedCount = row.expected_count;
|
|
297
|
-
c.firstSeenAt = row.first_seen_at;
|
|
298
|
-
c.lastSeenAt = row.last_seen_at;
|
|
299
|
-
c.deadSince = row.dead_since;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
export function reduceVerdict(reports) {
|
|
304
|
-
if (reports.length === 0) {
|
|
305
|
-
return { verdict: "unknown", reason: "no hook reports yet" };
|
|
306
|
-
}
|
|
307
|
-
const dead = reports.filter((r) => r.state === "dead");
|
|
308
|
-
if (dead.length > 0) {
|
|
309
|
-
return {
|
|
310
|
-
verdict: "degraded",
|
|
311
|
-
reason: `${dead.map((r) => r.hook).join(", ")} dead since ${dead[0].deadSince ?? "unknown"}; ${dead.length} affected hook(s)`,
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
if (reports.every((r) => r.state === "live")) {
|
|
315
|
-
return { verdict: "healthy", reason: "all hooks live" };
|
|
316
|
-
}
|
|
317
|
-
const pending = reports.filter((r) => r.state === "unknown").length;
|
|
318
|
-
return {
|
|
319
|
-
verdict: "unknown",
|
|
320
|
-
reason: `${pending} hook(s) without checkpoint`,
|
|
321
|
-
};
|
|
322
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import type { Store } from "./Store.js";
|
|
2
|
-
import type { Metrics } from "./metrics.js";
|
|
3
|
-
/**
|
|
4
|
-
* v0.4.0 InjectionLedger (K4-006/K4-007, plan §5.2, D4-04).
|
|
5
|
-
*
|
|
6
|
-
* The measurement half of the closed feedback loop: every lesson injected
|
|
7
|
-
* into a prompt is recorded in `kevin_injections`, and at `session.idle` the
|
|
8
|
-
* ledger settles each unmeasured row — if the same fingerprint failed again
|
|
9
|
-
* after the injection, the lesson is `ineffective`; otherwise `effective`.
|
|
10
|
-
* `precision_rate = effective / total` is the honest, measured value of the
|
|
11
|
-
* injection system (replacing Kevin_Token_Impact-style estimates).
|
|
12
|
-
*
|
|
13
|
-
* Schema (migration 005_v04_signal.sql):
|
|
14
|
-
* kevin_injections(id PK, memory_id, fingerprint, session_id, hook,
|
|
15
|
-
* tokens, injected_at, outcome)
|
|
16
|
-
*
|
|
17
|
-
* BUG-015 — a ledger row is only meaningful when its fingerprint can be
|
|
18
|
-
* matched against failing tool_calls (`COALESCE(error_fingerprint,
|
|
19
|
-
* fingerprint)`). Memories WITHOUT a fingerprint (agent-saved notes) can
|
|
20
|
-
* never match, so they are NOT recorded: settle() would otherwise mark
|
|
21
|
-
* them `effective` forever and inflate `precision_rate`. The caller
|
|
22
|
-
* (ContextInjector.recordInjections) skips them.
|
|
23
|
-
*/
|
|
24
|
-
export type InjectionHook = "pre_prompt" | "compacting";
|
|
25
|
-
export type InjectionOutcome = "unmeasured" | "effective" | "ineffective" | "inconclusive";
|
|
26
|
-
export interface InjectionRecordInput {
|
|
27
|
-
memoryId: string;
|
|
28
|
-
fingerprint: string;
|
|
29
|
-
sessionId: string;
|
|
30
|
-
hook: InjectionHook;
|
|
31
|
-
tokens: number;
|
|
32
|
-
/**
|
|
33
|
-
* v0.8.0 (K8-024 / plan §5.7) — the memory's layer, passed by the
|
|
34
|
-
* caller (the injector already knows it — no lookup on the hot path).
|
|
35
|
-
* Drives the `injections_from_shared` counter.
|
|
36
|
-
*/
|
|
37
|
-
layer?: string | null;
|
|
38
|
-
}
|
|
39
|
-
interface InjectionRow {
|
|
40
|
-
id: string;
|
|
41
|
-
memory_id: string;
|
|
42
|
-
fingerprint: string;
|
|
43
|
-
session_id: string;
|
|
44
|
-
hook: InjectionHook;
|
|
45
|
-
tokens: number;
|
|
46
|
-
injected_at: string;
|
|
47
|
-
outcome: InjectionOutcome;
|
|
48
|
-
}
|
|
49
|
-
export declare class InjectionLedger {
|
|
50
|
-
private readonly store;
|
|
51
|
-
private readonly metrics;
|
|
52
|
-
constructor(store: Store, metrics?: Metrics | null);
|
|
53
|
-
/**
|
|
54
|
-
* Records one injected memory. Idempotent at the row level (UUID PK);
|
|
55
|
-
* duplicates are expected only via the caller's per-session seen-set.
|
|
56
|
-
*/
|
|
57
|
-
record(input: InjectionRecordInput): void;
|
|
58
|
-
/**
|
|
59
|
-
* Settles every unmeasured injection of the session: a fingerprint that
|
|
60
|
-
* failed again (as a failing tool_call) after the injection is
|
|
61
|
-
* `ineffective`, otherwise `effective`. Idempotent — only
|
|
62
|
-
* `outcome = 'unmeasured'` rows are flipped, and the recurrence charge
|
|
63
|
-
* is `MAX(recurrence_count, n)` where n = all failing calls of the
|
|
64
|
-
* fingerprint after `injected_at` (a later idle re-computes n and
|
|
65
|
-
* catches up — plan §5.1 rule 4: 3 recurrences → stale).
|
|
66
|
-
*
|
|
67
|
-
* An ineffective injection also bumps the target memory's
|
|
68
|
-
* `recurrence_count` (negative evidence, plan §5.3) and stamps
|
|
69
|
-
* `last_injected_at` (monotonically — an older row can never
|
|
70
|
-
* regress a newer injection's timestamp; the column means "most
|
|
71
|
-
* recent injection").
|
|
72
|
-
*/
|
|
73
|
-
settle(sessionId: string): void;
|
|
74
|
-
/**
|
|
75
|
-
* Per-fingerprint failing tool-call counts for the session. Feeds
|
|
76
|
-
* QualityGate.canInject and the HITL suggestion block.
|
|
77
|
-
*/
|
|
78
|
-
recurrencesFor(sessionId: string): Map<string, number>;
|
|
79
|
-
/**
|
|
80
|
-
* v0.4.0 (K4-017) — recurrence counts for the QualityGate at
|
|
81
|
-
* injection time: only failing calls that happened AFTER the
|
|
82
|
-
* fingerprint was already injected this session count. The failure
|
|
83
|
-
* that *created* a lesson is not a recurrence — it precedes any
|
|
84
|
-
* injection (plan §5.1 rule 4, same `ts >= injected_at` semantics
|
|
85
|
-
* `settle` uses).
|
|
86
|
-
*/
|
|
87
|
-
postInjectionRecurrencesFor(sessionId: string): Map<string, number>;
|
|
88
|
-
/** Number of rows for the session not yet settled (drives tests and settle). */
|
|
89
|
-
unsettledForSession(sessionId: string): number;
|
|
90
|
-
/** Latest ledger rows for a session, newest first (used by tests/tools). */
|
|
91
|
-
rowsForSession(sessionId: string): InjectionRow[];
|
|
92
|
-
/**
|
|
93
|
-
* v0.5.0 (K5-005 / plan §8.4) — one grouped rollup over the whole
|
|
94
|
-
* ledger, zero-filled for every outcome. Consumed by `kevin_audit`.
|
|
95
|
-
*/
|
|
96
|
-
outcomeCounts(): Record<InjectionOutcome, number>;
|
|
97
|
-
}
|
|
98
|
-
export {};
|