omo-memory 0.1.11 → 0.1.13

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/mcp.js CHANGED
@@ -1,11 +1,15 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
4
  import { z } from "zod";
4
- import { bootstrapSession, exportMemory, memoryPaths, PurgeConfirmationError, purgeMemory, recentEvents, recordEvent, startSession, writeHandoff, } from "./memory.js";
5
+ import { registerGlobalOntologyTools } from "./mcpOntologyTools.js";
6
+ import { bootstrapSession, exportMemory, PurgeConfirmationError, purgeMemory, recentEvents, recordEvent, startSession, writeHandoff } from "./memory.js";
5
7
  import { initMemory } from "./memoryDb.js";
8
+ import { recallEvents } from "./memoryRecall.js";
9
+ import { memoryPaths } from "./memoryReport.js";
6
10
  import { resolveProjectContext } from "./projectContext.js";
7
11
  export async function runMcpServer() {
8
- const server = new McpServer({ name: "omo-memory", version: "0.1.2" });
12
+ const server = new McpServer({ name: "omo-memory", version: readPackageVersion() });
9
13
  server.registerTool("memory_init", {
10
14
  title: "Initialize OMO Memory",
11
15
  description: "Create or migrate the local OMO memory SQLite database.",
@@ -38,6 +42,7 @@ export async function runMcpServer() {
38
42
  throw error;
39
43
  }
40
44
  });
45
+ registerGlobalOntologyTools(server);
41
46
  server.registerTool("memory_start_session", {
42
47
  title: "Start OMO Session",
43
48
  description: "Record a new OMO adapter session for the current project.",
@@ -48,13 +53,21 @@ export async function runMcpServer() {
48
53
  }, async ({ host, adapter }) => jsonResult(startSession({ host, adapter })));
49
54
  server.registerTool("memory_bootstrap_session", {
50
55
  title: "Bootstrap OMO Session",
51
- description: "Start a host adapter session and return recent project memory in one call. Call this at the beginning of each Codex, OpenCode, or Grok session.",
56
+ description: "Start a host adapter session without reading or injecting recent memory.",
52
57
  inputSchema: {
53
58
  host: z.enum(["codex", "opencode", "grok", "unknown"]),
54
59
  adapter: z.string().min(1),
55
60
  limit: z.number().int().positive().max(100).default(5),
56
61
  },
57
62
  }, async ({ host, adapter, limit }) => jsonResult(bootstrapSession({ host, adapter, limit })));
63
+ server.registerTool("memory_recall_events", {
64
+ title: "Recall OMO Memory Events",
65
+ description: "Return recorded events only when the query text matches stored intent or content.",
66
+ inputSchema: {
67
+ query: z.string().min(1),
68
+ limit: z.number().int().positive().max(100).default(10),
69
+ },
70
+ }, async ({ query, limit }) => jsonResult({ events: recallEvents({ query, limit }) }));
58
71
  server.registerTool("memory_record_event", {
59
72
  title: "Record OMO Memory Event",
60
73
  description: "Append a summarized event to the current project's OMO memory ledger.",
@@ -82,6 +95,16 @@ export async function runMcpServer() {
82
95
  }, async ({ summaryMd, sessionId }) => jsonResult(writeHandoff(summaryMd, sessionId)));
83
96
  await server.connect(new StdioServerTransport());
84
97
  }
98
+ function readPackageVersion() {
99
+ const rawPackage = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
100
+ if (!isObject(rawPackage))
101
+ return "0.0.0";
102
+ const version = rawPackage["version"];
103
+ return typeof version === "string" && version.length > 0 ? version : "0.0.0";
104
+ }
105
+ function isObject(value) {
106
+ return value !== null && typeof value === "object";
107
+ }
85
108
  function jsonResult(value) {
86
109
  return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
87
110
  }
@@ -0,0 +1,117 @@
1
+ import { z } from "zod";
2
+ import { applyConceptExtraction, extractConceptCandidates } from "./conceptExtraction.js";
3
+ import { listGlobalMemory, migrateToGlobalMemory, scanForMemoryDbs } from "./globalMemory.js";
4
+ import { memoryPaths } from "./memoryReport.js";
5
+ import { createDurableMemory, listOntologyRows, supersedeDurableMemory, updateDurableRetention } from "./ontologyCore.js";
6
+ import { resolveProjectContext } from "./projectContext.js";
7
+ import { recomputeRetentionScores } from "./retentionRecompute.js";
8
+ const retentionClassSchema = z.enum(["forget", "temporary", "working", "durable", "permanent"]);
9
+ const nonBlankStringSchema = z.string().trim().min(1);
10
+ export function registerGlobalOntologyTools(server) {
11
+ server.registerTool("memory_global_scan", {
12
+ title: "Scan Global OMO Memory Sources",
13
+ description: "Explicitly scan a filesystem root for local OMO memory SQLite databases without importing them.",
14
+ inputSchema: { rootPath: nonBlankStringSchema },
15
+ }, async ({ rootPath }) => jsonResult(scanForMemoryDbs(rootPath)));
16
+ server.registerTool("memory_global_migrate", {
17
+ title: "Migrate OMO Memory To Global SQLite",
18
+ description: "Explicitly create or update a global OMO memory SQLite database from discovered local memory databases.",
19
+ inputSchema: { rootPath: nonBlankStringSchema, globalDbPath: nonBlankStringSchema },
20
+ }, async ({ rootPath, globalDbPath }) => jsonResult(migrateToGlobalMemory({ rootPath, globalDbPath })));
21
+ server.registerTool("memory_global_list", {
22
+ title: "List Global OMO Memory",
23
+ description: "List sources and counts from an explicit global OMO memory SQLite database.",
24
+ inputSchema: { globalDbPath: nonBlankStringSchema },
25
+ }, async ({ globalDbPath }) => jsonResult(listGlobalMemory(globalDbPath)));
26
+ registerOntologyTools(server);
27
+ }
28
+ function registerOntologyTools(server) {
29
+ server.registerTool("memory_ontology_candidates", {
30
+ title: "Extract OMO Ontology Candidates",
31
+ description: "Return deterministic ontology candidate labels from a short event summary without writing rows.",
32
+ inputSchema: { summary: nonBlankStringSchema, eventType: nonBlankStringSchema.optional() },
33
+ }, async ({ summary, eventType }) => jsonResult({ candidates: extractConceptCandidates(summary, eventType) }));
34
+ server.registerTool("memory_ontology_extract", {
35
+ title: "Write OMO Ontology Extraction",
36
+ description: "Explicitly extract concepts from a short event summary and write event-to-concept references.",
37
+ inputSchema: {
38
+ sourceEventId: nonBlankStringSchema,
39
+ summary: nonBlankStringSchema,
40
+ eventType: nonBlankStringSchema.optional(),
41
+ },
42
+ }, async ({ sourceEventId, summary, eventType }) => jsonResult(applyConceptExtraction(memoryPaths().dbPath, resolveProjectContext(), sourceEventId, summary, eventType)));
43
+ server.registerTool("memory_ontology_score", {
44
+ title: "Recompute OMO Ontology Scores",
45
+ description: "Explicitly recompute ontology retention scores for the local OMO memory SQLite database.",
46
+ inputSchema: { nowIso: z.string().datetime().optional() },
47
+ }, async ({ nowIso }) => jsonResult(recomputeRetentionScores({ dbPath: memoryPaths().dbPath, nowIso: nowIso ?? new Date().toISOString() })));
48
+ server.registerTool("memory_ontology_promote", {
49
+ title: "Promote OMO Durable Memory",
50
+ description: "Explicitly promote a summarized memory into durable ontology storage.",
51
+ inputSchema: {
52
+ type: nonBlankStringSchema,
53
+ summary: nonBlankStringSchema,
54
+ body: nonBlankStringSchema.optional(),
55
+ sourceEventId: nonBlankStringSchema.optional(),
56
+ sourceHandoffId: nonBlankStringSchema.optional(),
57
+ confidence: z.number().min(0).max(1).optional(),
58
+ status: nonBlankStringSchema.optional(),
59
+ retentionClass: retentionClassSchema.optional(),
60
+ },
61
+ }, async ({ type, summary, body, sourceEventId, sourceHandoffId, confidence, status, retentionClass }) => jsonResult(createDurableMemory(memoryPaths().dbPath, resolveProjectContext(), {
62
+ type,
63
+ summary,
64
+ ...(body === undefined ? {} : { body }),
65
+ ...(sourceEventId === undefined ? {} : { sourceEventId }),
66
+ ...(sourceHandoffId === undefined ? {} : { sourceHandoffId }),
67
+ ...(confidence === undefined ? {} : { confidence }),
68
+ ...(status === undefined ? {} : { status }),
69
+ ...(retentionClass === undefined ? {} : { retentionClass }),
70
+ })));
71
+ server.registerTool("memory_ontology_demote", {
72
+ title: "Demote OMO Durable Memory",
73
+ description: "Explicitly change the retention class for a durable ontology memory.",
74
+ inputSchema: { durableId: nonBlankStringSchema, retentionClass: retentionClassSchema.default("temporary") },
75
+ }, async ({ durableId, retentionClass }) => jsonResult(updateDurableRetention(memoryPaths().dbPath, resolveProjectContext(), durableId, { retentionClass })));
76
+ server.registerTool("memory_ontology_supersede", {
77
+ title: "Supersede OMO Durable Memory",
78
+ description: "Explicitly mark a durable memory superseded and create a successor memory.",
79
+ inputSchema: {
80
+ durableId: nonBlankStringSchema,
81
+ reason: nonBlankStringSchema.optional(),
82
+ newSummary: nonBlankStringSchema.optional(),
83
+ },
84
+ }, async ({ durableId, reason, newSummary }) => jsonResult(supersedeDurableMemory(memoryPaths().dbPath, resolveProjectContext(), durableId, {
85
+ ...(reason === undefined ? {} : { reason }),
86
+ ...(newSummary === undefined ? {} : { newSummary }),
87
+ })));
88
+ server.registerTool("memory_ontology_recall", {
89
+ title: "Recall OMO Ontology Rows",
90
+ description: "Explicitly recall ontology concepts and durable memories matching a query.",
91
+ inputSchema: { query: nonBlankStringSchema, limit: z.number().int().positive().max(100).default(10) },
92
+ }, async ({ query, limit }) => jsonResult(recallOntology(query, limit)));
93
+ }
94
+ function jsonResult(value) {
95
+ return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
96
+ }
97
+ function recallOntology(query, limit) {
98
+ const rows = listOntologyRows(memoryPaths().dbPath, resolveProjectContext());
99
+ const normalizedQuery = query.toLowerCase();
100
+ const queryTerms = normalizedQuery.split(/[^a-z0-9_-]+/).filter((term) => term.length >= 3);
101
+ return {
102
+ concepts: rows.concepts.filter((concept) => matchesConcept(concept, normalizedQuery, queryTerms)).slice(0, limit),
103
+ durableMemories: rows.durableMemories.filter((memory) => matchesDurableMemory(memory, normalizedQuery, queryTerms)).slice(0, limit),
104
+ };
105
+ }
106
+ function matchesConcept(concept, normalizedQuery, queryTerms) {
107
+ return matchesText([concept.label, concept.description ?? "", concept.aliasesJson], normalizedQuery, queryTerms);
108
+ }
109
+ function matchesDurableMemory(memory, normalizedQuery, queryTerms) {
110
+ return matchesText([memory.type, memory.summary, memory.body ?? ""], normalizedQuery, queryTerms);
111
+ }
112
+ function matchesText(values, normalizedQuery, queryTerms) {
113
+ const normalizedValues = values.map((value) => value.toLowerCase());
114
+ if (normalizedValues.some((value) => value.includes(normalizedQuery)))
115
+ return true;
116
+ return queryTerms.length > 0 && queryTerms.every((term) => normalizedValues.some((value) => value.includes(term)));
117
+ }
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
+ }