@sentry/junior-memory 0.147.0 → 0.148.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import {
3
3
  getSourceKey,
4
- isPrivateSource,
5
4
  type PluginRunContext,
6
5
  type PluginRunTranscriptEntry,
7
6
  type PluginTaskContext,
7
+ type Source,
8
8
  } from "@sentry/junior-plugin-api";
9
9
  import { z } from "zod";
10
10
  import {
@@ -57,6 +57,21 @@ const extractedMemoryCacheSchema = z.union([
57
57
  /** Where a passively extracted memory may be stored, or dropped when unproven. */
58
58
  type MemoryRouteTarget = "drop" | "personal" | "conversation";
59
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
+ }
74
+
60
75
  function recordCapturedMemory(
61
76
  captured: ReturnType<typeof capturedMemory>[],
62
77
  result: CreateMemoryResult,
@@ -232,8 +247,9 @@ export async function processMemorySession(
232
247
  ) {
233
248
  return;
234
249
  }
235
- // V1 passive learning only stores public channel facts outside local QA.
236
- if (run.source.platform !== "local" && isPrivateSource(run.source)) {
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)) {
237
253
  return;
238
254
  }
239
255
  const sourceKey = getSourceKey(run.source);
package/src/scope.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { isPrivateSource, type Identity } from "@sentry/junior-plugin-api";
1
+ import { type Actor, type Identity, type Source } from "@sentry/junior-plugin-api";
2
2
  import type {
3
3
  MemoryRuntimeContext,
4
4
  MemoryScope,
@@ -25,29 +25,40 @@ function uniqueScopes(scopes: ResolvedMemoryScope[]): ResolvedMemoryScope[] {
25
25
  ];
26
26
  }
27
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
+
28
54
  /** Derive viewer-visible memory scopes from canonical provider identities. */
29
55
  export function deriveViewerMemoryScopes(identities: Identity[]): {
30
56
  privateScopes: ResolvedMemoryScope[];
31
57
  publicScopes: ResolvedMemoryScope[];
32
58
  } {
33
59
  const privateScopes = identities.flatMap((identity) => {
34
- if (identity.provider === "local") {
35
- return [
36
- {
37
- scope: "personal" as const,
38
- scopeKey: `local:${identity.providerSubjectId}`,
39
- },
40
- ];
41
- }
42
- if (identity.provider === "slack" && identity.providerTenantId) {
43
- return [
44
- {
45
- scope: "personal" as const,
46
- scopeKey: `slack:${identity.providerTenantId}:${identity.providerSubjectId}`,
47
- },
48
- ];
49
- }
50
- return [];
60
+ const scope = personalScopeFromIdentity(identity);
61
+ return scope ? [scope] : [];
51
62
  });
52
63
  const publicScopes = identities.flatMap((identity) =>
53
64
  identity.provider === "slack" && identity.providerTenantId
@@ -65,29 +76,43 @@ export function deriveViewerMemoryScopes(identities: Identity[]): {
65
76
  };
66
77
  }
67
78
 
68
- function sourceConversationKey(ctx: MemoryRuntimeContext): string | undefined {
69
- if (ctx.source.platform === "local") {
70
- return ctx.source.conversationId;
71
- }
72
- if (!isPrivateSource(ctx.source)) {
73
- return `slack:${ctx.source.teamId}`;
74
- }
75
- const threadKey = ctx.source.threadTs ?? ctx.source.messageTs;
76
- if (!threadKey) {
77
- return undefined;
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
+ }
78
95
  }
79
- return `slack:${ctx.source.teamId}:${ctx.source.channelId}:${threadKey}`;
80
96
  }
81
97
 
82
- function actorScopeKey(ctx: MemoryRuntimeContext): string | undefined {
83
- const actor = ctx.actor;
84
- if (!actor || actor.platform === "system") {
98
+ /** Personal scope key for the Actor branch we actually have. */
99
+ function actorScopeKey(actor: Actor | undefined): string | undefined {
100
+ if (!actor) {
85
101
  return undefined;
86
102
  }
87
- if (actor.platform === "slack") {
88
- return `slack:${actor.teamId}:${actor.userId}`;
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
+ }
89
115
  }
90
- return `local:${actor.userId}`;
91
116
  }
92
117
 
93
118
  /** Derive the authority-bearing key for a requested memory scope. */
@@ -96,14 +121,14 @@ export function deriveMemoryScope(
96
121
  scope: MemoryScope,
97
122
  ): ResolvedMemoryScope {
98
123
  if (scope === "personal") {
99
- const scopeKey = actorScopeKey(ctx);
124
+ const scopeKey = actorScopeKey(ctx.actor);
100
125
  if (!scopeKey) {
101
126
  throw new Error("Personal memory requires actor context.");
102
127
  }
103
128
  return { scope, scopeKey };
104
129
  }
105
130
 
106
- const scopeKey = sourceConversationKey(ctx);
131
+ const scopeKey = sourceConversationKey(ctx.source);
107
132
  if (!scopeKey) {
108
133
  throw new Error("Conversation memory requires conversation context.");
109
134
  }
@@ -116,14 +141,14 @@ export function deriveMemorySubject(
116
141
  scope: ResolvedMemoryScope,
117
142
  ): ResolvedMemorySubject {
118
143
  if (scope.scope === "personal") {
119
- const subjectKey = actorScopeKey(ctx);
144
+ const subjectKey = actorScopeKey(ctx.actor);
120
145
  if (!subjectKey) {
121
146
  throw new Error("User-subject memory requires actor context.");
122
147
  }
123
148
  return { subjectType: "user", subjectKey };
124
149
  }
125
150
 
126
- const subjectKey = sourceConversationKey(ctx);
151
+ const subjectKey = sourceConversationKey(ctx.source);
127
152
  if (!subjectKey) {
128
153
  throw new Error(
129
154
  "Conversation-subject memory requires conversation context.",
package/src/store.ts CHANGED
@@ -37,6 +37,7 @@ import {
37
37
  memoryRuntimeContextSchema,
38
38
  type MemoryRuntimeContext,
39
39
  type MemoryScope,
40
+ type MemorySourcePlatform,
40
41
  } from "./types";
41
42
  import {
42
43
  deriveMemoryScope,
@@ -383,26 +384,47 @@ function boundedLimit(value: number | undefined, fallback: number): number {
383
384
  return Math.min(200, Math.max(1, Math.floor(value)));
384
385
  }
385
386
 
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
+
386
401
  /** Build the durable source attribution key from runtime-owned source fields. */
387
402
  function sourceKey(ctx: MemoryRuntimeContext): string {
388
- if (ctx.source.platform === "local") {
389
- return ctx.source.conversationId;
390
- }
391
- const threadKey = ctx.source.threadTs ?? ctx.source.messageTs;
392
- if (!threadKey) {
393
- throw new Error(
394
- "Memory source requires a Slack message or thread timestamp.",
395
- );
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
+ }
396
416
  }
397
- return `slack:${ctx.source.teamId}:${ctx.source.channelId}:${threadKey}`;
398
417
  }
399
418
 
400
419
  function sourceChannelPrefix(ctx: MemoryRuntimeContext): string | undefined {
401
- if (ctx.source.platform !== "slack") {
402
- return 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;
403
427
  }
404
- // TODO(v0.82.0): Replace Slack source-key prefix matching with typed source proximity metadata.
405
- return `slack:${ctx.source.teamId}:${ctx.source.channelId}:`;
406
428
  }
407
429
 
408
430
  /** Parse one SQL row into the public memory record projection. */
@@ -788,7 +810,7 @@ async function rememberDuplicateIdempotency(args: {
788
810
  scope: args.scope.scope,
789
811
  scopeKey: args.scope.scopeKey,
790
812
  sourceKey: sourceKey(args.runtimeContext),
791
- sourcePlatform: args.runtimeContext.source.platform,
813
+ sourcePlatform: memorySourcePlatform(args.runtimeContext.source),
792
814
  subjectKey: args.subject.subjectKey,
793
815
  subjectType: args.subject.subjectType,
794
816
  supersededAtMs: args.nowMs,
@@ -1305,7 +1327,7 @@ export function createMemoryStore(
1305
1327
  scope: scope.scope,
1306
1328
  scopeKey: scope.scopeKey,
1307
1329
  sourceKey: sourceKey(runtimeContext),
1308
- sourcePlatform: runtimeContext.source.platform,
1330
+ sourcePlatform: memorySourcePlatform(runtimeContext.source),
1309
1331
  subjectKey: subject.subjectKey,
1310
1332
  subjectType: subject.subjectType,
1311
1333
  kind: input.kind,
package/src/types.ts CHANGED
@@ -1,8 +1,4 @@
1
- import {
2
- actorSchema,
3
- platformSchema,
4
- sourceSchema,
5
- } from "@sentry/junior-plugin-api";
1
+ import { actorSchema, sourceSchema } from "@sentry/junior-plugin-api";
6
2
  import { z } from "zod";
7
3
 
8
4
  export const MEMORY_KINDS = ["preference", "procedure", "knowledge"] as const;
@@ -13,10 +9,8 @@ export const MEMORY_SUBJECT_TYPES = [
13
9
  "conversation",
14
10
  "general",
15
11
  ] as const;
16
- export const MEMORY_SOURCE_PLATFORMS = [
17
- "slack",
18
- "local",
19
- ] as const satisfies readonly z.output<typeof platformSchema>[];
12
+ // Durable attribution follows Source platform, including dashboard/web roots.
13
+ export const MEMORY_SOURCE_PLATFORMS = ["slack", "local", "web"] as const;
20
14
  export const MEMORY_EMBEDDING_METRICS = ["cosine"] as const;
21
15
  export const MEMORY_EMBEDDING_DIMENSIONS = 1536;
22
16