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/CHANGELOG.md +22 -0
- package/dist/index.cjs +2910 -459
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +261 -23
- package/dist/index.d.ts +261 -23
- package/dist/index.js +2910 -460
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
991
|
-
`CREATE INDEX IF NOT EXISTS
|
|
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
|
|
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 =
|
|
2104
|
+
PRAGMA busy_timeout = ${this.concurrency.lockTimeoutMs};
|
|
1375
2105
|
PRAGMA temp_store = MEMORY;
|
|
1376
|
-
PRAGMA cache_size = -
|
|
1377
|
-
PRAGMA mmap_size =
|
|
1378
|
-
PRAGMA wal_autocheckpoint =
|
|
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
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
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 (
|
|
1502
|
-
const
|
|
1503
|
-
|
|
1504
|
-
|
|
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
|
|
1617
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
1683
|
-
if (hits.length === 0) {
|
|
2434
|
+
if (topK <= 0) {
|
|
1684
2435
|
return [];
|
|
1685
2436
|
}
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
)
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
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
|
-
|
|
1754
|
-
|
|
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
|
-
|
|
1775
|
-
|
|
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
|
|
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(
|
|
1807
|
-
activeMemories: Number(active
|
|
1808
|
-
archivedMemories: Number(archived
|
|
1809
|
-
totalAgents: Number(agents
|
|
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
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1890
|
-
|
|
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
|
-
|
|
1893
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2839
|
+
deleteFtsByOrg,
|
|
2840
|
+
deleteFtsByOrgAgent,
|
|
2841
|
+
searchFts,
|
|
2842
|
+
deleteEmbeddingsByOrg,
|
|
2843
|
+
deleteEmbeddingsByOrgAgent,
|
|
2844
|
+
deleteEmbeddingsBlobByOrg,
|
|
2845
|
+
deleteEmbeddingsBlobByOrgAgent
|
|
1965
2846
|
};
|
|
1966
2847
|
}
|
|
1967
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
2159
|
-
if (
|
|
2160
|
-
return
|
|
3656
|
+
async handleDisconnect() {
|
|
3657
|
+
if (this.closed || this.reconnecting) {
|
|
3658
|
+
return;
|
|
2161
3659
|
}
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
if (
|
|
2166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
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
|
-
|
|
2177
|
-
if (
|
|
2178
|
-
throw new
|
|
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
|
-
|
|
2184
|
-
|
|
2185
|
-
return
|
|
3710
|
+
/** True when at least one subscribe() callback is registered. */
|
|
3711
|
+
hasSubscribers() {
|
|
3712
|
+
return this.subscriptions.size > 0;
|
|
2186
3713
|
}
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
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
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
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: "
|
|
2205
|
-
insertBatch: "
|
|
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
|
|
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, $
|
|
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
|
|
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
|
|
2255
|
-
FROM unnest($
|
|
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[], $
|
|
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
|
|
2269
|
-
var
|
|
2270
|
-
var
|
|
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
|
-
|
|
3878
|
+
insertFlushTimer = null;
|
|
3879
|
+
insertFlushInFlight = 0;
|
|
2285
3880
|
constructor(options) {
|
|
2286
|
-
this.
|
|
2287
|
-
this.
|
|
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
|
-
|
|
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 =
|
|
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 >=
|
|
2474
|
-
|
|
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
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
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.
|
|
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
|
|
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
|
|
4211
|
+
this.insertFlushInFlight -= 1;
|
|
2515
4212
|
if (this.insertQueue.length > 0) {
|
|
2516
|
-
this.
|
|
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
|
-
|
|
2537
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
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
|
-
/**
|
|
4301
|
+
/** Parallel unnest when HNSW is absent; sequential when index must stay consistent. */
|
|
2604
4302
|
async insertBatchChunked(inputs) {
|
|
2605
|
-
const chunkSize =
|
|
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
|
-
|
|
2614
|
-
|
|
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
|
|
2619
|
-
|
|
2620
|
-
|
|
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
|
-
|
|
2677
|
-
|
|
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
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
const
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
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
|
|
2854
|
-
const
|
|
2855
|
-
|
|
2856
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
2875
|
-
const
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
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
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
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
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
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
|
|
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
|
|
4742
|
-
memoryIds: [
|
|
6696
|
+
provider: this.storage?.name,
|
|
6697
|
+
memoryIds: [result.id],
|
|
4743
6698
|
returnedCount: 1,
|
|
4744
|
-
embeddingProvider: embedding
|
|
4745
|
-
model: embedding
|
|
4746
|
-
metadata,
|
|
4747
|
-
agentId:
|
|
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
|
|
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 —
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
5636
|
-
|
|
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;
|