@sentry/junior-memory 0.180.0 → 0.181.1

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.
@@ -1,421 +0,0 @@
1
- /**
2
- * SQL operations over memories visible to one authenticated viewer.
3
- *
4
- * A user may have several linked identities. This store combines their
5
- * identity-scoped personal memories with authorized public workspace scopes.
6
- */
7
- import { and, asc, desc, eq, gt, ilike, like, lt, or, sql } from "drizzle-orm";
8
- import { z } from "zod";
9
- import { juniorMemoryEmbeddings, juniorMemoryMemories } from "./db/schema";
10
- import type { ResolvedMemoryScope } from "./scope";
11
- import {
12
- activeVisiblePredicate,
13
- archiveExpiredMemoryBatch,
14
- parseMemoryRow,
15
- type MemoryDb,
16
- type MemoryRecord,
17
- } from "./store";
18
- import { MEMORY_KINDS, type MemorySourcePlatform } from "./types";
19
-
20
- const nonEmptyStringSchema = z.string().min(1);
21
- const memoryVisibilitySchema = z.enum(["private", "public"]);
22
- const personalMemoryCursorSchema = z
23
- .object({
24
- createdAtMs: z.number().finite(),
25
- id: nonEmptyStringSchema,
26
- })
27
- .strict();
28
- const personalMemoryPageInputSchema = z
29
- .object({
30
- cursor: personalMemoryCursorSchema.optional(),
31
- kind: z.enum(MEMORY_KINDS).optional(),
32
- limit: z.number().int().min(1).max(50),
33
- origin: z.enum(["automatic", "explicit"]).optional(),
34
- query: z.string().max(200).optional(),
35
- visibility: memoryVisibilitySchema.optional(),
36
- })
37
- .strict();
38
- const personalMemoryTimelineInputSchema = z
39
- .object({
40
- days: z.number().int().min(1).max(365),
41
- })
42
- .strict();
43
- const DAY_MS = 24 * 60 * 60 * 1_000;
44
-
45
- export type PersonalMemoryCursor = z.output<typeof personalMemoryCursorSchema>;
46
-
47
- export type PersonalMemoryPageInput = z.output<
48
- typeof personalMemoryPageInputSchema
49
- >;
50
-
51
- export type MemoryVisibility = z.output<typeof memoryVisibilitySchema>;
52
-
53
- export interface PersonalMemoryPage {
54
- memories: PersonalMemoryRecord[];
55
- nextCursor?: PersonalMemoryCursor;
56
- }
57
-
58
- /** Safe provenance attached to one viewer-visible memory. */
59
- export type PersonalMemoryRecord = MemoryRecord & {
60
- origin: "automatic" | "explicit" | "other";
61
- sourcePlatform: MemorySourcePlatform;
62
- visibility: MemoryVisibility;
63
- };
64
-
65
- /** Viewer-scoped active memory totals used by the dashboard. */
66
- export interface PersonalMemoryStats {
67
- active: number;
68
- automatic: number;
69
- createdThirtyDays: number;
70
- embedded: number;
71
- explicit: number;
72
- knowledge: number;
73
- personal: number;
74
- preference: number;
75
- procedure: number;
76
- public: number;
77
- }
78
-
79
- /** Viewer-scoped memory creation totals for one UTC calendar day. */
80
- export interface PersonalMemoryDay {
81
- date: string;
82
- personal: number;
83
- public: number;
84
- }
85
-
86
- /** Expected failure when a viewer does not own the requested memory. */
87
- export class PersonalMemoryNotFoundError extends Error {
88
- constructor() {
89
- super("Memory was not found for the authenticated viewer.");
90
- this.name = "PersonalMemoryNotFoundError";
91
- }
92
- }
93
-
94
- /** Viewer-scoped memory operations shared by dashboard and REST. */
95
- export interface PersonalMemoryCollection {
96
- /** Archive one exact personal memory owned by a linked identity. */
97
- archive(id: string): Promise<MemoryRecord>;
98
- /** Read one exact memory visible to a linked identity. */
99
- get(id: string): Promise<PersonalMemoryRecord>;
100
- /** List one stable page across every authorized viewer scope. */
101
- list(input: PersonalMemoryPageInput): Promise<PersonalMemoryPage>;
102
- /** Summarize active memories across every authorized viewer scope. */
103
- stats(): Promise<PersonalMemoryStats>;
104
- /** Read memory creation history across every authorized viewer scope. */
105
- timeline(input: { days: number }): Promise<PersonalMemoryDay[]>;
106
- }
107
-
108
- function scopePredicate(scopes: ResolvedMemoryScope[]) {
109
- if (scopes.length === 0) return undefined;
110
- return or(
111
- ...scopes.map((scope) =>
112
- and(
113
- eq(juniorMemoryMemories.scope, scope.scope),
114
- eq(juniorMemoryMemories.scopeKey, scope.scopeKey),
115
- ),
116
- ),
117
- );
118
- }
119
-
120
- function utcDate(ms: number): string {
121
- return new Date(ms).toISOString().slice(0, 10);
122
- }
123
-
124
- function searchTerms(query: string): string[] {
125
- return [
126
- ...new Set(
127
- query
128
- .toLowerCase()
129
- .split(/[^a-z0-9_'-]+/)
130
- .map((term) => term.trim())
131
- .filter((term) => term.length >= 2),
132
- ),
133
- ];
134
- }
135
-
136
- function memoryOrigin(
137
- idempotencyKey: string | null,
138
- ): PersonalMemoryRecord["origin"] {
139
- if (idempotencyKey?.startsWith("session:")) return "automatic";
140
- if (idempotencyKey?.startsWith("tool:")) return "explicit";
141
- return "other";
142
- }
143
-
144
- function memoryVisibility(
145
- scope: MemoryRecord["scope"],
146
- ): PersonalMemoryRecord["visibility"] {
147
- return scope === "personal" ? "private" : "public";
148
- }
149
-
150
- function personalMemoryRecord(
151
- row: typeof juniorMemoryMemories.$inferSelect,
152
- ): PersonalMemoryRecord {
153
- const memory = parseMemoryRow(row);
154
- return {
155
- ...memory,
156
- origin: memoryOrigin(row.idempotencyKey),
157
- sourcePlatform: row.sourcePlatform,
158
- visibility: memoryVisibility(memory.scope),
159
- };
160
- }
161
-
162
- function emptyStats(): PersonalMemoryStats {
163
- return {
164
- active: 0,
165
- automatic: 0,
166
- createdThirtyDays: 0,
167
- embedded: 0,
168
- explicit: 0,
169
- knowledge: 0,
170
- personal: 0,
171
- preference: 0,
172
- procedure: 0,
173
- public: 0,
174
- };
175
- }
176
-
177
- /** Build storage operations for every memory scope linked to one viewer. */
178
- export function createPersonalMemoryCollection(
179
- db: MemoryDb,
180
- scopes: {
181
- privateScopes: ResolvedMemoryScope[];
182
- publicScopes: ResolvedMemoryScope[];
183
- },
184
- options: { now?: () => number } = {},
185
- ): PersonalMemoryCollection {
186
- const { privateScopes, publicScopes } = scopes;
187
- const allScopes = [...privateScopes, ...publicScopes];
188
- const getNowMs = () => options.now?.() ?? Date.now();
189
-
190
- function scopesForVisibility(
191
- visibility: MemoryVisibility | undefined,
192
- ): ResolvedMemoryScope[] {
193
- if (visibility === "private") return privateScopes;
194
- if (visibility === "public") return publicScopes;
195
- return allScopes;
196
- }
197
-
198
- return {
199
- async archive(id) {
200
- const memoryId = nonEmptyStringSchema.parse(id);
201
- const nowMs = getNowMs();
202
- // Forget is personal-only; public workspace memories stay shared.
203
- const predicate = activeVisiblePredicate({
204
- nowMs,
205
- scopes: privateScopes,
206
- });
207
- if (!predicate) {
208
- throw new PersonalMemoryNotFoundError();
209
- }
210
- const updated = await db
211
- .update(juniorMemoryMemories)
212
- .set({
213
- archivedAtMs: nowMs,
214
- archiveReason: "user_removed",
215
- })
216
- .where(and(predicate, eq(juniorMemoryMemories.id, memoryId)))
217
- .returning();
218
- if (!updated[0]) {
219
- throw new PersonalMemoryNotFoundError();
220
- }
221
- await db
222
- .delete(juniorMemoryEmbeddings)
223
- .where(eq(juniorMemoryEmbeddings.memoryId, memoryId));
224
- return parseMemoryRow(updated[0]);
225
- },
226
-
227
- async get(id) {
228
- const memoryId = nonEmptyStringSchema.parse(id);
229
- const nowMs = getNowMs();
230
- const predicate = activeVisiblePredicate({ nowMs, scopes: allScopes });
231
- if (!predicate) {
232
- throw new PersonalMemoryNotFoundError();
233
- }
234
- const rows = await db
235
- .select()
236
- .from(juniorMemoryMemories)
237
- .where(and(predicate, eq(juniorMemoryMemories.id, memoryId)))
238
- .limit(1);
239
- if (!rows[0]) {
240
- throw new PersonalMemoryNotFoundError();
241
- }
242
- return personalMemoryRecord(rows[0]);
243
- },
244
-
245
- async list(input) {
246
- input = personalMemoryPageInputSchema.parse(input);
247
- const nowMs = getNowMs();
248
- const scopes = scopesForVisibility(input.visibility);
249
- await archiveExpiredMemoryBatch({ db, nowMs, scopes });
250
- const active = activeVisiblePredicate({ nowMs, scopes });
251
- if (!active) {
252
- return { memories: [] };
253
- }
254
-
255
- const cursor = input.cursor
256
- ? or(
257
- lt(juniorMemoryMemories.createdAtMs, input.cursor.createdAtMs),
258
- and(
259
- eq(juniorMemoryMemories.createdAtMs, input.cursor.createdAtMs),
260
- gt(juniorMemoryMemories.id, input.cursor.id),
261
- ),
262
- )
263
- : undefined;
264
- const terms = input.query ? searchTerms(input.query) : [];
265
- const search =
266
- input.query === undefined
267
- ? undefined
268
- : terms.length === 0
269
- ? sql`false`
270
- : or(
271
- ...terms.map((term) =>
272
- ilike(juniorMemoryMemories.content, `%${term}%`),
273
- ),
274
- );
275
- const kind = input.kind
276
- ? eq(juniorMemoryMemories.kind, input.kind)
277
- : undefined;
278
- const origin =
279
- input.origin === "automatic"
280
- ? like(juniorMemoryMemories.idempotencyKey, "session:%")
281
- : input.origin === "explicit"
282
- ? like(juniorMemoryMemories.idempotencyKey, "tool:%")
283
- : undefined;
284
- const rows = await db
285
- .select()
286
- .from(juniorMemoryMemories)
287
- .where(and(active, cursor, search, kind, origin))
288
- .orderBy(
289
- desc(juniorMemoryMemories.createdAtMs),
290
- asc(juniorMemoryMemories.id),
291
- )
292
- .limit(input.limit + 1);
293
- const hasNextPage = rows.length > input.limit;
294
- const memories = rows.slice(0, input.limit).map(personalMemoryRecord);
295
- const last = memories.at(-1);
296
- return {
297
- memories,
298
- ...(hasNextPage && last
299
- ? {
300
- nextCursor: {
301
- createdAtMs: last.createdAtMs,
302
- id: last.id,
303
- },
304
- }
305
- : undefined),
306
- };
307
- },
308
-
309
- async stats() {
310
- const nowMs = getNowMs();
311
- await archiveExpiredMemoryBatch({ db, nowMs, scopes: allScopes });
312
- const active = activeVisiblePredicate({ nowMs, scopes: allScopes });
313
- if (!active) {
314
- return emptyStats();
315
- }
316
- const [counts] = await db
317
- .select({
318
- active: sql<number>`count(*)`.mapWith(Number),
319
- automatic:
320
- sql<number>`count(*) filter (where ${juniorMemoryMemories.idempotencyKey} like 'session:%')`.mapWith(
321
- Number,
322
- ),
323
- createdThirtyDays:
324
- sql<number>`count(*) filter (where ${juniorMemoryMemories.createdAtMs} >= ${nowMs - 30 * 24 * 60 * 60 * 1_000})`.mapWith(
325
- Number,
326
- ),
327
- embedded:
328
- sql<number>`count(${juniorMemoryEmbeddings.memoryId})`.mapWith(
329
- Number,
330
- ),
331
- explicit:
332
- sql<number>`count(*) filter (where ${juniorMemoryMemories.idempotencyKey} like 'tool:%')`.mapWith(
333
- Number,
334
- ),
335
- knowledge:
336
- sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'knowledge')`.mapWith(
337
- Number,
338
- ),
339
- personal:
340
- sql<number>`count(*) filter (where ${juniorMemoryMemories.scope} = 'personal')`.mapWith(
341
- Number,
342
- ),
343
- preference:
344
- sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'preference')`.mapWith(
345
- Number,
346
- ),
347
- procedure:
348
- sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'procedure')`.mapWith(
349
- Number,
350
- ),
351
- public:
352
- sql<number>`count(*) filter (where ${juniorMemoryMemories.scope} = 'conversation')`.mapWith(
353
- Number,
354
- ),
355
- })
356
- .from(juniorMemoryMemories)
357
- .leftJoin(
358
- juniorMemoryEmbeddings,
359
- eq(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id),
360
- )
361
- .where(active);
362
- return {
363
- active: counts?.active ?? 0,
364
- automatic: counts?.automatic ?? 0,
365
- createdThirtyDays: counts?.createdThirtyDays ?? 0,
366
- embedded: counts?.embedded ?? 0,
367
- explicit: counts?.explicit ?? 0,
368
- knowledge: counts?.knowledge ?? 0,
369
- personal: counts?.personal ?? 0,
370
- preference: counts?.preference ?? 0,
371
- procedure: counts?.procedure ?? 0,
372
- public: counts?.public ?? 0,
373
- };
374
- },
375
-
376
- async timeline(input) {
377
- input = personalMemoryTimelineInputSchema.parse(input);
378
- const todayMs = Date.parse(`${utcDate(getNowMs())}T00:00:00.000Z`);
379
- const startMs = todayMs - (input.days - 1) * DAY_MS;
380
- const ownership = scopePredicate(allScopes);
381
- if (!ownership) {
382
- return Array.from({ length: input.days }, (_, index) => ({
383
- date: utcDate(startMs + index * DAY_MS),
384
- personal: 0,
385
- public: 0,
386
- }));
387
- }
388
- const rows = await db
389
- .select({
390
- date: sql<string>`to_char(to_timestamp(${juniorMemoryMemories.createdAtMs} / 1000.0) AT TIME ZONE 'UTC', 'YYYY-MM-DD')`.as(
391
- "date",
392
- ),
393
- personal:
394
- sql<number>`count(*) filter (where ${juniorMemoryMemories.scope} = 'personal')`.mapWith(
395
- Number,
396
- ),
397
- public:
398
- sql<number>`count(*) filter (where ${juniorMemoryMemories.scope} = 'conversation')`.mapWith(
399
- Number,
400
- ),
401
- })
402
- .from(juniorMemoryMemories)
403
- .where(
404
- and(ownership, gt(juniorMemoryMemories.createdAtMs, startMs - 1)),
405
- )
406
- .groupBy(
407
- sql`to_char(to_timestamp(${juniorMemoryMemories.createdAtMs} / 1000.0) AT TIME ZONE 'UTC', 'YYYY-MM-DD')`,
408
- );
409
- const byDate = new Map(rows.map((row) => [row.date, row]));
410
- return Array.from({ length: input.days }, (_, index) => {
411
- const date = utcDate(startMs + index * DAY_MS);
412
- const row = byDate.get(date);
413
- return {
414
- date,
415
- personal: row?.personal ?? 0,
416
- public: row?.public ?? 0,
417
- };
418
- });
419
- },
420
- };
421
- }
package/src/personal.ts DELETED
@@ -1,140 +0,0 @@
1
- /**
2
- * Authenticated-viewer memory access shared by REST and dashboard projections.
3
- *
4
- * One user may have multiple provider identities. This module adapts those
5
- * identities to the existing personal and public workspace scopes.
6
- */
7
- import { z } from "zod";
8
- import type { User } from "@sentry/junior-plugin-api";
9
- import {
10
- createPersonalMemoryCollection,
11
- type MemoryVisibility,
12
- type PersonalMemoryRecord,
13
- } from "./personal-store";
14
- import { deriveViewerMemoryScopes } from "./scope";
15
- import type { MemoryDb, MemoryRecord } from "./store";
16
- import type { MemoryKind } from "./types";
17
-
18
- const cursorSchema = z
19
- .object({
20
- createdAtMs: z.number().finite(),
21
- id: z.string().min(1),
22
- kind: z.enum(["preference", "procedure", "knowledge"]).optional(),
23
- origin: z.enum(["automatic", "explicit"]).optional(),
24
- query: z.string().max(200).optional(),
25
- version: z.literal(1),
26
- visibility: z.enum(["private", "public"]).optional(),
27
- })
28
- .strict();
29
-
30
- export interface ViewerMemoryPage {
31
- memories: PersonalMemoryRecord[];
32
- nextCursor?: string;
33
- }
34
-
35
- export interface ViewerMemoryPageInput {
36
- cursor?: string;
37
- kind?: MemoryKind;
38
- limit: number;
39
- origin?: "automatic" | "explicit";
40
- query?: string;
41
- visibility?: MemoryVisibility;
42
- }
43
-
44
- export class InvalidMemoryCursorError extends Error {
45
- constructor() {
46
- super("Memory cursor is invalid.");
47
- this.name = "InvalidMemoryCursorError";
48
- }
49
- }
50
-
51
- export { PersonalMemoryNotFoundError } from "./personal-store";
52
- export type { MemoryVisibility, PersonalMemoryRecord } from "./personal-store";
53
-
54
- function decodeCursor(
55
- value: string | undefined,
56
- input: Pick<
57
- ViewerMemoryPageInput,
58
- "kind" | "origin" | "query" | "visibility"
59
- >,
60
- ) {
61
- if (!value) return undefined;
62
- try {
63
- const parsed = cursorSchema.parse(
64
- JSON.parse(Buffer.from(value, "base64url").toString("utf8")),
65
- );
66
- if (
67
- parsed.query !== input.query ||
68
- parsed.kind !== input.kind ||
69
- parsed.origin !== input.origin ||
70
- parsed.visibility !== input.visibility
71
- ) {
72
- throw new InvalidMemoryCursorError();
73
- }
74
- return { createdAtMs: parsed.createdAtMs, id: parsed.id };
75
- } catch {
76
- throw new InvalidMemoryCursorError();
77
- }
78
- }
79
-
80
- function encodeCursor(
81
- cursor: { createdAtMs: number; id: string },
82
- input: Pick<
83
- ViewerMemoryPageInput,
84
- "kind" | "origin" | "query" | "visibility"
85
- >,
86
- ): string {
87
- return Buffer.from(
88
- JSON.stringify({
89
- ...cursor,
90
- ...(input.query ? { query: input.query } : undefined),
91
- ...(input.kind ? { kind: input.kind } : undefined),
92
- ...(input.origin ? { origin: input.origin } : undefined),
93
- ...(input.visibility ? { visibility: input.visibility } : undefined),
94
- version: 1,
95
- }),
96
- "utf8",
97
- ).toString("base64url");
98
- }
99
-
100
- /** Build viewer memory operations authorized by a user's linked identities. */
101
- export function createViewerMemories(db: MemoryDb, user: User) {
102
- const collection = createPersonalMemoryCollection(
103
- db,
104
- deriveViewerMemoryScopes(user.identities),
105
- );
106
- return {
107
- async archive(id: string): Promise<MemoryRecord> {
108
- return await collection.archive(id);
109
- },
110
- async get(id: string): Promise<PersonalMemoryRecord> {
111
- return await collection.get(id);
112
- },
113
- async list(input: ViewerMemoryPageInput): Promise<ViewerMemoryPage> {
114
- const query = input.query?.trim() || undefined;
115
- const filters = {
116
- ...(input.kind ? { kind: input.kind } : undefined),
117
- ...(input.origin ? { origin: input.origin } : undefined),
118
- ...(query ? { query } : undefined),
119
- ...(input.visibility ? { visibility: input.visibility } : undefined),
120
- };
121
- const page = await collection.list({
122
- cursor: decodeCursor(input.cursor, filters),
123
- ...filters,
124
- limit: input.limit,
125
- });
126
- return {
127
- memories: page.memories,
128
- ...(page.nextCursor
129
- ? { nextCursor: encodeCursor(page.nextCursor, filters) }
130
- : undefined),
131
- };
132
- },
133
- async stats() {
134
- return await collection.stats();
135
- },
136
- async timeline(input: { days: number }) {
137
- return await collection.timeline(input);
138
- },
139
- };
140
- }