@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/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
@@ -78,9 +78,39 @@ export interface MemoryEmbeddingProvider {
78
78
  vectors: number[][];
79
79
  }>;
80
80
  }
81
+ export interface MemorySupersessionInput {
82
+ candidate: {
83
+ content: string;
84
+ kind: "preference";
85
+ };
86
+ existingMemories: [
87
+ {
88
+ content: string;
89
+ id: string;
90
+ },
91
+ ...Array<{
92
+ content: string;
93
+ id: string;
94
+ }>
95
+ ];
96
+ runtimeContext: MemoryRuntimeContext;
97
+ }
98
+ export type MemorySupersessionDecision = {
99
+ decision: "supersedes_old";
100
+ supersededIds: [string, ...string[]];
101
+ } | {
102
+ decision: "distinct" | "uncertain";
103
+ };
104
+ export interface MemorySupersessionDecider {
105
+ /** Decide whether a new preference clearly replaces active old preferences. */
106
+ adjudicateSupersession(input: MemorySupersessionInput): Promise<MemorySupersessionDecision> | MemorySupersessionDecision;
107
+ }
81
108
  export interface MemoryStoreOptions {
82
109
  embedder?: MemoryEmbeddingProvider;
110
+ /** Maximum cosine distance for vector recall candidates. Model-dependent; tune when changing AI_EMBEDDING_MODEL. */
111
+ maxVectorDistance?: number;
83
112
  now?: () => number;
113
+ supersessionDecider?: MemorySupersessionDecider;
84
114
  }
85
115
  /** Context-bound storage operations for visible long-term memories. */
86
116
  export interface MemoryStore {
package/dist/tools.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Source, type Requester } from "@sentry/junior-plugin-api";
2
- import { type MemoryEmbeddingProvider, type MemoryDb } from "./store";
2
+ import { type MemoryEmbeddingProvider, type MemoryDb, type MemorySupersessionDecider } from "./store";
3
3
  import { type MemoryAgent } from "./agent";
4
4
  export type MemoryReviewer = Pick<MemoryAgent, "reviewCreateRequest">;
5
5
  /** Runtime-owned context used to bind memory tools to visible scopes. */
@@ -12,12 +12,15 @@ export interface MemoryToolContext {
12
12
  source: Source;
13
13
  userText?: string;
14
14
  }
15
+ export interface MemoryCreateToolContext extends MemoryToolContext {
16
+ supersessionDecider?: MemorySupersessionDecider;
17
+ }
15
18
  type MemoryWriteToolInput = {
16
19
  content: string;
17
20
  expires_at?: string;
18
21
  };
19
22
  /** Create a tool that submits an explicit memory candidate for storage. */
20
- export declare function createMemoryCreateTool(context: MemoryToolContext): {
23
+ export declare function createMemoryCreateTool(context: MemoryCreateToolContext): {
21
24
  description: string;
22
25
  executionMode: string;
23
26
  inputSchema: import("@sinclair/typebox").TObject<{
@@ -31,6 +34,7 @@ export declare function createMemoryCreateTool(context: MemoryToolContext): {
31
34
  expiresAtMs?: number | undefined;
32
35
  id: string;
33
36
  content: string;
37
+ observedAtMs: number;
34
38
  createdAtMs: number;
35
39
  };
36
40
  }>;
@@ -50,6 +54,7 @@ export declare function createMemoryRemoveTool(context: MemoryToolContext): {
50
54
  expiresAtMs?: number | undefined;
51
55
  id: string;
52
56
  content: string;
57
+ observedAtMs: number;
53
58
  createdAtMs: number;
54
59
  };
55
60
  }>;
@@ -72,6 +77,7 @@ export declare function createMemoryListTool(context: MemoryToolContext): {
72
77
  expiresAtMs?: number | undefined;
73
78
  id: string;
74
79
  content: string;
80
+ observedAtMs: number;
75
81
  createdAtMs: number;
76
82
  }[];
77
83
  }>;
@@ -96,6 +102,7 @@ export declare function createMemorySearchTool(context: MemoryToolContext): {
96
102
  expiresAtMs?: number | undefined;
97
103
  id: string;
98
104
  content: string;
105
+ observedAtMs: number;
99
106
  createdAtMs: number;
100
107
  }[];
101
108
  }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-memory",
3
- "version": "0.80.1",
3
+ "version": "0.82.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.80.1"
30
+ "@sentry/junior-plugin-api": "0.82.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^25.9.1",
package/src/agent.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import type { PluginModel } from "@sentry/junior-plugin-api";
2
2
  import { z } from "zod";
3
+ import type {
4
+ MemorySupersessionDecision,
5
+ MemorySupersessionInput,
6
+ } from "./store";
3
7
  import { MEMORY_KINDS, memoryRuntimeContextSchema } from "./types";
4
8
 
5
9
  const memoryKindSchema = z.enum(MEMORY_KINDS);
@@ -62,6 +66,28 @@ const extractSessionRequestSchema = z
62
66
  .min(1),
63
67
  })
64
68
  .strict();
69
+ const supersessionRequestSchema = z
70
+ .object({
71
+ candidate: z
72
+ .object({
73
+ content: z.string().min(1),
74
+ kind: z.literal("preference"),
75
+ })
76
+ .strict(),
77
+ existingMemories: z
78
+ .array(
79
+ z
80
+ .object({
81
+ content: z.string().min(1),
82
+ id: z.string().min(1),
83
+ })
84
+ .strict(),
85
+ )
86
+ .min(1)
87
+ .max(10),
88
+ runtimeContext: memoryRuntimeContextSchema,
89
+ })
90
+ .strict();
65
91
 
66
92
  const expiresAtMsSchema = z
67
93
  .number()
@@ -140,6 +166,19 @@ const extractMemoriesResponseSchema = z
140
166
  ),
141
167
  })
142
168
  .strict();
169
+ const supersessionDecisionResponseSchema = z.discriminatedUnion("decision", [
170
+ z
171
+ .object({
172
+ decision: z.literal("supersedes_old"),
173
+ supersededIds: z.array(z.string().min(1)).min(1).max(10),
174
+ })
175
+ .strict(),
176
+ z
177
+ .object({
178
+ decision: z.enum(["distinct", "uncertain"]),
179
+ })
180
+ .strict(),
181
+ ]);
143
182
 
144
183
  type MemoryReviewResponse = z.output<typeof memoryReviewResponseSchema>;
145
184
  type ExtractMemoriesResponse = z.output<typeof extractMemoriesResponseSchema>;
@@ -153,6 +192,10 @@ export type ExtractSessionRequest = z.output<
153
192
  export type ExtractedMemory = z.output<typeof extractedMemoryResultSchema>;
154
193
 
155
194
  export interface MemoryAgent {
195
+ /** Decide whether a new preference safely replaces active old preferences. */
196
+ adjudicateSupersession(
197
+ request: MemorySupersessionInput,
198
+ ): Promise<MemorySupersessionDecision> | MemorySupersessionDecision;
156
199
  extractSessionMemories(
157
200
  request: ExtractSessionRequest,
158
201
  ): Promise<ExtractedMemory[]> | ExtractedMemory[];
@@ -175,6 +218,12 @@ const MEMORY_EXTRACTION_SYSTEM = [
175
218
  "Reject secrets, credentials, private or sensitive personal details, gossip, speculative claims about other people, assistant/system implementation details, vague references, and low-durability chatter.",
176
219
  "If no public, durable, self-contained memory remains after rewriting, return an empty memories array.",
177
220
  ].join("\n");
221
+ const MEMORY_SUPERSESSION_SYSTEM = [
222
+ "You are Junior's memory supersession agent.",
223
+ "Decide whether a new requester preference clearly replaces existing active requester preferences.",
224
+ "Return supersedes_old only for obvious changed preferences about the same mutable slot.",
225
+ "If the facts are additive, different topics, duplicate, broader/narrower without direct replacement, or uncertain, do not supersede.",
226
+ ].join("\n");
178
227
  const CANONICAL_CONTENT_RULES = [
179
228
  "- Stored memory text must be a rewritten fact, not copied user wording or a sentence about who said it.",
180
229
  "- Store the minimum useful assertion supported by source evidence; do not add adjacent steps, caveats, or generalized advice.",
@@ -355,9 +404,52 @@ function sessionExtractionPrompt(request: ExtractSessionRequest): string {
355
404
  ].join("\n");
356
405
  }
357
406
 
407
+ function supersessionPrompt(request: MemorySupersessionInput): string {
408
+ return [
409
+ "<memory-supersession-input>",
410
+ "Decide whether the candidate preference clearly replaces one or more existing active preferences.",
411
+ "",
412
+ runtimeDescription({
413
+ runtimeContext: request.runtimeContext,
414
+ }),
415
+ "",
416
+ "<candidate>",
417
+ escapeXml(JSON.stringify(request.candidate)),
418
+ "</candidate>",
419
+ "",
420
+ "<existing-memories>",
421
+ escapeXml(JSON.stringify(request.existingMemories)),
422
+ "</existing-memories>",
423
+ "",
424
+ "<rules>",
425
+ "- Return supersedes_old only when the candidate and old memory describe the same mutable preference slot and the candidate is the newer value.",
426
+ "- Examples of same mutable slot: preferred programming language, preferred review style, preferred notification cadence, preferred tool for a task.",
427
+ "- Do not supersede when the candidate is just more specific, an additional preference, a different task/context, or the same value phrased differently.",
428
+ "- Do not supersede memories from different topics even if they are both preferences.",
429
+ "- Only return ids that appear in existing-memories.",
430
+ "- If unsure, return uncertain.",
431
+ "</rules>",
432
+ "</memory-supersession-input>",
433
+ ].join("\n");
434
+ }
435
+
358
436
  /** Create the memory-owned agent that reviews and extracts memory candidates. */
359
437
  export function createMemoryAgent(model: PluginModel): MemoryAgent {
360
438
  return {
439
+ async adjudicateSupersession(rawRequest) {
440
+ const request = supersessionRequestSchema.parse(
441
+ rawRequest,
442
+ ) as MemorySupersessionInput;
443
+ const result = await model.completeObject({
444
+ schema: supersessionDecisionResponseSchema,
445
+ system: MEMORY_SUPERSESSION_SYSTEM,
446
+ prompt: supersessionPrompt(request),
447
+ maxTokens: 400,
448
+ });
449
+ return supersessionDecisionResponseSchema.parse(
450
+ result.object,
451
+ ) as MemorySupersessionDecision;
452
+ },
361
453
  async extractSessionMemories(rawRequest) {
362
454
  const request = extractSessionRequestSchema.parse(rawRequest);
363
455
  const result = await model.completeObject({
package/src/plugin.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  createMemoryListTool,
7
7
  createMemoryRemoveTool,
8
8
  createMemorySearchTool,
9
+ type MemoryCreateToolContext,
9
10
  type MemoryReviewer,
10
11
  type MemoryToolContext,
11
12
  } from "./tools";
@@ -14,9 +15,13 @@ import { createMemoryPromptMessages } from "./recall";
14
15
  import type { MemoryDb } from "./store";
15
16
 
16
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;
17
20
 
18
21
  export interface MemoryPluginOptions {
19
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;
20
25
  }
21
26
 
22
27
  function memoryModelId(options: MemoryPluginOptions): string | undefined {
@@ -28,6 +33,20 @@ function memoryModelId(options: MemoryPluginOptions): string | undefined {
28
33
  return envModelId || undefined;
29
34
  }
30
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
+
31
50
  function memoryToolContext(ctx: {
32
51
  agent: MemoryReviewer;
33
52
  conversationId?: string;
@@ -48,6 +67,22 @@ function memoryToolContext(ctx: {
48
67
  };
49
68
  }
50
69
 
70
+ function memoryCreateToolContext(ctx: {
71
+ agent: MemoryReviewer;
72
+ conversationId?: string;
73
+ db: MemoryCreateToolContext["db"];
74
+ embedder?: MemoryCreateToolContext["embedder"];
75
+ requester?: MemoryCreateToolContext["requester"];
76
+ source: MemoryCreateToolContext["source"];
77
+ supersessionDecider: MemoryCreateToolContext["supersessionDecider"];
78
+ userText?: string;
79
+ }): MemoryCreateToolContext {
80
+ return {
81
+ ...memoryToolContext(ctx),
82
+ supersessionDecider: ctx.supersessionDecider,
83
+ };
84
+ }
85
+
51
86
  /** Create Junior's long-term memory plugin registration. */
52
87
  export function createMemoryPlugin(options: MemoryPluginOptions = {}) {
53
88
  const modelId = memoryModelId(options);
@@ -73,14 +108,23 @@ export function createMemoryPlugin(options: MemoryPluginOptions = {}) {
73
108
  },
74
109
  hooks: {
75
110
  tools(ctx) {
111
+ const agent = createMemoryAgent(ctx.model);
76
112
  const context = memoryToolContext({
77
113
  ...ctx,
78
- agent: createMemoryAgent(ctx.model),
114
+ agent,
79
115
  db: ctx.db as MemoryDb,
80
116
  embedder: ctx.embedder,
81
117
  });
82
118
  return {
83
- createMemory: createMemoryCreateTool(context),
119
+ createMemory: createMemoryCreateTool(
120
+ memoryCreateToolContext({
121
+ ...ctx,
122
+ agent,
123
+ db: ctx.db as MemoryDb,
124
+ embedder: ctx.embedder,
125
+ supersessionDecider: agent,
126
+ }),
127
+ ),
84
128
  removeMemory: createMemoryRemoveTool(context),
85
129
  listMemories: createMemoryListTool(context),
86
130
  searchMemories: createMemorySearchTool(context),
@@ -92,6 +136,7 @@ export function createMemoryPlugin(options: MemoryPluginOptions = {}) {
92
136
  ...(ctx.requester ? { requester: ctx.requester } : {}),
93
137
  db: ctx.db as MemoryDb,
94
138
  embedder: ctx.embedder,
139
+ maxVectorDistance: recallMaxVectorDistance(options),
95
140
  source: ctx.source,
96
141
  text: ctx.text,
97
142
  });
@@ -135,8 +135,10 @@ export async function processMemorySession(
135
135
  ...(run.requester ? { requester: run.requester } : {}),
136
136
  source: run.source,
137
137
  });
138
+ const agent = createMemoryAgent(context.model);
138
139
  const store = createMemoryStore(context.db as MemoryDb, runtimeContext, {
139
140
  embedder: context.embedder,
141
+ supersessionDecider: agent,
140
142
  });
141
143
  await store.archiveExpiredMemories();
142
144
  const memories = await getTaskMemories(context, async () => {
@@ -144,7 +146,6 @@ export async function processMemorySession(
144
146
  limit: 10,
145
147
  query: evidenceText,
146
148
  });
147
- const agent = createMemoryAgent(context.model);
148
149
  return await agent.extractSessionMemories({
149
150
  existingMemories: existingMemories.map((memory) => ({
150
151
  content: memory.content,
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;
@@ -32,6 +34,10 @@ function trimContent(content: string, maxLength: number): string {
32
34
  return `${trimmed.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
33
35
  }
34
36
 
37
+ function formatObservedDate(observedAtMs: number): string {
38
+ return new Date(observedAtMs).toISOString().slice(0, 10);
39
+ }
40
+
35
41
  function renderMemoryPrompt(memories: MemoryRecord[]): string | undefined {
36
42
  const header = "Relevant memories for this request:";
37
43
  const footer =
@@ -40,7 +46,10 @@ function renderMemoryPrompt(memories: MemoryRecord[]): string | undefined {
40
46
  let totalChars = header.length + footer.length + 2;
41
47
 
42
48
  for (const memory of memories) {
43
- const line = `- ${trimContent(memory.content, MAX_MEMORY_LINE_CHARS)}`;
49
+ const line = `- Observed ${formatObservedDate(memory.observedAtMs)}: ${trimContent(
50
+ memory.content,
51
+ MAX_MEMORY_LINE_CHARS,
52
+ )}`;
44
53
  if (totalChars + line.length + 1 > MAX_PROMPT_CHARS) {
45
54
  break;
46
55
  }
@@ -71,7 +80,12 @@ export async function createMemoryPromptMessages(
71
80
  const memories = await createMemoryStore(
72
81
  context.db,
73
82
  runtimeContext,
74
- context.embedder ? { embedder: context.embedder } : {},
83
+ {
84
+ ...(context.embedder ? { embedder: context.embedder } : {}),
85
+ ...(context.maxVectorDistance !== undefined
86
+ ? { maxVectorDistance: context.maxVectorDistance }
87
+ : {}),
88
+ },
75
89
  ).searchMemories({
76
90
  query: context.text,
77
91
  limit: DEFAULT_RECALL_LIMIT,