@jmtrin/opencode-kevin 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +24 -7
- 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/ConflictDetector.js +7 -29
- 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 +4 -0
- package/dist/plugin/ToolCallObserver.js +18 -5
- 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 +20 -5
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/index.js +24 -3
- 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 +4 -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/package.json +2 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const cache = new WeakMap();
|
|
2
|
+
export function hasColumn(store, table, column) {
|
|
3
|
+
const byStore = cache.get(store);
|
|
4
|
+
const key = `${table}.${column}`;
|
|
5
|
+
if (byStore?.get(key) === true)
|
|
6
|
+
return true;
|
|
7
|
+
try {
|
|
8
|
+
store.prepare(`SELECT ${column} FROM ${table} LIMIT 0`).get();
|
|
9
|
+
let m = byStore;
|
|
10
|
+
if (!m) {
|
|
11
|
+
m = new Map();
|
|
12
|
+
cache.set(store, m);
|
|
13
|
+
}
|
|
14
|
+
m.set(key, true);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// v1.1.0 (K11-011 / plan §5.5, D11-06) — named helpers delegating to the registry
|
|
22
|
+
export function hasIgnoredColumn(store) {
|
|
23
|
+
return hasColumn(store, "memories", "ignored");
|
|
24
|
+
}
|
|
25
|
+
export function hasCuratedColumn(store) {
|
|
26
|
+
return hasColumn(store, "memories", "curated");
|
|
27
|
+
}
|
|
28
|
+
export function hasTruthColumns(store) {
|
|
29
|
+
return hasColumn(store, "memories", "truth_penalty");
|
|
30
|
+
}
|
|
31
|
+
export function hasRepoIdColumn(store) {
|
|
32
|
+
return hasColumn(store, "memories", "repo_id");
|
|
33
|
+
}
|
|
34
|
+
export function hasLayerColumn(store) {
|
|
35
|
+
return hasColumn(store, "memories", "layer");
|
|
36
|
+
}
|
|
37
|
+
export function hasRecurrenceColumn(store) {
|
|
38
|
+
return hasColumn(store, "memories", "recurrence_count");
|
|
39
|
+
}
|
|
40
|
+
export function hasArchivedColumn(store) {
|
|
41
|
+
return hasColumn(store, "memories", "archived_at");
|
|
42
|
+
}
|
|
43
|
+
export function hasFeedbackColumns(store) {
|
|
44
|
+
return hasColumn(store, "memories", "feedback_positive");
|
|
45
|
+
}
|
|
46
|
+
export function hasFeedbackTable(store) {
|
|
47
|
+
try {
|
|
48
|
+
store.prepare("SELECT COUNT(*) FROM memory_feedback LIMIT 0").get();
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -28,6 +28,14 @@ export declare const CONTRACT_TOOL_ADDITIONS: readonly {
|
|
|
28
28
|
name: string;
|
|
29
29
|
since: string;
|
|
30
30
|
}[];
|
|
31
|
+
/**
|
|
32
|
+
* v1.1.0 (K11-007 / plan §5.1, D11-01) — metric keys added after the freeze,
|
|
33
|
+
* each carrying the `since` the deprecation policy requires (C-05).
|
|
34
|
+
*/
|
|
35
|
+
export declare const CONTRACT_METRIC_ADDITIONS: readonly {
|
|
36
|
+
name: string;
|
|
37
|
+
since: string;
|
|
38
|
+
}[];
|
|
31
39
|
/**
|
|
32
40
|
* v1.0.0 (K10-027 / plan §5.7) — the C-09 boundary addition. Stored is
|
|
33
41
|
* not trusted: anything reaching an artifact or a prompt is escaped at
|
package/dist/plugin/contract.js
CHANGED
|
@@ -41,8 +41,18 @@ export const CONTRACT_TOOL_NAMES = [
|
|
|
41
41
|
* added_bare.
|
|
42
42
|
*/
|
|
43
43
|
export const CONTRACT_TOOL_ADDITIONS = [
|
|
44
|
-
{ name: "kevin_contract", since: "1.0.0" },
|
|
45
44
|
{ name: "kevin_bench", since: "1.0.0" },
|
|
45
|
+
{ name: "kevin_contract", since: "1.0.0" },
|
|
46
|
+
{ name: "kevin_forget", since: "1.1.0" },
|
|
47
|
+
];
|
|
48
|
+
/**
|
|
49
|
+
* v1.1.0 (K11-007 / plan §5.1, D11-01) — metric keys added after the freeze,
|
|
50
|
+
* each carrying the `since` the deprecation policy requires (C-05).
|
|
51
|
+
*/
|
|
52
|
+
export const CONTRACT_METRIC_ADDITIONS = [
|
|
53
|
+
{ name: "bench_regression_failures", since: "1.1.0" },
|
|
54
|
+
{ name: "forget_requests_total", since: "1.1.0" },
|
|
55
|
+
{ name: "forget_tombstones_published", since: "1.1.0" },
|
|
46
56
|
];
|
|
47
57
|
/**
|
|
48
58
|
* v1.0.0 (K10-027 / plan §5.7) — the C-09 boundary addition. Stored is
|
|
@@ -76,12 +86,17 @@ export function describeContract(_input) {
|
|
|
76
86
|
// Derive clause values from live source wherever possible (plan §5.1).
|
|
77
87
|
// C-03 members added after the freeze carry their `since` as objects;
|
|
78
88
|
// the original frozen set stays plain strings.
|
|
79
|
-
const
|
|
89
|
+
const toolAdditions = [...CONTRACT_TOOL_ADDITIONS].sort((a, b) => a.name.localeCompare(b.name));
|
|
80
90
|
const toolValue = {
|
|
81
|
-
tools: [[...CONTRACT_TOOL_NAMES].sort(),
|
|
91
|
+
tools: [[...CONTRACT_TOOL_NAMES].sort(), toolAdditions].flat(),
|
|
82
92
|
};
|
|
83
93
|
const settingValue = { keys: [...KEVIN_CONFIG_KEYS].sort() };
|
|
84
|
-
|
|
94
|
+
// v1.1.0 — metric keys added after freeze carry `since` (C-05)
|
|
95
|
+
const metricAdditions = [...CONTRACT_METRIC_ADDITIONS].sort((a, b) => a.name.localeCompare(b.name));
|
|
96
|
+
const baseMetricKeys = Object.keys(METRIC_KEY_LABELS)
|
|
97
|
+
.filter((k) => !metricAdditions.some((a) => a.name === k))
|
|
98
|
+
.sort();
|
|
99
|
+
const metricValue = { keys: [...baseMetricKeys, ...metricAdditions].flat() };
|
|
85
100
|
const clauses = [
|
|
86
101
|
{
|
|
87
102
|
id: "C-01",
|
|
@@ -149,7 +164,7 @@ export function describeContract(_input) {
|
|
|
149
164
|
stability: "forward-only",
|
|
150
165
|
since: "0.1.0",
|
|
151
166
|
value: {
|
|
152
|
-
schema_version: "
|
|
167
|
+
schema_version: "012",
|
|
153
168
|
migrations_forward_only: true,
|
|
154
169
|
},
|
|
155
170
|
},
|
package/dist/plugin/index.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface KevinPluginOptions {
|
|
|
19
19
|
export declare const KEVIN_CONFIG_KEYS: readonly ["quality_gate_enabled", "lesson_snippet_injection", "patternminer_enabled", "cross_project_enabled", "llm_reflection_enabled", "tool_calls_dedup_enabled", "deterministic_retrieval", "pre_prompt_budget_tokens", "archive_after_days", "curation_enabled", "agents_md_path", "skill_emission_enabled", "reference_emission_enabled", "injection_confidence_floor", "repo_truth_enabled", "convention_mining_enabled", "conflict_detection_enabled", "error_lesson_mode", "shared_layer_enabled", "okf_path", "share_requires_approval", "author_identity_mode", "shared_confidence_floor", "hook_liveness_enabled", "native_registration_enabled", "host_probe_history_enabled", "dead_hook_report_threshold", "perf_enabled", "perf_ring_capacity", "perf_flush_on_idle", "contract_report_enabled"];
|
|
20
20
|
export declare const ERROR_LESSON_MODE_VALUES: readonly ["all", "triage_only"];
|
|
21
21
|
/** Plugin release version — stamped into generated files (K8-021/027). */
|
|
22
|
-
export declare const KEVIN_VERSION = "1.
|
|
22
|
+
export declare const KEVIN_VERSION = "1.1.0";
|
|
23
23
|
export interface RekeyCounts {
|
|
24
24
|
memories: number;
|
|
25
25
|
shared_entries: number;
|
package/dist/plugin/index.js
CHANGED
|
@@ -34,6 +34,7 @@ import { executeKevinConflicts } from "./kevin_conflicts.js";
|
|
|
34
34
|
import { buildKevinContract } from "./kevin_contract.js";
|
|
35
35
|
import { buildDoctor } from "./kevin_doctor.js";
|
|
36
36
|
import { buildKevinFacts } from "./kevin_facts.js";
|
|
37
|
+
import { handleForget } from "./kevin_forget.js";
|
|
37
38
|
import { handleNative } from "./kevin_native.js";
|
|
38
39
|
import { kevinPropose } from "./kevin_propose.js";
|
|
39
40
|
import { kevinPublish } from "./kevin_publish.js";
|
|
@@ -112,6 +113,7 @@ export const KEVIN_CONFIG_KEYS = [
|
|
|
112
113
|
"perf_flush_on_idle",
|
|
113
114
|
"contract_report_enabled",
|
|
114
115
|
];
|
|
116
|
+
// v1.1.0 (K11-007 / D11-08) — no new settings in 1.1.0; thresholds are constants (D11-03)
|
|
115
117
|
// v0.7.0 (K7-003 / plan §5.6, D7-12) — the explicit VALUE domain for
|
|
116
118
|
// `error_lesson_mode`. The setting is TEXT and must be compared with
|
|
117
119
|
// `=== "triage_only"`, never by truthiness; the domain here is enforced by
|
|
@@ -120,7 +122,7 @@ export const KEVIN_CONFIG_KEYS = [
|
|
|
120
122
|
// behaviour on the next reflection.
|
|
121
123
|
export const ERROR_LESSON_MODE_VALUES = ["all", "triage_only"];
|
|
122
124
|
/** Plugin release version — stamped into generated files (K8-021/027). */
|
|
123
|
-
export const KEVIN_VERSION = "1.
|
|
125
|
+
export const KEVIN_VERSION = "1.1.0";
|
|
124
126
|
function resolveMigrationsDir() {
|
|
125
127
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
126
128
|
return join(here, "..", "migrations");
|
|
@@ -870,8 +872,9 @@ export const KevinPlugin = async (input, options) => {
|
|
|
870
872
|
// monotone across releases; K6-024 extends this
|
|
871
873
|
// block with the remaining v0.6 fields. v0.8.0
|
|
872
874
|
// (K8-025) — 18 v0.7 + kevin_project +
|
|
873
|
-
// kevin_native = 23.
|
|
874
|
-
|
|
875
|
+
// kevin_native = 23. v1.0.0 (K10-018) — +kevin_contract +kevin_bench = 25.
|
|
876
|
+
// v1.1.0 (K11-007) — +kevin_forget = 26.
|
|
877
|
+
tool_count: 26,
|
|
875
878
|
v07,
|
|
876
879
|
memories_reflector: memoriesReflector,
|
|
877
880
|
memories_agent: memoriesAgent,
|
|
@@ -1461,6 +1464,24 @@ export const KevinPlugin = async (input, options) => {
|
|
|
1461
1464
|
};
|
|
1462
1465
|
},
|
|
1463
1466
|
}),
|
|
1467
|
+
// v1.1.0 (K11-007 / plan §5.1, D11-02) — kevin_forget: closes the sharing lifecycle.
|
|
1468
|
+
kevin_forget: tool({
|
|
1469
|
+
description: "Olvida memorias y publica tombstones en la capa compartida (v1.1.0, K11-005/006 / plan §5.1): dry-run por defecto — sin confirm no muta nada y devuelve el plan (archived, tombstone planned); con confirm:true archiva localmente (status='archived') y, cuando la memoria proyecta a la capa compartida (layer='shared' o shared_entry_id), publica un tombstone via el unico write path (SharedLayer.applyExport, D8-08). Segunda invocacion identica reporta noop.",
|
|
1470
|
+
args: {
|
|
1471
|
+
ids: tool.schema.array(tool.schema.string()).min(1),
|
|
1472
|
+
confirm: tool.schema.boolean().optional(),
|
|
1473
|
+
},
|
|
1474
|
+
async execute(args) {
|
|
1475
|
+
const okfPath = join(projectRoot, memoryService.getSetting("okf_path", ".kevin/knowledge.okf"));
|
|
1476
|
+
const result = handleForget({ ids: args.ids, confirm: args.confirm }, { store, memoryService, sharedLayer, okfPath, metrics });
|
|
1477
|
+
return {
|
|
1478
|
+
title: result.dry_run
|
|
1479
|
+
? "Plan de olvido (dry-run)"
|
|
1480
|
+
: "Olvido aplicado",
|
|
1481
|
+
output: JSON.stringify(result),
|
|
1482
|
+
};
|
|
1483
|
+
},
|
|
1484
|
+
}),
|
|
1464
1485
|
kevin_approve: tool({
|
|
1465
1486
|
description: "Aprueba o rechaza una propuesta de curacion (v0.6.0). reject: marca rejected, nada toca disco. approve: aplica el diff (unico call site de ArtifactWriter.apply, D6-01), marca applied y cura las memorias contribuyentes. Solo acepta propuestas pending.",
|
|
1466
1487
|
args: {
|
|
@@ -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"];
|
|
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,10 @@ 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",
|
|
60
64
|
];
|
|
61
65
|
const DEFAULT_FLUSH_MS = 1000;
|
|
62
66
|
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;
|