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.
- package/dist/management/helpers.js +6 -6
- package/dist/plugin/init.js +3 -0
- package/dist/storage/lifecycle.js +3 -3
- package/dist/storage/migrations/definitions.js +22 -0
- package/dist/storage/migrations/index.js +1 -1
- package/dist/storage/queries/base.js +1 -0
- package/dist/storage/queries/nodes.js +3 -1
- package/dist/storage/sqlite.js +63 -11
- package/package.json +1 -1
|
@@ -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(
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
DB_PATHS.
|
|
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
|
}
|
package/dist/plugin/init.js
CHANGED
|
@@ -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 =
|
|
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;
|
|
@@ -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 = {
|
package/dist/storage/sqlite.js
CHANGED
|
@@ -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(
|
|
31
|
-
return
|
|
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(
|
|
53
|
-
const key =
|
|
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
|
|
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
|
|
72
|
-
const dbPath = scopeDbPath(this.projectDirectory,
|
|
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,
|
|
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) {
|