@sentry/junior-memory 0.179.0 → 0.181.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/tools.ts CHANGED
@@ -7,6 +7,8 @@ import {
7
7
  type PluginToolOutput,
8
8
  type Source,
9
9
  type Actor,
10
+ type Identity,
11
+ type User,
10
12
  pluginToolOutputSchema,
11
13
  } from "@sentry/junior-plugin-api";
12
14
  import { z } from "zod";
@@ -43,8 +45,8 @@ const KNOWN_TOOL_INPUT_ERROR_MESSAGES = new Set([
43
45
  "Memory id is required.",
44
46
  "Memory was not found in the current context.",
45
47
  "Memory id prefix is ambiguous.",
46
- "Personal memory requires actor context.",
47
- "User-subject memory requires actor context.",
48
+ "Private memory requires a User.",
49
+ "User memory requires a User.",
48
50
  ]);
49
51
 
50
52
  /** Runtime-owned context used to bind memory tools to visible scopes. */
@@ -53,8 +55,12 @@ export interface MemoryToolContext {
53
55
  conversationId?: string;
54
56
  db: MemoryDb;
55
57
  embedder?: MemoryEmbeddingProvider;
58
+ locationId?: string;
56
59
  actor?: Actor;
57
60
  source: Source;
61
+ users: {
62
+ resolveActor(): Promise<{ identity: Identity; user?: User } | undefined>;
63
+ };
58
64
  userText?: string;
59
65
  }
60
66
 
@@ -79,27 +85,31 @@ function asToolInputError(error: unknown): never {
79
85
  throw error;
80
86
  }
81
87
 
82
- function memoryRuntimeContext(
88
+ async function memoryRuntimeContext(
83
89
  context: MemoryToolContext,
84
- ): MemoryRuntimeContext {
90
+ ): Promise<MemoryRuntimeContext> {
91
+ const actorUser = (await context.users.resolveActor())?.user;
85
92
  return memoryRuntimeContextSchema.parse({
86
93
  ...(context.conversationId
87
94
  ? { conversationId: context.conversationId }
88
- : {}),
89
- ...(context.actor ? { actor: context.actor } : {}),
95
+ : undefined),
96
+ ...(context.actor ? { actor: context.actor } : undefined),
97
+ ...(context.locationId ? { locationId: context.locationId } : undefined),
90
98
  source: context.source,
99
+ ...(actorUser ? { userId: actorUser.id } : undefined),
91
100
  });
92
101
  }
93
102
 
94
103
  function memoryStore(
95
104
  context: MemoryToolContext,
105
+ runtimeContext: MemoryRuntimeContext,
96
106
  options: { supersessionDecider?: MemorySupersessionDecider } = {},
97
107
  ) {
98
- return createMemoryStore(context.db, memoryRuntimeContext(context), {
108
+ return createMemoryStore(context.db, runtimeContext, {
99
109
  embedder: context.embedder,
100
110
  ...(options.supersessionDecider
101
111
  ? { supersessionDecider: options.supersessionDecider }
102
- : {}),
112
+ : undefined),
103
113
  });
104
114
  }
105
115
 
@@ -237,7 +247,7 @@ const createMemoryInputSchema = z
237
247
  .min(1)
238
248
  .max(MAX_TOOL_CONTENT_CHARS)
239
249
  .describe(
240
- "Self-contained public/shareable memory candidate. Include the subject in natural language when it matters; do not rely on surrounding chat context.",
250
+ "Self-contained memory candidate. Include the subject in natural language when it matters; do not rely on surrounding chat context.",
241
251
  ),
242
252
  expires_at: z
243
253
  .string()
@@ -355,7 +365,7 @@ function createInput(
355
365
  kind: input.kind,
356
366
  ...(input.expiresAtMs !== undefined
357
367
  ? { expiresAtMs: input.expiresAtMs }
358
- : {}),
368
+ : undefined),
359
369
  } satisfies CreateMemoryInput;
360
370
  }
361
371
 
@@ -375,7 +385,7 @@ function compactMemory(memory: MemoryRecord): MemoryToolProjection {
375
385
  observedAtMs: memory.observedAtMs,
376
386
  ...(memory.expiresAtMs !== undefined
377
387
  ? { expiresAtMs: memory.expiresAtMs }
378
- : {}),
388
+ : undefined),
379
389
  });
380
390
  }
381
391
 
@@ -400,7 +410,7 @@ export function createMemoryCreateTool(context: MemoryCreateToolContext) {
400
410
  readOnlyHint: false,
401
411
  },
402
412
  description:
403
- "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.",
413
+ "Explicit memory-write tool. Use only when the latest user message directly asks Junior to remember, store, save, or forget-and-replace a 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. Junior sets access, Location, Source, and subject. The memory agent rewrites the content and sets the memory kind.",
404
414
  executionMode: "sequential",
405
415
  inputSchema: createMemoryInputSchema,
406
416
  outputSchema: memoryCreateOutputSchema,
@@ -408,8 +418,8 @@ export function createMemoryCreateTool(context: MemoryCreateToolContext) {
408
418
  const parsedInput = parseMemoryToolInput(createMemoryInputSchema, input);
409
419
  const toolCallId = requireToolCallId(options.toolCallId);
410
420
  const requestedExpiresAtMs = parseExpiresAt(parsedInput.expires_at);
411
- const runtimeContext = memoryRuntimeContext(context);
412
- const store = memoryStore(context, {
421
+ const runtimeContext = await memoryRuntimeContext(context);
422
+ const store = memoryStore(context, runtimeContext, {
413
423
  supersessionDecider: context.supersessionDecider,
414
424
  });
415
425
  const review = await (async () => {
@@ -420,7 +430,7 @@ export function createMemoryCreateTool(context: MemoryCreateToolContext) {
420
430
  content: requireMemoryContent(parsedInput.content),
421
431
  ...(requestedExpiresAtMs !== undefined
422
432
  ? { expiresAtMs: requestedExpiresAtMs }
423
- : {}),
433
+ : undefined),
424
434
  runtimeContext,
425
435
  ...(context.userText?.trim()
426
436
  ? {
@@ -428,7 +438,7 @@ export function createMemoryCreateTool(context: MemoryCreateToolContext) {
428
438
  currentUserText: context.userText.trim(),
429
439
  },
430
440
  }
431
- : {}),
441
+ : undefined),
432
442
  }),
433
443
  ),
434
444
  );
@@ -492,15 +502,16 @@ export function createMemoryRemoveTool(context: MemoryToolContext) {
492
502
  readOnlyHint: false,
493
503
  },
494
504
  description:
495
- "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.",
505
+ "Forget one private memory owned by the current User. Public memories are read-only. Use only ids or short id prefixes returned by listMemories or searchMemories. Never remove memories by hidden Actor, provider, scope, or subject ids.",
496
506
  executionMode: "sequential",
497
507
  inputSchema: removeMemoryInputSchema,
498
508
  outputSchema: memorySingleOutputSchema,
499
509
  execute: async (input) => {
500
510
  const parsedInput = parseMemoryToolInput(removeMemoryInputSchema, input);
511
+ const runtimeContext = await memoryRuntimeContext(context);
501
512
  const memory = await (async () => {
502
513
  try {
503
- return await memoryStore(context).archiveMemory({
514
+ return await memoryStore(context, runtimeContext).archiveMemory({
504
515
  id: parsedInput.id,
505
516
  reason: "tool_removed",
506
517
  });
@@ -530,7 +541,8 @@ export function createMemoryListTool(context: MemoryToolContext) {
530
541
  outputSchema: memoryManyOutputSchema,
531
542
  execute: async (input) => {
532
543
  const parsedInput = parseMemoryToolInput(listMemoriesInputSchema, input);
533
- const memories = await memoryStore(context).listMemories({
544
+ const runtimeContext = await memoryRuntimeContext(context);
545
+ const memories = await memoryStore(context, runtimeContext).listMemories({
534
546
  limit: boundedLimit(parsedInput.limit, DEFAULT_RESULT_LIMIT),
535
547
  });
536
548
  return memoryToolResult("listMemories", {
@@ -544,7 +556,7 @@ export function createMemoryListTool(context: MemoryToolContext) {
544
556
  export function createMemorySearchTool(context: MemoryToolContext) {
545
557
  return definePluginTool({
546
558
  description:
547
- "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.",
559
+ "Search active memories visible in the current context. Use when the model needs targeted memory recall. Public memories are visible everywhere. Private memories belong to the current User.",
548
560
  annotations: {
549
561
  destructiveHint: false,
550
562
  idempotentHint: true,
@@ -558,7 +570,11 @@ export function createMemorySearchTool(context: MemoryToolContext) {
558
570
  searchMemoriesInputSchema,
559
571
  input,
560
572
  );
561
- const memories = await memoryStore(context).searchMemories({
573
+ const runtimeContext = await memoryRuntimeContext(context);
574
+ const memories = await memoryStore(
575
+ context,
576
+ runtimeContext,
577
+ ).searchMemories({
562
578
  query: parsedInput.query,
563
579
  limit: boundedLimit(parsedInput.limit, DEFAULT_SEARCH_LIMIT),
564
580
  });
package/src/types.ts CHANGED
@@ -3,7 +3,7 @@ import { z } from "zod";
3
3
 
4
4
  export const MEMORY_KINDS = ["preference", "procedure", "knowledge"] as const;
5
5
 
6
- export const MEMORY_SCOPES = ["personal", "conversation"] as const;
6
+ export const MEMORY_SCOPES = ["private", "public"] as const;
7
7
  export const MEMORY_SUBJECT_TYPES = [
8
8
  "user",
9
9
  "conversation",
@@ -22,12 +22,15 @@ export type MemoryEmbeddingMetric = (typeof MEMORY_EMBEDDING_METRICS)[number];
22
22
 
23
23
  const nonEmptyStringSchema = z.string().min(1);
24
24
 
25
- /** Runtime-owned memory invocation fields used for scope and source authority. */
25
+ /** Host data used to set memory access, subject, and source. */
26
26
  export const memoryRuntimeContextSchema = z
27
27
  .object({
28
28
  conversationId: nonEmptyStringSchema.optional(),
29
+ locationId: nonEmptyStringSchema.optional(),
29
30
  actor: actorSchema.optional(),
30
31
  source: sourceSchema,
32
+ /** User linked to the active Actor. */
33
+ userId: nonEmptyStringSchema.optional(),
31
34
  })
32
35
  .strict();
33
36
 
package/src/user-pages.ts CHANGED
@@ -1,7 +1,6 @@
1
- /** Project viewer-visible memories into Junior's core-rendered user page. */
1
+ /** Render memory in Junior's User page format. */
2
2
  import type { PluginUserPageDefinition } from "@sentry/junior-plugin-api";
3
- import { createViewerMemories } from "./personal";
4
- import type { MemoryVisibility, PersonalMemoryRecord } from "./personal-store";
3
+ import { listMemories, type MemoryVisibility, type MemoryView } from "./viewer";
5
4
  import type { MemoryDb } from "./store";
6
5
 
7
6
  function titleCase(value: string): string {
@@ -16,7 +15,7 @@ function rememberedDate(createdAtMs: number): string {
16
15
  }).format(new Date(createdAtMs));
17
16
  }
18
17
 
19
- function originLabel(origin: PersonalMemoryRecord["origin"]): string {
18
+ function originLabel(origin: MemoryView["origin"]): string {
20
19
  if (origin === "automatic") return "Automatic";
21
20
  if (origin === "explicit") return "Explicit";
22
21
  return "Other";
@@ -50,17 +49,16 @@ export function createMemoryUserPage(): PluginUserPageDefinition {
50
49
  description:
51
50
  "Personal and public memories Junior can use across conversations.",
52
51
  async read(ctx, input) {
53
- const memories = createViewerMemories(ctx.db as MemoryDb, ctx.viewer);
54
- const page = await memories.list({
52
+ const page = await listMemories(ctx.db as MemoryDb, ctx.viewer.id, {
55
53
  cursor: input.cursor,
56
54
  ...pageFilter(input.filter),
57
55
  limit: input.limit,
58
- ...(input.query ? { query: input.query } : {}),
56
+ ...(input.query ? { query: input.query } : undefined),
59
57
  });
60
58
  return {
61
59
  type: "list",
62
60
  emptyText: pageEmptyText(input),
63
- ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
61
+ ...(page.nextCursor ? { nextCursor: page.nextCursor } : undefined),
64
62
  searchPlaceholder: "Search memories",
65
63
  records: page.memories.map((memory) => ({
66
64
  actions:
package/src/viewer.ts ADDED
@@ -0,0 +1,393 @@
1
+ /**
2
+ * Memory access for an authenticated User.
3
+ *
4
+ * Every User can read public memory and private memory that they own.
5
+ */
6
+ import {
7
+ and,
8
+ asc,
9
+ desc,
10
+ eq,
11
+ gt,
12
+ ilike,
13
+ isNull,
14
+ like,
15
+ lt,
16
+ or,
17
+ sql,
18
+ } from "drizzle-orm";
19
+ import { z } from "zod";
20
+ import { juniorMemoryEmbeddings, juniorMemoryMemories } from "./db/schema";
21
+ import { publicMemoryScope } from "./scope";
22
+ import { parseMemoryRow, type MemoryDb, type MemoryRecord } from "./store";
23
+ import { MEMORY_KINDS, type MemorySourcePlatform } from "./types";
24
+
25
+ const DAY_MS = 24 * 60 * 60 * 1_000;
26
+ const nonEmptyStringSchema = z.string().min(1);
27
+ const memoryVisibilitySchema = z.enum(["private", "public"]);
28
+ const cursorSchema = z
29
+ .object({
30
+ createdAtMs: z.number().finite(),
31
+ id: nonEmptyStringSchema,
32
+ kind: z.enum(MEMORY_KINDS).optional(),
33
+ origin: z.enum(["automatic", "explicit"]).optional(),
34
+ query: z.string().max(200).optional(),
35
+ version: z.literal(1),
36
+ visibility: memoryVisibilitySchema.optional(),
37
+ })
38
+ .strict();
39
+ const pageInputSchema = z
40
+ .object({
41
+ cursor: z.string().min(1).max(1_000).optional(),
42
+ kind: z.enum(MEMORY_KINDS).optional(),
43
+ limit: z.number().int().min(1).max(50),
44
+ origin: z.enum(["automatic", "explicit"]).optional(),
45
+ query: z.string().max(200).optional(),
46
+ visibility: memoryVisibilitySchema.optional(),
47
+ })
48
+ .strict();
49
+ const timelineDaysSchema = z.number().int().min(1).max(365);
50
+
51
+ /** Access label returned by dashboard and REST memory views. */
52
+ export type MemoryVisibility = z.output<typeof memoryVisibilitySchema>;
53
+
54
+ /** Memory fields returned to an authenticated User. */
55
+ export type MemoryView = MemoryRecord & {
56
+ origin: "automatic" | "explicit" | "other";
57
+ sourcePlatform: MemorySourcePlatform;
58
+ visibility: MemoryVisibility;
59
+ };
60
+
61
+ interface MemoryPage {
62
+ memories: MemoryView[];
63
+ nextCursor?: string;
64
+ }
65
+
66
+ type MemoryPageInput = z.output<typeof pageInputSchema>;
67
+
68
+ /** Expected error for a malformed or mismatched page cursor. */
69
+ export class InvalidMemoryCursorError extends Error {
70
+ constructor() {
71
+ super("Memory cursor is invalid.");
72
+ this.name = "InvalidMemoryCursorError";
73
+ }
74
+ }
75
+
76
+ /** Expected error when the current User cannot access a memory. */
77
+ export class MemoryNotFoundError extends Error {
78
+ constructor() {
79
+ super("Memory was not found for this user.");
80
+ this.name = "MemoryNotFoundError";
81
+ }
82
+ }
83
+
84
+ function publicScopePredicate() {
85
+ return and(
86
+ eq(juniorMemoryMemories.scope, publicMemoryScope.scope),
87
+ eq(juniorMemoryMemories.scopeKey, publicMemoryScope.scopeKey),
88
+ );
89
+ }
90
+
91
+ function privateScopePredicate(userId: string) {
92
+ return and(
93
+ eq(juniorMemoryMemories.scope, "private"),
94
+ eq(juniorMemoryMemories.scopeKey, userId),
95
+ );
96
+ }
97
+
98
+ function visibleScopePredicate(userId: string, visibility?: MemoryVisibility) {
99
+ if (visibility === "public") return publicScopePredicate();
100
+ if (visibility === "private") return privateScopePredicate(userId);
101
+ return or(publicScopePredicate(), privateScopePredicate(userId));
102
+ }
103
+
104
+ function activeMemoryPredicate(
105
+ userId: string,
106
+ nowMs: number,
107
+ visibility?: MemoryVisibility,
108
+ ) {
109
+ return and(
110
+ visibleScopePredicate(userId, visibility),
111
+ isNull(juniorMemoryMemories.archivedAtMs),
112
+ isNull(juniorMemoryMemories.supersededAtMs),
113
+ isNull(juniorMemoryMemories.supersededById),
114
+ or(
115
+ isNull(juniorMemoryMemories.expiresAtMs),
116
+ gt(juniorMemoryMemories.expiresAtMs, nowMs),
117
+ ),
118
+ );
119
+ }
120
+
121
+ function utcDate(ms: number): string {
122
+ return new Date(ms).toISOString().slice(0, 10);
123
+ }
124
+
125
+ function searchTerms(query: string): string[] {
126
+ return [
127
+ ...new Set(
128
+ query
129
+ .toLowerCase()
130
+ .split(/[^a-z0-9_'-]+/)
131
+ .map((term) => term.trim())
132
+ .filter((term) => term.length >= 2),
133
+ ),
134
+ ];
135
+ }
136
+
137
+ function memoryOrigin(idempotencyKey: string | null): MemoryView["origin"] {
138
+ if (idempotencyKey?.startsWith("session:")) return "automatic";
139
+ if (idempotencyKey?.startsWith("tool:")) return "explicit";
140
+ return "other";
141
+ }
142
+
143
+ function toMemoryView(
144
+ row: typeof juniorMemoryMemories.$inferSelect,
145
+ ): MemoryView {
146
+ const memory = parseMemoryRow(row);
147
+ return {
148
+ ...memory,
149
+ origin: memoryOrigin(row.idempotencyKey),
150
+ sourcePlatform: row.sourcePlatform,
151
+ visibility: memory.scope,
152
+ };
153
+ }
154
+
155
+ function cursorFilters(input: MemoryPageInput) {
156
+ return {
157
+ kind: input.kind,
158
+ origin: input.origin,
159
+ query: input.query?.trim() || undefined,
160
+ visibility: input.visibility,
161
+ };
162
+ }
163
+
164
+ function decodeCursor(
165
+ value: string | undefined,
166
+ filters: ReturnType<typeof cursorFilters>,
167
+ ) {
168
+ if (!value) return undefined;
169
+ try {
170
+ const parsed = cursorSchema.parse(
171
+ JSON.parse(Buffer.from(value, "base64url").toString("utf8")),
172
+ );
173
+ if (
174
+ parsed.query !== filters.query ||
175
+ parsed.kind !== filters.kind ||
176
+ parsed.origin !== filters.origin ||
177
+ parsed.visibility !== filters.visibility
178
+ ) {
179
+ throw new InvalidMemoryCursorError();
180
+ }
181
+ return { createdAtMs: parsed.createdAtMs, id: parsed.id };
182
+ } catch {
183
+ throw new InvalidMemoryCursorError();
184
+ }
185
+ }
186
+
187
+ function encodeCursor(
188
+ createdBefore: { createdAtMs: number; id: string },
189
+ filters: ReturnType<typeof cursorFilters>,
190
+ ): string {
191
+ return Buffer.from(
192
+ JSON.stringify({ ...createdBefore, ...filters, version: 1 }),
193
+ "utf8",
194
+ ).toString("base64url");
195
+ }
196
+
197
+ /** Archive one active private memory owned by the authenticated User. */
198
+ export async function archiveMemory(db: MemoryDb, userId: string, id: string) {
199
+ const memoryId = nonEmptyStringSchema.parse(id);
200
+ const nowMs = Date.now();
201
+ const updated = await db
202
+ .update(juniorMemoryMemories)
203
+ .set({
204
+ archivedAtMs: nowMs,
205
+ archiveReason: "user_removed",
206
+ })
207
+ .where(
208
+ and(
209
+ activeMemoryPredicate(userId, nowMs, "private"),
210
+ eq(juniorMemoryMemories.id, memoryId),
211
+ ),
212
+ )
213
+ .returning();
214
+ if (!updated[0]) throw new MemoryNotFoundError();
215
+ await db
216
+ .delete(juniorMemoryEmbeddings)
217
+ .where(eq(juniorMemoryEmbeddings.memoryId, memoryId));
218
+ return parseMemoryRow(updated[0]);
219
+ }
220
+
221
+ /** Read one active memory visible to the authenticated User. */
222
+ export async function getMemory(
223
+ db: MemoryDb,
224
+ userId: string,
225
+ id: string,
226
+ ): Promise<MemoryView> {
227
+ const memoryId = nonEmptyStringSchema.parse(id);
228
+ const nowMs = Date.now();
229
+ const rows = await db
230
+ .select()
231
+ .from(juniorMemoryMemories)
232
+ .where(
233
+ and(
234
+ activeMemoryPredicate(userId, nowMs),
235
+ eq(juniorMemoryMemories.id, memoryId),
236
+ ),
237
+ )
238
+ .limit(1);
239
+ if (!rows[0]) throw new MemoryNotFoundError();
240
+ return toMemoryView(rows[0]);
241
+ }
242
+
243
+ /** List one stable page of active memory visible to the authenticated User. */
244
+ export async function listMemories(
245
+ db: MemoryDb,
246
+ userId: string,
247
+ input: MemoryPageInput,
248
+ ): Promise<MemoryPage> {
249
+ input = pageInputSchema.parse(input);
250
+ const filters = cursorFilters(input);
251
+ const cursor = decodeCursor(input.cursor, filters);
252
+ const active = activeMemoryPredicate(userId, Date.now(), input.visibility);
253
+ const createdBefore = cursor
254
+ ? or(
255
+ lt(juniorMemoryMemories.createdAtMs, cursor.createdAtMs),
256
+ and(
257
+ eq(juniorMemoryMemories.createdAtMs, cursor.createdAtMs),
258
+ gt(juniorMemoryMemories.id, cursor.id),
259
+ ),
260
+ )
261
+ : undefined;
262
+ const terms = input.query ? searchTerms(input.query) : [];
263
+ const search =
264
+ input.query === undefined
265
+ ? undefined
266
+ : terms.length === 0
267
+ ? sql`false`
268
+ : or(
269
+ ...terms.map((term) =>
270
+ ilike(juniorMemoryMemories.content, `%${term}%`),
271
+ ),
272
+ );
273
+ const kind = input.kind
274
+ ? eq(juniorMemoryMemories.kind, input.kind)
275
+ : undefined;
276
+ const origin =
277
+ input.origin === "automatic"
278
+ ? like(juniorMemoryMemories.idempotencyKey, "session:%")
279
+ : input.origin === "explicit"
280
+ ? like(juniorMemoryMemories.idempotencyKey, "tool:%")
281
+ : undefined;
282
+ const rows = await db
283
+ .select()
284
+ .from(juniorMemoryMemories)
285
+ .where(and(active, createdBefore, search, kind, origin))
286
+ .orderBy(
287
+ desc(juniorMemoryMemories.createdAtMs),
288
+ asc(juniorMemoryMemories.id),
289
+ )
290
+ .limit(input.limit + 1);
291
+ const memories = rows.slice(0, input.limit).map(toMemoryView);
292
+ const last = memories.at(-1);
293
+ if (rows.length <= input.limit || !last) return { memories };
294
+ const next = { createdAtMs: last.createdAtMs, id: last.id };
295
+ return { memories, nextCursor: encodeCursor(next, filters) };
296
+ }
297
+
298
+ /** Summarize active memory visible to the authenticated User. */
299
+ export async function getMemoryStats(db: MemoryDb, userId: string) {
300
+ const nowMs = Date.now();
301
+ const [counts] = await db
302
+ .select({
303
+ active: sql<number>`count(*)`.mapWith(Number),
304
+ automatic:
305
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.idempotencyKey} like 'session:%')`.mapWith(
306
+ Number,
307
+ ),
308
+ createdThirtyDays:
309
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.createdAtMs} >= ${nowMs - 30 * DAY_MS})`.mapWith(
310
+ Number,
311
+ ),
312
+ embedded: sql<number>`count(${juniorMemoryEmbeddings.memoryId})`.mapWith(
313
+ Number,
314
+ ),
315
+ explicit:
316
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.idempotencyKey} like 'tool:%')`.mapWith(
317
+ Number,
318
+ ),
319
+ knowledge:
320
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'knowledge')`.mapWith(
321
+ Number,
322
+ ),
323
+ private:
324
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.scope} = 'private')`.mapWith(
325
+ Number,
326
+ ),
327
+ preference:
328
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'preference')`.mapWith(
329
+ Number,
330
+ ),
331
+ procedure:
332
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'procedure')`.mapWith(
333
+ Number,
334
+ ),
335
+ public:
336
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.scope} = 'public')`.mapWith(
337
+ Number,
338
+ ),
339
+ })
340
+ .from(juniorMemoryMemories)
341
+ .leftJoin(
342
+ juniorMemoryEmbeddings,
343
+ eq(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id),
344
+ )
345
+ .where(activeMemoryPredicate(userId, nowMs));
346
+ if (!counts) throw new Error("Memory stats query returned no row.");
347
+ return counts;
348
+ }
349
+
350
+ /** Read daily memory creation totals visible to the authenticated User in UTC. */
351
+ export async function getMemoryTimeline(
352
+ db: MemoryDb,
353
+ userId: string,
354
+ days: number,
355
+ ) {
356
+ days = timelineDaysSchema.parse(days);
357
+ const todayMs = Date.parse(`${utcDate(Date.now())}T00:00:00.000Z`);
358
+ const startMs = todayMs - (days - 1) * DAY_MS;
359
+ const rows = await db
360
+ .select({
361
+ date: sql<string>`to_char(to_timestamp(${juniorMemoryMemories.createdAtMs} / 1000.0) AT TIME ZONE 'UTC', 'YYYY-MM-DD')`.as(
362
+ "date",
363
+ ),
364
+ private:
365
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.scope} = 'private')`.mapWith(
366
+ Number,
367
+ ),
368
+ public:
369
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.scope} = 'public')`.mapWith(
370
+ Number,
371
+ ),
372
+ })
373
+ .from(juniorMemoryMemories)
374
+ .where(
375
+ and(
376
+ visibleScopePredicate(userId),
377
+ gt(juniorMemoryMemories.createdAtMs, startMs - 1),
378
+ ),
379
+ )
380
+ .groupBy(
381
+ sql`to_char(to_timestamp(${juniorMemoryMemories.createdAtMs} / 1000.0) AT TIME ZONE 'UTC', 'YYYY-MM-DD')`,
382
+ );
383
+ const byDate = new Map(rows.map((row) => [row.date, row]));
384
+ return Array.from({ length: days }, (_, index) => {
385
+ const date = utcDate(startMs + index * DAY_MS);
386
+ const row = byDate.get(date);
387
+ return {
388
+ date,
389
+ private: row?.private ?? 0,
390
+ public: row?.public ?? 0,
391
+ };
392
+ });
393
+ }