@sentry/junior-memory 0.180.0 → 0.181.1

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/src/events.ts CHANGED
@@ -3,12 +3,23 @@ import { z } from "zod";
3
3
  import { MEMORY_KINDS, MEMORY_SCOPES } from "./types";
4
4
  import type { MemoryRecord } from "./store";
5
5
 
6
+ const capturedMemoryFields = {
7
+ content: z.string().min(1),
8
+ id: z.string().min(1),
9
+ kind: z.enum(MEMORY_KINDS),
10
+ observedAtMs: z.number().finite(),
11
+ };
12
+
13
+ const legacyCapturedMemorySchema = z
14
+ .object({
15
+ ...capturedMemoryFields,
16
+ scope: z.enum(["personal", "conversation"]),
17
+ })
18
+ .strict();
19
+
6
20
  const capturedMemorySchema = z
7
21
  .object({
8
- content: z.string().min(1),
9
- id: z.string().min(1),
10
- kind: z.enum(MEMORY_KINDS),
11
- observedAtMs: z.number().finite(),
22
+ ...capturedMemoryFields,
12
23
  scope: z.enum(MEMORY_SCOPES),
13
24
  })
14
25
  .strict();
@@ -28,9 +39,20 @@ const recalledMemoriesSchema = z
28
39
  })
29
40
  .strict();
30
41
 
31
- function renderCapturedMemories(
32
- event: z.output<typeof capturedMemoriesSchema>,
42
+ function currentScope(
43
+ scope: "personal" | "conversation" | "private" | "public",
33
44
  ) {
45
+ if (scope === "personal") return "private";
46
+ if (scope === "conversation") return "public";
47
+ return scope;
48
+ }
49
+
50
+ function renderCapturedMemories(event: {
51
+ memories: Array<
52
+ | z.output<typeof legacyCapturedMemorySchema>
53
+ | z.output<typeof capturedMemorySchema>
54
+ >;
55
+ }) {
34
56
  const count = event.memories.length;
35
57
  if (count === 0) return undefined;
36
58
  return {
@@ -38,7 +60,7 @@ function renderCapturedMemories(
38
60
  title: `${count} ${count === 1 ? "memory" : "memories"} captured`,
39
61
  details: event.memories.map((memory) => ({
40
62
  title: memory.content,
41
- metadata: [memory.kind, memory.scope],
63
+ metadata: [memory.kind, currentScope(memory.scope)],
42
64
  })),
43
65
  };
44
66
  }
@@ -49,7 +71,7 @@ export const memoriesCapturedEventV1 = defineConversationEvent({
49
71
  version: 1,
50
72
  schema: z
51
73
  .object({
52
- memories: z.array(capturedMemorySchema).min(1).max(100),
74
+ memories: z.array(legacyCapturedMemorySchema).min(1).max(100),
53
75
  })
54
76
  .strict(),
55
77
  renderEvent: renderCapturedMemories,
@@ -12,9 +12,9 @@ const WINDOWS = [7, 30, 90] as const;
12
12
 
13
13
  const memoryDaySchema = z
14
14
  .object({
15
- conversation: z.number().int().nonnegative(),
16
15
  date: z.string().date(),
17
- personal: z.number().int().nonnegative(),
16
+ private: z.number().int().nonnegative(),
17
+ public: z.number().int().nonnegative(),
18
18
  })
19
19
  .strict();
20
20
 
@@ -55,11 +55,11 @@ async function aggregateMemoryDays(args: { db: MemoryDb; nowMs: number }) {
55
55
  to_timestamp(${table.createdAtMs} / 1000.0) AT TIME ZONE 'UTC'
56
56
  ) AS day,
57
57
  count(*) FILTER (
58
- WHERE ${table.scope} = 'personal'
59
- )::integer AS personal,
58
+ WHERE ${table.scope} = 'private'
59
+ )::integer AS private,
60
60
  count(*) FILTER (
61
- WHERE ${table.scope} = 'conversation'
62
- )::integer AS conversation
61
+ WHERE ${table.scope} = 'public'
62
+ )::integer AS public
63
63
  FROM ${table}
64
64
  WHERE ${table.createdAtMs} >= ${start.getTime()}
65
65
  AND ${table.createdAtMs} < ${endExclusiveMs}
@@ -70,8 +70,8 @@ async function aggregateMemoryDays(args: { db: MemoryDb; nowMs: number }) {
70
70
  )
71
71
  SELECT
72
72
  to_char(days.day, 'YYYY-MM-DD') AS date,
73
- coalesce(daily.personal, 0)::integer AS personal,
74
- coalesce(daily.conversation, 0)::integer AS conversation
73
+ coalesce(daily.private, 0)::integer AS private,
74
+ coalesce(daily.public, 0)::integer AS public
75
75
  FROM days
76
76
  LEFT JOIN daily ON daily.day = days.day
77
77
  ORDER BY days.day
@@ -119,8 +119,8 @@ export async function buildMemoryOperationalReport(args: {
119
119
  args.db
120
120
  .select({
121
121
  active: sql<number>`count(*) filter (where ${active})`.mapWith(Number),
122
- conversation:
123
- sql<number>`count(*) filter (where ${active} and ${juniorMemoryMemories.scope} = 'conversation')`.mapWith(
122
+ public:
123
+ sql<number>`count(*) filter (where ${active} and ${juniorMemoryMemories.scope} = 'public')`.mapWith(
124
124
  Number,
125
125
  ),
126
126
  createdThirtyDays:
@@ -131,8 +131,8 @@ export async function buildMemoryOperationalReport(args: {
131
131
  sql<number>`count(${juniorMemoryEmbeddings.memoryId}) filter (where ${active})`.mapWith(
132
132
  Number,
133
133
  ),
134
- personal:
135
- sql<number>`count(*) filter (where ${active} and ${juniorMemoryMemories.scope} = 'personal')`.mapWith(
134
+ private:
135
+ sql<number>`count(*) filter (where ${active} and ${juniorMemoryMemories.scope} = 'private')`.mapWith(
136
136
  Number,
137
137
  ),
138
138
  })
@@ -171,12 +171,12 @@ export async function buildMemoryOperationalReport(args: {
171
171
  value: formatCount(counts?.createdThirtyDays ?? 0),
172
172
  },
173
173
  {
174
- label: "personal",
175
- value: formatCount(counts?.personal ?? 0),
174
+ label: "private",
175
+ value: formatCount(counts?.private ?? 0),
176
176
  },
177
177
  {
178
- label: "conversation",
179
- value: formatCount(counts?.conversation ?? 0),
178
+ label: "public",
179
+ value: formatCount(counts?.public ?? 0),
180
180
  },
181
181
  {
182
182
  label: "embedding coverage",
@@ -208,15 +208,15 @@ export async function buildMemoryOperationalReport(args: {
208
208
  id: day.date,
209
209
  label: day.date,
210
210
  values: {
211
- conversation: day.conversation,
212
- personal: day.personal,
211
+ private: day.private,
212
+ public: day.public,
213
213
  },
214
214
  })),
215
215
  description: "Memories stored per day by scope",
216
216
  id: "memories-created",
217
217
  series: [
218
- { key: "personal", label: "Personal" },
219
- { key: "conversation", label: "Conversation" },
218
+ { key: "private", label: "Private" },
219
+ { key: "public", label: "Public" },
220
220
  ],
221
221
  timeRangeDays: [...WINDOWS],
222
222
  title: "Memories created",
package/src/plugin.ts CHANGED
@@ -46,17 +46,23 @@ function memoryToolContext(ctx: {
46
46
  conversationId?: string;
47
47
  db: MemoryToolContext["db"];
48
48
  embedder?: MemoryToolContext["embedder"];
49
+ locationId?: string;
49
50
  actor?: MemoryToolContext["actor"];
50
51
  source: MemoryToolContext["source"];
52
+ users: MemoryToolContext["users"];
51
53
  userText?: string;
52
54
  }): MemoryToolContext {
53
55
  return {
54
56
  agent: ctx.agent,
55
- ...(ctx.conversationId ? { conversationId: ctx.conversationId } : undefined),
57
+ ...(ctx.conversationId
58
+ ? { conversationId: ctx.conversationId }
59
+ : undefined),
56
60
  ...(ctx.actor ? { actor: ctx.actor } : undefined),
57
61
  db: ctx.db,
58
62
  ...(ctx.embedder ? { embedder: ctx.embedder } : undefined),
63
+ ...(ctx.locationId ? { locationId: ctx.locationId } : undefined),
59
64
  source: ctx.source,
65
+ users: ctx.users,
60
66
  ...(ctx.userText ? { userText: ctx.userText } : undefined),
61
67
  };
62
68
  }
@@ -66,9 +72,11 @@ function memoryCreateToolContext(ctx: {
66
72
  conversationId?: string;
67
73
  db: MemoryCreateToolContext["db"];
68
74
  embedder?: MemoryCreateToolContext["embedder"];
75
+ locationId?: string;
69
76
  actor?: MemoryCreateToolContext["actor"];
70
77
  source: MemoryCreateToolContext["source"];
71
78
  supersessionDecider: MemoryCreateToolContext["supersessionDecider"];
79
+ users: MemoryCreateToolContext["users"];
72
80
  userText?: string;
73
81
  }): MemoryCreateToolContext {
74
82
  return {
@@ -162,9 +170,13 @@ export function memoryPlugin(options: MemoryPluginOptions = {}) {
162
170
  db: ctx.db as MemoryDb,
163
171
  embedder: ctx.embedder,
164
172
  events: ctx.events,
173
+ ...(ctx.locationId
174
+ ? { locationId: ctx.locationId }
175
+ : undefined),
165
176
  log: ctx.log,
166
177
  source: ctx.source,
167
178
  text: ctx.text,
179
+ users: ctx.users,
168
180
  });
169
181
  },
170
182
  }
@@ -4,7 +4,6 @@ import {
4
4
  type PluginRunContext,
5
5
  type PluginRunTranscriptEntry,
6
6
  type PluginTaskContext,
7
- type Source,
8
7
  } from "@sentry/junior-plugin-api";
9
8
  import { z } from "zod";
10
9
  import {
@@ -54,23 +53,8 @@ const extractedMemoryCacheSchema = z.union([
54
53
  .transform((memories) => ({ memories })),
55
54
  ]);
56
55
 
57
- /** Where a passively extracted memory may be stored, or dropped when unproven. */
58
- type MemoryRouteTarget = "drop" | "personal" | "conversation";
59
-
60
- /**
61
- * V1 passive learning opts in by Source branch, then public vs private.
62
- * Public API is the same as public Slack: shared conversation evidence may
63
- * learn. Private sources stay out. Local remains available for QA.
64
- */
65
- function allowsPassiveMemoryExtraction(source: Source): boolean {
66
- switch (source.platform) {
67
- case "local":
68
- return true;
69
- case "web":
70
- case "slack":
71
- return source.visibility === "public";
72
- }
73
- }
56
+ /** Subject for a passively extracted memory, or drop when unproven. */
57
+ type MemorySubjectTarget = "drop" | "user" | "conversation";
74
58
 
75
59
  function recordCapturedMemory(
76
60
  captured: ReturnType<typeof capturedMemory>[],
@@ -151,8 +135,8 @@ function citedEntries(
151
135
  function routeExtractedMemory(
152
136
  memory: ExtractedMemory,
153
137
  transcript: PluginRunTranscriptEntry[],
154
- run: Pick<PluginRunContext, "actor" | "actors">,
155
- ): MemoryRouteTarget {
138
+ run: Pick<PluginRunContext, "actor" | "actors" | "actorUserId">,
139
+ ): MemorySubjectTarget {
156
140
  const cited = citedEntries(memory.evidenceMessageIndices, transcript);
157
141
  if (!cited.valid) {
158
142
  return "drop";
@@ -160,6 +144,7 @@ function routeExtractedMemory(
160
144
  if (memory.kind === "preference") {
161
145
  // Only a run attributed to exactly one human run actor may store a preference.
162
146
  const exactlyOneHumanRunActor =
147
+ run.actorUserId !== undefined &&
163
148
  run.actor !== undefined &&
164
149
  run.actor.platform !== "system" &&
165
150
  run.actors.length === 1 &&
@@ -167,8 +152,8 @@ function routeExtractedMemory(
167
152
  if (!exactlyOneHumanRunActor) {
168
153
  return "drop";
169
154
  }
170
- // Never downgrade an unproven first-person preference to conversation scope.
171
- return cited.entries.every(isRunActorInstruction) ? "personal" : "drop";
155
+ // Never downgrade an unproven first-person preference to conversation subject.
156
+ return cited.entries.every(isRunActorInstruction) ? "user" : "drop";
172
157
  }
173
158
  return cited.entries.every(
174
159
  (entry) => isRunActorInstruction(entry) || isConversationEvidence(entry),
@@ -179,7 +164,7 @@ function routeExtractedMemory(
179
164
 
180
165
  function memoryIdempotencySuffix(
181
166
  memory: ExtractedMemory,
182
- target: MemoryRouteTarget,
167
+ target: MemorySubjectTarget,
183
168
  ): string {
184
169
  return createHash("sha256")
185
170
  .update(target)
@@ -197,13 +182,15 @@ function passiveInput(
197
182
  sessionId: string,
198
183
  memory: ExtractedMemory,
199
184
  sourceKey: string,
200
- target: MemoryRouteTarget,
185
+ target: MemorySubjectTarget,
201
186
  ): CreateMemoryInput {
202
187
  return {
203
188
  content: memory.content,
204
189
  idempotencyKey: `session:${sourceKey}:${sessionId}:${memoryIdempotencySuffix(memory, target)}`,
205
190
  kind: memory.kind,
206
- ...(memory.expiresAtMs !== null ? { expiresAtMs: memory.expiresAtMs } : undefined),
191
+ ...(memory.expiresAtMs !== null
192
+ ? { expiresAtMs: memory.expiresAtMs }
193
+ : undefined),
207
194
  };
208
195
  }
209
196
 
@@ -228,15 +215,17 @@ async function getTaskExtraction(
228
215
  /**
229
216
  * Extract and store memories from a completed session plugin task.
230
217
  *
231
- * Memory owns post-session extraction and consumes only the bounded plugin task
232
- * projection. Explicit memory tools and private non-local sources remain hard
233
- * boundaries so background retries cannot reinterpret user-directed mutations
234
- * or private conversations.
218
+ * Memory owns learning after a run and reads only the plugin run data.
219
+ * Explicit memory tools stay separate so retries do not reinterpret user
220
+ * requests.
235
221
  */
236
222
  export async function processMemorySession(
237
223
  context: PluginTaskContext,
238
224
  ): Promise<void> {
239
225
  const run = await context.run.load();
226
+ if (run.source.platform === "local") {
227
+ return;
228
+ }
240
229
  // Memory tool turns already own memory management or recall; do not reinterpret
241
230
  // recalled memory output as fresh passive-learning evidence.
242
231
  if (
@@ -247,13 +236,8 @@ export async function processMemorySession(
247
236
  ) {
248
237
  return;
249
238
  }
250
- // V1 passive learning is a Source-branch policy: local QA always, public
251
- // Slack/API by visibility, private sources never.
252
- if (!allowsPassiveMemoryExtraction(run.source)) {
253
- return;
254
- }
255
239
  const sourceKey = getSourceKey(run.source);
256
- if (!sourceKey) {
240
+ if (!sourceKey || (run.source.visibility === "private" && !run.actorUserId)) {
257
241
  return;
258
242
  }
259
243
  const transcript = run.transcript
@@ -270,8 +254,10 @@ export async function processMemorySession(
270
254
 
271
255
  const runtimeContext = memoryRuntimeContextSchema.parse({
272
256
  conversationId: run.conversationId,
257
+ ...(run.locationId ? { locationId: run.locationId } : undefined),
273
258
  ...(run.actor ? { actor: run.actor } : undefined),
274
259
  source: run.source,
260
+ ...(run.actorUserId ? { userId: run.actorUserId } : undefined),
275
261
  });
276
262
  const agent = createMemoryAgent(context.model);
277
263
  const store = createMemoryStore(context.db as MemoryDb, runtimeContext, {
package/src/ranking.ts CHANGED
@@ -9,7 +9,6 @@ export interface MemoryMatch {
9
9
  rank: number;
10
10
  };
11
11
  memory: MemoryRecord;
12
- sourceKey: string;
13
12
  vector?: {
14
13
  rank: number;
15
14
  };
@@ -33,13 +32,6 @@ function matchScore(
33
32
  );
34
33
  }
35
34
 
36
- function currentChannel(
37
- match: Pick<MemoryMatch, "sourceKey">,
38
- channelPrefix: string | undefined,
39
- ): boolean {
40
- return channelPrefix ? match.sourceKey.startsWith(channelPrefix) : false;
41
- }
42
-
43
35
  function observedAgeRank(memory: MemoryRecord, nowMs: number): number {
44
36
  const ageMs = Math.max(0, nowMs - memory.observedAtMs);
45
37
  if (ageMs <= 7 * ONE_DAY_MS) {
@@ -64,7 +56,6 @@ function positiveWeight(value: number | undefined, fallback: number): number {
64
56
  export function rankMemoryMatches(
65
57
  matches: MemoryMatch[],
66
58
  options: {
67
- channelPrefix?: string;
68
59
  /** Optional RRF weight for the lexical leg. Defaults to 1. */
69
60
  lexicalWeight?: number;
70
61
  nowMs: number;
@@ -83,9 +74,8 @@ export function rankMemoryMatches(
83
74
  byId.set(match.memory.id, match);
84
75
  continue;
85
76
  }
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.
77
+ // Keep the first rank from each search. The shared searches run first, so
78
+ // the smaller private searches cannot replace their ranks.
89
79
  byId.set(match.memory.id, {
90
80
  ...existing,
91
81
  ...(!existing.lexical && match.lexical
@@ -99,20 +89,13 @@ export function rankMemoryMatches(
99
89
  if (scoreDelta !== 0) {
100
90
  return scoreDelta;
101
91
  }
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
- }
111
- const channelDelta =
112
- Number(currentChannel(right, options.channelPrefix)) -
113
- Number(currentChannel(left, options.channelPrefix));
114
- if (channelDelta !== 0) {
115
- return channelDelta;
92
+ // Prefer private memory when RRF ties. Public and private searches can give
93
+ // the same rank to common words.
94
+ const privateDelta =
95
+ Number(right.memory.scope === "private") -
96
+ Number(left.memory.scope === "private");
97
+ if (privateDelta !== 0) {
98
+ return privateDelta;
116
99
  }
117
100
  return (
118
101
  observedAgeRank(right.memory, options.nowMs) -
package/src/recall.ts CHANGED
@@ -2,9 +2,11 @@ import {
2
2
  definePromptContext,
3
3
  type UserPromptContribution,
4
4
  type Actor,
5
+ type Identity,
5
6
  type PluginConversationEvents,
6
7
  type PluginLogger,
7
8
  type Source,
9
+ type User,
8
10
  } from "@sentry/junior-plugin-api";
9
11
  import { z } from "zod";
10
12
  import type { MemoryAgent, MemoryRecallResult } from "./agent";
@@ -28,9 +30,13 @@ export interface MemoryRecallContext {
28
30
  embedder?: MemoryEmbeddingProvider;
29
31
  events?: PluginConversationEvents;
30
32
  log: PluginLogger;
33
+ locationId?: string;
31
34
  actor?: Actor;
32
35
  source: Source;
33
36
  text: string;
37
+ users: {
38
+ resolveActor(): Promise<{ identity: Identity; user?: User } | undefined>;
39
+ };
34
40
  }
35
41
 
36
42
  function trimContent(content: string, maxLength: number): string {
@@ -50,6 +56,7 @@ const recalledMemorySchema = z
50
56
  id: z.string().min(1),
51
57
  content: z.string().min(1).max(MAX_MEMORY_LINE_CHARS),
52
58
  observedAtMs: z.number().finite(),
59
+ // Stored version 1 uses the old scope labels. Prompt rendering ignores them.
53
60
  scope: z.enum(["personal", "conversation"]),
54
61
  kind: z.enum(["preference", "procedure", "knowledge"]),
55
62
  })
@@ -82,7 +89,7 @@ function selectPromptMemories(memories: MemoryRecord[]): RecalledMemory[] {
82
89
  id: memory.id,
83
90
  content,
84
91
  observedAtMs: memory.observedAtMs,
85
- scope: memory.scope,
92
+ scope: memory.scope === "private" ? "personal" : "conversation",
86
93
  kind: memory.kind,
87
94
  });
88
95
  totalChars += line.length + 1;
@@ -138,12 +145,15 @@ export async function createMemoryPromptContributions(
138
145
  if (!context.text.trim()) {
139
146
  return undefined;
140
147
  }
148
+ const actorUser = (await context.users.resolveActor())?.user;
141
149
  const runtimeContext = memoryRuntimeContextSchema.parse({
142
150
  ...(context.conversationId
143
151
  ? { conversationId: context.conversationId }
144
152
  : undefined),
145
153
  ...(context.actor ? { actor: context.actor } : undefined),
154
+ ...(context.locationId ? { locationId: context.locationId } : undefined),
146
155
  source: context.source,
156
+ ...(actorUser ? { userId: actorUser.id } : undefined),
147
157
  });
148
158
  let embeddingCostUsd: number | undefined;
149
159
  const sourceEmbedder = context.embedder;