@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.
@@ -1,10 +1,10 @@
1
1
  /**
2
- * SQL operations over personal memories owned by one authenticated viewer.
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 without treating one actor as the canonical identity.
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 { MemoryRuntimeContext } from "./types";
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: MemoryRecord[];
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 personal memory operations shared by dashboard and REST. */
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 personal memory owned by a linked actor. */
59
- get(id: string): Promise<MemoryRecord>;
60
- /** List one stable page across every linked personal scope. */
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
- /** Build storage operations for every personal scope linked to one viewer. */
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 scopes = personalScopes(runtimeContexts);
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
- const predicate = activeVisiblePredicate({ nowMs, scopes });
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 parseMemoryRow(rows[0]);
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(parseMemoryRow);
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 personal-memory collection.
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 { createPersonalMemoryCollection } from "./personal-store";
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: MemoryRecord[];
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(value: string | undefined, query: string | undefined) {
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 (parsed.query !== query) {
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
- query: string | undefined,
101
+ input: Pick<ViewerMemoryPageInput, "kind" | "origin" | "query" | "visibility">,
82
102
  ): string {
83
103
  return Buffer.from(
84
- JSON.stringify({ ...cursor, ...(query ? { query } : {}), version: 1 }),
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 personal-memory operations authorized by a viewer's linked actors. */
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<MemoryRecord> {
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, query),
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, query) }
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-owned memories into Junior's core-rendered user page. */
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 { MemoryDb, MemoryRecord } from "./store";
4
+ import type {
5
+ MemoryVisibility,
6
+ PersonalMemoryRecord,
7
+ } from "./personal-store";
8
+ import type { MemoryDb } from "./store";
5
9
 
6
- function memoryKindLabel(kind: MemoryRecord["kind"]): string {
7
- return kind.charAt(0).toUpperCase() + kind.slice(1);
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
- /** Create the interactive personal Memories dashboard page. */
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
- description: "Personal facts and preferences Junior remembers about you.",
52
+ navigation: "primary",
53
+ description:
54
+ "Personal and public memories Junior can use across conversations.",
23
55
  async read(ctx, input) {
24
- const page = await createViewerMemories(
56
+ const memories = createViewerMemories(
25
57
  ctx.db as MemoryDb,
26
58
  ctx.viewer.actors,
27
- ).list({
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.query
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
- confirmation: "Forget this memory?",
43
- href: `/api/plugins/memory/memories/${encodeURIComponent(memory.id)}`,
44
- label: "Forget",
45
- method: "DELETE" as const,
46
- tone: "danger" as const,
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: memoryKindLabel(memory.kind) },
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
  };