omo-memory 0.1.14 → 0.1.16

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/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # OMO Memory
2
2
 
3
- OMO Memory is a host-neutral local session/work memory for OMO adapters.
3
+ OMO Memory is a host-neutral local session/work/event ledger for OMO adapters.
4
4
 
5
5
  It gives lazycodex, omo-on-opencode, lfg, and future OMO adapters a shared local SQLite ledger that can be accessed through both:
6
6
 
7
- - `omo-memory` CLI for install, inspection, search, and handoff workflows.
7
+ - `omo-memory` CLI for init, inspection, explicit event recall, handoff, export, purge, and global event import workflows.
8
8
  - `omo-memory mcp` stdio server for coding tools and agents.
9
9
 
10
10
  ## Product shape
@@ -23,13 +23,11 @@ npm run build
23
23
  node dist/cli.js init
24
24
  node dist/cli.js global scan --root ..
25
25
  node dist/cli.js global migrate --root .. --global-db ~/.omo/memory/global.sqlite
26
+ node dist/cli.js global list --global-db ~/.omo/memory/global.sqlite
26
27
  node dist/cli.js session start --host grok --adapter lfg
27
28
  node dist/cli.js event record --type decision --summary "Chose SQLite + MCP + CLI for OMO shared memory"
28
- node dist/cli.js ontology candidates
29
- node dist/cli.js ontology score
30
- node dist/cli.js ontology recall --query "sqlite retention"
31
- node dist/cli.js graph tui
32
29
  node dist/cli.js recent
30
+ node dist/cli.js recall --query "sqlite decision"
33
31
  node dist/cli.js mcp
34
32
  ```
35
33
 
@@ -42,9 +40,8 @@ npx -y omo-memory init
42
40
  npx -y omo-memory update
43
41
  npx -y omo-memory global scan --root .
44
42
  npx -y omo-memory global migrate --root . --global-db ~/.omo/memory/global.sqlite
43
+ npx -y omo-memory global list --global-db ~/.omo/memory/global.sqlite
45
44
  npx -y omo-memory session bootstrap --host codex --adapter lazycodex --limit 5
46
- npx -y omo-memory ontology recall --query "why did we choose sqlite" --limit 5
47
- npx -y omo-memory graph tui
48
45
  npx -y omo-memory recent --limit 5
49
46
  npx -y omo-memory recall --query "why did we choose sqlite" --limit 5
50
47
  npx -y omo-memory mcp
@@ -59,6 +56,8 @@ npm link
59
56
  omo-memory init
60
57
  ```
61
58
 
59
+ Install and update run a local Codex cleanup migration through `postinstall`. It removes stale `omo-memory@islee23520` hook state and legacy hook files from `~/.codex` when they exist; it does not touch unrelated OMO/LazyCodex hooks.
60
+
62
61
  ## MCP registration
63
62
 
64
63
  Register the same MCP server in every host that should read/write the current project's memory DB.
@@ -105,8 +104,7 @@ The response contains a new `sessionId` and project metadata only. It deliberate
105
104
  }
106
105
  ```
107
106
 
108
- This is local routing, not transcript scraping. OMO Memory does not automatically read full Codex or Grok transcripts. Hooks should record concise user actions, decisions, QA evidence, and handoffs; they should retrieve memory only when the user explicitly asks for OMO Memory or when the current user input can be matched to recorded intent.
109
- The packaged `scripts/omo-memory-user-prompt.mjs` helper is the supported UserPromptSubmit hook target for adapters that can invoke a command with the hook payload on stdin. It records only the current user prompt as a redacted `user_prompt` event, ignores assistant output, and exits successfully without blocking the host when OMO Memory is unavailable.
107
+ This is local routing, not transcript scraping. OMO Memory does not automatically read full Codex or Grok transcripts. Adapters should record concise user actions, decisions, QA evidence, and handoffs through the CLI or MCP tools; they should retrieve memory only when the user explicitly asks for OMO Memory or when the current user input can be matched to recorded intent.
110
108
 
111
109
  Use explicit retrieval for memory reads:
112
110
 
@@ -134,17 +132,11 @@ Initial stdio MCP tools:
134
132
  - `memory_global_scan`
135
133
  - `memory_global_migrate`
136
134
  - `memory_global_list`
137
- - `memory_ontology_candidates`
138
- - `memory_ontology_extract`
139
- - `memory_ontology_score`
140
- - `memory_ontology_promote`
141
- - `memory_ontology_demote`
142
- - `memory_ontology_supersede`
143
- - `memory_ontology_recall`
135
+
144
136
 
145
137
  ## Updates
146
138
 
147
- Installed CLI commands automatically launch a quiet background `npm install -g omo-memory@latest` at most once per day. MCP startup does not run the updater, so stdio handshakes stay clean.
139
+ Installed CLI commands automatically launch a quiet background `npm install -g omo-memory@latest` at most once per day. MCP startup does not run the updater, so stdio handshakes stay clean. The package `postinstall` cleanup also runs during these updates and removes legacy Codex `omo-memory@islee23520` hook registrations.
148
140
 
149
141
  Manual update:
150
142
 
@@ -158,54 +150,11 @@ Disable automatic update for pinned environments:
158
150
  OMO_MEMORY_AUTO_UPDATE=0 omo-memory doctor
159
151
  ```
160
152
 
161
- ## Second-brain layer
162
-
163
- The base ledger remains project-local and chronological: sessions, events, handoffs, and explicit recall. The second-brain layer adds deterministic ontology tables and lifecycle commands:
164
-
165
- - Global migration copies existing local `.omo/memory/state.sqlite` databases into one global SQLite store with source provenance and an aggregate OMO schema view. It does not delete or rewrite local project ledgers.
166
- - Concept extraction turns concise event summaries into vocabulary candidates and reference counts.
167
- - Retention scoring classifies memory as `forget`, `temporary`, `working`, `durable`, or `permanent`; manual pins force `permanent`.
168
- - Durable memories can be promoted, demoted, superseded, and recalled through CLI or MCP.
169
- - `omo-memory graph tui` opens an OpenTUI ontology graph viewer for concepts, relations, retention class, and detail panes. This command needs `bun` on `PATH` because OpenTUI's terminal renderer uses Bun native FFI; the rest of the CLI runs on Node.
170
-
171
- Retention classes:
172
-
173
- - `forget`: low-value or stale one-off context that can be dropped.
174
- - `temporary`: short-term context useful during a narrow task.
175
- - `working`: active project memory worth keeping across the current iteration.
176
- - `durable`: cross-session knowledge that should survive normal decay.
177
- - `permanent`: manually pinned or high-score knowledge; only explicit demote, supersede, or purge should change it.
178
-
179
- Ontology lifecycle commands:
180
-
181
- ```sh
182
- omo-memory ontology candidates
183
- omo-memory ontology score
184
- omo-memory ontology promote --concept linaforge --summary "Linaforge is an active game-engine project"
185
- omo-memory ontology recall --query "linaforge"
186
- omo-memory ontology demote --id <durable-id>
187
- omo-memory ontology supersede --id <durable-id> --summary "Updated durable memory"
188
- ```
189
-
190
- Global second-brain flow:
191
-
192
- ```sh
193
- omo-memory global scan --root /Users/ilseoblee/workspace
194
- omo-memory global migrate --root /Users/ilseoblee/workspace --global-db ~/.omo/memory/global.sqlite
195
- OMO_MEMORY_DB=~/.omo/memory/global.sqlite omo-memory ontology candidates
196
- OMO_MEMORY_DB=~/.omo/memory/global.sqlite omo-memory ontology score
197
- bun --version
198
- omo-memory graph tui --db ~/.omo/memory/global.sqlite --query linaforge
199
- ```
200
-
201
- OpenTUI graph controls:
153
+ ## Cross-project event import
202
154
 
203
- - `q`: quit.
204
- - `Up` / `Down`: move selected concept.
205
- - `Tab`: move to the next concept.
206
- - `/` or `f`: focus filter input when supported by the terminal runtime.
155
+ The base ledger is project-local and chronological: sessions, events, handoffs, and explicit recall. Global migration copies existing local `.omo/memory/state.sqlite` databases into one global SQLite store with source provenance so operators can search imported event history across projects. It does not delete or rewrite local project ledgers.
207
156
 
208
- The graph is terminal-native. It does not require a browser, web server, cloud service, or embeddings, but it does require Bun for the OpenTUI renderer.
157
+ This is not an automatic second brain or knowledge graph. OMO Memory does not ship automatic concept extraction, retention scoring, durable-memory curation, OpenTUI, or terminal graph commands. Use explicit event summaries, `recent`, `recall`, `handoff write`, and global event import for cross-session continuity.
209
158
 
210
159
  ## Non-goals for MVP
211
160
 
package/dist/cli.js CHANGED
@@ -1,17 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync } from "node:fs";
3
3
  import { maybeRunAutoUpdate, runAutoUpdate } from "./autoUpdate.js";
4
- import { applyConceptExtraction } from "./conceptExtraction.js";
5
- import { migrateToGlobalMemory, scanForMemoryDbs } from "./globalMemory.js";
6
- import { runGraphTui } from "./graphTui.js";
4
+ import { listGlobalMemory, migrateToGlobalMemory, scanForMemoryDbs } from "./globalMemory.js";
7
5
  import { runMcpServer } from "./mcp.js";
8
6
  import { bootstrapSession, exportMemory, purgeMemory, recentEvents, recordEvent, startSession, writeHandoff } from "./memory.js";
9
7
  import { initMemory } from "./memoryDb.js";
10
8
  import { recallEvents } from "./memoryRecall.js";
11
9
  import { doctorReport } from "./memoryReport.js";
12
- import { createDurableMemory, listOntologyRows, recordMemoryReference, supersedeDurableMemory, updateDurableRetention } from "./ontologyCore.js";
13
- import { defaultDbPath } from "./projectContext.js";
14
- import { recomputeRetentionScores } from "./retentionRecompute.js";
15
10
  async function main(argv) {
16
11
  const [command, subcommand, ...rest] = argv;
17
12
  if (command === undefined || command === "help" || command === "--help" || command === "-h") {
@@ -28,11 +23,6 @@ async function main(argv) {
28
23
  return;
29
24
  }
30
25
  maybeRunAutoUpdate(currentVersion);
31
- if (command === "graph" && subcommand === "tui") {
32
- const query = readFlag(rest, "--query");
33
- await runGraphTui({ dbPath: readFlag(rest, "--db") ?? defaultDbPath(), ...(query === undefined ? {} : { query }) });
34
- return;
35
- }
36
26
  const result = runCommand(command, subcommand, rest);
37
27
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
38
28
  }
@@ -53,9 +43,6 @@ function runCommand(command, subcommand, rest) {
53
43
  if (command === "global") {
54
44
  return runGlobalCommand(subcommand, rest);
55
45
  }
56
- if (command === "ontology") {
57
- return runOntologyCommand(subcommand, rest);
58
- }
59
46
  if (command === "session" && subcommand === "start") {
60
47
  const host = parseHost(readFlag(rest, "--host") ?? "unknown");
61
48
  const adapter = readFlag(rest, "--adapter") ?? "unknown";
@@ -104,81 +91,11 @@ function runGlobalCommand(subcommand, rest) {
104
91
  const globalDbPath = readFlag(rest, "--global-db") ?? fail("global migrate requires --global-db");
105
92
  return { ok: true, ...migrateToGlobalMemory({ rootPath, globalDbPath }) };
106
93
  }
107
- fail(`unknown command: global ${subcommand ?? ""}`.trim());
108
- }
109
- function runOntologyCommand(subcommand, rest) {
110
- const dbPath = defaultDbPath();
111
- if (subcommand === "candidates")
112
- return ontologyCandidates(dbPath);
113
- if (subcommand === "score" || subcommand === "recompute") {
114
- return { ok: true, ...recomputeRetentionScores({ dbPath, nowIso: new Date().toISOString() }) };
115
- }
116
- if (subcommand === "promote")
117
- return ontologyPromote(dbPath, rest);
118
- if (subcommand === "demote") {
119
- const id = readFlag(rest, "--id") ?? fail("ontology demote requires --id");
120
- return { ok: true, durableMemory: updateDurableRetention(dbPath, exportMemory(dbPath).project, id, { retentionClass: "temporary" }) };
121
- }
122
- if (subcommand === "supersede") {
123
- const id = readFlag(rest, "--id") ?? fail("ontology supersede requires --id");
124
- const newSummary = readFlag(rest, "--summary");
125
- return { ok: true, ...supersedeDurableMemory(dbPath, exportMemory(dbPath).project, id, newSummary === undefined ? {} : { newSummary }) };
126
- }
127
- if (subcommand === "recall") {
128
- const query = readFlag(rest, "--query") ?? fail("ontology recall requires --query");
129
- const limit = readPositiveIntFlag(rest, "--limit", 10);
130
- return { ok: true, durableMemories: recallDurableMemories(dbPath, query, limit) };
131
- }
132
- fail(`unknown command: ontology ${subcommand ?? ""}`.trim());
133
- }
134
- function ontologyCandidates(dbPath) {
135
- const memory = exportMemory(dbPath);
136
- let references = 0;
137
- for (const event of memory.events) {
138
- references += applyConceptExtraction(dbPath, memory.project, event.id, event.summary, event.type).references.length;
94
+ if (subcommand === "list") {
95
+ const globalDbPath = readFlag(rest, "--global-db") ?? fail("global list requires --global-db");
96
+ return { ok: true, ...listGlobalMemory(globalDbPath) };
139
97
  }
140
- return { ok: true, concepts: listOntologyRows(dbPath, memory.project).concepts, references };
141
- }
142
- function ontologyPromote(dbPath, rest) {
143
- const memory = exportMemory(dbPath);
144
- const conceptSelector = readFlag(rest, "--concept") ?? readFlag(rest, "--concept-id") ?? fail("ontology promote requires --concept");
145
- const concept = findConcept(memory.concepts, conceptSelector) ?? fail(`ontology promote candidate not found: ${conceptSelector}`);
146
- const summary = readFlag(rest, "--summary") ?? `Durable memory: ${concept.label}`;
147
- const body = readFlag(rest, "--body") ?? concept.description ?? concept.label;
148
- const durableMemory = createDurableMemory(dbPath, memory.project, {
149
- type: "concept",
150
- summary,
151
- body,
152
- confidence: Math.min(1, Math.max(0, concept.score / 100)),
153
- status: "active",
154
- retentionClass: "durable",
155
- });
156
- const reference = recordMemoryReference(dbPath, memory.project, {
157
- sourceType: "concept",
158
- sourceId: concept.id,
159
- targetType: "durable_memory",
160
- targetId: durableMemory.id,
161
- refKind: "promotes",
162
- weight: 1,
163
- });
164
- return { ok: true, concept, durableMemory, reference };
165
- }
166
- function findConcept(concepts, selector) {
167
- const normalized = selector.trim().toLowerCase();
168
- return concepts.find((concept) => concept.id === selector || concept.label.toLowerCase() === normalized);
169
- }
170
- function recallDurableMemories(dbPath, query, limit) {
171
- const memory = exportMemory(dbPath);
172
- const terms = query.toLowerCase().match(/[\p{L}\p{N}_-]{3,}/gu) ?? [];
173
- if (terms.length === 0)
174
- return [];
175
- return listOntologyRows(dbPath, memory.project)
176
- .durableMemories.filter((durable) => durable.status === "active")
177
- .filter((durable) => {
178
- const haystack = `${durable.summary} ${durable.body ?? ""}`.toLowerCase();
179
- return terms.some((term) => haystack.includes(term));
180
- })
181
- .slice(0, limit);
98
+ fail(`unknown command: global ${subcommand ?? ""}`.trim());
182
99
  }
183
100
  function readFlag(args, name) {
184
101
  const index = args.indexOf(name);
@@ -207,7 +124,7 @@ function fail(message) {
207
124
  throw new Error(message);
208
125
  }
209
126
  function printHelp() {
210
- process.stdout.write(`OMO Memory\n\nCommands:\n omo-memory init\n omo-memory doctor\n omo-memory update\n omo-memory export\n omo-memory purge --yes\n omo-memory global scan --root <path> [--json]\n omo-memory global migrate --root <path> --global-db <path> [--json]\n omo-memory ontology candidates\n omo-memory ontology score\n omo-memory ontology promote --concept <label|id> [--summary <text>] [--body <text>]\n omo-memory ontology demote --id <durable-id>\n omo-memory ontology supersede --id <durable-id> [--summary <text>]\n omo-memory ontology recall --query <text> [--limit <n>]\n omo-memory session start --host <codex|opencode|grok|unknown> --adapter <name>\n omo-memory session bootstrap --host <codex|opencode|grok|unknown> --adapter <name> [--limit <n>]\n omo-memory event record --type <type> --summary <text> [--session-id <id>]\n omo-memory recent [--limit <n>]\n omo-memory recall --query <text> [--limit <n>]\n omo-memory handoff write (--summary <text> | --summary-file <path>) [--session-id <id>]\n omo-memory graph tui [--db <path>] [--query <text>] (requires Bun on PATH)\n omo-memory mcp\n`);
127
+ process.stdout.write(`OMO Memory\n\nCommands:\n omo-memory init\n omo-memory doctor\n omo-memory update\n omo-memory export\n omo-memory purge --yes\n omo-memory global scan --root <path> [--json]\n omo-memory global migrate --root <path> --global-db <path> [--json]\n omo-memory global list --global-db <path> [--json]\n omo-memory session start --host <codex|opencode|grok|unknown> --adapter <name>\n omo-memory session bootstrap --host <codex|opencode|grok|unknown> --adapter <name> [--limit <n>]\n omo-memory event record --type <type> --summary <text> [--session-id <id>]\n omo-memory recent [--limit <n>]\n omo-memory recall --query <text> [--limit <n>]\n omo-memory handoff write (--summary <text> | --summary-file <path>) [--session-id <id>]\n omo-memory mcp\n`);
211
128
  }
212
129
  function readPackageVersion() {
213
130
  const rawPackage = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
package/dist/mcp.js CHANGED
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
- import { registerGlobalOntologyTools } from "./mcpOntologyTools.js";
5
+ import { registerGlobalTools } from "./mcpGlobalTools.js";
6
6
  import { bootstrapSession, exportMemory, PurgeConfirmationError, purgeMemory, recentEvents, recordEvent, startSession, writeHandoff } from "./memory.js";
7
7
  import { initMemory } from "./memoryDb.js";
8
8
  import { recallEvents } from "./memoryRecall.js";
@@ -42,7 +42,7 @@ export async function runMcpServer() {
42
42
  throw error;
43
43
  }
44
44
  });
45
- registerGlobalOntologyTools(server);
45
+ registerGlobalTools(server);
46
46
  server.registerTool("memory_start_session", {
47
47
  title: "Start OMO Session",
48
48
  description: "Record a new OMO adapter session for the current project.",
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ import { listGlobalMemory, migrateToGlobalMemory, scanForMemoryDbs } from "./globalMemory.js";
3
+ const nonBlankStringSchema = z.string().trim().min(1);
4
+ export function registerGlobalTools(server) {
5
+ server.registerTool("memory_global_scan", {
6
+ title: "Scan Global OMO Memory Sources",
7
+ description: "Explicitly scan a filesystem root for local OMO memory SQLite databases without importing them.",
8
+ inputSchema: { rootPath: nonBlankStringSchema },
9
+ }, async ({ rootPath }) => jsonResult(scanForMemoryDbs(rootPath)));
10
+ server.registerTool("memory_global_migrate", {
11
+ title: "Migrate OMO Memory To Global SQLite",
12
+ description: "Explicitly create or update a global OMO memory SQLite database from discovered local memory databases.",
13
+ inputSchema: { rootPath: nonBlankStringSchema, globalDbPath: nonBlankStringSchema },
14
+ }, async ({ rootPath, globalDbPath }) => jsonResult(migrateToGlobalMemory({ rootPath, globalDbPath })));
15
+ server.registerTool("memory_global_list", {
16
+ title: "List Global OMO Memory",
17
+ description: "List sources and counts from an explicit global OMO memory SQLite database.",
18
+ inputSchema: { globalDbPath: nonBlankStringSchema },
19
+ }, async ({ globalDbPath }) => jsonResult(listGlobalMemory(globalDbPath)));
20
+ }
21
+ function jsonResult(value) {
22
+ return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
23
+ }
package/dist/memory.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { migrate, openMemoryDb, SCHEMA_VERSION } from "./memoryDb.js";
2
+ import { migrate, openMemoryDb, SCHEMA_VERSION, tableExists } from "./memoryDb.js";
3
3
  import { redactSecrets } from "./privacy.js";
4
4
  import { defaultDbPath, resolveProjectContext } from "./projectContext.js";
5
5
  import { resolveStoredProject } from "./projectMigration.js";
@@ -116,8 +116,9 @@ export function exportMemory(dbPath = defaultDbPath()) {
116
116
  WHERE project_id = ? ORDER BY created_at ASC, id ASC
117
117
  `)
118
118
  .all(project.id);
119
- const concepts = db
120
- .prepare(`
119
+ const concepts = tableExists(db, "concepts")
120
+ ? db
121
+ .prepare(`
121
122
  SELECT id, kind, label, description, aliases_json AS aliasesJson, payload_json AS payloadJson,
122
123
  valid_from AS validFrom, valid_to AS validTo, created_at AS createdAt, updated_at AS updatedAt,
123
124
  COALESCE(score, 0) AS score,
@@ -128,38 +129,47 @@ export function exportMemory(dbPath = defaultDbPath()) {
128
129
  first_seen AS firstSeen, last_seen AS lastSeen
129
130
  FROM concepts WHERE project_id = ? ORDER BY created_at ASC, id ASC
130
131
  `)
131
- .all(project.id);
132
- const relations = db
133
- .prepare(`
132
+ .all(project.id)
133
+ : [];
134
+ const relations = tableExists(db, "relations")
135
+ ? db
136
+ .prepare(`
134
137
  SELECT id, source_type AS sourceType, source_id AS sourceId, target_type AS targetType, target_id AS targetId,
135
138
  relation, weight, payload_json AS payloadJson, valid_from AS validFrom, valid_to AS validTo,
136
139
  created_at AS createdAt, updated_at AS updatedAt
137
140
  FROM relations WHERE project_id = ? ORDER BY created_at ASC, id ASC
138
141
  `)
139
- .all(project.id);
140
- const durableMemories = db
141
- .prepare(`
142
+ .all(project.id)
143
+ : [];
144
+ const durableMemories = tableExists(db, "durable_memories")
145
+ ? db
146
+ .prepare(`
142
147
  SELECT id, type, summary, body, source_event_id AS sourceEventId, source_handoff_id AS sourceHandoffId,
143
148
  confidence, status, COALESCE(retention_class, 'durable') AS retentionClass,
144
149
  valid_from AS validFrom, valid_to AS validTo, created_at AS createdAt, updated_at AS updatedAt
145
150
  FROM durable_memories WHERE project_id = ? ORDER BY created_at ASC, id ASC
146
151
  `)
147
- .all(project.id);
148
- const decisionRecords = db
149
- .prepare(`
152
+ .all(project.id)
153
+ : [];
154
+ const decisionRecords = tableExists(db, "decision_records")
155
+ ? db
156
+ .prepare(`
150
157
  SELECT id, title, rationale, alternatives_json AS alternativesJson, evidence_json AS evidenceJson,
151
158
  status, reversible, source_event_id AS sourceEventId, supersedes_decision_id AS supersedesDecisionId,
152
159
  valid_from AS validFrom, valid_to AS validTo, created_at AS createdAt, updated_at AS updatedAt
153
160
  FROM decision_records WHERE project_id = ? ORDER BY created_at ASC, id ASC
154
161
  `)
155
- .all(project.id);
156
- const memoryReferences = db
157
- .prepare(`
162
+ .all(project.id)
163
+ : [];
164
+ const memoryReferences = tableExists(db, "memory_references")
165
+ ? db
166
+ .prepare(`
158
167
  SELECT id, source_type AS sourceType, source_id AS sourceId, target_type AS targetType, target_id AS targetId,
159
168
  ref_kind AS refKind, weight, created_at AS createdAt
160
169
  FROM memory_references WHERE project_id = ? ORDER BY created_at ASC, id ASC
161
170
  `)
162
- .all(project.id);
171
+ .all(project.id)
172
+ : [];
163
173
  return {
164
174
  schemaVersion: SCHEMA_VERSION,
165
175
  exportedAt: new Date().toISOString(),
@@ -187,18 +197,17 @@ export function purgeMemory(input, dbPath = defaultDbPath()) {
187
197
  migrate(db);
188
198
  const project = resolveStoredProject(db, resolveProjectContext());
189
199
  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;
200
+ const deleteLegacyRows = (table) => {
201
+ if (!tableExists(db, table))
202
+ return 0;
203
+ return db.prepare(`DELETE FROM ${table} WHERE project_id IN (SELECT id FROM projects WHERE id = ? OR repo_root = ?)`).run(project.id, project.repoRoot)
204
+ .changes;
205
+ };
206
+ const memoryReferences = deleteLegacyRows("memory_references");
207
+ const relations = deleteLegacyRows("relations");
208
+ const decisionRecords = deleteLegacyRows("decision_records");
209
+ const durableMemories = deleteLegacyRows("durable_memories");
210
+ const concepts = deleteLegacyRows("concepts");
202
211
  const events = db
203
212
  .prepare("DELETE FROM events WHERE project_id IN (SELECT id FROM projects WHERE id = ? OR repo_root = ?)")
204
213
  .run(project.id, project.repoRoot).changes;
@@ -208,9 +217,6 @@ export function purgeMemory(input, dbPath = defaultDbPath()) {
208
217
  const sessions = db
209
218
  .prepare("DELETE FROM sessions WHERE project_id IN (SELECT id FROM projects WHERE id = ? OR repo_root = ?)")
210
219
  .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;
214
220
  const projects = db.prepare("DELETE FROM projects WHERE id = ? OR repo_root = ?").run(project.id, project.repoRoot).changes;
215
221
  return { events, handoffs, sessions, projects, concepts, relations, durableMemories, decisionRecords, memoryReferences };
216
222
  });
package/dist/memoryDb.js CHANGED
@@ -57,105 +57,12 @@ 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);
156
60
  `);
157
- // schema v3 upgrade: add retention/reference columns to existing v1/v2 DBs (idempotent)
158
- const addCol = (sql) => {
61
+ // Legacy ontology compatibility: existing DBs may contain ontology tables from schema v2/v3.
62
+ // Do not create those tables for fresh DBs, but keep old DBs readable/purgeable.
63
+ const addCol = (table, sql) => {
64
+ if (!tableExists(db, table))
65
+ return;
159
66
  try {
160
67
  db.exec(sql);
161
68
  }
@@ -165,23 +72,46 @@ export function migrate(db) {
165
72
  throw e;
166
73
  }
167
74
  };
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);
75
+ addCol("concepts", "ALTER TABLE concepts ADD COLUMN score REAL NOT NULL DEFAULT 0");
76
+ addCol("concepts", "ALTER TABLE concepts ADD COLUMN retention_class TEXT NOT NULL DEFAULT 'working'");
77
+ addCol("concepts", "ALTER TABLE concepts ADD COLUMN manual_pin INTEGER NOT NULL DEFAULT 0");
78
+ addCol("concepts", "ALTER TABLE concepts ADD COLUMN ref_count INTEGER NOT NULL DEFAULT 0");
79
+ addCol("concepts", "ALTER TABLE concepts ADD COLUMN project_spread INTEGER NOT NULL DEFAULT 1");
80
+ addCol("concepts", "ALTER TABLE concepts ADD COLUMN first_seen TEXT");
81
+ addCol("concepts", "ALTER TABLE concepts ADD COLUMN last_seen TEXT");
82
+ addCol("concepts", "ALTER TABLE concepts ADD COLUMN valid_from TEXT");
83
+ addCol("concepts", "ALTER TABLE concepts ADD COLUMN valid_to TEXT");
84
+ addCol("relations", "ALTER TABLE relations ADD COLUMN valid_from TEXT");
85
+ addCol("relations", "ALTER TABLE relations ADD COLUMN valid_to TEXT");
86
+ addCol("durable_memories", "ALTER TABLE durable_memories ADD COLUMN source_handoff_id TEXT");
87
+ addCol("durable_memories", "ALTER TABLE durable_memories ADD COLUMN retention_class TEXT NOT NULL DEFAULT 'durable'");
88
+ addCol("durable_memories", "ALTER TABLE durable_memories ADD COLUMN valid_from TEXT");
89
+ addCol("durable_memories", "ALTER TABLE durable_memories ADD COLUMN valid_to TEXT");
90
+ addCol("decision_records", "ALTER TABLE decision_records ADD COLUMN alternatives_json TEXT");
91
+ addCol("decision_records", "ALTER TABLE decision_records ADD COLUMN evidence_json TEXT");
92
+ addCol("decision_records", "ALTER TABLE decision_records ADD COLUMN reversible INTEGER");
93
+ addCol("decision_records", "ALTER TABLE decision_records ADD COLUMN supersedes_decision_id TEXT");
94
+ addCol("decision_records", "ALTER TABLE decision_records ADD COLUMN valid_from TEXT");
95
+ addCol("decision_records", "ALTER TABLE decision_records ADD COLUMN valid_to TEXT");
96
+ addCol("memory_references", "ALTER TABLE memory_references ADD COLUMN ref_kind TEXT NOT NULL DEFAULT 'mentions'");
97
+ addCol("memory_references", "ALTER TABLE memory_references ADD COLUMN weight REAL NOT NULL DEFAULT 1");
98
+ if (tableExists(db, "memory_references")) {
99
+ compactMemoryReferences(db);
100
+ db.exec(`
101
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_references_unique_edge ON memory_references(
102
+ project_id, source_type, source_id, target_type, target_id, ref_kind
103
+ )
104
+ `);
105
+ }
106
+ if (tableExists(db, "concepts") && tableExists(db, "memory_references")) {
107
+ recomputeConceptReferenceCounts(db);
108
+ }
183
109
  db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', ?)").run(String(SCHEMA_VERSION));
184
110
  }
111
+ export function tableExists(db, tableName) {
112
+ const row = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(tableName);
113
+ return row !== undefined;
114
+ }
185
115
  function compactMemoryReferences(db) {
186
116
  db.exec(`
187
117
  DELETE FROM memory_references
@@ -1,4 +1,4 @@
1
- import { migrate, openMemoryDb } from "./memoryDb.js";
1
+ import { migrate, openMemoryDb, tableExists } from "./memoryDb.js";
2
2
  import { defaultDbPath, resolveProjectContext } from "./projectContext.js";
3
3
  import { resolveStoredProject } from "./projectMigration.js";
4
4
  export function memoryPaths() {
@@ -10,7 +10,7 @@ export function doctorReport(dbPath = defaultDbPath()) {
10
10
  migrate(db);
11
11
  const project = resolveStoredProject(db, resolveProjectContext());
12
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());
13
+ const count = (table) => (tableExists(db, table) ? Number(db.prepare(`SELECT COUNT(*) FROM ${table}`).pluck().get()) : 0);
14
14
  return {
15
15
  paths: { dbPath },
16
16
  schemaVersion,