@sentry/junior-memory 0.124.1 → 0.125.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.
@@ -1,8 +1,8 @@
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
7
  import { and, asc, desc, eq, gt, ilike, like, lt, or, sql } from "drizzle-orm";
8
8
  import { z } from "zod";
@@ -18,6 +18,7 @@ import {
18
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(),
@@ -31,6 +32,7 @@ const personalMemoryPageInputSchema = z
31
32
  limit: z.number().int().min(1).max(50),
32
33
  origin: z.enum(["automatic", "explicit"]).optional(),
33
34
  query: z.string().max(200).optional(),
35
+ visibility: memoryVisibilitySchema.optional(),
34
36
  })
35
37
  .strict();
36
38
  const personalMemoryTimelineInputSchema = z
@@ -46,15 +48,18 @@ export type PersonalMemoryPageInput = z.output<
46
48
  typeof personalMemoryPageInputSchema
47
49
  >;
48
50
 
51
+ export type MemoryVisibility = z.output<typeof memoryVisibilitySchema>;
52
+
49
53
  export interface PersonalMemoryPage {
50
54
  memories: PersonalMemoryRecord[];
51
55
  nextCursor?: PersonalMemoryCursor;
52
56
  }
53
57
 
54
- /** Safe provenance attached to one viewer-owned personal memory. */
58
+ /** Safe provenance attached to one viewer-visible memory. */
55
59
  export type PersonalMemoryRecord = MemoryRecord & {
56
60
  origin: "automatic" | "explicit" | "other";
57
61
  sourcePlatform: "local" | "slack";
62
+ visibility: MemoryVisibility;
58
63
  };
59
64
 
60
65
  /** Viewer-scoped active memory totals used by the dashboard. */
@@ -65,16 +70,17 @@ export interface PersonalMemoryStats {
65
70
  embedded: number;
66
71
  explicit: number;
67
72
  knowledge: number;
73
+ personal: number;
68
74
  preference: number;
69
75
  procedure: number;
76
+ public: number;
70
77
  }
71
78
 
72
79
  /** Viewer-scoped memory creation totals for one UTC calendar day. */
73
80
  export interface PersonalMemoryDay {
74
81
  date: string;
75
- knowledge: number;
76
- preference: number;
77
- procedure: number;
82
+ personal: number;
83
+ public: number;
78
84
  }
79
85
 
80
86
  /** Expected failure when a viewer does not own the requested memory. */
@@ -85,17 +91,17 @@ export class PersonalMemoryNotFoundError extends Error {
85
91
  }
86
92
  }
87
93
 
88
- /** Viewer-scoped personal memory operations shared by dashboard and REST. */
94
+ /** Viewer-scoped memory operations shared by dashboard and REST. */
89
95
  export interface PersonalMemoryCollection {
90
96
  /** Archive one exact personal memory owned by a linked actor. */
91
97
  archive(id: string): Promise<MemoryRecord>;
92
- /** Read one exact personal memory owned by a linked actor. */
93
- get(id: string): Promise<MemoryRecord>;
94
- /** 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. */
95
101
  list(input: PersonalMemoryPageInput): Promise<PersonalMemoryPage>;
96
- /** Summarize active memories across every linked personal scope. */
102
+ /** Summarize active memories across every authorized viewer scope. */
97
103
  stats(): Promise<PersonalMemoryStats>;
98
- /** Read memory creation history across every linked personal scope. */
104
+ /** Read memory creation history across every authorized viewer scope. */
99
105
  timeline(input: { days: number }): Promise<PersonalMemoryDay[]>;
100
106
  }
101
107
 
@@ -112,7 +118,30 @@ function personalScopes(
112
118
  ];
113
119
  }
114
120
 
115
- function personalScopePredicate(scopes: ResolvedMemoryScope[]) {
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[]) {
116
145
  if (scopes.length === 0) return undefined;
117
146
  return or(
118
147
  ...scopes.map((scope) =>
@@ -148,30 +177,67 @@ function memoryOrigin(
148
177
  return "other";
149
178
  }
150
179
 
180
+ function memoryVisibility(
181
+ scope: MemoryRecord["scope"],
182
+ ): PersonalMemoryRecord["visibility"] {
183
+ return scope === "personal" ? "private" : "public";
184
+ }
185
+
151
186
  function personalMemoryRecord(
152
187
  row: typeof juniorMemoryMemories.$inferSelect,
153
188
  ): PersonalMemoryRecord {
189
+ const memory = parseMemoryRow(row);
154
190
  return {
155
- ...parseMemoryRow(row),
191
+ ...memory,
156
192
  origin: memoryOrigin(row.idempotencyKey),
157
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,
158
210
  };
159
211
  }
160
212
 
161
- /** Build storage operations for every personal scope linked to one viewer. */
213
+ /** Build storage operations for every memory scope linked to one viewer. */
162
214
  export function createPersonalMemoryCollection(
163
215
  db: MemoryDb,
164
216
  runtimeContexts: MemoryRuntimeContext[],
165
217
  options: { now?: () => number } = {},
166
218
  ): PersonalMemoryCollection {
167
- const scopes = personalScopes(runtimeContexts);
219
+ const privateScopes = personalScopes(runtimeContexts);
220
+ const publicScopes = publicWorkspaceScopes(runtimeContexts);
221
+ const allScopes = [...privateScopes, ...publicScopes];
168
222
  const getNowMs = () => options.now?.() ?? Date.now();
169
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
+
170
232
  return {
171
233
  async archive(id) {
172
234
  const memoryId = nonEmptyStringSchema.parse(id);
173
235
  const nowMs = getNowMs();
174
- 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
+ });
175
241
  if (!predicate) {
176
242
  throw new PersonalMemoryNotFoundError();
177
243
  }
@@ -195,7 +261,7 @@ export function createPersonalMemoryCollection(
195
261
  async get(id) {
196
262
  const memoryId = nonEmptyStringSchema.parse(id);
197
263
  const nowMs = getNowMs();
198
- const predicate = activeVisiblePredicate({ nowMs, scopes });
264
+ const predicate = activeVisiblePredicate({ nowMs, scopes: allScopes });
199
265
  if (!predicate) {
200
266
  throw new PersonalMemoryNotFoundError();
201
267
  }
@@ -207,12 +273,13 @@ export function createPersonalMemoryCollection(
207
273
  if (!rows[0]) {
208
274
  throw new PersonalMemoryNotFoundError();
209
275
  }
210
- return parseMemoryRow(rows[0]);
276
+ return personalMemoryRecord(rows[0]);
211
277
  },
212
278
 
213
279
  async list(input) {
214
280
  input = personalMemoryPageInputSchema.parse(input);
215
281
  const nowMs = getNowMs();
282
+ const scopes = scopesForVisibility(input.visibility);
216
283
  await archiveExpiredMemoryBatch({ db, nowMs, scopes });
217
284
  const active = activeVisiblePredicate({ nowMs, scopes });
218
285
  if (!active) {
@@ -275,19 +342,10 @@ export function createPersonalMemoryCollection(
275
342
 
276
343
  async stats() {
277
344
  const nowMs = getNowMs();
278
- await archiveExpiredMemoryBatch({ db, nowMs, scopes });
279
- const active = activeVisiblePredicate({ nowMs, scopes });
345
+ await archiveExpiredMemoryBatch({ db, nowMs, scopes: allScopes });
346
+ const active = activeVisiblePredicate({ nowMs, scopes: allScopes });
280
347
  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
- };
348
+ return emptyStats();
291
349
  }
292
350
  const [counts] = await db
293
351
  .select({
@@ -312,6 +370,10 @@ export function createPersonalMemoryCollection(
312
370
  sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'knowledge')`.mapWith(
313
371
  Number,
314
372
  ),
373
+ personal:
374
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.scope} = 'personal')`.mapWith(
375
+ Number,
376
+ ),
315
377
  preference:
316
378
  sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'preference')`.mapWith(
317
379
  Number,
@@ -320,6 +382,10 @@ export function createPersonalMemoryCollection(
320
382
  sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'procedure')`.mapWith(
321
383
  Number,
322
384
  ),
385
+ public:
386
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.scope} = 'conversation')`.mapWith(
387
+ Number,
388
+ ),
323
389
  })
324
390
  .from(juniorMemoryMemories)
325
391
  .leftJoin(
@@ -334,8 +400,10 @@ export function createPersonalMemoryCollection(
334
400
  embedded: counts?.embedded ?? 0,
335
401
  explicit: counts?.explicit ?? 0,
336
402
  knowledge: counts?.knowledge ?? 0,
403
+ personal: counts?.personal ?? 0,
337
404
  preference: counts?.preference ?? 0,
338
405
  procedure: counts?.procedure ?? 0,
406
+ public: counts?.public ?? 0,
339
407
  };
340
408
  },
341
409
 
@@ -343,13 +411,12 @@ export function createPersonalMemoryCollection(
343
411
  input = personalMemoryTimelineInputSchema.parse(input);
344
412
  const todayMs = Date.parse(`${utcDate(getNowMs())}T00:00:00.000Z`);
345
413
  const startMs = todayMs - (input.days - 1) * DAY_MS;
346
- const ownership = personalScopePredicate(scopes);
414
+ const ownership = scopePredicate(allScopes);
347
415
  if (!ownership) {
348
416
  return Array.from({ length: input.days }, (_, index) => ({
349
417
  date: utcDate(startMs + index * DAY_MS),
350
- knowledge: 0,
351
- preference: 0,
352
- procedure: 0,
418
+ personal: 0,
419
+ public: 0,
353
420
  }));
354
421
  }
355
422
  const rows = await db
@@ -357,16 +424,12 @@ export function createPersonalMemoryCollection(
357
424
  date: sql<string>`to_char(to_timestamp(${juniorMemoryMemories.createdAtMs} / 1000.0) AT TIME ZONE 'UTC', 'YYYY-MM-DD')`.as(
358
425
  "date",
359
426
  ),
360
- knowledge:
361
- sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'knowledge')`.mapWith(
427
+ personal:
428
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.scope} = 'personal')`.mapWith(
362
429
  Number,
363
430
  ),
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(
431
+ public:
432
+ sql<number>`count(*) filter (where ${juniorMemoryMemories.scope} = 'conversation')`.mapWith(
370
433
  Number,
371
434
  ),
372
435
  })
@@ -383,9 +446,8 @@ export function createPersonalMemoryCollection(
383
446
  const row = byDate.get(date);
384
447
  return {
385
448
  date,
386
- knowledge: row?.knowledge ?? 0,
387
- preference: row?.preference ?? 0,
388
- procedure: row?.procedure ?? 0,
449
+ personal: row?.personal ?? 0,
450
+ public: row?.public ?? 0,
389
451
  };
390
452
  });
391
453
  },
package/src/personal.ts CHANGED
@@ -2,12 +2,14 @@
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
10
  import {
10
11
  createPersonalMemoryCollection,
12
+ type MemoryVisibility,
11
13
  type PersonalMemoryRecord,
12
14
  } from "./personal-store";
13
15
  import type { MemoryDb, MemoryRecord } from "./store";
@@ -21,6 +23,7 @@ const cursorSchema = z
21
23
  origin: z.enum(["automatic", "explicit"]).optional(),
22
24
  query: z.string().max(200).optional(),
23
25
  version: z.literal(1),
26
+ visibility: z.enum(["private", "public"]).optional(),
24
27
  })
25
28
  .strict();
26
29
 
@@ -35,6 +38,7 @@ export interface ViewerMemoryPageInput {
35
38
  limit: number;
36
39
  origin?: "automatic" | "explicit";
37
40
  query?: string;
41
+ visibility?: MemoryVisibility;
38
42
  }
39
43
 
40
44
  export class InvalidMemoryCursorError extends Error {
@@ -45,6 +49,7 @@ export class InvalidMemoryCursorError extends Error {
45
49
  }
46
50
 
47
51
  export { PersonalMemoryNotFoundError } from "./personal-store";
52
+ export type { MemoryVisibility, PersonalMemoryRecord } from "./personal-store";
48
53
 
49
54
  function runtimeContext(actor: PluginUserPageActor): MemoryRuntimeContext {
50
55
  if (actor.platform === "slack") {
@@ -70,7 +75,7 @@ function runtimeContext(actor: PluginUserPageActor): MemoryRuntimeContext {
70
75
 
71
76
  function decodeCursor(
72
77
  value: string | undefined,
73
- input: Pick<ViewerMemoryPageInput, "kind" | "origin" | "query">,
78
+ input: Pick<ViewerMemoryPageInput, "kind" | "origin" | "query" | "visibility">,
74
79
  ) {
75
80
  if (!value) return undefined;
76
81
  try {
@@ -80,7 +85,8 @@ function decodeCursor(
80
85
  if (
81
86
  parsed.query !== input.query ||
82
87
  parsed.kind !== input.kind ||
83
- parsed.origin !== input.origin
88
+ parsed.origin !== input.origin ||
89
+ parsed.visibility !== input.visibility
84
90
  ) {
85
91
  throw new InvalidMemoryCursorError();
86
92
  }
@@ -92,7 +98,7 @@ function decodeCursor(
92
98
 
93
99
  function encodeCursor(
94
100
  cursor: { createdAtMs: number; id: string },
95
- input: Pick<ViewerMemoryPageInput, "kind" | "origin" | "query">,
101
+ input: Pick<ViewerMemoryPageInput, "kind" | "origin" | "query" | "visibility">,
96
102
  ): string {
97
103
  return Buffer.from(
98
104
  JSON.stringify({
@@ -100,13 +106,14 @@ function encodeCursor(
100
106
  ...(input.query ? { query: input.query } : {}),
101
107
  ...(input.kind ? { kind: input.kind } : {}),
102
108
  ...(input.origin ? { origin: input.origin } : {}),
109
+ ...(input.visibility ? { visibility: input.visibility } : {}),
103
110
  version: 1,
104
111
  }),
105
112
  "utf8",
106
113
  ).toString("base64url");
107
114
  }
108
115
 
109
- /** Build personal-memory operations authorized by a viewer's linked actors. */
116
+ /** Build viewer memory operations authorized by a viewer's linked actors. */
110
117
  export function createViewerMemories(
111
118
  db: MemoryDb,
112
119
  actors: PluginUserPageActor[],
@@ -119,7 +126,7 @@ export function createViewerMemories(
119
126
  async archive(id: string): Promise<MemoryRecord> {
120
127
  return await collection.archive(id);
121
128
  },
122
- async get(id: string): Promise<MemoryRecord> {
129
+ async get(id: string): Promise<PersonalMemoryRecord> {
123
130
  return await collection.get(id);
124
131
  },
125
132
  async list(input: ViewerMemoryPageInput): Promise<ViewerMemoryPage> {
@@ -128,6 +135,7 @@ export function createViewerMemories(
128
135
  ...(input.kind ? { kind: input.kind } : {}),
129
136
  ...(input.origin ? { origin: input.origin } : {}),
130
137
  ...(query ? { query } : {}),
138
+ ...(input.visibility ? { visibility: input.visibility } : {}),
131
139
  };
132
140
  const page = await collection.list({
133
141
  cursor: decodeCursor(input.cursor, filters),
package/src/plugin.ts CHANGED
@@ -14,7 +14,11 @@ import {
14
14
  import { processMemorySession } from "./process-session";
15
15
  import { createMemoryPromptContributions } from "./recall";
16
16
  import { buildMemoryOperationalReport } from "./operational-report";
17
- import { memoriesCapturedEvent } from "./events";
17
+ import {
18
+ memoriesCapturedEvent,
19
+ memoriesCapturedEventV1,
20
+ memoriesRecalledEvent,
21
+ } from "./events";
18
22
  import type { MemoryDb } from "./store";
19
23
  import { createMemoryUserPage } from "./user-pages";
20
24
 
@@ -101,7 +105,11 @@ export function memoryPlugin(options: MemoryPluginOptions = {}) {
101
105
  ? { structuredModelId: modelId }
102
106
  : { structuredModel: "default" },
103
107
  packageName: "@sentry/junior-memory",
104
- conversationEvents: [memoriesCapturedEvent],
108
+ conversationEvents: [
109
+ memoriesCapturedEventV1,
110
+ memoriesCapturedEvent,
111
+ memoriesRecalledEvent,
112
+ ],
105
113
  cli: {
106
114
  commands: [createMemoryCliCommand()],
107
115
  },
@@ -115,8 +123,13 @@ export function memoryPlugin(options: MemoryPluginOptions = {}) {
115
123
  userPages: [createMemoryUserPage()],
116
124
  hooks: {
117
125
  async operationalReport(ctx) {
126
+ const extractionDays = await ctx.eventStats.costsByDay({
127
+ days: 90,
128
+ eventName: "memories_captured",
129
+ });
118
130
  return await buildMemoryOperationalReport({
119
131
  db: ctx.db as MemoryDb,
132
+ extractionDays,
120
133
  nowMs: ctx.nowMs,
121
134
  });
122
135
  },
@@ -124,6 +137,7 @@ export function memoryPlugin(options: MemoryPluginOptions = {}) {
124
137
  return createMemoryApi({
125
138
  actors: ctx.viewer.actors,
126
139
  db: ctx.db as MemoryDb,
140
+ eventStats: ctx.eventStats,
127
141
  });
128
142
  },
129
143
  tools(ctx) {
@@ -156,6 +170,7 @@ export function memoryPlugin(options: MemoryPluginOptions = {}) {
156
170
  ...(ctx.actor ? { actor: ctx.actor } : {}),
157
171
  db: ctx.db as MemoryDb,
158
172
  embedder: ctx.embedder,
173
+ events: ctx.events,
159
174
  log: ctx.log,
160
175
  maxVectorDistance: recallMaxVectorDistance(options),
161
176
  source: ctx.source,
@@ -17,6 +17,7 @@ import {
17
17
  createMemoryAgent,
18
18
  parseExtractedMemory,
19
19
  type ExtractedMemory,
20
+ type MemoryExtractionResult,
20
21
  } from "./agent";
21
22
  import { MEMORY_KINDS, memoryRuntimeContextSchema } from "./types";
22
23
  import { capturedMemory, memoriesCapturedEvent } from "./events";
@@ -28,20 +29,30 @@ const MEMORY_TOOL_NAMES = new Set([
28
29
  "searchMemories",
29
30
  ]);
30
31
  const MEMORY_TASK_STATE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
31
- const extractedMemoryCacheSchema = z.array(
32
+ const extractedMemorySchema = z
33
+ .object({
34
+ content: z.string().min(1),
35
+ expiresAtMs: z.number().finite().nullable(),
36
+ kind: z.enum(MEMORY_KINDS),
37
+ evidenceMessageIndices: z
38
+ .array(z.number().int().nonnegative())
39
+ .min(1)
40
+ .max(10),
41
+ })
42
+ .strict()
43
+ .transform(parseExtractedMemory);
44
+ const extractedMemoryCacheSchema = z.union([
32
45
  z
33
46
  .object({
34
- content: z.string().min(1),
35
- expiresAtMs: z.number().finite().nullable(),
36
- kind: z.enum(MEMORY_KINDS),
37
- evidenceMessageIndices: z
38
- .array(z.number().int().nonnegative())
39
- .min(1)
40
- .max(10),
47
+ costUsd: z.number().finite().nonnegative().optional(),
48
+ memories: z.array(extractedMemorySchema).max(5),
41
49
  })
42
- .strict()
43
- .transform(parseExtractedMemory),
44
- );
50
+ .strict(),
51
+ z
52
+ .array(extractedMemorySchema)
53
+ .max(5)
54
+ .transform((memories) => ({ memories })),
55
+ ]);
45
56
 
46
57
  /** Where a passively extracted memory may be stored, or dropped when unproven. */
47
58
  type MemoryRouteTarget = "drop" | "personal" | "conversation";
@@ -178,10 +189,10 @@ function passiveInput(
178
189
  };
179
190
  }
180
191
 
181
- async function getTaskMemories(
192
+ async function getTaskExtraction(
182
193
  context: PluginTaskContext,
183
- extract: () => Promise<ExtractedMemory[]>,
184
- ): Promise<ExtractedMemory[]> {
194
+ extract: () => Promise<MemoryExtractionResult>,
195
+ ): Promise<MemoryExtractionResult> {
185
196
  const cacheKey = `memory-extraction:${context.id}`;
186
197
  const cached = await context.state.get(cacheKey);
187
198
  if (cached !== undefined) {
@@ -191,11 +202,9 @@ async function getTaskMemories(
191
202
  }
192
203
  await context.state.delete(cacheKey);
193
204
  }
194
- const memories = await extract();
195
- if (memories.length > 0) {
196
- await context.state.set(cacheKey, memories, MEMORY_TASK_STATE_TTL_MS);
197
- }
198
- return memories;
205
+ const extraction = await extract();
206
+ await context.state.set(cacheKey, extraction, MEMORY_TASK_STATE_TTL_MS);
207
+ return extraction;
199
208
  }
200
209
 
201
210
  /**
@@ -251,7 +260,7 @@ export async function processMemorySession(
251
260
  supersessionDecider: agent,
252
261
  });
253
262
  await store.archiveExpiredMemories();
254
- const memories = await getTaskMemories(context, async () => {
263
+ const extraction = await getTaskExtraction(context, async () => {
255
264
  const existingMemories = await store.searchMemories({
256
265
  limit: 10,
257
266
  query: evidenceText,
@@ -265,14 +274,11 @@ export async function processMemorySession(
265
274
  runtimeContext,
266
275
  });
267
276
  });
268
- if (memories.length === 0) {
269
- return;
270
- }
271
277
 
272
278
  const captured: ReturnType<typeof capturedMemory>[] = [];
273
- for (const memory of memories) {
279
+ for (const memory of extraction.memories) {
274
280
  // The routing gate stays even though extraction is also actor-gated:
275
- // getTaskMemories caches extraction output for 7 days, so a retry can replay
281
+ // getTaskExtraction caches extraction output for 7 days, so a retry can replay
276
282
  // preference proposals cached before this gate existed.
277
283
  const target = routeExtractedMemory(memory, transcript, run);
278
284
  if (target === "drop") {
@@ -287,7 +293,12 @@ export async function processMemorySession(
287
293
  const result = await store.createMemory(input);
288
294
  recordCapturedMemory(captured, result);
289
295
  }
290
- if (captured.length > 0) {
291
- await context.events.emit(memoriesCapturedEvent({ memories: captured }));
292
- }
296
+ await context.events.emit(
297
+ memoriesCapturedEvent({
298
+ memories: captured,
299
+ ...(extraction.costUsd !== undefined
300
+ ? { costUsd: extraction.costUsd }
301
+ : {}),
302
+ }),
303
+ );
293
304
  }