@sentry/junior-memory 0.180.0 → 0.181.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/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 +487 -666
- 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/0010_memories_captured_scope_labels.sql +50 -0
- package/migrations/meta/0009_snapshot.json +411 -0
- package/migrations/meta/0010_snapshot.json +415 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +2 -2
- package/src/agent.ts +6 -8
- package/src/api.ts +33 -24
- package/src/db/schema.ts +3 -1
- package/src/events.ts +30 -8
- package/src/operational-report.ts +20 -20
- package/src/plugin.ts +13 -1
- package/src/process-session.ts +21 -35
- package/src/ranking.ts +9 -26
- package/src/recall.ts +11 -1
- package/src/scope.ts +34 -135
- package/src/store.ts +34 -91
- package/src/tools.ts +30 -14
- package/src/types.ts +5 -2
- package/src/user-pages.ts +4 -6
- 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/scope.ts
CHANGED
|
@@ -1,176 +1,75 @@
|
|
|
1
|
-
import { type Actor, type Identity, type Source } from "@sentry/junior-plugin-api";
|
|
2
1
|
import type {
|
|
3
2
|
MemoryRuntimeContext,
|
|
4
3
|
MemoryScope,
|
|
5
4
|
MemorySubjectType,
|
|
6
5
|
} from "./types";
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
const PUBLIC_SCOPE_KEY = "public";
|
|
8
|
+
|
|
9
|
+
/** Stored memory access rule. */
|
|
9
10
|
export interface ResolvedMemoryScope {
|
|
10
11
|
scope: MemoryScope;
|
|
11
12
|
scopeKey: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
/**
|
|
15
|
+
/** What a stored memory is about. */
|
|
15
16
|
export interface ResolvedMemorySubject {
|
|
16
|
-
subjectKey
|
|
17
|
-
subjectType: MemorySubjectType
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function uniqueScopes(scopes: ResolvedMemoryScope[]): ResolvedMemoryScope[] {
|
|
21
|
-
return [
|
|
22
|
-
...new Map(
|
|
23
|
-
scopes.map((scope) => [`${scope.scope}:${scope.scopeKey}`, scope]),
|
|
24
|
-
).values(),
|
|
25
|
-
];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Personal scope key for one verified provider identity, when one exists. */
|
|
29
|
-
function personalScopeFromIdentity(
|
|
30
|
-
identity: Identity,
|
|
31
|
-
): ResolvedMemoryScope | undefined {
|
|
32
|
-
if (identity.provider === "local") {
|
|
33
|
-
return {
|
|
34
|
-
scope: "personal",
|
|
35
|
-
scopeKey: `local:${identity.providerSubjectId}`,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
// Dashboard/web actors persist as junior identities keyed by verified email.
|
|
39
|
-
if (identity.provider === "junior") {
|
|
40
|
-
return {
|
|
41
|
-
scope: "personal",
|
|
42
|
-
scopeKey: `junior:${identity.providerSubjectId}`,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
if (identity.provider === "slack" && identity.providerTenantId) {
|
|
46
|
-
return {
|
|
47
|
-
scope: "personal",
|
|
48
|
-
scopeKey: `slack:${identity.providerTenantId}:${identity.providerSubjectId}`,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
return undefined;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Derive viewer-visible memory scopes from canonical provider identities. */
|
|
55
|
-
export function deriveViewerMemoryScopes(identities: Identity[]): {
|
|
56
|
-
privateScopes: ResolvedMemoryScope[];
|
|
57
|
-
publicScopes: ResolvedMemoryScope[];
|
|
58
|
-
} {
|
|
59
|
-
const privateScopes = identities.flatMap((identity) => {
|
|
60
|
-
const scope = personalScopeFromIdentity(identity);
|
|
61
|
-
return scope ? [scope] : [];
|
|
62
|
-
});
|
|
63
|
-
const publicScopes = identities.flatMap((identity) =>
|
|
64
|
-
identity.provider === "slack" && identity.providerTenantId
|
|
65
|
-
? [
|
|
66
|
-
{
|
|
67
|
-
scope: "conversation" as const,
|
|
68
|
-
scopeKey: `slack:${identity.providerTenantId}`,
|
|
69
|
-
},
|
|
70
|
-
]
|
|
71
|
-
: [],
|
|
72
|
-
);
|
|
73
|
-
return {
|
|
74
|
-
privateScopes: uniqueScopes(privateScopes),
|
|
75
|
-
publicScopes: uniqueScopes(publicScopes),
|
|
76
|
-
};
|
|
17
|
+
subjectKey: string;
|
|
18
|
+
subjectType: Extract<MemorySubjectType, "user" | "conversation">;
|
|
77
19
|
}
|
|
78
20
|
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return source.conversationId;
|
|
85
|
-
case "slack": {
|
|
86
|
-
if (source.visibility === "public") {
|
|
87
|
-
return `slack:${source.teamId}`;
|
|
88
|
-
}
|
|
89
|
-
const threadKey = source.threadTs ?? source.messageTs;
|
|
90
|
-
if (!threadKey) {
|
|
91
|
-
return undefined;
|
|
92
|
-
}
|
|
93
|
-
return `slack:${source.teamId}:${source.channelId}:${threadKey}`;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
21
|
+
/** Public memories are visible everywhere. */
|
|
22
|
+
export const publicMemoryScope: ResolvedMemoryScope = {
|
|
23
|
+
scope: "public",
|
|
24
|
+
scopeKey: PUBLIC_SCOPE_KEY,
|
|
25
|
+
};
|
|
97
26
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (!actor) {
|
|
101
|
-
return undefined;
|
|
102
|
-
}
|
|
103
|
-
switch (actor.platform) {
|
|
104
|
-
case "system":
|
|
105
|
-
return undefined;
|
|
106
|
-
case "slack":
|
|
107
|
-
return `slack:${actor.teamId}:${actor.userId}`;
|
|
108
|
-
case "local":
|
|
109
|
-
return `local:${actor.userId}`;
|
|
110
|
-
case "web": {
|
|
111
|
-
// Match junior identity personal scopes used by the dashboard viewer.
|
|
112
|
-
const email = actor.email?.trim().toLowerCase();
|
|
113
|
-
return email ? `junior:${email}` : undefined;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
27
|
+
function privateMemoryScope(userId: string): ResolvedMemoryScope {
|
|
28
|
+
return { scope: "private", scopeKey: userId };
|
|
116
29
|
}
|
|
117
30
|
|
|
118
|
-
/**
|
|
31
|
+
/** Set memory access from the Source. */
|
|
119
32
|
export function deriveMemoryScope(
|
|
120
33
|
ctx: MemoryRuntimeContext,
|
|
121
|
-
scope: MemoryScope,
|
|
122
34
|
): ResolvedMemoryScope {
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
if (!scopeKey) {
|
|
126
|
-
throw new Error("Personal memory requires actor context.");
|
|
127
|
-
}
|
|
128
|
-
return { scope, scopeKey };
|
|
35
|
+
if (ctx.source.visibility === "public") {
|
|
36
|
+
return publicMemoryScope;
|
|
129
37
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (!scopeKey) {
|
|
133
|
-
throw new Error("Conversation memory requires conversation context.");
|
|
38
|
+
if (!ctx.userId) {
|
|
39
|
+
throw new Error("Private memory requires a User.");
|
|
134
40
|
}
|
|
135
|
-
return
|
|
41
|
+
return privateMemoryScope(ctx.userId);
|
|
136
42
|
}
|
|
137
43
|
|
|
138
|
-
/**
|
|
44
|
+
/** Set what a memory is about. Access is set separately. */
|
|
139
45
|
export function deriveMemorySubject(
|
|
140
46
|
ctx: MemoryRuntimeContext,
|
|
141
|
-
|
|
47
|
+
subjectType: Extract<MemorySubjectType, "user" | "conversation">,
|
|
142
48
|
): ResolvedMemorySubject {
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
throw new Error("User-subject memory requires actor context.");
|
|
49
|
+
if (subjectType === "user") {
|
|
50
|
+
if (!ctx.userId) {
|
|
51
|
+
throw new Error("User memory requires a User.");
|
|
147
52
|
}
|
|
148
|
-
return { subjectType
|
|
53
|
+
return { subjectType, subjectKey: ctx.userId };
|
|
149
54
|
}
|
|
150
|
-
|
|
151
|
-
const subjectKey = sourceConversationKey(ctx.source);
|
|
55
|
+
const subjectKey = ctx.conversationId;
|
|
152
56
|
if (!subjectKey) {
|
|
153
57
|
throw new Error(
|
|
154
58
|
"Conversation-subject memory requires conversation context.",
|
|
155
59
|
);
|
|
156
60
|
}
|
|
157
|
-
return {
|
|
61
|
+
return {
|
|
62
|
+
subjectType,
|
|
63
|
+
subjectKey,
|
|
64
|
+
};
|
|
158
65
|
}
|
|
159
66
|
|
|
160
|
-
/** Return
|
|
67
|
+
/** Return the memory scopes that the current User can access. */
|
|
161
68
|
export function deriveVisibleMemoryScopes(
|
|
162
69
|
ctx: MemoryRuntimeContext,
|
|
163
70
|
): ResolvedMemoryScope[] {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
scopes.push(deriveMemoryScope(ctx, "personal"));
|
|
167
|
-
} catch {
|
|
168
|
-
// Personal memory is optional when a runtime surface has no actor.
|
|
169
|
-
}
|
|
170
|
-
try {
|
|
171
|
-
scopes.push(deriveMemoryScope(ctx, "conversation"));
|
|
172
|
-
} catch {
|
|
173
|
-
// Conversation memory is optional for synthetic invocations.
|
|
71
|
+
if (!ctx.userId) {
|
|
72
|
+
return [publicMemoryScope];
|
|
174
73
|
}
|
|
175
|
-
return
|
|
74
|
+
return [publicMemoryScope, privateMemoryScope(ctx.userId)];
|
|
176
75
|
}
|
package/src/store.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { cosineDistance } from "drizzle-orm/sql/functions";
|
|
|
25
25
|
import type { PgDatabase } from "drizzle-orm/pg-core";
|
|
26
26
|
import type { PgQueryResultHKT } from "drizzle-orm/pg-core/session";
|
|
27
27
|
import { z } from "zod";
|
|
28
|
+
import { getSourceKey } from "@sentry/junior-plugin-api";
|
|
28
29
|
import * as memorySqlSchema from "./db/schema";
|
|
29
30
|
import { juniorMemoryEmbeddings, juniorMemoryMemories } from "./db/schema";
|
|
30
31
|
import { rankMemoryMatches, type MemoryMatch } from "./ranking";
|
|
@@ -36,8 +37,6 @@ import {
|
|
|
36
37
|
MEMORY_KINDS,
|
|
37
38
|
memoryRuntimeContextSchema,
|
|
38
39
|
type MemoryRuntimeContext,
|
|
39
|
-
type MemoryScope,
|
|
40
|
-
type MemorySourcePlatform,
|
|
41
40
|
} from "./types";
|
|
42
41
|
import {
|
|
43
42
|
deriveMemoryScope,
|
|
@@ -150,6 +149,7 @@ const memoryRowSchema = z
|
|
|
150
149
|
expiresAtMs: optionalNumberSchema,
|
|
151
150
|
id: z.string().min(1),
|
|
152
151
|
idempotencyKey: optionalStringSchema,
|
|
152
|
+
locationId: optionalNonEmptyStringSchema,
|
|
153
153
|
observedAtMs: z.coerce.number(),
|
|
154
154
|
searchVector: z.string().optional(),
|
|
155
155
|
scope: z.enum(MEMORY_SCOPES),
|
|
@@ -334,16 +334,14 @@ export interface MemoryStore {
|
|
|
334
334
|
): Promise<ArchiveExpiredMemoriesResult>;
|
|
335
335
|
/** Archive a visible memory in the current runtime context. */
|
|
336
336
|
archiveMemory(input: ArchiveMemoryInput): Promise<MemoryRecord>;
|
|
337
|
-
/** Store a
|
|
337
|
+
/** Store a memory about the current User. The Source sets access. */
|
|
338
338
|
createMemory(input: CreateMemoryInput): Promise<CreateMemoryResult>;
|
|
339
|
-
/** Store a
|
|
339
|
+
/** Store a memory about the current Conversation. The Source sets access. */
|
|
340
340
|
createConversationMemory(
|
|
341
341
|
input: CreateMemoryInput,
|
|
342
342
|
): Promise<CreateMemoryResult>;
|
|
343
343
|
/** List active memories visible in the current runtime context. */
|
|
344
344
|
listMemories(input: ListMemoriesInput): Promise<MemoryRecord[]>;
|
|
345
|
-
/** List active personal memories owned by the current actor. */
|
|
346
|
-
listPersonalMemories(input: ListMemoriesInput): Promise<MemoryRecord[]>;
|
|
347
345
|
/**
|
|
348
346
|
* Retrieve a broad relevance-ranked candidate window for automatic recall.
|
|
349
347
|
* Prompt admission remains owned by the recall boundary.
|
|
@@ -384,50 +382,15 @@ function boundedLimit(value: number | undefined, fallback: number): number {
|
|
|
384
382
|
return Math.min(200, Math.max(1, Math.floor(value)));
|
|
385
383
|
}
|
|
386
384
|
|
|
387
|
-
/**
|
|
388
|
-
function memorySourcePlatform(
|
|
389
|
-
source: MemoryRuntimeContext["source"],
|
|
390
|
-
): MemorySourcePlatform {
|
|
391
|
-
switch (source.platform) {
|
|
392
|
-
case "slack":
|
|
393
|
-
return "slack";
|
|
394
|
-
case "local":
|
|
395
|
-
return "local";
|
|
396
|
-
case "web":
|
|
397
|
-
return "web";
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/** Build the durable source attribution key from runtime-owned source fields. */
|
|
385
|
+
/** Build the stored key for the Source. */
|
|
402
386
|
function sourceKey(ctx: MemoryRuntimeContext): string {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
return ctx.source.conversationId;
|
|
407
|
-
case "slack": {
|
|
408
|
-
const threadKey = ctx.source.threadTs ?? ctx.source.messageTs;
|
|
409
|
-
if (!threadKey) {
|
|
410
|
-
throw new Error(
|
|
411
|
-
"Memory source requires a Slack message or thread timestamp.",
|
|
412
|
-
);
|
|
413
|
-
}
|
|
414
|
-
return `slack:${ctx.source.teamId}:${ctx.source.channelId}:${threadKey}`;
|
|
415
|
-
}
|
|
387
|
+
const key = getSourceKey(ctx.source);
|
|
388
|
+
if (!key) {
|
|
389
|
+
throw new Error("Memory Source has no stable key.");
|
|
416
390
|
}
|
|
391
|
+
return key;
|
|
417
392
|
}
|
|
418
393
|
|
|
419
|
-
function sourceChannelPrefix(ctx: MemoryRuntimeContext): string | undefined {
|
|
420
|
-
switch (ctx.source.platform) {
|
|
421
|
-
case "slack":
|
|
422
|
-
// TODO(v0.82.0): Replace Slack source-key prefix matching with typed source proximity metadata.
|
|
423
|
-
return `slack:${ctx.source.teamId}:${ctx.source.channelId}:`;
|
|
424
|
-
case "web":
|
|
425
|
-
case "local":
|
|
426
|
-
return undefined;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/** Parse one SQL row into the public memory record projection. */
|
|
431
394
|
/** Parse one SQL row into the public memory projection. */
|
|
432
395
|
export function parseMemoryRow(row: unknown): MemoryRecord {
|
|
433
396
|
const parsed = memoryRowSchema.parse(row);
|
|
@@ -739,9 +702,7 @@ function activeScopedSubjectPredicate(args: {
|
|
|
739
702
|
eq(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
|
|
740
703
|
eq(juniorMemoryMemories.kind, args.kind),
|
|
741
704
|
eq(juniorMemoryMemories.subjectType, args.subject.subjectType),
|
|
742
|
-
args.subject.subjectKey
|
|
743
|
-
? isNull(juniorMemoryMemories.subjectKey)
|
|
744
|
-
: eq(juniorMemoryMemories.subjectKey, args.subject.subjectKey),
|
|
705
|
+
eq(juniorMemoryMemories.subjectKey, args.subject.subjectKey),
|
|
745
706
|
isNull(juniorMemoryMemories.archivedAtMs),
|
|
746
707
|
isNull(juniorMemoryMemories.supersededAtMs),
|
|
747
708
|
isNull(juniorMemoryMemories.supersededById),
|
|
@@ -806,11 +767,12 @@ async function rememberDuplicateIdempotency(args: {
|
|
|
806
767
|
targetId: args.duplicate.id,
|
|
807
768
|
}),
|
|
808
769
|
idempotencyKey: args.idempotencyKey,
|
|
770
|
+
locationId: args.runtimeContext.locationId,
|
|
809
771
|
observedAtMs: args.nowMs,
|
|
810
772
|
scope: args.scope.scope,
|
|
811
773
|
scopeKey: args.scope.scopeKey,
|
|
812
774
|
sourceKey: sourceKey(args.runtimeContext),
|
|
813
|
-
sourcePlatform:
|
|
775
|
+
sourcePlatform: args.runtimeContext.source.platform,
|
|
814
776
|
subjectKey: args.subject.subjectKey,
|
|
815
777
|
subjectType: args.subject.subjectType,
|
|
816
778
|
supersededAtMs: args.nowMs,
|
|
@@ -1081,7 +1043,6 @@ async function searchVisibleLexicalMemories(args: {
|
|
|
1081
1043
|
return rows.map((row, index) => ({
|
|
1082
1044
|
lexical: { rank: ranks[index] },
|
|
1083
1045
|
memory: parseMemoryRow(row.memory),
|
|
1084
|
-
sourceKey: row.memory.sourceKey,
|
|
1085
1046
|
}));
|
|
1086
1047
|
}
|
|
1087
1048
|
|
|
@@ -1148,7 +1109,6 @@ async function searchVisibleVectorMemories(args: {
|
|
|
1148
1109
|
return [
|
|
1149
1110
|
{
|
|
1150
1111
|
memory: parseMemoryRow(row.memory),
|
|
1151
|
-
sourceKey: row.memory.sourceKey,
|
|
1152
1112
|
vector: {
|
|
1153
1113
|
rank: ranks[index],
|
|
1154
1114
|
},
|
|
@@ -1208,13 +1168,13 @@ export function createMemoryStore(
|
|
|
1208
1168
|
/** Persist a memory under the plugin-derived scope and subject. */
|
|
1209
1169
|
async function createScopedMemory(
|
|
1210
1170
|
rawInput: CreateMemoryInput,
|
|
1211
|
-
|
|
1171
|
+
subjectType: ResolvedMemorySubject["subjectType"],
|
|
1212
1172
|
): Promise<CreateMemoryResult> {
|
|
1213
1173
|
const input = createMemoryInputSchema.parse(rawInput);
|
|
1214
1174
|
const nowMs = getNowMs();
|
|
1215
1175
|
const content = normalizeContent(input.content);
|
|
1216
|
-
const scope = deriveMemoryScope(runtimeContext
|
|
1217
|
-
const subject = deriveMemorySubject(runtimeContext,
|
|
1176
|
+
const scope = deriveMemoryScope(runtimeContext);
|
|
1177
|
+
const subject = deriveMemorySubject(runtimeContext, subjectType);
|
|
1218
1178
|
if (content.length > MAX_MEMORY_CONTENT_CHARS) {
|
|
1219
1179
|
throw new Error("Memory content exceeds the maximum length.");
|
|
1220
1180
|
}
|
|
@@ -1280,7 +1240,7 @@ export function createMemoryStore(
|
|
|
1280
1240
|
}
|
|
1281
1241
|
let supersededIds: string[] = [];
|
|
1282
1242
|
if (
|
|
1283
|
-
|
|
1243
|
+
subjectType === "user" &&
|
|
1284
1244
|
input.kind === "preference" &&
|
|
1285
1245
|
supersessionDecider &&
|
|
1286
1246
|
(input.expiresAtMs === undefined || input.expiresAtMs > nowMs)
|
|
@@ -1323,11 +1283,12 @@ export function createMemoryStore(
|
|
|
1323
1283
|
expiresAtMs: input.expiresAtMs,
|
|
1324
1284
|
id,
|
|
1325
1285
|
idempotencyKey: input.idempotencyKey,
|
|
1286
|
+
locationId: runtimeContext.locationId,
|
|
1326
1287
|
observedAtMs: nowMs,
|
|
1327
1288
|
scope: scope.scope,
|
|
1328
1289
|
scopeKey: scope.scopeKey,
|
|
1329
1290
|
sourceKey: sourceKey(runtimeContext),
|
|
1330
|
-
sourcePlatform:
|
|
1291
|
+
sourcePlatform: runtimeContext.source.platform,
|
|
1331
1292
|
subjectKey: subject.subjectKey,
|
|
1332
1293
|
subjectType: subject.subjectType,
|
|
1333
1294
|
kind: input.kind,
|
|
@@ -1419,10 +1380,8 @@ export function createMemoryStore(
|
|
|
1419
1380
|
* miss path. Each leg is a hard-capped top-k probe so Postgres work stays
|
|
1420
1381
|
* bounded even on broad queries.
|
|
1421
1382
|
*
|
|
1422
|
-
* Automatic recall also
|
|
1423
|
-
*
|
|
1424
|
-
* the shared lexical recency window before ranking, which buries older actor
|
|
1425
|
-
* preferences that explicit search still finds.
|
|
1383
|
+
* Automatic recall also searches private memory by itself. This keeps newer
|
|
1384
|
+
* public memory with common words from hiding older private memory.
|
|
1426
1385
|
*/
|
|
1427
1386
|
async function retrieveVisibleMemories(
|
|
1428
1387
|
rawInput: SearchMemoriesInput,
|
|
@@ -1442,11 +1401,11 @@ export function createMemoryStore(
|
|
|
1442
1401
|
? SEARCH_RETRIEVAL_OVERFETCH
|
|
1443
1402
|
: RECALL_RETRIEVAL_OVERFETCH;
|
|
1444
1403
|
const candidateLimit = retrievalLegLimit(limit, overfetch);
|
|
1445
|
-
const
|
|
1446
|
-
//
|
|
1447
|
-
//
|
|
1448
|
-
const
|
|
1449
|
-
vectorMaxDistance !== undefined &&
|
|
1404
|
+
const privateScopes = scopes.filter((scope) => scope.scope === "private");
|
|
1405
|
+
// Search private memory by itself during recall so public results cannot
|
|
1406
|
+
// fill both search windows.
|
|
1407
|
+
const probePrivate =
|
|
1408
|
+
vectorMaxDistance !== undefined && privateScopes.length > 0;
|
|
1450
1409
|
const query = normalizeRetrievalQuery(input.query);
|
|
1451
1410
|
let queryEmbedding: MemoryEmbedding | undefined;
|
|
1452
1411
|
if (embedder && query) {
|
|
@@ -1483,31 +1442,29 @@ export function createMemoryStore(
|
|
|
1483
1442
|
...lexicalArgs,
|
|
1484
1443
|
scopes,
|
|
1485
1444
|
}),
|
|
1486
|
-
queryEmbedding &&
|
|
1445
|
+
queryEmbedding && probePrivate
|
|
1487
1446
|
? searchVisibleVectorMemories({
|
|
1488
1447
|
db,
|
|
1489
1448
|
embedding: queryEmbedding,
|
|
1490
1449
|
limit: candidateLimit,
|
|
1491
1450
|
maxDistance: vectorMaxDistance,
|
|
1492
1451
|
nowMs,
|
|
1493
|
-
scopes:
|
|
1452
|
+
scopes: privateScopes,
|
|
1494
1453
|
})
|
|
1495
1454
|
: emptyMatches,
|
|
1496
|
-
|
|
1455
|
+
probePrivate
|
|
1497
1456
|
? searchVisibleLexicalMemories({
|
|
1498
1457
|
...lexicalArgs,
|
|
1499
|
-
scopes:
|
|
1458
|
+
scopes: privateScopes,
|
|
1500
1459
|
})
|
|
1501
1460
|
: emptyMatches,
|
|
1502
1461
|
]);
|
|
1503
|
-
const channelPrefix = sourceChannelPrefix(runtimeContext);
|
|
1504
1462
|
return rankMemoryMatches(matches.flat(), {
|
|
1505
1463
|
nowMs,
|
|
1506
1464
|
// Slight lexical preference protects exact ids/names/timezones on ties.
|
|
1507
1465
|
...(vectorMaxDistance === undefined
|
|
1508
1466
|
? undefined
|
|
1509
1467
|
: { lexicalWeight: 1, vectorWeight: 0.85 }),
|
|
1510
|
-
...(channelPrefix ? { channelPrefix } : undefined),
|
|
1511
1468
|
})
|
|
1512
1469
|
.slice(0, limit)
|
|
1513
1470
|
.map(({ memory }) => memory);
|
|
@@ -1519,7 +1476,7 @@ export function createMemoryStore(
|
|
|
1519
1476
|
},
|
|
1520
1477
|
|
|
1521
1478
|
async createMemory(input) {
|
|
1522
|
-
return await createScopedMemory(input, "
|
|
1479
|
+
return await createScopedMemory(input, "user");
|
|
1523
1480
|
},
|
|
1524
1481
|
|
|
1525
1482
|
async createConversationMemory(input) {
|
|
@@ -1543,23 +1500,6 @@ export function createMemoryStore(
|
|
|
1543
1500
|
});
|
|
1544
1501
|
},
|
|
1545
1502
|
|
|
1546
|
-
async listPersonalMemories(input) {
|
|
1547
|
-
input = listMemoriesInputSchema.parse(input);
|
|
1548
|
-
const nowMs = getNowMs();
|
|
1549
|
-
const scopes = [deriveMemoryScope(runtimeContext, "personal")];
|
|
1550
|
-
await archiveExpiredMemoryBatch({
|
|
1551
|
-
db,
|
|
1552
|
-
nowMs,
|
|
1553
|
-
scopes,
|
|
1554
|
-
});
|
|
1555
|
-
return await listVisibleMemories({
|
|
1556
|
-
db,
|
|
1557
|
-
limit: input.limit,
|
|
1558
|
-
nowMs,
|
|
1559
|
-
scopes,
|
|
1560
|
-
});
|
|
1561
|
-
},
|
|
1562
|
-
|
|
1563
1503
|
async recallMemories(input) {
|
|
1564
1504
|
return await retrieveVisibleMemories(input, RECALL_MAX_VECTOR_DISTANCE);
|
|
1565
1505
|
},
|
|
@@ -1571,7 +1511,10 @@ export function createMemoryStore(
|
|
|
1571
1511
|
async archiveMemory(input) {
|
|
1572
1512
|
input = archiveMemoryInputSchema.parse(input);
|
|
1573
1513
|
const nowMs = getNowMs();
|
|
1574
|
-
|
|
1514
|
+
// Public memory is shared and has no single user owner.
|
|
1515
|
+
const scopes = deriveVisibleMemoryScopes(runtimeContext).filter(
|
|
1516
|
+
(scope) => scope.scope === "private",
|
|
1517
|
+
);
|
|
1575
1518
|
const predicate = activeVisiblePredicate({ nowMs, scopes });
|
|
1576
1519
|
const idPrefix = input.id.trim();
|
|
1577
1520
|
if (!idPrefix) {
|
package/src/tools.ts
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
type PluginToolOutput,
|
|
8
8
|
type Source,
|
|
9
9
|
type Actor,
|
|
10
|
+
type Identity,
|
|
11
|
+
type User,
|
|
10
12
|
pluginToolOutputSchema,
|
|
11
13
|
} from "@sentry/junior-plugin-api";
|
|
12
14
|
import { z } from "zod";
|
|
@@ -43,8 +45,8 @@ const KNOWN_TOOL_INPUT_ERROR_MESSAGES = new Set([
|
|
|
43
45
|
"Memory id is required.",
|
|
44
46
|
"Memory was not found in the current context.",
|
|
45
47
|
"Memory id prefix is ambiguous.",
|
|
46
|
-
"
|
|
47
|
-
"User
|
|
48
|
+
"Private memory requires a User.",
|
|
49
|
+
"User memory requires a User.",
|
|
48
50
|
]);
|
|
49
51
|
|
|
50
52
|
/** Runtime-owned context used to bind memory tools to visible scopes. */
|
|
@@ -53,8 +55,12 @@ export interface MemoryToolContext {
|
|
|
53
55
|
conversationId?: string;
|
|
54
56
|
db: MemoryDb;
|
|
55
57
|
embedder?: MemoryEmbeddingProvider;
|
|
58
|
+
locationId?: string;
|
|
56
59
|
actor?: Actor;
|
|
57
60
|
source: Source;
|
|
61
|
+
users: {
|
|
62
|
+
resolveActor(): Promise<{ identity: Identity; user?: User } | undefined>;
|
|
63
|
+
};
|
|
58
64
|
userText?: string;
|
|
59
65
|
}
|
|
60
66
|
|
|
@@ -79,23 +85,27 @@ function asToolInputError(error: unknown): never {
|
|
|
79
85
|
throw error;
|
|
80
86
|
}
|
|
81
87
|
|
|
82
|
-
function memoryRuntimeContext(
|
|
88
|
+
async function memoryRuntimeContext(
|
|
83
89
|
context: MemoryToolContext,
|
|
84
|
-
): MemoryRuntimeContext {
|
|
90
|
+
): Promise<MemoryRuntimeContext> {
|
|
91
|
+
const actorUser = (await context.users.resolveActor())?.user;
|
|
85
92
|
return memoryRuntimeContextSchema.parse({
|
|
86
93
|
...(context.conversationId
|
|
87
94
|
? { conversationId: context.conversationId }
|
|
88
95
|
: undefined),
|
|
89
96
|
...(context.actor ? { actor: context.actor } : undefined),
|
|
97
|
+
...(context.locationId ? { locationId: context.locationId } : undefined),
|
|
90
98
|
source: context.source,
|
|
99
|
+
...(actorUser ? { userId: actorUser.id } : undefined),
|
|
91
100
|
});
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
function memoryStore(
|
|
95
104
|
context: MemoryToolContext,
|
|
105
|
+
runtimeContext: MemoryRuntimeContext,
|
|
96
106
|
options: { supersessionDecider?: MemorySupersessionDecider } = {},
|
|
97
107
|
) {
|
|
98
|
-
return createMemoryStore(context.db,
|
|
108
|
+
return createMemoryStore(context.db, runtimeContext, {
|
|
99
109
|
embedder: context.embedder,
|
|
100
110
|
...(options.supersessionDecider
|
|
101
111
|
? { supersessionDecider: options.supersessionDecider }
|
|
@@ -237,7 +247,7 @@ const createMemoryInputSchema = z
|
|
|
237
247
|
.min(1)
|
|
238
248
|
.max(MAX_TOOL_CONTENT_CHARS)
|
|
239
249
|
.describe(
|
|
240
|
-
"Self-contained
|
|
250
|
+
"Self-contained memory candidate. Include the subject in natural language when it matters; do not rely on surrounding chat context.",
|
|
241
251
|
),
|
|
242
252
|
expires_at: z
|
|
243
253
|
.string()
|
|
@@ -400,7 +410,7 @@ export function createMemoryCreateTool(context: MemoryCreateToolContext) {
|
|
|
400
410
|
readOnlyHint: false,
|
|
401
411
|
},
|
|
402
412
|
description:
|
|
403
|
-
"Explicit memory-write tool. Use only when the latest user message directly asks Junior to remember, store, save, or forget-and-replace a
|
|
413
|
+
"Explicit memory-write tool. Use only when the latest user message directly asks Junior to remember, store, save, or forget-and-replace a 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. Junior sets access, Location, Source, and subject. The memory agent rewrites the content and sets the memory kind.",
|
|
404
414
|
executionMode: "sequential",
|
|
405
415
|
inputSchema: createMemoryInputSchema,
|
|
406
416
|
outputSchema: memoryCreateOutputSchema,
|
|
@@ -408,8 +418,8 @@ export function createMemoryCreateTool(context: MemoryCreateToolContext) {
|
|
|
408
418
|
const parsedInput = parseMemoryToolInput(createMemoryInputSchema, input);
|
|
409
419
|
const toolCallId = requireToolCallId(options.toolCallId);
|
|
410
420
|
const requestedExpiresAtMs = parseExpiresAt(parsedInput.expires_at);
|
|
411
|
-
const runtimeContext = memoryRuntimeContext(context);
|
|
412
|
-
const store = memoryStore(context, {
|
|
421
|
+
const runtimeContext = await memoryRuntimeContext(context);
|
|
422
|
+
const store = memoryStore(context, runtimeContext, {
|
|
413
423
|
supersessionDecider: context.supersessionDecider,
|
|
414
424
|
});
|
|
415
425
|
const review = await (async () => {
|
|
@@ -492,15 +502,16 @@ export function createMemoryRemoveTool(context: MemoryToolContext) {
|
|
|
492
502
|
readOnlyHint: false,
|
|
493
503
|
},
|
|
494
504
|
description:
|
|
495
|
-
"Forget one memory
|
|
505
|
+
"Forget one private memory owned by the current User. Public memories are read-only. Use only ids or short id prefixes returned by listMemories or searchMemories. Never remove memories by hidden Actor, provider, scope, or subject ids.",
|
|
496
506
|
executionMode: "sequential",
|
|
497
507
|
inputSchema: removeMemoryInputSchema,
|
|
498
508
|
outputSchema: memorySingleOutputSchema,
|
|
499
509
|
execute: async (input) => {
|
|
500
510
|
const parsedInput = parseMemoryToolInput(removeMemoryInputSchema, input);
|
|
511
|
+
const runtimeContext = await memoryRuntimeContext(context);
|
|
501
512
|
const memory = await (async () => {
|
|
502
513
|
try {
|
|
503
|
-
return await memoryStore(context).archiveMemory({
|
|
514
|
+
return await memoryStore(context, runtimeContext).archiveMemory({
|
|
504
515
|
id: parsedInput.id,
|
|
505
516
|
reason: "tool_removed",
|
|
506
517
|
});
|
|
@@ -530,7 +541,8 @@ export function createMemoryListTool(context: MemoryToolContext) {
|
|
|
530
541
|
outputSchema: memoryManyOutputSchema,
|
|
531
542
|
execute: async (input) => {
|
|
532
543
|
const parsedInput = parseMemoryToolInput(listMemoriesInputSchema, input);
|
|
533
|
-
const
|
|
544
|
+
const runtimeContext = await memoryRuntimeContext(context);
|
|
545
|
+
const memories = await memoryStore(context, runtimeContext).listMemories({
|
|
534
546
|
limit: boundedLimit(parsedInput.limit, DEFAULT_RESULT_LIMIT),
|
|
535
547
|
});
|
|
536
548
|
return memoryToolResult("listMemories", {
|
|
@@ -544,7 +556,7 @@ export function createMemoryListTool(context: MemoryToolContext) {
|
|
|
544
556
|
export function createMemorySearchTool(context: MemoryToolContext) {
|
|
545
557
|
return definePluginTool({
|
|
546
558
|
description:
|
|
547
|
-
"Search active memories visible in the current context. Use when the model needs targeted memory recall.
|
|
559
|
+
"Search active memories visible in the current context. Use when the model needs targeted memory recall. Public memories are visible everywhere. Private memories belong to the current User.",
|
|
548
560
|
annotations: {
|
|
549
561
|
destructiveHint: false,
|
|
550
562
|
idempotentHint: true,
|
|
@@ -558,7 +570,11 @@ export function createMemorySearchTool(context: MemoryToolContext) {
|
|
|
558
570
|
searchMemoriesInputSchema,
|
|
559
571
|
input,
|
|
560
572
|
);
|
|
561
|
-
const
|
|
573
|
+
const runtimeContext = await memoryRuntimeContext(context);
|
|
574
|
+
const memories = await memoryStore(
|
|
575
|
+
context,
|
|
576
|
+
runtimeContext,
|
|
577
|
+
).searchMemories({
|
|
562
578
|
query: parsedInput.query,
|
|
563
579
|
limit: boundedLimit(parsedInput.limit, DEFAULT_SEARCH_LIMIT),
|
|
564
580
|
});
|
package/src/types.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
|
|
4
4
|
export const MEMORY_KINDS = ["preference", "procedure", "knowledge"] as const;
|
|
5
5
|
|
|
6
|
-
export const MEMORY_SCOPES = ["
|
|
6
|
+
export const MEMORY_SCOPES = ["private", "public"] as const;
|
|
7
7
|
export const MEMORY_SUBJECT_TYPES = [
|
|
8
8
|
"user",
|
|
9
9
|
"conversation",
|
|
@@ -22,12 +22,15 @@ export type MemoryEmbeddingMetric = (typeof MEMORY_EMBEDDING_METRICS)[number];
|
|
|
22
22
|
|
|
23
23
|
const nonEmptyStringSchema = z.string().min(1);
|
|
24
24
|
|
|
25
|
-
/**
|
|
25
|
+
/** Host data used to set memory access, subject, and source. */
|
|
26
26
|
export const memoryRuntimeContextSchema = z
|
|
27
27
|
.object({
|
|
28
28
|
conversationId: nonEmptyStringSchema.optional(),
|
|
29
|
+
locationId: nonEmptyStringSchema.optional(),
|
|
29
30
|
actor: actorSchema.optional(),
|
|
30
31
|
source: sourceSchema,
|
|
32
|
+
/** User linked to the active Actor. */
|
|
33
|
+
userId: nonEmptyStringSchema.optional(),
|
|
31
34
|
})
|
|
32
35
|
.strict();
|
|
33
36
|
|