@sentry/junior-memory 0.124.0 → 0.124.2
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/dist/api.d.ts +33 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +312 -40
- package/dist/index.js.map +1 -1
- package/dist/personal-store.d.ts +56 -7
- package/dist/personal.d.ts +13 -3
- package/dist/user-pages.d.ts +2 -2
- package/package.json +2 -2
- package/src/api.ts +64 -6
- package/src/index.ts +2 -0
- package/src/personal-store.ts +276 -16
- package/src/personal.ts +52 -13
- package/src/user-pages.ts +69 -21
package/src/personal-store.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SQL operations over
|
|
2
|
+
* SQL operations over memories visible to one authenticated viewer.
|
|
3
3
|
*
|
|
4
4
|
* A viewer may resolve to several runtime actors. This store combines their
|
|
5
|
-
* personal scopes
|
|
5
|
+
* personal scopes with authorized public workspace conversation scopes.
|
|
6
6
|
*/
|
|
7
|
-
import { and, asc, desc, eq, gt, ilike, lt, or, sql } from "drizzle-orm";
|
|
7
|
+
import { and, asc, desc, eq, gt, ilike, like, lt, or, sql } from "drizzle-orm";
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { juniorMemoryEmbeddings, juniorMemoryMemories } from "./db/schema";
|
|
10
10
|
import { deriveMemoryScope, type ResolvedMemoryScope } from "./scope";
|
|
@@ -15,9 +15,10 @@ import {
|
|
|
15
15
|
type MemoryDb,
|
|
16
16
|
type MemoryRecord,
|
|
17
17
|
} from "./store";
|
|
18
|
-
import type
|
|
18
|
+
import { MEMORY_KINDS, type MemoryRuntimeContext } from "./types";
|
|
19
19
|
|
|
20
20
|
const nonEmptyStringSchema = z.string().min(1);
|
|
21
|
+
const memoryVisibilitySchema = z.enum(["private", "public"]);
|
|
21
22
|
const personalMemoryCursorSchema = z
|
|
22
23
|
.object({
|
|
23
24
|
createdAtMs: z.number().finite(),
|
|
@@ -27,10 +28,19 @@ const personalMemoryCursorSchema = z
|
|
|
27
28
|
const personalMemoryPageInputSchema = z
|
|
28
29
|
.object({
|
|
29
30
|
cursor: personalMemoryCursorSchema.optional(),
|
|
31
|
+
kind: z.enum(MEMORY_KINDS).optional(),
|
|
30
32
|
limit: z.number().int().min(1).max(50),
|
|
33
|
+
origin: z.enum(["automatic", "explicit"]).optional(),
|
|
31
34
|
query: z.string().max(200).optional(),
|
|
35
|
+
visibility: memoryVisibilitySchema.optional(),
|
|
32
36
|
})
|
|
33
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;
|
|
34
44
|
|
|
35
45
|
export type PersonalMemoryCursor = z.output<typeof personalMemoryCursorSchema>;
|
|
36
46
|
|
|
@@ -38,11 +48,41 @@ export type PersonalMemoryPageInput = z.output<
|
|
|
38
48
|
typeof personalMemoryPageInputSchema
|
|
39
49
|
>;
|
|
40
50
|
|
|
51
|
+
export type MemoryVisibility = z.output<typeof memoryVisibilitySchema>;
|
|
52
|
+
|
|
41
53
|
export interface PersonalMemoryPage {
|
|
42
|
-
memories:
|
|
54
|
+
memories: PersonalMemoryRecord[];
|
|
43
55
|
nextCursor?: PersonalMemoryCursor;
|
|
44
56
|
}
|
|
45
57
|
|
|
58
|
+
/** Safe provenance attached to one viewer-visible memory. */
|
|
59
|
+
export type PersonalMemoryRecord = MemoryRecord & {
|
|
60
|
+
origin: "automatic" | "explicit" | "other";
|
|
61
|
+
sourcePlatform: "local" | "slack";
|
|
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
|
+
|
|
46
86
|
/** Expected failure when a viewer does not own the requested memory. */
|
|
47
87
|
export class PersonalMemoryNotFoundError extends Error {
|
|
48
88
|
constructor() {
|
|
@@ -51,14 +91,18 @@ export class PersonalMemoryNotFoundError extends Error {
|
|
|
51
91
|
}
|
|
52
92
|
}
|
|
53
93
|
|
|
54
|
-
/** Viewer-scoped
|
|
94
|
+
/** Viewer-scoped memory operations shared by dashboard and REST. */
|
|
55
95
|
export interface PersonalMemoryCollection {
|
|
56
96
|
/** Archive one exact personal memory owned by a linked actor. */
|
|
57
97
|
archive(id: string): Promise<MemoryRecord>;
|
|
58
|
-
/** Read one exact
|
|
59
|
-
get(id: string): Promise<
|
|
60
|
-
/** List one stable page across every
|
|
98
|
+
/** Read one exact memory visible to a linked actor. */
|
|
99
|
+
get(id: string): Promise<PersonalMemoryRecord>;
|
|
100
|
+
/** List one stable page across every authorized viewer scope. */
|
|
61
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[]>;
|
|
62
106
|
}
|
|
63
107
|
|
|
64
108
|
function personalScopes(
|
|
@@ -74,6 +118,45 @@ function personalScopes(
|
|
|
74
118
|
];
|
|
75
119
|
}
|
|
76
120
|
|
|
121
|
+
/** Public workspace conversation scopes authorized by the viewer's Slack teams. */
|
|
122
|
+
function publicWorkspaceScopes(
|
|
123
|
+
runtimeContexts: MemoryRuntimeContext[],
|
|
124
|
+
): ResolvedMemoryScope[] {
|
|
125
|
+
return [
|
|
126
|
+
...new Map(
|
|
127
|
+
runtimeContexts.flatMap((context) => {
|
|
128
|
+
if (context.source.platform !== "slack" || !context.actor) {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
if (context.actor.platform !== "slack") {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
const scope: ResolvedMemoryScope = {
|
|
135
|
+
scope: "conversation",
|
|
136
|
+
scopeKey: `slack:${context.actor.teamId}`,
|
|
137
|
+
};
|
|
138
|
+
return [[`${scope.scope}:${scope.scopeKey}`, scope] as const];
|
|
139
|
+
}),
|
|
140
|
+
).values(),
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function scopePredicate(scopes: ResolvedMemoryScope[]) {
|
|
145
|
+
if (scopes.length === 0) return undefined;
|
|
146
|
+
return or(
|
|
147
|
+
...scopes.map((scope) =>
|
|
148
|
+
and(
|
|
149
|
+
eq(juniorMemoryMemories.scope, scope.scope),
|
|
150
|
+
eq(juniorMemoryMemories.scopeKey, scope.scopeKey),
|
|
151
|
+
),
|
|
152
|
+
),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function utcDate(ms: number): string {
|
|
157
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
158
|
+
}
|
|
159
|
+
|
|
77
160
|
function searchTerms(query: string): string[] {
|
|
78
161
|
return [
|
|
79
162
|
...new Set(
|
|
@@ -86,20 +169,75 @@ function searchTerms(query: string): string[] {
|
|
|
86
169
|
];
|
|
87
170
|
}
|
|
88
171
|
|
|
89
|
-
|
|
172
|
+
function memoryOrigin(
|
|
173
|
+
idempotencyKey: string | null,
|
|
174
|
+
): PersonalMemoryRecord["origin"] {
|
|
175
|
+
if (idempotencyKey?.startsWith("session:")) return "automatic";
|
|
176
|
+
if (idempotencyKey?.startsWith("tool:")) return "explicit";
|
|
177
|
+
return "other";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function memoryVisibility(
|
|
181
|
+
scope: MemoryRecord["scope"],
|
|
182
|
+
): PersonalMemoryRecord["visibility"] {
|
|
183
|
+
return scope === "personal" ? "private" : "public";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function personalMemoryRecord(
|
|
187
|
+
row: typeof juniorMemoryMemories.$inferSelect,
|
|
188
|
+
): PersonalMemoryRecord {
|
|
189
|
+
const memory = parseMemoryRow(row);
|
|
190
|
+
return {
|
|
191
|
+
...memory,
|
|
192
|
+
origin: memoryOrigin(row.idempotencyKey),
|
|
193
|
+
sourcePlatform: row.sourcePlatform,
|
|
194
|
+
visibility: memoryVisibility(memory.scope),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function emptyStats(): PersonalMemoryStats {
|
|
199
|
+
return {
|
|
200
|
+
active: 0,
|
|
201
|
+
automatic: 0,
|
|
202
|
+
createdThirtyDays: 0,
|
|
203
|
+
embedded: 0,
|
|
204
|
+
explicit: 0,
|
|
205
|
+
knowledge: 0,
|
|
206
|
+
personal: 0,
|
|
207
|
+
preference: 0,
|
|
208
|
+
procedure: 0,
|
|
209
|
+
public: 0,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Build storage operations for every memory scope linked to one viewer. */
|
|
90
214
|
export function createPersonalMemoryCollection(
|
|
91
215
|
db: MemoryDb,
|
|
92
216
|
runtimeContexts: MemoryRuntimeContext[],
|
|
93
217
|
options: { now?: () => number } = {},
|
|
94
218
|
): PersonalMemoryCollection {
|
|
95
|
-
const
|
|
219
|
+
const privateScopes = personalScopes(runtimeContexts);
|
|
220
|
+
const publicScopes = publicWorkspaceScopes(runtimeContexts);
|
|
221
|
+
const allScopes = [...privateScopes, ...publicScopes];
|
|
96
222
|
const getNowMs = () => options.now?.() ?? Date.now();
|
|
97
223
|
|
|
224
|
+
function scopesForVisibility(
|
|
225
|
+
visibility: MemoryVisibility | undefined,
|
|
226
|
+
): ResolvedMemoryScope[] {
|
|
227
|
+
if (visibility === "private") return privateScopes;
|
|
228
|
+
if (visibility === "public") return publicScopes;
|
|
229
|
+
return allScopes;
|
|
230
|
+
}
|
|
231
|
+
|
|
98
232
|
return {
|
|
99
233
|
async archive(id) {
|
|
100
234
|
const memoryId = nonEmptyStringSchema.parse(id);
|
|
101
235
|
const nowMs = getNowMs();
|
|
102
|
-
|
|
236
|
+
// Forget is personal-only; public workspace memories stay shared.
|
|
237
|
+
const predicate = activeVisiblePredicate({
|
|
238
|
+
nowMs,
|
|
239
|
+
scopes: privateScopes,
|
|
240
|
+
});
|
|
103
241
|
if (!predicate) {
|
|
104
242
|
throw new PersonalMemoryNotFoundError();
|
|
105
243
|
}
|
|
@@ -123,7 +261,7 @@ export function createPersonalMemoryCollection(
|
|
|
123
261
|
async get(id) {
|
|
124
262
|
const memoryId = nonEmptyStringSchema.parse(id);
|
|
125
263
|
const nowMs = getNowMs();
|
|
126
|
-
const predicate = activeVisiblePredicate({ nowMs, scopes });
|
|
264
|
+
const predicate = activeVisiblePredicate({ nowMs, scopes: allScopes });
|
|
127
265
|
if (!predicate) {
|
|
128
266
|
throw new PersonalMemoryNotFoundError();
|
|
129
267
|
}
|
|
@@ -135,12 +273,13 @@ export function createPersonalMemoryCollection(
|
|
|
135
273
|
if (!rows[0]) {
|
|
136
274
|
throw new PersonalMemoryNotFoundError();
|
|
137
275
|
}
|
|
138
|
-
return
|
|
276
|
+
return personalMemoryRecord(rows[0]);
|
|
139
277
|
},
|
|
140
278
|
|
|
141
279
|
async list(input) {
|
|
142
280
|
input = personalMemoryPageInputSchema.parse(input);
|
|
143
281
|
const nowMs = getNowMs();
|
|
282
|
+
const scopes = scopesForVisibility(input.visibility);
|
|
144
283
|
await archiveExpiredMemoryBatch({ db, nowMs, scopes });
|
|
145
284
|
const active = activeVisiblePredicate({ nowMs, scopes });
|
|
146
285
|
if (!active) {
|
|
@@ -167,17 +306,26 @@ export function createPersonalMemoryCollection(
|
|
|
167
306
|
ilike(juniorMemoryMemories.content, `%${term}%`),
|
|
168
307
|
),
|
|
169
308
|
);
|
|
309
|
+
const kind = input.kind
|
|
310
|
+
? eq(juniorMemoryMemories.kind, input.kind)
|
|
311
|
+
: undefined;
|
|
312
|
+
const origin =
|
|
313
|
+
input.origin === "automatic"
|
|
314
|
+
? like(juniorMemoryMemories.idempotencyKey, "session:%")
|
|
315
|
+
: input.origin === "explicit"
|
|
316
|
+
? like(juniorMemoryMemories.idempotencyKey, "tool:%")
|
|
317
|
+
: undefined;
|
|
170
318
|
const rows = await db
|
|
171
319
|
.select()
|
|
172
320
|
.from(juniorMemoryMemories)
|
|
173
|
-
.where(and(active, cursor, search))
|
|
321
|
+
.where(and(active, cursor, search, kind, origin))
|
|
174
322
|
.orderBy(
|
|
175
323
|
desc(juniorMemoryMemories.createdAtMs),
|
|
176
324
|
asc(juniorMemoryMemories.id),
|
|
177
325
|
)
|
|
178
326
|
.limit(input.limit + 1);
|
|
179
327
|
const hasNextPage = rows.length > input.limit;
|
|
180
|
-
const memories = rows.slice(0, input.limit).map(
|
|
328
|
+
const memories = rows.slice(0, input.limit).map(personalMemoryRecord);
|
|
181
329
|
const last = memories.at(-1);
|
|
182
330
|
return {
|
|
183
331
|
memories,
|
|
@@ -191,5 +339,117 @@ export function createPersonalMemoryCollection(
|
|
|
191
339
|
: {}),
|
|
192
340
|
};
|
|
193
341
|
},
|
|
342
|
+
|
|
343
|
+
async stats() {
|
|
344
|
+
const nowMs = getNowMs();
|
|
345
|
+
await archiveExpiredMemoryBatch({ db, nowMs, scopes: allScopes });
|
|
346
|
+
const active = activeVisiblePredicate({ nowMs, scopes: allScopes });
|
|
347
|
+
if (!active) {
|
|
348
|
+
return emptyStats();
|
|
349
|
+
}
|
|
350
|
+
const [counts] = await db
|
|
351
|
+
.select({
|
|
352
|
+
active: sql<number>`count(*)`.mapWith(Number),
|
|
353
|
+
automatic:
|
|
354
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.idempotencyKey} like 'session:%')`.mapWith(
|
|
355
|
+
Number,
|
|
356
|
+
),
|
|
357
|
+
createdThirtyDays:
|
|
358
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.createdAtMs} >= ${nowMs - 30 * 24 * 60 * 60 * 1_000})`.mapWith(
|
|
359
|
+
Number,
|
|
360
|
+
),
|
|
361
|
+
embedded:
|
|
362
|
+
sql<number>`count(${juniorMemoryEmbeddings.memoryId})`.mapWith(
|
|
363
|
+
Number,
|
|
364
|
+
),
|
|
365
|
+
explicit:
|
|
366
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.idempotencyKey} like 'tool:%')`.mapWith(
|
|
367
|
+
Number,
|
|
368
|
+
),
|
|
369
|
+
knowledge:
|
|
370
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'knowledge')`.mapWith(
|
|
371
|
+
Number,
|
|
372
|
+
),
|
|
373
|
+
personal:
|
|
374
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.scope} = 'personal')`.mapWith(
|
|
375
|
+
Number,
|
|
376
|
+
),
|
|
377
|
+
preference:
|
|
378
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'preference')`.mapWith(
|
|
379
|
+
Number,
|
|
380
|
+
),
|
|
381
|
+
procedure:
|
|
382
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'procedure')`.mapWith(
|
|
383
|
+
Number,
|
|
384
|
+
),
|
|
385
|
+
public:
|
|
386
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.scope} = 'conversation')`.mapWith(
|
|
387
|
+
Number,
|
|
388
|
+
),
|
|
389
|
+
})
|
|
390
|
+
.from(juniorMemoryMemories)
|
|
391
|
+
.leftJoin(
|
|
392
|
+
juniorMemoryEmbeddings,
|
|
393
|
+
eq(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id),
|
|
394
|
+
)
|
|
395
|
+
.where(active);
|
|
396
|
+
return {
|
|
397
|
+
active: counts?.active ?? 0,
|
|
398
|
+
automatic: counts?.automatic ?? 0,
|
|
399
|
+
createdThirtyDays: counts?.createdThirtyDays ?? 0,
|
|
400
|
+
embedded: counts?.embedded ?? 0,
|
|
401
|
+
explicit: counts?.explicit ?? 0,
|
|
402
|
+
knowledge: counts?.knowledge ?? 0,
|
|
403
|
+
personal: counts?.personal ?? 0,
|
|
404
|
+
preference: counts?.preference ?? 0,
|
|
405
|
+
procedure: counts?.procedure ?? 0,
|
|
406
|
+
public: counts?.public ?? 0,
|
|
407
|
+
};
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
async timeline(input) {
|
|
411
|
+
input = personalMemoryTimelineInputSchema.parse(input);
|
|
412
|
+
const todayMs = Date.parse(`${utcDate(getNowMs())}T00:00:00.000Z`);
|
|
413
|
+
const startMs = todayMs - (input.days - 1) * DAY_MS;
|
|
414
|
+
const ownership = scopePredicate(allScopes);
|
|
415
|
+
if (!ownership) {
|
|
416
|
+
return Array.from({ length: input.days }, (_, index) => ({
|
|
417
|
+
date: utcDate(startMs + index * DAY_MS),
|
|
418
|
+
personal: 0,
|
|
419
|
+
public: 0,
|
|
420
|
+
}));
|
|
421
|
+
}
|
|
422
|
+
const rows = await db
|
|
423
|
+
.select({
|
|
424
|
+
date: sql<string>`to_char(to_timestamp(${juniorMemoryMemories.createdAtMs} / 1000.0) AT TIME ZONE 'UTC', 'YYYY-MM-DD')`.as(
|
|
425
|
+
"date",
|
|
426
|
+
),
|
|
427
|
+
personal:
|
|
428
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.scope} = 'personal')`.mapWith(
|
|
429
|
+
Number,
|
|
430
|
+
),
|
|
431
|
+
public:
|
|
432
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.scope} = 'conversation')`.mapWith(
|
|
433
|
+
Number,
|
|
434
|
+
),
|
|
435
|
+
})
|
|
436
|
+
.from(juniorMemoryMemories)
|
|
437
|
+
.where(
|
|
438
|
+
and(ownership, gt(juniorMemoryMemories.createdAtMs, startMs - 1)),
|
|
439
|
+
)
|
|
440
|
+
.groupBy(
|
|
441
|
+
sql`to_char(to_timestamp(${juniorMemoryMemories.createdAtMs} / 1000.0) AT TIME ZONE 'UTC', 'YYYY-MM-DD')`,
|
|
442
|
+
);
|
|
443
|
+
const byDate = new Map(rows.map((row) => [row.date, row]));
|
|
444
|
+
return Array.from({ length: input.days }, (_, index) => {
|
|
445
|
+
const date = utcDate(startMs + index * DAY_MS);
|
|
446
|
+
const row = byDate.get(date);
|
|
447
|
+
return {
|
|
448
|
+
date,
|
|
449
|
+
personal: row?.personal ?? 0,
|
|
450
|
+
public: row?.public ?? 0,
|
|
451
|
+
};
|
|
452
|
+
});
|
|
453
|
+
},
|
|
194
454
|
};
|
|
195
455
|
}
|
package/src/personal.ts
CHANGED
|
@@ -2,32 +2,43 @@
|
|
|
2
2
|
* Authenticated-viewer memory access shared by REST and dashboard projections.
|
|
3
3
|
*
|
|
4
4
|
* Viewer identity may resolve to multiple platform actors. This module keeps
|
|
5
|
-
* that federation behind one
|
|
5
|
+
* that federation behind one viewer-memory collection spanning personal and
|
|
6
|
+
* authorized public workspace scopes.
|
|
6
7
|
*/
|
|
7
8
|
import { z } from "zod";
|
|
8
9
|
import type { PluginUserPageActor } from "@sentry/junior-plugin-api";
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
createPersonalMemoryCollection,
|
|
12
|
+
type MemoryVisibility,
|
|
13
|
+
type PersonalMemoryRecord,
|
|
14
|
+
} from "./personal-store";
|
|
10
15
|
import type { MemoryDb, MemoryRecord } from "./store";
|
|
11
|
-
import type { MemoryRuntimeContext } from "./types";
|
|
16
|
+
import type { MemoryKind, MemoryRuntimeContext } from "./types";
|
|
12
17
|
|
|
13
18
|
const cursorSchema = z
|
|
14
19
|
.object({
|
|
15
20
|
createdAtMs: z.number().finite(),
|
|
16
21
|
id: z.string().min(1),
|
|
22
|
+
kind: z.enum(["preference", "procedure", "knowledge"]).optional(),
|
|
23
|
+
origin: z.enum(["automatic", "explicit"]).optional(),
|
|
17
24
|
query: z.string().max(200).optional(),
|
|
18
25
|
version: z.literal(1),
|
|
26
|
+
visibility: z.enum(["private", "public"]).optional(),
|
|
19
27
|
})
|
|
20
28
|
.strict();
|
|
21
29
|
|
|
22
30
|
export interface ViewerMemoryPage {
|
|
23
|
-
memories:
|
|
31
|
+
memories: PersonalMemoryRecord[];
|
|
24
32
|
nextCursor?: string;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
export interface ViewerMemoryPageInput {
|
|
28
36
|
cursor?: string;
|
|
37
|
+
kind?: MemoryKind;
|
|
29
38
|
limit: number;
|
|
39
|
+
origin?: "automatic" | "explicit";
|
|
30
40
|
query?: string;
|
|
41
|
+
visibility?: MemoryVisibility;
|
|
31
42
|
}
|
|
32
43
|
|
|
33
44
|
export class InvalidMemoryCursorError extends Error {
|
|
@@ -38,6 +49,7 @@ export class InvalidMemoryCursorError extends Error {
|
|
|
38
49
|
}
|
|
39
50
|
|
|
40
51
|
export { PersonalMemoryNotFoundError } from "./personal-store";
|
|
52
|
+
export type { MemoryVisibility, PersonalMemoryRecord } from "./personal-store";
|
|
41
53
|
|
|
42
54
|
function runtimeContext(actor: PluginUserPageActor): MemoryRuntimeContext {
|
|
43
55
|
if (actor.platform === "slack") {
|
|
@@ -61,13 +73,21 @@ function runtimeContext(actor: PluginUserPageActor): MemoryRuntimeContext {
|
|
|
61
73
|
};
|
|
62
74
|
}
|
|
63
75
|
|
|
64
|
-
function decodeCursor(
|
|
76
|
+
function decodeCursor(
|
|
77
|
+
value: string | undefined,
|
|
78
|
+
input: Pick<ViewerMemoryPageInput, "kind" | "origin" | "query" | "visibility">,
|
|
79
|
+
) {
|
|
65
80
|
if (!value) return undefined;
|
|
66
81
|
try {
|
|
67
82
|
const parsed = cursorSchema.parse(
|
|
68
83
|
JSON.parse(Buffer.from(value, "base64url").toString("utf8")),
|
|
69
84
|
);
|
|
70
|
-
if (
|
|
85
|
+
if (
|
|
86
|
+
parsed.query !== input.query ||
|
|
87
|
+
parsed.kind !== input.kind ||
|
|
88
|
+
parsed.origin !== input.origin ||
|
|
89
|
+
parsed.visibility !== input.visibility
|
|
90
|
+
) {
|
|
71
91
|
throw new InvalidMemoryCursorError();
|
|
72
92
|
}
|
|
73
93
|
return { createdAtMs: parsed.createdAtMs, id: parsed.id };
|
|
@@ -78,15 +98,22 @@ function decodeCursor(value: string | undefined, query: string | undefined) {
|
|
|
78
98
|
|
|
79
99
|
function encodeCursor(
|
|
80
100
|
cursor: { createdAtMs: number; id: string },
|
|
81
|
-
|
|
101
|
+
input: Pick<ViewerMemoryPageInput, "kind" | "origin" | "query" | "visibility">,
|
|
82
102
|
): string {
|
|
83
103
|
return Buffer.from(
|
|
84
|
-
JSON.stringify({
|
|
104
|
+
JSON.stringify({
|
|
105
|
+
...cursor,
|
|
106
|
+
...(input.query ? { query: input.query } : {}),
|
|
107
|
+
...(input.kind ? { kind: input.kind } : {}),
|
|
108
|
+
...(input.origin ? { origin: input.origin } : {}),
|
|
109
|
+
...(input.visibility ? { visibility: input.visibility } : {}),
|
|
110
|
+
version: 1,
|
|
111
|
+
}),
|
|
85
112
|
"utf8",
|
|
86
113
|
).toString("base64url");
|
|
87
114
|
}
|
|
88
115
|
|
|
89
|
-
/** Build
|
|
116
|
+
/** Build viewer memory operations authorized by a viewer's linked actors. */
|
|
90
117
|
export function createViewerMemories(
|
|
91
118
|
db: MemoryDb,
|
|
92
119
|
actors: PluginUserPageActor[],
|
|
@@ -99,22 +126,34 @@ export function createViewerMemories(
|
|
|
99
126
|
async archive(id: string): Promise<MemoryRecord> {
|
|
100
127
|
return await collection.archive(id);
|
|
101
128
|
},
|
|
102
|
-
async get(id: string): Promise<
|
|
129
|
+
async get(id: string): Promise<PersonalMemoryRecord> {
|
|
103
130
|
return await collection.get(id);
|
|
104
131
|
},
|
|
105
132
|
async list(input: ViewerMemoryPageInput): Promise<ViewerMemoryPage> {
|
|
106
133
|
const query = input.query?.trim() || undefined;
|
|
134
|
+
const filters = {
|
|
135
|
+
...(input.kind ? { kind: input.kind } : {}),
|
|
136
|
+
...(input.origin ? { origin: input.origin } : {}),
|
|
137
|
+
...(query ? { query } : {}),
|
|
138
|
+
...(input.visibility ? { visibility: input.visibility } : {}),
|
|
139
|
+
};
|
|
107
140
|
const page = await collection.list({
|
|
108
|
-
cursor: decodeCursor(input.cursor,
|
|
141
|
+
cursor: decodeCursor(input.cursor, filters),
|
|
142
|
+
...filters,
|
|
109
143
|
limit: input.limit,
|
|
110
|
-
...(query ? { query } : {}),
|
|
111
144
|
});
|
|
112
145
|
return {
|
|
113
146
|
memories: page.memories,
|
|
114
147
|
...(page.nextCursor
|
|
115
|
-
? { nextCursor: encodeCursor(page.nextCursor,
|
|
148
|
+
? { nextCursor: encodeCursor(page.nextCursor, filters) }
|
|
116
149
|
: {}),
|
|
117
150
|
};
|
|
118
151
|
},
|
|
152
|
+
async stats() {
|
|
153
|
+
return await collection.stats();
|
|
154
|
+
},
|
|
155
|
+
async timeline(input: { days: number }) {
|
|
156
|
+
return await collection.timeline(input);
|
|
157
|
+
},
|
|
119
158
|
};
|
|
120
159
|
}
|
package/src/user-pages.ts
CHANGED
|
@@ -1,56 +1,104 @@
|
|
|
1
|
-
/** Project viewer-
|
|
1
|
+
/** Project viewer-visible memories into Junior's core-rendered user page. */
|
|
2
2
|
import type { PluginUserPageDefinition } from "@sentry/junior-plugin-api";
|
|
3
3
|
import { createViewerMemories } from "./personal";
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
MemoryVisibility,
|
|
6
|
+
PersonalMemoryRecord,
|
|
7
|
+
} from "./personal-store";
|
|
8
|
+
import type { MemoryDb } from "./store";
|
|
5
9
|
|
|
6
|
-
function
|
|
7
|
-
return
|
|
10
|
+
function titleCase(value: string): string {
|
|
11
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
8
12
|
}
|
|
9
13
|
|
|
10
14
|
function rememberedDate(createdAtMs: number): string {
|
|
11
15
|
return new Intl.DateTimeFormat("en-US", {
|
|
12
16
|
dateStyle: "medium",
|
|
17
|
+
timeStyle: "short",
|
|
13
18
|
timeZone: "UTC",
|
|
14
19
|
}).format(new Date(createdAtMs));
|
|
15
20
|
}
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
function originLabel(origin: PersonalMemoryRecord["origin"]): string {
|
|
23
|
+
if (origin === "automatic") return "Automatic";
|
|
24
|
+
if (origin === "explicit") return "Explicit";
|
|
25
|
+
return "Other";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function visibilityLabel(visibility: MemoryVisibility): string {
|
|
29
|
+
return visibility === "public" ? "Public" : "Private";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function pageFilter(filter: string | undefined): {
|
|
33
|
+
visibility?: MemoryVisibility;
|
|
34
|
+
} {
|
|
35
|
+
if (filter === "private") return { visibility: "private" };
|
|
36
|
+
if (filter === "public") return { visibility: "public" };
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function pageEmptyText(input: { filter?: string; query?: string }): string {
|
|
41
|
+
if (input.query) return "No memories matched your search.";
|
|
42
|
+
if (input.filter === "private") return "No private memories yet.";
|
|
43
|
+
if (input.filter === "public") return "No public memories yet.";
|
|
44
|
+
return "No memories yet.";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Create the interactive Memories dashboard page. */
|
|
18
48
|
export function createMemoryUserPage(): PluginUserPageDefinition {
|
|
19
49
|
return {
|
|
20
50
|
id: "memories",
|
|
21
51
|
label: "Memories",
|
|
22
|
-
|
|
52
|
+
navigation: "primary",
|
|
53
|
+
description:
|
|
54
|
+
"Personal and public memories Junior can use across conversations.",
|
|
23
55
|
async read(ctx, input) {
|
|
24
|
-
const
|
|
56
|
+
const memories = createViewerMemories(
|
|
25
57
|
ctx.db as MemoryDb,
|
|
26
58
|
ctx.viewer.actors,
|
|
27
|
-
)
|
|
59
|
+
);
|
|
60
|
+
const page = await memories.list({
|
|
28
61
|
cursor: input.cursor,
|
|
62
|
+
...pageFilter(input.filter),
|
|
29
63
|
limit: input.limit,
|
|
30
64
|
...(input.query ? { query: input.query } : {}),
|
|
31
65
|
});
|
|
32
66
|
return {
|
|
33
67
|
type: "list",
|
|
34
|
-
emptyText: input
|
|
35
|
-
? "No memories matched your search."
|
|
36
|
-
: "No personal memories yet.",
|
|
68
|
+
emptyText: pageEmptyText(input),
|
|
37
69
|
...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
|
|
38
70
|
searchPlaceholder: "Search memories",
|
|
39
71
|
records: page.memories.map((memory) => ({
|
|
40
|
-
actions:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
72
|
+
actions:
|
|
73
|
+
memory.visibility === "private"
|
|
74
|
+
? [
|
|
75
|
+
{
|
|
76
|
+
confirmation: "Forget this memory?",
|
|
77
|
+
href: `/api/plugins/memory/memories/${encodeURIComponent(memory.id)}`,
|
|
78
|
+
label: "Forget",
|
|
79
|
+
method: "DELETE" as const,
|
|
80
|
+
tone: "danger" as const,
|
|
81
|
+
},
|
|
82
|
+
]
|
|
83
|
+
: [],
|
|
49
84
|
id: memory.id,
|
|
50
85
|
title: memory.content,
|
|
51
86
|
metadata: [
|
|
52
|
-
{ label: "Type", value:
|
|
87
|
+
{ label: "Type", value: titleCase(memory.kind) },
|
|
88
|
+
{ label: "Learned", value: originLabel(memory.origin) },
|
|
89
|
+
{ label: "Source", value: titleCase(memory.sourcePlatform) },
|
|
90
|
+
{
|
|
91
|
+
label: "Visibility",
|
|
92
|
+
value: visibilityLabel(memory.visibility),
|
|
93
|
+
},
|
|
53
94
|
{ label: "Remembered", value: rememberedDate(memory.createdAtMs) },
|
|
95
|
+
{ label: "Observed", value: rememberedDate(memory.observedAtMs) },
|
|
96
|
+
{
|
|
97
|
+
label: "Expires",
|
|
98
|
+
value: memory.expiresAtMs
|
|
99
|
+
? rememberedDate(memory.expiresAtMs)
|
|
100
|
+
: "Never",
|
|
101
|
+
},
|
|
54
102
|
],
|
|
55
103
|
})),
|
|
56
104
|
};
|