wolbarg 0.3.2 → 0.4.0

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/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import path5 from "node:path";
2
+ import { createHash } from 'crypto';
2
3
  import fs from 'fs/promises';
3
4
  import fs5 from "node:fs";
4
5
  import { DatabaseSync } from "node:sqlite";
5
6
  import * as sqliteVec from 'sqlite-vec';
6
7
  import { AsyncLocalStorage } from 'async_hooks';
8
+ import { EventEmitter } from 'events';
7
9
 
8
10
  // src/core/wolbarg.ts
9
11
 
@@ -47,6 +49,12 @@ var DatabaseError = class extends WolbargError {
47
49
  this.name = "DatabaseError";
48
50
  }
49
51
  };
52
+ var StorageLockedError = class extends WolbargError {
53
+ constructor(message, options) {
54
+ super(message, "WOLBARG_STORAGE_LOCKED", options);
55
+ this.name = "StorageLockedError";
56
+ }
57
+ };
50
58
  var EmbeddingError = class extends WolbargError {
51
59
  constructor(message, options) {
52
60
  super(message, "EMBEDDING_ERROR", options);
@@ -84,11 +92,11 @@ function wrapOperationError(operation, error) {
84
92
  const raw = error instanceof Error ? error.message : String(error);
85
93
  const lower = raw.toLowerCase();
86
94
  if (lower.includes("database is locked") || lower.includes("sqlite_busy")) {
87
- return new DatabaseError(formatOperationMessage(operation, raw), {
95
+ return new StorageLockedError(formatOperationMessage(operation, raw), {
88
96
  cause: error instanceof Error ? error : void 0,
89
97
  operation,
90
98
  reason: "SQLite database locked",
91
- suggestion: "Increase timeout or reduce concurrent writes."
99
+ suggestion: "Increase concurrency.maxRetries or concurrency.lockTimeoutMs, or consider the Postgres backend for high-concurrency multi-agent workloads."
92
100
  });
93
101
  }
94
102
  if (lower.includes("no such file") || lower.includes("enoent")) {
@@ -548,6 +556,404 @@ async function embedMany(provider, texts, concurrency = 8) {
548
556
  );
549
557
  return out;
550
558
  }
559
+ function resolveEmbeddingCacheConfig(input) {
560
+ return {
561
+ enabled: input?.enabled ?? true,
562
+ ttlMs: input?.ttlMs ?? null,
563
+ maxEntries: input?.maxEntries ?? null
564
+ };
565
+ }
566
+ function embeddingCacheKey(content, model) {
567
+ const hash = createHash("sha256").update(content, "utf8").digest("hex");
568
+ return `${hash}:${model}`;
569
+ }
570
+ function withEmbeddingCache(provider, store, config) {
571
+ let cacheHits = 0;
572
+ let cacheMisses = 0;
573
+ async function embedOne(text) {
574
+ if (!config.enabled) {
575
+ cacheMisses += 1;
576
+ return provider.embed(text);
577
+ }
578
+ const key = embeddingCacheKey(text, provider.model);
579
+ const cached = await store.get(key);
580
+ if (cached) {
581
+ cacheHits += 1;
582
+ return cached;
583
+ }
584
+ cacheMisses += 1;
585
+ const vector = await provider.embed(text);
586
+ void store.set(key, provider.model, vector);
587
+ if (config.maxEntries !== null) {
588
+ void store.evictIfNeeded(config.maxEntries);
589
+ }
590
+ return vector;
591
+ }
592
+ async function embedBatch(texts) {
593
+ if (!config.enabled || texts.length === 0) {
594
+ if (provider.embedBatch) {
595
+ cacheMisses += texts.length;
596
+ return provider.embedBatch(texts);
597
+ }
598
+ return Promise.all(texts.map((t) => embedOne(t)));
599
+ }
600
+ const results = new Array(texts.length).fill(
601
+ null
602
+ );
603
+ const missIndexes = [];
604
+ const missTexts = [];
605
+ for (let i = 0; i < texts.length; i += 1) {
606
+ const text = texts[i];
607
+ const key = embeddingCacheKey(text, provider.model);
608
+ const cached = await store.get(key);
609
+ if (cached) {
610
+ cacheHits += 1;
611
+ results[i] = cached;
612
+ } else {
613
+ missIndexes.push(i);
614
+ missTexts.push(text);
615
+ }
616
+ }
617
+ if (missTexts.length > 0) {
618
+ cacheMisses += missTexts.length;
619
+ const vectors = provider.embedBatch ? await provider.embedBatch(missTexts) : await Promise.all(missTexts.map((t) => provider.embed(t)));
620
+ for (let j = 0; j < missIndexes.length; j += 1) {
621
+ const idx = missIndexes[j];
622
+ const vector = vectors[j];
623
+ results[idx] = vector;
624
+ const key = embeddingCacheKey(missTexts[j], provider.model);
625
+ void store.set(key, provider.model, vector);
626
+ }
627
+ if (config.maxEntries !== null) {
628
+ void store.evictIfNeeded(config.maxEntries);
629
+ }
630
+ }
631
+ return results;
632
+ }
633
+ return {
634
+ model: provider.model,
635
+ embed: embedOne,
636
+ embedBatch,
637
+ validate: () => provider.validate(),
638
+ get cacheHits() {
639
+ return cacheHits;
640
+ },
641
+ get cacheMisses() {
642
+ return cacheMisses;
643
+ },
644
+ resetCacheStats() {
645
+ cacheHits = 0;
646
+ cacheMisses = 0;
647
+ }
648
+ };
649
+ }
650
+
651
+ // src/embedding/cache-store.ts
652
+ var TOUCH_FLUSH_THRESHOLD = 64;
653
+ var L1_DEFAULT_MAX = 5e4;
654
+ var L1Cache = class {
655
+ map = /* @__PURE__ */ new Map();
656
+ maxEntries;
657
+ constructor(maxEntries = L1_DEFAULT_MAX) {
658
+ this.maxEntries = maxEntries;
659
+ }
660
+ get(cacheKey, ttlMs) {
661
+ const entry = this.map.get(cacheKey);
662
+ if (!entry) {
663
+ return null;
664
+ }
665
+ if (ttlMs !== null && Date.now() - entry.createdAt > ttlMs) {
666
+ this.map.delete(cacheKey);
667
+ return null;
668
+ }
669
+ entry.lastUsedAt = Date.now();
670
+ this.map.delete(cacheKey);
671
+ this.map.set(cacheKey, entry);
672
+ return entry.vector;
673
+ }
674
+ set(cacheKey, model, vector) {
675
+ const now = Date.now();
676
+ if (this.map.has(cacheKey)) {
677
+ this.map.delete(cacheKey);
678
+ }
679
+ this.map.set(cacheKey, {
680
+ model,
681
+ vector,
682
+ createdAt: now,
683
+ lastUsedAt: now
684
+ });
685
+ this.evictIfNeeded();
686
+ }
687
+ touch(cacheKey) {
688
+ const entry = this.map.get(cacheKey);
689
+ if (!entry) {
690
+ return;
691
+ }
692
+ entry.lastUsedAt = Date.now();
693
+ this.map.delete(cacheKey);
694
+ this.map.set(cacheKey, entry);
695
+ }
696
+ evictIfNeeded(maxEntries) {
697
+ const limit = maxEntries ?? this.maxEntries;
698
+ while (this.map.size > limit) {
699
+ const oldest = this.map.keys().next().value;
700
+ if (oldest === void 0) {
701
+ break;
702
+ }
703
+ this.map.delete(oldest);
704
+ }
705
+ }
706
+ get size() {
707
+ return this.map.size;
708
+ }
709
+ };
710
+ var SqliteEmbeddingCacheStore = class {
711
+ ttlMs;
712
+ getDb;
713
+ l1 = new L1Cache();
714
+ pendingTouches = /* @__PURE__ */ new Set();
715
+ pendingSets = /* @__PURE__ */ new Map();
716
+ persistScheduled = false;
717
+ stmts = null;
718
+ constructor(dbOrGetter, options) {
719
+ this.ttlMs = options?.ttlMs ?? null;
720
+ this.getDb = typeof dbOrGetter === "function" ? dbOrGetter : () => dbOrGetter;
721
+ }
722
+ requireDb() {
723
+ try {
724
+ return this.getDb();
725
+ } catch {
726
+ return null;
727
+ }
728
+ }
729
+ ensureStatements(db) {
730
+ if (this.stmts) {
731
+ return this.stmts;
732
+ }
733
+ this.stmts = {
734
+ get: db.prepare(
735
+ `SELECT vector, last_used_at, created_at FROM embedding_cache WHERE cache_key = ?`
736
+ ),
737
+ delete: db.prepare(`DELETE FROM embedding_cache WHERE cache_key = ?`),
738
+ set: db.prepare(
739
+ `INSERT INTO embedding_cache (cache_key, model, vector, created_at, last_used_at)
740
+ VALUES (?, ?, ?, ?, ?)
741
+ ON CONFLICT(cache_key) DO UPDATE SET
742
+ model = excluded.model,
743
+ vector = excluded.vector,
744
+ last_used_at = excluded.last_used_at`
745
+ ),
746
+ touch: db.prepare(
747
+ `UPDATE embedding_cache SET last_used_at = ? WHERE cache_key = ?`
748
+ ),
749
+ count: db.prepare(`SELECT COUNT(*) AS c FROM embedding_cache`),
750
+ evict: db.prepare(
751
+ `DELETE FROM embedding_cache WHERE cache_key IN (
752
+ SELECT cache_key FROM embedding_cache
753
+ ORDER BY last_used_at ASC
754
+ LIMIT ?
755
+ )`
756
+ )
757
+ };
758
+ return this.stmts;
759
+ }
760
+ async get(cacheKey) {
761
+ const l1Hit = this.l1.get(cacheKey, this.ttlMs);
762
+ if (l1Hit) {
763
+ return l1Hit;
764
+ }
765
+ const pending = this.pendingSets.get(cacheKey);
766
+ if (pending) {
767
+ this.l1.set(cacheKey, pending.model, pending.vector);
768
+ return pending.vector;
769
+ }
770
+ return null;
771
+ }
772
+ async set(cacheKey, model, vector) {
773
+ this.l1.set(cacheKey, model, vector);
774
+ this.pendingSets.set(cacheKey, {
775
+ model,
776
+ vector,
777
+ createdAt: Date.now()
778
+ });
779
+ this.pendingTouches.delete(cacheKey);
780
+ this.schedulePersist();
781
+ }
782
+ async touch(cacheKey) {
783
+ this.l1.touch(cacheKey);
784
+ if (this.pendingSets.has(cacheKey)) {
785
+ return;
786
+ }
787
+ this.pendingTouches.add(cacheKey);
788
+ if (this.pendingTouches.size >= TOUCH_FLUSH_THRESHOLD) {
789
+ this.schedulePersist();
790
+ }
791
+ }
792
+ async flushTouches() {
793
+ await this.flushPending();
794
+ }
795
+ async evictIfNeeded(maxEntries) {
796
+ this.l1.evictIfNeeded(maxEntries);
797
+ }
798
+ schedulePersist() {
799
+ if (this.persistScheduled) {
800
+ return;
801
+ }
802
+ this.persistScheduled = true;
803
+ queueMicrotask(() => {
804
+ this.persistScheduled = false;
805
+ void this.flushPending();
806
+ });
807
+ }
808
+ async flushPending() {
809
+ if (this.pendingSets.size === 0 && this.pendingTouches.size === 0) {
810
+ return;
811
+ }
812
+ const db = this.requireDb();
813
+ if (!db) {
814
+ this.pendingSets.clear();
815
+ this.pendingTouches.clear();
816
+ return;
817
+ }
818
+ const sets = [...this.pendingSets.entries()];
819
+ this.pendingSets.clear();
820
+ const touches = [...this.pendingTouches];
821
+ this.pendingTouches.clear();
822
+ const now = (/* @__PURE__ */ new Date()).toISOString();
823
+ try {
824
+ const stmts = this.ensureStatements(db);
825
+ try {
826
+ db.exec("BEGIN IMMEDIATE");
827
+ } catch {
828
+ for (const [key, value] of sets) {
829
+ this.pendingSets.set(key, value);
830
+ }
831
+ for (const key of touches) {
832
+ this.pendingTouches.add(key);
833
+ }
834
+ this.schedulePersist();
835
+ return;
836
+ }
837
+ try {
838
+ for (const [key, value] of sets) {
839
+ stmts.set.run(
840
+ key,
841
+ value.model,
842
+ embeddingToBuffer(value.vector),
843
+ new Date(value.createdAt).toISOString(),
844
+ now
845
+ );
846
+ }
847
+ for (const key of touches) {
848
+ stmts.touch.run(now, key);
849
+ }
850
+ db.exec("COMMIT");
851
+ } catch {
852
+ try {
853
+ db.exec("ROLLBACK");
854
+ } catch {
855
+ }
856
+ this.stmts = null;
857
+ }
858
+ } catch {
859
+ this.stmts = null;
860
+ }
861
+ }
862
+ };
863
+ var MemoryEmbeddingCacheStore = class {
864
+ l1;
865
+ ttlMs;
866
+ constructor(options) {
867
+ this.ttlMs = options?.ttlMs ?? null;
868
+ this.l1 = new L1Cache(options?.maxEntries ?? L1_DEFAULT_MAX);
869
+ }
870
+ async get(cacheKey) {
871
+ return this.l1.get(cacheKey, this.ttlMs);
872
+ }
873
+ async set(cacheKey, model, vector) {
874
+ this.l1.set(cacheKey, model, vector);
875
+ }
876
+ async touch(cacheKey) {
877
+ this.l1.touch(cacheKey);
878
+ }
879
+ async evictIfNeeded(maxEntries) {
880
+ this.l1.evictIfNeeded(maxEntries);
881
+ }
882
+ };
883
+ var PostgresEmbeddingCacheStore = class {
884
+ ttlMs;
885
+ getClient;
886
+ l1 = new L1Cache();
887
+ pendingTouches = /* @__PURE__ */ new Set();
888
+ persistChain = Promise.resolve();
889
+ durableEnabled;
890
+ constructor(getClient, options) {
891
+ this.getClient = getClient;
892
+ this.ttlMs = options?.ttlMs ?? null;
893
+ this.durableEnabled = options?.durable === true;
894
+ }
895
+ async get(cacheKey) {
896
+ return this.l1.get(cacheKey, this.ttlMs);
897
+ }
898
+ async set(cacheKey, model, vector) {
899
+ this.l1.set(cacheKey, model, vector);
900
+ if (!this.durableEnabled) {
901
+ return;
902
+ }
903
+ const now = Date.now();
904
+ this.enqueuePersist(async (client) => {
905
+ const iso = new Date(now).toISOString();
906
+ await client.query(
907
+ `INSERT INTO embedding_cache (cache_key, model, vector, created_at, last_used_at)
908
+ VALUES ($1, $2, $3, $4::timestamptz, $5::timestamptz)
909
+ ON CONFLICT (cache_key) DO UPDATE SET
910
+ model = EXCLUDED.model,
911
+ vector = EXCLUDED.vector,
912
+ last_used_at = EXCLUDED.last_used_at`,
913
+ [cacheKey, model, embeddingToBuffer(vector), iso, iso]
914
+ );
915
+ });
916
+ }
917
+ async touch(cacheKey) {
918
+ this.l1.touch(cacheKey);
919
+ if (!this.durableEnabled) {
920
+ return;
921
+ }
922
+ this.pendingTouches.add(cacheKey);
923
+ if (this.pendingTouches.size >= TOUCH_FLUSH_THRESHOLD) {
924
+ void this.flushTouches();
925
+ }
926
+ }
927
+ async flushTouches() {
928
+ if (!this.durableEnabled || this.pendingTouches.size === 0) {
929
+ this.pendingTouches.clear();
930
+ return;
931
+ }
932
+ const keys = [...this.pendingTouches];
933
+ this.pendingTouches.clear();
934
+ const now = (/* @__PURE__ */ new Date()).toISOString();
935
+ this.enqueuePersist(async (client) => {
936
+ await client.query(
937
+ `UPDATE embedding_cache
938
+ SET last_used_at = $1::timestamptz
939
+ WHERE cache_key = ANY($2::text[])`,
940
+ [now, keys]
941
+ );
942
+ });
943
+ }
944
+ async evictIfNeeded(maxEntries) {
945
+ this.l1.evictIfNeeded(maxEntries);
946
+ }
947
+ enqueuePersist(work) {
948
+ this.persistChain = this.persistChain.then(async () => {
949
+ const client = this.getClient();
950
+ if (!client) {
951
+ return;
952
+ }
953
+ await work(client);
954
+ }).catch(() => void 0);
955
+ }
956
+ };
551
957
 
552
958
  // src/llm/index.ts
553
959
  var OpenAICompatibleLlmProvider = class {
@@ -902,8 +1308,104 @@ function matchesMetadata(metadata, filter) {
902
1308
  return compare(getField(metadata, filter.field), filter.op);
903
1309
  }
904
1310
 
1311
+ // src/filters/sql-compile.ts
1312
+ var FIELD_RE = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
1313
+ function jsonPath(field) {
1314
+ if (!FIELD_RE.test(field)) {
1315
+ return null;
1316
+ }
1317
+ return `$.${field}`;
1318
+ }
1319
+ function compileComparison(field, op) {
1320
+ const path7 = jsonPath(field);
1321
+ if (!path7) {
1322
+ return null;
1323
+ }
1324
+ const extract = `json_extract(metadata_json, '${path7}')`;
1325
+ if ("eq" in op) {
1326
+ const value = op.eq;
1327
+ if (value !== null && typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
1328
+ return null;
1329
+ }
1330
+ return { expression: `${extract} = ?`, params: [value] };
1331
+ }
1332
+ if ("contains" in op) {
1333
+ return {
1334
+ expression: `CAST(${extract} AS TEXT) LIKE '%' || ? || '%'`,
1335
+ params: [op.contains]
1336
+ };
1337
+ }
1338
+ if ("gt" in op) {
1339
+ return { expression: `${extract} > ?`, params: [op.gt] };
1340
+ }
1341
+ if ("gte" in op) {
1342
+ return { expression: `${extract} >= ?`, params: [op.gte] };
1343
+ }
1344
+ if ("lt" in op) {
1345
+ return { expression: `${extract} < ?`, params: [op.lt] };
1346
+ }
1347
+ if ("lte" in op) {
1348
+ return { expression: `${extract} <= ?`, params: [op.lte] };
1349
+ }
1350
+ if ("between" in op) {
1351
+ const [lo, hi] = op.between;
1352
+ return {
1353
+ expression: `${extract} >= ? AND ${extract} <= ?`,
1354
+ params: [lo, hi]
1355
+ };
1356
+ }
1357
+ return null;
1358
+ }
1359
+ function compileMetadataFilterToSql(filter) {
1360
+ if ("and" in filter) {
1361
+ if (filter.and.length === 0) {
1362
+ return { expression: "1", params: [] };
1363
+ }
1364
+ const parts = [];
1365
+ for (const child of filter.and) {
1366
+ const compiled = compileMetadataFilterToSql(child);
1367
+ if (!compiled) {
1368
+ return null;
1369
+ }
1370
+ parts.push(compiled);
1371
+ }
1372
+ return {
1373
+ expression: parts.map((p) => `(${p.expression})`).join(" AND "),
1374
+ params: parts.flatMap((p) => p.params)
1375
+ };
1376
+ }
1377
+ if ("or" in filter) {
1378
+ if (filter.or.length === 0) {
1379
+ return { expression: "0", params: [] };
1380
+ }
1381
+ const parts = [];
1382
+ for (const child of filter.or) {
1383
+ const compiled = compileMetadataFilterToSql(child);
1384
+ if (!compiled) {
1385
+ return null;
1386
+ }
1387
+ parts.push(compiled);
1388
+ }
1389
+ return {
1390
+ expression: parts.map((p) => `(${p.expression})`).join(" OR "),
1391
+ params: parts.flatMap((p) => p.params)
1392
+ };
1393
+ }
1394
+ if ("not" in filter) {
1395
+ const inner = compileMetadataFilterToSql(filter.not);
1396
+ if (!inner) {
1397
+ return null;
1398
+ }
1399
+ return {
1400
+ expression: `NOT (${inner.expression})`,
1401
+ params: inner.params
1402
+ };
1403
+ }
1404
+ return compileComparison(filter.field, filter.op);
1405
+ }
1406
+
905
1407
  // src/schema/index.ts
906
- var SCHEMA_VERSION = 2;
1408
+ var SCHEMA_VERSION = 4;
907
1409
  var META_KEYS = {
908
1410
  schemaVersion: "schema_version",
909
1411
  embeddingDimensions: "embedding_dimensions",
@@ -924,6 +1426,7 @@ CREATE TABLE IF NOT EXISTS memories (
924
1426
  metadata_json TEXT NOT NULL DEFAULT '{}',
925
1427
  archived INTEGER NOT NULL DEFAULT 0 CHECK (archived IN (0, 1)),
926
1428
  compressed_into TEXT NULL,
1429
+ content_hash TEXT NULL,
927
1430
  created_at TEXT NOT NULL,
928
1431
  updated_at TEXT NOT NULL,
929
1432
  FOREIGN KEY (compressed_into) REFERENCES memories(id) ON DELETE SET NULL
@@ -933,7 +1436,7 @@ var CREATE_HISTORY_TABLE = `
933
1436
  CREATE TABLE IF NOT EXISTS memory_history (
934
1437
  id TEXT PRIMARY KEY NOT NULL,
935
1438
  memory_id TEXT NOT NULL,
936
- event_type TEXT NOT NULL CHECK (event_type IN ('created', 'archived', 'compressed')),
1439
+ event_type TEXT NOT NULL CHECK (event_type IN ('created', 'archived', 'compressed', 'updated')),
937
1440
  related_memory_id TEXT NULL,
938
1441
  created_at TEXT NOT NULL,
939
1442
  FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
@@ -954,14 +1457,33 @@ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
954
1457
  tokenize = 'porter unicode61'
955
1458
  );
956
1459
  `;
1460
+ var CREATE_EMBEDDING_CACHE_TABLE = `
1461
+ CREATE TABLE IF NOT EXISTS embedding_cache (
1462
+ cache_key TEXT PRIMARY KEY NOT NULL,
1463
+ model TEXT NOT NULL,
1464
+ vector BLOB NOT NULL,
1465
+ created_at TEXT NOT NULL,
1466
+ last_used_at TEXT NOT NULL
1467
+ );
1468
+ `;
957
1469
  var CREATE_INDEXES = [
958
1470
  `CREATE INDEX IF NOT EXISTS idx_memories_org_agent ON memories(organization, agent);`,
959
1471
  `CREATE INDEX IF NOT EXISTS idx_memories_org_archived ON memories(organization, archived);`,
960
- /** Active-set covering path for org-scoped list / stats / compress. */
1472
+ /** Active-set path for org-scoped list / stats / compress. */
961
1473
  `CREATE INDEX IF NOT EXISTS idx_memories_org_active_created
962
1474
  ON memories(organization, created_at) WHERE archived = 0;`,
963
- `CREATE INDEX IF NOT EXISTS idx_memories_created_at ON memories(created_at);`,
964
- `CREATE INDEX IF NOT EXISTS idx_history_memory_id ON memory_history(memory_id);`
1475
+ /** Agent-scoped active list (compress / forget-by-agent). */
1476
+ `CREATE INDEX IF NOT EXISTS idx_memories_org_agent_active_created
1477
+ ON memories(organization, agent, created_at) WHERE archived = 0;`,
1478
+ `CREATE INDEX IF NOT EXISTS idx_history_memory_id ON memory_history(memory_id);`,
1479
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_org_agent_hash_active
1480
+ ON memories(organization, agent, content_hash)
1481
+ WHERE archived = 0 AND content_hash IS NOT NULL;`,
1482
+ `CREATE INDEX IF NOT EXISTS idx_embedding_cache_last_used
1483
+ ON embedding_cache(last_used_at);`
1484
+ ];
1485
+ var DROP_REDUNDANT_INDEXES_V4 = [
1486
+ `DROP INDEX IF EXISTS idx_memories_created_at;`
965
1487
  ];
966
1488
  function buildVectorTableSql(dimensions) {
967
1489
  if (!Number.isInteger(dimensions) || dimensions <= 0) {
@@ -985,35 +1507,42 @@ var SQL = {
985
1507
  insertMemory: `
986
1508
  INSERT INTO memories (
987
1509
  id, organization, agent, content_text, metadata_json,
988
- archived, compressed_into, created_at, updated_at
989
- ) VALUES (?, ?, ?, ?, ?, 0, NULL, ?, ?)
1510
+ archived, compressed_into, content_hash, created_at, updated_at
1511
+ ) VALUES (?, ?, ?, ?, ?, 0, NULL, ?, ?, ?)
990
1512
  RETURNING rowid, id, organization, agent, content_text, metadata_json,
991
- archived, compressed_into, created_at, updated_at
1513
+ archived, compressed_into, content_hash, created_at, updated_at
992
1514
  `,
993
1515
  getMemoryById: `
994
1516
  SELECT rowid, id, organization, agent, content_text, metadata_json,
995
- archived, compressed_into, created_at, updated_at
1517
+ archived, compressed_into, content_hash, created_at, updated_at
996
1518
  FROM memories
997
1519
  WHERE id = ? AND organization = ?
998
1520
  `,
999
1521
  getMemoryByRowid: `
1000
1522
  SELECT rowid, id, organization, agent, content_text, metadata_json,
1001
- archived, compressed_into, created_at, updated_at
1523
+ archived, compressed_into, content_hash, created_at, updated_at
1002
1524
  FROM memories
1003
1525
  WHERE rowid = ? AND organization = ?
1004
1526
  `,
1005
1527
  getMemoriesByRowidsPrefix: `
1006
1528
  SELECT rowid, id, organization, agent, content_text, metadata_json,
1007
- archived, compressed_into, created_at, updated_at
1529
+ archived, compressed_into, content_hash, created_at, updated_at
1008
1530
  FROM memories
1009
1531
  WHERE organization = ? AND rowid IN (
1010
1532
  `,
1011
1533
  listMemoriesBase: `
1012
1534
  SELECT rowid, id, organization, agent, content_text, metadata_json,
1013
- archived, compressed_into, created_at, updated_at
1535
+ archived, compressed_into, content_hash, created_at, updated_at
1014
1536
  FROM memories
1015
1537
  WHERE organization = ?
1016
1538
  `,
1539
+ findActiveByContentHash: `
1540
+ SELECT rowid, id, organization, agent, content_text, metadata_json,
1541
+ archived, compressed_into, content_hash, created_at, updated_at
1542
+ FROM memories
1543
+ WHERE organization = ? AND agent = ? AND content_hash = ? AND archived = 0
1544
+ LIMIT 1
1545
+ `,
1017
1546
  insertEmbedding: `
1018
1547
  INSERT INTO memory_embeddings (memory_rowid, embedding) VALUES (?, ?)
1019
1548
  `,
@@ -1096,6 +1625,7 @@ var SQL = {
1096
1625
  UPDATE memories
1097
1626
  SET content_text = COALESCE(?, content_text),
1098
1627
  metadata_json = COALESCE(?, metadata_json),
1628
+ content_hash = COALESCE(?, content_hash),
1099
1629
  updated_at = ?
1100
1630
  WHERE id = ? AND organization = ?
1101
1631
  `,
@@ -1105,6 +1635,52 @@ var SQL = {
1105
1635
  `,
1106
1636
  deleteFts: `
1107
1637
  DELETE FROM memories_fts WHERE memory_id = ?
1638
+ `,
1639
+ deleteFtsByOrg: `
1640
+ DELETE FROM memories_fts WHERE organization = ?
1641
+ `,
1642
+ deleteFtsByOrgAgent: `
1643
+ DELETE FROM memories_fts WHERE organization = ? AND agent = ?
1644
+ `,
1645
+ deleteEmbeddingsByOrg: `
1646
+ DELETE FROM memory_embeddings WHERE memory_rowid IN (
1647
+ SELECT rowid FROM memories WHERE organization = ?
1648
+ )
1649
+ `,
1650
+ deleteEmbeddingsByOrgAgent: `
1651
+ DELETE FROM memory_embeddings WHERE memory_rowid IN (
1652
+ SELECT rowid FROM memories WHERE organization = ? AND agent = ?
1653
+ )
1654
+ `,
1655
+ deleteEmbeddingsBlobByOrg: `
1656
+ DELETE FROM memory_embeddings_blob WHERE memory_rowid IN (
1657
+ SELECT rowid FROM memories WHERE organization = ?
1658
+ )
1659
+ `,
1660
+ deleteEmbeddingsBlobByOrgAgent: `
1661
+ DELETE FROM memory_embeddings_blob WHERE memory_rowid IN (
1662
+ SELECT rowid FROM memories WHERE organization = ? AND agent = ?
1663
+ )
1664
+ `,
1665
+ deleteArchivedEmbeddings: `
1666
+ DELETE FROM memory_embeddings WHERE memory_rowid IN (
1667
+ SELECT rowid FROM memories WHERE archived = 1
1668
+ )
1669
+ `,
1670
+ deleteArchivedEmbeddingsBlob: `
1671
+ DELETE FROM memory_embeddings_blob WHERE memory_rowid IN (
1672
+ SELECT rowid FROM memories WHERE archived = 1
1673
+ )
1674
+ `,
1675
+ /** Single-pass org stats (avoids 4 separate COUNT queries). */
1676
+ getStats: `
1677
+ SELECT
1678
+ COUNT(*) AS total,
1679
+ COALESCE(SUM(CASE WHEN archived = 0 THEN 1 ELSE 0 END), 0) AS active,
1680
+ COALESCE(SUM(CASE WHEN archived = 1 THEN 1 ELSE 0 END), 0) AS archived,
1681
+ COUNT(DISTINCT CASE WHEN archived = 0 THEN agent END) AS agents
1682
+ FROM memories
1683
+ WHERE organization = ?
1108
1684
  `
1109
1685
  };
1110
1686
 
@@ -1310,10 +1886,140 @@ function siftDown(dist, rows, i, n) {
1310
1886
  }
1311
1887
  }
1312
1888
 
1889
+ // src/storage/sqlite/concurrency-config.ts
1890
+ var DEFAULT_CONCURRENCY = {
1891
+ maxRetries: 5,
1892
+ baseBackoffMs: 50,
1893
+ maxBackoffMs: 2e3,
1894
+ lockTimeoutMs: 5e3
1895
+ };
1896
+ function resolveConcurrencyConfig(input) {
1897
+ const resolved = {
1898
+ maxRetries: input?.maxRetries ?? DEFAULT_CONCURRENCY.maxRetries,
1899
+ baseBackoffMs: input?.baseBackoffMs ?? DEFAULT_CONCURRENCY.baseBackoffMs,
1900
+ maxBackoffMs: input?.maxBackoffMs ?? DEFAULT_CONCURRENCY.maxBackoffMs,
1901
+ lockTimeoutMs: input?.lockTimeoutMs ?? DEFAULT_CONCURRENCY.lockTimeoutMs
1902
+ };
1903
+ assertPositiveInt(resolved.maxRetries, "concurrency.maxRetries");
1904
+ assertPositiveNumber(resolved.baseBackoffMs, "concurrency.baseBackoffMs");
1905
+ assertPositiveNumber(resolved.maxBackoffMs, "concurrency.maxBackoffMs");
1906
+ assertPositiveInt(resolved.lockTimeoutMs, "concurrency.lockTimeoutMs");
1907
+ if (resolved.maxBackoffMs < resolved.baseBackoffMs) {
1908
+ throw new ConfigurationError(
1909
+ "concurrency.maxBackoffMs must be >= concurrency.baseBackoffMs"
1910
+ );
1911
+ }
1912
+ return resolved;
1913
+ }
1914
+ function assertPositiveInt(value, field) {
1915
+ if (!Number.isInteger(value) || value < 0) {
1916
+ throw new ConfigurationError(`${field} must be a non-negative integer`);
1917
+ }
1918
+ }
1919
+ function assertPositiveNumber(value, field) {
1920
+ if (!Number.isFinite(value) || value < 0) {
1921
+ throw new ConfigurationError(`${field} must be a non-negative number`);
1922
+ }
1923
+ }
1924
+
1925
+ // src/storage/sqlite/transaction.ts
1926
+ function isSqliteBusyError(error) {
1927
+ const raw = error instanceof Error ? error.message : String(error);
1928
+ const lower = raw.toLowerCase();
1929
+ return lower.includes("database is locked") || lower.includes("sqlite_busy") || lower.includes("busy");
1930
+ }
1931
+ function sleep(ms) {
1932
+ return new Promise((resolve) => setTimeout(resolve, ms));
1933
+ }
1934
+ function backoffDelay(attempt, config) {
1935
+ const exp = Math.min(
1936
+ config.maxBackoffMs,
1937
+ config.baseBackoffMs * 2 ** attempt
1938
+ );
1939
+ const jitter = Math.random() * config.baseBackoffMs;
1940
+ return Math.min(config.maxBackoffMs, exp + jitter);
1941
+ }
1942
+ async function withImmediateTransaction(db, config, fn, onRetry) {
1943
+ let lastError;
1944
+ for (let attempt = 0; attempt <= config.maxRetries; attempt += 1) {
1945
+ try {
1946
+ db.exec("BEGIN IMMEDIATE");
1947
+ try {
1948
+ const result = await fn();
1949
+ db.exec("COMMIT");
1950
+ return result;
1951
+ } catch (error) {
1952
+ try {
1953
+ db.exec("ROLLBACK");
1954
+ } catch {
1955
+ }
1956
+ throw error;
1957
+ }
1958
+ } catch (error) {
1959
+ lastError = error;
1960
+ const isBusy = isSqliteBusyError(error);
1961
+ if (!isBusy) {
1962
+ throw error;
1963
+ }
1964
+ if (attempt >= config.maxRetries) {
1965
+ break;
1966
+ }
1967
+ const delay = backoffDelay(attempt, config);
1968
+ onRetry?.(attempt + 1, delay, error);
1969
+ await sleep(delay);
1970
+ }
1971
+ }
1972
+ throw new StorageLockedError(
1973
+ `SQLite write lock could not be acquired after ${config.maxRetries} retries`,
1974
+ {
1975
+ cause: lastError instanceof Error ? lastError : void 0,
1976
+ reason: "SQLITE_BUSY exhausted retries",
1977
+ suggestion: "Increase concurrency.maxRetries or concurrency.lockTimeoutMs, or consider the Postgres backend for high-concurrency multi-agent workloads."
1978
+ }
1979
+ );
1980
+ }
1981
+ function normalizeMemoryContent(text) {
1982
+ return text.normalize("NFC").trim().replace(/\s+/g, " ");
1983
+ }
1984
+ function hashMemoryContent(text) {
1985
+ const normalized = normalizeMemoryContent(text);
1986
+ return createHash("sha256").update(normalized, "utf8").digest("hex");
1987
+ }
1988
+ var DEFAULT_MEMORY_DEDUPE = {
1989
+ enabled: false,
1990
+ strategy: "exact-or-near",
1991
+ nearThreshold: 0.92,
1992
+ nearCandidateLimit: 8
1993
+ };
1994
+ function resolveMemoryDedupeConfig(input) {
1995
+ if (input === void 0) {
1996
+ return { ...DEFAULT_MEMORY_DEDUPE };
1997
+ }
1998
+ if (typeof input === "boolean") {
1999
+ return { ...DEFAULT_MEMORY_DEDUPE, enabled: input };
2000
+ }
2001
+ return {
2002
+ enabled: input.enabled ?? true,
2003
+ strategy: input.strategy ?? DEFAULT_MEMORY_DEDUPE.strategy,
2004
+ nearThreshold: input.nearThreshold ?? DEFAULT_MEMORY_DEDUPE.nearThreshold,
2005
+ nearCandidateLimit: input.nearCandidateLimit ?? DEFAULT_MEMORY_DEDUPE.nearCandidateLimit
2006
+ };
2007
+ }
2008
+ function mergeMemoryMetadata(existing, incoming) {
2009
+ return { ...existing, ...incoming };
2010
+ }
2011
+
1313
2012
  // src/storage/providers/sqlite.ts
1314
- var SqliteStorageProvider = class {
2013
+ var MAX_METADATA_SCAN = 5e4;
2014
+ var METADATA_PAGE_SIZE = 500;
2015
+ var BATCH_INSERT_CHUNK = 96;
2016
+ var MAX_ANN_OVERFETCH = 8192;
2017
+ var INSERT_COALESCE_THRESHOLD = 24;
2018
+ var INSERT_COALESCE_MAX = 96;
2019
+ var SqliteStorageProvider = class _SqliteStorageProvider {
1315
2020
  name = "sqlite";
1316
2021
  connectionString;
2022
+ concurrency;
1317
2023
  db = null;
1318
2024
  statements = null;
1319
2025
  vectorDimensions = null;
@@ -1324,13 +2030,37 @@ var SqliteStorageProvider = class {
1324
2030
  memoryIndexDirty = false;
1325
2031
  /** Resolved absolute path (or `:memory:`) — avoid re-resolving on size checks. */
1326
2032
  resolvedPath = null;
2033
+ retryLog = null;
2034
+ /** Cached prepared statements for `rowid IN (…)` lookups keyed by list length. */
2035
+ rowidInStatements = /* @__PURE__ */ new Map();
2036
+ /** Cached list SQL statements keyed by clause shape. */
2037
+ listStatements = /* @__PURE__ */ new Map();
2038
+ /** Cached multi-row insert statements keyed by row count. */
2039
+ batchInsertStatements = /* @__PURE__ */ new Map();
2040
+ /** Cached multi-row history inserts keyed by row count. */
2041
+ batchHistoryStatements = /* @__PURE__ */ new Map();
2042
+ /** Cached multi-row FTS inserts keyed by row count. */
2043
+ batchFtsStatements = /* @__PURE__ */ new Map();
2044
+ /** Cached multi-row blob embedding inserts keyed by row count. */
2045
+ batchBlobEmbStatements = /* @__PURE__ */ new Map();
2046
+ /** Coalesce concurrent insertMemory callers into one IMMEDIATE transaction. */
2047
+ insertQueue = [];
2048
+ insertFlushScheduled = false;
1327
2049
  constructor(options) {
1328
2050
  this.connectionString = options.connectionString;
2051
+ this.concurrency = resolveConcurrencyConfig(options.concurrency);
1329
2052
  }
1330
2053
  /** Absolute or relative path / `:memory:` used by this provider. */
1331
2054
  get path() {
1332
2055
  return this.connectionString;
1333
2056
  }
2057
+ /** Expose DB for embedding cache store (same connection). */
2058
+ getDatabase() {
2059
+ return this.db;
2060
+ }
2061
+ setRetryLogger(fn) {
2062
+ this.retryLog = fn;
2063
+ }
1334
2064
  async open() {
1335
2065
  try {
1336
2066
  const dbPath = this.resolvePath(this.connectionString);
@@ -1344,17 +2074,16 @@ var SqliteStorageProvider = class {
1344
2074
  db.exec(`
1345
2075
  PRAGMA synchronous = NORMAL;
1346
2076
  PRAGMA foreign_keys = ON;
1347
- PRAGMA busy_timeout = 5000;
2077
+ PRAGMA busy_timeout = ${this.concurrency.lockTimeoutMs};
1348
2078
  PRAGMA temp_store = MEMORY;
1349
- PRAGMA cache_size = -65536;
1350
- PRAGMA mmap_size = 268435456;
1351
- PRAGMA wal_autocheckpoint = 1000;
2079
+ PRAGMA cache_size = -32768;
2080
+ PRAGMA mmap_size = 134217728;
2081
+ PRAGMA wal_autocheckpoint = 2000;
1352
2082
  PRAGMA recursive_triggers = OFF;
1353
2083
  `);
1354
2084
  this.sqliteVecLoaded = this.tryLoadSqliteVec(db);
1355
2085
  this.runMigrations(db);
1356
2086
  this.statements = this.prepareStatements(db);
1357
- this.ensureFtsConsistency(db);
1358
2087
  const backend = this.readMetaString(META_KEYS.vectorBackend);
1359
2088
  const dims = this.readMetaNumber(META_KEYS.embeddingDimensions);
1360
2089
  if (backend) {
@@ -1386,10 +2115,18 @@ var SqliteStorageProvider = class {
1386
2115
  }
1387
2116
  }
1388
2117
  async close() {
2118
+ const deadline = Date.now() + 2e3;
2119
+ while (this.insertQueue.length > 0 && Date.now() < deadline) {
2120
+ await this.flushInsertQueue();
2121
+ }
1389
2122
  if (!this.db) {
1390
2123
  return;
1391
2124
  }
1392
2125
  try {
2126
+ try {
2127
+ this.db.exec("PRAGMA optimize;");
2128
+ } catch {
2129
+ }
1393
2130
  this.db.close();
1394
2131
  } catch (error) {
1395
2132
  throw new DatabaseError(`Failed to close database: ${this.describe(error)}`, {
@@ -1399,6 +2136,12 @@ var SqliteStorageProvider = class {
1399
2136
  this.db = null;
1400
2137
  this.statements = null;
1401
2138
  this.memoryIndex = null;
2139
+ this.rowidInStatements.clear();
2140
+ this.listStatements.clear();
2141
+ this.batchInsertStatements.clear();
2142
+ this.batchHistoryStatements.clear();
2143
+ this.batchFtsStatements.clear();
2144
+ this.batchBlobEmbStatements.clear();
1402
2145
  }
1403
2146
  }
1404
2147
  async ensureVectorSchema(dimensions) {
@@ -1431,16 +2174,64 @@ var SqliteStorageProvider = class {
1431
2174
  this.vectorDimensions = dimensions;
1432
2175
  }
1433
2176
  async insertMemory(input) {
1434
- const stmts = this.requireStatements();
1435
2177
  this.requireVectorReady();
1436
- return this.withTransaction(() => {
1437
- const row = stmts.insertMemory.get(
1438
- input.id,
1439
- input.organization,
1440
- input.agent,
1441
- input.contentText,
1442
- serializeMetadata(input.metadata),
1443
- input.createdAt,
2178
+ return new Promise((resolve, reject) => {
2179
+ this.insertQueue.push({ input, resolve, reject });
2180
+ if (this.insertQueue.length >= INSERT_COALESCE_THRESHOLD) {
2181
+ this.insertFlushScheduled = false;
2182
+ void this.flushInsertQueue();
2183
+ return;
2184
+ }
2185
+ this.scheduleInsertFlush();
2186
+ });
2187
+ }
2188
+ scheduleInsertFlush() {
2189
+ if (this.insertFlushScheduled) {
2190
+ return;
2191
+ }
2192
+ this.insertFlushScheduled = true;
2193
+ setImmediate(() => {
2194
+ this.insertFlushScheduled = false;
2195
+ void this.flushInsertQueue();
2196
+ });
2197
+ }
2198
+ async flushInsertQueue() {
2199
+ if (this.insertQueue.length === 0) {
2200
+ return;
2201
+ }
2202
+ const batch = this.insertQueue.splice(0, INSERT_COALESCE_MAX);
2203
+ try {
2204
+ if (batch.length === 1) {
2205
+ const row = await this.insertMemoryImmediate(batch[0].input);
2206
+ batch[0].resolve(row);
2207
+ } else {
2208
+ const rows = await this.insertMemoriesBatch(batch.map((b) => b.input));
2209
+ for (let i = 0; i < batch.length; i += 1) {
2210
+ batch[i].resolve(rows[i]);
2211
+ }
2212
+ }
2213
+ } catch (error) {
2214
+ for (const item of batch) {
2215
+ item.reject(error);
2216
+ }
2217
+ }
2218
+ if (this.insertQueue.length > 0) {
2219
+ void this.flushInsertQueue();
2220
+ }
2221
+ }
2222
+ /** Single-row insert without coalescing (used by flush of size 1). */
2223
+ async insertMemoryImmediate(input) {
2224
+ const stmts = this.requireStatements();
2225
+ const contentHash = input.contentHash !== void 0 ? input.contentHash : hashMemoryContent(input.contentText);
2226
+ return this.withTransaction(() => {
2227
+ const row = stmts.insertMemory.get(
2228
+ input.id,
2229
+ input.organization,
2230
+ input.agent,
2231
+ input.contentText,
2232
+ serializeMetadata(input.metadata),
2233
+ contentHash,
2234
+ input.createdAt,
1444
2235
  input.updatedAt
1445
2236
  );
1446
2237
  if (!row || row.rowid === void 0) {
@@ -1467,38 +2258,13 @@ var SqliteStorageProvider = class {
1467
2258
  if (inputs.length === 0) {
1468
2259
  return [];
1469
2260
  }
1470
- const stmts = this.requireStatements();
1471
2261
  this.requireVectorReady();
1472
2262
  return this.withTransaction(() => {
1473
2263
  const rows = [];
1474
- for (const input of inputs) {
1475
- const row = stmts.insertMemory.get(
1476
- input.id,
1477
- input.organization,
1478
- input.agent,
1479
- input.contentText,
1480
- serializeMetadata(input.metadata),
1481
- input.createdAt,
1482
- input.updatedAt
1483
- );
1484
- if (!row || row.rowid === void 0) {
1485
- throw new DatabaseError("Failed to read memory after batch insert");
1486
- }
1487
- this.insertEmbedding(row.rowid, input.embedding);
1488
- this.insertFtsRow(
1489
- input.id,
1490
- input.organization,
1491
- input.agent,
1492
- input.contentText
1493
- );
1494
- stmts.insertHistory.run(
1495
- crypto.randomUUID(),
1496
- input.id,
1497
- "created",
1498
- null,
1499
- input.createdAt
1500
- );
1501
- rows.push(row);
2264
+ for (let offset = 0; offset < inputs.length; offset += BATCH_INSERT_CHUNK) {
2265
+ const chunk = inputs.slice(offset, offset + BATCH_INSERT_CHUNK);
2266
+ const inserted = this.insertMemoryChunk(chunk);
2267
+ rows.push(...inserted);
1502
2268
  }
1503
2269
  return rows;
1504
2270
  });
@@ -1515,9 +2281,11 @@ var SqliteStorageProvider = class {
1515
2281
  }
1516
2282
  const contentText = input.contentText ?? existing.content_text;
1517
2283
  const metadataJson = input.metadata !== void 0 ? serializeMetadata(input.metadata) : existing.metadata_json;
2284
+ const contentHash = input.contentHash !== void 0 ? input.contentHash : input.contentText !== void 0 ? hashMemoryContent(input.contentText) : existing.content_hash ?? null;
1518
2285
  stmts.updateMemoryContent.run(
1519
2286
  input.contentText ?? null,
1520
2287
  input.metadata !== void 0 ? metadataJson : null,
2288
+ contentHash,
1521
2289
  input.updatedAt,
1522
2290
  input.id,
1523
2291
  input.organization
@@ -1534,9 +2302,25 @@ var SqliteStorageProvider = class {
1534
2302
  contentText
1535
2303
  );
1536
2304
  }
2305
+ stmts.insertHistory.run(
2306
+ crypto.randomUUID(),
2307
+ input.id,
2308
+ "updated",
2309
+ null,
2310
+ input.updatedAt
2311
+ );
1537
2312
  return stmts.getMemoryById.get(input.id, input.organization);
1538
2313
  });
1539
2314
  }
2315
+ async findActiveByContentHash(organization, agent, contentHash) {
2316
+ const stmts = this.requireStatements();
2317
+ const row = stmts.findActiveByContentHash.get(
2318
+ organization,
2319
+ agent,
2320
+ contentHash
2321
+ );
2322
+ return row ?? null;
2323
+ }
1540
2324
  async searchByMetadata(filter, limit) {
1541
2325
  const rows = await this.listMemories(filter, limit);
1542
2326
  if (!filter.metadata) {
@@ -1582,13 +2366,11 @@ var SqliteStorageProvider = class {
1582
2366
  if (rowids.length === 0) {
1583
2367
  return out;
1584
2368
  }
1585
- const db = this.requireDb();
1586
2369
  const chunkSize = 400;
1587
2370
  for (let offset = 0; offset < rowids.length; offset += chunkSize) {
1588
2371
  const chunk = rowids.slice(offset, offset + chunkSize);
1589
- const placeholders = chunk.map(() => "?").join(",");
1590
- const sql = `${SQL.getMemoriesByRowidsPrefix}${placeholders})`;
1591
- const rows = db.prepare(sql).all(organization, ...chunk);
2372
+ const stmt = this.getRowidInStatement(chunk.length);
2373
+ const rows = stmt.all(organization, ...chunk);
1592
2374
  for (const row of rows) {
1593
2375
  if (row.rowid !== void 0) {
1594
2376
  out.set(row.rowid, row);
@@ -1598,41 +2380,11 @@ var SqliteStorageProvider = class {
1598
2380
  return out;
1599
2381
  }
1600
2382
  async listMemories(filter, limit) {
1601
- const db = this.requireDb();
1602
- const clauses = [`organization = ?`];
1603
- const params = [filter.organization];
1604
- if (filter.agent) {
1605
- clauses.push(`agent = ?`);
1606
- params.push(filter.agent);
1607
- }
1608
- if (!filter.includeArchived) {
1609
- clauses.push(`archived = 0`);
1610
- }
1611
- let sql = `
1612
- SELECT rowid, id, organization, agent, content_text, metadata_json,
1613
- archived, compressed_into, created_at, updated_at
1614
- FROM memories
1615
- WHERE ${clauses.join(" AND ")}
1616
- ORDER BY created_at ASC
1617
- `;
1618
- if (limit !== void 0 && !filter.metadata) {
1619
- sql += ` LIMIT ?`;
1620
- params.push(limit);
1621
- }
1622
2383
  try {
1623
- let rows = db.prepare(sql).all(...params);
1624
2384
  if (filter.metadata) {
1625
- rows = rows.filter(
1626
- (row) => matchesMetadata(
1627
- deserializeMetadata(row.metadata_json),
1628
- filter.metadata
1629
- )
1630
- );
1631
- if (limit !== void 0) {
1632
- rows = rows.slice(0, limit);
1633
- }
2385
+ return this.listMemoriesWithMetadata(filter, limit);
1634
2386
  }
1635
- return rows;
2387
+ return this.listMemoriesIndexed(filter, limit);
1636
2388
  } catch (error) {
1637
2389
  throw new DatabaseError(`Failed to list memories: ${this.describe(error)}`, {
1638
2390
  cause: error instanceof Error ? error : void 0
@@ -1647,26 +2399,44 @@ var SqliteStorageProvider = class {
1647
2399
  return this.searchWithBlobFallback(embedding, topK);
1648
2400
  }
1649
2401
  /**
1650
- * Org-scoped KNN + memory rows.
1651
- * Caller (Wolbarg.recall) already passes an overfetched topK do not multiply again.
2402
+ * Org-scoped KNN + memory rows with adaptive overfetch.
2403
+ * Global ANN is post-filtered by org/agent/archived; underfill triggers larger k.
1652
2404
  */
1653
2405
  async searchVectorsWithMemories(embedding, topK, organization, options) {
1654
2406
  this.requireVectorReady();
1655
- const hits = await this.searchVectors(embedding, topK);
1656
- if (hits.length === 0) {
2407
+ if (topK <= 0) {
1657
2408
  return [];
1658
2409
  }
1659
- const map = await this.getMemoriesByRowids(
1660
- hits.map((h) => h.memoryRowid),
1661
- organization
1662
- );
1663
- const out = [];
1664
- for (const hit of hits) {
1665
- const row = map.get(hit.memoryRowid);
1666
- if (!row) continue;
1667
- if (options?.agent && row.agent !== options.agent) continue;
1668
- if (!options?.includeArchived && row.archived === 1) continue;
1669
- out.push({ row, distance: hit.distance });
2410
+ let fetchK = topK;
2411
+ const maxFetch = Math.min(Math.max(topK * 64, 512), MAX_ANN_OVERFETCH);
2412
+ let out = [];
2413
+ while (true) {
2414
+ const hits = await this.searchVectors(embedding, fetchK);
2415
+ if (hits.length === 0) {
2416
+ return [];
2417
+ }
2418
+ const map = await this.getMemoriesByRowids(
2419
+ hits.map((h) => h.memoryRowid),
2420
+ organization
2421
+ );
2422
+ out = [];
2423
+ for (const hit of hits) {
2424
+ const row = map.get(hit.memoryRowid);
2425
+ if (!row) continue;
2426
+ if (options?.agent && row.agent !== options.agent) continue;
2427
+ if (!options?.includeArchived && row.archived === 1) continue;
2428
+ out.push({ row, distance: hit.distance });
2429
+ if (out.length >= topK) {
2430
+ return out;
2431
+ }
2432
+ }
2433
+ if (hits.length < fetchK) {
2434
+ break;
2435
+ }
2436
+ if (fetchK >= maxFetch) {
2437
+ break;
2438
+ }
2439
+ fetchK = Math.min(fetchK * 4, maxFetch);
1670
2440
  }
1671
2441
  return out;
1672
2442
  }
@@ -1675,6 +2445,7 @@ var SqliteStorageProvider = class {
1675
2445
  const archived = [];
1676
2446
  return this.withTransaction(() => {
1677
2447
  for (const id of ids) {
2448
+ const existing = stmts.getMemoryById.get(id, organization);
1678
2449
  const result = stmts.archiveMemory.run(
1679
2450
  compressedIntoId,
1680
2451
  archivedAt,
@@ -1683,6 +2454,9 @@ var SqliteStorageProvider = class {
1683
2454
  );
1684
2455
  if (Number(result.changes) > 0) {
1685
2456
  archived.push(id);
2457
+ if (existing?.rowid !== void 0) {
2458
+ this.deleteEmbedding(existing.rowid);
2459
+ }
1686
2460
  this.deleteFts(id);
1687
2461
  stmts.insertHistory.run(
1688
2462
  crypto.randomUUID(),
@@ -1723,17 +2497,8 @@ var SqliteStorageProvider = class {
1723
2497
  throw new DatabaseError("deleteMemoriesByFilter requires an agent filter");
1724
2498
  }
1725
2499
  return this.withTransaction(() => {
1726
- const listed = this.listMemoriesSync({
1727
- organization: filter.organization,
1728
- agent,
1729
- includeArchived: true
1730
- });
1731
- for (const memory of listed) {
1732
- if (memory.rowid !== void 0) {
1733
- this.deleteEmbedding(memory.rowid);
1734
- }
1735
- this.deleteFts(memory.id);
1736
- }
2500
+ this.deleteEmbeddingsForScope(filter.organization, agent);
2501
+ this.deleteFtsForScope(filter.organization, agent);
1737
2502
  const result = stmts.deleteMemoriesByOrgAgent.run(
1738
2503
  filter.organization,
1739
2504
  agent
@@ -1744,13 +2509,8 @@ var SqliteStorageProvider = class {
1744
2509
  async clearOrganization(organization) {
1745
2510
  const stmts = this.requireStatements();
1746
2511
  return this.withTransaction(() => {
1747
- const listed = this.listMemoriesSync({ organization, includeArchived: true });
1748
- for (const memory of listed) {
1749
- if (memory.rowid !== void 0) {
1750
- this.deleteEmbedding(memory.rowid);
1751
- }
1752
- this.deleteFts(memory.id);
1753
- }
2512
+ this.deleteEmbeddingsForScope(organization);
2513
+ this.deleteFtsForScope(organization);
1754
2514
  const result = stmts.deleteMemoriesByOrg.run(organization);
1755
2515
  return Number(result.changes);
1756
2516
  });
@@ -1771,15 +2531,12 @@ var SqliteStorageProvider = class {
1771
2531
  }
1772
2532
  async getStats(organization) {
1773
2533
  const stmts = this.requireStatements();
1774
- const memories = stmts.countMemories.get(organization);
1775
- const active = stmts.countActiveMemories.get(organization);
1776
- const archived = stmts.countArchivedMemories.get(organization);
1777
- const agents = stmts.countAgents.get(organization);
2534
+ const row = stmts.getStats.get(organization);
1778
2535
  return {
1779
- totalMemories: Number(memories.count),
1780
- activeMemories: Number(active.count),
1781
- archivedMemories: Number(archived.count),
1782
- totalAgents: Number(agents.count)
2536
+ totalMemories: Number(row.total),
2537
+ activeMemories: Number(row.active),
2538
+ archivedMemories: Number(row.archived),
2539
+ totalAgents: Number(row.agents)
1783
2540
  };
1784
2541
  }
1785
2542
  async getDatabaseSizeBytes() {
@@ -1812,59 +2569,159 @@ var SqliteStorageProvider = class {
1812
2569
  );
1813
2570
  }
1814
2571
  }
1815
- withTransaction(fn) {
2572
+ async withTransaction(fn) {
1816
2573
  const db = this.requireDb();
1817
- db.exec("BEGIN");
1818
- try {
1819
- const result = fn();
1820
- db.exec("COMMIT");
1821
- return result;
1822
- } catch (error) {
1823
- try {
1824
- db.exec("ROLLBACK");
1825
- } catch {
1826
- }
1827
- if (error instanceof DatabaseError || error instanceof InitializationError) {
1828
- throw error;
2574
+ return withImmediateTransaction(
2575
+ db,
2576
+ this.concurrency,
2577
+ fn,
2578
+ (attempt, delayMs) => {
2579
+ this.retryLog?.(
2580
+ `SQLITE_BUSY retry attempt=${attempt} backoffMs=${Math.round(delayMs)}`
2581
+ );
1829
2582
  }
1830
- throw new DatabaseError(`Transaction failed: ${this.describe(error)}`, {
1831
- cause: error instanceof Error ? error : void 0
1832
- });
1833
- }
2583
+ );
1834
2584
  }
1835
2585
  // ─── internals ───────────────────────────────────────────────────────────
1836
2586
  tryLoadSqliteVec(db) {
2587
+ const plat = `${process.platform}-${process.arch}`;
2588
+ if (_SqliteStorageProvider.sqliteVecUnsupported.has(plat)) {
2589
+ return false;
2590
+ }
1837
2591
  try {
1838
2592
  sqliteVec.load(db);
1839
2593
  return true;
1840
2594
  } catch {
2595
+ _SqliteStorageProvider.sqliteVecUnsupported.add(plat);
1841
2596
  return false;
1842
2597
  }
1843
2598
  }
2599
+ static sqliteVecUnsupported = /* @__PURE__ */ new Set();
1844
2600
  runMigrations(db) {
1845
2601
  db.exec(CREATE_META_TABLE);
1846
2602
  db.exec(CREATE_MEMORIES_TABLE);
1847
2603
  db.exec(CREATE_HISTORY_TABLE);
1848
2604
  db.exec(CREATE_BLOB_EMBEDDINGS_TABLE);
1849
- for (const indexSql of CREATE_INDEXES) {
1850
- db.exec(indexSql);
1851
- }
2605
+ db.exec(CREATE_EMBEDDING_CACHE_TABLE);
1852
2606
  const current = this.readMetaNumberFromDb(db, META_KEYS.schemaVersion);
1853
2607
  if (current === null) {
1854
2608
  db.exec(CREATE_FTS_TABLE);
1855
2609
  this.backfillFts(db);
2610
+ this.migrateToV3(db);
2611
+ for (const dropSql of DROP_REDUNDANT_INDEXES_V4) {
2612
+ db.exec(dropSql);
2613
+ }
2614
+ for (const indexSql of CREATE_INDEXES) {
2615
+ db.exec(indexSql);
2616
+ }
1856
2617
  db.prepare(SQL.setMeta).run(META_KEYS.schemaVersion, String(SCHEMA_VERSION));
2618
+ this.ensureFtsConsistency(db);
1857
2619
  } else if (current > SCHEMA_VERSION) {
1858
2620
  throw new InitializationError(
1859
2621
  `Database schema version ${current} is newer than this SDK (supports ${SCHEMA_VERSION}).`
1860
2622
  );
1861
2623
  } else if (current < SCHEMA_VERSION) {
1862
- db.exec(CREATE_FTS_TABLE);
1863
- this.backfillFts(db);
2624
+ if (current < 3) {
2625
+ db.exec(CREATE_FTS_TABLE);
2626
+ this.backfillFts(db);
2627
+ this.migrateToV3(db);
2628
+ }
2629
+ if (current < 4) {
2630
+ this.migrateToV4(db);
2631
+ }
2632
+ for (const indexSql of CREATE_INDEXES) {
2633
+ db.exec(indexSql);
2634
+ }
1864
2635
  db.prepare(SQL.setMeta).run(META_KEYS.schemaVersion, String(SCHEMA_VERSION));
1865
- } else {
1866
- db.exec(CREATE_FTS_TABLE);
2636
+ this.ensureFtsConsistency(db);
2637
+ }
2638
+ }
2639
+ /**
2640
+ * Schema v4: drop unused global created_at index, strip archived vectors
2641
+ * from ANN, add agent-active covering index (via CREATE_INDEXES).
2642
+ */
2643
+ migrateToV4(db) {
2644
+ for (const dropSql of DROP_REDUNDANT_INDEXES_V4) {
2645
+ db.exec(dropSql);
2646
+ }
2647
+ try {
2648
+ db.prepare(SQL.deleteArchivedEmbeddings).run();
2649
+ } catch {
2650
+ }
2651
+ try {
2652
+ db.prepare(SQL.deleteArchivedEmbeddingsBlob).run();
2653
+ } catch {
1867
2654
  }
2655
+ this.memoryIndexDirty = true;
2656
+ }
2657
+ /**
2658
+ * Schema v3: content_hash column, history 'updated' event, embedding_cache.
2659
+ * SQLite cannot ALTER CHECK constraints — rebuild memory_history when needed.
2660
+ */
2661
+ migrateToV3(db) {
2662
+ const cols = db.prepare(`PRAGMA table_info(memories)`).all();
2663
+ const hasHash = cols.some((c) => c.name === "content_hash");
2664
+ if (!hasHash) {
2665
+ db.exec(`ALTER TABLE memories ADD COLUMN content_hash TEXT`);
2666
+ }
2667
+ const active = db.prepare(
2668
+ `SELECT id, content_text, updated_at FROM memories WHERE archived = 0 ORDER BY updated_at DESC`
2669
+ ).all();
2670
+ const seen = /* @__PURE__ */ new Set();
2671
+ const updateHash = db.prepare(
2672
+ `UPDATE memories SET content_hash = ? WHERE id = ?`
2673
+ );
2674
+ for (const row of active) {
2675
+ const hash = hashMemoryContent(row.content_text);
2676
+ updateHash.run(hash, row.id);
2677
+ }
2678
+ const activeFull = db.prepare(
2679
+ `SELECT id, organization, agent, content_text, updated_at
2680
+ FROM memories WHERE archived = 0
2681
+ ORDER BY updated_at DESC`
2682
+ ).all();
2683
+ seen.clear();
2684
+ const clearHash = db.prepare(
2685
+ `UPDATE memories SET content_hash = NULL WHERE id = ?`
2686
+ );
2687
+ const setHash = db.prepare(
2688
+ `UPDATE memories SET content_hash = ? WHERE id = ?`
2689
+ );
2690
+ for (const row of activeFull) {
2691
+ const hash = hashMemoryContent(row.content_text);
2692
+ const key = `${row.organization}\0${row.agent}\0${hash}`;
2693
+ if (seen.has(key)) {
2694
+ clearHash.run(row.id);
2695
+ } else {
2696
+ seen.add(key);
2697
+ setHash.run(hash, row.id);
2698
+ }
2699
+ }
2700
+ this.migrateHistoryAllowUpdated(db);
2701
+ db.exec(CREATE_EMBEDDING_CACHE_TABLE);
2702
+ }
2703
+ migrateHistoryAllowUpdated(db) {
2704
+ const sql = db.prepare(
2705
+ `SELECT sql FROM sqlite_master WHERE type='table' AND name='memory_history'`
2706
+ ).get();
2707
+ if (sql?.sql?.includes("'updated'")) {
2708
+ return;
2709
+ }
2710
+ db.exec(`
2711
+ CREATE TABLE IF NOT EXISTS memory_history_v3 (
2712
+ id TEXT PRIMARY KEY NOT NULL,
2713
+ memory_id TEXT NOT NULL,
2714
+ event_type TEXT NOT NULL CHECK (event_type IN ('created', 'archived', 'compressed', 'updated')),
2715
+ related_memory_id TEXT NULL,
2716
+ created_at TEXT NOT NULL,
2717
+ FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
2718
+ );
2719
+ INSERT INTO memory_history_v3 (id, memory_id, event_type, related_memory_id, created_at)
2720
+ SELECT id, memory_id, event_type, related_memory_id, created_at FROM memory_history;
2721
+ DROP TABLE memory_history;
2722
+ ALTER TABLE memory_history_v3 RENAME TO memory_history;
2723
+ CREATE INDEX IF NOT EXISTS idx_history_memory_id ON memory_history(memory_id);
2724
+ `);
1868
2725
  }
1869
2726
  /**
1870
2727
  * Rebuild FTS from active (non-archived) memories when counts diverge.
@@ -1896,13 +2753,33 @@ var SqliteStorageProvider = class {
1896
2753
  prepareStatements(db) {
1897
2754
  let insertFts = null;
1898
2755
  let deleteFts = null;
2756
+ let deleteFtsByOrg = null;
2757
+ let deleteFtsByOrgAgent = null;
1899
2758
  let searchFts = null;
1900
2759
  try {
1901
2760
  insertFts = db.prepare(SQL.insertFts);
1902
2761
  deleteFts = db.prepare(SQL.deleteFts);
2762
+ deleteFtsByOrg = db.prepare(SQL.deleteFtsByOrg);
2763
+ deleteFtsByOrgAgent = db.prepare(SQL.deleteFtsByOrgAgent);
1903
2764
  searchFts = db.prepare(SQL.searchFts);
1904
2765
  } catch {
1905
2766
  }
2767
+ let deleteEmbeddingsByOrg = null;
2768
+ let deleteEmbeddingsByOrgAgent = null;
2769
+ let deleteEmbeddingsBlobByOrg = null;
2770
+ let deleteEmbeddingsBlobByOrgAgent = null;
2771
+ try {
2772
+ deleteEmbeddingsByOrg = db.prepare(SQL.deleteEmbeddingsByOrg);
2773
+ deleteEmbeddingsByOrgAgent = db.prepare(SQL.deleteEmbeddingsByOrgAgent);
2774
+ } catch {
2775
+ }
2776
+ try {
2777
+ deleteEmbeddingsBlobByOrg = db.prepare(SQL.deleteEmbeddingsBlobByOrg);
2778
+ deleteEmbeddingsBlobByOrgAgent = db.prepare(
2779
+ SQL.deleteEmbeddingsBlobByOrgAgent
2780
+ );
2781
+ } catch {
2782
+ }
1906
2783
  return {
1907
2784
  getMeta: db.prepare(SQL.getMeta),
1908
2785
  setMeta: db.prepare(SQL.setMeta),
@@ -1910,6 +2787,7 @@ var SqliteStorageProvider = class {
1910
2787
  updateMemoryContent: db.prepare(SQL.updateMemoryContent),
1911
2788
  getMemoryById: db.prepare(SQL.getMemoryById),
1912
2789
  getMemoryByRowid: db.prepare(SQL.getMemoryByRowid),
2790
+ findActiveByContentHash: db.prepare(SQL.findActiveByContentHash),
1913
2791
  insertEmbedding: null,
1914
2792
  deleteEmbedding: null,
1915
2793
  searchVectors: null,
@@ -1922,10 +2800,7 @@ var SqliteStorageProvider = class {
1922
2800
  deleteMemoriesByOrgAgent: db.prepare(SQL.deleteMemoriesByOrgAgent),
1923
2801
  insertHistory: db.prepare(SQL.insertHistory),
1924
2802
  getHistory: db.prepare(SQL.getHistory),
1925
- countMemories: db.prepare(SQL.countMemories),
1926
- countActiveMemories: db.prepare(SQL.countActiveMemories),
1927
- countArchivedMemories: db.prepare(SQL.countArchivedMemories),
1928
- countAgents: db.prepare(SQL.countAgents),
2803
+ getStats: db.prepare(SQL.getStats),
1929
2804
  listRowidsForOrg: db.prepare(SQL.listRowidsForOrg),
1930
2805
  listRowidsForOrgAgent: db.prepare(SQL.listRowidsForOrgAgent),
1931
2806
  vectorTableExists: db.prepare(SQL.vectorTableExists),
@@ -1934,11 +2809,16 @@ var SqliteStorageProvider = class {
1934
2809
  ),
1935
2810
  insertFts,
1936
2811
  deleteFts,
1937
- searchFts
2812
+ deleteFtsByOrg,
2813
+ deleteFtsByOrgAgent,
2814
+ searchFts,
2815
+ deleteEmbeddingsByOrg,
2816
+ deleteEmbeddingsByOrgAgent,
2817
+ deleteEmbeddingsBlobByOrg,
2818
+ deleteEmbeddingsBlobByOrgAgent
1938
2819
  };
1939
2820
  }
1940
- listMemoriesSync(filter) {
1941
- const db = this.requireDb();
2821
+ listMemoriesIndexed(filter, limit) {
1942
2822
  const clauses = [`organization = ?`];
1943
2823
  const params = [filter.organization];
1944
2824
  if (filter.agent) {
@@ -1948,13 +2828,316 @@ var SqliteStorageProvider = class {
1948
2828
  if (!filter.includeArchived) {
1949
2829
  clauses.push(`archived = 0`);
1950
2830
  }
1951
- const sql = `
2831
+ const key = `${filter.agent ? "a" : "_"}:${filter.includeArchived ? "A" : "0"}:${limit !== void 0 ? "L" : "_"}`;
2832
+ let stmt = this.listStatements.get(key);
2833
+ if (!stmt) {
2834
+ let sql = `
2835
+ SELECT rowid, id, organization, agent, content_text, metadata_json,
2836
+ archived, compressed_into, content_hash, created_at, updated_at
2837
+ FROM memories
2838
+ WHERE ${clauses.join(" AND ")}
2839
+ ORDER BY created_at ASC
2840
+ `;
2841
+ if (limit !== void 0) {
2842
+ sql += ` LIMIT ?`;
2843
+ }
2844
+ stmt = this.requireDb().prepare(sql);
2845
+ this.listStatements.set(key, stmt);
2846
+ }
2847
+ if (limit !== void 0) {
2848
+ return stmt.all(...params, limit);
2849
+ }
2850
+ return stmt.all(...params);
2851
+ }
2852
+ /**
2853
+ * Metadata-filtered list: push filters to json_extract when possible;
2854
+ * otherwise page with a hard scan cap (never load unbounded orgs into RAM).
2855
+ */
2856
+ listMemoriesWithMetadata(filter, limit) {
2857
+ const want = limit ?? MAX_METADATA_SCAN;
2858
+ const compiled = filter.metadata ? compileMetadataFilterToSql(filter.metadata) : null;
2859
+ if (compiled) {
2860
+ const clauses2 = [`organization = ?`, `(${compiled.expression})`];
2861
+ const params = [filter.organization, ...compiled.params];
2862
+ if (filter.agent) {
2863
+ clauses2.push(`agent = ?`);
2864
+ params.push(filter.agent);
2865
+ }
2866
+ if (!filter.includeArchived) {
2867
+ clauses2.push(`archived = 0`);
2868
+ }
2869
+ const sql = `
2870
+ SELECT rowid, id, organization, agent, content_text, metadata_json,
2871
+ archived, compressed_into, content_hash, created_at, updated_at
2872
+ FROM memories
2873
+ WHERE ${clauses2.join(" AND ")}
2874
+ ORDER BY created_at ASC
2875
+ LIMIT ?
2876
+ `;
2877
+ params.push(want);
2878
+ return this.requireDb().prepare(sql).all(...params);
2879
+ }
2880
+ const matched = [];
2881
+ let offset = 0;
2882
+ const db = this.requireDb();
2883
+ const clauses = [`organization = ?`];
2884
+ const baseParams = [filter.organization];
2885
+ if (filter.agent) {
2886
+ clauses.push(`agent = ?`);
2887
+ baseParams.push(filter.agent);
2888
+ }
2889
+ if (!filter.includeArchived) {
2890
+ clauses.push(`archived = 0`);
2891
+ }
2892
+ const pageSql = `
1952
2893
  SELECT rowid, id, organization, agent, content_text, metadata_json,
1953
- archived, compressed_into, created_at, updated_at
2894
+ archived, compressed_into, content_hash, created_at, updated_at
1954
2895
  FROM memories
1955
2896
  WHERE ${clauses.join(" AND ")}
2897
+ ORDER BY created_at ASC
2898
+ LIMIT ? OFFSET ?
1956
2899
  `;
1957
- return db.prepare(sql).all(...params);
2900
+ const pageStmt = db.prepare(pageSql);
2901
+ while (matched.length < want && offset < MAX_METADATA_SCAN) {
2902
+ const pageLimit = Math.min(METADATA_PAGE_SIZE, MAX_METADATA_SCAN - offset);
2903
+ const page = pageStmt.all(
2904
+ ...baseParams,
2905
+ pageLimit,
2906
+ offset
2907
+ );
2908
+ if (page.length === 0) {
2909
+ break;
2910
+ }
2911
+ for (const row of page) {
2912
+ if (matchesMetadata(
2913
+ deserializeMetadata(row.metadata_json),
2914
+ filter.metadata
2915
+ )) {
2916
+ matched.push(row);
2917
+ if (matched.length >= want) {
2918
+ return matched;
2919
+ }
2920
+ }
2921
+ }
2922
+ offset += page.length;
2923
+ if (page.length < pageLimit) {
2924
+ break;
2925
+ }
2926
+ }
2927
+ return matched;
2928
+ }
2929
+ getRowidInStatement(count) {
2930
+ let stmt = this.rowidInStatements.get(count);
2931
+ if (!stmt) {
2932
+ const placeholders = Array.from({ length: count }, () => "?").join(",");
2933
+ const sql = `${SQL.getMemoriesByRowidsPrefix}${placeholders})`;
2934
+ stmt = this.requireDb().prepare(sql);
2935
+ this.rowidInStatements.set(count, stmt);
2936
+ }
2937
+ return stmt;
2938
+ }
2939
+ insertMemoryChunk(inputs) {
2940
+ const stmts = this.requireStatements();
2941
+ const n = inputs.length;
2942
+ if (n === 1) {
2943
+ const input = inputs[0];
2944
+ const contentHash = input.contentHash !== void 0 ? input.contentHash : hashMemoryContent(input.contentText);
2945
+ const row = stmts.insertMemory.get(
2946
+ input.id,
2947
+ input.organization,
2948
+ input.agent,
2949
+ input.contentText,
2950
+ serializeMetadata(input.metadata),
2951
+ contentHash,
2952
+ input.createdAt,
2953
+ input.updatedAt
2954
+ );
2955
+ if (!row || row.rowid === void 0) {
2956
+ throw new DatabaseError("Failed to read memory after batch insert");
2957
+ }
2958
+ this.insertEmbedding(row.rowid, input.embedding);
2959
+ this.insertFtsRow(
2960
+ input.id,
2961
+ input.organization,
2962
+ input.agent,
2963
+ input.contentText
2964
+ );
2965
+ stmts.insertHistory.run(
2966
+ crypto.randomUUID(),
2967
+ input.id,
2968
+ "created",
2969
+ null,
2970
+ input.createdAt
2971
+ );
2972
+ return [row];
2973
+ }
2974
+ let stmt = this.batchInsertStatements.get(n);
2975
+ if (!stmt) {
2976
+ const valueRow = "(?, ?, ?, ?, ?, 0, NULL, ?, ?, ?)";
2977
+ const values = Array.from({ length: n }, () => valueRow).join(", ");
2978
+ const sql = `
2979
+ INSERT INTO memories (
2980
+ id, organization, agent, content_text, metadata_json,
2981
+ archived, compressed_into, content_hash, created_at, updated_at
2982
+ ) VALUES ${values}
2983
+ RETURNING rowid, id, organization, agent, content_text, metadata_json,
2984
+ archived, compressed_into, content_hash, created_at, updated_at
2985
+ `;
2986
+ stmt = this.requireDb().prepare(sql);
2987
+ this.batchInsertStatements.set(n, stmt);
2988
+ }
2989
+ const binds = [];
2990
+ for (const input of inputs) {
2991
+ const contentHash = input.contentHash !== void 0 ? input.contentHash : hashMemoryContent(input.contentText);
2992
+ binds.push(
2993
+ input.id,
2994
+ input.organization,
2995
+ input.agent,
2996
+ input.contentText,
2997
+ serializeMetadata(input.metadata),
2998
+ contentHash,
2999
+ input.createdAt,
3000
+ input.updatedAt
3001
+ );
3002
+ }
3003
+ const rows = stmt.all(...binds);
3004
+ if (rows.length !== n) {
3005
+ throw new DatabaseError("Failed to read memories after batch insert");
3006
+ }
3007
+ for (const row of rows) {
3008
+ if (row.rowid === void 0) {
3009
+ throw new DatabaseError("Failed to read memory rowid after batch insert");
3010
+ }
3011
+ }
3012
+ this.insertEmbeddingsBatch(rows, inputs);
3013
+ this.insertFtsBatch(inputs);
3014
+ this.insertHistoryBatch(inputs);
3015
+ return rows;
3016
+ }
3017
+ insertEmbeddingsBatch(rows, inputs) {
3018
+ const n = rows.length;
3019
+ if (this.vectorBackend === "sqlite-vec") {
3020
+ for (let i = 0; i < n; i += 1) {
3021
+ this.insertEmbedding(rows[i].rowid, inputs[i].embedding);
3022
+ }
3023
+ return;
3024
+ }
3025
+ let stmt = this.batchBlobEmbStatements.get(n);
3026
+ if (!stmt) {
3027
+ const values = Array.from({ length: n }, () => "(?, ?)").join(", ");
3028
+ stmt = this.requireDb().prepare(
3029
+ `INSERT INTO memory_embeddings_blob (memory_rowid, embedding) VALUES ${values}`
3030
+ );
3031
+ this.batchBlobEmbStatements.set(n, stmt);
3032
+ }
3033
+ const binds = [];
3034
+ for (let i = 0; i < n; i += 1) {
3035
+ binds.push(rows[i].rowid, embeddingToBuffer(inputs[i].embedding));
3036
+ }
3037
+ stmt.run(...binds);
3038
+ if (this.memoryIndex && this.vectorDimensions !== null) {
3039
+ for (let i = 0; i < n; i += 1) {
3040
+ this.memoryIndex.upsert(rows[i].rowid, inputs[i].embedding);
3041
+ }
3042
+ this.memoryIndexDirty = false;
3043
+ } else {
3044
+ this.memoryIndexDirty = true;
3045
+ }
3046
+ }
3047
+ insertFtsBatch(inputs) {
3048
+ const stmts = this.requireStatements();
3049
+ if (!stmts.insertFts) {
3050
+ return;
3051
+ }
3052
+ const n = inputs.length;
3053
+ let stmt = this.batchFtsStatements.get(n);
3054
+ if (!stmt) {
3055
+ const values = Array.from({ length: n }, () => "(?, ?, ?, ?)").join(", ");
3056
+ stmt = this.requireDb().prepare(
3057
+ `INSERT INTO memories_fts (content_text, memory_id, organization, agent) VALUES ${values}`
3058
+ );
3059
+ this.batchFtsStatements.set(n, stmt);
3060
+ }
3061
+ const binds = [];
3062
+ for (const input of inputs) {
3063
+ binds.push(
3064
+ input.contentText,
3065
+ input.id,
3066
+ input.organization,
3067
+ input.agent
3068
+ );
3069
+ }
3070
+ try {
3071
+ stmt.run(...binds);
3072
+ } catch {
3073
+ for (const input of inputs) {
3074
+ this.insertFtsRow(
3075
+ input.id,
3076
+ input.organization,
3077
+ input.agent,
3078
+ input.contentText
3079
+ );
3080
+ }
3081
+ }
3082
+ }
3083
+ insertHistoryBatch(inputs) {
3084
+ const n = inputs.length;
3085
+ let stmt = this.batchHistoryStatements.get(n);
3086
+ if (!stmt) {
3087
+ const values = Array.from({ length: n }, () => "(?, ?, ?, ?, ?)").join(
3088
+ ", "
3089
+ );
3090
+ stmt = this.requireDb().prepare(
3091
+ `INSERT INTO memory_history (id, memory_id, event_type, related_memory_id, created_at)
3092
+ VALUES ${values}`
3093
+ );
3094
+ this.batchHistoryStatements.set(n, stmt);
3095
+ }
3096
+ const binds = [];
3097
+ for (const input of inputs) {
3098
+ binds.push(
3099
+ crypto.randomUUID(),
3100
+ input.id,
3101
+ "created",
3102
+ null,
3103
+ input.createdAt
3104
+ );
3105
+ }
3106
+ stmt.run(...binds);
3107
+ }
3108
+ deleteEmbeddingsForScope(organization, agent) {
3109
+ const stmts = this.requireStatements();
3110
+ try {
3111
+ if (this.vectorBackend === "sqlite-vec") {
3112
+ if (agent) {
3113
+ stmts.deleteEmbeddingsByOrgAgent?.run(organization, agent);
3114
+ } else {
3115
+ stmts.deleteEmbeddingsByOrg?.run(organization);
3116
+ }
3117
+ } else {
3118
+ if (agent) {
3119
+ stmts.deleteEmbeddingsBlobByOrgAgent?.run(organization, agent);
3120
+ } else {
3121
+ stmts.deleteEmbeddingsBlobByOrg?.run(organization);
3122
+ }
3123
+ this.memoryIndexDirty = true;
3124
+ if (!agent) {
3125
+ this.memoryIndex?.clear();
3126
+ }
3127
+ }
3128
+ } catch {
3129
+ }
3130
+ }
3131
+ deleteFtsForScope(organization, agent) {
3132
+ const stmts = this.requireStatements();
3133
+ try {
3134
+ if (agent) {
3135
+ stmts.deleteFtsByOrgAgent?.run(organization, agent);
3136
+ } else {
3137
+ stmts.deleteFtsByOrg?.run(organization);
3138
+ }
3139
+ } catch {
3140
+ }
1958
3141
  }
1959
3142
  /** Insert-only FTS row (new memory IDs never collide). */
1960
3143
  insertFtsRow(memoryId, organization, agent, contentText) {
@@ -2038,6 +3221,13 @@ var SqliteStorageProvider = class {
2038
3221
  stmts.insertEmbeddingBlob = null;
2039
3222
  stmts.deleteEmbeddingBlob = null;
2040
3223
  stmts.listEmbeddingsBlob = null;
3224
+ try {
3225
+ stmts.deleteEmbeddingsByOrg = db.prepare(SQL.deleteEmbeddingsByOrg);
3226
+ stmts.deleteEmbeddingsByOrgAgent = db.prepare(
3227
+ SQL.deleteEmbeddingsByOrgAgent
3228
+ );
3229
+ } catch {
3230
+ }
2041
3231
  return;
2042
3232
  }
2043
3233
  stmts.insertEmbeddingBlob = db.prepare(SQL.insertEmbeddingBlob);
@@ -2046,6 +3236,13 @@ var SqliteStorageProvider = class {
2046
3236
  stmts.insertEmbedding = null;
2047
3237
  stmts.deleteEmbedding = null;
2048
3238
  stmts.searchVectors = null;
3239
+ try {
3240
+ stmts.deleteEmbeddingsBlobByOrg = db.prepare(SQL.deleteEmbeddingsBlobByOrg);
3241
+ stmts.deleteEmbeddingsBlobByOrgAgent = db.prepare(
3242
+ SQL.deleteEmbeddingsBlobByOrgAgent
3243
+ );
3244
+ } catch {
3245
+ }
2049
3246
  }
2050
3247
  insertEmbedding(rowid, embedding) {
2051
3248
  const stmts = this.requireStatements();
@@ -2054,7 +3251,12 @@ var SqliteStorageProvider = class {
2054
3251
  return;
2055
3252
  }
2056
3253
  stmts.insertEmbeddingBlob.run(rowid, embeddingToBuffer(embedding));
2057
- this.memoryIndexDirty = true;
3254
+ if (this.memoryIndex) {
3255
+ this.memoryIndex.upsert(rowid, embedding);
3256
+ this.memoryIndexDirty = false;
3257
+ } else {
3258
+ this.memoryIndexDirty = true;
3259
+ }
2058
3260
  }
2059
3261
  deleteEmbedding(rowid) {
2060
3262
  const stmts = this.requireStatements();
@@ -2125,58 +3327,418 @@ var SqliteStorageProvider = class {
2125
3327
  if (!row) {
2126
3328
  return null;
2127
3329
  }
2128
- const parsed = Number(row.value);
2129
- return Number.isFinite(parsed) ? parsed : null;
3330
+ const parsed = Number(row.value);
3331
+ return Number.isFinite(parsed) ? parsed : null;
3332
+ }
3333
+ resolvePath(connectionString) {
3334
+ if (connectionString === ":memory:") {
3335
+ return ":memory:";
3336
+ }
3337
+ return path5.isAbsolute(connectionString) ? connectionString : path5.resolve(process.cwd(), connectionString);
3338
+ }
3339
+ requireDb() {
3340
+ if (!this.db) {
3341
+ throw new DatabaseError("Database is not open. Call init() first.");
3342
+ }
3343
+ return this.db;
3344
+ }
3345
+ requireStatements() {
3346
+ if (!this.statements) {
3347
+ throw new DatabaseError("Database statements are not prepared. Call init() first.");
3348
+ }
3349
+ return this.statements;
3350
+ }
3351
+ requireVectorReady() {
3352
+ if (!this.vectorBackend || this.vectorDimensions === null) {
3353
+ throw new DatabaseError(
3354
+ "Vector index is not ready. Embedding dimensions have not been initialized."
3355
+ );
3356
+ }
3357
+ }
3358
+ toVectorParam(embedding) {
3359
+ const buffer = embeddingToBuffer(embedding);
3360
+ return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
3361
+ }
3362
+ describe(error) {
3363
+ if (error instanceof Error) {
3364
+ return error.message;
3365
+ }
3366
+ return String(error);
3367
+ }
3368
+ };
3369
+ var SqliteDatabaseProvider = SqliteStorageProvider;
3370
+ function sanitizeFtsQuery(query) {
3371
+ const terms = query.split(/\s+/).map((t) => t.replace(/["']/g, "").replace(/[^\p{L}\p{N}_-]/gu, "")).filter((t) => t.length > 0);
3372
+ if (terms.length === 0) {
3373
+ return "";
3374
+ }
3375
+ return terms.map((t) => `"${t}"`).join(" OR ");
3376
+ }
3377
+
3378
+ // src/filters/sql-compile-postgres.ts
3379
+ var FIELD_RE2 = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
3380
+ function jsonbTextExtract(field) {
3381
+ if (!FIELD_RE2.test(field)) {
3382
+ return null;
3383
+ }
3384
+ const parts = field.split(".");
3385
+ if (parts.length === 1) {
3386
+ return `metadata_json->>'${parts[0]}'`;
3387
+ }
3388
+ const path7 = parts.join(",");
3389
+ return `metadata_json #>> '{${path7}}'`;
3390
+ }
3391
+ function jsonbContainment(field, value, paramIndex) {
3392
+ if (!FIELD_RE2.test(field)) {
3393
+ return null;
3394
+ }
3395
+ const parts = field.split(".");
3396
+ let obj = value;
3397
+ for (let i = parts.length - 1; i >= 0; i -= 1) {
3398
+ obj = { [parts[i]]: obj };
3399
+ }
3400
+ return {
3401
+ expression: `metadata_json @> $${paramIndex}::jsonb`,
3402
+ params: [JSON.stringify(obj)]
3403
+ };
3404
+ }
3405
+ function compileComparison2(field, op, startIndex) {
3406
+ if ("eq" in op) {
3407
+ const value = op.eq;
3408
+ if (value !== null && typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
3409
+ return null;
3410
+ }
3411
+ return jsonbContainment(field, value, startIndex);
3412
+ }
3413
+ const extract = jsonbTextExtract(field);
3414
+ if (!extract) {
3415
+ return null;
3416
+ }
3417
+ if ("contains" in op) {
3418
+ return {
3419
+ expression: `${extract} LIKE '%' || $${startIndex} || '%'`,
3420
+ params: [op.contains]
3421
+ };
3422
+ }
3423
+ if ("gt" in op) {
3424
+ return {
3425
+ expression: `(${extract})::numeric > $${startIndex}`,
3426
+ params: [op.gt]
3427
+ };
3428
+ }
3429
+ if ("gte" in op) {
3430
+ return {
3431
+ expression: `(${extract})::numeric >= $${startIndex}`,
3432
+ params: [op.gte]
3433
+ };
3434
+ }
3435
+ if ("lt" in op) {
3436
+ return {
3437
+ expression: `(${extract})::numeric < $${startIndex}`,
3438
+ params: [op.lt]
3439
+ };
3440
+ }
3441
+ if ("lte" in op) {
3442
+ return {
3443
+ expression: `(${extract})::numeric <= $${startIndex}`,
3444
+ params: [op.lte]
3445
+ };
3446
+ }
3447
+ if ("between" in op) {
3448
+ const [lo, hi] = op.between;
3449
+ return {
3450
+ expression: `(${extract})::numeric >= $${startIndex} AND (${extract})::numeric <= $${startIndex + 1}`,
3451
+ params: [lo, hi]
3452
+ };
3453
+ }
3454
+ return null;
3455
+ }
3456
+ function compileInner(filter, startIndex) {
3457
+ if ("and" in filter) {
3458
+ if (filter.and.length === 0) {
3459
+ return { expression: "TRUE", params: [] };
3460
+ }
3461
+ const parts = [];
3462
+ let idx = startIndex;
3463
+ for (const child of filter.and) {
3464
+ const compiled = compileInner(child, idx);
3465
+ if (!compiled) {
3466
+ return null;
3467
+ }
3468
+ parts.push(compiled);
3469
+ idx += compiled.params.length;
3470
+ }
3471
+ return {
3472
+ expression: parts.map((p) => `(${p.expression})`).join(" AND "),
3473
+ params: parts.flatMap((p) => p.params)
3474
+ };
3475
+ }
3476
+ if ("or" in filter) {
3477
+ if (filter.or.length === 0) {
3478
+ return { expression: "FALSE", params: [] };
3479
+ }
3480
+ const parts = [];
3481
+ let idx = startIndex;
3482
+ for (const child of filter.or) {
3483
+ const compiled = compileInner(child, idx);
3484
+ if (!compiled) {
3485
+ return null;
3486
+ }
3487
+ parts.push(compiled);
3488
+ idx += compiled.params.length;
3489
+ }
3490
+ return {
3491
+ expression: parts.map((p) => `(${p.expression})`).join(" OR "),
3492
+ params: parts.flatMap((p) => p.params)
3493
+ };
3494
+ }
3495
+ if ("not" in filter) {
3496
+ const inner = compileInner(filter.not, startIndex);
3497
+ if (!inner) {
3498
+ return null;
3499
+ }
3500
+ return {
3501
+ expression: `NOT (${inner.expression})`,
3502
+ params: inner.params
3503
+ };
3504
+ }
3505
+ return compileComparison2(filter.field, filter.op, startIndex);
3506
+ }
3507
+ function compileMetadataFilterToPostgres(filter, startIndex = 1) {
3508
+ return compileInner(filter, startIndex);
3509
+ }
3510
+
3511
+ // src/subscribe/postgres-listener.ts
3512
+ var WOLBARG_NOTIFY_CHANNEL = "wolbarg_events";
3513
+ function matchesFilter(filter, event) {
3514
+ if (filter.organization !== event.organization) {
3515
+ return false;
3516
+ }
3517
+ if (filter.agent !== void 0 && filter.agent !== event.agent) {
3518
+ return false;
3519
+ }
3520
+ if (filter.event === void 0) {
3521
+ return true;
3522
+ }
3523
+ const events = Array.isArray(filter.event) ? filter.event : [filter.event];
3524
+ return events.some(
3525
+ (e) => e === "*" || e === event.event
3526
+ );
3527
+ }
3528
+ function serializeNotifyPayload(event) {
3529
+ const payload = JSON.stringify({
3530
+ event: event.event,
3531
+ organization: event.organization,
3532
+ agent: event.agent,
3533
+ memoryId: event.memoryId,
3534
+ timestamp: event.timestamp,
3535
+ traceId: event.traceId,
3536
+ sessionId: event.sessionId,
3537
+ upsertAction: event.upsertAction
3538
+ });
3539
+ if (payload.length > 7900) {
3540
+ return JSON.stringify({
3541
+ event: event.event,
3542
+ organization: event.organization,
3543
+ agent: event.agent,
3544
+ memoryId: Array.isArray(event.memoryId) ? event.memoryId.slice(0, 20) : event.memoryId,
3545
+ timestamp: event.timestamp
3546
+ });
3547
+ }
3548
+ return payload;
3549
+ }
3550
+ function parseNotifyPayload(raw) {
3551
+ try {
3552
+ const parsed = JSON.parse(raw);
3553
+ if (!parsed || typeof parsed.event !== "string" || typeof parsed.organization !== "string" || typeof parsed.agent !== "string" || typeof parsed.timestamp !== "string") {
3554
+ return null;
3555
+ }
3556
+ return parsed;
3557
+ } catch {
3558
+ return null;
3559
+ }
3560
+ }
3561
+ var PostgresSubscribeListener = class {
3562
+ subscriptions = /* @__PURE__ */ new Map();
3563
+ nextId = 1;
3564
+ client = null;
3565
+ closed = false;
3566
+ reconnecting = false;
3567
+ listenInFlight = null;
3568
+ reconnectDelayMs;
3569
+ connect;
3570
+ onError;
3571
+ constructor(options) {
3572
+ this.connect = options.connect;
3573
+ this.reconnectDelayMs = options.reconnectDelayMs ?? 1e3;
3574
+ this.onError = options.onError;
3575
+ }
3576
+ async start() {
3577
+ if (this.closed) {
3578
+ return;
3579
+ }
3580
+ await this.ensureListening();
3581
+ }
3582
+ async ensureListening() {
3583
+ if (this.client || this.closed) {
3584
+ return;
3585
+ }
3586
+ if (this.listenInFlight) {
3587
+ await this.listenInFlight;
3588
+ return;
3589
+ }
3590
+ this.listenInFlight = this.openListenConnection();
3591
+ try {
3592
+ await this.listenInFlight;
3593
+ } finally {
3594
+ this.listenInFlight = null;
3595
+ }
3596
+ }
3597
+ async openListenConnection() {
3598
+ if (this.client || this.closed) {
3599
+ return;
3600
+ }
3601
+ const client = await this.connect();
3602
+ if (this.closed) {
3603
+ try {
3604
+ client.release(true);
3605
+ } catch {
3606
+ }
3607
+ return;
3608
+ }
3609
+ this.client = client;
3610
+ client.on("notification", (msg) => {
3611
+ if (msg.channel !== WOLBARG_NOTIFY_CHANNEL || !msg.payload) {
3612
+ return;
3613
+ }
3614
+ const event = parseNotifyPayload(msg.payload);
3615
+ if (!event) {
3616
+ return;
3617
+ }
3618
+ this.dispatch(event);
3619
+ });
3620
+ client.on("error", (error) => {
3621
+ this.onError?.(error);
3622
+ void this.handleDisconnect();
3623
+ });
3624
+ client.on("end", () => {
3625
+ void this.handleDisconnect();
3626
+ });
3627
+ await client.query(`LISTEN ${WOLBARG_NOTIFY_CHANNEL}`);
2130
3628
  }
2131
- resolvePath(connectionString) {
2132
- if (connectionString === ":memory:") {
2133
- return ":memory:";
3629
+ async handleDisconnect() {
3630
+ if (this.closed || this.reconnecting) {
3631
+ return;
2134
3632
  }
2135
- return path5.isAbsolute(connectionString) ? connectionString : path5.resolve(process.cwd(), connectionString);
2136
- }
2137
- requireDb() {
2138
- if (!this.db) {
2139
- throw new DatabaseError("Database is not open. Call init() first.");
3633
+ this.reconnecting = true;
3634
+ const prev = this.client;
3635
+ this.client = null;
3636
+ if (prev) {
3637
+ try {
3638
+ prev.release(true);
3639
+ } catch {
3640
+ }
2140
3641
  }
2141
- return this.db;
3642
+ try {
3643
+ await new Promise((r) => setTimeout(r, this.reconnectDelayMs));
3644
+ if (!this.closed) {
3645
+ await this.ensureListening();
3646
+ }
3647
+ } catch (error) {
3648
+ this.onError?.(error);
3649
+ this.reconnecting = false;
3650
+ if (!this.closed) {
3651
+ void this.handleDisconnect();
3652
+ }
3653
+ return;
3654
+ }
3655
+ this.reconnecting = false;
2142
3656
  }
2143
- requireStatements() {
2144
- if (!this.statements) {
2145
- throw new DatabaseError("Database statements are not prepared. Call init() first.");
3657
+ dispatch(event) {
3658
+ for (const sub of this.subscriptions.values()) {
3659
+ if (!matchesFilter(sub.filter, event)) {
3660
+ continue;
3661
+ }
3662
+ try {
3663
+ sub.callback(event);
3664
+ } catch (error) {
3665
+ console.error(
3666
+ "[wolbarg] subscribe callback error:",
3667
+ error instanceof Error ? error.message : error
3668
+ );
3669
+ }
2146
3670
  }
2147
- return this.statements;
2148
3671
  }
2149
- requireVectorReady() {
2150
- if (!this.vectorBackend || this.vectorDimensions === null) {
2151
- throw new DatabaseError(
2152
- "Vector index is not ready. Embedding dimensions have not been initialized."
2153
- );
3672
+ subscribe(filter, callback) {
3673
+ if (this.closed) {
3674
+ throw new Error("Subscribe backend is closed");
2154
3675
  }
3676
+ const id = this.nextId++;
3677
+ this.subscriptions.set(id, { filter, callback });
3678
+ void this.ensureListening();
3679
+ return () => {
3680
+ this.subscriptions.delete(id);
3681
+ };
2155
3682
  }
2156
- toVectorParam(embedding) {
2157
- const buffer = embeddingToBuffer(embedding);
2158
- return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
3683
+ /** True when at least one subscribe() callback is registered. */
3684
+ hasSubscribers() {
3685
+ return this.subscriptions.size > 0;
2159
3686
  }
2160
- describe(error) {
2161
- if (error instanceof Error) {
2162
- return error.message;
3687
+ /**
3688
+ * Local emit is a no-op for Postgres — writers use NOTIFY in-transaction.
3689
+ * Kept so the shared SubscribeBackend interface is uniform.
3690
+ */
3691
+ emit(_event) {
3692
+ }
3693
+ async close() {
3694
+ this.closed = true;
3695
+ this.subscriptions.clear();
3696
+ const pending = this.listenInFlight;
3697
+ if (pending) {
3698
+ await pending.catch(() => void 0);
3699
+ }
3700
+ const client = this.client;
3701
+ this.client = null;
3702
+ if (!client) {
3703
+ return;
3704
+ }
3705
+ try {
3706
+ client.release(true);
3707
+ } catch {
3708
+ try {
3709
+ client.end?.();
3710
+ } catch {
3711
+ }
2163
3712
  }
2164
- return String(error);
2165
3713
  }
2166
3714
  };
2167
- var SqliteDatabaseProvider = SqliteStorageProvider;
2168
- function sanitizeFtsQuery(query) {
2169
- const terms = query.split(/\s+/).map((t) => t.replace(/["']/g, "").replace(/[^\p{L}\p{N}_-]/gu, "")).filter((t) => t.length > 0);
2170
- if (terms.length === 0) {
2171
- return "";
2172
- }
2173
- return terms.map((t) => `"${t}"`).join(" OR ");
3715
+ async function notifyMemoryChange(client, event) {
3716
+ const payload = serializeNotifyPayload(event);
3717
+ await client.query(`SELECT pg_notify($1, $2)`, [
3718
+ WOLBARG_NOTIFY_CHANNEL,
3719
+ payload
3720
+ ]);
2174
3721
  }
3722
+ function createPostgresListenerFromPool(pool, onError) {
3723
+ return new PostgresSubscribeListener({
3724
+ connect: () => pool.connect(),
3725
+ onError
3726
+ });
3727
+ }
3728
+
3729
+ // src/storage/providers/postgres.ts
2175
3730
  var txStore = new AsyncLocalStorage();
2176
3731
  var STMT = {
2177
- insertOne: "Wolbarg_insert_one_v1",
2178
- insertBatch: "Wolbarg_insert_batch_v1"
3732
+ insertOne: "Wolbarg_insert_one_v5",
3733
+ insertBatch: "Wolbarg_insert_batch_v5"
2179
3734
  };
3735
+ function toFloat4Param(embedding) {
3736
+ const out = new Array(embedding.length);
3737
+ for (let i = 0; i < embedding.length; i += 1) {
3738
+ out[i] = embedding[i];
3739
+ }
3740
+ return out;
3741
+ }
2180
3742
  function toVectorLiteral(embedding) {
2181
3743
  const n = embedding.length;
2182
3744
  let s = "[";
@@ -2186,17 +3748,45 @@ function toVectorLiteral(embedding) {
2186
3748
  }
2187
3749
  return s + "]";
2188
3750
  }
3751
+ function appendConnectionOption(connectionString, key, value) {
3752
+ try {
3753
+ const url = new URL(connectionString);
3754
+ if (key === "options") {
3755
+ const existing = url.searchParams.get("options");
3756
+ url.searchParams.set(
3757
+ "options",
3758
+ existing ? `${existing} ${value}`.trim() : value
3759
+ );
3760
+ }
3761
+ return url.toString();
3762
+ } catch {
3763
+ const sep = connectionString.includes("?") ? "&" : "?";
3764
+ return `${connectionString}${sep}${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
3765
+ }
3766
+ }
3767
+ function withPoolStartupOptions(connectionString, durableWrites) {
3768
+ const flags = [
3769
+ "-c jit=off",
3770
+ // Bake HNSW search GUCs into the connection — avoids per-recall set_config.
3771
+ "-c hnsw.ef_search=40",
3772
+ "-c hnsw.iterative_scan=relaxed_order"
3773
+ ];
3774
+ if (!durableWrites) {
3775
+ flags.unshift("-c synchronous_commit=off");
3776
+ }
3777
+ return appendConnectionOption(connectionString, "options", flags.join(" "));
3778
+ }
2189
3779
  var INSERT_ONE_SQL = `WITH mem AS (
2190
3780
  INSERT INTO memories (
2191
3781
  id, organization, agent, content_text, metadata_json,
2192
- archived, compressed_into, created_at, updated_at
2193
- ) VALUES ($1,$2,$3,$4,$5::jsonb,false,NULL,$6,$7)
3782
+ archived, compressed_into, content_hash, created_at, updated_at
3783
+ ) VALUES ($1,$2,$3,$4,$5::jsonb,false,NULL,$9,$6,$7)
2194
3784
  RETURNING id, organization, agent, content_text, metadata_json,
2195
- archived::int AS archived, compressed_into, created_at, updated_at
3785
+ archived::int AS archived, compressed_into, content_hash, created_at, updated_at
2196
3786
  ),
2197
3787
  hist AS (
2198
3788
  INSERT INTO memory_history (id, memory_id, event_type, related_memory_id, created_at)
2199
- SELECT $8, id, 'created', NULL, $6 FROM mem
3789
+ SELECT gen_random_uuid()::text, id, 'created', NULL, $6 FROM mem
2200
3790
  ),
2201
3791
  mapped AS (
2202
3792
  INSERT INTO memory_row_map (memory_id)
@@ -2204,9 +3794,8 @@ var INSERT_ONE_SQL = `WITH mem AS (
2204
3794
  ON CONFLICT (memory_id) DO NOTHING
2205
3795
  ),
2206
3796
  emb AS (
2207
- INSERT INTO memory_embeddings (memory_id, embedding)
2208
- SELECT id, $9::vector FROM mem
2209
- ON CONFLICT (memory_id) DO UPDATE SET embedding = EXCLUDED.embedding
3797
+ INSERT INTO memory_embeddings (memory_id, embedding, organization, agent, archived)
3798
+ SELECT id, $8::float4[]::vector, $2, $3, false FROM mem
2210
3799
  )
2211
3800
  SELECT * FROM mem`;
2212
3801
  var INSERT_BATCH_SQL = `WITH mem AS (
@@ -2219,13 +3808,12 @@ var INSERT_BATCH_SQL = `WITH mem AS (
2219
3808
  $1::text[], $2::text[], $3::text[], $4::text[],
2220
3809
  $5::text[], $6::timestamptz[], $7::timestamptz[]
2221
3810
  ) AS t(id, org, agent, txt, meta, c, u)
2222
- RETURNING id, organization, agent, content_text, metadata_json,
2223
- archived::int AS archived, compressed_into, created_at, updated_at
3811
+ RETURNING id
2224
3812
  ),
2225
3813
  hist AS (
2226
3814
  INSERT INTO memory_history (id, memory_id, event_type, related_memory_id, created_at)
2227
- SELECT h, m, 'created', NULL, c
2228
- FROM unnest($8::text[], $1::text[], $6::timestamptz[]) AS t(h, m, c)
3815
+ SELECT gen_random_uuid()::text, id, 'created', NULL, c
3816
+ FROM unnest($1::text[], $6::timestamptz[]) AS t(id, c)
2229
3817
  ),
2230
3818
  mapped AS (
2231
3819
  INSERT INTO memory_row_map (memory_id)
@@ -2233,31 +3821,42 @@ var INSERT_BATCH_SQL = `WITH mem AS (
2233
3821
  ON CONFLICT (memory_id) DO NOTHING
2234
3822
  ),
2235
3823
  emb AS (
2236
- INSERT INTO memory_embeddings (memory_id, embedding)
2237
- SELECT id, emb::vector
2238
- FROM unnest($1::text[], $9::text[]) AS t(id, emb)
2239
- ON CONFLICT (memory_id) DO UPDATE SET embedding = EXCLUDED.embedding
3824
+ INSERT INTO memory_embeddings (memory_id, embedding, organization, agent, archived)
3825
+ SELECT id, emb::vector, org, agent, false
3826
+ FROM unnest($1::text[], $8::text[], $2::text[], $3::text[]) AS t(id, emb, org, agent)
2240
3827
  )
2241
- SELECT * FROM mem`;
2242
- var COPY_BATCH_THRESHOLD = 64;
2243
- var PostgresStorageProvider = class {
3828
+ SELECT id FROM mem`;
3829
+ var COALESCE_FLUSH_MAX = 128;
3830
+ var COALESCE_FLUSH_THRESHOLD = 48;
3831
+ var COALESCE_MAX_PARALLEL = 24;
3832
+ var BULK_CHUNK_NO_HNSW = 500;
3833
+ var BULK_CHUNK_WITH_HNSW = 250;
3834
+ var COPY_BATCH_THRESHOLD = 48;
3835
+ var PostgresStorageProvider = class _PostgresStorageProvider {
2244
3836
  name = "postgres";
2245
3837
  connectionString;
2246
3838
  maxPoolSize;
3839
+ durableWrites;
2247
3840
  pool = null;
2248
3841
  vectorDimensions = null;
2249
3842
  hasPgvector = false;
2250
3843
  hnswIndexEnsured = false;
2251
3844
  hnswCreateFailures = 0;
3845
+ /** Dedup concurrent CREATE INDEX so mixed read/write storms don't pile up. */
3846
+ hnswBuildInFlight = null;
2252
3847
  hasContentTsv = false;
2253
- iterativeScanEnabled = false;
2254
3848
  /** Coalesce concurrent insertMemory callers into one unnest batch. */
2255
3849
  insertQueue = [];
2256
3850
  insertFlushScheduled = false;
2257
- insertFlushInFlight = false;
3851
+ insertFlushTimer = null;
3852
+ insertFlushInFlight = 0;
2258
3853
  constructor(options) {
2259
- this.connectionString = options.connectionString;
2260
- this.maxPoolSize = options.maxPoolSize ?? 32;
3854
+ this.maxPoolSize = options.maxPoolSize ?? 64;
3855
+ this.durableWrites = options.durableWrites !== false;
3856
+ this.connectionString = withPoolStartupOptions(
3857
+ options.connectionString,
3858
+ this.durableWrites
3859
+ );
2261
3860
  }
2262
3861
  getPoolStats() {
2263
3862
  const pool = this.pool;
@@ -2268,6 +3867,10 @@ var PostgresStorageProvider = class {
2268
3867
  waiting: pool?.waitingCount ?? 0
2269
3868
  };
2270
3869
  }
3870
+ /** Dedicated pool accessor for LISTEN/NOTIFY subscribe backend. */
3871
+ getPool() {
3872
+ return this.pool;
3873
+ }
2271
3874
  async open() {
2272
3875
  let PoolCtor;
2273
3876
  try {
@@ -2283,6 +3886,7 @@ var PostgresStorageProvider = class {
2283
3886
  connectionString: this.connectionString,
2284
3887
  max: this.maxPoolSize,
2285
3888
  idleTimeoutMillis: 3e4,
3889
+ connectionTimeoutMillis: 1e4,
2286
3890
  allowExitOnIdle: true,
2287
3891
  keepAlive: true
2288
3892
  });
@@ -2291,7 +3895,17 @@ var PostgresStorageProvider = class {
2291
3895
  const dims = await this.getEmbeddingDimensions();
2292
3896
  if (dims !== null) {
2293
3897
  this.vectorDimensions = dims;
2294
- await this.ensureVectorTables(dims);
3898
+ const schemaKey = `${this.connectionString}::${dims}`;
3899
+ if (!_PostgresStorageProvider.vectorSchemaReady.has(schemaKey)) {
3900
+ await this.ensureVectorTables(dims);
3901
+ _PostgresStorageProvider.vectorSchemaReady.add(schemaKey);
3902
+ } else {
3903
+ this.hasPgvector = true;
3904
+ const idx = await this.query(
3905
+ `SELECT 1 FROM pg_indexes WHERE indexname = 'idx_memory_embeddings_hnsw' LIMIT 1`
3906
+ ).catch(() => ({ rows: [] }));
3907
+ this.hnswIndexEnsured = idx.rows.length > 0;
3908
+ }
2295
3909
  }
2296
3910
  } catch (error) {
2297
3911
  await this.pool?.end().catch(() => void 0);
@@ -2306,12 +3920,45 @@ var PostgresStorageProvider = class {
2306
3920
  }
2307
3921
  }
2308
3922
  async close() {
3923
+ if (this.insertFlushTimer) {
3924
+ clearTimeout(this.insertFlushTimer);
3925
+ this.insertFlushTimer = null;
3926
+ }
3927
+ const deadline = Date.now() + 5e3;
3928
+ while ((this.insertQueue.length > 0 || this.insertFlushInFlight > 0) && Date.now() < deadline) {
3929
+ if (this.insertQueue.length > 0) {
3930
+ await this.flushInsertQueue();
3931
+ } else {
3932
+ await new Promise((r) => setTimeout(r, 5));
3933
+ }
3934
+ }
2309
3935
  if (!this.pool) {
2310
3936
  return;
2311
3937
  }
2312
3938
  await this.pool.end();
2313
3939
  this.pool = null;
2314
3940
  }
3941
+ /**
3942
+ * Run a search query. HNSW GUCs are baked into pool startup options —
3943
+ * no per-recall connect()+set_config round-trip.
3944
+ */
3945
+ async withSearchSession(fn) {
3946
+ return fn(this.requirePool());
3947
+ }
3948
+ /**
3949
+ * Drop HNSW so bulk / concurrent inserts avoid graph maintenance.
3950
+ * Next recall rebuilds the index (lazy).
3951
+ */
3952
+ async dropVectorIndex() {
3953
+ await this.query(`DROP INDEX IF EXISTS idx_memory_embeddings_hnsw`).catch(
3954
+ () => void 0
3955
+ );
3956
+ this.hnswIndexEnsured = false;
3957
+ }
3958
+ /** Force HNSW build now (e.g. before timed recall benches). */
3959
+ async ensureVectorIndex() {
3960
+ await this.ensureHnswIndex();
3961
+ }
2315
3962
  async ensureVectorSchema(dimensions) {
2316
3963
  const existing = await this.getEmbeddingDimensions();
2317
3964
  if (existing !== null && existing !== dimensions) {
@@ -2334,6 +3981,34 @@ var PostgresStorageProvider = class {
2334
3981
  embedding vector(${dimensions})
2335
3982
  )
2336
3983
  `);
3984
+ await this.query(
3985
+ `ALTER TABLE memory_embeddings ADD COLUMN IF NOT EXISTS organization TEXT`
3986
+ ).catch(() => void 0);
3987
+ await this.query(
3988
+ `ALTER TABLE memory_embeddings ADD COLUMN IF NOT EXISTS agent TEXT`
3989
+ ).catch(() => void 0);
3990
+ await this.query(
3991
+ `ALTER TABLE memory_embeddings ADD COLUMN IF NOT EXISTS archived BOOLEAN NOT NULL DEFAULT false`
3992
+ ).catch(() => void 0);
3993
+ const needsBackfill = await this.query(
3994
+ `SELECT 1 FROM memory_embeddings WHERE organization IS NULL LIMIT 1`
3995
+ ).catch(() => ({ rows: [] }));
3996
+ if (needsBackfill.rows.length > 0) {
3997
+ await this.query(`
3998
+ UPDATE memory_embeddings e
3999
+ SET organization = m.organization,
4000
+ agent = m.agent,
4001
+ archived = m.archived
4002
+ FROM memories m
4003
+ WHERE m.id = e.memory_id
4004
+ AND e.organization IS NULL
4005
+ `).catch(() => void 0);
4006
+ }
4007
+ await this.query(
4008
+ `CREATE INDEX IF NOT EXISTS idx_memory_embeddings_org_active
4009
+ ON memory_embeddings (organization)
4010
+ WHERE archived = false`
4011
+ ).catch(() => void 0);
2337
4012
  const idx = await this.query(
2338
4013
  `SELECT 1 FROM pg_indexes WHERE indexname = 'idx_memory_embeddings_hnsw' LIMIT 1`
2339
4014
  );
@@ -2347,6 +4022,10 @@ var PostgresStorageProvider = class {
2347
4022
  `);
2348
4023
  }
2349
4024
  }
4025
+ /** Cross-process subscribe: NOTIFY after a committed write. */
4026
+ async notifyChange(event) {
4027
+ await notifyMemoryChange(this.requirePool(), event);
4028
+ }
2350
4029
  /**
2351
4030
  * Soft reset for a single organization. Drops HNSW only when the embeddings
2352
4031
  * table is empty so other corpora on a shared bench DB stay intact.
@@ -2380,22 +4059,38 @@ var PostgresStorageProvider = class {
2380
4059
  if (!this.hasPgvector || this.hnswIndexEnsured) {
2381
4060
  return;
2382
4061
  }
4062
+ if (this.hnswBuildInFlight) {
4063
+ await this.hnswBuildInFlight;
4064
+ return;
4065
+ }
4066
+ this.hnswBuildInFlight = this.buildHnswIndex();
4067
+ try {
4068
+ await this.hnswBuildInFlight;
4069
+ } finally {
4070
+ this.hnswBuildInFlight = null;
4071
+ }
4072
+ }
4073
+ async buildHnswIndex() {
4074
+ if (this.hnswIndexEnsured) {
4075
+ return;
4076
+ }
2383
4077
  try {
2384
4078
  await this.query(`
2385
- CREATE INDEX IF NOT EXISTS idx_memory_embeddings_hnsw
4079
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_memory_embeddings_hnsw
2386
4080
  ON memory_embeddings USING hnsw (embedding vector_cosine_ops)
2387
- WITH (m = 16, ef_construction = 64)
4081
+ WITH (m = 16, ef_construction = 32)
2388
4082
  `);
2389
4083
  this.hnswIndexEnsured = true;
2390
4084
  this.hnswCreateFailures = 0;
2391
- if (!this.iterativeScanEnabled) {
2392
- try {
2393
- await this.query(`SET hnsw.iterative_scan = relaxed_order`);
2394
- this.iterativeScanEnabled = true;
2395
- } catch {
2396
- }
2397
- }
2398
4085
  } catch (error) {
4086
+ const idx = await this.query(
4087
+ `SELECT 1 FROM pg_indexes WHERE indexname = 'idx_memory_embeddings_hnsw' LIMIT 1`
4088
+ ).catch(() => ({ rows: [] }));
4089
+ if (idx.rows.length > 0) {
4090
+ this.hnswIndexEnsured = true;
4091
+ this.hnswCreateFailures = 0;
4092
+ return;
4093
+ }
2399
4094
  this.hnswCreateFailures += 1;
2400
4095
  if (this.hnswCreateFailures >= 3) {
2401
4096
  throw new DatabaseError(
@@ -2427,48 +4122,50 @@ var PostgresStorageProvider = class {
2427
4122
  }
2428
4123
  async insertMemory(input) {
2429
4124
  this.requireVectorReady();
2430
- if (!this.insertFlushInFlight && this.insertQueue.length === 0) {
2431
- this.insertFlushInFlight = true;
2432
- try {
2433
- return await this.insertMemoryImmediate(input);
2434
- } finally {
2435
- this.insertFlushInFlight = false;
2436
- if (this.insertQueue.length > 0) {
2437
- this.insertFlushScheduled = true;
2438
- queueMicrotask(() => {
2439
- void this.flushInsertQueue();
2440
- });
2441
- }
2442
- }
2443
- }
2444
4125
  return new Promise((resolve, reject) => {
2445
4126
  this.insertQueue.push({ input, resolve, reject });
2446
- if (this.insertQueue.length >= 16) {
2447
- if (this.insertFlushScheduled) {
2448
- this.insertFlushScheduled = false;
2449
- }
4127
+ if (this.insertQueue.length >= COALESCE_FLUSH_THRESHOLD) {
4128
+ this.clearInsertFlushTimer();
2450
4129
  void this.flushInsertQueue();
2451
4130
  return;
2452
4131
  }
2453
- if (!this.insertFlushScheduled) {
2454
- this.insertFlushScheduled = true;
2455
- queueMicrotask(() => {
2456
- void this.flushInsertQueue();
2457
- });
2458
- }
4132
+ this.scheduleInsertFlush();
4133
+ });
4134
+ }
4135
+ clearInsertFlushTimer() {
4136
+ if (this.insertFlushTimer) {
4137
+ clearImmediate(this.insertFlushTimer);
4138
+ clearTimeout(this.insertFlushTimer);
4139
+ this.insertFlushTimer = null;
4140
+ }
4141
+ this.insertFlushScheduled = false;
4142
+ }
4143
+ /** Flush coalesced inserts after a turn so concurrent remember() can join. */
4144
+ scheduleInsertFlush() {
4145
+ if (this.insertFlushScheduled) {
4146
+ return;
4147
+ }
4148
+ this.insertFlushScheduled = true;
4149
+ this.insertFlushTimer = setImmediate(() => {
4150
+ this.insertFlushTimer = null;
4151
+ this.insertFlushScheduled = false;
4152
+ void this.flushInsertQueue();
2459
4153
  });
2460
4154
  }
2461
4155
  async flushInsertQueue() {
2462
- if (this.insertFlushInFlight) {
4156
+ if (this.insertFlushInFlight >= COALESCE_MAX_PARALLEL) {
4157
+ this.scheduleInsertFlush();
2463
4158
  return;
2464
4159
  }
2465
- const batch = this.insertQueue;
2466
- this.insertQueue = [];
2467
- this.insertFlushScheduled = false;
4160
+ const batch = this.insertQueue.splice(0, COALESCE_FLUSH_MAX);
4161
+ this.clearInsertFlushTimer();
2468
4162
  if (batch.length === 0) {
2469
4163
  return;
2470
4164
  }
2471
- this.insertFlushInFlight = true;
4165
+ this.insertFlushInFlight += 1;
4166
+ if (this.insertQueue.length > 0 && this.insertFlushInFlight < COALESCE_MAX_PARALLEL) {
4167
+ void this.flushInsertQueue();
4168
+ }
2472
4169
  try {
2473
4170
  if (batch.length === 1) {
2474
4171
  const row = await this.insertMemoryImmediate(batch[0].input);
@@ -2484,20 +4181,16 @@ var PostgresStorageProvider = class {
2484
4181
  item.reject(error);
2485
4182
  }
2486
4183
  } finally {
2487
- this.insertFlushInFlight = false;
4184
+ this.insertFlushInFlight -= 1;
2488
4185
  if (this.insertQueue.length > 0) {
2489
- this.insertFlushScheduled = true;
2490
- queueMicrotask(() => {
2491
- void this.flushInsertQueue();
2492
- });
2493
- } else {
2494
- this.insertFlushScheduled = false;
4186
+ void this.flushInsertQueue();
2495
4187
  }
2496
4188
  }
2497
4189
  }
2498
4190
  /** Single-row insert without coalescing (used by flush + batch of 1). */
2499
4191
  async insertMemoryImmediate(input) {
2500
4192
  if (this.hasPgvector) {
4193
+ const contentHash = input.contentHash !== void 0 ? input.contentHash : hashMemoryContent(input.contentText);
2501
4194
  const inserted = await this.queryNamed(STMT.insertOne, INSERT_ONE_SQL, [
2502
4195
  input.id,
2503
4196
  input.organization,
@@ -2506,8 +4199,8 @@ var PostgresStorageProvider = class {
2506
4199
  serializeMetadata(input.metadata),
2507
4200
  input.createdAt,
2508
4201
  input.updatedAt,
2509
- crypto.randomUUID(),
2510
- toVectorLiteral(input.embedding)
4202
+ toFloat4Param(input.embedding),
4203
+ contentHash
2511
4204
  ]);
2512
4205
  return this.mapRow(inserted.rows[0]);
2513
4206
  }
@@ -2543,7 +4236,6 @@ var PostgresStorageProvider = class {
2543
4236
  const metas = new Array(inputs.length);
2544
4237
  const created = new Array(inputs.length);
2545
4238
  const updated = new Array(inputs.length);
2546
- const histIds = new Array(inputs.length);
2547
4239
  const vectors = new Array(inputs.length);
2548
4240
  for (let i = 0; i < inputs.length; i += 1) {
2549
4241
  const input = inputs[i];
@@ -2554,10 +4246,9 @@ var PostgresStorageProvider = class {
2554
4246
  metas[i] = serializeMetadata(input.metadata);
2555
4247
  created[i] = input.createdAt;
2556
4248
  updated[i] = input.updatedAt;
2557
- histIds[i] = crypto.randomUUID();
2558
4249
  vectors[i] = toVectorLiteral(input.embedding);
2559
4250
  }
2560
- const inserted = await this.queryNamed(STMT.insertBatch, INSERT_BATCH_SQL, [
4251
+ await this.queryNamed(STMT.insertBatch, INSERT_BATCH_SQL, [
2561
4252
  ids,
2562
4253
  orgs,
2563
4254
  agents,
@@ -2565,17 +4256,24 @@ var PostgresStorageProvider = class {
2565
4256
  metas,
2566
4257
  created,
2567
4258
  updated,
2568
- histIds,
2569
4259
  vectors
2570
4260
  ]);
2571
- const byId = new Map(
2572
- inserted.rows.map((r) => [String(r.id), this.mapRow(r)])
2573
- );
2574
- return ids.map((id) => byId.get(id));
4261
+ return inputs.map((input, i) => ({
4262
+ id: ids[i],
4263
+ organization: orgs[i],
4264
+ agent: agents[i],
4265
+ content_text: texts[i],
4266
+ metadata_json: metas[i],
4267
+ archived: 0,
4268
+ compressed_into: null,
4269
+ content_hash: input.contentHash !== void 0 ? input.contentHash : null,
4270
+ created_at: created[i],
4271
+ updated_at: updated[i]
4272
+ }));
2575
4273
  }
2576
- /** Split large ingest batches into parallel unnest chunks (pool pipelining). */
4274
+ /** Parallel unnest when HNSW is absent; sequential when index must stay consistent. */
2577
4275
  async insertBatchChunked(inputs) {
2578
- const chunkSize = 128;
4276
+ const chunkSize = this.hnswIndexEnsured ? BULK_CHUNK_WITH_HNSW : BULK_CHUNK_NO_HNSW;
2579
4277
  if (inputs.length <= chunkSize) {
2580
4278
  return this.insertBatchPgvector(inputs);
2581
4279
  }
@@ -2583,14 +4281,26 @@ var PostgresStorageProvider = class {
2583
4281
  for (let i = 0; i < inputs.length; i += chunkSize) {
2584
4282
  chunks.push(inputs.slice(i, i + chunkSize));
2585
4283
  }
2586
- const results = await Promise.all(
2587
- chunks.map((chunk) => this.insertBatchPgvector(chunk))
2588
- );
4284
+ if (!this.hnswIndexEnsured) {
4285
+ const parts = await Promise.all(
4286
+ chunks.map((chunk) => this.insertBatchPgvector(chunk))
4287
+ );
4288
+ const out2 = new Array(inputs.length);
4289
+ let offset2 = 0;
4290
+ for (const part of parts) {
4291
+ for (let j = 0; j < part.length; j += 1) {
4292
+ out2[offset2 + j] = part[j];
4293
+ }
4294
+ offset2 += part.length;
4295
+ }
4296
+ return out2;
4297
+ }
2589
4298
  const out = new Array(inputs.length);
2590
4299
  let offset = 0;
2591
- for (const part of results) {
2592
- for (let i = 0; i < part.length; i += 1) {
2593
- out[offset + i] = part[i];
4300
+ for (const chunk of chunks) {
4301
+ const part = await this.insertBatchPgvector(chunk);
4302
+ for (let j = 0; j < part.length; j += 1) {
4303
+ out[offset + j] = part[j];
2594
4304
  }
2595
4305
  offset += part.length;
2596
4306
  }
@@ -2602,13 +4312,14 @@ var PostgresStorageProvider = class {
2602
4312
  input.embedding.byteOffset,
2603
4313
  input.embedding.byteLength
2604
4314
  );
4315
+ const contentHash = input.contentHash !== void 0 ? input.contentHash : hashMemoryContent(input.contentText);
2605
4316
  const inserted = await this.query(
2606
4317
  `INSERT INTO memories (
2607
4318
  id, organization, agent, content_text, metadata_json,
2608
- archived, compressed_into, created_at, updated_at
2609
- ) VALUES ($1,$2,$3,$4,$5::jsonb,false,NULL,$6,$7)
4319
+ archived, compressed_into, content_hash, created_at, updated_at
4320
+ ) VALUES ($1,$2,$3,$4,$5::jsonb,false,NULL,$8,$6,$7)
2610
4321
  RETURNING id, organization, agent, content_text, metadata_json,
2611
- archived::int AS archived, compressed_into, created_at, updated_at`,
4322
+ archived::int AS archived, compressed_into, content_hash, created_at, updated_at`,
2612
4323
  [
2613
4324
  input.id,
2614
4325
  input.organization,
@@ -2616,7 +4327,8 @@ var PostgresStorageProvider = class {
2616
4327
  input.contentText,
2617
4328
  serializeMetadata(input.metadata),
2618
4329
  input.createdAt,
2619
- input.updatedAt
4330
+ input.updatedAt,
4331
+ contentHash
2620
4332
  ]
2621
4333
  );
2622
4334
  const row = this.mapRow(inserted.rows[0]);
@@ -2642,15 +4354,18 @@ var PostgresStorageProvider = class {
2642
4354
  if (!existing) {
2643
4355
  return null;
2644
4356
  }
4357
+ const contentHash = input.contentHash !== void 0 ? input.contentHash : input.contentText !== void 0 ? hashMemoryContent(input.contentText) : existing.content_hash ?? null;
2645
4358
  await this.query(
2646
4359
  `UPDATE memories SET
2647
4360
  content_text = COALESCE($1, content_text),
2648
4361
  metadata_json = COALESCE($2::jsonb, metadata_json),
2649
- updated_at = $3
2650
- WHERE id = $4 AND organization = $5`,
4362
+ content_hash = COALESCE($3, content_hash),
4363
+ updated_at = $4
4364
+ WHERE id = $5 AND organization = $6`,
2651
4365
  [
2652
4366
  input.contentText ?? null,
2653
4367
  input.metadata !== void 0 ? serializeMetadata(input.metadata) : null,
4368
+ contentHash,
2654
4369
  input.updatedAt,
2655
4370
  input.id,
2656
4371
  input.organization
@@ -2660,12 +4375,29 @@ var PostgresStorageProvider = class {
2660
4375
  await this.deleteEmbedding(input.id);
2661
4376
  await this.insertEmbedding(input.id, input.embedding);
2662
4377
  }
4378
+ await this.query(
4379
+ `INSERT INTO memory_history (id, memory_id, event_type, related_memory_id, created_at)
4380
+ VALUES ($1,$2,'updated',NULL,$3)`,
4381
+ [crypto.randomUUID(), input.id, input.updatedAt]
4382
+ );
2663
4383
  return this.getMemoryById(input.id, input.organization);
2664
4384
  }
4385
+ async findActiveByContentHash(organization, agent, contentHash) {
4386
+ const result = await this.query(
4387
+ `SELECT id, organization, agent, content_text, metadata_json,
4388
+ archived::int AS archived, compressed_into, content_hash, created_at, updated_at
4389
+ FROM memories
4390
+ WHERE organization = $1 AND agent = $2 AND content_hash = $3 AND archived = false
4391
+ LIMIT 1`,
4392
+ [organization, agent, contentHash]
4393
+ );
4394
+ const row = result.rows[0];
4395
+ return row ? this.mapRow(row) : null;
4396
+ }
2665
4397
  async getMemoryById(id, organization) {
2666
4398
  const result = await this.query(
2667
4399
  `SELECT id, organization, agent, content_text, metadata_json,
2668
- archived::int AS archived, compressed_into, created_at, updated_at
4400
+ archived::int AS archived, compressed_into, content_hash, created_at, updated_at
2669
4401
  FROM memories WHERE id = $1 AND organization = $2`,
2670
4402
  [id, organization]
2671
4403
  );
@@ -2708,6 +4440,33 @@ var PostgresStorageProvider = class {
2708
4440
  return out;
2709
4441
  }
2710
4442
  async listMemories(filter, limit) {
4443
+ const want = limit !== void 0 ? limit : filter.metadata ? 1e4 : void 0;
4444
+ const compiled = filter.metadata ? compileMetadataFilterToPostgres(filter.metadata, 2) : null;
4445
+ if (compiled) {
4446
+ const clauses2 = [`organization = $1`, `(${compiled.expression})`];
4447
+ const params2 = [filter.organization, ...compiled.params];
4448
+ let idx2 = params2.length + 1;
4449
+ if (filter.agent) {
4450
+ clauses2.push(`agent = $${idx2++}`);
4451
+ params2.push(filter.agent);
4452
+ }
4453
+ if (!filter.includeArchived) {
4454
+ clauses2.push(`archived = false`);
4455
+ }
4456
+ let sql2 = `
4457
+ SELECT id, organization, agent, content_text, metadata_json,
4458
+ archived::int AS archived, compressed_into, created_at, updated_at
4459
+ FROM memories
4460
+ WHERE ${clauses2.join(" AND ")}
4461
+ ORDER BY created_at ASC
4462
+ `;
4463
+ if (want !== void 0) {
4464
+ sql2 += ` LIMIT $${idx2}`;
4465
+ params2.push(want);
4466
+ }
4467
+ const result2 = await this.query(sql2, params2);
4468
+ return result2.rows.map((r) => this.mapRow(r));
4469
+ }
2711
4470
  const clauses = [`organization = $1`];
2712
4471
  const params = [filter.organization];
2713
4472
  let idx = 2;
@@ -2778,27 +4537,30 @@ var PostgresStorageProvider = class {
2778
4537
  this.requireVectorReady();
2779
4538
  if (this.hasPgvector) {
2780
4539
  await this.ensureHnswIndex();
2781
- const result2 = await this.query(
2782
- `SELECT r.row_num AS memory_rowid, ann.distance
2783
- FROM (
2784
- SELECT e.memory_id, (e.embedding <=> $1::vector) AS distance
2785
- FROM memory_embeddings e
2786
- ORDER BY e.embedding <=> $1::vector
2787
- LIMIT $2
2788
- ) ann
2789
- JOIN memory_row_map r ON r.memory_id = ann.memory_id
2790
- ORDER BY ann.distance`,
2791
- [toVectorLiteral(embedding), topK]
2792
- );
2793
- const hits = new Array(result2.rows.length);
2794
- for (let i = 0; i < result2.rows.length; i += 1) {
2795
- const row = result2.rows[i];
2796
- hits[i] = {
2797
- memoryRowid: Number(row.memory_rowid),
2798
- distance: Number(row.distance)
2799
- };
2800
- }
2801
- return hits;
4540
+ return this.withSearchSession(async (client) => {
4541
+ const result2 = await client.query(
4542
+ `SELECT r.row_num AS memory_rowid, ann.distance
4543
+ FROM (
4544
+ SELECT e.memory_id, (e.embedding <=> $1::float4[]::vector) AS distance
4545
+ FROM memory_embeddings e
4546
+ WHERE e.archived = false
4547
+ ORDER BY e.embedding <=> $1::float4[]::vector
4548
+ LIMIT $2
4549
+ ) ann
4550
+ JOIN memory_row_map r ON r.memory_id = ann.memory_id
4551
+ ORDER BY ann.distance`,
4552
+ [toFloat4Param(embedding), topK]
4553
+ );
4554
+ const hits = new Array(result2.rows.length);
4555
+ for (let i = 0; i < result2.rows.length; i += 1) {
4556
+ const row = result2.rows[i];
4557
+ hits[i] = {
4558
+ memoryRowid: Number(row.memory_rowid),
4559
+ distance: Number(row.distance)
4560
+ };
4561
+ }
4562
+ return hits;
4563
+ });
2802
4564
  }
2803
4565
  const result = await this.query(
2804
4566
  `SELECT r.row_num AS memory_rowid, e.embedding
@@ -2823,65 +4585,66 @@ var PostgresStorageProvider = class {
2823
4585
  async searchVectorsWithMemories(embedding, topK, organization, options) {
2824
4586
  this.requireVectorReady();
2825
4587
  if (!this.hasPgvector) {
2826
- const hits = await this.searchVectors(embedding, topK);
2827
- const map = await this.getMemoriesByRowids(
2828
- hits.map((h) => h.memoryRowid),
2829
- organization
2830
- );
2831
- const out = [];
2832
- for (const hit of hits) {
2833
- const row = map.get(hit.memoryRowid);
2834
- if (!row) continue;
2835
- if (options?.agent && row.agent !== options.agent) continue;
2836
- if (!options?.includeArchived && row.archived === 1) continue;
2837
- out.push({ row, distance: hit.distance });
4588
+ const clauses = [`m.organization = $1`];
4589
+ const params2 = [organization];
4590
+ if (options?.agent) {
4591
+ clauses.push(`m.agent = $2`);
4592
+ params2.push(options.agent);
2838
4593
  }
2839
- return out;
4594
+ if (!options?.includeArchived) {
4595
+ clauses.push(`m.archived = false`);
4596
+ }
4597
+ const result2 = await this.query(
4598
+ `SELECT m.id, m.organization, m.agent, m.content_text, m.metadata_json,
4599
+ m.archived::int AS archived, m.compressed_into, m.created_at, m.updated_at,
4600
+ r.row_num AS rowid, e.embedding
4601
+ FROM memory_embeddings_blob e
4602
+ JOIN memory_row_map r ON r.memory_id = e.memory_id
4603
+ JOIN memories m ON m.id = e.memory_id
4604
+ WHERE ${clauses.join(" AND ")}`,
4605
+ params2
4606
+ );
4607
+ const scored = result2.rows.map((row) => {
4608
+ const buf = row.embedding;
4609
+ const vec2 = new Float32Array(
4610
+ buf.buffer,
4611
+ buf.byteOffset,
4612
+ buf.byteLength / Float32Array.BYTES_PER_ELEMENT
4613
+ );
4614
+ return {
4615
+ row: this.mapRow(row),
4616
+ distance: cosineDistance(embedding, vec2)
4617
+ };
4618
+ });
4619
+ scored.sort((a, b) => a.distance - b.distance);
4620
+ return scored.slice(0, topK);
2840
4621
  }
2841
4622
  await this.ensureHnswIndex();
2842
- const vec = toVectorLiteral(embedding);
4623
+ const vec = toFloat4Param(embedding);
2843
4624
  const mapHits = (rows) => rows.map((row) => ({
2844
4625
  row: this.mapRow(row),
2845
4626
  distance: Number(row.distance)
2846
4627
  }));
2847
- let overfetch = Math.min(Math.max(topK * 8, topK), 512);
2848
- const maxFetch = Math.min(Math.max(topK * 64, 512), 8192);
2849
- for (; ; ) {
2850
- const agentClause = options?.agent ? "AND m.agent = $5" : "";
2851
- const archivedClause = options?.includeArchived ? "" : "AND m.archived = false";
2852
- const params = options?.agent ? [vec, overfetch, organization, topK, options.agent] : [vec, overfetch, organization, topK];
2853
- const ann = await this.query(
2854
- `WITH ann AS (
2855
- SELECT e.memory_id, (e.embedding <=> $1::vector) AS distance
2856
- FROM memory_embeddings e
2857
- ORDER BY e.embedding <=> $1::vector
2858
- LIMIT $2
2859
- )
2860
- SELECT m.id, m.organization, m.agent, m.content_text, m.metadata_json,
2861
- m.archived::int AS archived, m.compressed_into, m.created_at, m.updated_at,
2862
- r.row_num AS rowid,
2863
- ann.distance,
2864
- (SELECT COUNT(*)::int FROM ann) AS ann_fetched
2865
- FROM ann
2866
- JOIN memory_row_map r ON r.memory_id = ann.memory_id
2867
- JOIN memories m ON m.id = ann.memory_id
2868
- WHERE m.organization = $3
2869
- ${agentClause}
2870
- ${archivedClause}
2871
- ORDER BY ann.distance
2872
- LIMIT $4`,
2873
- params
2874
- );
2875
- const annFetched = Number(ann.rows[0]?.ann_fetched ?? ann.rows.length);
2876
- if (ann.rows.length >= topK || annFetched < overfetch || overfetch >= maxFetch) {
2877
- return mapHits(ann.rows.slice(0, topK));
2878
- }
2879
- const next = Math.min(overfetch * 4, maxFetch);
2880
- if (next === overfetch) {
2881
- return mapHits(ann.rows.slice(0, topK));
2882
- }
2883
- overfetch = next;
2884
- }
4628
+ const agentClause = options?.agent ? "AND e.agent = $4" : "";
4629
+ const archivedClause = options?.includeArchived ? "" : "AND e.archived = false";
4630
+ const params = options?.agent ? [vec, topK, organization, options.agent] : [vec, topK, organization];
4631
+ const result = await this.query(
4632
+ `SELECT m.id, m.organization, m.agent, m.content_text, m.metadata_json,
4633
+ m.archived::int AS archived, m.compressed_into, m.created_at, m.updated_at,
4634
+ r.row_num AS rowid,
4635
+ (e.embedding <=> $1::float4[]::vector) AS distance
4636
+ FROM memory_embeddings e
4637
+ JOIN memory_row_map r ON r.memory_id = e.memory_id
4638
+ JOIN memories m ON m.id = e.memory_id
4639
+ WHERE e.organization = $3
4640
+ ${agentClause}
4641
+ ${archivedClause}
4642
+ AND m.organization = $3
4643
+ ORDER BY e.embedding <=> $1::float4[]::vector
4644
+ LIMIT $2`,
4645
+ params
4646
+ );
4647
+ return mapHits(result.rows);
2885
4648
  }
2886
4649
  async archiveMemories(ids, organization, compressedIntoId, archivedAt) {
2887
4650
  if (ids.length === 0) {
@@ -2898,6 +4661,12 @@ var PostgresStorageProvider = class {
2898
4661
  if (archived.length === 0) {
2899
4662
  return [];
2900
4663
  }
4664
+ await this.query(
4665
+ `UPDATE memory_embeddings
4666
+ SET archived = true
4667
+ WHERE memory_id = ANY($1::text[])`,
4668
+ [archived]
4669
+ ).catch(() => void 0);
2901
4670
  const histIds = [];
2902
4671
  const memIds = [];
2903
4672
  const types = [];
@@ -3021,6 +4790,20 @@ var PostgresStorageProvider = class {
3021
4790
  value TEXT NOT NULL
3022
4791
  )
3023
4792
  `);
4793
+ const versionRow = await this.query(
4794
+ `SELECT value FROM Wolbarg_meta WHERE key = $1`,
4795
+ [META_KEYS.schemaVersion]
4796
+ ).catch(() => ({ rows: [] }));
4797
+ const current = versionRow.rows[0]?.value !== void 0 ? Number(versionRow.rows[0].value) : null;
4798
+ if (current === SCHEMA_VERSION) {
4799
+ const tsvProbe2 = await this.query(
4800
+ `SELECT 1 FROM information_schema.columns
4801
+ WHERE table_name = 'memories' AND column_name = 'content_tsv'
4802
+ LIMIT 1`
4803
+ ).catch(() => ({ rows: [] }));
4804
+ this.hasContentTsv = tsvProbe2.rows.length > 0;
4805
+ return;
4806
+ }
3024
4807
  await this.query(`
3025
4808
  CREATE TABLE IF NOT EXISTS memories (
3026
4809
  id TEXT PRIMARY KEY NOT NULL,
@@ -3038,7 +4821,7 @@ var PostgresStorageProvider = class {
3038
4821
  CREATE TABLE IF NOT EXISTS memory_history (
3039
4822
  id TEXT PRIMARY KEY NOT NULL,
3040
4823
  memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
3041
- event_type TEXT NOT NULL CHECK (event_type IN ('created', 'archived', 'compressed')),
4824
+ event_type TEXT NOT NULL CHECK (event_type IN ('created', 'archived', 'compressed', 'updated')),
3042
4825
  related_memory_id TEXT NULL,
3043
4826
  created_at TIMESTAMPTZ NOT NULL
3044
4827
  )
@@ -3053,8 +4836,8 @@ var PostgresStorageProvider = class {
3053
4836
  `CREATE INDEX IF NOT EXISTS idx_memories_org_agent ON memories(organization, agent)`
3054
4837
  );
3055
4838
  await this.query(
3056
- `CREATE INDEX IF NOT EXISTS idx_memories_org_archived ON memories(organization, archived)`
3057
- );
4839
+ `DROP INDEX IF EXISTS idx_memories_org_archived`
4840
+ ).catch(() => void 0);
3058
4841
  await this.query(
3059
4842
  `CREATE INDEX IF NOT EXISTS idx_memories_org_active_created
3060
4843
  ON memories(organization, created_at) WHERE archived = false`
@@ -3062,53 +4845,132 @@ var PostgresStorageProvider = class {
3062
4845
  await this.query(
3063
4846
  `CREATE INDEX IF NOT EXISTS idx_memories_metadata ON memories USING GIN (metadata_json)`
3064
4847
  );
3065
- try {
3066
- await this.query(`
3067
- ALTER TABLE memories
3068
- ADD COLUMN IF NOT EXISTS content_tsv tsvector
3069
- GENERATED ALWAYS AS (to_tsvector('english', content_text)) STORED
3070
- `);
3071
- await this.query(
3072
- `CREATE INDEX IF NOT EXISTS idx_memories_content_tsv ON memories USING GIN (content_tsv)`
3073
- );
3074
- this.hasContentTsv = true;
3075
- } catch {
3076
- await this.query(
3077
- `CREATE INDEX IF NOT EXISTS idx_memories_fts
3078
- ON memories USING GIN (to_tsvector('english', content_text))`
3079
- ).catch(() => void 0);
3080
- this.hasContentTsv = false;
3081
- }
3082
- const versionRow = await this.query(
4848
+ await this.query(
4849
+ `ALTER TABLE memories ADD COLUMN IF NOT EXISTS content_hash TEXT`
4850
+ ).catch(() => void 0);
4851
+ await this.query(
4852
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_org_agent_hash_active
4853
+ ON memories(organization, agent, content_hash)
4854
+ WHERE archived = false AND content_hash IS NOT NULL`
4855
+ ).catch(() => void 0);
4856
+ await this.query(`
4857
+ CREATE TABLE IF NOT EXISTS embedding_cache (
4858
+ cache_key TEXT PRIMARY KEY NOT NULL,
4859
+ model TEXT NOT NULL,
4860
+ vector BYTEA NOT NULL,
4861
+ created_at TIMESTAMPTZ NOT NULL,
4862
+ last_used_at TIMESTAMPTZ NOT NULL
4863
+ )
4864
+ `).catch(() => void 0);
4865
+ await this.query(`
4866
+ DO $$
4867
+ BEGIN
4868
+ ALTER TABLE memory_history DROP CONSTRAINT IF EXISTS memory_history_event_type_check;
4869
+ ALTER TABLE memory_history ADD CONSTRAINT memory_history_event_type_check
4870
+ CHECK (event_type IN ('created', 'archived', 'compressed', 'updated'));
4871
+ EXCEPTION WHEN others THEN
4872
+ NULL;
4873
+ END $$;
4874
+ `).catch(() => void 0);
4875
+ const priorVersion = await this.query(
3083
4876
  `SELECT value FROM Wolbarg_meta WHERE key = $1`,
3084
4877
  [META_KEYS.schemaVersion]
3085
- );
3086
- if (!versionRow.rows[0]) {
3087
- await this.query(
3088
- `INSERT INTO Wolbarg_meta (key, value) VALUES ($1, $2)`,
3089
- [META_KEYS.schemaVersion, String(SCHEMA_VERSION)]
3090
- );
4878
+ ).catch(() => ({ rows: [] }));
4879
+ const prior = priorVersion.rows[0]?.value !== void 0 ? Number(priorVersion.rows[0].value) : null;
4880
+ const needsHashBackfill = prior === null || !Number.isFinite(prior) || prior < 3;
4881
+ if (needsHashBackfill) {
4882
+ const active = await this.query(
4883
+ `SELECT id, organization, agent, content_text, updated_at
4884
+ FROM memories WHERE archived = false AND content_hash IS NULL
4885
+ ORDER BY updated_at DESC`
4886
+ ).catch(() => ({ rows: [] }));
4887
+ const seen = /* @__PURE__ */ new Set();
4888
+ for (const row of active.rows) {
4889
+ const hash = hashMemoryContent(String(row.content_text));
4890
+ const key = `${row.organization}\0${row.agent}\0${hash}`;
4891
+ if (seen.has(key)) {
4892
+ await this.query(
4893
+ `UPDATE memories SET content_hash = NULL WHERE id = $1`,
4894
+ [row.id]
4895
+ ).catch(() => void 0);
4896
+ } else {
4897
+ seen.add(key);
4898
+ await this.query(
4899
+ `UPDATE memories SET content_hash = $1 WHERE id = $2`,
4900
+ [hash, row.id]
4901
+ ).catch(() => void 0);
4902
+ }
4903
+ }
4904
+ }
4905
+ await this.query(
4906
+ `INSERT INTO Wolbarg_meta (key, value) VALUES ($1, $2)
4907
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`,
4908
+ [META_KEYS.schemaVersion, String(SCHEMA_VERSION)]
4909
+ ).catch(() => void 0);
4910
+ const tsvProbe = await this.query(
4911
+ `SELECT 1 FROM information_schema.columns
4912
+ WHERE table_name = 'memories' AND column_name = 'content_tsv'
4913
+ LIMIT 1`
4914
+ ).catch(() => ({ rows: [] }));
4915
+ if (tsvProbe.rows.length > 0) {
4916
+ this.hasContentTsv = true;
4917
+ } else {
4918
+ try {
4919
+ await this.query(`
4920
+ ALTER TABLE memories
4921
+ ADD COLUMN IF NOT EXISTS content_tsv tsvector
4922
+ GENERATED ALWAYS AS (to_tsvector('english', content_text)) STORED
4923
+ `);
4924
+ await this.query(
4925
+ `CREATE INDEX IF NOT EXISTS idx_memories_content_tsv ON memories USING GIN (content_tsv)`
4926
+ );
4927
+ this.hasContentTsv = true;
4928
+ } catch {
4929
+ await this.query(
4930
+ `CREATE INDEX IF NOT EXISTS idx_memories_fts
4931
+ ON memories USING GIN (to_tsvector('english', content_text))`
4932
+ ).catch(() => void 0);
4933
+ this.hasContentTsv = false;
4934
+ }
3091
4935
  }
3092
4936
  }
3093
4937
  async tryEnablePgvector() {
4938
+ const cached = _PostgresStorageProvider.pgvectorByConn.get(
4939
+ this.connectionString
4940
+ );
4941
+ if (cached !== void 0) {
4942
+ return cached;
4943
+ }
3094
4944
  try {
3095
4945
  await this.query(`CREATE EXTENSION IF NOT EXISTS vector`);
4946
+ _PostgresStorageProvider.pgvectorByConn.set(this.connectionString, true);
3096
4947
  return true;
3097
4948
  } catch {
4949
+ _PostgresStorageProvider.pgvectorByConn.set(this.connectionString, false);
3098
4950
  return false;
3099
4951
  }
3100
4952
  }
4953
+ static pgvectorByConn = /* @__PURE__ */ new Map();
4954
+ static vectorSchemaReady = /* @__PURE__ */ new Set();
3101
4955
  async insertEmbedding(memoryId, embedding) {
3102
4956
  if (this.hasPgvector) {
3103
4957
  await this.query(
3104
4958
  `WITH mapped AS (
3105
4959
  INSERT INTO memory_row_map (memory_id) VALUES ($1)
3106
4960
  ON CONFLICT (memory_id) DO NOTHING
4961
+ ),
4962
+ meta AS (
4963
+ SELECT organization, agent, archived FROM memories WHERE id = $1
3107
4964
  )
3108
- INSERT INTO memory_embeddings (memory_id, embedding)
3109
- VALUES ($1, $2::vector)
3110
- ON CONFLICT (memory_id) DO UPDATE SET embedding = EXCLUDED.embedding`,
3111
- [memoryId, toVectorLiteral(embedding)]
4965
+ INSERT INTO memory_embeddings (memory_id, embedding, organization, agent, archived)
4966
+ SELECT $1, $2::float4[]::vector, meta.organization, meta.agent, meta.archived
4967
+ FROM meta
4968
+ ON CONFLICT (memory_id) DO UPDATE SET
4969
+ embedding = EXCLUDED.embedding,
4970
+ organization = EXCLUDED.organization,
4971
+ agent = EXCLUDED.agent,
4972
+ archived = EXCLUDED.archived`,
4973
+ [memoryId, toFloat4Param(embedding)]
3112
4974
  );
3113
4975
  return;
3114
4976
  }
@@ -3163,6 +5025,7 @@ var PostgresStorageProvider = class {
3163
5025
  metadata_json,
3164
5026
  archived: Number(row.archived ?? 0),
3165
5027
  compressed_into: row.compressed_into === null || row.compressed_into === void 0 ? null : String(row.compressed_into),
5028
+ content_hash: row.content_hash === null || row.content_hash === void 0 ? null : String(row.content_hash),
3166
5029
  created_at: created,
3167
5030
  updated_at: updated,
3168
5031
  rowid: row.rowid !== void 0 ? Number(row.rowid) : void 0
@@ -3195,7 +5058,7 @@ var PostgresStorageProvider = class {
3195
5058
  };
3196
5059
 
3197
5060
  // src/storage/index.ts
3198
- function createStorageProvider(config) {
5061
+ function createStorageProvider(config, options) {
3199
5062
  const connectionString = resolveDatabaseUrl(config);
3200
5063
  if (!connectionString) {
3201
5064
  throw new ConfigurationError(
@@ -3204,13 +5067,15 @@ function createStorageProvider(config) {
3204
5067
  }
3205
5068
  if (config.provider === "sqlite") {
3206
5069
  return new SqliteStorageProvider({
3207
- connectionString
5070
+ connectionString,
5071
+ concurrency: options?.concurrency
3208
5072
  });
3209
5073
  }
3210
5074
  if (config.provider === "postgres") {
3211
5075
  return new PostgresStorageProvider({
3212
5076
  connectionString,
3213
- maxPoolSize: config.maxPoolSize
5077
+ maxPoolSize: config.maxPoolSize,
5078
+ durableWrites: config.durableWrites
3214
5079
  });
3215
5080
  }
3216
5081
  throw new ConfigurationError(
@@ -3534,7 +5399,8 @@ function normalizeDatabaseConfig(config) {
3534
5399
  provider: "postgres",
3535
5400
  connectionString,
3536
5401
  url: connectionString,
3537
- ..."maxPoolSize" in config && config.maxPoolSize !== void 0 ? { maxPoolSize: config.maxPoolSize } : {}
5402
+ ..."maxPoolSize" in config && config.maxPoolSize !== void 0 ? { maxPoolSize: config.maxPoolSize } : {},
5403
+ ..."durableWrites" in config && config.durableWrites !== void 0 ? { durableWrites: config.durableWrites } : {}
3538
5404
  };
3539
5405
  }
3540
5406
  return {
@@ -4533,6 +6399,66 @@ Ensure no exclusive lock is held and the destination directory is writable.`,
4533
6399
  }
4534
6400
  }
4535
6401
  }
6402
+ function matchesFilter2(filter, event) {
6403
+ if (filter.organization !== event.organization) {
6404
+ return false;
6405
+ }
6406
+ if (filter.agent !== void 0 && filter.agent !== event.agent) {
6407
+ return false;
6408
+ }
6409
+ if (filter.event === void 0) {
6410
+ return true;
6411
+ }
6412
+ const events = Array.isArray(filter.event) ? filter.event : [filter.event];
6413
+ return events.some(
6414
+ (e) => e === "*" || e === event.event
6415
+ );
6416
+ }
6417
+ var SqliteSubscribeEmitter = class {
6418
+ emitter = new EventEmitter();
6419
+ subscriptions = /* @__PURE__ */ new Map();
6420
+ nextId = 1;
6421
+ closed = false;
6422
+ constructor() {
6423
+ this.emitter.setMaxListeners(0);
6424
+ this.emitter.on("change", (event) => {
6425
+ for (const sub of this.subscriptions.values()) {
6426
+ if (!matchesFilter2(sub.filter, event)) {
6427
+ continue;
6428
+ }
6429
+ try {
6430
+ sub.callback(event);
6431
+ } catch (error) {
6432
+ console.error(
6433
+ "[wolbarg] subscribe callback error:",
6434
+ error instanceof Error ? error.message : error
6435
+ );
6436
+ }
6437
+ }
6438
+ });
6439
+ }
6440
+ subscribe(filter, callback) {
6441
+ if (this.closed) {
6442
+ throw new Error("Subscribe backend is closed");
6443
+ }
6444
+ const id = this.nextId++;
6445
+ this.subscriptions.set(id, { filter, callback });
6446
+ return () => {
6447
+ this.subscriptions.delete(id);
6448
+ };
6449
+ }
6450
+ emit(event) {
6451
+ if (this.closed) {
6452
+ return;
6453
+ }
6454
+ this.emitter.emit("change", event);
6455
+ }
6456
+ async close() {
6457
+ this.closed = true;
6458
+ this.subscriptions.clear();
6459
+ this.emitter.removeAllListeners();
6460
+ }
6461
+ };
4536
6462
 
4537
6463
  // src/core/wolbarg.ts
4538
6464
  var Wolbarg = class {
@@ -4555,6 +6481,11 @@ var Wolbarg = class {
4555
6481
  checkpointProvider = null;
4556
6482
  memoryDbPath = null;
4557
6483
  transfer = new SqliteMemoryTransferProvider();
6484
+ subscribeBackend = null;
6485
+ pgListenBackend = null;
6486
+ memoryDedupe = resolveMemoryDedupeConfig();
6487
+ embeddingCacheConfig = resolveEmbeddingCacheConfig();
6488
+ rawEmbedding = null;
4558
6489
  constructor(options) {
4559
6490
  this.telemetry = new TelemetryEmitter(null, { enabled: false, level: "off" });
4560
6491
  if (!options) {
@@ -4562,14 +6493,22 @@ var Wolbarg = class {
4562
6493
  }
4563
6494
  const validated = validateWolbargOptions(options);
4564
6495
  this.organization = validated.organization;
6496
+ this.memoryDedupe = resolveMemoryDedupeConfig(validated.memory?.dedupe);
6497
+ this.embeddingCacheConfig = resolveEmbeddingCacheConfig(
6498
+ validated.embeddingCache
6499
+ );
4565
6500
  const storageInput = validated.storage;
4566
- this.storage = isStorageProvider(storageInput) ? storageInput : createStorageProvider(storageInput);
6501
+ this.storage = isStorageProvider(storageInput) ? storageInput : createStorageProvider(storageInput, {
6502
+ concurrency: validated.concurrency
6503
+ });
4567
6504
  if (!isStorageProvider(storageInput)) {
4568
6505
  this.memoryDbPath = resolveDatabaseUrl(storageInput);
4569
6506
  } else if (storageInput instanceof SqliteStorageProvider) {
4570
6507
  this.memoryDbPath = storageInput.path;
4571
6508
  }
4572
- this.embedding = isEmbeddingProvider(validated.embedding) ? validated.embedding : createEmbeddingProvider(validated.embedding);
6509
+ const embedding = isEmbeddingProvider(validated.embedding) ? validated.embedding : createEmbeddingProvider(validated.embedding);
6510
+ this.rawEmbedding = embedding;
6511
+ this.embedding = embedding;
4573
6512
  if (validated.llm) {
4574
6513
  this.llm = isLlmProvider(validated.llm) ? validated.llm : createLlmProvider(validated.llm);
4575
6514
  this.compression = validated.compression ?? createCompressionProvider(this.llm);
@@ -4582,6 +6521,7 @@ var Wolbarg = class {
4582
6521
  this.vision = validated.vision ?? null;
4583
6522
  this.chunking = validated.chunking ?? null;
4584
6523
  this.retrievalConfig = validated.retrieval ?? {};
6524
+ this.subscribeBackend = new SqliteSubscribeEmitter();
4585
6525
  if (validated.telemetry) {
4586
6526
  if (isTelemetryProvider(validated.telemetry)) {
4587
6527
  this.telemetry = new TelemetryEmitter(validated.telemetry, {
@@ -4657,9 +6597,54 @@ var Wolbarg = class {
4657
6597
  await this.storage.open();
4658
6598
  await this.telemetry.open();
4659
6599
  await this.checkpointProvider?.open();
4660
- const probe = await this.embedding.validate();
4661
- await this.storage.ensureVectorSchema(probe.dimensions);
4662
- this.embeddingDimensions = probe.dimensions;
6600
+ if (this.rawEmbedding && this.embeddingCacheConfig.enabled) {
6601
+ if (this.storage instanceof SqliteStorageProvider) {
6602
+ const sqlite2 = this.storage;
6603
+ const store = new SqliteEmbeddingCacheStore(() => sqlite2.getDatabase(), {
6604
+ ttlMs: this.embeddingCacheConfig.ttlMs
6605
+ });
6606
+ this.embedding = withEmbeddingCache(
6607
+ this.rawEmbedding,
6608
+ store,
6609
+ this.embeddingCacheConfig
6610
+ );
6611
+ } else if (this.storage instanceof PostgresStorageProvider) {
6612
+ this.embedding = withEmbeddingCache(
6613
+ this.rawEmbedding,
6614
+ new PostgresEmbeddingCacheStore(() => null, {
6615
+ ttlMs: this.embeddingCacheConfig.ttlMs,
6616
+ durable: false
6617
+ }),
6618
+ this.embeddingCacheConfig
6619
+ );
6620
+ } else {
6621
+ this.embedding = withEmbeddingCache(
6622
+ this.rawEmbedding,
6623
+ new MemoryEmbeddingCacheStore({
6624
+ ttlMs: this.embeddingCacheConfig.ttlMs
6625
+ }),
6626
+ this.embeddingCacheConfig
6627
+ );
6628
+ }
6629
+ }
6630
+ if (this.storage instanceof PostgresStorageProvider) {
6631
+ }
6632
+ if (this.storage instanceof SqliteStorageProvider) {
6633
+ this.storage.setRetryLogger((msg) => {
6634
+ if (typeof console !== "undefined" && console.debug) {
6635
+ console.debug(`[wolbarg] ${msg}`);
6636
+ }
6637
+ });
6638
+ }
6639
+ const storedDims = await this.storage.getEmbeddingDimensions();
6640
+ if (storedDims !== null && storedDims > 0) {
6641
+ await this.storage.ensureVectorSchema(storedDims);
6642
+ this.embeddingDimensions = storedDims;
6643
+ } else {
6644
+ const probe = await this.embedding.validate();
6645
+ await this.storage.ensureVectorSchema(probe.dimensions);
6646
+ this.embeddingDimensions = probe.dimensions;
6647
+ }
4663
6648
  this.initialized = true;
4664
6649
  this.telemetry.emitStartup(this.storage.name);
4665
6650
  } catch (error) {
@@ -4675,64 +6660,76 @@ var Wolbarg = class {
4675
6660
  );
4676
6661
  }
4677
6662
  }
4678
- /** Store a semantic memory for an agent. */
6663
+ /** Store a semantic memory for an agent (may upsert when dedupe is enabled). */
4679
6664
  async remember(options) {
4680
6665
  const trace = this.telemetry.start("remember");
4681
6666
  try {
4682
- const { storage, embedding, organization } = await this.requireReady();
4683
- assertNonEmptyString(options.agent, "agent");
4684
- if (!options.content || typeof options.content.text !== "string") {
4685
- throw new ValidationError("content.text must be a string");
4686
- }
4687
- assertNonEmptyString(options.content.text, "content.text");
4688
- const metadata = options.metadata ?? {};
4689
- if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) {
4690
- throw new ValidationError("metadata must be a plain object when provided");
4691
- }
4692
- const tEmbed = performance.now();
4693
- const vector = await embedding.embed(options.content.text);
4694
- trace.mark("embeddingMs", performance.now() - tEmbed);
4695
- this.assertEmbeddingDimensions(vector.length);
4696
- const id = createId();
4697
- const timestamp = nowIso();
4698
- const record = await this.withWriteLock(async () => {
4699
- const tStore = performance.now();
4700
- const row = await storage.insertMemory({
4701
- id,
4702
- organization,
4703
- agent: options.agent.trim(),
4704
- contentText: options.content.text,
4705
- metadata,
4706
- embedding: vector,
4707
- createdAt: timestamp,
4708
- updatedAt: timestamp
4709
- });
4710
- trace.mark("databaseWriteMs", performance.now() - tStore);
4711
- return toMemoryRecord(row);
4712
- });
6667
+ const result = await this.rememberOne(options, trace);
4713
6668
  trace.success({
4714
- provider: storage.name,
4715
- memoryIds: [record.id],
6669
+ provider: this.storage?.name,
6670
+ memoryIds: [result.id],
4716
6671
  returnedCount: 1,
4717
- embeddingProvider: embedding.model,
4718
- model: embedding.model,
4719
- metadata,
4720
- agentId: options.agent.trim(),
4721
- tags: telemetryTags(metadata)
6672
+ embeddingProvider: this.embedding?.model,
6673
+ model: this.embedding?.model,
6674
+ metadata: result.metadata,
6675
+ agentId: result.agent,
6676
+ tags: telemetryTags(result.metadata),
6677
+ extra: { upsertAction: result.action }
4722
6678
  });
4723
- return record;
6679
+ return result;
4724
6680
  } catch (error) {
4725
6681
  trace.failure(error, { agentId: options.agent });
4726
6682
  throw wrapOperationError("remember", error);
4727
6683
  }
4728
6684
  }
4729
- /** Batch remember — one parent event + child traces per item. */
6685
+ /** Batch remember — sequential when dedupe enabled; otherwise one TX batch. */
4730
6686
  async rememberBatch(items) {
4731
6687
  const parent = this.telemetry.start("rememberBatch");
4732
6688
  try {
4733
6689
  if (!Array.isArray(items) || items.length === 0) {
4734
6690
  throw new ValidationError("rememberBatch requires a non-empty array");
4735
6691
  }
6692
+ const anyDedupe = items.some((item) => {
6693
+ const cfg = resolveMemoryDedupeConfig(
6694
+ item.dedupe ?? this.memoryDedupe
6695
+ );
6696
+ return cfg.enabled;
6697
+ }) || this.memoryDedupe.enabled;
6698
+ if (anyDedupe) {
6699
+ const out = [];
6700
+ for (const item of items) {
6701
+ const child = parent.child("remember");
6702
+ const result = await this.rememberOne(item, child);
6703
+ child.success({
6704
+ provider: this.storage?.name,
6705
+ memoryIds: [result.id],
6706
+ returnedCount: 1,
6707
+ metadata: result.metadata,
6708
+ agentId: result.agent,
6709
+ tags: telemetryTags(result.metadata),
6710
+ extra: { upsertAction: result.action }
6711
+ });
6712
+ out.push(result);
6713
+ }
6714
+ parent.success({
6715
+ provider: this.storage?.name,
6716
+ memoryIds: out.map((r) => r.id),
6717
+ returnedCount: out.length,
6718
+ embeddingProvider: this.embedding?.model,
6719
+ model: this.embedding?.model,
6720
+ agentId: commonAgent(items.map((item) => item.agent.trim()))
6721
+ });
6722
+ this.emitChange({
6723
+ event: "remember",
6724
+ organization: this.organization,
6725
+ agent: commonAgent(items.map((i) => i.agent.trim())) ?? items[0].agent,
6726
+ memoryId: out.map((r) => r.id),
6727
+ timestamp: nowIso(),
6728
+ traceId: parent.context.traceId,
6729
+ sessionId: this.telemetry.sessionId
6730
+ });
6731
+ return out;
6732
+ }
4736
6733
  const { storage, embedding, organization } = await this.requireReady();
4737
6734
  const tEmbed = performance.now();
4738
6735
  const texts = items.map((item, i) => {
@@ -4757,7 +6754,9 @@ var Wolbarg = class {
4757
6754
  metadata: item.metadata ?? {},
4758
6755
  embedding: vectors[i],
4759
6756
  createdAt: timestamp,
4760
- updatedAt: timestamp
6757
+ updatedAt: timestamp,
6758
+ contentHash: null
6759
+ // batch path is append-only; dedupe uses sequential rememberOne
4761
6760
  }));
4762
6761
  for (let i = 0; i < inputs.length; i += 1) {
4763
6762
  const child = parent.child("remember");
@@ -4776,7 +6775,10 @@ var Wolbarg = class {
4776
6775
  parent.mark("databaseWriteMs", performance.now() - tStore);
4777
6776
  return result;
4778
6777
  });
4779
- const records = rows.map(toMemoryRecord);
6778
+ const records = rows.map((row) => ({
6779
+ ...toMemoryRecord(row),
6780
+ action: "created"
6781
+ }));
4780
6782
  parent.success({
4781
6783
  provider: storage.name,
4782
6784
  memoryIds: records.map((r) => r.id),
@@ -4785,12 +6787,379 @@ var Wolbarg = class {
4785
6787
  model: embedding.model,
4786
6788
  agentId: commonAgent(items.map((item) => item.agent.trim()))
4787
6789
  });
6790
+ this.emitChange({
6791
+ event: "remember",
6792
+ organization,
6793
+ agent: commonAgent(items.map((i) => i.agent.trim())) ?? items[0].agent,
6794
+ memoryId: records.map((r) => r.id),
6795
+ timestamp,
6796
+ traceId: parent.context.traceId,
6797
+ sessionId: this.telemetry.sessionId
6798
+ });
4788
6799
  return records;
4789
6800
  } catch (error) {
4790
6801
  parent.failure(error);
4791
6802
  throw wrapOperationError("rememberBatch", error);
4792
6803
  }
4793
6804
  }
6805
+ /**
6806
+ * Update an existing memory by id (re-embeds when content changes).
6807
+ */
6808
+ async update(options) {
6809
+ const trace = this.telemetry.start("remember");
6810
+ try {
6811
+ const { storage, embedding, organization } = await this.requireReady();
6812
+ assertNonEmptyString(options.id, "id");
6813
+ const existing = await storage.getMemoryById(options.id.trim(), organization);
6814
+ if (!existing) {
6815
+ throw new MemoryNotFoundError(`Memory not found: ${options.id}`);
6816
+ }
6817
+ const contentText = options.content?.text ?? existing.content_text;
6818
+ if (options.content) {
6819
+ assertNonEmptyString(options.content.text, "content.text");
6820
+ }
6821
+ const existingMeta = deserializeMetadata(existing.metadata_json);
6822
+ const metadata = options.metadata !== void 0 ? mergeMemoryMetadata(existingMeta, options.metadata) : existingMeta;
6823
+ let vector;
6824
+ if (options.content !== void 0) {
6825
+ const tEmbed = performance.now();
6826
+ vector = await embedding.embed(contentText);
6827
+ trace.mark("embeddingMs", performance.now() - tEmbed);
6828
+ this.assertEmbeddingDimensions(vector.length);
6829
+ }
6830
+ const timestamp = nowIso();
6831
+ const row = await this.withWriteLock(async () => {
6832
+ const tStore = performance.now();
6833
+ const updated = await storage.updateMemory({
6834
+ id: existing.id,
6835
+ organization,
6836
+ contentText: options.content !== void 0 ? contentText : void 0,
6837
+ metadata,
6838
+ embedding: vector,
6839
+ updatedAt: timestamp,
6840
+ contentHash: options.content !== void 0 ? hashMemoryContent(contentText) : void 0
6841
+ });
6842
+ trace.mark("databaseWriteMs", performance.now() - tStore);
6843
+ return updated;
6844
+ });
6845
+ if (!row) {
6846
+ throw new MemoryNotFoundError(`Memory not found: ${options.id}`);
6847
+ }
6848
+ const result = {
6849
+ ...toMemoryRecord(row),
6850
+ action: "updated"
6851
+ };
6852
+ this.emitChange({
6853
+ event: "update",
6854
+ organization,
6855
+ agent: result.agent,
6856
+ memoryId: result.id,
6857
+ timestamp,
6858
+ traceId: trace.context.traceId,
6859
+ sessionId: this.telemetry.sessionId,
6860
+ upsertAction: "updated"
6861
+ });
6862
+ trace.success({
6863
+ provider: storage.name,
6864
+ memoryIds: [result.id],
6865
+ returnedCount: 1,
6866
+ agentId: result.agent,
6867
+ extra: { upsertAction: "updated" }
6868
+ });
6869
+ return result;
6870
+ } catch (error) {
6871
+ trace.failure(error);
6872
+ throw wrapOperationError("update", error);
6873
+ }
6874
+ }
6875
+ /**
6876
+ * Subscribe to memory change events.
6877
+ *
6878
+ * **SQLite:** delivers events only within this Node.js process.
6879
+ * A second process writing the same `memory.db` will not notify subscribers here.
6880
+ */
6881
+ subscribe(filter, callback) {
6882
+ const org = filter.organization || this.organization;
6883
+ if (!org) {
6884
+ throw new ValidationError(
6885
+ "subscribe requires organization (set on Wolbarg or in filter)"
6886
+ );
6887
+ }
6888
+ const normalized = {
6889
+ ...filter,
6890
+ organization: org
6891
+ };
6892
+ if (this.storage instanceof PostgresStorageProvider) {
6893
+ if (!this.pgListenBackend) {
6894
+ const pool = this.storage.getPool?.();
6895
+ if (pool) {
6896
+ this.pgListenBackend = createPostgresListenerFromPool(pool);
6897
+ }
6898
+ }
6899
+ if (this.pgListenBackend) {
6900
+ return this.pgListenBackend.subscribe(normalized, callback);
6901
+ }
6902
+ }
6903
+ if (!this.subscribeBackend) {
6904
+ this.subscribeBackend = new SqliteSubscribeEmitter();
6905
+ }
6906
+ return this.subscribeBackend.subscribe(normalized, callback);
6907
+ }
6908
+ emitChange(event) {
6909
+ try {
6910
+ if (this.storage instanceof PostgresStorageProvider) {
6911
+ const listener = this.pgListenBackend;
6912
+ if (listener?.hasSubscribers?.()) {
6913
+ void this.storage.notifyChange(event).catch((error) => {
6914
+ console.error(
6915
+ "[wolbarg] NOTIFY failed:",
6916
+ error instanceof Error ? error.message : error
6917
+ );
6918
+ });
6919
+ }
6920
+ return;
6921
+ }
6922
+ this.subscribeBackend?.emit(event);
6923
+ } catch (error) {
6924
+ console.error(
6925
+ "[wolbarg] emitChange error:",
6926
+ error instanceof Error ? error.message : error
6927
+ );
6928
+ }
6929
+ }
6930
+ /** Internal remember with optional upsert/dedupe. */
6931
+ async rememberOne(options, trace) {
6932
+ const { storage, embedding, organization } = await this.requireReady();
6933
+ assertNonEmptyString(options.agent, "agent");
6934
+ if (!options.content || typeof options.content.text !== "string") {
6935
+ throw new ValidationError("content.text must be a string");
6936
+ }
6937
+ assertNonEmptyString(options.content.text, "content.text");
6938
+ const metadata = options.metadata ?? {};
6939
+ if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) {
6940
+ throw new ValidationError("metadata must be a plain object when provided");
6941
+ }
6942
+ const agent = options.agent.trim();
6943
+ const text = options.content.text;
6944
+ const dedupe = resolveMemoryDedupeConfig(
6945
+ options.dedupe !== void 0 ? options.dedupe : this.memoryDedupe
6946
+ );
6947
+ const tEmbed = performance.now();
6948
+ const vector = await embedding.embed(text);
6949
+ trace.mark("embeddingMs", performance.now() - tEmbed);
6950
+ this.assertEmbeddingDimensions(vector.length);
6951
+ const timestamp = nowIso();
6952
+ const contentHash = hashMemoryContent(text);
6953
+ if (!dedupe.enabled) {
6954
+ const id2 = createId();
6955
+ const record = await this.withWriteLock(async () => {
6956
+ const tStore = performance.now();
6957
+ const row = await storage.insertMemory({
6958
+ id: id2,
6959
+ organization,
6960
+ agent,
6961
+ contentText: text,
6962
+ metadata,
6963
+ embedding: vector,
6964
+ createdAt: timestamp,
6965
+ updatedAt: timestamp,
6966
+ // Null hash when dedupe is off — unique index allows duplicate text.
6967
+ contentHash: null
6968
+ });
6969
+ trace.mark("databaseWriteMs", performance.now() - tStore);
6970
+ return toMemoryRecord(row);
6971
+ });
6972
+ this.emitChange({
6973
+ event: "remember",
6974
+ organization,
6975
+ agent,
6976
+ memoryId: record.id,
6977
+ timestamp,
6978
+ traceId: trace.context.traceId,
6979
+ sessionId: this.telemetry.sessionId,
6980
+ upsertAction: "created"
6981
+ });
6982
+ return { ...record, action: "created" };
6983
+ }
6984
+ let existing = null;
6985
+ if (dedupe.strategy === "exact" || dedupe.strategy === "exact-or-near") {
6986
+ if (typeof storage.findActiveByContentHash === "function") {
6987
+ existing = await storage.findActiveByContentHash(
6988
+ organization,
6989
+ agent,
6990
+ contentHash
6991
+ );
6992
+ }
6993
+ }
6994
+ if (existing) {
6995
+ const merged = mergeMemoryMetadata(
6996
+ deserializeMetadata(existing.metadata_json),
6997
+ metadata
6998
+ );
6999
+ const row = await this.withWriteLock(async () => {
7000
+ const tStore = performance.now();
7001
+ const updated = await storage.updateMemory({
7002
+ id: existing.id,
7003
+ organization,
7004
+ metadata: merged,
7005
+ updatedAt: timestamp,
7006
+ contentHash
7007
+ });
7008
+ trace.mark("databaseWriteMs", performance.now() - tStore);
7009
+ return updated;
7010
+ }, { exclusive: true });
7011
+ const record = toMemoryRecord(row);
7012
+ this.emitChange({
7013
+ event: "update",
7014
+ organization,
7015
+ agent,
7016
+ memoryId: record.id,
7017
+ timestamp,
7018
+ traceId: trace.context.traceId,
7019
+ sessionId: this.telemetry.sessionId,
7020
+ upsertAction: "updated"
7021
+ });
7022
+ return { ...record, action: "updated" };
7023
+ }
7024
+ if (dedupe.strategy === "near" || dedupe.strategy === "exact-or-near") {
7025
+ const near = await this.findNearDuplicate(
7026
+ storage,
7027
+ organization,
7028
+ agent,
7029
+ vector,
7030
+ dedupe.nearThreshold,
7031
+ dedupe.nearCandidateLimit
7032
+ );
7033
+ if (near) {
7034
+ const merged = mergeMemoryMetadata(
7035
+ deserializeMetadata(near.metadata_json),
7036
+ metadata
7037
+ );
7038
+ const row = await this.withWriteLock(async () => {
7039
+ const tStore = performance.now();
7040
+ const updated = await storage.updateMemory({
7041
+ id: near.id,
7042
+ organization,
7043
+ contentText: text,
7044
+ metadata: merged,
7045
+ embedding: vector,
7046
+ updatedAt: timestamp,
7047
+ contentHash
7048
+ });
7049
+ trace.mark("databaseWriteMs", performance.now() - tStore);
7050
+ return updated;
7051
+ }, { exclusive: true });
7052
+ const record = toMemoryRecord(row);
7053
+ this.emitChange({
7054
+ event: "update",
7055
+ organization,
7056
+ agent,
7057
+ memoryId: record.id,
7058
+ timestamp,
7059
+ traceId: trace.context.traceId,
7060
+ sessionId: this.telemetry.sessionId,
7061
+ upsertAction: "updated"
7062
+ });
7063
+ return { ...record, action: "updated" };
7064
+ }
7065
+ }
7066
+ const id = createId();
7067
+ try {
7068
+ const record = await this.withWriteLock(async () => {
7069
+ const tStore = performance.now();
7070
+ const row = await storage.insertMemory({
7071
+ id,
7072
+ organization,
7073
+ agent,
7074
+ contentText: text,
7075
+ metadata,
7076
+ embedding: vector,
7077
+ createdAt: timestamp,
7078
+ updatedAt: timestamp,
7079
+ contentHash
7080
+ });
7081
+ trace.mark("databaseWriteMs", performance.now() - tStore);
7082
+ return toMemoryRecord(row);
7083
+ }, { exclusive: true });
7084
+ this.emitChange({
7085
+ event: "remember",
7086
+ organization,
7087
+ agent,
7088
+ memoryId: record.id,
7089
+ timestamp,
7090
+ traceId: trace.context.traceId,
7091
+ sessionId: this.telemetry.sessionId,
7092
+ upsertAction: "created"
7093
+ });
7094
+ return { ...record, action: "created" };
7095
+ } catch (error) {
7096
+ const msg = error instanceof Error ? error.message : String(error);
7097
+ if (msg.toLowerCase().includes("unique") && typeof storage.findActiveByContentHash === "function") {
7098
+ const raced = await storage.findActiveByContentHash(
7099
+ organization,
7100
+ agent,
7101
+ contentHash
7102
+ );
7103
+ if (raced) {
7104
+ const merged = mergeMemoryMetadata(
7105
+ deserializeMetadata(raced.metadata_json),
7106
+ metadata
7107
+ );
7108
+ const row = await this.withWriteLock(
7109
+ async () => storage.updateMemory({
7110
+ id: raced.id,
7111
+ organization,
7112
+ metadata: merged,
7113
+ updatedAt: nowIso(),
7114
+ contentHash
7115
+ }),
7116
+ { exclusive: true }
7117
+ );
7118
+ const record = toMemoryRecord(row);
7119
+ this.emitChange({
7120
+ event: "update",
7121
+ organization,
7122
+ agent,
7123
+ memoryId: record.id,
7124
+ timestamp: nowIso(),
7125
+ upsertAction: "updated"
7126
+ });
7127
+ return { ...record, action: "updated" };
7128
+ }
7129
+ }
7130
+ throw error;
7131
+ }
7132
+ }
7133
+ async findNearDuplicate(storage, organization, agent, vector, threshold, limit) {
7134
+ if (typeof storage.searchVectorsWithMemories === "function") {
7135
+ const hits2 = await storage.searchVectorsWithMemories(
7136
+ vector,
7137
+ limit,
7138
+ organization,
7139
+ { agent, includeArchived: false }
7140
+ );
7141
+ let best2 = null;
7142
+ for (const { row, distance } of hits2) {
7143
+ if (row.archived === 1) continue;
7144
+ const sim = distanceToSimilarity(distance);
7145
+ if (sim >= threshold && (!best2 || sim > best2.sim)) {
7146
+ best2 = { row, sim };
7147
+ }
7148
+ }
7149
+ return best2?.row ?? null;
7150
+ }
7151
+ const hits = await storage.searchVectors(vector, limit);
7152
+ let best = null;
7153
+ for (const hit of hits) {
7154
+ const row = await storage.getMemoryByRowid(hit.memoryRowid, organization);
7155
+ if (!row || row.archived === 1 || row.agent !== agent) continue;
7156
+ const sim = distanceToSimilarity(hit.distance);
7157
+ if (sim >= threshold && (!best || sim > best.sim)) {
7158
+ best = { row, sim };
7159
+ }
7160
+ }
7161
+ return best?.row ?? null;
7162
+ }
4794
7163
  async recall(options) {
4795
7164
  const trace = this.telemetry.start("recall");
4796
7165
  const explain = options.explain === true;
@@ -5178,6 +7547,14 @@ var Wolbarg = class {
5178
7547
  returnedCount: 1,
5179
7548
  agentId: options.agent.trim()
5180
7549
  });
7550
+ this.emitChange({
7551
+ event: "compress",
7552
+ organization,
7553
+ agent: options.agent.trim(),
7554
+ memoryId: [result.summary.id, ...result.archivedIds],
7555
+ timestamp,
7556
+ sessionId: this.telemetry.sessionId
7557
+ });
5181
7558
  return result;
5182
7559
  } catch (error) {
5183
7560
  trace.failure(error, { agentId: options.agent });
@@ -5272,9 +7649,54 @@ var Wolbarg = class {
5272
7649
  },
5273
7650
  embedding: vectors[i],
5274
7651
  createdAt: timestamp,
5275
- updatedAt: timestamp
7652
+ updatedAt: timestamp,
7653
+ contentHash: this.memoryDedupe.enabled ? hashMemoryContent(chunk.text) : null
5276
7654
  }));
5277
- const rows = await this.withWriteLock(async () => {
7655
+ let rows;
7656
+ if (this.memoryDedupe.enabled) {
7657
+ const memories = [];
7658
+ for (let i = 0; i < chunks.length; i += 1) {
7659
+ const result3 = await this.rememberOne(
7660
+ {
7661
+ agent: options.agent.trim(),
7662
+ content: { text: chunks[i].text },
7663
+ metadata: {
7664
+ ...baseMeta,
7665
+ ingest: true,
7666
+ chunkIndex: chunks[i].index,
7667
+ chunkCount: chunks.length,
7668
+ sourceFilename: loaded.filename ?? null
7669
+ }
7670
+ },
7671
+ trace
7672
+ );
7673
+ memories.push(result3);
7674
+ }
7675
+ const result2 = {
7676
+ memories,
7677
+ extractedChars: text.length,
7678
+ chunkCount: chunks.length,
7679
+ usedOcr,
7680
+ usedVision
7681
+ };
7682
+ trace.success({
7683
+ provider: storage.name,
7684
+ memoryIds: result2.memories.map((m) => m.id),
7685
+ returnedCount: result2.memories.length,
7686
+ agentId: options.agent.trim(),
7687
+ tags: telemetryTags(baseMeta)
7688
+ });
7689
+ this.emitChange({
7690
+ event: "ingest",
7691
+ organization,
7692
+ agent: options.agent.trim(),
7693
+ memoryId: result2.memories.map((m) => m.id),
7694
+ timestamp,
7695
+ sessionId: this.telemetry.sessionId
7696
+ });
7697
+ return result2;
7698
+ }
7699
+ rows = await this.withWriteLock(async () => {
5278
7700
  const tWrite = performance.now();
5279
7701
  const result2 = await storage.insertMemoriesBatch(inputs);
5280
7702
  trace.mark("databaseWriteMs", performance.now() - tWrite);
@@ -5294,6 +7716,14 @@ var Wolbarg = class {
5294
7716
  agentId: options.agent.trim(),
5295
7717
  tags: telemetryTags(baseMeta)
5296
7718
  });
7719
+ this.emitChange({
7720
+ event: "ingest",
7721
+ organization,
7722
+ agent: options.agent.trim(),
7723
+ memoryId: result.memories.map((m) => m.id),
7724
+ timestamp,
7725
+ sessionId: this.telemetry.sessionId
7726
+ });
5297
7727
  return result;
5298
7728
  } catch (error) {
5299
7729
  trace.failure(error, { agentId: options.agent });
@@ -5332,6 +7762,16 @@ var Wolbarg = class {
5332
7762
  filters: "filter" in options ? options.filter : null,
5333
7763
  agentId: "filter" in options ? options.filter?.agent?.trim() ?? null : null
5334
7764
  });
7765
+ if (deleted > 0) {
7766
+ this.emitChange({
7767
+ event: "forget",
7768
+ organization,
7769
+ agent: "filter" in options && options.filter?.agent?.trim() || ("id" in options ? "*" : "*"),
7770
+ memoryId: "id" in options && options.id ? options.id.trim() : [],
7771
+ timestamp: nowIso(),
7772
+ sessionId: this.telemetry.sessionId
7773
+ });
7774
+ }
5335
7775
  return deleted;
5336
7776
  } catch (error) {
5337
7777
  trace.failure(error, {
@@ -5605,8 +8045,13 @@ var Wolbarg = class {
5605
8045
  get sessionId() {
5606
8046
  return this.telemetry.sessionId;
5607
8047
  }
5608
- withWriteLock(fn) {
5609
- if (this.storage?.name === "sqlite") {
8048
+ /**
8049
+ * Optional process-wide lock for SQLite read-modify-write (dedupe upsert).
8050
+ * Plain inserts must NOT take this lock — the provider coalesces concurrent
8051
+ * insertMemory calls under a single BEGIN IMMEDIATE for multi-writer throughput.
8052
+ */
8053
+ withWriteLock(fn, options) {
8054
+ if (options?.exclusive && this.storage?.name === "sqlite") {
5610
8055
  return this.writeMutex.runExclusive(fn);
5611
8056
  }
5612
8057
  return fn();
@@ -5615,6 +8060,10 @@ var Wolbarg = class {
5615
8060
  if (this.storage) {
5616
8061
  this.telemetry.emitShutdown(this.storage.name);
5617
8062
  }
8063
+ await this.subscribeBackend?.close().catch(() => void 0);
8064
+ await this.pgListenBackend?.close().catch(() => void 0);
8065
+ this.subscribeBackend = null;
8066
+ this.pgListenBackend = null;
5618
8067
  await this.telemetry.close().catch(() => void 0);
5619
8068
  await this.checkpointProvider?.close().catch(() => void 0);
5620
8069
  if (this.storage) {
@@ -5622,6 +8071,7 @@ var Wolbarg = class {
5622
8071
  }
5623
8072
  this.storage = null;
5624
8073
  this.embedding = null;
8074
+ this.rawEmbedding = null;
5625
8075
  this.llm = null;
5626
8076
  this.compression = null;
5627
8077
  this.organization = null;
@@ -6226,6 +8676,6 @@ function percentile(sorted, p) {
6226
8676
  return sorted[idx];
6227
8677
  }
6228
8678
 
6229
- export { CompressionError, ConfigurationError, DatabaseError, EmbeddingError, FixedChunkingStrategy, HeadingChunkingStrategy, InitializationError, LlmCompressionProvider, MarkdownChunkingStrategy, MemoryNotFoundError, NoopTelemetryProvider, ParagraphChunkingStrategy, PostgresStorageProvider, ProviderNotConfiguredError, SentenceChunkingStrategy, SqliteCheckpointProvider, SqliteDatabaseProvider, SqliteEventDatabase, SqliteStorageProvider, SqliteTelemetryProvider, TelemetryEmitter, ValidationError, Wolbarg, WolbargError, WolbargLogger, bgeReranker, bm25, cohereReranker, createChunkingStrategy, createCompressionProvider, createDatabaseProvider, createEmbeddingProvider, createLlmProvider, createStorageProvider, createTelemetryProvider, wolbarg2 as createWolbarg, crossEncoder, geminiEmbedding, geminiVision, jinaReranker, lmStudioEmbedding, meta, ollamaEmbedding, ollamaLlm, openRouterEmbedding, openRouterLlm, openaiCompatibleEmbedding, openaiCompatibleLlm, openaiEmbedding, openaiLlm, openaiReranker, openaiVision, postgres, postgresConfig, runBenchmark, sqlite, sqliteCheckpoint, sqliteConfig, sqliteTelemetry, summarizeBenchmark, tesseract, togetherEmbedding, vllmEmbedding, wolbarg, wrapOperationError };
8679
+ export { CompressionError, ConfigurationError, DatabaseError, EmbeddingError, FixedChunkingStrategy, HeadingChunkingStrategy, InitializationError, LlmCompressionProvider, MarkdownChunkingStrategy, MemoryNotFoundError, NoopTelemetryProvider, ParagraphChunkingStrategy, PostgresStorageProvider, ProviderNotConfiguredError, SentenceChunkingStrategy, SqliteCheckpointProvider, SqliteDatabaseProvider, SqliteEventDatabase, SqliteStorageProvider, SqliteTelemetryProvider, StorageLockedError, TelemetryEmitter, ValidationError, Wolbarg, WolbargError, WolbargLogger, bgeReranker, bm25, cohereReranker, createChunkingStrategy, createCompressionProvider, createDatabaseProvider, createEmbeddingProvider, createLlmProvider, createStorageProvider, createTelemetryProvider, wolbarg2 as createWolbarg, crossEncoder, geminiEmbedding, geminiVision, jinaReranker, lmStudioEmbedding, meta, ollamaEmbedding, ollamaLlm, openRouterEmbedding, openRouterLlm, openaiCompatibleEmbedding, openaiCompatibleLlm, openaiEmbedding, openaiLlm, openaiReranker, openaiVision, postgres, postgresConfig, runBenchmark, sqlite, sqliteCheckpoint, sqliteConfig, sqliteTelemetry, summarizeBenchmark, tesseract, togetherEmbedding, vllmEmbedding, wolbarg, wrapOperationError };
6230
8680
  //# sourceMappingURL=index.js.map
6231
8681
  //# sourceMappingURL=index.js.map