@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/index.js CHANGED
@@ -84,6 +84,19 @@ var extractSessionRequestSchema = z2.object({
84
84
  ])
85
85
  ).min(1)
86
86
  }).strict();
87
+ var supersessionRequestSchema = z2.object({
88
+ candidate: z2.object({
89
+ content: z2.string().min(1),
90
+ kind: z2.literal("preference")
91
+ }).strict(),
92
+ existingMemories: z2.array(
93
+ z2.object({
94
+ content: z2.string().min(1),
95
+ id: z2.string().min(1)
96
+ }).strict()
97
+ ).min(1).max(10),
98
+ runtimeContext: memoryRuntimeContextSchema
99
+ }).strict();
87
100
  var expiresAtMsSchema = z2.number().finite().nullable().describe(
88
101
  "Expiration timestamp when the fact should expire, otherwise null."
89
102
  );
@@ -134,6 +147,15 @@ var extractMemoriesResponseSchema = z2.object({
134
147
  "Accepted public/shareable durable memories from the completed run. Return one object per distinct source assertion and classify it with kind."
135
148
  )
136
149
  }).strict();
150
+ var supersessionDecisionResponseSchema = z2.discriminatedUnion("decision", [
151
+ z2.object({
152
+ decision: z2.literal("supersedes_old"),
153
+ supersededIds: z2.array(z2.string().min(1)).min(1).max(10)
154
+ }).strict(),
155
+ z2.object({
156
+ decision: z2.enum(["distinct", "uncertain"])
157
+ }).strict()
158
+ ]);
137
159
  var MEMORY_REVIEW_SYSTEM = [
138
160
  "You are Junior's memory review agent.",
139
161
  "Review one memory candidate and return one structured review decision.",
@@ -148,6 +170,12 @@ var MEMORY_EXTRACTION_SYSTEM = [
148
170
  "Reject secrets, credentials, private or sensitive personal details, gossip, speculative claims about other people, assistant/system implementation details, vague references, and low-durability chatter.",
149
171
  "If no public, durable, self-contained memory remains after rewriting, return an empty memories array."
150
172
  ].join("\n");
173
+ var MEMORY_SUPERSESSION_SYSTEM = [
174
+ "You are Junior's memory supersession agent.",
175
+ "Decide whether a new requester preference clearly replaces existing active requester preferences.",
176
+ "Return supersedes_old only for obvious changed preferences about the same mutable slot.",
177
+ "If the facts are additive, different topics, duplicate, broader/narrower without direct replacement, or uncertain, do not supersede."
178
+ ].join("\n");
151
179
  var CANONICAL_CONTENT_RULES = [
152
180
  "- Stored memory text must be a rewritten fact, not copied user wording or a sentence about who said it.",
153
181
  "- Store the minimum useful assertion supported by source evidence; do not add adjacent steps, caveats, or generalized advice.",
@@ -302,8 +330,50 @@ function sessionExtractionPrompt(request) {
302
330
  "</memory-extraction-input>"
303
331
  ].join("\n");
304
332
  }
333
+ function supersessionPrompt(request) {
334
+ return [
335
+ "<memory-supersession-input>",
336
+ "Decide whether the candidate preference clearly replaces one or more existing active preferences.",
337
+ "",
338
+ runtimeDescription({
339
+ runtimeContext: request.runtimeContext
340
+ }),
341
+ "",
342
+ "<candidate>",
343
+ escapeXml(JSON.stringify(request.candidate)),
344
+ "</candidate>",
345
+ "",
346
+ "<existing-memories>",
347
+ escapeXml(JSON.stringify(request.existingMemories)),
348
+ "</existing-memories>",
349
+ "",
350
+ "<rules>",
351
+ "- Return supersedes_old only when the candidate and old memory describe the same mutable preference slot and the candidate is the newer value.",
352
+ "- Examples of same mutable slot: preferred programming language, preferred review style, preferred notification cadence, preferred tool for a task.",
353
+ "- Do not supersede when the candidate is just more specific, an additional preference, a different task/context, or the same value phrased differently.",
354
+ "- Do not supersede memories from different topics even if they are both preferences.",
355
+ "- Only return ids that appear in existing-memories.",
356
+ "- If unsure, return uncertain.",
357
+ "</rules>",
358
+ "</memory-supersession-input>"
359
+ ].join("\n");
360
+ }
305
361
  function createMemoryAgent(model) {
306
362
  return {
363
+ async adjudicateSupersession(rawRequest) {
364
+ const request = supersessionRequestSchema.parse(
365
+ rawRequest
366
+ );
367
+ const result = await model.completeObject({
368
+ schema: supersessionDecisionResponseSchema,
369
+ system: MEMORY_SUPERSESSION_SYSTEM,
370
+ prompt: supersessionPrompt(request),
371
+ maxTokens: 400
372
+ });
373
+ return supersessionDecisionResponseSchema.parse(
374
+ result.object
375
+ );
376
+ },
307
377
  async extractSessionMemories(rawRequest) {
308
378
  const request = extractSessionRequestSchema.parse(rawRequest);
309
379
  const result = await model.completeObject({
@@ -612,6 +682,7 @@ import {
612
682
  ilike as ilike2,
613
683
  inArray,
614
684
  isNull as isNull2,
685
+ isNotNull,
615
686
  like,
616
687
  lte,
617
688
  or as or2,
@@ -692,9 +763,25 @@ function deriveVisibleMemoryScopes(ctx) {
692
763
  var DEFAULT_LIST_LIMIT = 50;
693
764
  var DEFAULT_SEARCH_LIMIT = 10;
694
765
  var DEFAULT_EXPIRED_ARCHIVE_LIMIT = 100;
766
+ var HIGH_CONFIDENCE_DUPLICATE_DISTANCE = 0.015;
767
+ var SUPERSESSION_CANDIDATE_LIMIT = 10;
695
768
  var VECTOR_SEARCH_OVERFETCH = 4;
696
769
  var MAX_MEMORY_CONTENT_CHARS = 4e3;
697
770
  var EMBEDDING_METRIC = "cosine";
771
+ var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
772
+ var RELEVANCE_NEAR_TIE_DELTA = 0.01;
773
+ var SOURCE_CHANNEL_BOOST = 0.05;
774
+ var SUPERSESSION_STOP_TERMS = /* @__PURE__ */ new Set([
775
+ "and",
776
+ "for",
777
+ "prefer",
778
+ "prefers",
779
+ "preference",
780
+ "the",
781
+ "use",
782
+ "uses",
783
+ "with"
784
+ ]);
698
785
  var nonEmptyStringSchema2 = z3.string().min(1);
699
786
  var memoryContentSchema = z3.string().refine((content) => content.trim().length > 0, {
700
787
  message: "Memory content is required."
@@ -800,6 +887,9 @@ function normalizeContent(content) {
800
887
  function hashEmbeddedContent(content) {
801
888
  return createHash("sha256").update(content, "utf8").digest("hex");
802
889
  }
890
+ function idempotencyAliasId(args) {
891
+ return `alias:${createHash("sha256").update(args.scope.scope).update("\0").update(args.scope.scopeKey).update("\0").update(args.idempotencyKey).update("\0").update(args.targetId).digest("hex")}`;
892
+ }
803
893
  function boundedLimit(value, fallback) {
804
894
  if (typeof value !== "number" || !Number.isFinite(value)) {
805
895
  return fallback;
@@ -818,6 +908,12 @@ function sourceKey(ctx) {
818
908
  }
819
909
  return `slack:${ctx.source.teamId}:${ctx.source.channelId}:${threadKey}`;
820
910
  }
911
+ function sourceChannelPrefix(ctx) {
912
+ if (ctx.source.platform !== "slack") {
913
+ return void 0;
914
+ }
915
+ return `slack:${ctx.source.teamId}:${ctx.source.channelId}:`;
916
+ }
821
917
  function parseMemoryRow(row) {
822
918
  const parsed = memoryRowSchema.parse(row);
823
919
  return memoryRecordSchema.parse({
@@ -865,7 +961,7 @@ function activeVisiblePredicate(args) {
865
961
  );
866
962
  }
867
963
  async function findByIdempotencyKey(args) {
868
- const rows = await args.db.select().from(juniorMemoryMemories).where(
964
+ const activeRows = await args.db.select().from(juniorMemoryMemories).where(
869
965
  and2(
870
966
  eq3(juniorMemoryMemories.scope, args.scope.scope),
871
967
  eq3(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
@@ -879,7 +975,49 @@ async function findByIdempotencyKey(args) {
879
975
  )
880
976
  )
881
977
  ).limit(1);
882
- return rows[0] ? parseMemoryRow(rows[0]) : void 0;
978
+ if (activeRows[0]) {
979
+ return parseMemoryRow(activeRows[0]);
980
+ }
981
+ const aliasRows = await args.db.select({ supersededById: juniorMemoryMemories.supersededById }).from(juniorMemoryMemories).where(
982
+ and2(
983
+ eq3(juniorMemoryMemories.scope, args.scope.scope),
984
+ eq3(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
985
+ eq3(juniorMemoryMemories.idempotencyKey, args.idempotencyKey),
986
+ isNull2(juniorMemoryMemories.archivedAtMs),
987
+ isNotNull(juniorMemoryMemories.supersededAtMs),
988
+ isNotNull(juniorMemoryMemories.supersededById),
989
+ or2(
990
+ isNull2(juniorMemoryMemories.expiresAtMs),
991
+ gt2(juniorMemoryMemories.expiresAtMs, args.nowMs)
992
+ )
993
+ )
994
+ ).orderBy(
995
+ desc2(juniorMemoryMemories.createdAtMs),
996
+ asc(juniorMemoryMemories.id)
997
+ );
998
+ for (const alias of aliasRows) {
999
+ if (!alias.supersededById) {
1000
+ continue;
1001
+ }
1002
+ const rows = await args.db.select().from(juniorMemoryMemories).where(
1003
+ and2(
1004
+ eq3(juniorMemoryMemories.id, alias.supersededById),
1005
+ eq3(juniorMemoryMemories.scope, args.scope.scope),
1006
+ eq3(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
1007
+ isNull2(juniorMemoryMemories.archivedAtMs),
1008
+ isNull2(juniorMemoryMemories.supersededAtMs),
1009
+ isNull2(juniorMemoryMemories.supersededById),
1010
+ or2(
1011
+ isNull2(juniorMemoryMemories.expiresAtMs),
1012
+ gt2(juniorMemoryMemories.expiresAtMs, args.nowMs)
1013
+ )
1014
+ )
1015
+ ).limit(1);
1016
+ if (rows[0]) {
1017
+ return parseMemoryRow(rows[0]);
1018
+ }
1019
+ }
1020
+ return void 0;
883
1021
  }
884
1022
  async function archiveExpiredMemoryBatch(args) {
885
1023
  const scopePredicate = visibleScopePredicate(args.scopes);
@@ -951,7 +1089,7 @@ async function embedOne(embedder, text2) {
951
1089
  };
952
1090
  }
953
1091
  async function storeEmbedding(args) {
954
- if (!args.embedder) {
1092
+ if (!args.embedder && !args.embedding) {
955
1093
  return;
956
1094
  }
957
1095
  try {
@@ -963,10 +1101,18 @@ async function storeEmbedding(args) {
963
1101
  return;
964
1102
  }
965
1103
  let embedding;
966
- try {
967
- embedding = await embedOne(args.embedder, args.content);
968
- } catch {
969
- return;
1104
+ if (args.embedding) {
1105
+ embedding = args.embedding;
1106
+ } else {
1107
+ const embedder = args.embedder;
1108
+ if (!embedder) {
1109
+ return;
1110
+ }
1111
+ try {
1112
+ embedding = await embedOne(embedder, args.content);
1113
+ } catch {
1114
+ return;
1115
+ }
970
1116
  }
971
1117
  try {
972
1118
  await args.db.insert(juniorMemoryEmbeddings).values({
@@ -983,6 +1129,192 @@ async function storeEmbedding(args) {
983
1129
  return;
984
1130
  }
985
1131
  }
1132
+ function activeScopedSubjectPredicate(args) {
1133
+ const predicate = and2(
1134
+ eq3(juniorMemoryMemories.scope, args.scope.scope),
1135
+ eq3(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
1136
+ eq3(juniorMemoryMemories.kind, args.kind),
1137
+ eq3(juniorMemoryMemories.subjectType, args.subject.subjectType),
1138
+ args.subject.subjectKey === void 0 ? isNull2(juniorMemoryMemories.subjectKey) : eq3(juniorMemoryMemories.subjectKey, args.subject.subjectKey),
1139
+ isNull2(juniorMemoryMemories.archivedAtMs),
1140
+ isNull2(juniorMemoryMemories.supersededAtMs),
1141
+ isNull2(juniorMemoryMemories.supersededById),
1142
+ or2(
1143
+ isNull2(juniorMemoryMemories.expiresAtMs),
1144
+ gt2(juniorMemoryMemories.expiresAtMs, args.nowMs)
1145
+ )
1146
+ );
1147
+ if (!predicate) {
1148
+ throw new Error("Memory duplicate predicate is empty.");
1149
+ }
1150
+ return predicate;
1151
+ }
1152
+ async function findExactDuplicateMemory(args) {
1153
+ const rows = await args.db.select().from(juniorMemoryMemories).where(
1154
+ and2(
1155
+ activeScopedSubjectPredicate(args),
1156
+ eq3(juniorMemoryMemories.content, args.content)
1157
+ )
1158
+ ).orderBy(
1159
+ desc2(juniorMemoryMemories.createdAtMs),
1160
+ asc(juniorMemoryMemories.id)
1161
+ ).limit(1);
1162
+ return rows[0] ? parseMemoryRow(rows[0]) : void 0;
1163
+ }
1164
+ async function findVectorDuplicateMemory(args) {
1165
+ const distance = cosineDistance(
1166
+ juniorMemoryEmbeddings.embedding,
1167
+ args.embedding.vector
1168
+ );
1169
+ const rows = await args.db.select({
1170
+ contentHash: juniorMemoryEmbeddings.contentHash,
1171
+ distance,
1172
+ memory: juniorMemoryMemories
1173
+ }).from(juniorMemoryMemories).innerJoin(
1174
+ juniorMemoryEmbeddings,
1175
+ eq3(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id)
1176
+ ).where(
1177
+ and2(
1178
+ activeScopedSubjectPredicate(args),
1179
+ eq3(juniorMemoryEmbeddings.provider, args.embedding.provider),
1180
+ eq3(juniorMemoryEmbeddings.model, args.embedding.model),
1181
+ eq3(juniorMemoryEmbeddings.dimensions, MEMORY_EMBEDDING_DIMENSIONS),
1182
+ eq3(juniorMemoryEmbeddings.metric, EMBEDDING_METRIC),
1183
+ lte(distance, HIGH_CONFIDENCE_DUPLICATE_DISTANCE)
1184
+ )
1185
+ ).orderBy(
1186
+ distance,
1187
+ desc2(juniorMemoryMemories.createdAtMs),
1188
+ asc(juniorMemoryMemories.id)
1189
+ ).limit(1);
1190
+ const row = rows[0];
1191
+ if (!row || hashEmbeddedContent(row.memory.content) !== row.contentHash) {
1192
+ return void 0;
1193
+ }
1194
+ return parseMemoryRow(row.memory);
1195
+ }
1196
+ async function rememberDuplicateIdempotency(args) {
1197
+ if (args.idempotencyKey === void 0) {
1198
+ return;
1199
+ }
1200
+ await args.db.insert(juniorMemoryMemories).values({
1201
+ content: args.content,
1202
+ createdAtMs: args.nowMs,
1203
+ expiresAtMs: args.duplicate.expiresAtMs,
1204
+ id: idempotencyAliasId({
1205
+ idempotencyKey: args.idempotencyKey,
1206
+ scope: args.scope,
1207
+ targetId: args.duplicate.id
1208
+ }),
1209
+ idempotencyKey: args.idempotencyKey,
1210
+ observedAtMs: args.nowMs,
1211
+ scope: args.scope.scope,
1212
+ scopeKey: args.scope.scopeKey,
1213
+ sourceKey: sourceKey(args.runtimeContext),
1214
+ sourcePlatform: args.runtimeContext.source.platform,
1215
+ subjectKey: args.subject.subjectKey,
1216
+ subjectType: args.subject.subjectType,
1217
+ supersededAtMs: args.nowMs,
1218
+ supersededById: args.duplicate.id,
1219
+ kind: args.duplicate.kind
1220
+ }).onConflictDoNothing();
1221
+ }
1222
+ async function listSupersessionCandidates(args) {
1223
+ const predicate = activeScopedSubjectPredicate(args);
1224
+ const terms = searchTerms(args.content).filter(
1225
+ (term) => !SUPERSESSION_STOP_TERMS.has(term)
1226
+ );
1227
+ const lexicalRows = terms.length > 0 ? await args.db.select().from(juniorMemoryMemories).where(
1228
+ and2(
1229
+ predicate,
1230
+ or2(
1231
+ ...terms.map(
1232
+ (term) => ilike2(juniorMemoryMemories.content, `%${term}%`)
1233
+ )
1234
+ )
1235
+ )
1236
+ ) : [];
1237
+ const lexicalCandidates = lexicalRows.map(parseMemoryRow).map((memory) => ({
1238
+ memory,
1239
+ score: searchScore(memory, terms)
1240
+ })).filter((candidate) => candidate.score > 1).sort(
1241
+ (left, right) => right.score - left.score || right.memory.createdAtMs - left.memory.createdAtMs || left.memory.id.localeCompare(right.memory.id)
1242
+ ).map((candidate) => candidate.memory);
1243
+ const vectorCandidates = args.embedding ? await listVectorSupersessionCandidates({
1244
+ db: args.db,
1245
+ embedding: args.embedding,
1246
+ kind: args.kind,
1247
+ nowMs: args.nowMs,
1248
+ scope: args.scope,
1249
+ subject: args.subject
1250
+ }) : [];
1251
+ const byId = /* @__PURE__ */ new Map();
1252
+ for (const memory of [...lexicalCandidates, ...vectorCandidates]) {
1253
+ if (byId.size >= SUPERSESSION_CANDIDATE_LIMIT) {
1254
+ break;
1255
+ }
1256
+ byId.set(memory.id, memory);
1257
+ }
1258
+ return [...byId.values()];
1259
+ }
1260
+ async function listVectorSupersessionCandidates(args) {
1261
+ const distance = cosineDistance(
1262
+ juniorMemoryEmbeddings.embedding,
1263
+ args.embedding.vector
1264
+ );
1265
+ const rows = await args.db.select({
1266
+ contentHash: juniorMemoryEmbeddings.contentHash,
1267
+ distance,
1268
+ memory: juniorMemoryMemories
1269
+ }).from(juniorMemoryMemories).innerJoin(
1270
+ juniorMemoryEmbeddings,
1271
+ eq3(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id)
1272
+ ).where(
1273
+ and2(
1274
+ activeScopedSubjectPredicate(args),
1275
+ eq3(juniorMemoryEmbeddings.provider, args.embedding.provider),
1276
+ eq3(juniorMemoryEmbeddings.model, args.embedding.model),
1277
+ eq3(juniorMemoryEmbeddings.dimensions, MEMORY_EMBEDDING_DIMENSIONS),
1278
+ eq3(juniorMemoryEmbeddings.metric, EMBEDDING_METRIC)
1279
+ )
1280
+ ).orderBy(
1281
+ distance,
1282
+ desc2(juniorMemoryMemories.createdAtMs),
1283
+ asc(juniorMemoryMemories.id)
1284
+ ).limit(SUPERSESSION_CANDIDATE_LIMIT);
1285
+ return rows.flatMap((row) => {
1286
+ if (hashEmbeddedContent(row.memory.content) !== row.contentHash) {
1287
+ return [];
1288
+ }
1289
+ return [parseMemoryRow(row.memory)];
1290
+ });
1291
+ }
1292
+ async function decideSupersededIds(args) {
1293
+ if (args.kind !== "preference" || !args.decider || args.candidates.length === 0) {
1294
+ return [];
1295
+ }
1296
+ const existingMemories = args.candidates.map((memory) => ({
1297
+ content: memory.content,
1298
+ id: memory.id
1299
+ }));
1300
+ const candidateIds = new Set(args.candidates.map((memory) => memory.id));
1301
+ try {
1302
+ const decision = await args.decider.adjudicateSupersession({
1303
+ candidate: {
1304
+ content: args.content,
1305
+ kind: "preference"
1306
+ },
1307
+ existingMemories,
1308
+ runtimeContext: args.runtimeContext
1309
+ });
1310
+ if (decision.decision !== "supersedes_old") {
1311
+ return [];
1312
+ }
1313
+ return decision.supersededIds.filter((id) => candidateIds.has(id));
1314
+ } catch {
1315
+ return [];
1316
+ }
1317
+ }
986
1318
  async function listVisibleMemories(args) {
987
1319
  const predicate = activeVisiblePredicate(args);
988
1320
  if (!predicate) {
@@ -1014,7 +1346,11 @@ async function searchVisibleMemories(args) {
1014
1346
  )
1015
1347
  )
1016
1348
  );
1017
- return rows.map(parseMemoryRow);
1349
+ return rows.map((row) => ({
1350
+ memory: parseMemoryRow(row),
1351
+ score: 0,
1352
+ sourceKey: row.sourceKey
1353
+ }));
1018
1354
  }
1019
1355
  async function searchVisibleVectorMemories(args) {
1020
1356
  if (!args.embedder) {
@@ -1059,15 +1395,38 @@ async function searchVisibleVectorMemories(args) {
1059
1395
  if (row.distance === null || !Number.isFinite(distanceValue) || hashEmbeddedContent(row.memory.content) !== row.contentHash) {
1060
1396
  return [];
1061
1397
  }
1398
+ if (args.maxDistance !== void 0 && distanceValue > args.maxDistance) {
1399
+ return [];
1400
+ }
1062
1401
  return [
1063
1402
  {
1064
1403
  memory: parseMemoryRow(row.memory),
1065
- score: 1 / (1 + Math.max(0, distanceValue))
1404
+ score: 1 / (1 + Math.max(0, distanceValue)),
1405
+ sourceKey: row.memory.sourceKey
1066
1406
  }
1067
1407
  ];
1068
1408
  });
1069
1409
  }
1070
- function mergeSearchCandidates(candidates) {
1410
+ function sourceBoost(candidate, currentChannelPrefix) {
1411
+ if (!currentChannelPrefix) {
1412
+ return 0;
1413
+ }
1414
+ return candidate.sourceKey.startsWith(currentChannelPrefix) ? SOURCE_CHANNEL_BOOST : 0;
1415
+ }
1416
+ function observedAgeBoost(memory, nowMs) {
1417
+ const ageMs = Math.max(0, nowMs - memory.observedAtMs);
1418
+ if (ageMs <= 7 * ONE_DAY_MS) {
1419
+ return 0.15;
1420
+ }
1421
+ if (ageMs <= 30 * ONE_DAY_MS) {
1422
+ return 0.1;
1423
+ }
1424
+ if (ageMs <= 90 * ONE_DAY_MS) {
1425
+ return 0.05;
1426
+ }
1427
+ return 0;
1428
+ }
1429
+ function mergeSearchCandidates(candidates, nowMs, currentChannelKey) {
1071
1430
  const byId = /* @__PURE__ */ new Map();
1072
1431
  for (const candidate of candidates) {
1073
1432
  const existing = byId.get(candidate.memory.id);
@@ -1077,17 +1436,28 @@ function mergeSearchCandidates(candidates) {
1077
1436
  }
1078
1437
  byId.set(candidate.memory.id, {
1079
1438
  memory: candidate.memory,
1080
- score: candidate.score
1439
+ score: candidate.score,
1440
+ sourceKey: candidate.sourceKey
1081
1441
  });
1082
1442
  }
1083
- return [...byId.values()].sort(
1084
- (left, right) => right.score - left.score || right.memory.createdAtMs - left.memory.createdAtMs || left.memory.id.localeCompare(right.memory.id)
1085
- ).map((candidate) => candidate.memory);
1443
+ return [...byId.values()].sort((left, right) => {
1444
+ const scoreDelta = right.score - left.score;
1445
+ if (Math.abs(scoreDelta) > RELEVANCE_NEAR_TIE_DELTA) {
1446
+ return scoreDelta;
1447
+ }
1448
+ const sourceDelta = sourceBoost(right, currentChannelKey) - sourceBoost(left, currentChannelKey);
1449
+ if (sourceDelta !== 0) {
1450
+ return sourceDelta;
1451
+ }
1452
+ return observedAgeBoost(right.memory, nowMs) - observedAgeBoost(left.memory, nowMs) || right.memory.observedAtMs - left.memory.observedAtMs || left.memory.id.localeCompare(right.memory.id);
1453
+ }).map((candidate) => candidate.memory);
1086
1454
  }
1087
1455
  function createMemoryStore(db, context, options = {}) {
1088
1456
  const runtimeContext = memoryRuntimeContextSchema.parse(context);
1089
1457
  const parsedOptions = memoryStoreOptionsSchema.parse({ now: options.now });
1090
1458
  const embedder = options.embedder;
1459
+ const maxVectorDistance = options.maxVectorDistance;
1460
+ const supersessionDecider = options.supersessionDecider;
1091
1461
  const getNowMs = parsedOptions.now ?? Date.now;
1092
1462
  async function archiveExpiredVisibleMemories(input, nowMs) {
1093
1463
  input = archiveExpiredMemoriesInputSchema.parse(input ?? {});
@@ -1119,35 +1489,162 @@ function createMemoryStore(db, context, options = {}) {
1119
1489
  nowMs,
1120
1490
  scopes: [scope]
1121
1491
  });
1122
- const id = randomUUID();
1123
- const rows = await db.insert(juniorMemoryMemories).values({
1492
+ if (input.idempotencyKey !== void 0) {
1493
+ const idempotent2 = await findByIdempotencyKey({
1494
+ db,
1495
+ idempotencyKey: input.idempotencyKey,
1496
+ nowMs,
1497
+ scope
1498
+ });
1499
+ if (idempotent2) {
1500
+ await storeEmbedding({
1501
+ content: idempotent2.content,
1502
+ db,
1503
+ embedder,
1504
+ memoryId: idempotent2.id,
1505
+ nowMs
1506
+ });
1507
+ return { created: false, memory: idempotent2 };
1508
+ }
1509
+ }
1510
+ const exactDuplicate = await findExactDuplicateMemory({
1124
1511
  content,
1125
- createdAtMs: nowMs,
1126
- expiresAtMs: input.expiresAtMs,
1127
- id,
1128
- idempotencyKey: input.idempotencyKey,
1129
- observedAtMs: nowMs,
1130
- scope: scope.scope,
1131
- scopeKey: scope.scopeKey,
1132
- sourceKey: sourceKey(runtimeContext),
1133
- sourcePlatform: runtimeContext.source.platform,
1134
- subjectKey: subject.subjectKey,
1135
- subjectType: subject.subjectType,
1136
- kind: input.kind
1137
- }).onConflictDoNothing({
1138
- target: [
1139
- juniorMemoryMemories.scope,
1140
- juniorMemoryMemories.scopeKey,
1141
- juniorMemoryMemories.idempotencyKey
1142
- ],
1143
- where: sql2`${juniorMemoryMemories.idempotencyKey} IS NOT NULL AND ${juniorMemoryMemories.archivedAtMs} IS NULL AND ${juniorMemoryMemories.supersededAtMs} IS NULL AND ${juniorMemoryMemories.supersededById} IS NULL`
1144
- }).returning();
1512
+ db,
1513
+ kind: input.kind,
1514
+ nowMs,
1515
+ scope,
1516
+ subject
1517
+ });
1518
+ if (exactDuplicate) {
1519
+ await rememberDuplicateIdempotency({
1520
+ content,
1521
+ db,
1522
+ duplicate: exactDuplicate,
1523
+ idempotencyKey: input.idempotencyKey,
1524
+ nowMs,
1525
+ runtimeContext,
1526
+ scope,
1527
+ subject
1528
+ });
1529
+ await storeEmbedding({
1530
+ content: exactDuplicate.content,
1531
+ db,
1532
+ embedder,
1533
+ memoryId: exactDuplicate.id,
1534
+ nowMs
1535
+ });
1536
+ return { created: false, memory: exactDuplicate };
1537
+ }
1538
+ let candidateEmbedding;
1539
+ if (embedder) {
1540
+ try {
1541
+ candidateEmbedding = await embedOne(embedder, content);
1542
+ } catch {
1543
+ candidateEmbedding = void 0;
1544
+ }
1545
+ }
1546
+ const vectorDuplicate = candidateEmbedding ? await findVectorDuplicateMemory({
1547
+ db,
1548
+ embedding: candidateEmbedding,
1549
+ kind: input.kind,
1550
+ nowMs,
1551
+ scope,
1552
+ subject
1553
+ }) : void 0;
1554
+ if (vectorDuplicate) {
1555
+ await rememberDuplicateIdempotency({
1556
+ content,
1557
+ db,
1558
+ duplicate: vectorDuplicate,
1559
+ idempotencyKey: input.idempotencyKey,
1560
+ nowMs,
1561
+ runtimeContext,
1562
+ scope,
1563
+ subject
1564
+ });
1565
+ await storeEmbedding({
1566
+ content: vectorDuplicate.content,
1567
+ db,
1568
+ embedder,
1569
+ memoryId: vectorDuplicate.id,
1570
+ nowMs
1571
+ });
1572
+ return { created: false, memory: vectorDuplicate };
1573
+ }
1574
+ let supersededIds = [];
1575
+ if (scopeKind === "personal" && input.kind === "preference" && supersessionDecider && (input.expiresAtMs === void 0 || input.expiresAtMs > nowMs)) {
1576
+ const supersessionCandidates = await listSupersessionCandidates({
1577
+ content,
1578
+ db,
1579
+ ...candidateEmbedding ? { embedding: candidateEmbedding } : {},
1580
+ kind: input.kind,
1581
+ nowMs,
1582
+ scope,
1583
+ subject
1584
+ });
1585
+ supersededIds = await decideSupersededIds({
1586
+ candidates: supersessionCandidates,
1587
+ content,
1588
+ decider: supersessionDecider,
1589
+ kind: input.kind,
1590
+ runtimeContext
1591
+ });
1592
+ }
1593
+ const id = randomUUID();
1594
+ const rows = await db.transaction(async (tx) => {
1595
+ const inserted = await tx.insert(juniorMemoryMemories).values({
1596
+ content,
1597
+ createdAtMs: nowMs,
1598
+ expiresAtMs: input.expiresAtMs,
1599
+ id,
1600
+ idempotencyKey: input.idempotencyKey,
1601
+ observedAtMs: nowMs,
1602
+ scope: scope.scope,
1603
+ scopeKey: scope.scopeKey,
1604
+ sourceKey: sourceKey(runtimeContext),
1605
+ sourcePlatform: runtimeContext.source.platform,
1606
+ subjectKey: subject.subjectKey,
1607
+ subjectType: subject.subjectType,
1608
+ kind: input.kind
1609
+ }).onConflictDoNothing({
1610
+ target: [
1611
+ juniorMemoryMemories.scope,
1612
+ juniorMemoryMemories.scopeKey,
1613
+ juniorMemoryMemories.idempotencyKey
1614
+ ],
1615
+ where: sql2`${juniorMemoryMemories.idempotencyKey} IS NOT NULL AND ${juniorMemoryMemories.archivedAtMs} IS NULL AND ${juniorMemoryMemories.supersededAtMs} IS NULL AND ${juniorMemoryMemories.supersededById} IS NULL`
1616
+ }).returning();
1617
+ const insertedMemory = inserted[0];
1618
+ if (!insertedMemory || supersededIds.length === 0) {
1619
+ return inserted;
1620
+ }
1621
+ const superseded = await tx.update(juniorMemoryMemories).set({
1622
+ supersededAtMs: nowMs,
1623
+ supersededById: insertedMemory.id
1624
+ }).where(
1625
+ and2(
1626
+ inArray(juniorMemoryMemories.id, supersededIds),
1627
+ activeScopedSubjectPredicate({
1628
+ kind: input.kind,
1629
+ nowMs,
1630
+ scope,
1631
+ subject
1632
+ })
1633
+ )
1634
+ ).returning({ id: juniorMemoryMemories.id });
1635
+ const idsToClean = superseded.map((row) => row.id);
1636
+ if (idsToClean.length > 0) {
1637
+ await tx.delete(juniorMemoryEmbeddings).where(inArray(juniorMemoryEmbeddings.memoryId, idsToClean));
1638
+ }
1639
+ return inserted;
1640
+ });
1145
1641
  if (rows[0]) {
1146
1642
  const memory = parseMemoryRow(rows[0]);
1147
1643
  await storeEmbedding({
1148
1644
  content: memory.content,
1149
1645
  db,
1150
1646
  embedder,
1647
+ embedding: candidateEmbedding,
1151
1648
  memoryId: memory.id,
1152
1649
  nowMs
1153
1650
  });
@@ -1211,6 +1708,7 @@ function createMemoryStore(db, context, options = {}) {
1211
1708
  db,
1212
1709
  embedder,
1213
1710
  limit: limit * VECTOR_SEARCH_OVERFETCH,
1711
+ ...maxVectorDistance !== void 0 ? { maxDistance: maxVectorDistance } : {},
1214
1712
  nowMs,
1215
1713
  query: input.query,
1216
1714
  scopes
@@ -1222,11 +1720,15 @@ function createMemoryStore(db, context, options = {}) {
1222
1720
  scopes
1223
1721
  });
1224
1722
  const terms = searchTerms(input.query);
1225
- const lexicalCandidates = candidates.map((memory) => ({ memory, score: searchScore(memory, terms) })).filter((item) => item.score > 0);
1226
- return mergeSearchCandidates([
1227
- ...vectorCandidates,
1228
- ...lexicalCandidates
1229
- ]).slice(0, limit);
1723
+ const lexicalCandidates = candidates.map((candidate) => ({
1724
+ ...candidate,
1725
+ score: searchScore(candidate.memory, terms)
1726
+ })).filter((item) => item.score > 0);
1727
+ return mergeSearchCandidates(
1728
+ [...vectorCandidates, ...lexicalCandidates],
1729
+ nowMs,
1730
+ sourceChannelPrefix(runtimeContext)
1731
+ ).slice(0, limit);
1230
1732
  },
1231
1733
  async archiveMemory(input) {
1232
1734
  input = archiveMemoryInputSchema.parse(input);
@@ -1297,9 +1799,10 @@ function memoryRuntimeContext(context) {
1297
1799
  source: context.source
1298
1800
  });
1299
1801
  }
1300
- function memoryStore(context) {
1802
+ function memoryStore(context, options = {}) {
1301
1803
  return createMemoryStore(context.db, memoryRuntimeContext(context), {
1302
- embedder: context.embedder
1804
+ embedder: context.embedder,
1805
+ ...options.supersessionDecider ? { supersessionDecider: options.supersessionDecider } : {}
1303
1806
  });
1304
1807
  }
1305
1808
  function boundedLimit2(value, fallback) {
@@ -1442,6 +1945,16 @@ var searchMemoriesInputSchema2 = Type.Object(
1442
1945
  },
1443
1946
  { additionalProperties: false }
1444
1947
  );
1948
+ var memoryToolProjectionSchema = Type.Object(
1949
+ {
1950
+ id: Type.String({ minLength: 1 }),
1951
+ content: Type.String({ minLength: 1 }),
1952
+ createdAtMs: Type.Number(),
1953
+ observedAtMs: Type.Number(),
1954
+ expiresAtMs: Type.Optional(Type.Number())
1955
+ },
1956
+ { additionalProperties: false }
1957
+ );
1445
1958
  function parseToolInput(schema, input) {
1446
1959
  try {
1447
1960
  if (!Value.Check(schema, input)) {
@@ -1476,12 +1989,13 @@ function targetForKind(kind) {
1476
1989
  return "conversation";
1477
1990
  }
1478
1991
  function compactMemory(memory) {
1479
- return {
1992
+ return Value.Parse(memoryToolProjectionSchema, {
1480
1993
  id: memory.id,
1481
1994
  content: memory.content,
1482
1995
  createdAtMs: memory.createdAtMs,
1996
+ observedAtMs: memory.observedAtMs,
1483
1997
  ...memory.expiresAtMs !== void 0 ? { expiresAtMs: memory.expiresAtMs } : {}
1484
- };
1998
+ });
1485
1999
  }
1486
2000
  function createMemoryCreateTool(context) {
1487
2001
  return {
@@ -1496,7 +2010,9 @@ function createMemoryCreateTool(context) {
1496
2010
  const toolCallId = requireToolCallId(options.toolCallId);
1497
2011
  const requestedExpiresAtMs = parseExpiresAt(parsedInput.expires_at);
1498
2012
  const runtimeContext = memoryRuntimeContext(context);
1499
- const store = memoryStore(context);
2013
+ const store = memoryStore(context, {
2014
+ supersessionDecider: context.supersessionDecider
2015
+ });
1500
2016
  const review = await (async () => {
1501
2017
  try {
1502
2018
  return parseMemoryReview(
@@ -1699,8 +2215,10 @@ async function processMemorySession(context) {
1699
2215
  ...run.requester ? { requester: run.requester } : {},
1700
2216
  source: run.source
1701
2217
  });
2218
+ const agent = createMemoryAgent(context.model);
1702
2219
  const store = createMemoryStore(context.db, runtimeContext, {
1703
- embedder: context.embedder
2220
+ embedder: context.embedder,
2221
+ supersessionDecider: agent
1704
2222
  });
1705
2223
  await store.archiveExpiredMemories();
1706
2224
  const memories = await getTaskMemories(context, async () => {
@@ -1708,7 +2226,6 @@ async function processMemorySession(context) {
1708
2226
  limit: 10,
1709
2227
  query: evidenceText
1710
2228
  });
1711
- const agent = createMemoryAgent(context.model);
1712
2229
  return await agent.extractSessionMemories({
1713
2230
  existingMemories: existingMemories.map((memory) => ({
1714
2231
  content: memory.content
@@ -1735,8 +2252,8 @@ async function processMemorySession(context) {
1735
2252
 
1736
2253
  // src/recall.ts
1737
2254
  var DEFAULT_RECALL_LIMIT = 5;
1738
- var MAX_PROMPT_CHARS = 1600;
1739
- var MAX_MEMORY_LINE_CHARS = 320;
2255
+ var MAX_PROMPT_CHARS = 4e3;
2256
+ var MAX_MEMORY_LINE_CHARS = 600;
1740
2257
  function trimContent(content, maxLength) {
1741
2258
  const trimmed = content.trim();
1742
2259
  if (trimmed.length <= maxLength) {
@@ -1744,13 +2261,19 @@ function trimContent(content, maxLength) {
1744
2261
  }
1745
2262
  return `${trimmed.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
1746
2263
  }
2264
+ function formatObservedDate(observedAtMs) {
2265
+ return new Date(observedAtMs).toISOString().slice(0, 10);
2266
+ }
1747
2267
  function renderMemoryPrompt(memories) {
1748
2268
  const header = "Relevant memories for this request:";
1749
2269
  const footer = "Treat these as possibly stale context. Current user instructions and repository evidence take priority.";
1750
2270
  const lines = [];
1751
2271
  let totalChars = header.length + footer.length + 2;
1752
2272
  for (const memory of memories) {
1753
- const line = `- ${trimContent(memory.content, MAX_MEMORY_LINE_CHARS)}`;
2273
+ const line = `- Observed ${formatObservedDate(memory.observedAtMs)}: ${trimContent(
2274
+ memory.content,
2275
+ MAX_MEMORY_LINE_CHARS
2276
+ )}`;
1754
2277
  if (totalChars + line.length + 1 > MAX_PROMPT_CHARS) {
1755
2278
  break;
1756
2279
  }
@@ -1777,7 +2300,10 @@ async function createMemoryPromptMessages(context) {
1777
2300
  const memories = await createMemoryStore(
1778
2301
  context.db,
1779
2302
  runtimeContext,
1780
- context.embedder ? { embedder: context.embedder } : {}
2303
+ {
2304
+ ...context.embedder ? { embedder: context.embedder } : {},
2305
+ ...context.maxVectorDistance !== void 0 ? { maxVectorDistance: context.maxVectorDistance } : {}
2306
+ }
1781
2307
  ).searchMemories({
1782
2308
  query: context.text,
1783
2309
  limit: DEFAULT_RECALL_LIMIT
@@ -1788,6 +2314,8 @@ async function createMemoryPromptMessages(context) {
1788
2314
 
1789
2315
  // src/plugin.ts
1790
2316
  var MEMORY_MODEL_ENV = "AI_MEMORY_MODEL";
2317
+ var MEMORY_RECALL_MAX_VECTOR_DISTANCE_ENV = "MEMORY_RECALL_MAX_VECTOR_DISTANCE";
2318
+ var DEFAULT_RECALL_MAX_VECTOR_DISTANCE = 0.45;
1791
2319
  function memoryModelId(options) {
1792
2320
  const explicitModelId = options.modelId?.trim();
1793
2321
  if (explicitModelId) {
@@ -1796,6 +2324,19 @@ function memoryModelId(options) {
1796
2324
  const envModelId = process.env[MEMORY_MODEL_ENV]?.trim();
1797
2325
  return envModelId || void 0;
1798
2326
  }
2327
+ function recallMaxVectorDistance(options) {
2328
+ if (options.recallMaxVectorDistance !== void 0) {
2329
+ return options.recallMaxVectorDistance;
2330
+ }
2331
+ const raw = process.env[MEMORY_RECALL_MAX_VECTOR_DISTANCE_ENV]?.trim();
2332
+ if (raw) {
2333
+ const parsed = Number(raw);
2334
+ if (Number.isFinite(parsed) && parsed > 0) {
2335
+ return Math.min(parsed, 1);
2336
+ }
2337
+ }
2338
+ return DEFAULT_RECALL_MAX_VECTOR_DISTANCE;
2339
+ }
1799
2340
  function memoryToolContext(ctx) {
1800
2341
  return {
1801
2342
  agent: ctx.agent,
@@ -1807,6 +2348,12 @@ function memoryToolContext(ctx) {
1807
2348
  ...ctx.userText ? { userText: ctx.userText } : {}
1808
2349
  };
1809
2350
  }
2351
+ function memoryCreateToolContext(ctx) {
2352
+ return {
2353
+ ...memoryToolContext(ctx),
2354
+ supersessionDecider: ctx.supersessionDecider
2355
+ };
2356
+ }
1810
2357
  function createMemoryPlugin(options = {}) {
1811
2358
  const modelId = memoryModelId(options);
1812
2359
  return defineJuniorPlugin({
@@ -1829,14 +2376,23 @@ function createMemoryPlugin(options = {}) {
1829
2376
  },
1830
2377
  hooks: {
1831
2378
  tools(ctx) {
2379
+ const agent = createMemoryAgent(ctx.model);
1832
2380
  const context = memoryToolContext({
1833
2381
  ...ctx,
1834
- agent: createMemoryAgent(ctx.model),
2382
+ agent,
1835
2383
  db: ctx.db,
1836
2384
  embedder: ctx.embedder
1837
2385
  });
1838
2386
  return {
1839
- createMemory: createMemoryCreateTool(context),
2387
+ createMemory: createMemoryCreateTool(
2388
+ memoryCreateToolContext({
2389
+ ...ctx,
2390
+ agent,
2391
+ db: ctx.db,
2392
+ embedder: ctx.embedder,
2393
+ supersessionDecider: agent
2394
+ })
2395
+ ),
1840
2396
  removeMemory: createMemoryRemoveTool(context),
1841
2397
  listMemories: createMemoryListTool(context),
1842
2398
  searchMemories: createMemorySearchTool(context)
@@ -1848,6 +2404,7 @@ function createMemoryPlugin(options = {}) {
1848
2404
  ...ctx.requester ? { requester: ctx.requester } : {},
1849
2405
  db: ctx.db,
1850
2406
  embedder: ctx.embedder,
2407
+ maxVectorDistance: recallMaxVectorDistance(options),
1851
2408
  source: ctx.source,
1852
2409
  text: ctx.text
1853
2410
  });