@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/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
- /** Runtime-derived visibility scope used for memory authorization checks. */
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
- /** Runtime-derived subject classification stored for filtering and rendering. */
15
+ /** What a stored memory is about. */
15
16
  export interface ResolvedMemorySubject {
16
- subjectKey?: string;
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
- /** Conversation-scoped key for the Source branch we actually have. */
80
- function sourceConversationKey(source: Source): string | undefined {
81
- switch (source.platform) {
82
- case "web":
83
- case "local":
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
- /** Personal scope key for the Actor branch we actually have. */
99
- function actorScopeKey(actor: Actor | undefined): string | undefined {
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
- /** Derive the authority-bearing key for a requested memory scope. */
31
+ /** Set memory access from the Source. */
119
32
  export function deriveMemoryScope(
120
33
  ctx: MemoryRuntimeContext,
121
- scope: MemoryScope,
122
34
  ): ResolvedMemoryScope {
123
- if (scope === "personal") {
124
- const scopeKey = actorScopeKey(ctx.actor);
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
- const scopeKey = sourceConversationKey(ctx.source);
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 { scope, scopeKey };
41
+ return privateMemoryScope(ctx.userId);
136
42
  }
137
43
 
138
- /** Derive the memory subject from the already-authorized write scope. */
44
+ /** Set what a memory is about. Access is set separately. */
139
45
  export function deriveMemorySubject(
140
46
  ctx: MemoryRuntimeContext,
141
- scope: ResolvedMemoryScope,
47
+ subjectType: Extract<MemorySubjectType, "user" | "conversation">,
142
48
  ): ResolvedMemorySubject {
143
- if (scope.scope === "personal") {
144
- const subjectKey = actorScopeKey(ctx.actor);
145
- if (!subjectKey) {
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: "user", subjectKey };
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 { subjectType: "conversation", subjectKey };
61
+ return {
62
+ subjectType,
63
+ subjectKey,
64
+ };
158
65
  }
159
66
 
160
- /** Return every visible scope for memory retrieval in the current context. */
67
+ /** Return the memory scopes that the current User can access. */
161
68
  export function deriveVisibleMemoryScopes(
162
69
  ctx: MemoryRuntimeContext,
163
70
  ): ResolvedMemoryScope[] {
164
- const scopes: ResolvedMemoryScope[] = [];
165
- try {
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 scopes;
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 personal memory for the current actor. */
337
+ /** Store a memory about the current User. The Source sets access. */
338
338
  createMemory(input: CreateMemoryInput): Promise<CreateMemoryResult>;
339
- /** Store a conversation memory for the current source conversation. */
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
- /** Map runtime Source platform onto the durable memory source platform. */
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
- switch (ctx.source.platform) {
404
- case "web":
405
- case "local":
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 === undefined
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: memorySourcePlatform(args.runtimeContext.source),
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
- scopeKind: MemoryScope,
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, scopeKind);
1217
- const subject = deriveMemorySubject(runtimeContext, scope);
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
- scopeKind === "personal" &&
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: memorySourcePlatform(runtimeContext.source),
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 runs personal-scope-only probes. Workspace
1423
- * conversation memories sharing common tokens (for example "time") can fill
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 personalScopes = scopes.filter((scope) => scope.scope === "personal");
1446
- // Automatic recall only: keep a personal-scope probe so workspace noise
1447
- // cannot monopolize the shared lexical recency window.
1448
- const probePersonal =
1449
- vectorMaxDistance !== undefined && personalScopes.length > 0;
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 && probePersonal
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: personalScopes,
1452
+ scopes: privateScopes,
1494
1453
  })
1495
1454
  : emptyMatches,
1496
- probePersonal
1455
+ probePrivate
1497
1456
  ? searchVisibleLexicalMemories({
1498
1457
  ...lexicalArgs,
1499
- scopes: personalScopes,
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, "personal");
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
- const scopes = deriveVisibleMemoryScopes(runtimeContext);
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
- "Personal memory requires actor context.",
47
- "User-subject memory requires actor context.",
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, memoryRuntimeContext(context), {
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 public/shareable memory candidate. Include the subject in natural language when it matters; do not rely on surrounding chat context.",
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 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.",
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 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.",
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 memories = await memoryStore(context).listMemories({
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. The tool searches only the current actor and active conversation scopes.",
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 memories = await memoryStore(context).searchMemories({
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 = ["personal", "conversation"] as const;
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
- /** Runtime-owned memory invocation fields used for scope and source authority. */
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