memorix 1.9.4 → 1.9.6
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/CHANGELOG.md +75 -1
- package/README.md +12 -6
- package/README.zh-CN.md +12 -6
- package/dist/cli/index.js +2533 -585
- package/dist/index.d.ts +15 -8
- package/dist/index.js +1201 -284
- package/dist/maintenance-runner.js +387 -216
- package/dist/media-audio-runner.js +300 -129
- package/dist/media-video-runner.js +300 -129
- package/dist/memcode-runtime/CHANGELOG.md +75 -1
- package/dist/sdk.d.ts +1 -0
- package/dist/sdk.js +1882 -998
- package/dist/types.d.ts +1 -1
- package/dist/vector-backfill-runner.js +300 -129
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +47 -3
- package/docs/API_REFERENCE.md +39 -1
- package/docs/CONFIGURATION.md +58 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/DOCKER.md +23 -3
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/KNOWN_ISSUES_AND_ROADMAP.md +61 -1
- package/docs/MULTI-DEVICE-SYNC-RESEARCH.md +4 -0
- package/docs/PERFORMANCE.md +46 -2
- package/docs/README.md +2 -1
- package/docs/SETUP.md +56 -1
- package/package.json +3 -2
- package/plugins/antigravity/memorix/plugin.json +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/hermes/memorix/plugin.yaml +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/cli/capability-map.ts +1 -0
- package/src/cli/command-guide.ts +15 -3
- package/src/cli/commands/agent-integrations.ts +57 -0
- package/src/cli/commands/background.ts +11 -2
- package/src/cli/commands/context.ts +2 -0
- package/src/cli/commands/continuity.ts +155 -0
- package/src/cli/commands/init.ts +5 -2
- package/src/cli/commands/serve-http.ts +435 -84
- package/src/cli/commands/setup.ts +77 -10
- package/src/cli/commands/sync-store.ts +47 -12
- package/src/cli/commands/sync.ts +26 -0
- package/src/cli/index.ts +3 -1
- package/src/cli/tui/ConfigureView.tsx +2 -0
- package/src/codegraph/auto-context.ts +17 -0
- package/src/config/legacy-loader.ts +2 -2
- package/src/config/resolved-config.ts +8 -5
- package/src/config/yaml-loader.ts +20 -10
- package/src/dashboard/server.ts +10 -12
- package/src/embedding/api-provider.ts +4 -2
- package/src/embedding/fastembed-provider.ts +47 -14
- package/src/embedding/transformers-provider.ts +29 -7
- package/src/evaluation/memory-quality-gate.ts +90 -0
- package/src/hooks/official-skills.ts +2 -0
- package/src/integrations/registry.ts +1 -1
- package/src/knowledge/context-assembly.ts +1 -0
- package/src/knowledge/context-receipt.ts +15 -0
- package/src/knowledge/task-continuity.ts +413 -0
- package/src/knowledge/workset.ts +77 -0
- package/src/llm/provider.ts +2 -1
- package/src/memory/graph.ts +10 -8
- package/src/memory/session.ts +1 -1
- package/src/orchestrate/coordinator.ts +60 -0
- package/src/orchestrate/effect-ledger.ts +160 -0
- package/src/orchestrate/pipeline-trace.ts +40 -6
- package/src/runtime/maintenance-jobs.ts +12 -6
- package/src/sdk.ts +24 -0
- package/src/server/mcp-binding-store.ts +14 -3
- package/src/server/modern-mcp-bridge.ts +153 -27
- package/src/server/tool-profile.ts +3 -2
- package/src/server.ts +131 -7
- package/src/store/file-lock.ts +28 -3
- package/src/store/graph-store.ts +178 -46
- package/src/store/persistence.ts +14 -9
- package/src/store/sqlite-db.ts +150 -7
- package/src/store/sqlite-store.ts +32 -27
- package/src/sync/engine.ts +9 -7
- package/src/sync/namespace.ts +23 -0
- package/src/sync/policy.ts +19 -4
- package/src/sync/store-port.ts +43 -13
- package/src/sync/types.ts +2 -0
- package/src/team/team-store.ts +62 -27
- package/src/types.ts +1 -1
- package/src/workspace/mcp-adapters/grok.ts +32 -0
package/src/store/graph-store.ts
CHANGED
|
@@ -11,8 +11,12 @@ import { loadGraphJsonl } from './persistence.js';
|
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import fs from 'node:fs';
|
|
13
13
|
|
|
14
|
+
export const DEFAULT_GRAPH_PROJECT_ID = '__default__';
|
|
15
|
+
const LEGACY_GRAPH_PROJECT_ID = '__legacy__';
|
|
16
|
+
const MAX_GRAPH_STORE_CACHE = 32;
|
|
17
|
+
|
|
14
18
|
export interface GraphStore {
|
|
15
|
-
init(dataDir: string): Promise<void>;
|
|
19
|
+
init(dataDir: string, projectId?: string): Promise<void>;
|
|
16
20
|
loadEntities(): Entity[];
|
|
17
21
|
loadRelations(): Relation[];
|
|
18
22
|
insertEntities(entities: Entity[]): void;
|
|
@@ -46,9 +50,61 @@ function entityToRow(entity: Entity): Record<string, unknown> {
|
|
|
46
50
|
};
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
const CREATE_SCOPED_GRAPH_ENTITIES = `
|
|
54
|
+
CREATE TABLE IF NOT EXISTS graph_entities (
|
|
55
|
+
projectId TEXT NOT NULL,
|
|
56
|
+
name TEXT NOT NULL,
|
|
57
|
+
entityType TEXT NOT NULL DEFAULT '',
|
|
58
|
+
observations TEXT NOT NULL DEFAULT '[]',
|
|
59
|
+
PRIMARY KEY (projectId, name)
|
|
60
|
+
);`;
|
|
61
|
+
|
|
62
|
+
const CREATE_SCOPED_GRAPH_RELATIONS = `
|
|
63
|
+
CREATE TABLE IF NOT EXISTS graph_relations (
|
|
64
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
65
|
+
projectId TEXT NOT NULL,
|
|
66
|
+
from_entity TEXT NOT NULL,
|
|
67
|
+
to_entity TEXT NOT NULL,
|
|
68
|
+
relationType TEXT NOT NULL DEFAULT '',
|
|
69
|
+
UNIQUE(projectId, from_entity, to_entity, relationType)
|
|
70
|
+
);`;
|
|
71
|
+
|
|
72
|
+
function ensureScopedGraphSchema(db: any): void {
|
|
73
|
+
const entityColumns = db.prepare('PRAGMA table_info(graph_entities)').all() as Array<{ name: string }>;
|
|
74
|
+
if (!entityColumns.some((column) => column.name === 'projectId')) {
|
|
75
|
+
const migrate = db.transaction(() => {
|
|
76
|
+
db.exec('ALTER TABLE graph_entities RENAME TO graph_entities_legacy_unscoped');
|
|
77
|
+
db.exec(CREATE_SCOPED_GRAPH_ENTITIES);
|
|
78
|
+
db.exec(`INSERT INTO graph_entities (projectId, name, entityType, observations)
|
|
79
|
+
SELECT '${LEGACY_GRAPH_PROJECT_ID}', name, entityType, observations
|
|
80
|
+
FROM graph_entities_legacy_unscoped`);
|
|
81
|
+
db.exec('DROP TABLE graph_entities_legacy_unscoped');
|
|
82
|
+
});
|
|
83
|
+
migrate();
|
|
84
|
+
} else {
|
|
85
|
+
db.exec(CREATE_SCOPED_GRAPH_ENTITIES);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const relationColumns = db.prepare('PRAGMA table_info(graph_relations)').all() as Array<{ name: string }>;
|
|
89
|
+
if (!relationColumns.some((column) => column.name === 'projectId')) {
|
|
90
|
+
const migrate = db.transaction(() => {
|
|
91
|
+
db.exec('ALTER TABLE graph_relations RENAME TO graph_relations_legacy_unscoped');
|
|
92
|
+
db.exec(CREATE_SCOPED_GRAPH_RELATIONS);
|
|
93
|
+
db.exec(`INSERT INTO graph_relations (projectId, from_entity, to_entity, relationType)
|
|
94
|
+
SELECT '${LEGACY_GRAPH_PROJECT_ID}', from_entity, to_entity, relationType
|
|
95
|
+
FROM graph_relations_legacy_unscoped`);
|
|
96
|
+
db.exec('DROP TABLE graph_relations_legacy_unscoped');
|
|
97
|
+
});
|
|
98
|
+
migrate();
|
|
99
|
+
} else {
|
|
100
|
+
db.exec(CREATE_SCOPED_GRAPH_RELATIONS);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
49
104
|
export class GraphSqliteStore implements GraphStore {
|
|
50
105
|
private db: any = null;
|
|
51
106
|
private dataDir: string = '';
|
|
107
|
+
private projectId = DEFAULT_GRAPH_PROJECT_ID;
|
|
52
108
|
|
|
53
109
|
private stmtInsertEntity: any = null;
|
|
54
110
|
private stmtUpdateEntityObs: any = null;
|
|
@@ -61,40 +117,44 @@ export class GraphSqliteStore implements GraphStore {
|
|
|
61
117
|
private stmtDeleteRelationsByEntity: any = null;
|
|
62
118
|
private stmtSelectAllRelations: any = null;
|
|
63
119
|
|
|
64
|
-
async init(dataDir: string): Promise<void> {
|
|
120
|
+
async init(dataDir: string, projectId = DEFAULT_GRAPH_PROJECT_ID): Promise<void> {
|
|
65
121
|
this.dataDir = dataDir;
|
|
122
|
+
this.projectId = projectId;
|
|
66
123
|
this.db = getDatabase(dataDir);
|
|
124
|
+
ensureScopedGraphSchema(this.db);
|
|
67
125
|
|
|
68
126
|
// Prepare entity statements
|
|
69
127
|
this.stmtInsertEntity = this.db.prepare(
|
|
70
|
-
`INSERT OR REPLACE INTO graph_entities (name, entityType, observations) VALUES (@name, @entityType, @observations)`
|
|
128
|
+
`INSERT OR REPLACE INTO graph_entities (projectId, name, entityType, observations) VALUES (@projectId, @name, @entityType, @observations)`
|
|
71
129
|
);
|
|
72
130
|
this.stmtUpdateEntityObs = this.db.prepare(
|
|
73
|
-
`UPDATE graph_entities SET observations = @observations WHERE name = @name`
|
|
131
|
+
`UPDATE graph_entities SET observations = @observations WHERE projectId = @projectId AND name = @name`
|
|
74
132
|
);
|
|
75
|
-
this.stmtDeleteEntity = this.db.prepare(`DELETE FROM graph_entities WHERE name = ?`);
|
|
76
|
-
this.stmtSelectAllEntities = this.db.prepare(`SELECT * FROM graph_entities`);
|
|
77
|
-
this.stmtSelectEntityByName = this.db.prepare(`SELECT * FROM graph_entities WHERE name = ?`);
|
|
133
|
+
this.stmtDeleteEntity = this.db.prepare(`DELETE FROM graph_entities WHERE projectId = ? AND name = ?`);
|
|
134
|
+
this.stmtSelectAllEntities = this.db.prepare(`SELECT * FROM graph_entities WHERE projectId = ? ORDER BY name`);
|
|
135
|
+
this.stmtSelectEntityByName = this.db.prepare(`SELECT * FROM graph_entities WHERE projectId = ? AND name = ?`);
|
|
78
136
|
|
|
79
137
|
// Prepare relation statements
|
|
80
138
|
this.stmtInsertRelation = this.db.prepare(
|
|
81
|
-
`INSERT OR IGNORE INTO graph_relations (from_entity, to_entity, relationType) VALUES (@from, @to, @relationType)`
|
|
139
|
+
`INSERT OR IGNORE INTO graph_relations (projectId, from_entity, to_entity, relationType) VALUES (@projectId, @from, @to, @relationType)`
|
|
82
140
|
);
|
|
83
141
|
this.stmtDeleteRelation = this.db.prepare(
|
|
84
|
-
`DELETE FROM graph_relations WHERE from_entity = ? AND to_entity = ? AND relationType = ?`
|
|
142
|
+
`DELETE FROM graph_relations WHERE projectId = ? AND from_entity = ? AND to_entity = ? AND relationType = ?`
|
|
85
143
|
);
|
|
86
144
|
this.stmtDeleteRelationsByEntity = this.db.prepare(
|
|
87
|
-
`DELETE FROM graph_relations WHERE from_entity = ? OR to_entity =
|
|
145
|
+
`DELETE FROM graph_relations WHERE projectId = ? AND (from_entity = ? OR to_entity = ?)`
|
|
88
146
|
);
|
|
89
|
-
this.stmtSelectAllRelations = this.db.prepare(`SELECT * FROM graph_relations`);
|
|
147
|
+
this.stmtSelectAllRelations = this.db.prepare(`SELECT * FROM graph_relations WHERE projectId = ? ORDER BY id`);
|
|
90
148
|
|
|
91
149
|
// One-time migration from graph.jsonl
|
|
92
150
|
await this.migrateFromJsonlIfNeeded();
|
|
93
151
|
}
|
|
94
152
|
|
|
95
153
|
private async migrateFromJsonlIfNeeded(): Promise<void> {
|
|
96
|
-
const count = this.db.prepare(`SELECT COUNT(*) AS cnt FROM graph_entities
|
|
154
|
+
const count = this.db.prepare(`SELECT COUNT(*) AS cnt FROM graph_entities WHERE projectId = ?`).get(this.projectId);
|
|
155
|
+
const totalCount = this.db.prepare(`SELECT COUNT(*) AS cnt FROM graph_entities`).get();
|
|
97
156
|
if (count.cnt > 0) return;
|
|
157
|
+
if (totalCount.cnt > 0) return;
|
|
98
158
|
|
|
99
159
|
const jsonlPath = path.join(this.dataDir, 'graph.jsonl');
|
|
100
160
|
if (!fs.existsSync(jsonlPath)) return;
|
|
@@ -105,12 +165,65 @@ export class GraphSqliteStore implements GraphStore {
|
|
|
105
165
|
|
|
106
166
|
console.error(`[memorix] Migrating graph from JSONL to SQLite (${data.entities.length} entities, ${data.relations.length} relations)...`);
|
|
107
167
|
|
|
168
|
+
const observations = this.db.prepare('SELECT id, projectId FROM observations').all() as Array<{ id: number; projectId: string }>;
|
|
169
|
+
const projectByObservationId = new Map(observations.map((row) => [row.id, row.projectId]));
|
|
170
|
+
const projectsByEntity = new Map<string, Set<string>>();
|
|
171
|
+
for (const entity of data.entities) {
|
|
172
|
+
const projects = new Set<string>();
|
|
173
|
+
for (const content of entity.observations ?? []) {
|
|
174
|
+
const match = /^\[#(\d+)\]/.exec(content);
|
|
175
|
+
const projectId = match ? projectByObservationId.get(Number(match[1])) : undefined;
|
|
176
|
+
if (projectId) projects.add(projectId);
|
|
177
|
+
}
|
|
178
|
+
projectsByEntity.set(entity.name, projects);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Legacy graph.jsonl had no project column. Recover the ownership that
|
|
182
|
+
// can be proven from observation references, then propagate it through
|
|
183
|
+
// unambiguous relation endpoints. A relation crossing two projects is
|
|
184
|
+
// intentionally quarantined instead of leaking into both views.
|
|
185
|
+
let changed = true;
|
|
186
|
+
while (changed) {
|
|
187
|
+
changed = false;
|
|
188
|
+
for (const relation of data.relations) {
|
|
189
|
+
const from = projectsByEntity.get(relation.from) ?? new Set<string>();
|
|
190
|
+
const to = projectsByEntity.get(relation.to) ?? new Set<string>();
|
|
191
|
+
if (from.size > 0 && to.size === 0) {
|
|
192
|
+
projectsByEntity.set(relation.to, new Set(from));
|
|
193
|
+
changed = true;
|
|
194
|
+
} else if (to.size > 0 && from.size === 0) {
|
|
195
|
+
projectsByEntity.set(relation.from, new Set(to));
|
|
196
|
+
changed = true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const entityRows = data.entities.flatMap((entity) => {
|
|
202
|
+
const projects = projectsByEntity.get(entity.name) ?? new Set<string>();
|
|
203
|
+
const owners = projects.size > 0 ? [...projects] : [this.projectId];
|
|
204
|
+
return owners.map((projectId) => ({ ...entityToRow(entity), projectId }));
|
|
205
|
+
});
|
|
206
|
+
const projectSet = new Set(entityRows.map((row) => String(row.projectId)));
|
|
207
|
+
const relationRows = data.relations.flatMap((relation) => {
|
|
208
|
+
const from = projectsByEntity.get(relation.from) ?? new Set<string>();
|
|
209
|
+
const to = projectsByEntity.get(relation.to) ?? new Set<string>();
|
|
210
|
+
const shared = [...from].filter((projectId) => to.has(projectId));
|
|
211
|
+
const owners = shared.length > 0
|
|
212
|
+
? shared
|
|
213
|
+
: from.size === 0 && to.size === 0
|
|
214
|
+
? [this.projectId]
|
|
215
|
+
: [];
|
|
216
|
+
return owners
|
|
217
|
+
.filter((projectId) => projectSet.has(projectId))
|
|
218
|
+
.map((projectId) => ({ projectId, from: relation.from, to: relation.to, relationType: relation.relationType }));
|
|
219
|
+
});
|
|
220
|
+
|
|
108
221
|
const insertAll = this.db.transaction(() => {
|
|
109
|
-
for (const entity of
|
|
110
|
-
this.stmtInsertEntity.run(
|
|
222
|
+
for (const entity of entityRows) {
|
|
223
|
+
this.stmtInsertEntity.run(entity);
|
|
111
224
|
}
|
|
112
|
-
for (const
|
|
113
|
-
this.stmtInsertRelation.run(
|
|
225
|
+
for (const relation of relationRows) {
|
|
226
|
+
this.stmtInsertRelation.run(relation);
|
|
114
227
|
}
|
|
115
228
|
});
|
|
116
229
|
insertAll();
|
|
@@ -122,11 +235,11 @@ export class GraphSqliteStore implements GraphStore {
|
|
|
122
235
|
}
|
|
123
236
|
|
|
124
237
|
loadEntities(): Entity[] {
|
|
125
|
-
return this.stmtSelectAllEntities.all().map(rowToEntity);
|
|
238
|
+
return this.stmtSelectAllEntities.all(this.projectId).map(rowToEntity);
|
|
126
239
|
}
|
|
127
240
|
|
|
128
241
|
loadRelations(): Relation[] {
|
|
129
|
-
return this.stmtSelectAllRelations.all().map((row: any) => ({
|
|
242
|
+
return this.stmtSelectAllRelations.all(this.projectId).map((row: any) => ({
|
|
130
243
|
from: row.from_entity,
|
|
131
244
|
to: row.to_entity,
|
|
132
245
|
relationType: row.relationType,
|
|
@@ -136,7 +249,7 @@ export class GraphSqliteStore implements GraphStore {
|
|
|
136
249
|
insertEntities(entities: Entity[]): void {
|
|
137
250
|
const insertAll = this.db.transaction(() => {
|
|
138
251
|
for (const entity of entities) {
|
|
139
|
-
this.stmtInsertEntity.run(entityToRow(entity));
|
|
252
|
+
this.stmtInsertEntity.run({ ...entityToRow(entity), projectId: this.projectId });
|
|
140
253
|
}
|
|
141
254
|
});
|
|
142
255
|
insertAll();
|
|
@@ -145,7 +258,7 @@ export class GraphSqliteStore implements GraphStore {
|
|
|
145
258
|
insertRelations(relations: Relation[]): void {
|
|
146
259
|
const insertAll = this.db.transaction(() => {
|
|
147
260
|
for (const rel of relations) {
|
|
148
|
-
this.stmtInsertRelation.run({ from: rel.from, to: rel.to, relationType: rel.relationType });
|
|
261
|
+
this.stmtInsertRelation.run({ projectId: this.projectId, from: rel.from, to: rel.to, relationType: rel.relationType });
|
|
149
262
|
}
|
|
150
263
|
});
|
|
151
264
|
insertAll();
|
|
@@ -154,8 +267,8 @@ export class GraphSqliteStore implements GraphStore {
|
|
|
154
267
|
deleteEntities(names: string[]): void {
|
|
155
268
|
const deleteAll = this.db.transaction(() => {
|
|
156
269
|
for (const name of names) {
|
|
157
|
-
this.stmtDeleteRelationsByEntity.run(name, name);
|
|
158
|
-
this.stmtDeleteEntity.run(name);
|
|
270
|
+
this.stmtDeleteRelationsByEntity.run(this.projectId, name, name);
|
|
271
|
+
this.stmtDeleteEntity.run(this.projectId, name);
|
|
159
272
|
}
|
|
160
273
|
});
|
|
161
274
|
deleteAll();
|
|
@@ -164,7 +277,7 @@ export class GraphSqliteStore implements GraphStore {
|
|
|
164
277
|
deleteRelations(relations: Relation[]): void {
|
|
165
278
|
const deleteAll = this.db.transaction(() => {
|
|
166
279
|
for (const rel of relations) {
|
|
167
|
-
this.stmtDeleteRelation.run(rel.from, rel.to, rel.relationType);
|
|
280
|
+
this.stmtDeleteRelation.run(this.projectId, rel.from, rel.to, rel.relationType);
|
|
168
281
|
}
|
|
169
282
|
});
|
|
170
283
|
deleteAll();
|
|
@@ -173,13 +286,13 @@ export class GraphSqliteStore implements GraphStore {
|
|
|
173
286
|
addObservations(updates: { entityName: string; contents: string[] }[]): void {
|
|
174
287
|
const updateAll = this.db.transaction(() => {
|
|
175
288
|
for (const u of updates) {
|
|
176
|
-
const row = this.stmtSelectEntityByName.get(u.entityName);
|
|
289
|
+
const row = this.stmtSelectEntityByName.get(this.projectId, u.entityName);
|
|
177
290
|
if (!row) continue;
|
|
178
291
|
const existing: string[] = safeJsonParse(row.observations, []);
|
|
179
292
|
const newObs = u.contents.filter(c => !existing.includes(c));
|
|
180
293
|
if (newObs.length > 0) {
|
|
181
294
|
existing.push(...newObs);
|
|
182
|
-
this.stmtUpdateEntityObs.run({ name: u.entityName, observations: JSON.stringify(existing) });
|
|
295
|
+
this.stmtUpdateEntityObs.run({ projectId: this.projectId, name: u.entityName, observations: JSON.stringify(existing) });
|
|
183
296
|
}
|
|
184
297
|
}
|
|
185
298
|
});
|
|
@@ -189,11 +302,11 @@ export class GraphSqliteStore implements GraphStore {
|
|
|
189
302
|
deleteObservations(deletions: { entityName: string; observations: string[] }[]): void {
|
|
190
303
|
const deleteAll = this.db.transaction(() => {
|
|
191
304
|
for (const d of deletions) {
|
|
192
|
-
const row = this.stmtSelectEntityByName.get(d.entityName);
|
|
305
|
+
const row = this.stmtSelectEntityByName.get(this.projectId, d.entityName);
|
|
193
306
|
if (!row) continue;
|
|
194
307
|
const existing: string[] = safeJsonParse(row.observations, []);
|
|
195
308
|
const filtered = existing.filter(o => !d.observations.includes(o));
|
|
196
|
-
this.stmtUpdateEntityObs.run({ name: d.entityName, observations: JSON.stringify(filtered) });
|
|
309
|
+
this.stmtUpdateEntityObs.run({ projectId: this.projectId, name: d.entityName, observations: JSON.stringify(filtered) });
|
|
197
310
|
}
|
|
198
311
|
});
|
|
199
312
|
deleteAll();
|
|
@@ -205,13 +318,13 @@ export class GraphSqliteStore implements GraphStore {
|
|
|
205
318
|
*/
|
|
206
319
|
replaceAll(entities: Entity[], relations: Relation[]): void {
|
|
207
320
|
const replaceAll = this.db.transaction(() => {
|
|
208
|
-
this.db.prepare('DELETE FROM graph_relations').run();
|
|
209
|
-
this.db.prepare('DELETE FROM graph_entities').run();
|
|
321
|
+
this.db.prepare('DELETE FROM graph_relations WHERE projectId = ?').run(this.projectId);
|
|
322
|
+
this.db.prepare('DELETE FROM graph_entities WHERE projectId = ?').run(this.projectId);
|
|
210
323
|
for (const entity of entities) {
|
|
211
|
-
this.stmtInsertEntity.run(entityToRow(entity));
|
|
324
|
+
this.stmtInsertEntity.run({ ...entityToRow(entity), projectId: this.projectId });
|
|
212
325
|
}
|
|
213
326
|
for (const rel of relations) {
|
|
214
|
-
this.stmtInsertRelation.run({ from: rel.from, to: rel.to, relationType: rel.relationType });
|
|
327
|
+
this.stmtInsertRelation.run({ projectId: this.projectId, from: rel.from, to: rel.to, relationType: rel.relationType });
|
|
215
328
|
}
|
|
216
329
|
});
|
|
217
330
|
replaceAll();
|
|
@@ -224,26 +337,45 @@ export class GraphSqliteStore implements GraphStore {
|
|
|
224
337
|
|
|
225
338
|
// ── Singleton ──────────────────────────────────────────────────────
|
|
226
339
|
|
|
227
|
-
|
|
228
|
-
let
|
|
340
|
+
const _graphStores = new Map<string, GraphSqliteStore>();
|
|
341
|
+
let _activeGraphKey: string | null = null;
|
|
342
|
+
|
|
343
|
+
function graphStoreKey(dataDir: string, projectId: string): string {
|
|
344
|
+
return `${path.resolve(dataDir)}\0${projectId}`;
|
|
345
|
+
}
|
|
229
346
|
|
|
230
|
-
export async function initGraphStore(dataDir: string): Promise<GraphSqliteStore> {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
347
|
+
export async function initGraphStore(dataDir: string, projectId = DEFAULT_GRAPH_PROJECT_ID): Promise<GraphSqliteStore> {
|
|
348
|
+
const key = graphStoreKey(dataDir, projectId);
|
|
349
|
+
const existing = _graphStores.get(key);
|
|
350
|
+
if (existing) {
|
|
351
|
+
_graphStores.delete(key);
|
|
352
|
+
_graphStores.set(key, existing);
|
|
353
|
+
_activeGraphKey = key;
|
|
354
|
+
return existing;
|
|
355
|
+
}
|
|
356
|
+
const store = new GraphSqliteStore();
|
|
357
|
+
await store.init(dataDir, projectId);
|
|
358
|
+
_graphStores.set(key, store);
|
|
359
|
+
while (_graphStores.size > MAX_GRAPH_STORE_CACHE) {
|
|
360
|
+
const oldest = _graphStores.keys().next().value as string | undefined;
|
|
361
|
+
if (!oldest) break;
|
|
362
|
+
_graphStores.delete(oldest);
|
|
363
|
+
}
|
|
364
|
+
_activeGraphKey = key;
|
|
365
|
+
return store;
|
|
236
366
|
}
|
|
237
367
|
|
|
238
|
-
export function getGraphStore(): GraphSqliteStore {
|
|
239
|
-
|
|
240
|
-
|
|
368
|
+
export function getGraphStore(dataDir?: string, projectId?: string): GraphSqliteStore {
|
|
369
|
+
const key = dataDir && projectId ? graphStoreKey(dataDir, projectId) : _activeGraphKey;
|
|
370
|
+
const store = key ? _graphStores.get(key) : undefined;
|
|
371
|
+
if (!store) throw new Error('[memorix] GraphStore not initialized — call initGraphStore() first');
|
|
372
|
+
return store;
|
|
241
373
|
}
|
|
242
374
|
|
|
243
375
|
export function resetGraphStore(): void {
|
|
244
|
-
|
|
245
|
-
try {
|
|
376
|
+
for (const store of _graphStores.values()) {
|
|
377
|
+
try { store.close(); } catch { /* best-effort */ }
|
|
246
378
|
}
|
|
247
|
-
|
|
248
|
-
|
|
379
|
+
_graphStores.clear();
|
|
380
|
+
_activeGraphKey = null;
|
|
249
381
|
}
|
package/src/store/persistence.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { promises as fs } from 'node:fs';
|
|
17
17
|
import path from 'node:path';
|
|
18
18
|
import os from 'node:os';
|
|
19
|
+
import { atomicWriteFile, withFileLock } from './file-lock.js';
|
|
19
20
|
|
|
20
21
|
// ── Re-export JSON helpers for backward compat ────────────────────
|
|
21
22
|
export {
|
|
@@ -105,6 +106,12 @@ export async function listProjectDirs(baseDir?: string): Promise<string[]> {
|
|
|
105
106
|
export async function migrateSubdirsToFlat(baseDir?: string): Promise<boolean> {
|
|
106
107
|
const base = baseDir ?? resolveDefaultDataDir();
|
|
107
108
|
await fs.mkdir(base, { recursive: true });
|
|
109
|
+
return withFileLock(base, () => migrateSubdirsToFlatUnlocked(base));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function migrateSubdirsToFlatUnlocked(baseDir?: string): Promise<boolean> {
|
|
113
|
+
const base = baseDir ?? resolveDefaultDataDir();
|
|
114
|
+
await fs.mkdir(base, { recursive: true });
|
|
108
115
|
|
|
109
116
|
// Find all subdirectories that contain observations.json
|
|
110
117
|
let entries: import('node:fs').Dirent[];
|
|
@@ -171,7 +178,9 @@ export async function migrateSubdirsToFlat(baseDir?: string): Promise<boolean> {
|
|
|
171
178
|
for (const o of obs) {
|
|
172
179
|
// Deduplicate by title+createdAt+projectId (same observation from migration overlap)
|
|
173
180
|
const isDuplicate = allObs.some(
|
|
174
|
-
(existing) => existing.title === o.title
|
|
181
|
+
(existing) => existing.title === o.title
|
|
182
|
+
&& existing.createdAt === o.createdAt
|
|
183
|
+
&& existing.projectId === o.projectId,
|
|
175
184
|
);
|
|
176
185
|
if (!isDuplicate) {
|
|
177
186
|
allObs.push(o);
|
|
@@ -212,12 +221,8 @@ export async function migrateSubdirsToFlat(baseDir?: string): Promise<boolean> {
|
|
|
212
221
|
}
|
|
213
222
|
|
|
214
223
|
// Write merged data to base directory
|
|
215
|
-
await
|
|
216
|
-
await
|
|
217
|
-
path.join(base, 'counter.json'),
|
|
218
|
-
JSON.stringify({ nextId: allObs.length + 1 }),
|
|
219
|
-
'utf-8',
|
|
220
|
-
);
|
|
224
|
+
await atomicWriteFile(path.join(base, 'observations.json'), JSON.stringify(allObs, null, 2));
|
|
225
|
+
await atomicWriteFile(path.join(base, 'counter.json'), JSON.stringify({ nextId: allObs.length + 1 }));
|
|
221
226
|
|
|
222
227
|
// Write merged graph
|
|
223
228
|
const graphLines = [
|
|
@@ -225,7 +230,7 @@ export async function migrateSubdirsToFlat(baseDir?: string): Promise<boolean> {
|
|
|
225
230
|
...mergedRelations.map((r) => JSON.stringify({ type: 'relation', from: r.from, to: r.to, relationType: r.relationType })),
|
|
226
231
|
];
|
|
227
232
|
if (graphLines.length > 0) {
|
|
228
|
-
await
|
|
233
|
+
await atomicWriteFile(path.join(base, 'graph.jsonl'), graphLines.join('\n'));
|
|
229
234
|
}
|
|
230
235
|
|
|
231
236
|
// Also merge sessions if present
|
|
@@ -243,7 +248,7 @@ export async function migrateSubdirsToFlat(baseDir?: string): Promise<boolean> {
|
|
|
243
248
|
} catch { /* no sessions */ }
|
|
244
249
|
}
|
|
245
250
|
if (allSessions.length > 0) {
|
|
246
|
-
await
|
|
251
|
+
await atomicWriteFile(path.join(base, 'sessions.json'), JSON.stringify(allSessions, null, 2));
|
|
247
252
|
}
|
|
248
253
|
|
|
249
254
|
// Move subdirectories to backup
|
package/src/store/sqlite-db.ts
CHANGED
|
@@ -217,19 +217,22 @@ CREATE TABLE IF NOT EXISTS chat_transcript (
|
|
|
217
217
|
|
|
218
218
|
const CREATE_GRAPH_ENTITIES_TABLE = `
|
|
219
219
|
CREATE TABLE IF NOT EXISTS graph_entities (
|
|
220
|
-
|
|
220
|
+
projectId TEXT NOT NULL,
|
|
221
|
+
name TEXT NOT NULL,
|
|
221
222
|
entityType TEXT NOT NULL DEFAULT '',
|
|
222
|
-
observations TEXT NOT NULL DEFAULT '[]'
|
|
223
|
+
observations TEXT NOT NULL DEFAULT '[]',
|
|
224
|
+
PRIMARY KEY (projectId, name)
|
|
223
225
|
);
|
|
224
226
|
`;
|
|
225
227
|
|
|
226
228
|
const CREATE_GRAPH_RELATIONS_TABLE = `
|
|
227
229
|
CREATE TABLE IF NOT EXISTS graph_relations (
|
|
228
230
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
231
|
+
projectId TEXT NOT NULL,
|
|
229
232
|
from_entity TEXT NOT NULL,
|
|
230
233
|
to_entity TEXT NOT NULL,
|
|
231
234
|
relationType TEXT NOT NULL DEFAULT '',
|
|
232
|
-
UNIQUE(from_entity, to_entity, relationType)
|
|
235
|
+
UNIQUE(projectId, from_entity, to_entity, relationType)
|
|
233
236
|
);
|
|
234
237
|
`;
|
|
235
238
|
|
|
@@ -818,6 +821,45 @@ CREATE TABLE IF NOT EXISTS memory_feedback_events (
|
|
|
818
821
|
);
|
|
819
822
|
`;
|
|
820
823
|
|
|
824
|
+
const CREATE_TASK_CONTINUITY_EVENTS_TABLE = `
|
|
825
|
+
CREATE TABLE IF NOT EXISTS task_continuity_events (
|
|
826
|
+
id TEXT PRIMARY KEY,
|
|
827
|
+
project_id TEXT NOT NULL,
|
|
828
|
+
task_id TEXT NOT NULL,
|
|
829
|
+
task TEXT NOT NULL,
|
|
830
|
+
kind TEXT NOT NULL,
|
|
831
|
+
content TEXT NOT NULL,
|
|
832
|
+
item_status TEXT,
|
|
833
|
+
source_ref TEXT,
|
|
834
|
+
evidence_json TEXT NOT NULL DEFAULT '[]',
|
|
835
|
+
actor TEXT,
|
|
836
|
+
created_at TEXT NOT NULL,
|
|
837
|
+
event_key TEXT,
|
|
838
|
+
item_key TEXT,
|
|
839
|
+
supersedes_id TEXT
|
|
840
|
+
);
|
|
841
|
+
`;
|
|
842
|
+
|
|
843
|
+
const CREATE_AGENT_EFFECTS_TABLE = `
|
|
844
|
+
CREATE TABLE IF NOT EXISTS agent_effects (
|
|
845
|
+
id TEXT PRIMARY KEY,
|
|
846
|
+
pipeline_id TEXT NOT NULL,
|
|
847
|
+
task_id TEXT NOT NULL,
|
|
848
|
+
attempt INTEGER NOT NULL,
|
|
849
|
+
effect_key TEXT NOT NULL,
|
|
850
|
+
tool TEXT NOT NULL,
|
|
851
|
+
risk_tier TEXT NOT NULL,
|
|
852
|
+
replay_policy TEXT NOT NULL,
|
|
853
|
+
status TEXT NOT NULL,
|
|
854
|
+
call_id TEXT,
|
|
855
|
+
idempotency_key TEXT NOT NULL,
|
|
856
|
+
input_fingerprint TEXT,
|
|
857
|
+
detail TEXT,
|
|
858
|
+
started_at INTEGER NOT NULL,
|
|
859
|
+
finished_at INTEGER
|
|
860
|
+
);
|
|
861
|
+
`;
|
|
862
|
+
|
|
821
863
|
const CREATE_MCP_BINDINGS_TABLE = `
|
|
822
864
|
CREATE TABLE IF NOT EXISTS mcp_bindings (
|
|
823
865
|
handle_id TEXT PRIMARY KEY,
|
|
@@ -896,7 +938,18 @@ CREATE INDEX IF NOT EXISTS idx_evidence_cards_candidate ON evidence_cards(projec
|
|
|
896
938
|
CREATE INDEX IF NOT EXISTS idx_evidence_card_events_card ON evidence_card_events(card_id, created_at);
|
|
897
939
|
CREATE INDEX IF NOT EXISTS idx_memory_feedback_states_project ON memory_feedback_states(project_id, candidate_kind, updated_at DESC);
|
|
898
940
|
CREATE INDEX IF NOT EXISTS idx_memory_feedback_events_candidate ON memory_feedback_events(project_id, candidate_kind, candidate_id, observed_at DESC);
|
|
941
|
+
CREATE INDEX IF NOT EXISTS idx_task_continuity_project_task ON task_continuity_events(project_id, task_id, created_at ASC);
|
|
942
|
+
CREATE INDEX IF NOT EXISTS idx_task_continuity_project_recent ON task_continuity_events(project_id, created_at DESC);
|
|
943
|
+
CREATE INDEX IF NOT EXISTS idx_task_continuity_item ON task_continuity_events(project_id, task_id, kind, item_key, created_at DESC);
|
|
944
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_task_continuity_event_key
|
|
945
|
+
ON task_continuity_events(project_id, task_id, event_key)
|
|
946
|
+
WHERE event_key IS NOT NULL;
|
|
947
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_effect_identity
|
|
948
|
+
ON agent_effects(pipeline_id, task_id, attempt, effect_key);
|
|
949
|
+
CREATE INDEX IF NOT EXISTS idx_agent_effect_pipeline ON agent_effects(pipeline_id, started_at ASC);
|
|
899
950
|
CREATE INDEX IF NOT EXISTS idx_mcp_bindings_project ON mcp_bindings(project_id, last_used_at DESC);
|
|
951
|
+
CREATE INDEX IF NOT EXISTS idx_mcp_bindings_root ON mcp_bindings(project_root, last_used_at DESC);
|
|
952
|
+
CREATE INDEX IF NOT EXISTS idx_mcp_bindings_expiry ON mcp_bindings(expires_at);
|
|
900
953
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_maintenance_jobs_active_dedupe
|
|
901
954
|
ON maintenance_jobs(project_id, kind, dedupe_key)
|
|
902
955
|
WHERE status IN ('pending', 'running', 'retry');
|
|
@@ -1075,6 +1128,8 @@ const SCHEMA_MIGRATIONS: SchemaMigration[] = [
|
|
|
1075
1128
|
db.exec('CREATE INDEX IF NOT EXISTS idx_memory_feedback_states_project ON memory_feedback_states(project_id, candidate_kind, updated_at DESC)');
|
|
1076
1129
|
db.exec('CREATE INDEX IF NOT EXISTS idx_memory_feedback_events_candidate ON memory_feedback_events(project_id, candidate_kind, candidate_id, observed_at DESC)');
|
|
1077
1130
|
db.exec('CREATE INDEX IF NOT EXISTS idx_mcp_bindings_project ON mcp_bindings(project_id, last_used_at DESC)');
|
|
1131
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_mcp_bindings_root ON mcp_bindings(project_root, last_used_at DESC)');
|
|
1132
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_mcp_bindings_expiry ON mcp_bindings(expires_at)');
|
|
1078
1133
|
},
|
|
1079
1134
|
},
|
|
1080
1135
|
{
|
|
@@ -1086,6 +1141,18 @@ const SCHEMA_MIGRATIONS: SchemaMigration[] = [
|
|
|
1086
1141
|
addColumnIfMissing(db, 'code_edges', 'source', 'source TEXT');
|
|
1087
1142
|
},
|
|
1088
1143
|
},
|
|
1144
|
+
{
|
|
1145
|
+
id: '1.9-continuity-v2',
|
|
1146
|
+
apply: (db) => {
|
|
1147
|
+
addColumnIfMissing(db, 'task_continuity_events', 'event_key', 'event_key TEXT');
|
|
1148
|
+
addColumnIfMissing(db, 'task_continuity_events', 'item_key', 'item_key TEXT');
|
|
1149
|
+
addColumnIfMissing(db, 'task_continuity_events', 'supersedes_id', 'supersedes_id TEXT');
|
|
1150
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_task_continuity_item ON task_continuity_events(project_id, task_id, kind, item_key, created_at DESC)');
|
|
1151
|
+
db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_task_continuity_event_key
|
|
1152
|
+
ON task_continuity_events(project_id, task_id, event_key)
|
|
1153
|
+
WHERE event_key IS NOT NULL`);
|
|
1154
|
+
},
|
|
1155
|
+
},
|
|
1089
1156
|
];
|
|
1090
1157
|
|
|
1091
1158
|
function applySchemaMigrations(db: any): void {
|
|
@@ -1107,6 +1174,28 @@ function applySchemaMigrations(db: any): void {
|
|
|
1107
1174
|
// ── Singleton cache ─────────────────────────────────────────────────
|
|
1108
1175
|
|
|
1109
1176
|
const _dbCache = new Map<string, any>();
|
|
1177
|
+
const _dbLeases = new Map<string, number>();
|
|
1178
|
+
|
|
1179
|
+
export interface DatabaseLease {
|
|
1180
|
+
dataDir: string;
|
|
1181
|
+
db: any;
|
|
1182
|
+
release: () => void;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
export interface DatabaseStats {
|
|
1186
|
+
cached: number;
|
|
1187
|
+
leased: number;
|
|
1188
|
+
leasedDataDirs: number;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function normalizeDataDir(dataDir: string): string {
|
|
1192
|
+
return path.resolve(dataDir);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function touchDatabase(normalized: string, db: any): void {
|
|
1196
|
+
_dbCache.delete(normalized);
|
|
1197
|
+
_dbCache.set(normalized, db);
|
|
1198
|
+
}
|
|
1110
1199
|
|
|
1111
1200
|
/**
|
|
1112
1201
|
* Get or create a shared SQLite database handle for the given data directory.
|
|
@@ -1117,10 +1206,13 @@ const _dbCache = new Map<string, any>();
|
|
|
1117
1206
|
* Callers must NOT close the returned handle directly — use closeDatabase().
|
|
1118
1207
|
*/
|
|
1119
1208
|
export function getDatabase(dataDir: string): any {
|
|
1120
|
-
const normalized =
|
|
1209
|
+
const normalized = normalizeDataDir(dataDir);
|
|
1121
1210
|
assertNotHomeDataDir(normalized);
|
|
1122
1211
|
const existing = _dbCache.get(normalized);
|
|
1123
|
-
if (existing)
|
|
1212
|
+
if (existing) {
|
|
1213
|
+
touchDatabase(normalized, existing);
|
|
1214
|
+
return existing;
|
|
1215
|
+
}
|
|
1124
1216
|
|
|
1125
1217
|
loadBetterSqlite3();
|
|
1126
1218
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
@@ -1158,6 +1250,8 @@ export function getDatabase(dataDir: string): any {
|
|
|
1158
1250
|
db.exec(CREATE_MAINTENANCE_JOBS_TABLE);
|
|
1159
1251
|
db.exec(CREATE_MAINTENANCE_TARGETS_TABLE);
|
|
1160
1252
|
db.exec(CREATE_COMPACTION_CHECKPOINTS_TABLE);
|
|
1253
|
+
db.exec(CREATE_TASK_CONTINUITY_EVENTS_TABLE);
|
|
1254
|
+
db.exec(CREATE_AGENT_EFFECTS_TABLE);
|
|
1161
1255
|
|
|
1162
1256
|
// Phase 3a migration: add sourceSnapshot + updatedAt to mini_skills
|
|
1163
1257
|
// Idempotent — ALTER TABLE ADD COLUMN throws if column already exists
|
|
@@ -1191,7 +1285,7 @@ export function getDatabase(dataDir: string): any {
|
|
|
1191
1285
|
db.prepare(`INSERT OR IGNORE INTO meta (key, value) VALUES ('next_id', '1')`).run();
|
|
1192
1286
|
db.prepare(`INSERT OR IGNORE INTO meta (key, value) VALUES ('mini_skills_generation', '0')`).run();
|
|
1193
1287
|
|
|
1194
|
-
|
|
1288
|
+
touchDatabase(normalized, db);
|
|
1195
1289
|
return db;
|
|
1196
1290
|
} catch (error) {
|
|
1197
1291
|
// A schema/migration/FTS failure must not leave an uncached connection
|
|
@@ -1201,12 +1295,60 @@ export function getDatabase(dataDir: string): any {
|
|
|
1201
1295
|
}
|
|
1202
1296
|
}
|
|
1203
1297
|
|
|
1298
|
+
/**
|
|
1299
|
+
* Hold a database handle across an HTTP/session lifetime. Idle eviction only
|
|
1300
|
+
* closes handles with no active leases, so cache pressure cannot invalidate a
|
|
1301
|
+
* store that is still serving a request.
|
|
1302
|
+
*/
|
|
1303
|
+
export function acquireDatabase(dataDir: string): DatabaseLease {
|
|
1304
|
+
const normalized = normalizeDataDir(dataDir);
|
|
1305
|
+
const db = getDatabase(normalized);
|
|
1306
|
+
_dbLeases.set(normalized, (_dbLeases.get(normalized) ?? 0) + 1);
|
|
1307
|
+
let released = false;
|
|
1308
|
+
return {
|
|
1309
|
+
dataDir: normalized,
|
|
1310
|
+
db,
|
|
1311
|
+
release: () => {
|
|
1312
|
+
if (released) return;
|
|
1313
|
+
released = true;
|
|
1314
|
+
const count = (_dbLeases.get(normalized) ?? 0) - 1;
|
|
1315
|
+
if (count > 0) _dbLeases.set(normalized, count);
|
|
1316
|
+
else _dbLeases.delete(normalized);
|
|
1317
|
+
},
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/** Return bounded resource information for health and diagnostics. */
|
|
1322
|
+
export function getDatabaseStats(): DatabaseStats {
|
|
1323
|
+
let leased = 0;
|
|
1324
|
+
for (const count of _dbLeases.values()) leased += count;
|
|
1325
|
+
return { cached: _dbCache.size, leased, leasedDataDirs: _dbLeases.size };
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
/**
|
|
1329
|
+
* Evict least-recently-used unleased handles until the cache is within its
|
|
1330
|
+
* target. Stores that need a handle across awaits must hold a lease first.
|
|
1331
|
+
*/
|
|
1332
|
+
export function evictIdleDatabases(maxCached: number): number {
|
|
1333
|
+
const target = Math.max(0, Math.floor(maxCached));
|
|
1334
|
+
let evicted = 0;
|
|
1335
|
+
for (const [normalized, db] of [..._dbCache]) {
|
|
1336
|
+
if (_dbCache.size <= target) break;
|
|
1337
|
+
if ((_dbLeases.get(normalized) ?? 0) > 0) continue;
|
|
1338
|
+
try { db.close(); } catch { /* best-effort */ }
|
|
1339
|
+
_dbCache.delete(normalized);
|
|
1340
|
+
evicted++;
|
|
1341
|
+
}
|
|
1342
|
+
return evicted;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1204
1345
|
/**
|
|
1205
1346
|
* Close and remove a cached database handle for the given data directory.
|
|
1206
1347
|
* Safe to call even if no handle exists.
|
|
1207
1348
|
*/
|
|
1208
1349
|
export function closeDatabase(dataDir: string): void {
|
|
1209
|
-
const normalized =
|
|
1350
|
+
const normalized = normalizeDataDir(dataDir);
|
|
1351
|
+
if ((_dbLeases.get(normalized) ?? 0) > 0) return;
|
|
1210
1352
|
const db = _dbCache.get(normalized);
|
|
1211
1353
|
if (db) {
|
|
1212
1354
|
try { db.close(); } catch { /* best-effort */ }
|
|
@@ -1222,6 +1364,7 @@ export function closeAllDatabases(): void {
|
|
|
1222
1364
|
try { db.close(); } catch { /* best-effort */ }
|
|
1223
1365
|
_dbCache.delete(key);
|
|
1224
1366
|
}
|
|
1367
|
+
_dbLeases.clear();
|
|
1225
1368
|
}
|
|
1226
1369
|
|
|
1227
1370
|
/**
|