gnosys 5.12.0 → 5.12.3
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 +48 -7
- package/dist/index.js +199 -14
- package/dist/lib/addCommand.js +0 -1
- package/dist/lib/archive.js +10 -6
- package/dist/lib/askCommand.js +1 -1
- 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/chat/choose.js +2 -2
- package/dist/lib/clientReadOverlay.js +3 -0
- package/dist/lib/config.d.ts +1 -48
- package/dist/lib/configCommand.js +2 -2
- package/dist/lib/db.d.ts +16 -1
- package/dist/lib/db.js +248 -133
- package/dist/lib/dbWrite.d.ts +1 -1
- package/dist/lib/dearchiveCommand.js +1 -1
- package/dist/lib/docxExtract.js +1 -1
- package/dist/lib/dream.d.ts +8 -0
- package/dist/lib/dream.js +35 -1
- package/dist/lib/dreamLogCommand.js +1 -1
- package/dist/lib/dreamRunLog.d.ts +1 -1
- package/dist/lib/dreamRunLog.js +26 -4
- package/dist/lib/embeddings.js +0 -3
- 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/ftsQuery.d.ts +25 -0
- package/dist/lib/ftsQuery.js +42 -0
- package/dist/lib/hybridSearch.d.ts +9 -0
- package/dist/lib/hybridSearch.js +14 -0
- package/dist/lib/hybridSearchCommand.js +6 -1
- package/dist/lib/importProject.js +2 -1
- package/dist/lib/llm.js +1 -1
- package/dist/lib/lock.d.ts +1 -1
- package/dist/lib/lock.js +5 -3
- package/dist/lib/migrate.js +0 -1
- package/dist/lib/multimodalIngest.js +1 -1
- package/dist/lib/platform.d.ts +0 -6
- package/dist/lib/platform.js +0 -28
- package/dist/lib/readCommand.js +11 -10
- package/dist/lib/remoteWizard.d.ts +1 -1
- package/dist/lib/remoteWizard.js +4 -4
- package/dist/lib/resolver.d.ts +17 -0
- package/dist/lib/resolver.js +43 -17
- package/dist/lib/rulesGen.d.ts +8 -0
- package/dist/lib/rulesGen.js +16 -0
- package/dist/lib/search.d.ts +0 -2
- package/dist/lib/search.js +33 -27
- package/dist/lib/semanticSearchCommand.js +1 -1
- package/dist/lib/setup/sections/providers.js +56 -4
- package/dist/lib/setup/sections/routing.js +42 -5
- package/dist/lib/setup/sections/taskRoutingEditor.d.ts +1 -5
- package/dist/lib/setup/sections/taskRoutingEditor.js +0 -10
- package/dist/lib/setup/ui/header.js +0 -1
- package/dist/lib/setup/ui/status.d.ts +0 -1
- package/dist/lib/setup/ui/status.js +0 -2
- package/dist/lib/setup.d.ts +0 -15
- package/dist/lib/setup.js +13 -158
- package/dist/lib/staleCommand.js +2 -2
- package/dist/lib/syncClient.d.ts +0 -6
- package/dist/lib/syncClient.js +36 -14
- package/dist/lib/syncDoctorCommand.js +2 -2
- package/dist/lib/syncIngest.d.ts +11 -0
- package/dist/lib/syncIngest.js +24 -1
- package/dist/lib/syncIngestStartup.js +2 -2
- package/dist/lib/syncSnapshot.d.ts +2 -0
- package/dist/lib/syncSnapshot.js +4 -0
- package/dist/lib/syncStaging.d.ts +0 -2
- package/dist/lib/syncStaging.js +0 -2
- package/dist/lib/updateCommand.js +1 -1
- package/dist/lib/webBuildCommand.js +1 -1
- package/dist/lib/webIndex.js +0 -1
- package/dist/lib/webIngestCommand.js +1 -1
- package/dist/sandbox/client.js +1 -1
- package/dist/sandbox/manager.js +1 -14
- package/dist/sandbox/server.js +3 -5
- package/package.json +5 -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;
|
|
@@ -22,6 +21,7 @@ import { enableWAL } from "./lock.js";
|
|
|
22
21
|
import { getGnosysHome as getGnosysHomeImpl, getCentralDbPath as getCentralDbPathImpl } from "./paths.js";
|
|
23
22
|
import { readMachineConfig } from "./machineConfig.js";
|
|
24
23
|
import { logError } from "./log.js";
|
|
24
|
+
import { ftsTerms, ftsAndQuery, ftsOrQuery } from "./ftsQuery.js";
|
|
25
25
|
import { ulid } from "ulidx";
|
|
26
26
|
// ─── Schema ─────────────────────────────────────────────────────────────
|
|
27
27
|
const SCHEMA_VERSION = 5;
|
|
@@ -51,6 +51,9 @@ CREATE TABLE IF NOT EXISTS memories (
|
|
|
51
51
|
source_file TEXT,
|
|
52
52
|
source_page TEXT,
|
|
53
53
|
source_timerange TEXT,
|
|
54
|
+
attachment_data BLOB,
|
|
55
|
+
attachment_mime TEXT,
|
|
56
|
+
attachment_name TEXT,
|
|
54
57
|
project_id TEXT,
|
|
55
58
|
scope TEXT DEFAULT 'project' CHECK(scope IN ('project','user','global'))
|
|
56
59
|
);
|
|
@@ -243,12 +246,27 @@ const MEMORY_COLUMNS = new Set([
|
|
|
243
246
|
"status", "tier", "supersedes", "superseded_by", "last_reinforced",
|
|
244
247
|
"created", "modified", "embedding", "source_path",
|
|
245
248
|
"source_file", "source_page", "source_timerange",
|
|
249
|
+
"attachment_data", "attachment_mime", "attachment_name",
|
|
246
250
|
"project_id", "scope",
|
|
247
251
|
]);
|
|
248
252
|
const PROJECT_COLUMNS = new Set([
|
|
249
253
|
"name", "working_directory", "root_id", "rel_path", "user",
|
|
250
254
|
"agent_rules_target", "obsidian_vault", "created", "modified",
|
|
251
255
|
]);
|
|
256
|
+
/**
|
|
257
|
+
* v5.12.x perf: full DbMemory shape with the two BLOB columns projected as
|
|
258
|
+
* NULL. List-style reads (recall, federation, list) never consume embedding
|
|
259
|
+
* or attachment bytes. Measured win on embedding-only rows is modest (~4%
|
|
260
|
+
* plus avoided Buffer churn), but attachments are the real reason: a single
|
|
261
|
+
* ~10MB gnosys_attach blob would otherwise be hydrated on EVERY list call.
|
|
262
|
+
* Blob consumers use getAllEmbeddings/getEmbedding/getMemoryAttachment, and
|
|
263
|
+
* getAllMemories/getMemoriesByProject still return full rows (remote sync
|
|
264
|
+
* and project export push them verbatim).
|
|
265
|
+
*/
|
|
266
|
+
const LEAN_MEMORY_PROJECTION = [
|
|
267
|
+
"id", ...[...MEMORY_COLUMNS].filter((c) => c !== "embedding" && c !== "attachment_data"),
|
|
268
|
+
"NULL AS embedding", "NULL AS attachment_data",
|
|
269
|
+
].join(", ");
|
|
252
270
|
// ─── FNV-1a hash (same as embeddings.ts) ────────────────────────────────
|
|
253
271
|
function fnv1a(str) {
|
|
254
272
|
let hash = 0x811c9dc5;
|
|
@@ -260,8 +278,10 @@ function fnv1a(str) {
|
|
|
260
278
|
}
|
|
261
279
|
// ─── GnosysDB Class ─────────────────────────────────────────────────────
|
|
262
280
|
export class GnosysDB {
|
|
263
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
264
281
|
db = null;
|
|
282
|
+
/** v5.12.x perf: prepared-statement cache, keyed by SQL. Invalidated on
|
|
283
|
+
* reopen()/close() — statements are bound to their connection handle. */
|
|
284
|
+
stmtCache = new Map();
|
|
265
285
|
storePath;
|
|
266
286
|
available = false;
|
|
267
287
|
dbFilePath;
|
|
@@ -381,7 +401,8 @@ export class GnosysDB {
|
|
|
381
401
|
try {
|
|
382
402
|
fs.mkdirSync(storePath, { recursive: true, mode: 0o700 });
|
|
383
403
|
this.db = new Database(this.dbFilePath);
|
|
384
|
-
|
|
404
|
+
// Longer busy timeout for network shares (10s)
|
|
405
|
+
enableWAL(this.db, 10000);
|
|
385
406
|
try {
|
|
386
407
|
fs.chmodSync(storePath, 0o700);
|
|
387
408
|
fs.chmodSync(this.dbFilePath, 0o600);
|
|
@@ -396,13 +417,11 @@ export class GnosysDB {
|
|
|
396
417
|
// best-effort (Windows / network FS)
|
|
397
418
|
}
|
|
398
419
|
this.db.pragma("foreign_keys = ON");
|
|
399
|
-
// Longer busy timeout for network shares (10s)
|
|
400
|
-
this.db.pragma("busy_timeout = 10000");
|
|
401
420
|
this.applySchema();
|
|
402
421
|
this.available = true;
|
|
403
422
|
return; // Success
|
|
404
423
|
}
|
|
405
|
-
catch (
|
|
424
|
+
catch (_err) {
|
|
406
425
|
this.db = null;
|
|
407
426
|
if (attempt < maxRetries) {
|
|
408
427
|
// Synchronous delay for constructor (network share retry)
|
|
@@ -471,6 +490,7 @@ export class GnosysDB {
|
|
|
471
490
|
* file handles after a WAL checkpoint or remount.
|
|
472
491
|
*/
|
|
473
492
|
reopen() {
|
|
493
|
+
this.stmtCache.clear();
|
|
474
494
|
try {
|
|
475
495
|
this.db?.close();
|
|
476
496
|
}
|
|
@@ -483,9 +503,19 @@ export class GnosysDB {
|
|
|
483
503
|
return;
|
|
484
504
|
try {
|
|
485
505
|
this.db = new Database(this.dbFilePath);
|
|
486
|
-
|
|
506
|
+
// Longer busy timeout for network shares (10s)
|
|
507
|
+
enableWAL(this.db, 10000);
|
|
487
508
|
this.db.pragma("foreign_keys = ON");
|
|
488
|
-
|
|
509
|
+
// v5.12.1: heal FTS triggers on recovery. updateMemory/deleteMemory may
|
|
510
|
+
// drop a trigger in their inconsistency fallback; recreating here
|
|
511
|
+
// (idempotent CREATE TRIGGER IF NOT EXISTS) means recovery restores
|
|
512
|
+
// them instead of waiting for the next process start.
|
|
513
|
+
try {
|
|
514
|
+
this.db.exec(FTS_TRIGGERS_SQL);
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
// non-fatal — applySchema() heals at next open
|
|
518
|
+
}
|
|
489
519
|
this.available = true;
|
|
490
520
|
}
|
|
491
521
|
catch {
|
|
@@ -508,15 +538,28 @@ export class GnosysDB {
|
|
|
508
538
|
* Read methods are also wrapped because reads against stale pages can
|
|
509
539
|
* surface the same error.
|
|
510
540
|
*/
|
|
541
|
+
/**
|
|
542
|
+
* Prepare-with-cache for fixed-SQL hot paths. Dynamic SQL (e.g. the
|
|
543
|
+
* field-built UPDATE in updateMemory) must keep using db.prepare directly
|
|
544
|
+
* so the cache stays bounded. Cache is invalidated on reopen()/close().
|
|
545
|
+
*/
|
|
546
|
+
prep(sql) {
|
|
547
|
+
let stmt = this.stmtCache.get(sql);
|
|
548
|
+
if (!stmt) {
|
|
549
|
+
stmt = this.db.prepare(sql);
|
|
550
|
+
this.stmtCache.set(sql, stmt);
|
|
551
|
+
}
|
|
552
|
+
return stmt;
|
|
553
|
+
}
|
|
511
554
|
withRecovery(fn) {
|
|
512
555
|
try {
|
|
513
556
|
return fn();
|
|
514
557
|
}
|
|
515
558
|
catch (err) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
if (!
|
|
559
|
+
// v5.12.1: use the shared corruption detector — it also matches
|
|
560
|
+
// SQLITE_NOTADB ("file is not a database"), which surfaces on network
|
|
561
|
+
// shares when a sync layer swaps the file under a live handle.
|
|
562
|
+
if (!GnosysDB.isCorruptionError(err))
|
|
520
563
|
throw err;
|
|
521
564
|
// One-shot recovery: reopen and retry. If the reopen itself fails or
|
|
522
565
|
// the retry surfaces the same error, that's a real corruption case —
|
|
@@ -687,6 +730,29 @@ export class GnosysDB {
|
|
|
687
730
|
}
|
|
688
731
|
}
|
|
689
732
|
if (fromVersion < 5) {
|
|
733
|
+
// v4 → v5 (v5.12): inline binary attachments carried in the memory row.
|
|
734
|
+
// Additive columns only — existing rows get NULLs. These ride the same
|
|
735
|
+
// row-copy sync path as `embedding`, so attachments travel machine to
|
|
736
|
+
// machine for free.
|
|
737
|
+
try {
|
|
738
|
+
this.db.exec("ALTER TABLE memories ADD COLUMN attachment_data BLOB");
|
|
739
|
+
}
|
|
740
|
+
catch {
|
|
741
|
+
// Column already exists — fine
|
|
742
|
+
}
|
|
743
|
+
try {
|
|
744
|
+
this.db.exec("ALTER TABLE memories ADD COLUMN attachment_mime TEXT");
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
// Column already exists — fine
|
|
748
|
+
}
|
|
749
|
+
try {
|
|
750
|
+
this.db.exec("ALTER TABLE memories ADD COLUMN attachment_name TEXT");
|
|
751
|
+
}
|
|
752
|
+
catch {
|
|
753
|
+
// Column already exists — fine
|
|
754
|
+
}
|
|
755
|
+
// Sync staging / multi-machine ledger tables (from network-mcp work on feat).
|
|
690
756
|
try {
|
|
691
757
|
this.db.exec(`
|
|
692
758
|
CREATE TABLE IF NOT EXISTS sync_staging_ledger (
|
|
@@ -749,23 +815,26 @@ export class GnosysDB {
|
|
|
749
815
|
// ─── Memory CRUD ────────────────────────────────────────────────────
|
|
750
816
|
insertMemory(mem) {
|
|
751
817
|
return this.withRecovery(() => {
|
|
752
|
-
const stmt = this.
|
|
818
|
+
const stmt = this.prep(`
|
|
753
819
|
INSERT OR REPLACE INTO memories
|
|
754
820
|
(id, title, category, content, summary, tags, relevance, author, authority,
|
|
755
821
|
confidence, reinforcement_count, content_hash, status, tier, supersedes,
|
|
756
822
|
superseded_by, last_reinforced, created, modified, embedding, source_path,
|
|
757
823
|
source_file, source_page, source_timerange,
|
|
824
|
+
attachment_data, attachment_mime, attachment_name,
|
|
758
825
|
project_id, scope)
|
|
759
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
826
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
760
827
|
`);
|
|
761
|
-
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");
|
|
828
|
+
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");
|
|
762
829
|
});
|
|
763
830
|
}
|
|
764
831
|
getMemory(id) {
|
|
765
|
-
return this.withRecovery(() => this.
|
|
832
|
+
return this.withRecovery(() => this.prep("SELECT * FROM memories WHERE id = ?").get(id) || null);
|
|
766
833
|
}
|
|
767
834
|
getActiveMemories() {
|
|
768
|
-
|
|
835
|
+
// v5.12.x perf: project NULL for the two BLOB columns — see
|
|
836
|
+
// LEAN_MEMORY_PROJECTION for the rationale and measured numbers.
|
|
837
|
+
return this.withRecovery(() => this.prep(`SELECT ${LEAN_MEMORY_PROJECTION} FROM memories WHERE tier = 'active' AND status = 'active'`).all());
|
|
769
838
|
}
|
|
770
839
|
getAllMemories() {
|
|
771
840
|
return this.withRecovery(() => this.db.prepare("SELECT * FROM memories").all());
|
|
@@ -795,20 +864,20 @@ export class GnosysDB {
|
|
|
795
864
|
this.db.pragma(`busy_timeout = ${Math.max(0, Math.floor(ms))}`);
|
|
796
865
|
}
|
|
797
866
|
getMemoriesByCategory(category) {
|
|
798
|
-
return this.
|
|
867
|
+
return this.withRecovery(() => this.prep("SELECT * FROM memories WHERE category = ? AND tier = 'active'").all(category));
|
|
799
868
|
}
|
|
800
869
|
getRelationshipsForMemoryIds(ids) {
|
|
801
870
|
if (ids.length === 0)
|
|
802
871
|
return [];
|
|
803
872
|
const placeholders = ids.map(() => "?").join(",");
|
|
804
|
-
return this.db
|
|
873
|
+
return this.withRecovery(() => this.db
|
|
805
874
|
.prepare(`SELECT * FROM relationships WHERE source_id IN (${placeholders}) OR target_id IN (${placeholders})`)
|
|
806
|
-
.all(...ids, ...ids);
|
|
875
|
+
.all(...ids, ...ids));
|
|
807
876
|
}
|
|
808
877
|
getAuditEntriesByProject(projectId) {
|
|
809
|
-
return this.db
|
|
878
|
+
return this.withRecovery(() => this.db
|
|
810
879
|
.prepare("SELECT * FROM audit_log WHERE memory_id IN (SELECT id FROM memories WHERE project_id = ?) ORDER BY id")
|
|
811
|
-
.all(projectId);
|
|
880
|
+
.all(projectId));
|
|
812
881
|
}
|
|
813
882
|
updateMemory(id, updates) {
|
|
814
883
|
const fields = [];
|
|
@@ -825,17 +894,18 @@ export class GnosysDB {
|
|
|
825
894
|
return;
|
|
826
895
|
values.push(id);
|
|
827
896
|
const sql = `UPDATE memories SET ${fields.join(", ")} WHERE id = ?`;
|
|
828
|
-
|
|
829
|
-
this.db.prepare(sql).run(...values);
|
|
830
|
-
}
|
|
831
|
-
catch {
|
|
832
|
-
// FTS5 update trigger may fail if INSERT OR REPLACE left FTS inconsistent.
|
|
833
|
-
// Workaround: drop the trigger, update manually, rebuild FTS entry.
|
|
834
|
-
this.db.exec("DROP TRIGGER IF EXISTS memories_fts_au");
|
|
835
|
-
this.db.prepare(sql).run(...values);
|
|
836
|
-
// Recreate trigger
|
|
897
|
+
return this.withRecovery(() => {
|
|
837
898
|
try {
|
|
838
|
-
this.db.
|
|
899
|
+
this.db.prepare(sql).run(...values);
|
|
900
|
+
}
|
|
901
|
+
catch {
|
|
902
|
+
// FTS5 update trigger may fail if INSERT OR REPLACE left FTS inconsistent.
|
|
903
|
+
// Workaround: drop the trigger, update manually, rebuild FTS entry.
|
|
904
|
+
this.db.exec("DROP TRIGGER IF EXISTS memories_fts_au");
|
|
905
|
+
this.db.prepare(sql).run(...values);
|
|
906
|
+
// Recreate trigger
|
|
907
|
+
try {
|
|
908
|
+
this.db.exec(`
|
|
839
909
|
CREATE TRIGGER IF NOT EXISTS memories_fts_au AFTER UPDATE ON memories BEGIN
|
|
840
910
|
INSERT INTO memories_fts(memories_fts, id, title, category, tags, relevance, content, summary)
|
|
841
911
|
VALUES ('delete', old.id, old.title, old.category, old.tags, old.relevance, old.content, old.summary);
|
|
@@ -843,66 +913,73 @@ export class GnosysDB {
|
|
|
843
913
|
VALUES (new.id, new.title, new.category, new.tags, new.relevance, new.content, new.summary);
|
|
844
914
|
END;
|
|
845
915
|
`);
|
|
916
|
+
}
|
|
917
|
+
catch {
|
|
918
|
+
// Trigger recreation failed — not critical
|
|
919
|
+
}
|
|
846
920
|
}
|
|
847
|
-
|
|
848
|
-
// Trigger recreation failed — not critical
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
// Manually sync FTS: remove old entry, insert updated entry (reliable for standalone FTS5)
|
|
852
|
-
try {
|
|
853
|
-
this.db.prepare("DELETE FROM memories_fts WHERE id = ?").run(id);
|
|
854
|
-
}
|
|
855
|
-
catch {
|
|
856
|
-
// Old FTS entry may not exist — that's OK
|
|
857
|
-
}
|
|
858
|
-
const newMem = this.db.prepare("SELECT * FROM memories WHERE id = ?").get(id);
|
|
859
|
-
if (newMem) {
|
|
921
|
+
// Manually sync FTS: remove old entry, insert updated entry (reliable for standalone FTS5)
|
|
860
922
|
try {
|
|
861
|
-
this.db.prepare("
|
|
923
|
+
this.db.prepare("DELETE FROM memories_fts WHERE id = ?").run(id);
|
|
862
924
|
}
|
|
863
925
|
catch {
|
|
864
|
-
// FTS
|
|
926
|
+
// Old FTS entry may not exist — that's OK
|
|
865
927
|
}
|
|
866
|
-
|
|
928
|
+
const newMem = this.db.prepare("SELECT * FROM memories WHERE id = ?").get(id);
|
|
929
|
+
if (newMem) {
|
|
930
|
+
try {
|
|
931
|
+
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);
|
|
932
|
+
}
|
|
933
|
+
catch {
|
|
934
|
+
// FTS insert may fail — not critical
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
});
|
|
867
938
|
}
|
|
868
939
|
deleteMemory(id) {
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
this.db.prepare("DELETE FROM memories WHERE id = ?").run(id);
|
|
872
|
-
}
|
|
873
|
-
catch {
|
|
874
|
-
// FTS trigger failed — drop trigger, delete without it
|
|
875
|
-
this.db.exec("DROP TRIGGER IF EXISTS memories_fts_ad");
|
|
876
|
-
this.db.prepare("DELETE FROM memories WHERE id = ?").run(id);
|
|
877
|
-
// Recreate trigger
|
|
940
|
+
return this.withRecovery(() => {
|
|
941
|
+
// FTS5 delete trigger may fail if INSERT OR REPLACE left FTS inconsistent.
|
|
878
942
|
try {
|
|
879
|
-
this.db.
|
|
943
|
+
this.db.prepare("DELETE FROM memories WHERE id = ?").run(id);
|
|
944
|
+
}
|
|
945
|
+
catch {
|
|
946
|
+
// FTS trigger failed — drop trigger, delete without it
|
|
947
|
+
this.db.exec("DROP TRIGGER IF EXISTS memories_fts_ad");
|
|
948
|
+
this.db.prepare("DELETE FROM memories WHERE id = ?").run(id);
|
|
949
|
+
// Recreate trigger
|
|
950
|
+
try {
|
|
951
|
+
this.db.exec(`
|
|
880
952
|
CREATE TRIGGER IF NOT EXISTS memories_fts_ad AFTER DELETE ON memories BEGIN
|
|
881
953
|
INSERT INTO memories_fts(memories_fts, id, title, category, tags, relevance, content, summary)
|
|
882
954
|
VALUES ('delete', old.id, old.title, old.category, old.tags, old.relevance, old.content, old.summary);
|
|
883
955
|
END;
|
|
884
956
|
`);
|
|
957
|
+
}
|
|
958
|
+
catch {
|
|
959
|
+
// Trigger recreation failed — not critical
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
// Ensure FTS entry is also removed (direct DELETE is reliable for standalone FTS5)
|
|
963
|
+
try {
|
|
964
|
+
this.db.prepare("DELETE FROM memories_fts WHERE id = ?").run(id);
|
|
885
965
|
}
|
|
886
966
|
catch {
|
|
887
|
-
//
|
|
967
|
+
// FTS entry may not exist — that's OK
|
|
888
968
|
}
|
|
889
|
-
}
|
|
890
|
-
// Ensure FTS entry is also removed (direct DELETE is reliable for standalone FTS5)
|
|
891
|
-
try {
|
|
892
|
-
this.db.prepare("DELETE FROM memories_fts WHERE id = ?").run(id);
|
|
893
|
-
}
|
|
894
|
-
catch {
|
|
895
|
-
// FTS entry may not exist — that's OK
|
|
896
|
-
}
|
|
969
|
+
});
|
|
897
970
|
}
|
|
898
971
|
getMemoryCount() {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
972
|
+
return this.withRecovery(() => {
|
|
973
|
+
const active = this.db.prepare("SELECT COUNT(*) as cnt FROM memories WHERE tier = 'active'").get().cnt;
|
|
974
|
+
const archived = this.db.prepare("SELECT COUNT(*) as cnt FROM memories WHERE tier = 'archive'").get().cnt;
|
|
975
|
+
return { active, archived, total: active + archived };
|
|
976
|
+
});
|
|
902
977
|
}
|
|
903
978
|
getCategories() {
|
|
904
|
-
|
|
905
|
-
|
|
979
|
+
return this.withRecovery(() => {
|
|
980
|
+
const rows = this.db.prepare("SELECT DISTINCT category FROM memories WHERE tier = 'active' ORDER BY category").all();
|
|
981
|
+
return rows.map((r) => r.category);
|
|
982
|
+
});
|
|
906
983
|
}
|
|
907
984
|
// ─── Scoped Queries (v3.0) ──────────────────────────────────────────
|
|
908
985
|
/**
|
|
@@ -912,13 +989,13 @@ export class GnosysDB {
|
|
|
912
989
|
const sql = includeArchived
|
|
913
990
|
? "SELECT * FROM memories WHERE project_id = ?"
|
|
914
991
|
: "SELECT * FROM memories WHERE project_id = ? AND tier = 'active' AND status = 'active'";
|
|
915
|
-
return this.
|
|
992
|
+
return this.withRecovery(() => this.prep(sql).all(projectId));
|
|
916
993
|
}
|
|
917
994
|
/**
|
|
918
995
|
* Get memories by scope (project, user, global).
|
|
919
996
|
*/
|
|
920
997
|
getMemoriesByScope(scope) {
|
|
921
|
-
return this.
|
|
998
|
+
return this.withRecovery(() => this.prep("SELECT * FROM memories WHERE scope = ? AND tier = 'active' AND status = 'active'").all(scope));
|
|
922
999
|
}
|
|
923
1000
|
// ─── Project Identity (v3.0) ──────────────────────────────────────
|
|
924
1001
|
insertProject(project) {
|
|
@@ -982,10 +1059,14 @@ export class GnosysDB {
|
|
|
982
1059
|
if (fields.length === 0)
|
|
983
1060
|
return;
|
|
984
1061
|
values.push(id);
|
|
985
|
-
this.
|
|
1062
|
+
this.withRecovery(() => {
|
|
1063
|
+
this.db.prepare(`UPDATE projects SET ${fields.join(", ")} WHERE id = ?`).run(...values);
|
|
1064
|
+
});
|
|
986
1065
|
}
|
|
987
1066
|
deleteProject(id) {
|
|
988
|
-
this.
|
|
1067
|
+
this.withRecovery(() => {
|
|
1068
|
+
this.db.prepare("DELETE FROM projects WHERE id = ?").run(id);
|
|
1069
|
+
});
|
|
989
1070
|
}
|
|
990
1071
|
/**
|
|
991
1072
|
* Generate the next sequential ID for a category.
|
|
@@ -1002,43 +1083,50 @@ export class GnosysDB {
|
|
|
1002
1083
|
* The `projectId` parameter is accepted for API compatibility but no longer
|
|
1003
1084
|
* used for ID generation (ULIDs don't need project scoping for uniqueness).
|
|
1004
1085
|
*/
|
|
1005
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1006
1086
|
getNextId(category, projectId) {
|
|
1007
1087
|
const prefix = category.substring(0, 4);
|
|
1008
1088
|
return `${prefix}-${ulid()}`;
|
|
1009
1089
|
}
|
|
1010
1090
|
// ─── FTS5 Search ────────────────────────────────────────────────────
|
|
1011
1091
|
searchFts(query, limit = 20) {
|
|
1012
|
-
const
|
|
1013
|
-
if (
|
|
1092
|
+
const terms = ftsTerms(query);
|
|
1093
|
+
if (terms.length === 0)
|
|
1014
1094
|
return [];
|
|
1015
1095
|
// v5.8.0 (#7): join memories so callers can render project-prefixed IDs.
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1096
|
+
return this.withRecovery(() => {
|
|
1097
|
+
try {
|
|
1098
|
+
const run = (match) => this.prep(`
|
|
1099
|
+
SELECT m.id AS id, m.title AS title,
|
|
1100
|
+
snippet(memories_fts, 5, '>>>', '<<<', '...', 40) as snippet,
|
|
1101
|
+
fts.rank AS rank,
|
|
1102
|
+
m.project_id AS project_id
|
|
1103
|
+
FROM memories_fts fts
|
|
1104
|
+
JOIN memories m ON m.id = fts.id
|
|
1105
|
+
WHERE memories_fts MATCH ?
|
|
1106
|
+
ORDER BY fts.rank
|
|
1107
|
+
LIMIT ?
|
|
1108
|
+
`).all(match, limit);
|
|
1109
|
+
// v5.12.3: AND first (precision), OR retry when AND finds nothing —
|
|
1110
|
+
// multi-word queries previously required every term to match.
|
|
1111
|
+
const results = run(ftsAndQuery(terms));
|
|
1112
|
+
if (results.length > 0 || terms.length === 1)
|
|
1113
|
+
return results;
|
|
1114
|
+
return run(ftsOrQuery(terms));
|
|
1115
|
+
}
|
|
1116
|
+
catch {
|
|
1117
|
+
// FTS5 syntax error — fallback to LIKE
|
|
1118
|
+
const pattern = `%${terms.join(" ")}%`;
|
|
1119
|
+
return this.prep(`
|
|
1033
1120
|
SELECT id, title, substr(content, 1, 200) as snippet, 0 as rank, project_id
|
|
1034
1121
|
FROM memories WHERE content LIKE ? OR title LIKE ? OR tags LIKE ?
|
|
1035
1122
|
LIMIT ?
|
|
1036
1123
|
`).all(pattern, pattern, pattern, limit);
|
|
1037
|
-
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1038
1126
|
}
|
|
1039
1127
|
discoverFts(query, limit = 20) {
|
|
1040
|
-
const
|
|
1041
|
-
if (
|
|
1128
|
+
const terms = ftsTerms(query);
|
|
1129
|
+
if (terms.length === 0)
|
|
1042
1130
|
return [];
|
|
1043
1131
|
// v5.7.1 (#14): join `memories` so callers can render project-prefixed IDs.
|
|
1044
1132
|
const select = `
|
|
@@ -1049,51 +1137,72 @@ export class GnosysDB {
|
|
|
1049
1137
|
ORDER BY fts.rank
|
|
1050
1138
|
LIMIT ?
|
|
1051
1139
|
`;
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
if (results.length > 0)
|
|
1056
|
-
return results;
|
|
1057
|
-
return this.db.prepare(select).all(safeQuery, limit);
|
|
1058
|
-
}
|
|
1059
|
-
catch {
|
|
1140
|
+
// Let corruption escape to withRecovery (reopen + retry); plain FTS
|
|
1141
|
+
// syntax failures still degrade gracefully to "no results".
|
|
1142
|
+
const tryRun = (match) => {
|
|
1060
1143
|
try {
|
|
1061
|
-
return this.
|
|
1144
|
+
return this.prep(select).all(match, limit);
|
|
1062
1145
|
}
|
|
1063
|
-
catch {
|
|
1146
|
+
catch (err) {
|
|
1147
|
+
if (GnosysDB.isCorruptionError(err))
|
|
1148
|
+
throw err;
|
|
1064
1149
|
return [];
|
|
1065
1150
|
}
|
|
1066
|
-
}
|
|
1151
|
+
};
|
|
1152
|
+
// v5.12.3: precision-to-recall ladder. AND on the metadata columns,
|
|
1153
|
+
// AND anywhere, then OR retries — multi-word queries previously
|
|
1154
|
+
// required every term to match, so long queries returned nothing.
|
|
1155
|
+
// Parens scope the column filter to the whole expression (a bare
|
|
1156
|
+
// `{cols} : a b` only filtered the first term).
|
|
1157
|
+
const colScoped = (expr) => `{relevance title tags} : (${expr})`;
|
|
1158
|
+
const andExpr = ftsAndQuery(terms);
|
|
1159
|
+
return this.withRecovery(() => {
|
|
1160
|
+
let results = tryRun(colScoped(andExpr));
|
|
1161
|
+
if (results.length > 0)
|
|
1162
|
+
return results;
|
|
1163
|
+
results = tryRun(andExpr);
|
|
1164
|
+
if (results.length > 0 || terms.length === 1)
|
|
1165
|
+
return results;
|
|
1166
|
+
const orExpr = ftsOrQuery(terms);
|
|
1167
|
+
results = tryRun(colScoped(orExpr));
|
|
1168
|
+
if (results.length > 0)
|
|
1169
|
+
return results;
|
|
1170
|
+
return tryRun(orExpr);
|
|
1171
|
+
});
|
|
1067
1172
|
}
|
|
1068
1173
|
// ─── Relationships ──────────────────────────────────────────────────
|
|
1069
1174
|
insertRelationship(rel) {
|
|
1070
|
-
this.
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1175
|
+
this.withRecovery(() => {
|
|
1176
|
+
this.db.prepare(`
|
|
1177
|
+
INSERT OR IGNORE INTO relationships (source_id, target_id, rel_type, label, confidence, created)
|
|
1178
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1179
|
+
`).run(rel.source_id, rel.target_id, rel.rel_type, rel.label, rel.confidence, rel.created);
|
|
1180
|
+
});
|
|
1074
1181
|
}
|
|
1075
1182
|
getRelationshipsFrom(id) {
|
|
1076
|
-
return this.db.prepare("SELECT * FROM relationships WHERE source_id = ?").all(id);
|
|
1183
|
+
return this.withRecovery(() => this.db.prepare("SELECT * FROM relationships WHERE source_id = ?").all(id));
|
|
1077
1184
|
}
|
|
1078
1185
|
getRelationshipsTo(id) {
|
|
1079
|
-
return this.db.prepare("SELECT * FROM relationships WHERE target_id = ?").all(id);
|
|
1186
|
+
return this.withRecovery(() => this.db.prepare("SELECT * FROM relationships WHERE target_id = ?").all(id));
|
|
1080
1187
|
}
|
|
1081
1188
|
// ─── Summaries ──────────────────────────────────────────────────────
|
|
1082
1189
|
upsertSummary(summary) {
|
|
1083
|
-
this.
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1190
|
+
this.withRecovery(() => {
|
|
1191
|
+
this.db.prepare(`
|
|
1192
|
+
INSERT INTO summaries (id, scope, scope_key, content, source_ids, created, modified)
|
|
1193
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1194
|
+
ON CONFLICT(scope, scope_key) DO UPDATE SET
|
|
1195
|
+
content = excluded.content,
|
|
1196
|
+
source_ids = excluded.source_ids,
|
|
1197
|
+
modified = excluded.modified
|
|
1198
|
+
`).run(summary.id, summary.scope, summary.scope_key, summary.content, summary.source_ids, summary.created, summary.modified);
|
|
1199
|
+
});
|
|
1091
1200
|
}
|
|
1092
1201
|
getSummary(scope, scopeKey) {
|
|
1093
|
-
return this.db.prepare("SELECT * FROM summaries WHERE scope = ? AND scope_key = ?").get(scope, scopeKey) || null;
|
|
1202
|
+
return this.withRecovery(() => this.db.prepare("SELECT * FROM summaries WHERE scope = ? AND scope_key = ?").get(scope, scopeKey) || null);
|
|
1094
1203
|
}
|
|
1095
1204
|
getAllSummaries() {
|
|
1096
|
-
return this.db.prepare("SELECT * FROM summaries").all();
|
|
1205
|
+
return this.withRecovery(() => this.db.prepare("SELECT * FROM summaries").all());
|
|
1097
1206
|
}
|
|
1098
1207
|
// ─── Audit ──────────────────────────────────────────────────────────
|
|
1099
1208
|
logAudit(entry) {
|
|
@@ -1145,21 +1254,26 @@ export class GnosysDB {
|
|
|
1145
1254
|
}
|
|
1146
1255
|
// ─── Embeddings ─────────────────────────────────────────────────────
|
|
1147
1256
|
updateEmbedding(id, embedding) {
|
|
1148
|
-
this.
|
|
1257
|
+
this.withRecovery(() => {
|
|
1258
|
+
this.db.prepare("UPDATE memories SET embedding = ? WHERE id = ?").run(embedding, id);
|
|
1259
|
+
});
|
|
1149
1260
|
}
|
|
1150
1261
|
getEmbedding(id) {
|
|
1151
|
-
|
|
1152
|
-
|
|
1262
|
+
return this.withRecovery(() => {
|
|
1263
|
+
const row = this.db.prepare("SELECT embedding FROM memories WHERE id = ?").get(id);
|
|
1264
|
+
return row?.embedding || null;
|
|
1265
|
+
});
|
|
1153
1266
|
}
|
|
1154
1267
|
getAllEmbeddings() {
|
|
1155
|
-
return this.db.prepare("SELECT id, embedding FROM memories WHERE embedding IS NOT NULL").all();
|
|
1268
|
+
return this.withRecovery(() => this.db.prepare("SELECT id, embedding FROM memories WHERE embedding IS NOT NULL").all());
|
|
1156
1269
|
}
|
|
1157
1270
|
getEmbeddingCount() {
|
|
1158
|
-
|
|
1159
|
-
|
|
1271
|
+
return this.withRecovery(() => {
|
|
1272
|
+
const row = this.db.prepare("SELECT COUNT(*) as cnt FROM memories WHERE embedding IS NOT NULL").get();
|
|
1273
|
+
return row.cnt;
|
|
1274
|
+
});
|
|
1160
1275
|
}
|
|
1161
1276
|
// ─── Transactions ───────────────────────────────────────────────────
|
|
1162
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1163
1277
|
transaction(fn) {
|
|
1164
1278
|
return this.db.transaction(fn)();
|
|
1165
1279
|
}
|
|
@@ -1403,6 +1517,7 @@ export class GnosysDB {
|
|
|
1403
1517
|
}
|
|
1404
1518
|
// ─── Lifecycle ──────────────────────────────────────────────────────
|
|
1405
1519
|
close() {
|
|
1520
|
+
this.stmtCache.clear();
|
|
1406
1521
|
this.db?.close();
|
|
1407
1522
|
}
|
|
1408
1523
|
// ─── Migration Status ───────────────────────────────────────────────
|
package/dist/lib/dbWrite.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* become optional — controlled by config.
|
|
15
15
|
*/
|
|
16
16
|
import type { GnosysDB } from "./db.js";
|
|
17
|
-
import {
|
|
17
|
+
import type { MemoryFrontmatter } from "./store.js";
|
|
18
18
|
/**
|
|
19
19
|
* Sync a memory write to gnosys.db after it's been written to .md.
|
|
20
20
|
* Call this after GnosysStore.writeMemory() or updateMemory().
|
|
@@ -18,7 +18,7 @@ export async function runDearchiveCommand(getResolver, query, opts) {
|
|
|
18
18
|
console.error("Archive not available. Install it with: npm install better-sqlite3");
|
|
19
19
|
process.exit(1);
|
|
20
20
|
}
|
|
21
|
-
const results = archive.searchArchive(query, parseInt(opts.limit));
|
|
21
|
+
const results = archive.searchArchive(query, parseInt(opts.limit, 10));
|
|
22
22
|
if (results.length === 0) {
|
|
23
23
|
console.log(`No archived memories found matching "${query}".`);
|
|
24
24
|
return;
|