mnemocyte 0.0.8 → 0.1.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/README.md +109 -1
- package/dist/index.d.mts +263 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +492 -60
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -1
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { drizzle } from "drizzle-orm/postgres-js";
|
|
2
2
|
import postgres from "postgres";
|
|
3
|
-
import { and, eq, gt, inArray, isNotNull, isNull, lt, or, sql } from "drizzle-orm";
|
|
3
|
+
import { and, desc, eq, gt, inArray, isNotNull, isNull, lt, or, sql } from "drizzle-orm";
|
|
4
4
|
import { customType, index, integer, jsonb, pgTable, real, text, timestamp } from "drizzle-orm/pg-core";
|
|
5
5
|
//#region \0rolldown/runtime.js
|
|
6
6
|
var __defProp = Object.defineProperty;
|
|
@@ -74,6 +74,56 @@ function createDatabase(databaseUrl) {
|
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
76
|
//#endregion
|
|
77
|
+
//#region src/errors.ts
|
|
78
|
+
/**
|
|
79
|
+
* Error type thrown by every public Mnemocyte API.
|
|
80
|
+
*
|
|
81
|
+
* Use the {@link MnemocyteError.code} property — not the message — to branch
|
|
82
|
+
* on failure modes, or call {@link isMnemocyteError} as a type guard when
|
|
83
|
+
* working with `unknown` errors.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* try {
|
|
88
|
+
* await client.recall({ entityId: "u1", query: "hi" });
|
|
89
|
+
* } catch (err) {
|
|
90
|
+
* if (isMnemocyteError(err) && err.code === "DB") {
|
|
91
|
+
* // handle database failure
|
|
92
|
+
* }
|
|
93
|
+
* throw err;
|
|
94
|
+
* }
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
var MnemocyteError = class extends Error {
|
|
98
|
+
/** Stable, machine-readable failure code. See {@link MnemocyteErrorCode}. */
|
|
99
|
+
code;
|
|
100
|
+
/** Underlying cause, if any (e.g. an original driver or embedder error). */
|
|
101
|
+
cause;
|
|
102
|
+
/**
|
|
103
|
+
* @param message - Human-readable error message.
|
|
104
|
+
* @param code - Stable failure code; see {@link MnemocyteErrorCode}.
|
|
105
|
+
* @param cause - Optional underlying cause (driver error, network error, etc.).
|
|
106
|
+
*/
|
|
107
|
+
constructor(message, code, cause) {
|
|
108
|
+
super(message);
|
|
109
|
+
this.name = "MnemocyteError";
|
|
110
|
+
this.code = code;
|
|
111
|
+
this.cause = cause;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Type guard for {@link MnemocyteError}.
|
|
116
|
+
*
|
|
117
|
+
* Prefer this over `instanceof` when working across module boundaries or
|
|
118
|
+
* bundlers where multiple copies of the class may exist.
|
|
119
|
+
*
|
|
120
|
+
* @param error - Value to test (typically a caught `unknown`).
|
|
121
|
+
* @returns `true` if `error` is a {@link MnemocyteError}.
|
|
122
|
+
*/
|
|
123
|
+
function isMnemocyteError(error) {
|
|
124
|
+
return error instanceof MnemocyteError;
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
77
127
|
//#region src/context/formatter.ts
|
|
78
128
|
function escapeXml(value) {
|
|
79
129
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
@@ -152,56 +202,6 @@ async function buildContext(options) {
|
|
|
152
202
|
return trimToTokenBudget(formatContext(await options.recall(options.input), options.input.query, format), maxTokens, tokenCounter);
|
|
153
203
|
}
|
|
154
204
|
//#endregion
|
|
155
|
-
//#region src/errors.ts
|
|
156
|
-
/**
|
|
157
|
-
* Error type thrown by every public Mnemocyte API.
|
|
158
|
-
*
|
|
159
|
-
* Use the {@link MnemocyteError.code} property — not the message — to branch
|
|
160
|
-
* on failure modes, or call {@link isMnemocyteError} as a type guard when
|
|
161
|
-
* working with `unknown` errors.
|
|
162
|
-
*
|
|
163
|
-
* @example
|
|
164
|
-
* ```ts
|
|
165
|
-
* try {
|
|
166
|
-
* await client.recall({ entityId: "u1", query: "hi" });
|
|
167
|
-
* } catch (err) {
|
|
168
|
-
* if (isMnemocyteError(err) && err.code === "DB") {
|
|
169
|
-
* // handle database failure
|
|
170
|
-
* }
|
|
171
|
-
* throw err;
|
|
172
|
-
* }
|
|
173
|
-
* ```
|
|
174
|
-
*/
|
|
175
|
-
var MnemocyteError = class extends Error {
|
|
176
|
-
/** Stable, machine-readable failure code. See {@link MnemocyteErrorCode}. */
|
|
177
|
-
code;
|
|
178
|
-
/** Underlying cause, if any (e.g. an original driver or embedder error). */
|
|
179
|
-
cause;
|
|
180
|
-
/**
|
|
181
|
-
* @param message - Human-readable error message.
|
|
182
|
-
* @param code - Stable failure code; see {@link MnemocyteErrorCode}.
|
|
183
|
-
* @param cause - Optional underlying cause (driver error, network error, etc.).
|
|
184
|
-
*/
|
|
185
|
-
constructor(message, code, cause) {
|
|
186
|
-
super(message);
|
|
187
|
-
this.name = "MnemocyteError";
|
|
188
|
-
this.code = code;
|
|
189
|
-
this.cause = cause;
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
/**
|
|
193
|
-
* Type guard for {@link MnemocyteError}.
|
|
194
|
-
*
|
|
195
|
-
* Prefer this over `instanceof` when working across module boundaries or
|
|
196
|
-
* bundlers where multiple copies of the class may exist.
|
|
197
|
-
*
|
|
198
|
-
* @param error - Value to test (typically a caught `unknown`).
|
|
199
|
-
* @returns `true` if `error` is a {@link MnemocyteError}.
|
|
200
|
-
*/
|
|
201
|
-
function isMnemocyteError(error) {
|
|
202
|
-
return error instanceof MnemocyteError;
|
|
203
|
-
}
|
|
204
|
-
//#endregion
|
|
205
205
|
//#region src/observability.ts
|
|
206
206
|
function withMetadata(event, metadata) {
|
|
207
207
|
return {
|
|
@@ -442,6 +442,10 @@ function normalizeTags(tags) {
|
|
|
442
442
|
function createId() {
|
|
443
443
|
return `mem_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
444
444
|
}
|
|
445
|
+
/** Generate a unique audit-event id (`evt_*`). */
|
|
446
|
+
function createEventId() {
|
|
447
|
+
return `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
448
|
+
}
|
|
445
449
|
function isExpired(memory, now) {
|
|
446
450
|
return memory.expiresAt !== null && memory.expiresAt.getTime() <= now.getTime();
|
|
447
451
|
}
|
|
@@ -450,6 +454,7 @@ function cloneMemory(memory) {
|
|
|
450
454
|
...memory,
|
|
451
455
|
tags: [...memory.tags],
|
|
452
456
|
metadata: { ...memory.metadata },
|
|
457
|
+
supersededAt: memory.supersededAt ? new Date(memory.supersededAt) : null,
|
|
453
458
|
expiresAt: memory.expiresAt ? new Date(memory.expiresAt) : null,
|
|
454
459
|
lastAccessedAt: memory.lastAccessedAt ? new Date(memory.lastAccessedAt) : null,
|
|
455
460
|
createdAt: new Date(memory.createdAt),
|
|
@@ -470,6 +475,7 @@ function rowToMemory(row) {
|
|
|
470
475
|
embeddingModel: row.embeddingModel,
|
|
471
476
|
embeddingDimensions: row.embeddingDimensions,
|
|
472
477
|
supersededBy: row.supersededBy,
|
|
478
|
+
supersededAt: row.supersededAt,
|
|
473
479
|
expiresAt: row.expiresAt,
|
|
474
480
|
lastAccessedAt: row.lastAccessedAt,
|
|
475
481
|
accessCount: row.accessCount,
|
|
@@ -546,6 +552,49 @@ function matchesPruneFilter(memory, input, now) {
|
|
|
546
552
|
if (input.maxImportance !== void 0 && IMPORTANCE_RANK[memory.importance] > IMPORTANCE_RANK[input.maxImportance]) return false;
|
|
547
553
|
return true;
|
|
548
554
|
}
|
|
555
|
+
/**
|
|
556
|
+
* Validate a {@link FindDuplicatesInput} and throw a `"VALIDATION"`
|
|
557
|
+
* {@link MnemocyteError} for malformed `threshold` / `limit` values.
|
|
558
|
+
*/
|
|
559
|
+
function validateFindDuplicatesInput(input) {
|
|
560
|
+
assertNonEmptyString(input.entityId, "entityId");
|
|
561
|
+
if (input.threshold !== void 0) {
|
|
562
|
+
if (!Number.isFinite(input.threshold) || input.threshold < 0 || input.threshold > 1) throw new MnemocyteError("threshold must be a number between 0 and 1.", "VALIDATION");
|
|
563
|
+
}
|
|
564
|
+
if (input.limit !== void 0) assertLimit(input.limit);
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Return `true` when a memory should participate in a duplicate scan,
|
|
568
|
+
* given the filters on `input` and a shared `now` timestamp.
|
|
569
|
+
*/
|
|
570
|
+
function matchesDuplicateFilter(memory, input, now) {
|
|
571
|
+
if (memory.entityId !== input.entityId) return false;
|
|
572
|
+
if (input.includeSuperseded !== true && memory.supersededBy !== null) return false;
|
|
573
|
+
if (input.includeExpired !== true && isExpired(memory, now)) return false;
|
|
574
|
+
if (input.types !== void 0 && !input.types.includes(memory.type)) return false;
|
|
575
|
+
if (input.tags !== void 0 && input.tags.length > 0 && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Validate a {@link ConsolidateInput} and throw a `"VALIDATION"`
|
|
580
|
+
* {@link MnemocyteError} for malformed inputs (empty `supersededIds`
|
|
581
|
+
* or a `survivorId` appearing in `supersededIds`).
|
|
582
|
+
*/
|
|
583
|
+
function validateConsolidateInput(input) {
|
|
584
|
+
assertNonEmptyString(input.entityId, "entityId");
|
|
585
|
+
assertNonEmptyString(input.survivorId, "survivorId");
|
|
586
|
+
if (input.supersededIds.length === 0) throw new MnemocyteError("supersededIds must contain at least one memory id.", "VALIDATION");
|
|
587
|
+
if (input.supersededIds.includes(input.survivorId)) throw new MnemocyteError("survivorId must not appear in supersededIds.", "VALIDATION");
|
|
588
|
+
for (const id of input.supersededIds) assertNonEmptyString(id, "supersededIds[*]");
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Validate a {@link ListAuditLogInput} and throw a `"VALIDATION"`
|
|
592
|
+
* {@link MnemocyteError} when malformed.
|
|
593
|
+
*/
|
|
594
|
+
function validateListAuditLogInput(input) {
|
|
595
|
+
assertNonEmptyString(input.entityId, "entityId");
|
|
596
|
+
if (input.limit !== void 0) assertLimit(input.limit);
|
|
597
|
+
}
|
|
549
598
|
function matchesRecallFilter(memory, input, now) {
|
|
550
599
|
if (memory.entityId !== input.entityId) return false;
|
|
551
600
|
if (!input.includeSuperseded && memory.supersededBy !== null) return false;
|
|
@@ -658,10 +707,30 @@ function toScoredMemory(memory, vector, lexical, input, config) {
|
|
|
658
707
|
//#region src/memory/in-memory.ts
|
|
659
708
|
function createInMemoryClient(config) {
|
|
660
709
|
const memories = /* @__PURE__ */ new Map();
|
|
710
|
+
const auditLog = [];
|
|
661
711
|
let closed = false;
|
|
662
712
|
function assertOpen() {
|
|
663
713
|
if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
|
|
664
714
|
}
|
|
715
|
+
function recordAudit(entityId, description, metadata) {
|
|
716
|
+
if (config.audit?.enabled !== true) return;
|
|
717
|
+
auditLog.push({
|
|
718
|
+
id: createEventId(),
|
|
719
|
+
entityId,
|
|
720
|
+
description,
|
|
721
|
+
metadata,
|
|
722
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
function cloneAuditEvent(event) {
|
|
726
|
+
return {
|
|
727
|
+
id: event.id,
|
|
728
|
+
entityId: event.entityId,
|
|
729
|
+
description: event.description,
|
|
730
|
+
metadata: { ...event.metadata },
|
|
731
|
+
timestamp: new Date(event.timestamp)
|
|
732
|
+
};
|
|
733
|
+
}
|
|
665
734
|
async function remember(input) {
|
|
666
735
|
return observe(config, "in-memory", "remember", { entityId: input.entityId }, async () => {
|
|
667
736
|
assertOpen();
|
|
@@ -684,6 +753,7 @@ function createInMemoryClient(config) {
|
|
|
684
753
|
embeddingModel: config.embedder.model,
|
|
685
754
|
embeddingDimensions: config.embedder.dimensions,
|
|
686
755
|
supersededBy: null,
|
|
756
|
+
supersededAt: null,
|
|
687
757
|
expiresAt: input.expiresAt ?? null,
|
|
688
758
|
lastAccessedAt: null,
|
|
689
759
|
accessCount: 0,
|
|
@@ -692,6 +762,11 @@ function createInMemoryClient(config) {
|
|
|
692
762
|
embedding
|
|
693
763
|
};
|
|
694
764
|
memories.set(memory.id, memory);
|
|
765
|
+
recordAudit(memory.entityId, "memory.created", {
|
|
766
|
+
memoryId: memory.id,
|
|
767
|
+
type: memory.type,
|
|
768
|
+
importance: memory.importance
|
|
769
|
+
});
|
|
695
770
|
return cloneMemory(memory);
|
|
696
771
|
}, (memory) => ({
|
|
697
772
|
memoryId: memory.id,
|
|
@@ -759,6 +834,7 @@ function createInMemoryClient(config) {
|
|
|
759
834
|
const memory = memories.get(input.memoryId);
|
|
760
835
|
if (!memory || memory.entityId !== input.entityId) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
|
|
761
836
|
memories.delete(input.memoryId);
|
|
837
|
+
recordAudit(memory.entityId, "memory.deleted", { memoryId: memory.id });
|
|
762
838
|
});
|
|
763
839
|
},
|
|
764
840
|
async forgetAll(input) {
|
|
@@ -770,6 +846,7 @@ function createInMemoryClient(config) {
|
|
|
770
846
|
memories.delete(id);
|
|
771
847
|
deleted += 1;
|
|
772
848
|
}
|
|
849
|
+
recordAudit(input.entityId, "entity.cleared", { count: deleted });
|
|
773
850
|
return deleted;
|
|
774
851
|
}, (count) => ({ count })).then(() => void 0);
|
|
775
852
|
},
|
|
@@ -781,7 +858,10 @@ function createInMemoryClient(config) {
|
|
|
781
858
|
const matched = [];
|
|
782
859
|
for (const memory of memories.values()) if (matchesPruneFilter(memory, input, now)) matched.push(memory.id);
|
|
783
860
|
const dryRun = input.dryRun === true;
|
|
784
|
-
if (!dryRun)
|
|
861
|
+
if (!dryRun) {
|
|
862
|
+
for (const id of matched) memories.delete(id);
|
|
863
|
+
if (matched.length > 0 && input.entityId !== void 0) recordAudit(input.entityId, "memory.pruned", { count: matched.length });
|
|
864
|
+
}
|
|
785
865
|
return {
|
|
786
866
|
matchedCount: matched.length,
|
|
787
867
|
deletedCount: dryRun ? 0 : matched.length,
|
|
@@ -789,6 +869,53 @@ function createInMemoryClient(config) {
|
|
|
789
869
|
};
|
|
790
870
|
}, (result) => ({ count: result.deletedCount }));
|
|
791
871
|
},
|
|
872
|
+
async findDuplicates(input) {
|
|
873
|
+
return observe(config, "in-memory", "findDuplicates", { entityId: input.entityId }, async () => {
|
|
874
|
+
assertOpen();
|
|
875
|
+
validateFindDuplicatesInput(input);
|
|
876
|
+
const threshold = input.threshold ?? .95;
|
|
877
|
+
const limit = input.limit ?? 100;
|
|
878
|
+
const now = /* @__PURE__ */ new Date();
|
|
879
|
+
const candidates = Array.from(memories.values()).filter((memory) => matchesDuplicateFilter(memory, input, now));
|
|
880
|
+
const pairs = [];
|
|
881
|
+
for (let i = 0; i < candidates.length; i += 1) {
|
|
882
|
+
const a = candidates[i];
|
|
883
|
+
if (a === void 0) continue;
|
|
884
|
+
for (let j = i + 1; j < candidates.length; j += 1) {
|
|
885
|
+
const b = candidates[j];
|
|
886
|
+
if (b === void 0) continue;
|
|
887
|
+
const similarity = Math.max(0, Math.min(1, cosineSimilarity(a.embedding, b.embedding)));
|
|
888
|
+
if (similarity >= threshold) pairs.push({
|
|
889
|
+
a: cloneMemory(a),
|
|
890
|
+
b: cloneMemory(b),
|
|
891
|
+
similarity
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
pairs.sort((x, y) => y.similarity - x.similarity);
|
|
896
|
+
return pairs.slice(0, limit);
|
|
897
|
+
}, (result) => ({ count: result.length }));
|
|
898
|
+
},
|
|
899
|
+
async listAuditLog(input) {
|
|
900
|
+
return observe(config, "in-memory", "listAuditLog", { entityId: input.entityId }, async () => {
|
|
901
|
+
assertOpen();
|
|
902
|
+
validateListAuditLogInput(input);
|
|
903
|
+
const limit = input.limit ?? 50;
|
|
904
|
+
return auditLog.map((event, idx) => ({
|
|
905
|
+
event,
|
|
906
|
+
idx
|
|
907
|
+
})).filter(({ event }) => {
|
|
908
|
+
if (event.entityId !== input.entityId) return false;
|
|
909
|
+
if (input.before !== void 0 && event.timestamp.getTime() >= input.before.getTime()) return false;
|
|
910
|
+
if (input.after !== void 0 && event.timestamp.getTime() <= input.after.getTime()) return false;
|
|
911
|
+
return true;
|
|
912
|
+
}).sort((a, b) => {
|
|
913
|
+
const dt = b.event.timestamp.getTime() - a.event.timestamp.getTime();
|
|
914
|
+
if (dt !== 0) return dt;
|
|
915
|
+
return b.idx - a.idx;
|
|
916
|
+
}).slice(0, limit).map(({ event }) => cloneAuditEvent(event));
|
|
917
|
+
}, (result) => ({ count: result.length }));
|
|
918
|
+
},
|
|
792
919
|
async stats(input) {
|
|
793
920
|
return observe(config, "in-memory", "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
|
|
794
921
|
assertOpen();
|
|
@@ -814,6 +941,7 @@ function createInMemoryClient(config) {
|
|
|
814
941
|
};
|
|
815
942
|
});
|
|
816
943
|
},
|
|
944
|
+
experimental: createExperimental(),
|
|
817
945
|
async close() {
|
|
818
946
|
return observe(config, "in-memory", "close", {}, async () => {
|
|
819
947
|
closed = true;
|
|
@@ -821,11 +949,62 @@ function createInMemoryClient(config) {
|
|
|
821
949
|
});
|
|
822
950
|
}
|
|
823
951
|
};
|
|
952
|
+
function createExperimental() {
|
|
953
|
+
return { async consolidate(input) {
|
|
954
|
+
return observe(config, "in-memory", "consolidate", {
|
|
955
|
+
entityId: input.entityId,
|
|
956
|
+
memoryId: input.survivorId
|
|
957
|
+
}, async () => {
|
|
958
|
+
assertOpen();
|
|
959
|
+
validateConsolidateInput(input);
|
|
960
|
+
const survivor = memories.get(input.survivorId);
|
|
961
|
+
if (!survivor || survivor.entityId !== input.entityId) throw new MnemocyteError("Survivor memory was not found.", "NOT_FOUND");
|
|
962
|
+
if (survivor.supersededBy !== null) throw new MnemocyteError("Survivor memory is itself superseded.", "VALIDATION");
|
|
963
|
+
const losers = [];
|
|
964
|
+
for (const id of input.supersededIds) {
|
|
965
|
+
const memory = memories.get(id);
|
|
966
|
+
if (!memory || memory.entityId !== input.entityId) throw new MnemocyteError("Superseded memory was not found.", "NOT_FOUND");
|
|
967
|
+
if (memory.supersededBy !== null) continue;
|
|
968
|
+
losers.push(memory);
|
|
969
|
+
}
|
|
970
|
+
const now = /* @__PURE__ */ new Date();
|
|
971
|
+
const newSupersededIds = [];
|
|
972
|
+
for (const loser of losers) {
|
|
973
|
+
loser.supersededBy = survivor.id;
|
|
974
|
+
loser.supersededAt = now;
|
|
975
|
+
loser.updatedAt = now;
|
|
976
|
+
newSupersededIds.push(loser.id);
|
|
977
|
+
recordAudit(input.entityId, "memory.superseded", {
|
|
978
|
+
memoryId: loser.id,
|
|
979
|
+
supersededBy: survivor.id
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
if (input.mergeTags !== false && losers.length > 0) {
|
|
983
|
+
const mergedTags = new Set(survivor.tags);
|
|
984
|
+
for (const loser of losers) for (const tag of loser.tags) mergedTags.add(tag);
|
|
985
|
+
if (mergedTags.size !== survivor.tags.length) {
|
|
986
|
+
survivor.tags = [...mergedTags];
|
|
987
|
+
survivor.updatedAt = now;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return {
|
|
991
|
+
survivorId: survivor.id,
|
|
992
|
+
supersededCount: newSupersededIds.length,
|
|
993
|
+
supersededIds: newSupersededIds
|
|
994
|
+
};
|
|
995
|
+
}, (result) => ({ count: result.supersededCount }));
|
|
996
|
+
} };
|
|
997
|
+
}
|
|
824
998
|
}
|
|
825
999
|
//#endregion
|
|
826
1000
|
//#region src/db/queries/events.ts
|
|
827
|
-
async function
|
|
828
|
-
|
|
1001
|
+
async function insertEvent(db, row) {
|
|
1002
|
+
const inserted = (await db.insert(eventsTable).values(row).returning())[0];
|
|
1003
|
+
if (!inserted) throw new Error("Event insert returned no rows.");
|
|
1004
|
+
return inserted;
|
|
1005
|
+
}
|
|
1006
|
+
async function listEvents(db, filter) {
|
|
1007
|
+
return db.select().from(eventsTable).where(and(eq(eventsTable.entityId, filter.entityId), filter.before ? lt(eventsTable.timestamp, filter.before) : void 0, filter.after ? gt(eventsTable.timestamp, filter.after) : void 0)).orderBy(desc(eventsTable.timestamp)).limit(filter.limit ?? 50);
|
|
829
1008
|
}
|
|
830
1009
|
//#endregion
|
|
831
1010
|
//#region src/db/queries/memories.ts
|
|
@@ -840,6 +1019,9 @@ async function insertMemory(db, row) {
|
|
|
840
1019
|
if (!inserted) throw new Error("Memory insert returned no rows.");
|
|
841
1020
|
return inserted;
|
|
842
1021
|
}
|
|
1022
|
+
async function getMemoryById(db, entityId, memoryId) {
|
|
1023
|
+
return (await db.select().from(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), eq(memoriesTable.id, memoryId))).limit(1))[0] ?? null;
|
|
1024
|
+
}
|
|
843
1025
|
async function listMemories(db, filter) {
|
|
844
1026
|
return db.select().from(memoriesTable).where(filterConditions(filter));
|
|
845
1027
|
}
|
|
@@ -858,6 +1040,136 @@ async function countPruneMatches(db, filter) {
|
|
|
858
1040
|
async function pruneMemories(db, filter) {
|
|
859
1041
|
return (await db.delete(memoriesTable).where(pruneConditions(filter)).returning({ id: memoriesTable.id })).length;
|
|
860
1042
|
}
|
|
1043
|
+
async function findDuplicatePairs(db, input) {
|
|
1044
|
+
return (await db.execute(sql`
|
|
1045
|
+
SELECT
|
|
1046
|
+
a.id AS "aId",
|
|
1047
|
+
a.entity_id AS "aEntityId",
|
|
1048
|
+
a.content AS "aContent",
|
|
1049
|
+
a.type AS "aType",
|
|
1050
|
+
a.importance AS "aImportance",
|
|
1051
|
+
a.tags AS "aTags",
|
|
1052
|
+
a.source AS "aSource",
|
|
1053
|
+
a.metadata AS "aMetadata",
|
|
1054
|
+
a.confidence AS "aConfidence",
|
|
1055
|
+
a.embedding AS "aEmbedding",
|
|
1056
|
+
a.embedding_model AS "aEmbeddingModel",
|
|
1057
|
+
a.embedding_dimensions AS "aEmbeddingDimensions",
|
|
1058
|
+
a.superseded_by AS "aSupersededBy",
|
|
1059
|
+
a.superseded_at AS "aSupersededAt",
|
|
1060
|
+
a.expires_at AS "aExpiresAt",
|
|
1061
|
+
a.last_accessed_at AS "aLastAccessedAt",
|
|
1062
|
+
a.access_count AS "aAccessCount",
|
|
1063
|
+
a.created_at AS "aCreatedAt",
|
|
1064
|
+
a.updated_at AS "aUpdatedAt",
|
|
1065
|
+
b.id AS "bId",
|
|
1066
|
+
b.entity_id AS "bEntityId",
|
|
1067
|
+
b.content AS "bContent",
|
|
1068
|
+
b.type AS "bType",
|
|
1069
|
+
b.importance AS "bImportance",
|
|
1070
|
+
b.tags AS "bTags",
|
|
1071
|
+
b.source AS "bSource",
|
|
1072
|
+
b.metadata AS "bMetadata",
|
|
1073
|
+
b.confidence AS "bConfidence",
|
|
1074
|
+
b.embedding AS "bEmbedding",
|
|
1075
|
+
b.embedding_model AS "bEmbeddingModel",
|
|
1076
|
+
b.embedding_dimensions AS "bEmbeddingDimensions",
|
|
1077
|
+
b.superseded_by AS "bSupersededBy",
|
|
1078
|
+
b.superseded_at AS "bSupersededAt",
|
|
1079
|
+
b.expires_at AS "bExpiresAt",
|
|
1080
|
+
b.last_accessed_at AS "bLastAccessedAt",
|
|
1081
|
+
b.access_count AS "bAccessCount",
|
|
1082
|
+
b.created_at AS "bCreatedAt",
|
|
1083
|
+
b.updated_at AS "bUpdatedAt",
|
|
1084
|
+
1 - (a.embedding <=> b.embedding) AS "similarity"
|
|
1085
|
+
FROM mnemocyte_memories a
|
|
1086
|
+
JOIN mnemocyte_memories b
|
|
1087
|
+
ON b.entity_id = a.entity_id
|
|
1088
|
+
AND a.id < b.id
|
|
1089
|
+
AND a.embedding IS NOT NULL
|
|
1090
|
+
AND b.embedding IS NOT NULL
|
|
1091
|
+
WHERE
|
|
1092
|
+
a.entity_id = ${input.entityId}
|
|
1093
|
+
${input.includeSuperseded ? sql`` : sql`AND a.superseded_by IS NULL AND b.superseded_by IS NULL`}
|
|
1094
|
+
${input.includeExpired ? sql`` : sql`AND (a.expires_at IS NULL OR a.expires_at > now()) AND (b.expires_at IS NULL OR b.expires_at > now())`}
|
|
1095
|
+
${input.types && input.types.length > 0 ? sql`AND a.type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)}) AND b.type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)})` : sql``}
|
|
1096
|
+
${input.tags && input.tags.length > 0 ? sql`AND a.tags @> ${input.tags} AND b.tags @> ${input.tags}` : sql``}
|
|
1097
|
+
AND 1 - (a.embedding <=> b.embedding) >= ${input.threshold}
|
|
1098
|
+
ORDER BY "similarity" DESC
|
|
1099
|
+
LIMIT ${input.limit}
|
|
1100
|
+
`)).map((row) => {
|
|
1101
|
+
return {
|
|
1102
|
+
a: {
|
|
1103
|
+
id: row.aId,
|
|
1104
|
+
entityId: row.aEntityId,
|
|
1105
|
+
content: row.aContent,
|
|
1106
|
+
type: row.aType,
|
|
1107
|
+
importance: row.aImportance,
|
|
1108
|
+
tags: row.aTags,
|
|
1109
|
+
source: row.aSource,
|
|
1110
|
+
metadata: row.aMetadata,
|
|
1111
|
+
confidence: row.aConfidence,
|
|
1112
|
+
embedding: row.aEmbedding,
|
|
1113
|
+
embeddingModel: row.aEmbeddingModel,
|
|
1114
|
+
embeddingDimensions: row.aEmbeddingDimensions,
|
|
1115
|
+
supersededBy: row.aSupersededBy,
|
|
1116
|
+
supersededAt: row.aSupersededAt,
|
|
1117
|
+
expiresAt: row.aExpiresAt,
|
|
1118
|
+
lastAccessedAt: row.aLastAccessedAt,
|
|
1119
|
+
accessCount: row.aAccessCount,
|
|
1120
|
+
createdAt: row.aCreatedAt,
|
|
1121
|
+
updatedAt: row.aUpdatedAt
|
|
1122
|
+
},
|
|
1123
|
+
b: {
|
|
1124
|
+
id: row.bId,
|
|
1125
|
+
entityId: row.bEntityId,
|
|
1126
|
+
content: row.bContent,
|
|
1127
|
+
type: row.bType,
|
|
1128
|
+
importance: row.bImportance,
|
|
1129
|
+
tags: row.bTags,
|
|
1130
|
+
source: row.bSource,
|
|
1131
|
+
metadata: row.bMetadata,
|
|
1132
|
+
confidence: row.bConfidence,
|
|
1133
|
+
embedding: row.bEmbedding,
|
|
1134
|
+
embeddingModel: row.bEmbeddingModel,
|
|
1135
|
+
embeddingDimensions: row.bEmbeddingDimensions,
|
|
1136
|
+
supersededBy: row.bSupersededBy,
|
|
1137
|
+
supersededAt: row.bSupersededAt,
|
|
1138
|
+
expiresAt: row.bExpiresAt,
|
|
1139
|
+
lastAccessedAt: row.bLastAccessedAt,
|
|
1140
|
+
accessCount: row.bAccessCount,
|
|
1141
|
+
createdAt: row.bCreatedAt,
|
|
1142
|
+
updatedAt: row.bUpdatedAt
|
|
1143
|
+
},
|
|
1144
|
+
similarity: Number(row.similarity) || 0
|
|
1145
|
+
};
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
async function loadConsolidationTargets(db, entityId, ids) {
|
|
1149
|
+
if (ids.length === 0) return [];
|
|
1150
|
+
return await db.select({
|
|
1151
|
+
id: memoriesTable.id,
|
|
1152
|
+
tags: memoriesTable.tags,
|
|
1153
|
+
supersededBy: memoriesTable.supersededBy
|
|
1154
|
+
}).from(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), inArray(memoriesTable.id, [...ids])));
|
|
1155
|
+
}
|
|
1156
|
+
async function markMemoriesSuperseded(db, params) {
|
|
1157
|
+
if (params.ids.length === 0) return [];
|
|
1158
|
+
return await db.update(memoriesTable).set({
|
|
1159
|
+
supersededBy: params.survivorId,
|
|
1160
|
+
supersededAt: params.now,
|
|
1161
|
+
updatedAt: params.now
|
|
1162
|
+
}).where(and(eq(memoriesTable.entityId, params.entityId), inArray(memoriesTable.id, [...params.ids]), isNull(memoriesTable.supersededBy))).returning({
|
|
1163
|
+
id: memoriesTable.id,
|
|
1164
|
+
tags: memoriesTable.tags
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
async function setMemoryTags(db, params) {
|
|
1168
|
+
await db.update(memoriesTable).set({
|
|
1169
|
+
tags: [...params.tags],
|
|
1170
|
+
updatedAt: params.now
|
|
1171
|
+
}).where(and(eq(memoriesTable.entityId, params.entityId), eq(memoriesTable.id, params.memoryId)));
|
|
1172
|
+
}
|
|
861
1173
|
async function markMemoryAccessed(db, memoryIds) {
|
|
862
1174
|
if (memoryIds.length === 0) return;
|
|
863
1175
|
await db.update(memoriesTable).set({
|
|
@@ -982,6 +1294,15 @@ async function hybridRecall(input) {
|
|
|
982
1294
|
}
|
|
983
1295
|
//#endregion
|
|
984
1296
|
//#region src/memory/postgres.ts
|
|
1297
|
+
function rowToAuditEvent(row) {
|
|
1298
|
+
return {
|
|
1299
|
+
id: row.id,
|
|
1300
|
+
entityId: row.entityId,
|
|
1301
|
+
description: row.description,
|
|
1302
|
+
metadata: row.metadata,
|
|
1303
|
+
timestamp: row.timestamp
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
985
1306
|
const IMPORTANCE_LEVELS = [
|
|
986
1307
|
"low",
|
|
987
1308
|
"normal",
|
|
@@ -1008,6 +1329,18 @@ function createPostgresClient(config, handle) {
|
|
|
1008
1329
|
function assertOpen() {
|
|
1009
1330
|
if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
|
|
1010
1331
|
}
|
|
1332
|
+
async function recordAudit(entityId, description, metadata) {
|
|
1333
|
+
if (config.audit?.enabled !== true) return;
|
|
1334
|
+
try {
|
|
1335
|
+
await insertEvent(handle.db, {
|
|
1336
|
+
id: createEventId(),
|
|
1337
|
+
entityId,
|
|
1338
|
+
description,
|
|
1339
|
+
metadata,
|
|
1340
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1341
|
+
});
|
|
1342
|
+
} catch {}
|
|
1343
|
+
}
|
|
1011
1344
|
async function remember(input) {
|
|
1012
1345
|
return observe(config, "postgres", "remember", { entityId: input.entityId }, async () => {
|
|
1013
1346
|
assertOpen();
|
|
@@ -1017,7 +1350,7 @@ function createPostgresClient(config, handle) {
|
|
|
1017
1350
|
...config.provider === void 0 ? {} : { resilience: config.provider }
|
|
1018
1351
|
});
|
|
1019
1352
|
const now = /* @__PURE__ */ new Date();
|
|
1020
|
-
|
|
1353
|
+
const memory = rowToMemory(await insertMemory(handle.db, {
|
|
1021
1354
|
id: createId(),
|
|
1022
1355
|
entityId: input.entityId,
|
|
1023
1356
|
content: input.content,
|
|
@@ -1038,6 +1371,12 @@ function createPostgresClient(config, handle) {
|
|
|
1038
1371
|
createdAt: now,
|
|
1039
1372
|
updatedAt: now
|
|
1040
1373
|
}));
|
|
1374
|
+
await recordAudit(memory.entityId, "memory.created", {
|
|
1375
|
+
memoryId: memory.id,
|
|
1376
|
+
type: memory.type,
|
|
1377
|
+
importance: memory.importance
|
|
1378
|
+
});
|
|
1379
|
+
return memory;
|
|
1041
1380
|
}, (memory) => ({
|
|
1042
1381
|
memoryId: memory.id,
|
|
1043
1382
|
count: 1
|
|
@@ -1097,14 +1436,17 @@ function createPostgresClient(config, handle) {
|
|
|
1097
1436
|
assertNonEmptyString(input.entityId, "entityId");
|
|
1098
1437
|
assertNonEmptyString(input.memoryId, "memoryId");
|
|
1099
1438
|
if (!await deleteMemory(handle.db, input.entityId, input.memoryId)) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
|
|
1439
|
+
await recordAudit(input.entityId, "memory.deleted", { memoryId: input.memoryId });
|
|
1100
1440
|
});
|
|
1101
1441
|
},
|
|
1102
1442
|
async forgetAll(input) {
|
|
1103
1443
|
return observe(config, "postgres", "forgetAll", { entityId: input.entityId }, async () => {
|
|
1104
1444
|
assertOpen();
|
|
1105
1445
|
assertNonEmptyString(input.entityId, "entityId");
|
|
1106
|
-
await
|
|
1107
|
-
|
|
1446
|
+
const deletedCount = await deleteMemoriesForEntity(handle.db, input.entityId);
|
|
1447
|
+
await recordAudit(input.entityId, "entity.cleared", { count: deletedCount });
|
|
1448
|
+
return deletedCount;
|
|
1449
|
+
}, (count) => ({ count })).then(() => void 0);
|
|
1108
1450
|
},
|
|
1109
1451
|
async prune(input) {
|
|
1110
1452
|
return observe(config, "postgres", "prune", input.entityId === void 0 ? {} : { entityId: input.entityId }, async () => {
|
|
@@ -1117,6 +1459,7 @@ function createPostgresClient(config, handle) {
|
|
|
1117
1459
|
dryRun: true
|
|
1118
1460
|
};
|
|
1119
1461
|
const deletedCount = await pruneMemories(handle.db, filter);
|
|
1462
|
+
if (deletedCount > 0 && input.entityId !== void 0) await recordAudit(input.entityId, "memory.pruned", { count: deletedCount });
|
|
1120
1463
|
return {
|
|
1121
1464
|
matchedCount: deletedCount,
|
|
1122
1465
|
deletedCount,
|
|
@@ -1124,6 +1467,39 @@ function createPostgresClient(config, handle) {
|
|
|
1124
1467
|
};
|
|
1125
1468
|
}, (result) => ({ count: result.deletedCount }));
|
|
1126
1469
|
},
|
|
1470
|
+
async findDuplicates(input) {
|
|
1471
|
+
return observe(config, "postgres", "findDuplicates", { entityId: input.entityId }, async () => {
|
|
1472
|
+
assertOpen();
|
|
1473
|
+
validateFindDuplicatesInput(input);
|
|
1474
|
+
const threshold = input.threshold ?? .95;
|
|
1475
|
+
const limit = input.limit ?? 100;
|
|
1476
|
+
return (await findDuplicatePairs(handle.db, {
|
|
1477
|
+
entityId: input.entityId,
|
|
1478
|
+
threshold,
|
|
1479
|
+
limit,
|
|
1480
|
+
...input.types === void 0 ? {} : { types: input.types },
|
|
1481
|
+
...input.tags === void 0 ? {} : { tags: input.tags },
|
|
1482
|
+
...input.includeSuperseded === void 0 ? {} : { includeSuperseded: input.includeSuperseded },
|
|
1483
|
+
...input.includeExpired === void 0 ? {} : { includeExpired: input.includeExpired }
|
|
1484
|
+
})).map((row) => ({
|
|
1485
|
+
a: rowToMemory(row.a),
|
|
1486
|
+
b: rowToMemory(row.b),
|
|
1487
|
+
similarity: Math.max(0, Math.min(1, row.similarity))
|
|
1488
|
+
}));
|
|
1489
|
+
}, (result) => ({ count: result.length }));
|
|
1490
|
+
},
|
|
1491
|
+
async listAuditLog(input) {
|
|
1492
|
+
return observe(config, "postgres", "listAuditLog", { entityId: input.entityId }, async () => {
|
|
1493
|
+
assertOpen();
|
|
1494
|
+
validateListAuditLogInput(input);
|
|
1495
|
+
return (await listEvents(handle.db, {
|
|
1496
|
+
entityId: input.entityId,
|
|
1497
|
+
limit: input.limit ?? 50,
|
|
1498
|
+
...input.before === void 0 ? {} : { before: input.before },
|
|
1499
|
+
...input.after === void 0 ? {} : { after: input.after }
|
|
1500
|
+
})).map(rowToAuditEvent);
|
|
1501
|
+
}, (result) => ({ count: result.length }));
|
|
1502
|
+
},
|
|
1127
1503
|
async stats(input) {
|
|
1128
1504
|
return observe(config, "postgres", "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
|
|
1129
1505
|
assertOpen();
|
|
@@ -1152,6 +1528,7 @@ function createPostgresClient(config, handle) {
|
|
|
1152
1528
|
};
|
|
1153
1529
|
});
|
|
1154
1530
|
},
|
|
1531
|
+
experimental: createExperimental(),
|
|
1155
1532
|
async close() {
|
|
1156
1533
|
return observe(config, "postgres", "close", {}, async () => {
|
|
1157
1534
|
closed = true;
|
|
@@ -1159,9 +1536,61 @@ function createPostgresClient(config, handle) {
|
|
|
1159
1536
|
});
|
|
1160
1537
|
}
|
|
1161
1538
|
};
|
|
1539
|
+
function createExperimental() {
|
|
1540
|
+
return { async consolidate(input) {
|
|
1541
|
+
return observe(config, "postgres", "consolidate", {
|
|
1542
|
+
entityId: input.entityId,
|
|
1543
|
+
memoryId: input.survivorId
|
|
1544
|
+
}, async () => {
|
|
1545
|
+
assertOpen();
|
|
1546
|
+
validateConsolidateInput(input);
|
|
1547
|
+
const survivor = await getMemoryById(handle.db, input.entityId, input.survivorId);
|
|
1548
|
+
if (!survivor) throw new MnemocyteError("Survivor memory was not found.", "NOT_FOUND");
|
|
1549
|
+
if (survivor.supersededBy !== null) throw new MnemocyteError("Survivor memory is itself superseded.", "VALIDATION");
|
|
1550
|
+
const targets = await loadConsolidationTargets(handle.db, input.entityId, input.supersededIds);
|
|
1551
|
+
const foundIds = new Set(targets.map((t) => t.id));
|
|
1552
|
+
for (const id of input.supersededIds) if (!foundIds.has(id)) throw new MnemocyteError("Superseded memory was not found.", "NOT_FOUND");
|
|
1553
|
+
const losers = targets.filter((target) => target.supersededBy === null);
|
|
1554
|
+
if (losers.length === 0) return {
|
|
1555
|
+
survivorId: survivor.id,
|
|
1556
|
+
supersededCount: 0,
|
|
1557
|
+
supersededIds: []
|
|
1558
|
+
};
|
|
1559
|
+
const now = /* @__PURE__ */ new Date();
|
|
1560
|
+
const updated = await markMemoriesSuperseded(handle.db, {
|
|
1561
|
+
survivorId: survivor.id,
|
|
1562
|
+
entityId: input.entityId,
|
|
1563
|
+
ids: losers.map((loser) => loser.id),
|
|
1564
|
+
now
|
|
1565
|
+
});
|
|
1566
|
+
const newSupersededIds = updated.map((row) => row.id);
|
|
1567
|
+
for (const id of newSupersededIds) await recordAudit(input.entityId, "memory.superseded", {
|
|
1568
|
+
memoryId: id,
|
|
1569
|
+
supersededBy: survivor.id
|
|
1570
|
+
});
|
|
1571
|
+
if (input.mergeTags !== false && updated.length > 0) {
|
|
1572
|
+
const mergedTags = new Set(survivor.tags);
|
|
1573
|
+
for (const row of updated) for (const tag of row.tags) mergedTags.add(tag);
|
|
1574
|
+
if (mergedTags.size !== survivor.tags.length) await setMemoryTags(handle.db, {
|
|
1575
|
+
entityId: input.entityId,
|
|
1576
|
+
memoryId: survivor.id,
|
|
1577
|
+
tags: [...mergedTags],
|
|
1578
|
+
now
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
return {
|
|
1582
|
+
survivorId: survivor.id,
|
|
1583
|
+
supersededCount: newSupersededIds.length,
|
|
1584
|
+
supersededIds: newSupersededIds
|
|
1585
|
+
};
|
|
1586
|
+
}, (result) => ({ count: result.supersededCount }));
|
|
1587
|
+
} };
|
|
1588
|
+
}
|
|
1162
1589
|
}
|
|
1163
1590
|
//#endregion
|
|
1164
1591
|
//#region src/client.ts
|
|
1592
|
+
/** Embedding dimensionality pinned by `migrations/0000_initial.sql`. */
|
|
1593
|
+
const POSTGRES_EMBEDDING_DIMENSIONS = 1536;
|
|
1165
1594
|
/**
|
|
1166
1595
|
* Create a Mnemocyte client.
|
|
1167
1596
|
*
|
|
@@ -1176,14 +1605,16 @@ function createPostgresClient(config, handle) {
|
|
|
1176
1605
|
*
|
|
1177
1606
|
* @param config - Client configuration. {@link MnemocyteConfig.embedder} is required.
|
|
1178
1607
|
* @returns A {@link MnemocyteClient} instance.
|
|
1179
|
-
* @throws {MnemocyteError} With code `"CONFIG"` if `embedder` is missing or
|
|
1180
|
-
*
|
|
1608
|
+
* @throws {MnemocyteError} With code `"CONFIG"` if `embedder` is missing or
|
|
1609
|
+
* invalid (including when `databaseUrl` is set and `embedder.dimensions`
|
|
1610
|
+
* is not 1536 — the dimensionality pinned by the bundled migration), or
|
|
1611
|
+
* `"VALIDATION"` if `databaseUrl` is provided but empty.
|
|
1181
1612
|
*
|
|
1182
1613
|
* @example Postgres-backed client
|
|
1183
1614
|
* ```ts
|
|
1184
1615
|
* const client = createMnemocyte({
|
|
1185
1616
|
* databaseUrl: process.env.DATABASE_URL,
|
|
1186
|
-
* embedder,
|
|
1617
|
+
* embedder, // embedder.dimensions MUST be 1536 for the Postgres backend
|
|
1187
1618
|
* });
|
|
1188
1619
|
* ```
|
|
1189
1620
|
*/
|
|
@@ -1191,6 +1622,7 @@ function createMnemocyte(config) {
|
|
|
1191
1622
|
assertEmbedder(config.embedder);
|
|
1192
1623
|
if (config.databaseUrl) {
|
|
1193
1624
|
assertNonEmptyString(config.databaseUrl, "databaseUrl");
|
|
1625
|
+
if (config.embedder.dimensions !== POSTGRES_EMBEDDING_DIMENSIONS) throw new MnemocyteError(`embedder.dimensions must be ${POSTGRES_EMBEDDING_DIMENSIONS} for the Postgres backend (got ${config.embedder.dimensions}). The bundled migration pins the vector column to vector(${POSTGRES_EMBEDDING_DIMENSIONS}).`, "CONFIG");
|
|
1194
1626
|
return createPostgresClient(config, createDatabase(config.databaseUrl));
|
|
1195
1627
|
}
|
|
1196
1628
|
return createInMemoryClient(config);
|