@sentry/junior-memory 0.134.0 → 0.136.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-memory",
3
- "version": "0.134.0",
3
+ "version": "0.136.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -27,7 +27,7 @@
27
27
  "commander": "^14.0.3",
28
28
  "drizzle-orm": "^0.45.2",
29
29
  "zod": "^4.4.3",
30
- "@sentry/junior-plugin-api": "0.134.0"
30
+ "@sentry/junior-plugin-api": "0.136.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^25.9.1",
package/src/api.ts CHANGED
@@ -27,6 +27,8 @@ export const memoryApiSchema = z
27
27
  id: z.string().min(1),
28
28
  kind: z.enum(["preference", "procedure", "knowledge"]),
29
29
  observedAt: z.iso.datetime(),
30
+ origin: z.enum(["automatic", "explicit", "other"]),
31
+ sourcePlatform: z.enum(["local", "slack"]),
30
32
  visibility: z.enum(["private", "public"]),
31
33
  })
32
34
  .strict();
@@ -118,6 +120,8 @@ function apiMemory(
118
120
  id: memory.id,
119
121
  kind: memory.kind,
120
122
  observedAt: new Date(memory.observedAtMs).toISOString(),
123
+ origin: memory.origin,
124
+ sourcePlatform: memory.sourcePlatform,
121
125
  visibility: memory.visibility,
122
126
  };
123
127
  }
package/src/ranking.ts CHANGED
@@ -83,10 +83,15 @@ export function rankMemoryMatches(
83
83
  byId.set(match.memory.id, match);
84
84
  continue;
85
85
  }
86
+ // Keep the first rank per modality. Shared legs are fused before personal
87
+ // probes, so a smaller personal top-k cannot overwrite a shared dense rank
88
+ // with an inflated top rank for the same memory.
86
89
  byId.set(match.memory.id, {
87
90
  ...existing,
88
- ...(match.lexical ? { lexical: match.lexical } : {}),
89
- ...(match.vector ? { vector: match.vector } : {}),
91
+ ...(!existing.lexical && match.lexical
92
+ ? { lexical: match.lexical }
93
+ : {}),
94
+ ...(!existing.vector && match.vector ? { vector: match.vector } : {}),
90
95
  });
91
96
  }
92
97
  return [...byId.values()].sort((left, right) => {
@@ -94,6 +99,15 @@ export function rankMemoryMatches(
94
99
  if (scoreDelta !== 0) {
95
100
  return scoreDelta;
96
101
  }
102
+ // Prefer actor preferences over workspace knowledge when RRF ties. Shared
103
+ // lexical legs often assign the same top rank to recent conversation noise
104
+ // and a personal-scope probe hit for the same common token.
105
+ const personalDelta =
106
+ Number(right.memory.scope === "personal") -
107
+ Number(left.memory.scope === "personal");
108
+ if (personalDelta !== 0) {
109
+ return personalDelta;
110
+ }
97
111
  const channelDelta =
98
112
  Number(currentChannel(right, options.channelPrefix)) -
99
113
  Number(currentChannel(left, options.channelPrefix));
package/src/store.ts CHANGED
@@ -1066,30 +1066,17 @@ async function searchVisibleLexicalMemories(args: {
1066
1066
  /** Search active visible records with pgvector cosine distance. */
1067
1067
  async function searchVisibleVectorMemories(args: {
1068
1068
  db: MemoryDb;
1069
- embedder: MemoryEmbeddingProvider | undefined;
1069
+ embedding: MemoryEmbedding;
1070
1070
  limit: number;
1071
1071
  maxDistance?: number;
1072
1072
  nowMs: number;
1073
- query: string;
1074
1073
  scopes: ResolvedMemoryScope[];
1075
1074
  }): Promise<MemoryMatch[]> {
1076
- if (!args.embedder) {
1077
- return [];
1078
- }
1079
1075
  const predicate = activeVisiblePredicate(args);
1080
1076
  if (!predicate) {
1081
1077
  return [];
1082
1078
  }
1083
- const query = normalizeRetrievalQuery(args.query);
1084
- if (!query) {
1085
- return [];
1086
- }
1087
- let embedding: Awaited<ReturnType<typeof embedOne>>;
1088
- try {
1089
- embedding = await embedOne(args.embedder, query);
1090
- } catch {
1091
- return [];
1092
- }
1079
+ const embedding = args.embedding;
1093
1080
  const distance = cosineDistance(
1094
1081
  juniorMemoryEmbeddings.embedding,
1095
1082
  embedding.vector,
@@ -1409,6 +1396,11 @@ export function createMemoryStore(
1409
1396
  * vectors already hit: that drops exact/token memories and serializes the
1410
1397
  * miss path. Each leg is a hard-capped top-k probe so Postgres work stays
1411
1398
  * bounded even on broad queries.
1399
+ *
1400
+ * Automatic recall also runs personal-scope-only probes. Workspace
1401
+ * conversation memories sharing common tokens (for example "time") can fill
1402
+ * the shared lexical recency window before ranking, which buries older actor
1403
+ * preferences that explicit search still finds.
1412
1404
  */
1413
1405
  async function retrieveVisibleMemories(
1414
1406
  rawInput: SearchMemoriesInput,
@@ -1428,30 +1420,66 @@ export function createMemoryStore(
1428
1420
  ? SEARCH_RETRIEVAL_OVERFETCH
1429
1421
  : RECALL_RETRIEVAL_OVERFETCH;
1430
1422
  const candidateLimit = retrievalLegLimit(limit, overfetch);
1423
+ const personalScopes = scopes.filter((scope) => scope.scope === "personal");
1424
+ // Automatic recall only: keep a personal-scope probe so workspace noise
1425
+ // cannot monopolize the shared lexical recency window.
1426
+ const probePersonal =
1427
+ vectorMaxDistance !== undefined && personalScopes.length > 0;
1428
+ const query = normalizeRetrievalQuery(input.query);
1429
+ let queryEmbedding: MemoryEmbedding | undefined;
1430
+ if (embedder && query) {
1431
+ try {
1432
+ queryEmbedding = await embedOne(embedder, query);
1433
+ } catch {
1434
+ queryEmbedding = undefined;
1435
+ }
1436
+ }
1437
+ const emptyMatches = Promise.resolve([] as MemoryMatch[]);
1438
+ const lexicalArgs = {
1439
+ db,
1440
+ limit: candidateLimit,
1441
+ nowMs,
1442
+ query: input.query,
1443
+ };
1431
1444
  // Always run both legs in parallel. Conditional lexical skip is unsafe:
1432
1445
  // one in-threshold vector distractor can hide a stronger lexical hit.
1433
- const [vectorMatches, lexicalMatches] = await Promise.all([
1434
- searchVisibleVectorMemories({
1435
- db,
1436
- embedder,
1437
- limit: candidateLimit,
1438
- ...(vectorMaxDistance !== undefined
1439
- ? { maxDistance: vectorMaxDistance }
1440
- : {}),
1441
- nowMs,
1442
- query: input.query,
1443
- scopes,
1444
- }),
1446
+ // Embed once up front; vector probes only run when that embedding exists.
1447
+ const matches = await Promise.all([
1448
+ queryEmbedding
1449
+ ? searchVisibleVectorMemories({
1450
+ db,
1451
+ embedding: queryEmbedding,
1452
+ limit: candidateLimit,
1453
+ ...(vectorMaxDistance !== undefined
1454
+ ? { maxDistance: vectorMaxDistance }
1455
+ : {}),
1456
+ nowMs,
1457
+ scopes,
1458
+ })
1459
+ : emptyMatches,
1445
1460
  searchVisibleLexicalMemories({
1446
- db,
1447
- limit: candidateLimit,
1448
- nowMs,
1449
- query: input.query,
1461
+ ...lexicalArgs,
1450
1462
  scopes,
1451
1463
  }),
1464
+ queryEmbedding && probePersonal
1465
+ ? searchVisibleVectorMemories({
1466
+ db,
1467
+ embedding: queryEmbedding,
1468
+ limit: candidateLimit,
1469
+ maxDistance: vectorMaxDistance,
1470
+ nowMs,
1471
+ scopes: personalScopes,
1472
+ })
1473
+ : emptyMatches,
1474
+ probePersonal
1475
+ ? searchVisibleLexicalMemories({
1476
+ ...lexicalArgs,
1477
+ scopes: personalScopes,
1478
+ })
1479
+ : emptyMatches,
1452
1480
  ]);
1453
1481
  const channelPrefix = sourceChannelPrefix(runtimeContext);
1454
- return rankMemoryMatches([...vectorMatches, ...lexicalMatches], {
1482
+ return rankMemoryMatches(matches.flat(), {
1455
1483
  nowMs,
1456
1484
  // Slight lexical preference protects exact ids/names/timezones on ties.
1457
1485
  ...(vectorMaxDistance === undefined