omo-memory 0.1.11 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/memory.js CHANGED
@@ -9,32 +9,6 @@ export class PurgeConfirmationError extends Error {
9
9
  this.name = "PurgeConfirmationError";
10
10
  }
11
11
  }
12
- export function memoryPaths() {
13
- return { dbPath: defaultDbPath() };
14
- }
15
- export function doctorReport(dbPath = defaultDbPath()) {
16
- const db = openMemoryDb(dbPath);
17
- try {
18
- migrate(db);
19
- const project = resolveStoredProject(db, resolveProjectContext());
20
- const schemaVersion = Number(db.prepare("SELECT value FROM schema_meta WHERE key = 'schema_version'").pluck().get());
21
- const count = (table) => Number(db.prepare(`SELECT COUNT(*) FROM ${table}`).pluck().get());
22
- return {
23
- paths: { dbPath },
24
- schemaVersion,
25
- project,
26
- counts: {
27
- projects: count("projects"),
28
- sessions: count("sessions"),
29
- events: count("events"),
30
- handoffs: count("handoffs"),
31
- },
32
- };
33
- }
34
- finally {
35
- db.close();
36
- }
37
- }
38
12
  export function upsertProject(db, project) {
39
13
  const now = new Date().toISOString();
40
14
  db.prepare(`
@@ -64,8 +38,7 @@ export function startSession(input, dbPath = defaultDbPath()) {
64
38
  }
65
39
  }
66
40
  export function bootstrapSession(input, dbPath = defaultDbPath()) {
67
- const session = startSession({ host: input.host, adapter: input.adapter }, dbPath);
68
- return { ...session, recentEvents: recentEvents(input.limit, dbPath) };
41
+ return startSession({ host: input.host, adapter: input.adapter }, dbPath);
69
42
  }
70
43
  export function recordEvent(input, dbPath = defaultDbPath()) {
71
44
  const db = openMemoryDb(dbPath);
@@ -141,6 +114,50 @@ export function exportMemory(dbPath = defaultDbPath()) {
141
114
  .prepare(`
142
115
  SELECT id, session_id AS sessionId, summary_md AS summaryMd, created_at AS createdAt FROM handoffs
143
116
  WHERE project_id = ? ORDER BY created_at ASC, id ASC
117
+ `)
118
+ .all(project.id);
119
+ const concepts = db
120
+ .prepare(`
121
+ SELECT id, kind, label, description, aliases_json AS aliasesJson, payload_json AS payloadJson,
122
+ valid_from AS validFrom, valid_to AS validTo, created_at AS createdAt, updated_at AS updatedAt,
123
+ COALESCE(score, 0) AS score,
124
+ COALESCE(retention_class, 'working') AS retentionClass,
125
+ COALESCE(manual_pin, 0) AS manualPin,
126
+ COALESCE(ref_count, 0) AS refCount,
127
+ COALESCE(project_spread, 1) AS projectSpread,
128
+ first_seen AS firstSeen, last_seen AS lastSeen
129
+ FROM concepts WHERE project_id = ? ORDER BY created_at ASC, id ASC
130
+ `)
131
+ .all(project.id);
132
+ const relations = db
133
+ .prepare(`
134
+ SELECT id, source_type AS sourceType, source_id AS sourceId, target_type AS targetType, target_id AS targetId,
135
+ relation, weight, payload_json AS payloadJson, valid_from AS validFrom, valid_to AS validTo,
136
+ created_at AS createdAt, updated_at AS updatedAt
137
+ FROM relations WHERE project_id = ? ORDER BY created_at ASC, id ASC
138
+ `)
139
+ .all(project.id);
140
+ const durableMemories = db
141
+ .prepare(`
142
+ SELECT id, type, summary, body, source_event_id AS sourceEventId, source_handoff_id AS sourceHandoffId,
143
+ confidence, status, COALESCE(retention_class, 'durable') AS retentionClass,
144
+ valid_from AS validFrom, valid_to AS validTo, created_at AS createdAt, updated_at AS updatedAt
145
+ FROM durable_memories WHERE project_id = ? ORDER BY created_at ASC, id ASC
146
+ `)
147
+ .all(project.id);
148
+ const decisionRecords = db
149
+ .prepare(`
150
+ SELECT id, title, rationale, alternatives_json AS alternativesJson, evidence_json AS evidenceJson,
151
+ status, reversible, source_event_id AS sourceEventId, supersedes_decision_id AS supersedesDecisionId,
152
+ valid_from AS validFrom, valid_to AS validTo, created_at AS createdAt, updated_at AS updatedAt
153
+ FROM decision_records WHERE project_id = ? ORDER BY created_at ASC, id ASC
154
+ `)
155
+ .all(project.id);
156
+ const memoryReferences = db
157
+ .prepare(`
158
+ SELECT id, source_type AS sourceType, source_id AS sourceId, target_type AS targetType, target_id AS targetId,
159
+ ref_kind AS refKind, weight, created_at AS createdAt
160
+ FROM memory_references WHERE project_id = ? ORDER BY created_at ASC, id ASC
144
161
  `)
145
162
  .all(project.id);
146
163
  return {
@@ -151,6 +168,11 @@ export function exportMemory(dbPath = defaultDbPath()) {
151
168
  sessions,
152
169
  events,
153
170
  handoffs,
171
+ concepts,
172
+ relations,
173
+ durableMemories,
174
+ decisionRecords,
175
+ memoryReferences,
154
176
  };
155
177
  }
156
178
  finally {
@@ -165,6 +187,18 @@ export function purgeMemory(input, dbPath = defaultDbPath()) {
165
187
  migrate(db);
166
188
  const project = resolveStoredProject(db, resolveProjectContext());
167
189
  const deleteProject = db.transaction(() => {
190
+ const relations = db
191
+ .prepare("DELETE FROM relations WHERE project_id IN (SELECT id FROM projects WHERE id = ? OR repo_root = ?)")
192
+ .run(project.id, project.repoRoot).changes;
193
+ const decisionRecords = db
194
+ .prepare("DELETE FROM decision_records WHERE project_id IN (SELECT id FROM projects WHERE id = ? OR repo_root = ?)")
195
+ .run(project.id, project.repoRoot).changes;
196
+ const durableMemories = db
197
+ .prepare("DELETE FROM durable_memories WHERE project_id IN (SELECT id FROM projects WHERE id = ? OR repo_root = ?)")
198
+ .run(project.id, project.repoRoot).changes;
199
+ const concepts = db
200
+ .prepare("DELETE FROM concepts WHERE project_id IN (SELECT id FROM projects WHERE id = ? OR repo_root = ?)")
201
+ .run(project.id, project.repoRoot).changes;
168
202
  const events = db
169
203
  .prepare("DELETE FROM events WHERE project_id IN (SELECT id FROM projects WHERE id = ? OR repo_root = ?)")
170
204
  .run(project.id, project.repoRoot).changes;
@@ -174,8 +208,11 @@ export function purgeMemory(input, dbPath = defaultDbPath()) {
174
208
  const sessions = db
175
209
  .prepare("DELETE FROM sessions WHERE project_id IN (SELECT id FROM projects WHERE id = ? OR repo_root = ?)")
176
210
  .run(project.id, project.repoRoot).changes;
211
+ const memoryReferences = db
212
+ .prepare("DELETE FROM memory_references WHERE project_id IN (SELECT id FROM projects WHERE id = ? OR repo_root = ?)")
213
+ .run(project.id, project.repoRoot).changes;
177
214
  const projects = db.prepare("DELETE FROM projects WHERE id = ? OR repo_root = ?").run(project.id, project.repoRoot).changes;
178
- return { events, handoffs, sessions, projects };
215
+ return { events, handoffs, sessions, projects, concepts, relations, durableMemories, decisionRecords, memoryReferences };
179
216
  });
180
217
  return { project, deleted: deleteProject() };
181
218
  }
package/dist/memoryDb.js CHANGED
@@ -2,7 +2,7 @@ import { mkdirSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import Database from "better-sqlite3";
4
4
  import { defaultDbPath } from "./projectContext.js";
5
- export const SCHEMA_VERSION = 1;
5
+ export const SCHEMA_VERSION = 3;
6
6
  export function openMemoryDb(dbPath = defaultDbPath()) {
7
7
  mkdirSync(dirname(dbPath), { recursive: true });
8
8
  const db = new Database(dbPath);
@@ -57,9 +57,153 @@ export function migrate(db) {
57
57
  FOREIGN KEY(project_id) REFERENCES projects(id),
58
58
  FOREIGN KEY(session_id) REFERENCES sessions(id)
59
59
  );
60
+
61
+ CREATE TABLE IF NOT EXISTS concepts (
62
+ id TEXT PRIMARY KEY,
63
+ project_id TEXT NOT NULL,
64
+ kind TEXT NOT NULL,
65
+ label TEXT NOT NULL,
66
+ description TEXT,
67
+ aliases_json TEXT NOT NULL DEFAULT '[]',
68
+ payload_json TEXT NOT NULL DEFAULT '{}',
69
+ valid_from TEXT,
70
+ valid_to TEXT,
71
+ created_at TEXT NOT NULL,
72
+ updated_at TEXT NOT NULL,
73
+ FOREIGN KEY(project_id) REFERENCES projects(id)
74
+ );
75
+ CREATE INDEX IF NOT EXISTS idx_concepts_project_kind ON concepts(project_id, kind);
76
+ CREATE INDEX IF NOT EXISTS idx_concepts_project_label ON concepts(project_id, label);
77
+ CREATE INDEX IF NOT EXISTS idx_concepts_valid_to ON concepts(project_id, valid_to);
78
+
79
+ CREATE TABLE IF NOT EXISTS durable_memories (
80
+ id TEXT PRIMARY KEY,
81
+ project_id TEXT NOT NULL,
82
+ type TEXT NOT NULL,
83
+ summary TEXT NOT NULL,
84
+ body TEXT,
85
+ source_event_id TEXT,
86
+ source_handoff_id TEXT,
87
+ confidence REAL NOT NULL DEFAULT 0,
88
+ status TEXT NOT NULL,
89
+ valid_from TEXT,
90
+ valid_to TEXT,
91
+ created_at TEXT NOT NULL,
92
+ updated_at TEXT NOT NULL,
93
+ FOREIGN KEY(project_id) REFERENCES projects(id),
94
+ FOREIGN KEY(source_event_id) REFERENCES events(id),
95
+ FOREIGN KEY(source_handoff_id) REFERENCES handoffs(id)
96
+ );
97
+ CREATE INDEX IF NOT EXISTS idx_durable_memories_project_type ON durable_memories(project_id, type);
98
+ CREATE INDEX IF NOT EXISTS idx_durable_memories_project_status ON durable_memories(project_id, status);
99
+ CREATE INDEX IF NOT EXISTS idx_durable_memories_source_event ON durable_memories(source_event_id);
100
+
101
+ CREATE TABLE IF NOT EXISTS decision_records (
102
+ id TEXT PRIMARY KEY,
103
+ project_id TEXT NOT NULL,
104
+ title TEXT NOT NULL,
105
+ rationale TEXT NOT NULL,
106
+ alternatives_json TEXT NOT NULL DEFAULT '[]',
107
+ evidence_json TEXT NOT NULL DEFAULT '[]',
108
+ status TEXT NOT NULL,
109
+ reversible INTEGER NOT NULL DEFAULT 1,
110
+ source_event_id TEXT,
111
+ supersedes_decision_id TEXT,
112
+ valid_from TEXT,
113
+ valid_to TEXT,
114
+ created_at TEXT NOT NULL,
115
+ updated_at TEXT NOT NULL,
116
+ FOREIGN KEY(project_id) REFERENCES projects(id),
117
+ FOREIGN KEY(source_event_id) REFERENCES events(id),
118
+ FOREIGN KEY(supersedes_decision_id) REFERENCES decision_records(id)
119
+ );
120
+ CREATE INDEX IF NOT EXISTS idx_decision_records_project_status ON decision_records(project_id, status);
121
+ CREATE INDEX IF NOT EXISTS idx_decision_records_source_event ON decision_records(source_event_id);
122
+
123
+ CREATE TABLE IF NOT EXISTS relations (
124
+ id TEXT PRIMARY KEY,
125
+ project_id TEXT NOT NULL,
126
+ source_type TEXT NOT NULL,
127
+ source_id TEXT NOT NULL,
128
+ target_type TEXT NOT NULL,
129
+ target_id TEXT NOT NULL,
130
+ relation TEXT NOT NULL,
131
+ weight REAL NOT NULL DEFAULT 1,
132
+ payload_json TEXT NOT NULL DEFAULT '{}',
133
+ valid_from TEXT,
134
+ valid_to TEXT,
135
+ created_at TEXT NOT NULL,
136
+ updated_at TEXT NOT NULL,
137
+ FOREIGN KEY(project_id) REFERENCES projects(id)
138
+ );
139
+ CREATE INDEX IF NOT EXISTS idx_relations_project_source ON relations(project_id, source_type, source_id);
140
+ CREATE INDEX IF NOT EXISTS idx_relations_project_target ON relations(project_id, target_type, target_id);
141
+ CREATE INDEX IF NOT EXISTS idx_relations_project_relation ON relations(project_id, relation);
142
+
143
+ CREATE TABLE IF NOT EXISTS memory_references (
144
+ id TEXT PRIMARY KEY,
145
+ project_id TEXT NOT NULL,
146
+ source_type TEXT NOT NULL,
147
+ source_id TEXT NOT NULL,
148
+ target_type TEXT NOT NULL,
149
+ target_id TEXT NOT NULL,
150
+ ref_kind TEXT NOT NULL DEFAULT 'mentions',
151
+ weight REAL NOT NULL DEFAULT 1,
152
+ created_at TEXT NOT NULL,
153
+ FOREIGN KEY(project_id) REFERENCES projects(id)
154
+ );
155
+ CREATE INDEX IF NOT EXISTS idx_memory_references_project_source ON memory_references(project_id, source_type, source_id);
60
156
  `);
157
+ // schema v3 upgrade: add retention/reference columns to existing v1/v2 DBs (idempotent)
158
+ const addCol = (sql) => {
159
+ try {
160
+ db.exec(sql);
161
+ }
162
+ catch (e) {
163
+ const m = String(e.message || e);
164
+ if (!/duplicate column name|already exists/i.test(m))
165
+ throw e;
166
+ }
167
+ };
168
+ addCol("ALTER TABLE concepts ADD COLUMN score REAL NOT NULL DEFAULT 0");
169
+ addCol("ALTER TABLE concepts ADD COLUMN retention_class TEXT NOT NULL DEFAULT 'working'");
170
+ addCol("ALTER TABLE concepts ADD COLUMN manual_pin INTEGER NOT NULL DEFAULT 0");
171
+ addCol("ALTER TABLE concepts ADD COLUMN ref_count INTEGER NOT NULL DEFAULT 0");
172
+ addCol("ALTER TABLE concepts ADD COLUMN project_spread INTEGER NOT NULL DEFAULT 1");
173
+ addCol("ALTER TABLE concepts ADD COLUMN first_seen TEXT");
174
+ addCol("ALTER TABLE concepts ADD COLUMN last_seen TEXT");
175
+ addCol("ALTER TABLE durable_memories ADD COLUMN retention_class TEXT NOT NULL DEFAULT 'durable'");
176
+ compactMemoryReferences(db);
177
+ db.exec(`
178
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_references_unique_edge ON memory_references(
179
+ project_id, source_type, source_id, target_type, target_id, ref_kind
180
+ )
181
+ `);
182
+ recomputeConceptReferenceCounts(db);
61
183
  db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', ?)").run(String(SCHEMA_VERSION));
62
184
  }
185
+ function compactMemoryReferences(db) {
186
+ db.exec(`
187
+ DELETE FROM memory_references
188
+ WHERE id NOT IN (
189
+ SELECT MIN(id)
190
+ FROM memory_references
191
+ GROUP BY project_id, source_type, source_id, target_type, target_id, ref_kind
192
+ )
193
+ `);
194
+ }
195
+ function recomputeConceptReferenceCounts(db) {
196
+ db.exec(`
197
+ UPDATE concepts
198
+ SET ref_count = (
199
+ SELECT COUNT(*)
200
+ FROM memory_references
201
+ WHERE memory_references.project_id = concepts.project_id
202
+ AND memory_references.target_type = 'concept'
203
+ AND memory_references.target_id = concepts.id
204
+ )
205
+ `);
206
+ }
63
207
  export function initMemory(dbPath = defaultDbPath()) {
64
208
  const db = openMemoryDb(dbPath);
65
209
  try {
@@ -0,0 +1,56 @@
1
+ import { migrate, openMemoryDb } from "./memoryDb.js";
2
+ import { defaultDbPath, resolveProjectContext } from "./projectContext.js";
3
+ import { resolveStoredProject } from "./projectMigration.js";
4
+ export function recallEvents(input, dbPath = defaultDbPath()) {
5
+ const terms = recallTerms(input.query);
6
+ if (terms.length === 0)
7
+ return [];
8
+ const db = openMemoryDb(dbPath);
9
+ try {
10
+ migrate(db);
11
+ const project = resolveStoredProject(db, resolveProjectContext());
12
+ const minimumMatches = Math.min(2, terms.length);
13
+ const score = terms.map(() => "CASE WHEN LOWER(summary) LIKE ? ESCAPE '\\' THEN 1 ELSE 0 END").join(" + ");
14
+ const patternArgs = terms.map((term) => `%${escapeLikeTerm(term)}%`);
15
+ return db
16
+ .prepare(`
17
+ SELECT id, type, summary, created_at AS createdAt, session_id AS sessionId
18
+ FROM events
19
+ WHERE project_id = ? AND (${score}) >= ?
20
+ ORDER BY created_at DESC
21
+ LIMIT ?
22
+ `)
23
+ .all(project.id, ...patternArgs, minimumMatches, input.limit);
24
+ }
25
+ finally {
26
+ db.close();
27
+ }
28
+ }
29
+ function recallTerms(query) {
30
+ return Array.from(new Set(query.toLocaleLowerCase().match(/[\p{L}\p{N}_-]{3,}/gu) ?? []))
31
+ .filter((term) => !GENERIC_RECALL_TERMS.has(term))
32
+ .slice(0, 8);
33
+ }
34
+ function escapeLikeTerm(term) {
35
+ return term.replace(/[\\%_]/g, (char) => `\\${char}`);
36
+ }
37
+ const GENERIC_RECALL_TERMS = new Set([
38
+ "action",
39
+ "asked",
40
+ "current",
41
+ "help",
42
+ "memory",
43
+ "need",
44
+ "needs",
45
+ "omo",
46
+ "please",
47
+ "prompt",
48
+ "request",
49
+ "requested",
50
+ "session",
51
+ "user",
52
+ "want",
53
+ "wants",
54
+ "work",
55
+ "working",
56
+ ]);
@@ -0,0 +1,33 @@
1
+ import { migrate, openMemoryDb } from "./memoryDb.js";
2
+ import { defaultDbPath, resolveProjectContext } from "./projectContext.js";
3
+ import { resolveStoredProject } from "./projectMigration.js";
4
+ export function memoryPaths() {
5
+ return { dbPath: defaultDbPath() };
6
+ }
7
+ export function doctorReport(dbPath = defaultDbPath()) {
8
+ const db = openMemoryDb(dbPath);
9
+ try {
10
+ migrate(db);
11
+ const project = resolveStoredProject(db, resolveProjectContext());
12
+ const schemaVersion = Number(db.prepare("SELECT value FROM schema_meta WHERE key = 'schema_version'").pluck().get());
13
+ const count = (table) => Number(db.prepare(`SELECT COUNT(*) FROM ${table}`).pluck().get());
14
+ return {
15
+ paths: { dbPath },
16
+ schemaVersion,
17
+ project,
18
+ counts: {
19
+ projects: count("projects"),
20
+ sessions: count("sessions"),
21
+ events: count("events"),
22
+ handoffs: count("handoffs"),
23
+ concepts: count("concepts"),
24
+ relations: count("relations"),
25
+ durableMemories: count("durable_memories"),
26
+ decisionRecords: count("decision_records"),
27
+ },
28
+ };
29
+ }
30
+ finally {
31
+ db.close();
32
+ }
33
+ }
@@ -0,0 +1,142 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { upsertProject } from "./memory.js";
3
+ import { migrate, openMemoryDb } from "./memoryDb.js";
4
+ import { redactSecrets } from "./privacy.js";
5
+ export { listOntologyRows } from "./ontologyQueries.js";
6
+ export { supersedeDurableMemory } from "./ontologySupersede.js";
7
+ function nowIso() {
8
+ return new Date().toISOString();
9
+ }
10
+ function normalizeLabel(label) {
11
+ return label.trim().toLowerCase();
12
+ }
13
+ export function upsertConcept(dbPath, project, input) {
14
+ const db = openMemoryDb(dbPath);
15
+ try {
16
+ migrate(db);
17
+ upsertProject(db, project);
18
+ const label = normalizeLabel(input.label);
19
+ const now = nowIso();
20
+ const existing = db.prepare(`SELECT id FROM concepts WHERE project_id = ? AND label = ? LIMIT 1`).get(project.id, label);
21
+ if (existing?.id) {
22
+ db.prepare("UPDATE concepts SET last_seen = ?, updated_at = ? WHERE id = ? AND project_id = ?").run(now, now, existing.id, project.id);
23
+ const row = db
24
+ .prepare(`SELECT id, kind, label, description, aliases_json AS aliasesJson, payload_json AS payloadJson,
25
+ valid_from AS validFrom, valid_to AS validTo, created_at AS createdAt, updated_at AS updatedAt,
26
+ COALESCE(score, 0) AS score, COALESCE(retention_class, 'working') AS retentionClass,
27
+ COALESCE(manual_pin, 0) AS manualPin, COALESCE(ref_count, 0) AS refCount,
28
+ COALESCE(project_spread, 1) AS projectSpread, first_seen AS firstSeen, last_seen AS lastSeen
29
+ FROM concepts WHERE id = ?`)
30
+ .get(existing.id);
31
+ return row;
32
+ }
33
+ const id = randomUUID();
34
+ const score = input.score ?? 0;
35
+ const retentionClass = input.retentionClass ?? "working";
36
+ const manualPin = input.manualPin ?? 0;
37
+ const created = now;
38
+ db.prepare(`
39
+ INSERT INTO concepts (
40
+ id, project_id, kind, label, description, aliases_json, payload_json,
41
+ valid_from, valid_to, created_at, updated_at,
42
+ score, retention_class, manual_pin, ref_count, project_spread, first_seen, last_seen
43
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
44
+ `).run(id, project.id, input.kind, label, input.description ?? null, "[]", "{}", null, null, created, created, score, retentionClass, manualPin, 0, 1, created, created);
45
+ const row = db
46
+ .prepare(`SELECT id, kind, label, description, aliases_json AS aliasesJson, payload_json AS payloadJson,
47
+ valid_from AS validFrom, valid_to AS validTo, created_at AS createdAt, updated_at AS updatedAt,
48
+ COALESCE(score, 0) AS score, COALESCE(retention_class, 'working') AS retentionClass,
49
+ COALESCE(manual_pin, 0) AS manualPin, COALESCE(ref_count, 0) AS refCount,
50
+ COALESCE(project_spread, 1) AS projectSpread, first_seen AS firstSeen, last_seen AS lastSeen
51
+ FROM concepts WHERE id = ?`)
52
+ .get(id);
53
+ return row;
54
+ }
55
+ finally {
56
+ db.close();
57
+ }
58
+ }
59
+ export function createDurableMemory(dbPath, project, input) {
60
+ const db = openMemoryDb(dbPath);
61
+ try {
62
+ migrate(db);
63
+ upsertProject(db, project);
64
+ const id = randomUUID();
65
+ const created = nowIso();
66
+ const redactedSummary = redactSecrets(input.summary);
67
+ const redactedBody = input.body == null ? null : redactSecrets(input.body);
68
+ const status = input.status ?? "active";
69
+ const retentionClass = input.retentionClass ?? "durable";
70
+ const confidence = input.confidence ?? 0;
71
+ db.prepare(`
72
+ INSERT INTO durable_memories (
73
+ id, project_id, type, summary, body, source_event_id, source_handoff_id,
74
+ confidence, status, retention_class, valid_from, valid_to, created_at, updated_at
75
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
76
+ `).run(id, project.id, input.type, redactedSummary, redactedBody, input.sourceEventId ?? null, input.sourceHandoffId ?? null, confidence, status, retentionClass, null, null, created, created);
77
+ const row = db
78
+ .prepare(`SELECT id, type, summary, body, source_event_id AS sourceEventId, source_handoff_id AS sourceHandoffId,
79
+ confidence, status, COALESCE(retention_class, 'durable') AS retentionClass,
80
+ valid_from AS validFrom, valid_to AS validTo, created_at AS createdAt, updated_at AS updatedAt
81
+ FROM durable_memories WHERE id = ?`)
82
+ .get(id);
83
+ return row;
84
+ }
85
+ finally {
86
+ db.close();
87
+ }
88
+ }
89
+ export function recordMemoryReference(dbPath, project, input) {
90
+ const db = openMemoryDb(dbPath);
91
+ try {
92
+ migrate(db);
93
+ upsertProject(db, project);
94
+ const id = randomUUID();
95
+ const created = nowIso();
96
+ const refKind = input.refKind ?? "mentions";
97
+ const weight = input.weight ?? 1;
98
+ const inserted = db
99
+ .prepare(`
100
+ INSERT INTO memory_references (
101
+ id, project_id, source_type, source_id, target_type, target_id, ref_kind, weight, created_at
102
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
103
+ ON CONFLICT(project_id, source_type, source_id, target_type, target_id, ref_kind) DO NOTHING
104
+ `)
105
+ .run(id, project.id, input.sourceType, input.sourceId, input.targetType, input.targetId, refKind, weight, created);
106
+ if (inserted.changes > 0 && input.targetType === "concept") {
107
+ db.prepare("UPDATE concepts SET ref_count = COALESCE(ref_count, 0) + 1, last_seen = ?, updated_at = ? WHERE id = ? AND project_id = ?").run(created, created, input.targetId, project.id);
108
+ }
109
+ const row = db
110
+ .prepare(`SELECT id, source_type AS sourceType, source_id AS sourceId, target_type AS targetType, target_id AS targetId,
111
+ ref_kind AS refKind, weight, created_at AS createdAt
112
+ FROM memory_references
113
+ WHERE project_id = ? AND source_type = ? AND source_id = ? AND target_type = ? AND target_id = ? AND ref_kind = ?`)
114
+ .get(project.id, input.sourceType, input.sourceId, input.targetType, input.targetId, refKind);
115
+ return row;
116
+ }
117
+ finally {
118
+ db.close();
119
+ }
120
+ }
121
+ export function updateDurableRetention(dbPath, project, durableId, update) {
122
+ const db = openMemoryDb(dbPath);
123
+ try {
124
+ migrate(db);
125
+ const existing = db.prepare("SELECT retention_class FROM durable_memories WHERE id = ? AND project_id = ?").get(durableId, project.id);
126
+ if (!existing) {
127
+ throw new Error("durable memory not found for project");
128
+ }
129
+ const now = nowIso();
130
+ db.prepare("UPDATE durable_memories SET retention_class = COALESCE(?, retention_class), updated_at = ? WHERE id = ? AND project_id = ?").run(update.retentionClass ?? null, now, durableId, project.id);
131
+ const row = db
132
+ .prepare(`SELECT id, type, summary, body, source_event_id AS sourceEventId, source_handoff_id AS sourceHandoffId,
133
+ confidence, status, COALESCE(retention_class, 'durable') AS retentionClass,
134
+ valid_from AS validFrom, valid_to AS validTo, created_at AS createdAt, updated_at AS updatedAt
135
+ FROM durable_memories WHERE id = ?`)
136
+ .get(durableId);
137
+ return row;
138
+ }
139
+ finally {
140
+ db.close();
141
+ }
142
+ }