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.cjs CHANGED
@@ -1,11 +1,13 @@
1
1
  'use strict';
2
2
 
3
3
  var path5 = require("node:path");
4
+ var crypto$1 = require('crypto');
4
5
  var fs = require('fs/promises');
5
6
  var fs5 = require("node:fs");
6
7
  var sqlite$1 = require("node:sqlite");
7
8
  var sqliteVec = require('sqlite-vec');
8
9
  var async_hooks = require('async_hooks');
10
+ var events = require('events');
9
11
 
10
12
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
13
 
@@ -74,6 +76,12 @@ var DatabaseError = class extends WolbargError {
74
76
  this.name = "DatabaseError";
75
77
  }
76
78
  };
79
+ var StorageLockedError = class extends WolbargError {
80
+ constructor(message, options) {
81
+ super(message, "WOLBARG_STORAGE_LOCKED", options);
82
+ this.name = "StorageLockedError";
83
+ }
84
+ };
77
85
  var EmbeddingError = class extends WolbargError {
78
86
  constructor(message, options) {
79
87
  super(message, "EMBEDDING_ERROR", options);
@@ -111,11 +119,11 @@ function wrapOperationError(operation, error) {
111
119
  const raw = error instanceof Error ? error.message : String(error);
112
120
  const lower = raw.toLowerCase();
113
121
  if (lower.includes("database is locked") || lower.includes("sqlite_busy")) {
114
- return new DatabaseError(formatOperationMessage(operation, raw), {
122
+ return new StorageLockedError(formatOperationMessage(operation, raw), {
115
123
  cause: error instanceof Error ? error : void 0,
116
124
  operation,
117
125
  reason: "SQLite database locked",
118
- suggestion: "Increase timeout or reduce concurrent writes."
126
+ suggestion: "Increase concurrency.maxRetries or concurrency.lockTimeoutMs, or consider the Postgres backend for high-concurrency multi-agent workloads."
119
127
  });
120
128
  }
121
129
  if (lower.includes("no such file") || lower.includes("enoent")) {
@@ -575,6 +583,404 @@ async function embedMany(provider, texts, concurrency = 8) {
575
583
  );
576
584
  return out;
577
585
  }
586
+ function resolveEmbeddingCacheConfig(input) {
587
+ return {
588
+ enabled: input?.enabled ?? true,
589
+ ttlMs: input?.ttlMs ?? null,
590
+ maxEntries: input?.maxEntries ?? null
591
+ };
592
+ }
593
+ function embeddingCacheKey(content, model) {
594
+ const hash = crypto$1.createHash("sha256").update(content, "utf8").digest("hex");
595
+ return `${hash}:${model}`;
596
+ }
597
+ function withEmbeddingCache(provider, store, config) {
598
+ let cacheHits = 0;
599
+ let cacheMisses = 0;
600
+ async function embedOne(text) {
601
+ if (!config.enabled) {
602
+ cacheMisses += 1;
603
+ return provider.embed(text);
604
+ }
605
+ const key = embeddingCacheKey(text, provider.model);
606
+ const cached = await store.get(key);
607
+ if (cached) {
608
+ cacheHits += 1;
609
+ return cached;
610
+ }
611
+ cacheMisses += 1;
612
+ const vector = await provider.embed(text);
613
+ void store.set(key, provider.model, vector);
614
+ if (config.maxEntries !== null) {
615
+ void store.evictIfNeeded(config.maxEntries);
616
+ }
617
+ return vector;
618
+ }
619
+ async function embedBatch(texts) {
620
+ if (!config.enabled || texts.length === 0) {
621
+ if (provider.embedBatch) {
622
+ cacheMisses += texts.length;
623
+ return provider.embedBatch(texts);
624
+ }
625
+ return Promise.all(texts.map((t) => embedOne(t)));
626
+ }
627
+ const results = new Array(texts.length).fill(
628
+ null
629
+ );
630
+ const missIndexes = [];
631
+ const missTexts = [];
632
+ for (let i = 0; i < texts.length; i += 1) {
633
+ const text = texts[i];
634
+ const key = embeddingCacheKey(text, provider.model);
635
+ const cached = await store.get(key);
636
+ if (cached) {
637
+ cacheHits += 1;
638
+ results[i] = cached;
639
+ } else {
640
+ missIndexes.push(i);
641
+ missTexts.push(text);
642
+ }
643
+ }
644
+ if (missTexts.length > 0) {
645
+ cacheMisses += missTexts.length;
646
+ const vectors = provider.embedBatch ? await provider.embedBatch(missTexts) : await Promise.all(missTexts.map((t) => provider.embed(t)));
647
+ for (let j = 0; j < missIndexes.length; j += 1) {
648
+ const idx = missIndexes[j];
649
+ const vector = vectors[j];
650
+ results[idx] = vector;
651
+ const key = embeddingCacheKey(missTexts[j], provider.model);
652
+ void store.set(key, provider.model, vector);
653
+ }
654
+ if (config.maxEntries !== null) {
655
+ void store.evictIfNeeded(config.maxEntries);
656
+ }
657
+ }
658
+ return results;
659
+ }
660
+ return {
661
+ model: provider.model,
662
+ embed: embedOne,
663
+ embedBatch,
664
+ validate: () => provider.validate(),
665
+ get cacheHits() {
666
+ return cacheHits;
667
+ },
668
+ get cacheMisses() {
669
+ return cacheMisses;
670
+ },
671
+ resetCacheStats() {
672
+ cacheHits = 0;
673
+ cacheMisses = 0;
674
+ }
675
+ };
676
+ }
677
+
678
+ // src/embedding/cache-store.ts
679
+ var TOUCH_FLUSH_THRESHOLD = 64;
680
+ var L1_DEFAULT_MAX = 5e4;
681
+ var L1Cache = class {
682
+ map = /* @__PURE__ */ new Map();
683
+ maxEntries;
684
+ constructor(maxEntries = L1_DEFAULT_MAX) {
685
+ this.maxEntries = maxEntries;
686
+ }
687
+ get(cacheKey, ttlMs) {
688
+ const entry = this.map.get(cacheKey);
689
+ if (!entry) {
690
+ return null;
691
+ }
692
+ if (ttlMs !== null && Date.now() - entry.createdAt > ttlMs) {
693
+ this.map.delete(cacheKey);
694
+ return null;
695
+ }
696
+ entry.lastUsedAt = Date.now();
697
+ this.map.delete(cacheKey);
698
+ this.map.set(cacheKey, entry);
699
+ return entry.vector;
700
+ }
701
+ set(cacheKey, model, vector) {
702
+ const now = Date.now();
703
+ if (this.map.has(cacheKey)) {
704
+ this.map.delete(cacheKey);
705
+ }
706
+ this.map.set(cacheKey, {
707
+ model,
708
+ vector,
709
+ createdAt: now,
710
+ lastUsedAt: now
711
+ });
712
+ this.evictIfNeeded();
713
+ }
714
+ touch(cacheKey) {
715
+ const entry = this.map.get(cacheKey);
716
+ if (!entry) {
717
+ return;
718
+ }
719
+ entry.lastUsedAt = Date.now();
720
+ this.map.delete(cacheKey);
721
+ this.map.set(cacheKey, entry);
722
+ }
723
+ evictIfNeeded(maxEntries) {
724
+ const limit = maxEntries ?? this.maxEntries;
725
+ while (this.map.size > limit) {
726
+ const oldest = this.map.keys().next().value;
727
+ if (oldest === void 0) {
728
+ break;
729
+ }
730
+ this.map.delete(oldest);
731
+ }
732
+ }
733
+ get size() {
734
+ return this.map.size;
735
+ }
736
+ };
737
+ var SqliteEmbeddingCacheStore = class {
738
+ ttlMs;
739
+ getDb;
740
+ l1 = new L1Cache();
741
+ pendingTouches = /* @__PURE__ */ new Set();
742
+ pendingSets = /* @__PURE__ */ new Map();
743
+ persistScheduled = false;
744
+ stmts = null;
745
+ constructor(dbOrGetter, options) {
746
+ this.ttlMs = options?.ttlMs ?? null;
747
+ this.getDb = typeof dbOrGetter === "function" ? dbOrGetter : () => dbOrGetter;
748
+ }
749
+ requireDb() {
750
+ try {
751
+ return this.getDb();
752
+ } catch {
753
+ return null;
754
+ }
755
+ }
756
+ ensureStatements(db) {
757
+ if (this.stmts) {
758
+ return this.stmts;
759
+ }
760
+ this.stmts = {
761
+ get: db.prepare(
762
+ `SELECT vector, last_used_at, created_at FROM embedding_cache WHERE cache_key = ?`
763
+ ),
764
+ delete: db.prepare(`DELETE FROM embedding_cache WHERE cache_key = ?`),
765
+ set: db.prepare(
766
+ `INSERT INTO embedding_cache (cache_key, model, vector, created_at, last_used_at)
767
+ VALUES (?, ?, ?, ?, ?)
768
+ ON CONFLICT(cache_key) DO UPDATE SET
769
+ model = excluded.model,
770
+ vector = excluded.vector,
771
+ last_used_at = excluded.last_used_at`
772
+ ),
773
+ touch: db.prepare(
774
+ `UPDATE embedding_cache SET last_used_at = ? WHERE cache_key = ?`
775
+ ),
776
+ count: db.prepare(`SELECT COUNT(*) AS c FROM embedding_cache`),
777
+ evict: db.prepare(
778
+ `DELETE FROM embedding_cache WHERE cache_key IN (
779
+ SELECT cache_key FROM embedding_cache
780
+ ORDER BY last_used_at ASC
781
+ LIMIT ?
782
+ )`
783
+ )
784
+ };
785
+ return this.stmts;
786
+ }
787
+ async get(cacheKey) {
788
+ const l1Hit = this.l1.get(cacheKey, this.ttlMs);
789
+ if (l1Hit) {
790
+ return l1Hit;
791
+ }
792
+ const pending = this.pendingSets.get(cacheKey);
793
+ if (pending) {
794
+ this.l1.set(cacheKey, pending.model, pending.vector);
795
+ return pending.vector;
796
+ }
797
+ return null;
798
+ }
799
+ async set(cacheKey, model, vector) {
800
+ this.l1.set(cacheKey, model, vector);
801
+ this.pendingSets.set(cacheKey, {
802
+ model,
803
+ vector,
804
+ createdAt: Date.now()
805
+ });
806
+ this.pendingTouches.delete(cacheKey);
807
+ this.schedulePersist();
808
+ }
809
+ async touch(cacheKey) {
810
+ this.l1.touch(cacheKey);
811
+ if (this.pendingSets.has(cacheKey)) {
812
+ return;
813
+ }
814
+ this.pendingTouches.add(cacheKey);
815
+ if (this.pendingTouches.size >= TOUCH_FLUSH_THRESHOLD) {
816
+ this.schedulePersist();
817
+ }
818
+ }
819
+ async flushTouches() {
820
+ await this.flushPending();
821
+ }
822
+ async evictIfNeeded(maxEntries) {
823
+ this.l1.evictIfNeeded(maxEntries);
824
+ }
825
+ schedulePersist() {
826
+ if (this.persistScheduled) {
827
+ return;
828
+ }
829
+ this.persistScheduled = true;
830
+ queueMicrotask(() => {
831
+ this.persistScheduled = false;
832
+ void this.flushPending();
833
+ });
834
+ }
835
+ async flushPending() {
836
+ if (this.pendingSets.size === 0 && this.pendingTouches.size === 0) {
837
+ return;
838
+ }
839
+ const db = this.requireDb();
840
+ if (!db) {
841
+ this.pendingSets.clear();
842
+ this.pendingTouches.clear();
843
+ return;
844
+ }
845
+ const sets = [...this.pendingSets.entries()];
846
+ this.pendingSets.clear();
847
+ const touches = [...this.pendingTouches];
848
+ this.pendingTouches.clear();
849
+ const now = (/* @__PURE__ */ new Date()).toISOString();
850
+ try {
851
+ const stmts = this.ensureStatements(db);
852
+ try {
853
+ db.exec("BEGIN IMMEDIATE");
854
+ } catch {
855
+ for (const [key, value] of sets) {
856
+ this.pendingSets.set(key, value);
857
+ }
858
+ for (const key of touches) {
859
+ this.pendingTouches.add(key);
860
+ }
861
+ this.schedulePersist();
862
+ return;
863
+ }
864
+ try {
865
+ for (const [key, value] of sets) {
866
+ stmts.set.run(
867
+ key,
868
+ value.model,
869
+ embeddingToBuffer(value.vector),
870
+ new Date(value.createdAt).toISOString(),
871
+ now
872
+ );
873
+ }
874
+ for (const key of touches) {
875
+ stmts.touch.run(now, key);
876
+ }
877
+ db.exec("COMMIT");
878
+ } catch {
879
+ try {
880
+ db.exec("ROLLBACK");
881
+ } catch {
882
+ }
883
+ this.stmts = null;
884
+ }
885
+ } catch {
886
+ this.stmts = null;
887
+ }
888
+ }
889
+ };
890
+ var MemoryEmbeddingCacheStore = class {
891
+ l1;
892
+ ttlMs;
893
+ constructor(options) {
894
+ this.ttlMs = options?.ttlMs ?? null;
895
+ this.l1 = new L1Cache(options?.maxEntries ?? L1_DEFAULT_MAX);
896
+ }
897
+ async get(cacheKey) {
898
+ return this.l1.get(cacheKey, this.ttlMs);
899
+ }
900
+ async set(cacheKey, model, vector) {
901
+ this.l1.set(cacheKey, model, vector);
902
+ }
903
+ async touch(cacheKey) {
904
+ this.l1.touch(cacheKey);
905
+ }
906
+ async evictIfNeeded(maxEntries) {
907
+ this.l1.evictIfNeeded(maxEntries);
908
+ }
909
+ };
910
+ var PostgresEmbeddingCacheStore = class {
911
+ ttlMs;
912
+ getClient;
913
+ l1 = new L1Cache();
914
+ pendingTouches = /* @__PURE__ */ new Set();
915
+ persistChain = Promise.resolve();
916
+ durableEnabled;
917
+ constructor(getClient, options) {
918
+ this.getClient = getClient;
919
+ this.ttlMs = options?.ttlMs ?? null;
920
+ this.durableEnabled = options?.durable === true;
921
+ }
922
+ async get(cacheKey) {
923
+ return this.l1.get(cacheKey, this.ttlMs);
924
+ }
925
+ async set(cacheKey, model, vector) {
926
+ this.l1.set(cacheKey, model, vector);
927
+ if (!this.durableEnabled) {
928
+ return;
929
+ }
930
+ const now = Date.now();
931
+ this.enqueuePersist(async (client) => {
932
+ const iso = new Date(now).toISOString();
933
+ await client.query(
934
+ `INSERT INTO embedding_cache (cache_key, model, vector, created_at, last_used_at)
935
+ VALUES ($1, $2, $3, $4::timestamptz, $5::timestamptz)
936
+ ON CONFLICT (cache_key) DO UPDATE SET
937
+ model = EXCLUDED.model,
938
+ vector = EXCLUDED.vector,
939
+ last_used_at = EXCLUDED.last_used_at`,
940
+ [cacheKey, model, embeddingToBuffer(vector), iso, iso]
941
+ );
942
+ });
943
+ }
944
+ async touch(cacheKey) {
945
+ this.l1.touch(cacheKey);
946
+ if (!this.durableEnabled) {
947
+ return;
948
+ }
949
+ this.pendingTouches.add(cacheKey);
950
+ if (this.pendingTouches.size >= TOUCH_FLUSH_THRESHOLD) {
951
+ void this.flushTouches();
952
+ }
953
+ }
954
+ async flushTouches() {
955
+ if (!this.durableEnabled || this.pendingTouches.size === 0) {
956
+ this.pendingTouches.clear();
957
+ return;
958
+ }
959
+ const keys = [...this.pendingTouches];
960
+ this.pendingTouches.clear();
961
+ const now = (/* @__PURE__ */ new Date()).toISOString();
962
+ this.enqueuePersist(async (client) => {
963
+ await client.query(
964
+ `UPDATE embedding_cache
965
+ SET last_used_at = $1::timestamptz
966
+ WHERE cache_key = ANY($2::text[])`,
967
+ [now, keys]
968
+ );
969
+ });
970
+ }
971
+ async evictIfNeeded(maxEntries) {
972
+ this.l1.evictIfNeeded(maxEntries);
973
+ }
974
+ enqueuePersist(work) {
975
+ this.persistChain = this.persistChain.then(async () => {
976
+ const client = this.getClient();
977
+ if (!client) {
978
+ return;
979
+ }
980
+ await work(client);
981
+ }).catch(() => void 0);
982
+ }
983
+ };
578
984
 
579
985
  // src/llm/index.ts
580
986
  var OpenAICompatibleLlmProvider = class {
@@ -929,8 +1335,104 @@ function matchesMetadata(metadata, filter) {
929
1335
  return compare(getField(metadata, filter.field), filter.op);
930
1336
  }
931
1337
 
1338
+ // src/filters/sql-compile.ts
1339
+ var FIELD_RE = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
1340
+ function jsonPath(field) {
1341
+ if (!FIELD_RE.test(field)) {
1342
+ return null;
1343
+ }
1344
+ return `$.${field}`;
1345
+ }
1346
+ function compileComparison(field, op) {
1347
+ const path7 = jsonPath(field);
1348
+ if (!path7) {
1349
+ return null;
1350
+ }
1351
+ const extract = `json_extract(metadata_json, '${path7}')`;
1352
+ if ("eq" in op) {
1353
+ const value = op.eq;
1354
+ if (value !== null && typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
1355
+ return null;
1356
+ }
1357
+ return { expression: `${extract} = ?`, params: [value] };
1358
+ }
1359
+ if ("contains" in op) {
1360
+ return {
1361
+ expression: `CAST(${extract} AS TEXT) LIKE '%' || ? || '%'`,
1362
+ params: [op.contains]
1363
+ };
1364
+ }
1365
+ if ("gt" in op) {
1366
+ return { expression: `${extract} > ?`, params: [op.gt] };
1367
+ }
1368
+ if ("gte" in op) {
1369
+ return { expression: `${extract} >= ?`, params: [op.gte] };
1370
+ }
1371
+ if ("lt" in op) {
1372
+ return { expression: `${extract} < ?`, params: [op.lt] };
1373
+ }
1374
+ if ("lte" in op) {
1375
+ return { expression: `${extract} <= ?`, params: [op.lte] };
1376
+ }
1377
+ if ("between" in op) {
1378
+ const [lo, hi] = op.between;
1379
+ return {
1380
+ expression: `${extract} >= ? AND ${extract} <= ?`,
1381
+ params: [lo, hi]
1382
+ };
1383
+ }
1384
+ return null;
1385
+ }
1386
+ function compileMetadataFilterToSql(filter) {
1387
+ if ("and" in filter) {
1388
+ if (filter.and.length === 0) {
1389
+ return { expression: "1", params: [] };
1390
+ }
1391
+ const parts = [];
1392
+ for (const child of filter.and) {
1393
+ const compiled = compileMetadataFilterToSql(child);
1394
+ if (!compiled) {
1395
+ return null;
1396
+ }
1397
+ parts.push(compiled);
1398
+ }
1399
+ return {
1400
+ expression: parts.map((p) => `(${p.expression})`).join(" AND "),
1401
+ params: parts.flatMap((p) => p.params)
1402
+ };
1403
+ }
1404
+ if ("or" in filter) {
1405
+ if (filter.or.length === 0) {
1406
+ return { expression: "0", params: [] };
1407
+ }
1408
+ const parts = [];
1409
+ for (const child of filter.or) {
1410
+ const compiled = compileMetadataFilterToSql(child);
1411
+ if (!compiled) {
1412
+ return null;
1413
+ }
1414
+ parts.push(compiled);
1415
+ }
1416
+ return {
1417
+ expression: parts.map((p) => `(${p.expression})`).join(" OR "),
1418
+ params: parts.flatMap((p) => p.params)
1419
+ };
1420
+ }
1421
+ if ("not" in filter) {
1422
+ const inner = compileMetadataFilterToSql(filter.not);
1423
+ if (!inner) {
1424
+ return null;
1425
+ }
1426
+ return {
1427
+ expression: `NOT (${inner.expression})`,
1428
+ params: inner.params
1429
+ };
1430
+ }
1431
+ return compileComparison(filter.field, filter.op);
1432
+ }
1433
+
932
1434
  // src/schema/index.ts
933
- var SCHEMA_VERSION = 2;
1435
+ var SCHEMA_VERSION = 4;
934
1436
  var META_KEYS = {
935
1437
  schemaVersion: "schema_version",
936
1438
  embeddingDimensions: "embedding_dimensions",
@@ -951,6 +1453,7 @@ CREATE TABLE IF NOT EXISTS memories (
951
1453
  metadata_json TEXT NOT NULL DEFAULT '{}',
952
1454
  archived INTEGER NOT NULL DEFAULT 0 CHECK (archived IN (0, 1)),
953
1455
  compressed_into TEXT NULL,
1456
+ content_hash TEXT NULL,
954
1457
  created_at TEXT NOT NULL,
955
1458
  updated_at TEXT NOT NULL,
956
1459
  FOREIGN KEY (compressed_into) REFERENCES memories(id) ON DELETE SET NULL
@@ -960,7 +1463,7 @@ var CREATE_HISTORY_TABLE = `
960
1463
  CREATE TABLE IF NOT EXISTS memory_history (
961
1464
  id TEXT PRIMARY KEY NOT NULL,
962
1465
  memory_id TEXT NOT NULL,
963
- event_type TEXT NOT NULL CHECK (event_type IN ('created', 'archived', 'compressed')),
1466
+ event_type TEXT NOT NULL CHECK (event_type IN ('created', 'archived', 'compressed', 'updated')),
964
1467
  related_memory_id TEXT NULL,
965
1468
  created_at TEXT NOT NULL,
966
1469
  FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
@@ -981,14 +1484,33 @@ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
981
1484
  tokenize = 'porter unicode61'
982
1485
  );
983
1486
  `;
1487
+ var CREATE_EMBEDDING_CACHE_TABLE = `
1488
+ CREATE TABLE IF NOT EXISTS embedding_cache (
1489
+ cache_key TEXT PRIMARY KEY NOT NULL,
1490
+ model TEXT NOT NULL,
1491
+ vector BLOB NOT NULL,
1492
+ created_at TEXT NOT NULL,
1493
+ last_used_at TEXT NOT NULL
1494
+ );
1495
+ `;
984
1496
  var CREATE_INDEXES = [
985
1497
  `CREATE INDEX IF NOT EXISTS idx_memories_org_agent ON memories(organization, agent);`,
986
1498
  `CREATE INDEX IF NOT EXISTS idx_memories_org_archived ON memories(organization, archived);`,
987
- /** Active-set covering path for org-scoped list / stats / compress. */
1499
+ /** Active-set path for org-scoped list / stats / compress. */
988
1500
  `CREATE INDEX IF NOT EXISTS idx_memories_org_active_created
989
1501
  ON memories(organization, created_at) WHERE archived = 0;`,
990
- `CREATE INDEX IF NOT EXISTS idx_memories_created_at ON memories(created_at);`,
991
- `CREATE INDEX IF NOT EXISTS idx_history_memory_id ON memory_history(memory_id);`
1502
+ /** Agent-scoped active list (compress / forget-by-agent). */
1503
+ `CREATE INDEX IF NOT EXISTS idx_memories_org_agent_active_created
1504
+ ON memories(organization, agent, created_at) WHERE archived = 0;`,
1505
+ `CREATE INDEX IF NOT EXISTS idx_history_memory_id ON memory_history(memory_id);`,
1506
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_org_agent_hash_active
1507
+ ON memories(organization, agent, content_hash)
1508
+ WHERE archived = 0 AND content_hash IS NOT NULL;`,
1509
+ `CREATE INDEX IF NOT EXISTS idx_embedding_cache_last_used
1510
+ ON embedding_cache(last_used_at);`
1511
+ ];
1512
+ var DROP_REDUNDANT_INDEXES_V4 = [
1513
+ `DROP INDEX IF EXISTS idx_memories_created_at;`
992
1514
  ];
993
1515
  function buildVectorTableSql(dimensions) {
994
1516
  if (!Number.isInteger(dimensions) || dimensions <= 0) {
@@ -1012,35 +1534,42 @@ var SQL = {
1012
1534
  insertMemory: `
1013
1535
  INSERT INTO memories (
1014
1536
  id, organization, agent, content_text, metadata_json,
1015
- archived, compressed_into, created_at, updated_at
1016
- ) VALUES (?, ?, ?, ?, ?, 0, NULL, ?, ?)
1537
+ archived, compressed_into, content_hash, created_at, updated_at
1538
+ ) VALUES (?, ?, ?, ?, ?, 0, NULL, ?, ?, ?)
1017
1539
  RETURNING rowid, id, organization, agent, content_text, metadata_json,
1018
- archived, compressed_into, created_at, updated_at
1540
+ archived, compressed_into, content_hash, created_at, updated_at
1019
1541
  `,
1020
1542
  getMemoryById: `
1021
1543
  SELECT rowid, id, organization, agent, content_text, metadata_json,
1022
- archived, compressed_into, created_at, updated_at
1544
+ archived, compressed_into, content_hash, created_at, updated_at
1023
1545
  FROM memories
1024
1546
  WHERE id = ? AND organization = ?
1025
1547
  `,
1026
1548
  getMemoryByRowid: `
1027
1549
  SELECT rowid, id, organization, agent, content_text, metadata_json,
1028
- archived, compressed_into, created_at, updated_at
1550
+ archived, compressed_into, content_hash, created_at, updated_at
1029
1551
  FROM memories
1030
1552
  WHERE rowid = ? AND organization = ?
1031
1553
  `,
1032
1554
  getMemoriesByRowidsPrefix: `
1033
1555
  SELECT rowid, id, organization, agent, content_text, metadata_json,
1034
- archived, compressed_into, created_at, updated_at
1556
+ archived, compressed_into, content_hash, created_at, updated_at
1035
1557
  FROM memories
1036
1558
  WHERE organization = ? AND rowid IN (
1037
1559
  `,
1038
1560
  listMemoriesBase: `
1039
1561
  SELECT rowid, id, organization, agent, content_text, metadata_json,
1040
- archived, compressed_into, created_at, updated_at
1562
+ archived, compressed_into, content_hash, created_at, updated_at
1041
1563
  FROM memories
1042
1564
  WHERE organization = ?
1043
1565
  `,
1566
+ findActiveByContentHash: `
1567
+ SELECT rowid, id, organization, agent, content_text, metadata_json,
1568
+ archived, compressed_into, content_hash, created_at, updated_at
1569
+ FROM memories
1570
+ WHERE organization = ? AND agent = ? AND content_hash = ? AND archived = 0
1571
+ LIMIT 1
1572
+ `,
1044
1573
  insertEmbedding: `
1045
1574
  INSERT INTO memory_embeddings (memory_rowid, embedding) VALUES (?, ?)
1046
1575
  `,
@@ -1123,6 +1652,7 @@ var SQL = {
1123
1652
  UPDATE memories
1124
1653
  SET content_text = COALESCE(?, content_text),
1125
1654
  metadata_json = COALESCE(?, metadata_json),
1655
+ content_hash = COALESCE(?, content_hash),
1126
1656
  updated_at = ?
1127
1657
  WHERE id = ? AND organization = ?
1128
1658
  `,
@@ -1132,6 +1662,52 @@ var SQL = {
1132
1662
  `,
1133
1663
  deleteFts: `
1134
1664
  DELETE FROM memories_fts WHERE memory_id = ?
1665
+ `,
1666
+ deleteFtsByOrg: `
1667
+ DELETE FROM memories_fts WHERE organization = ?
1668
+ `,
1669
+ deleteFtsByOrgAgent: `
1670
+ DELETE FROM memories_fts WHERE organization = ? AND agent = ?
1671
+ `,
1672
+ deleteEmbeddingsByOrg: `
1673
+ DELETE FROM memory_embeddings WHERE memory_rowid IN (
1674
+ SELECT rowid FROM memories WHERE organization = ?
1675
+ )
1676
+ `,
1677
+ deleteEmbeddingsByOrgAgent: `
1678
+ DELETE FROM memory_embeddings WHERE memory_rowid IN (
1679
+ SELECT rowid FROM memories WHERE organization = ? AND agent = ?
1680
+ )
1681
+ `,
1682
+ deleteEmbeddingsBlobByOrg: `
1683
+ DELETE FROM memory_embeddings_blob WHERE memory_rowid IN (
1684
+ SELECT rowid FROM memories WHERE organization = ?
1685
+ )
1686
+ `,
1687
+ deleteEmbeddingsBlobByOrgAgent: `
1688
+ DELETE FROM memory_embeddings_blob WHERE memory_rowid IN (
1689
+ SELECT rowid FROM memories WHERE organization = ? AND agent = ?
1690
+ )
1691
+ `,
1692
+ deleteArchivedEmbeddings: `
1693
+ DELETE FROM memory_embeddings WHERE memory_rowid IN (
1694
+ SELECT rowid FROM memories WHERE archived = 1
1695
+ )
1696
+ `,
1697
+ deleteArchivedEmbeddingsBlob: `
1698
+ DELETE FROM memory_embeddings_blob WHERE memory_rowid IN (
1699
+ SELECT rowid FROM memories WHERE archived = 1
1700
+ )
1701
+ `,
1702
+ /** Single-pass org stats (avoids 4 separate COUNT queries). */
1703
+ getStats: `
1704
+ SELECT
1705
+ COUNT(*) AS total,
1706
+ COALESCE(SUM(CASE WHEN archived = 0 THEN 1 ELSE 0 END), 0) AS active,
1707
+ COALESCE(SUM(CASE WHEN archived = 1 THEN 1 ELSE 0 END), 0) AS archived,
1708
+ COUNT(DISTINCT CASE WHEN archived = 0 THEN agent END) AS agents
1709
+ FROM memories
1710
+ WHERE organization = ?
1135
1711
  `
1136
1712
  };
1137
1713
 
@@ -1337,10 +1913,140 @@ function siftDown(dist, rows, i, n) {
1337
1913
  }
1338
1914
  }
1339
1915
 
1916
+ // src/storage/sqlite/concurrency-config.ts
1917
+ var DEFAULT_CONCURRENCY = {
1918
+ maxRetries: 5,
1919
+ baseBackoffMs: 50,
1920
+ maxBackoffMs: 2e3,
1921
+ lockTimeoutMs: 5e3
1922
+ };
1923
+ function resolveConcurrencyConfig(input) {
1924
+ const resolved = {
1925
+ maxRetries: input?.maxRetries ?? DEFAULT_CONCURRENCY.maxRetries,
1926
+ baseBackoffMs: input?.baseBackoffMs ?? DEFAULT_CONCURRENCY.baseBackoffMs,
1927
+ maxBackoffMs: input?.maxBackoffMs ?? DEFAULT_CONCURRENCY.maxBackoffMs,
1928
+ lockTimeoutMs: input?.lockTimeoutMs ?? DEFAULT_CONCURRENCY.lockTimeoutMs
1929
+ };
1930
+ assertPositiveInt(resolved.maxRetries, "concurrency.maxRetries");
1931
+ assertPositiveNumber(resolved.baseBackoffMs, "concurrency.baseBackoffMs");
1932
+ assertPositiveNumber(resolved.maxBackoffMs, "concurrency.maxBackoffMs");
1933
+ assertPositiveInt(resolved.lockTimeoutMs, "concurrency.lockTimeoutMs");
1934
+ if (resolved.maxBackoffMs < resolved.baseBackoffMs) {
1935
+ throw new ConfigurationError(
1936
+ "concurrency.maxBackoffMs must be >= concurrency.baseBackoffMs"
1937
+ );
1938
+ }
1939
+ return resolved;
1940
+ }
1941
+ function assertPositiveInt(value, field) {
1942
+ if (!Number.isInteger(value) || value < 0) {
1943
+ throw new ConfigurationError(`${field} must be a non-negative integer`);
1944
+ }
1945
+ }
1946
+ function assertPositiveNumber(value, field) {
1947
+ if (!Number.isFinite(value) || value < 0) {
1948
+ throw new ConfigurationError(`${field} must be a non-negative number`);
1949
+ }
1950
+ }
1951
+
1952
+ // src/storage/sqlite/transaction.ts
1953
+ function isSqliteBusyError(error) {
1954
+ const raw = error instanceof Error ? error.message : String(error);
1955
+ const lower = raw.toLowerCase();
1956
+ return lower.includes("database is locked") || lower.includes("sqlite_busy") || lower.includes("busy");
1957
+ }
1958
+ function sleep(ms) {
1959
+ return new Promise((resolve) => setTimeout(resolve, ms));
1960
+ }
1961
+ function backoffDelay(attempt, config) {
1962
+ const exp = Math.min(
1963
+ config.maxBackoffMs,
1964
+ config.baseBackoffMs * 2 ** attempt
1965
+ );
1966
+ const jitter = Math.random() * config.baseBackoffMs;
1967
+ return Math.min(config.maxBackoffMs, exp + jitter);
1968
+ }
1969
+ async function withImmediateTransaction(db, config, fn, onRetry) {
1970
+ let lastError;
1971
+ for (let attempt = 0; attempt <= config.maxRetries; attempt += 1) {
1972
+ try {
1973
+ db.exec("BEGIN IMMEDIATE");
1974
+ try {
1975
+ const result = await fn();
1976
+ db.exec("COMMIT");
1977
+ return result;
1978
+ } catch (error) {
1979
+ try {
1980
+ db.exec("ROLLBACK");
1981
+ } catch {
1982
+ }
1983
+ throw error;
1984
+ }
1985
+ } catch (error) {
1986
+ lastError = error;
1987
+ const isBusy = isSqliteBusyError(error);
1988
+ if (!isBusy) {
1989
+ throw error;
1990
+ }
1991
+ if (attempt >= config.maxRetries) {
1992
+ break;
1993
+ }
1994
+ const delay = backoffDelay(attempt, config);
1995
+ onRetry?.(attempt + 1, delay, error);
1996
+ await sleep(delay);
1997
+ }
1998
+ }
1999
+ throw new StorageLockedError(
2000
+ `SQLite write lock could not be acquired after ${config.maxRetries} retries`,
2001
+ {
2002
+ cause: lastError instanceof Error ? lastError : void 0,
2003
+ reason: "SQLITE_BUSY exhausted retries",
2004
+ suggestion: "Increase concurrency.maxRetries or concurrency.lockTimeoutMs, or consider the Postgres backend for high-concurrency multi-agent workloads."
2005
+ }
2006
+ );
2007
+ }
2008
+ function normalizeMemoryContent(text) {
2009
+ return text.normalize("NFC").trim().replace(/\s+/g, " ");
2010
+ }
2011
+ function hashMemoryContent(text) {
2012
+ const normalized = normalizeMemoryContent(text);
2013
+ return crypto$1.createHash("sha256").update(normalized, "utf8").digest("hex");
2014
+ }
2015
+ var DEFAULT_MEMORY_DEDUPE = {
2016
+ enabled: false,
2017
+ strategy: "exact-or-near",
2018
+ nearThreshold: 0.92,
2019
+ nearCandidateLimit: 8
2020
+ };
2021
+ function resolveMemoryDedupeConfig(input) {
2022
+ if (input === void 0) {
2023
+ return { ...DEFAULT_MEMORY_DEDUPE };
2024
+ }
2025
+ if (typeof input === "boolean") {
2026
+ return { ...DEFAULT_MEMORY_DEDUPE, enabled: input };
2027
+ }
2028
+ return {
2029
+ enabled: input.enabled ?? true,
2030
+ strategy: input.strategy ?? DEFAULT_MEMORY_DEDUPE.strategy,
2031
+ nearThreshold: input.nearThreshold ?? DEFAULT_MEMORY_DEDUPE.nearThreshold,
2032
+ nearCandidateLimit: input.nearCandidateLimit ?? DEFAULT_MEMORY_DEDUPE.nearCandidateLimit
2033
+ };
2034
+ }
2035
+ function mergeMemoryMetadata(existing, incoming) {
2036
+ return { ...existing, ...incoming };
2037
+ }
2038
+
1340
2039
  // src/storage/providers/sqlite.ts
1341
- var SqliteStorageProvider = class {
2040
+ var MAX_METADATA_SCAN = 5e4;
2041
+ var METADATA_PAGE_SIZE = 500;
2042
+ var BATCH_INSERT_CHUNK = 96;
2043
+ var MAX_ANN_OVERFETCH = 8192;
2044
+ var INSERT_COALESCE_THRESHOLD = 24;
2045
+ var INSERT_COALESCE_MAX = 96;
2046
+ var SqliteStorageProvider = class _SqliteStorageProvider {
1342
2047
  name = "sqlite";
1343
2048
  connectionString;
2049
+ concurrency;
1344
2050
  db = null;
1345
2051
  statements = null;
1346
2052
  vectorDimensions = null;
@@ -1351,13 +2057,37 @@ var SqliteStorageProvider = class {
1351
2057
  memoryIndexDirty = false;
1352
2058
  /** Resolved absolute path (or `:memory:`) — avoid re-resolving on size checks. */
1353
2059
  resolvedPath = null;
2060
+ retryLog = null;
2061
+ /** Cached prepared statements for `rowid IN (…)` lookups keyed by list length. */
2062
+ rowidInStatements = /* @__PURE__ */ new Map();
2063
+ /** Cached list SQL statements keyed by clause shape. */
2064
+ listStatements = /* @__PURE__ */ new Map();
2065
+ /** Cached multi-row insert statements keyed by row count. */
2066
+ batchInsertStatements = /* @__PURE__ */ new Map();
2067
+ /** Cached multi-row history inserts keyed by row count. */
2068
+ batchHistoryStatements = /* @__PURE__ */ new Map();
2069
+ /** Cached multi-row FTS inserts keyed by row count. */
2070
+ batchFtsStatements = /* @__PURE__ */ new Map();
2071
+ /** Cached multi-row blob embedding inserts keyed by row count. */
2072
+ batchBlobEmbStatements = /* @__PURE__ */ new Map();
2073
+ /** Coalesce concurrent insertMemory callers into one IMMEDIATE transaction. */
2074
+ insertQueue = [];
2075
+ insertFlushScheduled = false;
1354
2076
  constructor(options) {
1355
2077
  this.connectionString = options.connectionString;
2078
+ this.concurrency = resolveConcurrencyConfig(options.concurrency);
1356
2079
  }
1357
2080
  /** Absolute or relative path / `:memory:` used by this provider. */
1358
2081
  get path() {
1359
2082
  return this.connectionString;
1360
2083
  }
2084
+ /** Expose DB for embedding cache store (same connection). */
2085
+ getDatabase() {
2086
+ return this.db;
2087
+ }
2088
+ setRetryLogger(fn) {
2089
+ this.retryLog = fn;
2090
+ }
1361
2091
  async open() {
1362
2092
  try {
1363
2093
  const dbPath = this.resolvePath(this.connectionString);
@@ -1371,17 +2101,16 @@ var SqliteStorageProvider = class {
1371
2101
  db.exec(`
1372
2102
  PRAGMA synchronous = NORMAL;
1373
2103
  PRAGMA foreign_keys = ON;
1374
- PRAGMA busy_timeout = 5000;
2104
+ PRAGMA busy_timeout = ${this.concurrency.lockTimeoutMs};
1375
2105
  PRAGMA temp_store = MEMORY;
1376
- PRAGMA cache_size = -65536;
1377
- PRAGMA mmap_size = 268435456;
1378
- PRAGMA wal_autocheckpoint = 1000;
2106
+ PRAGMA cache_size = -32768;
2107
+ PRAGMA mmap_size = 134217728;
2108
+ PRAGMA wal_autocheckpoint = 2000;
1379
2109
  PRAGMA recursive_triggers = OFF;
1380
2110
  `);
1381
2111
  this.sqliteVecLoaded = this.tryLoadSqliteVec(db);
1382
2112
  this.runMigrations(db);
1383
2113
  this.statements = this.prepareStatements(db);
1384
- this.ensureFtsConsistency(db);
1385
2114
  const backend = this.readMetaString(META_KEYS.vectorBackend);
1386
2115
  const dims = this.readMetaNumber(META_KEYS.embeddingDimensions);
1387
2116
  if (backend) {
@@ -1413,10 +2142,18 @@ var SqliteStorageProvider = class {
1413
2142
  }
1414
2143
  }
1415
2144
  async close() {
2145
+ const deadline = Date.now() + 2e3;
2146
+ while (this.insertQueue.length > 0 && Date.now() < deadline) {
2147
+ await this.flushInsertQueue();
2148
+ }
1416
2149
  if (!this.db) {
1417
2150
  return;
1418
2151
  }
1419
2152
  try {
2153
+ try {
2154
+ this.db.exec("PRAGMA optimize;");
2155
+ } catch {
2156
+ }
1420
2157
  this.db.close();
1421
2158
  } catch (error) {
1422
2159
  throw new DatabaseError(`Failed to close database: ${this.describe(error)}`, {
@@ -1426,6 +2163,12 @@ var SqliteStorageProvider = class {
1426
2163
  this.db = null;
1427
2164
  this.statements = null;
1428
2165
  this.memoryIndex = null;
2166
+ this.rowidInStatements.clear();
2167
+ this.listStatements.clear();
2168
+ this.batchInsertStatements.clear();
2169
+ this.batchHistoryStatements.clear();
2170
+ this.batchFtsStatements.clear();
2171
+ this.batchBlobEmbStatements.clear();
1429
2172
  }
1430
2173
  }
1431
2174
  async ensureVectorSchema(dimensions) {
@@ -1458,16 +2201,64 @@ var SqliteStorageProvider = class {
1458
2201
  this.vectorDimensions = dimensions;
1459
2202
  }
1460
2203
  async insertMemory(input) {
1461
- const stmts = this.requireStatements();
1462
2204
  this.requireVectorReady();
1463
- return this.withTransaction(() => {
1464
- const row = stmts.insertMemory.get(
1465
- input.id,
1466
- input.organization,
1467
- input.agent,
1468
- input.contentText,
1469
- serializeMetadata(input.metadata),
1470
- input.createdAt,
2205
+ return new Promise((resolve, reject) => {
2206
+ this.insertQueue.push({ input, resolve, reject });
2207
+ if (this.insertQueue.length >= INSERT_COALESCE_THRESHOLD) {
2208
+ this.insertFlushScheduled = false;
2209
+ void this.flushInsertQueue();
2210
+ return;
2211
+ }
2212
+ this.scheduleInsertFlush();
2213
+ });
2214
+ }
2215
+ scheduleInsertFlush() {
2216
+ if (this.insertFlushScheduled) {
2217
+ return;
2218
+ }
2219
+ this.insertFlushScheduled = true;
2220
+ setImmediate(() => {
2221
+ this.insertFlushScheduled = false;
2222
+ void this.flushInsertQueue();
2223
+ });
2224
+ }
2225
+ async flushInsertQueue() {
2226
+ if (this.insertQueue.length === 0) {
2227
+ return;
2228
+ }
2229
+ const batch = this.insertQueue.splice(0, INSERT_COALESCE_MAX);
2230
+ try {
2231
+ if (batch.length === 1) {
2232
+ const row = await this.insertMemoryImmediate(batch[0].input);
2233
+ batch[0].resolve(row);
2234
+ } else {
2235
+ const rows = await this.insertMemoriesBatch(batch.map((b) => b.input));
2236
+ for (let i = 0; i < batch.length; i += 1) {
2237
+ batch[i].resolve(rows[i]);
2238
+ }
2239
+ }
2240
+ } catch (error) {
2241
+ for (const item of batch) {
2242
+ item.reject(error);
2243
+ }
2244
+ }
2245
+ if (this.insertQueue.length > 0) {
2246
+ void this.flushInsertQueue();
2247
+ }
2248
+ }
2249
+ /** Single-row insert without coalescing (used by flush of size 1). */
2250
+ async insertMemoryImmediate(input) {
2251
+ const stmts = this.requireStatements();
2252
+ const contentHash = input.contentHash !== void 0 ? input.contentHash : hashMemoryContent(input.contentText);
2253
+ return this.withTransaction(() => {
2254
+ const row = stmts.insertMemory.get(
2255
+ input.id,
2256
+ input.organization,
2257
+ input.agent,
2258
+ input.contentText,
2259
+ serializeMetadata(input.metadata),
2260
+ contentHash,
2261
+ input.createdAt,
1471
2262
  input.updatedAt
1472
2263
  );
1473
2264
  if (!row || row.rowid === void 0) {
@@ -1494,38 +2285,13 @@ var SqliteStorageProvider = class {
1494
2285
  if (inputs.length === 0) {
1495
2286
  return [];
1496
2287
  }
1497
- const stmts = this.requireStatements();
1498
2288
  this.requireVectorReady();
1499
2289
  return this.withTransaction(() => {
1500
2290
  const rows = [];
1501
- for (const input of inputs) {
1502
- const row = stmts.insertMemory.get(
1503
- input.id,
1504
- input.organization,
1505
- input.agent,
1506
- input.contentText,
1507
- serializeMetadata(input.metadata),
1508
- input.createdAt,
1509
- input.updatedAt
1510
- );
1511
- if (!row || row.rowid === void 0) {
1512
- throw new DatabaseError("Failed to read memory after batch insert");
1513
- }
1514
- this.insertEmbedding(row.rowid, input.embedding);
1515
- this.insertFtsRow(
1516
- input.id,
1517
- input.organization,
1518
- input.agent,
1519
- input.contentText
1520
- );
1521
- stmts.insertHistory.run(
1522
- crypto.randomUUID(),
1523
- input.id,
1524
- "created",
1525
- null,
1526
- input.createdAt
1527
- );
1528
- rows.push(row);
2291
+ for (let offset = 0; offset < inputs.length; offset += BATCH_INSERT_CHUNK) {
2292
+ const chunk = inputs.slice(offset, offset + BATCH_INSERT_CHUNK);
2293
+ const inserted = this.insertMemoryChunk(chunk);
2294
+ rows.push(...inserted);
1529
2295
  }
1530
2296
  return rows;
1531
2297
  });
@@ -1542,9 +2308,11 @@ var SqliteStorageProvider = class {
1542
2308
  }
1543
2309
  const contentText = input.contentText ?? existing.content_text;
1544
2310
  const metadataJson = input.metadata !== void 0 ? serializeMetadata(input.metadata) : existing.metadata_json;
2311
+ const contentHash = input.contentHash !== void 0 ? input.contentHash : input.contentText !== void 0 ? hashMemoryContent(input.contentText) : existing.content_hash ?? null;
1545
2312
  stmts.updateMemoryContent.run(
1546
2313
  input.contentText ?? null,
1547
2314
  input.metadata !== void 0 ? metadataJson : null,
2315
+ contentHash,
1548
2316
  input.updatedAt,
1549
2317
  input.id,
1550
2318
  input.organization
@@ -1561,9 +2329,25 @@ var SqliteStorageProvider = class {
1561
2329
  contentText
1562
2330
  );
1563
2331
  }
2332
+ stmts.insertHistory.run(
2333
+ crypto.randomUUID(),
2334
+ input.id,
2335
+ "updated",
2336
+ null,
2337
+ input.updatedAt
2338
+ );
1564
2339
  return stmts.getMemoryById.get(input.id, input.organization);
1565
2340
  });
1566
2341
  }
2342
+ async findActiveByContentHash(organization, agent, contentHash) {
2343
+ const stmts = this.requireStatements();
2344
+ const row = stmts.findActiveByContentHash.get(
2345
+ organization,
2346
+ agent,
2347
+ contentHash
2348
+ );
2349
+ return row ?? null;
2350
+ }
1567
2351
  async searchByMetadata(filter, limit) {
1568
2352
  const rows = await this.listMemories(filter, limit);
1569
2353
  if (!filter.metadata) {
@@ -1609,13 +2393,11 @@ var SqliteStorageProvider = class {
1609
2393
  if (rowids.length === 0) {
1610
2394
  return out;
1611
2395
  }
1612
- const db = this.requireDb();
1613
2396
  const chunkSize = 400;
1614
2397
  for (let offset = 0; offset < rowids.length; offset += chunkSize) {
1615
2398
  const chunk = rowids.slice(offset, offset + chunkSize);
1616
- const placeholders = chunk.map(() => "?").join(",");
1617
- const sql = `${SQL.getMemoriesByRowidsPrefix}${placeholders})`;
1618
- const rows = db.prepare(sql).all(organization, ...chunk);
2399
+ const stmt = this.getRowidInStatement(chunk.length);
2400
+ const rows = stmt.all(organization, ...chunk);
1619
2401
  for (const row of rows) {
1620
2402
  if (row.rowid !== void 0) {
1621
2403
  out.set(row.rowid, row);
@@ -1625,41 +2407,11 @@ var SqliteStorageProvider = class {
1625
2407
  return out;
1626
2408
  }
1627
2409
  async listMemories(filter, limit) {
1628
- const db = this.requireDb();
1629
- const clauses = [`organization = ?`];
1630
- const params = [filter.organization];
1631
- if (filter.agent) {
1632
- clauses.push(`agent = ?`);
1633
- params.push(filter.agent);
1634
- }
1635
- if (!filter.includeArchived) {
1636
- clauses.push(`archived = 0`);
1637
- }
1638
- let sql = `
1639
- SELECT rowid, id, organization, agent, content_text, metadata_json,
1640
- archived, compressed_into, created_at, updated_at
1641
- FROM memories
1642
- WHERE ${clauses.join(" AND ")}
1643
- ORDER BY created_at ASC
1644
- `;
1645
- if (limit !== void 0 && !filter.metadata) {
1646
- sql += ` LIMIT ?`;
1647
- params.push(limit);
1648
- }
1649
2410
  try {
1650
- let rows = db.prepare(sql).all(...params);
1651
2411
  if (filter.metadata) {
1652
- rows = rows.filter(
1653
- (row) => matchesMetadata(
1654
- deserializeMetadata(row.metadata_json),
1655
- filter.metadata
1656
- )
1657
- );
1658
- if (limit !== void 0) {
1659
- rows = rows.slice(0, limit);
1660
- }
2412
+ return this.listMemoriesWithMetadata(filter, limit);
1661
2413
  }
1662
- return rows;
2414
+ return this.listMemoriesIndexed(filter, limit);
1663
2415
  } catch (error) {
1664
2416
  throw new DatabaseError(`Failed to list memories: ${this.describe(error)}`, {
1665
2417
  cause: error instanceof Error ? error : void 0
@@ -1674,26 +2426,44 @@ var SqliteStorageProvider = class {
1674
2426
  return this.searchWithBlobFallback(embedding, topK);
1675
2427
  }
1676
2428
  /**
1677
- * Org-scoped KNN + memory rows.
1678
- * Caller (Wolbarg.recall) already passes an overfetched topK do not multiply again.
2429
+ * Org-scoped KNN + memory rows with adaptive overfetch.
2430
+ * Global ANN is post-filtered by org/agent/archived; underfill triggers larger k.
1679
2431
  */
1680
2432
  async searchVectorsWithMemories(embedding, topK, organization, options) {
1681
2433
  this.requireVectorReady();
1682
- const hits = await this.searchVectors(embedding, topK);
1683
- if (hits.length === 0) {
2434
+ if (topK <= 0) {
1684
2435
  return [];
1685
2436
  }
1686
- const map = await this.getMemoriesByRowids(
1687
- hits.map((h) => h.memoryRowid),
1688
- organization
1689
- );
1690
- const out = [];
1691
- for (const hit of hits) {
1692
- const row = map.get(hit.memoryRowid);
1693
- if (!row) continue;
1694
- if (options?.agent && row.agent !== options.agent) continue;
1695
- if (!options?.includeArchived && row.archived === 1) continue;
1696
- out.push({ row, distance: hit.distance });
2437
+ let fetchK = topK;
2438
+ const maxFetch = Math.min(Math.max(topK * 64, 512), MAX_ANN_OVERFETCH);
2439
+ let out = [];
2440
+ while (true) {
2441
+ const hits = await this.searchVectors(embedding, fetchK);
2442
+ if (hits.length === 0) {
2443
+ return [];
2444
+ }
2445
+ const map = await this.getMemoriesByRowids(
2446
+ hits.map((h) => h.memoryRowid),
2447
+ organization
2448
+ );
2449
+ out = [];
2450
+ for (const hit of hits) {
2451
+ const row = map.get(hit.memoryRowid);
2452
+ if (!row) continue;
2453
+ if (options?.agent && row.agent !== options.agent) continue;
2454
+ if (!options?.includeArchived && row.archived === 1) continue;
2455
+ out.push({ row, distance: hit.distance });
2456
+ if (out.length >= topK) {
2457
+ return out;
2458
+ }
2459
+ }
2460
+ if (hits.length < fetchK) {
2461
+ break;
2462
+ }
2463
+ if (fetchK >= maxFetch) {
2464
+ break;
2465
+ }
2466
+ fetchK = Math.min(fetchK * 4, maxFetch);
1697
2467
  }
1698
2468
  return out;
1699
2469
  }
@@ -1702,6 +2472,7 @@ var SqliteStorageProvider = class {
1702
2472
  const archived = [];
1703
2473
  return this.withTransaction(() => {
1704
2474
  for (const id of ids) {
2475
+ const existing = stmts.getMemoryById.get(id, organization);
1705
2476
  const result = stmts.archiveMemory.run(
1706
2477
  compressedIntoId,
1707
2478
  archivedAt,
@@ -1710,6 +2481,9 @@ var SqliteStorageProvider = class {
1710
2481
  );
1711
2482
  if (Number(result.changes) > 0) {
1712
2483
  archived.push(id);
2484
+ if (existing?.rowid !== void 0) {
2485
+ this.deleteEmbedding(existing.rowid);
2486
+ }
1713
2487
  this.deleteFts(id);
1714
2488
  stmts.insertHistory.run(
1715
2489
  crypto.randomUUID(),
@@ -1750,17 +2524,8 @@ var SqliteStorageProvider = class {
1750
2524
  throw new DatabaseError("deleteMemoriesByFilter requires an agent filter");
1751
2525
  }
1752
2526
  return this.withTransaction(() => {
1753
- const listed = this.listMemoriesSync({
1754
- organization: filter.organization,
1755
- agent,
1756
- includeArchived: true
1757
- });
1758
- for (const memory of listed) {
1759
- if (memory.rowid !== void 0) {
1760
- this.deleteEmbedding(memory.rowid);
1761
- }
1762
- this.deleteFts(memory.id);
1763
- }
2527
+ this.deleteEmbeddingsForScope(filter.organization, agent);
2528
+ this.deleteFtsForScope(filter.organization, agent);
1764
2529
  const result = stmts.deleteMemoriesByOrgAgent.run(
1765
2530
  filter.organization,
1766
2531
  agent
@@ -1771,13 +2536,8 @@ var SqliteStorageProvider = class {
1771
2536
  async clearOrganization(organization) {
1772
2537
  const stmts = this.requireStatements();
1773
2538
  return this.withTransaction(() => {
1774
- const listed = this.listMemoriesSync({ organization, includeArchived: true });
1775
- for (const memory of listed) {
1776
- if (memory.rowid !== void 0) {
1777
- this.deleteEmbedding(memory.rowid);
1778
- }
1779
- this.deleteFts(memory.id);
1780
- }
2539
+ this.deleteEmbeddingsForScope(organization);
2540
+ this.deleteFtsForScope(organization);
1781
2541
  const result = stmts.deleteMemoriesByOrg.run(organization);
1782
2542
  return Number(result.changes);
1783
2543
  });
@@ -1798,15 +2558,12 @@ var SqliteStorageProvider = class {
1798
2558
  }
1799
2559
  async getStats(organization) {
1800
2560
  const stmts = this.requireStatements();
1801
- const memories = stmts.countMemories.get(organization);
1802
- const active = stmts.countActiveMemories.get(organization);
1803
- const archived = stmts.countArchivedMemories.get(organization);
1804
- const agents = stmts.countAgents.get(organization);
2561
+ const row = stmts.getStats.get(organization);
1805
2562
  return {
1806
- totalMemories: Number(memories.count),
1807
- activeMemories: Number(active.count),
1808
- archivedMemories: Number(archived.count),
1809
- totalAgents: Number(agents.count)
2563
+ totalMemories: Number(row.total),
2564
+ activeMemories: Number(row.active),
2565
+ archivedMemories: Number(row.archived),
2566
+ totalAgents: Number(row.agents)
1810
2567
  };
1811
2568
  }
1812
2569
  async getDatabaseSizeBytes() {
@@ -1839,59 +2596,159 @@ var SqliteStorageProvider = class {
1839
2596
  );
1840
2597
  }
1841
2598
  }
1842
- withTransaction(fn) {
2599
+ async withTransaction(fn) {
1843
2600
  const db = this.requireDb();
1844
- db.exec("BEGIN");
1845
- try {
1846
- const result = fn();
1847
- db.exec("COMMIT");
1848
- return result;
1849
- } catch (error) {
1850
- try {
1851
- db.exec("ROLLBACK");
1852
- } catch {
1853
- }
1854
- if (error instanceof DatabaseError || error instanceof InitializationError) {
1855
- throw error;
2601
+ return withImmediateTransaction(
2602
+ db,
2603
+ this.concurrency,
2604
+ fn,
2605
+ (attempt, delayMs) => {
2606
+ this.retryLog?.(
2607
+ `SQLITE_BUSY retry attempt=${attempt} backoffMs=${Math.round(delayMs)}`
2608
+ );
1856
2609
  }
1857
- throw new DatabaseError(`Transaction failed: ${this.describe(error)}`, {
1858
- cause: error instanceof Error ? error : void 0
1859
- });
1860
- }
2610
+ );
1861
2611
  }
1862
2612
  // ─── internals ───────────────────────────────────────────────────────────
1863
2613
  tryLoadSqliteVec(db) {
2614
+ const plat = `${process.platform}-${process.arch}`;
2615
+ if (_SqliteStorageProvider.sqliteVecUnsupported.has(plat)) {
2616
+ return false;
2617
+ }
1864
2618
  try {
1865
2619
  sqliteVec__namespace.load(db);
1866
2620
  return true;
1867
2621
  } catch {
2622
+ _SqliteStorageProvider.sqliteVecUnsupported.add(plat);
1868
2623
  return false;
1869
2624
  }
1870
2625
  }
2626
+ static sqliteVecUnsupported = /* @__PURE__ */ new Set();
1871
2627
  runMigrations(db) {
1872
2628
  db.exec(CREATE_META_TABLE);
1873
2629
  db.exec(CREATE_MEMORIES_TABLE);
1874
2630
  db.exec(CREATE_HISTORY_TABLE);
1875
2631
  db.exec(CREATE_BLOB_EMBEDDINGS_TABLE);
1876
- for (const indexSql of CREATE_INDEXES) {
1877
- db.exec(indexSql);
1878
- }
2632
+ db.exec(CREATE_EMBEDDING_CACHE_TABLE);
1879
2633
  const current = this.readMetaNumberFromDb(db, META_KEYS.schemaVersion);
1880
2634
  if (current === null) {
1881
2635
  db.exec(CREATE_FTS_TABLE);
1882
2636
  this.backfillFts(db);
2637
+ this.migrateToV3(db);
2638
+ for (const dropSql of DROP_REDUNDANT_INDEXES_V4) {
2639
+ db.exec(dropSql);
2640
+ }
2641
+ for (const indexSql of CREATE_INDEXES) {
2642
+ db.exec(indexSql);
2643
+ }
1883
2644
  db.prepare(SQL.setMeta).run(META_KEYS.schemaVersion, String(SCHEMA_VERSION));
2645
+ this.ensureFtsConsistency(db);
1884
2646
  } else if (current > SCHEMA_VERSION) {
1885
2647
  throw new InitializationError(
1886
2648
  `Database schema version ${current} is newer than this SDK (supports ${SCHEMA_VERSION}).`
1887
2649
  );
1888
2650
  } else if (current < SCHEMA_VERSION) {
1889
- db.exec(CREATE_FTS_TABLE);
1890
- this.backfillFts(db);
2651
+ if (current < 3) {
2652
+ db.exec(CREATE_FTS_TABLE);
2653
+ this.backfillFts(db);
2654
+ this.migrateToV3(db);
2655
+ }
2656
+ if (current < 4) {
2657
+ this.migrateToV4(db);
2658
+ }
2659
+ for (const indexSql of CREATE_INDEXES) {
2660
+ db.exec(indexSql);
2661
+ }
1891
2662
  db.prepare(SQL.setMeta).run(META_KEYS.schemaVersion, String(SCHEMA_VERSION));
1892
- } else {
1893
- db.exec(CREATE_FTS_TABLE);
2663
+ this.ensureFtsConsistency(db);
2664
+ }
2665
+ }
2666
+ /**
2667
+ * Schema v4: drop unused global created_at index, strip archived vectors
2668
+ * from ANN, add agent-active covering index (via CREATE_INDEXES).
2669
+ */
2670
+ migrateToV4(db) {
2671
+ for (const dropSql of DROP_REDUNDANT_INDEXES_V4) {
2672
+ db.exec(dropSql);
2673
+ }
2674
+ try {
2675
+ db.prepare(SQL.deleteArchivedEmbeddings).run();
2676
+ } catch {
2677
+ }
2678
+ try {
2679
+ db.prepare(SQL.deleteArchivedEmbeddingsBlob).run();
2680
+ } catch {
1894
2681
  }
2682
+ this.memoryIndexDirty = true;
2683
+ }
2684
+ /**
2685
+ * Schema v3: content_hash column, history 'updated' event, embedding_cache.
2686
+ * SQLite cannot ALTER CHECK constraints — rebuild memory_history when needed.
2687
+ */
2688
+ migrateToV3(db) {
2689
+ const cols = db.prepare(`PRAGMA table_info(memories)`).all();
2690
+ const hasHash = cols.some((c) => c.name === "content_hash");
2691
+ if (!hasHash) {
2692
+ db.exec(`ALTER TABLE memories ADD COLUMN content_hash TEXT`);
2693
+ }
2694
+ const active = db.prepare(
2695
+ `SELECT id, content_text, updated_at FROM memories WHERE archived = 0 ORDER BY updated_at DESC`
2696
+ ).all();
2697
+ const seen = /* @__PURE__ */ new Set();
2698
+ const updateHash = db.prepare(
2699
+ `UPDATE memories SET content_hash = ? WHERE id = ?`
2700
+ );
2701
+ for (const row of active) {
2702
+ const hash = hashMemoryContent(row.content_text);
2703
+ updateHash.run(hash, row.id);
2704
+ }
2705
+ const activeFull = db.prepare(
2706
+ `SELECT id, organization, agent, content_text, updated_at
2707
+ FROM memories WHERE archived = 0
2708
+ ORDER BY updated_at DESC`
2709
+ ).all();
2710
+ seen.clear();
2711
+ const clearHash = db.prepare(
2712
+ `UPDATE memories SET content_hash = NULL WHERE id = ?`
2713
+ );
2714
+ const setHash = db.prepare(
2715
+ `UPDATE memories SET content_hash = ? WHERE id = ?`
2716
+ );
2717
+ for (const row of activeFull) {
2718
+ const hash = hashMemoryContent(row.content_text);
2719
+ const key = `${row.organization}\0${row.agent}\0${hash}`;
2720
+ if (seen.has(key)) {
2721
+ clearHash.run(row.id);
2722
+ } else {
2723
+ seen.add(key);
2724
+ setHash.run(hash, row.id);
2725
+ }
2726
+ }
2727
+ this.migrateHistoryAllowUpdated(db);
2728
+ db.exec(CREATE_EMBEDDING_CACHE_TABLE);
2729
+ }
2730
+ migrateHistoryAllowUpdated(db) {
2731
+ const sql = db.prepare(
2732
+ `SELECT sql FROM sqlite_master WHERE type='table' AND name='memory_history'`
2733
+ ).get();
2734
+ if (sql?.sql?.includes("'updated'")) {
2735
+ return;
2736
+ }
2737
+ db.exec(`
2738
+ CREATE TABLE IF NOT EXISTS memory_history_v3 (
2739
+ id TEXT PRIMARY KEY NOT NULL,
2740
+ memory_id TEXT NOT NULL,
2741
+ event_type TEXT NOT NULL CHECK (event_type IN ('created', 'archived', 'compressed', 'updated')),
2742
+ related_memory_id TEXT NULL,
2743
+ created_at TEXT NOT NULL,
2744
+ FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
2745
+ );
2746
+ INSERT INTO memory_history_v3 (id, memory_id, event_type, related_memory_id, created_at)
2747
+ SELECT id, memory_id, event_type, related_memory_id, created_at FROM memory_history;
2748
+ DROP TABLE memory_history;
2749
+ ALTER TABLE memory_history_v3 RENAME TO memory_history;
2750
+ CREATE INDEX IF NOT EXISTS idx_history_memory_id ON memory_history(memory_id);
2751
+ `);
1895
2752
  }
1896
2753
  /**
1897
2754
  * Rebuild FTS from active (non-archived) memories when counts diverge.
@@ -1923,13 +2780,33 @@ var SqliteStorageProvider = class {
1923
2780
  prepareStatements(db) {
1924
2781
  let insertFts = null;
1925
2782
  let deleteFts = null;
2783
+ let deleteFtsByOrg = null;
2784
+ let deleteFtsByOrgAgent = null;
1926
2785
  let searchFts = null;
1927
2786
  try {
1928
2787
  insertFts = db.prepare(SQL.insertFts);
1929
2788
  deleteFts = db.prepare(SQL.deleteFts);
2789
+ deleteFtsByOrg = db.prepare(SQL.deleteFtsByOrg);
2790
+ deleteFtsByOrgAgent = db.prepare(SQL.deleteFtsByOrgAgent);
1930
2791
  searchFts = db.prepare(SQL.searchFts);
1931
2792
  } catch {
1932
2793
  }
2794
+ let deleteEmbeddingsByOrg = null;
2795
+ let deleteEmbeddingsByOrgAgent = null;
2796
+ let deleteEmbeddingsBlobByOrg = null;
2797
+ let deleteEmbeddingsBlobByOrgAgent = null;
2798
+ try {
2799
+ deleteEmbeddingsByOrg = db.prepare(SQL.deleteEmbeddingsByOrg);
2800
+ deleteEmbeddingsByOrgAgent = db.prepare(SQL.deleteEmbeddingsByOrgAgent);
2801
+ } catch {
2802
+ }
2803
+ try {
2804
+ deleteEmbeddingsBlobByOrg = db.prepare(SQL.deleteEmbeddingsBlobByOrg);
2805
+ deleteEmbeddingsBlobByOrgAgent = db.prepare(
2806
+ SQL.deleteEmbeddingsBlobByOrgAgent
2807
+ );
2808
+ } catch {
2809
+ }
1933
2810
  return {
1934
2811
  getMeta: db.prepare(SQL.getMeta),
1935
2812
  setMeta: db.prepare(SQL.setMeta),
@@ -1937,6 +2814,7 @@ var SqliteStorageProvider = class {
1937
2814
  updateMemoryContent: db.prepare(SQL.updateMemoryContent),
1938
2815
  getMemoryById: db.prepare(SQL.getMemoryById),
1939
2816
  getMemoryByRowid: db.prepare(SQL.getMemoryByRowid),
2817
+ findActiveByContentHash: db.prepare(SQL.findActiveByContentHash),
1940
2818
  insertEmbedding: null,
1941
2819
  deleteEmbedding: null,
1942
2820
  searchVectors: null,
@@ -1949,10 +2827,7 @@ var SqliteStorageProvider = class {
1949
2827
  deleteMemoriesByOrgAgent: db.prepare(SQL.deleteMemoriesByOrgAgent),
1950
2828
  insertHistory: db.prepare(SQL.insertHistory),
1951
2829
  getHistory: db.prepare(SQL.getHistory),
1952
- countMemories: db.prepare(SQL.countMemories),
1953
- countActiveMemories: db.prepare(SQL.countActiveMemories),
1954
- countArchivedMemories: db.prepare(SQL.countArchivedMemories),
1955
- countAgents: db.prepare(SQL.countAgents),
2830
+ getStats: db.prepare(SQL.getStats),
1956
2831
  listRowidsForOrg: db.prepare(SQL.listRowidsForOrg),
1957
2832
  listRowidsForOrgAgent: db.prepare(SQL.listRowidsForOrgAgent),
1958
2833
  vectorTableExists: db.prepare(SQL.vectorTableExists),
@@ -1961,11 +2836,16 @@ var SqliteStorageProvider = class {
1961
2836
  ),
1962
2837
  insertFts,
1963
2838
  deleteFts,
1964
- searchFts
2839
+ deleteFtsByOrg,
2840
+ deleteFtsByOrgAgent,
2841
+ searchFts,
2842
+ deleteEmbeddingsByOrg,
2843
+ deleteEmbeddingsByOrgAgent,
2844
+ deleteEmbeddingsBlobByOrg,
2845
+ deleteEmbeddingsBlobByOrgAgent
1965
2846
  };
1966
2847
  }
1967
- listMemoriesSync(filter) {
1968
- const db = this.requireDb();
2848
+ listMemoriesIndexed(filter, limit) {
1969
2849
  const clauses = [`organization = ?`];
1970
2850
  const params = [filter.organization];
1971
2851
  if (filter.agent) {
@@ -1975,13 +2855,316 @@ var SqliteStorageProvider = class {
1975
2855
  if (!filter.includeArchived) {
1976
2856
  clauses.push(`archived = 0`);
1977
2857
  }
1978
- const sql = `
2858
+ const key = `${filter.agent ? "a" : "_"}:${filter.includeArchived ? "A" : "0"}:${limit !== void 0 ? "L" : "_"}`;
2859
+ let stmt = this.listStatements.get(key);
2860
+ if (!stmt) {
2861
+ let sql = `
2862
+ SELECT rowid, id, organization, agent, content_text, metadata_json,
2863
+ archived, compressed_into, content_hash, created_at, updated_at
2864
+ FROM memories
2865
+ WHERE ${clauses.join(" AND ")}
2866
+ ORDER BY created_at ASC
2867
+ `;
2868
+ if (limit !== void 0) {
2869
+ sql += ` LIMIT ?`;
2870
+ }
2871
+ stmt = this.requireDb().prepare(sql);
2872
+ this.listStatements.set(key, stmt);
2873
+ }
2874
+ if (limit !== void 0) {
2875
+ return stmt.all(...params, limit);
2876
+ }
2877
+ return stmt.all(...params);
2878
+ }
2879
+ /**
2880
+ * Metadata-filtered list: push filters to json_extract when possible;
2881
+ * otherwise page with a hard scan cap (never load unbounded orgs into RAM).
2882
+ */
2883
+ listMemoriesWithMetadata(filter, limit) {
2884
+ const want = limit ?? MAX_METADATA_SCAN;
2885
+ const compiled = filter.metadata ? compileMetadataFilterToSql(filter.metadata) : null;
2886
+ if (compiled) {
2887
+ const clauses2 = [`organization = ?`, `(${compiled.expression})`];
2888
+ const params = [filter.organization, ...compiled.params];
2889
+ if (filter.agent) {
2890
+ clauses2.push(`agent = ?`);
2891
+ params.push(filter.agent);
2892
+ }
2893
+ if (!filter.includeArchived) {
2894
+ clauses2.push(`archived = 0`);
2895
+ }
2896
+ const sql = `
2897
+ SELECT rowid, id, organization, agent, content_text, metadata_json,
2898
+ archived, compressed_into, content_hash, created_at, updated_at
2899
+ FROM memories
2900
+ WHERE ${clauses2.join(" AND ")}
2901
+ ORDER BY created_at ASC
2902
+ LIMIT ?
2903
+ `;
2904
+ params.push(want);
2905
+ return this.requireDb().prepare(sql).all(...params);
2906
+ }
2907
+ const matched = [];
2908
+ let offset = 0;
2909
+ const db = this.requireDb();
2910
+ const clauses = [`organization = ?`];
2911
+ const baseParams = [filter.organization];
2912
+ if (filter.agent) {
2913
+ clauses.push(`agent = ?`);
2914
+ baseParams.push(filter.agent);
2915
+ }
2916
+ if (!filter.includeArchived) {
2917
+ clauses.push(`archived = 0`);
2918
+ }
2919
+ const pageSql = `
1979
2920
  SELECT rowid, id, organization, agent, content_text, metadata_json,
1980
- archived, compressed_into, created_at, updated_at
2921
+ archived, compressed_into, content_hash, created_at, updated_at
1981
2922
  FROM memories
1982
2923
  WHERE ${clauses.join(" AND ")}
2924
+ ORDER BY created_at ASC
2925
+ LIMIT ? OFFSET ?
1983
2926
  `;
1984
- return db.prepare(sql).all(...params);
2927
+ const pageStmt = db.prepare(pageSql);
2928
+ while (matched.length < want && offset < MAX_METADATA_SCAN) {
2929
+ const pageLimit = Math.min(METADATA_PAGE_SIZE, MAX_METADATA_SCAN - offset);
2930
+ const page = pageStmt.all(
2931
+ ...baseParams,
2932
+ pageLimit,
2933
+ offset
2934
+ );
2935
+ if (page.length === 0) {
2936
+ break;
2937
+ }
2938
+ for (const row of page) {
2939
+ if (matchesMetadata(
2940
+ deserializeMetadata(row.metadata_json),
2941
+ filter.metadata
2942
+ )) {
2943
+ matched.push(row);
2944
+ if (matched.length >= want) {
2945
+ return matched;
2946
+ }
2947
+ }
2948
+ }
2949
+ offset += page.length;
2950
+ if (page.length < pageLimit) {
2951
+ break;
2952
+ }
2953
+ }
2954
+ return matched;
2955
+ }
2956
+ getRowidInStatement(count) {
2957
+ let stmt = this.rowidInStatements.get(count);
2958
+ if (!stmt) {
2959
+ const placeholders = Array.from({ length: count }, () => "?").join(",");
2960
+ const sql = `${SQL.getMemoriesByRowidsPrefix}${placeholders})`;
2961
+ stmt = this.requireDb().prepare(sql);
2962
+ this.rowidInStatements.set(count, stmt);
2963
+ }
2964
+ return stmt;
2965
+ }
2966
+ insertMemoryChunk(inputs) {
2967
+ const stmts = this.requireStatements();
2968
+ const n = inputs.length;
2969
+ if (n === 1) {
2970
+ const input = inputs[0];
2971
+ const contentHash = input.contentHash !== void 0 ? input.contentHash : hashMemoryContent(input.contentText);
2972
+ const row = stmts.insertMemory.get(
2973
+ input.id,
2974
+ input.organization,
2975
+ input.agent,
2976
+ input.contentText,
2977
+ serializeMetadata(input.metadata),
2978
+ contentHash,
2979
+ input.createdAt,
2980
+ input.updatedAt
2981
+ );
2982
+ if (!row || row.rowid === void 0) {
2983
+ throw new DatabaseError("Failed to read memory after batch insert");
2984
+ }
2985
+ this.insertEmbedding(row.rowid, input.embedding);
2986
+ this.insertFtsRow(
2987
+ input.id,
2988
+ input.organization,
2989
+ input.agent,
2990
+ input.contentText
2991
+ );
2992
+ stmts.insertHistory.run(
2993
+ crypto.randomUUID(),
2994
+ input.id,
2995
+ "created",
2996
+ null,
2997
+ input.createdAt
2998
+ );
2999
+ return [row];
3000
+ }
3001
+ let stmt = this.batchInsertStatements.get(n);
3002
+ if (!stmt) {
3003
+ const valueRow = "(?, ?, ?, ?, ?, 0, NULL, ?, ?, ?)";
3004
+ const values = Array.from({ length: n }, () => valueRow).join(", ");
3005
+ const sql = `
3006
+ INSERT INTO memories (
3007
+ id, organization, agent, content_text, metadata_json,
3008
+ archived, compressed_into, content_hash, created_at, updated_at
3009
+ ) VALUES ${values}
3010
+ RETURNING rowid, id, organization, agent, content_text, metadata_json,
3011
+ archived, compressed_into, content_hash, created_at, updated_at
3012
+ `;
3013
+ stmt = this.requireDb().prepare(sql);
3014
+ this.batchInsertStatements.set(n, stmt);
3015
+ }
3016
+ const binds = [];
3017
+ for (const input of inputs) {
3018
+ const contentHash = input.contentHash !== void 0 ? input.contentHash : hashMemoryContent(input.contentText);
3019
+ binds.push(
3020
+ input.id,
3021
+ input.organization,
3022
+ input.agent,
3023
+ input.contentText,
3024
+ serializeMetadata(input.metadata),
3025
+ contentHash,
3026
+ input.createdAt,
3027
+ input.updatedAt
3028
+ );
3029
+ }
3030
+ const rows = stmt.all(...binds);
3031
+ if (rows.length !== n) {
3032
+ throw new DatabaseError("Failed to read memories after batch insert");
3033
+ }
3034
+ for (const row of rows) {
3035
+ if (row.rowid === void 0) {
3036
+ throw new DatabaseError("Failed to read memory rowid after batch insert");
3037
+ }
3038
+ }
3039
+ this.insertEmbeddingsBatch(rows, inputs);
3040
+ this.insertFtsBatch(inputs);
3041
+ this.insertHistoryBatch(inputs);
3042
+ return rows;
3043
+ }
3044
+ insertEmbeddingsBatch(rows, inputs) {
3045
+ const n = rows.length;
3046
+ if (this.vectorBackend === "sqlite-vec") {
3047
+ for (let i = 0; i < n; i += 1) {
3048
+ this.insertEmbedding(rows[i].rowid, inputs[i].embedding);
3049
+ }
3050
+ return;
3051
+ }
3052
+ let stmt = this.batchBlobEmbStatements.get(n);
3053
+ if (!stmt) {
3054
+ const values = Array.from({ length: n }, () => "(?, ?)").join(", ");
3055
+ stmt = this.requireDb().prepare(
3056
+ `INSERT INTO memory_embeddings_blob (memory_rowid, embedding) VALUES ${values}`
3057
+ );
3058
+ this.batchBlobEmbStatements.set(n, stmt);
3059
+ }
3060
+ const binds = [];
3061
+ for (let i = 0; i < n; i += 1) {
3062
+ binds.push(rows[i].rowid, embeddingToBuffer(inputs[i].embedding));
3063
+ }
3064
+ stmt.run(...binds);
3065
+ if (this.memoryIndex && this.vectorDimensions !== null) {
3066
+ for (let i = 0; i < n; i += 1) {
3067
+ this.memoryIndex.upsert(rows[i].rowid, inputs[i].embedding);
3068
+ }
3069
+ this.memoryIndexDirty = false;
3070
+ } else {
3071
+ this.memoryIndexDirty = true;
3072
+ }
3073
+ }
3074
+ insertFtsBatch(inputs) {
3075
+ const stmts = this.requireStatements();
3076
+ if (!stmts.insertFts) {
3077
+ return;
3078
+ }
3079
+ const n = inputs.length;
3080
+ let stmt = this.batchFtsStatements.get(n);
3081
+ if (!stmt) {
3082
+ const values = Array.from({ length: n }, () => "(?, ?, ?, ?)").join(", ");
3083
+ stmt = this.requireDb().prepare(
3084
+ `INSERT INTO memories_fts (content_text, memory_id, organization, agent) VALUES ${values}`
3085
+ );
3086
+ this.batchFtsStatements.set(n, stmt);
3087
+ }
3088
+ const binds = [];
3089
+ for (const input of inputs) {
3090
+ binds.push(
3091
+ input.contentText,
3092
+ input.id,
3093
+ input.organization,
3094
+ input.agent
3095
+ );
3096
+ }
3097
+ try {
3098
+ stmt.run(...binds);
3099
+ } catch {
3100
+ for (const input of inputs) {
3101
+ this.insertFtsRow(
3102
+ input.id,
3103
+ input.organization,
3104
+ input.agent,
3105
+ input.contentText
3106
+ );
3107
+ }
3108
+ }
3109
+ }
3110
+ insertHistoryBatch(inputs) {
3111
+ const n = inputs.length;
3112
+ let stmt = this.batchHistoryStatements.get(n);
3113
+ if (!stmt) {
3114
+ const values = Array.from({ length: n }, () => "(?, ?, ?, ?, ?)").join(
3115
+ ", "
3116
+ );
3117
+ stmt = this.requireDb().prepare(
3118
+ `INSERT INTO memory_history (id, memory_id, event_type, related_memory_id, created_at)
3119
+ VALUES ${values}`
3120
+ );
3121
+ this.batchHistoryStatements.set(n, stmt);
3122
+ }
3123
+ const binds = [];
3124
+ for (const input of inputs) {
3125
+ binds.push(
3126
+ crypto.randomUUID(),
3127
+ input.id,
3128
+ "created",
3129
+ null,
3130
+ input.createdAt
3131
+ );
3132
+ }
3133
+ stmt.run(...binds);
3134
+ }
3135
+ deleteEmbeddingsForScope(organization, agent) {
3136
+ const stmts = this.requireStatements();
3137
+ try {
3138
+ if (this.vectorBackend === "sqlite-vec") {
3139
+ if (agent) {
3140
+ stmts.deleteEmbeddingsByOrgAgent?.run(organization, agent);
3141
+ } else {
3142
+ stmts.deleteEmbeddingsByOrg?.run(organization);
3143
+ }
3144
+ } else {
3145
+ if (agent) {
3146
+ stmts.deleteEmbeddingsBlobByOrgAgent?.run(organization, agent);
3147
+ } else {
3148
+ stmts.deleteEmbeddingsBlobByOrg?.run(organization);
3149
+ }
3150
+ this.memoryIndexDirty = true;
3151
+ if (!agent) {
3152
+ this.memoryIndex?.clear();
3153
+ }
3154
+ }
3155
+ } catch {
3156
+ }
3157
+ }
3158
+ deleteFtsForScope(organization, agent) {
3159
+ const stmts = this.requireStatements();
3160
+ try {
3161
+ if (agent) {
3162
+ stmts.deleteFtsByOrgAgent?.run(organization, agent);
3163
+ } else {
3164
+ stmts.deleteFtsByOrg?.run(organization);
3165
+ }
3166
+ } catch {
3167
+ }
1985
3168
  }
1986
3169
  /** Insert-only FTS row (new memory IDs never collide). */
1987
3170
  insertFtsRow(memoryId, organization, agent, contentText) {
@@ -2065,6 +3248,13 @@ var SqliteStorageProvider = class {
2065
3248
  stmts.insertEmbeddingBlob = null;
2066
3249
  stmts.deleteEmbeddingBlob = null;
2067
3250
  stmts.listEmbeddingsBlob = null;
3251
+ try {
3252
+ stmts.deleteEmbeddingsByOrg = db.prepare(SQL.deleteEmbeddingsByOrg);
3253
+ stmts.deleteEmbeddingsByOrgAgent = db.prepare(
3254
+ SQL.deleteEmbeddingsByOrgAgent
3255
+ );
3256
+ } catch {
3257
+ }
2068
3258
  return;
2069
3259
  }
2070
3260
  stmts.insertEmbeddingBlob = db.prepare(SQL.insertEmbeddingBlob);
@@ -2073,6 +3263,13 @@ var SqliteStorageProvider = class {
2073
3263
  stmts.insertEmbedding = null;
2074
3264
  stmts.deleteEmbedding = null;
2075
3265
  stmts.searchVectors = null;
3266
+ try {
3267
+ stmts.deleteEmbeddingsBlobByOrg = db.prepare(SQL.deleteEmbeddingsBlobByOrg);
3268
+ stmts.deleteEmbeddingsBlobByOrgAgent = db.prepare(
3269
+ SQL.deleteEmbeddingsBlobByOrgAgent
3270
+ );
3271
+ } catch {
3272
+ }
2076
3273
  }
2077
3274
  insertEmbedding(rowid, embedding) {
2078
3275
  const stmts = this.requireStatements();
@@ -2081,7 +3278,12 @@ var SqliteStorageProvider = class {
2081
3278
  return;
2082
3279
  }
2083
3280
  stmts.insertEmbeddingBlob.run(rowid, embeddingToBuffer(embedding));
2084
- this.memoryIndexDirty = true;
3281
+ if (this.memoryIndex) {
3282
+ this.memoryIndex.upsert(rowid, embedding);
3283
+ this.memoryIndexDirty = false;
3284
+ } else {
3285
+ this.memoryIndexDirty = true;
3286
+ }
2085
3287
  }
2086
3288
  deleteEmbedding(rowid) {
2087
3289
  const stmts = this.requireStatements();
@@ -2152,58 +3354,418 @@ var SqliteStorageProvider = class {
2152
3354
  if (!row) {
2153
3355
  return null;
2154
3356
  }
2155
- const parsed = Number(row.value);
2156
- return Number.isFinite(parsed) ? parsed : null;
3357
+ const parsed = Number(row.value);
3358
+ return Number.isFinite(parsed) ? parsed : null;
3359
+ }
3360
+ resolvePath(connectionString) {
3361
+ if (connectionString === ":memory:") {
3362
+ return ":memory:";
3363
+ }
3364
+ return path5__default.default.isAbsolute(connectionString) ? connectionString : path5__default.default.resolve(process.cwd(), connectionString);
3365
+ }
3366
+ requireDb() {
3367
+ if (!this.db) {
3368
+ throw new DatabaseError("Database is not open. Call init() first.");
3369
+ }
3370
+ return this.db;
3371
+ }
3372
+ requireStatements() {
3373
+ if (!this.statements) {
3374
+ throw new DatabaseError("Database statements are not prepared. Call init() first.");
3375
+ }
3376
+ return this.statements;
3377
+ }
3378
+ requireVectorReady() {
3379
+ if (!this.vectorBackend || this.vectorDimensions === null) {
3380
+ throw new DatabaseError(
3381
+ "Vector index is not ready. Embedding dimensions have not been initialized."
3382
+ );
3383
+ }
3384
+ }
3385
+ toVectorParam(embedding) {
3386
+ const buffer = embeddingToBuffer(embedding);
3387
+ return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
3388
+ }
3389
+ describe(error) {
3390
+ if (error instanceof Error) {
3391
+ return error.message;
3392
+ }
3393
+ return String(error);
3394
+ }
3395
+ };
3396
+ var SqliteDatabaseProvider = SqliteStorageProvider;
3397
+ function sanitizeFtsQuery(query) {
3398
+ const terms = query.split(/\s+/).map((t) => t.replace(/["']/g, "").replace(/[^\p{L}\p{N}_-]/gu, "")).filter((t) => t.length > 0);
3399
+ if (terms.length === 0) {
3400
+ return "";
3401
+ }
3402
+ return terms.map((t) => `"${t}"`).join(" OR ");
3403
+ }
3404
+
3405
+ // src/filters/sql-compile-postgres.ts
3406
+ var FIELD_RE2 = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
3407
+ function jsonbTextExtract(field) {
3408
+ if (!FIELD_RE2.test(field)) {
3409
+ return null;
3410
+ }
3411
+ const parts = field.split(".");
3412
+ if (parts.length === 1) {
3413
+ return `metadata_json->>'${parts[0]}'`;
3414
+ }
3415
+ const path7 = parts.join(",");
3416
+ return `metadata_json #>> '{${path7}}'`;
3417
+ }
3418
+ function jsonbContainment(field, value, paramIndex) {
3419
+ if (!FIELD_RE2.test(field)) {
3420
+ return null;
3421
+ }
3422
+ const parts = field.split(".");
3423
+ let obj = value;
3424
+ for (let i = parts.length - 1; i >= 0; i -= 1) {
3425
+ obj = { [parts[i]]: obj };
3426
+ }
3427
+ return {
3428
+ expression: `metadata_json @> $${paramIndex}::jsonb`,
3429
+ params: [JSON.stringify(obj)]
3430
+ };
3431
+ }
3432
+ function compileComparison2(field, op, startIndex) {
3433
+ if ("eq" in op) {
3434
+ const value = op.eq;
3435
+ if (value !== null && typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
3436
+ return null;
3437
+ }
3438
+ return jsonbContainment(field, value, startIndex);
3439
+ }
3440
+ const extract = jsonbTextExtract(field);
3441
+ if (!extract) {
3442
+ return null;
3443
+ }
3444
+ if ("contains" in op) {
3445
+ return {
3446
+ expression: `${extract} LIKE '%' || $${startIndex} || '%'`,
3447
+ params: [op.contains]
3448
+ };
3449
+ }
3450
+ if ("gt" in op) {
3451
+ return {
3452
+ expression: `(${extract})::numeric > $${startIndex}`,
3453
+ params: [op.gt]
3454
+ };
3455
+ }
3456
+ if ("gte" in op) {
3457
+ return {
3458
+ expression: `(${extract})::numeric >= $${startIndex}`,
3459
+ params: [op.gte]
3460
+ };
3461
+ }
3462
+ if ("lt" in op) {
3463
+ return {
3464
+ expression: `(${extract})::numeric < $${startIndex}`,
3465
+ params: [op.lt]
3466
+ };
3467
+ }
3468
+ if ("lte" in op) {
3469
+ return {
3470
+ expression: `(${extract})::numeric <= $${startIndex}`,
3471
+ params: [op.lte]
3472
+ };
3473
+ }
3474
+ if ("between" in op) {
3475
+ const [lo, hi] = op.between;
3476
+ return {
3477
+ expression: `(${extract})::numeric >= $${startIndex} AND (${extract})::numeric <= $${startIndex + 1}`,
3478
+ params: [lo, hi]
3479
+ };
3480
+ }
3481
+ return null;
3482
+ }
3483
+ function compileInner(filter, startIndex) {
3484
+ if ("and" in filter) {
3485
+ if (filter.and.length === 0) {
3486
+ return { expression: "TRUE", params: [] };
3487
+ }
3488
+ const parts = [];
3489
+ let idx = startIndex;
3490
+ for (const child of filter.and) {
3491
+ const compiled = compileInner(child, idx);
3492
+ if (!compiled) {
3493
+ return null;
3494
+ }
3495
+ parts.push(compiled);
3496
+ idx += compiled.params.length;
3497
+ }
3498
+ return {
3499
+ expression: parts.map((p) => `(${p.expression})`).join(" AND "),
3500
+ params: parts.flatMap((p) => p.params)
3501
+ };
3502
+ }
3503
+ if ("or" in filter) {
3504
+ if (filter.or.length === 0) {
3505
+ return { expression: "FALSE", params: [] };
3506
+ }
3507
+ const parts = [];
3508
+ let idx = startIndex;
3509
+ for (const child of filter.or) {
3510
+ const compiled = compileInner(child, idx);
3511
+ if (!compiled) {
3512
+ return null;
3513
+ }
3514
+ parts.push(compiled);
3515
+ idx += compiled.params.length;
3516
+ }
3517
+ return {
3518
+ expression: parts.map((p) => `(${p.expression})`).join(" OR "),
3519
+ params: parts.flatMap((p) => p.params)
3520
+ };
3521
+ }
3522
+ if ("not" in filter) {
3523
+ const inner = compileInner(filter.not, startIndex);
3524
+ if (!inner) {
3525
+ return null;
3526
+ }
3527
+ return {
3528
+ expression: `NOT (${inner.expression})`,
3529
+ params: inner.params
3530
+ };
3531
+ }
3532
+ return compileComparison2(filter.field, filter.op, startIndex);
3533
+ }
3534
+ function compileMetadataFilterToPostgres(filter, startIndex = 1) {
3535
+ return compileInner(filter, startIndex);
3536
+ }
3537
+
3538
+ // src/subscribe/postgres-listener.ts
3539
+ var WOLBARG_NOTIFY_CHANNEL = "wolbarg_events";
3540
+ function matchesFilter(filter, event) {
3541
+ if (filter.organization !== event.organization) {
3542
+ return false;
3543
+ }
3544
+ if (filter.agent !== void 0 && filter.agent !== event.agent) {
3545
+ return false;
3546
+ }
3547
+ if (filter.event === void 0) {
3548
+ return true;
3549
+ }
3550
+ const events = Array.isArray(filter.event) ? filter.event : [filter.event];
3551
+ return events.some(
3552
+ (e) => e === "*" || e === event.event
3553
+ );
3554
+ }
3555
+ function serializeNotifyPayload(event) {
3556
+ const payload = JSON.stringify({
3557
+ event: event.event,
3558
+ organization: event.organization,
3559
+ agent: event.agent,
3560
+ memoryId: event.memoryId,
3561
+ timestamp: event.timestamp,
3562
+ traceId: event.traceId,
3563
+ sessionId: event.sessionId,
3564
+ upsertAction: event.upsertAction
3565
+ });
3566
+ if (payload.length > 7900) {
3567
+ return JSON.stringify({
3568
+ event: event.event,
3569
+ organization: event.organization,
3570
+ agent: event.agent,
3571
+ memoryId: Array.isArray(event.memoryId) ? event.memoryId.slice(0, 20) : event.memoryId,
3572
+ timestamp: event.timestamp
3573
+ });
3574
+ }
3575
+ return payload;
3576
+ }
3577
+ function parseNotifyPayload(raw) {
3578
+ try {
3579
+ const parsed = JSON.parse(raw);
3580
+ if (!parsed || typeof parsed.event !== "string" || typeof parsed.organization !== "string" || typeof parsed.agent !== "string" || typeof parsed.timestamp !== "string") {
3581
+ return null;
3582
+ }
3583
+ return parsed;
3584
+ } catch {
3585
+ return null;
3586
+ }
3587
+ }
3588
+ var PostgresSubscribeListener = class {
3589
+ subscriptions = /* @__PURE__ */ new Map();
3590
+ nextId = 1;
3591
+ client = null;
3592
+ closed = false;
3593
+ reconnecting = false;
3594
+ listenInFlight = null;
3595
+ reconnectDelayMs;
3596
+ connect;
3597
+ onError;
3598
+ constructor(options) {
3599
+ this.connect = options.connect;
3600
+ this.reconnectDelayMs = options.reconnectDelayMs ?? 1e3;
3601
+ this.onError = options.onError;
3602
+ }
3603
+ async start() {
3604
+ if (this.closed) {
3605
+ return;
3606
+ }
3607
+ await this.ensureListening();
3608
+ }
3609
+ async ensureListening() {
3610
+ if (this.client || this.closed) {
3611
+ return;
3612
+ }
3613
+ if (this.listenInFlight) {
3614
+ await this.listenInFlight;
3615
+ return;
3616
+ }
3617
+ this.listenInFlight = this.openListenConnection();
3618
+ try {
3619
+ await this.listenInFlight;
3620
+ } finally {
3621
+ this.listenInFlight = null;
3622
+ }
3623
+ }
3624
+ async openListenConnection() {
3625
+ if (this.client || this.closed) {
3626
+ return;
3627
+ }
3628
+ const client = await this.connect();
3629
+ if (this.closed) {
3630
+ try {
3631
+ client.release(true);
3632
+ } catch {
3633
+ }
3634
+ return;
3635
+ }
3636
+ this.client = client;
3637
+ client.on("notification", (msg) => {
3638
+ if (msg.channel !== WOLBARG_NOTIFY_CHANNEL || !msg.payload) {
3639
+ return;
3640
+ }
3641
+ const event = parseNotifyPayload(msg.payload);
3642
+ if (!event) {
3643
+ return;
3644
+ }
3645
+ this.dispatch(event);
3646
+ });
3647
+ client.on("error", (error) => {
3648
+ this.onError?.(error);
3649
+ void this.handleDisconnect();
3650
+ });
3651
+ client.on("end", () => {
3652
+ void this.handleDisconnect();
3653
+ });
3654
+ await client.query(`LISTEN ${WOLBARG_NOTIFY_CHANNEL}`);
2157
3655
  }
2158
- resolvePath(connectionString) {
2159
- if (connectionString === ":memory:") {
2160
- return ":memory:";
3656
+ async handleDisconnect() {
3657
+ if (this.closed || this.reconnecting) {
3658
+ return;
2161
3659
  }
2162
- return path5__default.default.isAbsolute(connectionString) ? connectionString : path5__default.default.resolve(process.cwd(), connectionString);
2163
- }
2164
- requireDb() {
2165
- if (!this.db) {
2166
- throw new DatabaseError("Database is not open. Call init() first.");
3660
+ this.reconnecting = true;
3661
+ const prev = this.client;
3662
+ this.client = null;
3663
+ if (prev) {
3664
+ try {
3665
+ prev.release(true);
3666
+ } catch {
3667
+ }
2167
3668
  }
2168
- return this.db;
3669
+ try {
3670
+ await new Promise((r) => setTimeout(r, this.reconnectDelayMs));
3671
+ if (!this.closed) {
3672
+ await this.ensureListening();
3673
+ }
3674
+ } catch (error) {
3675
+ this.onError?.(error);
3676
+ this.reconnecting = false;
3677
+ if (!this.closed) {
3678
+ void this.handleDisconnect();
3679
+ }
3680
+ return;
3681
+ }
3682
+ this.reconnecting = false;
2169
3683
  }
2170
- requireStatements() {
2171
- if (!this.statements) {
2172
- throw new DatabaseError("Database statements are not prepared. Call init() first.");
3684
+ dispatch(event) {
3685
+ for (const sub of this.subscriptions.values()) {
3686
+ if (!matchesFilter(sub.filter, event)) {
3687
+ continue;
3688
+ }
3689
+ try {
3690
+ sub.callback(event);
3691
+ } catch (error) {
3692
+ console.error(
3693
+ "[wolbarg] subscribe callback error:",
3694
+ error instanceof Error ? error.message : error
3695
+ );
3696
+ }
2173
3697
  }
2174
- return this.statements;
2175
3698
  }
2176
- requireVectorReady() {
2177
- if (!this.vectorBackend || this.vectorDimensions === null) {
2178
- throw new DatabaseError(
2179
- "Vector index is not ready. Embedding dimensions have not been initialized."
2180
- );
3699
+ subscribe(filter, callback) {
3700
+ if (this.closed) {
3701
+ throw new Error("Subscribe backend is closed");
2181
3702
  }
3703
+ const id = this.nextId++;
3704
+ this.subscriptions.set(id, { filter, callback });
3705
+ void this.ensureListening();
3706
+ return () => {
3707
+ this.subscriptions.delete(id);
3708
+ };
2182
3709
  }
2183
- toVectorParam(embedding) {
2184
- const buffer = embeddingToBuffer(embedding);
2185
- return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
3710
+ /** True when at least one subscribe() callback is registered. */
3711
+ hasSubscribers() {
3712
+ return this.subscriptions.size > 0;
2186
3713
  }
2187
- describe(error) {
2188
- if (error instanceof Error) {
2189
- return error.message;
3714
+ /**
3715
+ * Local emit is a no-op for Postgres — writers use NOTIFY in-transaction.
3716
+ * Kept so the shared SubscribeBackend interface is uniform.
3717
+ */
3718
+ emit(_event) {
3719
+ }
3720
+ async close() {
3721
+ this.closed = true;
3722
+ this.subscriptions.clear();
3723
+ const pending = this.listenInFlight;
3724
+ if (pending) {
3725
+ await pending.catch(() => void 0);
3726
+ }
3727
+ const client = this.client;
3728
+ this.client = null;
3729
+ if (!client) {
3730
+ return;
3731
+ }
3732
+ try {
3733
+ client.release(true);
3734
+ } catch {
3735
+ try {
3736
+ client.end?.();
3737
+ } catch {
3738
+ }
2190
3739
  }
2191
- return String(error);
2192
3740
  }
2193
3741
  };
2194
- var SqliteDatabaseProvider = SqliteStorageProvider;
2195
- function sanitizeFtsQuery(query) {
2196
- const terms = query.split(/\s+/).map((t) => t.replace(/["']/g, "").replace(/[^\p{L}\p{N}_-]/gu, "")).filter((t) => t.length > 0);
2197
- if (terms.length === 0) {
2198
- return "";
2199
- }
2200
- return terms.map((t) => `"${t}"`).join(" OR ");
3742
+ async function notifyMemoryChange(client, event) {
3743
+ const payload = serializeNotifyPayload(event);
3744
+ await client.query(`SELECT pg_notify($1, $2)`, [
3745
+ WOLBARG_NOTIFY_CHANNEL,
3746
+ payload
3747
+ ]);
2201
3748
  }
3749
+ function createPostgresListenerFromPool(pool, onError) {
3750
+ return new PostgresSubscribeListener({
3751
+ connect: () => pool.connect(),
3752
+ onError
3753
+ });
3754
+ }
3755
+
3756
+ // src/storage/providers/postgres.ts
2202
3757
  var txStore = new async_hooks.AsyncLocalStorage();
2203
3758
  var STMT = {
2204
- insertOne: "Wolbarg_insert_one_v1",
2205
- insertBatch: "Wolbarg_insert_batch_v1"
3759
+ insertOne: "Wolbarg_insert_one_v5",
3760
+ insertBatch: "Wolbarg_insert_batch_v5"
2206
3761
  };
3762
+ function toFloat4Param(embedding) {
3763
+ const out = new Array(embedding.length);
3764
+ for (let i = 0; i < embedding.length; i += 1) {
3765
+ out[i] = embedding[i];
3766
+ }
3767
+ return out;
3768
+ }
2207
3769
  function toVectorLiteral(embedding) {
2208
3770
  const n = embedding.length;
2209
3771
  let s = "[";
@@ -2213,17 +3775,45 @@ function toVectorLiteral(embedding) {
2213
3775
  }
2214
3776
  return s + "]";
2215
3777
  }
3778
+ function appendConnectionOption(connectionString, key, value) {
3779
+ try {
3780
+ const url = new URL(connectionString);
3781
+ if (key === "options") {
3782
+ const existing = url.searchParams.get("options");
3783
+ url.searchParams.set(
3784
+ "options",
3785
+ existing ? `${existing} ${value}`.trim() : value
3786
+ );
3787
+ }
3788
+ return url.toString();
3789
+ } catch {
3790
+ const sep = connectionString.includes("?") ? "&" : "?";
3791
+ return `${connectionString}${sep}${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
3792
+ }
3793
+ }
3794
+ function withPoolStartupOptions(connectionString, durableWrites) {
3795
+ const flags = [
3796
+ "-c jit=off",
3797
+ // Bake HNSW search GUCs into the connection — avoids per-recall set_config.
3798
+ "-c hnsw.ef_search=40",
3799
+ "-c hnsw.iterative_scan=relaxed_order"
3800
+ ];
3801
+ if (!durableWrites) {
3802
+ flags.unshift("-c synchronous_commit=off");
3803
+ }
3804
+ return appendConnectionOption(connectionString, "options", flags.join(" "));
3805
+ }
2216
3806
  var INSERT_ONE_SQL = `WITH mem AS (
2217
3807
  INSERT INTO memories (
2218
3808
  id, organization, agent, content_text, metadata_json,
2219
- archived, compressed_into, created_at, updated_at
2220
- ) VALUES ($1,$2,$3,$4,$5::jsonb,false,NULL,$6,$7)
3809
+ archived, compressed_into, content_hash, created_at, updated_at
3810
+ ) VALUES ($1,$2,$3,$4,$5::jsonb,false,NULL,$9,$6,$7)
2221
3811
  RETURNING id, organization, agent, content_text, metadata_json,
2222
- archived::int AS archived, compressed_into, created_at, updated_at
3812
+ archived::int AS archived, compressed_into, content_hash, created_at, updated_at
2223
3813
  ),
2224
3814
  hist AS (
2225
3815
  INSERT INTO memory_history (id, memory_id, event_type, related_memory_id, created_at)
2226
- SELECT $8, id, 'created', NULL, $6 FROM mem
3816
+ SELECT gen_random_uuid()::text, id, 'created', NULL, $6 FROM mem
2227
3817
  ),
2228
3818
  mapped AS (
2229
3819
  INSERT INTO memory_row_map (memory_id)
@@ -2231,9 +3821,8 @@ var INSERT_ONE_SQL = `WITH mem AS (
2231
3821
  ON CONFLICT (memory_id) DO NOTHING
2232
3822
  ),
2233
3823
  emb AS (
2234
- INSERT INTO memory_embeddings (memory_id, embedding)
2235
- SELECT id, $9::vector FROM mem
2236
- ON CONFLICT (memory_id) DO UPDATE SET embedding = EXCLUDED.embedding
3824
+ INSERT INTO memory_embeddings (memory_id, embedding, organization, agent, archived)
3825
+ SELECT id, $8::float4[]::vector, $2, $3, false FROM mem
2237
3826
  )
2238
3827
  SELECT * FROM mem`;
2239
3828
  var INSERT_BATCH_SQL = `WITH mem AS (
@@ -2246,13 +3835,12 @@ var INSERT_BATCH_SQL = `WITH mem AS (
2246
3835
  $1::text[], $2::text[], $3::text[], $4::text[],
2247
3836
  $5::text[], $6::timestamptz[], $7::timestamptz[]
2248
3837
  ) AS t(id, org, agent, txt, meta, c, u)
2249
- RETURNING id, organization, agent, content_text, metadata_json,
2250
- archived::int AS archived, compressed_into, created_at, updated_at
3838
+ RETURNING id
2251
3839
  ),
2252
3840
  hist AS (
2253
3841
  INSERT INTO memory_history (id, memory_id, event_type, related_memory_id, created_at)
2254
- SELECT h, m, 'created', NULL, c
2255
- FROM unnest($8::text[], $1::text[], $6::timestamptz[]) AS t(h, m, c)
3842
+ SELECT gen_random_uuid()::text, id, 'created', NULL, c
3843
+ FROM unnest($1::text[], $6::timestamptz[]) AS t(id, c)
2256
3844
  ),
2257
3845
  mapped AS (
2258
3846
  INSERT INTO memory_row_map (memory_id)
@@ -2260,31 +3848,42 @@ var INSERT_BATCH_SQL = `WITH mem AS (
2260
3848
  ON CONFLICT (memory_id) DO NOTHING
2261
3849
  ),
2262
3850
  emb AS (
2263
- INSERT INTO memory_embeddings (memory_id, embedding)
2264
- SELECT id, emb::vector
2265
- FROM unnest($1::text[], $9::text[]) AS t(id, emb)
2266
- ON CONFLICT (memory_id) DO UPDATE SET embedding = EXCLUDED.embedding
3851
+ INSERT INTO memory_embeddings (memory_id, embedding, organization, agent, archived)
3852
+ SELECT id, emb::vector, org, agent, false
3853
+ FROM unnest($1::text[], $8::text[], $2::text[], $3::text[]) AS t(id, emb, org, agent)
2267
3854
  )
2268
- SELECT * FROM mem`;
2269
- var COPY_BATCH_THRESHOLD = 64;
2270
- var PostgresStorageProvider = class {
3855
+ SELECT id FROM mem`;
3856
+ var COALESCE_FLUSH_MAX = 128;
3857
+ var COALESCE_FLUSH_THRESHOLD = 48;
3858
+ var COALESCE_MAX_PARALLEL = 24;
3859
+ var BULK_CHUNK_NO_HNSW = 500;
3860
+ var BULK_CHUNK_WITH_HNSW = 250;
3861
+ var COPY_BATCH_THRESHOLD = 48;
3862
+ var PostgresStorageProvider = class _PostgresStorageProvider {
2271
3863
  name = "postgres";
2272
3864
  connectionString;
2273
3865
  maxPoolSize;
3866
+ durableWrites;
2274
3867
  pool = null;
2275
3868
  vectorDimensions = null;
2276
3869
  hasPgvector = false;
2277
3870
  hnswIndexEnsured = false;
2278
3871
  hnswCreateFailures = 0;
3872
+ /** Dedup concurrent CREATE INDEX so mixed read/write storms don't pile up. */
3873
+ hnswBuildInFlight = null;
2279
3874
  hasContentTsv = false;
2280
- iterativeScanEnabled = false;
2281
3875
  /** Coalesce concurrent insertMemory callers into one unnest batch. */
2282
3876
  insertQueue = [];
2283
3877
  insertFlushScheduled = false;
2284
- insertFlushInFlight = false;
3878
+ insertFlushTimer = null;
3879
+ insertFlushInFlight = 0;
2285
3880
  constructor(options) {
2286
- this.connectionString = options.connectionString;
2287
- this.maxPoolSize = options.maxPoolSize ?? 32;
3881
+ this.maxPoolSize = options.maxPoolSize ?? 64;
3882
+ this.durableWrites = options.durableWrites !== false;
3883
+ this.connectionString = withPoolStartupOptions(
3884
+ options.connectionString,
3885
+ this.durableWrites
3886
+ );
2288
3887
  }
2289
3888
  getPoolStats() {
2290
3889
  const pool = this.pool;
@@ -2295,6 +3894,10 @@ var PostgresStorageProvider = class {
2295
3894
  waiting: pool?.waitingCount ?? 0
2296
3895
  };
2297
3896
  }
3897
+ /** Dedicated pool accessor for LISTEN/NOTIFY subscribe backend. */
3898
+ getPool() {
3899
+ return this.pool;
3900
+ }
2298
3901
  async open() {
2299
3902
  let PoolCtor;
2300
3903
  try {
@@ -2310,6 +3913,7 @@ var PostgresStorageProvider = class {
2310
3913
  connectionString: this.connectionString,
2311
3914
  max: this.maxPoolSize,
2312
3915
  idleTimeoutMillis: 3e4,
3916
+ connectionTimeoutMillis: 1e4,
2313
3917
  allowExitOnIdle: true,
2314
3918
  keepAlive: true
2315
3919
  });
@@ -2318,7 +3922,17 @@ var PostgresStorageProvider = class {
2318
3922
  const dims = await this.getEmbeddingDimensions();
2319
3923
  if (dims !== null) {
2320
3924
  this.vectorDimensions = dims;
2321
- await this.ensureVectorTables(dims);
3925
+ const schemaKey = `${this.connectionString}::${dims}`;
3926
+ if (!_PostgresStorageProvider.vectorSchemaReady.has(schemaKey)) {
3927
+ await this.ensureVectorTables(dims);
3928
+ _PostgresStorageProvider.vectorSchemaReady.add(schemaKey);
3929
+ } else {
3930
+ this.hasPgvector = true;
3931
+ const idx = await this.query(
3932
+ `SELECT 1 FROM pg_indexes WHERE indexname = 'idx_memory_embeddings_hnsw' LIMIT 1`
3933
+ ).catch(() => ({ rows: [] }));
3934
+ this.hnswIndexEnsured = idx.rows.length > 0;
3935
+ }
2322
3936
  }
2323
3937
  } catch (error) {
2324
3938
  await this.pool?.end().catch(() => void 0);
@@ -2333,12 +3947,45 @@ var PostgresStorageProvider = class {
2333
3947
  }
2334
3948
  }
2335
3949
  async close() {
3950
+ if (this.insertFlushTimer) {
3951
+ clearTimeout(this.insertFlushTimer);
3952
+ this.insertFlushTimer = null;
3953
+ }
3954
+ const deadline = Date.now() + 5e3;
3955
+ while ((this.insertQueue.length > 0 || this.insertFlushInFlight > 0) && Date.now() < deadline) {
3956
+ if (this.insertQueue.length > 0) {
3957
+ await this.flushInsertQueue();
3958
+ } else {
3959
+ await new Promise((r) => setTimeout(r, 5));
3960
+ }
3961
+ }
2336
3962
  if (!this.pool) {
2337
3963
  return;
2338
3964
  }
2339
3965
  await this.pool.end();
2340
3966
  this.pool = null;
2341
3967
  }
3968
+ /**
3969
+ * Run a search query. HNSW GUCs are baked into pool startup options —
3970
+ * no per-recall connect()+set_config round-trip.
3971
+ */
3972
+ async withSearchSession(fn) {
3973
+ return fn(this.requirePool());
3974
+ }
3975
+ /**
3976
+ * Drop HNSW so bulk / concurrent inserts avoid graph maintenance.
3977
+ * Next recall rebuilds the index (lazy).
3978
+ */
3979
+ async dropVectorIndex() {
3980
+ await this.query(`DROP INDEX IF EXISTS idx_memory_embeddings_hnsw`).catch(
3981
+ () => void 0
3982
+ );
3983
+ this.hnswIndexEnsured = false;
3984
+ }
3985
+ /** Force HNSW build now (e.g. before timed recall benches). */
3986
+ async ensureVectorIndex() {
3987
+ await this.ensureHnswIndex();
3988
+ }
2342
3989
  async ensureVectorSchema(dimensions) {
2343
3990
  const existing = await this.getEmbeddingDimensions();
2344
3991
  if (existing !== null && existing !== dimensions) {
@@ -2361,6 +4008,34 @@ var PostgresStorageProvider = class {
2361
4008
  embedding vector(${dimensions})
2362
4009
  )
2363
4010
  `);
4011
+ await this.query(
4012
+ `ALTER TABLE memory_embeddings ADD COLUMN IF NOT EXISTS organization TEXT`
4013
+ ).catch(() => void 0);
4014
+ await this.query(
4015
+ `ALTER TABLE memory_embeddings ADD COLUMN IF NOT EXISTS agent TEXT`
4016
+ ).catch(() => void 0);
4017
+ await this.query(
4018
+ `ALTER TABLE memory_embeddings ADD COLUMN IF NOT EXISTS archived BOOLEAN NOT NULL DEFAULT false`
4019
+ ).catch(() => void 0);
4020
+ const needsBackfill = await this.query(
4021
+ `SELECT 1 FROM memory_embeddings WHERE organization IS NULL LIMIT 1`
4022
+ ).catch(() => ({ rows: [] }));
4023
+ if (needsBackfill.rows.length > 0) {
4024
+ await this.query(`
4025
+ UPDATE memory_embeddings e
4026
+ SET organization = m.organization,
4027
+ agent = m.agent,
4028
+ archived = m.archived
4029
+ FROM memories m
4030
+ WHERE m.id = e.memory_id
4031
+ AND e.organization IS NULL
4032
+ `).catch(() => void 0);
4033
+ }
4034
+ await this.query(
4035
+ `CREATE INDEX IF NOT EXISTS idx_memory_embeddings_org_active
4036
+ ON memory_embeddings (organization)
4037
+ WHERE archived = false`
4038
+ ).catch(() => void 0);
2364
4039
  const idx = await this.query(
2365
4040
  `SELECT 1 FROM pg_indexes WHERE indexname = 'idx_memory_embeddings_hnsw' LIMIT 1`
2366
4041
  );
@@ -2374,6 +4049,10 @@ var PostgresStorageProvider = class {
2374
4049
  `);
2375
4050
  }
2376
4051
  }
4052
+ /** Cross-process subscribe: NOTIFY after a committed write. */
4053
+ async notifyChange(event) {
4054
+ await notifyMemoryChange(this.requirePool(), event);
4055
+ }
2377
4056
  /**
2378
4057
  * Soft reset for a single organization. Drops HNSW only when the embeddings
2379
4058
  * table is empty so other corpora on a shared bench DB stay intact.
@@ -2407,22 +4086,38 @@ var PostgresStorageProvider = class {
2407
4086
  if (!this.hasPgvector || this.hnswIndexEnsured) {
2408
4087
  return;
2409
4088
  }
4089
+ if (this.hnswBuildInFlight) {
4090
+ await this.hnswBuildInFlight;
4091
+ return;
4092
+ }
4093
+ this.hnswBuildInFlight = this.buildHnswIndex();
4094
+ try {
4095
+ await this.hnswBuildInFlight;
4096
+ } finally {
4097
+ this.hnswBuildInFlight = null;
4098
+ }
4099
+ }
4100
+ async buildHnswIndex() {
4101
+ if (this.hnswIndexEnsured) {
4102
+ return;
4103
+ }
2410
4104
  try {
2411
4105
  await this.query(`
2412
- CREATE INDEX IF NOT EXISTS idx_memory_embeddings_hnsw
4106
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_memory_embeddings_hnsw
2413
4107
  ON memory_embeddings USING hnsw (embedding vector_cosine_ops)
2414
- WITH (m = 16, ef_construction = 64)
4108
+ WITH (m = 16, ef_construction = 32)
2415
4109
  `);
2416
4110
  this.hnswIndexEnsured = true;
2417
4111
  this.hnswCreateFailures = 0;
2418
- if (!this.iterativeScanEnabled) {
2419
- try {
2420
- await this.query(`SET hnsw.iterative_scan = relaxed_order`);
2421
- this.iterativeScanEnabled = true;
2422
- } catch {
2423
- }
2424
- }
2425
4112
  } catch (error) {
4113
+ const idx = await this.query(
4114
+ `SELECT 1 FROM pg_indexes WHERE indexname = 'idx_memory_embeddings_hnsw' LIMIT 1`
4115
+ ).catch(() => ({ rows: [] }));
4116
+ if (idx.rows.length > 0) {
4117
+ this.hnswIndexEnsured = true;
4118
+ this.hnswCreateFailures = 0;
4119
+ return;
4120
+ }
2426
4121
  this.hnswCreateFailures += 1;
2427
4122
  if (this.hnswCreateFailures >= 3) {
2428
4123
  throw new DatabaseError(
@@ -2454,48 +4149,50 @@ var PostgresStorageProvider = class {
2454
4149
  }
2455
4150
  async insertMemory(input) {
2456
4151
  this.requireVectorReady();
2457
- if (!this.insertFlushInFlight && this.insertQueue.length === 0) {
2458
- this.insertFlushInFlight = true;
2459
- try {
2460
- return await this.insertMemoryImmediate(input);
2461
- } finally {
2462
- this.insertFlushInFlight = false;
2463
- if (this.insertQueue.length > 0) {
2464
- this.insertFlushScheduled = true;
2465
- queueMicrotask(() => {
2466
- void this.flushInsertQueue();
2467
- });
2468
- }
2469
- }
2470
- }
2471
4152
  return new Promise((resolve, reject) => {
2472
4153
  this.insertQueue.push({ input, resolve, reject });
2473
- if (this.insertQueue.length >= 16) {
2474
- if (this.insertFlushScheduled) {
2475
- this.insertFlushScheduled = false;
2476
- }
4154
+ if (this.insertQueue.length >= COALESCE_FLUSH_THRESHOLD) {
4155
+ this.clearInsertFlushTimer();
2477
4156
  void this.flushInsertQueue();
2478
4157
  return;
2479
4158
  }
2480
- if (!this.insertFlushScheduled) {
2481
- this.insertFlushScheduled = true;
2482
- queueMicrotask(() => {
2483
- void this.flushInsertQueue();
2484
- });
2485
- }
4159
+ this.scheduleInsertFlush();
4160
+ });
4161
+ }
4162
+ clearInsertFlushTimer() {
4163
+ if (this.insertFlushTimer) {
4164
+ clearImmediate(this.insertFlushTimer);
4165
+ clearTimeout(this.insertFlushTimer);
4166
+ this.insertFlushTimer = null;
4167
+ }
4168
+ this.insertFlushScheduled = false;
4169
+ }
4170
+ /** Flush coalesced inserts after a turn so concurrent remember() can join. */
4171
+ scheduleInsertFlush() {
4172
+ if (this.insertFlushScheduled) {
4173
+ return;
4174
+ }
4175
+ this.insertFlushScheduled = true;
4176
+ this.insertFlushTimer = setImmediate(() => {
4177
+ this.insertFlushTimer = null;
4178
+ this.insertFlushScheduled = false;
4179
+ void this.flushInsertQueue();
2486
4180
  });
2487
4181
  }
2488
4182
  async flushInsertQueue() {
2489
- if (this.insertFlushInFlight) {
4183
+ if (this.insertFlushInFlight >= COALESCE_MAX_PARALLEL) {
4184
+ this.scheduleInsertFlush();
2490
4185
  return;
2491
4186
  }
2492
- const batch = this.insertQueue;
2493
- this.insertQueue = [];
2494
- this.insertFlushScheduled = false;
4187
+ const batch = this.insertQueue.splice(0, COALESCE_FLUSH_MAX);
4188
+ this.clearInsertFlushTimer();
2495
4189
  if (batch.length === 0) {
2496
4190
  return;
2497
4191
  }
2498
- this.insertFlushInFlight = true;
4192
+ this.insertFlushInFlight += 1;
4193
+ if (this.insertQueue.length > 0 && this.insertFlushInFlight < COALESCE_MAX_PARALLEL) {
4194
+ void this.flushInsertQueue();
4195
+ }
2499
4196
  try {
2500
4197
  if (batch.length === 1) {
2501
4198
  const row = await this.insertMemoryImmediate(batch[0].input);
@@ -2511,20 +4208,16 @@ var PostgresStorageProvider = class {
2511
4208
  item.reject(error);
2512
4209
  }
2513
4210
  } finally {
2514
- this.insertFlushInFlight = false;
4211
+ this.insertFlushInFlight -= 1;
2515
4212
  if (this.insertQueue.length > 0) {
2516
- this.insertFlushScheduled = true;
2517
- queueMicrotask(() => {
2518
- void this.flushInsertQueue();
2519
- });
2520
- } else {
2521
- this.insertFlushScheduled = false;
4213
+ void this.flushInsertQueue();
2522
4214
  }
2523
4215
  }
2524
4216
  }
2525
4217
  /** Single-row insert without coalescing (used by flush + batch of 1). */
2526
4218
  async insertMemoryImmediate(input) {
2527
4219
  if (this.hasPgvector) {
4220
+ const contentHash = input.contentHash !== void 0 ? input.contentHash : hashMemoryContent(input.contentText);
2528
4221
  const inserted = await this.queryNamed(STMT.insertOne, INSERT_ONE_SQL, [
2529
4222
  input.id,
2530
4223
  input.organization,
@@ -2533,8 +4226,8 @@ var PostgresStorageProvider = class {
2533
4226
  serializeMetadata(input.metadata),
2534
4227
  input.createdAt,
2535
4228
  input.updatedAt,
2536
- crypto.randomUUID(),
2537
- toVectorLiteral(input.embedding)
4229
+ toFloat4Param(input.embedding),
4230
+ contentHash
2538
4231
  ]);
2539
4232
  return this.mapRow(inserted.rows[0]);
2540
4233
  }
@@ -2570,7 +4263,6 @@ var PostgresStorageProvider = class {
2570
4263
  const metas = new Array(inputs.length);
2571
4264
  const created = new Array(inputs.length);
2572
4265
  const updated = new Array(inputs.length);
2573
- const histIds = new Array(inputs.length);
2574
4266
  const vectors = new Array(inputs.length);
2575
4267
  for (let i = 0; i < inputs.length; i += 1) {
2576
4268
  const input = inputs[i];
@@ -2581,10 +4273,9 @@ var PostgresStorageProvider = class {
2581
4273
  metas[i] = serializeMetadata(input.metadata);
2582
4274
  created[i] = input.createdAt;
2583
4275
  updated[i] = input.updatedAt;
2584
- histIds[i] = crypto.randomUUID();
2585
4276
  vectors[i] = toVectorLiteral(input.embedding);
2586
4277
  }
2587
- const inserted = await this.queryNamed(STMT.insertBatch, INSERT_BATCH_SQL, [
4278
+ await this.queryNamed(STMT.insertBatch, INSERT_BATCH_SQL, [
2588
4279
  ids,
2589
4280
  orgs,
2590
4281
  agents,
@@ -2592,17 +4283,24 @@ var PostgresStorageProvider = class {
2592
4283
  metas,
2593
4284
  created,
2594
4285
  updated,
2595
- histIds,
2596
4286
  vectors
2597
4287
  ]);
2598
- const byId = new Map(
2599
- inserted.rows.map((r) => [String(r.id), this.mapRow(r)])
2600
- );
2601
- return ids.map((id) => byId.get(id));
4288
+ return inputs.map((input, i) => ({
4289
+ id: ids[i],
4290
+ organization: orgs[i],
4291
+ agent: agents[i],
4292
+ content_text: texts[i],
4293
+ metadata_json: metas[i],
4294
+ archived: 0,
4295
+ compressed_into: null,
4296
+ content_hash: input.contentHash !== void 0 ? input.contentHash : null,
4297
+ created_at: created[i],
4298
+ updated_at: updated[i]
4299
+ }));
2602
4300
  }
2603
- /** Split large ingest batches into parallel unnest chunks (pool pipelining). */
4301
+ /** Parallel unnest when HNSW is absent; sequential when index must stay consistent. */
2604
4302
  async insertBatchChunked(inputs) {
2605
- const chunkSize = 128;
4303
+ const chunkSize = this.hnswIndexEnsured ? BULK_CHUNK_WITH_HNSW : BULK_CHUNK_NO_HNSW;
2606
4304
  if (inputs.length <= chunkSize) {
2607
4305
  return this.insertBatchPgvector(inputs);
2608
4306
  }
@@ -2610,14 +4308,26 @@ var PostgresStorageProvider = class {
2610
4308
  for (let i = 0; i < inputs.length; i += chunkSize) {
2611
4309
  chunks.push(inputs.slice(i, i + chunkSize));
2612
4310
  }
2613
- const results = await Promise.all(
2614
- chunks.map((chunk) => this.insertBatchPgvector(chunk))
2615
- );
4311
+ if (!this.hnswIndexEnsured) {
4312
+ const parts = await Promise.all(
4313
+ chunks.map((chunk) => this.insertBatchPgvector(chunk))
4314
+ );
4315
+ const out2 = new Array(inputs.length);
4316
+ let offset2 = 0;
4317
+ for (const part of parts) {
4318
+ for (let j = 0; j < part.length; j += 1) {
4319
+ out2[offset2 + j] = part[j];
4320
+ }
4321
+ offset2 += part.length;
4322
+ }
4323
+ return out2;
4324
+ }
2616
4325
  const out = new Array(inputs.length);
2617
4326
  let offset = 0;
2618
- for (const part of results) {
2619
- for (let i = 0; i < part.length; i += 1) {
2620
- out[offset + i] = part[i];
4327
+ for (const chunk of chunks) {
4328
+ const part = await this.insertBatchPgvector(chunk);
4329
+ for (let j = 0; j < part.length; j += 1) {
4330
+ out[offset + j] = part[j];
2621
4331
  }
2622
4332
  offset += part.length;
2623
4333
  }
@@ -2629,13 +4339,14 @@ var PostgresStorageProvider = class {
2629
4339
  input.embedding.byteOffset,
2630
4340
  input.embedding.byteLength
2631
4341
  );
4342
+ const contentHash = input.contentHash !== void 0 ? input.contentHash : hashMemoryContent(input.contentText);
2632
4343
  const inserted = await this.query(
2633
4344
  `INSERT INTO memories (
2634
4345
  id, organization, agent, content_text, metadata_json,
2635
- archived, compressed_into, created_at, updated_at
2636
- ) VALUES ($1,$2,$3,$4,$5::jsonb,false,NULL,$6,$7)
4346
+ archived, compressed_into, content_hash, created_at, updated_at
4347
+ ) VALUES ($1,$2,$3,$4,$5::jsonb,false,NULL,$8,$6,$7)
2637
4348
  RETURNING id, organization, agent, content_text, metadata_json,
2638
- archived::int AS archived, compressed_into, created_at, updated_at`,
4349
+ archived::int AS archived, compressed_into, content_hash, created_at, updated_at`,
2639
4350
  [
2640
4351
  input.id,
2641
4352
  input.organization,
@@ -2643,7 +4354,8 @@ var PostgresStorageProvider = class {
2643
4354
  input.contentText,
2644
4355
  serializeMetadata(input.metadata),
2645
4356
  input.createdAt,
2646
- input.updatedAt
4357
+ input.updatedAt,
4358
+ contentHash
2647
4359
  ]
2648
4360
  );
2649
4361
  const row = this.mapRow(inserted.rows[0]);
@@ -2669,15 +4381,18 @@ var PostgresStorageProvider = class {
2669
4381
  if (!existing) {
2670
4382
  return null;
2671
4383
  }
4384
+ const contentHash = input.contentHash !== void 0 ? input.contentHash : input.contentText !== void 0 ? hashMemoryContent(input.contentText) : existing.content_hash ?? null;
2672
4385
  await this.query(
2673
4386
  `UPDATE memories SET
2674
4387
  content_text = COALESCE($1, content_text),
2675
4388
  metadata_json = COALESCE($2::jsonb, metadata_json),
2676
- updated_at = $3
2677
- WHERE id = $4 AND organization = $5`,
4389
+ content_hash = COALESCE($3, content_hash),
4390
+ updated_at = $4
4391
+ WHERE id = $5 AND organization = $6`,
2678
4392
  [
2679
4393
  input.contentText ?? null,
2680
4394
  input.metadata !== void 0 ? serializeMetadata(input.metadata) : null,
4395
+ contentHash,
2681
4396
  input.updatedAt,
2682
4397
  input.id,
2683
4398
  input.organization
@@ -2687,12 +4402,29 @@ var PostgresStorageProvider = class {
2687
4402
  await this.deleteEmbedding(input.id);
2688
4403
  await this.insertEmbedding(input.id, input.embedding);
2689
4404
  }
4405
+ await this.query(
4406
+ `INSERT INTO memory_history (id, memory_id, event_type, related_memory_id, created_at)
4407
+ VALUES ($1,$2,'updated',NULL,$3)`,
4408
+ [crypto.randomUUID(), input.id, input.updatedAt]
4409
+ );
2690
4410
  return this.getMemoryById(input.id, input.organization);
2691
4411
  }
4412
+ async findActiveByContentHash(organization, agent, contentHash) {
4413
+ const result = await this.query(
4414
+ `SELECT id, organization, agent, content_text, metadata_json,
4415
+ archived::int AS archived, compressed_into, content_hash, created_at, updated_at
4416
+ FROM memories
4417
+ WHERE organization = $1 AND agent = $2 AND content_hash = $3 AND archived = false
4418
+ LIMIT 1`,
4419
+ [organization, agent, contentHash]
4420
+ );
4421
+ const row = result.rows[0];
4422
+ return row ? this.mapRow(row) : null;
4423
+ }
2692
4424
  async getMemoryById(id, organization) {
2693
4425
  const result = await this.query(
2694
4426
  `SELECT id, organization, agent, content_text, metadata_json,
2695
- archived::int AS archived, compressed_into, created_at, updated_at
4427
+ archived::int AS archived, compressed_into, content_hash, created_at, updated_at
2696
4428
  FROM memories WHERE id = $1 AND organization = $2`,
2697
4429
  [id, organization]
2698
4430
  );
@@ -2735,6 +4467,33 @@ var PostgresStorageProvider = class {
2735
4467
  return out;
2736
4468
  }
2737
4469
  async listMemories(filter, limit) {
4470
+ const want = limit !== void 0 ? limit : filter.metadata ? 1e4 : void 0;
4471
+ const compiled = filter.metadata ? compileMetadataFilterToPostgres(filter.metadata, 2) : null;
4472
+ if (compiled) {
4473
+ const clauses2 = [`organization = $1`, `(${compiled.expression})`];
4474
+ const params2 = [filter.organization, ...compiled.params];
4475
+ let idx2 = params2.length + 1;
4476
+ if (filter.agent) {
4477
+ clauses2.push(`agent = $${idx2++}`);
4478
+ params2.push(filter.agent);
4479
+ }
4480
+ if (!filter.includeArchived) {
4481
+ clauses2.push(`archived = false`);
4482
+ }
4483
+ let sql2 = `
4484
+ SELECT id, organization, agent, content_text, metadata_json,
4485
+ archived::int AS archived, compressed_into, created_at, updated_at
4486
+ FROM memories
4487
+ WHERE ${clauses2.join(" AND ")}
4488
+ ORDER BY created_at ASC
4489
+ `;
4490
+ if (want !== void 0) {
4491
+ sql2 += ` LIMIT $${idx2}`;
4492
+ params2.push(want);
4493
+ }
4494
+ const result2 = await this.query(sql2, params2);
4495
+ return result2.rows.map((r) => this.mapRow(r));
4496
+ }
2738
4497
  const clauses = [`organization = $1`];
2739
4498
  const params = [filter.organization];
2740
4499
  let idx = 2;
@@ -2805,27 +4564,30 @@ var PostgresStorageProvider = class {
2805
4564
  this.requireVectorReady();
2806
4565
  if (this.hasPgvector) {
2807
4566
  await this.ensureHnswIndex();
2808
- const result2 = await this.query(
2809
- `SELECT r.row_num AS memory_rowid, ann.distance
2810
- FROM (
2811
- SELECT e.memory_id, (e.embedding <=> $1::vector) AS distance
2812
- FROM memory_embeddings e
2813
- ORDER BY e.embedding <=> $1::vector
2814
- LIMIT $2
2815
- ) ann
2816
- JOIN memory_row_map r ON r.memory_id = ann.memory_id
2817
- ORDER BY ann.distance`,
2818
- [toVectorLiteral(embedding), topK]
2819
- );
2820
- const hits = new Array(result2.rows.length);
2821
- for (let i = 0; i < result2.rows.length; i += 1) {
2822
- const row = result2.rows[i];
2823
- hits[i] = {
2824
- memoryRowid: Number(row.memory_rowid),
2825
- distance: Number(row.distance)
2826
- };
2827
- }
2828
- return hits;
4567
+ return this.withSearchSession(async (client) => {
4568
+ const result2 = await client.query(
4569
+ `SELECT r.row_num AS memory_rowid, ann.distance
4570
+ FROM (
4571
+ SELECT e.memory_id, (e.embedding <=> $1::float4[]::vector) AS distance
4572
+ FROM memory_embeddings e
4573
+ WHERE e.archived = false
4574
+ ORDER BY e.embedding <=> $1::float4[]::vector
4575
+ LIMIT $2
4576
+ ) ann
4577
+ JOIN memory_row_map r ON r.memory_id = ann.memory_id
4578
+ ORDER BY ann.distance`,
4579
+ [toFloat4Param(embedding), topK]
4580
+ );
4581
+ const hits = new Array(result2.rows.length);
4582
+ for (let i = 0; i < result2.rows.length; i += 1) {
4583
+ const row = result2.rows[i];
4584
+ hits[i] = {
4585
+ memoryRowid: Number(row.memory_rowid),
4586
+ distance: Number(row.distance)
4587
+ };
4588
+ }
4589
+ return hits;
4590
+ });
2829
4591
  }
2830
4592
  const result = await this.query(
2831
4593
  `SELECT r.row_num AS memory_rowid, e.embedding
@@ -2850,65 +4612,66 @@ var PostgresStorageProvider = class {
2850
4612
  async searchVectorsWithMemories(embedding, topK, organization, options) {
2851
4613
  this.requireVectorReady();
2852
4614
  if (!this.hasPgvector) {
2853
- const hits = await this.searchVectors(embedding, topK);
2854
- const map = await this.getMemoriesByRowids(
2855
- hits.map((h) => h.memoryRowid),
2856
- organization
2857
- );
2858
- const out = [];
2859
- for (const hit of hits) {
2860
- const row = map.get(hit.memoryRowid);
2861
- if (!row) continue;
2862
- if (options?.agent && row.agent !== options.agent) continue;
2863
- if (!options?.includeArchived && row.archived === 1) continue;
2864
- out.push({ row, distance: hit.distance });
4615
+ const clauses = [`m.organization = $1`];
4616
+ const params2 = [organization];
4617
+ if (options?.agent) {
4618
+ clauses.push(`m.agent = $2`);
4619
+ params2.push(options.agent);
2865
4620
  }
2866
- return out;
4621
+ if (!options?.includeArchived) {
4622
+ clauses.push(`m.archived = false`);
4623
+ }
4624
+ const result2 = await this.query(
4625
+ `SELECT m.id, m.organization, m.agent, m.content_text, m.metadata_json,
4626
+ m.archived::int AS archived, m.compressed_into, m.created_at, m.updated_at,
4627
+ r.row_num AS rowid, e.embedding
4628
+ FROM memory_embeddings_blob e
4629
+ JOIN memory_row_map r ON r.memory_id = e.memory_id
4630
+ JOIN memories m ON m.id = e.memory_id
4631
+ WHERE ${clauses.join(" AND ")}`,
4632
+ params2
4633
+ );
4634
+ const scored = result2.rows.map((row) => {
4635
+ const buf = row.embedding;
4636
+ const vec2 = new Float32Array(
4637
+ buf.buffer,
4638
+ buf.byteOffset,
4639
+ buf.byteLength / Float32Array.BYTES_PER_ELEMENT
4640
+ );
4641
+ return {
4642
+ row: this.mapRow(row),
4643
+ distance: cosineDistance(embedding, vec2)
4644
+ };
4645
+ });
4646
+ scored.sort((a, b) => a.distance - b.distance);
4647
+ return scored.slice(0, topK);
2867
4648
  }
2868
4649
  await this.ensureHnswIndex();
2869
- const vec = toVectorLiteral(embedding);
4650
+ const vec = toFloat4Param(embedding);
2870
4651
  const mapHits = (rows) => rows.map((row) => ({
2871
4652
  row: this.mapRow(row),
2872
4653
  distance: Number(row.distance)
2873
4654
  }));
2874
- let overfetch = Math.min(Math.max(topK * 8, topK), 512);
2875
- const maxFetch = Math.min(Math.max(topK * 64, 512), 8192);
2876
- for (; ; ) {
2877
- const agentClause = options?.agent ? "AND m.agent = $5" : "";
2878
- const archivedClause = options?.includeArchived ? "" : "AND m.archived = false";
2879
- const params = options?.agent ? [vec, overfetch, organization, topK, options.agent] : [vec, overfetch, organization, topK];
2880
- const ann = await this.query(
2881
- `WITH ann AS (
2882
- SELECT e.memory_id, (e.embedding <=> $1::vector) AS distance
2883
- FROM memory_embeddings e
2884
- ORDER BY e.embedding <=> $1::vector
2885
- LIMIT $2
2886
- )
2887
- SELECT m.id, m.organization, m.agent, m.content_text, m.metadata_json,
2888
- m.archived::int AS archived, m.compressed_into, m.created_at, m.updated_at,
2889
- r.row_num AS rowid,
2890
- ann.distance,
2891
- (SELECT COUNT(*)::int FROM ann) AS ann_fetched
2892
- FROM ann
2893
- JOIN memory_row_map r ON r.memory_id = ann.memory_id
2894
- JOIN memories m ON m.id = ann.memory_id
2895
- WHERE m.organization = $3
2896
- ${agentClause}
2897
- ${archivedClause}
2898
- ORDER BY ann.distance
2899
- LIMIT $4`,
2900
- params
2901
- );
2902
- const annFetched = Number(ann.rows[0]?.ann_fetched ?? ann.rows.length);
2903
- if (ann.rows.length >= topK || annFetched < overfetch || overfetch >= maxFetch) {
2904
- return mapHits(ann.rows.slice(0, topK));
2905
- }
2906
- const next = Math.min(overfetch * 4, maxFetch);
2907
- if (next === overfetch) {
2908
- return mapHits(ann.rows.slice(0, topK));
2909
- }
2910
- overfetch = next;
2911
- }
4655
+ const agentClause = options?.agent ? "AND e.agent = $4" : "";
4656
+ const archivedClause = options?.includeArchived ? "" : "AND e.archived = false";
4657
+ const params = options?.agent ? [vec, topK, organization, options.agent] : [vec, topK, organization];
4658
+ const result = await this.query(
4659
+ `SELECT m.id, m.organization, m.agent, m.content_text, m.metadata_json,
4660
+ m.archived::int AS archived, m.compressed_into, m.created_at, m.updated_at,
4661
+ r.row_num AS rowid,
4662
+ (e.embedding <=> $1::float4[]::vector) AS distance
4663
+ FROM memory_embeddings e
4664
+ JOIN memory_row_map r ON r.memory_id = e.memory_id
4665
+ JOIN memories m ON m.id = e.memory_id
4666
+ WHERE e.organization = $3
4667
+ ${agentClause}
4668
+ ${archivedClause}
4669
+ AND m.organization = $3
4670
+ ORDER BY e.embedding <=> $1::float4[]::vector
4671
+ LIMIT $2`,
4672
+ params
4673
+ );
4674
+ return mapHits(result.rows);
2912
4675
  }
2913
4676
  async archiveMemories(ids, organization, compressedIntoId, archivedAt) {
2914
4677
  if (ids.length === 0) {
@@ -2925,6 +4688,12 @@ var PostgresStorageProvider = class {
2925
4688
  if (archived.length === 0) {
2926
4689
  return [];
2927
4690
  }
4691
+ await this.query(
4692
+ `UPDATE memory_embeddings
4693
+ SET archived = true
4694
+ WHERE memory_id = ANY($1::text[])`,
4695
+ [archived]
4696
+ ).catch(() => void 0);
2928
4697
  const histIds = [];
2929
4698
  const memIds = [];
2930
4699
  const types = [];
@@ -3048,6 +4817,20 @@ var PostgresStorageProvider = class {
3048
4817
  value TEXT NOT NULL
3049
4818
  )
3050
4819
  `);
4820
+ const versionRow = await this.query(
4821
+ `SELECT value FROM Wolbarg_meta WHERE key = $1`,
4822
+ [META_KEYS.schemaVersion]
4823
+ ).catch(() => ({ rows: [] }));
4824
+ const current = versionRow.rows[0]?.value !== void 0 ? Number(versionRow.rows[0].value) : null;
4825
+ if (current === SCHEMA_VERSION) {
4826
+ const tsvProbe2 = await this.query(
4827
+ `SELECT 1 FROM information_schema.columns
4828
+ WHERE table_name = 'memories' AND column_name = 'content_tsv'
4829
+ LIMIT 1`
4830
+ ).catch(() => ({ rows: [] }));
4831
+ this.hasContentTsv = tsvProbe2.rows.length > 0;
4832
+ return;
4833
+ }
3051
4834
  await this.query(`
3052
4835
  CREATE TABLE IF NOT EXISTS memories (
3053
4836
  id TEXT PRIMARY KEY NOT NULL,
@@ -3065,7 +4848,7 @@ var PostgresStorageProvider = class {
3065
4848
  CREATE TABLE IF NOT EXISTS memory_history (
3066
4849
  id TEXT PRIMARY KEY NOT NULL,
3067
4850
  memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
3068
- event_type TEXT NOT NULL CHECK (event_type IN ('created', 'archived', 'compressed')),
4851
+ event_type TEXT NOT NULL CHECK (event_type IN ('created', 'archived', 'compressed', 'updated')),
3069
4852
  related_memory_id TEXT NULL,
3070
4853
  created_at TIMESTAMPTZ NOT NULL
3071
4854
  )
@@ -3080,8 +4863,8 @@ var PostgresStorageProvider = class {
3080
4863
  `CREATE INDEX IF NOT EXISTS idx_memories_org_agent ON memories(organization, agent)`
3081
4864
  );
3082
4865
  await this.query(
3083
- `CREATE INDEX IF NOT EXISTS idx_memories_org_archived ON memories(organization, archived)`
3084
- );
4866
+ `DROP INDEX IF EXISTS idx_memories_org_archived`
4867
+ ).catch(() => void 0);
3085
4868
  await this.query(
3086
4869
  `CREATE INDEX IF NOT EXISTS idx_memories_org_active_created
3087
4870
  ON memories(organization, created_at) WHERE archived = false`
@@ -3089,53 +4872,132 @@ var PostgresStorageProvider = class {
3089
4872
  await this.query(
3090
4873
  `CREATE INDEX IF NOT EXISTS idx_memories_metadata ON memories USING GIN (metadata_json)`
3091
4874
  );
3092
- try {
3093
- await this.query(`
3094
- ALTER TABLE memories
3095
- ADD COLUMN IF NOT EXISTS content_tsv tsvector
3096
- GENERATED ALWAYS AS (to_tsvector('english', content_text)) STORED
3097
- `);
3098
- await this.query(
3099
- `CREATE INDEX IF NOT EXISTS idx_memories_content_tsv ON memories USING GIN (content_tsv)`
3100
- );
3101
- this.hasContentTsv = true;
3102
- } catch {
3103
- await this.query(
3104
- `CREATE INDEX IF NOT EXISTS idx_memories_fts
3105
- ON memories USING GIN (to_tsvector('english', content_text))`
3106
- ).catch(() => void 0);
3107
- this.hasContentTsv = false;
3108
- }
3109
- const versionRow = await this.query(
4875
+ await this.query(
4876
+ `ALTER TABLE memories ADD COLUMN IF NOT EXISTS content_hash TEXT`
4877
+ ).catch(() => void 0);
4878
+ await this.query(
4879
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_org_agent_hash_active
4880
+ ON memories(organization, agent, content_hash)
4881
+ WHERE archived = false AND content_hash IS NOT NULL`
4882
+ ).catch(() => void 0);
4883
+ await this.query(`
4884
+ CREATE TABLE IF NOT EXISTS embedding_cache (
4885
+ cache_key TEXT PRIMARY KEY NOT NULL,
4886
+ model TEXT NOT NULL,
4887
+ vector BYTEA NOT NULL,
4888
+ created_at TIMESTAMPTZ NOT NULL,
4889
+ last_used_at TIMESTAMPTZ NOT NULL
4890
+ )
4891
+ `).catch(() => void 0);
4892
+ await this.query(`
4893
+ DO $$
4894
+ BEGIN
4895
+ ALTER TABLE memory_history DROP CONSTRAINT IF EXISTS memory_history_event_type_check;
4896
+ ALTER TABLE memory_history ADD CONSTRAINT memory_history_event_type_check
4897
+ CHECK (event_type IN ('created', 'archived', 'compressed', 'updated'));
4898
+ EXCEPTION WHEN others THEN
4899
+ NULL;
4900
+ END $$;
4901
+ `).catch(() => void 0);
4902
+ const priorVersion = await this.query(
3110
4903
  `SELECT value FROM Wolbarg_meta WHERE key = $1`,
3111
4904
  [META_KEYS.schemaVersion]
3112
- );
3113
- if (!versionRow.rows[0]) {
3114
- await this.query(
3115
- `INSERT INTO Wolbarg_meta (key, value) VALUES ($1, $2)`,
3116
- [META_KEYS.schemaVersion, String(SCHEMA_VERSION)]
3117
- );
4905
+ ).catch(() => ({ rows: [] }));
4906
+ const prior = priorVersion.rows[0]?.value !== void 0 ? Number(priorVersion.rows[0].value) : null;
4907
+ const needsHashBackfill = prior === null || !Number.isFinite(prior) || prior < 3;
4908
+ if (needsHashBackfill) {
4909
+ const active = await this.query(
4910
+ `SELECT id, organization, agent, content_text, updated_at
4911
+ FROM memories WHERE archived = false AND content_hash IS NULL
4912
+ ORDER BY updated_at DESC`
4913
+ ).catch(() => ({ rows: [] }));
4914
+ const seen = /* @__PURE__ */ new Set();
4915
+ for (const row of active.rows) {
4916
+ const hash = hashMemoryContent(String(row.content_text));
4917
+ const key = `${row.organization}\0${row.agent}\0${hash}`;
4918
+ if (seen.has(key)) {
4919
+ await this.query(
4920
+ `UPDATE memories SET content_hash = NULL WHERE id = $1`,
4921
+ [row.id]
4922
+ ).catch(() => void 0);
4923
+ } else {
4924
+ seen.add(key);
4925
+ await this.query(
4926
+ `UPDATE memories SET content_hash = $1 WHERE id = $2`,
4927
+ [hash, row.id]
4928
+ ).catch(() => void 0);
4929
+ }
4930
+ }
4931
+ }
4932
+ await this.query(
4933
+ `INSERT INTO Wolbarg_meta (key, value) VALUES ($1, $2)
4934
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`,
4935
+ [META_KEYS.schemaVersion, String(SCHEMA_VERSION)]
4936
+ ).catch(() => void 0);
4937
+ const tsvProbe = await this.query(
4938
+ `SELECT 1 FROM information_schema.columns
4939
+ WHERE table_name = 'memories' AND column_name = 'content_tsv'
4940
+ LIMIT 1`
4941
+ ).catch(() => ({ rows: [] }));
4942
+ if (tsvProbe.rows.length > 0) {
4943
+ this.hasContentTsv = true;
4944
+ } else {
4945
+ try {
4946
+ await this.query(`
4947
+ ALTER TABLE memories
4948
+ ADD COLUMN IF NOT EXISTS content_tsv tsvector
4949
+ GENERATED ALWAYS AS (to_tsvector('english', content_text)) STORED
4950
+ `);
4951
+ await this.query(
4952
+ `CREATE INDEX IF NOT EXISTS idx_memories_content_tsv ON memories USING GIN (content_tsv)`
4953
+ );
4954
+ this.hasContentTsv = true;
4955
+ } catch {
4956
+ await this.query(
4957
+ `CREATE INDEX IF NOT EXISTS idx_memories_fts
4958
+ ON memories USING GIN (to_tsvector('english', content_text))`
4959
+ ).catch(() => void 0);
4960
+ this.hasContentTsv = false;
4961
+ }
3118
4962
  }
3119
4963
  }
3120
4964
  async tryEnablePgvector() {
4965
+ const cached = _PostgresStorageProvider.pgvectorByConn.get(
4966
+ this.connectionString
4967
+ );
4968
+ if (cached !== void 0) {
4969
+ return cached;
4970
+ }
3121
4971
  try {
3122
4972
  await this.query(`CREATE EXTENSION IF NOT EXISTS vector`);
4973
+ _PostgresStorageProvider.pgvectorByConn.set(this.connectionString, true);
3123
4974
  return true;
3124
4975
  } catch {
4976
+ _PostgresStorageProvider.pgvectorByConn.set(this.connectionString, false);
3125
4977
  return false;
3126
4978
  }
3127
4979
  }
4980
+ static pgvectorByConn = /* @__PURE__ */ new Map();
4981
+ static vectorSchemaReady = /* @__PURE__ */ new Set();
3128
4982
  async insertEmbedding(memoryId, embedding) {
3129
4983
  if (this.hasPgvector) {
3130
4984
  await this.query(
3131
4985
  `WITH mapped AS (
3132
4986
  INSERT INTO memory_row_map (memory_id) VALUES ($1)
3133
4987
  ON CONFLICT (memory_id) DO NOTHING
4988
+ ),
4989
+ meta AS (
4990
+ SELECT organization, agent, archived FROM memories WHERE id = $1
3134
4991
  )
3135
- INSERT INTO memory_embeddings (memory_id, embedding)
3136
- VALUES ($1, $2::vector)
3137
- ON CONFLICT (memory_id) DO UPDATE SET embedding = EXCLUDED.embedding`,
3138
- [memoryId, toVectorLiteral(embedding)]
4992
+ INSERT INTO memory_embeddings (memory_id, embedding, organization, agent, archived)
4993
+ SELECT $1, $2::float4[]::vector, meta.organization, meta.agent, meta.archived
4994
+ FROM meta
4995
+ ON CONFLICT (memory_id) DO UPDATE SET
4996
+ embedding = EXCLUDED.embedding,
4997
+ organization = EXCLUDED.organization,
4998
+ agent = EXCLUDED.agent,
4999
+ archived = EXCLUDED.archived`,
5000
+ [memoryId, toFloat4Param(embedding)]
3139
5001
  );
3140
5002
  return;
3141
5003
  }
@@ -3190,6 +5052,7 @@ var PostgresStorageProvider = class {
3190
5052
  metadata_json,
3191
5053
  archived: Number(row.archived ?? 0),
3192
5054
  compressed_into: row.compressed_into === null || row.compressed_into === void 0 ? null : String(row.compressed_into),
5055
+ content_hash: row.content_hash === null || row.content_hash === void 0 ? null : String(row.content_hash),
3193
5056
  created_at: created,
3194
5057
  updated_at: updated,
3195
5058
  rowid: row.rowid !== void 0 ? Number(row.rowid) : void 0
@@ -3222,7 +5085,7 @@ var PostgresStorageProvider = class {
3222
5085
  };
3223
5086
 
3224
5087
  // src/storage/index.ts
3225
- function createStorageProvider(config) {
5088
+ function createStorageProvider(config, options) {
3226
5089
  const connectionString = resolveDatabaseUrl(config);
3227
5090
  if (!connectionString) {
3228
5091
  throw new ConfigurationError(
@@ -3231,13 +5094,15 @@ function createStorageProvider(config) {
3231
5094
  }
3232
5095
  if (config.provider === "sqlite") {
3233
5096
  return new SqliteStorageProvider({
3234
- connectionString
5097
+ connectionString,
5098
+ concurrency: options?.concurrency
3235
5099
  });
3236
5100
  }
3237
5101
  if (config.provider === "postgres") {
3238
5102
  return new PostgresStorageProvider({
3239
5103
  connectionString,
3240
- maxPoolSize: config.maxPoolSize
5104
+ maxPoolSize: config.maxPoolSize,
5105
+ durableWrites: config.durableWrites
3241
5106
  });
3242
5107
  }
3243
5108
  throw new ConfigurationError(
@@ -3561,7 +5426,8 @@ function normalizeDatabaseConfig(config) {
3561
5426
  provider: "postgres",
3562
5427
  connectionString,
3563
5428
  url: connectionString,
3564
- ..."maxPoolSize" in config && config.maxPoolSize !== void 0 ? { maxPoolSize: config.maxPoolSize } : {}
5429
+ ..."maxPoolSize" in config && config.maxPoolSize !== void 0 ? { maxPoolSize: config.maxPoolSize } : {},
5430
+ ..."durableWrites" in config && config.durableWrites !== void 0 ? { durableWrites: config.durableWrites } : {}
3565
5431
  };
3566
5432
  }
3567
5433
  return {
@@ -4560,6 +6426,66 @@ Ensure no exclusive lock is held and the destination directory is writable.`,
4560
6426
  }
4561
6427
  }
4562
6428
  }
6429
+ function matchesFilter2(filter, event) {
6430
+ if (filter.organization !== event.organization) {
6431
+ return false;
6432
+ }
6433
+ if (filter.agent !== void 0 && filter.agent !== event.agent) {
6434
+ return false;
6435
+ }
6436
+ if (filter.event === void 0) {
6437
+ return true;
6438
+ }
6439
+ const events = Array.isArray(filter.event) ? filter.event : [filter.event];
6440
+ return events.some(
6441
+ (e) => e === "*" || e === event.event
6442
+ );
6443
+ }
6444
+ var SqliteSubscribeEmitter = class {
6445
+ emitter = new events.EventEmitter();
6446
+ subscriptions = /* @__PURE__ */ new Map();
6447
+ nextId = 1;
6448
+ closed = false;
6449
+ constructor() {
6450
+ this.emitter.setMaxListeners(0);
6451
+ this.emitter.on("change", (event) => {
6452
+ for (const sub of this.subscriptions.values()) {
6453
+ if (!matchesFilter2(sub.filter, event)) {
6454
+ continue;
6455
+ }
6456
+ try {
6457
+ sub.callback(event);
6458
+ } catch (error) {
6459
+ console.error(
6460
+ "[wolbarg] subscribe callback error:",
6461
+ error instanceof Error ? error.message : error
6462
+ );
6463
+ }
6464
+ }
6465
+ });
6466
+ }
6467
+ subscribe(filter, callback) {
6468
+ if (this.closed) {
6469
+ throw new Error("Subscribe backend is closed");
6470
+ }
6471
+ const id = this.nextId++;
6472
+ this.subscriptions.set(id, { filter, callback });
6473
+ return () => {
6474
+ this.subscriptions.delete(id);
6475
+ };
6476
+ }
6477
+ emit(event) {
6478
+ if (this.closed) {
6479
+ return;
6480
+ }
6481
+ this.emitter.emit("change", event);
6482
+ }
6483
+ async close() {
6484
+ this.closed = true;
6485
+ this.subscriptions.clear();
6486
+ this.emitter.removeAllListeners();
6487
+ }
6488
+ };
4563
6489
 
4564
6490
  // src/core/wolbarg.ts
4565
6491
  var Wolbarg = class {
@@ -4582,6 +6508,11 @@ var Wolbarg = class {
4582
6508
  checkpointProvider = null;
4583
6509
  memoryDbPath = null;
4584
6510
  transfer = new SqliteMemoryTransferProvider();
6511
+ subscribeBackend = null;
6512
+ pgListenBackend = null;
6513
+ memoryDedupe = resolveMemoryDedupeConfig();
6514
+ embeddingCacheConfig = resolveEmbeddingCacheConfig();
6515
+ rawEmbedding = null;
4585
6516
  constructor(options) {
4586
6517
  this.telemetry = new TelemetryEmitter(null, { enabled: false, level: "off" });
4587
6518
  if (!options) {
@@ -4589,14 +6520,22 @@ var Wolbarg = class {
4589
6520
  }
4590
6521
  const validated = validateWolbargOptions(options);
4591
6522
  this.organization = validated.organization;
6523
+ this.memoryDedupe = resolveMemoryDedupeConfig(validated.memory?.dedupe);
6524
+ this.embeddingCacheConfig = resolveEmbeddingCacheConfig(
6525
+ validated.embeddingCache
6526
+ );
4592
6527
  const storageInput = validated.storage;
4593
- this.storage = isStorageProvider(storageInput) ? storageInput : createStorageProvider(storageInput);
6528
+ this.storage = isStorageProvider(storageInput) ? storageInput : createStorageProvider(storageInput, {
6529
+ concurrency: validated.concurrency
6530
+ });
4594
6531
  if (!isStorageProvider(storageInput)) {
4595
6532
  this.memoryDbPath = resolveDatabaseUrl(storageInput);
4596
6533
  } else if (storageInput instanceof SqliteStorageProvider) {
4597
6534
  this.memoryDbPath = storageInput.path;
4598
6535
  }
4599
- this.embedding = isEmbeddingProvider(validated.embedding) ? validated.embedding : createEmbeddingProvider(validated.embedding);
6536
+ const embedding = isEmbeddingProvider(validated.embedding) ? validated.embedding : createEmbeddingProvider(validated.embedding);
6537
+ this.rawEmbedding = embedding;
6538
+ this.embedding = embedding;
4600
6539
  if (validated.llm) {
4601
6540
  this.llm = isLlmProvider(validated.llm) ? validated.llm : createLlmProvider(validated.llm);
4602
6541
  this.compression = validated.compression ?? createCompressionProvider(this.llm);
@@ -4609,6 +6548,7 @@ var Wolbarg = class {
4609
6548
  this.vision = validated.vision ?? null;
4610
6549
  this.chunking = validated.chunking ?? null;
4611
6550
  this.retrievalConfig = validated.retrieval ?? {};
6551
+ this.subscribeBackend = new SqliteSubscribeEmitter();
4612
6552
  if (validated.telemetry) {
4613
6553
  if (isTelemetryProvider(validated.telemetry)) {
4614
6554
  this.telemetry = new TelemetryEmitter(validated.telemetry, {
@@ -4684,9 +6624,54 @@ var Wolbarg = class {
4684
6624
  await this.storage.open();
4685
6625
  await this.telemetry.open();
4686
6626
  await this.checkpointProvider?.open();
4687
- const probe = await this.embedding.validate();
4688
- await this.storage.ensureVectorSchema(probe.dimensions);
4689
- this.embeddingDimensions = probe.dimensions;
6627
+ if (this.rawEmbedding && this.embeddingCacheConfig.enabled) {
6628
+ if (this.storage instanceof SqliteStorageProvider) {
6629
+ const sqlite2 = this.storage;
6630
+ const store = new SqliteEmbeddingCacheStore(() => sqlite2.getDatabase(), {
6631
+ ttlMs: this.embeddingCacheConfig.ttlMs
6632
+ });
6633
+ this.embedding = withEmbeddingCache(
6634
+ this.rawEmbedding,
6635
+ store,
6636
+ this.embeddingCacheConfig
6637
+ );
6638
+ } else if (this.storage instanceof PostgresStorageProvider) {
6639
+ this.embedding = withEmbeddingCache(
6640
+ this.rawEmbedding,
6641
+ new PostgresEmbeddingCacheStore(() => null, {
6642
+ ttlMs: this.embeddingCacheConfig.ttlMs,
6643
+ durable: false
6644
+ }),
6645
+ this.embeddingCacheConfig
6646
+ );
6647
+ } else {
6648
+ this.embedding = withEmbeddingCache(
6649
+ this.rawEmbedding,
6650
+ new MemoryEmbeddingCacheStore({
6651
+ ttlMs: this.embeddingCacheConfig.ttlMs
6652
+ }),
6653
+ this.embeddingCacheConfig
6654
+ );
6655
+ }
6656
+ }
6657
+ if (this.storage instanceof PostgresStorageProvider) {
6658
+ }
6659
+ if (this.storage instanceof SqliteStorageProvider) {
6660
+ this.storage.setRetryLogger((msg) => {
6661
+ if (typeof console !== "undefined" && console.debug) {
6662
+ console.debug(`[wolbarg] ${msg}`);
6663
+ }
6664
+ });
6665
+ }
6666
+ const storedDims = await this.storage.getEmbeddingDimensions();
6667
+ if (storedDims !== null && storedDims > 0) {
6668
+ await this.storage.ensureVectorSchema(storedDims);
6669
+ this.embeddingDimensions = storedDims;
6670
+ } else {
6671
+ const probe = await this.embedding.validate();
6672
+ await this.storage.ensureVectorSchema(probe.dimensions);
6673
+ this.embeddingDimensions = probe.dimensions;
6674
+ }
4690
6675
  this.initialized = true;
4691
6676
  this.telemetry.emitStartup(this.storage.name);
4692
6677
  } catch (error) {
@@ -4702,64 +6687,76 @@ var Wolbarg = class {
4702
6687
  );
4703
6688
  }
4704
6689
  }
4705
- /** Store a semantic memory for an agent. */
6690
+ /** Store a semantic memory for an agent (may upsert when dedupe is enabled). */
4706
6691
  async remember(options) {
4707
6692
  const trace = this.telemetry.start("remember");
4708
6693
  try {
4709
- const { storage, embedding, organization } = await this.requireReady();
4710
- assertNonEmptyString(options.agent, "agent");
4711
- if (!options.content || typeof options.content.text !== "string") {
4712
- throw new ValidationError("content.text must be a string");
4713
- }
4714
- assertNonEmptyString(options.content.text, "content.text");
4715
- const metadata = options.metadata ?? {};
4716
- if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) {
4717
- throw new ValidationError("metadata must be a plain object when provided");
4718
- }
4719
- const tEmbed = performance.now();
4720
- const vector = await embedding.embed(options.content.text);
4721
- trace.mark("embeddingMs", performance.now() - tEmbed);
4722
- this.assertEmbeddingDimensions(vector.length);
4723
- const id = createId();
4724
- const timestamp = nowIso();
4725
- const record = await this.withWriteLock(async () => {
4726
- const tStore = performance.now();
4727
- const row = await storage.insertMemory({
4728
- id,
4729
- organization,
4730
- agent: options.agent.trim(),
4731
- contentText: options.content.text,
4732
- metadata,
4733
- embedding: vector,
4734
- createdAt: timestamp,
4735
- updatedAt: timestamp
4736
- });
4737
- trace.mark("databaseWriteMs", performance.now() - tStore);
4738
- return toMemoryRecord(row);
4739
- });
6694
+ const result = await this.rememberOne(options, trace);
4740
6695
  trace.success({
4741
- provider: storage.name,
4742
- memoryIds: [record.id],
6696
+ provider: this.storage?.name,
6697
+ memoryIds: [result.id],
4743
6698
  returnedCount: 1,
4744
- embeddingProvider: embedding.model,
4745
- model: embedding.model,
4746
- metadata,
4747
- agentId: options.agent.trim(),
4748
- tags: telemetryTags(metadata)
6699
+ embeddingProvider: this.embedding?.model,
6700
+ model: this.embedding?.model,
6701
+ metadata: result.metadata,
6702
+ agentId: result.agent,
6703
+ tags: telemetryTags(result.metadata),
6704
+ extra: { upsertAction: result.action }
4749
6705
  });
4750
- return record;
6706
+ return result;
4751
6707
  } catch (error) {
4752
6708
  trace.failure(error, { agentId: options.agent });
4753
6709
  throw wrapOperationError("remember", error);
4754
6710
  }
4755
6711
  }
4756
- /** Batch remember — one parent event + child traces per item. */
6712
+ /** Batch remember — sequential when dedupe enabled; otherwise one TX batch. */
4757
6713
  async rememberBatch(items) {
4758
6714
  const parent = this.telemetry.start("rememberBatch");
4759
6715
  try {
4760
6716
  if (!Array.isArray(items) || items.length === 0) {
4761
6717
  throw new ValidationError("rememberBatch requires a non-empty array");
4762
6718
  }
6719
+ const anyDedupe = items.some((item) => {
6720
+ const cfg = resolveMemoryDedupeConfig(
6721
+ item.dedupe ?? this.memoryDedupe
6722
+ );
6723
+ return cfg.enabled;
6724
+ }) || this.memoryDedupe.enabled;
6725
+ if (anyDedupe) {
6726
+ const out = [];
6727
+ for (const item of items) {
6728
+ const child = parent.child("remember");
6729
+ const result = await this.rememberOne(item, child);
6730
+ child.success({
6731
+ provider: this.storage?.name,
6732
+ memoryIds: [result.id],
6733
+ returnedCount: 1,
6734
+ metadata: result.metadata,
6735
+ agentId: result.agent,
6736
+ tags: telemetryTags(result.metadata),
6737
+ extra: { upsertAction: result.action }
6738
+ });
6739
+ out.push(result);
6740
+ }
6741
+ parent.success({
6742
+ provider: this.storage?.name,
6743
+ memoryIds: out.map((r) => r.id),
6744
+ returnedCount: out.length,
6745
+ embeddingProvider: this.embedding?.model,
6746
+ model: this.embedding?.model,
6747
+ agentId: commonAgent(items.map((item) => item.agent.trim()))
6748
+ });
6749
+ this.emitChange({
6750
+ event: "remember",
6751
+ organization: this.organization,
6752
+ agent: commonAgent(items.map((i) => i.agent.trim())) ?? items[0].agent,
6753
+ memoryId: out.map((r) => r.id),
6754
+ timestamp: nowIso(),
6755
+ traceId: parent.context.traceId,
6756
+ sessionId: this.telemetry.sessionId
6757
+ });
6758
+ return out;
6759
+ }
4763
6760
  const { storage, embedding, organization } = await this.requireReady();
4764
6761
  const tEmbed = performance.now();
4765
6762
  const texts = items.map((item, i) => {
@@ -4784,7 +6781,9 @@ var Wolbarg = class {
4784
6781
  metadata: item.metadata ?? {},
4785
6782
  embedding: vectors[i],
4786
6783
  createdAt: timestamp,
4787
- updatedAt: timestamp
6784
+ updatedAt: timestamp,
6785
+ contentHash: null
6786
+ // batch path is append-only; dedupe uses sequential rememberOne
4788
6787
  }));
4789
6788
  for (let i = 0; i < inputs.length; i += 1) {
4790
6789
  const child = parent.child("remember");
@@ -4803,7 +6802,10 @@ var Wolbarg = class {
4803
6802
  parent.mark("databaseWriteMs", performance.now() - tStore);
4804
6803
  return result;
4805
6804
  });
4806
- const records = rows.map(toMemoryRecord);
6805
+ const records = rows.map((row) => ({
6806
+ ...toMemoryRecord(row),
6807
+ action: "created"
6808
+ }));
4807
6809
  parent.success({
4808
6810
  provider: storage.name,
4809
6811
  memoryIds: records.map((r) => r.id),
@@ -4812,12 +6814,379 @@ var Wolbarg = class {
4812
6814
  model: embedding.model,
4813
6815
  agentId: commonAgent(items.map((item) => item.agent.trim()))
4814
6816
  });
6817
+ this.emitChange({
6818
+ event: "remember",
6819
+ organization,
6820
+ agent: commonAgent(items.map((i) => i.agent.trim())) ?? items[0].agent,
6821
+ memoryId: records.map((r) => r.id),
6822
+ timestamp,
6823
+ traceId: parent.context.traceId,
6824
+ sessionId: this.telemetry.sessionId
6825
+ });
4815
6826
  return records;
4816
6827
  } catch (error) {
4817
6828
  parent.failure(error);
4818
6829
  throw wrapOperationError("rememberBatch", error);
4819
6830
  }
4820
6831
  }
6832
+ /**
6833
+ * Update an existing memory by id (re-embeds when content changes).
6834
+ */
6835
+ async update(options) {
6836
+ const trace = this.telemetry.start("remember");
6837
+ try {
6838
+ const { storage, embedding, organization } = await this.requireReady();
6839
+ assertNonEmptyString(options.id, "id");
6840
+ const existing = await storage.getMemoryById(options.id.trim(), organization);
6841
+ if (!existing) {
6842
+ throw new MemoryNotFoundError(`Memory not found: ${options.id}`);
6843
+ }
6844
+ const contentText = options.content?.text ?? existing.content_text;
6845
+ if (options.content) {
6846
+ assertNonEmptyString(options.content.text, "content.text");
6847
+ }
6848
+ const existingMeta = deserializeMetadata(existing.metadata_json);
6849
+ const metadata = options.metadata !== void 0 ? mergeMemoryMetadata(existingMeta, options.metadata) : existingMeta;
6850
+ let vector;
6851
+ if (options.content !== void 0) {
6852
+ const tEmbed = performance.now();
6853
+ vector = await embedding.embed(contentText);
6854
+ trace.mark("embeddingMs", performance.now() - tEmbed);
6855
+ this.assertEmbeddingDimensions(vector.length);
6856
+ }
6857
+ const timestamp = nowIso();
6858
+ const row = await this.withWriteLock(async () => {
6859
+ const tStore = performance.now();
6860
+ const updated = await storage.updateMemory({
6861
+ id: existing.id,
6862
+ organization,
6863
+ contentText: options.content !== void 0 ? contentText : void 0,
6864
+ metadata,
6865
+ embedding: vector,
6866
+ updatedAt: timestamp,
6867
+ contentHash: options.content !== void 0 ? hashMemoryContent(contentText) : void 0
6868
+ });
6869
+ trace.mark("databaseWriteMs", performance.now() - tStore);
6870
+ return updated;
6871
+ });
6872
+ if (!row) {
6873
+ throw new MemoryNotFoundError(`Memory not found: ${options.id}`);
6874
+ }
6875
+ const result = {
6876
+ ...toMemoryRecord(row),
6877
+ action: "updated"
6878
+ };
6879
+ this.emitChange({
6880
+ event: "update",
6881
+ organization,
6882
+ agent: result.agent,
6883
+ memoryId: result.id,
6884
+ timestamp,
6885
+ traceId: trace.context.traceId,
6886
+ sessionId: this.telemetry.sessionId,
6887
+ upsertAction: "updated"
6888
+ });
6889
+ trace.success({
6890
+ provider: storage.name,
6891
+ memoryIds: [result.id],
6892
+ returnedCount: 1,
6893
+ agentId: result.agent,
6894
+ extra: { upsertAction: "updated" }
6895
+ });
6896
+ return result;
6897
+ } catch (error) {
6898
+ trace.failure(error);
6899
+ throw wrapOperationError("update", error);
6900
+ }
6901
+ }
6902
+ /**
6903
+ * Subscribe to memory change events.
6904
+ *
6905
+ * **SQLite:** delivers events only within this Node.js process.
6906
+ * A second process writing the same `memory.db` will not notify subscribers here.
6907
+ */
6908
+ subscribe(filter, callback) {
6909
+ const org = filter.organization || this.organization;
6910
+ if (!org) {
6911
+ throw new ValidationError(
6912
+ "subscribe requires organization (set on Wolbarg or in filter)"
6913
+ );
6914
+ }
6915
+ const normalized = {
6916
+ ...filter,
6917
+ organization: org
6918
+ };
6919
+ if (this.storage instanceof PostgresStorageProvider) {
6920
+ if (!this.pgListenBackend) {
6921
+ const pool = this.storage.getPool?.();
6922
+ if (pool) {
6923
+ this.pgListenBackend = createPostgresListenerFromPool(pool);
6924
+ }
6925
+ }
6926
+ if (this.pgListenBackend) {
6927
+ return this.pgListenBackend.subscribe(normalized, callback);
6928
+ }
6929
+ }
6930
+ if (!this.subscribeBackend) {
6931
+ this.subscribeBackend = new SqliteSubscribeEmitter();
6932
+ }
6933
+ return this.subscribeBackend.subscribe(normalized, callback);
6934
+ }
6935
+ emitChange(event) {
6936
+ try {
6937
+ if (this.storage instanceof PostgresStorageProvider) {
6938
+ const listener = this.pgListenBackend;
6939
+ if (listener?.hasSubscribers?.()) {
6940
+ void this.storage.notifyChange(event).catch((error) => {
6941
+ console.error(
6942
+ "[wolbarg] NOTIFY failed:",
6943
+ error instanceof Error ? error.message : error
6944
+ );
6945
+ });
6946
+ }
6947
+ return;
6948
+ }
6949
+ this.subscribeBackend?.emit(event);
6950
+ } catch (error) {
6951
+ console.error(
6952
+ "[wolbarg] emitChange error:",
6953
+ error instanceof Error ? error.message : error
6954
+ );
6955
+ }
6956
+ }
6957
+ /** Internal remember with optional upsert/dedupe. */
6958
+ async rememberOne(options, trace) {
6959
+ const { storage, embedding, organization } = await this.requireReady();
6960
+ assertNonEmptyString(options.agent, "agent");
6961
+ if (!options.content || typeof options.content.text !== "string") {
6962
+ throw new ValidationError("content.text must be a string");
6963
+ }
6964
+ assertNonEmptyString(options.content.text, "content.text");
6965
+ const metadata = options.metadata ?? {};
6966
+ if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) {
6967
+ throw new ValidationError("metadata must be a plain object when provided");
6968
+ }
6969
+ const agent = options.agent.trim();
6970
+ const text = options.content.text;
6971
+ const dedupe = resolveMemoryDedupeConfig(
6972
+ options.dedupe !== void 0 ? options.dedupe : this.memoryDedupe
6973
+ );
6974
+ const tEmbed = performance.now();
6975
+ const vector = await embedding.embed(text);
6976
+ trace.mark("embeddingMs", performance.now() - tEmbed);
6977
+ this.assertEmbeddingDimensions(vector.length);
6978
+ const timestamp = nowIso();
6979
+ const contentHash = hashMemoryContent(text);
6980
+ if (!dedupe.enabled) {
6981
+ const id2 = createId();
6982
+ const record = await this.withWriteLock(async () => {
6983
+ const tStore = performance.now();
6984
+ const row = await storage.insertMemory({
6985
+ id: id2,
6986
+ organization,
6987
+ agent,
6988
+ contentText: text,
6989
+ metadata,
6990
+ embedding: vector,
6991
+ createdAt: timestamp,
6992
+ updatedAt: timestamp,
6993
+ // Null hash when dedupe is off — unique index allows duplicate text.
6994
+ contentHash: null
6995
+ });
6996
+ trace.mark("databaseWriteMs", performance.now() - tStore);
6997
+ return toMemoryRecord(row);
6998
+ });
6999
+ this.emitChange({
7000
+ event: "remember",
7001
+ organization,
7002
+ agent,
7003
+ memoryId: record.id,
7004
+ timestamp,
7005
+ traceId: trace.context.traceId,
7006
+ sessionId: this.telemetry.sessionId,
7007
+ upsertAction: "created"
7008
+ });
7009
+ return { ...record, action: "created" };
7010
+ }
7011
+ let existing = null;
7012
+ if (dedupe.strategy === "exact" || dedupe.strategy === "exact-or-near") {
7013
+ if (typeof storage.findActiveByContentHash === "function") {
7014
+ existing = await storage.findActiveByContentHash(
7015
+ organization,
7016
+ agent,
7017
+ contentHash
7018
+ );
7019
+ }
7020
+ }
7021
+ if (existing) {
7022
+ const merged = mergeMemoryMetadata(
7023
+ deserializeMetadata(existing.metadata_json),
7024
+ metadata
7025
+ );
7026
+ const row = await this.withWriteLock(async () => {
7027
+ const tStore = performance.now();
7028
+ const updated = await storage.updateMemory({
7029
+ id: existing.id,
7030
+ organization,
7031
+ metadata: merged,
7032
+ updatedAt: timestamp,
7033
+ contentHash
7034
+ });
7035
+ trace.mark("databaseWriteMs", performance.now() - tStore);
7036
+ return updated;
7037
+ }, { exclusive: true });
7038
+ const record = toMemoryRecord(row);
7039
+ this.emitChange({
7040
+ event: "update",
7041
+ organization,
7042
+ agent,
7043
+ memoryId: record.id,
7044
+ timestamp,
7045
+ traceId: trace.context.traceId,
7046
+ sessionId: this.telemetry.sessionId,
7047
+ upsertAction: "updated"
7048
+ });
7049
+ return { ...record, action: "updated" };
7050
+ }
7051
+ if (dedupe.strategy === "near" || dedupe.strategy === "exact-or-near") {
7052
+ const near = await this.findNearDuplicate(
7053
+ storage,
7054
+ organization,
7055
+ agent,
7056
+ vector,
7057
+ dedupe.nearThreshold,
7058
+ dedupe.nearCandidateLimit
7059
+ );
7060
+ if (near) {
7061
+ const merged = mergeMemoryMetadata(
7062
+ deserializeMetadata(near.metadata_json),
7063
+ metadata
7064
+ );
7065
+ const row = await this.withWriteLock(async () => {
7066
+ const tStore = performance.now();
7067
+ const updated = await storage.updateMemory({
7068
+ id: near.id,
7069
+ organization,
7070
+ contentText: text,
7071
+ metadata: merged,
7072
+ embedding: vector,
7073
+ updatedAt: timestamp,
7074
+ contentHash
7075
+ });
7076
+ trace.mark("databaseWriteMs", performance.now() - tStore);
7077
+ return updated;
7078
+ }, { exclusive: true });
7079
+ const record = toMemoryRecord(row);
7080
+ this.emitChange({
7081
+ event: "update",
7082
+ organization,
7083
+ agent,
7084
+ memoryId: record.id,
7085
+ timestamp,
7086
+ traceId: trace.context.traceId,
7087
+ sessionId: this.telemetry.sessionId,
7088
+ upsertAction: "updated"
7089
+ });
7090
+ return { ...record, action: "updated" };
7091
+ }
7092
+ }
7093
+ const id = createId();
7094
+ try {
7095
+ const record = await this.withWriteLock(async () => {
7096
+ const tStore = performance.now();
7097
+ const row = await storage.insertMemory({
7098
+ id,
7099
+ organization,
7100
+ agent,
7101
+ contentText: text,
7102
+ metadata,
7103
+ embedding: vector,
7104
+ createdAt: timestamp,
7105
+ updatedAt: timestamp,
7106
+ contentHash
7107
+ });
7108
+ trace.mark("databaseWriteMs", performance.now() - tStore);
7109
+ return toMemoryRecord(row);
7110
+ }, { exclusive: true });
7111
+ this.emitChange({
7112
+ event: "remember",
7113
+ organization,
7114
+ agent,
7115
+ memoryId: record.id,
7116
+ timestamp,
7117
+ traceId: trace.context.traceId,
7118
+ sessionId: this.telemetry.sessionId,
7119
+ upsertAction: "created"
7120
+ });
7121
+ return { ...record, action: "created" };
7122
+ } catch (error) {
7123
+ const msg = error instanceof Error ? error.message : String(error);
7124
+ if (msg.toLowerCase().includes("unique") && typeof storage.findActiveByContentHash === "function") {
7125
+ const raced = await storage.findActiveByContentHash(
7126
+ organization,
7127
+ agent,
7128
+ contentHash
7129
+ );
7130
+ if (raced) {
7131
+ const merged = mergeMemoryMetadata(
7132
+ deserializeMetadata(raced.metadata_json),
7133
+ metadata
7134
+ );
7135
+ const row = await this.withWriteLock(
7136
+ async () => storage.updateMemory({
7137
+ id: raced.id,
7138
+ organization,
7139
+ metadata: merged,
7140
+ updatedAt: nowIso(),
7141
+ contentHash
7142
+ }),
7143
+ { exclusive: true }
7144
+ );
7145
+ const record = toMemoryRecord(row);
7146
+ this.emitChange({
7147
+ event: "update",
7148
+ organization,
7149
+ agent,
7150
+ memoryId: record.id,
7151
+ timestamp: nowIso(),
7152
+ upsertAction: "updated"
7153
+ });
7154
+ return { ...record, action: "updated" };
7155
+ }
7156
+ }
7157
+ throw error;
7158
+ }
7159
+ }
7160
+ async findNearDuplicate(storage, organization, agent, vector, threshold, limit) {
7161
+ if (typeof storage.searchVectorsWithMemories === "function") {
7162
+ const hits2 = await storage.searchVectorsWithMemories(
7163
+ vector,
7164
+ limit,
7165
+ organization,
7166
+ { agent, includeArchived: false }
7167
+ );
7168
+ let best2 = null;
7169
+ for (const { row, distance } of hits2) {
7170
+ if (row.archived === 1) continue;
7171
+ const sim = distanceToSimilarity(distance);
7172
+ if (sim >= threshold && (!best2 || sim > best2.sim)) {
7173
+ best2 = { row, sim };
7174
+ }
7175
+ }
7176
+ return best2?.row ?? null;
7177
+ }
7178
+ const hits = await storage.searchVectors(vector, limit);
7179
+ let best = null;
7180
+ for (const hit of hits) {
7181
+ const row = await storage.getMemoryByRowid(hit.memoryRowid, organization);
7182
+ if (!row || row.archived === 1 || row.agent !== agent) continue;
7183
+ const sim = distanceToSimilarity(hit.distance);
7184
+ if (sim >= threshold && (!best || sim > best.sim)) {
7185
+ best = { row, sim };
7186
+ }
7187
+ }
7188
+ return best?.row ?? null;
7189
+ }
4821
7190
  async recall(options) {
4822
7191
  const trace = this.telemetry.start("recall");
4823
7192
  const explain = options.explain === true;
@@ -5205,6 +7574,14 @@ var Wolbarg = class {
5205
7574
  returnedCount: 1,
5206
7575
  agentId: options.agent.trim()
5207
7576
  });
7577
+ this.emitChange({
7578
+ event: "compress",
7579
+ organization,
7580
+ agent: options.agent.trim(),
7581
+ memoryId: [result.summary.id, ...result.archivedIds],
7582
+ timestamp,
7583
+ sessionId: this.telemetry.sessionId
7584
+ });
5208
7585
  return result;
5209
7586
  } catch (error) {
5210
7587
  trace.failure(error, { agentId: options.agent });
@@ -5299,9 +7676,54 @@ var Wolbarg = class {
5299
7676
  },
5300
7677
  embedding: vectors[i],
5301
7678
  createdAt: timestamp,
5302
- updatedAt: timestamp
7679
+ updatedAt: timestamp,
7680
+ contentHash: this.memoryDedupe.enabled ? hashMemoryContent(chunk.text) : null
5303
7681
  }));
5304
- const rows = await this.withWriteLock(async () => {
7682
+ let rows;
7683
+ if (this.memoryDedupe.enabled) {
7684
+ const memories = [];
7685
+ for (let i = 0; i < chunks.length; i += 1) {
7686
+ const result3 = await this.rememberOne(
7687
+ {
7688
+ agent: options.agent.trim(),
7689
+ content: { text: chunks[i].text },
7690
+ metadata: {
7691
+ ...baseMeta,
7692
+ ingest: true,
7693
+ chunkIndex: chunks[i].index,
7694
+ chunkCount: chunks.length,
7695
+ sourceFilename: loaded.filename ?? null
7696
+ }
7697
+ },
7698
+ trace
7699
+ );
7700
+ memories.push(result3);
7701
+ }
7702
+ const result2 = {
7703
+ memories,
7704
+ extractedChars: text.length,
7705
+ chunkCount: chunks.length,
7706
+ usedOcr,
7707
+ usedVision
7708
+ };
7709
+ trace.success({
7710
+ provider: storage.name,
7711
+ memoryIds: result2.memories.map((m) => m.id),
7712
+ returnedCount: result2.memories.length,
7713
+ agentId: options.agent.trim(),
7714
+ tags: telemetryTags(baseMeta)
7715
+ });
7716
+ this.emitChange({
7717
+ event: "ingest",
7718
+ organization,
7719
+ agent: options.agent.trim(),
7720
+ memoryId: result2.memories.map((m) => m.id),
7721
+ timestamp,
7722
+ sessionId: this.telemetry.sessionId
7723
+ });
7724
+ return result2;
7725
+ }
7726
+ rows = await this.withWriteLock(async () => {
5305
7727
  const tWrite = performance.now();
5306
7728
  const result2 = await storage.insertMemoriesBatch(inputs);
5307
7729
  trace.mark("databaseWriteMs", performance.now() - tWrite);
@@ -5321,6 +7743,14 @@ var Wolbarg = class {
5321
7743
  agentId: options.agent.trim(),
5322
7744
  tags: telemetryTags(baseMeta)
5323
7745
  });
7746
+ this.emitChange({
7747
+ event: "ingest",
7748
+ organization,
7749
+ agent: options.agent.trim(),
7750
+ memoryId: result.memories.map((m) => m.id),
7751
+ timestamp,
7752
+ sessionId: this.telemetry.sessionId
7753
+ });
5324
7754
  return result;
5325
7755
  } catch (error) {
5326
7756
  trace.failure(error, { agentId: options.agent });
@@ -5359,6 +7789,16 @@ var Wolbarg = class {
5359
7789
  filters: "filter" in options ? options.filter : null,
5360
7790
  agentId: "filter" in options ? options.filter?.agent?.trim() ?? null : null
5361
7791
  });
7792
+ if (deleted > 0) {
7793
+ this.emitChange({
7794
+ event: "forget",
7795
+ organization,
7796
+ agent: "filter" in options && options.filter?.agent?.trim() || ("id" in options ? "*" : "*"),
7797
+ memoryId: "id" in options && options.id ? options.id.trim() : [],
7798
+ timestamp: nowIso(),
7799
+ sessionId: this.telemetry.sessionId
7800
+ });
7801
+ }
5362
7802
  return deleted;
5363
7803
  } catch (error) {
5364
7804
  trace.failure(error, {
@@ -5632,8 +8072,13 @@ var Wolbarg = class {
5632
8072
  get sessionId() {
5633
8073
  return this.telemetry.sessionId;
5634
8074
  }
5635
- withWriteLock(fn) {
5636
- if (this.storage?.name === "sqlite") {
8075
+ /**
8076
+ * Optional process-wide lock for SQLite read-modify-write (dedupe upsert).
8077
+ * Plain inserts must NOT take this lock — the provider coalesces concurrent
8078
+ * insertMemory calls under a single BEGIN IMMEDIATE for multi-writer throughput.
8079
+ */
8080
+ withWriteLock(fn, options) {
8081
+ if (options?.exclusive && this.storage?.name === "sqlite") {
5637
8082
  return this.writeMutex.runExclusive(fn);
5638
8083
  }
5639
8084
  return fn();
@@ -5642,6 +8087,10 @@ var Wolbarg = class {
5642
8087
  if (this.storage) {
5643
8088
  this.telemetry.emitShutdown(this.storage.name);
5644
8089
  }
8090
+ await this.subscribeBackend?.close().catch(() => void 0);
8091
+ await this.pgListenBackend?.close().catch(() => void 0);
8092
+ this.subscribeBackend = null;
8093
+ this.pgListenBackend = null;
5645
8094
  await this.telemetry.close().catch(() => void 0);
5646
8095
  await this.checkpointProvider?.close().catch(() => void 0);
5647
8096
  if (this.storage) {
@@ -5649,6 +8098,7 @@ var Wolbarg = class {
5649
8098
  }
5650
8099
  this.storage = null;
5651
8100
  this.embedding = null;
8101
+ this.rawEmbedding = null;
5652
8102
  this.llm = null;
5653
8103
  this.compression = null;
5654
8104
  this.organization = null;
@@ -6273,6 +8723,7 @@ exports.SqliteDatabaseProvider = SqliteDatabaseProvider;
6273
8723
  exports.SqliteEventDatabase = SqliteEventDatabase;
6274
8724
  exports.SqliteStorageProvider = SqliteStorageProvider;
6275
8725
  exports.SqliteTelemetryProvider = SqliteTelemetryProvider;
8726
+ exports.StorageLockedError = StorageLockedError;
6276
8727
  exports.TelemetryEmitter = TelemetryEmitter;
6277
8728
  exports.ValidationError = ValidationError;
6278
8729
  exports.Wolbarg = Wolbarg;