@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.
- package/dist/agent.d.ts +38 -0
- package/dist/api.d.ts +2 -0
- package/dist/db/schema.d.ts +2 -2
- package/dist/index.js +131 -81
- package/dist/index.js.map +1 -1
- package/dist/personal-store.d.ts +2 -1
- package/dist/store.d.ts +13 -0
- package/dist/types.d.ts +14 -1
- package/migrations/0008_web_memory_source_platform.sql +2 -0
- package/migrations/meta/0008_snapshot.json +409 -0
- package/migrations/meta/_journal.json +8 -1
- package/package.json +2 -2
- package/src/agent.ts +36 -28
- package/src/api.ts +2 -1
- package/src/db/schema.ts +1 -1
- package/src/personal-store.ts +2 -2
- package/src/process-session.ts +19 -3
- package/src/scope.ts +64 -39
- package/src/store.ts +37 -15
- package/src/types.ts +3 -9
package/src/process-session.ts
CHANGED
|
@@ -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
|
|
236
|
-
|
|
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 {
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
if (!actor
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
402
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
17
|
-
|
|
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
|
|