@sentry/junior-memory 0.124.0 → 0.124.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.
- package/dist/api.d.ts +20 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +240 -23
- package/dist/index.js.map +1 -1
- package/dist/personal-store.d.ts +40 -2
- package/dist/personal.d.ts +9 -1
- package/package.json +2 -2
- package/src/api.ts +54 -1
- package/src/index.ts +2 -0
- package/src/personal-store.ts +203 -5
- package/src/personal.ts +41 -10
- package/src/user-pages.ts +46 -9
package/dist/personal-store.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { type MemoryDb, type MemoryRecord } from "./store";
|
|
3
|
-
import type
|
|
3
|
+
import { type MemoryRuntimeContext } from "./types";
|
|
4
4
|
declare const personalMemoryCursorSchema: z.ZodObject<{
|
|
5
5
|
createdAtMs: z.ZodNumber;
|
|
6
6
|
id: z.ZodString;
|
|
@@ -10,15 +10,47 @@ declare const personalMemoryPageInputSchema: z.ZodObject<{
|
|
|
10
10
|
createdAtMs: z.ZodNumber;
|
|
11
11
|
id: z.ZodString;
|
|
12
12
|
}, z.core.$strict>>;
|
|
13
|
+
kind: z.ZodOptional<z.ZodEnum<{
|
|
14
|
+
preference: "preference";
|
|
15
|
+
procedure: "procedure";
|
|
16
|
+
knowledge: "knowledge";
|
|
17
|
+
}>>;
|
|
13
18
|
limit: z.ZodNumber;
|
|
19
|
+
origin: z.ZodOptional<z.ZodEnum<{
|
|
20
|
+
automatic: "automatic";
|
|
21
|
+
explicit: "explicit";
|
|
22
|
+
}>>;
|
|
14
23
|
query: z.ZodOptional<z.ZodString>;
|
|
15
24
|
}, z.core.$strict>;
|
|
16
25
|
export type PersonalMemoryCursor = z.output<typeof personalMemoryCursorSchema>;
|
|
17
26
|
export type PersonalMemoryPageInput = z.output<typeof personalMemoryPageInputSchema>;
|
|
18
27
|
export interface PersonalMemoryPage {
|
|
19
|
-
memories:
|
|
28
|
+
memories: PersonalMemoryRecord[];
|
|
20
29
|
nextCursor?: PersonalMemoryCursor;
|
|
21
30
|
}
|
|
31
|
+
/** Safe provenance attached to one viewer-owned personal memory. */
|
|
32
|
+
export type PersonalMemoryRecord = MemoryRecord & {
|
|
33
|
+
origin: "automatic" | "explicit" | "other";
|
|
34
|
+
sourcePlatform: "local" | "slack";
|
|
35
|
+
};
|
|
36
|
+
/** Viewer-scoped active memory totals used by the dashboard. */
|
|
37
|
+
export interface PersonalMemoryStats {
|
|
38
|
+
active: number;
|
|
39
|
+
automatic: number;
|
|
40
|
+
createdThirtyDays: number;
|
|
41
|
+
embedded: number;
|
|
42
|
+
explicit: number;
|
|
43
|
+
knowledge: number;
|
|
44
|
+
preference: number;
|
|
45
|
+
procedure: number;
|
|
46
|
+
}
|
|
47
|
+
/** Viewer-scoped memory creation totals for one UTC calendar day. */
|
|
48
|
+
export interface PersonalMemoryDay {
|
|
49
|
+
date: string;
|
|
50
|
+
knowledge: number;
|
|
51
|
+
preference: number;
|
|
52
|
+
procedure: number;
|
|
53
|
+
}
|
|
22
54
|
/** Expected failure when a viewer does not own the requested memory. */
|
|
23
55
|
export declare class PersonalMemoryNotFoundError extends Error {
|
|
24
56
|
constructor();
|
|
@@ -31,6 +63,12 @@ export interface PersonalMemoryCollection {
|
|
|
31
63
|
get(id: string): Promise<MemoryRecord>;
|
|
32
64
|
/** List one stable page across every linked personal scope. */
|
|
33
65
|
list(input: PersonalMemoryPageInput): Promise<PersonalMemoryPage>;
|
|
66
|
+
/** Summarize active memories across every linked personal scope. */
|
|
67
|
+
stats(): Promise<PersonalMemoryStats>;
|
|
68
|
+
/** Read memory creation history across every linked personal scope. */
|
|
69
|
+
timeline(input: {
|
|
70
|
+
days: number;
|
|
71
|
+
}): Promise<PersonalMemoryDay[]>;
|
|
34
72
|
}
|
|
35
73
|
/** Build storage operations for every personal scope linked to one viewer. */
|
|
36
74
|
export declare function createPersonalMemoryCollection(db: MemoryDb, runtimeContexts: MemoryRuntimeContext[], options?: {
|
package/dist/personal.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import type { PluginUserPageActor } from "@sentry/junior-plugin-api";
|
|
2
|
+
import { type PersonalMemoryRecord } from "./personal-store";
|
|
2
3
|
import type { MemoryDb, MemoryRecord } from "./store";
|
|
4
|
+
import type { MemoryKind } from "./types";
|
|
3
5
|
export interface ViewerMemoryPage {
|
|
4
|
-
memories:
|
|
6
|
+
memories: PersonalMemoryRecord[];
|
|
5
7
|
nextCursor?: string;
|
|
6
8
|
}
|
|
7
9
|
export interface ViewerMemoryPageInput {
|
|
8
10
|
cursor?: string;
|
|
11
|
+
kind?: MemoryKind;
|
|
9
12
|
limit: number;
|
|
13
|
+
origin?: "automatic" | "explicit";
|
|
10
14
|
query?: string;
|
|
11
15
|
}
|
|
12
16
|
export declare class InvalidMemoryCursorError extends Error {
|
|
@@ -18,4 +22,8 @@ export declare function createViewerMemories(db: MemoryDb, actors: PluginUserPag
|
|
|
18
22
|
archive(id: string): Promise<MemoryRecord>;
|
|
19
23
|
get(id: string): Promise<MemoryRecord>;
|
|
20
24
|
list(input: ViewerMemoryPageInput): Promise<ViewerMemoryPage>;
|
|
25
|
+
stats(): Promise<import("./personal-store").PersonalMemoryStats>;
|
|
26
|
+
timeline(input: {
|
|
27
|
+
days: number;
|
|
28
|
+
}): Promise<import("./personal-store").PersonalMemoryDay[]>;
|
|
21
29
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-memory",
|
|
3
|
-
"version": "0.124.
|
|
3
|
+
"version": "0.124.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"commander": "^14.0.3",
|
|
28
28
|
"drizzle-orm": "^0.45.2",
|
|
29
29
|
"zod": "^4.4.3",
|
|
30
|
-
"@sentry/junior-plugin-api": "0.124.
|
|
30
|
+
"@sentry/junior-plugin-api": "0.124.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^25.9.1",
|
package/src/api.ts
CHANGED
|
@@ -36,7 +36,38 @@ export const memoryListResponseSchema = z
|
|
|
36
36
|
})
|
|
37
37
|
.strict();
|
|
38
38
|
|
|
39
|
+
const memoryDashboardDaySchema = z
|
|
40
|
+
.object({
|
|
41
|
+
date: z.iso.date(),
|
|
42
|
+
knowledge: z.number().int().min(0),
|
|
43
|
+
preference: z.number().int().min(0),
|
|
44
|
+
procedure: z.number().int().min(0),
|
|
45
|
+
})
|
|
46
|
+
.strict();
|
|
47
|
+
|
|
48
|
+
export const memoryDashboardResponseSchema = z
|
|
49
|
+
.object({
|
|
50
|
+
days: z.array(memoryDashboardDaySchema).length(90),
|
|
51
|
+
generatedAt: z.iso.datetime(),
|
|
52
|
+
stats: z
|
|
53
|
+
.object({
|
|
54
|
+
active: z.number().int().min(0),
|
|
55
|
+
automatic: z.number().int().min(0),
|
|
56
|
+
createdThirtyDays: z.number().int().min(0),
|
|
57
|
+
embedded: z.number().int().min(0),
|
|
58
|
+
explicit: z.number().int().min(0),
|
|
59
|
+
knowledge: z.number().int().min(0),
|
|
60
|
+
preference: z.number().int().min(0),
|
|
61
|
+
procedure: z.number().int().min(0),
|
|
62
|
+
})
|
|
63
|
+
.strict(),
|
|
64
|
+
})
|
|
65
|
+
.strict();
|
|
66
|
+
|
|
39
67
|
export type MemoryApi = z.output<typeof memoryApiSchema>;
|
|
68
|
+
export type MemoryDashboardResponse = z.output<
|
|
69
|
+
typeof memoryDashboardResponseSchema
|
|
70
|
+
>;
|
|
40
71
|
export type MemoryListResponse = z.output<typeof memoryListResponseSchema>;
|
|
41
72
|
|
|
42
73
|
const memoryListQuerySchema = z
|
|
@@ -94,13 +125,35 @@ export function createMemoryApi(options: MemoryApiOptions): PluginRouteApp {
|
|
|
94
125
|
const url = new URL(request.url);
|
|
95
126
|
const memoryPath = /^\/memories\/([^/]+)$/.exec(url.pathname);
|
|
96
127
|
const isCollection = url.pathname === "/memories";
|
|
97
|
-
|
|
128
|
+
const isDashboard = url.pathname === "/dashboard";
|
|
129
|
+
if (!isCollection && !isDashboard && !memoryPath) {
|
|
98
130
|
return json({ error: "Not found." }, 404);
|
|
99
131
|
}
|
|
100
132
|
|
|
101
133
|
const actors = await options.actors(email);
|
|
102
134
|
const memories = createViewerMemories(options.db, actors);
|
|
103
135
|
try {
|
|
136
|
+
if (
|
|
137
|
+
isDashboard &&
|
|
138
|
+
(request.method === "GET" || request.method === "HEAD")
|
|
139
|
+
) {
|
|
140
|
+
const [stats, days] = await Promise.all([
|
|
141
|
+
memories.stats(),
|
|
142
|
+
memories.timeline({ days: 90 }),
|
|
143
|
+
]);
|
|
144
|
+
const body = memoryDashboardResponseSchema.parse({
|
|
145
|
+
days,
|
|
146
|
+
generatedAt: new Date().toISOString(),
|
|
147
|
+
stats,
|
|
148
|
+
});
|
|
149
|
+
return request.method === "HEAD"
|
|
150
|
+
? new Response(null, {
|
|
151
|
+
headers: { "cache-control": "no-store" },
|
|
152
|
+
status: 200,
|
|
153
|
+
})
|
|
154
|
+
: json(body);
|
|
155
|
+
}
|
|
156
|
+
|
|
104
157
|
if (
|
|
105
158
|
isCollection &&
|
|
106
159
|
(request.method === "GET" || request.method === "HEAD")
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export { memoryPlugin } from "./plugin";
|
|
2
2
|
export {
|
|
3
3
|
memoryApiSchema,
|
|
4
|
+
memoryDashboardResponseSchema,
|
|
4
5
|
memoryListResponseSchema,
|
|
5
6
|
type MemoryApi,
|
|
7
|
+
type MemoryDashboardResponse,
|
|
6
8
|
type MemoryListResponse,
|
|
7
9
|
} from "./api";
|
|
8
10
|
export type { MemoryPluginOptions } from "./plugin";
|
package/src/personal-store.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* A viewer may resolve to several runtime actors. This store combines their
|
|
5
5
|
* personal scopes without treating one actor as the canonical identity.
|
|
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,7 +15,7 @@ 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
21
|
const personalMemoryCursorSchema = z
|
|
@@ -27,10 +27,18 @@ const personalMemoryCursorSchema = z
|
|
|
27
27
|
const personalMemoryPageInputSchema = z
|
|
28
28
|
.object({
|
|
29
29
|
cursor: personalMemoryCursorSchema.optional(),
|
|
30
|
+
kind: z.enum(MEMORY_KINDS).optional(),
|
|
30
31
|
limit: z.number().int().min(1).max(50),
|
|
32
|
+
origin: z.enum(["automatic", "explicit"]).optional(),
|
|
31
33
|
query: z.string().max(200).optional(),
|
|
32
34
|
})
|
|
33
35
|
.strict();
|
|
36
|
+
const personalMemoryTimelineInputSchema = z
|
|
37
|
+
.object({
|
|
38
|
+
days: z.number().int().min(1).max(365),
|
|
39
|
+
})
|
|
40
|
+
.strict();
|
|
41
|
+
const DAY_MS = 24 * 60 * 60 * 1_000;
|
|
34
42
|
|
|
35
43
|
export type PersonalMemoryCursor = z.output<typeof personalMemoryCursorSchema>;
|
|
36
44
|
|
|
@@ -39,10 +47,36 @@ export type PersonalMemoryPageInput = z.output<
|
|
|
39
47
|
>;
|
|
40
48
|
|
|
41
49
|
export interface PersonalMemoryPage {
|
|
42
|
-
memories:
|
|
50
|
+
memories: PersonalMemoryRecord[];
|
|
43
51
|
nextCursor?: PersonalMemoryCursor;
|
|
44
52
|
}
|
|
45
53
|
|
|
54
|
+
/** Safe provenance attached to one viewer-owned personal memory. */
|
|
55
|
+
export type PersonalMemoryRecord = MemoryRecord & {
|
|
56
|
+
origin: "automatic" | "explicit" | "other";
|
|
57
|
+
sourcePlatform: "local" | "slack";
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Viewer-scoped active memory totals used by the dashboard. */
|
|
61
|
+
export interface PersonalMemoryStats {
|
|
62
|
+
active: number;
|
|
63
|
+
automatic: number;
|
|
64
|
+
createdThirtyDays: number;
|
|
65
|
+
embedded: number;
|
|
66
|
+
explicit: number;
|
|
67
|
+
knowledge: number;
|
|
68
|
+
preference: number;
|
|
69
|
+
procedure: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Viewer-scoped memory creation totals for one UTC calendar day. */
|
|
73
|
+
export interface PersonalMemoryDay {
|
|
74
|
+
date: string;
|
|
75
|
+
knowledge: number;
|
|
76
|
+
preference: number;
|
|
77
|
+
procedure: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
46
80
|
/** Expected failure when a viewer does not own the requested memory. */
|
|
47
81
|
export class PersonalMemoryNotFoundError extends Error {
|
|
48
82
|
constructor() {
|
|
@@ -59,6 +93,10 @@ export interface PersonalMemoryCollection {
|
|
|
59
93
|
get(id: string): Promise<MemoryRecord>;
|
|
60
94
|
/** List one stable page across every linked personal scope. */
|
|
61
95
|
list(input: PersonalMemoryPageInput): Promise<PersonalMemoryPage>;
|
|
96
|
+
/** Summarize active memories across every linked personal scope. */
|
|
97
|
+
stats(): Promise<PersonalMemoryStats>;
|
|
98
|
+
/** Read memory creation history across every linked personal scope. */
|
|
99
|
+
timeline(input: { days: number }): Promise<PersonalMemoryDay[]>;
|
|
62
100
|
}
|
|
63
101
|
|
|
64
102
|
function personalScopes(
|
|
@@ -74,6 +112,22 @@ function personalScopes(
|
|
|
74
112
|
];
|
|
75
113
|
}
|
|
76
114
|
|
|
115
|
+
function personalScopePredicate(scopes: ResolvedMemoryScope[]) {
|
|
116
|
+
if (scopes.length === 0) return undefined;
|
|
117
|
+
return or(
|
|
118
|
+
...scopes.map((scope) =>
|
|
119
|
+
and(
|
|
120
|
+
eq(juniorMemoryMemories.scope, scope.scope),
|
|
121
|
+
eq(juniorMemoryMemories.scopeKey, scope.scopeKey),
|
|
122
|
+
),
|
|
123
|
+
),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function utcDate(ms: number): string {
|
|
128
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
129
|
+
}
|
|
130
|
+
|
|
77
131
|
function searchTerms(query: string): string[] {
|
|
78
132
|
return [
|
|
79
133
|
...new Set(
|
|
@@ -86,6 +140,24 @@ function searchTerms(query: string): string[] {
|
|
|
86
140
|
];
|
|
87
141
|
}
|
|
88
142
|
|
|
143
|
+
function memoryOrigin(
|
|
144
|
+
idempotencyKey: string | null,
|
|
145
|
+
): PersonalMemoryRecord["origin"] {
|
|
146
|
+
if (idempotencyKey?.startsWith("session:")) return "automatic";
|
|
147
|
+
if (idempotencyKey?.startsWith("tool:")) return "explicit";
|
|
148
|
+
return "other";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function personalMemoryRecord(
|
|
152
|
+
row: typeof juniorMemoryMemories.$inferSelect,
|
|
153
|
+
): PersonalMemoryRecord {
|
|
154
|
+
return {
|
|
155
|
+
...parseMemoryRow(row),
|
|
156
|
+
origin: memoryOrigin(row.idempotencyKey),
|
|
157
|
+
sourcePlatform: row.sourcePlatform,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
89
161
|
/** Build storage operations for every personal scope linked to one viewer. */
|
|
90
162
|
export function createPersonalMemoryCollection(
|
|
91
163
|
db: MemoryDb,
|
|
@@ -167,17 +239,26 @@ export function createPersonalMemoryCollection(
|
|
|
167
239
|
ilike(juniorMemoryMemories.content, `%${term}%`),
|
|
168
240
|
),
|
|
169
241
|
);
|
|
242
|
+
const kind = input.kind
|
|
243
|
+
? eq(juniorMemoryMemories.kind, input.kind)
|
|
244
|
+
: undefined;
|
|
245
|
+
const origin =
|
|
246
|
+
input.origin === "automatic"
|
|
247
|
+
? like(juniorMemoryMemories.idempotencyKey, "session:%")
|
|
248
|
+
: input.origin === "explicit"
|
|
249
|
+
? like(juniorMemoryMemories.idempotencyKey, "tool:%")
|
|
250
|
+
: undefined;
|
|
170
251
|
const rows = await db
|
|
171
252
|
.select()
|
|
172
253
|
.from(juniorMemoryMemories)
|
|
173
|
-
.where(and(active, cursor, search))
|
|
254
|
+
.where(and(active, cursor, search, kind, origin))
|
|
174
255
|
.orderBy(
|
|
175
256
|
desc(juniorMemoryMemories.createdAtMs),
|
|
176
257
|
asc(juniorMemoryMemories.id),
|
|
177
258
|
)
|
|
178
259
|
.limit(input.limit + 1);
|
|
179
260
|
const hasNextPage = rows.length > input.limit;
|
|
180
|
-
const memories = rows.slice(0, input.limit).map(
|
|
261
|
+
const memories = rows.slice(0, input.limit).map(personalMemoryRecord);
|
|
181
262
|
const last = memories.at(-1);
|
|
182
263
|
return {
|
|
183
264
|
memories,
|
|
@@ -191,5 +272,122 @@ export function createPersonalMemoryCollection(
|
|
|
191
272
|
: {}),
|
|
192
273
|
};
|
|
193
274
|
},
|
|
275
|
+
|
|
276
|
+
async stats() {
|
|
277
|
+
const nowMs = getNowMs();
|
|
278
|
+
await archiveExpiredMemoryBatch({ db, nowMs, scopes });
|
|
279
|
+
const active = activeVisiblePredicate({ nowMs, scopes });
|
|
280
|
+
if (!active) {
|
|
281
|
+
return {
|
|
282
|
+
active: 0,
|
|
283
|
+
automatic: 0,
|
|
284
|
+
createdThirtyDays: 0,
|
|
285
|
+
embedded: 0,
|
|
286
|
+
explicit: 0,
|
|
287
|
+
knowledge: 0,
|
|
288
|
+
preference: 0,
|
|
289
|
+
procedure: 0,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const [counts] = await db
|
|
293
|
+
.select({
|
|
294
|
+
active: sql<number>`count(*)`.mapWith(Number),
|
|
295
|
+
automatic:
|
|
296
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.idempotencyKey} like 'session:%')`.mapWith(
|
|
297
|
+
Number,
|
|
298
|
+
),
|
|
299
|
+
createdThirtyDays:
|
|
300
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.createdAtMs} >= ${nowMs - 30 * 24 * 60 * 60 * 1_000})`.mapWith(
|
|
301
|
+
Number,
|
|
302
|
+
),
|
|
303
|
+
embedded:
|
|
304
|
+
sql<number>`count(${juniorMemoryEmbeddings.memoryId})`.mapWith(
|
|
305
|
+
Number,
|
|
306
|
+
),
|
|
307
|
+
explicit:
|
|
308
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.idempotencyKey} like 'tool:%')`.mapWith(
|
|
309
|
+
Number,
|
|
310
|
+
),
|
|
311
|
+
knowledge:
|
|
312
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'knowledge')`.mapWith(
|
|
313
|
+
Number,
|
|
314
|
+
),
|
|
315
|
+
preference:
|
|
316
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'preference')`.mapWith(
|
|
317
|
+
Number,
|
|
318
|
+
),
|
|
319
|
+
procedure:
|
|
320
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'procedure')`.mapWith(
|
|
321
|
+
Number,
|
|
322
|
+
),
|
|
323
|
+
})
|
|
324
|
+
.from(juniorMemoryMemories)
|
|
325
|
+
.leftJoin(
|
|
326
|
+
juniorMemoryEmbeddings,
|
|
327
|
+
eq(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id),
|
|
328
|
+
)
|
|
329
|
+
.where(active);
|
|
330
|
+
return {
|
|
331
|
+
active: counts?.active ?? 0,
|
|
332
|
+
automatic: counts?.automatic ?? 0,
|
|
333
|
+
createdThirtyDays: counts?.createdThirtyDays ?? 0,
|
|
334
|
+
embedded: counts?.embedded ?? 0,
|
|
335
|
+
explicit: counts?.explicit ?? 0,
|
|
336
|
+
knowledge: counts?.knowledge ?? 0,
|
|
337
|
+
preference: counts?.preference ?? 0,
|
|
338
|
+
procedure: counts?.procedure ?? 0,
|
|
339
|
+
};
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
async timeline(input) {
|
|
343
|
+
input = personalMemoryTimelineInputSchema.parse(input);
|
|
344
|
+
const todayMs = Date.parse(`${utcDate(getNowMs())}T00:00:00.000Z`);
|
|
345
|
+
const startMs = todayMs - (input.days - 1) * DAY_MS;
|
|
346
|
+
const ownership = personalScopePredicate(scopes);
|
|
347
|
+
if (!ownership) {
|
|
348
|
+
return Array.from({ length: input.days }, (_, index) => ({
|
|
349
|
+
date: utcDate(startMs + index * DAY_MS),
|
|
350
|
+
knowledge: 0,
|
|
351
|
+
preference: 0,
|
|
352
|
+
procedure: 0,
|
|
353
|
+
}));
|
|
354
|
+
}
|
|
355
|
+
const rows = await db
|
|
356
|
+
.select({
|
|
357
|
+
date: sql<string>`to_char(to_timestamp(${juniorMemoryMemories.createdAtMs} / 1000.0) AT TIME ZONE 'UTC', 'YYYY-MM-DD')`.as(
|
|
358
|
+
"date",
|
|
359
|
+
),
|
|
360
|
+
knowledge:
|
|
361
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'knowledge')`.mapWith(
|
|
362
|
+
Number,
|
|
363
|
+
),
|
|
364
|
+
preference:
|
|
365
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'preference')`.mapWith(
|
|
366
|
+
Number,
|
|
367
|
+
),
|
|
368
|
+
procedure:
|
|
369
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'procedure')`.mapWith(
|
|
370
|
+
Number,
|
|
371
|
+
),
|
|
372
|
+
})
|
|
373
|
+
.from(juniorMemoryMemories)
|
|
374
|
+
.where(
|
|
375
|
+
and(ownership, gt(juniorMemoryMemories.createdAtMs, startMs - 1)),
|
|
376
|
+
)
|
|
377
|
+
.groupBy(
|
|
378
|
+
sql`to_char(to_timestamp(${juniorMemoryMemories.createdAtMs} / 1000.0) AT TIME ZONE 'UTC', 'YYYY-MM-DD')`,
|
|
379
|
+
);
|
|
380
|
+
const byDate = new Map(rows.map((row) => [row.date, row]));
|
|
381
|
+
return Array.from({ length: input.days }, (_, index) => {
|
|
382
|
+
const date = utcDate(startMs + index * DAY_MS);
|
|
383
|
+
const row = byDate.get(date);
|
|
384
|
+
return {
|
|
385
|
+
date,
|
|
386
|
+
knowledge: row?.knowledge ?? 0,
|
|
387
|
+
preference: row?.preference ?? 0,
|
|
388
|
+
procedure: row?.procedure ?? 0,
|
|
389
|
+
};
|
|
390
|
+
});
|
|
391
|
+
},
|
|
194
392
|
};
|
|
195
393
|
}
|
package/src/personal.ts
CHANGED
|
@@ -6,27 +6,34 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import type { PluginUserPageActor } from "@sentry/junior-plugin-api";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
createPersonalMemoryCollection,
|
|
11
|
+
type PersonalMemoryRecord,
|
|
12
|
+
} from "./personal-store";
|
|
10
13
|
import type { MemoryDb, MemoryRecord } from "./store";
|
|
11
|
-
import type { MemoryRuntimeContext } from "./types";
|
|
14
|
+
import type { MemoryKind, MemoryRuntimeContext } from "./types";
|
|
12
15
|
|
|
13
16
|
const cursorSchema = z
|
|
14
17
|
.object({
|
|
15
18
|
createdAtMs: z.number().finite(),
|
|
16
19
|
id: z.string().min(1),
|
|
20
|
+
kind: z.enum(["preference", "procedure", "knowledge"]).optional(),
|
|
21
|
+
origin: z.enum(["automatic", "explicit"]).optional(),
|
|
17
22
|
query: z.string().max(200).optional(),
|
|
18
23
|
version: z.literal(1),
|
|
19
24
|
})
|
|
20
25
|
.strict();
|
|
21
26
|
|
|
22
27
|
export interface ViewerMemoryPage {
|
|
23
|
-
memories:
|
|
28
|
+
memories: PersonalMemoryRecord[];
|
|
24
29
|
nextCursor?: string;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
export interface ViewerMemoryPageInput {
|
|
28
33
|
cursor?: string;
|
|
34
|
+
kind?: MemoryKind;
|
|
29
35
|
limit: number;
|
|
36
|
+
origin?: "automatic" | "explicit";
|
|
30
37
|
query?: string;
|
|
31
38
|
}
|
|
32
39
|
|
|
@@ -61,13 +68,20 @@ function runtimeContext(actor: PluginUserPageActor): MemoryRuntimeContext {
|
|
|
61
68
|
};
|
|
62
69
|
}
|
|
63
70
|
|
|
64
|
-
function decodeCursor(
|
|
71
|
+
function decodeCursor(
|
|
72
|
+
value: string | undefined,
|
|
73
|
+
input: Pick<ViewerMemoryPageInput, "kind" | "origin" | "query">,
|
|
74
|
+
) {
|
|
65
75
|
if (!value) return undefined;
|
|
66
76
|
try {
|
|
67
77
|
const parsed = cursorSchema.parse(
|
|
68
78
|
JSON.parse(Buffer.from(value, "base64url").toString("utf8")),
|
|
69
79
|
);
|
|
70
|
-
if (
|
|
80
|
+
if (
|
|
81
|
+
parsed.query !== input.query ||
|
|
82
|
+
parsed.kind !== input.kind ||
|
|
83
|
+
parsed.origin !== input.origin
|
|
84
|
+
) {
|
|
71
85
|
throw new InvalidMemoryCursorError();
|
|
72
86
|
}
|
|
73
87
|
return { createdAtMs: parsed.createdAtMs, id: parsed.id };
|
|
@@ -78,10 +92,16 @@ function decodeCursor(value: string | undefined, query: string | undefined) {
|
|
|
78
92
|
|
|
79
93
|
function encodeCursor(
|
|
80
94
|
cursor: { createdAtMs: number; id: string },
|
|
81
|
-
|
|
95
|
+
input: Pick<ViewerMemoryPageInput, "kind" | "origin" | "query">,
|
|
82
96
|
): string {
|
|
83
97
|
return Buffer.from(
|
|
84
|
-
JSON.stringify({
|
|
98
|
+
JSON.stringify({
|
|
99
|
+
...cursor,
|
|
100
|
+
...(input.query ? { query: input.query } : {}),
|
|
101
|
+
...(input.kind ? { kind: input.kind } : {}),
|
|
102
|
+
...(input.origin ? { origin: input.origin } : {}),
|
|
103
|
+
version: 1,
|
|
104
|
+
}),
|
|
85
105
|
"utf8",
|
|
86
106
|
).toString("base64url");
|
|
87
107
|
}
|
|
@@ -104,17 +124,28 @@ export function createViewerMemories(
|
|
|
104
124
|
},
|
|
105
125
|
async list(input: ViewerMemoryPageInput): Promise<ViewerMemoryPage> {
|
|
106
126
|
const query = input.query?.trim() || undefined;
|
|
127
|
+
const filters = {
|
|
128
|
+
...(input.kind ? { kind: input.kind } : {}),
|
|
129
|
+
...(input.origin ? { origin: input.origin } : {}),
|
|
130
|
+
...(query ? { query } : {}),
|
|
131
|
+
};
|
|
107
132
|
const page = await collection.list({
|
|
108
|
-
cursor: decodeCursor(input.cursor,
|
|
133
|
+
cursor: decodeCursor(input.cursor, filters),
|
|
134
|
+
...filters,
|
|
109
135
|
limit: input.limit,
|
|
110
|
-
...(query ? { query } : {}),
|
|
111
136
|
});
|
|
112
137
|
return {
|
|
113
138
|
memories: page.memories,
|
|
114
139
|
...(page.nextCursor
|
|
115
|
-
? { nextCursor: encodeCursor(page.nextCursor,
|
|
140
|
+
? { nextCursor: encodeCursor(page.nextCursor, filters) }
|
|
116
141
|
: {}),
|
|
117
142
|
};
|
|
118
143
|
},
|
|
144
|
+
async stats() {
|
|
145
|
+
return await collection.stats();
|
|
146
|
+
},
|
|
147
|
+
async timeline(input: { days: number }) {
|
|
148
|
+
return await collection.timeline(input);
|
|
149
|
+
},
|
|
119
150
|
};
|
|
120
151
|
}
|
package/src/user-pages.ts
CHANGED
|
@@ -1,39 +1,66 @@
|
|
|
1
1
|
/** Project viewer-owned 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 { PersonalMemoryRecord } from "./personal-store";
|
|
5
|
+
import type { MemoryDb } from "./store";
|
|
5
6
|
|
|
6
|
-
function
|
|
7
|
-
return
|
|
7
|
+
function titleCase(value: string): string {
|
|
8
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
function rememberedDate(createdAtMs: number): string {
|
|
11
12
|
return new Intl.DateTimeFormat("en-US", {
|
|
12
13
|
dateStyle: "medium",
|
|
14
|
+
timeStyle: "short",
|
|
13
15
|
timeZone: "UTC",
|
|
14
16
|
}).format(new Date(createdAtMs));
|
|
15
17
|
}
|
|
16
18
|
|
|
19
|
+
function originLabel(origin: PersonalMemoryRecord["origin"]): string {
|
|
20
|
+
if (origin === "automatic") return "Automatic";
|
|
21
|
+
if (origin === "explicit") return "Explicit";
|
|
22
|
+
return "Other";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function pageFilter(filter: string | undefined): {
|
|
26
|
+
kind?: "preference";
|
|
27
|
+
origin?: "automatic" | "explicit";
|
|
28
|
+
} {
|
|
29
|
+
if (filter === "preferences") return { kind: "preference" };
|
|
30
|
+
if (filter === "automatic") return { origin: "automatic" };
|
|
31
|
+
if (filter === "explicit") return { origin: "explicit" };
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pageEmptyText(input: { filter?: string; query?: string }): string {
|
|
36
|
+
if (input.query) return "No memories matched your search.";
|
|
37
|
+
if (input.filter === "preferences") return "No preferences yet.";
|
|
38
|
+
if (input.filter === "automatic") return "No learned memories yet.";
|
|
39
|
+
if (input.filter === "explicit") return "No saved memories yet.";
|
|
40
|
+
return "No personal memories yet.";
|
|
41
|
+
}
|
|
42
|
+
|
|
17
43
|
/** Create the interactive personal Memories dashboard page. */
|
|
18
44
|
export function createMemoryUserPage(): PluginUserPageDefinition {
|
|
19
45
|
return {
|
|
20
46
|
id: "memories",
|
|
21
47
|
label: "Memories",
|
|
48
|
+
navigation: "primary",
|
|
22
49
|
description: "Personal facts and preferences Junior remembers about you.",
|
|
23
50
|
async read(ctx, input) {
|
|
24
|
-
const
|
|
51
|
+
const memories = createViewerMemories(
|
|
25
52
|
ctx.db as MemoryDb,
|
|
26
53
|
ctx.viewer.actors,
|
|
27
|
-
)
|
|
54
|
+
);
|
|
55
|
+
const page = await memories.list({
|
|
28
56
|
cursor: input.cursor,
|
|
57
|
+
...pageFilter(input.filter),
|
|
29
58
|
limit: input.limit,
|
|
30
59
|
...(input.query ? { query: input.query } : {}),
|
|
31
60
|
});
|
|
32
61
|
return {
|
|
33
62
|
type: "list",
|
|
34
|
-
emptyText: input
|
|
35
|
-
? "No memories matched your search."
|
|
36
|
-
: "No personal memories yet.",
|
|
63
|
+
emptyText: pageEmptyText(input),
|
|
37
64
|
...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
|
|
38
65
|
searchPlaceholder: "Search memories",
|
|
39
66
|
records: page.memories.map((memory) => ({
|
|
@@ -49,8 +76,18 @@ export function createMemoryUserPage(): PluginUserPageDefinition {
|
|
|
49
76
|
id: memory.id,
|
|
50
77
|
title: memory.content,
|
|
51
78
|
metadata: [
|
|
52
|
-
{ label: "Type", value:
|
|
79
|
+
{ label: "Type", value: titleCase(memory.kind) },
|
|
80
|
+
{ label: "Learned", value: originLabel(memory.origin) },
|
|
81
|
+
{ label: "Source", value: titleCase(memory.sourcePlatform) },
|
|
82
|
+
{ label: "Visibility", value: "Only you" },
|
|
53
83
|
{ label: "Remembered", value: rememberedDate(memory.createdAtMs) },
|
|
84
|
+
{ label: "Observed", value: rememberedDate(memory.observedAtMs) },
|
|
85
|
+
{
|
|
86
|
+
label: "Expires",
|
|
87
|
+
value: memory.expiresAtMs
|
|
88
|
+
? rememberedDate(memory.expiresAtMs)
|
|
89
|
+
: "Never",
|
|
90
|
+
},
|
|
54
91
|
],
|
|
55
92
|
})),
|
|
56
93
|
};
|