@sentry/junior-memory 0.122.1 → 0.123.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/store.d.ts CHANGED
@@ -123,7 +123,11 @@ export type CreateMemoryInput = z.output<typeof createMemoryInputSchema>;
123
123
  /** Result of a memory write after idempotency checks. */
124
124
  export interface CreateMemoryResult {
125
125
  created: boolean;
126
+ /** True when this call found the memory previously written for the same input identity. */
127
+ idempotent?: true;
126
128
  memory: MemoryRecord;
129
+ /** Memory ids made inactive by this write. */
130
+ supersededIds?: string[];
127
131
  }
128
132
  export type ListMemoriesInput = z.output<typeof listMemoriesInputSchema>;
129
133
  export type SearchMemoriesInput = z.output<typeof searchMemoriesInputSchema>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-memory",
3
- "version": "0.122.1",
3
+ "version": "0.123.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.122.1"
30
+ "@sentry/junior-plugin-api": "0.123.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^25.9.1",
package/src/events.ts ADDED
@@ -0,0 +1,59 @@
1
+ import { defineConversationEvent } from "@sentry/junior-plugin-api";
2
+ import { z } from "zod";
3
+ import { MEMORY_KINDS, MEMORY_SCOPES } from "./types";
4
+ import type { MemoryRecord } from "./store";
5
+
6
+ const MAX_EVENT_PREVIEW_CHARS = 500;
7
+
8
+ const capturedMemorySchema = z
9
+ .object({
10
+ content: z.string().min(1),
11
+ id: z.string().min(1),
12
+ kind: z.enum(MEMORY_KINDS),
13
+ observedAtMs: z.number().finite(),
14
+ scope: z.enum(MEMORY_SCOPES),
15
+ })
16
+ .strict();
17
+
18
+ function eventPreview(content: string): string {
19
+ const trimmed = content.trim();
20
+ if (trimmed.length <= MAX_EVENT_PREVIEW_CHARS) return trimmed;
21
+ return `${trimmed.slice(0, MAX_EVENT_PREVIEW_CHARS - 3).trimEnd()}...`;
22
+ }
23
+
24
+ /** Durable transcript event emitted after background memory capture. */
25
+ export const memoriesCapturedEvent = defineConversationEvent({
26
+ name: "memories_captured",
27
+ version: 1,
28
+ schema: z
29
+ .object({
30
+ memories: z.array(capturedMemorySchema).min(1).max(100),
31
+ })
32
+ .strict(),
33
+ renderEvent(event) {
34
+ const count = event.memories.length;
35
+ return {
36
+ icon: "brain",
37
+ title: count === 1 ? "Memory captured" : "Memories captured",
38
+ preview:
39
+ count === 1
40
+ ? eventPreview(event.memories[0]!.content)
41
+ : `${count} memories`,
42
+ details: event.memories.map((memory) => ({
43
+ title: memory.content,
44
+ metadata: [memory.kind, memory.scope],
45
+ })),
46
+ };
47
+ },
48
+ });
49
+
50
+ /** Select the stable, safe memory fields retained in conversation history. */
51
+ export function capturedMemory(memory: MemoryRecord) {
52
+ return {
53
+ content: memory.content,
54
+ id: memory.id,
55
+ kind: memory.kind,
56
+ observedAtMs: memory.observedAtMs,
57
+ scope: memory.scope,
58
+ };
59
+ }
package/src/plugin.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  import { processMemorySession } from "./process-session";
15
15
  import { createMemoryPromptContributions } from "./recall";
16
16
  import { buildMemoryOperationalReport } from "./operational-report";
17
+ import { memoriesCapturedEvent } from "./events";
17
18
  import type { MemoryDb } from "./store";
18
19
  import { createMemoryUserPage } from "./user-pages";
19
20
 
@@ -100,6 +101,7 @@ export function memoryPlugin(options: MemoryPluginOptions = {}) {
100
101
  ? { structuredModelId: modelId }
101
102
  : { structuredModel: "default" },
102
103
  packageName: "@sentry/junior-memory",
104
+ conversationEvents: [memoriesCapturedEvent],
103
105
  cli: {
104
106
  commands: [createMemoryCliCommand()],
105
107
  },
@@ -10,6 +10,7 @@ import { z } from "zod";
10
10
  import {
11
11
  createMemoryStore,
12
12
  type CreateMemoryInput,
13
+ type CreateMemoryResult,
13
14
  type MemoryDb,
14
15
  } from "./store";
15
16
  import {
@@ -18,6 +19,7 @@ import {
18
19
  type ExtractedMemory,
19
20
  } from "./agent";
20
21
  import { MEMORY_KINDS, memoryRuntimeContextSchema } from "./types";
22
+ import { capturedMemory, memoriesCapturedEvent } from "./events";
21
23
 
22
24
  const MEMORY_TOOL_NAMES = new Set([
23
25
  "createMemory",
@@ -44,6 +46,21 @@ const extractedMemoryCacheSchema = z.array(
44
46
  /** Where a passively extracted memory may be stored, or dropped when unproven. */
45
47
  type MemoryRouteTarget = "drop" | "personal" | "conversation";
46
48
 
49
+ function recordCapturedMemory(
50
+ captured: ReturnType<typeof capturedMemory>[],
51
+ result: CreateMemoryResult,
52
+ ): void {
53
+ const supersededIds = new Set(result.supersededIds ?? []);
54
+ for (let index = captured.length - 1; index >= 0; index -= 1) {
55
+ if (supersededIds.has(captured[index]!.id)) {
56
+ captured.splice(index, 1);
57
+ }
58
+ }
59
+ if (result.created || result.idempotent) {
60
+ captured.push(capturedMemory(result.memory));
61
+ }
62
+ }
63
+
47
64
  /** A cited entry is a run-actor durable instruction evidence entry. */
48
65
  function isRunActorInstruction(entry: PluginRunTranscriptEntry): boolean {
49
66
  return (
@@ -252,6 +269,7 @@ export async function processMemorySession(
252
269
  return;
253
270
  }
254
271
 
272
+ const captured: ReturnType<typeof capturedMemory>[] = [];
255
273
  for (const memory of memories) {
256
274
  // The routing gate stays even though extraction is also actor-gated:
257
275
  // getTaskMemories caches extraction output for 7 days, so a retry can replay
@@ -262,9 +280,14 @@ export async function processMemorySession(
262
280
  }
263
281
  const input = passiveInput(run.runId, memory, sourceKey, target);
264
282
  if (target === "conversation") {
265
- await store.createConversationMemory(input);
283
+ const result = await store.createConversationMemory(input);
284
+ recordCapturedMemory(captured, result);
266
285
  continue;
267
286
  }
268
- await store.createMemory(input);
287
+ const result = await store.createMemory(input);
288
+ recordCapturedMemory(captured, result);
289
+ }
290
+ if (captured.length > 0) {
291
+ await context.events.emit(memoriesCapturedEvent({ memories: captured }));
269
292
  }
270
293
  }
package/src/store.ts CHANGED
@@ -248,7 +248,11 @@ export type CreateMemoryInput = z.output<typeof createMemoryInputSchema>;
248
248
  /** Result of a memory write after idempotency checks. */
249
249
  export interface CreateMemoryResult {
250
250
  created: boolean;
251
+ /** True when this call found the memory previously written for the same input identity. */
252
+ idempotent?: true;
251
253
  memory: MemoryRecord;
254
+ /** Memory ids made inactive by this write. */
255
+ supersededIds?: string[];
252
256
  }
253
257
 
254
258
  export type ListMemoriesInput = z.output<typeof listMemoriesInputSchema>;
@@ -441,12 +445,17 @@ export function activeVisiblePredicate(args: {
441
445
  }
442
446
 
443
447
  /** Resolve retry attempts for the same scoped write idempotency key. */
448
+ interface IdempotencyMatch {
449
+ memory: MemoryRecord;
450
+ outcome: "created" | "duplicate";
451
+ }
452
+
444
453
  async function findByIdempotencyKey(args: {
445
454
  db: MemoryDb;
446
455
  idempotencyKey: string;
447
456
  nowMs: number;
448
457
  scope: ResolvedMemoryScope;
449
- }): Promise<MemoryRecord | undefined> {
458
+ }): Promise<IdempotencyMatch | undefined> {
450
459
  const activeRows = await args.db
451
460
  .select()
452
461
  .from(juniorMemoryMemories)
@@ -466,7 +475,7 @@ async function findByIdempotencyKey(args: {
466
475
  )
467
476
  .limit(1);
468
477
  if (activeRows[0]) {
469
- return parseMemoryRow(activeRows[0]);
478
+ return { memory: parseMemoryRow(activeRows[0]), outcome: "created" };
470
479
  }
471
480
 
472
481
  const aliasRows = await args.db
@@ -513,7 +522,7 @@ async function findByIdempotencyKey(args: {
513
522
  )
514
523
  .limit(1);
515
524
  if (rows[0]) {
516
- return parseMemoryRow(rows[0]);
525
+ return { memory: parseMemoryRow(rows[0]), outcome: "duplicate" };
517
526
  }
518
527
  }
519
528
  return undefined;
@@ -1183,13 +1192,15 @@ export function createMemoryStore(
1183
1192
  });
1184
1193
  if (idempotent) {
1185
1194
  await storeEmbedding({
1186
- content: idempotent.content,
1195
+ content: idempotent.memory.content,
1187
1196
  db,
1188
1197
  embedder,
1189
- memoryId: idempotent.id,
1198
+ memoryId: idempotent.memory.id,
1190
1199
  nowMs,
1191
1200
  });
1192
- return { created: false, memory: idempotent };
1201
+ return idempotent.outcome === "created"
1202
+ ? { created: false, idempotent: true, memory: idempotent.memory }
1203
+ : { created: false, memory: idempotent.memory };
1193
1204
  }
1194
1205
  }
1195
1206
 
@@ -1256,7 +1267,7 @@ export function createMemoryStore(
1256
1267
  }
1257
1268
 
1258
1269
  const id = randomUUID();
1259
- const rows = await db.transaction(async (tx) => {
1270
+ const write = await db.transaction(async (tx) => {
1260
1271
  const inserted = await tx
1261
1272
  .insert(juniorMemoryMemories)
1262
1273
  .values({
@@ -1285,7 +1296,7 @@ export function createMemoryStore(
1285
1296
  .returning();
1286
1297
  const insertedMemory = inserted[0];
1287
1298
  if (!insertedMemory || supersededIds.length === 0) {
1288
- return inserted;
1299
+ return { inserted, supersededIds: [] };
1289
1300
  }
1290
1301
  const superseded = await tx
1291
1302
  .update(juniorMemoryMemories)
@@ -1311,10 +1322,10 @@ export function createMemoryStore(
1311
1322
  .delete(juniorMemoryEmbeddings)
1312
1323
  .where(inArray(juniorMemoryEmbeddings.memoryId, idsToClean));
1313
1324
  }
1314
- return inserted;
1325
+ return { inserted, supersededIds: idsToClean };
1315
1326
  });
1316
- if (rows[0]) {
1317
- const memory = parseMemoryRow(rows[0]);
1327
+ if (write.inserted[0]) {
1328
+ const memory = parseMemoryRow(write.inserted[0]);
1318
1329
  await storeEmbedding({
1319
1330
  content: memory.content,
1320
1331
  db,
@@ -1323,7 +1334,13 @@ export function createMemoryStore(
1323
1334
  memoryId: memory.id,
1324
1335
  nowMs,
1325
1336
  });
1326
- return { created: true, memory };
1337
+ return {
1338
+ created: true,
1339
+ memory,
1340
+ ...(write.supersededIds.length > 0
1341
+ ? { supersededIds: write.supersededIds }
1342
+ : {}),
1343
+ };
1327
1344
  }
1328
1345
 
1329
1346
  const idempotent = await findByIdempotencyKey({
@@ -1336,13 +1353,15 @@ export function createMemoryStore(
1336
1353
  throw new Error("Memory idempotency conflict did not resolve.");
1337
1354
  }
1338
1355
  await storeEmbedding({
1339
- content: idempotent.content,
1356
+ content: idempotent.memory.content,
1340
1357
  db,
1341
1358
  embedder,
1342
- memoryId: idempotent.id,
1359
+ memoryId: idempotent.memory.id,
1343
1360
  nowMs,
1344
1361
  });
1345
- return { created: false, memory: idempotent };
1362
+ return idempotent.outcome === "created"
1363
+ ? { created: false, idempotent: true, memory: idempotent.memory }
1364
+ : { created: false, memory: idempotent.memory };
1346
1365
  }
1347
1366
 
1348
1367
  async function retrieveVisibleMemories(
package/src/tools.ts CHANGED
@@ -420,6 +420,12 @@ function memoryToolResult<TData extends Record<string, unknown>>(
420
420
  /** Create a tool that submits an explicit memory candidate for storage. */
421
421
  export function createMemoryCreateTool(context: MemoryCreateToolContext) {
422
422
  return definePluginTool({
423
+ annotations: {
424
+ destructiveHint: false,
425
+ idempotentHint: true,
426
+ openWorldHint: false,
427
+ readOnlyHint: false,
428
+ },
423
429
  description:
424
430
  "Explicit memory-write tool. Use only when the latest user message directly asks Junior to remember, store, save, or forget-and-replace a public/shareable fact. Do not use for ordinary statements like 'I prefer X', 'I use Y', or 'X goes before Y' unless the user also asks you to remember/store/save it; passive memory learning handles those after the visible reply. Pass one self-contained natural-language candidate preserving the user's explicit memory intent. Do not ask the user to rephrase ordinary first-person facts, and do not rewrite them into display-name or third-person wording. Do not include secrets, private personal details, medical/legal/financial/sensitive facts, or another person's personal preference, opinion, habit, identity, relationship, workflow, or private life. Runtime context derives actor, scope, source, and subject ids; the memory agent decides canonical stored content and memory kind, then the plugin derives storage target from kind.",
425
431
  executionMode: "sequential",
@@ -506,6 +512,12 @@ export function createMemoryCreateTool(context: MemoryCreateToolContext) {
506
512
  /** Create a tool that archives a visible memory in the active context. */
507
513
  export function createMemoryRemoveTool(context: MemoryToolContext) {
508
514
  return definePluginTool({
515
+ annotations: {
516
+ destructiveHint: true,
517
+ idempotentHint: true,
518
+ openWorldHint: false,
519
+ readOnlyHint: false,
520
+ },
509
521
  description:
510
522
  "Forget one memory visible in the active context. Use only ids or short id prefixes returned by listMemories or searchMemories. Never remove memories by hidden actor, Slack, scope, or subject identifiers.",
511
523
  executionMode: "sequential",
@@ -535,7 +547,12 @@ export function createMemoryListTool(context: MemoryToolContext) {
535
547
  return definePluginTool({
536
548
  description:
537
549
  "List active memories visible in the current context. Use when the user asks what Junior remembers or when memory ids are needed before removing a memory.",
538
- annotations: { readOnlyHint: true, destructiveHint: false },
550
+ annotations: {
551
+ destructiveHint: false,
552
+ idempotentHint: true,
553
+ openWorldHint: false,
554
+ readOnlyHint: true,
555
+ },
539
556
  inputSchema: listMemoriesInputSchema,
540
557
  outputSchema: memoryManyOutputSchema,
541
558
  execute: async (input) => {
@@ -555,7 +572,12 @@ export function createMemorySearchTool(context: MemoryToolContext) {
555
572
  return definePluginTool({
556
573
  description:
557
574
  "Search active memories visible in the current context. Use when the model needs targeted memory recall. The tool searches only the current actor and active conversation scopes.",
558
- annotations: { readOnlyHint: true, destructiveHint: false },
575
+ annotations: {
576
+ destructiveHint: false,
577
+ idempotentHint: true,
578
+ openWorldHint: false,
579
+ readOnlyHint: true,
580
+ },
559
581
  inputSchema: searchMemoriesInputSchema,
560
582
  outputSchema: memoryManyOutputSchema,
561
583
  execute: async (input) => {