@sentry/junior-memory 0.197.0 → 0.199.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/user-pages.ts DELETED
@@ -1,99 +0,0 @@
1
- /** Render memory in Junior's User page format. */
2
- import type { PluginUserPageDefinition } from "@sentry/junior-plugin-api";
3
- import { listMemories, type MemoryVisibility, type MemoryView } from "./viewer";
4
- import type { MemoryDb } from "./store";
5
-
6
- function titleCase(value: string): string {
7
- return value.charAt(0).toUpperCase() + value.slice(1);
8
- }
9
-
10
- function rememberedDate(createdAtMs: number): string {
11
- return new Intl.DateTimeFormat("en-US", {
12
- dateStyle: "medium",
13
- timeStyle: "short",
14
- timeZone: "UTC",
15
- }).format(new Date(createdAtMs));
16
- }
17
-
18
- function originLabel(origin: MemoryView["origin"]): string {
19
- if (origin === "automatic") return "Automatic";
20
- if (origin === "explicit") return "Explicit";
21
- return "Other";
22
- }
23
-
24
- function visibilityLabel(visibility: MemoryVisibility): string {
25
- return visibility === "public" ? "Public" : "Private";
26
- }
27
-
28
- function pageFilter(filter: string | undefined): {
29
- visibility?: MemoryVisibility;
30
- } {
31
- if (filter === "private") return { visibility: "private" };
32
- if (filter === "public") return { visibility: "public" };
33
- return {};
34
- }
35
-
36
- function pageEmptyText(input: { filter?: string; query?: string }): string {
37
- if (input.query) return "No memories matched your search.";
38
- if (input.filter === "private") return "No private memories yet.";
39
- if (input.filter === "public") return "No public memories yet.";
40
- return "No memories yet.";
41
- }
42
-
43
- /** Create the interactive Memories dashboard page. */
44
- export function createMemoryUserPage(): PluginUserPageDefinition {
45
- return {
46
- id: "memories",
47
- label: "Memories",
48
- navigation: "primary",
49
- description:
50
- "Personal and public memories Junior can use across conversations.",
51
- async read(ctx, input) {
52
- const page = await listMemories(ctx.db as MemoryDb, ctx.viewer.id, {
53
- cursor: input.cursor,
54
- ...pageFilter(input.filter),
55
- limit: input.limit,
56
- ...(input.query ? { query: input.query } : undefined),
57
- });
58
- return {
59
- type: "list",
60
- emptyText: pageEmptyText(input),
61
- ...(page.nextCursor ? { nextCursor: page.nextCursor } : undefined),
62
- searchPlaceholder: "Search memories",
63
- records: page.memories.map((memory) => ({
64
- actions:
65
- memory.visibility === "private"
66
- ? [
67
- {
68
- confirmation: "Forget this memory?",
69
- href: `/api/plugins/memory/memories/${encodeURIComponent(memory.id)}`,
70
- label: "Forget",
71
- method: "DELETE" as const,
72
- tone: "danger" as const,
73
- },
74
- ]
75
- : [],
76
- id: memory.id,
77
- title: memory.content,
78
- metadata: [
79
- { label: "Type", value: titleCase(memory.kind) },
80
- { label: "Learned", value: originLabel(memory.origin) },
81
- { label: "Source", value: titleCase(memory.sourcePlatform) },
82
- {
83
- label: "Visibility",
84
- value: visibilityLabel(memory.visibility),
85
- },
86
- { label: "Remembered", value: rememberedDate(memory.createdAtMs) },
87
- { label: "Observed", value: rememberedDate(memory.observedAtMs) },
88
- {
89
- label: "Expires",
90
- value: memory.expiresAtMs
91
- ? rememberedDate(memory.expiresAtMs)
92
- : "Never",
93
- },
94
- ],
95
- })),
96
- };
97
- },
98
- };
99
- }
package/src/viewer.ts DELETED
@@ -1,441 +0,0 @@
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
- }
394
-
395
- /** Read hourly memory creation totals visible to the authenticated User in UTC. */
396
- export async function getMemoryTimelineHours(
397
- db: MemoryDb,
398
- userId: string,
399
- hours = 24,
400
- ) {
401
- const end = new Date();
402
- end.setUTCMinutes(0, 0, 0);
403
- const startMs = end.getTime() - (hours - 1) * 60 * 60 * 1_000;
404
- const rows = await db
405
- .select({
406
- date: sql<string>`to_char(to_timestamp(${juniorMemoryMemories.createdAtMs} / 1000.0) AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24')`.as(
407
- "date",
408
- ),
409
- private:
410
- sql<number>`count(*) filter (where ${juniorMemoryMemories.scope} = 'private')`.mapWith(
411
- Number,
412
- ),
413
- public:
414
- sql<number>`count(*) filter (where ${juniorMemoryMemories.scope} = 'public')`.mapWith(
415
- Number,
416
- ),
417
- })
418
- .from(juniorMemoryMemories)
419
- .where(
420
- and(
421
- visibleScopePredicate(userId),
422
- gt(juniorMemoryMemories.createdAtMs, startMs - 1),
423
- ),
424
- )
425
- .groupBy(
426
- sql`to_char(to_timestamp(${juniorMemoryMemories.createdAtMs} / 1000.0) AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24')`,
427
- );
428
- const byHour = new Map(rows.map((row) => [row.date, row]));
429
- return Array.from({ length: hours }, (_, index) => {
430
- const date = new Date(startMs + index * 60 * 60 * 1_000)
431
- .toISOString()
432
- .slice(0, 13);
433
- const row = byHour.get(date);
434
- return {
435
- date,
436
- private: row?.private ?? 0,
437
- public: row?.public ?? 0,
438
- };
439
- });
440
- }
441
-