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.
Files changed (265) hide show
  1. package/dist/cli.js +377 -5162
  2. package/dist/index.js +542 -244
  3. package/dist/lib/addCommand.d.ts +9 -0
  4. package/dist/lib/addCommand.js +102 -0
  5. package/dist/lib/addStructuredCommand.d.ts +16 -0
  6. package/dist/lib/addStructuredCommand.js +103 -0
  7. package/dist/lib/ambiguityCommand.d.ts +4 -0
  8. package/dist/lib/ambiguityCommand.js +36 -0
  9. package/dist/lib/apiKeyVault.d.ts +78 -0
  10. package/dist/lib/apiKeyVault.js +447 -0
  11. package/dist/lib/archive.js +0 -2
  12. package/dist/lib/askCommand.d.ts +13 -0
  13. package/dist/lib/askCommand.js +145 -0
  14. package/dist/lib/attachCommand.d.ts +17 -0
  15. package/dist/lib/attachCommand.js +66 -0
  16. package/dist/lib/attachments.d.ts +43 -2
  17. package/dist/lib/attachments.js +81 -2
  18. package/dist/lib/audioExtract.js +4 -1
  19. package/dist/lib/auditCommand.d.ts +7 -0
  20. package/dist/lib/auditCommand.js +27 -0
  21. package/dist/lib/backupCommand.d.ts +6 -0
  22. package/dist/lib/backupCommand.js +54 -0
  23. package/dist/lib/bootstrapCommand.d.ts +15 -0
  24. package/dist/lib/bootstrapCommand.js +51 -0
  25. package/dist/lib/briefingCommand.d.ts +7 -0
  26. package/dist/lib/briefingCommand.js +92 -0
  27. package/dist/lib/centralizeCommand.d.ts +5 -0
  28. package/dist/lib/centralizeCommand.js +16 -0
  29. package/dist/lib/chat/choose.js +2 -2
  30. package/dist/lib/chatCommand.d.ts +12 -0
  31. package/dist/lib/chatCommand.js +46 -0
  32. package/dist/lib/checkCommand.d.ts +4 -0
  33. package/dist/lib/checkCommand.js +133 -0
  34. package/dist/lib/clientReadOverlay.d.ts +27 -0
  35. package/dist/lib/clientReadOverlay.js +76 -0
  36. package/dist/lib/clientReadResolve.d.ts +32 -0
  37. package/dist/lib/clientReadResolve.js +84 -0
  38. package/dist/lib/commitContextCommand.d.ts +9 -0
  39. package/dist/lib/commitContextCommand.js +142 -0
  40. package/dist/lib/config.d.ts +41 -48
  41. package/dist/lib/config.js +58 -57
  42. package/dist/lib/configCommand.d.ts +10 -0
  43. package/dist/lib/configCommand.js +321 -0
  44. package/dist/lib/connectCommand.d.ts +8 -0
  45. package/dist/lib/connectCommand.js +19 -0
  46. package/dist/lib/db.d.ts +68 -1
  47. package/dist/lib/db.js +385 -120
  48. package/dist/lib/dbWrite.d.ts +1 -1
  49. package/dist/lib/dearchiveCommand.d.ts +7 -0
  50. package/dist/lib/dearchiveCommand.js +41 -0
  51. package/dist/lib/discoverCommand.d.ts +9 -0
  52. package/dist/lib/discoverCommand.js +87 -0
  53. package/dist/lib/doctorCommand.d.ts +6 -0
  54. package/dist/lib/doctorCommand.js +256 -0
  55. package/dist/lib/docxExtract.js +1 -1
  56. package/dist/lib/dream.d.ts +50 -2
  57. package/dist/lib/dream.js +324 -30
  58. package/dist/lib/dreamCommand.d.ts +10 -0
  59. package/dist/lib/dreamCommand.js +195 -0
  60. package/dist/lib/dreamLaunchd.d.ts +2 -0
  61. package/dist/lib/dreamLaunchd.js +72 -0
  62. package/dist/lib/dreamLogCommand.d.ts +10 -0
  63. package/dist/lib/dreamLogCommand.js +58 -0
  64. package/dist/lib/dreamReport.d.ts +7 -0
  65. package/dist/lib/dreamReport.js +114 -0
  66. package/dist/lib/dreamRunLog.d.ts +121 -0
  67. package/dist/lib/dreamRunLog.js +234 -0
  68. package/dist/lib/embeddings.js +3 -3
  69. package/dist/lib/exportCommand.d.ts +18 -0
  70. package/dist/lib/exportCommand.js +101 -0
  71. package/dist/lib/exportProject.d.ts +3 -2
  72. package/dist/lib/exportProject.js +2 -1
  73. package/dist/lib/federated.js +1 -1
  74. package/dist/lib/fsearchCommand.d.ts +8 -0
  75. package/dist/lib/fsearchCommand.js +44 -0
  76. package/dist/lib/graphCommand.d.ts +4 -0
  77. package/dist/lib/graphCommand.js +68 -0
  78. package/dist/lib/helperGenerateCommand.d.ts +5 -0
  79. package/dist/lib/helperGenerateCommand.js +27 -0
  80. package/dist/lib/historyCommand.d.ts +5 -0
  81. package/dist/lib/historyCommand.js +51 -0
  82. package/dist/lib/hybridSearchCommand.d.ts +12 -0
  83. package/dist/lib/hybridSearchCommand.js +95 -0
  84. package/dist/lib/importCommand.d.ts +16 -0
  85. package/dist/lib/importCommand.js +89 -0
  86. package/dist/lib/importProject.js +2 -1
  87. package/dist/lib/importProjectCommand.d.ts +6 -0
  88. package/dist/lib/importProjectCommand.js +43 -0
  89. package/dist/lib/ingestCommand.d.ts +13 -0
  90. package/dist/lib/ingestCommand.js +95 -0
  91. package/dist/lib/installOutput.d.ts +36 -0
  92. package/dist/lib/installOutput.js +55 -0
  93. package/dist/lib/lensCommand.d.ts +20 -0
  94. package/dist/lib/lensCommand.js +61 -0
  95. package/dist/lib/lensing.d.ts +1 -0
  96. package/dist/lib/lensing.js +50 -9
  97. package/dist/lib/linksCommand.d.ts +7 -0
  98. package/dist/lib/linksCommand.js +48 -0
  99. package/dist/lib/listCommand.d.ts +8 -0
  100. package/dist/lib/listCommand.js +74 -0
  101. package/dist/lib/llm.d.ts +1 -1
  102. package/dist/lib/llm.js +27 -9
  103. package/dist/lib/localDiskCheck.d.ts +17 -0
  104. package/dist/lib/localDiskCheck.js +54 -0
  105. package/dist/lib/lock.d.ts +1 -1
  106. package/dist/lib/lock.js +5 -3
  107. package/dist/lib/machineConfig.d.ts +11 -1
  108. package/dist/lib/machineConfig.js +16 -0
  109. package/dist/lib/machineRegistry.d.ts +61 -0
  110. package/dist/lib/machineRegistry.js +80 -0
  111. package/dist/lib/maintainCommand.d.ts +8 -0
  112. package/dist/lib/maintainCommand.js +34 -0
  113. package/dist/lib/masterLease.d.ts +20 -0
  114. package/dist/lib/masterLease.js +68 -0
  115. package/dist/lib/migrate.js +0 -1
  116. package/dist/lib/migrateCommand.d.ts +7 -0
  117. package/dist/lib/migrateCommand.js +158 -0
  118. package/dist/lib/migrateDbCommand.d.ts +9 -0
  119. package/dist/lib/migrateDbCommand.js +94 -0
  120. package/dist/lib/modelValidation.d.ts +5 -0
  121. package/dist/lib/modelValidation.js +27 -0
  122. package/dist/lib/multimodalIngest.js +1 -1
  123. package/dist/lib/openrouterTiers.d.ts +29 -0
  124. package/dist/lib/openrouterTiers.js +113 -0
  125. package/dist/lib/platform.d.ts +0 -6
  126. package/dist/lib/platform.js +0 -28
  127. package/dist/lib/prefCommand.d.ts +10 -0
  128. package/dist/lib/prefCommand.js +118 -0
  129. package/dist/lib/projectsCommand.d.ts +8 -0
  130. package/dist/lib/projectsCommand.js +131 -0
  131. package/dist/lib/readCommand.d.ts +7 -0
  132. package/dist/lib/readCommand.js +63 -0
  133. package/dist/lib/recall.d.ts +3 -0
  134. package/dist/lib/recall.js +19 -4
  135. package/dist/lib/recallCommand.d.ts +11 -0
  136. package/dist/lib/recallCommand.js +112 -0
  137. package/dist/lib/reflectCommand.d.ts +8 -0
  138. package/dist/lib/reflectCommand.js +61 -0
  139. package/dist/lib/reindexCommand.d.ts +4 -0
  140. package/dist/lib/reindexCommand.js +34 -0
  141. package/dist/lib/reindexGraphCommand.d.ts +4 -0
  142. package/dist/lib/reindexGraphCommand.js +12 -0
  143. package/dist/lib/reinforceCommand.d.ts +8 -0
  144. package/dist/lib/reinforceCommand.js +40 -0
  145. package/dist/lib/remote.d.ts +5 -1
  146. package/dist/lib/remote.js +5 -1
  147. package/dist/lib/remoteWizard.d.ts +24 -5
  148. package/dist/lib/remoteWizard.js +308 -319
  149. package/dist/lib/restoreCommand.d.ts +5 -0
  150. package/dist/lib/restoreCommand.js +35 -0
  151. package/dist/lib/rulesGen.d.ts +8 -0
  152. package/dist/lib/rulesGen.js +16 -0
  153. package/dist/lib/sandboxStartCommand.d.ts +6 -0
  154. package/dist/lib/sandboxStartCommand.js +25 -0
  155. package/dist/lib/sandboxStatusCommand.d.ts +4 -0
  156. package/dist/lib/sandboxStatusCommand.js +24 -0
  157. package/dist/lib/sandboxStopCommand.d.ts +4 -0
  158. package/dist/lib/sandboxStopCommand.js +21 -0
  159. package/dist/lib/search.d.ts +0 -2
  160. package/dist/lib/search.js +0 -7
  161. package/dist/lib/searchCommand.d.ts +9 -0
  162. package/dist/lib/searchCommand.js +90 -0
  163. package/dist/lib/semanticSearchCommand.d.ts +8 -0
  164. package/dist/lib/semanticSearchCommand.js +52 -0
  165. package/dist/lib/setup/configSetRender.js +2 -0
  166. package/dist/lib/setup/providerGlyphs.d.ts +19 -0
  167. package/dist/lib/setup/providerGlyphs.js +42 -0
  168. package/dist/lib/setup/remoteRender.d.ts +31 -1
  169. package/dist/lib/setup/remoteRender.js +95 -4
  170. package/dist/lib/setup/sections/providers.d.ts +17 -0
  171. package/dist/lib/setup/sections/providers.js +307 -0
  172. package/dist/lib/setup/sections/routing.d.ts +2 -6
  173. package/dist/lib/setup/sections/routing.js +67 -82
  174. package/dist/lib/setup/sections/taskRoutingEditor.d.ts +13 -0
  175. package/dist/lib/setup/sections/taskRoutingEditor.js +139 -0
  176. package/dist/lib/setup/summary.d.ts +9 -0
  177. package/dist/lib/setup/summary.js +51 -37
  178. package/dist/lib/setup/ui/header.js +0 -1
  179. package/dist/lib/setup.d.ts +105 -15
  180. package/dist/lib/setup.js +747 -287
  181. package/dist/lib/setupKeys.d.ts +42 -0
  182. package/dist/lib/setupKeys.js +564 -0
  183. package/dist/lib/setupRemoteCommand.d.ts +4 -0
  184. package/dist/lib/setupRemoteCommand.js +28 -0
  185. package/dist/lib/setupRemotePullCommand.d.ts +5 -0
  186. package/dist/lib/setupRemotePullCommand.js +52 -0
  187. package/dist/lib/setupRemotePushCommand.d.ts +5 -0
  188. package/dist/lib/setupRemotePushCommand.js +57 -0
  189. package/dist/lib/setupRemoteResolveCommand.d.ts +4 -0
  190. package/dist/lib/setupRemoteResolveCommand.js +48 -0
  191. package/dist/lib/setupRemoteStatusCommand.d.ts +4 -0
  192. package/dist/lib/setupRemoteStatusCommand.js +73 -0
  193. package/dist/lib/setupRemoteSyncCommand.d.ts +6 -0
  194. package/dist/lib/setupRemoteSyncCommand.js +65 -0
  195. package/dist/lib/setupSyncProjectsCommand.d.ts +4 -0
  196. package/dist/lib/setupSyncProjectsCommand.js +292 -0
  197. package/dist/lib/staleCommand.d.ts +8 -0
  198. package/dist/lib/staleCommand.js +34 -0
  199. package/dist/lib/statsCommand.d.ts +6 -0
  200. package/dist/lib/statsCommand.js +142 -0
  201. package/dist/lib/statusCommand.d.ts +18 -0
  202. package/dist/lib/statusCommand.js +250 -0
  203. package/dist/lib/storesCommand.d.ts +2 -0
  204. package/dist/lib/storesCommand.js +4 -0
  205. package/dist/lib/syncClient.d.ts +41 -0
  206. package/dist/lib/syncClient.js +234 -0
  207. package/dist/lib/syncCommand.d.ts +6 -0
  208. package/dist/lib/syncCommand.js +57 -0
  209. package/dist/lib/syncDoctorCommand.d.ts +5 -0
  210. package/dist/lib/syncDoctorCommand.js +100 -0
  211. package/dist/lib/syncIngest.d.ts +30 -0
  212. package/dist/lib/syncIngest.js +175 -0
  213. package/dist/lib/syncIngestLaunchd.d.ts +8 -0
  214. package/dist/lib/syncIngestLaunchd.js +93 -0
  215. package/dist/lib/syncIngestStartup.d.ts +5 -0
  216. package/dist/lib/syncIngestStartup.js +29 -0
  217. package/dist/lib/syncIngestSystemd.d.ts +10 -0
  218. package/dist/lib/syncIngestSystemd.js +97 -0
  219. package/dist/lib/syncIngestTimer.d.ts +8 -0
  220. package/dist/lib/syncIngestTimer.js +27 -0
  221. package/dist/lib/syncIngestTimerCommand.d.ts +7 -0
  222. package/dist/lib/syncIngestTimerCommand.js +83 -0
  223. package/dist/lib/syncLock.d.ts +6 -0
  224. package/dist/lib/syncLock.js +74 -0
  225. package/dist/lib/syncSnapshot.d.ts +32 -0
  226. package/dist/lib/syncSnapshot.js +188 -0
  227. package/dist/lib/syncStaging.d.ts +79 -0
  228. package/dist/lib/syncStaging.js +237 -0
  229. package/dist/lib/tagsAddCommand.d.ts +8 -0
  230. package/dist/lib/tagsAddCommand.js +18 -0
  231. package/dist/lib/tagsCommand.d.ts +4 -0
  232. package/dist/lib/tagsCommand.js +16 -0
  233. package/dist/lib/timelineCommand.d.ts +7 -0
  234. package/dist/lib/timelineCommand.js +49 -0
  235. package/dist/lib/traceCommand.d.ts +6 -0
  236. package/dist/lib/traceCommand.js +39 -0
  237. package/dist/lib/traverseCommand.d.ts +6 -0
  238. package/dist/lib/traverseCommand.js +58 -0
  239. package/dist/lib/updateCommand.d.ts +13 -0
  240. package/dist/lib/updateCommand.js +67 -0
  241. package/dist/lib/updateStatusCommand.d.ts +5 -0
  242. package/dist/lib/updateStatusCommand.js +38 -0
  243. package/dist/lib/webAddCommand.d.ts +8 -0
  244. package/dist/lib/webAddCommand.js +55 -0
  245. package/dist/lib/webBuildCommand.d.ts +10 -0
  246. package/dist/lib/webBuildCommand.js +65 -0
  247. package/dist/lib/webBuildIndexCommand.d.ts +8 -0
  248. package/dist/lib/webBuildIndexCommand.js +37 -0
  249. package/dist/lib/webIndex.js +0 -1
  250. package/dist/lib/webIngestCommand.d.ts +11 -0
  251. package/dist/lib/webIngestCommand.js +51 -0
  252. package/dist/lib/webInitCommand.d.ts +9 -0
  253. package/dist/lib/webInitCommand.js +167 -0
  254. package/dist/lib/webRemoveCommand.d.ts +5 -0
  255. package/dist/lib/webRemoveCommand.js +41 -0
  256. package/dist/lib/webStatusCommand.d.ts +5 -0
  257. package/dist/lib/webStatusCommand.js +94 -0
  258. package/dist/lib/webUpdateCommand.d.ts +7 -0
  259. package/dist/lib/webUpdateCommand.js +72 -0
  260. package/dist/lib/workingSetCommand.d.ts +6 -0
  261. package/dist/lib/workingSetCommand.js +37 -0
  262. package/dist/sandbox/client.js +1 -1
  263. package/dist/sandbox/manager.js +1 -14
  264. package/dist/sandbox/server.js +3 -5
  265. 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 = 4;
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
- enableWAL(this.db);
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 (err) {
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
- enableWAL(this.db);
505
+ // Longer busy timeout for network shares (10s)
506
+ enableWAL(this.db, 10000);
442
507
  this.db.pragma("foreign_keys = ON");
443
- this.db.pragma("busy_timeout = 10000");
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
- const errAny = err;
472
- const isCorrupt = errAny?.code === "SQLITE_CORRUPT" ||
473
- /database disk image is malformed/i.test(errAny?.message ?? "");
474
- if (!isCorrupt)
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.db.prepare(`
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.db.prepare("SELECT * FROM memories WHERE id = ?").get(id) || null);
831
+ return this.withRecovery(() => this.prep("SELECT * FROM memories WHERE id = ?").get(id) || null);
672
832
  }
673
833
  getActiveMemories() {
674
- return this.withRecovery(() => this.db.prepare("SELECT * FROM memories WHERE tier = 'active' AND status = 'active'").all());
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.db.prepare("SELECT * FROM memories WHERE category = ? AND tier = 'active'").all(category);
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
- try {
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.exec(`
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
- catch {
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("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);
922
+ this.db.prepare("DELETE FROM memories_fts WHERE id = ?").run(id);
768
923
  }
769
924
  catch {
770
- // FTS insert may failnot critical
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
- // FTS5 delete trigger may fail if INSERT OR REPLACE left FTS inconsistent.
776
- try {
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.exec(`
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
- // Trigger recreation failednot critical
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
- const active = this.db.prepare("SELECT COUNT(*) as cnt FROM memories WHERE tier = 'active'").get().cnt;
806
- const archived = this.db.prepare("SELECT COUNT(*) as cnt FROM memories WHERE tier = 'archive'").get().cnt;
807
- return { active, archived, total: active + archived };
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
- const rows = this.db.prepare("SELECT DISTINCT category FROM memories WHERE tier = 'active' ORDER BY category").all();
811
- return rows.map((r) => r.category);
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.db.prepare(sql).all(projectId);
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.db.prepare("SELECT * FROM memories WHERE scope = ? AND tier = 'active' AND status = 'active'").all(scope);
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.db.prepare(`UPDATE projects SET ${fields.join(", ")} WHERE id = ?`).run(...values);
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.db.prepare("DELETE FROM projects WHERE id = ?").run(id);
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
- try {
923
- return this.db.prepare(`
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
- catch {
936
- // FTS5 syntax error — fallback to LIKE
937
- const pattern = `%${safeQuery}%`;
938
- return this.db.prepare(`
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
- try {
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
- return this.db.prepare(select).all(safeQuery, limit);
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
- return [];
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.db.prepare(`
977
- INSERT OR IGNORE INTO relationships (source_id, target_id, rel_type, label, confidence, created)
978
- VALUES (?, ?, ?, ?, ?, ?)
979
- `).run(rel.source_id, rel.target_id, rel.rel_type, rel.label, rel.confidence, rel.created);
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.db.prepare(`
990
- INSERT INTO summaries (id, scope, scope_key, content, source_ids, created, modified)
991
- VALUES (?, ?, ?, ?, ?, ?, ?)
992
- ON CONFLICT(scope, scope_key) DO UPDATE SET
993
- content = excluded.content,
994
- source_ids = excluded.source_ids,
995
- modified = excluded.modified
996
- `).run(summary.id, summary.scope, summary.scope_key, summary.content, summary.source_ids, summary.created, summary.modified);
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.db.prepare("UPDATE memories SET embedding = ? WHERE id = ?").run(embedding, id);
1239
+ this.withRecovery(() => {
1240
+ this.db.prepare("UPDATE memories SET embedding = ? WHERE id = ?").run(embedding, id);
1241
+ });
1055
1242
  }
1056
1243
  getEmbedding(id) {
1057
- const row = this.db.prepare("SELECT embedding FROM memories WHERE id = ?").get(id);
1058
- return row?.embedding || null;
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
- const row = this.db.prepare("SELECT COUNT(*) as cnt FROM memories WHERE embedding IS NOT NULL").get();
1065
- return row.cnt;
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 ───────────────────────────────────────────────