@sentry/junior-memory 0.80.0 → 0.81.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +3 -0
- package/dist/index.js +553 -47
- package/dist/index.js.map +1 -1
- package/dist/store.d.ts +28 -0
- package/dist/tools.d.ts +9 -2
- package/package.json +2 -2
- package/src/agent.ts +92 -0
- package/src/plugin.ts +28 -2
- package/src/process-session.ts +2 -1
- package/src/recall.ts +8 -1
- package/src/store.ts +633 -48
- package/src/tools.ts +33 -7
package/src/store.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
ilike,
|
|
16
16
|
inArray,
|
|
17
17
|
isNull,
|
|
18
|
+
isNotNull,
|
|
18
19
|
like,
|
|
19
20
|
lte,
|
|
20
21
|
or,
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
import {
|
|
41
42
|
deriveMemoryScope,
|
|
42
43
|
deriveMemorySubject,
|
|
44
|
+
type ResolvedMemorySubject,
|
|
43
45
|
deriveVisibleMemoryScopes,
|
|
44
46
|
type ResolvedMemoryScope,
|
|
45
47
|
} from "./scope";
|
|
@@ -47,9 +49,24 @@ import {
|
|
|
47
49
|
const DEFAULT_LIST_LIMIT = 50;
|
|
48
50
|
const DEFAULT_SEARCH_LIMIT = 10;
|
|
49
51
|
const DEFAULT_EXPIRED_ARCHIVE_LIMIT = 100;
|
|
52
|
+
const HIGH_CONFIDENCE_DUPLICATE_DISTANCE = 0.015;
|
|
53
|
+
const SUPERSESSION_CANDIDATE_LIMIT = 10;
|
|
50
54
|
const VECTOR_SEARCH_OVERFETCH = 4;
|
|
51
55
|
const MAX_MEMORY_CONTENT_CHARS = 4_000;
|
|
52
56
|
const EMBEDDING_METRIC = "cosine";
|
|
57
|
+
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
58
|
+
const RELEVANCE_NEAR_TIE_DELTA = 0.01;
|
|
59
|
+
const SUPERSESSION_STOP_TERMS = new Set([
|
|
60
|
+
"and",
|
|
61
|
+
"for",
|
|
62
|
+
"prefer",
|
|
63
|
+
"prefers",
|
|
64
|
+
"preference",
|
|
65
|
+
"the",
|
|
66
|
+
"use",
|
|
67
|
+
"uses",
|
|
68
|
+
"with",
|
|
69
|
+
]);
|
|
53
70
|
|
|
54
71
|
export type MemoryDb = PgDatabase<PgQueryResultHKT, typeof memorySqlSchema>;
|
|
55
72
|
|
|
@@ -58,6 +75,12 @@ interface SearchCandidate {
|
|
|
58
75
|
score: number;
|
|
59
76
|
}
|
|
60
77
|
|
|
78
|
+
interface MemoryEmbedding {
|
|
79
|
+
model: string;
|
|
80
|
+
provider: string;
|
|
81
|
+
vector: number[];
|
|
82
|
+
}
|
|
83
|
+
|
|
61
84
|
const nonEmptyStringSchema = z.string().min(1);
|
|
62
85
|
const memoryContentSchema = z
|
|
63
86
|
.string()
|
|
@@ -215,9 +238,44 @@ export interface MemoryEmbeddingProvider {
|
|
|
215
238
|
}>;
|
|
216
239
|
}
|
|
217
240
|
|
|
241
|
+
export interface MemorySupersessionInput {
|
|
242
|
+
candidate: {
|
|
243
|
+
content: string;
|
|
244
|
+
kind: "preference";
|
|
245
|
+
};
|
|
246
|
+
existingMemories: [
|
|
247
|
+
{
|
|
248
|
+
content: string;
|
|
249
|
+
id: string;
|
|
250
|
+
},
|
|
251
|
+
...Array<{
|
|
252
|
+
content: string;
|
|
253
|
+
id: string;
|
|
254
|
+
}>,
|
|
255
|
+
];
|
|
256
|
+
runtimeContext: MemoryRuntimeContext;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export type MemorySupersessionDecision =
|
|
260
|
+
| {
|
|
261
|
+
decision: "supersedes_old";
|
|
262
|
+
supersededIds: [string, ...string[]];
|
|
263
|
+
}
|
|
264
|
+
| {
|
|
265
|
+
decision: "distinct" | "uncertain";
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
export interface MemorySupersessionDecider {
|
|
269
|
+
/** Decide whether a new preference clearly replaces active old preferences. */
|
|
270
|
+
adjudicateSupersession(
|
|
271
|
+
input: MemorySupersessionInput,
|
|
272
|
+
): Promise<MemorySupersessionDecision> | MemorySupersessionDecision;
|
|
273
|
+
}
|
|
274
|
+
|
|
218
275
|
export interface MemoryStoreOptions {
|
|
219
276
|
embedder?: MemoryEmbeddingProvider;
|
|
220
277
|
now?: () => number;
|
|
278
|
+
supersessionDecider?: MemorySupersessionDecider;
|
|
221
279
|
}
|
|
222
280
|
|
|
223
281
|
/** Context-bound storage operations for visible long-term memories. */
|
|
@@ -248,6 +306,22 @@ function hashEmbeddedContent(content: string): string {
|
|
|
248
306
|
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
249
307
|
}
|
|
250
308
|
|
|
309
|
+
function idempotencyAliasId(args: {
|
|
310
|
+
idempotencyKey: string;
|
|
311
|
+
scope: ResolvedMemoryScope;
|
|
312
|
+
targetId: string;
|
|
313
|
+
}): string {
|
|
314
|
+
return `alias:${createHash("sha256")
|
|
315
|
+
.update(args.scope.scope)
|
|
316
|
+
.update("\0")
|
|
317
|
+
.update(args.scope.scopeKey)
|
|
318
|
+
.update("\0")
|
|
319
|
+
.update(args.idempotencyKey)
|
|
320
|
+
.update("\0")
|
|
321
|
+
.update(args.targetId)
|
|
322
|
+
.digest("hex")}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
251
325
|
function boundedLimit(value: number | undefined, fallback: number): number {
|
|
252
326
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
253
327
|
return fallback;
|
|
@@ -336,7 +410,7 @@ async function findByIdempotencyKey(args: {
|
|
|
336
410
|
nowMs: number;
|
|
337
411
|
scope: ResolvedMemoryScope;
|
|
338
412
|
}): Promise<MemoryRecord | undefined> {
|
|
339
|
-
const
|
|
413
|
+
const activeRows = await args.db
|
|
340
414
|
.select()
|
|
341
415
|
.from(juniorMemoryMemories)
|
|
342
416
|
.where(
|
|
@@ -354,7 +428,58 @@ async function findByIdempotencyKey(args: {
|
|
|
354
428
|
),
|
|
355
429
|
)
|
|
356
430
|
.limit(1);
|
|
357
|
-
|
|
431
|
+
if (activeRows[0]) {
|
|
432
|
+
return parseMemoryRow(activeRows[0]);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const aliasRows = await args.db
|
|
436
|
+
.select({ supersededById: juniorMemoryMemories.supersededById })
|
|
437
|
+
.from(juniorMemoryMemories)
|
|
438
|
+
.where(
|
|
439
|
+
and(
|
|
440
|
+
eq(juniorMemoryMemories.scope, args.scope.scope),
|
|
441
|
+
eq(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
|
|
442
|
+
eq(juniorMemoryMemories.idempotencyKey, args.idempotencyKey),
|
|
443
|
+
isNull(juniorMemoryMemories.archivedAtMs),
|
|
444
|
+
isNotNull(juniorMemoryMemories.supersededAtMs),
|
|
445
|
+
isNotNull(juniorMemoryMemories.supersededById),
|
|
446
|
+
or(
|
|
447
|
+
isNull(juniorMemoryMemories.expiresAtMs),
|
|
448
|
+
gt(juniorMemoryMemories.expiresAtMs, args.nowMs),
|
|
449
|
+
),
|
|
450
|
+
),
|
|
451
|
+
)
|
|
452
|
+
.orderBy(
|
|
453
|
+
desc(juniorMemoryMemories.createdAtMs),
|
|
454
|
+
asc(juniorMemoryMemories.id),
|
|
455
|
+
);
|
|
456
|
+
for (const alias of aliasRows) {
|
|
457
|
+
if (!alias.supersededById) {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
const rows = await args.db
|
|
461
|
+
.select()
|
|
462
|
+
.from(juniorMemoryMemories)
|
|
463
|
+
.where(
|
|
464
|
+
and(
|
|
465
|
+
eq(juniorMemoryMemories.id, alias.supersededById),
|
|
466
|
+
eq(juniorMemoryMemories.scope, args.scope.scope),
|
|
467
|
+
eq(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
|
|
468
|
+
isNull(juniorMemoryMemories.archivedAtMs),
|
|
469
|
+
isNull(juniorMemoryMemories.supersededAtMs),
|
|
470
|
+
isNull(juniorMemoryMemories.supersededById),
|
|
471
|
+
or(
|
|
472
|
+
isNull(juniorMemoryMemories.expiresAtMs),
|
|
473
|
+
gt(juniorMemoryMemories.expiresAtMs, args.nowMs),
|
|
474
|
+
),
|
|
475
|
+
),
|
|
476
|
+
)
|
|
477
|
+
.limit(1);
|
|
478
|
+
if (rows[0]) {
|
|
479
|
+
return parseMemoryRow(rows[0]);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return undefined;
|
|
358
483
|
}
|
|
359
484
|
|
|
360
485
|
/**
|
|
@@ -442,11 +567,7 @@ function searchTerms(query: string): string[] {
|
|
|
442
567
|
async function embedOne(
|
|
443
568
|
embedder: MemoryEmbeddingProvider,
|
|
444
569
|
text: string,
|
|
445
|
-
): Promise<{
|
|
446
|
-
model: string;
|
|
447
|
-
provider: string;
|
|
448
|
-
vector: number[];
|
|
449
|
-
}> {
|
|
570
|
+
): Promise<MemoryEmbedding> {
|
|
450
571
|
const normalized = normalizeContent(text);
|
|
451
572
|
if (!normalized) {
|
|
452
573
|
throw new Error("Embedding text is required.");
|
|
@@ -469,10 +590,11 @@ async function storeEmbedding(args: {
|
|
|
469
590
|
content: string;
|
|
470
591
|
db: MemoryDb;
|
|
471
592
|
embedder: MemoryEmbeddingProvider | undefined;
|
|
593
|
+
embedding?: MemoryEmbedding;
|
|
472
594
|
memoryId: string;
|
|
473
595
|
nowMs: number;
|
|
474
596
|
}): Promise<void> {
|
|
475
|
-
if (!args.embedder) {
|
|
597
|
+
if (!args.embedder && !args.embedding) {
|
|
476
598
|
return;
|
|
477
599
|
}
|
|
478
600
|
try {
|
|
@@ -488,10 +610,18 @@ async function storeEmbedding(args: {
|
|
|
488
610
|
return;
|
|
489
611
|
}
|
|
490
612
|
let embedding: Awaited<ReturnType<typeof embedOne>>;
|
|
491
|
-
|
|
492
|
-
embedding =
|
|
493
|
-
}
|
|
494
|
-
|
|
613
|
+
if (args.embedding) {
|
|
614
|
+
embedding = args.embedding;
|
|
615
|
+
} else {
|
|
616
|
+
const embedder = args.embedder;
|
|
617
|
+
if (!embedder) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
try {
|
|
621
|
+
embedding = await embedOne(embedder, args.content);
|
|
622
|
+
} catch {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
495
625
|
}
|
|
496
626
|
try {
|
|
497
627
|
await args.db
|
|
@@ -512,6 +642,293 @@ async function storeEmbedding(args: {
|
|
|
512
642
|
}
|
|
513
643
|
}
|
|
514
644
|
|
|
645
|
+
function activeScopedSubjectPredicate(args: {
|
|
646
|
+
kind: MemoryRecord["kind"];
|
|
647
|
+
nowMs: number;
|
|
648
|
+
scope: ResolvedMemoryScope;
|
|
649
|
+
subject: ResolvedMemorySubject;
|
|
650
|
+
}): SQL {
|
|
651
|
+
const predicate = and(
|
|
652
|
+
eq(juniorMemoryMemories.scope, args.scope.scope),
|
|
653
|
+
eq(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
|
|
654
|
+
eq(juniorMemoryMemories.kind, args.kind),
|
|
655
|
+
eq(juniorMemoryMemories.subjectType, args.subject.subjectType),
|
|
656
|
+
args.subject.subjectKey === undefined
|
|
657
|
+
? isNull(juniorMemoryMemories.subjectKey)
|
|
658
|
+
: eq(juniorMemoryMemories.subjectKey, args.subject.subjectKey),
|
|
659
|
+
isNull(juniorMemoryMemories.archivedAtMs),
|
|
660
|
+
isNull(juniorMemoryMemories.supersededAtMs),
|
|
661
|
+
isNull(juniorMemoryMemories.supersededById),
|
|
662
|
+
or(
|
|
663
|
+
isNull(juniorMemoryMemories.expiresAtMs),
|
|
664
|
+
gt(juniorMemoryMemories.expiresAtMs, args.nowMs),
|
|
665
|
+
),
|
|
666
|
+
);
|
|
667
|
+
if (!predicate) {
|
|
668
|
+
throw new Error("Memory duplicate predicate is empty.");
|
|
669
|
+
}
|
|
670
|
+
return predicate;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async function findExactDuplicateMemory(args: {
|
|
674
|
+
content: string;
|
|
675
|
+
db: MemoryDb;
|
|
676
|
+
kind: MemoryRecord["kind"];
|
|
677
|
+
nowMs: number;
|
|
678
|
+
scope: ResolvedMemoryScope;
|
|
679
|
+
subject: ResolvedMemorySubject;
|
|
680
|
+
}): Promise<MemoryRecord | undefined> {
|
|
681
|
+
const rows = await args.db
|
|
682
|
+
.select()
|
|
683
|
+
.from(juniorMemoryMemories)
|
|
684
|
+
.where(
|
|
685
|
+
and(
|
|
686
|
+
activeScopedSubjectPredicate(args),
|
|
687
|
+
eq(juniorMemoryMemories.content, args.content),
|
|
688
|
+
),
|
|
689
|
+
)
|
|
690
|
+
.orderBy(
|
|
691
|
+
desc(juniorMemoryMemories.createdAtMs),
|
|
692
|
+
asc(juniorMemoryMemories.id),
|
|
693
|
+
)
|
|
694
|
+
.limit(1);
|
|
695
|
+
return rows[0] ? parseMemoryRow(rows[0]) : undefined;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async function findVectorDuplicateMemory(args: {
|
|
699
|
+
db: MemoryDb;
|
|
700
|
+
embedding: MemoryEmbedding;
|
|
701
|
+
kind: MemoryRecord["kind"];
|
|
702
|
+
nowMs: number;
|
|
703
|
+
scope: ResolvedMemoryScope;
|
|
704
|
+
subject: ResolvedMemorySubject;
|
|
705
|
+
}): Promise<MemoryRecord | undefined> {
|
|
706
|
+
const distance = cosineDistance(
|
|
707
|
+
juniorMemoryEmbeddings.embedding,
|
|
708
|
+
args.embedding.vector,
|
|
709
|
+
);
|
|
710
|
+
const rows = await args.db
|
|
711
|
+
.select({
|
|
712
|
+
contentHash: juniorMemoryEmbeddings.contentHash,
|
|
713
|
+
distance,
|
|
714
|
+
memory: juniorMemoryMemories,
|
|
715
|
+
})
|
|
716
|
+
.from(juniorMemoryMemories)
|
|
717
|
+
.innerJoin(
|
|
718
|
+
juniorMemoryEmbeddings,
|
|
719
|
+
eq(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id),
|
|
720
|
+
)
|
|
721
|
+
.where(
|
|
722
|
+
and(
|
|
723
|
+
activeScopedSubjectPredicate(args),
|
|
724
|
+
eq(juniorMemoryEmbeddings.provider, args.embedding.provider),
|
|
725
|
+
eq(juniorMemoryEmbeddings.model, args.embedding.model),
|
|
726
|
+
eq(juniorMemoryEmbeddings.dimensions, MEMORY_EMBEDDING_DIMENSIONS),
|
|
727
|
+
eq(juniorMemoryEmbeddings.metric, EMBEDDING_METRIC),
|
|
728
|
+
lte(distance, HIGH_CONFIDENCE_DUPLICATE_DISTANCE),
|
|
729
|
+
),
|
|
730
|
+
)
|
|
731
|
+
.orderBy(
|
|
732
|
+
distance,
|
|
733
|
+
desc(juniorMemoryMemories.createdAtMs),
|
|
734
|
+
asc(juniorMemoryMemories.id),
|
|
735
|
+
)
|
|
736
|
+
.limit(1);
|
|
737
|
+
const row = rows[0];
|
|
738
|
+
if (!row || hashEmbeddedContent(row.memory.content) !== row.contentHash) {
|
|
739
|
+
return undefined;
|
|
740
|
+
}
|
|
741
|
+
return parseMemoryRow(row.memory);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
async function rememberDuplicateIdempotency(args: {
|
|
745
|
+
content: string;
|
|
746
|
+
db: MemoryDb;
|
|
747
|
+
duplicate: MemoryRecord;
|
|
748
|
+
idempotencyKey?: string;
|
|
749
|
+
nowMs: number;
|
|
750
|
+
runtimeContext: MemoryRuntimeContext;
|
|
751
|
+
scope: ResolvedMemoryScope;
|
|
752
|
+
subject: ResolvedMemorySubject;
|
|
753
|
+
}): Promise<void> {
|
|
754
|
+
if (args.idempotencyKey === undefined) {
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
await args.db
|
|
758
|
+
.insert(juniorMemoryMemories)
|
|
759
|
+
.values({
|
|
760
|
+
content: args.content,
|
|
761
|
+
createdAtMs: args.nowMs,
|
|
762
|
+
expiresAtMs: args.duplicate.expiresAtMs,
|
|
763
|
+
id: idempotencyAliasId({
|
|
764
|
+
idempotencyKey: args.idempotencyKey,
|
|
765
|
+
scope: args.scope,
|
|
766
|
+
targetId: args.duplicate.id,
|
|
767
|
+
}),
|
|
768
|
+
idempotencyKey: args.idempotencyKey,
|
|
769
|
+
observedAtMs: args.nowMs,
|
|
770
|
+
scope: args.scope.scope,
|
|
771
|
+
scopeKey: args.scope.scopeKey,
|
|
772
|
+
sourceKey: sourceKey(args.runtimeContext),
|
|
773
|
+
sourcePlatform: args.runtimeContext.source.platform,
|
|
774
|
+
subjectKey: args.subject.subjectKey,
|
|
775
|
+
subjectType: args.subject.subjectType,
|
|
776
|
+
supersededAtMs: args.nowMs,
|
|
777
|
+
supersededById: args.duplicate.id,
|
|
778
|
+
kind: args.duplicate.kind,
|
|
779
|
+
})
|
|
780
|
+
.onConflictDoNothing();
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
async function listSupersessionCandidates(args: {
|
|
784
|
+
content: string;
|
|
785
|
+
db: MemoryDb;
|
|
786
|
+
embedding?: MemoryEmbedding;
|
|
787
|
+
kind: MemoryRecord["kind"];
|
|
788
|
+
nowMs: number;
|
|
789
|
+
scope: ResolvedMemoryScope;
|
|
790
|
+
subject: ResolvedMemorySubject;
|
|
791
|
+
}): Promise<MemoryRecord[]> {
|
|
792
|
+
const predicate = activeScopedSubjectPredicate(args);
|
|
793
|
+
const terms = searchTerms(args.content).filter(
|
|
794
|
+
(term) => !SUPERSESSION_STOP_TERMS.has(term),
|
|
795
|
+
);
|
|
796
|
+
const lexicalRows =
|
|
797
|
+
terms.length > 0
|
|
798
|
+
? await args.db
|
|
799
|
+
.select()
|
|
800
|
+
.from(juniorMemoryMemories)
|
|
801
|
+
.where(
|
|
802
|
+
and(
|
|
803
|
+
predicate,
|
|
804
|
+
or(
|
|
805
|
+
...terms.map((term) =>
|
|
806
|
+
ilike(juniorMemoryMemories.content, `%${term}%`),
|
|
807
|
+
),
|
|
808
|
+
),
|
|
809
|
+
),
|
|
810
|
+
)
|
|
811
|
+
: [];
|
|
812
|
+
const lexicalCandidates = lexicalRows
|
|
813
|
+
.map(parseMemoryRow)
|
|
814
|
+
.map((memory) => ({
|
|
815
|
+
memory,
|
|
816
|
+
score: searchScore(memory, terms),
|
|
817
|
+
}))
|
|
818
|
+
.filter((candidate) => candidate.score > 1)
|
|
819
|
+
.sort(
|
|
820
|
+
(left, right) =>
|
|
821
|
+
right.score - left.score ||
|
|
822
|
+
right.memory.createdAtMs - left.memory.createdAtMs ||
|
|
823
|
+
left.memory.id.localeCompare(right.memory.id),
|
|
824
|
+
)
|
|
825
|
+
.map((candidate) => candidate.memory);
|
|
826
|
+
|
|
827
|
+
const vectorCandidates = args.embedding
|
|
828
|
+
? await listVectorSupersessionCandidates({
|
|
829
|
+
db: args.db,
|
|
830
|
+
embedding: args.embedding,
|
|
831
|
+
kind: args.kind,
|
|
832
|
+
nowMs: args.nowMs,
|
|
833
|
+
scope: args.scope,
|
|
834
|
+
subject: args.subject,
|
|
835
|
+
})
|
|
836
|
+
: [];
|
|
837
|
+
const byId = new Map<string, MemoryRecord>();
|
|
838
|
+
for (const memory of [...lexicalCandidates, ...vectorCandidates]) {
|
|
839
|
+
if (byId.size >= SUPERSESSION_CANDIDATE_LIMIT) {
|
|
840
|
+
break;
|
|
841
|
+
}
|
|
842
|
+
byId.set(memory.id, memory);
|
|
843
|
+
}
|
|
844
|
+
return [...byId.values()];
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
async function listVectorSupersessionCandidates(args: {
|
|
848
|
+
db: MemoryDb;
|
|
849
|
+
embedding: MemoryEmbedding;
|
|
850
|
+
kind: MemoryRecord["kind"];
|
|
851
|
+
nowMs: number;
|
|
852
|
+
scope: ResolvedMemoryScope;
|
|
853
|
+
subject: ResolvedMemorySubject;
|
|
854
|
+
}): Promise<MemoryRecord[]> {
|
|
855
|
+
const distance = cosineDistance(
|
|
856
|
+
juniorMemoryEmbeddings.embedding,
|
|
857
|
+
args.embedding.vector,
|
|
858
|
+
);
|
|
859
|
+
const rows = await args.db
|
|
860
|
+
.select({
|
|
861
|
+
contentHash: juniorMemoryEmbeddings.contentHash,
|
|
862
|
+
distance,
|
|
863
|
+
memory: juniorMemoryMemories,
|
|
864
|
+
})
|
|
865
|
+
.from(juniorMemoryMemories)
|
|
866
|
+
.innerJoin(
|
|
867
|
+
juniorMemoryEmbeddings,
|
|
868
|
+
eq(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id),
|
|
869
|
+
)
|
|
870
|
+
.where(
|
|
871
|
+
and(
|
|
872
|
+
activeScopedSubjectPredicate(args),
|
|
873
|
+
eq(juniorMemoryEmbeddings.provider, args.embedding.provider),
|
|
874
|
+
eq(juniorMemoryEmbeddings.model, args.embedding.model),
|
|
875
|
+
eq(juniorMemoryEmbeddings.dimensions, MEMORY_EMBEDDING_DIMENSIONS),
|
|
876
|
+
eq(juniorMemoryEmbeddings.metric, EMBEDDING_METRIC),
|
|
877
|
+
),
|
|
878
|
+
)
|
|
879
|
+
.orderBy(
|
|
880
|
+
distance,
|
|
881
|
+
desc(juniorMemoryMemories.createdAtMs),
|
|
882
|
+
asc(juniorMemoryMemories.id),
|
|
883
|
+
)
|
|
884
|
+
.limit(SUPERSESSION_CANDIDATE_LIMIT);
|
|
885
|
+
return rows.flatMap((row) => {
|
|
886
|
+
if (hashEmbeddedContent(row.memory.content) !== row.contentHash) {
|
|
887
|
+
return [];
|
|
888
|
+
}
|
|
889
|
+
return [parseMemoryRow(row.memory)];
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
async function decideSupersededIds(args: {
|
|
894
|
+
candidates: MemoryRecord[];
|
|
895
|
+
content: string;
|
|
896
|
+
decider: MemorySupersessionDecider | undefined;
|
|
897
|
+
kind: MemoryRecord["kind"];
|
|
898
|
+
runtimeContext: MemoryRuntimeContext;
|
|
899
|
+
}): Promise<string[]> {
|
|
900
|
+
// Supersession is conservative: model uncertainty or decider failure leaves
|
|
901
|
+
// both memories active rather than hiding a possibly valid old preference.
|
|
902
|
+
if (
|
|
903
|
+
args.kind !== "preference" ||
|
|
904
|
+
!args.decider ||
|
|
905
|
+
args.candidates.length === 0
|
|
906
|
+
) {
|
|
907
|
+
return [];
|
|
908
|
+
}
|
|
909
|
+
const existingMemories = args.candidates.map((memory) => ({
|
|
910
|
+
content: memory.content,
|
|
911
|
+
id: memory.id,
|
|
912
|
+
})) as MemorySupersessionInput["existingMemories"];
|
|
913
|
+
const candidateIds = new Set(args.candidates.map((memory) => memory.id));
|
|
914
|
+
try {
|
|
915
|
+
const decision = await args.decider.adjudicateSupersession({
|
|
916
|
+
candidate: {
|
|
917
|
+
content: args.content,
|
|
918
|
+
kind: "preference",
|
|
919
|
+
},
|
|
920
|
+
existingMemories,
|
|
921
|
+
runtimeContext: args.runtimeContext,
|
|
922
|
+
});
|
|
923
|
+
if (decision.decision !== "supersedes_old") {
|
|
924
|
+
return [];
|
|
925
|
+
}
|
|
926
|
+
return decision.supersededIds.filter((id) => candidateIds.has(id));
|
|
927
|
+
} catch {
|
|
928
|
+
return [];
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
515
932
|
/** List active records for the runtime-derived visible scopes. */
|
|
516
933
|
async function listVisibleMemories(args: {
|
|
517
934
|
db: MemoryDb;
|
|
@@ -637,8 +1054,26 @@ async function searchVisibleVectorMemories(args: {
|
|
|
637
1054
|
});
|
|
638
1055
|
}
|
|
639
1056
|
|
|
640
|
-
/**
|
|
641
|
-
function
|
|
1057
|
+
/** Score only observation age for near-tie reranking; this is not lifecycle decay. */
|
|
1058
|
+
function observedAgeBoost(memory: MemoryRecord, nowMs: number): number {
|
|
1059
|
+
const ageMs = Math.max(0, nowMs - memory.observedAtMs);
|
|
1060
|
+
if (ageMs <= 7 * ONE_DAY_MS) {
|
|
1061
|
+
return 0.15;
|
|
1062
|
+
}
|
|
1063
|
+
if (ageMs <= 30 * ONE_DAY_MS) {
|
|
1064
|
+
return 0.1;
|
|
1065
|
+
}
|
|
1066
|
+
if (ageMs <= 90 * ONE_DAY_MS) {
|
|
1067
|
+
return 0.05;
|
|
1068
|
+
}
|
|
1069
|
+
return 0;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/** Fuse retrieval candidates while keeping observed age to near-tie reranking. */
|
|
1073
|
+
function mergeSearchCandidates(
|
|
1074
|
+
candidates: SearchCandidate[],
|
|
1075
|
+
nowMs: number,
|
|
1076
|
+
): MemoryRecord[] {
|
|
642
1077
|
const byId = new Map<
|
|
643
1078
|
string,
|
|
644
1079
|
{
|
|
@@ -658,12 +1093,18 @@ function mergeSearchCandidates(candidates: SearchCandidate[]): MemoryRecord[] {
|
|
|
658
1093
|
});
|
|
659
1094
|
}
|
|
660
1095
|
return [...byId.values()]
|
|
661
|
-
.sort(
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
1096
|
+
.sort((left, right) => {
|
|
1097
|
+
const scoreDelta = right.score - left.score;
|
|
1098
|
+
if (Math.abs(scoreDelta) > RELEVANCE_NEAR_TIE_DELTA) {
|
|
1099
|
+
return scoreDelta;
|
|
1100
|
+
}
|
|
1101
|
+
return (
|
|
1102
|
+
observedAgeBoost(right.memory, nowMs) -
|
|
1103
|
+
observedAgeBoost(left.memory, nowMs) ||
|
|
1104
|
+
right.memory.observedAtMs - left.memory.observedAtMs ||
|
|
1105
|
+
left.memory.id.localeCompare(right.memory.id)
|
|
1106
|
+
);
|
|
1107
|
+
})
|
|
667
1108
|
.map((candidate) => candidate.memory);
|
|
668
1109
|
}
|
|
669
1110
|
|
|
@@ -676,6 +1117,7 @@ export function createMemoryStore(
|
|
|
676
1117
|
const runtimeContext = memoryRuntimeContextSchema.parse(context);
|
|
677
1118
|
const parsedOptions = memoryStoreOptionsSchema.parse({ now: options.now });
|
|
678
1119
|
const embedder = options.embedder;
|
|
1120
|
+
const supersessionDecider = options.supersessionDecider;
|
|
679
1121
|
const getNowMs = parsedOptions.now ?? Date.now;
|
|
680
1122
|
|
|
681
1123
|
async function archiveExpiredVisibleMemories(
|
|
@@ -716,40 +1158,183 @@ export function createMemoryStore(
|
|
|
716
1158
|
nowMs,
|
|
717
1159
|
scopes: [scope],
|
|
718
1160
|
});
|
|
1161
|
+
if (input.idempotencyKey !== undefined) {
|
|
1162
|
+
const idempotent = await findByIdempotencyKey({
|
|
1163
|
+
db,
|
|
1164
|
+
idempotencyKey: input.idempotencyKey,
|
|
1165
|
+
nowMs,
|
|
1166
|
+
scope,
|
|
1167
|
+
});
|
|
1168
|
+
if (idempotent) {
|
|
1169
|
+
await storeEmbedding({
|
|
1170
|
+
content: idempotent.content,
|
|
1171
|
+
db,
|
|
1172
|
+
embedder,
|
|
1173
|
+
memoryId: idempotent.id,
|
|
1174
|
+
nowMs,
|
|
1175
|
+
});
|
|
1176
|
+
return { created: false, memory: idempotent };
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
719
1179
|
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
.
|
|
1180
|
+
const exactDuplicate = await findExactDuplicateMemory({
|
|
1181
|
+
content,
|
|
1182
|
+
db,
|
|
1183
|
+
kind: input.kind,
|
|
1184
|
+
nowMs,
|
|
1185
|
+
scope,
|
|
1186
|
+
subject,
|
|
1187
|
+
});
|
|
1188
|
+
if (exactDuplicate) {
|
|
1189
|
+
await rememberDuplicateIdempotency({
|
|
724
1190
|
content,
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
id,
|
|
1191
|
+
db,
|
|
1192
|
+
duplicate: exactDuplicate,
|
|
728
1193
|
idempotencyKey: input.idempotencyKey,
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
1194
|
+
nowMs,
|
|
1195
|
+
runtimeContext,
|
|
1196
|
+
scope,
|
|
1197
|
+
subject,
|
|
1198
|
+
});
|
|
1199
|
+
await storeEmbedding({
|
|
1200
|
+
content: exactDuplicate.content,
|
|
1201
|
+
db,
|
|
1202
|
+
embedder,
|
|
1203
|
+
memoryId: exactDuplicate.id,
|
|
1204
|
+
nowMs,
|
|
1205
|
+
});
|
|
1206
|
+
return { created: false, memory: exactDuplicate };
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
let candidateEmbedding: MemoryEmbedding | undefined;
|
|
1210
|
+
if (embedder) {
|
|
1211
|
+
try {
|
|
1212
|
+
candidateEmbedding = await embedOne(embedder, content);
|
|
1213
|
+
} catch {
|
|
1214
|
+
candidateEmbedding = undefined;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
const vectorDuplicate = candidateEmbedding
|
|
1218
|
+
? await findVectorDuplicateMemory({
|
|
1219
|
+
db,
|
|
1220
|
+
embedding: candidateEmbedding,
|
|
1221
|
+
kind: input.kind,
|
|
1222
|
+
nowMs,
|
|
1223
|
+
scope,
|
|
1224
|
+
subject,
|
|
1225
|
+
})
|
|
1226
|
+
: undefined;
|
|
1227
|
+
if (vectorDuplicate) {
|
|
1228
|
+
await rememberDuplicateIdempotency({
|
|
1229
|
+
content,
|
|
1230
|
+
db,
|
|
1231
|
+
duplicate: vectorDuplicate,
|
|
1232
|
+
idempotencyKey: input.idempotencyKey,
|
|
1233
|
+
nowMs,
|
|
1234
|
+
runtimeContext,
|
|
1235
|
+
scope,
|
|
1236
|
+
subject,
|
|
1237
|
+
});
|
|
1238
|
+
await storeEmbedding({
|
|
1239
|
+
content: vectorDuplicate.content,
|
|
1240
|
+
db,
|
|
1241
|
+
embedder,
|
|
1242
|
+
memoryId: vectorDuplicate.id,
|
|
1243
|
+
nowMs,
|
|
1244
|
+
});
|
|
1245
|
+
return { created: false, memory: vectorDuplicate };
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
let supersededIds: string[] = [];
|
|
1249
|
+
if (
|
|
1250
|
+
scopeKind === "personal" &&
|
|
1251
|
+
input.kind === "preference" &&
|
|
1252
|
+
supersessionDecider &&
|
|
1253
|
+
(input.expiresAtMs === undefined || input.expiresAtMs > nowMs)
|
|
1254
|
+
) {
|
|
1255
|
+
const supersessionCandidates = await listSupersessionCandidates({
|
|
1256
|
+
content,
|
|
1257
|
+
db,
|
|
1258
|
+
...(candidateEmbedding ? { embedding: candidateEmbedding } : {}),
|
|
736
1259
|
kind: input.kind,
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
1260
|
+
nowMs,
|
|
1261
|
+
scope,
|
|
1262
|
+
subject,
|
|
1263
|
+
});
|
|
1264
|
+
supersededIds = await decideSupersededIds({
|
|
1265
|
+
candidates: supersessionCandidates,
|
|
1266
|
+
content,
|
|
1267
|
+
decider: supersessionDecider,
|
|
1268
|
+
kind: input.kind,
|
|
1269
|
+
runtimeContext,
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
const id = randomUUID();
|
|
1274
|
+
const rows = await db.transaction(async (tx) => {
|
|
1275
|
+
const inserted = await tx
|
|
1276
|
+
.insert(juniorMemoryMemories)
|
|
1277
|
+
.values({
|
|
1278
|
+
content,
|
|
1279
|
+
createdAtMs: nowMs,
|
|
1280
|
+
expiresAtMs: input.expiresAtMs,
|
|
1281
|
+
id,
|
|
1282
|
+
idempotencyKey: input.idempotencyKey,
|
|
1283
|
+
observedAtMs: nowMs,
|
|
1284
|
+
scope: scope.scope,
|
|
1285
|
+
scopeKey: scope.scopeKey,
|
|
1286
|
+
sourceKey: sourceKey(runtimeContext),
|
|
1287
|
+
sourcePlatform: runtimeContext.source.platform,
|
|
1288
|
+
subjectKey: subject.subjectKey,
|
|
1289
|
+
subjectType: subject.subjectType,
|
|
1290
|
+
kind: input.kind,
|
|
1291
|
+
})
|
|
1292
|
+
.onConflictDoNothing({
|
|
1293
|
+
target: [
|
|
1294
|
+
juniorMemoryMemories.scope,
|
|
1295
|
+
juniorMemoryMemories.scopeKey,
|
|
1296
|
+
juniorMemoryMemories.idempotencyKey,
|
|
1297
|
+
],
|
|
1298
|
+
where: sql`${juniorMemoryMemories.idempotencyKey} IS NOT NULL AND ${juniorMemoryMemories.archivedAtMs} IS NULL AND ${juniorMemoryMemories.supersededAtMs} IS NULL AND ${juniorMemoryMemories.supersededById} IS NULL`,
|
|
1299
|
+
})
|
|
1300
|
+
.returning();
|
|
1301
|
+
const insertedMemory = inserted[0];
|
|
1302
|
+
if (!insertedMemory || supersededIds.length === 0) {
|
|
1303
|
+
return inserted;
|
|
1304
|
+
}
|
|
1305
|
+
const superseded = await tx
|
|
1306
|
+
.update(juniorMemoryMemories)
|
|
1307
|
+
.set({
|
|
1308
|
+
supersededAtMs: nowMs,
|
|
1309
|
+
supersededById: insertedMemory.id,
|
|
1310
|
+
})
|
|
1311
|
+
.where(
|
|
1312
|
+
and(
|
|
1313
|
+
inArray(juniorMemoryMemories.id, supersededIds),
|
|
1314
|
+
activeScopedSubjectPredicate({
|
|
1315
|
+
kind: input.kind,
|
|
1316
|
+
nowMs,
|
|
1317
|
+
scope,
|
|
1318
|
+
subject,
|
|
1319
|
+
}),
|
|
1320
|
+
),
|
|
1321
|
+
)
|
|
1322
|
+
.returning({ id: juniorMemoryMemories.id });
|
|
1323
|
+
const idsToClean = superseded.map((row) => row.id);
|
|
1324
|
+
if (idsToClean.length > 0) {
|
|
1325
|
+
await tx
|
|
1326
|
+
.delete(juniorMemoryEmbeddings)
|
|
1327
|
+
.where(inArray(juniorMemoryEmbeddings.memoryId, idsToClean));
|
|
1328
|
+
}
|
|
1329
|
+
return inserted;
|
|
1330
|
+
});
|
|
747
1331
|
if (rows[0]) {
|
|
748
1332
|
const memory = parseMemoryRow(rows[0]);
|
|
749
1333
|
await storeEmbedding({
|
|
750
1334
|
content: memory.content,
|
|
751
1335
|
db,
|
|
752
1336
|
embedder,
|
|
1337
|
+
embedding: candidateEmbedding,
|
|
753
1338
|
memoryId: memory.id,
|
|
754
1339
|
nowMs,
|
|
755
1340
|
});
|
|
@@ -833,10 +1418,10 @@ export function createMemoryStore(
|
|
|
833
1418
|
const lexicalCandidates = candidates
|
|
834
1419
|
.map((memory) => ({ memory, score: searchScore(memory, terms) }))
|
|
835
1420
|
.filter((item) => item.score > 0);
|
|
836
|
-
return mergeSearchCandidates(
|
|
837
|
-
...vectorCandidates,
|
|
838
|
-
|
|
839
|
-
|
|
1421
|
+
return mergeSearchCandidates(
|
|
1422
|
+
[...vectorCandidates, ...lexicalCandidates],
|
|
1423
|
+
nowMs,
|
|
1424
|
+
).slice(0, limit);
|
|
840
1425
|
},
|
|
841
1426
|
|
|
842
1427
|
async archiveMemory(input) {
|