@sentry/junior-memory 0.119.0 → 0.120.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/README.md +22 -6
- package/dist/agent.d.ts +11 -1
- package/dist/api.d.ts +45 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +633 -269
- package/dist/index.js.map +1 -1
- package/dist/personal-store.d.ts +39 -0
- package/dist/personal.d.ts +21 -0
- package/dist/ranking.d.ts +20 -0
- package/dist/recall.d.ts +4 -1
- package/dist/store.d.ts +25 -0
- package/dist/user-pages.d.ts +2 -2
- package/migrations/0005_short_jamie_braddock.sql +1 -0
- package/migrations/meta/0005_snapshot.json +364 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +2 -2
- package/src/agent.ts +65 -1
- package/src/api.ts +168 -0
- package/src/db/schema.ts +5 -0
- package/src/index.ts +6 -0
- package/src/personal-store.ts +195 -0
- package/src/personal.ts +120 -0
- package/src/plugin.ts +9 -0
- package/src/ranking.ts +98 -0
- package/src/recall.ts +31 -4
- package/src/store.ts +151 -236
- package/src/user-pages.ts +27 -62
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;
|
|
@@ -122,16 +127,38 @@ export async function createMemoryPromptContributions(
|
|
|
122
127
|
...(context.actor ? { actor: context.actor } : {}),
|
|
123
128
|
source: context.source,
|
|
124
129
|
});
|
|
125
|
-
const
|
|
130
|
+
const candidates = await createMemoryStore(context.db, runtimeContext, {
|
|
126
131
|
...(context.embedder ? { embedder: context.embedder } : {}),
|
|
127
132
|
...(context.maxVectorDistance !== undefined
|
|
128
133
|
? { maxVectorDistance: context.maxVectorDistance }
|
|
129
134
|
: {}),
|
|
130
|
-
}).
|
|
135
|
+
}).recallMemories({
|
|
131
136
|
query: context.text,
|
|
132
|
-
limit:
|
|
137
|
+
limit: RECALL_CANDIDATE_LIMIT,
|
|
133
138
|
});
|
|
134
|
-
|
|
139
|
+
if (candidates.length === 0) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
let relevantIds: string[];
|
|
143
|
+
try {
|
|
144
|
+
relevantIds = await context.agent.selectRelevantMemories({
|
|
145
|
+
candidates: candidates.map(({ content, id }) => ({ content, id })),
|
|
146
|
+
userRequest: context.text,
|
|
147
|
+
});
|
|
148
|
+
} catch {
|
|
149
|
+
// Automatic recall is optional context; a relevance-model failure must not
|
|
150
|
+
// prevent the user's turn from continuing without recalled memory.
|
|
151
|
+
context.log.warn("memory_recall_selection_failed");
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
const candidatesById = new Map(
|
|
155
|
+
candidates.map((memory) => [memory.id, memory]),
|
|
156
|
+
);
|
|
157
|
+
const relevant = relevantIds
|
|
158
|
+
.map((id) => candidatesById.get(id))
|
|
159
|
+
.filter((memory): memory is MemoryRecord => memory !== undefined)
|
|
160
|
+
.slice(0, DEFAULT_RECALL_LIMIT);
|
|
161
|
+
const selected = selectPromptMemories(relevant);
|
|
135
162
|
if (selected.length === 0) {
|
|
136
163
|
return undefined;
|
|
137
164
|
}
|
package/src/store.ts
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
desc,
|
|
13
13
|
eq,
|
|
14
14
|
gt,
|
|
15
|
-
ilike,
|
|
16
15
|
inArray,
|
|
17
16
|
isNull,
|
|
18
17
|
isNotNull,
|
|
@@ -28,6 +27,7 @@ import type { PgQueryResultHKT } from "drizzle-orm/pg-core/session";
|
|
|
28
27
|
import { z } from "zod";
|
|
29
28
|
import * as memorySqlSchema from "./db/schema";
|
|
30
29
|
import { juniorMemoryEmbeddings, juniorMemoryMemories } from "./db/schema";
|
|
30
|
+
import { rankMemoryMatches, type MemoryMatch } from "./ranking";
|
|
31
31
|
import {
|
|
32
32
|
MEMORY_EMBEDDING_DIMENSIONS,
|
|
33
33
|
MEMORY_SCOPES,
|
|
@@ -49,24 +49,14 @@ import {
|
|
|
49
49
|
const DEFAULT_LIST_LIMIT = 50;
|
|
50
50
|
const DEFAULT_SEARCH_LIMIT = 10;
|
|
51
51
|
const DEFAULT_EXPIRED_ARCHIVE_LIMIT = 100;
|
|
52
|
-
const HIGH_CONFIDENCE_DUPLICATE_DISTANCE = 0.015;
|
|
53
52
|
const PREFERENCE_ADJUDICATION_CANDIDATE_LIMIT = 10;
|
|
54
53
|
const PREFERENCE_ADJUDICATION_VECTOR_LIMIT = 5;
|
|
55
54
|
const VECTOR_SEARCH_OVERFETCH = 4;
|
|
56
55
|
const MAX_MEMORY_CONTENT_CHARS = 4_000;
|
|
57
56
|
const EMBEDDING_METRIC = "cosine";
|
|
58
|
-
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
59
|
-
const RELEVANCE_NEAR_TIE_DELTA = 0.01;
|
|
60
|
-
const SOURCE_CHANNEL_BOOST = 0.05;
|
|
61
57
|
|
|
62
58
|
export type MemoryDb = PgDatabase<PgQueryResultHKT, typeof memorySqlSchema>;
|
|
63
59
|
|
|
64
|
-
interface SearchCandidate {
|
|
65
|
-
memory: MemoryRecord;
|
|
66
|
-
score: number;
|
|
67
|
-
sourceKey: string;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
60
|
interface MemoryEmbedding {
|
|
71
61
|
model: string;
|
|
72
62
|
provider: string;
|
|
@@ -326,6 +316,11 @@ export interface MemoryStore {
|
|
|
326
316
|
listMemories(input: ListMemoriesInput): Promise<MemoryRecord[]>;
|
|
327
317
|
/** List active personal memories owned by the current actor. */
|
|
328
318
|
listPersonalMemories(input: ListMemoriesInput): Promise<MemoryRecord[]>;
|
|
319
|
+
/**
|
|
320
|
+
* Retrieve a broad relevance-ranked candidate window for automatic recall.
|
|
321
|
+
* Prompt admission remains owned by the recall boundary.
|
|
322
|
+
*/
|
|
323
|
+
recallMemories(input: SearchMemoriesInput): Promise<MemoryRecord[]>;
|
|
329
324
|
/** Search active memories visible in the current runtime context. */
|
|
330
325
|
searchMemories(input: SearchMemoriesInput): Promise<MemoryRecord[]>;
|
|
331
326
|
}
|
|
@@ -384,7 +379,8 @@ function sourceChannelPrefix(ctx: MemoryRuntimeContext): string | undefined {
|
|
|
384
379
|
}
|
|
385
380
|
|
|
386
381
|
/** Parse one SQL row into the public memory record projection. */
|
|
387
|
-
|
|
382
|
+
/** Parse one SQL row into the public memory projection. */
|
|
383
|
+
export function parseMemoryRow(row: unknown): MemoryRecord {
|
|
388
384
|
const parsed = memoryRowSchema.parse(row);
|
|
389
385
|
return memoryRecordSchema.parse({
|
|
390
386
|
id: parsed.id,
|
|
@@ -423,7 +419,8 @@ function visibleScopePredicate(scopes: ResolvedMemoryScope[]): SQL | undefined {
|
|
|
423
419
|
);
|
|
424
420
|
}
|
|
425
421
|
|
|
426
|
-
|
|
422
|
+
/** Build the active-row predicate for already-authorized memory scopes. */
|
|
423
|
+
export function activeVisiblePredicate(args: {
|
|
427
424
|
nowMs: number;
|
|
428
425
|
scopes: ResolvedMemoryScope[];
|
|
429
426
|
}): SQL | undefined {
|
|
@@ -525,7 +522,7 @@ async function findByIdempotencyKey(args: {
|
|
|
525
522
|
/**
|
|
526
523
|
* Archive a bounded batch of expired active rows and remove their derived vectors.
|
|
527
524
|
*/
|
|
528
|
-
async function archiveExpiredMemoryBatch(args: {
|
|
525
|
+
export async function archiveExpiredMemoryBatch(args: {
|
|
529
526
|
db: MemoryDb;
|
|
530
527
|
idempotencyKey?: string;
|
|
531
528
|
limit?: number;
|
|
@@ -583,27 +580,38 @@ async function archiveExpiredMemoryBatch(args: {
|
|
|
583
580
|
return { archivedCount: archivedIds.length };
|
|
584
581
|
}
|
|
585
582
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
583
|
+
/**
|
|
584
|
+
* Find path-like, dotted, underscored, and multi-part hyphenated identifiers.
|
|
585
|
+
* Two-part hyphenated prose such as "long-running" remains ordinary text.
|
|
586
|
+
*/
|
|
587
|
+
function structuredQueryIdentifiers(query: string): string[] {
|
|
588
|
+
const matches =
|
|
589
|
+
query
|
|
590
|
+
.toLowerCase()
|
|
591
|
+
.match(
|
|
592
|
+
/(?:[a-z0-9][a-z0-9._-]*\/)+[a-z0-9][a-z0-9._-]*|[a-z0-9]+(?:[._][a-z0-9]+)+|[a-z0-9]+(?:-[a-z0-9]+){2,}/g,
|
|
593
|
+
) ?? [];
|
|
596
594
|
return [
|
|
597
|
-
...new Set(
|
|
598
|
-
query
|
|
599
|
-
.toLowerCase()
|
|
600
|
-
.split(/[^a-z0-9_'-]+/)
|
|
601
|
-
.map((term) => term.trim())
|
|
602
|
-
.filter((term) => term.length >= 2),
|
|
603
|
-
),
|
|
595
|
+
...new Set(matches.map((identifier) => identifier.replace(/\.+$/, ""))),
|
|
604
596
|
];
|
|
605
597
|
}
|
|
606
598
|
|
|
599
|
+
function denseRanks<T>(
|
|
600
|
+
values: T[],
|
|
601
|
+
key: (value: T) => string | number,
|
|
602
|
+
): number[] {
|
|
603
|
+
let previous: string | number | undefined;
|
|
604
|
+
let rank = 0;
|
|
605
|
+
return values.map((value, index) => {
|
|
606
|
+
const current = key(value);
|
|
607
|
+
if (index === 0 || current !== previous) {
|
|
608
|
+
rank = index + 1;
|
|
609
|
+
previous = current;
|
|
610
|
+
}
|
|
611
|
+
return rank;
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
607
615
|
async function embedOne(
|
|
608
616
|
embedder: MemoryEmbeddingProvider,
|
|
609
617
|
text: string,
|
|
@@ -735,52 +743,6 @@ async function findExactDuplicateMemory(args: {
|
|
|
735
743
|
return rows[0] ? parseMemoryRow(rows[0]) : undefined;
|
|
736
744
|
}
|
|
737
745
|
|
|
738
|
-
async function findVectorDuplicateMemory(args: {
|
|
739
|
-
db: MemoryDb;
|
|
740
|
-
embedding: MemoryEmbedding;
|
|
741
|
-
kind: MemoryRecord["kind"];
|
|
742
|
-
nowMs: number;
|
|
743
|
-
scope: ResolvedMemoryScope;
|
|
744
|
-
subject: ResolvedMemorySubject;
|
|
745
|
-
}): Promise<MemoryRecord | undefined> {
|
|
746
|
-
const distance = cosineDistance(
|
|
747
|
-
juniorMemoryEmbeddings.embedding,
|
|
748
|
-
args.embedding.vector,
|
|
749
|
-
);
|
|
750
|
-
const rows = await args.db
|
|
751
|
-
.select({
|
|
752
|
-
contentHash: juniorMemoryEmbeddings.contentHash,
|
|
753
|
-
distance,
|
|
754
|
-
memory: juniorMemoryMemories,
|
|
755
|
-
})
|
|
756
|
-
.from(juniorMemoryMemories)
|
|
757
|
-
.innerJoin(
|
|
758
|
-
juniorMemoryEmbeddings,
|
|
759
|
-
eq(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id),
|
|
760
|
-
)
|
|
761
|
-
.where(
|
|
762
|
-
and(
|
|
763
|
-
activeScopedSubjectPredicate(args),
|
|
764
|
-
eq(juniorMemoryEmbeddings.provider, args.embedding.provider),
|
|
765
|
-
eq(juniorMemoryEmbeddings.model, args.embedding.model),
|
|
766
|
-
eq(juniorMemoryEmbeddings.dimensions, MEMORY_EMBEDDING_DIMENSIONS),
|
|
767
|
-
eq(juniorMemoryEmbeddings.metric, EMBEDDING_METRIC),
|
|
768
|
-
lte(distance, HIGH_CONFIDENCE_DUPLICATE_DISTANCE),
|
|
769
|
-
),
|
|
770
|
-
)
|
|
771
|
-
.orderBy(
|
|
772
|
-
distance,
|
|
773
|
-
desc(juniorMemoryMemories.createdAtMs),
|
|
774
|
-
asc(juniorMemoryMemories.id),
|
|
775
|
-
)
|
|
776
|
-
.limit(1);
|
|
777
|
-
const row = rows[0];
|
|
778
|
-
if (!row || hashEmbeddedContent(row.memory.content) !== row.contentHash) {
|
|
779
|
-
return undefined;
|
|
780
|
-
}
|
|
781
|
-
return parseMemoryRow(row.memory);
|
|
782
|
-
}
|
|
783
|
-
|
|
784
746
|
async function rememberDuplicateIdempotency(args: {
|
|
785
747
|
content: string;
|
|
786
748
|
db: MemoryDb;
|
|
@@ -989,38 +951,73 @@ async function listVisibleMemories(args: {
|
|
|
989
951
|
return rows.map(parseMemoryRow);
|
|
990
952
|
}
|
|
991
953
|
|
|
992
|
-
/** Search active visible records with
|
|
993
|
-
async function
|
|
954
|
+
/** Search active visible records with PostgreSQL full-text ranking. */
|
|
955
|
+
async function searchVisibleLexicalMemories(args: {
|
|
994
956
|
db: MemoryDb;
|
|
957
|
+
limit: number;
|
|
995
958
|
nowMs: number;
|
|
996
959
|
query: string;
|
|
997
960
|
scopes: ResolvedMemoryScope[];
|
|
998
|
-
}): Promise<
|
|
999
|
-
const terms = searchTerms(args.query);
|
|
1000
|
-
if (terms.length === 0) {
|
|
1001
|
-
return [];
|
|
1002
|
-
}
|
|
961
|
+
}): Promise<MemoryMatch[]> {
|
|
1003
962
|
const predicate = activeVisiblePredicate(args);
|
|
1004
963
|
if (!predicate) {
|
|
1005
964
|
return [];
|
|
1006
965
|
}
|
|
966
|
+
const queryVector = sql`to_tsvector('english', ${args.query})`;
|
|
967
|
+
const tsquery = sql`(
|
|
968
|
+
SELECT COALESCE(
|
|
969
|
+
string_agg(quote_literal(term), ' | ')::tsquery,
|
|
970
|
+
''::tsquery
|
|
971
|
+
)
|
|
972
|
+
FROM unnest(tsvector_to_array(${queryVector})) AS query_terms(term)
|
|
973
|
+
)`;
|
|
974
|
+
const textsearch = sql`to_tsvector('english', ${juniorMemoryMemories.content})`;
|
|
975
|
+
const textRank = sql<number>`ts_rank_cd(${textsearch}, ${tsquery})`;
|
|
976
|
+
const identifiers = structuredQueryIdentifiers(args.query);
|
|
977
|
+
const contentTokens = sql`regexp_split_to_array(lower(${juniorMemoryMemories.content}), '[^a-z0-9._/-]+')`;
|
|
978
|
+
const identifierArray =
|
|
979
|
+
identifiers.length > 0
|
|
980
|
+
? sql`ARRAY[${sql.join(
|
|
981
|
+
identifiers.map((identifier) => sql`${identifier}`),
|
|
982
|
+
sql`, `,
|
|
983
|
+
)}]::text[]`
|
|
984
|
+
: undefined;
|
|
985
|
+
const exactIdentifierMatch =
|
|
986
|
+
identifierArray !== undefined
|
|
987
|
+
? sql<boolean>`EXISTS (
|
|
988
|
+
SELECT 1
|
|
989
|
+
FROM unnest(${contentTokens}) AS content_token(value)
|
|
990
|
+
CROSS JOIN unnest(${identifierArray}) AS query_identifier(value)
|
|
991
|
+
WHERE strpos(
|
|
992
|
+
'/' || regexp_replace(content_token.value, '\\.+$', '') || '/',
|
|
993
|
+
'/' || query_identifier.value || '/'
|
|
994
|
+
) > 0
|
|
995
|
+
)`
|
|
996
|
+
: sql<boolean>`false`;
|
|
1007
997
|
const rows = await args.db
|
|
1008
|
-
.select(
|
|
998
|
+
.select({
|
|
999
|
+
exactIdentifierMatch,
|
|
1000
|
+
memory: juniorMemoryMemories,
|
|
1001
|
+
textRank,
|
|
1002
|
+
})
|
|
1009
1003
|
.from(juniorMemoryMemories)
|
|
1010
|
-
.where(
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1004
|
+
.where(and(predicate, sql`${textsearch} @@ ${tsquery}`))
|
|
1005
|
+
.orderBy(
|
|
1006
|
+
...(identifiers.length > 0 ? [desc(exactIdentifierMatch)] : []),
|
|
1007
|
+
desc(textRank),
|
|
1008
|
+
desc(juniorMemoryMemories.observedAtMs),
|
|
1009
|
+
asc(juniorMemoryMemories.id),
|
|
1010
|
+
)
|
|
1011
|
+
.limit(args.limit);
|
|
1012
|
+
const ranks = denseRanks(
|
|
1013
|
+
rows,
|
|
1014
|
+
(row) => `${row.exactIdentifierMatch}:${Number(row.textRank)}`,
|
|
1015
|
+
);
|
|
1016
|
+
return rows.map((row, index) => ({
|
|
1017
|
+
exactIdentifier: row.exactIdentifierMatch,
|
|
1018
|
+
lexical: { rank: ranks[index] },
|
|
1019
|
+
memory: parseMemoryRow(row.memory),
|
|
1020
|
+
sourceKey: row.memory.sourceKey,
|
|
1024
1021
|
}));
|
|
1025
1022
|
}
|
|
1026
1023
|
|
|
@@ -1033,7 +1030,7 @@ async function searchVisibleVectorMemories(args: {
|
|
|
1033
1030
|
nowMs: number;
|
|
1034
1031
|
query: string;
|
|
1035
1032
|
scopes: ResolvedMemoryScope[];
|
|
1036
|
-
}): Promise<
|
|
1033
|
+
}): Promise<MemoryMatch[]> {
|
|
1037
1034
|
if (!args.embedder) {
|
|
1038
1035
|
return [];
|
|
1039
1036
|
}
|
|
@@ -1077,7 +1074,8 @@ async function searchVisibleVectorMemories(args: {
|
|
|
1077
1074
|
asc(juniorMemoryMemories.id),
|
|
1078
1075
|
)
|
|
1079
1076
|
.limit(args.limit);
|
|
1080
|
-
|
|
1077
|
+
const ranks = denseRanks(rows, (row) => Number(row.distance));
|
|
1078
|
+
return rows.flatMap((row, index) => {
|
|
1081
1079
|
const distanceValue = Number(row.distance);
|
|
1082
1080
|
if (
|
|
1083
1081
|
row.distance === null ||
|
|
@@ -1091,89 +1089,17 @@ async function searchVisibleVectorMemories(args: {
|
|
|
1091
1089
|
}
|
|
1092
1090
|
return [
|
|
1093
1091
|
{
|
|
1092
|
+
exactIdentifier: false,
|
|
1094
1093
|
memory: parseMemoryRow(row.memory),
|
|
1095
|
-
score: 1 / (1 + Math.max(0, distanceValue)),
|
|
1096
1094
|
sourceKey: row.memory.sourceKey,
|
|
1095
|
+
vector: {
|
|
1096
|
+
rank: ranks[index],
|
|
1097
|
+
},
|
|
1097
1098
|
},
|
|
1098
1099
|
];
|
|
1099
1100
|
});
|
|
1100
1101
|
}
|
|
1101
1102
|
|
|
1102
|
-
function sourceBoost(
|
|
1103
|
-
candidate: Pick<SearchCandidate, "sourceKey">,
|
|
1104
|
-
currentChannelPrefix: string | undefined,
|
|
1105
|
-
): number {
|
|
1106
|
-
if (!currentChannelPrefix) {
|
|
1107
|
-
return 0;
|
|
1108
|
-
}
|
|
1109
|
-
return candidate.sourceKey.startsWith(currentChannelPrefix)
|
|
1110
|
-
? SOURCE_CHANNEL_BOOST
|
|
1111
|
-
: 0;
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
/** Score only observation age for near-tie reranking; this is not lifecycle decay. */
|
|
1115
|
-
function observedAgeBoost(memory: MemoryRecord, nowMs: number): number {
|
|
1116
|
-
const ageMs = Math.max(0, nowMs - memory.observedAtMs);
|
|
1117
|
-
if (ageMs <= 7 * ONE_DAY_MS) {
|
|
1118
|
-
return 0.15;
|
|
1119
|
-
}
|
|
1120
|
-
if (ageMs <= 30 * ONE_DAY_MS) {
|
|
1121
|
-
return 0.1;
|
|
1122
|
-
}
|
|
1123
|
-
if (ageMs <= 90 * ONE_DAY_MS) {
|
|
1124
|
-
return 0.05;
|
|
1125
|
-
}
|
|
1126
|
-
return 0;
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
/** Fuse retrieval candidates while keeping observed age to near-tie reranking. */
|
|
1130
|
-
function mergeSearchCandidates(
|
|
1131
|
-
candidates: SearchCandidate[],
|
|
1132
|
-
nowMs: number,
|
|
1133
|
-
currentChannelKey?: string,
|
|
1134
|
-
): MemoryRecord[] {
|
|
1135
|
-
const byId = new Map<
|
|
1136
|
-
string,
|
|
1137
|
-
{
|
|
1138
|
-
memory: MemoryRecord;
|
|
1139
|
-
score: number;
|
|
1140
|
-
sourceKey: string;
|
|
1141
|
-
}
|
|
1142
|
-
>();
|
|
1143
|
-
for (const candidate of candidates) {
|
|
1144
|
-
const existing = byId.get(candidate.memory.id);
|
|
1145
|
-
if (existing) {
|
|
1146
|
-
existing.score += candidate.score;
|
|
1147
|
-
continue;
|
|
1148
|
-
}
|
|
1149
|
-
byId.set(candidate.memory.id, {
|
|
1150
|
-
memory: candidate.memory,
|
|
1151
|
-
score: candidate.score,
|
|
1152
|
-
sourceKey: candidate.sourceKey,
|
|
1153
|
-
});
|
|
1154
|
-
}
|
|
1155
|
-
return [...byId.values()]
|
|
1156
|
-
.sort((left, right) => {
|
|
1157
|
-
const scoreDelta = right.score - left.score;
|
|
1158
|
-
if (Math.abs(scoreDelta) > RELEVANCE_NEAR_TIE_DELTA) {
|
|
1159
|
-
return scoreDelta;
|
|
1160
|
-
}
|
|
1161
|
-
const sourceDelta =
|
|
1162
|
-
sourceBoost(right, currentChannelKey) -
|
|
1163
|
-
sourceBoost(left, currentChannelKey);
|
|
1164
|
-
if (sourceDelta !== 0) {
|
|
1165
|
-
return sourceDelta;
|
|
1166
|
-
}
|
|
1167
|
-
return (
|
|
1168
|
-
observedAgeBoost(right.memory, nowMs) -
|
|
1169
|
-
observedAgeBoost(left.memory, nowMs) ||
|
|
1170
|
-
right.memory.observedAtMs - left.memory.observedAtMs ||
|
|
1171
|
-
left.memory.id.localeCompare(right.memory.id)
|
|
1172
|
-
);
|
|
1173
|
-
})
|
|
1174
|
-
.map((candidate) => candidate.memory);
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
1103
|
/** Create a context-bound SQL-backed store for explicit memory operations. */
|
|
1178
1104
|
export function createMemoryStore(
|
|
1179
1105
|
db: MemoryDb,
|
|
@@ -1294,27 +1220,6 @@ export function createMemoryStore(
|
|
|
1294
1220
|
candidateEmbedding = undefined;
|
|
1295
1221
|
}
|
|
1296
1222
|
}
|
|
1297
|
-
const vectorDuplicate = candidateEmbedding
|
|
1298
|
-
? await findVectorDuplicateMemory({
|
|
1299
|
-
db,
|
|
1300
|
-
embedding: candidateEmbedding,
|
|
1301
|
-
kind: input.kind,
|
|
1302
|
-
nowMs,
|
|
1303
|
-
scope,
|
|
1304
|
-
subject,
|
|
1305
|
-
})
|
|
1306
|
-
: undefined;
|
|
1307
|
-
if (vectorDuplicate) {
|
|
1308
|
-
return await reuseDuplicateMemory({
|
|
1309
|
-
content,
|
|
1310
|
-
duplicate: vectorDuplicate,
|
|
1311
|
-
idempotencyKey: input.idempotencyKey,
|
|
1312
|
-
nowMs,
|
|
1313
|
-
scope,
|
|
1314
|
-
subject,
|
|
1315
|
-
});
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
1223
|
let supersededIds: string[] = [];
|
|
1319
1224
|
if (
|
|
1320
1225
|
scopeKind === "personal" &&
|
|
@@ -1440,6 +1345,49 @@ export function createMemoryStore(
|
|
|
1440
1345
|
return { created: false, memory: idempotent };
|
|
1441
1346
|
}
|
|
1442
1347
|
|
|
1348
|
+
async function retrieveVisibleMemories(
|
|
1349
|
+
rawInput: SearchMemoriesInput,
|
|
1350
|
+
vectorMaxDistance: number | undefined,
|
|
1351
|
+
): Promise<MemoryRecord[]> {
|
|
1352
|
+
const input = searchMemoriesInputSchema.parse(rawInput);
|
|
1353
|
+
const nowMs = getNowMs();
|
|
1354
|
+
const scopes = deriveVisibleMemoryScopes(runtimeContext);
|
|
1355
|
+
await archiveExpiredMemoryBatch({
|
|
1356
|
+
db,
|
|
1357
|
+
nowMs,
|
|
1358
|
+
scopes,
|
|
1359
|
+
});
|
|
1360
|
+
const limit = boundedLimit(input.limit, DEFAULT_SEARCH_LIMIT);
|
|
1361
|
+
const candidateLimit = limit * VECTOR_SEARCH_OVERFETCH;
|
|
1362
|
+
const [vectorMatches, lexicalMatches] = await Promise.all([
|
|
1363
|
+
searchVisibleVectorMemories({
|
|
1364
|
+
db,
|
|
1365
|
+
embedder,
|
|
1366
|
+
limit: candidateLimit,
|
|
1367
|
+
...(vectorMaxDistance !== undefined
|
|
1368
|
+
? { maxDistance: vectorMaxDistance }
|
|
1369
|
+
: {}),
|
|
1370
|
+
nowMs,
|
|
1371
|
+
query: input.query,
|
|
1372
|
+
scopes,
|
|
1373
|
+
}),
|
|
1374
|
+
searchVisibleLexicalMemories({
|
|
1375
|
+
db,
|
|
1376
|
+
limit: candidateLimit,
|
|
1377
|
+
nowMs,
|
|
1378
|
+
query: input.query,
|
|
1379
|
+
scopes,
|
|
1380
|
+
}),
|
|
1381
|
+
]);
|
|
1382
|
+
const channelPrefix = sourceChannelPrefix(runtimeContext);
|
|
1383
|
+
return rankMemoryMatches([...vectorMatches, ...lexicalMatches], {
|
|
1384
|
+
nowMs,
|
|
1385
|
+
...(channelPrefix ? { channelPrefix } : {}),
|
|
1386
|
+
})
|
|
1387
|
+
.slice(0, limit)
|
|
1388
|
+
.map(({ memory }) => memory);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1443
1391
|
return {
|
|
1444
1392
|
async archiveExpiredMemories(input) {
|
|
1445
1393
|
return await archiveExpiredVisibleMemories(input, getNowMs());
|
|
@@ -1487,45 +1435,12 @@ export function createMemoryStore(
|
|
|
1487
1435
|
});
|
|
1488
1436
|
},
|
|
1489
1437
|
|
|
1438
|
+
async recallMemories(input) {
|
|
1439
|
+
return await retrieveVisibleMemories(input, maxVectorDistance);
|
|
1440
|
+
},
|
|
1441
|
+
|
|
1490
1442
|
async searchMemories(input) {
|
|
1491
|
-
|
|
1492
|
-
const nowMs = getNowMs();
|
|
1493
|
-
const scopes = deriveVisibleMemoryScopes(runtimeContext);
|
|
1494
|
-
await archiveExpiredMemoryBatch({
|
|
1495
|
-
db,
|
|
1496
|
-
nowMs,
|
|
1497
|
-
scopes,
|
|
1498
|
-
});
|
|
1499
|
-
const limit = boundedLimit(input.limit, DEFAULT_SEARCH_LIMIT);
|
|
1500
|
-
const vectorCandidates = await searchVisibleVectorMemories({
|
|
1501
|
-
db,
|
|
1502
|
-
embedder,
|
|
1503
|
-
limit: limit * VECTOR_SEARCH_OVERFETCH,
|
|
1504
|
-
...(maxVectorDistance !== undefined
|
|
1505
|
-
? { maxDistance: maxVectorDistance }
|
|
1506
|
-
: {}),
|
|
1507
|
-
nowMs,
|
|
1508
|
-
query: input.query,
|
|
1509
|
-
scopes,
|
|
1510
|
-
});
|
|
1511
|
-
const candidates = await searchVisibleMemories({
|
|
1512
|
-
db,
|
|
1513
|
-
nowMs,
|
|
1514
|
-
query: input.query,
|
|
1515
|
-
scopes,
|
|
1516
|
-
});
|
|
1517
|
-
const terms = searchTerms(input.query);
|
|
1518
|
-
const lexicalCandidates = candidates
|
|
1519
|
-
.map((candidate) => ({
|
|
1520
|
-
...candidate,
|
|
1521
|
-
score: searchScore(candidate.memory, terms),
|
|
1522
|
-
}))
|
|
1523
|
-
.filter((item) => item.score > 0);
|
|
1524
|
-
return mergeSearchCandidates(
|
|
1525
|
-
[...vectorCandidates, ...lexicalCandidates],
|
|
1526
|
-
nowMs,
|
|
1527
|
-
sourceChannelPrefix(runtimeContext),
|
|
1528
|
-
).slice(0, limit);
|
|
1443
|
+
return await retrieveVisibleMemories(input, undefined);
|
|
1529
1444
|
},
|
|
1530
1445
|
|
|
1531
1446
|
async archiveMemory(input) {
|
package/src/user-pages.ts
CHANGED
|
@@ -1,36 +1,7 @@
|
|
|
1
|
-
/** Project
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from "@sentry/junior-plugin-api";
|
|
6
|
-
import { createMemoryStore, type MemoryDb, type MemoryRecord } from "./store";
|
|
7
|
-
import type { MemoryRuntimeContext } from "./types";
|
|
8
|
-
|
|
9
|
-
const MEMORY_PAGE_LIMIT = 100;
|
|
10
|
-
|
|
11
|
-
function runtimeContext(
|
|
12
|
-
actor: PluginUserPageActor,
|
|
13
|
-
): MemoryRuntimeContext | undefined {
|
|
14
|
-
if (actor.platform === "slack") {
|
|
15
|
-
return {
|
|
16
|
-
actor,
|
|
17
|
-
source: {
|
|
18
|
-
platform: "slack",
|
|
19
|
-
type: "priv",
|
|
20
|
-
teamId: actor.teamId,
|
|
21
|
-
channelId: "DDASHBOARD",
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
return {
|
|
26
|
-
actor,
|
|
27
|
-
source: {
|
|
28
|
-
platform: "local",
|
|
29
|
-
type: "priv",
|
|
30
|
-
conversationId: "local:dashboard:memories",
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
}
|
|
1
|
+
/** Project viewer-owned memories into Junior's core-rendered user page. */
|
|
2
|
+
import type { PluginUserPageDefinition } from "@sentry/junior-plugin-api";
|
|
3
|
+
import { createViewerMemories } from "./personal";
|
|
4
|
+
import type { MemoryDb, MemoryRecord } from "./store";
|
|
34
5
|
|
|
35
6
|
function memoryKindLabel(kind: MemoryRecord["kind"]): string {
|
|
36
7
|
return kind.charAt(0).toUpperCase() + kind.slice(1);
|
|
@@ -43,44 +14,38 @@ function rememberedDate(createdAtMs: number): string {
|
|
|
43
14
|
}).format(new Date(createdAtMs));
|
|
44
15
|
}
|
|
45
16
|
|
|
46
|
-
|
|
47
|
-
db: MemoryDb,
|
|
48
|
-
actors: PluginUserPageActor[],
|
|
49
|
-
): Promise<MemoryRecord[]> {
|
|
50
|
-
const memories = await Promise.all(
|
|
51
|
-
actors.flatMap((actor) => {
|
|
52
|
-
const context = runtimeContext(actor);
|
|
53
|
-
return context
|
|
54
|
-
? [
|
|
55
|
-
createMemoryStore(db, context).listPersonalMemories({
|
|
56
|
-
limit: MEMORY_PAGE_LIMIT,
|
|
57
|
-
}),
|
|
58
|
-
]
|
|
59
|
-
: [];
|
|
60
|
-
}),
|
|
61
|
-
);
|
|
62
|
-
return [
|
|
63
|
-
...new Map(memories.flat().map((memory) => [memory.id, memory])).values(),
|
|
64
|
-
]
|
|
65
|
-
.sort((left, right) => right.createdAtMs - left.createdAtMs)
|
|
66
|
-
.slice(0, MEMORY_PAGE_LIMIT);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Create the read-only personal Memories dashboard page. */
|
|
17
|
+
/** Create the interactive personal Memories dashboard page. */
|
|
70
18
|
export function createMemoryUserPage(): PluginUserPageDefinition {
|
|
71
19
|
return {
|
|
72
20
|
id: "memories",
|
|
73
21
|
label: "Memories",
|
|
74
22
|
description: "Personal facts and preferences Junior remembers about you.",
|
|
75
|
-
async read(ctx) {
|
|
76
|
-
const
|
|
23
|
+
async read(ctx, input) {
|
|
24
|
+
const page = await createViewerMemories(
|
|
77
25
|
ctx.db as MemoryDb,
|
|
78
26
|
ctx.viewer.actors,
|
|
79
|
-
)
|
|
27
|
+
).list({
|
|
28
|
+
cursor: input.cursor,
|
|
29
|
+
limit: input.limit,
|
|
30
|
+
...(input.query ? { query: input.query } : {}),
|
|
31
|
+
});
|
|
80
32
|
return {
|
|
81
33
|
type: "list",
|
|
82
|
-
emptyText:
|
|
83
|
-
|
|
34
|
+
emptyText: input.query
|
|
35
|
+
? "No memories matched your search."
|
|
36
|
+
: "No personal memories yet.",
|
|
37
|
+
...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
|
|
38
|
+
searchPlaceholder: "Search memories",
|
|
39
|
+
records: page.memories.map((memory) => ({
|
|
40
|
+
actions: [
|
|
41
|
+
{
|
|
42
|
+
confirmation: "Forget this memory?",
|
|
43
|
+
href: `/api/plugins/memory/memories/${encodeURIComponent(memory.id)}`,
|
|
44
|
+
label: "Forget",
|
|
45
|
+
method: "DELETE" as const,
|
|
46
|
+
tone: "danger" as const,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
84
49
|
id: memory.id,
|
|
85
50
|
title: memory.content,
|
|
86
51
|
metadata: [
|