@sentry/junior-memory 0.80.1 → 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/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,24 @@ 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 SUPERSESSION_STOP_TERMS = /* @__PURE__ */ new Set([
774
+ "and",
775
+ "for",
776
+ "prefer",
777
+ "prefers",
778
+ "preference",
779
+ "the",
780
+ "use",
781
+ "uses",
782
+ "with"
783
+ ]);
698
784
  var nonEmptyStringSchema2 = z3.string().min(1);
699
785
  var memoryContentSchema = z3.string().refine((content) => content.trim().length > 0, {
700
786
  message: "Memory content is required."
@@ -800,6 +886,9 @@ function normalizeContent(content) {
800
886
  function hashEmbeddedContent(content) {
801
887
  return createHash("sha256").update(content, "utf8").digest("hex");
802
888
  }
889
+ function idempotencyAliasId(args) {
890
+ 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")}`;
891
+ }
803
892
  function boundedLimit(value, fallback) {
804
893
  if (typeof value !== "number" || !Number.isFinite(value)) {
805
894
  return fallback;
@@ -865,7 +954,7 @@ function activeVisiblePredicate(args) {
865
954
  );
866
955
  }
867
956
  async function findByIdempotencyKey(args) {
868
- const rows = await args.db.select().from(juniorMemoryMemories).where(
957
+ const activeRows = await args.db.select().from(juniorMemoryMemories).where(
869
958
  and2(
870
959
  eq3(juniorMemoryMemories.scope, args.scope.scope),
871
960
  eq3(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
@@ -879,7 +968,49 @@ async function findByIdempotencyKey(args) {
879
968
  )
880
969
  )
881
970
  ).limit(1);
882
- return rows[0] ? parseMemoryRow(rows[0]) : void 0;
971
+ if (activeRows[0]) {
972
+ return parseMemoryRow(activeRows[0]);
973
+ }
974
+ const aliasRows = await args.db.select({ supersededById: juniorMemoryMemories.supersededById }).from(juniorMemoryMemories).where(
975
+ and2(
976
+ eq3(juniorMemoryMemories.scope, args.scope.scope),
977
+ eq3(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
978
+ eq3(juniorMemoryMemories.idempotencyKey, args.idempotencyKey),
979
+ isNull2(juniorMemoryMemories.archivedAtMs),
980
+ isNotNull(juniorMemoryMemories.supersededAtMs),
981
+ isNotNull(juniorMemoryMemories.supersededById),
982
+ or2(
983
+ isNull2(juniorMemoryMemories.expiresAtMs),
984
+ gt2(juniorMemoryMemories.expiresAtMs, args.nowMs)
985
+ )
986
+ )
987
+ ).orderBy(
988
+ desc2(juniorMemoryMemories.createdAtMs),
989
+ asc(juniorMemoryMemories.id)
990
+ );
991
+ for (const alias of aliasRows) {
992
+ if (!alias.supersededById) {
993
+ continue;
994
+ }
995
+ const rows = await args.db.select().from(juniorMemoryMemories).where(
996
+ and2(
997
+ eq3(juniorMemoryMemories.id, alias.supersededById),
998
+ eq3(juniorMemoryMemories.scope, args.scope.scope),
999
+ eq3(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
1000
+ isNull2(juniorMemoryMemories.archivedAtMs),
1001
+ isNull2(juniorMemoryMemories.supersededAtMs),
1002
+ isNull2(juniorMemoryMemories.supersededById),
1003
+ or2(
1004
+ isNull2(juniorMemoryMemories.expiresAtMs),
1005
+ gt2(juniorMemoryMemories.expiresAtMs, args.nowMs)
1006
+ )
1007
+ )
1008
+ ).limit(1);
1009
+ if (rows[0]) {
1010
+ return parseMemoryRow(rows[0]);
1011
+ }
1012
+ }
1013
+ return void 0;
883
1014
  }
884
1015
  async function archiveExpiredMemoryBatch(args) {
885
1016
  const scopePredicate = visibleScopePredicate(args.scopes);
@@ -951,7 +1082,7 @@ async function embedOne(embedder, text2) {
951
1082
  };
952
1083
  }
953
1084
  async function storeEmbedding(args) {
954
- if (!args.embedder) {
1085
+ if (!args.embedder && !args.embedding) {
955
1086
  return;
956
1087
  }
957
1088
  try {
@@ -963,10 +1094,18 @@ async function storeEmbedding(args) {
963
1094
  return;
964
1095
  }
965
1096
  let embedding;
966
- try {
967
- embedding = await embedOne(args.embedder, args.content);
968
- } catch {
969
- return;
1097
+ if (args.embedding) {
1098
+ embedding = args.embedding;
1099
+ } else {
1100
+ const embedder = args.embedder;
1101
+ if (!embedder) {
1102
+ return;
1103
+ }
1104
+ try {
1105
+ embedding = await embedOne(embedder, args.content);
1106
+ } catch {
1107
+ return;
1108
+ }
970
1109
  }
971
1110
  try {
972
1111
  await args.db.insert(juniorMemoryEmbeddings).values({
@@ -983,6 +1122,192 @@ async function storeEmbedding(args) {
983
1122
  return;
984
1123
  }
985
1124
  }
1125
+ function activeScopedSubjectPredicate(args) {
1126
+ const predicate = and2(
1127
+ eq3(juniorMemoryMemories.scope, args.scope.scope),
1128
+ eq3(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
1129
+ eq3(juniorMemoryMemories.kind, args.kind),
1130
+ eq3(juniorMemoryMemories.subjectType, args.subject.subjectType),
1131
+ args.subject.subjectKey === void 0 ? isNull2(juniorMemoryMemories.subjectKey) : eq3(juniorMemoryMemories.subjectKey, args.subject.subjectKey),
1132
+ isNull2(juniorMemoryMemories.archivedAtMs),
1133
+ isNull2(juniorMemoryMemories.supersededAtMs),
1134
+ isNull2(juniorMemoryMemories.supersededById),
1135
+ or2(
1136
+ isNull2(juniorMemoryMemories.expiresAtMs),
1137
+ gt2(juniorMemoryMemories.expiresAtMs, args.nowMs)
1138
+ )
1139
+ );
1140
+ if (!predicate) {
1141
+ throw new Error("Memory duplicate predicate is empty.");
1142
+ }
1143
+ return predicate;
1144
+ }
1145
+ async function findExactDuplicateMemory(args) {
1146
+ const rows = await args.db.select().from(juniorMemoryMemories).where(
1147
+ and2(
1148
+ activeScopedSubjectPredicate(args),
1149
+ eq3(juniorMemoryMemories.content, args.content)
1150
+ )
1151
+ ).orderBy(
1152
+ desc2(juniorMemoryMemories.createdAtMs),
1153
+ asc(juniorMemoryMemories.id)
1154
+ ).limit(1);
1155
+ return rows[0] ? parseMemoryRow(rows[0]) : void 0;
1156
+ }
1157
+ async function findVectorDuplicateMemory(args) {
1158
+ const distance = cosineDistance(
1159
+ juniorMemoryEmbeddings.embedding,
1160
+ args.embedding.vector
1161
+ );
1162
+ const rows = await args.db.select({
1163
+ contentHash: juniorMemoryEmbeddings.contentHash,
1164
+ distance,
1165
+ memory: juniorMemoryMemories
1166
+ }).from(juniorMemoryMemories).innerJoin(
1167
+ juniorMemoryEmbeddings,
1168
+ eq3(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id)
1169
+ ).where(
1170
+ and2(
1171
+ activeScopedSubjectPredicate(args),
1172
+ eq3(juniorMemoryEmbeddings.provider, args.embedding.provider),
1173
+ eq3(juniorMemoryEmbeddings.model, args.embedding.model),
1174
+ eq3(juniorMemoryEmbeddings.dimensions, MEMORY_EMBEDDING_DIMENSIONS),
1175
+ eq3(juniorMemoryEmbeddings.metric, EMBEDDING_METRIC),
1176
+ lte(distance, HIGH_CONFIDENCE_DUPLICATE_DISTANCE)
1177
+ )
1178
+ ).orderBy(
1179
+ distance,
1180
+ desc2(juniorMemoryMemories.createdAtMs),
1181
+ asc(juniorMemoryMemories.id)
1182
+ ).limit(1);
1183
+ const row = rows[0];
1184
+ if (!row || hashEmbeddedContent(row.memory.content) !== row.contentHash) {
1185
+ return void 0;
1186
+ }
1187
+ return parseMemoryRow(row.memory);
1188
+ }
1189
+ async function rememberDuplicateIdempotency(args) {
1190
+ if (args.idempotencyKey === void 0) {
1191
+ return;
1192
+ }
1193
+ await args.db.insert(juniorMemoryMemories).values({
1194
+ content: args.content,
1195
+ createdAtMs: args.nowMs,
1196
+ expiresAtMs: args.duplicate.expiresAtMs,
1197
+ id: idempotencyAliasId({
1198
+ idempotencyKey: args.idempotencyKey,
1199
+ scope: args.scope,
1200
+ targetId: args.duplicate.id
1201
+ }),
1202
+ idempotencyKey: args.idempotencyKey,
1203
+ observedAtMs: args.nowMs,
1204
+ scope: args.scope.scope,
1205
+ scopeKey: args.scope.scopeKey,
1206
+ sourceKey: sourceKey(args.runtimeContext),
1207
+ sourcePlatform: args.runtimeContext.source.platform,
1208
+ subjectKey: args.subject.subjectKey,
1209
+ subjectType: args.subject.subjectType,
1210
+ supersededAtMs: args.nowMs,
1211
+ supersededById: args.duplicate.id,
1212
+ kind: args.duplicate.kind
1213
+ }).onConflictDoNothing();
1214
+ }
1215
+ async function listSupersessionCandidates(args) {
1216
+ const predicate = activeScopedSubjectPredicate(args);
1217
+ const terms = searchTerms(args.content).filter(
1218
+ (term) => !SUPERSESSION_STOP_TERMS.has(term)
1219
+ );
1220
+ const lexicalRows = terms.length > 0 ? await args.db.select().from(juniorMemoryMemories).where(
1221
+ and2(
1222
+ predicate,
1223
+ or2(
1224
+ ...terms.map(
1225
+ (term) => ilike2(juniorMemoryMemories.content, `%${term}%`)
1226
+ )
1227
+ )
1228
+ )
1229
+ ) : [];
1230
+ const lexicalCandidates = lexicalRows.map(parseMemoryRow).map((memory) => ({
1231
+ memory,
1232
+ score: searchScore(memory, terms)
1233
+ })).filter((candidate) => candidate.score > 1).sort(
1234
+ (left, right) => right.score - left.score || right.memory.createdAtMs - left.memory.createdAtMs || left.memory.id.localeCompare(right.memory.id)
1235
+ ).map((candidate) => candidate.memory);
1236
+ const vectorCandidates = args.embedding ? await listVectorSupersessionCandidates({
1237
+ db: args.db,
1238
+ embedding: args.embedding,
1239
+ kind: args.kind,
1240
+ nowMs: args.nowMs,
1241
+ scope: args.scope,
1242
+ subject: args.subject
1243
+ }) : [];
1244
+ const byId = /* @__PURE__ */ new Map();
1245
+ for (const memory of [...lexicalCandidates, ...vectorCandidates]) {
1246
+ if (byId.size >= SUPERSESSION_CANDIDATE_LIMIT) {
1247
+ break;
1248
+ }
1249
+ byId.set(memory.id, memory);
1250
+ }
1251
+ return [...byId.values()];
1252
+ }
1253
+ async function listVectorSupersessionCandidates(args) {
1254
+ const distance = cosineDistance(
1255
+ juniorMemoryEmbeddings.embedding,
1256
+ args.embedding.vector
1257
+ );
1258
+ const rows = await args.db.select({
1259
+ contentHash: juniorMemoryEmbeddings.contentHash,
1260
+ distance,
1261
+ memory: juniorMemoryMemories
1262
+ }).from(juniorMemoryMemories).innerJoin(
1263
+ juniorMemoryEmbeddings,
1264
+ eq3(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id)
1265
+ ).where(
1266
+ and2(
1267
+ activeScopedSubjectPredicate(args),
1268
+ eq3(juniorMemoryEmbeddings.provider, args.embedding.provider),
1269
+ eq3(juniorMemoryEmbeddings.model, args.embedding.model),
1270
+ eq3(juniorMemoryEmbeddings.dimensions, MEMORY_EMBEDDING_DIMENSIONS),
1271
+ eq3(juniorMemoryEmbeddings.metric, EMBEDDING_METRIC)
1272
+ )
1273
+ ).orderBy(
1274
+ distance,
1275
+ desc2(juniorMemoryMemories.createdAtMs),
1276
+ asc(juniorMemoryMemories.id)
1277
+ ).limit(SUPERSESSION_CANDIDATE_LIMIT);
1278
+ return rows.flatMap((row) => {
1279
+ if (hashEmbeddedContent(row.memory.content) !== row.contentHash) {
1280
+ return [];
1281
+ }
1282
+ return [parseMemoryRow(row.memory)];
1283
+ });
1284
+ }
1285
+ async function decideSupersededIds(args) {
1286
+ if (args.kind !== "preference" || !args.decider || args.candidates.length === 0) {
1287
+ return [];
1288
+ }
1289
+ const existingMemories = args.candidates.map((memory) => ({
1290
+ content: memory.content,
1291
+ id: memory.id
1292
+ }));
1293
+ const candidateIds = new Set(args.candidates.map((memory) => memory.id));
1294
+ try {
1295
+ const decision = await args.decider.adjudicateSupersession({
1296
+ candidate: {
1297
+ content: args.content,
1298
+ kind: "preference"
1299
+ },
1300
+ existingMemories,
1301
+ runtimeContext: args.runtimeContext
1302
+ });
1303
+ if (decision.decision !== "supersedes_old") {
1304
+ return [];
1305
+ }
1306
+ return decision.supersededIds.filter((id) => candidateIds.has(id));
1307
+ } catch {
1308
+ return [];
1309
+ }
1310
+ }
986
1311
  async function listVisibleMemories(args) {
987
1312
  const predicate = activeVisiblePredicate(args);
988
1313
  if (!predicate) {
@@ -1067,7 +1392,20 @@ async function searchVisibleVectorMemories(args) {
1067
1392
  ];
1068
1393
  });
1069
1394
  }
1070
- function mergeSearchCandidates(candidates) {
1395
+ function observedAgeBoost(memory, nowMs) {
1396
+ const ageMs = Math.max(0, nowMs - memory.observedAtMs);
1397
+ if (ageMs <= 7 * ONE_DAY_MS) {
1398
+ return 0.15;
1399
+ }
1400
+ if (ageMs <= 30 * ONE_DAY_MS) {
1401
+ return 0.1;
1402
+ }
1403
+ if (ageMs <= 90 * ONE_DAY_MS) {
1404
+ return 0.05;
1405
+ }
1406
+ return 0;
1407
+ }
1408
+ function mergeSearchCandidates(candidates, nowMs) {
1071
1409
  const byId = /* @__PURE__ */ new Map();
1072
1410
  for (const candidate of candidates) {
1073
1411
  const existing = byId.get(candidate.memory.id);
@@ -1080,14 +1418,19 @@ function mergeSearchCandidates(candidates) {
1080
1418
  score: candidate.score
1081
1419
  });
1082
1420
  }
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);
1421
+ return [...byId.values()].sort((left, right) => {
1422
+ const scoreDelta = right.score - left.score;
1423
+ if (Math.abs(scoreDelta) > RELEVANCE_NEAR_TIE_DELTA) {
1424
+ return scoreDelta;
1425
+ }
1426
+ return observedAgeBoost(right.memory, nowMs) - observedAgeBoost(left.memory, nowMs) || right.memory.observedAtMs - left.memory.observedAtMs || left.memory.id.localeCompare(right.memory.id);
1427
+ }).map((candidate) => candidate.memory);
1086
1428
  }
1087
1429
  function createMemoryStore(db, context, options = {}) {
1088
1430
  const runtimeContext = memoryRuntimeContextSchema.parse(context);
1089
1431
  const parsedOptions = memoryStoreOptionsSchema.parse({ now: options.now });
1090
1432
  const embedder = options.embedder;
1433
+ const supersessionDecider = options.supersessionDecider;
1091
1434
  const getNowMs = parsedOptions.now ?? Date.now;
1092
1435
  async function archiveExpiredVisibleMemories(input, nowMs) {
1093
1436
  input = archiveExpiredMemoriesInputSchema.parse(input ?? {});
@@ -1119,35 +1462,162 @@ function createMemoryStore(db, context, options = {}) {
1119
1462
  nowMs,
1120
1463
  scopes: [scope]
1121
1464
  });
1122
- const id = randomUUID();
1123
- const rows = await db.insert(juniorMemoryMemories).values({
1465
+ if (input.idempotencyKey !== void 0) {
1466
+ const idempotent2 = await findByIdempotencyKey({
1467
+ db,
1468
+ idempotencyKey: input.idempotencyKey,
1469
+ nowMs,
1470
+ scope
1471
+ });
1472
+ if (idempotent2) {
1473
+ await storeEmbedding({
1474
+ content: idempotent2.content,
1475
+ db,
1476
+ embedder,
1477
+ memoryId: idempotent2.id,
1478
+ nowMs
1479
+ });
1480
+ return { created: false, memory: idempotent2 };
1481
+ }
1482
+ }
1483
+ const exactDuplicate = await findExactDuplicateMemory({
1124
1484
  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();
1485
+ db,
1486
+ kind: input.kind,
1487
+ nowMs,
1488
+ scope,
1489
+ subject
1490
+ });
1491
+ if (exactDuplicate) {
1492
+ await rememberDuplicateIdempotency({
1493
+ content,
1494
+ db,
1495
+ duplicate: exactDuplicate,
1496
+ idempotencyKey: input.idempotencyKey,
1497
+ nowMs,
1498
+ runtimeContext,
1499
+ scope,
1500
+ subject
1501
+ });
1502
+ await storeEmbedding({
1503
+ content: exactDuplicate.content,
1504
+ db,
1505
+ embedder,
1506
+ memoryId: exactDuplicate.id,
1507
+ nowMs
1508
+ });
1509
+ return { created: false, memory: exactDuplicate };
1510
+ }
1511
+ let candidateEmbedding;
1512
+ if (embedder) {
1513
+ try {
1514
+ candidateEmbedding = await embedOne(embedder, content);
1515
+ } catch {
1516
+ candidateEmbedding = void 0;
1517
+ }
1518
+ }
1519
+ const vectorDuplicate = candidateEmbedding ? await findVectorDuplicateMemory({
1520
+ db,
1521
+ embedding: candidateEmbedding,
1522
+ kind: input.kind,
1523
+ nowMs,
1524
+ scope,
1525
+ subject
1526
+ }) : void 0;
1527
+ if (vectorDuplicate) {
1528
+ await rememberDuplicateIdempotency({
1529
+ content,
1530
+ db,
1531
+ duplicate: vectorDuplicate,
1532
+ idempotencyKey: input.idempotencyKey,
1533
+ nowMs,
1534
+ runtimeContext,
1535
+ scope,
1536
+ subject
1537
+ });
1538
+ await storeEmbedding({
1539
+ content: vectorDuplicate.content,
1540
+ db,
1541
+ embedder,
1542
+ memoryId: vectorDuplicate.id,
1543
+ nowMs
1544
+ });
1545
+ return { created: false, memory: vectorDuplicate };
1546
+ }
1547
+ let supersededIds = [];
1548
+ if (scopeKind === "personal" && input.kind === "preference" && supersessionDecider && (input.expiresAtMs === void 0 || input.expiresAtMs > nowMs)) {
1549
+ const supersessionCandidates = await listSupersessionCandidates({
1550
+ content,
1551
+ db,
1552
+ ...candidateEmbedding ? { embedding: candidateEmbedding } : {},
1553
+ kind: input.kind,
1554
+ nowMs,
1555
+ scope,
1556
+ subject
1557
+ });
1558
+ supersededIds = await decideSupersededIds({
1559
+ candidates: supersessionCandidates,
1560
+ content,
1561
+ decider: supersessionDecider,
1562
+ kind: input.kind,
1563
+ runtimeContext
1564
+ });
1565
+ }
1566
+ const id = randomUUID();
1567
+ const rows = await db.transaction(async (tx) => {
1568
+ const inserted = await tx.insert(juniorMemoryMemories).values({
1569
+ content,
1570
+ createdAtMs: nowMs,
1571
+ expiresAtMs: input.expiresAtMs,
1572
+ id,
1573
+ idempotencyKey: input.idempotencyKey,
1574
+ observedAtMs: nowMs,
1575
+ scope: scope.scope,
1576
+ scopeKey: scope.scopeKey,
1577
+ sourceKey: sourceKey(runtimeContext),
1578
+ sourcePlatform: runtimeContext.source.platform,
1579
+ subjectKey: subject.subjectKey,
1580
+ subjectType: subject.subjectType,
1581
+ kind: input.kind
1582
+ }).onConflictDoNothing({
1583
+ target: [
1584
+ juniorMemoryMemories.scope,
1585
+ juniorMemoryMemories.scopeKey,
1586
+ juniorMemoryMemories.idempotencyKey
1587
+ ],
1588
+ where: sql2`${juniorMemoryMemories.idempotencyKey} IS NOT NULL AND ${juniorMemoryMemories.archivedAtMs} IS NULL AND ${juniorMemoryMemories.supersededAtMs} IS NULL AND ${juniorMemoryMemories.supersededById} IS NULL`
1589
+ }).returning();
1590
+ const insertedMemory = inserted[0];
1591
+ if (!insertedMemory || supersededIds.length === 0) {
1592
+ return inserted;
1593
+ }
1594
+ const superseded = await tx.update(juniorMemoryMemories).set({
1595
+ supersededAtMs: nowMs,
1596
+ supersededById: insertedMemory.id
1597
+ }).where(
1598
+ and2(
1599
+ inArray(juniorMemoryMemories.id, supersededIds),
1600
+ activeScopedSubjectPredicate({
1601
+ kind: input.kind,
1602
+ nowMs,
1603
+ scope,
1604
+ subject
1605
+ })
1606
+ )
1607
+ ).returning({ id: juniorMemoryMemories.id });
1608
+ const idsToClean = superseded.map((row) => row.id);
1609
+ if (idsToClean.length > 0) {
1610
+ await tx.delete(juniorMemoryEmbeddings).where(inArray(juniorMemoryEmbeddings.memoryId, idsToClean));
1611
+ }
1612
+ return inserted;
1613
+ });
1145
1614
  if (rows[0]) {
1146
1615
  const memory = parseMemoryRow(rows[0]);
1147
1616
  await storeEmbedding({
1148
1617
  content: memory.content,
1149
1618
  db,
1150
1619
  embedder,
1620
+ embedding: candidateEmbedding,
1151
1621
  memoryId: memory.id,
1152
1622
  nowMs
1153
1623
  });
@@ -1223,10 +1693,10 @@ function createMemoryStore(db, context, options = {}) {
1223
1693
  });
1224
1694
  const terms = searchTerms(input.query);
1225
1695
  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);
1696
+ return mergeSearchCandidates(
1697
+ [...vectorCandidates, ...lexicalCandidates],
1698
+ nowMs
1699
+ ).slice(0, limit);
1230
1700
  },
1231
1701
  async archiveMemory(input) {
1232
1702
  input = archiveMemoryInputSchema.parse(input);
@@ -1297,9 +1767,10 @@ function memoryRuntimeContext(context) {
1297
1767
  source: context.source
1298
1768
  });
1299
1769
  }
1300
- function memoryStore(context) {
1770
+ function memoryStore(context, options = {}) {
1301
1771
  return createMemoryStore(context.db, memoryRuntimeContext(context), {
1302
- embedder: context.embedder
1772
+ embedder: context.embedder,
1773
+ ...options.supersessionDecider ? { supersessionDecider: options.supersessionDecider } : {}
1303
1774
  });
1304
1775
  }
1305
1776
  function boundedLimit2(value, fallback) {
@@ -1442,6 +1913,16 @@ var searchMemoriesInputSchema2 = Type.Object(
1442
1913
  },
1443
1914
  { additionalProperties: false }
1444
1915
  );
1916
+ var memoryToolProjectionSchema = Type.Object(
1917
+ {
1918
+ id: Type.String({ minLength: 1 }),
1919
+ content: Type.String({ minLength: 1 }),
1920
+ createdAtMs: Type.Number(),
1921
+ observedAtMs: Type.Number(),
1922
+ expiresAtMs: Type.Optional(Type.Number())
1923
+ },
1924
+ { additionalProperties: false }
1925
+ );
1445
1926
  function parseToolInput(schema, input) {
1446
1927
  try {
1447
1928
  if (!Value.Check(schema, input)) {
@@ -1476,12 +1957,13 @@ function targetForKind(kind) {
1476
1957
  return "conversation";
1477
1958
  }
1478
1959
  function compactMemory(memory) {
1479
- return {
1960
+ return Value.Parse(memoryToolProjectionSchema, {
1480
1961
  id: memory.id,
1481
1962
  content: memory.content,
1482
1963
  createdAtMs: memory.createdAtMs,
1964
+ observedAtMs: memory.observedAtMs,
1483
1965
  ...memory.expiresAtMs !== void 0 ? { expiresAtMs: memory.expiresAtMs } : {}
1484
- };
1966
+ });
1485
1967
  }
1486
1968
  function createMemoryCreateTool(context) {
1487
1969
  return {
@@ -1496,7 +1978,9 @@ function createMemoryCreateTool(context) {
1496
1978
  const toolCallId = requireToolCallId(options.toolCallId);
1497
1979
  const requestedExpiresAtMs = parseExpiresAt(parsedInput.expires_at);
1498
1980
  const runtimeContext = memoryRuntimeContext(context);
1499
- const store = memoryStore(context);
1981
+ const store = memoryStore(context, {
1982
+ supersessionDecider: context.supersessionDecider
1983
+ });
1500
1984
  const review = await (async () => {
1501
1985
  try {
1502
1986
  return parseMemoryReview(
@@ -1699,8 +2183,10 @@ async function processMemorySession(context) {
1699
2183
  ...run.requester ? { requester: run.requester } : {},
1700
2184
  source: run.source
1701
2185
  });
2186
+ const agent = createMemoryAgent(context.model);
1702
2187
  const store = createMemoryStore(context.db, runtimeContext, {
1703
- embedder: context.embedder
2188
+ embedder: context.embedder,
2189
+ supersessionDecider: agent
1704
2190
  });
1705
2191
  await store.archiveExpiredMemories();
1706
2192
  const memories = await getTaskMemories(context, async () => {
@@ -1708,7 +2194,6 @@ async function processMemorySession(context) {
1708
2194
  limit: 10,
1709
2195
  query: evidenceText
1710
2196
  });
1711
- const agent = createMemoryAgent(context.model);
1712
2197
  return await agent.extractSessionMemories({
1713
2198
  existingMemories: existingMemories.map((memory) => ({
1714
2199
  content: memory.content
@@ -1744,13 +2229,19 @@ function trimContent(content, maxLength) {
1744
2229
  }
1745
2230
  return `${trimmed.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
1746
2231
  }
2232
+ function formatObservedDate(observedAtMs) {
2233
+ return new Date(observedAtMs).toISOString().slice(0, 10);
2234
+ }
1747
2235
  function renderMemoryPrompt(memories) {
1748
2236
  const header = "Relevant memories for this request:";
1749
2237
  const footer = "Treat these as possibly stale context. Current user instructions and repository evidence take priority.";
1750
2238
  const lines = [];
1751
2239
  let totalChars = header.length + footer.length + 2;
1752
2240
  for (const memory of memories) {
1753
- const line = `- ${trimContent(memory.content, MAX_MEMORY_LINE_CHARS)}`;
2241
+ const line = `- Observed ${formatObservedDate(memory.observedAtMs)}: ${trimContent(
2242
+ memory.content,
2243
+ MAX_MEMORY_LINE_CHARS
2244
+ )}`;
1754
2245
  if (totalChars + line.length + 1 > MAX_PROMPT_CHARS) {
1755
2246
  break;
1756
2247
  }
@@ -1807,6 +2298,12 @@ function memoryToolContext(ctx) {
1807
2298
  ...ctx.userText ? { userText: ctx.userText } : {}
1808
2299
  };
1809
2300
  }
2301
+ function memoryCreateToolContext(ctx) {
2302
+ return {
2303
+ ...memoryToolContext(ctx),
2304
+ supersessionDecider: ctx.supersessionDecider
2305
+ };
2306
+ }
1810
2307
  function createMemoryPlugin(options = {}) {
1811
2308
  const modelId = memoryModelId(options);
1812
2309
  return defineJuniorPlugin({
@@ -1829,14 +2326,23 @@ function createMemoryPlugin(options = {}) {
1829
2326
  },
1830
2327
  hooks: {
1831
2328
  tools(ctx) {
2329
+ const agent = createMemoryAgent(ctx.model);
1832
2330
  const context = memoryToolContext({
1833
2331
  ...ctx,
1834
- agent: createMemoryAgent(ctx.model),
2332
+ agent,
1835
2333
  db: ctx.db,
1836
2334
  embedder: ctx.embedder
1837
2335
  });
1838
2336
  return {
1839
- createMemory: createMemoryCreateTool(context),
2337
+ createMemory: createMemoryCreateTool(
2338
+ memoryCreateToolContext({
2339
+ ...ctx,
2340
+ agent,
2341
+ db: ctx.db,
2342
+ embedder: ctx.embedder,
2343
+ supersessionDecider: agent
2344
+ })
2345
+ ),
1840
2346
  removeMemory: createMemoryRemoveTool(context),
1841
2347
  listMemories: createMemoryListTool(context),
1842
2348
  searchMemories: createMemorySearchTool(context)