@sentry/junior-memory 0.119.0 → 0.121.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/src/plugin.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { defineJuniorPlugin } from "@sentry/junior-plugin-api";
2
2
  import { createMemoryAgent } from "./agent";
3
+ import { createMemoryApi } from "./api";
3
4
  import { createMemoryCliCommand } from "./cli";
4
5
  import {
5
6
  createMemoryCreateTool,
@@ -12,6 +13,7 @@ import {
12
13
  } from "./tools";
13
14
  import { processMemorySession } from "./process-session";
14
15
  import { createMemoryPromptContributions } from "./recall";
16
+ import { buildMemoryOperationalReport } from "./operational-report";
15
17
  import type { MemoryDb } from "./store";
16
18
  import { createMemoryUserPage } from "./user-pages";
17
19
 
@@ -110,6 +112,18 @@ export function memoryPlugin(options: MemoryPluginOptions = {}) {
110
112
  },
111
113
  userPages: [createMemoryUserPage()],
112
114
  hooks: {
115
+ async operationalReport(ctx) {
116
+ return await buildMemoryOperationalReport({
117
+ db: ctx.db as MemoryDb,
118
+ nowMs: ctx.nowMs,
119
+ });
120
+ },
121
+ apiRoutes(ctx) {
122
+ return createMemoryApi({
123
+ actors: ctx.viewer.actors,
124
+ db: ctx.db as MemoryDb,
125
+ });
126
+ },
113
127
  tools(ctx) {
114
128
  const agent = createMemoryAgent(ctx.model);
115
129
  const context = memoryToolContext({
@@ -135,10 +149,12 @@ export function memoryPlugin(options: MemoryPluginOptions = {}) {
135
149
  },
136
150
  async userPrompt(ctx) {
137
151
  return await createMemoryPromptContributions({
152
+ agent: createMemoryAgent(ctx.model),
138
153
  ...(ctx.conversationId ? { conversationId: ctx.conversationId } : {}),
139
154
  ...(ctx.actor ? { actor: ctx.actor } : {}),
140
155
  db: ctx.db as MemoryDb,
141
156
  embedder: ctx.embedder,
157
+ log: ctx.log,
142
158
  maxVectorDistance: recallMaxVectorDistance(options),
143
159
  source: ctx.source,
144
160
  text: ctx.text,
package/src/ranking.ts ADDED
@@ -0,0 +1,98 @@
1
+ import type { MemoryRecord } from "./store";
2
+
3
+ const RECIPROCAL_RANK_FUSION_K = 60;
4
+ const ONE_DAY_MS = 24 * 60 * 60 * 1000;
5
+
6
+ export interface MemoryMatch {
7
+ exactIdentifier: boolean;
8
+ lexical?: {
9
+ rank: number;
10
+ };
11
+ memory: MemoryRecord;
12
+ sourceKey: string;
13
+ vector?: {
14
+ rank: number;
15
+ };
16
+ }
17
+
18
+ function reciprocalRank(rank: number): number {
19
+ return 1 / (RECIPROCAL_RANK_FUSION_K + rank);
20
+ }
21
+
22
+ function matchScore(match: MemoryMatch): number {
23
+ return (
24
+ (match.vector ? reciprocalRank(match.vector.rank) : 0) +
25
+ (match.lexical ? reciprocalRank(match.lexical.rank) : 0)
26
+ );
27
+ }
28
+
29
+ function currentChannel(
30
+ match: Pick<MemoryMatch, "sourceKey">,
31
+ channelPrefix: string | undefined,
32
+ ): boolean {
33
+ return channelPrefix ? match.sourceKey.startsWith(channelPrefix) : false;
34
+ }
35
+
36
+ function observedAgeRank(memory: MemoryRecord, nowMs: number): number {
37
+ const ageMs = Math.max(0, nowMs - memory.observedAtMs);
38
+ if (ageMs <= 7 * ONE_DAY_MS) {
39
+ return 3;
40
+ }
41
+ if (ageMs <= 30 * ONE_DAY_MS) {
42
+ return 2;
43
+ }
44
+ if (ageMs <= 90 * ONE_DAY_MS) {
45
+ return 1;
46
+ }
47
+ return 0;
48
+ }
49
+
50
+ /**
51
+ * Keep exact structured-identifier matches in the strongest relevance tier,
52
+ * then fuse lexical and vector ranks without comparing provider raw scores.
53
+ */
54
+ export function rankMemoryMatches(
55
+ matches: MemoryMatch[],
56
+ options: {
57
+ channelPrefix?: string;
58
+ nowMs: number;
59
+ },
60
+ ): MemoryMatch[] {
61
+ const byId = new Map<string, MemoryMatch>();
62
+ for (const match of matches) {
63
+ const existing = byId.get(match.memory.id);
64
+ if (!existing) {
65
+ byId.set(match.memory.id, match);
66
+ continue;
67
+ }
68
+ byId.set(match.memory.id, {
69
+ ...existing,
70
+ exactIdentifier: existing.exactIdentifier || match.exactIdentifier,
71
+ ...(match.lexical ? { lexical: match.lexical } : {}),
72
+ ...(match.vector ? { vector: match.vector } : {}),
73
+ });
74
+ }
75
+ return [...byId.values()].sort((left, right) => {
76
+ const identifierDelta =
77
+ Number(right.exactIdentifier) - Number(left.exactIdentifier);
78
+ if (identifierDelta !== 0) {
79
+ return identifierDelta;
80
+ }
81
+ const scoreDelta = matchScore(right) - matchScore(left);
82
+ if (scoreDelta !== 0) {
83
+ return scoreDelta;
84
+ }
85
+ const channelDelta =
86
+ Number(currentChannel(right, options.channelPrefix)) -
87
+ Number(currentChannel(left, options.channelPrefix));
88
+ if (channelDelta !== 0) {
89
+ return channelDelta;
90
+ }
91
+ return (
92
+ observedAgeRank(right.memory, options.nowMs) -
93
+ observedAgeRank(left.memory, options.nowMs) ||
94
+ right.memory.observedAtMs - left.memory.observedAtMs ||
95
+ left.memory.id.localeCompare(right.memory.id)
96
+ );
97
+ });
98
+ }
package/src/recall.ts CHANGED
@@ -2,9 +2,11 @@ import {
2
2
  definePromptContext,
3
3
  type UserPromptContribution,
4
4
  type Actor,
5
+ type PluginLogger,
5
6
  type Source,
6
7
  } from "@sentry/junior-plugin-api";
7
8
  import { z } from "zod";
9
+ import type { MemoryAgent } from "./agent";
8
10
  import {
9
11
  createMemoryStore,
10
12
  type MemoryDb,
@@ -14,13 +16,16 @@ import {
14
16
  import { memoryRuntimeContextSchema } from "./types";
15
17
 
16
18
  const DEFAULT_RECALL_LIMIT = 5;
19
+ const RECALL_CANDIDATE_LIMIT = 20;
17
20
  const MAX_PROMPT_CHARS = 4_000;
18
21
  const MAX_MEMORY_LINE_CHARS = 600;
19
22
 
20
23
  export interface MemoryRecallContext {
24
+ agent: Pick<MemoryAgent, "selectRelevantMemories">;
21
25
  conversationId?: string;
22
26
  db: MemoryDb;
23
27
  embedder?: MemoryEmbeddingProvider;
28
+ log: PluginLogger;
24
29
  /** Maximum cosine distance for vector recall. Passed through to the memory store. */
25
30
  maxVectorDistance?: number;
26
31
  actor?: Actor;
@@ -103,12 +108,7 @@ const memoryRecallContext = definePromptContext({
103
108
  renderPrompt: (content) => renderMemoryPrompt(content.memories),
104
109
  });
105
110
 
106
- /**
107
- * Build active memory recall contributions.
108
- *
109
- * Personal recall remains prompt-only so conversation reporting cannot widen
110
- * actor-scoped memory visibility.
111
- */
111
+ /** Build active memory recall contributions. */
112
112
  export async function createMemoryPromptContributions(
113
113
  context: MemoryRecallContext,
114
114
  ): Promise<UserPromptContribution[] | undefined> {
@@ -122,21 +122,40 @@ export async function createMemoryPromptContributions(
122
122
  ...(context.actor ? { actor: context.actor } : {}),
123
123
  source: context.source,
124
124
  });
125
- const memories = await createMemoryStore(context.db, runtimeContext, {
125
+ const candidates = await createMemoryStore(context.db, runtimeContext, {
126
126
  ...(context.embedder ? { embedder: context.embedder } : {}),
127
127
  ...(context.maxVectorDistance !== undefined
128
128
  ? { maxVectorDistance: context.maxVectorDistance }
129
129
  : {}),
130
- }).searchMemories({
130
+ }).recallMemories({
131
131
  query: context.text,
132
- limit: DEFAULT_RECALL_LIMIT,
132
+ limit: RECALL_CANDIDATE_LIMIT,
133
133
  });
134
- const selected = selectPromptMemories(memories);
135
- if (selected.length === 0) {
134
+ if (candidates.length === 0) {
135
+ return undefined;
136
+ }
137
+ let relevantIds: string[];
138
+ try {
139
+ relevantIds = await context.agent.selectRelevantMemories({
140
+ candidates: candidates.map(({ content, id }) => ({ content, id })),
141
+ userRequest: context.text,
142
+ });
143
+ } catch {
144
+ // Automatic recall is optional context; a relevance-model failure must not
145
+ // prevent the user's turn from continuing without recalled memory.
146
+ context.log.warn("memory_recall_selection_failed");
136
147
  return undefined;
137
148
  }
138
- if (selected.some((memory) => memory.scope === "personal")) {
139
- return [{ text: renderMemoryPrompt(selected) }];
149
+ const candidatesById = new Map(
150
+ candidates.map((memory) => [memory.id, memory]),
151
+ );
152
+ const relevant = relevantIds
153
+ .map((id) => candidatesById.get(id))
154
+ .filter((memory): memory is MemoryRecord => memory !== undefined)
155
+ .slice(0, DEFAULT_RECALL_LIMIT);
156
+ const selected = selectPromptMemories(relevant);
157
+ if (selected.length === 0) {
158
+ return undefined;
140
159
  }
141
160
  return [memoryRecallContext({ memories: selected })];
142
161
  }