gnosys 5.12.0 → 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 +48 -7
- package/dist/index.js +179 -10
- package/dist/lib/addCommand.js +0 -1
- package/dist/lib/archive.js +0 -2
- 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 +216 -119
- 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/hybridSearchCommand.js +1 -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/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 +0 -7
- 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;
|
|
@@ -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
|
);
|
|
@@ -243,12 +245,27 @@ const MEMORY_COLUMNS = new Set([
|
|
|
243
245
|
"status", "tier", "supersedes", "superseded_by", "last_reinforced",
|
|
244
246
|
"created", "modified", "embedding", "source_path",
|
|
245
247
|
"source_file", "source_page", "source_timerange",
|
|
248
|
+
"attachment_data", "attachment_mime", "attachment_name",
|
|
246
249
|
"project_id", "scope",
|
|
247
250
|
]);
|
|
248
251
|
const PROJECT_COLUMNS = new Set([
|
|
249
252
|
"name", "working_directory", "root_id", "rel_path", "user",
|
|
250
253
|
"agent_rules_target", "obsidian_vault", "created", "modified",
|
|
251
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(", ");
|
|
252
269
|
// ─── FNV-1a hash (same as embeddings.ts) ────────────────────────────────
|
|
253
270
|
function fnv1a(str) {
|
|
254
271
|
let hash = 0x811c9dc5;
|
|
@@ -260,8 +277,10 @@ function fnv1a(str) {
|
|
|
260
277
|
}
|
|
261
278
|
// ─── GnosysDB Class ─────────────────────────────────────────────────────
|
|
262
279
|
export class GnosysDB {
|
|
263
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
264
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();
|
|
265
284
|
storePath;
|
|
266
285
|
available = false;
|
|
267
286
|
dbFilePath;
|
|
@@ -381,7 +400,8 @@ export class GnosysDB {
|
|
|
381
400
|
try {
|
|
382
401
|
fs.mkdirSync(storePath, { recursive: true, mode: 0o700 });
|
|
383
402
|
this.db = new Database(this.dbFilePath);
|
|
384
|
-
|
|
403
|
+
// Longer busy timeout for network shares (10s)
|
|
404
|
+
enableWAL(this.db, 10000);
|
|
385
405
|
try {
|
|
386
406
|
fs.chmodSync(storePath, 0o700);
|
|
387
407
|
fs.chmodSync(this.dbFilePath, 0o600);
|
|
@@ -396,13 +416,11 @@ export class GnosysDB {
|
|
|
396
416
|
// best-effort (Windows / network FS)
|
|
397
417
|
}
|
|
398
418
|
this.db.pragma("foreign_keys = ON");
|
|
399
|
-
// Longer busy timeout for network shares (10s)
|
|
400
|
-
this.db.pragma("busy_timeout = 10000");
|
|
401
419
|
this.applySchema();
|
|
402
420
|
this.available = true;
|
|
403
421
|
return; // Success
|
|
404
422
|
}
|
|
405
|
-
catch (
|
|
423
|
+
catch (_err) {
|
|
406
424
|
this.db = null;
|
|
407
425
|
if (attempt < maxRetries) {
|
|
408
426
|
// Synchronous delay for constructor (network share retry)
|
|
@@ -471,6 +489,7 @@ export class GnosysDB {
|
|
|
471
489
|
* file handles after a WAL checkpoint or remount.
|
|
472
490
|
*/
|
|
473
491
|
reopen() {
|
|
492
|
+
this.stmtCache.clear();
|
|
474
493
|
try {
|
|
475
494
|
this.db?.close();
|
|
476
495
|
}
|
|
@@ -483,9 +502,19 @@ export class GnosysDB {
|
|
|
483
502
|
return;
|
|
484
503
|
try {
|
|
485
504
|
this.db = new Database(this.dbFilePath);
|
|
486
|
-
|
|
505
|
+
// Longer busy timeout for network shares (10s)
|
|
506
|
+
enableWAL(this.db, 10000);
|
|
487
507
|
this.db.pragma("foreign_keys = ON");
|
|
488
|
-
|
|
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
|
+
}
|
|
489
518
|
this.available = true;
|
|
490
519
|
}
|
|
491
520
|
catch {
|
|
@@ -508,15 +537,28 @@ export class GnosysDB {
|
|
|
508
537
|
* Read methods are also wrapped because reads against stale pages can
|
|
509
538
|
* surface the same error.
|
|
510
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
|
+
}
|
|
511
553
|
withRecovery(fn) {
|
|
512
554
|
try {
|
|
513
555
|
return fn();
|
|
514
556
|
}
|
|
515
557
|
catch (err) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
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))
|
|
520
562
|
throw err;
|
|
521
563
|
// One-shot recovery: reopen and retry. If the reopen itself fails or
|
|
522
564
|
// the retry surfaces the same error, that's a real corruption case —
|
|
@@ -687,6 +729,29 @@ export class GnosysDB {
|
|
|
687
729
|
}
|
|
688
730
|
}
|
|
689
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).
|
|
690
755
|
try {
|
|
691
756
|
this.db.exec(`
|
|
692
757
|
CREATE TABLE IF NOT EXISTS sync_staging_ledger (
|
|
@@ -749,23 +814,26 @@ export class GnosysDB {
|
|
|
749
814
|
// ─── Memory CRUD ────────────────────────────────────────────────────
|
|
750
815
|
insertMemory(mem) {
|
|
751
816
|
return this.withRecovery(() => {
|
|
752
|
-
const stmt = this.
|
|
817
|
+
const stmt = this.prep(`
|
|
753
818
|
INSERT OR REPLACE INTO memories
|
|
754
819
|
(id, title, category, content, summary, tags, relevance, author, authority,
|
|
755
820
|
confidence, reinforcement_count, content_hash, status, tier, supersedes,
|
|
756
821
|
superseded_by, last_reinforced, created, modified, embedding, source_path,
|
|
757
822
|
source_file, source_page, source_timerange,
|
|
823
|
+
attachment_data, attachment_mime, attachment_name,
|
|
758
824
|
project_id, scope)
|
|
759
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
825
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
760
826
|
`);
|
|
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");
|
|
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");
|
|
762
828
|
});
|
|
763
829
|
}
|
|
764
830
|
getMemory(id) {
|
|
765
|
-
return this.withRecovery(() => this.
|
|
831
|
+
return this.withRecovery(() => this.prep("SELECT * FROM memories WHERE id = ?").get(id) || null);
|
|
766
832
|
}
|
|
767
833
|
getActiveMemories() {
|
|
768
|
-
|
|
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());
|
|
769
837
|
}
|
|
770
838
|
getAllMemories() {
|
|
771
839
|
return this.withRecovery(() => this.db.prepare("SELECT * FROM memories").all());
|
|
@@ -795,20 +863,20 @@ export class GnosysDB {
|
|
|
795
863
|
this.db.pragma(`busy_timeout = ${Math.max(0, Math.floor(ms))}`);
|
|
796
864
|
}
|
|
797
865
|
getMemoriesByCategory(category) {
|
|
798
|
-
return this.
|
|
866
|
+
return this.withRecovery(() => this.prep("SELECT * FROM memories WHERE category = ? AND tier = 'active'").all(category));
|
|
799
867
|
}
|
|
800
868
|
getRelationshipsForMemoryIds(ids) {
|
|
801
869
|
if (ids.length === 0)
|
|
802
870
|
return [];
|
|
803
871
|
const placeholders = ids.map(() => "?").join(",");
|
|
804
|
-
return this.db
|
|
872
|
+
return this.withRecovery(() => this.db
|
|
805
873
|
.prepare(`SELECT * FROM relationships WHERE source_id IN (${placeholders}) OR target_id IN (${placeholders})`)
|
|
806
|
-
.all(...ids, ...ids);
|
|
874
|
+
.all(...ids, ...ids));
|
|
807
875
|
}
|
|
808
876
|
getAuditEntriesByProject(projectId) {
|
|
809
|
-
return this.db
|
|
877
|
+
return this.withRecovery(() => this.db
|
|
810
878
|
.prepare("SELECT * FROM audit_log WHERE memory_id IN (SELECT id FROM memories WHERE project_id = ?) ORDER BY id")
|
|
811
|
-
.all(projectId);
|
|
879
|
+
.all(projectId));
|
|
812
880
|
}
|
|
813
881
|
updateMemory(id, updates) {
|
|
814
882
|
const fields = [];
|
|
@@ -825,17 +893,18 @@ export class GnosysDB {
|
|
|
825
893
|
return;
|
|
826
894
|
values.push(id);
|
|
827
895
|
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
|
|
896
|
+
return this.withRecovery(() => {
|
|
837
897
|
try {
|
|
838
|
-
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(`
|
|
839
908
|
CREATE TRIGGER IF NOT EXISTS memories_fts_au AFTER UPDATE ON memories BEGIN
|
|
840
909
|
INSERT INTO memories_fts(memories_fts, id, title, category, tags, relevance, content, summary)
|
|
841
910
|
VALUES ('delete', old.id, old.title, old.category, old.tags, old.relevance, old.content, old.summary);
|
|
@@ -843,66 +912,73 @@ export class GnosysDB {
|
|
|
843
912
|
VALUES (new.id, new.title, new.category, new.tags, new.relevance, new.content, new.summary);
|
|
844
913
|
END;
|
|
845
914
|
`);
|
|
915
|
+
}
|
|
916
|
+
catch {
|
|
917
|
+
// Trigger recreation failed — not critical
|
|
918
|
+
}
|
|
846
919
|
}
|
|
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) {
|
|
920
|
+
// Manually sync FTS: remove old entry, insert updated entry (reliable for standalone FTS5)
|
|
860
921
|
try {
|
|
861
|
-
this.db.prepare("
|
|
922
|
+
this.db.prepare("DELETE FROM memories_fts WHERE id = ?").run(id);
|
|
862
923
|
}
|
|
863
924
|
catch {
|
|
864
|
-
// FTS
|
|
925
|
+
// Old FTS entry may not exist — that's OK
|
|
865
926
|
}
|
|
866
|
-
|
|
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
|
+
});
|
|
867
937
|
}
|
|
868
938
|
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
|
|
939
|
+
return this.withRecovery(() => {
|
|
940
|
+
// FTS5 delete trigger may fail if INSERT OR REPLACE left FTS inconsistent.
|
|
878
941
|
try {
|
|
879
|
-
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(`
|
|
880
951
|
CREATE TRIGGER IF NOT EXISTS memories_fts_ad AFTER DELETE ON memories BEGIN
|
|
881
952
|
INSERT INTO memories_fts(memories_fts, id, title, category, tags, relevance, content, summary)
|
|
882
953
|
VALUES ('delete', old.id, old.title, old.category, old.tags, old.relevance, old.content, old.summary);
|
|
883
954
|
END;
|
|
884
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);
|
|
885
964
|
}
|
|
886
965
|
catch {
|
|
887
|
-
//
|
|
966
|
+
// FTS entry may not exist — that's OK
|
|
888
967
|
}
|
|
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
|
-
}
|
|
968
|
+
});
|
|
897
969
|
}
|
|
898
970
|
getMemoryCount() {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
+
});
|
|
902
976
|
}
|
|
903
977
|
getCategories() {
|
|
904
|
-
|
|
905
|
-
|
|
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
|
+
});
|
|
906
982
|
}
|
|
907
983
|
// ─── Scoped Queries (v3.0) ──────────────────────────────────────────
|
|
908
984
|
/**
|
|
@@ -912,13 +988,13 @@ export class GnosysDB {
|
|
|
912
988
|
const sql = includeArchived
|
|
913
989
|
? "SELECT * FROM memories WHERE project_id = ?"
|
|
914
990
|
: "SELECT * FROM memories WHERE project_id = ? AND tier = 'active' AND status = 'active'";
|
|
915
|
-
return this.
|
|
991
|
+
return this.withRecovery(() => this.prep(sql).all(projectId));
|
|
916
992
|
}
|
|
917
993
|
/**
|
|
918
994
|
* Get memories by scope (project, user, global).
|
|
919
995
|
*/
|
|
920
996
|
getMemoriesByScope(scope) {
|
|
921
|
-
return this.
|
|
997
|
+
return this.withRecovery(() => this.prep("SELECT * FROM memories WHERE scope = ? AND tier = 'active' AND status = 'active'").all(scope));
|
|
922
998
|
}
|
|
923
999
|
// ─── Project Identity (v3.0) ──────────────────────────────────────
|
|
924
1000
|
insertProject(project) {
|
|
@@ -982,10 +1058,14 @@ export class GnosysDB {
|
|
|
982
1058
|
if (fields.length === 0)
|
|
983
1059
|
return;
|
|
984
1060
|
values.push(id);
|
|
985
|
-
this.
|
|
1061
|
+
this.withRecovery(() => {
|
|
1062
|
+
this.db.prepare(`UPDATE projects SET ${fields.join(", ")} WHERE id = ?`).run(...values);
|
|
1063
|
+
});
|
|
986
1064
|
}
|
|
987
1065
|
deleteProject(id) {
|
|
988
|
-
this.
|
|
1066
|
+
this.withRecovery(() => {
|
|
1067
|
+
this.db.prepare("DELETE FROM projects WHERE id = ?").run(id);
|
|
1068
|
+
});
|
|
989
1069
|
}
|
|
990
1070
|
/**
|
|
991
1071
|
* Generate the next sequential ID for a category.
|
|
@@ -1002,7 +1082,6 @@ export class GnosysDB {
|
|
|
1002
1082
|
* The `projectId` parameter is accepted for API compatibility but no longer
|
|
1003
1083
|
* used for ID generation (ULIDs don't need project scoping for uniqueness).
|
|
1004
1084
|
*/
|
|
1005
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1006
1085
|
getNextId(category, projectId) {
|
|
1007
1086
|
const prefix = category.substring(0, 4);
|
|
1008
1087
|
return `${prefix}-${ulid()}`;
|
|
@@ -1013,8 +1092,9 @@ export class GnosysDB {
|
|
|
1013
1092
|
if (!safeQuery)
|
|
1014
1093
|
return [];
|
|
1015
1094
|
// v5.8.0 (#7): join memories so callers can render project-prefixed IDs.
|
|
1016
|
-
|
|
1017
|
-
|
|
1095
|
+
return this.withRecovery(() => {
|
|
1096
|
+
try {
|
|
1097
|
+
return this.prep(`
|
|
1018
1098
|
SELECT m.id AS id, m.title AS title,
|
|
1019
1099
|
snippet(memories_fts, 5, '>>>', '<<<', '...', 40) as snippet,
|
|
1020
1100
|
fts.rank AS rank,
|
|
@@ -1025,16 +1105,17 @@ export class GnosysDB {
|
|
|
1025
1105
|
ORDER BY fts.rank
|
|
1026
1106
|
LIMIT ?
|
|
1027
1107
|
`).all(safeQuery, limit);
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1108
|
+
}
|
|
1109
|
+
catch {
|
|
1110
|
+
// FTS5 syntax error — fallback to LIKE
|
|
1111
|
+
const pattern = `%${safeQuery}%`;
|
|
1112
|
+
return this.prep(`
|
|
1033
1113
|
SELECT id, title, substr(content, 1, 200) as snippet, 0 as rank, project_id
|
|
1034
1114
|
FROM memories WHERE content LIKE ? OR title LIKE ? OR tags LIKE ?
|
|
1035
1115
|
LIMIT ?
|
|
1036
1116
|
`).all(pattern, pattern, pattern, limit);
|
|
1037
|
-
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1038
1119
|
}
|
|
1039
1120
|
discoverFts(query, limit = 20) {
|
|
1040
1121
|
const safeQuery = query.replace(/['"]/g, "").trim();
|
|
@@ -1049,51 +1130,61 @@ export class GnosysDB {
|
|
|
1049
1130
|
ORDER BY fts.rank
|
|
1050
1131
|
LIMIT ?
|
|
1051
1132
|
`;
|
|
1052
|
-
|
|
1053
|
-
const colQuery = `{relevance title tags} : ${safeQuery}`;
|
|
1054
|
-
const results = this.db.prepare(select).all(colQuery, limit);
|
|
1055
|
-
if (results.length > 0)
|
|
1056
|
-
return results;
|
|
1057
|
-
return this.db.prepare(select).all(safeQuery, limit);
|
|
1058
|
-
}
|
|
1059
|
-
catch {
|
|
1133
|
+
return this.withRecovery(() => {
|
|
1060
1134
|
try {
|
|
1061
|
-
|
|
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);
|
|
1062
1140
|
}
|
|
1063
1141
|
catch {
|
|
1064
|
-
|
|
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
|
+
}
|
|
1065
1152
|
}
|
|
1066
|
-
}
|
|
1153
|
+
});
|
|
1067
1154
|
}
|
|
1068
1155
|
// ─── Relationships ──────────────────────────────────────────────────
|
|
1069
1156
|
insertRelationship(rel) {
|
|
1070
|
-
this.
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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
|
+
});
|
|
1074
1163
|
}
|
|
1075
1164
|
getRelationshipsFrom(id) {
|
|
1076
|
-
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));
|
|
1077
1166
|
}
|
|
1078
1167
|
getRelationshipsTo(id) {
|
|
1079
|
-
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));
|
|
1080
1169
|
}
|
|
1081
1170
|
// ─── Summaries ──────────────────────────────────────────────────────
|
|
1082
1171
|
upsertSummary(summary) {
|
|
1083
|
-
this.
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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
|
+
});
|
|
1091
1182
|
}
|
|
1092
1183
|
getSummary(scope, scopeKey) {
|
|
1093
|
-
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);
|
|
1094
1185
|
}
|
|
1095
1186
|
getAllSummaries() {
|
|
1096
|
-
return this.db.prepare("SELECT * FROM summaries").all();
|
|
1187
|
+
return this.withRecovery(() => this.db.prepare("SELECT * FROM summaries").all());
|
|
1097
1188
|
}
|
|
1098
1189
|
// ─── Audit ──────────────────────────────────────────────────────────
|
|
1099
1190
|
logAudit(entry) {
|
|
@@ -1145,21 +1236,26 @@ export class GnosysDB {
|
|
|
1145
1236
|
}
|
|
1146
1237
|
// ─── Embeddings ─────────────────────────────────────────────────────
|
|
1147
1238
|
updateEmbedding(id, embedding) {
|
|
1148
|
-
this.
|
|
1239
|
+
this.withRecovery(() => {
|
|
1240
|
+
this.db.prepare("UPDATE memories SET embedding = ? WHERE id = ?").run(embedding, id);
|
|
1241
|
+
});
|
|
1149
1242
|
}
|
|
1150
1243
|
getEmbedding(id) {
|
|
1151
|
-
|
|
1152
|
-
|
|
1244
|
+
return this.withRecovery(() => {
|
|
1245
|
+
const row = this.db.prepare("SELECT embedding FROM memories WHERE id = ?").get(id);
|
|
1246
|
+
return row?.embedding || null;
|
|
1247
|
+
});
|
|
1153
1248
|
}
|
|
1154
1249
|
getAllEmbeddings() {
|
|
1155
|
-
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());
|
|
1156
1251
|
}
|
|
1157
1252
|
getEmbeddingCount() {
|
|
1158
|
-
|
|
1159
|
-
|
|
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
|
+
});
|
|
1160
1257
|
}
|
|
1161
1258
|
// ─── Transactions ───────────────────────────────────────────────────
|
|
1162
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1163
1259
|
transaction(fn) {
|
|
1164
1260
|
return this.db.transaction(fn)();
|
|
1165
1261
|
}
|
|
@@ -1403,6 +1499,7 @@ export class GnosysDB {
|
|
|
1403
1499
|
}
|
|
1404
1500
|
// ─── Lifecycle ──────────────────────────────────────────────────────
|
|
1405
1501
|
close() {
|
|
1502
|
+
this.stmtCache.clear();
|
|
1406
1503
|
this.db?.close();
|
|
1407
1504
|
}
|
|
1408
1505
|
// ─── 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;
|
package/dist/lib/docxExtract.js
CHANGED
|
@@ -30,7 +30,7 @@ export async function extractDocxText(filePath) {
|
|
|
30
30
|
await assertDocxDecompressedSizeWithinLimit(buffer);
|
|
31
31
|
const result = await mammoth.convertToHtml({ buffer });
|
|
32
32
|
const html = result.value;
|
|
33
|
-
if (!html
|
|
33
|
+
if (!html?.trim()) {
|
|
34
34
|
return [];
|
|
35
35
|
}
|
|
36
36
|
// Convert HTML to markdown
|
package/dist/lib/dream.d.ts
CHANGED
|
@@ -107,6 +107,14 @@ export declare class GnosysDreamEngine {
|
|
|
107
107
|
private providerInitError;
|
|
108
108
|
private createPhase;
|
|
109
109
|
private finishPhase;
|
|
110
|
+
/**
|
|
111
|
+
* v5.12.1 crash safety: persist analyzed fingerprints at every phase
|
|
112
|
+
* boundary, not only in finalize(). A crash mid-run previously lost all
|
|
113
|
+
* pendingFingerprints, so the next run re-analyzed (and re-paid for) the
|
|
114
|
+
* same memory sets and could double-create summaries. Checkpointing only
|
|
115
|
+
* merges fingerprints — lastRunAt / watermarks remain finalize()'s job.
|
|
116
|
+
*/
|
|
117
|
+
private checkpointFingerprints;
|
|
110
118
|
private addTouched;
|
|
111
119
|
private recordLLMSkip;
|
|
112
120
|
private llmCalls;
|