opencode-fractal-memory 0.6.5 → 0.6.7

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.
@@ -4,11 +4,10 @@ import * as os from "node:os";
4
4
  import { Database } from "bun:sqlite";
5
5
  import { memLog } from "../logging";
6
6
  export const DB_PATHS = {};
7
- export function initDbPaths(projectDir) {
8
- const globalDbPath = path.join(os.homedir(), ".config", "opencode", "memory.db");
9
- const projectDbPath = path.join(projectDir, ".opencode", "memory.db");
10
- DB_PATHS.global = globalDbPath;
11
- DB_PATHS.project = projectDbPath;
7
+ export function initDbPaths(_projectDir) {
8
+ const unifiedDbPath = path.join(os.homedir(), ".config", "opencode", "memory.db");
9
+ DB_PATHS.global = unifiedDbPath;
10
+ DB_PATHS.project = unifiedDbPath;
12
11
  }
13
12
  export function openDb(scope) {
14
13
  const dbPath = DB_PATHS[scope] || scope;
@@ -88,8 +87,9 @@ export function queryNodes(scope) {
88
87
  sticky, confidence, created_at, updated_at, parent_ids,
89
88
  LENGTH(content) as content_length, metadata
90
89
  FROM memory_nodes
90
+ WHERE scope = ?
91
91
  ORDER BY level, importance DESC
92
- `).all();
92
+ `).all(scope);
93
93
  db.close();
94
94
  return rows.map((r) => rowToNode(r));
95
95
  }
@@ -14,6 +14,9 @@ import { setHighContextThreshold, setCriticalContextThreshold, setMaxInjectionTo
14
14
  export async function initStorage(directory) {
15
15
  memLog("info", "init", "Creating memory store", { directory });
16
16
  const store = createMemoryStore(directory);
17
+ memLog("info", "init", "Migrating project DB to unified storage");
18
+ const migrated = await store.migrateFromProjectDb();
19
+ memLog("info", "init", "Project DB migration complete", { migrated });
17
20
  memLog("info", "init", "Ensuring seed nodes");
18
21
  await store.ensureSeed();
19
22
  memLog("info", "init", "Ensuring models");
@@ -2,15 +2,15 @@ import { randomUUID } from "node:crypto";
2
2
  import { rowToNode } from "./queries/base";
3
3
  import { queryGetNode } from "./queries/nodes";
4
4
  import { withRetry } from "./utils";
5
- export async function ensureSeed(getDb, seeds) {
5
+ export async function ensureSeed(getDb, seeds, projectName) {
6
6
  for (const seed of seeds) {
7
7
  const db = await getDb(seed.scope);
8
- const existing = db.query("SELECT id FROM memory_nodes WHERE label = ?").get(seed.label);
8
+ const existing = db.query("SELECT id FROM memory_nodes WHERE label = ? AND scope = ?").get(seed.label, seed.scope);
9
9
  if (existing)
10
10
  continue;
11
11
  const now = Date.now();
12
12
  await withRetry(() => {
13
- db.run("INSERT INTO memory_nodes (id, scope, label, content, summary, level, parent_ids, embedding, created_at, updated_at, importance, access_count, last_accessed, type, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [randomUUID(), seed.scope, seed.label, "", null, 0, null, null, now, now, 0.5, 0, null, "note", null]);
13
+ db.run("INSERT INTO memory_nodes (id, scope, label, content, summary, level, parent_ids, embedding, created_at, updated_at, importance, access_count, last_accessed, type, metadata, project_name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [randomUUID(), seed.scope, seed.label, "", null, 0, null, null, now, now, 0.5, 0, null, "note", null, seed.scope === "project" ? projectName ?? null : null]);
14
14
  });
15
15
  }
16
16
  }
@@ -371,4 +371,26 @@ export const MIGRATIONS = [
371
371
  catch { /* column may already exist */ }
372
372
  },
373
373
  },
374
+ {
375
+ version: 21,
376
+ name: "add-project-name",
377
+ up: (db) => {
378
+ try {
379
+ db.run("ALTER TABLE memory_nodes ADD COLUMN project_name TEXT");
380
+ }
381
+ catch { /* column may already exist */ }
382
+ try {
383
+ db.run("ALTER TABLE bm25_index ADD COLUMN project_name TEXT");
384
+ }
385
+ catch { /* column may already exist */ }
386
+ try {
387
+ db.run("ALTER TABLE bm25_doc_stats ADD COLUMN project_name TEXT");
388
+ }
389
+ catch { /* column may already exist */ }
390
+ try {
391
+ db.run("ALTER TABLE playbooks ADD COLUMN project_name TEXT");
392
+ }
393
+ catch { /* column may already exist */ }
394
+ },
395
+ },
374
396
  ];
@@ -1,6 +1,6 @@
1
1
  import { MIGRATIONS } from "./definitions";
2
2
  export { MIGRATIONS } from "./definitions";
3
- export const CURRENT_VERSION = 20;
3
+ export const CURRENT_VERSION = 21;
4
4
  export function getCurrentVersion(db) {
5
5
  const row = db.query("PRAGMA user_version").get();
6
6
  return row?.user_version ?? 0;
@@ -40,5 +40,6 @@ export function rowToNode(row) {
40
40
  usefulnessScore: row.usefulness_score ?? 0,
41
41
  timesUsed: row.times_used ?? 0,
42
42
  timesHelpful: row.times_helpful ?? 0,
43
+ projectName: row.project_name ?? null,
43
44
  };
44
45
  }
@@ -81,7 +81,7 @@ export async function queryCreateNode(db, node, storeLinks, updateLinksForNewNod
81
81
  const usefulnessScore = node.usefulnessScore ?? 0;
82
82
  const timesUsed = node.timesUsed ?? 0;
83
83
  const timesHelpful = node.timesHelpful ?? 0;
84
- db.run("INSERT INTO memory_nodes (id, scope, label, content, summary, level, parent_ids, embedding, embedding_blob, created_at, updated_at, importance, access_count, last_accessed, type, metadata, sticky, ttl_days, expires_at, confidence, usefulness_score, times_used, times_helpful) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
84
+ db.run("INSERT INTO memory_nodes (id, scope, label, content, summary, level, parent_ids, embedding, embedding_blob, created_at, updated_at, importance, access_count, last_accessed, type, metadata, sticky, ttl_days, expires_at, confidence, usefulness_score, times_used, times_helpful, project_name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
85
85
  id,
86
86
  node.scope,
87
87
  node.label ?? "",
@@ -105,6 +105,7 @@ export async function queryCreateNode(db, node, storeLinks, updateLinksForNewNod
105
105
  usefulnessScore,
106
106
  timesUsed,
107
107
  timesHelpful,
108
+ node.projectName ?? null,
108
109
  ]);
109
110
  // Store links
110
111
  await storeLinks(node.scope, id, node.content);
@@ -141,6 +142,7 @@ export async function queryCreateNode(db, node, storeLinks, updateLinksForNewNod
141
142
  usefulnessScore: node.usefulnessScore ?? 0,
142
143
  timesUsed: node.timesUsed ?? 0,
143
144
  timesHelpful: node.timesHelpful ?? 0,
145
+ projectName: node.projectName ?? null,
144
146
  };
145
147
  }
146
148
  const UPDATE_FIELDS = {
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
2
3
  import * as os from "node:os";
3
4
  import * as path from "node:path";
4
5
  import { Database } from "bun:sqlite";
@@ -27,10 +28,8 @@ const SEED_BLOCKS = [
27
28
  { scope: "global", label: "human" },
28
29
  { scope: "project", label: "project" },
29
30
  ];
30
- function scopeDbPath(projectDirectory, scope, globalDbPath) {
31
- return scope === "global"
32
- ? (globalDbPath ?? path.join(os.homedir(), ".config", "opencode", "memory.db"))
33
- : path.join(projectDirectory, ".opencode", "memory.db");
31
+ function scopeDbPath(_projectDirectory, _scope, globalDbPath) {
32
+ return globalDbPath ?? path.join(os.homedir(), ".config", "opencode", "memory.db");
34
33
  }
35
34
  function validateLabel(label) {
36
35
  const trimmed = label.trim();
@@ -45,19 +44,21 @@ class SqliteMemoryStore {
45
44
  idScopeCache = new Map();
46
45
  projectDirectory;
47
46
  globalDbPath;
47
+ projectName;
48
48
  constructor(projectDirectory, globalDbPath) {
49
49
  this.projectDirectory = projectDirectory;
50
50
  this.globalDbPath = globalDbPath;
51
+ this.projectName = path.basename(projectDirectory);
51
52
  }
52
- async getDb(scope) {
53
- const key = `${scope}:${this.projectDirectory}:${this.globalDbPath ?? ""}`;
53
+ async getDb(_scope) {
54
+ const key = this.projectDirectory;
54
55
  if (this.dbs.has(key)) {
55
56
  return this.dbs.get(key);
56
57
  }
57
58
  const existing = this.dbInitPromises.get(key);
58
59
  if (existing)
59
60
  return existing;
60
- const promise = this.initDb(key, scope);
61
+ const promise = this.initDb(key);
61
62
  this.dbInitPromises.set(key, promise);
62
63
  try {
63
64
  const db = await promise;
@@ -68,8 +69,8 @@ class SqliteMemoryStore {
68
69
  throw err;
69
70
  }
70
71
  }
71
- async initDb(key, scope) {
72
- const dbPath = scopeDbPath(this.projectDirectory, scope, this.globalDbPath);
72
+ async initDb(key) {
73
+ const dbPath = scopeDbPath(this.projectDirectory, "global", this.globalDbPath);
73
74
  const dbDir = path.dirname(dbPath);
74
75
  await fs.mkdir(dbDir, { recursive: true });
75
76
  const db = new Database(dbPath);
@@ -100,7 +101,55 @@ class SqliteMemoryStore {
100
101
  this.idScopeCache.clear();
101
102
  }
102
103
  async ensureSeed() {
103
- return ensureSeedFn((s) => this.getDb(s), SEED_BLOCKS);
104
+ return ensureSeedFn((s) => this.getDb(s), SEED_BLOCKS, this.projectName);
105
+ }
106
+ async migrateFromProjectDb() {
107
+ const unifiedDb = await this.getDb();
108
+ const oldDbPath = path.join(this.projectDirectory, ".opencode", "memory.db");
109
+ if (!existsSync(oldDbPath))
110
+ return 0;
111
+ memLog("info", "storage", "Migrating project DB to unified storage", { path: oldDbPath, projectName: this.projectName });
112
+ const oldDb = new Database(oldDbPath);
113
+ let migrated = 0;
114
+ try {
115
+ const oldNodes = oldDb.query("SELECT * FROM memory_nodes").all();
116
+ for (const oldRow of oldNodes) {
117
+ const existing = unifiedDb.query("SELECT id FROM memory_nodes WHERE id = ?").get(oldRow.id);
118
+ if (existing)
119
+ continue;
120
+ unifiedDb.run(`INSERT INTO memory_nodes (id, scope, label, content, summary, level, parent_ids, embedding, embedding_blob, created_at, updated_at, importance, access_count, last_accessed, type, metadata, sticky, ttl_days, expires_at, confidence, last_verified, usefulness_score, times_used, times_helpful, project_name)
121
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
122
+ oldRow.id, oldRow.scope, oldRow.label, oldRow.content,
123
+ oldRow.summary, oldRow.level, oldRow.parent_ids,
124
+ oldRow.embedding, oldRow.embedding_blob,
125
+ oldRow.created_at, oldRow.updated_at, oldRow.importance,
126
+ oldRow.access_count, oldRow.last_accessed, oldRow.type,
127
+ oldRow.metadata, oldRow.sticky ?? 0,
128
+ oldRow.ttl_days, oldRow.expires_at,
129
+ oldRow.confidence ?? 0.5, oldRow.last_verified,
130
+ oldRow.usefulness_score ?? 0, oldRow.times_used ?? 0,
131
+ oldRow.times_helpful ?? 0, this.projectName,
132
+ ]);
133
+ const bm25Rows = oldDb.query("SELECT * FROM bm25_index WHERE node_id = ?").all(oldRow.id);
134
+ for (const bm25 of bm25Rows) {
135
+ unifiedDb.run("INSERT OR IGNORE INTO bm25_index (term, node_id, frequency, scope, project_name) VALUES (?, ?, ?, ?, ?)", [bm25.term, bm25.node_id, bm25.frequency, bm25.scope, this.projectName]);
136
+ }
137
+ const docStats = oldDb.query("SELECT * FROM bm25_doc_stats WHERE node_id = ?").get(oldRow.id);
138
+ if (docStats) {
139
+ unifiedDb.run("INSERT OR IGNORE INTO bm25_doc_stats (node_id, token_count, scope, project_name) VALUES (?, ?, ?, ?)", [docStats.node_id, docStats.token_count, docStats.scope, this.projectName]);
140
+ }
141
+ migrated++;
142
+ }
143
+ const oldLinks = oldDb.query("SELECT * FROM memory_links").all();
144
+ for (const link of oldLinks) {
145
+ unifiedDb.run("INSERT OR IGNORE INTO memory_links (source_id, target_label, target_id) VALUES (?, ?, ?)", [link.source_id, link.target_label, link.target_id]);
146
+ }
147
+ memLog("info", "storage", "Project DB migration complete", { migrated });
148
+ }
149
+ finally {
150
+ oldDb.close();
151
+ }
152
+ return migrated;
104
153
  }
105
154
  async listNodes(scope, level, limit = 50, offset = 0, includeExpired) {
106
155
  const scopes = scope === "all" ? ["global", "project"] : [scope];
@@ -126,8 +175,11 @@ class SqliteMemoryStore {
126
175
  }
127
176
  async createNode(node) {
128
177
  const db = await this.getDb(node.scope);
178
+ const nodeWithProject = node.scope === "project" && !node.projectName
179
+ ? { ...node, projectName: this.projectName }
180
+ : node;
129
181
  return await withRetryableTransaction(db, async () => {
130
- return await queryCreateNode(db, node, (scope, id, content) => this.storeLinks(scope, id, content), (scope, label, id) => this.updateLinksForNewNode(scope, label, id), (db, id, content, label, scope) => updateBM25Index(db, id, content, label, scope));
182
+ return await queryCreateNode(db, nodeWithProject, (scope, id, content) => this.storeLinks(scope, id, content), (scope, label, id) => this.updateLinksForNewNode(scope, label, id), (db, id, content, label, scope) => updateBM25Index(db, id, content, label, scope));
131
183
  });
132
184
  }
133
185
  async updateNode(id, updates) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-fractal-memory",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "description": "Fractal memory system for OpenCode with semantic search and automatic compression.",
5
5
  "main": "dist/plugin.js",
6
6
  "exports": {