@sentry/junior-memory 0.179.0 → 0.181.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/README.md +25 -24
- package/dist/agent.d.ts +4 -0
- package/dist/api.d.ts +3 -4
- package/dist/db/schema.d.ts +19 -2
- package/dist/events.d.ts +3 -3
- package/dist/index.js +527 -706
- package/dist/index.js.map +1 -1
- package/dist/process-session.d.ts +3 -4
- package/dist/ranking.d.ts +0 -2
- package/dist/recall.d.ts +9 -2
- package/dist/scope.d.ts +11 -15
- package/dist/store.d.ts +6 -7
- package/dist/tools.d.ts +8 -1
- package/dist/types.d.ts +4 -2
- package/dist/user-pages.d.ts +1 -1
- package/dist/viewer.d.ts +85 -0
- package/migrations/0009_faithful_whizzer.sql +114 -0
- package/migrations/meta/0009_snapshot.json +411 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +4 -3
- package/src/agent.ts +9 -11
- package/src/api.ts +36 -27
- package/src/db/schema.ts +3 -1
- package/src/events.ts +30 -8
- package/src/operational-report.ts +20 -20
- package/src/plugin.ts +19 -7
- package/src/process-session.ts +23 -37
- package/src/ranking.ts +11 -28
- package/src/recall.ts +16 -6
- package/src/scope.ts +34 -135
- package/src/store.ts +43 -100
- package/src/tools.ts +37 -21
- package/src/types.ts +5 -2
- package/src/user-pages.ts +6 -8
- package/src/viewer.ts +393 -0
- package/dist/personal-store.d.ts +0 -92
- package/dist/personal.d.ts +0 -31
- package/src/personal-store.ts +0 -421
- package/src/personal.ts +0 -140
package/src/db/schema.ts
CHANGED
|
@@ -48,6 +48,8 @@ export const juniorMemoryMemories = pgTable(
|
|
|
48
48
|
enum: MEMORY_SOURCE_PLATFORMS,
|
|
49
49
|
}).notNull(),
|
|
50
50
|
sourceKey: text("source_key").notNull(),
|
|
51
|
+
/** Location where Junior learned the memory, when known. */
|
|
52
|
+
locationId: text("location_id"),
|
|
51
53
|
idempotencyKey: text("idempotency_key"),
|
|
52
54
|
observedAtMs: bigint("observed_at_ms", { mode: "number" }).notNull(),
|
|
53
55
|
createdAtMs: bigint("created_at_ms", { mode: "number" }).notNull(),
|
|
@@ -80,7 +82,7 @@ export const juniorMemoryMemories = pgTable(
|
|
|
80
82
|
),
|
|
81
83
|
check(
|
|
82
84
|
"junior_memory_memories_scope_check",
|
|
83
|
-
sql`${table.scope} IN ('
|
|
85
|
+
sql`${table.scope} IN ('private', 'public')`,
|
|
84
86
|
),
|
|
85
87
|
check(
|
|
86
88
|
"junior_memory_memories_kind_check",
|
package/src/events.ts
CHANGED
|
@@ -3,12 +3,23 @@ import { z } from "zod";
|
|
|
3
3
|
import { MEMORY_KINDS, MEMORY_SCOPES } from "./types";
|
|
4
4
|
import type { MemoryRecord } from "./store";
|
|
5
5
|
|
|
6
|
+
const capturedMemoryFields = {
|
|
7
|
+
content: z.string().min(1),
|
|
8
|
+
id: z.string().min(1),
|
|
9
|
+
kind: z.enum(MEMORY_KINDS),
|
|
10
|
+
observedAtMs: z.number().finite(),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const legacyCapturedMemorySchema = z
|
|
14
|
+
.object({
|
|
15
|
+
...capturedMemoryFields,
|
|
16
|
+
scope: z.enum(["personal", "conversation"]),
|
|
17
|
+
})
|
|
18
|
+
.strict();
|
|
19
|
+
|
|
6
20
|
const capturedMemorySchema = z
|
|
7
21
|
.object({
|
|
8
|
-
|
|
9
|
-
id: z.string().min(1),
|
|
10
|
-
kind: z.enum(MEMORY_KINDS),
|
|
11
|
-
observedAtMs: z.number().finite(),
|
|
22
|
+
...capturedMemoryFields,
|
|
12
23
|
scope: z.enum(MEMORY_SCOPES),
|
|
13
24
|
})
|
|
14
25
|
.strict();
|
|
@@ -28,9 +39,20 @@ const recalledMemoriesSchema = z
|
|
|
28
39
|
})
|
|
29
40
|
.strict();
|
|
30
41
|
|
|
31
|
-
function
|
|
32
|
-
|
|
42
|
+
function currentScope(
|
|
43
|
+
scope: "personal" | "conversation" | "private" | "public",
|
|
33
44
|
) {
|
|
45
|
+
if (scope === "personal") return "private";
|
|
46
|
+
if (scope === "conversation") return "public";
|
|
47
|
+
return scope;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderCapturedMemories(event: {
|
|
51
|
+
memories: Array<
|
|
52
|
+
| z.output<typeof legacyCapturedMemorySchema>
|
|
53
|
+
| z.output<typeof capturedMemorySchema>
|
|
54
|
+
>;
|
|
55
|
+
}) {
|
|
34
56
|
const count = event.memories.length;
|
|
35
57
|
if (count === 0) return undefined;
|
|
36
58
|
return {
|
|
@@ -38,7 +60,7 @@ function renderCapturedMemories(
|
|
|
38
60
|
title: `${count} ${count === 1 ? "memory" : "memories"} captured`,
|
|
39
61
|
details: event.memories.map((memory) => ({
|
|
40
62
|
title: memory.content,
|
|
41
|
-
metadata: [memory.kind, memory.scope],
|
|
63
|
+
metadata: [memory.kind, currentScope(memory.scope)],
|
|
42
64
|
})),
|
|
43
65
|
};
|
|
44
66
|
}
|
|
@@ -49,7 +71,7 @@ export const memoriesCapturedEventV1 = defineConversationEvent({
|
|
|
49
71
|
version: 1,
|
|
50
72
|
schema: z
|
|
51
73
|
.object({
|
|
52
|
-
memories: z.array(
|
|
74
|
+
memories: z.array(legacyCapturedMemorySchema).min(1).max(100),
|
|
53
75
|
})
|
|
54
76
|
.strict(),
|
|
55
77
|
renderEvent: renderCapturedMemories,
|
|
@@ -12,9 +12,9 @@ const WINDOWS = [7, 30, 90] as const;
|
|
|
12
12
|
|
|
13
13
|
const memoryDaySchema = z
|
|
14
14
|
.object({
|
|
15
|
-
conversation: z.number().int().nonnegative(),
|
|
16
15
|
date: z.string().date(),
|
|
17
|
-
|
|
16
|
+
private: z.number().int().nonnegative(),
|
|
17
|
+
public: z.number().int().nonnegative(),
|
|
18
18
|
})
|
|
19
19
|
.strict();
|
|
20
20
|
|
|
@@ -55,11 +55,11 @@ async function aggregateMemoryDays(args: { db: MemoryDb; nowMs: number }) {
|
|
|
55
55
|
to_timestamp(${table.createdAtMs} / 1000.0) AT TIME ZONE 'UTC'
|
|
56
56
|
) AS day,
|
|
57
57
|
count(*) FILTER (
|
|
58
|
-
WHERE ${table.scope} = '
|
|
59
|
-
)::integer AS
|
|
58
|
+
WHERE ${table.scope} = 'private'
|
|
59
|
+
)::integer AS private,
|
|
60
60
|
count(*) FILTER (
|
|
61
|
-
WHERE ${table.scope} = '
|
|
62
|
-
)::integer AS
|
|
61
|
+
WHERE ${table.scope} = 'public'
|
|
62
|
+
)::integer AS public
|
|
63
63
|
FROM ${table}
|
|
64
64
|
WHERE ${table.createdAtMs} >= ${start.getTime()}
|
|
65
65
|
AND ${table.createdAtMs} < ${endExclusiveMs}
|
|
@@ -70,8 +70,8 @@ async function aggregateMemoryDays(args: { db: MemoryDb; nowMs: number }) {
|
|
|
70
70
|
)
|
|
71
71
|
SELECT
|
|
72
72
|
to_char(days.day, 'YYYY-MM-DD') AS date,
|
|
73
|
-
coalesce(daily.
|
|
74
|
-
coalesce(daily.
|
|
73
|
+
coalesce(daily.private, 0)::integer AS private,
|
|
74
|
+
coalesce(daily.public, 0)::integer AS public
|
|
75
75
|
FROM days
|
|
76
76
|
LEFT JOIN daily ON daily.day = days.day
|
|
77
77
|
ORDER BY days.day
|
|
@@ -119,8 +119,8 @@ export async function buildMemoryOperationalReport(args: {
|
|
|
119
119
|
args.db
|
|
120
120
|
.select({
|
|
121
121
|
active: sql<number>`count(*) filter (where ${active})`.mapWith(Number),
|
|
122
|
-
|
|
123
|
-
sql<number>`count(*) filter (where ${active} and ${juniorMemoryMemories.scope} = '
|
|
122
|
+
public:
|
|
123
|
+
sql<number>`count(*) filter (where ${active} and ${juniorMemoryMemories.scope} = 'public')`.mapWith(
|
|
124
124
|
Number,
|
|
125
125
|
),
|
|
126
126
|
createdThirtyDays:
|
|
@@ -131,8 +131,8 @@ export async function buildMemoryOperationalReport(args: {
|
|
|
131
131
|
sql<number>`count(${juniorMemoryEmbeddings.memoryId}) filter (where ${active})`.mapWith(
|
|
132
132
|
Number,
|
|
133
133
|
),
|
|
134
|
-
|
|
135
|
-
sql<number>`count(*) filter (where ${active} and ${juniorMemoryMemories.scope} = '
|
|
134
|
+
private:
|
|
135
|
+
sql<number>`count(*) filter (where ${active} and ${juniorMemoryMemories.scope} = 'private')`.mapWith(
|
|
136
136
|
Number,
|
|
137
137
|
),
|
|
138
138
|
})
|
|
@@ -171,12 +171,12 @@ export async function buildMemoryOperationalReport(args: {
|
|
|
171
171
|
value: formatCount(counts?.createdThirtyDays ?? 0),
|
|
172
172
|
},
|
|
173
173
|
{
|
|
174
|
-
label: "
|
|
175
|
-
value: formatCount(counts?.
|
|
174
|
+
label: "private",
|
|
175
|
+
value: formatCount(counts?.private ?? 0),
|
|
176
176
|
},
|
|
177
177
|
{
|
|
178
|
-
label: "
|
|
179
|
-
value: formatCount(counts?.
|
|
178
|
+
label: "public",
|
|
179
|
+
value: formatCount(counts?.public ?? 0),
|
|
180
180
|
},
|
|
181
181
|
{
|
|
182
182
|
label: "embedding coverage",
|
|
@@ -208,15 +208,15 @@ export async function buildMemoryOperationalReport(args: {
|
|
|
208
208
|
id: day.date,
|
|
209
209
|
label: day.date,
|
|
210
210
|
values: {
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
private: day.private,
|
|
212
|
+
public: day.public,
|
|
213
213
|
},
|
|
214
214
|
})),
|
|
215
215
|
description: "Memories stored per day by scope",
|
|
216
216
|
id: "memories-created",
|
|
217
217
|
series: [
|
|
218
|
-
{ key: "
|
|
219
|
-
{ key: "
|
|
218
|
+
{ key: "private", label: "Private" },
|
|
219
|
+
{ key: "public", label: "Public" },
|
|
220
220
|
],
|
|
221
221
|
timeRangeDays: [...WINDOWS],
|
|
222
222
|
title: "Memories created",
|
package/src/plugin.ts
CHANGED
|
@@ -46,18 +46,24 @@ function memoryToolContext(ctx: {
|
|
|
46
46
|
conversationId?: string;
|
|
47
47
|
db: MemoryToolContext["db"];
|
|
48
48
|
embedder?: MemoryToolContext["embedder"];
|
|
49
|
+
locationId?: string;
|
|
49
50
|
actor?: MemoryToolContext["actor"];
|
|
50
51
|
source: MemoryToolContext["source"];
|
|
52
|
+
users: MemoryToolContext["users"];
|
|
51
53
|
userText?: string;
|
|
52
54
|
}): MemoryToolContext {
|
|
53
55
|
return {
|
|
54
56
|
agent: ctx.agent,
|
|
55
|
-
...(ctx.conversationId
|
|
56
|
-
|
|
57
|
+
...(ctx.conversationId
|
|
58
|
+
? { conversationId: ctx.conversationId }
|
|
59
|
+
: undefined),
|
|
60
|
+
...(ctx.actor ? { actor: ctx.actor } : undefined),
|
|
57
61
|
db: ctx.db,
|
|
58
|
-
...(ctx.embedder ? { embedder: ctx.embedder } :
|
|
62
|
+
...(ctx.embedder ? { embedder: ctx.embedder } : undefined),
|
|
63
|
+
...(ctx.locationId ? { locationId: ctx.locationId } : undefined),
|
|
59
64
|
source: ctx.source,
|
|
60
|
-
|
|
65
|
+
users: ctx.users,
|
|
66
|
+
...(ctx.userText ? { userText: ctx.userText } : undefined),
|
|
61
67
|
};
|
|
62
68
|
}
|
|
63
69
|
|
|
@@ -66,9 +72,11 @@ function memoryCreateToolContext(ctx: {
|
|
|
66
72
|
conversationId?: string;
|
|
67
73
|
db: MemoryCreateToolContext["db"];
|
|
68
74
|
embedder?: MemoryCreateToolContext["embedder"];
|
|
75
|
+
locationId?: string;
|
|
69
76
|
actor?: MemoryCreateToolContext["actor"];
|
|
70
77
|
source: MemoryCreateToolContext["source"];
|
|
71
78
|
supersessionDecider: MemoryCreateToolContext["supersessionDecider"];
|
|
79
|
+
users: MemoryCreateToolContext["users"];
|
|
72
80
|
userText?: string;
|
|
73
81
|
}): MemoryCreateToolContext {
|
|
74
82
|
return {
|
|
@@ -157,18 +165,22 @@ export function memoryPlugin(options: MemoryPluginOptions = {}) {
|
|
|
157
165
|
agent: createMemoryAgent(ctx.model),
|
|
158
166
|
...(ctx.conversationId
|
|
159
167
|
? { conversationId: ctx.conversationId }
|
|
160
|
-
:
|
|
161
|
-
...(ctx.actor ? { actor: ctx.actor } :
|
|
168
|
+
: undefined),
|
|
169
|
+
...(ctx.actor ? { actor: ctx.actor } : undefined),
|
|
162
170
|
db: ctx.db as MemoryDb,
|
|
163
171
|
embedder: ctx.embedder,
|
|
164
172
|
events: ctx.events,
|
|
173
|
+
...(ctx.locationId
|
|
174
|
+
? { locationId: ctx.locationId }
|
|
175
|
+
: undefined),
|
|
165
176
|
log: ctx.log,
|
|
166
177
|
source: ctx.source,
|
|
167
178
|
text: ctx.text,
|
|
179
|
+
users: ctx.users,
|
|
168
180
|
});
|
|
169
181
|
},
|
|
170
182
|
}
|
|
171
|
-
:
|
|
183
|
+
: undefined),
|
|
172
184
|
},
|
|
173
185
|
});
|
|
174
186
|
}
|
package/src/process-session.ts
CHANGED
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
type PluginRunContext,
|
|
5
5
|
type PluginRunTranscriptEntry,
|
|
6
6
|
type PluginTaskContext,
|
|
7
|
-
type Source,
|
|
8
7
|
} from "@sentry/junior-plugin-api";
|
|
9
8
|
import { z } from "zod";
|
|
10
9
|
import {
|
|
@@ -54,23 +53,8 @@ const extractedMemoryCacheSchema = z.union([
|
|
|
54
53
|
.transform((memories) => ({ memories })),
|
|
55
54
|
]);
|
|
56
55
|
|
|
57
|
-
/**
|
|
58
|
-
type
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* V1 passive learning opts in by Source branch, then public vs private.
|
|
62
|
-
* Public API is the same as public Slack: shared conversation evidence may
|
|
63
|
-
* learn. Private sources stay out. Local remains available for QA.
|
|
64
|
-
*/
|
|
65
|
-
function allowsPassiveMemoryExtraction(source: Source): boolean {
|
|
66
|
-
switch (source.platform) {
|
|
67
|
-
case "local":
|
|
68
|
-
return true;
|
|
69
|
-
case "web":
|
|
70
|
-
case "slack":
|
|
71
|
-
return source.visibility === "public";
|
|
72
|
-
}
|
|
73
|
-
}
|
|
56
|
+
/** Subject for a passively extracted memory, or drop when unproven. */
|
|
57
|
+
type MemorySubjectTarget = "drop" | "user" | "conversation";
|
|
74
58
|
|
|
75
59
|
function recordCapturedMemory(
|
|
76
60
|
captured: ReturnType<typeof capturedMemory>[],
|
|
@@ -151,8 +135,8 @@ function citedEntries(
|
|
|
151
135
|
function routeExtractedMemory(
|
|
152
136
|
memory: ExtractedMemory,
|
|
153
137
|
transcript: PluginRunTranscriptEntry[],
|
|
154
|
-
run: Pick<PluginRunContext, "actor" | "actors">,
|
|
155
|
-
):
|
|
138
|
+
run: Pick<PluginRunContext, "actor" | "actors" | "actorUserId">,
|
|
139
|
+
): MemorySubjectTarget {
|
|
156
140
|
const cited = citedEntries(memory.evidenceMessageIndices, transcript);
|
|
157
141
|
if (!cited.valid) {
|
|
158
142
|
return "drop";
|
|
@@ -160,6 +144,7 @@ function routeExtractedMemory(
|
|
|
160
144
|
if (memory.kind === "preference") {
|
|
161
145
|
// Only a run attributed to exactly one human run actor may store a preference.
|
|
162
146
|
const exactlyOneHumanRunActor =
|
|
147
|
+
run.actorUserId !== undefined &&
|
|
163
148
|
run.actor !== undefined &&
|
|
164
149
|
run.actor.platform !== "system" &&
|
|
165
150
|
run.actors.length === 1 &&
|
|
@@ -167,8 +152,8 @@ function routeExtractedMemory(
|
|
|
167
152
|
if (!exactlyOneHumanRunActor) {
|
|
168
153
|
return "drop";
|
|
169
154
|
}
|
|
170
|
-
// Never downgrade an unproven first-person preference to conversation
|
|
171
|
-
return cited.entries.every(isRunActorInstruction) ? "
|
|
155
|
+
// Never downgrade an unproven first-person preference to conversation subject.
|
|
156
|
+
return cited.entries.every(isRunActorInstruction) ? "user" : "drop";
|
|
172
157
|
}
|
|
173
158
|
return cited.entries.every(
|
|
174
159
|
(entry) => isRunActorInstruction(entry) || isConversationEvidence(entry),
|
|
@@ -179,7 +164,7 @@ function routeExtractedMemory(
|
|
|
179
164
|
|
|
180
165
|
function memoryIdempotencySuffix(
|
|
181
166
|
memory: ExtractedMemory,
|
|
182
|
-
target:
|
|
167
|
+
target: MemorySubjectTarget,
|
|
183
168
|
): string {
|
|
184
169
|
return createHash("sha256")
|
|
185
170
|
.update(target)
|
|
@@ -197,13 +182,15 @@ function passiveInput(
|
|
|
197
182
|
sessionId: string,
|
|
198
183
|
memory: ExtractedMemory,
|
|
199
184
|
sourceKey: string,
|
|
200
|
-
target:
|
|
185
|
+
target: MemorySubjectTarget,
|
|
201
186
|
): CreateMemoryInput {
|
|
202
187
|
return {
|
|
203
188
|
content: memory.content,
|
|
204
189
|
idempotencyKey: `session:${sourceKey}:${sessionId}:${memoryIdempotencySuffix(memory, target)}`,
|
|
205
190
|
kind: memory.kind,
|
|
206
|
-
...(memory.expiresAtMs !== null
|
|
191
|
+
...(memory.expiresAtMs !== null
|
|
192
|
+
? { expiresAtMs: memory.expiresAtMs }
|
|
193
|
+
: undefined),
|
|
207
194
|
};
|
|
208
195
|
}
|
|
209
196
|
|
|
@@ -228,15 +215,17 @@ async function getTaskExtraction(
|
|
|
228
215
|
/**
|
|
229
216
|
* Extract and store memories from a completed session plugin task.
|
|
230
217
|
*
|
|
231
|
-
* Memory owns
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
* or private conversations.
|
|
218
|
+
* Memory owns learning after a run and reads only the plugin run data.
|
|
219
|
+
* Explicit memory tools stay separate so retries do not reinterpret user
|
|
220
|
+
* requests.
|
|
235
221
|
*/
|
|
236
222
|
export async function processMemorySession(
|
|
237
223
|
context: PluginTaskContext,
|
|
238
224
|
): Promise<void> {
|
|
239
225
|
const run = await context.run.load();
|
|
226
|
+
if (run.source.platform === "local") {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
240
229
|
// Memory tool turns already own memory management or recall; do not reinterpret
|
|
241
230
|
// recalled memory output as fresh passive-learning evidence.
|
|
242
231
|
if (
|
|
@@ -247,13 +236,8 @@ export async function processMemorySession(
|
|
|
247
236
|
) {
|
|
248
237
|
return;
|
|
249
238
|
}
|
|
250
|
-
// V1 passive learning is a Source-branch policy: local QA always, public
|
|
251
|
-
// Slack/API by visibility, private sources never.
|
|
252
|
-
if (!allowsPassiveMemoryExtraction(run.source)) {
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
239
|
const sourceKey = getSourceKey(run.source);
|
|
256
|
-
if (!sourceKey) {
|
|
240
|
+
if (!sourceKey || (run.source.visibility === "private" && !run.actorUserId)) {
|
|
257
241
|
return;
|
|
258
242
|
}
|
|
259
243
|
const transcript = run.transcript
|
|
@@ -270,8 +254,10 @@ export async function processMemorySession(
|
|
|
270
254
|
|
|
271
255
|
const runtimeContext = memoryRuntimeContextSchema.parse({
|
|
272
256
|
conversationId: run.conversationId,
|
|
273
|
-
...(run.
|
|
257
|
+
...(run.locationId ? { locationId: run.locationId } : undefined),
|
|
258
|
+
...(run.actor ? { actor: run.actor } : undefined),
|
|
274
259
|
source: run.source,
|
|
260
|
+
...(run.actorUserId ? { userId: run.actorUserId } : undefined),
|
|
275
261
|
});
|
|
276
262
|
const agent = createMemoryAgent(context.model);
|
|
277
263
|
const store = createMemoryStore(context.db as MemoryDb, runtimeContext, {
|
|
@@ -317,7 +303,7 @@ export async function processMemorySession(
|
|
|
317
303
|
memories: captured,
|
|
318
304
|
...(extraction.costUsd !== undefined
|
|
319
305
|
? { costUsd: extraction.costUsd }
|
|
320
|
-
:
|
|
306
|
+
: undefined),
|
|
321
307
|
}),
|
|
322
308
|
);
|
|
323
309
|
}
|
package/src/ranking.ts
CHANGED
|
@@ -9,7 +9,6 @@ export interface MemoryMatch {
|
|
|
9
9
|
rank: number;
|
|
10
10
|
};
|
|
11
11
|
memory: MemoryRecord;
|
|
12
|
-
sourceKey: string;
|
|
13
12
|
vector?: {
|
|
14
13
|
rank: number;
|
|
15
14
|
};
|
|
@@ -33,13 +32,6 @@ function matchScore(
|
|
|
33
32
|
);
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
function currentChannel(
|
|
37
|
-
match: Pick<MemoryMatch, "sourceKey">,
|
|
38
|
-
channelPrefix: string | undefined,
|
|
39
|
-
): boolean {
|
|
40
|
-
return channelPrefix ? match.sourceKey.startsWith(channelPrefix) : false;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
35
|
function observedAgeRank(memory: MemoryRecord, nowMs: number): number {
|
|
44
36
|
const ageMs = Math.max(0, nowMs - memory.observedAtMs);
|
|
45
37
|
if (ageMs <= 7 * ONE_DAY_MS) {
|
|
@@ -64,7 +56,6 @@ function positiveWeight(value: number | undefined, fallback: number): number {
|
|
|
64
56
|
export function rankMemoryMatches(
|
|
65
57
|
matches: MemoryMatch[],
|
|
66
58
|
options: {
|
|
67
|
-
channelPrefix?: string;
|
|
68
59
|
/** Optional RRF weight for the lexical leg. Defaults to 1. */
|
|
69
60
|
lexicalWeight?: number;
|
|
70
61
|
nowMs: number;
|
|
@@ -83,15 +74,14 @@ export function rankMemoryMatches(
|
|
|
83
74
|
byId.set(match.memory.id, match);
|
|
84
75
|
continue;
|
|
85
76
|
}
|
|
86
|
-
// Keep the first rank
|
|
87
|
-
//
|
|
88
|
-
// with an inflated top rank for the same memory.
|
|
77
|
+
// Keep the first rank from each search. The shared searches run first, so
|
|
78
|
+
// the smaller private searches cannot replace their ranks.
|
|
89
79
|
byId.set(match.memory.id, {
|
|
90
80
|
...existing,
|
|
91
81
|
...(!existing.lexical && match.lexical
|
|
92
82
|
? { lexical: match.lexical }
|
|
93
|
-
:
|
|
94
|
-
...(!existing.vector && match.vector ? { vector: match.vector } :
|
|
83
|
+
: undefined),
|
|
84
|
+
...(!existing.vector && match.vector ? { vector: match.vector } : undefined),
|
|
95
85
|
});
|
|
96
86
|
}
|
|
97
87
|
return [...byId.values()].sort((left, right) => {
|
|
@@ -99,20 +89,13 @@ export function rankMemoryMatches(
|
|
|
99
89
|
if (scoreDelta !== 0) {
|
|
100
90
|
return scoreDelta;
|
|
101
91
|
}
|
|
102
|
-
// Prefer
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
Number(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return personalDelta;
|
|
110
|
-
}
|
|
111
|
-
const channelDelta =
|
|
112
|
-
Number(currentChannel(right, options.channelPrefix)) -
|
|
113
|
-
Number(currentChannel(left, options.channelPrefix));
|
|
114
|
-
if (channelDelta !== 0) {
|
|
115
|
-
return channelDelta;
|
|
92
|
+
// Prefer private memory when RRF ties. Public and private searches can give
|
|
93
|
+
// the same rank to common words.
|
|
94
|
+
const privateDelta =
|
|
95
|
+
Number(right.memory.scope === "private") -
|
|
96
|
+
Number(left.memory.scope === "private");
|
|
97
|
+
if (privateDelta !== 0) {
|
|
98
|
+
return privateDelta;
|
|
116
99
|
}
|
|
117
100
|
return (
|
|
118
101
|
observedAgeRank(right.memory, options.nowMs) -
|
package/src/recall.ts
CHANGED
|
@@ -2,9 +2,11 @@ import {
|
|
|
2
2
|
definePromptContext,
|
|
3
3
|
type UserPromptContribution,
|
|
4
4
|
type Actor,
|
|
5
|
+
type Identity,
|
|
5
6
|
type PluginConversationEvents,
|
|
6
7
|
type PluginLogger,
|
|
7
8
|
type Source,
|
|
9
|
+
type User,
|
|
8
10
|
} from "@sentry/junior-plugin-api";
|
|
9
11
|
import { z } from "zod";
|
|
10
12
|
import type { MemoryAgent, MemoryRecallResult } from "./agent";
|
|
@@ -28,9 +30,13 @@ export interface MemoryRecallContext {
|
|
|
28
30
|
embedder?: MemoryEmbeddingProvider;
|
|
29
31
|
events?: PluginConversationEvents;
|
|
30
32
|
log: PluginLogger;
|
|
33
|
+
locationId?: string;
|
|
31
34
|
actor?: Actor;
|
|
32
35
|
source: Source;
|
|
33
36
|
text: string;
|
|
37
|
+
users: {
|
|
38
|
+
resolveActor(): Promise<{ identity: Identity; user?: User } | undefined>;
|
|
39
|
+
};
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
function trimContent(content: string, maxLength: number): string {
|
|
@@ -50,6 +56,7 @@ const recalledMemorySchema = z
|
|
|
50
56
|
id: z.string().min(1),
|
|
51
57
|
content: z.string().min(1).max(MAX_MEMORY_LINE_CHARS),
|
|
52
58
|
observedAtMs: z.number().finite(),
|
|
59
|
+
// Stored version 1 uses the old scope labels. Prompt rendering ignores them.
|
|
53
60
|
scope: z.enum(["personal", "conversation"]),
|
|
54
61
|
kind: z.enum(["preference", "procedure", "knowledge"]),
|
|
55
62
|
})
|
|
@@ -82,7 +89,7 @@ function selectPromptMemories(memories: MemoryRecord[]): RecalledMemory[] {
|
|
|
82
89
|
id: memory.id,
|
|
83
90
|
content,
|
|
84
91
|
observedAtMs: memory.observedAtMs,
|
|
85
|
-
scope: memory.scope,
|
|
92
|
+
scope: memory.scope === "private" ? "personal" : "conversation",
|
|
86
93
|
kind: memory.kind,
|
|
87
94
|
});
|
|
88
95
|
totalChars += line.length + 1;
|
|
@@ -119,7 +126,7 @@ async function emitRecallOutcome(args: {
|
|
|
119
126
|
await args.events?.emit(
|
|
120
127
|
memoriesRecalledEvent({
|
|
121
128
|
memories: args.memories,
|
|
122
|
-
...(args.costUsd !== undefined ? { costUsd: args.costUsd } :
|
|
129
|
+
...(args.costUsd !== undefined ? { costUsd: args.costUsd } : undefined),
|
|
123
130
|
}),
|
|
124
131
|
);
|
|
125
132
|
}
|
|
@@ -138,12 +145,15 @@ export async function createMemoryPromptContributions(
|
|
|
138
145
|
if (!context.text.trim()) {
|
|
139
146
|
return undefined;
|
|
140
147
|
}
|
|
148
|
+
const actorUser = (await context.users.resolveActor())?.user;
|
|
141
149
|
const runtimeContext = memoryRuntimeContextSchema.parse({
|
|
142
150
|
...(context.conversationId
|
|
143
151
|
? { conversationId: context.conversationId }
|
|
144
|
-
:
|
|
145
|
-
...(context.actor ? { actor: context.actor } :
|
|
152
|
+
: undefined),
|
|
153
|
+
...(context.actor ? { actor: context.actor } : undefined),
|
|
154
|
+
...(context.locationId ? { locationId: context.locationId } : undefined),
|
|
146
155
|
source: context.source,
|
|
156
|
+
...(actorUser ? { userId: actorUser.id } : undefined),
|
|
147
157
|
});
|
|
148
158
|
let embeddingCostUsd: number | undefined;
|
|
149
159
|
const sourceEmbedder = context.embedder;
|
|
@@ -164,7 +174,7 @@ export async function createMemoryPromptContributions(
|
|
|
164
174
|
});
|
|
165
175
|
if (candidates.length === 0) {
|
|
166
176
|
await emitRecallOutcome({
|
|
167
|
-
...(embeddingCostUsd !== undefined ? { costUsd: embeddingCostUsd } :
|
|
177
|
+
...(embeddingCostUsd !== undefined ? { costUsd: embeddingCostUsd } : undefined),
|
|
168
178
|
events: context.events,
|
|
169
179
|
memories: [],
|
|
170
180
|
});
|
|
@@ -191,7 +201,7 @@ export async function createMemoryPromptContributions(
|
|
|
191
201
|
const selected = selectPromptMemories(relevant);
|
|
192
202
|
const costUsd = addUsd(embeddingCostUsd, recall.costUsd);
|
|
193
203
|
await emitRecallOutcome({
|
|
194
|
-
...(costUsd !== undefined ? { costUsd } :
|
|
204
|
+
...(costUsd !== undefined ? { costUsd } : undefined),
|
|
195
205
|
events: context.events,
|
|
196
206
|
memories: selected.map(({ id }) => id),
|
|
197
207
|
});
|