@sentry/junior-memory 0.76.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/store.ts ADDED
@@ -0,0 +1,761 @@
1
+ /**
2
+ * SQL-backed memory store boundary.
3
+ *
4
+ * This module owns row parsing plus visible create/list/search/archive
5
+ * operations. Visibility, expiration, and supersession are enforced before
6
+ * records leave the store.
7
+ */
8
+ import { createHash, randomUUID } from "node:crypto";
9
+ import {
10
+ and,
11
+ asc,
12
+ desc,
13
+ eq,
14
+ gt,
15
+ ilike,
16
+ isNull,
17
+ like,
18
+ or,
19
+ sql,
20
+ type SQL,
21
+ } from "drizzle-orm";
22
+ import { cosineDistance } from "drizzle-orm/sql/functions";
23
+ import type { PgDatabase } from "drizzle-orm/pg-core";
24
+ import type { PgQueryResultHKT } from "drizzle-orm/pg-core/session";
25
+ import { z } from "zod";
26
+ import * as memorySqlSchema from "./db/schema";
27
+ import { juniorMemoryEmbeddings, juniorMemoryMemories } from "./db/schema";
28
+ import {
29
+ MEMORY_EMBEDDING_DIMENSIONS,
30
+ MEMORY_SCOPES,
31
+ MEMORY_SOURCE_PLATFORMS,
32
+ MEMORY_SUBJECT_TYPES,
33
+ MEMORY_TYPES,
34
+ memoryRuntimeContextSchema,
35
+ type MemoryRuntimeContext,
36
+ type MemoryScope,
37
+ } from "./types";
38
+ import {
39
+ deriveMemoryScope,
40
+ deriveMemorySubject,
41
+ deriveVisibleMemoryScopes,
42
+ type ResolvedMemoryScope,
43
+ } from "./scope";
44
+
45
+ const DEFAULT_LIST_LIMIT = 50;
46
+ const DEFAULT_SEARCH_LIMIT = 10;
47
+ const VECTOR_SEARCH_OVERFETCH = 4;
48
+ const MAX_MEMORY_CONTENT_CHARS = 4_000;
49
+ const EMBEDDING_METRIC = "cosine";
50
+
51
+ export type MemoryDb = PgDatabase<PgQueryResultHKT, typeof memorySqlSchema>;
52
+
53
+ interface SearchCandidate {
54
+ memory: MemoryRecord;
55
+ score: number;
56
+ }
57
+
58
+ const nonEmptyStringSchema = z.string().min(1);
59
+ const memoryContentSchema = z
60
+ .string()
61
+ .refine((content) => content.trim().length > 0, {
62
+ message: "Memory content is required.",
63
+ });
64
+ const numberSchema = z.number().finite();
65
+ const createMemoryInputSchema = z
66
+ .object({
67
+ content: memoryContentSchema,
68
+ expiresAtMs: numberSchema.optional(),
69
+ idempotencyKey: nonEmptyStringSchema,
70
+ })
71
+ .strict();
72
+ const listMemoriesInputSchema = z
73
+ .object({
74
+ limit: numberSchema.optional(),
75
+ })
76
+ .strict();
77
+ const searchMemoriesInputSchema = z
78
+ .object({
79
+ limit: numberSchema.optional(),
80
+ query: nonEmptyStringSchema,
81
+ })
82
+ .strict();
83
+ const archiveMemoryInputSchema = z
84
+ .object({
85
+ id: nonEmptyStringSchema,
86
+ reason: nonEmptyStringSchema.optional(),
87
+ })
88
+ .strict();
89
+ const clockSchema = z.function({ input: [], output: numberSchema }).optional();
90
+ const memoryStoreOptionsSchema = z
91
+ .object({
92
+ now: clockSchema,
93
+ })
94
+ .strict();
95
+ const optionalNumberSchema = z.preprocess(
96
+ (value) => (value === null ? undefined : value),
97
+ z.coerce.number().optional(),
98
+ );
99
+ const optionalStringSchema = z.preprocess(
100
+ (value) => (value === null ? undefined : value),
101
+ z.string().optional(),
102
+ );
103
+ const optionalNonEmptyStringSchema = z.preprocess(
104
+ (value) => (value === null ? undefined : value),
105
+ z.string().min(1).optional(),
106
+ );
107
+ const memoryRowSchema = z
108
+ .object({
109
+ archivedAtMs: optionalNumberSchema,
110
+ archiveReason: optionalStringSchema,
111
+ content: memoryContentSchema,
112
+ createdAtMs: z.coerce.number(),
113
+ expiresAtMs: optionalNumberSchema,
114
+ id: z.string().min(1),
115
+ idempotencyKey: optionalStringSchema,
116
+ observedAtMs: z.coerce.number(),
117
+ scope: z.enum(MEMORY_SCOPES),
118
+ scopeKey: z.string().min(1),
119
+ sourceKey: z.string().min(1),
120
+ sourcePlatform: z.enum(MEMORY_SOURCE_PLATFORMS),
121
+ subjectKey: optionalNonEmptyStringSchema,
122
+ subjectType: z.enum(MEMORY_SUBJECT_TYPES),
123
+ supersededAtMs: optionalNumberSchema,
124
+ supersededById: optionalStringSchema,
125
+ type: z.enum(MEMORY_TYPES),
126
+ })
127
+ .strict()
128
+ .superRefine((row, ctx) => {
129
+ if (row.subjectType === "general") {
130
+ if (row.subjectKey !== undefined) {
131
+ ctx.addIssue({
132
+ code: "custom",
133
+ message: "General-subject memory rows must not have a subject key.",
134
+ path: ["subjectKey"],
135
+ });
136
+ }
137
+ return;
138
+ }
139
+ if (row.subjectKey === undefined) {
140
+ ctx.addIssue({
141
+ code: "custom",
142
+ message: "User and conversation memory rows require a subject key.",
143
+ path: ["subjectKey"],
144
+ });
145
+ }
146
+ });
147
+
148
+ const memoryRecordSchema = z
149
+ .object({
150
+ archivedAtMs: numberSchema.optional(),
151
+ archiveReason: nonEmptyStringSchema.optional(),
152
+ content: memoryContentSchema,
153
+ createdAtMs: numberSchema,
154
+ expiresAtMs: numberSchema.optional(),
155
+ id: nonEmptyStringSchema,
156
+ observedAtMs: numberSchema,
157
+ scope: z.enum(MEMORY_SCOPES),
158
+ subjectType: z.enum(MEMORY_SUBJECT_TYPES),
159
+ supersededAtMs: numberSchema.optional(),
160
+ supersededById: nonEmptyStringSchema.optional(),
161
+ type: z.enum(MEMORY_TYPES),
162
+ })
163
+ .strict();
164
+ const embeddingVectorSchema = z
165
+ .array(numberSchema)
166
+ .length(MEMORY_EMBEDDING_DIMENSIONS);
167
+ const embeddingResultSchema = z
168
+ .object({
169
+ dimensions: z.literal(MEMORY_EMBEDDING_DIMENSIONS),
170
+ model: nonEmptyStringSchema,
171
+ provider: nonEmptyStringSchema,
172
+ vectors: z.array(embeddingVectorSchema),
173
+ })
174
+ .strict();
175
+
176
+ export type MemoryRecord = z.output<typeof memoryRecordSchema>;
177
+ export type CreateMemoryInput = z.output<typeof createMemoryInputSchema>;
178
+
179
+ /** Result of a memory write after idempotency checks. */
180
+ export interface CreateMemoryResult {
181
+ created: boolean;
182
+ memory: MemoryRecord;
183
+ }
184
+
185
+ export type ListMemoriesInput = z.output<typeof listMemoriesInputSchema>;
186
+
187
+ export type SearchMemoriesInput = z.output<typeof searchMemoriesInputSchema>;
188
+
189
+ export type ArchiveMemoryInput = z.output<typeof archiveMemoryInputSchema>;
190
+
191
+ export interface MemoryEmbeddingProvider {
192
+ /** Embed normalized memory text for derived vector retrieval. */
193
+ embedTexts(input: { texts: string[] }): Promise<{
194
+ dimensions: number;
195
+ model: string;
196
+ provider: string;
197
+ vectors: number[][];
198
+ }>;
199
+ }
200
+
201
+ export interface MemoryStoreOptions {
202
+ embedder?: MemoryEmbeddingProvider;
203
+ now?: () => number;
204
+ }
205
+
206
+ /** Context-bound storage operations for visible long-term memories. */
207
+ export interface MemoryStore {
208
+ /** Archive a visible memory in the current runtime context. */
209
+ archiveMemory(input: ArchiveMemoryInput): Promise<MemoryRecord>;
210
+ /** Store a personal memory for the current requester. */
211
+ createMemory(input: CreateMemoryInput): Promise<CreateMemoryResult>;
212
+ /** Store a conversation memory for the current source conversation. */
213
+ createConversationMemory(
214
+ input: CreateMemoryInput,
215
+ ): Promise<CreateMemoryResult>;
216
+ /** List active memories visible in the current runtime context. */
217
+ listMemories(input: ListMemoriesInput): Promise<MemoryRecord[]>;
218
+ /** Search active memories visible in the current runtime context. */
219
+ searchMemories(input: SearchMemoriesInput): Promise<MemoryRecord[]>;
220
+ }
221
+
222
+ function normalizeContent(content: string): string {
223
+ return content.replace(/\s+/g, " ").trim();
224
+ }
225
+
226
+ function hashEmbeddedContent(content: string): string {
227
+ return createHash("sha256").update(content, "utf8").digest("hex");
228
+ }
229
+
230
+ function boundedLimit(value: number | undefined, fallback: number): number {
231
+ if (typeof value !== "number" || !Number.isFinite(value)) {
232
+ return fallback;
233
+ }
234
+ return Math.min(200, Math.max(1, Math.floor(value)));
235
+ }
236
+
237
+ /** Build the durable source attribution key from runtime-owned source fields. */
238
+ function sourceKey(ctx: MemoryRuntimeContext): string {
239
+ if (ctx.source.platform === "local") {
240
+ return ctx.source.conversationId;
241
+ }
242
+ const threadKey = ctx.source.threadTs ?? ctx.source.messageTs;
243
+ if (!threadKey) {
244
+ throw new Error(
245
+ "Memory source requires a Slack message or thread timestamp.",
246
+ );
247
+ }
248
+ return `slack:${ctx.source.teamId}:${ctx.source.channelId}:${threadKey}`;
249
+ }
250
+
251
+ /** Parse one SQL row into the public memory record projection. */
252
+ function parseMemoryRow(row: unknown): MemoryRecord {
253
+ const parsed = memoryRowSchema.parse(row);
254
+ return memoryRecordSchema.parse({
255
+ id: parsed.id,
256
+ scope: parsed.scope,
257
+ type: parsed.type,
258
+ subjectType: parsed.subjectType,
259
+ content: parsed.content,
260
+ observedAtMs: parsed.observedAtMs,
261
+ createdAtMs: parsed.createdAtMs,
262
+ ...(parsed.expiresAtMs !== undefined
263
+ ? { expiresAtMs: parsed.expiresAtMs }
264
+ : {}),
265
+ ...(parsed.supersededAtMs !== undefined
266
+ ? { supersededAtMs: parsed.supersededAtMs }
267
+ : {}),
268
+ ...(parsed.supersededById ? { supersededById: parsed.supersededById } : {}),
269
+ ...(parsed.archivedAtMs !== undefined
270
+ ? { archivedAtMs: parsed.archivedAtMs }
271
+ : {}),
272
+ ...(parsed.archiveReason ? { archiveReason: parsed.archiveReason } : {}),
273
+ });
274
+ }
275
+
276
+ /** Build the scoped SQL predicate and ordered params for visible memory reads. */
277
+ function visibleScopePredicate(scopes: ResolvedMemoryScope[]): SQL | undefined {
278
+ if (scopes.length === 0) {
279
+ return undefined;
280
+ }
281
+ return or(
282
+ ...scopes.map((scope) =>
283
+ and(
284
+ eq(juniorMemoryMemories.scope, scope.scope),
285
+ eq(juniorMemoryMemories.scopeKey, scope.scopeKey),
286
+ ),
287
+ ),
288
+ );
289
+ }
290
+
291
+ function activeVisiblePredicate(args: {
292
+ nowMs: number;
293
+ scopes: ResolvedMemoryScope[];
294
+ }): SQL | undefined {
295
+ const scopePredicate = visibleScopePredicate(args.scopes);
296
+ if (!scopePredicate) {
297
+ return undefined;
298
+ }
299
+ return and(
300
+ scopePredicate,
301
+ isNull(juniorMemoryMemories.archivedAtMs),
302
+ isNull(juniorMemoryMemories.supersededAtMs),
303
+ isNull(juniorMemoryMemories.supersededById),
304
+ or(
305
+ isNull(juniorMemoryMemories.expiresAtMs),
306
+ gt(juniorMemoryMemories.expiresAtMs, args.nowMs),
307
+ ),
308
+ );
309
+ }
310
+
311
+ /** Resolve retry attempts for the same scoped write idempotency key. */
312
+ async function findByIdempotencyKey(args: {
313
+ db: MemoryDb;
314
+ idempotencyKey: string;
315
+ scope: ResolvedMemoryScope;
316
+ }): Promise<MemoryRecord | undefined> {
317
+ const rows = await args.db
318
+ .select()
319
+ .from(juniorMemoryMemories)
320
+ .where(
321
+ and(
322
+ eq(juniorMemoryMemories.scope, args.scope.scope),
323
+ eq(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
324
+ eq(juniorMemoryMemories.idempotencyKey, args.idempotencyKey),
325
+ isNull(juniorMemoryMemories.archivedAtMs),
326
+ isNull(juniorMemoryMemories.supersededAtMs),
327
+ isNull(juniorMemoryMemories.supersededById),
328
+ ),
329
+ )
330
+ .limit(1);
331
+ return rows[0] ? parseMemoryRow(rows[0]) : undefined;
332
+ }
333
+
334
+ function searchScore(memory: MemoryRecord, terms: string[]): number {
335
+ const haystack = memory.content.toLowerCase();
336
+ // Lexical score is a retrieval fallback signal, not a memory policy decision.
337
+ return terms.reduce(
338
+ (score, term) => score + (haystack.includes(term) ? 1 : 0),
339
+ 0,
340
+ );
341
+ }
342
+
343
+ function searchTerms(query: string): string[] {
344
+ return [
345
+ ...new Set(
346
+ query
347
+ .toLowerCase()
348
+ .split(/[^a-z0-9_'-]+/)
349
+ .map((term) => term.trim())
350
+ .filter((term) => term.length >= 2),
351
+ ),
352
+ ];
353
+ }
354
+
355
+ async function embedOne(
356
+ embedder: MemoryEmbeddingProvider,
357
+ text: string,
358
+ ): Promise<{
359
+ model: string;
360
+ provider: string;
361
+ vector: number[];
362
+ }> {
363
+ const normalized = normalizeContent(text);
364
+ if (!normalized) {
365
+ throw new Error("Embedding text is required.");
366
+ }
367
+ const result = embeddingResultSchema.parse(
368
+ await embedder.embedTexts({ texts: [normalized] }),
369
+ );
370
+ if (result.vectors.length !== 1) {
371
+ throw new Error("Embedding provider returned an unexpected vector count.");
372
+ }
373
+ return {
374
+ model: result.model,
375
+ provider: result.provider,
376
+ vector: result.vectors[0],
377
+ };
378
+ }
379
+
380
+ /** Store the derived vector index; failures must not block memory persistence. */
381
+ async function storeEmbedding(args: {
382
+ content: string;
383
+ db: MemoryDb;
384
+ embedder: MemoryEmbeddingProvider | undefined;
385
+ memoryId: string;
386
+ nowMs: number;
387
+ }): Promise<void> {
388
+ if (!args.embedder) {
389
+ return;
390
+ }
391
+ try {
392
+ const existing = await args.db
393
+ .select({ memoryId: juniorMemoryEmbeddings.memoryId })
394
+ .from(juniorMemoryEmbeddings)
395
+ .where(eq(juniorMemoryEmbeddings.memoryId, args.memoryId))
396
+ .limit(1);
397
+ if (existing[0]) {
398
+ return;
399
+ }
400
+ } catch {
401
+ return;
402
+ }
403
+ let embedding: Awaited<ReturnType<typeof embedOne>>;
404
+ try {
405
+ embedding = await embedOne(args.embedder, args.content);
406
+ } catch {
407
+ return;
408
+ }
409
+ try {
410
+ await args.db
411
+ .insert(juniorMemoryEmbeddings)
412
+ .values({
413
+ contentHash: hashEmbeddedContent(args.content),
414
+ createdAtMs: args.nowMs,
415
+ dimensions: MEMORY_EMBEDDING_DIMENSIONS,
416
+ embedding: embedding.vector,
417
+ memoryId: args.memoryId,
418
+ metric: EMBEDDING_METRIC,
419
+ model: embedding.model,
420
+ provider: embedding.provider,
421
+ })
422
+ .onConflictDoNothing();
423
+ } catch {
424
+ return;
425
+ }
426
+ }
427
+
428
+ /** List active records for the runtime-derived visible scopes. */
429
+ async function listVisibleMemories(args: {
430
+ db: MemoryDb;
431
+ limit?: number;
432
+ nowMs: number;
433
+ scopes: ResolvedMemoryScope[];
434
+ }): Promise<MemoryRecord[]> {
435
+ const predicate = activeVisiblePredicate(args);
436
+ if (!predicate) {
437
+ return [];
438
+ }
439
+ const limit = boundedLimit(args.limit, DEFAULT_LIST_LIMIT);
440
+ const rows = await args.db
441
+ .select()
442
+ .from(juniorMemoryMemories)
443
+ .where(predicate)
444
+ .orderBy(
445
+ desc(juniorMemoryMemories.createdAtMs),
446
+ asc(juniorMemoryMemories.id),
447
+ )
448
+ .limit(limit);
449
+ return rows.map(parseMemoryRow);
450
+ }
451
+
452
+ /** Search active visible records with the V1 lexical matcher. */
453
+ async function searchVisibleMemories(args: {
454
+ db: MemoryDb;
455
+ nowMs: number;
456
+ query: string;
457
+ scopes: ResolvedMemoryScope[];
458
+ }): Promise<MemoryRecord[]> {
459
+ const terms = searchTerms(args.query);
460
+ if (terms.length === 0) {
461
+ return [];
462
+ }
463
+ const predicate = activeVisiblePredicate(args);
464
+ if (!predicate) {
465
+ return [];
466
+ }
467
+ const rows = await args.db
468
+ .select()
469
+ .from(juniorMemoryMemories)
470
+ .where(
471
+ and(
472
+ predicate,
473
+ or(
474
+ ...terms.map((term) =>
475
+ ilike(juniorMemoryMemories.content, `%${term}%`),
476
+ ),
477
+ ),
478
+ ),
479
+ );
480
+ return rows.map(parseMemoryRow);
481
+ }
482
+
483
+ /** Search active visible records with exact pgvector cosine distance. */
484
+ async function searchVisibleVectorMemories(args: {
485
+ db: MemoryDb;
486
+ embedder: MemoryEmbeddingProvider | undefined;
487
+ limit: number;
488
+ nowMs: number;
489
+ query: string;
490
+ scopes: ResolvedMemoryScope[];
491
+ }): Promise<SearchCandidate[]> {
492
+ if (!args.embedder) {
493
+ return [];
494
+ }
495
+ const predicate = activeVisiblePredicate(args);
496
+ if (!predicate) {
497
+ return [];
498
+ }
499
+ let embedding: Awaited<ReturnType<typeof embedOne>>;
500
+ try {
501
+ embedding = await embedOne(args.embedder, args.query);
502
+ } catch {
503
+ return [];
504
+ }
505
+ const distance = cosineDistance(
506
+ juniorMemoryEmbeddings.embedding,
507
+ embedding.vector,
508
+ );
509
+ const rows = await args.db
510
+ .select({
511
+ contentHash: juniorMemoryEmbeddings.contentHash,
512
+ distance,
513
+ memory: juniorMemoryMemories,
514
+ })
515
+ .from(juniorMemoryMemories)
516
+ .innerJoin(
517
+ juniorMemoryEmbeddings,
518
+ eq(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id),
519
+ )
520
+ .where(
521
+ and(
522
+ predicate,
523
+ eq(juniorMemoryEmbeddings.provider, embedding.provider),
524
+ eq(juniorMemoryEmbeddings.model, embedding.model),
525
+ eq(juniorMemoryEmbeddings.dimensions, MEMORY_EMBEDDING_DIMENSIONS),
526
+ eq(juniorMemoryEmbeddings.metric, EMBEDDING_METRIC),
527
+ ),
528
+ )
529
+ .orderBy(
530
+ distance,
531
+ desc(juniorMemoryMemories.createdAtMs),
532
+ asc(juniorMemoryMemories.id),
533
+ )
534
+ .limit(args.limit);
535
+ return rows.flatMap((row) => {
536
+ const distanceValue = Number(row.distance);
537
+ if (
538
+ row.distance === null ||
539
+ !Number.isFinite(distanceValue) ||
540
+ hashEmbeddedContent(row.memory.content) !== row.contentHash
541
+ ) {
542
+ return [];
543
+ }
544
+ return [
545
+ {
546
+ memory: parseMemoryRow(row.memory),
547
+ score: 1 / (1 + Math.max(0, distanceValue)),
548
+ },
549
+ ];
550
+ });
551
+ }
552
+
553
+ /** Fuse higher-is-better retrieval candidates before applying the final limit. */
554
+ function mergeSearchCandidates(candidates: SearchCandidate[]): MemoryRecord[] {
555
+ const byId = new Map<
556
+ string,
557
+ {
558
+ memory: MemoryRecord;
559
+ score: number;
560
+ }
561
+ >();
562
+ for (const candidate of candidates) {
563
+ const existing = byId.get(candidate.memory.id);
564
+ if (existing) {
565
+ existing.score += candidate.score;
566
+ continue;
567
+ }
568
+ byId.set(candidate.memory.id, {
569
+ memory: candidate.memory,
570
+ score: candidate.score,
571
+ });
572
+ }
573
+ return [...byId.values()]
574
+ .sort(
575
+ (left, right) =>
576
+ right.score - left.score ||
577
+ right.memory.createdAtMs - left.memory.createdAtMs ||
578
+ left.memory.id.localeCompare(right.memory.id),
579
+ )
580
+ .map((candidate) => candidate.memory);
581
+ }
582
+
583
+ /** Create a context-bound SQL-backed store for explicit memory operations. */
584
+ export function createMemoryStore(
585
+ db: MemoryDb,
586
+ context: MemoryRuntimeContext,
587
+ options: MemoryStoreOptions = {},
588
+ ): MemoryStore {
589
+ const runtimeContext = memoryRuntimeContextSchema.parse(context);
590
+ const parsedOptions = memoryStoreOptionsSchema.parse({ now: options.now });
591
+ const embedder = options.embedder;
592
+ const getNowMs = parsedOptions.now ?? Date.now;
593
+
594
+ /** Persist a memory under the plugin-derived scope and subject. */
595
+ async function createScopedMemory(
596
+ rawInput: CreateMemoryInput,
597
+ scopeKind: MemoryScope,
598
+ ): Promise<CreateMemoryResult> {
599
+ const input = createMemoryInputSchema.parse(rawInput);
600
+ const nowMs = getNowMs();
601
+ const content = normalizeContent(input.content);
602
+ const scope = deriveMemoryScope(runtimeContext, scopeKind);
603
+ const subject = deriveMemorySubject(runtimeContext, scope);
604
+ if (content.length > MAX_MEMORY_CONTENT_CHARS) {
605
+ throw new Error("Memory content exceeds the maximum length.");
606
+ }
607
+
608
+ const id = randomUUID();
609
+ const rows = await db
610
+ .insert(juniorMemoryMemories)
611
+ .values({
612
+ content,
613
+ createdAtMs: nowMs,
614
+ expiresAtMs: input.expiresAtMs,
615
+ id,
616
+ idempotencyKey: input.idempotencyKey,
617
+ observedAtMs: nowMs,
618
+ scope: scope.scope,
619
+ scopeKey: scope.scopeKey,
620
+ sourceKey: sourceKey(runtimeContext),
621
+ sourcePlatform: runtimeContext.source.platform,
622
+ subjectKey: subject.subjectKey,
623
+ subjectType: subject.subjectType,
624
+ type: "knowledge",
625
+ })
626
+ .onConflictDoNothing({
627
+ target: [
628
+ juniorMemoryMemories.scope,
629
+ juniorMemoryMemories.scopeKey,
630
+ juniorMemoryMemories.idempotencyKey,
631
+ ],
632
+ where: sql`${juniorMemoryMemories.idempotencyKey} IS NOT NULL AND ${juniorMemoryMemories.archivedAtMs} IS NULL AND ${juniorMemoryMemories.supersededAtMs} IS NULL AND ${juniorMemoryMemories.supersededById} IS NULL`,
633
+ })
634
+ .returning();
635
+ if (rows[0]) {
636
+ const memory = parseMemoryRow(rows[0]);
637
+ await storeEmbedding({
638
+ content: memory.content,
639
+ db,
640
+ embedder,
641
+ memoryId: memory.id,
642
+ nowMs,
643
+ });
644
+ return { created: true, memory };
645
+ }
646
+
647
+ const idempotent = await findByIdempotencyKey({
648
+ db,
649
+ idempotencyKey: input.idempotencyKey,
650
+ scope,
651
+ });
652
+ if (!idempotent) {
653
+ throw new Error("Memory idempotency conflict did not resolve.");
654
+ }
655
+ await storeEmbedding({
656
+ content: idempotent.content,
657
+ db,
658
+ embedder,
659
+ memoryId: idempotent.id,
660
+ nowMs,
661
+ });
662
+ return { created: false, memory: idempotent };
663
+ }
664
+
665
+ return {
666
+ async createMemory(input) {
667
+ return await createScopedMemory(input, "personal");
668
+ },
669
+
670
+ async createConversationMemory(input) {
671
+ return await createScopedMemory(input, "conversation");
672
+ },
673
+
674
+ async listMemories(input) {
675
+ input = listMemoriesInputSchema.parse(input);
676
+ const nowMs = getNowMs();
677
+ const scopes = deriveVisibleMemoryScopes(runtimeContext);
678
+ return await listVisibleMemories({
679
+ db,
680
+ limit: input.limit,
681
+ nowMs,
682
+ scopes,
683
+ });
684
+ },
685
+
686
+ async searchMemories(input) {
687
+ input = searchMemoriesInputSchema.parse(input);
688
+ const nowMs = getNowMs();
689
+ const scopes = deriveVisibleMemoryScopes(runtimeContext);
690
+ const limit = boundedLimit(input.limit, DEFAULT_SEARCH_LIMIT);
691
+ const vectorCandidates = await searchVisibleVectorMemories({
692
+ db,
693
+ embedder,
694
+ limit: limit * VECTOR_SEARCH_OVERFETCH,
695
+ nowMs,
696
+ query: input.query,
697
+ scopes,
698
+ });
699
+ const candidates = await searchVisibleMemories({
700
+ db,
701
+ nowMs,
702
+ query: input.query,
703
+ scopes,
704
+ });
705
+ const terms = searchTerms(input.query);
706
+ const lexicalCandidates = candidates
707
+ .map((memory) => ({ memory, score: searchScore(memory, terms) }))
708
+ .filter((item) => item.score > 0);
709
+ return mergeSearchCandidates([
710
+ ...vectorCandidates,
711
+ ...lexicalCandidates,
712
+ ]).slice(0, limit);
713
+ },
714
+
715
+ async archiveMemory(input) {
716
+ input = archiveMemoryInputSchema.parse(input);
717
+ const nowMs = getNowMs();
718
+ const scopes = deriveVisibleMemoryScopes(runtimeContext);
719
+ const predicate = activeVisiblePredicate({ nowMs, scopes });
720
+ const idPrefix = input.id.trim();
721
+ if (!idPrefix) {
722
+ throw new Error("Memory id is required.");
723
+ }
724
+ const rows = predicate
725
+ ? await db
726
+ .select()
727
+ .from(juniorMemoryMemories)
728
+ .where(
729
+ and(
730
+ predicate,
731
+ or(
732
+ eq(juniorMemoryMemories.id, idPrefix),
733
+ like(juniorMemoryMemories.id, `${idPrefix}%`),
734
+ ),
735
+ ),
736
+ )
737
+ .orderBy(asc(juniorMemoryMemories.id))
738
+ .limit(2)
739
+ : [];
740
+ if (rows.length === 0) {
741
+ throw new Error("Memory was not found in the current context.");
742
+ }
743
+ if (rows.length > 1) {
744
+ throw new Error("Memory id prefix is ambiguous.");
745
+ }
746
+ const memory = parseMemoryRow(rows[0]);
747
+ const updated = await db
748
+ .update(juniorMemoryMemories)
749
+ .set({
750
+ archivedAtMs: nowMs,
751
+ archiveReason: input.reason ?? "user_removed",
752
+ })
753
+ .where(eq(juniorMemoryMemories.id, memory.id))
754
+ .returning();
755
+ await db
756
+ .delete(juniorMemoryEmbeddings)
757
+ .where(eq(juniorMemoryEmbeddings.memoryId, memory.id));
758
+ return parseMemoryRow(updated[0]);
759
+ },
760
+ };
761
+ }