@sentry/junior-memory 0.106.0 → 0.107.1

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/src/store.ts CHANGED
@@ -50,24 +50,14 @@ const DEFAULT_LIST_LIMIT = 50;
50
50
  const DEFAULT_SEARCH_LIMIT = 10;
51
51
  const DEFAULT_EXPIRED_ARCHIVE_LIMIT = 100;
52
52
  const HIGH_CONFIDENCE_DUPLICATE_DISTANCE = 0.015;
53
- const SUPERSESSION_CANDIDATE_LIMIT = 10;
53
+ const PREFERENCE_ADJUDICATION_CANDIDATE_LIMIT = 10;
54
+ const PREFERENCE_ADJUDICATION_VECTOR_LIMIT = 5;
54
55
  const VECTOR_SEARCH_OVERFETCH = 4;
55
56
  const MAX_MEMORY_CONTENT_CHARS = 4_000;
56
57
  const EMBEDDING_METRIC = "cosine";
57
58
  const ONE_DAY_MS = 24 * 60 * 60 * 1000;
58
59
  const RELEVANCE_NEAR_TIE_DELTA = 0.01;
59
60
  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
- ]);
71
61
 
72
62
  export type MemoryDb = PgDatabase<PgQueryResultHKT, typeof memorySqlSchema>;
73
63
 
@@ -206,6 +196,61 @@ const embeddingResultSchema = z
206
196
  vectors: z.array(embeddingVectorSchema),
207
197
  })
208
198
  .strict();
199
+ const memorySupersessionCandidateSchema = z
200
+ .object({
201
+ content: z.string().min(1),
202
+ id: z.string().min(1),
203
+ })
204
+ .strict();
205
+ const memorySupersessionCandidatesSchema = z
206
+ .array(memorySupersessionCandidateSchema)
207
+ .min(1)
208
+ .max(PREFERENCE_ADJUDICATION_CANDIDATE_LIMIT);
209
+ const supersededIdsSchema = z
210
+ .array(z.string().min(1))
211
+ .min(1)
212
+ .max(PREFERENCE_ADJUDICATION_CANDIDATE_LIMIT);
213
+
214
+ /** Validated preference comparison input supplied to a supersession decider. */
215
+ export const memorySupersessionInputSchema = z
216
+ .object({
217
+ candidate: z
218
+ .object({
219
+ content: z.string().min(1),
220
+ kind: z.literal("preference"),
221
+ })
222
+ .strict(),
223
+ existingMemories: memorySupersessionCandidatesSchema,
224
+ runtimeContext: memoryRuntimeContextSchema,
225
+ })
226
+ .strict();
227
+
228
+ /**
229
+ * Validated preference decision whose referenced ids must come from the
230
+ * supplied existing memories.
231
+ */
232
+ export const memorySupersessionDecisionSchema = z.discriminatedUnion(
233
+ "decision",
234
+ [
235
+ z
236
+ .object({
237
+ decision: z.literal("duplicate"),
238
+ duplicateId: z.string().min(1),
239
+ })
240
+ .strict(),
241
+ z
242
+ .object({
243
+ decision: z.literal("supersedes_old"),
244
+ supersededIds: supersededIdsSchema,
245
+ })
246
+ .strict(),
247
+ z
248
+ .object({
249
+ decision: z.enum(["distinct", "uncertain"]),
250
+ })
251
+ .strict(),
252
+ ],
253
+ );
209
254
 
210
255
  export type MemoryRecord = z.output<typeof memoryRecordSchema>;
211
256
  export type CreateMemoryInput = z.output<typeof createMemoryInputSchema>;
@@ -240,35 +285,16 @@ export interface MemoryEmbeddingProvider {
240
285
  }>;
241
286
  }
242
287
 
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
- }
288
+ export type MemorySupersessionInput = z.output<
289
+ typeof memorySupersessionInputSchema
290
+ >;
260
291
 
261
- export type MemorySupersessionDecision =
262
- | {
263
- decision: "supersedes_old";
264
- supersededIds: [string, ...string[]];
265
- }
266
- | {
267
- decision: "distinct" | "uncertain";
268
- };
292
+ export type MemorySupersessionDecision = z.output<
293
+ typeof memorySupersessionDecisionSchema
294
+ >;
269
295
 
270
296
  export interface MemorySupersessionDecider {
271
- /** Decide whether a new preference clearly replaces active old preferences. */
297
+ /** Classify a new preference against related active preferences. */
272
298
  adjudicateSupersession(
273
299
  input: MemorySupersessionInput,
274
300
  ): Promise<MemorySupersessionDecision> | MemorySupersessionDecision;
@@ -792,74 +818,52 @@ async function rememberDuplicateIdempotency(args: {
792
818
  .onConflictDoNothing();
793
819
  }
794
820
 
795
- async function listSupersessionCandidates(args: {
796
- content: string;
821
+ /** Select semantic preferences, then fill the window by recency for unembedded records. */
822
+ async function listPreferenceAdjudicationCandidates(args: {
797
823
  db: MemoryDb;
798
824
  embedding?: MemoryEmbedding;
799
- kind: MemoryRecord["kind"];
800
825
  nowMs: number;
801
826
  scope: ResolvedMemoryScope;
802
827
  subject: ResolvedMemorySubject;
803
828
  }): 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
829
  const vectorCandidates = args.embedding
840
- ? await listVectorSupersessionCandidates({
830
+ ? await listVectorPreferenceAdjudicationCandidates({
841
831
  db: args.db,
842
832
  embedding: args.embedding,
843
- kind: args.kind,
844
833
  nowMs: args.nowMs,
845
834
  scope: args.scope,
846
835
  subject: args.subject,
847
836
  })
848
837
  : [];
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()];
838
+ const recentCandidates = (
839
+ await args.db
840
+ .select()
841
+ .from(juniorMemoryMemories)
842
+ .where(
843
+ activeScopedSubjectPredicate({
844
+ ...args,
845
+ kind: "preference",
846
+ }),
847
+ )
848
+ .orderBy(
849
+ desc(juniorMemoryMemories.createdAtMs),
850
+ asc(juniorMemoryMemories.id),
851
+ )
852
+ .limit(PREFERENCE_ADJUDICATION_CANDIDATE_LIMIT)
853
+ ).map(parseMemoryRow);
854
+ return [
855
+ ...new Map(
856
+ [...vectorCandidates, ...recentCandidates].map((memory) => [
857
+ memory.id,
858
+ memory,
859
+ ]),
860
+ ).values(),
861
+ ].slice(0, PREFERENCE_ADJUDICATION_CANDIDATE_LIMIT);
857
862
  }
858
863
 
859
- async function listVectorSupersessionCandidates(args: {
864
+ async function listVectorPreferenceAdjudicationCandidates(args: {
860
865
  db: MemoryDb;
861
866
  embedding: MemoryEmbedding;
862
- kind: MemoryRecord["kind"];
863
867
  nowMs: number;
864
868
  scope: ResolvedMemoryScope;
865
869
  subject: ResolvedMemorySubject;
@@ -881,7 +885,7 @@ async function listVectorSupersessionCandidates(args: {
881
885
  )
882
886
  .where(
883
887
  and(
884
- activeScopedSubjectPredicate(args),
888
+ activeScopedSubjectPredicate({ ...args, kind: "preference" }),
885
889
  eq(juniorMemoryEmbeddings.provider, args.embedding.provider),
886
890
  eq(juniorMemoryEmbeddings.model, args.embedding.model),
887
891
  eq(juniorMemoryEmbeddings.dimensions, MEMORY_EMBEDDING_DIMENSIONS),
@@ -893,7 +897,7 @@ async function listVectorSupersessionCandidates(args: {
893
897
  desc(juniorMemoryMemories.createdAtMs),
894
898
  asc(juniorMemoryMemories.id),
895
899
  )
896
- .limit(SUPERSESSION_CANDIDATE_LIMIT);
900
+ .limit(PREFERENCE_ADJUDICATION_VECTOR_LIMIT);
897
901
  return rows.flatMap((row) => {
898
902
  if (hashEmbeddedContent(row.memory.content) !== row.contentHash) {
899
903
  return [];
@@ -902,26 +906,32 @@ async function listVectorSupersessionCandidates(args: {
902
906
  });
903
907
  }
904
908
 
905
- async function decideSupersededIds(args: {
909
+ type PreferenceAdjudicationResult =
910
+ | { decision: "create" }
911
+ | { decision: "duplicate"; memory: MemoryRecord }
912
+ | { decision: "supersede"; ids: [string, ...string[]] };
913
+
914
+ /**
915
+ * Normalize a preference decision to known duplicate or supersession targets.
916
+ * Uncertainty, invalid ids, and model failure leave existing memories active.
917
+ */
918
+ async function adjudicatePreferenceCandidate(args: {
906
919
  candidates: MemoryRecord[];
907
920
  content: string;
908
- decider: MemorySupersessionDecider | undefined;
909
- kind: MemoryRecord["kind"];
921
+ decider: MemorySupersessionDecider;
910
922
  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 [];
923
+ }): Promise<PreferenceAdjudicationResult> {
924
+ const [firstCandidate, ...remainingCandidates] = args.candidates;
925
+ if (!firstCandidate) {
926
+ return { decision: "create" };
920
927
  }
921
- const existingMemories = args.candidates.map((memory) => ({
922
- content: memory.content,
923
- id: memory.id,
924
- })) as MemorySupersessionInput["existingMemories"];
928
+ const existingMemories = [
929
+ { content: firstCandidate.content, id: firstCandidate.id },
930
+ ...remainingCandidates.map((memory) => ({
931
+ content: memory.content,
932
+ id: memory.id,
933
+ })),
934
+ ];
925
935
  const candidateIds = new Set(args.candidates.map((memory) => memory.id));
926
936
  try {
927
937
  const decision = await args.decider.adjudicateSupersession({
@@ -932,12 +942,24 @@ async function decideSupersededIds(args: {
932
942
  existingMemories,
933
943
  runtimeContext: args.runtimeContext,
934
944
  });
935
- if (decision.decision !== "supersedes_old") {
936
- return [];
945
+ if (decision.decision === "duplicate") {
946
+ const memory = args.candidates.find(
947
+ (candidate) => candidate.id === decision.duplicateId,
948
+ );
949
+ return memory
950
+ ? { decision: "duplicate", memory }
951
+ : { decision: "create" };
952
+ }
953
+ if (decision.decision === "supersedes_old") {
954
+ const ids = decision.supersededIds.filter((id) => candidateIds.has(id));
955
+ const [firstId, ...remainingIds] = ids;
956
+ return firstId
957
+ ? { decision: "supersede", ids: [firstId, ...remainingIds] }
958
+ : { decision: "create" };
937
959
  }
938
- return decision.supersededIds.filter((id) => candidateIds.has(id));
960
+ return { decision: "create" };
939
961
  } catch {
940
- return [];
962
+ return { decision: "create" };
941
963
  }
942
964
  }
943
965
 
@@ -1176,6 +1198,29 @@ export function createMemoryStore(
1176
1198
  });
1177
1199
  }
1178
1200
 
1201
+ async function reuseDuplicateMemory(args: {
1202
+ content: string;
1203
+ duplicate: MemoryRecord;
1204
+ idempotencyKey?: string;
1205
+ nowMs: number;
1206
+ scope: ResolvedMemoryScope;
1207
+ subject: ResolvedMemorySubject;
1208
+ }): Promise<CreateMemoryResult> {
1209
+ await rememberDuplicateIdempotency({
1210
+ ...args,
1211
+ db,
1212
+ runtimeContext,
1213
+ });
1214
+ await storeEmbedding({
1215
+ content: args.duplicate.content,
1216
+ db,
1217
+ embedder,
1218
+ memoryId: args.duplicate.id,
1219
+ nowMs: args.nowMs,
1220
+ });
1221
+ return { created: false, memory: args.duplicate };
1222
+ }
1223
+
1179
1224
  /** Persist a memory under the plugin-derived scope and subject. */
1180
1225
  async function createScopedMemory(
1181
1226
  rawInput: CreateMemoryInput,
@@ -1229,24 +1274,14 @@ export function createMemoryStore(
1229
1274
  subject,
1230
1275
  });
1231
1276
  if (exactDuplicate) {
1232
- await rememberDuplicateIdempotency({
1277
+ return await reuseDuplicateMemory({
1233
1278
  content,
1234
- db,
1235
1279
  duplicate: exactDuplicate,
1236
1280
  idempotencyKey: input.idempotencyKey,
1237
1281
  nowMs,
1238
- runtimeContext,
1239
1282
  scope,
1240
1283
  subject,
1241
1284
  });
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
1285
  }
1251
1286
 
1252
1287
  let candidateEmbedding: MemoryEmbedding | undefined;
@@ -1268,24 +1303,14 @@ export function createMemoryStore(
1268
1303
  })
1269
1304
  : undefined;
1270
1305
  if (vectorDuplicate) {
1271
- await rememberDuplicateIdempotency({
1306
+ return await reuseDuplicateMemory({
1272
1307
  content,
1273
- db,
1274
1308
  duplicate: vectorDuplicate,
1275
1309
  idempotencyKey: input.idempotencyKey,
1276
1310
  nowMs,
1277
- runtimeContext,
1278
1311
  scope,
1279
1312
  subject,
1280
1313
  });
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
1314
  }
1290
1315
 
1291
1316
  let supersededIds: string[] = [];
@@ -1295,22 +1320,32 @@ export function createMemoryStore(
1295
1320
  supersessionDecider &&
1296
1321
  (input.expiresAtMs === undefined || input.expiresAtMs > nowMs)
1297
1322
  ) {
1298
- const supersessionCandidates = await listSupersessionCandidates({
1299
- content,
1323
+ const preferenceCandidates = await listPreferenceAdjudicationCandidates({
1300
1324
  db,
1301
1325
  ...(candidateEmbedding ? { embedding: candidateEmbedding } : {}),
1302
- kind: input.kind,
1303
1326
  nowMs,
1304
1327
  scope,
1305
1328
  subject,
1306
1329
  });
1307
- supersededIds = await decideSupersededIds({
1308
- candidates: supersessionCandidates,
1330
+ const adjudication = await adjudicatePreferenceCandidate({
1331
+ candidates: preferenceCandidates,
1309
1332
  content,
1310
1333
  decider: supersessionDecider,
1311
- kind: input.kind,
1312
1334
  runtimeContext,
1313
1335
  });
1336
+ if (adjudication.decision === "duplicate") {
1337
+ return await reuseDuplicateMemory({
1338
+ content,
1339
+ duplicate: adjudication.memory,
1340
+ idempotencyKey: input.idempotencyKey,
1341
+ nowMs,
1342
+ scope,
1343
+ subject,
1344
+ });
1345
+ }
1346
+ if (adjudication.decision === "supersede") {
1347
+ supersededIds = adjudication.ids;
1348
+ }
1314
1349
  }
1315
1350
 
1316
1351
  const id = randomUUID();