@sentry/junior-memory 0.80.1 → 0.82.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 +611 -54
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +2 -0
- package/dist/recall.d.ts +2 -0
- package/dist/store.d.ts +30 -0
- package/dist/tools.d.ts +9 -2
- package/package.json +2 -2
- package/src/agent.ts +92 -0
- package/src/plugin.ts +47 -2
- package/src/process-session.ts +2 -1
- package/src/recall.ts +18 -4
- package/src/store.ts +686 -51
- 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,15 +49,38 @@ 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 SOURCE_CHANNEL_BOOST = 0.05;
|
|
60
|
+
const SUPERSESSION_STOP_TERMS = new Set([
|
|
61
|
+
"and",
|
|
62
|
+
"for",
|
|
63
|
+
"prefer",
|
|
64
|
+
"prefers",
|
|
65
|
+
"preference",
|
|
66
|
+
"the",
|
|
67
|
+
"use",
|
|
68
|
+
"uses",
|
|
69
|
+
"with",
|
|
70
|
+
]);
|
|
53
71
|
|
|
54
72
|
export type MemoryDb = PgDatabase<PgQueryResultHKT, typeof memorySqlSchema>;
|
|
55
73
|
|
|
56
74
|
interface SearchCandidate {
|
|
57
75
|
memory: MemoryRecord;
|
|
58
76
|
score: number;
|
|
77
|
+
sourceKey: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface MemoryEmbedding {
|
|
81
|
+
model: string;
|
|
82
|
+
provider: string;
|
|
83
|
+
vector: number[];
|
|
59
84
|
}
|
|
60
85
|
|
|
61
86
|
const nonEmptyStringSchema = z.string().min(1);
|
|
@@ -215,9 +240,46 @@ export interface MemoryEmbeddingProvider {
|
|
|
215
240
|
}>;
|
|
216
241
|
}
|
|
217
242
|
|
|
243
|
+
export interface MemorySupersessionInput {
|
|
244
|
+
candidate: {
|
|
245
|
+
content: string;
|
|
246
|
+
kind: "preference";
|
|
247
|
+
};
|
|
248
|
+
existingMemories: [
|
|
249
|
+
{
|
|
250
|
+
content: string;
|
|
251
|
+
id: string;
|
|
252
|
+
},
|
|
253
|
+
...Array<{
|
|
254
|
+
content: string;
|
|
255
|
+
id: string;
|
|
256
|
+
}>,
|
|
257
|
+
];
|
|
258
|
+
runtimeContext: MemoryRuntimeContext;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export type MemorySupersessionDecision =
|
|
262
|
+
| {
|
|
263
|
+
decision: "supersedes_old";
|
|
264
|
+
supersededIds: [string, ...string[]];
|
|
265
|
+
}
|
|
266
|
+
| {
|
|
267
|
+
decision: "distinct" | "uncertain";
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
export interface MemorySupersessionDecider {
|
|
271
|
+
/** Decide whether a new preference clearly replaces active old preferences. */
|
|
272
|
+
adjudicateSupersession(
|
|
273
|
+
input: MemorySupersessionInput,
|
|
274
|
+
): Promise<MemorySupersessionDecision> | MemorySupersessionDecision;
|
|
275
|
+
}
|
|
276
|
+
|
|
218
277
|
export interface MemoryStoreOptions {
|
|
219
278
|
embedder?: MemoryEmbeddingProvider;
|
|
279
|
+
/** Maximum cosine distance for vector recall candidates. Model-dependent; tune when changing AI_EMBEDDING_MODEL. */
|
|
280
|
+
maxVectorDistance?: number;
|
|
220
281
|
now?: () => number;
|
|
282
|
+
supersessionDecider?: MemorySupersessionDecider;
|
|
221
283
|
}
|
|
222
284
|
|
|
223
285
|
/** Context-bound storage operations for visible long-term memories. */
|
|
@@ -248,6 +310,22 @@ function hashEmbeddedContent(content: string): string {
|
|
|
248
310
|
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
249
311
|
}
|
|
250
312
|
|
|
313
|
+
function idempotencyAliasId(args: {
|
|
314
|
+
idempotencyKey: string;
|
|
315
|
+
scope: ResolvedMemoryScope;
|
|
316
|
+
targetId: string;
|
|
317
|
+
}): string {
|
|
318
|
+
return `alias:${createHash("sha256")
|
|
319
|
+
.update(args.scope.scope)
|
|
320
|
+
.update("\0")
|
|
321
|
+
.update(args.scope.scopeKey)
|
|
322
|
+
.update("\0")
|
|
323
|
+
.update(args.idempotencyKey)
|
|
324
|
+
.update("\0")
|
|
325
|
+
.update(args.targetId)
|
|
326
|
+
.digest("hex")}`;
|
|
327
|
+
}
|
|
328
|
+
|
|
251
329
|
function boundedLimit(value: number | undefined, fallback: number): number {
|
|
252
330
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
253
331
|
return fallback;
|
|
@@ -269,6 +347,14 @@ function sourceKey(ctx: MemoryRuntimeContext): string {
|
|
|
269
347
|
return `slack:${ctx.source.teamId}:${ctx.source.channelId}:${threadKey}`;
|
|
270
348
|
}
|
|
271
349
|
|
|
350
|
+
function sourceChannelPrefix(ctx: MemoryRuntimeContext): string | undefined {
|
|
351
|
+
if (ctx.source.platform !== "slack") {
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
// TODO(v0.82.0): Replace Slack source-key prefix matching with typed source proximity metadata.
|
|
355
|
+
return `slack:${ctx.source.teamId}:${ctx.source.channelId}:`;
|
|
356
|
+
}
|
|
357
|
+
|
|
272
358
|
/** Parse one SQL row into the public memory record projection. */
|
|
273
359
|
function parseMemoryRow(row: unknown): MemoryRecord {
|
|
274
360
|
const parsed = memoryRowSchema.parse(row);
|
|
@@ -336,7 +422,7 @@ async function findByIdempotencyKey(args: {
|
|
|
336
422
|
nowMs: number;
|
|
337
423
|
scope: ResolvedMemoryScope;
|
|
338
424
|
}): Promise<MemoryRecord | undefined> {
|
|
339
|
-
const
|
|
425
|
+
const activeRows = await args.db
|
|
340
426
|
.select()
|
|
341
427
|
.from(juniorMemoryMemories)
|
|
342
428
|
.where(
|
|
@@ -354,7 +440,58 @@ async function findByIdempotencyKey(args: {
|
|
|
354
440
|
),
|
|
355
441
|
)
|
|
356
442
|
.limit(1);
|
|
357
|
-
|
|
443
|
+
if (activeRows[0]) {
|
|
444
|
+
return parseMemoryRow(activeRows[0]);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const aliasRows = await args.db
|
|
448
|
+
.select({ supersededById: juniorMemoryMemories.supersededById })
|
|
449
|
+
.from(juniorMemoryMemories)
|
|
450
|
+
.where(
|
|
451
|
+
and(
|
|
452
|
+
eq(juniorMemoryMemories.scope, args.scope.scope),
|
|
453
|
+
eq(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
|
|
454
|
+
eq(juniorMemoryMemories.idempotencyKey, args.idempotencyKey),
|
|
455
|
+
isNull(juniorMemoryMemories.archivedAtMs),
|
|
456
|
+
isNotNull(juniorMemoryMemories.supersededAtMs),
|
|
457
|
+
isNotNull(juniorMemoryMemories.supersededById),
|
|
458
|
+
or(
|
|
459
|
+
isNull(juniorMemoryMemories.expiresAtMs),
|
|
460
|
+
gt(juniorMemoryMemories.expiresAtMs, args.nowMs),
|
|
461
|
+
),
|
|
462
|
+
),
|
|
463
|
+
)
|
|
464
|
+
.orderBy(
|
|
465
|
+
desc(juniorMemoryMemories.createdAtMs),
|
|
466
|
+
asc(juniorMemoryMemories.id),
|
|
467
|
+
);
|
|
468
|
+
for (const alias of aliasRows) {
|
|
469
|
+
if (!alias.supersededById) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const rows = await args.db
|
|
473
|
+
.select()
|
|
474
|
+
.from(juniorMemoryMemories)
|
|
475
|
+
.where(
|
|
476
|
+
and(
|
|
477
|
+
eq(juniorMemoryMemories.id, alias.supersededById),
|
|
478
|
+
eq(juniorMemoryMemories.scope, args.scope.scope),
|
|
479
|
+
eq(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
|
|
480
|
+
isNull(juniorMemoryMemories.archivedAtMs),
|
|
481
|
+
isNull(juniorMemoryMemories.supersededAtMs),
|
|
482
|
+
isNull(juniorMemoryMemories.supersededById),
|
|
483
|
+
or(
|
|
484
|
+
isNull(juniorMemoryMemories.expiresAtMs),
|
|
485
|
+
gt(juniorMemoryMemories.expiresAtMs, args.nowMs),
|
|
486
|
+
),
|
|
487
|
+
),
|
|
488
|
+
)
|
|
489
|
+
.limit(1);
|
|
490
|
+
if (rows[0]) {
|
|
491
|
+
return parseMemoryRow(rows[0]);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return undefined;
|
|
358
495
|
}
|
|
359
496
|
|
|
360
497
|
/**
|
|
@@ -442,11 +579,7 @@ function searchTerms(query: string): string[] {
|
|
|
442
579
|
async function embedOne(
|
|
443
580
|
embedder: MemoryEmbeddingProvider,
|
|
444
581
|
text: string,
|
|
445
|
-
): Promise<{
|
|
446
|
-
model: string;
|
|
447
|
-
provider: string;
|
|
448
|
-
vector: number[];
|
|
449
|
-
}> {
|
|
582
|
+
): Promise<MemoryEmbedding> {
|
|
450
583
|
const normalized = normalizeContent(text);
|
|
451
584
|
if (!normalized) {
|
|
452
585
|
throw new Error("Embedding text is required.");
|
|
@@ -469,10 +602,11 @@ async function storeEmbedding(args: {
|
|
|
469
602
|
content: string;
|
|
470
603
|
db: MemoryDb;
|
|
471
604
|
embedder: MemoryEmbeddingProvider | undefined;
|
|
605
|
+
embedding?: MemoryEmbedding;
|
|
472
606
|
memoryId: string;
|
|
473
607
|
nowMs: number;
|
|
474
608
|
}): Promise<void> {
|
|
475
|
-
if (!args.embedder) {
|
|
609
|
+
if (!args.embedder && !args.embedding) {
|
|
476
610
|
return;
|
|
477
611
|
}
|
|
478
612
|
try {
|
|
@@ -488,10 +622,18 @@ async function storeEmbedding(args: {
|
|
|
488
622
|
return;
|
|
489
623
|
}
|
|
490
624
|
let embedding: Awaited<ReturnType<typeof embedOne>>;
|
|
491
|
-
|
|
492
|
-
embedding =
|
|
493
|
-
}
|
|
494
|
-
|
|
625
|
+
if (args.embedding) {
|
|
626
|
+
embedding = args.embedding;
|
|
627
|
+
} else {
|
|
628
|
+
const embedder = args.embedder;
|
|
629
|
+
if (!embedder) {
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
try {
|
|
633
|
+
embedding = await embedOne(embedder, args.content);
|
|
634
|
+
} catch {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
495
637
|
}
|
|
496
638
|
try {
|
|
497
639
|
await args.db
|
|
@@ -512,6 +654,293 @@ async function storeEmbedding(args: {
|
|
|
512
654
|
}
|
|
513
655
|
}
|
|
514
656
|
|
|
657
|
+
function activeScopedSubjectPredicate(args: {
|
|
658
|
+
kind: MemoryRecord["kind"];
|
|
659
|
+
nowMs: number;
|
|
660
|
+
scope: ResolvedMemoryScope;
|
|
661
|
+
subject: ResolvedMemorySubject;
|
|
662
|
+
}): SQL {
|
|
663
|
+
const predicate = and(
|
|
664
|
+
eq(juniorMemoryMemories.scope, args.scope.scope),
|
|
665
|
+
eq(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
|
|
666
|
+
eq(juniorMemoryMemories.kind, args.kind),
|
|
667
|
+
eq(juniorMemoryMemories.subjectType, args.subject.subjectType),
|
|
668
|
+
args.subject.subjectKey === undefined
|
|
669
|
+
? isNull(juniorMemoryMemories.subjectKey)
|
|
670
|
+
: eq(juniorMemoryMemories.subjectKey, args.subject.subjectKey),
|
|
671
|
+
isNull(juniorMemoryMemories.archivedAtMs),
|
|
672
|
+
isNull(juniorMemoryMemories.supersededAtMs),
|
|
673
|
+
isNull(juniorMemoryMemories.supersededById),
|
|
674
|
+
or(
|
|
675
|
+
isNull(juniorMemoryMemories.expiresAtMs),
|
|
676
|
+
gt(juniorMemoryMemories.expiresAtMs, args.nowMs),
|
|
677
|
+
),
|
|
678
|
+
);
|
|
679
|
+
if (!predicate) {
|
|
680
|
+
throw new Error("Memory duplicate predicate is empty.");
|
|
681
|
+
}
|
|
682
|
+
return predicate;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async function findExactDuplicateMemory(args: {
|
|
686
|
+
content: string;
|
|
687
|
+
db: MemoryDb;
|
|
688
|
+
kind: MemoryRecord["kind"];
|
|
689
|
+
nowMs: number;
|
|
690
|
+
scope: ResolvedMemoryScope;
|
|
691
|
+
subject: ResolvedMemorySubject;
|
|
692
|
+
}): Promise<MemoryRecord | undefined> {
|
|
693
|
+
const rows = await args.db
|
|
694
|
+
.select()
|
|
695
|
+
.from(juniorMemoryMemories)
|
|
696
|
+
.where(
|
|
697
|
+
and(
|
|
698
|
+
activeScopedSubjectPredicate(args),
|
|
699
|
+
eq(juniorMemoryMemories.content, args.content),
|
|
700
|
+
),
|
|
701
|
+
)
|
|
702
|
+
.orderBy(
|
|
703
|
+
desc(juniorMemoryMemories.createdAtMs),
|
|
704
|
+
asc(juniorMemoryMemories.id),
|
|
705
|
+
)
|
|
706
|
+
.limit(1);
|
|
707
|
+
return rows[0] ? parseMemoryRow(rows[0]) : undefined;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async function findVectorDuplicateMemory(args: {
|
|
711
|
+
db: MemoryDb;
|
|
712
|
+
embedding: MemoryEmbedding;
|
|
713
|
+
kind: MemoryRecord["kind"];
|
|
714
|
+
nowMs: number;
|
|
715
|
+
scope: ResolvedMemoryScope;
|
|
716
|
+
subject: ResolvedMemorySubject;
|
|
717
|
+
}): Promise<MemoryRecord | undefined> {
|
|
718
|
+
const distance = cosineDistance(
|
|
719
|
+
juniorMemoryEmbeddings.embedding,
|
|
720
|
+
args.embedding.vector,
|
|
721
|
+
);
|
|
722
|
+
const rows = await args.db
|
|
723
|
+
.select({
|
|
724
|
+
contentHash: juniorMemoryEmbeddings.contentHash,
|
|
725
|
+
distance,
|
|
726
|
+
memory: juniorMemoryMemories,
|
|
727
|
+
})
|
|
728
|
+
.from(juniorMemoryMemories)
|
|
729
|
+
.innerJoin(
|
|
730
|
+
juniorMemoryEmbeddings,
|
|
731
|
+
eq(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id),
|
|
732
|
+
)
|
|
733
|
+
.where(
|
|
734
|
+
and(
|
|
735
|
+
activeScopedSubjectPredicate(args),
|
|
736
|
+
eq(juniorMemoryEmbeddings.provider, args.embedding.provider),
|
|
737
|
+
eq(juniorMemoryEmbeddings.model, args.embedding.model),
|
|
738
|
+
eq(juniorMemoryEmbeddings.dimensions, MEMORY_EMBEDDING_DIMENSIONS),
|
|
739
|
+
eq(juniorMemoryEmbeddings.metric, EMBEDDING_METRIC),
|
|
740
|
+
lte(distance, HIGH_CONFIDENCE_DUPLICATE_DISTANCE),
|
|
741
|
+
),
|
|
742
|
+
)
|
|
743
|
+
.orderBy(
|
|
744
|
+
distance,
|
|
745
|
+
desc(juniorMemoryMemories.createdAtMs),
|
|
746
|
+
asc(juniorMemoryMemories.id),
|
|
747
|
+
)
|
|
748
|
+
.limit(1);
|
|
749
|
+
const row = rows[0];
|
|
750
|
+
if (!row || hashEmbeddedContent(row.memory.content) !== row.contentHash) {
|
|
751
|
+
return undefined;
|
|
752
|
+
}
|
|
753
|
+
return parseMemoryRow(row.memory);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
async function rememberDuplicateIdempotency(args: {
|
|
757
|
+
content: string;
|
|
758
|
+
db: MemoryDb;
|
|
759
|
+
duplicate: MemoryRecord;
|
|
760
|
+
idempotencyKey?: string;
|
|
761
|
+
nowMs: number;
|
|
762
|
+
runtimeContext: MemoryRuntimeContext;
|
|
763
|
+
scope: ResolvedMemoryScope;
|
|
764
|
+
subject: ResolvedMemorySubject;
|
|
765
|
+
}): Promise<void> {
|
|
766
|
+
if (args.idempotencyKey === undefined) {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
await args.db
|
|
770
|
+
.insert(juniorMemoryMemories)
|
|
771
|
+
.values({
|
|
772
|
+
content: args.content,
|
|
773
|
+
createdAtMs: args.nowMs,
|
|
774
|
+
expiresAtMs: args.duplicate.expiresAtMs,
|
|
775
|
+
id: idempotencyAliasId({
|
|
776
|
+
idempotencyKey: args.idempotencyKey,
|
|
777
|
+
scope: args.scope,
|
|
778
|
+
targetId: args.duplicate.id,
|
|
779
|
+
}),
|
|
780
|
+
idempotencyKey: args.idempotencyKey,
|
|
781
|
+
observedAtMs: args.nowMs,
|
|
782
|
+
scope: args.scope.scope,
|
|
783
|
+
scopeKey: args.scope.scopeKey,
|
|
784
|
+
sourceKey: sourceKey(args.runtimeContext),
|
|
785
|
+
sourcePlatform: args.runtimeContext.source.platform,
|
|
786
|
+
subjectKey: args.subject.subjectKey,
|
|
787
|
+
subjectType: args.subject.subjectType,
|
|
788
|
+
supersededAtMs: args.nowMs,
|
|
789
|
+
supersededById: args.duplicate.id,
|
|
790
|
+
kind: args.duplicate.kind,
|
|
791
|
+
})
|
|
792
|
+
.onConflictDoNothing();
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async function listSupersessionCandidates(args: {
|
|
796
|
+
content: string;
|
|
797
|
+
db: MemoryDb;
|
|
798
|
+
embedding?: MemoryEmbedding;
|
|
799
|
+
kind: MemoryRecord["kind"];
|
|
800
|
+
nowMs: number;
|
|
801
|
+
scope: ResolvedMemoryScope;
|
|
802
|
+
subject: ResolvedMemorySubject;
|
|
803
|
+
}): Promise<MemoryRecord[]> {
|
|
804
|
+
const predicate = activeScopedSubjectPredicate(args);
|
|
805
|
+
const terms = searchTerms(args.content).filter(
|
|
806
|
+
(term) => !SUPERSESSION_STOP_TERMS.has(term),
|
|
807
|
+
);
|
|
808
|
+
const lexicalRows =
|
|
809
|
+
terms.length > 0
|
|
810
|
+
? await args.db
|
|
811
|
+
.select()
|
|
812
|
+
.from(juniorMemoryMemories)
|
|
813
|
+
.where(
|
|
814
|
+
and(
|
|
815
|
+
predicate,
|
|
816
|
+
or(
|
|
817
|
+
...terms.map((term) =>
|
|
818
|
+
ilike(juniorMemoryMemories.content, `%${term}%`),
|
|
819
|
+
),
|
|
820
|
+
),
|
|
821
|
+
),
|
|
822
|
+
)
|
|
823
|
+
: [];
|
|
824
|
+
const lexicalCandidates = lexicalRows
|
|
825
|
+
.map(parseMemoryRow)
|
|
826
|
+
.map((memory) => ({
|
|
827
|
+
memory,
|
|
828
|
+
score: searchScore(memory, terms),
|
|
829
|
+
}))
|
|
830
|
+
.filter((candidate) => candidate.score > 1)
|
|
831
|
+
.sort(
|
|
832
|
+
(left, right) =>
|
|
833
|
+
right.score - left.score ||
|
|
834
|
+
right.memory.createdAtMs - left.memory.createdAtMs ||
|
|
835
|
+
left.memory.id.localeCompare(right.memory.id),
|
|
836
|
+
)
|
|
837
|
+
.map((candidate) => candidate.memory);
|
|
838
|
+
|
|
839
|
+
const vectorCandidates = args.embedding
|
|
840
|
+
? await listVectorSupersessionCandidates({
|
|
841
|
+
db: args.db,
|
|
842
|
+
embedding: args.embedding,
|
|
843
|
+
kind: args.kind,
|
|
844
|
+
nowMs: args.nowMs,
|
|
845
|
+
scope: args.scope,
|
|
846
|
+
subject: args.subject,
|
|
847
|
+
})
|
|
848
|
+
: [];
|
|
849
|
+
const byId = new Map<string, MemoryRecord>();
|
|
850
|
+
for (const memory of [...lexicalCandidates, ...vectorCandidates]) {
|
|
851
|
+
if (byId.size >= SUPERSESSION_CANDIDATE_LIMIT) {
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
byId.set(memory.id, memory);
|
|
855
|
+
}
|
|
856
|
+
return [...byId.values()];
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
async function listVectorSupersessionCandidates(args: {
|
|
860
|
+
db: MemoryDb;
|
|
861
|
+
embedding: MemoryEmbedding;
|
|
862
|
+
kind: MemoryRecord["kind"];
|
|
863
|
+
nowMs: number;
|
|
864
|
+
scope: ResolvedMemoryScope;
|
|
865
|
+
subject: ResolvedMemorySubject;
|
|
866
|
+
}): Promise<MemoryRecord[]> {
|
|
867
|
+
const distance = cosineDistance(
|
|
868
|
+
juniorMemoryEmbeddings.embedding,
|
|
869
|
+
args.embedding.vector,
|
|
870
|
+
);
|
|
871
|
+
const rows = await args.db
|
|
872
|
+
.select({
|
|
873
|
+
contentHash: juniorMemoryEmbeddings.contentHash,
|
|
874
|
+
distance,
|
|
875
|
+
memory: juniorMemoryMemories,
|
|
876
|
+
})
|
|
877
|
+
.from(juniorMemoryMemories)
|
|
878
|
+
.innerJoin(
|
|
879
|
+
juniorMemoryEmbeddings,
|
|
880
|
+
eq(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id),
|
|
881
|
+
)
|
|
882
|
+
.where(
|
|
883
|
+
and(
|
|
884
|
+
activeScopedSubjectPredicate(args),
|
|
885
|
+
eq(juniorMemoryEmbeddings.provider, args.embedding.provider),
|
|
886
|
+
eq(juniorMemoryEmbeddings.model, args.embedding.model),
|
|
887
|
+
eq(juniorMemoryEmbeddings.dimensions, MEMORY_EMBEDDING_DIMENSIONS),
|
|
888
|
+
eq(juniorMemoryEmbeddings.metric, EMBEDDING_METRIC),
|
|
889
|
+
),
|
|
890
|
+
)
|
|
891
|
+
.orderBy(
|
|
892
|
+
distance,
|
|
893
|
+
desc(juniorMemoryMemories.createdAtMs),
|
|
894
|
+
asc(juniorMemoryMemories.id),
|
|
895
|
+
)
|
|
896
|
+
.limit(SUPERSESSION_CANDIDATE_LIMIT);
|
|
897
|
+
return rows.flatMap((row) => {
|
|
898
|
+
if (hashEmbeddedContent(row.memory.content) !== row.contentHash) {
|
|
899
|
+
return [];
|
|
900
|
+
}
|
|
901
|
+
return [parseMemoryRow(row.memory)];
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
async function decideSupersededIds(args: {
|
|
906
|
+
candidates: MemoryRecord[];
|
|
907
|
+
content: string;
|
|
908
|
+
decider: MemorySupersessionDecider | undefined;
|
|
909
|
+
kind: MemoryRecord["kind"];
|
|
910
|
+
runtimeContext: MemoryRuntimeContext;
|
|
911
|
+
}): Promise<string[]> {
|
|
912
|
+
// Supersession is conservative: model uncertainty or decider failure leaves
|
|
913
|
+
// both memories active rather than hiding a possibly valid old preference.
|
|
914
|
+
if (
|
|
915
|
+
args.kind !== "preference" ||
|
|
916
|
+
!args.decider ||
|
|
917
|
+
args.candidates.length === 0
|
|
918
|
+
) {
|
|
919
|
+
return [];
|
|
920
|
+
}
|
|
921
|
+
const existingMemories = args.candidates.map((memory) => ({
|
|
922
|
+
content: memory.content,
|
|
923
|
+
id: memory.id,
|
|
924
|
+
})) as MemorySupersessionInput["existingMemories"];
|
|
925
|
+
const candidateIds = new Set(args.candidates.map((memory) => memory.id));
|
|
926
|
+
try {
|
|
927
|
+
const decision = await args.decider.adjudicateSupersession({
|
|
928
|
+
candidate: {
|
|
929
|
+
content: args.content,
|
|
930
|
+
kind: "preference",
|
|
931
|
+
},
|
|
932
|
+
existingMemories,
|
|
933
|
+
runtimeContext: args.runtimeContext,
|
|
934
|
+
});
|
|
935
|
+
if (decision.decision !== "supersedes_old") {
|
|
936
|
+
return [];
|
|
937
|
+
}
|
|
938
|
+
return decision.supersededIds.filter((id) => candidateIds.has(id));
|
|
939
|
+
} catch {
|
|
940
|
+
return [];
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
515
944
|
/** List active records for the runtime-derived visible scopes. */
|
|
516
945
|
async function listVisibleMemories(args: {
|
|
517
946
|
db: MemoryDb;
|
|
@@ -542,7 +971,7 @@ async function searchVisibleMemories(args: {
|
|
|
542
971
|
nowMs: number;
|
|
543
972
|
query: string;
|
|
544
973
|
scopes: ResolvedMemoryScope[];
|
|
545
|
-
}): Promise<
|
|
974
|
+
}): Promise<SearchCandidate[]> {
|
|
546
975
|
const terms = searchTerms(args.query);
|
|
547
976
|
if (terms.length === 0) {
|
|
548
977
|
return [];
|
|
@@ -564,7 +993,11 @@ async function searchVisibleMemories(args: {
|
|
|
564
993
|
),
|
|
565
994
|
),
|
|
566
995
|
);
|
|
567
|
-
return rows.map(
|
|
996
|
+
return rows.map((row) => ({
|
|
997
|
+
memory: parseMemoryRow(row),
|
|
998
|
+
score: 0,
|
|
999
|
+
sourceKey: row.sourceKey,
|
|
1000
|
+
}));
|
|
568
1001
|
}
|
|
569
1002
|
|
|
570
1003
|
/** Search active visible records with exact pgvector cosine distance. */
|
|
@@ -572,6 +1005,7 @@ async function searchVisibleVectorMemories(args: {
|
|
|
572
1005
|
db: MemoryDb;
|
|
573
1006
|
embedder: MemoryEmbeddingProvider | undefined;
|
|
574
1007
|
limit: number;
|
|
1008
|
+
maxDistance?: number;
|
|
575
1009
|
nowMs: number;
|
|
576
1010
|
query: string;
|
|
577
1011
|
scopes: ResolvedMemoryScope[];
|
|
@@ -628,22 +1062,58 @@ async function searchVisibleVectorMemories(args: {
|
|
|
628
1062
|
) {
|
|
629
1063
|
return [];
|
|
630
1064
|
}
|
|
1065
|
+
if (args.maxDistance !== undefined && distanceValue > args.maxDistance) {
|
|
1066
|
+
return [];
|
|
1067
|
+
}
|
|
631
1068
|
return [
|
|
632
1069
|
{
|
|
633
1070
|
memory: parseMemoryRow(row.memory),
|
|
634
1071
|
score: 1 / (1 + Math.max(0, distanceValue)),
|
|
1072
|
+
sourceKey: row.memory.sourceKey,
|
|
635
1073
|
},
|
|
636
1074
|
];
|
|
637
1075
|
});
|
|
638
1076
|
}
|
|
639
1077
|
|
|
640
|
-
|
|
641
|
-
|
|
1078
|
+
function sourceBoost(
|
|
1079
|
+
candidate: Pick<SearchCandidate, "sourceKey">,
|
|
1080
|
+
currentChannelPrefix: string | undefined,
|
|
1081
|
+
): number {
|
|
1082
|
+
if (!currentChannelPrefix) {
|
|
1083
|
+
return 0;
|
|
1084
|
+
}
|
|
1085
|
+
return candidate.sourceKey.startsWith(currentChannelPrefix)
|
|
1086
|
+
? SOURCE_CHANNEL_BOOST
|
|
1087
|
+
: 0;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/** Score only observation age for near-tie reranking; this is not lifecycle decay. */
|
|
1091
|
+
function observedAgeBoost(memory: MemoryRecord, nowMs: number): number {
|
|
1092
|
+
const ageMs = Math.max(0, nowMs - memory.observedAtMs);
|
|
1093
|
+
if (ageMs <= 7 * ONE_DAY_MS) {
|
|
1094
|
+
return 0.15;
|
|
1095
|
+
}
|
|
1096
|
+
if (ageMs <= 30 * ONE_DAY_MS) {
|
|
1097
|
+
return 0.1;
|
|
1098
|
+
}
|
|
1099
|
+
if (ageMs <= 90 * ONE_DAY_MS) {
|
|
1100
|
+
return 0.05;
|
|
1101
|
+
}
|
|
1102
|
+
return 0;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/** Fuse retrieval candidates while keeping observed age to near-tie reranking. */
|
|
1106
|
+
function mergeSearchCandidates(
|
|
1107
|
+
candidates: SearchCandidate[],
|
|
1108
|
+
nowMs: number,
|
|
1109
|
+
currentChannelKey?: string,
|
|
1110
|
+
): MemoryRecord[] {
|
|
642
1111
|
const byId = new Map<
|
|
643
1112
|
string,
|
|
644
1113
|
{
|
|
645
1114
|
memory: MemoryRecord;
|
|
646
1115
|
score: number;
|
|
1116
|
+
sourceKey: string;
|
|
647
1117
|
}
|
|
648
1118
|
>();
|
|
649
1119
|
for (const candidate of candidates) {
|
|
@@ -655,15 +1125,28 @@ function mergeSearchCandidates(candidates: SearchCandidate[]): MemoryRecord[] {
|
|
|
655
1125
|
byId.set(candidate.memory.id, {
|
|
656
1126
|
memory: candidate.memory,
|
|
657
1127
|
score: candidate.score,
|
|
1128
|
+
sourceKey: candidate.sourceKey,
|
|
658
1129
|
});
|
|
659
1130
|
}
|
|
660
1131
|
return [...byId.values()]
|
|
661
|
-
.sort(
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
1132
|
+
.sort((left, right) => {
|
|
1133
|
+
const scoreDelta = right.score - left.score;
|
|
1134
|
+
if (Math.abs(scoreDelta) > RELEVANCE_NEAR_TIE_DELTA) {
|
|
1135
|
+
return scoreDelta;
|
|
1136
|
+
}
|
|
1137
|
+
const sourceDelta =
|
|
1138
|
+
sourceBoost(right, currentChannelKey) -
|
|
1139
|
+
sourceBoost(left, currentChannelKey);
|
|
1140
|
+
if (sourceDelta !== 0) {
|
|
1141
|
+
return sourceDelta;
|
|
1142
|
+
}
|
|
1143
|
+
return (
|
|
1144
|
+
observedAgeBoost(right.memory, nowMs) -
|
|
1145
|
+
observedAgeBoost(left.memory, nowMs) ||
|
|
1146
|
+
right.memory.observedAtMs - left.memory.observedAtMs ||
|
|
1147
|
+
left.memory.id.localeCompare(right.memory.id)
|
|
1148
|
+
);
|
|
1149
|
+
})
|
|
667
1150
|
.map((candidate) => candidate.memory);
|
|
668
1151
|
}
|
|
669
1152
|
|
|
@@ -676,6 +1159,8 @@ export function createMemoryStore(
|
|
|
676
1159
|
const runtimeContext = memoryRuntimeContextSchema.parse(context);
|
|
677
1160
|
const parsedOptions = memoryStoreOptionsSchema.parse({ now: options.now });
|
|
678
1161
|
const embedder = options.embedder;
|
|
1162
|
+
const maxVectorDistance = options.maxVectorDistance;
|
|
1163
|
+
const supersessionDecider = options.supersessionDecider;
|
|
679
1164
|
const getNowMs = parsedOptions.now ?? Date.now;
|
|
680
1165
|
|
|
681
1166
|
async function archiveExpiredVisibleMemories(
|
|
@@ -716,40 +1201,183 @@ export function createMemoryStore(
|
|
|
716
1201
|
nowMs,
|
|
717
1202
|
scopes: [scope],
|
|
718
1203
|
});
|
|
1204
|
+
if (input.idempotencyKey !== undefined) {
|
|
1205
|
+
const idempotent = await findByIdempotencyKey({
|
|
1206
|
+
db,
|
|
1207
|
+
idempotencyKey: input.idempotencyKey,
|
|
1208
|
+
nowMs,
|
|
1209
|
+
scope,
|
|
1210
|
+
});
|
|
1211
|
+
if (idempotent) {
|
|
1212
|
+
await storeEmbedding({
|
|
1213
|
+
content: idempotent.content,
|
|
1214
|
+
db,
|
|
1215
|
+
embedder,
|
|
1216
|
+
memoryId: idempotent.id,
|
|
1217
|
+
nowMs,
|
|
1218
|
+
});
|
|
1219
|
+
return { created: false, memory: idempotent };
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
719
1222
|
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
.
|
|
1223
|
+
const exactDuplicate = await findExactDuplicateMemory({
|
|
1224
|
+
content,
|
|
1225
|
+
db,
|
|
1226
|
+
kind: input.kind,
|
|
1227
|
+
nowMs,
|
|
1228
|
+
scope,
|
|
1229
|
+
subject,
|
|
1230
|
+
});
|
|
1231
|
+
if (exactDuplicate) {
|
|
1232
|
+
await rememberDuplicateIdempotency({
|
|
1233
|
+
content,
|
|
1234
|
+
db,
|
|
1235
|
+
duplicate: exactDuplicate,
|
|
1236
|
+
idempotencyKey: input.idempotencyKey,
|
|
1237
|
+
nowMs,
|
|
1238
|
+
runtimeContext,
|
|
1239
|
+
scope,
|
|
1240
|
+
subject,
|
|
1241
|
+
});
|
|
1242
|
+
await storeEmbedding({
|
|
1243
|
+
content: exactDuplicate.content,
|
|
1244
|
+
db,
|
|
1245
|
+
embedder,
|
|
1246
|
+
memoryId: exactDuplicate.id,
|
|
1247
|
+
nowMs,
|
|
1248
|
+
});
|
|
1249
|
+
return { created: false, memory: exactDuplicate };
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
let candidateEmbedding: MemoryEmbedding | undefined;
|
|
1253
|
+
if (embedder) {
|
|
1254
|
+
try {
|
|
1255
|
+
candidateEmbedding = await embedOne(embedder, content);
|
|
1256
|
+
} catch {
|
|
1257
|
+
candidateEmbedding = undefined;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
const vectorDuplicate = candidateEmbedding
|
|
1261
|
+
? await findVectorDuplicateMemory({
|
|
1262
|
+
db,
|
|
1263
|
+
embedding: candidateEmbedding,
|
|
1264
|
+
kind: input.kind,
|
|
1265
|
+
nowMs,
|
|
1266
|
+
scope,
|
|
1267
|
+
subject,
|
|
1268
|
+
})
|
|
1269
|
+
: undefined;
|
|
1270
|
+
if (vectorDuplicate) {
|
|
1271
|
+
await rememberDuplicateIdempotency({
|
|
724
1272
|
content,
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
id,
|
|
1273
|
+
db,
|
|
1274
|
+
duplicate: vectorDuplicate,
|
|
728
1275
|
idempotencyKey: input.idempotencyKey,
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
1276
|
+
nowMs,
|
|
1277
|
+
runtimeContext,
|
|
1278
|
+
scope,
|
|
1279
|
+
subject,
|
|
1280
|
+
});
|
|
1281
|
+
await storeEmbedding({
|
|
1282
|
+
content: vectorDuplicate.content,
|
|
1283
|
+
db,
|
|
1284
|
+
embedder,
|
|
1285
|
+
memoryId: vectorDuplicate.id,
|
|
1286
|
+
nowMs,
|
|
1287
|
+
});
|
|
1288
|
+
return { created: false, memory: vectorDuplicate };
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
let supersededIds: string[] = [];
|
|
1292
|
+
if (
|
|
1293
|
+
scopeKind === "personal" &&
|
|
1294
|
+
input.kind === "preference" &&
|
|
1295
|
+
supersessionDecider &&
|
|
1296
|
+
(input.expiresAtMs === undefined || input.expiresAtMs > nowMs)
|
|
1297
|
+
) {
|
|
1298
|
+
const supersessionCandidates = await listSupersessionCandidates({
|
|
1299
|
+
content,
|
|
1300
|
+
db,
|
|
1301
|
+
...(candidateEmbedding ? { embedding: candidateEmbedding } : {}),
|
|
736
1302
|
kind: input.kind,
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
1303
|
+
nowMs,
|
|
1304
|
+
scope,
|
|
1305
|
+
subject,
|
|
1306
|
+
});
|
|
1307
|
+
supersededIds = await decideSupersededIds({
|
|
1308
|
+
candidates: supersessionCandidates,
|
|
1309
|
+
content,
|
|
1310
|
+
decider: supersessionDecider,
|
|
1311
|
+
kind: input.kind,
|
|
1312
|
+
runtimeContext,
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
const id = randomUUID();
|
|
1317
|
+
const rows = await db.transaction(async (tx) => {
|
|
1318
|
+
const inserted = await tx
|
|
1319
|
+
.insert(juniorMemoryMemories)
|
|
1320
|
+
.values({
|
|
1321
|
+
content,
|
|
1322
|
+
createdAtMs: nowMs,
|
|
1323
|
+
expiresAtMs: input.expiresAtMs,
|
|
1324
|
+
id,
|
|
1325
|
+
idempotencyKey: input.idempotencyKey,
|
|
1326
|
+
observedAtMs: nowMs,
|
|
1327
|
+
scope: scope.scope,
|
|
1328
|
+
scopeKey: scope.scopeKey,
|
|
1329
|
+
sourceKey: sourceKey(runtimeContext),
|
|
1330
|
+
sourcePlatform: runtimeContext.source.platform,
|
|
1331
|
+
subjectKey: subject.subjectKey,
|
|
1332
|
+
subjectType: subject.subjectType,
|
|
1333
|
+
kind: input.kind,
|
|
1334
|
+
})
|
|
1335
|
+
.onConflictDoNothing({
|
|
1336
|
+
target: [
|
|
1337
|
+
juniorMemoryMemories.scope,
|
|
1338
|
+
juniorMemoryMemories.scopeKey,
|
|
1339
|
+
juniorMemoryMemories.idempotencyKey,
|
|
1340
|
+
],
|
|
1341
|
+
where: sql`${juniorMemoryMemories.idempotencyKey} IS NOT NULL AND ${juniorMemoryMemories.archivedAtMs} IS NULL AND ${juniorMemoryMemories.supersededAtMs} IS NULL AND ${juniorMemoryMemories.supersededById} IS NULL`,
|
|
1342
|
+
})
|
|
1343
|
+
.returning();
|
|
1344
|
+
const insertedMemory = inserted[0];
|
|
1345
|
+
if (!insertedMemory || supersededIds.length === 0) {
|
|
1346
|
+
return inserted;
|
|
1347
|
+
}
|
|
1348
|
+
const superseded = await tx
|
|
1349
|
+
.update(juniorMemoryMemories)
|
|
1350
|
+
.set({
|
|
1351
|
+
supersededAtMs: nowMs,
|
|
1352
|
+
supersededById: insertedMemory.id,
|
|
1353
|
+
})
|
|
1354
|
+
.where(
|
|
1355
|
+
and(
|
|
1356
|
+
inArray(juniorMemoryMemories.id, supersededIds),
|
|
1357
|
+
activeScopedSubjectPredicate({
|
|
1358
|
+
kind: input.kind,
|
|
1359
|
+
nowMs,
|
|
1360
|
+
scope,
|
|
1361
|
+
subject,
|
|
1362
|
+
}),
|
|
1363
|
+
),
|
|
1364
|
+
)
|
|
1365
|
+
.returning({ id: juniorMemoryMemories.id });
|
|
1366
|
+
const idsToClean = superseded.map((row) => row.id);
|
|
1367
|
+
if (idsToClean.length > 0) {
|
|
1368
|
+
await tx
|
|
1369
|
+
.delete(juniorMemoryEmbeddings)
|
|
1370
|
+
.where(inArray(juniorMemoryEmbeddings.memoryId, idsToClean));
|
|
1371
|
+
}
|
|
1372
|
+
return inserted;
|
|
1373
|
+
});
|
|
747
1374
|
if (rows[0]) {
|
|
748
1375
|
const memory = parseMemoryRow(rows[0]);
|
|
749
1376
|
await storeEmbedding({
|
|
750
1377
|
content: memory.content,
|
|
751
1378
|
db,
|
|
752
1379
|
embedder,
|
|
1380
|
+
embedding: candidateEmbedding,
|
|
753
1381
|
memoryId: memory.id,
|
|
754
1382
|
nowMs,
|
|
755
1383
|
});
|
|
@@ -819,6 +1447,9 @@ export function createMemoryStore(
|
|
|
819
1447
|
db,
|
|
820
1448
|
embedder,
|
|
821
1449
|
limit: limit * VECTOR_SEARCH_OVERFETCH,
|
|
1450
|
+
...(maxVectorDistance !== undefined
|
|
1451
|
+
? { maxDistance: maxVectorDistance }
|
|
1452
|
+
: {}),
|
|
822
1453
|
nowMs,
|
|
823
1454
|
query: input.query,
|
|
824
1455
|
scopes,
|
|
@@ -831,12 +1462,16 @@ export function createMemoryStore(
|
|
|
831
1462
|
});
|
|
832
1463
|
const terms = searchTerms(input.query);
|
|
833
1464
|
const lexicalCandidates = candidates
|
|
834
|
-
.map((
|
|
1465
|
+
.map((candidate) => ({
|
|
1466
|
+
...candidate,
|
|
1467
|
+
score: searchScore(candidate.memory, terms),
|
|
1468
|
+
}))
|
|
835
1469
|
.filter((item) => item.score > 0);
|
|
836
|
-
return mergeSearchCandidates(
|
|
837
|
-
...vectorCandidates,
|
|
838
|
-
|
|
839
|
-
|
|
1470
|
+
return mergeSearchCandidates(
|
|
1471
|
+
[...vectorCandidates, ...lexicalCandidates],
|
|
1472
|
+
nowMs,
|
|
1473
|
+
sourceChannelPrefix(runtimeContext),
|
|
1474
|
+
).slice(0, limit);
|
|
840
1475
|
},
|
|
841
1476
|
|
|
842
1477
|
async archiveMemory(input) {
|