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