@sentry/junior-memory 0.122.1 → 0.123.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.
- package/dist/events.d.ts +19 -0
- package/dist/index.js +130 -39
- package/dist/index.js.map +1 -1
- package/dist/store.d.ts +4 -0
- package/package.json +2 -2
- package/src/events.ts +59 -0
- package/src/plugin.ts +2 -0
- package/src/process-session.ts +25 -2
- package/src/store.ts +34 -15
- package/src/tools.ts +24 -2
package/dist/store.d.ts
CHANGED
|
@@ -123,7 +123,11 @@ export type CreateMemoryInput = z.output<typeof createMemoryInputSchema>;
|
|
|
123
123
|
/** Result of a memory write after idempotency checks. */
|
|
124
124
|
export interface CreateMemoryResult {
|
|
125
125
|
created: boolean;
|
|
126
|
+
/** True when this call found the memory previously written for the same input identity. */
|
|
127
|
+
idempotent?: true;
|
|
126
128
|
memory: MemoryRecord;
|
|
129
|
+
/** Memory ids made inactive by this write. */
|
|
130
|
+
supersededIds?: string[];
|
|
127
131
|
}
|
|
128
132
|
export type ListMemoriesInput = z.output<typeof listMemoriesInputSchema>;
|
|
129
133
|
export type SearchMemoriesInput = z.output<typeof searchMemoriesInputSchema>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.123.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.
|
|
30
|
+
"@sentry/junior-plugin-api": "0.123.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^25.9.1",
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { defineConversationEvent } from "@sentry/junior-plugin-api";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { MEMORY_KINDS, MEMORY_SCOPES } from "./types";
|
|
4
|
+
import type { MemoryRecord } from "./store";
|
|
5
|
+
|
|
6
|
+
const MAX_EVENT_PREVIEW_CHARS = 500;
|
|
7
|
+
|
|
8
|
+
const capturedMemorySchema = z
|
|
9
|
+
.object({
|
|
10
|
+
content: z.string().min(1),
|
|
11
|
+
id: z.string().min(1),
|
|
12
|
+
kind: z.enum(MEMORY_KINDS),
|
|
13
|
+
observedAtMs: z.number().finite(),
|
|
14
|
+
scope: z.enum(MEMORY_SCOPES),
|
|
15
|
+
})
|
|
16
|
+
.strict();
|
|
17
|
+
|
|
18
|
+
function eventPreview(content: string): string {
|
|
19
|
+
const trimmed = content.trim();
|
|
20
|
+
if (trimmed.length <= MAX_EVENT_PREVIEW_CHARS) return trimmed;
|
|
21
|
+
return `${trimmed.slice(0, MAX_EVENT_PREVIEW_CHARS - 3).trimEnd()}...`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Durable transcript event emitted after background memory capture. */
|
|
25
|
+
export const memoriesCapturedEvent = defineConversationEvent({
|
|
26
|
+
name: "memories_captured",
|
|
27
|
+
version: 1,
|
|
28
|
+
schema: z
|
|
29
|
+
.object({
|
|
30
|
+
memories: z.array(capturedMemorySchema).min(1).max(100),
|
|
31
|
+
})
|
|
32
|
+
.strict(),
|
|
33
|
+
renderEvent(event) {
|
|
34
|
+
const count = event.memories.length;
|
|
35
|
+
return {
|
|
36
|
+
icon: "brain",
|
|
37
|
+
title: count === 1 ? "Memory captured" : "Memories captured",
|
|
38
|
+
preview:
|
|
39
|
+
count === 1
|
|
40
|
+
? eventPreview(event.memories[0]!.content)
|
|
41
|
+
: `${count} memories`,
|
|
42
|
+
details: event.memories.map((memory) => ({
|
|
43
|
+
title: memory.content,
|
|
44
|
+
metadata: [memory.kind, memory.scope],
|
|
45
|
+
})),
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/** Select the stable, safe memory fields retained in conversation history. */
|
|
51
|
+
export function capturedMemory(memory: MemoryRecord) {
|
|
52
|
+
return {
|
|
53
|
+
content: memory.content,
|
|
54
|
+
id: memory.id,
|
|
55
|
+
kind: memory.kind,
|
|
56
|
+
observedAtMs: memory.observedAtMs,
|
|
57
|
+
scope: memory.scope,
|
|
58
|
+
};
|
|
59
|
+
}
|
package/src/plugin.ts
CHANGED
|
@@ -14,6 +14,7 @@ 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
18
|
import type { MemoryDb } from "./store";
|
|
18
19
|
import { createMemoryUserPage } from "./user-pages";
|
|
19
20
|
|
|
@@ -100,6 +101,7 @@ export function memoryPlugin(options: MemoryPluginOptions = {}) {
|
|
|
100
101
|
? { structuredModelId: modelId }
|
|
101
102
|
: { structuredModel: "default" },
|
|
102
103
|
packageName: "@sentry/junior-memory",
|
|
104
|
+
conversationEvents: [memoriesCapturedEvent],
|
|
103
105
|
cli: {
|
|
104
106
|
commands: [createMemoryCliCommand()],
|
|
105
107
|
},
|
package/src/process-session.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { z } from "zod";
|
|
|
10
10
|
import {
|
|
11
11
|
createMemoryStore,
|
|
12
12
|
type CreateMemoryInput,
|
|
13
|
+
type CreateMemoryResult,
|
|
13
14
|
type MemoryDb,
|
|
14
15
|
} from "./store";
|
|
15
16
|
import {
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
type ExtractedMemory,
|
|
19
20
|
} from "./agent";
|
|
20
21
|
import { MEMORY_KINDS, memoryRuntimeContextSchema } from "./types";
|
|
22
|
+
import { capturedMemory, memoriesCapturedEvent } from "./events";
|
|
21
23
|
|
|
22
24
|
const MEMORY_TOOL_NAMES = new Set([
|
|
23
25
|
"createMemory",
|
|
@@ -44,6 +46,21 @@ const extractedMemoryCacheSchema = z.array(
|
|
|
44
46
|
/** Where a passively extracted memory may be stored, or dropped when unproven. */
|
|
45
47
|
type MemoryRouteTarget = "drop" | "personal" | "conversation";
|
|
46
48
|
|
|
49
|
+
function recordCapturedMemory(
|
|
50
|
+
captured: ReturnType<typeof capturedMemory>[],
|
|
51
|
+
result: CreateMemoryResult,
|
|
52
|
+
): void {
|
|
53
|
+
const supersededIds = new Set(result.supersededIds ?? []);
|
|
54
|
+
for (let index = captured.length - 1; index >= 0; index -= 1) {
|
|
55
|
+
if (supersededIds.has(captured[index]!.id)) {
|
|
56
|
+
captured.splice(index, 1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (result.created || result.idempotent) {
|
|
60
|
+
captured.push(capturedMemory(result.memory));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
47
64
|
/** A cited entry is a run-actor durable instruction evidence entry. */
|
|
48
65
|
function isRunActorInstruction(entry: PluginRunTranscriptEntry): boolean {
|
|
49
66
|
return (
|
|
@@ -252,6 +269,7 @@ export async function processMemorySession(
|
|
|
252
269
|
return;
|
|
253
270
|
}
|
|
254
271
|
|
|
272
|
+
const captured: ReturnType<typeof capturedMemory>[] = [];
|
|
255
273
|
for (const memory of memories) {
|
|
256
274
|
// The routing gate stays even though extraction is also actor-gated:
|
|
257
275
|
// getTaskMemories caches extraction output for 7 days, so a retry can replay
|
|
@@ -262,9 +280,14 @@ export async function processMemorySession(
|
|
|
262
280
|
}
|
|
263
281
|
const input = passiveInput(run.runId, memory, sourceKey, target);
|
|
264
282
|
if (target === "conversation") {
|
|
265
|
-
await store.createConversationMemory(input);
|
|
283
|
+
const result = await store.createConversationMemory(input);
|
|
284
|
+
recordCapturedMemory(captured, result);
|
|
266
285
|
continue;
|
|
267
286
|
}
|
|
268
|
-
await store.createMemory(input);
|
|
287
|
+
const result = await store.createMemory(input);
|
|
288
|
+
recordCapturedMemory(captured, result);
|
|
289
|
+
}
|
|
290
|
+
if (captured.length > 0) {
|
|
291
|
+
await context.events.emit(memoriesCapturedEvent({ memories: captured }));
|
|
269
292
|
}
|
|
270
293
|
}
|
package/src/store.ts
CHANGED
|
@@ -248,7 +248,11 @@ export type CreateMemoryInput = z.output<typeof createMemoryInputSchema>;
|
|
|
248
248
|
/** Result of a memory write after idempotency checks. */
|
|
249
249
|
export interface CreateMemoryResult {
|
|
250
250
|
created: boolean;
|
|
251
|
+
/** True when this call found the memory previously written for the same input identity. */
|
|
252
|
+
idempotent?: true;
|
|
251
253
|
memory: MemoryRecord;
|
|
254
|
+
/** Memory ids made inactive by this write. */
|
|
255
|
+
supersededIds?: string[];
|
|
252
256
|
}
|
|
253
257
|
|
|
254
258
|
export type ListMemoriesInput = z.output<typeof listMemoriesInputSchema>;
|
|
@@ -441,12 +445,17 @@ export function activeVisiblePredicate(args: {
|
|
|
441
445
|
}
|
|
442
446
|
|
|
443
447
|
/** Resolve retry attempts for the same scoped write idempotency key. */
|
|
448
|
+
interface IdempotencyMatch {
|
|
449
|
+
memory: MemoryRecord;
|
|
450
|
+
outcome: "created" | "duplicate";
|
|
451
|
+
}
|
|
452
|
+
|
|
444
453
|
async function findByIdempotencyKey(args: {
|
|
445
454
|
db: MemoryDb;
|
|
446
455
|
idempotencyKey: string;
|
|
447
456
|
nowMs: number;
|
|
448
457
|
scope: ResolvedMemoryScope;
|
|
449
|
-
}): Promise<
|
|
458
|
+
}): Promise<IdempotencyMatch | undefined> {
|
|
450
459
|
const activeRows = await args.db
|
|
451
460
|
.select()
|
|
452
461
|
.from(juniorMemoryMemories)
|
|
@@ -466,7 +475,7 @@ async function findByIdempotencyKey(args: {
|
|
|
466
475
|
)
|
|
467
476
|
.limit(1);
|
|
468
477
|
if (activeRows[0]) {
|
|
469
|
-
return parseMemoryRow(activeRows[0]);
|
|
478
|
+
return { memory: parseMemoryRow(activeRows[0]), outcome: "created" };
|
|
470
479
|
}
|
|
471
480
|
|
|
472
481
|
const aliasRows = await args.db
|
|
@@ -513,7 +522,7 @@ async function findByIdempotencyKey(args: {
|
|
|
513
522
|
)
|
|
514
523
|
.limit(1);
|
|
515
524
|
if (rows[0]) {
|
|
516
|
-
return parseMemoryRow(rows[0]);
|
|
525
|
+
return { memory: parseMemoryRow(rows[0]), outcome: "duplicate" };
|
|
517
526
|
}
|
|
518
527
|
}
|
|
519
528
|
return undefined;
|
|
@@ -1183,13 +1192,15 @@ export function createMemoryStore(
|
|
|
1183
1192
|
});
|
|
1184
1193
|
if (idempotent) {
|
|
1185
1194
|
await storeEmbedding({
|
|
1186
|
-
content: idempotent.content,
|
|
1195
|
+
content: idempotent.memory.content,
|
|
1187
1196
|
db,
|
|
1188
1197
|
embedder,
|
|
1189
|
-
memoryId: idempotent.id,
|
|
1198
|
+
memoryId: idempotent.memory.id,
|
|
1190
1199
|
nowMs,
|
|
1191
1200
|
});
|
|
1192
|
-
return
|
|
1201
|
+
return idempotent.outcome === "created"
|
|
1202
|
+
? { created: false, idempotent: true, memory: idempotent.memory }
|
|
1203
|
+
: { created: false, memory: idempotent.memory };
|
|
1193
1204
|
}
|
|
1194
1205
|
}
|
|
1195
1206
|
|
|
@@ -1256,7 +1267,7 @@ export function createMemoryStore(
|
|
|
1256
1267
|
}
|
|
1257
1268
|
|
|
1258
1269
|
const id = randomUUID();
|
|
1259
|
-
const
|
|
1270
|
+
const write = await db.transaction(async (tx) => {
|
|
1260
1271
|
const inserted = await tx
|
|
1261
1272
|
.insert(juniorMemoryMemories)
|
|
1262
1273
|
.values({
|
|
@@ -1285,7 +1296,7 @@ export function createMemoryStore(
|
|
|
1285
1296
|
.returning();
|
|
1286
1297
|
const insertedMemory = inserted[0];
|
|
1287
1298
|
if (!insertedMemory || supersededIds.length === 0) {
|
|
1288
|
-
return inserted;
|
|
1299
|
+
return { inserted, supersededIds: [] };
|
|
1289
1300
|
}
|
|
1290
1301
|
const superseded = await tx
|
|
1291
1302
|
.update(juniorMemoryMemories)
|
|
@@ -1311,10 +1322,10 @@ export function createMemoryStore(
|
|
|
1311
1322
|
.delete(juniorMemoryEmbeddings)
|
|
1312
1323
|
.where(inArray(juniorMemoryEmbeddings.memoryId, idsToClean));
|
|
1313
1324
|
}
|
|
1314
|
-
return inserted;
|
|
1325
|
+
return { inserted, supersededIds: idsToClean };
|
|
1315
1326
|
});
|
|
1316
|
-
if (
|
|
1317
|
-
const memory = parseMemoryRow(
|
|
1327
|
+
if (write.inserted[0]) {
|
|
1328
|
+
const memory = parseMemoryRow(write.inserted[0]);
|
|
1318
1329
|
await storeEmbedding({
|
|
1319
1330
|
content: memory.content,
|
|
1320
1331
|
db,
|
|
@@ -1323,7 +1334,13 @@ export function createMemoryStore(
|
|
|
1323
1334
|
memoryId: memory.id,
|
|
1324
1335
|
nowMs,
|
|
1325
1336
|
});
|
|
1326
|
-
return {
|
|
1337
|
+
return {
|
|
1338
|
+
created: true,
|
|
1339
|
+
memory,
|
|
1340
|
+
...(write.supersededIds.length > 0
|
|
1341
|
+
? { supersededIds: write.supersededIds }
|
|
1342
|
+
: {}),
|
|
1343
|
+
};
|
|
1327
1344
|
}
|
|
1328
1345
|
|
|
1329
1346
|
const idempotent = await findByIdempotencyKey({
|
|
@@ -1336,13 +1353,15 @@ export function createMemoryStore(
|
|
|
1336
1353
|
throw new Error("Memory idempotency conflict did not resolve.");
|
|
1337
1354
|
}
|
|
1338
1355
|
await storeEmbedding({
|
|
1339
|
-
content: idempotent.content,
|
|
1356
|
+
content: idempotent.memory.content,
|
|
1340
1357
|
db,
|
|
1341
1358
|
embedder,
|
|
1342
|
-
memoryId: idempotent.id,
|
|
1359
|
+
memoryId: idempotent.memory.id,
|
|
1343
1360
|
nowMs,
|
|
1344
1361
|
});
|
|
1345
|
-
return
|
|
1362
|
+
return idempotent.outcome === "created"
|
|
1363
|
+
? { created: false, idempotent: true, memory: idempotent.memory }
|
|
1364
|
+
: { created: false, memory: idempotent.memory };
|
|
1346
1365
|
}
|
|
1347
1366
|
|
|
1348
1367
|
async function retrieveVisibleMemories(
|
package/src/tools.ts
CHANGED
|
@@ -420,6 +420,12 @@ function memoryToolResult<TData extends Record<string, unknown>>(
|
|
|
420
420
|
/** Create a tool that submits an explicit memory candidate for storage. */
|
|
421
421
|
export function createMemoryCreateTool(context: MemoryCreateToolContext) {
|
|
422
422
|
return definePluginTool({
|
|
423
|
+
annotations: {
|
|
424
|
+
destructiveHint: false,
|
|
425
|
+
idempotentHint: true,
|
|
426
|
+
openWorldHint: false,
|
|
427
|
+
readOnlyHint: false,
|
|
428
|
+
},
|
|
423
429
|
description:
|
|
424
430
|
"Explicit memory-write tool. Use only when the latest user message directly asks Junior to remember, store, save, or forget-and-replace a public/shareable fact. Do not use for ordinary statements like 'I prefer X', 'I use Y', or 'X goes before Y' unless the user also asks you to remember/store/save it; passive memory learning handles those after the visible reply. Pass one self-contained natural-language candidate preserving the user's explicit memory intent. Do not ask the user to rephrase ordinary first-person facts, and do not rewrite them into display-name or third-person wording. Do not include secrets, private personal details, medical/legal/financial/sensitive facts, or another person's personal preference, opinion, habit, identity, relationship, workflow, or private life. Runtime context derives actor, scope, source, and subject ids; the memory agent decides canonical stored content and memory kind, then the plugin derives storage target from kind.",
|
|
425
431
|
executionMode: "sequential",
|
|
@@ -506,6 +512,12 @@ export function createMemoryCreateTool(context: MemoryCreateToolContext) {
|
|
|
506
512
|
/** Create a tool that archives a visible memory in the active context. */
|
|
507
513
|
export function createMemoryRemoveTool(context: MemoryToolContext) {
|
|
508
514
|
return definePluginTool({
|
|
515
|
+
annotations: {
|
|
516
|
+
destructiveHint: true,
|
|
517
|
+
idempotentHint: true,
|
|
518
|
+
openWorldHint: false,
|
|
519
|
+
readOnlyHint: false,
|
|
520
|
+
},
|
|
509
521
|
description:
|
|
510
522
|
"Forget one memory visible in the active context. Use only ids or short id prefixes returned by listMemories or searchMemories. Never remove memories by hidden actor, Slack, scope, or subject identifiers.",
|
|
511
523
|
executionMode: "sequential",
|
|
@@ -535,7 +547,12 @@ export function createMemoryListTool(context: MemoryToolContext) {
|
|
|
535
547
|
return definePluginTool({
|
|
536
548
|
description:
|
|
537
549
|
"List active memories visible in the current context. Use when the user asks what Junior remembers or when memory ids are needed before removing a memory.",
|
|
538
|
-
annotations: {
|
|
550
|
+
annotations: {
|
|
551
|
+
destructiveHint: false,
|
|
552
|
+
idempotentHint: true,
|
|
553
|
+
openWorldHint: false,
|
|
554
|
+
readOnlyHint: true,
|
|
555
|
+
},
|
|
539
556
|
inputSchema: listMemoriesInputSchema,
|
|
540
557
|
outputSchema: memoryManyOutputSchema,
|
|
541
558
|
execute: async (input) => {
|
|
@@ -555,7 +572,12 @@ export function createMemorySearchTool(context: MemoryToolContext) {
|
|
|
555
572
|
return definePluginTool({
|
|
556
573
|
description:
|
|
557
574
|
"Search active memories visible in the current context. Use when the model needs targeted memory recall. The tool searches only the current actor and active conversation scopes.",
|
|
558
|
-
annotations: {
|
|
575
|
+
annotations: {
|
|
576
|
+
destructiveHint: false,
|
|
577
|
+
idempotentHint: true,
|
|
578
|
+
openWorldHint: false,
|
|
579
|
+
readOnlyHint: true,
|
|
580
|
+
},
|
|
559
581
|
inputSchema: searchMemoriesInputSchema,
|
|
560
582
|
outputSchema: memoryManyOutputSchema,
|
|
561
583
|
execute: async (input) => {
|