@sentry/junior-memory 0.120.0 → 0.121.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ import type { PluginOperationalReportContent } from "@sentry/junior-plugin-api";
2
+ import type { MemoryDb } from "./store";
3
+ /** Build aggregate memory storage and indexing diagnostics for the System page. */
4
+ export declare function buildMemoryOperationalReport(args: {
5
+ db: MemoryDb;
6
+ nowMs: number;
7
+ }): Promise<PluginOperationalReportContent>;
package/dist/recall.d.ts CHANGED
@@ -31,10 +31,5 @@ export declare const memoryRecallContextSchema: z.ZodObject<{
31
31
  }>;
32
32
  }, z.core.$strict>>;
33
33
  }, z.core.$strict>;
34
- /**
35
- * Build active memory recall contributions.
36
- *
37
- * Personal recall remains prompt-only so conversation reporting cannot widen
38
- * actor-scoped memory visibility.
39
- */
34
+ /** Build active memory recall contributions. */
40
35
  export declare function createMemoryPromptContributions(context: MemoryRecallContext): Promise<UserPromptContribution[] | undefined>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-memory",
3
- "version": "0.120.0",
3
+ "version": "0.121.0",
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.120.0"
30
+ "@sentry/junior-plugin-api": "0.121.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^25.9.1",
@@ -0,0 +1,191 @@
1
+ import type { PluginOperationalReportContent } from "@sentry/junior-plugin-api";
2
+ import { and, eq, gt, isNull, or, sql } from "drizzle-orm";
3
+ import { z } from "zod";
4
+ import { juniorMemoryEmbeddings, juniorMemoryMemories } from "./db/schema";
5
+ import type { MemoryDb } from "./store";
6
+
7
+ const DAY_MS = 24 * 60 * 60 * 1_000;
8
+ const WINDOWS = [7, 30, 90] as const;
9
+
10
+ const memoryDaySchema = z
11
+ .object({
12
+ conversation: z.number().int().nonnegative(),
13
+ date: z.string().date(),
14
+ personal: z.number().int().nonnegative(),
15
+ })
16
+ .strict();
17
+
18
+ function queryRows(result: unknown): unknown[] {
19
+ if (
20
+ typeof result !== "object" ||
21
+ result === null ||
22
+ !("rows" in result) ||
23
+ !Array.isArray(result.rows)
24
+ ) {
25
+ throw new TypeError("Memory activity query did not return rows");
26
+ }
27
+ return result.rows;
28
+ }
29
+
30
+ function startOfUtcDay(value: number): Date {
31
+ const date = new Date(value);
32
+ date.setUTCHours(0, 0, 0, 0);
33
+ return date;
34
+ }
35
+
36
+ async function aggregateMemoryDays(args: { db: MemoryDb; nowMs: number }) {
37
+ const end = startOfUtcDay(args.nowMs);
38
+ const start = startOfUtcDay(args.nowMs - (WINDOWS.at(-1)! - 1) * DAY_MS);
39
+ const endExclusiveMs = end.getTime() + DAY_MS;
40
+ const table = juniorMemoryMemories;
41
+ const result = await args.db.execute(sql`
42
+ WITH days AS (
43
+ SELECT generate_series(
44
+ date_trunc('day', ${start}::timestamptz AT TIME ZONE 'UTC'),
45
+ date_trunc('day', ${end}::timestamptz AT TIME ZONE 'UTC'),
46
+ interval '1 day'
47
+ ) AS day
48
+ ), daily AS (
49
+ SELECT
50
+ date_trunc(
51
+ 'day',
52
+ to_timestamp(${table.createdAtMs} / 1000.0) AT TIME ZONE 'UTC'
53
+ ) AS day,
54
+ count(*) FILTER (
55
+ WHERE ${table.scope} = 'personal'
56
+ )::integer AS personal,
57
+ count(*) FILTER (
58
+ WHERE ${table.scope} = 'conversation'
59
+ )::integer AS conversation
60
+ FROM ${table}
61
+ WHERE ${table.createdAtMs} >= ${start.getTime()}
62
+ AND ${table.createdAtMs} < ${endExclusiveMs}
63
+ GROUP BY date_trunc(
64
+ 'day',
65
+ to_timestamp(${table.createdAtMs} / 1000.0) AT TIME ZONE 'UTC'
66
+ )
67
+ )
68
+ SELECT
69
+ to_char(days.day, 'YYYY-MM-DD') AS date,
70
+ coalesce(daily.personal, 0)::integer AS personal,
71
+ coalesce(daily.conversation, 0)::integer AS conversation
72
+ FROM days
73
+ LEFT JOIN daily ON daily.day = days.day
74
+ ORDER BY days.day
75
+ `);
76
+ return z.array(memoryDaySchema).parse(queryRows(result));
77
+ }
78
+
79
+ function formatCount(value: number): string {
80
+ return new Intl.NumberFormat("en-US").format(value);
81
+ }
82
+
83
+ function formatPercent(value: number): string {
84
+ return new Intl.NumberFormat("en-US", {
85
+ maximumFractionDigits: 0,
86
+ style: "percent",
87
+ }).format(value);
88
+ }
89
+
90
+ /** Build aggregate memory storage and indexing diagnostics for the System page. */
91
+ export async function buildMemoryOperationalReport(args: {
92
+ db: MemoryDb;
93
+ nowMs: number;
94
+ }): Promise<PluginOperationalReportContent> {
95
+ const active = and(
96
+ isNull(juniorMemoryMemories.archivedAtMs),
97
+ isNull(juniorMemoryMemories.supersededAtMs),
98
+ isNull(juniorMemoryMemories.supersededById),
99
+ or(
100
+ isNull(juniorMemoryMemories.expiresAtMs),
101
+ gt(juniorMemoryMemories.expiresAtMs, args.nowMs),
102
+ ),
103
+ );
104
+ const [[counts], memoryDays] = await Promise.all([
105
+ args.db
106
+ .select({
107
+ active: sql<number>`count(*) filter (where ${active})`.mapWith(Number),
108
+ conversation:
109
+ sql<number>`count(*) filter (where ${active} and ${juniorMemoryMemories.scope} = 'conversation')`.mapWith(
110
+ Number,
111
+ ),
112
+ createdThirtyDays:
113
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.createdAtMs} >= ${args.nowMs - 30 * DAY_MS})`.mapWith(
114
+ Number,
115
+ ),
116
+ embedded:
117
+ sql<number>`count(${juniorMemoryEmbeddings.memoryId}) filter (where ${active})`.mapWith(
118
+ Number,
119
+ ),
120
+ personal:
121
+ sql<number>`count(*) filter (where ${active} and ${juniorMemoryMemories.scope} = 'personal')`.mapWith(
122
+ Number,
123
+ ),
124
+ })
125
+ .from(juniorMemoryMemories)
126
+ .leftJoin(
127
+ juniorMemoryEmbeddings,
128
+ eq(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id),
129
+ ),
130
+ aggregateMemoryDays(args),
131
+ ]);
132
+
133
+ const activeCount = counts?.active ?? 0;
134
+ const embeddedCount = counts?.embedded ?? 0;
135
+ const embeddingCoverage = activeCount === 0 ? 0 : embeddedCount / activeCount;
136
+
137
+ return {
138
+ generatedAt: new Date(args.nowMs).toISOString(),
139
+ title: "Memory",
140
+ metrics: [
141
+ {
142
+ label: "active memories",
143
+ tone: activeCount > 0 ? "good" : "neutral",
144
+ value: formatCount(activeCount),
145
+ },
146
+ {
147
+ label: "created · 30d",
148
+ value: formatCount(counts?.createdThirtyDays ?? 0),
149
+ },
150
+ {
151
+ label: "personal",
152
+ value: formatCount(counts?.personal ?? 0),
153
+ },
154
+ {
155
+ label: "conversation",
156
+ value: formatCount(counts?.conversation ?? 0),
157
+ },
158
+ {
159
+ label: "embedding coverage",
160
+ tone:
161
+ activeCount === 0
162
+ ? "neutral"
163
+ : embeddedCount === activeCount
164
+ ? "good"
165
+ : "warning",
166
+ value: formatPercent(embeddingCoverage),
167
+ },
168
+ ],
169
+ widgets: [
170
+ {
171
+ categories: memoryDays.map((day) => ({
172
+ id: day.date,
173
+ label: day.date,
174
+ values: {
175
+ conversation: day.conversation,
176
+ personal: day.personal,
177
+ },
178
+ })),
179
+ description: "Memories stored per day by scope",
180
+ id: "memories-created",
181
+ series: [
182
+ { key: "personal", label: "Personal" },
183
+ { key: "conversation", label: "Conversation" },
184
+ ],
185
+ timeRangeDays: [...WINDOWS],
186
+ title: "Memories created",
187
+ type: "bar_chart",
188
+ },
189
+ ],
190
+ };
191
+ }
package/src/plugin.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  } from "./tools";
14
14
  import { processMemorySession } from "./process-session";
15
15
  import { createMemoryPromptContributions } from "./recall";
16
+ import { buildMemoryOperationalReport } from "./operational-report";
16
17
  import type { MemoryDb } from "./store";
17
18
  import { createMemoryUserPage } from "./user-pages";
18
19
 
@@ -111,6 +112,12 @@ export function memoryPlugin(options: MemoryPluginOptions = {}) {
111
112
  },
112
113
  userPages: [createMemoryUserPage()],
113
114
  hooks: {
115
+ async operationalReport(ctx) {
116
+ return await buildMemoryOperationalReport({
117
+ db: ctx.db as MemoryDb,
118
+ nowMs: ctx.nowMs,
119
+ });
120
+ },
114
121
  apiRoutes(ctx) {
115
122
  return createMemoryApi({
116
123
  actors: ctx.viewer.actors,
package/src/recall.ts CHANGED
@@ -108,12 +108,7 @@ const memoryRecallContext = definePromptContext({
108
108
  renderPrompt: (content) => renderMemoryPrompt(content.memories),
109
109
  });
110
110
 
111
- /**
112
- * Build active memory recall contributions.
113
- *
114
- * Personal recall remains prompt-only so conversation reporting cannot widen
115
- * actor-scoped memory visibility.
116
- */
111
+ /** Build active memory recall contributions. */
117
112
  export async function createMemoryPromptContributions(
118
113
  context: MemoryRecallContext,
119
114
  ): Promise<UserPromptContribution[] | undefined> {
@@ -162,8 +157,5 @@ export async function createMemoryPromptContributions(
162
157
  if (selected.length === 0) {
163
158
  return undefined;
164
159
  }
165
- if (selected.some((memory) => memory.scope === "personal")) {
166
- return [{ text: renderMemoryPrompt(selected) }];
167
- }
168
160
  return [memoryRecallContext({ memories: selected })];
169
161
  }