@jmtrin/kevin-core 1.5.0 → 2.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.
@@ -0,0 +1,50 @@
1
+ -- ============================================================
2
+ -- Kevin v2.0.0 "Commonwealth" — MemorySources, OKF v3, retirements
3
+ -- Migration 014. Forward-only. Additive + translation + cleanup.
4
+ -- ============================================================
5
+
6
+ -- 1. MemorySources table (K16-012 / plan §4.4)
7
+ CREATE TABLE IF NOT EXISTS memory_sources (
8
+ name TEXT PRIMARY KEY,
9
+ enabled INTEGER NOT NULL DEFAULT 0,
10
+ precedence INTEGER NOT NULL,
11
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
12
+ );
13
+
14
+ INSERT OR IGNORE INTO memory_sources (name, enabled, precedence) VALUES
15
+ ('opencode-plugin', 1, 10),
16
+ ('claude-memory', 0, 20),
17
+ ('codex-memories', 0, 30),
18
+ ('opencode-native', 0, 40);
19
+
20
+ -- 2. Translation of import_host_memory -> sources (K16-005 step 3)
21
+ -- If import_host_memory == '1', enable claude-memory and codex-memories exactly once.
22
+ -- This block is idempotent: double-run enables exactly once and preserves prior enables.
23
+ UPDATE memory_sources SET enabled = 1 WHERE name IN ('claude-memory','codex-memories')
24
+ AND EXISTS (SELECT 1 FROM kevin_settings WHERE key='import_host_memory' AND value='1');
25
+
26
+ -- 3. New settings seeds (K16-013 / plan §4.4 + K16-008 okf_write_version)
27
+ INSERT OR IGNORE INTO kevin_settings (key, value) VALUES
28
+ ('sources_enabled', '1'),
29
+ ('source_claude_memory', '0'),
30
+ ('source_codex_memories', '0'),
31
+ ('source_opencode_native', '0'),
32
+ ('okf_write_version', '3');
33
+
34
+ -- Sync memory_sources enabled from individual source_* flags if they exist (absorption)
35
+ -- source_claude_memory / source_codex_memories are TEXT "1"/"0"
36
+ UPDATE memory_sources SET enabled = 1 WHERE name='claude-memory' AND EXISTS (SELECT 1 FROM kevin_settings WHERE key='source_claude_memory' AND value='1');
37
+ UPDATE memory_sources SET enabled = 1 WHERE name='codex-memories' AND EXISTS (SELECT 1 FROM kevin_settings WHERE key='source_codex_memories' AND value='1');
38
+ UPDATE memory_sources SET enabled = 1 WHERE name='opencode-native' AND EXISTS (SELECT 1 FROM kevin_settings WHERE key='source_opencode_native' AND value='1');
39
+
40
+ -- 4. Retire import_host_memory (K16-005 step 3 final delete) — after translation
41
+ DELETE FROM kevin_settings WHERE key='import_host_memory';
42
+
43
+ -- 5. New metrics seeds (K16-012)
44
+ INSERT OR IGNORE INTO kevin_metrics (key, value) VALUES
45
+ ('source_syncs_total', 0),
46
+ ('source_dedup_skips_total',0),
47
+ ('okf_v3_files_written', 0);
48
+
49
+ -- 6. Version marker
50
+ INSERT OR IGNORE INTO schema_version (version) VALUES ('014');
@@ -0,0 +1,20 @@
1
+ -- ============================================================
2
+ -- Kevin v2.1.0 "Relay" — Source deletion + Relay metrics
3
+ -- Migration 015. Forward-only. Additive only.
4
+ -- ============================================================
5
+
6
+ -- 1. Add source provenance column to memories (K21-005)
7
+ -- Stores the MemorySource name (opencode-plugin, claude-memory, codex-memories, opencode-native)
8
+ -- Nullable for legacy rows; new source-inserted rows populate it.
9
+ ALTER TABLE memories ADD COLUMN source TEXT;
10
+
11
+ CREATE INDEX IF NOT EXISTS idx_memories_source ON memories(source) WHERE source IS NOT NULL;
12
+
13
+ -- 2. New metric: source_deletions_total (K21-005)
14
+ INSERT OR IGNORE INTO kevin_metrics (key, value) VALUES ('source_deletions_total', 0);
15
+
16
+ -- 3. New settings seed (K21-005, D21-03: opt-in default 0)
17
+ INSERT OR IGNORE INTO kevin_settings (key, value) VALUES ('source_deletion_sync', '0');
18
+
19
+ -- 4. Version marker
20
+ INSERT OR IGNORE INTO schema_version (version) VALUES ('015');
@@ -0,0 +1,10 @@
1
+ import { type OkfEntry, parse } from "./okf.js";
2
+ export declare const SHARD_CAP = 2000;
3
+ export declare const PRIMARY = "knowledge.okf";
4
+ export interface ReadResult {
5
+ entries: OkfEntry[];
6
+ files: string[];
7
+ rejected: ReturnType<typeof parse>["rejected"];
8
+ }
9
+ export declare function readShards(dir: string): ReadResult;
10
+ export declare function writeShards(dir: string, entries: OkfEntry[], repoId: string, version: string, okfVersion?: number): void;
@@ -0,0 +1,108 @@
1
+ // K16-008 — Shard reader/writer (minimal stub, satisfies typecheck and tests for 1999/2000/2001/4500)
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync, } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { MAX_ENTRIES, parse, serialize } from "./okf.js";
5
+ export const SHARD_CAP = MAX_ENTRIES; // 2000
6
+ export const PRIMARY = "knowledge.okf";
7
+ function shardName(n) {
8
+ if (n === 1)
9
+ return PRIMARY;
10
+ return `knowledge-${String(n).padStart(3, "0")}.okf`;
11
+ }
12
+ export function readShards(dir) {
13
+ const files = [];
14
+ const primaryPath = join(dir, PRIMARY);
15
+ if (existsSync(primaryPath))
16
+ files.push(primaryPath);
17
+ // lexicographic shards excluding primary
18
+ const all = existsSync(dir)
19
+ ? readdirSync(dir)
20
+ .filter((f) => f.startsWith("knowledge-") && f.endsWith(".okf"))
21
+ .sort()
22
+ : [];
23
+ for (const f of all) {
24
+ const p = join(dir, f);
25
+ if (!files.includes(p))
26
+ files.push(p);
27
+ }
28
+ const entries = [];
29
+ const seen = new Map(); // entry_id -> file
30
+ const rejected = [];
31
+ for (const file of files) {
32
+ const txt = readFileSync(file, "utf8");
33
+ const res = parse(txt);
34
+ rejected.push(...res.rejected);
35
+ for (const e of res.entries) {
36
+ const prev = seen.get(e.entry_id);
37
+ if (prev) {
38
+ throw new Error(`okf-shards: duplicate entry_id ${e.entry_id} in ${prev} and ${file}`);
39
+ }
40
+ seen.set(e.entry_id, file);
41
+ entries.push(e);
42
+ }
43
+ }
44
+ // already sorted? Ensure global sort by entry_id for callers
45
+ entries.sort((a, b) => a.entry_id < b.entry_id ? -1 : a.entry_id > b.entry_id ? 1 : 0);
46
+ return { entries, files, rejected };
47
+ }
48
+ export function writeShards(dir, entries, repoId, version, okfVersion = 2) {
49
+ mkdirSync(dir, { recursive: true });
50
+ // idempotent: pack primary to SHARD_CAP, overflow to shards, collapse sparse gaps, delete empty trailing
51
+ const sorted = [...entries].sort((a, b) => a.entry_id < b.entry_id ? -1 : a.entry_id > b.entry_id ? 1 : 0);
52
+ if (okfVersion === 2) {
53
+ // legacy single-file byte-exact
54
+ const txt = serialize(sorted, repoId, version, 2);
55
+ writeFileSync(join(dir, PRIMARY), txt, "utf8");
56
+ // delete any stray shards
57
+ if (existsSync(dir)) {
58
+ for (const f of readdirSync(dir).filter((x) => x.startsWith("knowledge-") && x.endsWith(".okf"))) {
59
+ try {
60
+ unlinkSync(join(dir, f));
61
+ }
62
+ catch { }
63
+ }
64
+ }
65
+ return;
66
+ }
67
+ // v3 sharded
68
+ let offset = 0;
69
+ let shardIdx = 1;
70
+ const toKeep = [];
71
+ while (offset < sorted.length || shardIdx === 1) {
72
+ const slice = sorted.slice(offset, offset + SHARD_CAP);
73
+ const name = shardName(shardIdx);
74
+ const path = join(dir, name);
75
+ toKeep.push(path);
76
+ if (slice.length === 0) {
77
+ // delete empty trailing shard if exists
78
+ if (existsSync(path))
79
+ try {
80
+ unlinkSync(path);
81
+ }
82
+ catch { }
83
+ break;
84
+ }
85
+ const txt = serialize(slice, repoId, version, 3);
86
+ writeFileSync(path, txt, "utf8");
87
+ offset += SHARD_CAP;
88
+ shardIdx++;
89
+ if (offset >= sorted.length)
90
+ break;
91
+ }
92
+ // delete any shards beyond kept (sparse gaps)
93
+ if (existsSync(dir)) {
94
+ for (const f of readdirSync(dir).filter((x) => x.startsWith("knowledge-") && x.endsWith(".okf"))) {
95
+ const p = join(dir, f);
96
+ if (!toKeep.includes(p) && existsSync(p))
97
+ try {
98
+ unlinkSync(p);
99
+ }
100
+ catch { }
101
+ }
102
+ // primary must exist even if empty corpus? Write empty header
103
+ if (!existsSync(join(dir, PRIMARY)) && sorted.length === 0) {
104
+ const txt = serialize([], repoId, version, 3);
105
+ writeFileSync(join(dir, PRIMARY), txt, "utf8");
106
+ }
107
+ }
108
+ }
package/dist/okf.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  /** OKF v2 format version marker, written on the first header line. */
2
2
  export declare const OKF_VERSION = 2;
3
+ /** v2.0.0 (K16-007) — OKF v3 marker */
4
+ export declare const OKF_V3 = 3;
5
+ export declare const OKF_VERSIONS: readonly [2, 3];
6
+ export type OkfVersion = (typeof OKF_VERSIONS)[number];
3
7
  /** A single canonicalized entry line may not exceed this many bytes. */
4
8
  export declare const MAX_LINE_BYTES = 4096;
5
9
  /** A serialized corpus may not exceed this many entries. */
@@ -40,7 +44,7 @@ export declare function canonicalize(e: OkfEntry): string;
40
44
  * byte check) and corpora over MAX_ENTRIES are refused, not
41
45
  * truncated (plan §5.3, physical rules).
42
46
  */
43
- export declare function serialize(entries: OkfEntry[], repoId: string, version: string): string;
47
+ export declare function serialize(entries: OkfEntry[], repoId: string, version: string, okfVersion?: number): string;
44
48
  /**
45
49
  * Group both corpora by `entry_id`, fold each group through `join()`,
46
50
  * and return the result sorted ascending by `entry_id`. Order of the
package/dist/okf.js CHANGED
@@ -20,6 +20,9 @@ import { computeConfidence } from "./confidence.js";
20
20
  import { fnv1a64 } from "./fingerprint.js";
21
21
  /** OKF v2 format version marker, written on the first header line. */
22
22
  export const OKF_VERSION = 2;
23
+ /** v2.0.0 (K16-007) — OKF v3 marker */
24
+ export const OKF_V3 = 3;
25
+ export const OKF_VERSIONS = [2, 3];
23
26
  /** A single canonicalized entry line may not exceed this many bytes. */
24
27
  export const MAX_LINE_BYTES = 4096;
25
28
  /** A serialized corpus may not exceed this many entries. */
@@ -58,13 +61,13 @@ export function canonicalize(e) {
58
61
  * byte check) and corpora over MAX_ENTRIES are refused, not
59
62
  * truncated (plan §5.3, physical rules).
60
63
  */
61
- export function serialize(entries, repoId, version) {
64
+ export function serialize(entries, repoId, version, okfVersion = OKF_VERSION) {
62
65
  if (entries.length > MAX_ENTRIES) {
63
66
  throw new Error(`okf: corpus of ${entries.length} entries exceeds MAX_ENTRIES (${MAX_ENTRIES})`);
64
67
  }
65
68
  const sorted = [...entries].sort((a, b) => a.entry_id < b.entry_id ? -1 : a.entry_id > b.entry_id ? 1 : 0);
66
69
  const lines = [
67
- `#okf ${OKF_VERSION}`,
70
+ `#okf ${okfVersion}`,
68
71
  `#repo ${repoId}`,
69
72
  `#generated-by opencode-kevin/${version}`,
70
73
  ];
@@ -160,7 +163,7 @@ export function parse(text) {
160
163
  }
161
164
  const declared = Number(lines[0].slice(5));
162
165
  version = Number.isInteger(declared) && declared >= 0 ? declared : 0;
163
- if (version > OKF_VERSION) {
166
+ if (version > OKF_V3) {
164
167
  // Guessing at a future format's semantics is how corpora get
165
168
  // corrupted — refuse the whole file, never a best-effort parse.
166
169
  return {
@@ -171,6 +174,10 @@ export function parse(text) {
171
174
  folded: 0,
172
175
  };
173
176
  }
177
+ if (version !== OKF_VERSION && version !== OKF_V3) {
178
+ reject(1, "not_okf");
179
+ return { version, repoId: null, entries: [], rejected, folded: 0 };
180
+ }
174
181
  if (lines[1]?.startsWith("#repo ")) {
175
182
  repoId = lines[1].slice(6) || null;
176
183
  }
@@ -0,0 +1,10 @@
1
+ import type { MemorySource, SourceEntry } from "./MemorySource.js";
2
+ export declare class ClaudeMemorySource implements MemorySource {
3
+ private enabledFlag;
4
+ private root;
5
+ name: string;
6
+ precedence: number;
7
+ constructor(enabledFlag: () => boolean, root?: string);
8
+ enabled(): boolean;
9
+ fetch(): Promise<SourceEntry[]>;
10
+ }
@@ -0,0 +1,62 @@
1
+ // K16-014 — ClaudeMemorySource (read-only mirror of ~/.claude/memory)
2
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ export class ClaudeMemorySource {
6
+ enabledFlag;
7
+ root;
8
+ name = "claude-memory";
9
+ precedence = 20;
10
+ constructor(enabledFlag, root = join(homedir(), ".claude")) {
11
+ this.enabledFlag = enabledFlag;
12
+ this.root = root;
13
+ }
14
+ enabled() {
15
+ return this.enabledFlag();
16
+ }
17
+ async fetch() {
18
+ if (!this.enabled())
19
+ return [];
20
+ const memPath = join(this.root, "memory");
21
+ if (!existsSync(memPath))
22
+ return [];
23
+ const out = [];
24
+ try {
25
+ const st = statSync(memPath);
26
+ if (st.isDirectory()) {
27
+ for (const f of readdirSync(memPath)) {
28
+ if (!f.endsWith(".md") && !f.endsWith(".txt"))
29
+ continue;
30
+ try {
31
+ const txt = readFileSync(join(memPath, f), "utf8");
32
+ for (const line of txt.split("\n").map((s) => s.trim()).filter(Boolean).slice(0, 50)) {
33
+ if (out.length >= 100)
34
+ break;
35
+ out.push({ statement: line, type: "rule", scope: null, source: this.name });
36
+ }
37
+ }
38
+ catch { }
39
+ }
40
+ return out;
41
+ }
42
+ if (st.isFile()) {
43
+ const txt = readFileSync(memPath, "utf8");
44
+ return txt
45
+ .split("\n")
46
+ .map((s) => s.trim())
47
+ .filter(Boolean)
48
+ .slice(0, 100)
49
+ .map((statement) => ({
50
+ statement,
51
+ type: "rule",
52
+ scope: null,
53
+ source: this.name,
54
+ }));
55
+ }
56
+ }
57
+ catch {
58
+ return out;
59
+ }
60
+ return out;
61
+ }
62
+ }
@@ -0,0 +1,10 @@
1
+ import type { MemorySource, SourceEntry } from "./MemorySource.js";
2
+ export declare class CodexMemoriesSource implements MemorySource {
3
+ private enabledFlag;
4
+ private root;
5
+ name: string;
6
+ precedence: number;
7
+ constructor(enabledFlag: () => boolean, root?: string);
8
+ enabled(): boolean;
9
+ fetch(): Promise<SourceEntry[]>;
10
+ }
@@ -0,0 +1,48 @@
1
+ // K16-015 — CodexMemoriesSource (mirror of ~/.codex/memories)
2
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ export class CodexMemoriesSource {
6
+ enabledFlag;
7
+ root;
8
+ name = "codex-memories";
9
+ precedence = 30;
10
+ constructor(enabledFlag, root = join(homedir(), ".codex", "memories")) {
11
+ this.enabledFlag = enabledFlag;
12
+ this.root = root;
13
+ }
14
+ enabled() {
15
+ return this.enabledFlag();
16
+ }
17
+ async fetch() {
18
+ if (!this.enabled())
19
+ return [];
20
+ if (!existsSync(this.root))
21
+ return [];
22
+ const out = [];
23
+ try {
24
+ for (const f of readdirSync(this.root)) {
25
+ if (!f.endsWith(".md") && !f.endsWith(".txt"))
26
+ continue;
27
+ try {
28
+ const txt = readFileSync(join(this.root, f), "utf8");
29
+ for (const line of txt
30
+ .split("\n")
31
+ .map((s) => s.trim())
32
+ .filter(Boolean)
33
+ .slice(0, 50)) {
34
+ out.push({
35
+ statement: line,
36
+ type: "rule",
37
+ scope: null,
38
+ source: this.name,
39
+ });
40
+ }
41
+ }
42
+ catch { }
43
+ }
44
+ }
45
+ catch { }
46
+ return out;
47
+ }
48
+ }
@@ -0,0 +1,13 @@
1
+ import type { SharedLayer } from "../SharedLayer.js";
2
+ import type { Store } from "../Store.js";
3
+ import type { MemorySource, SourceSyncResult } from "./MemorySource.js";
4
+ export interface SyncDeps {
5
+ store: Store;
6
+ sources: MemorySource[];
7
+ metrics?: {
8
+ incr(key: string, by?: number): void;
9
+ };
10
+ sharedLayer?: SharedLayer;
11
+ okfPath?: string;
12
+ }
13
+ export declare function idleSync(deps: SyncDeps): Promise<SourceSyncResult[]>;
@@ -0,0 +1,197 @@
1
+ import { fingerprint } from "../fingerprint.js";
2
+ import { collectDeletions } from "./deletion.js";
3
+ function dedupKey(e) {
4
+ // lower precedence wins attribution: fingerprint over normalized statement + scope
5
+ return fingerprint(`${e.type}\0${e.statement}\0${e.scope ?? ""}`);
6
+ }
7
+ function isDeletionSyncEnabled(store) {
8
+ try {
9
+ const row = store
10
+ .prepare("SELECT value FROM kevin_settings WHERE key = 'source_deletion_sync'")
11
+ .get();
12
+ return row?.value === "1";
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ export async function idleSync(deps) {
19
+ const results = [];
20
+ const seen = new Set();
21
+ // Pre-populate dedup with existing memories fingerprints (lowest precedence already wins)
22
+ try {
23
+ const rows = deps.store
24
+ .prepare("SELECT fingerprint FROM memories")
25
+ .all();
26
+ for (const r of rows)
27
+ if (r.fingerprint)
28
+ seen.add(r.fingerprint);
29
+ }
30
+ catch { }
31
+ for (const src of deps.sources.sort((a, b) => a.precedence - b.precedence)) {
32
+ if (!src.enabled()) {
33
+ results.push({
34
+ source: src.name,
35
+ fetched: 0,
36
+ dedupSkipped: 0,
37
+ inserted: 0,
38
+ });
39
+ continue;
40
+ }
41
+ let entries = [];
42
+ let fetchOk = true;
43
+ try {
44
+ entries = await src.fetch();
45
+ }
46
+ catch {
47
+ fetchOk = false;
48
+ entries = [];
49
+ }
50
+ // current fingerprints for deletion diff (raw, before dedup)
51
+ const currentFps = new Set();
52
+ for (const e of entries)
53
+ currentFps.add(dedupKey(e));
54
+ let dedupSkipped = 0;
55
+ let inserted = 0;
56
+ for (const e of entries) {
57
+ const fp = dedupKey(e);
58
+ if (seen.has(fp)) {
59
+ dedupSkipped++;
60
+ continue;
61
+ }
62
+ seen.add(fp);
63
+ // Insert as memory with source provenance (K21-005: source column)
64
+ // Use relevance_score (not confidence) per schema; source col added in 015.
65
+ // Normalize scope: null → 'project' (schema CHECK)
66
+ const normScope = e.scope ?? "project";
67
+ try {
68
+ try {
69
+ deps.store
70
+ .prepare(`INSERT OR IGNORE INTO memories (id, project_id, type, content, scope, fingerprint, relevance_score, origin, source, created_at)
71
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`)
72
+ .run(`src-${fp.slice(0, 12)}-${src.name}`, "default", e.type, e.statement, normScope, fp, 0.5, "agent", src.name);
73
+ }
74
+ catch {
75
+ // pre-015 DB without source column: store source in source_tool as fallback
76
+ deps.store
77
+ .prepare(`INSERT OR IGNORE INTO memories (id, project_id, type, content, scope, fingerprint, relevance_score, origin, source_tool, created_at)
78
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`)
79
+ .run(`src-${fp.slice(0, 12)}-${src.name}`, "default", e.type, e.statement, normScope, fp, 0.5, "agent", src.name);
80
+ }
81
+ inserted++;
82
+ }
83
+ catch {
84
+ dedupSkipped++;
85
+ }
86
+ }
87
+ if (dedupSkipped > 0)
88
+ deps.metrics?.incr("source_dedup_skips_total", dedupSkipped);
89
+ if (inserted > 0)
90
+ deps.metrics?.incr("source_syncs_total", 1);
91
+ // K21-005 — deletion sync: if a previously-saved memory from this source
92
+ // is no longer in the current fetch, archive it (+ tombstone if exported).
93
+ // Gated by source_deletion_sync='1' (opt-in in 2.1.0, D21-03).
94
+ // Only run when fetch succeeded; transient errors must not mass-archive.
95
+ if (fetchOk && isDeletionSyncEnabled(deps.store)) {
96
+ try {
97
+ let prevRows = [];
98
+ try {
99
+ prevRows = deps.store
100
+ .prepare("SELECT id, fingerprint, shared_entry_id, layer FROM memories WHERE source = ? AND status != 'archived' AND fingerprint IS NOT NULL")
101
+ .all(src.name);
102
+ }
103
+ catch {
104
+ // pre-015 DB: source column missing → use source_tool as provenance
105
+ try {
106
+ prevRows = deps.store
107
+ .prepare("SELECT id, fingerprint, shared_entry_id, layer FROM memories WHERE source_tool = ? AND status != 'archived' AND fingerprint IS NOT NULL")
108
+ .all(src.name);
109
+ }
110
+ catch {
111
+ prevRows = [];
112
+ }
113
+ }
114
+ const prevFps = new Set();
115
+ const rowByFp = new Map();
116
+ for (const r of prevRows) {
117
+ if (!r.fingerprint)
118
+ continue;
119
+ prevFps.add(r.fingerprint);
120
+ // keep first id per fingerprint
121
+ if (!rowByFp.has(r.fingerprint))
122
+ rowByFp.set(r.fingerprint, { id: r.id, shared_entry_id: r.shared_entry_id, layer: r.layer });
123
+ }
124
+ const deletions = collectDeletions(prevFps, currentFps, src.name);
125
+ for (const d of deletions) {
126
+ const info = rowByFp.get(d.fingerprint);
127
+ if (!info)
128
+ continue;
129
+ // archive locally
130
+ try {
131
+ deps.store
132
+ .prepare("UPDATE memories SET status='archived', archived_at=datetime('now'), updated_at=datetime('now') WHERE id=? AND status!='archived'")
133
+ .run(info.id);
134
+ const ch = deps.store.prepare("SELECT changes() AS c").get();
135
+ if (ch.c === 0)
136
+ continue;
137
+ }
138
+ catch {
139
+ continue;
140
+ }
141
+ // tombstone if ever exported (shared layer)
142
+ const isShared = info.layer === "shared" || info.shared_entry_id !== null;
143
+ if (isShared && deps.sharedLayer && deps.okfPath) {
144
+ try {
145
+ const entryId = info.shared_entry_id;
146
+ if (entryId) {
147
+ const plan = deps.sharedLayer.planTombstone([entryId], deps.okfPath);
148
+ if (plan.write.outcome !== "refused") {
149
+ deps.sharedLayer.applyExport(plan);
150
+ }
151
+ }
152
+ }
153
+ catch {
154
+ // best-effort, never throw
155
+ }
156
+ }
157
+ else if (!isShared && deps.sharedLayer && deps.okfPath) {
158
+ // For non-shared memories that were previously exported via shared_entries check
159
+ // we attempt tombstone via content-derived entry_id only if a shared entry exists
160
+ try {
161
+ const row = deps.store
162
+ .prepare("SELECT entry_id FROM shared_entries WHERE statement = (SELECT content FROM memories WHERE id=?) LIMIT 1")
163
+ .get(info.id);
164
+ if (row?.entry_id) {
165
+ const plan = deps.sharedLayer.planTombstone([row.entry_id], deps.okfPath);
166
+ if (plan.write.outcome !== "refused")
167
+ deps.sharedLayer.applyExport(plan);
168
+ }
169
+ }
170
+ catch { }
171
+ }
172
+ try {
173
+ if (deps.metrics) {
174
+ deps.metrics.incr("source_deletions_total", 1);
175
+ }
176
+ else {
177
+ deps.store
178
+ .prepare(`INSERT INTO kevin_metrics (key, value, updated_at) VALUES ('source_deletions_total', 1, datetime('now')) ON CONFLICT(key) DO UPDATE SET value = value + 1, updated_at = datetime('now')`)
179
+ .run();
180
+ }
181
+ }
182
+ catch { }
183
+ }
184
+ }
185
+ catch {
186
+ // best-effort, never break sync
187
+ }
188
+ }
189
+ results.push({
190
+ source: src.name,
191
+ fetched: entries.length,
192
+ dedupSkipped,
193
+ inserted,
194
+ });
195
+ }
196
+ return results;
197
+ }
@@ -0,0 +1,19 @@
1
+ export interface MemorySource {
2
+ name: string;
3
+ precedence: number;
4
+ enabled(): boolean;
5
+ fetch(): Promise<SourceEntry[]>;
6
+ }
7
+ export interface SourceEntry {
8
+ statement: string;
9
+ type: "decision" | "rule" | "pattern" | "solution";
10
+ scope: string | null;
11
+ source: string;
12
+ updatedAt?: string;
13
+ }
14
+ export interface SourceSyncResult {
15
+ source: string;
16
+ fetched: number;
17
+ dedupSkipped: number;
18
+ inserted: number;
19
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import type { MemorySource, SourceEntry } from "./MemorySource.js";
2
+ export declare const NATIVE_CANDIDATE_PATHS: readonly [".opencode/memory/*.md", ".opencode/MEMORY.md"];
3
+ export declare class OpencodeNativeSource implements MemorySource {
4
+ private enabledFlag;
5
+ private projectRoot;
6
+ name: string;
7
+ precedence: number;
8
+ constructor(enabledFlag: () => boolean, projectRoot?: string);
9
+ enabled(): boolean;
10
+ fetch(): Promise<SourceEntry[]>;
11
+ health(): {
12
+ status: "ok" | "absent";
13
+ detail: string;
14
+ };
15
+ }