@sentry/junior-memory 0.78.0 → 0.80.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/src/index.ts CHANGED
@@ -13,4 +13,5 @@ export type {
13
13
  MemoryStoreOptions,
14
14
  SearchMemoriesInput,
15
15
  } from "./store";
16
- export type { MemoryRuntimeContext } from "./types";
16
+ export { MEMORY_KINDS } from "./types";
17
+ export type { MemoryKind, MemoryRuntimeContext } from "./types";
@@ -10,8 +10,16 @@ import {
10
10
  type CreateMemoryInput,
11
11
  type MemoryDb,
12
12
  } from "./store";
13
- import { createMemoryAgent, type ExtractedMemory } from "./agent";
14
- import { memoryRuntimeContextSchema } from "./types";
13
+ import {
14
+ createMemoryAgent,
15
+ parseExtractedMemory,
16
+ type ExtractedMemory,
17
+ } from "./agent";
18
+ import {
19
+ MEMORY_KINDS,
20
+ memoryRuntimeContextSchema,
21
+ type MemoryKind,
22
+ } from "./types";
15
23
 
16
24
  const MEMORY_TOOL_NAMES = new Set([
17
25
  "createMemory",
@@ -25,14 +33,24 @@ const extractedMemoryCacheSchema = z.array(
25
33
  .object({
26
34
  content: z.string().min(1),
27
35
  expiresAtMs: z.number().finite().nullable(),
28
- target: z.enum(["requester", "conversation"]),
36
+ kind: z.enum(MEMORY_KINDS),
29
37
  })
30
- .strict(),
38
+ .strict()
39
+ .transform(parseExtractedMemory),
31
40
  );
32
41
 
42
+ function targetForKind(kind: MemoryKind): "requester" | "conversation" {
43
+ if (kind === "preference") {
44
+ return "requester";
45
+ }
46
+ return "conversation";
47
+ }
48
+
33
49
  function memoryIdempotencySuffix(memory: ExtractedMemory): string {
34
50
  return createHash("sha256")
35
- .update(memory.target)
51
+ .update(targetForKind(memory.kind))
52
+ .update("\0")
53
+ .update(memory.kind)
36
54
  .update("\0")
37
55
  .update(memory.content)
38
56
  .update("\0")
@@ -49,6 +67,7 @@ function passiveInput(
49
67
  return {
50
68
  content: memory.content,
51
69
  idempotencyKey: `session:${sourceKey}:${sessionId}:${memoryIdempotencySuffix(memory)}`,
70
+ kind: memory.kind,
52
71
  ...(memory.expiresAtMs !== null ? { expiresAtMs: memory.expiresAtMs } : {}),
53
72
  };
54
73
  }
@@ -119,6 +138,7 @@ export async function processMemorySession(
119
138
  const store = createMemoryStore(context.db as MemoryDb, runtimeContext, {
120
139
  embedder: context.embedder,
121
140
  });
141
+ await store.archiveExpiredMemories();
122
142
  const memories = await getTaskMemories(context, async () => {
123
143
  const existingMemories = await store.searchMemories({
124
144
  limit: 10,
@@ -139,7 +159,7 @@ export async function processMemorySession(
139
159
 
140
160
  for (const memory of memories) {
141
161
  const input = passiveInput(run.runId, memory, sourceKey);
142
- if (memory.target === "conversation") {
162
+ if (targetForKind(memory.kind) === "conversation") {
143
163
  await store.createConversationMemory(input);
144
164
  continue;
145
165
  }
package/src/store.ts CHANGED
@@ -13,8 +13,10 @@ import {
13
13
  eq,
14
14
  gt,
15
15
  ilike,
16
+ inArray,
16
17
  isNull,
17
18
  like,
19
+ lte,
18
20
  or,
19
21
  sql,
20
22
  type SQL,
@@ -30,7 +32,7 @@ import {
30
32
  MEMORY_SCOPES,
31
33
  MEMORY_SOURCE_PLATFORMS,
32
34
  MEMORY_SUBJECT_TYPES,
33
- MEMORY_TYPES,
35
+ MEMORY_KINDS,
34
36
  memoryRuntimeContextSchema,
35
37
  type MemoryRuntimeContext,
36
38
  type MemoryScope,
@@ -44,6 +46,7 @@ import {
44
46
 
45
47
  const DEFAULT_LIST_LIMIT = 50;
46
48
  const DEFAULT_SEARCH_LIMIT = 10;
49
+ const DEFAULT_EXPIRED_ARCHIVE_LIMIT = 100;
47
50
  const VECTOR_SEARCH_OVERFETCH = 4;
48
51
  const MAX_MEMORY_CONTENT_CHARS = 4_000;
49
52
  const EMBEDDING_METRIC = "cosine";
@@ -67,6 +70,7 @@ const createMemoryInputSchema = z
67
70
  content: memoryContentSchema,
68
71
  expiresAtMs: numberSchema.optional(),
69
72
  idempotencyKey: nonEmptyStringSchema,
73
+ kind: z.enum(MEMORY_KINDS),
70
74
  })
71
75
  .strict();
72
76
  const listMemoriesInputSchema = z
@@ -86,6 +90,11 @@ const archiveMemoryInputSchema = z
86
90
  reason: nonEmptyStringSchema.optional(),
87
91
  })
88
92
  .strict();
93
+ const archiveExpiredMemoriesInputSchema = z
94
+ .object({
95
+ limit: numberSchema.optional(),
96
+ })
97
+ .strict();
89
98
  const clockSchema = z.function({ input: [], output: numberSchema }).optional();
90
99
  const memoryStoreOptionsSchema = z
91
100
  .object({
@@ -122,7 +131,7 @@ const memoryRowSchema = z
122
131
  subjectType: z.enum(MEMORY_SUBJECT_TYPES),
123
132
  supersededAtMs: optionalNumberSchema,
124
133
  supersededById: optionalStringSchema,
125
- type: z.enum(MEMORY_TYPES),
134
+ kind: z.enum(MEMORY_KINDS),
126
135
  })
127
136
  .strict()
128
137
  .superRefine((row, ctx) => {
@@ -158,7 +167,7 @@ const memoryRecordSchema = z
158
167
  subjectType: z.enum(MEMORY_SUBJECT_TYPES),
159
168
  supersededAtMs: numberSchema.optional(),
160
169
  supersededById: nonEmptyStringSchema.optional(),
161
- type: z.enum(MEMORY_TYPES),
170
+ kind: z.enum(MEMORY_KINDS),
162
171
  })
163
172
  .strict();
164
173
  const embeddingVectorSchema = z
@@ -188,6 +197,14 @@ export type SearchMemoriesInput = z.output<typeof searchMemoriesInputSchema>;
188
197
 
189
198
  export type ArchiveMemoryInput = z.output<typeof archiveMemoryInputSchema>;
190
199
 
200
+ export type ArchiveExpiredMemoriesInput = z.output<
201
+ typeof archiveExpiredMemoriesInputSchema
202
+ >;
203
+
204
+ export interface ArchiveExpiredMemoriesResult {
205
+ archivedCount: number;
206
+ }
207
+
191
208
  export interface MemoryEmbeddingProvider {
192
209
  /** Embed normalized memory text for derived vector retrieval. */
193
210
  embedTexts(input: { texts: string[] }): Promise<{
@@ -205,6 +222,10 @@ export interface MemoryStoreOptions {
205
222
 
206
223
  /** Context-bound storage operations for visible long-term memories. */
207
224
  export interface MemoryStore {
225
+ /** Archive expired memories visible in the current runtime context. */
226
+ archiveExpiredMemories(
227
+ input?: ArchiveExpiredMemoriesInput,
228
+ ): Promise<ArchiveExpiredMemoriesResult>;
208
229
  /** Archive a visible memory in the current runtime context. */
209
230
  archiveMemory(input: ArchiveMemoryInput): Promise<MemoryRecord>;
210
231
  /** Store a personal memory for the current requester. */
@@ -254,7 +275,7 @@ function parseMemoryRow(row: unknown): MemoryRecord {
254
275
  return memoryRecordSchema.parse({
255
276
  id: parsed.id,
256
277
  scope: parsed.scope,
257
- type: parsed.type,
278
+ kind: parsed.kind,
258
279
  subjectType: parsed.subjectType,
259
280
  content: parsed.content,
260
281
  observedAtMs: parsed.observedAtMs,
@@ -312,6 +333,7 @@ function activeVisiblePredicate(args: {
312
333
  async function findByIdempotencyKey(args: {
313
334
  db: MemoryDb;
314
335
  idempotencyKey: string;
336
+ nowMs: number;
315
337
  scope: ResolvedMemoryScope;
316
338
  }): Promise<MemoryRecord | undefined> {
317
339
  const rows = await args.db
@@ -325,12 +347,77 @@ async function findByIdempotencyKey(args: {
325
347
  isNull(juniorMemoryMemories.archivedAtMs),
326
348
  isNull(juniorMemoryMemories.supersededAtMs),
327
349
  isNull(juniorMemoryMemories.supersededById),
350
+ or(
351
+ isNull(juniorMemoryMemories.expiresAtMs),
352
+ gt(juniorMemoryMemories.expiresAtMs, args.nowMs),
353
+ ),
328
354
  ),
329
355
  )
330
356
  .limit(1);
331
357
  return rows[0] ? parseMemoryRow(rows[0]) : undefined;
332
358
  }
333
359
 
360
+ /**
361
+ * Archive a bounded batch of expired active rows and remove their derived vectors.
362
+ */
363
+ async function archiveExpiredMemoryBatch(args: {
364
+ db: MemoryDb;
365
+ idempotencyKey?: string;
366
+ limit?: number;
367
+ nowMs: number;
368
+ scopes: ResolvedMemoryScope[];
369
+ }): Promise<ArchiveExpiredMemoriesResult> {
370
+ const scopePredicate = visibleScopePredicate(args.scopes);
371
+ if (!scopePredicate) {
372
+ return { archivedCount: 0 };
373
+ }
374
+ const predicates: SQL[] = [
375
+ scopePredicate,
376
+ isNull(juniorMemoryMemories.archivedAtMs),
377
+ isNull(juniorMemoryMemories.supersededAtMs),
378
+ isNull(juniorMemoryMemories.supersededById),
379
+ lte(juniorMemoryMemories.expiresAtMs, args.nowMs),
380
+ ];
381
+ if (args.idempotencyKey !== undefined) {
382
+ predicates.push(
383
+ eq(juniorMemoryMemories.idempotencyKey, args.idempotencyKey),
384
+ );
385
+ }
386
+
387
+ const archivedIds = await args.db.transaction(async (tx) => {
388
+ const expired = await tx
389
+ .select({ id: juniorMemoryMemories.id })
390
+ .from(juniorMemoryMemories)
391
+ .where(and(...predicates))
392
+ .orderBy(
393
+ asc(juniorMemoryMemories.expiresAtMs),
394
+ asc(juniorMemoryMemories.id),
395
+ )
396
+ .limit(boundedLimit(args.limit, DEFAULT_EXPIRED_ARCHIVE_LIMIT));
397
+ const ids = expired.map((row) => row.id);
398
+ if (ids.length === 0) {
399
+ return [];
400
+ }
401
+
402
+ const archived = await tx
403
+ .update(juniorMemoryMemories)
404
+ .set({
405
+ archivedAtMs: args.nowMs,
406
+ archiveReason: "expired",
407
+ })
408
+ .where(and(inArray(juniorMemoryMemories.id, ids), ...predicates))
409
+ .returning({ id: juniorMemoryMemories.id });
410
+ const idsToClean = archived.map((row) => row.id);
411
+ if (idsToClean.length > 0) {
412
+ await tx
413
+ .delete(juniorMemoryEmbeddings)
414
+ .where(inArray(juniorMemoryEmbeddings.memoryId, idsToClean));
415
+ }
416
+ return idsToClean;
417
+ });
418
+ return { archivedCount: archivedIds.length };
419
+ }
420
+
334
421
  function searchScore(memory: MemoryRecord, terms: string[]): number {
335
422
  const haystack = memory.content.toLowerCase();
336
423
  // Lexical score is a retrieval fallback signal, not a memory policy decision.
@@ -591,6 +678,19 @@ export function createMemoryStore(
591
678
  const embedder = options.embedder;
592
679
  const getNowMs = parsedOptions.now ?? Date.now;
593
680
 
681
+ async function archiveExpiredVisibleMemories(
682
+ input: ArchiveExpiredMemoriesInput | undefined,
683
+ nowMs: number,
684
+ ): Promise<ArchiveExpiredMemoriesResult> {
685
+ input = archiveExpiredMemoriesInputSchema.parse(input ?? {});
686
+ return await archiveExpiredMemoryBatch({
687
+ db,
688
+ limit: input.limit,
689
+ nowMs,
690
+ scopes: deriveVisibleMemoryScopes(runtimeContext),
691
+ });
692
+ }
693
+
594
694
  /** Persist a memory under the plugin-derived scope and subject. */
595
695
  async function createScopedMemory(
596
696
  rawInput: CreateMemoryInput,
@@ -604,6 +704,18 @@ export function createMemoryStore(
604
704
  if (content.length > MAX_MEMORY_CONTENT_CHARS) {
605
705
  throw new Error("Memory content exceeds the maximum length.");
606
706
  }
707
+ await archiveExpiredMemoryBatch({
708
+ db,
709
+ nowMs,
710
+ scopes: [scope],
711
+ });
712
+ await archiveExpiredMemoryBatch({
713
+ db,
714
+ idempotencyKey: input.idempotencyKey,
715
+ limit: 1,
716
+ nowMs,
717
+ scopes: [scope],
718
+ });
607
719
 
608
720
  const id = randomUUID();
609
721
  const rows = await db
@@ -621,7 +733,7 @@ export function createMemoryStore(
621
733
  sourcePlatform: runtimeContext.source.platform,
622
734
  subjectKey: subject.subjectKey,
623
735
  subjectType: subject.subjectType,
624
- type: "knowledge",
736
+ kind: input.kind,
625
737
  })
626
738
  .onConflictDoNothing({
627
739
  target: [
@@ -647,6 +759,7 @@ export function createMemoryStore(
647
759
  const idempotent = await findByIdempotencyKey({
648
760
  db,
649
761
  idempotencyKey: input.idempotencyKey,
762
+ nowMs,
650
763
  scope,
651
764
  });
652
765
  if (!idempotent) {
@@ -663,6 +776,10 @@ export function createMemoryStore(
663
776
  }
664
777
 
665
778
  return {
779
+ async archiveExpiredMemories(input) {
780
+ return await archiveExpiredVisibleMemories(input, getNowMs());
781
+ },
782
+
666
783
  async createMemory(input) {
667
784
  return await createScopedMemory(input, "personal");
668
785
  },
@@ -675,6 +792,11 @@ export function createMemoryStore(
675
792
  input = listMemoriesInputSchema.parse(input);
676
793
  const nowMs = getNowMs();
677
794
  const scopes = deriveVisibleMemoryScopes(runtimeContext);
795
+ await archiveExpiredMemoryBatch({
796
+ db,
797
+ nowMs,
798
+ scopes,
799
+ });
678
800
  return await listVisibleMemories({
679
801
  db,
680
802
  limit: input.limit,
@@ -687,6 +809,11 @@ export function createMemoryStore(
687
809
  input = searchMemoriesInputSchema.parse(input);
688
810
  const nowMs = getNowMs();
689
811
  const scopes = deriveVisibleMemoryScopes(runtimeContext);
812
+ await archiveExpiredMemoryBatch({
813
+ db,
814
+ nowMs,
815
+ scopes,
816
+ });
690
817
  const limit = boundedLimit(input.limit, DEFAULT_SEARCH_LIMIT);
691
818
  const vectorCandidates = await searchVisibleVectorMemories({
692
819
  db,
package/src/tools.ts CHANGED
@@ -19,7 +19,11 @@ import {
19
19
  parseMemoryReview,
20
20
  type MemoryAgent,
21
21
  } from "./agent";
22
- import { memoryRuntimeContextSchema, type MemoryRuntimeContext } from "./types";
22
+ import {
23
+ memoryRuntimeContextSchema,
24
+ type MemoryKind,
25
+ type MemoryRuntimeContext,
26
+ } from "./types";
23
27
 
24
28
  export type MemoryReviewer = Pick<MemoryAgent, "reviewCreateRequest">;
25
29
 
@@ -299,18 +303,26 @@ function sourceIdempotencyKey(context: MemoryToolContext): string {
299
303
 
300
304
  function createInput(
301
305
  context: MemoryToolContext,
302
- input: { content: string; expiresAtMs?: number },
306
+ input: { content: string; expiresAtMs?: number; kind: MemoryKind },
303
307
  toolCallId: string,
304
308
  ) {
305
309
  return {
306
310
  content: requireMemoryContent(input.content),
307
311
  idempotencyKey: `tool:${sourceIdempotencyKey(context)}:${toolCallId}`,
312
+ kind: input.kind,
308
313
  ...(input.expiresAtMs !== undefined
309
314
  ? { expiresAtMs: input.expiresAtMs }
310
315
  : {}),
311
316
  } satisfies CreateMemoryInput;
312
317
  }
313
318
 
319
+ function targetForKind(kind: MemoryKind): "requester" | "conversation" {
320
+ if (kind === "preference") {
321
+ return "requester";
322
+ }
323
+ return "conversation";
324
+ }
325
+
314
326
  /** Return the model-visible projection without hidden ownership/source fields. */
315
327
  function compactMemory(memory: MemoryRecord) {
316
328
  return {
@@ -327,7 +339,7 @@ function compactMemory(memory: MemoryRecord) {
327
339
  export function createMemoryCreateTool(context: MemoryToolContext) {
328
340
  return {
329
341
  description:
330
- "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 the canonical stored content, subject, and target.",
342
+ "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.",
331
343
  executionMode: "sequential",
332
344
  inputSchema: createMemoryInputSchema,
333
345
  execute: async (input, options) => {
@@ -382,6 +394,7 @@ export function createMemoryCreateTool(context: MemoryToolContext) {
382
394
  context,
383
395
  {
384
396
  content: review.content,
397
+ kind: review.kind,
385
398
  ...(review.expiresAtMs !== undefined
386
399
  ? { expiresAtMs: review.expiresAtMs }
387
400
  : requestedExpiresAtMs !== undefined
@@ -392,7 +405,7 @@ export function createMemoryCreateTool(context: MemoryToolContext) {
392
405
  );
393
406
  const result = await (async () => {
394
407
  try {
395
- if (review.target === "conversation") {
408
+ if (targetForKind(review.kind) === "conversation") {
396
409
  return await store.createConversationMemory(memoryInput);
397
410
  }
398
411
  return await store.createMemory(memoryInput);
package/src/types.ts CHANGED
@@ -7,16 +7,7 @@ import {
7
7
  } from "@sentry/junior-plugin-api";
8
8
  import { z } from "zod";
9
9
 
10
- export const MEMORY_TYPES = [
11
- "preference",
12
- "identity",
13
- "relationship",
14
- "knowledge",
15
- "context",
16
- "event",
17
- "task",
18
- "observation",
19
- ] as const;
10
+ export const MEMORY_KINDS = ["preference", "procedure", "knowledge"] as const;
20
11
 
21
12
  export const MEMORY_SCOPES = ["personal", "conversation"] as const;
22
13
  export const MEMORY_SUBJECT_TYPES = [
@@ -31,7 +22,7 @@ export const MEMORY_SOURCE_PLATFORMS = [
31
22
  export const MEMORY_EMBEDDING_METRICS = ["cosine"] as const;
32
23
  export const MEMORY_EMBEDDING_DIMENSIONS = 1536;
33
24
 
34
- export type MemoryType = (typeof MEMORY_TYPES)[number];
25
+ export type MemoryKind = (typeof MEMORY_KINDS)[number];
35
26
  export type MemoryScope = (typeof MEMORY_SCOPES)[number];
36
27
  export type MemorySubjectType = (typeof MEMORY_SUBJECT_TYPES)[number];
37
28
  export type MemorySourcePlatform = (typeof MEMORY_SOURCE_PLATFORMS)[number];