gnosys 5.11.4 → 5.12.2
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/cli.js +377 -5162
- package/dist/index.js +542 -244
- package/dist/lib/addCommand.d.ts +9 -0
- package/dist/lib/addCommand.js +102 -0
- package/dist/lib/addStructuredCommand.d.ts +16 -0
- package/dist/lib/addStructuredCommand.js +103 -0
- package/dist/lib/ambiguityCommand.d.ts +4 -0
- package/dist/lib/ambiguityCommand.js +36 -0
- package/dist/lib/apiKeyVault.d.ts +78 -0
- package/dist/lib/apiKeyVault.js +447 -0
- package/dist/lib/archive.js +0 -2
- package/dist/lib/askCommand.d.ts +13 -0
- package/dist/lib/askCommand.js +145 -0
- package/dist/lib/attachCommand.d.ts +17 -0
- package/dist/lib/attachCommand.js +66 -0
- package/dist/lib/attachments.d.ts +43 -2
- package/dist/lib/attachments.js +81 -2
- package/dist/lib/audioExtract.js +4 -1
- package/dist/lib/auditCommand.d.ts +7 -0
- package/dist/lib/auditCommand.js +27 -0
- package/dist/lib/backupCommand.d.ts +6 -0
- package/dist/lib/backupCommand.js +54 -0
- package/dist/lib/bootstrapCommand.d.ts +15 -0
- package/dist/lib/bootstrapCommand.js +51 -0
- package/dist/lib/briefingCommand.d.ts +7 -0
- package/dist/lib/briefingCommand.js +92 -0
- package/dist/lib/centralizeCommand.d.ts +5 -0
- package/dist/lib/centralizeCommand.js +16 -0
- package/dist/lib/chat/choose.js +2 -2
- package/dist/lib/chatCommand.d.ts +12 -0
- package/dist/lib/chatCommand.js +46 -0
- package/dist/lib/checkCommand.d.ts +4 -0
- package/dist/lib/checkCommand.js +133 -0
- package/dist/lib/clientReadOverlay.d.ts +27 -0
- package/dist/lib/clientReadOverlay.js +76 -0
- package/dist/lib/clientReadResolve.d.ts +32 -0
- package/dist/lib/clientReadResolve.js +84 -0
- package/dist/lib/commitContextCommand.d.ts +9 -0
- package/dist/lib/commitContextCommand.js +142 -0
- package/dist/lib/config.d.ts +41 -48
- package/dist/lib/config.js +58 -57
- package/dist/lib/configCommand.d.ts +10 -0
- package/dist/lib/configCommand.js +321 -0
- package/dist/lib/connectCommand.d.ts +8 -0
- package/dist/lib/connectCommand.js +19 -0
- package/dist/lib/db.d.ts +68 -1
- package/dist/lib/db.js +385 -120
- package/dist/lib/dbWrite.d.ts +1 -1
- package/dist/lib/dearchiveCommand.d.ts +7 -0
- package/dist/lib/dearchiveCommand.js +41 -0
- package/dist/lib/discoverCommand.d.ts +9 -0
- package/dist/lib/discoverCommand.js +87 -0
- package/dist/lib/doctorCommand.d.ts +6 -0
- package/dist/lib/doctorCommand.js +256 -0
- package/dist/lib/docxExtract.js +1 -1
- package/dist/lib/dream.d.ts +50 -2
- package/dist/lib/dream.js +324 -30
- package/dist/lib/dreamCommand.d.ts +10 -0
- package/dist/lib/dreamCommand.js +195 -0
- package/dist/lib/dreamLaunchd.d.ts +2 -0
- package/dist/lib/dreamLaunchd.js +72 -0
- package/dist/lib/dreamLogCommand.d.ts +10 -0
- package/dist/lib/dreamLogCommand.js +58 -0
- package/dist/lib/dreamReport.d.ts +7 -0
- package/dist/lib/dreamReport.js +114 -0
- package/dist/lib/dreamRunLog.d.ts +121 -0
- package/dist/lib/dreamRunLog.js +234 -0
- package/dist/lib/embeddings.js +3 -3
- package/dist/lib/exportCommand.d.ts +18 -0
- package/dist/lib/exportCommand.js +101 -0
- package/dist/lib/exportProject.d.ts +3 -2
- package/dist/lib/exportProject.js +2 -1
- package/dist/lib/federated.js +1 -1
- package/dist/lib/fsearchCommand.d.ts +8 -0
- package/dist/lib/fsearchCommand.js +44 -0
- package/dist/lib/graphCommand.d.ts +4 -0
- package/dist/lib/graphCommand.js +68 -0
- package/dist/lib/helperGenerateCommand.d.ts +5 -0
- package/dist/lib/helperGenerateCommand.js +27 -0
- package/dist/lib/historyCommand.d.ts +5 -0
- package/dist/lib/historyCommand.js +51 -0
- package/dist/lib/hybridSearchCommand.d.ts +12 -0
- package/dist/lib/hybridSearchCommand.js +95 -0
- package/dist/lib/importCommand.d.ts +16 -0
- package/dist/lib/importCommand.js +89 -0
- package/dist/lib/importProject.js +2 -1
- package/dist/lib/importProjectCommand.d.ts +6 -0
- package/dist/lib/importProjectCommand.js +43 -0
- package/dist/lib/ingestCommand.d.ts +13 -0
- package/dist/lib/ingestCommand.js +95 -0
- package/dist/lib/installOutput.d.ts +36 -0
- package/dist/lib/installOutput.js +55 -0
- package/dist/lib/lensCommand.d.ts +20 -0
- package/dist/lib/lensCommand.js +61 -0
- package/dist/lib/lensing.d.ts +1 -0
- package/dist/lib/lensing.js +50 -9
- package/dist/lib/linksCommand.d.ts +7 -0
- package/dist/lib/linksCommand.js +48 -0
- package/dist/lib/listCommand.d.ts +8 -0
- package/dist/lib/listCommand.js +74 -0
- package/dist/lib/llm.d.ts +1 -1
- package/dist/lib/llm.js +27 -9
- package/dist/lib/localDiskCheck.d.ts +17 -0
- package/dist/lib/localDiskCheck.js +54 -0
- package/dist/lib/lock.d.ts +1 -1
- package/dist/lib/lock.js +5 -3
- package/dist/lib/machineConfig.d.ts +11 -1
- package/dist/lib/machineConfig.js +16 -0
- package/dist/lib/machineRegistry.d.ts +61 -0
- package/dist/lib/machineRegistry.js +80 -0
- package/dist/lib/maintainCommand.d.ts +8 -0
- package/dist/lib/maintainCommand.js +34 -0
- package/dist/lib/masterLease.d.ts +20 -0
- package/dist/lib/masterLease.js +68 -0
- package/dist/lib/migrate.js +0 -1
- package/dist/lib/migrateCommand.d.ts +7 -0
- package/dist/lib/migrateCommand.js +158 -0
- package/dist/lib/migrateDbCommand.d.ts +9 -0
- package/dist/lib/migrateDbCommand.js +94 -0
- package/dist/lib/modelValidation.d.ts +5 -0
- package/dist/lib/modelValidation.js +27 -0
- package/dist/lib/multimodalIngest.js +1 -1
- package/dist/lib/openrouterTiers.d.ts +29 -0
- package/dist/lib/openrouterTiers.js +113 -0
- package/dist/lib/platform.d.ts +0 -6
- package/dist/lib/platform.js +0 -28
- package/dist/lib/prefCommand.d.ts +10 -0
- package/dist/lib/prefCommand.js +118 -0
- package/dist/lib/projectsCommand.d.ts +8 -0
- package/dist/lib/projectsCommand.js +131 -0
- package/dist/lib/readCommand.d.ts +7 -0
- package/dist/lib/readCommand.js +63 -0
- package/dist/lib/recall.d.ts +3 -0
- package/dist/lib/recall.js +19 -4
- package/dist/lib/recallCommand.d.ts +11 -0
- package/dist/lib/recallCommand.js +112 -0
- package/dist/lib/reflectCommand.d.ts +8 -0
- package/dist/lib/reflectCommand.js +61 -0
- package/dist/lib/reindexCommand.d.ts +4 -0
- package/dist/lib/reindexCommand.js +34 -0
- package/dist/lib/reindexGraphCommand.d.ts +4 -0
- package/dist/lib/reindexGraphCommand.js +12 -0
- package/dist/lib/reinforceCommand.d.ts +8 -0
- package/dist/lib/reinforceCommand.js +40 -0
- package/dist/lib/remote.d.ts +5 -1
- package/dist/lib/remote.js +5 -1
- package/dist/lib/remoteWizard.d.ts +24 -5
- package/dist/lib/remoteWizard.js +308 -319
- package/dist/lib/restoreCommand.d.ts +5 -0
- package/dist/lib/restoreCommand.js +35 -0
- package/dist/lib/rulesGen.d.ts +8 -0
- package/dist/lib/rulesGen.js +16 -0
- package/dist/lib/sandboxStartCommand.d.ts +6 -0
- package/dist/lib/sandboxStartCommand.js +25 -0
- package/dist/lib/sandboxStatusCommand.d.ts +4 -0
- package/dist/lib/sandboxStatusCommand.js +24 -0
- package/dist/lib/sandboxStopCommand.d.ts +4 -0
- package/dist/lib/sandboxStopCommand.js +21 -0
- package/dist/lib/search.d.ts +0 -2
- package/dist/lib/search.js +0 -7
- package/dist/lib/searchCommand.d.ts +9 -0
- package/dist/lib/searchCommand.js +90 -0
- package/dist/lib/semanticSearchCommand.d.ts +8 -0
- package/dist/lib/semanticSearchCommand.js +52 -0
- package/dist/lib/setup/configSetRender.js +2 -0
- package/dist/lib/setup/providerGlyphs.d.ts +19 -0
- package/dist/lib/setup/providerGlyphs.js +42 -0
- package/dist/lib/setup/remoteRender.d.ts +31 -1
- package/dist/lib/setup/remoteRender.js +95 -4
- package/dist/lib/setup/sections/providers.d.ts +17 -0
- package/dist/lib/setup/sections/providers.js +307 -0
- package/dist/lib/setup/sections/routing.d.ts +2 -6
- package/dist/lib/setup/sections/routing.js +67 -82
- package/dist/lib/setup/sections/taskRoutingEditor.d.ts +13 -0
- package/dist/lib/setup/sections/taskRoutingEditor.js +139 -0
- package/dist/lib/setup/summary.d.ts +9 -0
- package/dist/lib/setup/summary.js +51 -37
- package/dist/lib/setup/ui/header.js +0 -1
- package/dist/lib/setup.d.ts +105 -15
- package/dist/lib/setup.js +747 -287
- package/dist/lib/setupKeys.d.ts +42 -0
- package/dist/lib/setupKeys.js +564 -0
- package/dist/lib/setupRemoteCommand.d.ts +4 -0
- package/dist/lib/setupRemoteCommand.js +28 -0
- package/dist/lib/setupRemotePullCommand.d.ts +5 -0
- package/dist/lib/setupRemotePullCommand.js +52 -0
- package/dist/lib/setupRemotePushCommand.d.ts +5 -0
- package/dist/lib/setupRemotePushCommand.js +57 -0
- package/dist/lib/setupRemoteResolveCommand.d.ts +4 -0
- package/dist/lib/setupRemoteResolveCommand.js +48 -0
- package/dist/lib/setupRemoteStatusCommand.d.ts +4 -0
- package/dist/lib/setupRemoteStatusCommand.js +73 -0
- package/dist/lib/setupRemoteSyncCommand.d.ts +6 -0
- package/dist/lib/setupRemoteSyncCommand.js +65 -0
- package/dist/lib/setupSyncProjectsCommand.d.ts +4 -0
- package/dist/lib/setupSyncProjectsCommand.js +292 -0
- package/dist/lib/staleCommand.d.ts +8 -0
- package/dist/lib/staleCommand.js +34 -0
- package/dist/lib/statsCommand.d.ts +6 -0
- package/dist/lib/statsCommand.js +142 -0
- package/dist/lib/statusCommand.d.ts +18 -0
- package/dist/lib/statusCommand.js +250 -0
- package/dist/lib/storesCommand.d.ts +2 -0
- package/dist/lib/storesCommand.js +4 -0
- package/dist/lib/syncClient.d.ts +41 -0
- package/dist/lib/syncClient.js +234 -0
- package/dist/lib/syncCommand.d.ts +6 -0
- package/dist/lib/syncCommand.js +57 -0
- package/dist/lib/syncDoctorCommand.d.ts +5 -0
- package/dist/lib/syncDoctorCommand.js +100 -0
- package/dist/lib/syncIngest.d.ts +30 -0
- package/dist/lib/syncIngest.js +175 -0
- package/dist/lib/syncIngestLaunchd.d.ts +8 -0
- package/dist/lib/syncIngestLaunchd.js +93 -0
- package/dist/lib/syncIngestStartup.d.ts +5 -0
- package/dist/lib/syncIngestStartup.js +29 -0
- package/dist/lib/syncIngestSystemd.d.ts +10 -0
- package/dist/lib/syncIngestSystemd.js +97 -0
- package/dist/lib/syncIngestTimer.d.ts +8 -0
- package/dist/lib/syncIngestTimer.js +27 -0
- package/dist/lib/syncIngestTimerCommand.d.ts +7 -0
- package/dist/lib/syncIngestTimerCommand.js +83 -0
- package/dist/lib/syncLock.d.ts +6 -0
- package/dist/lib/syncLock.js +74 -0
- package/dist/lib/syncSnapshot.d.ts +32 -0
- package/dist/lib/syncSnapshot.js +188 -0
- package/dist/lib/syncStaging.d.ts +79 -0
- package/dist/lib/syncStaging.js +237 -0
- package/dist/lib/tagsAddCommand.d.ts +8 -0
- package/dist/lib/tagsAddCommand.js +18 -0
- package/dist/lib/tagsCommand.d.ts +4 -0
- package/dist/lib/tagsCommand.js +16 -0
- package/dist/lib/timelineCommand.d.ts +7 -0
- package/dist/lib/timelineCommand.js +49 -0
- package/dist/lib/traceCommand.d.ts +6 -0
- package/dist/lib/traceCommand.js +39 -0
- package/dist/lib/traverseCommand.d.ts +6 -0
- package/dist/lib/traverseCommand.js +58 -0
- package/dist/lib/updateCommand.d.ts +13 -0
- package/dist/lib/updateCommand.js +67 -0
- package/dist/lib/updateStatusCommand.d.ts +5 -0
- package/dist/lib/updateStatusCommand.js +38 -0
- package/dist/lib/webAddCommand.d.ts +8 -0
- package/dist/lib/webAddCommand.js +55 -0
- package/dist/lib/webBuildCommand.d.ts +10 -0
- package/dist/lib/webBuildCommand.js +65 -0
- package/dist/lib/webBuildIndexCommand.d.ts +8 -0
- package/dist/lib/webBuildIndexCommand.js +37 -0
- package/dist/lib/webIndex.js +0 -1
- package/dist/lib/webIngestCommand.d.ts +11 -0
- package/dist/lib/webIngestCommand.js +51 -0
- package/dist/lib/webInitCommand.d.ts +9 -0
- package/dist/lib/webInitCommand.js +167 -0
- package/dist/lib/webRemoveCommand.d.ts +5 -0
- package/dist/lib/webRemoveCommand.js +41 -0
- package/dist/lib/webStatusCommand.d.ts +5 -0
- package/dist/lib/webStatusCommand.js +94 -0
- package/dist/lib/webUpdateCommand.d.ts +7 -0
- package/dist/lib/webUpdateCommand.js +72 -0
- package/dist/lib/workingSetCommand.d.ts +6 -0
- package/dist/lib/workingSetCommand.js +37 -0
- package/dist/sandbox/client.js +1 -1
- package/dist/sandbox/manager.js +1 -14
- package/dist/sandbox/server.js +3 -5
- package/package.json +6 -2
package/dist/lib/db.js
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
* + projects table (v3.0) for project identity registry.
|
|
9
9
|
*/
|
|
10
10
|
// Dynamic import — gracefully handles missing native module
|
|
11
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
11
|
let Database = null;
|
|
13
12
|
try {
|
|
14
13
|
Database = (await import("better-sqlite3")).default;
|
|
@@ -24,7 +23,7 @@ import { readMachineConfig } from "./machineConfig.js";
|
|
|
24
23
|
import { logError } from "./log.js";
|
|
25
24
|
import { ulid } from "ulidx";
|
|
26
25
|
// ─── Schema ─────────────────────────────────────────────────────────────
|
|
27
|
-
const SCHEMA_VERSION =
|
|
26
|
+
const SCHEMA_VERSION = 5;
|
|
28
27
|
const SCHEMA_SQL = `
|
|
29
28
|
CREATE TABLE IF NOT EXISTS memories (
|
|
30
29
|
id TEXT PRIMARY KEY,
|
|
@@ -51,6 +50,9 @@ CREATE TABLE IF NOT EXISTS memories (
|
|
|
51
50
|
source_file TEXT,
|
|
52
51
|
source_page TEXT,
|
|
53
52
|
source_timerange TEXT,
|
|
53
|
+
attachment_data BLOB,
|
|
54
|
+
attachment_mime TEXT,
|
|
55
|
+
attachment_name TEXT,
|
|
54
56
|
project_id TEXT,
|
|
55
57
|
scope TEXT DEFAULT 'project' CHECK(scope IN ('project','user','global'))
|
|
56
58
|
);
|
|
@@ -171,6 +173,51 @@ CREATE TABLE IF NOT EXISTS sync_conflicts (
|
|
|
171
173
|
);
|
|
172
174
|
|
|
173
175
|
CREATE INDEX IF NOT EXISTS idx_sync_conflicts_status ON sync_conflicts(status);
|
|
176
|
+
|
|
177
|
+
-- v13 multi-machine sync (master DB)
|
|
178
|
+
|
|
179
|
+
CREATE TABLE IF NOT EXISTS sync_staging_ledger (
|
|
180
|
+
staging_key TEXT PRIMARY KEY,
|
|
181
|
+
machine_id TEXT NOT NULL,
|
|
182
|
+
memory_ulid TEXT,
|
|
183
|
+
first_seen_at TEXT NOT NULL,
|
|
184
|
+
ingest_epoch INTEGER,
|
|
185
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
CREATE INDEX IF NOT EXISTS idx_sync_staging_status ON sync_staging_ledger(status);
|
|
189
|
+
CREATE INDEX IF NOT EXISTS idx_sync_staging_first_seen ON sync_staging_ledger(first_seen_at);
|
|
190
|
+
|
|
191
|
+
CREATE TABLE IF NOT EXISTS sync_processed_ulids (
|
|
192
|
+
ulid TEXT PRIMARY KEY,
|
|
193
|
+
ingested_at TEXT NOT NULL,
|
|
194
|
+
ingest_epoch INTEGER
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
CREATE TABLE IF NOT EXISTS sync_pending_adds (
|
|
198
|
+
id TEXT PRIMARY KEY,
|
|
199
|
+
title TEXT NOT NULL,
|
|
200
|
+
category TEXT NOT NULL,
|
|
201
|
+
content TEXT NOT NULL,
|
|
202
|
+
tags TEXT DEFAULT '',
|
|
203
|
+
project_id TEXT,
|
|
204
|
+
scope TEXT DEFAULT 'project',
|
|
205
|
+
created TEXT NOT NULL,
|
|
206
|
+
cleared_at TEXT
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
CREATE INDEX IF NOT EXISTS idx_sync_pending_adds_cleared ON sync_pending_adds(cleared_at);
|
|
210
|
+
|
|
211
|
+
CREATE TABLE IF NOT EXISTS sync_snapshot_manifest (
|
|
212
|
+
singleton_id INTEGER PRIMARY KEY CHECK (singleton_id = 1),
|
|
213
|
+
epoch INTEGER NOT NULL,
|
|
214
|
+
seq INTEGER NOT NULL,
|
|
215
|
+
snapshot_path TEXT NOT NULL,
|
|
216
|
+
published_at TEXT NOT NULL,
|
|
217
|
+
checksum TEXT,
|
|
218
|
+
size_bytes INTEGER,
|
|
219
|
+
heartbeat_at TEXT
|
|
220
|
+
);
|
|
174
221
|
`;
|
|
175
222
|
// FTS5 sync triggers — created separately (can't use IF NOT EXISTS on triggers)
|
|
176
223
|
const FTS_TRIGGERS_SQL = `
|
|
@@ -198,12 +245,27 @@ const MEMORY_COLUMNS = new Set([
|
|
|
198
245
|
"status", "tier", "supersedes", "superseded_by", "last_reinforced",
|
|
199
246
|
"created", "modified", "embedding", "source_path",
|
|
200
247
|
"source_file", "source_page", "source_timerange",
|
|
248
|
+
"attachment_data", "attachment_mime", "attachment_name",
|
|
201
249
|
"project_id", "scope",
|
|
202
250
|
]);
|
|
203
251
|
const PROJECT_COLUMNS = new Set([
|
|
204
252
|
"name", "working_directory", "root_id", "rel_path", "user",
|
|
205
253
|
"agent_rules_target", "obsidian_vault", "created", "modified",
|
|
206
254
|
]);
|
|
255
|
+
/**
|
|
256
|
+
* v5.12.x perf: full DbMemory shape with the two BLOB columns projected as
|
|
257
|
+
* NULL. List-style reads (recall, federation, list) never consume embedding
|
|
258
|
+
* or attachment bytes. Measured win on embedding-only rows is modest (~4%
|
|
259
|
+
* plus avoided Buffer churn), but attachments are the real reason: a single
|
|
260
|
+
* ~10MB gnosys_attach blob would otherwise be hydrated on EVERY list call.
|
|
261
|
+
* Blob consumers use getAllEmbeddings/getEmbedding/getMemoryAttachment, and
|
|
262
|
+
* getAllMemories/getMemoriesByProject still return full rows (remote sync
|
|
263
|
+
* and project export push them verbatim).
|
|
264
|
+
*/
|
|
265
|
+
const LEAN_MEMORY_PROJECTION = [
|
|
266
|
+
"id", ...[...MEMORY_COLUMNS].filter((c) => c !== "embedding" && c !== "attachment_data"),
|
|
267
|
+
"NULL AS embedding", "NULL AS attachment_data",
|
|
268
|
+
].join(", ");
|
|
207
269
|
// ─── FNV-1a hash (same as embeddings.ts) ────────────────────────────────
|
|
208
270
|
function fnv1a(str) {
|
|
209
271
|
let hash = 0x811c9dc5;
|
|
@@ -215,8 +277,10 @@ function fnv1a(str) {
|
|
|
215
277
|
}
|
|
216
278
|
// ─── GnosysDB Class ─────────────────────────────────────────────────────
|
|
217
279
|
export class GnosysDB {
|
|
218
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
219
280
|
db = null;
|
|
281
|
+
/** v5.12.x perf: prepared-statement cache, keyed by SQL. Invalidated on
|
|
282
|
+
* reopen()/close() — statements are bound to their connection handle. */
|
|
283
|
+
stmtCache = new Map();
|
|
220
284
|
storePath;
|
|
221
285
|
available = false;
|
|
222
286
|
dbFilePath;
|
|
@@ -336,7 +400,8 @@ export class GnosysDB {
|
|
|
336
400
|
try {
|
|
337
401
|
fs.mkdirSync(storePath, { recursive: true, mode: 0o700 });
|
|
338
402
|
this.db = new Database(this.dbFilePath);
|
|
339
|
-
|
|
403
|
+
// Longer busy timeout for network shares (10s)
|
|
404
|
+
enableWAL(this.db, 10000);
|
|
340
405
|
try {
|
|
341
406
|
fs.chmodSync(storePath, 0o700);
|
|
342
407
|
fs.chmodSync(this.dbFilePath, 0o600);
|
|
@@ -351,13 +416,11 @@ export class GnosysDB {
|
|
|
351
416
|
// best-effort (Windows / network FS)
|
|
352
417
|
}
|
|
353
418
|
this.db.pragma("foreign_keys = ON");
|
|
354
|
-
// Longer busy timeout for network shares (10s)
|
|
355
|
-
this.db.pragma("busy_timeout = 10000");
|
|
356
419
|
this.applySchema();
|
|
357
420
|
this.available = true;
|
|
358
421
|
return; // Success
|
|
359
422
|
}
|
|
360
|
-
catch (
|
|
423
|
+
catch (_err) {
|
|
361
424
|
this.db = null;
|
|
362
425
|
if (attempt < maxRetries) {
|
|
363
426
|
// Synchronous delay for constructor (network share retry)
|
|
@@ -426,6 +489,7 @@ export class GnosysDB {
|
|
|
426
489
|
* file handles after a WAL checkpoint or remount.
|
|
427
490
|
*/
|
|
428
491
|
reopen() {
|
|
492
|
+
this.stmtCache.clear();
|
|
429
493
|
try {
|
|
430
494
|
this.db?.close();
|
|
431
495
|
}
|
|
@@ -438,9 +502,19 @@ export class GnosysDB {
|
|
|
438
502
|
return;
|
|
439
503
|
try {
|
|
440
504
|
this.db = new Database(this.dbFilePath);
|
|
441
|
-
|
|
505
|
+
// Longer busy timeout for network shares (10s)
|
|
506
|
+
enableWAL(this.db, 10000);
|
|
442
507
|
this.db.pragma("foreign_keys = ON");
|
|
443
|
-
|
|
508
|
+
// v5.12.1: heal FTS triggers on recovery. updateMemory/deleteMemory may
|
|
509
|
+
// drop a trigger in their inconsistency fallback; recreating here
|
|
510
|
+
// (idempotent CREATE TRIGGER IF NOT EXISTS) means recovery restores
|
|
511
|
+
// them instead of waiting for the next process start.
|
|
512
|
+
try {
|
|
513
|
+
this.db.exec(FTS_TRIGGERS_SQL);
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
// non-fatal — applySchema() heals at next open
|
|
517
|
+
}
|
|
444
518
|
this.available = true;
|
|
445
519
|
}
|
|
446
520
|
catch {
|
|
@@ -463,15 +537,28 @@ export class GnosysDB {
|
|
|
463
537
|
* Read methods are also wrapped because reads against stale pages can
|
|
464
538
|
* surface the same error.
|
|
465
539
|
*/
|
|
540
|
+
/**
|
|
541
|
+
* Prepare-with-cache for fixed-SQL hot paths. Dynamic SQL (e.g. the
|
|
542
|
+
* field-built UPDATE in updateMemory) must keep using db.prepare directly
|
|
543
|
+
* so the cache stays bounded. Cache is invalidated on reopen()/close().
|
|
544
|
+
*/
|
|
545
|
+
prep(sql) {
|
|
546
|
+
let stmt = this.stmtCache.get(sql);
|
|
547
|
+
if (!stmt) {
|
|
548
|
+
stmt = this.db.prepare(sql);
|
|
549
|
+
this.stmtCache.set(sql, stmt);
|
|
550
|
+
}
|
|
551
|
+
return stmt;
|
|
552
|
+
}
|
|
466
553
|
withRecovery(fn) {
|
|
467
554
|
try {
|
|
468
555
|
return fn();
|
|
469
556
|
}
|
|
470
557
|
catch (err) {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
if (!
|
|
558
|
+
// v5.12.1: use the shared corruption detector — it also matches
|
|
559
|
+
// SQLITE_NOTADB ("file is not a database"), which surfaces on network
|
|
560
|
+
// shares when a sync layer swaps the file under a live handle.
|
|
561
|
+
if (!GnosysDB.isCorruptionError(err))
|
|
475
562
|
throw err;
|
|
476
563
|
// One-shot recovery: reopen and retry. If the reopen itself fails or
|
|
477
564
|
// the retry surfaces the same error, that's a real corruption case —
|
|
@@ -641,6 +728,78 @@ export class GnosysDB {
|
|
|
641
728
|
// Table may already exist
|
|
642
729
|
}
|
|
643
730
|
}
|
|
731
|
+
if (fromVersion < 5) {
|
|
732
|
+
// v4 → v5 (v5.12): inline binary attachments carried in the memory row.
|
|
733
|
+
// Additive columns only — existing rows get NULLs. These ride the same
|
|
734
|
+
// row-copy sync path as `embedding`, so attachments travel machine to
|
|
735
|
+
// machine for free.
|
|
736
|
+
try {
|
|
737
|
+
this.db.exec("ALTER TABLE memories ADD COLUMN attachment_data BLOB");
|
|
738
|
+
}
|
|
739
|
+
catch {
|
|
740
|
+
// Column already exists — fine
|
|
741
|
+
}
|
|
742
|
+
try {
|
|
743
|
+
this.db.exec("ALTER TABLE memories ADD COLUMN attachment_mime TEXT");
|
|
744
|
+
}
|
|
745
|
+
catch {
|
|
746
|
+
// Column already exists — fine
|
|
747
|
+
}
|
|
748
|
+
try {
|
|
749
|
+
this.db.exec("ALTER TABLE memories ADD COLUMN attachment_name TEXT");
|
|
750
|
+
}
|
|
751
|
+
catch {
|
|
752
|
+
// Column already exists — fine
|
|
753
|
+
}
|
|
754
|
+
// Sync staging / multi-machine ledger tables (from network-mcp work on feat).
|
|
755
|
+
try {
|
|
756
|
+
this.db.exec(`
|
|
757
|
+
CREATE TABLE IF NOT EXISTS sync_staging_ledger (
|
|
758
|
+
staging_key TEXT PRIMARY KEY,
|
|
759
|
+
machine_id TEXT NOT NULL,
|
|
760
|
+
memory_ulid TEXT,
|
|
761
|
+
first_seen_at TEXT NOT NULL,
|
|
762
|
+
ingest_epoch INTEGER,
|
|
763
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
764
|
+
);
|
|
765
|
+
CREATE INDEX IF NOT EXISTS idx_sync_staging_status ON sync_staging_ledger(status);
|
|
766
|
+
CREATE INDEX IF NOT EXISTS idx_sync_staging_first_seen ON sync_staging_ledger(first_seen_at);
|
|
767
|
+
|
|
768
|
+
CREATE TABLE IF NOT EXISTS sync_processed_ulids (
|
|
769
|
+
ulid TEXT PRIMARY KEY,
|
|
770
|
+
ingested_at TEXT NOT NULL,
|
|
771
|
+
ingest_epoch INTEGER
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
CREATE TABLE IF NOT EXISTS sync_pending_adds (
|
|
775
|
+
id TEXT PRIMARY KEY,
|
|
776
|
+
title TEXT NOT NULL,
|
|
777
|
+
category TEXT NOT NULL,
|
|
778
|
+
content TEXT NOT NULL,
|
|
779
|
+
tags TEXT DEFAULT '',
|
|
780
|
+
project_id TEXT,
|
|
781
|
+
scope TEXT DEFAULT 'project',
|
|
782
|
+
created TEXT NOT NULL,
|
|
783
|
+
cleared_at TEXT
|
|
784
|
+
);
|
|
785
|
+
CREATE INDEX IF NOT EXISTS idx_sync_pending_adds_cleared ON sync_pending_adds(cleared_at);
|
|
786
|
+
|
|
787
|
+
CREATE TABLE IF NOT EXISTS sync_snapshot_manifest (
|
|
788
|
+
singleton_id INTEGER PRIMARY KEY CHECK (singleton_id = 1),
|
|
789
|
+
epoch INTEGER NOT NULL,
|
|
790
|
+
seq INTEGER NOT NULL,
|
|
791
|
+
snapshot_path TEXT NOT NULL,
|
|
792
|
+
published_at TEXT NOT NULL,
|
|
793
|
+
checksum TEXT,
|
|
794
|
+
size_bytes INTEGER,
|
|
795
|
+
heartbeat_at TEXT
|
|
796
|
+
);
|
|
797
|
+
`);
|
|
798
|
+
}
|
|
799
|
+
catch {
|
|
800
|
+
// Sync tables/indexes may already exist — fine
|
|
801
|
+
}
|
|
802
|
+
}
|
|
644
803
|
this.db.pragma(`user_version = ${SCHEMA_VERSION}`);
|
|
645
804
|
}
|
|
646
805
|
isAvailable() {
|
|
@@ -655,23 +814,26 @@ export class GnosysDB {
|
|
|
655
814
|
// ─── Memory CRUD ────────────────────────────────────────────────────
|
|
656
815
|
insertMemory(mem) {
|
|
657
816
|
return this.withRecovery(() => {
|
|
658
|
-
const stmt = this.
|
|
817
|
+
const stmt = this.prep(`
|
|
659
818
|
INSERT OR REPLACE INTO memories
|
|
660
819
|
(id, title, category, content, summary, tags, relevance, author, authority,
|
|
661
820
|
confidence, reinforcement_count, content_hash, status, tier, supersedes,
|
|
662
821
|
superseded_by, last_reinforced, created, modified, embedding, source_path,
|
|
663
822
|
source_file, source_page, source_timerange,
|
|
823
|
+
attachment_data, attachment_mime, attachment_name,
|
|
664
824
|
project_id, scope)
|
|
665
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
825
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
666
826
|
`);
|
|
667
|
-
stmt.run(mem.id, mem.title, mem.category, mem.content, mem.summary || null, mem.tags, mem.relevance, mem.author, mem.authority, mem.confidence, mem.reinforcement_count, mem.content_hash, mem.status, mem.tier, mem.supersedes || null, mem.superseded_by || null, mem.last_reinforced || null, mem.created, mem.modified, mem.embedding || null, mem.source_path || null, mem.source_file || null, mem.source_page || null, mem.source_timerange || null, mem.project_id || null, mem.scope || "project");
|
|
827
|
+
stmt.run(mem.id, mem.title, mem.category, mem.content, mem.summary || null, mem.tags, mem.relevance, mem.author, mem.authority, mem.confidence, mem.reinforcement_count, mem.content_hash, mem.status, mem.tier, mem.supersedes || null, mem.superseded_by || null, mem.last_reinforced || null, mem.created, mem.modified, mem.embedding || null, mem.source_path || null, mem.source_file || null, mem.source_page || null, mem.source_timerange || null, mem.attachment_data || null, mem.attachment_mime || null, mem.attachment_name || null, mem.project_id || null, mem.scope || "project");
|
|
668
828
|
});
|
|
669
829
|
}
|
|
670
830
|
getMemory(id) {
|
|
671
|
-
return this.withRecovery(() => this.
|
|
831
|
+
return this.withRecovery(() => this.prep("SELECT * FROM memories WHERE id = ?").get(id) || null);
|
|
672
832
|
}
|
|
673
833
|
getActiveMemories() {
|
|
674
|
-
|
|
834
|
+
// v5.12.x perf: project NULL for the two BLOB columns — see
|
|
835
|
+
// LEAN_MEMORY_PROJECTION for the rationale and measured numbers.
|
|
836
|
+
return this.withRecovery(() => this.prep(`SELECT ${LEAN_MEMORY_PROJECTION} FROM memories WHERE tier = 'active' AND status = 'active'`).all());
|
|
675
837
|
}
|
|
676
838
|
getAllMemories() {
|
|
677
839
|
return this.withRecovery(() => this.db.prepare("SELECT * FROM memories").all());
|
|
@@ -701,20 +863,20 @@ export class GnosysDB {
|
|
|
701
863
|
this.db.pragma(`busy_timeout = ${Math.max(0, Math.floor(ms))}`);
|
|
702
864
|
}
|
|
703
865
|
getMemoriesByCategory(category) {
|
|
704
|
-
return this.
|
|
866
|
+
return this.withRecovery(() => this.prep("SELECT * FROM memories WHERE category = ? AND tier = 'active'").all(category));
|
|
705
867
|
}
|
|
706
868
|
getRelationshipsForMemoryIds(ids) {
|
|
707
869
|
if (ids.length === 0)
|
|
708
870
|
return [];
|
|
709
871
|
const placeholders = ids.map(() => "?").join(",");
|
|
710
|
-
return this.db
|
|
872
|
+
return this.withRecovery(() => this.db
|
|
711
873
|
.prepare(`SELECT * FROM relationships WHERE source_id IN (${placeholders}) OR target_id IN (${placeholders})`)
|
|
712
|
-
.all(...ids, ...ids);
|
|
874
|
+
.all(...ids, ...ids));
|
|
713
875
|
}
|
|
714
876
|
getAuditEntriesByProject(projectId) {
|
|
715
|
-
return this.db
|
|
877
|
+
return this.withRecovery(() => this.db
|
|
716
878
|
.prepare("SELECT * FROM audit_log WHERE memory_id IN (SELECT id FROM memories WHERE project_id = ?) ORDER BY id")
|
|
717
|
-
.all(projectId);
|
|
879
|
+
.all(projectId));
|
|
718
880
|
}
|
|
719
881
|
updateMemory(id, updates) {
|
|
720
882
|
const fields = [];
|
|
@@ -731,17 +893,18 @@ export class GnosysDB {
|
|
|
731
893
|
return;
|
|
732
894
|
values.push(id);
|
|
733
895
|
const sql = `UPDATE memories SET ${fields.join(", ")} WHERE id = ?`;
|
|
734
|
-
|
|
735
|
-
this.db.prepare(sql).run(...values);
|
|
736
|
-
}
|
|
737
|
-
catch {
|
|
738
|
-
// FTS5 update trigger may fail if INSERT OR REPLACE left FTS inconsistent.
|
|
739
|
-
// Workaround: drop the trigger, update manually, rebuild FTS entry.
|
|
740
|
-
this.db.exec("DROP TRIGGER IF EXISTS memories_fts_au");
|
|
741
|
-
this.db.prepare(sql).run(...values);
|
|
742
|
-
// Recreate trigger
|
|
896
|
+
return this.withRecovery(() => {
|
|
743
897
|
try {
|
|
744
|
-
this.db.
|
|
898
|
+
this.db.prepare(sql).run(...values);
|
|
899
|
+
}
|
|
900
|
+
catch {
|
|
901
|
+
// FTS5 update trigger may fail if INSERT OR REPLACE left FTS inconsistent.
|
|
902
|
+
// Workaround: drop the trigger, update manually, rebuild FTS entry.
|
|
903
|
+
this.db.exec("DROP TRIGGER IF EXISTS memories_fts_au");
|
|
904
|
+
this.db.prepare(sql).run(...values);
|
|
905
|
+
// Recreate trigger
|
|
906
|
+
try {
|
|
907
|
+
this.db.exec(`
|
|
745
908
|
CREATE TRIGGER IF NOT EXISTS memories_fts_au AFTER UPDATE ON memories BEGIN
|
|
746
909
|
INSERT INTO memories_fts(memories_fts, id, title, category, tags, relevance, content, summary)
|
|
747
910
|
VALUES ('delete', old.id, old.title, old.category, old.tags, old.relevance, old.content, old.summary);
|
|
@@ -749,66 +912,73 @@ export class GnosysDB {
|
|
|
749
912
|
VALUES (new.id, new.title, new.category, new.tags, new.relevance, new.content, new.summary);
|
|
750
913
|
END;
|
|
751
914
|
`);
|
|
915
|
+
}
|
|
916
|
+
catch {
|
|
917
|
+
// Trigger recreation failed — not critical
|
|
918
|
+
}
|
|
752
919
|
}
|
|
753
|
-
|
|
754
|
-
// Trigger recreation failed — not critical
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
// Manually sync FTS: remove old entry, insert updated entry (reliable for standalone FTS5)
|
|
758
|
-
try {
|
|
759
|
-
this.db.prepare("DELETE FROM memories_fts WHERE id = ?").run(id);
|
|
760
|
-
}
|
|
761
|
-
catch {
|
|
762
|
-
// Old FTS entry may not exist — that's OK
|
|
763
|
-
}
|
|
764
|
-
const newMem = this.db.prepare("SELECT * FROM memories WHERE id = ?").get(id);
|
|
765
|
-
if (newMem) {
|
|
920
|
+
// Manually sync FTS: remove old entry, insert updated entry (reliable for standalone FTS5)
|
|
766
921
|
try {
|
|
767
|
-
this.db.prepare("
|
|
922
|
+
this.db.prepare("DELETE FROM memories_fts WHERE id = ?").run(id);
|
|
768
923
|
}
|
|
769
924
|
catch {
|
|
770
|
-
// FTS
|
|
925
|
+
// Old FTS entry may not exist — that's OK
|
|
771
926
|
}
|
|
772
|
-
|
|
927
|
+
const newMem = this.db.prepare("SELECT * FROM memories WHERE id = ?").get(id);
|
|
928
|
+
if (newMem) {
|
|
929
|
+
try {
|
|
930
|
+
this.db.prepare("INSERT INTO memories_fts(id, title, category, tags, relevance, content, summary) VALUES (?, ?, ?, ?, ?, ?, ?)").run(newMem.id, newMem.title, newMem.category, newMem.tags, newMem.relevance, newMem.content, newMem.summary);
|
|
931
|
+
}
|
|
932
|
+
catch {
|
|
933
|
+
// FTS insert may fail — not critical
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
});
|
|
773
937
|
}
|
|
774
938
|
deleteMemory(id) {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
this.db.prepare("DELETE FROM memories WHERE id = ?").run(id);
|
|
778
|
-
}
|
|
779
|
-
catch {
|
|
780
|
-
// FTS trigger failed — drop trigger, delete without it
|
|
781
|
-
this.db.exec("DROP TRIGGER IF EXISTS memories_fts_ad");
|
|
782
|
-
this.db.prepare("DELETE FROM memories WHERE id = ?").run(id);
|
|
783
|
-
// Recreate trigger
|
|
939
|
+
return this.withRecovery(() => {
|
|
940
|
+
// FTS5 delete trigger may fail if INSERT OR REPLACE left FTS inconsistent.
|
|
784
941
|
try {
|
|
785
|
-
this.db.
|
|
942
|
+
this.db.prepare("DELETE FROM memories WHERE id = ?").run(id);
|
|
943
|
+
}
|
|
944
|
+
catch {
|
|
945
|
+
// FTS trigger failed — drop trigger, delete without it
|
|
946
|
+
this.db.exec("DROP TRIGGER IF EXISTS memories_fts_ad");
|
|
947
|
+
this.db.prepare("DELETE FROM memories WHERE id = ?").run(id);
|
|
948
|
+
// Recreate trigger
|
|
949
|
+
try {
|
|
950
|
+
this.db.exec(`
|
|
786
951
|
CREATE TRIGGER IF NOT EXISTS memories_fts_ad AFTER DELETE ON memories BEGIN
|
|
787
952
|
INSERT INTO memories_fts(memories_fts, id, title, category, tags, relevance, content, summary)
|
|
788
953
|
VALUES ('delete', old.id, old.title, old.category, old.tags, old.relevance, old.content, old.summary);
|
|
789
954
|
END;
|
|
790
955
|
`);
|
|
956
|
+
}
|
|
957
|
+
catch {
|
|
958
|
+
// Trigger recreation failed — not critical
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
// Ensure FTS entry is also removed (direct DELETE is reliable for standalone FTS5)
|
|
962
|
+
try {
|
|
963
|
+
this.db.prepare("DELETE FROM memories_fts WHERE id = ?").run(id);
|
|
791
964
|
}
|
|
792
965
|
catch {
|
|
793
|
-
//
|
|
966
|
+
// FTS entry may not exist — that's OK
|
|
794
967
|
}
|
|
795
|
-
}
|
|
796
|
-
// Ensure FTS entry is also removed (direct DELETE is reliable for standalone FTS5)
|
|
797
|
-
try {
|
|
798
|
-
this.db.prepare("DELETE FROM memories_fts WHERE id = ?").run(id);
|
|
799
|
-
}
|
|
800
|
-
catch {
|
|
801
|
-
// FTS entry may not exist — that's OK
|
|
802
|
-
}
|
|
968
|
+
});
|
|
803
969
|
}
|
|
804
970
|
getMemoryCount() {
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
971
|
+
return this.withRecovery(() => {
|
|
972
|
+
const active = this.db.prepare("SELECT COUNT(*) as cnt FROM memories WHERE tier = 'active'").get().cnt;
|
|
973
|
+
const archived = this.db.prepare("SELECT COUNT(*) as cnt FROM memories WHERE tier = 'archive'").get().cnt;
|
|
974
|
+
return { active, archived, total: active + archived };
|
|
975
|
+
});
|
|
808
976
|
}
|
|
809
977
|
getCategories() {
|
|
810
|
-
|
|
811
|
-
|
|
978
|
+
return this.withRecovery(() => {
|
|
979
|
+
const rows = this.db.prepare("SELECT DISTINCT category FROM memories WHERE tier = 'active' ORDER BY category").all();
|
|
980
|
+
return rows.map((r) => r.category);
|
|
981
|
+
});
|
|
812
982
|
}
|
|
813
983
|
// ─── Scoped Queries (v3.0) ──────────────────────────────────────────
|
|
814
984
|
/**
|
|
@@ -818,13 +988,13 @@ export class GnosysDB {
|
|
|
818
988
|
const sql = includeArchived
|
|
819
989
|
? "SELECT * FROM memories WHERE project_id = ?"
|
|
820
990
|
: "SELECT * FROM memories WHERE project_id = ? AND tier = 'active' AND status = 'active'";
|
|
821
|
-
return this.
|
|
991
|
+
return this.withRecovery(() => this.prep(sql).all(projectId));
|
|
822
992
|
}
|
|
823
993
|
/**
|
|
824
994
|
* Get memories by scope (project, user, global).
|
|
825
995
|
*/
|
|
826
996
|
getMemoriesByScope(scope) {
|
|
827
|
-
return this.
|
|
997
|
+
return this.withRecovery(() => this.prep("SELECT * FROM memories WHERE scope = ? AND tier = 'active' AND status = 'active'").all(scope));
|
|
828
998
|
}
|
|
829
999
|
// ─── Project Identity (v3.0) ──────────────────────────────────────
|
|
830
1000
|
insertProject(project) {
|
|
@@ -888,10 +1058,14 @@ export class GnosysDB {
|
|
|
888
1058
|
if (fields.length === 0)
|
|
889
1059
|
return;
|
|
890
1060
|
values.push(id);
|
|
891
|
-
this.
|
|
1061
|
+
this.withRecovery(() => {
|
|
1062
|
+
this.db.prepare(`UPDATE projects SET ${fields.join(", ")} WHERE id = ?`).run(...values);
|
|
1063
|
+
});
|
|
892
1064
|
}
|
|
893
1065
|
deleteProject(id) {
|
|
894
|
-
this.
|
|
1066
|
+
this.withRecovery(() => {
|
|
1067
|
+
this.db.prepare("DELETE FROM projects WHERE id = ?").run(id);
|
|
1068
|
+
});
|
|
895
1069
|
}
|
|
896
1070
|
/**
|
|
897
1071
|
* Generate the next sequential ID for a category.
|
|
@@ -908,7 +1082,6 @@ export class GnosysDB {
|
|
|
908
1082
|
* The `projectId` parameter is accepted for API compatibility but no longer
|
|
909
1083
|
* used for ID generation (ULIDs don't need project scoping for uniqueness).
|
|
910
1084
|
*/
|
|
911
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
912
1085
|
getNextId(category, projectId) {
|
|
913
1086
|
const prefix = category.substring(0, 4);
|
|
914
1087
|
return `${prefix}-${ulid()}`;
|
|
@@ -919,8 +1092,9 @@ export class GnosysDB {
|
|
|
919
1092
|
if (!safeQuery)
|
|
920
1093
|
return [];
|
|
921
1094
|
// v5.8.0 (#7): join memories so callers can render project-prefixed IDs.
|
|
922
|
-
|
|
923
|
-
|
|
1095
|
+
return this.withRecovery(() => {
|
|
1096
|
+
try {
|
|
1097
|
+
return this.prep(`
|
|
924
1098
|
SELECT m.id AS id, m.title AS title,
|
|
925
1099
|
snippet(memories_fts, 5, '>>>', '<<<', '...', 40) as snippet,
|
|
926
1100
|
fts.rank AS rank,
|
|
@@ -931,16 +1105,17 @@ export class GnosysDB {
|
|
|
931
1105
|
ORDER BY fts.rank
|
|
932
1106
|
LIMIT ?
|
|
933
1107
|
`).all(safeQuery, limit);
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1108
|
+
}
|
|
1109
|
+
catch {
|
|
1110
|
+
// FTS5 syntax error — fallback to LIKE
|
|
1111
|
+
const pattern = `%${safeQuery}%`;
|
|
1112
|
+
return this.prep(`
|
|
939
1113
|
SELECT id, title, substr(content, 1, 200) as snippet, 0 as rank, project_id
|
|
940
1114
|
FROM memories WHERE content LIKE ? OR title LIKE ? OR tags LIKE ?
|
|
941
1115
|
LIMIT ?
|
|
942
1116
|
`).all(pattern, pattern, pattern, limit);
|
|
943
|
-
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
944
1119
|
}
|
|
945
1120
|
discoverFts(query, limit = 20) {
|
|
946
1121
|
const safeQuery = query.replace(/['"]/g, "").trim();
|
|
@@ -955,51 +1130,61 @@ export class GnosysDB {
|
|
|
955
1130
|
ORDER BY fts.rank
|
|
956
1131
|
LIMIT ?
|
|
957
1132
|
`;
|
|
958
|
-
|
|
959
|
-
const colQuery = `{relevance title tags} : ${safeQuery}`;
|
|
960
|
-
const results = this.db.prepare(select).all(colQuery, limit);
|
|
961
|
-
if (results.length > 0)
|
|
962
|
-
return results;
|
|
963
|
-
return this.db.prepare(select).all(safeQuery, limit);
|
|
964
|
-
}
|
|
965
|
-
catch {
|
|
1133
|
+
return this.withRecovery(() => {
|
|
966
1134
|
try {
|
|
967
|
-
|
|
1135
|
+
const colQuery = `{relevance title tags} : ${safeQuery}`;
|
|
1136
|
+
const results = this.prep(select).all(colQuery, limit);
|
|
1137
|
+
if (results.length > 0)
|
|
1138
|
+
return results;
|
|
1139
|
+
return this.prep(select).all(safeQuery, limit);
|
|
968
1140
|
}
|
|
969
1141
|
catch {
|
|
970
|
-
|
|
1142
|
+
try {
|
|
1143
|
+
return this.prep(select).all(safeQuery, limit);
|
|
1144
|
+
}
|
|
1145
|
+
catch (err) {
|
|
1146
|
+
// Let corruption escape to withRecovery (reopen + retry); plain FTS
|
|
1147
|
+
// syntax failures still degrade gracefully to "no results".
|
|
1148
|
+
if (GnosysDB.isCorruptionError(err))
|
|
1149
|
+
throw err;
|
|
1150
|
+
return [];
|
|
1151
|
+
}
|
|
971
1152
|
}
|
|
972
|
-
}
|
|
1153
|
+
});
|
|
973
1154
|
}
|
|
974
1155
|
// ─── Relationships ──────────────────────────────────────────────────
|
|
975
1156
|
insertRelationship(rel) {
|
|
976
|
-
this.
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1157
|
+
this.withRecovery(() => {
|
|
1158
|
+
this.db.prepare(`
|
|
1159
|
+
INSERT OR IGNORE INTO relationships (source_id, target_id, rel_type, label, confidence, created)
|
|
1160
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1161
|
+
`).run(rel.source_id, rel.target_id, rel.rel_type, rel.label, rel.confidence, rel.created);
|
|
1162
|
+
});
|
|
980
1163
|
}
|
|
981
1164
|
getRelationshipsFrom(id) {
|
|
982
|
-
return this.db.prepare("SELECT * FROM relationships WHERE source_id = ?").all(id);
|
|
1165
|
+
return this.withRecovery(() => this.db.prepare("SELECT * FROM relationships WHERE source_id = ?").all(id));
|
|
983
1166
|
}
|
|
984
1167
|
getRelationshipsTo(id) {
|
|
985
|
-
return this.db.prepare("SELECT * FROM relationships WHERE target_id = ?").all(id);
|
|
1168
|
+
return this.withRecovery(() => this.db.prepare("SELECT * FROM relationships WHERE target_id = ?").all(id));
|
|
986
1169
|
}
|
|
987
1170
|
// ─── Summaries ──────────────────────────────────────────────────────
|
|
988
1171
|
upsertSummary(summary) {
|
|
989
|
-
this.
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1172
|
+
this.withRecovery(() => {
|
|
1173
|
+
this.db.prepare(`
|
|
1174
|
+
INSERT INTO summaries (id, scope, scope_key, content, source_ids, created, modified)
|
|
1175
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1176
|
+
ON CONFLICT(scope, scope_key) DO UPDATE SET
|
|
1177
|
+
content = excluded.content,
|
|
1178
|
+
source_ids = excluded.source_ids,
|
|
1179
|
+
modified = excluded.modified
|
|
1180
|
+
`).run(summary.id, summary.scope, summary.scope_key, summary.content, summary.source_ids, summary.created, summary.modified);
|
|
1181
|
+
});
|
|
997
1182
|
}
|
|
998
1183
|
getSummary(scope, scopeKey) {
|
|
999
|
-
return this.db.prepare("SELECT * FROM summaries WHERE scope = ? AND scope_key = ?").get(scope, scopeKey) || null;
|
|
1184
|
+
return this.withRecovery(() => this.db.prepare("SELECT * FROM summaries WHERE scope = ? AND scope_key = ?").get(scope, scopeKey) || null);
|
|
1000
1185
|
}
|
|
1001
1186
|
getAllSummaries() {
|
|
1002
|
-
return this.db.prepare("SELECT * FROM summaries").all();
|
|
1187
|
+
return this.withRecovery(() => this.db.prepare("SELECT * FROM summaries").all());
|
|
1003
1188
|
}
|
|
1004
1189
|
// ─── Audit ──────────────────────────────────────────────────────────
|
|
1005
1190
|
logAudit(entry) {
|
|
@@ -1051,21 +1236,26 @@ export class GnosysDB {
|
|
|
1051
1236
|
}
|
|
1052
1237
|
// ─── Embeddings ─────────────────────────────────────────────────────
|
|
1053
1238
|
updateEmbedding(id, embedding) {
|
|
1054
|
-
this.
|
|
1239
|
+
this.withRecovery(() => {
|
|
1240
|
+
this.db.prepare("UPDATE memories SET embedding = ? WHERE id = ?").run(embedding, id);
|
|
1241
|
+
});
|
|
1055
1242
|
}
|
|
1056
1243
|
getEmbedding(id) {
|
|
1057
|
-
|
|
1058
|
-
|
|
1244
|
+
return this.withRecovery(() => {
|
|
1245
|
+
const row = this.db.prepare("SELECT embedding FROM memories WHERE id = ?").get(id);
|
|
1246
|
+
return row?.embedding || null;
|
|
1247
|
+
});
|
|
1059
1248
|
}
|
|
1060
1249
|
getAllEmbeddings() {
|
|
1061
|
-
return this.db.prepare("SELECT id, embedding FROM memories WHERE embedding IS NOT NULL").all();
|
|
1250
|
+
return this.withRecovery(() => this.db.prepare("SELECT id, embedding FROM memories WHERE embedding IS NOT NULL").all());
|
|
1062
1251
|
}
|
|
1063
1252
|
getEmbeddingCount() {
|
|
1064
|
-
|
|
1065
|
-
|
|
1253
|
+
return this.withRecovery(() => {
|
|
1254
|
+
const row = this.db.prepare("SELECT COUNT(*) as cnt FROM memories WHERE embedding IS NOT NULL").get();
|
|
1255
|
+
return row.cnt;
|
|
1256
|
+
});
|
|
1066
1257
|
}
|
|
1067
1258
|
// ─── Transactions ───────────────────────────────────────────────────
|
|
1068
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1069
1259
|
transaction(fn) {
|
|
1070
1260
|
return this.db.transaction(fn)();
|
|
1071
1261
|
}
|
|
@@ -1233,8 +1423,83 @@ export class GnosysDB {
|
|
|
1233
1423
|
resolveConflict(memoryId) {
|
|
1234
1424
|
this.db.prepare("UPDATE sync_conflicts SET status = 'resolved' WHERE memory_id = ?").run(memoryId);
|
|
1235
1425
|
}
|
|
1426
|
+
// ─── v13 multi-machine sync (master + client local overlay) ───────────
|
|
1427
|
+
recordStagingLedgerEntry(entry) {
|
|
1428
|
+
if (entry.status === undefined) {
|
|
1429
|
+
this.db.prepare(`
|
|
1430
|
+
INSERT INTO sync_staging_ledger (staging_key, machine_id, memory_ulid, first_seen_at, ingest_epoch, status)
|
|
1431
|
+
VALUES (?, ?, ?, ?, ?, 'pending')
|
|
1432
|
+
ON CONFLICT(staging_key) DO UPDATE SET
|
|
1433
|
+
memory_ulid = COALESCE(excluded.memory_ulid, sync_staging_ledger.memory_ulid),
|
|
1434
|
+
ingest_epoch = COALESCE(excluded.ingest_epoch, sync_staging_ledger.ingest_epoch)
|
|
1435
|
+
`).run(entry.stagingKey, entry.machineId, entry.memoryUlid ?? null, entry.firstSeenAt, entry.ingestEpoch ?? null);
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
this.db.prepare(`
|
|
1439
|
+
INSERT INTO sync_staging_ledger (staging_key, machine_id, memory_ulid, first_seen_at, ingest_epoch, status)
|
|
1440
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1441
|
+
ON CONFLICT(staging_key) DO UPDATE SET
|
|
1442
|
+
memory_ulid = COALESCE(excluded.memory_ulid, sync_staging_ledger.memory_ulid),
|
|
1443
|
+
ingest_epoch = COALESCE(excluded.ingest_epoch, sync_staging_ledger.ingest_epoch),
|
|
1444
|
+
status = excluded.status
|
|
1445
|
+
`).run(entry.stagingKey, entry.machineId, entry.memoryUlid ?? null, entry.firstSeenAt, entry.ingestEpoch ?? null, entry.status);
|
|
1446
|
+
}
|
|
1447
|
+
isUlidProcessed(ulid) {
|
|
1448
|
+
const row = this.db.prepare("SELECT 1 FROM sync_processed_ulids WHERE ulid = ?").get(ulid);
|
|
1449
|
+
return Boolean(row);
|
|
1450
|
+
}
|
|
1451
|
+
markUlidProcessed(ulid, ingestEpoch) {
|
|
1452
|
+
this.db.prepare(`
|
|
1453
|
+
INSERT OR IGNORE INTO sync_processed_ulids (ulid, ingested_at, ingest_epoch)
|
|
1454
|
+
VALUES (?, ?, ?)
|
|
1455
|
+
`).run(ulid, new Date().toISOString(), ingestEpoch ?? null);
|
|
1456
|
+
}
|
|
1457
|
+
countPendingStagingLedger() {
|
|
1458
|
+
const row = this.db.prepare("SELECT COUNT(*) as cnt FROM sync_staging_ledger WHERE status = 'pending'").get();
|
|
1459
|
+
return row.cnt;
|
|
1460
|
+
}
|
|
1461
|
+
getStagingLedgerFirstSeenAt(stagingKey) {
|
|
1462
|
+
const row = this.db.prepare("SELECT first_seen_at FROM sync_staging_ledger WHERE staging_key = ?").get(stagingKey);
|
|
1463
|
+
return row?.first_seen_at ?? null;
|
|
1464
|
+
}
|
|
1465
|
+
insertPendingAdd(row) {
|
|
1466
|
+
this.db.prepare(`
|
|
1467
|
+
INSERT OR REPLACE INTO sync_pending_adds
|
|
1468
|
+
(id, title, category, content, tags, project_id, scope, created, cleared_at)
|
|
1469
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL)
|
|
1470
|
+
`).run(row.id, row.title, row.category, row.content, row.tags ?? "", row.project_id ?? null, row.scope ?? "project", row.created);
|
|
1471
|
+
}
|
|
1472
|
+
listActivePendingAdds() {
|
|
1473
|
+
return this.db.prepare("SELECT id, title, category, content, tags, project_id, scope, created FROM sync_pending_adds WHERE cleared_at IS NULL ORDER BY created ASC").all();
|
|
1474
|
+
}
|
|
1475
|
+
clearPendingAdd(id) {
|
|
1476
|
+
this.db.prepare("UPDATE sync_pending_adds SET cleared_at = ? WHERE id = ? AND cleared_at IS NULL").run(new Date().toISOString(), id);
|
|
1477
|
+
}
|
|
1478
|
+
getSnapshotManifest() {
|
|
1479
|
+
const row = this.db.prepare("SELECT epoch, seq, snapshot_path, published_at, checksum, size_bytes, heartbeat_at FROM sync_snapshot_manifest WHERE singleton_id = 1").get();
|
|
1480
|
+
return row ?? null;
|
|
1481
|
+
}
|
|
1482
|
+
publishSnapshotManifest(manifest) {
|
|
1483
|
+
this.db.prepare(`
|
|
1484
|
+
INSERT INTO sync_snapshot_manifest
|
|
1485
|
+
(singleton_id, epoch, seq, snapshot_path, published_at, checksum, size_bytes, heartbeat_at)
|
|
1486
|
+
VALUES (1, ?, ?, ?, ?, ?, ?, ?)
|
|
1487
|
+
ON CONFLICT(singleton_id) DO UPDATE SET
|
|
1488
|
+
epoch = excluded.epoch,
|
|
1489
|
+
seq = excluded.seq,
|
|
1490
|
+
snapshot_path = excluded.snapshot_path,
|
|
1491
|
+
published_at = excluded.published_at,
|
|
1492
|
+
checksum = excluded.checksum,
|
|
1493
|
+
size_bytes = excluded.size_bytes,
|
|
1494
|
+
heartbeat_at = excluded.heartbeat_at
|
|
1495
|
+
`).run(manifest.epoch, manifest.seq, manifest.snapshotPath, manifest.publishedAt, manifest.checksum ?? null, manifest.sizeBytes ?? null, manifest.heartbeatAt ?? null);
|
|
1496
|
+
}
|
|
1497
|
+
touchSnapshotHeartbeat(at) {
|
|
1498
|
+
this.db.prepare("UPDATE sync_snapshot_manifest SET heartbeat_at = ? WHERE singleton_id = 1").run(at);
|
|
1499
|
+
}
|
|
1236
1500
|
// ─── Lifecycle ──────────────────────────────────────────────────────
|
|
1237
1501
|
close() {
|
|
1502
|
+
this.stmtCache.clear();
|
|
1238
1503
|
this.db?.close();
|
|
1239
1504
|
}
|
|
1240
1505
|
// ─── Migration Status ───────────────────────────────────────────────
|