@jmtrin/opencode-kevin 1.0.0 → 1.2.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 +54 -8
- package/dist/migrations/001_initial.sql +91 -91
- package/dist/migrations/002_indexes.sql +13 -13
- package/dist/migrations/003_v02_signal.sql +57 -57
- package/dist/migrations/004_v03_knowledge.sql +138 -138
- package/dist/migrations/005_v04_signal.sql +57 -57
- package/dist/migrations/006_v05_glassbox.sql +118 -118
- package/dist/migrations/007_v06_pull.sql +144 -144
- package/dist/migrations/012_v11_drift.sql +24 -0
- package/dist/plugin/Archiver.js +2 -17
- package/dist/plugin/CausalChain.js +32 -13
- package/dist/plugin/ChatBridge.d.ts +41 -0
- package/dist/plugin/ChatBridge.js +103 -0
- package/dist/plugin/ConflictDetector.js +7 -29
- package/dist/plugin/DashboardHtml.d.ts +5 -0
- package/dist/plugin/DashboardHtml.js +180 -0
- package/dist/plugin/Feedback.js +2 -19
- package/dist/plugin/HookLiveness.d.ts +1 -0
- package/dist/plugin/HookLiveness.js +11 -27
- package/dist/plugin/InjectionLedger.js +104 -57
- package/dist/plugin/Materializer.js +1 -88
- package/dist/plugin/MemoryService.d.ts +59 -1
- package/dist/plugin/MemoryService.js +13 -106
- package/dist/plugin/Migrate.js +5 -0
- package/dist/plugin/Retrospective.js +7 -0
- package/dist/plugin/ToolCallObserver.js +18 -5
- package/dist/plugin/TuiActions.d.ts +43 -0
- package/dist/plugin/TuiActions.js +181 -0
- package/dist/plugin/TuiSnapshots.d.ts +24 -0
- package/dist/plugin/TuiSnapshots.js +158 -0
- package/dist/plugin/capabilities.d.ts +2 -0
- package/dist/plugin/capabilities.js +3 -0
- package/dist/plugin/columns.d.ts +11 -0
- package/dist/plugin/columns.js +54 -0
- package/dist/plugin/contract.d.ts +8 -0
- package/dist/plugin/contract.js +23 -5
- package/dist/plugin/index.d.ts +2 -2
- package/dist/plugin/index.js +315 -10
- package/dist/plugin/kevin_audit.d.ts +17 -1
- package/dist/plugin/kevin_audit.js +69 -1
- package/dist/plugin/kevin_forget.d.ts +33 -0
- package/dist/plugin/kevin_forget.js +260 -0
- package/dist/plugin/kevin_why.js +1 -18
- package/dist/plugin/metrics.d.ts +1 -1
- package/dist/plugin/metrics.js +8 -0
- package/dist/plugin/query-tokenizer.js +56 -8
- package/dist/plugin/time-ms.d.ts +1 -0
- package/dist/plugin/time-ms.js +16 -0
- package/dist/plugin/tui-types.d.ts +59 -0
- package/dist/plugin/tui-types.js +4 -0
- package/dist/plugin/tui.d.ts +18 -0
- package/dist/plugin/tui.js +198 -0
- package/package.json +8 -2
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
1
4
|
import { effectivePrePromptCap } from "./ContextInjector.js";
|
|
2
5
|
import { hasRepoIdColumn } from "./MemoryService.js";
|
|
3
6
|
import { contractDigest, describeContract } from "./contract.js";
|
|
@@ -21,7 +24,7 @@ function scalar(store, sql, ...params) {
|
|
|
21
24
|
const row = store.prepare(sql).get(...params);
|
|
22
25
|
return row.n ?? 0;
|
|
23
26
|
}
|
|
24
|
-
export function buildAudit(store, metrics, capabilities = ALL_FALSE_CAPABILITIES, projectId, repoId) {
|
|
27
|
+
export function buildAudit(store, metrics, capabilities = ALL_FALSE_CAPABILITIES, projectId, repoId, tuiRoot) {
|
|
25
28
|
let partial = false;
|
|
26
29
|
// Memories block: `total` + the three GROUP BYs are pre-006 safe; the
|
|
27
30
|
// lifecycle counters need the 006 columns and fall back separately.
|
|
@@ -542,6 +545,70 @@ export function buildAudit(store, metrics, capabilities = ALL_FALSE_CAPABILITIES
|
|
|
542
545
|
clause_count: liveContract.clauses.length,
|
|
543
546
|
deprecated_count: liveContract.clauses.filter((c) => c.deprecated).length,
|
|
544
547
|
};
|
|
548
|
+
// v1.2.0 (K12-014/K12-019) — TUI block: best-effort fs introspection (absent → nulls)
|
|
549
|
+
let tui;
|
|
550
|
+
try {
|
|
551
|
+
const enabled_setting = settings.tui_snapshots_enabled ?? "0";
|
|
552
|
+
const root = tuiRoot ?? join(homedir(), ".opencode-kevin", "tui");
|
|
553
|
+
let last_flush_age_s = null;
|
|
554
|
+
try {
|
|
555
|
+
const metaRaw = readFileSync(join(root, "meta.json"), "utf8");
|
|
556
|
+
const meta = JSON.parse(metaRaw);
|
|
557
|
+
if (meta.generatedAt) {
|
|
558
|
+
const age = (Date.now() - Date.parse(meta.generatedAt)) / 1000;
|
|
559
|
+
if (Number.isFinite(age) && age >= 0)
|
|
560
|
+
last_flush_age_s = Math.floor(age);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
catch { }
|
|
564
|
+
let mailbox_depth = null;
|
|
565
|
+
try {
|
|
566
|
+
if (existsSync(join(root, "actions.json"))) {
|
|
567
|
+
const raw = readFileSync(join(root, "actions.json"), "utf8");
|
|
568
|
+
const parsed = JSON.parse(raw);
|
|
569
|
+
if (Array.isArray(parsed.actions))
|
|
570
|
+
mailbox_depth = parsed.actions.length;
|
|
571
|
+
else
|
|
572
|
+
mailbox_depth = null;
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
mailbox_depth = null;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
mailbox_depth = null;
|
|
580
|
+
}
|
|
581
|
+
let last_results = null;
|
|
582
|
+
try {
|
|
583
|
+
const raw = readFileSync(join(root, "results.json"), "utf8");
|
|
584
|
+
last_results = JSON.parse(raw);
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
last_results = null;
|
|
588
|
+
}
|
|
589
|
+
let dashboard_last_write_age_s = null;
|
|
590
|
+
try {
|
|
591
|
+
const st = statSync(join(root, "dashboard.html"));
|
|
592
|
+
const age = (Date.now() - st.mtimeMs) / 1000;
|
|
593
|
+
if (Number.isFinite(age) && age >= 0)
|
|
594
|
+
dashboard_last_write_age_s = Math.floor(age);
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
dashboard_last_write_age_s = null;
|
|
598
|
+
}
|
|
599
|
+
const bridge_interceptions = metrics.get("tui_actions_invoked") ?? 0;
|
|
600
|
+
tui = {
|
|
601
|
+
enabled_setting,
|
|
602
|
+
last_flush_age_s,
|
|
603
|
+
mailbox_depth,
|
|
604
|
+
last_results,
|
|
605
|
+
dashboard_last_write_age_s,
|
|
606
|
+
bridge_interceptions,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
tui = undefined;
|
|
611
|
+
}
|
|
545
612
|
return {
|
|
546
613
|
memories,
|
|
547
614
|
injections,
|
|
@@ -558,6 +625,7 @@ export function buildAudit(store, metrics, capabilities = ALL_FALSE_CAPABILITIES
|
|
|
558
625
|
host,
|
|
559
626
|
perf,
|
|
560
627
|
contract,
|
|
628
|
+
tui,
|
|
561
629
|
partial,
|
|
562
630
|
};
|
|
563
631
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { MemoryService } from "./MemoryService.js";
|
|
2
|
+
import type { SharedLayer } from "./SharedLayer.js";
|
|
3
|
+
import type { Store } from "./Store.js";
|
|
4
|
+
import type { Metrics } from "./metrics.js";
|
|
5
|
+
export interface ForgetInput {
|
|
6
|
+
ids: string[];
|
|
7
|
+
confirm?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface Deps {
|
|
10
|
+
store: Store;
|
|
11
|
+
memoryService: MemoryService;
|
|
12
|
+
sharedLayer: SharedLayer;
|
|
13
|
+
okfPath: string;
|
|
14
|
+
metrics: Metrics;
|
|
15
|
+
}
|
|
16
|
+
export interface ForgetResult {
|
|
17
|
+
action: "forget";
|
|
18
|
+
ok: boolean;
|
|
19
|
+
dry_run: boolean;
|
|
20
|
+
per_id: Array<{
|
|
21
|
+
id: string;
|
|
22
|
+
archived: boolean;
|
|
23
|
+
reason?: string;
|
|
24
|
+
tombstone?: {
|
|
25
|
+
entry_id: string;
|
|
26
|
+
planned: boolean;
|
|
27
|
+
applied: boolean;
|
|
28
|
+
};
|
|
29
|
+
}>;
|
|
30
|
+
noop?: boolean;
|
|
31
|
+
reason?: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function handleForget(input: ForgetInput, deps: Deps): ForgetResult;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { computeEntryId } from "./okf.js";
|
|
2
|
+
export function handleForget(input, deps) {
|
|
3
|
+
// v1.1.0 — every invocation counts, including dry runs and refusals (K11-005)
|
|
4
|
+
try {
|
|
5
|
+
deps.metrics.incr("forget_requests_total", 1);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
// best-effort
|
|
9
|
+
}
|
|
10
|
+
if (!input.ids || input.ids.length === 0) {
|
|
11
|
+
return {
|
|
12
|
+
action: "forget",
|
|
13
|
+
ok: false,
|
|
14
|
+
dry_run: input.confirm !== true,
|
|
15
|
+
per_id: [],
|
|
16
|
+
reason: "no_ids",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const isDry = input.confirm !== true;
|
|
20
|
+
const per_id = [];
|
|
21
|
+
let anyChange = false;
|
|
22
|
+
let anyTombstonePlanned = false;
|
|
23
|
+
// Helper to compute entry_id for a memory row
|
|
24
|
+
const getEntryId = (row) => {
|
|
25
|
+
if (row.shared_entry_id)
|
|
26
|
+
return row.shared_entry_id;
|
|
27
|
+
return computeEntryId(row.type, row.content, row.scope);
|
|
28
|
+
};
|
|
29
|
+
// For dry_run: just plan, mutate nothing
|
|
30
|
+
if (isDry) {
|
|
31
|
+
for (const id of input.ids) {
|
|
32
|
+
const row = deps.store
|
|
33
|
+
.prepare("SELECT id, type, content, scope, status, layer, shared_entry_id FROM memories WHERE id = ?")
|
|
34
|
+
.get(id);
|
|
35
|
+
if (!row) {
|
|
36
|
+
per_id.push({ id, archived: false, reason: "not_found" });
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (row.status === "archived") {
|
|
40
|
+
// Idempotence: already archived
|
|
41
|
+
const isShared = row.layer === "shared" || row.shared_entry_id !== null;
|
|
42
|
+
if (isShared) {
|
|
43
|
+
const entryId = getEntryId(row);
|
|
44
|
+
try {
|
|
45
|
+
const plan = deps.sharedLayer.planTombstone([entryId], deps.okfPath);
|
|
46
|
+
const planned = plan.write.outcome !== "refused" && plan.entriesAdded > 0;
|
|
47
|
+
// Already archived implies tombstone already applied, so planned should be false (noop)
|
|
48
|
+
per_id.push({
|
|
49
|
+
id,
|
|
50
|
+
archived: false,
|
|
51
|
+
reason: "already_archived",
|
|
52
|
+
tombstone: { entry_id: entryId, planned: false, applied: false },
|
|
53
|
+
});
|
|
54
|
+
void plan;
|
|
55
|
+
void planned;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
per_id.push({
|
|
59
|
+
id,
|
|
60
|
+
archived: false,
|
|
61
|
+
reason: "already_archived",
|
|
62
|
+
tombstone: { entry_id: entryId, planned: false, applied: false },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
per_id.push({ id, archived: false, reason: "already_archived" });
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
// Would archive
|
|
72
|
+
anyChange = true;
|
|
73
|
+
const isShared = row.layer === "shared" || row.shared_entry_id !== null;
|
|
74
|
+
if (isShared) {
|
|
75
|
+
const entryId = getEntryId(row);
|
|
76
|
+
let planned = false;
|
|
77
|
+
try {
|
|
78
|
+
const plan = deps.sharedLayer.planTombstone([entryId], deps.okfPath);
|
|
79
|
+
// planTombstone returns ExportPlan with write outcome; if refused, planned false
|
|
80
|
+
planned = plan.write.outcome !== "refused";
|
|
81
|
+
anyTombstonePlanned = anyTombstonePlanned || planned;
|
|
82
|
+
per_id.push({
|
|
83
|
+
id,
|
|
84
|
+
archived: true,
|
|
85
|
+
tombstone: { entry_id: entryId, planned, applied: false },
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
per_id.push({
|
|
90
|
+
id,
|
|
91
|
+
archived: true,
|
|
92
|
+
tombstone: { entry_id: entryId, planned: false, applied: false },
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
per_id.push({ id, archived: true });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const noop = !anyChange;
|
|
101
|
+
return {
|
|
102
|
+
action: "forget",
|
|
103
|
+
ok: true,
|
|
104
|
+
dry_run: true,
|
|
105
|
+
per_id,
|
|
106
|
+
...(noop ? { noop: true } : {}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// Apply mode: confirm === true
|
|
110
|
+
// We need to archive locally and publish tombstones through applyExport
|
|
111
|
+
let partial = false;
|
|
112
|
+
let appliedTombstones = 0;
|
|
113
|
+
for (const id of input.ids) {
|
|
114
|
+
const row = deps.store
|
|
115
|
+
.prepare("SELECT id, type, content, scope, status, layer, shared_entry_id FROM memories WHERE id = ?")
|
|
116
|
+
.get(id);
|
|
117
|
+
if (!row) {
|
|
118
|
+
per_id.push({ id, archived: false, reason: "not_found" });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (row.status === "archived") {
|
|
122
|
+
const isShared = row.layer === "shared" || row.shared_entry_id !== null;
|
|
123
|
+
if (isShared) {
|
|
124
|
+
const entryId = getEntryId(row);
|
|
125
|
+
per_id.push({
|
|
126
|
+
id,
|
|
127
|
+
archived: false,
|
|
128
|
+
reason: "already_archived",
|
|
129
|
+
tombstone: { entry_id: entryId, planned: false, applied: false },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
per_id.push({ id, archived: false, reason: "already_archived" });
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
// Archive locally in a transaction
|
|
138
|
+
let archived = false;
|
|
139
|
+
try {
|
|
140
|
+
deps.store.transaction(() => {
|
|
141
|
+
deps.store
|
|
142
|
+
.prepare(`UPDATE memories SET status = 'archived', archived_at = datetime('now'), updated_at = datetime('now') WHERE id = ? AND status != 'archived'`)
|
|
143
|
+
.run(id);
|
|
144
|
+
const changes = deps.store.prepare("SELECT changes() AS c").get();
|
|
145
|
+
if (changes.c > 0)
|
|
146
|
+
archived = true;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
per_id.push({ id, archived: false, reason: "db_error" });
|
|
151
|
+
partial = true;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
anyChange = anyChange || archived;
|
|
155
|
+
const isShared = row.layer === "shared" || row.shared_entry_id !== null;
|
|
156
|
+
if (!isShared) {
|
|
157
|
+
per_id.push({ id, archived });
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const entryId = getEntryId(row);
|
|
161
|
+
// Plan tombstone
|
|
162
|
+
let plan;
|
|
163
|
+
try {
|
|
164
|
+
plan = deps.sharedLayer.planTombstone([entryId], deps.okfPath);
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
// plan failure: rollback DB archive for this id (best-effort)
|
|
168
|
+
try {
|
|
169
|
+
deps.store
|
|
170
|
+
.prepare(`UPDATE memories SET status = 'active', archived_at = NULL WHERE id = ?`)
|
|
171
|
+
.run(id);
|
|
172
|
+
}
|
|
173
|
+
catch { }
|
|
174
|
+
per_id.push({
|
|
175
|
+
id,
|
|
176
|
+
archived: false,
|
|
177
|
+
reason: "plan_failed",
|
|
178
|
+
tombstone: { entry_id: entryId, planned: false, applied: false },
|
|
179
|
+
});
|
|
180
|
+
partial = true;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (plan.write.outcome === "refused") {
|
|
184
|
+
// Refusal reasons are reused verbatim (repo_mismatch, unknown_entry)
|
|
185
|
+
const reason = plan.write.reason ?? "refused";
|
|
186
|
+
per_id.push({
|
|
187
|
+
id,
|
|
188
|
+
archived,
|
|
189
|
+
reason,
|
|
190
|
+
tombstone: { entry_id: entryId, planned: false, applied: false },
|
|
191
|
+
});
|
|
192
|
+
// DB archive already done; keep it (local archive is independent of shared refusal)
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
// Apply through single write funnel
|
|
196
|
+
try {
|
|
197
|
+
const applied = deps.sharedLayer.applyExport(plan);
|
|
198
|
+
const wasWritten = applied.applied === "written";
|
|
199
|
+
const wasNoop = applied.applied === "noop";
|
|
200
|
+
if (wasWritten)
|
|
201
|
+
appliedTombstones++;
|
|
202
|
+
// per_id tombstone: planned true if not refused, applied true only if written
|
|
203
|
+
per_id.push({
|
|
204
|
+
id,
|
|
205
|
+
archived,
|
|
206
|
+
tombstone: {
|
|
207
|
+
entry_id: entryId,
|
|
208
|
+
planned: true,
|
|
209
|
+
applied: wasWritten,
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
void wasNoop;
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
// v1.1.0 — failure mid-way: transaction rollback restores DB; already-applied OKF write is reported honestly
|
|
216
|
+
// For this id, DB was already archived, but file write failed. We attempt to rollback DB for this id.
|
|
217
|
+
try {
|
|
218
|
+
deps.store
|
|
219
|
+
.prepare(`UPDATE memories SET status = 'active', archived_at = NULL WHERE id = ?`)
|
|
220
|
+
.run(id);
|
|
221
|
+
archived = false;
|
|
222
|
+
}
|
|
223
|
+
catch { }
|
|
224
|
+
per_id.push({
|
|
225
|
+
id,
|
|
226
|
+
archived: false,
|
|
227
|
+
reason: "partial",
|
|
228
|
+
tombstone: { entry_id: entryId, planned: true, applied: false },
|
|
229
|
+
});
|
|
230
|
+
partial = true;
|
|
231
|
+
// Continue to next id? According spec, failure mid-way reports ok:false reason partial
|
|
232
|
+
// We keep processing remaining ids? For now we continue but mark partial.
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Metrics: increment forget_tombstones_published by applied count (only when written, not noop)
|
|
236
|
+
if (appliedTombstones > 0) {
|
|
237
|
+
try {
|
|
238
|
+
deps.metrics.incr("forget_tombstones_published", appliedTombstones);
|
|
239
|
+
}
|
|
240
|
+
catch { }
|
|
241
|
+
}
|
|
242
|
+
const noop = !anyChange && per_id.every((p) => p.archived === false);
|
|
243
|
+
if (partial) {
|
|
244
|
+
return {
|
|
245
|
+
action: "forget",
|
|
246
|
+
ok: false,
|
|
247
|
+
dry_run: false,
|
|
248
|
+
per_id,
|
|
249
|
+
reason: "partial",
|
|
250
|
+
...(noop ? { noop: true } : {}),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
action: "forget",
|
|
255
|
+
ok: true,
|
|
256
|
+
dry_run: false,
|
|
257
|
+
per_id,
|
|
258
|
+
...(noop ? { noop: true } : {}),
|
|
259
|
+
};
|
|
260
|
+
}
|
package/dist/plugin/kevin_why.js
CHANGED
|
@@ -1,24 +1,7 @@
|
|
|
1
1
|
import { TS_CODE_RULES } from "./Reflector.js";
|
|
2
|
+
import { hasFeedbackColumns } from "./columns.js";
|
|
2
3
|
import { computeConfidence } from "./confidence.js";
|
|
3
4
|
import { toMatchClause, tokenizeQuery } from "./query-tokenizer.js";
|
|
4
|
-
// v0.5.0 (K5-010 / plan §5.3) — pre-006 DBs lack the feedback columns;
|
|
5
|
-
// kevin_why must not reference them (it degrades via the try/catch below,
|
|
6
|
-
// which is for missing ROWS, not missing COLUMNS). Probe once per store.
|
|
7
|
-
const feedbackColumnCache = new WeakMap();
|
|
8
|
-
function hasFeedbackColumns(store) {
|
|
9
|
-
const cached = feedbackColumnCache.get(store);
|
|
10
|
-
if (cached !== undefined)
|
|
11
|
-
return cached;
|
|
12
|
-
try {
|
|
13
|
-
store.prepare("SELECT feedback_positive FROM memories LIMIT 1").get();
|
|
14
|
-
feedbackColumnCache.set(store, true);
|
|
15
|
-
return true;
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
feedbackColumnCache.set(store, false);
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
5
|
export function kevinWhy(store, query) {
|
|
23
6
|
// v0.3.0 fix (bug #7) — the old code wrapped the WHOLE query in a
|
|
24
7
|
// single quoted phrase, so it only matched the exact full-string
|
package/dist/plugin/metrics.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { Store } from "./Store.js";
|
|
|
6
6
|
* underlying table is empty (e.g., before 003 is applied, on a fresh
|
|
7
7
|
* :memory: test DB, or after a manual wipe).
|
|
8
8
|
*/
|
|
9
|
-
export declare const METRIC_KEYS: readonly ["tokens_injected_pre_prompt", "tokens_injected_compacting", "reflections_throttled", "duplicate_suppressions", "tool_calls_deduped", "patterns_mined", "patterns_causal", "causal_links", "memories_superseded", "injections_total", "injections_effective", "injections_ineffective", "patterns_promoted_new", "injections_inconclusive", "injections_blocked_seen", "injections_blocked_weak", "injections_blocked_recurrence", "injections_blocked_stale", "injections_blocked_ignored", "feedback_positive_total", "feedback_negative_total", "memories_archived", "proposals_created", "proposals_approved", "proposals_rejected", "artifact_writes_total", "artifact_writes_noop", "injections_blocked_confidence", "repo_facts_scanned", "memories_contradicted", "conventions_mined", "conflicts_detected", "error_lessons_suppressed", "shared_entries_total", "shared_entries_imported", "shared_entries_exported", "okf_merge_folds", "rekey_events", "injections_from_shared"];
|
|
9
|
+
export declare const METRIC_KEYS: readonly ["tokens_injected_pre_prompt", "tokens_injected_compacting", "reflections_throttled", "duplicate_suppressions", "tool_calls_deduped", "patterns_mined", "patterns_causal", "causal_links", "memories_superseded", "injections_total", "injections_effective", "injections_ineffective", "patterns_promoted_new", "injections_inconclusive", "injections_blocked_seen", "injections_blocked_weak", "injections_blocked_recurrence", "injections_blocked_stale", "injections_blocked_ignored", "feedback_positive_total", "feedback_negative_total", "memories_archived", "proposals_created", "proposals_approved", "proposals_rejected", "artifact_writes_total", "artifact_writes_noop", "injections_blocked_confidence", "repo_facts_scanned", "memories_contradicted", "conventions_mined", "conflicts_detected", "error_lessons_suppressed", "shared_entries_total", "shared_entries_imported", "shared_entries_exported", "okf_merge_folds", "rekey_events", "injections_from_shared", "bench_regression_failures", "forget_requests_total", "forget_tombstones_published", "tui_snapshots_flushed", "tui_actions_invoked"];
|
|
10
10
|
export type MetricKey = (typeof METRIC_KEYS)[number];
|
|
11
11
|
/**
|
|
12
12
|
* Cheap token estimate used when bumping the `tokens_injected_*` counters.
|
package/dist/plugin/metrics.js
CHANGED
|
@@ -57,6 +57,14 @@ export const METRIC_KEYS = [
|
|
|
57
57
|
"okf_merge_folds",
|
|
58
58
|
"rekey_events",
|
|
59
59
|
"injections_from_shared",
|
|
60
|
+
// v1.1.0 (K11-001 / plan §4, D11-01) — drift metrics; order matches 012 seed.
|
|
61
|
+
"bench_regression_failures",
|
|
62
|
+
"forget_requests_total",
|
|
63
|
+
"forget_tombstones_published",
|
|
64
|
+
// v1.2.0 (K12-001 / plan §4) — surface metrics; no migration this
|
|
65
|
+
// release — rows are created on first incr via upsert (K12-001).
|
|
66
|
+
"tui_snapshots_flushed",
|
|
67
|
+
"tui_actions_invoked",
|
|
60
68
|
];
|
|
61
69
|
const DEFAULT_FLUSH_MS = 1000;
|
|
62
70
|
function zeroCache() {
|
|
@@ -4,68 +4,116 @@
|
|
|
4
4
|
* Injection recall ORs the quoted tokens (`"t1" OR "t2"`) while
|
|
5
5
|
* `kevin_why` ANDs them (`"t1" AND "t2"`).
|
|
6
6
|
*/
|
|
7
|
+
// v1.1.0 (K11-012 / plan §5.5, D11-05) — single source for STOP_WORDS (union of three lists)
|
|
7
8
|
export const STOP_WORDS = new Set([
|
|
8
9
|
"a",
|
|
10
|
+
"about",
|
|
11
|
+
"after",
|
|
12
|
+
"again",
|
|
13
|
+
"all",
|
|
14
|
+
"also",
|
|
9
15
|
"an",
|
|
10
16
|
"and",
|
|
17
|
+
"any",
|
|
11
18
|
"are",
|
|
19
|
+
"as",
|
|
12
20
|
"at",
|
|
13
21
|
"be",
|
|
14
22
|
"been",
|
|
23
|
+
"before",
|
|
24
|
+
"being",
|
|
15
25
|
"but",
|
|
16
26
|
"by",
|
|
27
|
+
"can",
|
|
28
|
+
"como",
|
|
29
|
+
"con",
|
|
30
|
+
"could",
|
|
31
|
+
"de",
|
|
17
32
|
"did",
|
|
18
33
|
"do",
|
|
19
34
|
"does",
|
|
20
35
|
"el",
|
|
36
|
+
"en",
|
|
21
37
|
"eso",
|
|
22
38
|
"for",
|
|
39
|
+
"from",
|
|
40
|
+
"had",
|
|
41
|
+
"has",
|
|
42
|
+
"have",
|
|
43
|
+
"he",
|
|
44
|
+
"her",
|
|
45
|
+
"his",
|
|
23
46
|
"how",
|
|
24
47
|
"i",
|
|
25
48
|
"if",
|
|
26
49
|
"in",
|
|
50
|
+
"into",
|
|
27
51
|
"is",
|
|
28
52
|
"it",
|
|
53
|
+
"its",
|
|
29
54
|
"la",
|
|
30
55
|
"las",
|
|
31
56
|
"los",
|
|
57
|
+
"may",
|
|
32
58
|
"mi",
|
|
59
|
+
"might",
|
|
60
|
+
"more",
|
|
61
|
+
"most",
|
|
62
|
+
"must",
|
|
33
63
|
"my",
|
|
64
|
+
"not",
|
|
34
65
|
"o",
|
|
35
66
|
"of",
|
|
36
67
|
"on",
|
|
68
|
+
"one",
|
|
37
69
|
"or",
|
|
70
|
+
"our",
|
|
38
71
|
"para",
|
|
72
|
+
"per",
|
|
39
73
|
"por",
|
|
40
74
|
"que",
|
|
75
|
+
"shall",
|
|
41
76
|
"she",
|
|
77
|
+
"sin",
|
|
78
|
+
"so",
|
|
42
79
|
"su",
|
|
80
|
+
"than",
|
|
43
81
|
"that",
|
|
44
82
|
"the",
|
|
83
|
+
"their",
|
|
84
|
+
"them",
|
|
85
|
+
"then",
|
|
86
|
+
"there",
|
|
87
|
+
"these",
|
|
88
|
+
"they",
|
|
45
89
|
"this",
|
|
90
|
+
"those",
|
|
91
|
+
"through",
|
|
46
92
|
"to",
|
|
93
|
+
"too",
|
|
47
94
|
"tu",
|
|
48
95
|
"un",
|
|
49
96
|
"una",
|
|
97
|
+
"under",
|
|
98
|
+
"up",
|
|
99
|
+
"us",
|
|
100
|
+
"via",
|
|
101
|
+
"was",
|
|
50
102
|
"we",
|
|
51
103
|
"were",
|
|
52
104
|
"what",
|
|
53
105
|
"when",
|
|
54
106
|
"where",
|
|
55
107
|
"which",
|
|
108
|
+
"while",
|
|
56
109
|
"who",
|
|
57
110
|
"why",
|
|
111
|
+
"will",
|
|
58
112
|
"with",
|
|
113
|
+
"would",
|
|
59
114
|
"y",
|
|
60
115
|
"you",
|
|
61
|
-
"
|
|
62
|
-
"con",
|
|
63
|
-
"de",
|
|
64
|
-
"en",
|
|
65
|
-
"he",
|
|
66
|
-
"they",
|
|
67
|
-
"was",
|
|
68
|
-
"sin",
|
|
116
|
+
"your",
|
|
69
117
|
]);
|
|
70
118
|
/** Lowercase, split on whitespace, drop stopwords. Returns raw tokens. */
|
|
71
119
|
export function tokenizeQuery(query) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function toMs(legacyValue: string | null | undefined, msValue: number | null | undefined): number | null;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// v1.1.0 (K11-003/K11-004 / plan §5.2, D11-01) — millisecond helper.
|
|
2
|
+
// Readers prefer _ms and fall back to legacy second-granularity column.
|
|
3
|
+
// The helper is the single implementation imported by InjectionLedger and
|
|
4
|
+
// CausalChain (moved here in K11-004 to avoid duplication).
|
|
5
|
+
export function toMs(legacyValue, msValue) {
|
|
6
|
+
if (typeof msValue === "number" && !Number.isNaN(msValue))
|
|
7
|
+
return msValue;
|
|
8
|
+
if (!legacyValue)
|
|
9
|
+
return null;
|
|
10
|
+
// SQLite datetime('now') is 'YYYY-MM-DD HH:MM:SS' UTC
|
|
11
|
+
const iso = legacyValue.includes("T")
|
|
12
|
+
? legacyValue
|
|
13
|
+
: `${legacyValue.replace(" ", "T")}Z`;
|
|
14
|
+
const n = Date.parse(iso);
|
|
15
|
+
return Number.isNaN(n) ? null : n;
|
|
16
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface ProposalView {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly kind: string;
|
|
4
|
+
readonly target_path: string;
|
|
5
|
+
readonly diff: string;
|
|
6
|
+
readonly memory_ids: readonly string[];
|
|
7
|
+
readonly created_at: string;
|
|
8
|
+
readonly truncated?: boolean;
|
|
9
|
+
readonly token?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ConflictView {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly kind: string;
|
|
14
|
+
readonly a_summary: string;
|
|
15
|
+
readonly b_summary: string;
|
|
16
|
+
readonly opened_at: string;
|
|
17
|
+
}
|
|
18
|
+
export interface HealthView {
|
|
19
|
+
readonly verdict: string;
|
|
20
|
+
readonly reason: string;
|
|
21
|
+
readonly hooks: readonly {
|
|
22
|
+
readonly hook: string;
|
|
23
|
+
readonly state: string;
|
|
24
|
+
readonly fire_count: number;
|
|
25
|
+
readonly expected_count: number;
|
|
26
|
+
}[];
|
|
27
|
+
readonly perf: readonly {
|
|
28
|
+
readonly scope: string;
|
|
29
|
+
readonly p95: number;
|
|
30
|
+
readonly budget_p95: number;
|
|
31
|
+
readonly within_budget: boolean;
|
|
32
|
+
}[];
|
|
33
|
+
readonly contract_digest: string;
|
|
34
|
+
readonly counters: Record<string, number>;
|
|
35
|
+
}
|
|
36
|
+
export interface TuiSnapshotSet {
|
|
37
|
+
readonly generatedAt: string;
|
|
38
|
+
readonly proposals: readonly ProposalView[];
|
|
39
|
+
readonly conflicts: readonly ConflictView[];
|
|
40
|
+
readonly health: HealthView;
|
|
41
|
+
}
|
|
42
|
+
export type TuiAction = {
|
|
43
|
+
readonly type: "approve";
|
|
44
|
+
readonly proposalId: string;
|
|
45
|
+
readonly token: string;
|
|
46
|
+
} | {
|
|
47
|
+
readonly type: "reject";
|
|
48
|
+
readonly proposalId: string;
|
|
49
|
+
readonly token: string;
|
|
50
|
+
readonly note?: string;
|
|
51
|
+
} | {
|
|
52
|
+
readonly type: "acknowledge";
|
|
53
|
+
readonly conflictId: string;
|
|
54
|
+
};
|
|
55
|
+
export interface ActionResult {
|
|
56
|
+
readonly action: TuiAction;
|
|
57
|
+
readonly status: "applied" | "rejected" | "stale_skipped" | "error";
|
|
58
|
+
readonly detail?: string;
|
|
59
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TuiPlugin } from "@opencode-ai/plugin/tui";
|
|
2
|
+
import type { ConflictView, HealthView, ProposalView } from "./tui-types.js";
|
|
3
|
+
export declare function tuiRoot(): string;
|
|
4
|
+
export declare function readJsonSafe(name: string): {
|
|
5
|
+
data: unknown;
|
|
6
|
+
} | {
|
|
7
|
+
error: "missing" | "corrupt";
|
|
8
|
+
};
|
|
9
|
+
export declare function truncateSummary(text: string, max?: number): string;
|
|
10
|
+
export declare function formatProposalRow(p: ProposalView): string;
|
|
11
|
+
export declare function formatConflictRow(c: ConflictView): string;
|
|
12
|
+
export declare function formatHealthVerdict(h: HealthView): string;
|
|
13
|
+
export declare const tui: TuiPlugin;
|
|
14
|
+
declare const _default: {
|
|
15
|
+
id: string;
|
|
16
|
+
tui: TuiPlugin;
|
|
17
|
+
};
|
|
18
|
+
export default _default;
|