@sentry/junior-memory 0.81.0 → 0.83.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/plugin.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export interface MemoryPluginOptions {
2
2
  modelId?: string;
3
+ /** Maximum cosine distance for vector recall candidates. Defaults to MEMORY_RECALL_MAX_VECTOR_DISTANCE env or 0.45. */
4
+ recallMaxVectorDistance?: number;
3
5
  }
4
6
  /** Create Junior's long-term memory plugin registration. */
5
7
  export declare function createMemoryPlugin(options?: MemoryPluginOptions): import("@sentry/junior-plugin-api").PluginRegistration;
package/dist/recall.d.ts CHANGED
@@ -4,6 +4,8 @@ export interface MemoryRecallContext {
4
4
  conversationId?: string;
5
5
  db: MemoryDb;
6
6
  embedder?: MemoryEmbeddingProvider;
7
+ /** Maximum cosine distance for vector recall. Passed through to the memory store. */
8
+ maxVectorDistance?: number;
7
9
  requester?: Requester;
8
10
  source: Source;
9
11
  text: string;
package/dist/store.d.ts CHANGED
@@ -107,6 +107,8 @@ export interface MemorySupersessionDecider {
107
107
  }
108
108
  export interface MemoryStoreOptions {
109
109
  embedder?: MemoryEmbeddingProvider;
110
+ /** Maximum cosine distance for vector recall candidates. Model-dependent; tune when changing AI_EMBEDDING_MODEL. */
111
+ maxVectorDistance?: number;
110
112
  now?: () => number;
111
113
  supersessionDecider?: MemorySupersessionDecider;
112
114
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-memory",
3
- "version": "0.81.0",
3
+ "version": "0.83.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.81.0"
30
+ "@sentry/junior-plugin-api": "0.83.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^25.9.1",
package/src/plugin.ts CHANGED
@@ -15,9 +15,13 @@ import { createMemoryPromptMessages } from "./recall";
15
15
  import type { MemoryDb } from "./store";
16
16
 
17
17
  const MEMORY_MODEL_ENV = "AI_MEMORY_MODEL";
18
+ const MEMORY_RECALL_MAX_VECTOR_DISTANCE_ENV = "MEMORY_RECALL_MAX_VECTOR_DISTANCE";
19
+ const DEFAULT_RECALL_MAX_VECTOR_DISTANCE = 0.45;
18
20
 
19
21
  export interface MemoryPluginOptions {
20
22
  modelId?: string;
23
+ /** Maximum cosine distance for vector recall candidates. Defaults to MEMORY_RECALL_MAX_VECTOR_DISTANCE env or 0.45. */
24
+ recallMaxVectorDistance?: number;
21
25
  }
22
26
 
23
27
  function memoryModelId(options: MemoryPluginOptions): string | undefined {
@@ -29,6 +33,20 @@ function memoryModelId(options: MemoryPluginOptions): string | undefined {
29
33
  return envModelId || undefined;
30
34
  }
31
35
 
36
+ function recallMaxVectorDistance(options: MemoryPluginOptions): number {
37
+ if (options.recallMaxVectorDistance !== undefined) {
38
+ return options.recallMaxVectorDistance;
39
+ }
40
+ const raw = process.env[MEMORY_RECALL_MAX_VECTOR_DISTANCE_ENV]?.trim();
41
+ if (raw) {
42
+ const parsed = Number(raw);
43
+ if (Number.isFinite(parsed) && parsed > 0) {
44
+ return Math.min(parsed, 1);
45
+ }
46
+ }
47
+ return DEFAULT_RECALL_MAX_VECTOR_DISTANCE;
48
+ }
49
+
32
50
  function memoryToolContext(ctx: {
33
51
  agent: MemoryReviewer;
34
52
  conversationId?: string;
@@ -118,6 +136,7 @@ export function createMemoryPlugin(options: MemoryPluginOptions = {}) {
118
136
  ...(ctx.requester ? { requester: ctx.requester } : {}),
119
137
  db: ctx.db as MemoryDb,
120
138
  embedder: ctx.embedder,
139
+ maxVectorDistance: recallMaxVectorDistance(options),
121
140
  source: ctx.source,
122
141
  text: ctx.text,
123
142
  });
package/src/recall.ts CHANGED
@@ -12,13 +12,15 @@ import {
12
12
  import { memoryRuntimeContextSchema } from "./types";
13
13
 
14
14
  const DEFAULT_RECALL_LIMIT = 5;
15
- const MAX_PROMPT_CHARS = 1_600;
16
- const MAX_MEMORY_LINE_CHARS = 320;
15
+ const MAX_PROMPT_CHARS = 4_000;
16
+ const MAX_MEMORY_LINE_CHARS = 600;
17
17
 
18
18
  export interface MemoryRecallContext {
19
19
  conversationId?: string;
20
20
  db: MemoryDb;
21
21
  embedder?: MemoryEmbeddingProvider;
22
+ /** Maximum cosine distance for vector recall. Passed through to the memory store. */
23
+ maxVectorDistance?: number;
22
24
  requester?: Requester;
23
25
  source: Source;
24
26
  text: string;
@@ -78,7 +80,12 @@ export async function createMemoryPromptMessages(
78
80
  const memories = await createMemoryStore(
79
81
  context.db,
80
82
  runtimeContext,
81
- context.embedder ? { embedder: context.embedder } : {},
83
+ {
84
+ ...(context.embedder ? { embedder: context.embedder } : {}),
85
+ ...(context.maxVectorDistance !== undefined
86
+ ? { maxVectorDistance: context.maxVectorDistance }
87
+ : {}),
88
+ },
82
89
  ).searchMemories({
83
90
  query: context.text,
84
91
  limit: DEFAULT_RECALL_LIMIT,
package/src/store.ts CHANGED
@@ -56,6 +56,7 @@ const MAX_MEMORY_CONTENT_CHARS = 4_000;
56
56
  const EMBEDDING_METRIC = "cosine";
57
57
  const ONE_DAY_MS = 24 * 60 * 60 * 1000;
58
58
  const RELEVANCE_NEAR_TIE_DELTA = 0.01;
59
+ const SOURCE_CHANNEL_BOOST = 0.05;
59
60
  const SUPERSESSION_STOP_TERMS = new Set([
60
61
  "and",
61
62
  "for",
@@ -73,6 +74,7 @@ export type MemoryDb = PgDatabase<PgQueryResultHKT, typeof memorySqlSchema>;
73
74
  interface SearchCandidate {
74
75
  memory: MemoryRecord;
75
76
  score: number;
77
+ sourceKey: string;
76
78
  }
77
79
 
78
80
  interface MemoryEmbedding {
@@ -274,6 +276,8 @@ export interface MemorySupersessionDecider {
274
276
 
275
277
  export interface MemoryStoreOptions {
276
278
  embedder?: MemoryEmbeddingProvider;
279
+ /** Maximum cosine distance for vector recall candidates. Model-dependent; tune when changing AI_EMBEDDING_MODEL. */
280
+ maxVectorDistance?: number;
277
281
  now?: () => number;
278
282
  supersessionDecider?: MemorySupersessionDecider;
279
283
  }
@@ -343,6 +347,14 @@ function sourceKey(ctx: MemoryRuntimeContext): string {
343
347
  return `slack:${ctx.source.teamId}:${ctx.source.channelId}:${threadKey}`;
344
348
  }
345
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
+
346
358
  /** Parse one SQL row into the public memory record projection. */
347
359
  function parseMemoryRow(row: unknown): MemoryRecord {
348
360
  const parsed = memoryRowSchema.parse(row);
@@ -959,7 +971,7 @@ async function searchVisibleMemories(args: {
959
971
  nowMs: number;
960
972
  query: string;
961
973
  scopes: ResolvedMemoryScope[];
962
- }): Promise<MemoryRecord[]> {
974
+ }): Promise<SearchCandidate[]> {
963
975
  const terms = searchTerms(args.query);
964
976
  if (terms.length === 0) {
965
977
  return [];
@@ -981,7 +993,11 @@ async function searchVisibleMemories(args: {
981
993
  ),
982
994
  ),
983
995
  );
984
- return rows.map(parseMemoryRow);
996
+ return rows.map((row) => ({
997
+ memory: parseMemoryRow(row),
998
+ score: 0,
999
+ sourceKey: row.sourceKey,
1000
+ }));
985
1001
  }
986
1002
 
987
1003
  /** Search active visible records with exact pgvector cosine distance. */
@@ -989,6 +1005,7 @@ async function searchVisibleVectorMemories(args: {
989
1005
  db: MemoryDb;
990
1006
  embedder: MemoryEmbeddingProvider | undefined;
991
1007
  limit: number;
1008
+ maxDistance?: number;
992
1009
  nowMs: number;
993
1010
  query: string;
994
1011
  scopes: ResolvedMemoryScope[];
@@ -1045,15 +1062,31 @@ async function searchVisibleVectorMemories(args: {
1045
1062
  ) {
1046
1063
  return [];
1047
1064
  }
1065
+ if (args.maxDistance !== undefined && distanceValue > args.maxDistance) {
1066
+ return [];
1067
+ }
1048
1068
  return [
1049
1069
  {
1050
1070
  memory: parseMemoryRow(row.memory),
1051
1071
  score: 1 / (1 + Math.max(0, distanceValue)),
1072
+ sourceKey: row.memory.sourceKey,
1052
1073
  },
1053
1074
  ];
1054
1075
  });
1055
1076
  }
1056
1077
 
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
+
1057
1090
  /** Score only observation age for near-tie reranking; this is not lifecycle decay. */
1058
1091
  function observedAgeBoost(memory: MemoryRecord, nowMs: number): number {
1059
1092
  const ageMs = Math.max(0, nowMs - memory.observedAtMs);
@@ -1073,12 +1106,14 @@ function observedAgeBoost(memory: MemoryRecord, nowMs: number): number {
1073
1106
  function mergeSearchCandidates(
1074
1107
  candidates: SearchCandidate[],
1075
1108
  nowMs: number,
1109
+ currentChannelKey?: string,
1076
1110
  ): MemoryRecord[] {
1077
1111
  const byId = new Map<
1078
1112
  string,
1079
1113
  {
1080
1114
  memory: MemoryRecord;
1081
1115
  score: number;
1116
+ sourceKey: string;
1082
1117
  }
1083
1118
  >();
1084
1119
  for (const candidate of candidates) {
@@ -1090,6 +1125,7 @@ function mergeSearchCandidates(
1090
1125
  byId.set(candidate.memory.id, {
1091
1126
  memory: candidate.memory,
1092
1127
  score: candidate.score,
1128
+ sourceKey: candidate.sourceKey,
1093
1129
  });
1094
1130
  }
1095
1131
  return [...byId.values()]
@@ -1098,6 +1134,12 @@ function mergeSearchCandidates(
1098
1134
  if (Math.abs(scoreDelta) > RELEVANCE_NEAR_TIE_DELTA) {
1099
1135
  return scoreDelta;
1100
1136
  }
1137
+ const sourceDelta =
1138
+ sourceBoost(right, currentChannelKey) -
1139
+ sourceBoost(left, currentChannelKey);
1140
+ if (sourceDelta !== 0) {
1141
+ return sourceDelta;
1142
+ }
1101
1143
  return (
1102
1144
  observedAgeBoost(right.memory, nowMs) -
1103
1145
  observedAgeBoost(left.memory, nowMs) ||
@@ -1117,6 +1159,7 @@ export function createMemoryStore(
1117
1159
  const runtimeContext = memoryRuntimeContextSchema.parse(context);
1118
1160
  const parsedOptions = memoryStoreOptionsSchema.parse({ now: options.now });
1119
1161
  const embedder = options.embedder;
1162
+ const maxVectorDistance = options.maxVectorDistance;
1120
1163
  const supersessionDecider = options.supersessionDecider;
1121
1164
  const getNowMs = parsedOptions.now ?? Date.now;
1122
1165
 
@@ -1404,6 +1447,9 @@ export function createMemoryStore(
1404
1447
  db,
1405
1448
  embedder,
1406
1449
  limit: limit * VECTOR_SEARCH_OVERFETCH,
1450
+ ...(maxVectorDistance !== undefined
1451
+ ? { maxDistance: maxVectorDistance }
1452
+ : {}),
1407
1453
  nowMs,
1408
1454
  query: input.query,
1409
1455
  scopes,
@@ -1416,11 +1462,15 @@ export function createMemoryStore(
1416
1462
  });
1417
1463
  const terms = searchTerms(input.query);
1418
1464
  const lexicalCandidates = candidates
1419
- .map((memory) => ({ memory, score: searchScore(memory, terms) }))
1465
+ .map((candidate) => ({
1466
+ ...candidate,
1467
+ score: searchScore(candidate.memory, terms),
1468
+ }))
1420
1469
  .filter((item) => item.score > 0);
1421
1470
  return mergeSearchCandidates(
1422
1471
  [...vectorCandidates, ...lexicalCandidates],
1423
1472
  nowMs,
1473
+ sourceChannelPrefix(runtimeContext),
1424
1474
  ).slice(0, limit);
1425
1475
  },
1426
1476