@sentry/junior-memory 0.197.0 → 0.199.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/index.d.ts +887 -7
- package/package.json +5 -6
- package/dist/agent.d.ts +0 -280
- package/dist/api.d.ts +0 -121
- package/dist/cli/format.d.ts +0 -5
- package/dist/cli/index.d.ts +0 -3
- package/dist/cli/search.d.ts +0 -4
- package/dist/cli/show.d.ts +0 -4
- package/dist/db/schema.d.ts +0 -479
- package/dist/events.d.ts +0 -35
- package/dist/operational-report.d.ts +0 -8
- package/dist/plugin.d.ts +0 -9
- package/dist/process-session.d.ts +0 -9
- package/dist/ranking.d.ts +0 -18
- package/dist/recall.d.ts +0 -41
- package/dist/scope.d.ts +0 -19
- package/dist/store.d.ts +0 -229
- package/dist/tools.d.ts +0 -164
- package/dist/types.d.ts +0 -78
- package/dist/user-pages.d.ts +0 -4
- package/dist/viewer.d.ts +0 -91
- package/src/agent.ts +0 -663
- package/src/api.ts +0 -288
- package/src/cli/format.ts +0 -30
- package/src/cli/index.ts +0 -15
- package/src/cli/search.ts +0 -119
- package/src/cli/show.ts +0 -44
- package/src/db/schema.ts +0 -147
- package/src/events.ts +0 -107
- package/src/index.ts +0 -25
- package/src/operational-report.ts +0 -228
- package/src/plugin.ts +0 -186
- package/src/process-session.ts +0 -311
- package/src/ranking.ts +0 -107
- package/src/recall.ts +0 -212
- package/src/scope.ts +0 -75
- package/src/store.ts +0 -1578
- package/src/tools.ts +0 -586
- package/src/types.ts +0 -37
- package/src/user-pages.ts +0 -99
- package/src/viewer.ts +0 -441
package/src/store.ts
DELETED
|
@@ -1,1578 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SQL-backed memory store boundary.
|
|
3
|
-
*
|
|
4
|
-
* This module owns row parsing plus visible create/list/search/archive
|
|
5
|
-
* operations. Visibility, expiration, and supersession are enforced before
|
|
6
|
-
* records leave the store.
|
|
7
|
-
*/
|
|
8
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
9
|
-
import {
|
|
10
|
-
and,
|
|
11
|
-
asc,
|
|
12
|
-
desc,
|
|
13
|
-
eq,
|
|
14
|
-
gt,
|
|
15
|
-
inArray,
|
|
16
|
-
isNull,
|
|
17
|
-
isNotNull,
|
|
18
|
-
like,
|
|
19
|
-
lte,
|
|
20
|
-
or,
|
|
21
|
-
sql,
|
|
22
|
-
type SQL,
|
|
23
|
-
} from "drizzle-orm";
|
|
24
|
-
import { cosineDistance } from "drizzle-orm/sql/functions";
|
|
25
|
-
import type { PgDatabase } from "drizzle-orm/pg-core";
|
|
26
|
-
import type { PgQueryResultHKT } from "drizzle-orm/pg-core/session";
|
|
27
|
-
import { z } from "zod";
|
|
28
|
-
import { getSourceKey } from "@sentry/junior-plugin-api";
|
|
29
|
-
import * as memorySqlSchema from "./db/schema";
|
|
30
|
-
import { juniorMemoryEmbeddings, juniorMemoryMemories } from "./db/schema";
|
|
31
|
-
import { rankMemoryMatches, type MemoryMatch } from "./ranking";
|
|
32
|
-
import {
|
|
33
|
-
MEMORY_EMBEDDING_DIMENSIONS,
|
|
34
|
-
MEMORY_SCOPES,
|
|
35
|
-
MEMORY_SOURCE_PLATFORMS,
|
|
36
|
-
MEMORY_SUBJECT_TYPES,
|
|
37
|
-
MEMORY_KINDS,
|
|
38
|
-
memoryRuntimeContextSchema,
|
|
39
|
-
type MemoryRuntimeContext,
|
|
40
|
-
type MemorySourcePlatform,
|
|
41
|
-
} from "./types";
|
|
42
|
-
import {
|
|
43
|
-
deriveMemoryScope,
|
|
44
|
-
deriveMemorySubject,
|
|
45
|
-
type ResolvedMemorySubject,
|
|
46
|
-
deriveVisibleMemoryScopes,
|
|
47
|
-
type ResolvedMemoryScope,
|
|
48
|
-
} from "./scope";
|
|
49
|
-
|
|
50
|
-
const DEFAULT_LIST_LIMIT = 50;
|
|
51
|
-
const DEFAULT_SEARCH_LIMIT = 10;
|
|
52
|
-
const DEFAULT_EXPIRED_ARCHIVE_LIMIT = 100;
|
|
53
|
-
const PREFERENCE_ADJUDICATION_CANDIDATE_LIMIT = 10;
|
|
54
|
-
const PREFERENCE_ADJUDICATION_VECTOR_LIMIT = 5;
|
|
55
|
-
/** Explicit search overfetch: keep a wider fusion window for tool/CLI search. */
|
|
56
|
-
const SEARCH_RETRIEVAL_OVERFETCH = 4;
|
|
57
|
-
/**
|
|
58
|
-
* Automatic recall overfetch. Recall already asks for ~20 candidates before the
|
|
59
|
-
* relevance gate, so each hybrid leg only needs a small top-k probe.
|
|
60
|
-
*/
|
|
61
|
-
const RECALL_RETRIEVAL_OVERFETCH = 2;
|
|
62
|
-
/**
|
|
63
|
-
* Absolute ceiling per retrieval leg. Matches the store limit ceiling so a
|
|
64
|
-
* single healthy leg can still fill the caller's requested result window.
|
|
65
|
-
*/
|
|
66
|
-
const MAX_RETRIEVAL_LEG_CANDIDATES = 200;
|
|
67
|
-
/** Cap ts_rank_cd work after GIN filtering; ranking is not indexable. */
|
|
68
|
-
const MAX_LEXICAL_RANK_CANDIDATES = 200;
|
|
69
|
-
/** Expand the GIN match window before ts_rank_cd, still under the hard cap. */
|
|
70
|
-
const LEXICAL_RANK_WINDOW_MULTIPLIER = 4;
|
|
71
|
-
/** Bound query text before embedding / FTS construction. */
|
|
72
|
-
const MAX_RETRIEVAL_QUERY_CHARS = 1_500;
|
|
73
|
-
const MAX_MEMORY_CONTENT_CHARS = 4_000;
|
|
74
|
-
const EMBEDDING_METRIC = "cosine";
|
|
75
|
-
/**
|
|
76
|
-
* Cosine distance cutoff for automatic recall only (not explicit search).
|
|
77
|
-
* Tuned for text-embedding-3-small; retune if the embedding model changes.
|
|
78
|
-
*/
|
|
79
|
-
const RECALL_MAX_VECTOR_DISTANCE = 0.45;
|
|
80
|
-
|
|
81
|
-
export type MemoryDb = PgDatabase<PgQueryResultHKT, typeof memorySqlSchema>;
|
|
82
|
-
|
|
83
|
-
interface MemoryEmbedding {
|
|
84
|
-
model: string;
|
|
85
|
-
provider: string;
|
|
86
|
-
vector: number[];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const nonEmptyStringSchema = z.string().min(1);
|
|
90
|
-
const memoryContentSchema = z
|
|
91
|
-
.string()
|
|
92
|
-
.refine((content) => content.trim().length > 0, {
|
|
93
|
-
message: "Memory content is required.",
|
|
94
|
-
});
|
|
95
|
-
const numberSchema = z.number().finite();
|
|
96
|
-
const createMemoryInputSchema = z
|
|
97
|
-
.object({
|
|
98
|
-
content: memoryContentSchema,
|
|
99
|
-
expiresAtMs: numberSchema.optional(),
|
|
100
|
-
idempotencyKey: nonEmptyStringSchema,
|
|
101
|
-
kind: z.enum(MEMORY_KINDS),
|
|
102
|
-
})
|
|
103
|
-
.strict();
|
|
104
|
-
const listMemoriesInputSchema = z
|
|
105
|
-
.object({
|
|
106
|
-
limit: numberSchema.optional(),
|
|
107
|
-
})
|
|
108
|
-
.strict();
|
|
109
|
-
const searchMemoriesInputSchema = z
|
|
110
|
-
.object({
|
|
111
|
-
limit: numberSchema.optional(),
|
|
112
|
-
query: nonEmptyStringSchema,
|
|
113
|
-
})
|
|
114
|
-
.strict();
|
|
115
|
-
const archiveMemoryInputSchema = z
|
|
116
|
-
.object({
|
|
117
|
-
id: nonEmptyStringSchema,
|
|
118
|
-
reason: nonEmptyStringSchema.optional(),
|
|
119
|
-
})
|
|
120
|
-
.strict();
|
|
121
|
-
const archiveExpiredMemoriesInputSchema = z
|
|
122
|
-
.object({
|
|
123
|
-
limit: numberSchema.optional(),
|
|
124
|
-
})
|
|
125
|
-
.strict();
|
|
126
|
-
const clockSchema = z.function({ input: [], output: numberSchema }).optional();
|
|
127
|
-
const memoryStoreOptionsSchema = z
|
|
128
|
-
.object({
|
|
129
|
-
now: clockSchema,
|
|
130
|
-
})
|
|
131
|
-
.strict();
|
|
132
|
-
const optionalNumberSchema = z.preprocess(
|
|
133
|
-
(value) => (value === null ? undefined : value),
|
|
134
|
-
z.coerce.number().optional(),
|
|
135
|
-
);
|
|
136
|
-
const optionalStringSchema = z.preprocess(
|
|
137
|
-
(value) => (value === null ? undefined : value),
|
|
138
|
-
z.string().optional(),
|
|
139
|
-
);
|
|
140
|
-
const optionalNonEmptyStringSchema = z.preprocess(
|
|
141
|
-
(value) => (value === null ? undefined : value),
|
|
142
|
-
z.string().min(1).optional(),
|
|
143
|
-
);
|
|
144
|
-
const memoryRowSchema = z
|
|
145
|
-
.object({
|
|
146
|
-
archivedAtMs: optionalNumberSchema,
|
|
147
|
-
archiveReason: optionalStringSchema,
|
|
148
|
-
content: memoryContentSchema,
|
|
149
|
-
createdAtMs: z.coerce.number(),
|
|
150
|
-
expiresAtMs: optionalNumberSchema,
|
|
151
|
-
id: z.string().min(1),
|
|
152
|
-
idempotencyKey: optionalStringSchema,
|
|
153
|
-
locationId: optionalNonEmptyStringSchema,
|
|
154
|
-
observedAtMs: z.coerce.number(),
|
|
155
|
-
searchVector: z.string().optional(),
|
|
156
|
-
scope: z.enum(MEMORY_SCOPES),
|
|
157
|
-
scopeKey: z.string().min(1),
|
|
158
|
-
sourceKey: z.string().min(1),
|
|
159
|
-
sourcePlatform: z.enum(MEMORY_SOURCE_PLATFORMS),
|
|
160
|
-
subjectKey: optionalNonEmptyStringSchema,
|
|
161
|
-
subjectType: z.enum(MEMORY_SUBJECT_TYPES),
|
|
162
|
-
supersededAtMs: optionalNumberSchema,
|
|
163
|
-
supersededById: optionalStringSchema,
|
|
164
|
-
kind: z.enum(MEMORY_KINDS),
|
|
165
|
-
})
|
|
166
|
-
.strict()
|
|
167
|
-
.superRefine((row, ctx) => {
|
|
168
|
-
if (row.subjectType === "general") {
|
|
169
|
-
if (row.subjectKey !== undefined) {
|
|
170
|
-
ctx.addIssue({
|
|
171
|
-
code: "custom",
|
|
172
|
-
message: "General-subject memory rows must not have a subject key.",
|
|
173
|
-
path: ["subjectKey"],
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
if (row.subjectKey === undefined) {
|
|
179
|
-
ctx.addIssue({
|
|
180
|
-
code: "custom",
|
|
181
|
-
message: "User and conversation memory rows require a subject key.",
|
|
182
|
-
path: ["subjectKey"],
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
function storedMemorySource(
|
|
188
|
-
source: MemoryRuntimeContext["source"],
|
|
189
|
-
): MemorySourcePlatform {
|
|
190
|
-
switch (source.kind) {
|
|
191
|
-
case "slack":
|
|
192
|
-
case "local":
|
|
193
|
-
case "web":
|
|
194
|
-
return source.kind;
|
|
195
|
-
case "resource_event":
|
|
196
|
-
case "scheduled_task":
|
|
197
|
-
case "event_task":
|
|
198
|
-
case "plugin_dispatch":
|
|
199
|
-
case "agent_invocation":
|
|
200
|
-
throw new Error(`${source.kind} Source cannot own a Memory.`);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const memoryRecordSchema = z
|
|
205
|
-
.object({
|
|
206
|
-
archivedAtMs: numberSchema.optional(),
|
|
207
|
-
archiveReason: nonEmptyStringSchema.optional(),
|
|
208
|
-
content: memoryContentSchema,
|
|
209
|
-
createdAtMs: numberSchema,
|
|
210
|
-
expiresAtMs: numberSchema.optional(),
|
|
211
|
-
id: nonEmptyStringSchema,
|
|
212
|
-
observedAtMs: numberSchema,
|
|
213
|
-
scope: z.enum(MEMORY_SCOPES),
|
|
214
|
-
subjectType: z.enum(MEMORY_SUBJECT_TYPES),
|
|
215
|
-
supersededAtMs: numberSchema.optional(),
|
|
216
|
-
supersededById: nonEmptyStringSchema.optional(),
|
|
217
|
-
kind: z.enum(MEMORY_KINDS),
|
|
218
|
-
})
|
|
219
|
-
.strict();
|
|
220
|
-
const embeddingVectorSchema = z
|
|
221
|
-
.array(numberSchema)
|
|
222
|
-
.length(MEMORY_EMBEDDING_DIMENSIONS);
|
|
223
|
-
const embeddingResultSchema = z
|
|
224
|
-
.object({
|
|
225
|
-
costUsd: z.number().finite().nonnegative().optional(),
|
|
226
|
-
dimensions: z.literal(MEMORY_EMBEDDING_DIMENSIONS),
|
|
227
|
-
model: nonEmptyStringSchema,
|
|
228
|
-
provider: nonEmptyStringSchema,
|
|
229
|
-
vectors: z.array(embeddingVectorSchema),
|
|
230
|
-
})
|
|
231
|
-
.strict();
|
|
232
|
-
const memorySupersessionCandidateSchema = z
|
|
233
|
-
.object({
|
|
234
|
-
content: z.string().min(1),
|
|
235
|
-
id: z.string().min(1),
|
|
236
|
-
})
|
|
237
|
-
.strict();
|
|
238
|
-
const memorySupersessionCandidatesSchema = z
|
|
239
|
-
.array(memorySupersessionCandidateSchema)
|
|
240
|
-
.min(1)
|
|
241
|
-
.max(PREFERENCE_ADJUDICATION_CANDIDATE_LIMIT);
|
|
242
|
-
const supersededIdsSchema = z
|
|
243
|
-
.array(z.string().min(1))
|
|
244
|
-
.min(1)
|
|
245
|
-
.max(PREFERENCE_ADJUDICATION_CANDIDATE_LIMIT);
|
|
246
|
-
|
|
247
|
-
/** Validated preference comparison input supplied to a supersession decider. */
|
|
248
|
-
export const memorySupersessionInputSchema = z
|
|
249
|
-
.object({
|
|
250
|
-
candidate: z
|
|
251
|
-
.object({
|
|
252
|
-
content: z.string().min(1),
|
|
253
|
-
kind: z.literal("preference"),
|
|
254
|
-
})
|
|
255
|
-
.strict(),
|
|
256
|
-
existingMemories: memorySupersessionCandidatesSchema,
|
|
257
|
-
runtimeContext: memoryRuntimeContextSchema,
|
|
258
|
-
})
|
|
259
|
-
.strict();
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Validated preference decision whose referenced ids must come from the
|
|
263
|
-
* supplied existing memories.
|
|
264
|
-
*/
|
|
265
|
-
export const memorySupersessionDecisionSchema = z.discriminatedUnion(
|
|
266
|
-
"decision",
|
|
267
|
-
[
|
|
268
|
-
z
|
|
269
|
-
.object({
|
|
270
|
-
decision: z.literal("duplicate"),
|
|
271
|
-
duplicateId: z.string().min(1),
|
|
272
|
-
})
|
|
273
|
-
.strict(),
|
|
274
|
-
z
|
|
275
|
-
.object({
|
|
276
|
-
decision: z.literal("supersedes_old"),
|
|
277
|
-
supersededIds: supersededIdsSchema,
|
|
278
|
-
})
|
|
279
|
-
.strict(),
|
|
280
|
-
z
|
|
281
|
-
.object({
|
|
282
|
-
decision: z.enum(["distinct", "uncertain"]),
|
|
283
|
-
})
|
|
284
|
-
.strict(),
|
|
285
|
-
],
|
|
286
|
-
);
|
|
287
|
-
|
|
288
|
-
export type MemoryRecord = z.output<typeof memoryRecordSchema>;
|
|
289
|
-
export type CreateMemoryInput = z.output<typeof createMemoryInputSchema>;
|
|
290
|
-
|
|
291
|
-
/** Result of a memory write after idempotency checks. */
|
|
292
|
-
export interface CreateMemoryResult {
|
|
293
|
-
created: boolean;
|
|
294
|
-
/** True when this call found the memory previously written for the same input identity. */
|
|
295
|
-
idempotent?: true;
|
|
296
|
-
memory: MemoryRecord;
|
|
297
|
-
/** Memory ids made inactive by this write. */
|
|
298
|
-
supersededIds?: string[];
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
export type ListMemoriesInput = z.output<typeof listMemoriesInputSchema>;
|
|
302
|
-
|
|
303
|
-
export type SearchMemoriesInput = z.output<typeof searchMemoriesInputSchema>;
|
|
304
|
-
|
|
305
|
-
export type ArchiveMemoryInput = z.output<typeof archiveMemoryInputSchema>;
|
|
306
|
-
|
|
307
|
-
export type ArchiveExpiredMemoriesInput = z.output<
|
|
308
|
-
typeof archiveExpiredMemoriesInputSchema
|
|
309
|
-
>;
|
|
310
|
-
|
|
311
|
-
export interface ArchiveExpiredMemoriesResult {
|
|
312
|
-
archivedCount: number;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
export interface MemoryEmbeddingProvider {
|
|
316
|
-
/** Embed normalized memory text for derived vector retrieval. */
|
|
317
|
-
embedTexts(input: { texts: string[] }): Promise<{
|
|
318
|
-
costUsd?: number;
|
|
319
|
-
dimensions: number;
|
|
320
|
-
model: string;
|
|
321
|
-
provider: string;
|
|
322
|
-
vectors: number[][];
|
|
323
|
-
}>;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
export type MemorySupersessionInput = z.output<
|
|
327
|
-
typeof memorySupersessionInputSchema
|
|
328
|
-
>;
|
|
329
|
-
|
|
330
|
-
export type MemorySupersessionDecision = z.output<
|
|
331
|
-
typeof memorySupersessionDecisionSchema
|
|
332
|
-
>;
|
|
333
|
-
|
|
334
|
-
export interface MemorySupersessionDecider {
|
|
335
|
-
/** Classify a new preference against related active preferences. */
|
|
336
|
-
adjudicateSupersession(
|
|
337
|
-
input: MemorySupersessionInput,
|
|
338
|
-
): Promise<MemorySupersessionDecision> | MemorySupersessionDecision;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
export interface MemoryStoreOptions {
|
|
342
|
-
embedder?: MemoryEmbeddingProvider;
|
|
343
|
-
now?: () => number;
|
|
344
|
-
supersessionDecider?: MemorySupersessionDecider;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/** Context-bound storage operations for visible long-term memories. */
|
|
348
|
-
export interface MemoryStore {
|
|
349
|
-
/** Archive expired memories visible in the current runtime context. */
|
|
350
|
-
archiveExpiredMemories(
|
|
351
|
-
input?: ArchiveExpiredMemoriesInput,
|
|
352
|
-
): Promise<ArchiveExpiredMemoriesResult>;
|
|
353
|
-
/** Archive a visible memory in the current runtime context. */
|
|
354
|
-
archiveMemory(input: ArchiveMemoryInput): Promise<MemoryRecord>;
|
|
355
|
-
/** Store a memory about the current User. The Source sets access. */
|
|
356
|
-
createMemory(input: CreateMemoryInput): Promise<CreateMemoryResult>;
|
|
357
|
-
/** Store a memory about the current Conversation. The Source sets access. */
|
|
358
|
-
createConversationMemory(
|
|
359
|
-
input: CreateMemoryInput,
|
|
360
|
-
): Promise<CreateMemoryResult>;
|
|
361
|
-
/** List active memories visible in the current runtime context. */
|
|
362
|
-
listMemories(input: ListMemoriesInput): Promise<MemoryRecord[]>;
|
|
363
|
-
/**
|
|
364
|
-
* Retrieve a broad relevance-ranked candidate window for automatic recall.
|
|
365
|
-
* Prompt admission remains owned by the recall boundary.
|
|
366
|
-
*/
|
|
367
|
-
recallMemories(input: SearchMemoriesInput): Promise<MemoryRecord[]>;
|
|
368
|
-
/** Search active memories visible in the current runtime context. */
|
|
369
|
-
searchMemories(input: SearchMemoriesInput): Promise<MemoryRecord[]>;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function normalizeContent(content: string): string {
|
|
373
|
-
return content.replace(/\s+/g, " ").trim();
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function hashEmbeddedContent(content: string): string {
|
|
377
|
-
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function idempotencyAliasId(args: {
|
|
381
|
-
idempotencyKey: string;
|
|
382
|
-
scope: ResolvedMemoryScope;
|
|
383
|
-
targetId: string;
|
|
384
|
-
}): string {
|
|
385
|
-
return `alias:${createHash("sha256")
|
|
386
|
-
.update(args.scope.scope)
|
|
387
|
-
.update("\0")
|
|
388
|
-
.update(args.scope.scopeKey)
|
|
389
|
-
.update("\0")
|
|
390
|
-
.update(args.idempotencyKey)
|
|
391
|
-
.update("\0")
|
|
392
|
-
.update(args.targetId)
|
|
393
|
-
.digest("hex")}`;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function boundedLimit(value: number | undefined, fallback: number): number {
|
|
397
|
-
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
398
|
-
return fallback;
|
|
399
|
-
}
|
|
400
|
-
return Math.min(200, Math.max(1, Math.floor(value)));
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/** Build the stored key for the Source. */
|
|
404
|
-
function sourceKey(ctx: MemoryRuntimeContext): string {
|
|
405
|
-
const key = getSourceKey(ctx.source);
|
|
406
|
-
if (!key) {
|
|
407
|
-
throw new Error("Memory Source has no stable key.");
|
|
408
|
-
}
|
|
409
|
-
return key;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/** Parse one SQL row into the public memory projection. */
|
|
413
|
-
export function parseMemoryRow(row: unknown): MemoryRecord {
|
|
414
|
-
const parsed = memoryRowSchema.parse(row);
|
|
415
|
-
return memoryRecordSchema.parse({
|
|
416
|
-
id: parsed.id,
|
|
417
|
-
scope: parsed.scope,
|
|
418
|
-
kind: parsed.kind,
|
|
419
|
-
subjectType: parsed.subjectType,
|
|
420
|
-
content: parsed.content,
|
|
421
|
-
observedAtMs: parsed.observedAtMs,
|
|
422
|
-
createdAtMs: parsed.createdAtMs,
|
|
423
|
-
...(parsed.expiresAtMs !== undefined
|
|
424
|
-
? { expiresAtMs: parsed.expiresAtMs }
|
|
425
|
-
: undefined),
|
|
426
|
-
...(parsed.supersededAtMs !== undefined
|
|
427
|
-
? { supersededAtMs: parsed.supersededAtMs }
|
|
428
|
-
: undefined),
|
|
429
|
-
...(parsed.supersededById ? { supersededById: parsed.supersededById } : undefined),
|
|
430
|
-
...(parsed.archivedAtMs !== undefined
|
|
431
|
-
? { archivedAtMs: parsed.archivedAtMs }
|
|
432
|
-
: undefined),
|
|
433
|
-
...(parsed.archiveReason ? { archiveReason: parsed.archiveReason } : undefined),
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/** Build the scoped SQL predicate and ordered params for visible memory reads. */
|
|
438
|
-
function visibleScopePredicate(scopes: ResolvedMemoryScope[]): SQL | undefined {
|
|
439
|
-
if (scopes.length === 0) {
|
|
440
|
-
return undefined;
|
|
441
|
-
}
|
|
442
|
-
return or(
|
|
443
|
-
...scopes.map((scope) =>
|
|
444
|
-
and(
|
|
445
|
-
eq(juniorMemoryMemories.scope, scope.scope),
|
|
446
|
-
eq(juniorMemoryMemories.scopeKey, scope.scopeKey),
|
|
447
|
-
),
|
|
448
|
-
),
|
|
449
|
-
);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/** Build the active-row predicate for already-authorized memory scopes. */
|
|
453
|
-
export function activeVisiblePredicate(args: {
|
|
454
|
-
nowMs: number;
|
|
455
|
-
scopes: ResolvedMemoryScope[];
|
|
456
|
-
}): SQL | undefined {
|
|
457
|
-
const scopePredicate = visibleScopePredicate(args.scopes);
|
|
458
|
-
if (!scopePredicate) {
|
|
459
|
-
return undefined;
|
|
460
|
-
}
|
|
461
|
-
return and(
|
|
462
|
-
scopePredicate,
|
|
463
|
-
isNull(juniorMemoryMemories.archivedAtMs),
|
|
464
|
-
isNull(juniorMemoryMemories.supersededAtMs),
|
|
465
|
-
isNull(juniorMemoryMemories.supersededById),
|
|
466
|
-
or(
|
|
467
|
-
isNull(juniorMemoryMemories.expiresAtMs),
|
|
468
|
-
gt(juniorMemoryMemories.expiresAtMs, args.nowMs),
|
|
469
|
-
),
|
|
470
|
-
);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/** Resolve retry attempts for the same scoped write idempotency key. */
|
|
474
|
-
interface IdempotencyMatch {
|
|
475
|
-
memory: MemoryRecord;
|
|
476
|
-
outcome: "created" | "duplicate";
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
async function findByIdempotencyKey(args: {
|
|
480
|
-
db: MemoryDb;
|
|
481
|
-
idempotencyKey: string;
|
|
482
|
-
nowMs: number;
|
|
483
|
-
scope: ResolvedMemoryScope;
|
|
484
|
-
}): Promise<IdempotencyMatch | undefined> {
|
|
485
|
-
const activeRows = await args.db
|
|
486
|
-
.select()
|
|
487
|
-
.from(juniorMemoryMemories)
|
|
488
|
-
.where(
|
|
489
|
-
and(
|
|
490
|
-
eq(juniorMemoryMemories.scope, args.scope.scope),
|
|
491
|
-
eq(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
|
|
492
|
-
eq(juniorMemoryMemories.idempotencyKey, args.idempotencyKey),
|
|
493
|
-
isNull(juniorMemoryMemories.archivedAtMs),
|
|
494
|
-
isNull(juniorMemoryMemories.supersededAtMs),
|
|
495
|
-
isNull(juniorMemoryMemories.supersededById),
|
|
496
|
-
or(
|
|
497
|
-
isNull(juniorMemoryMemories.expiresAtMs),
|
|
498
|
-
gt(juniorMemoryMemories.expiresAtMs, args.nowMs),
|
|
499
|
-
),
|
|
500
|
-
),
|
|
501
|
-
)
|
|
502
|
-
.limit(1);
|
|
503
|
-
if (activeRows[0]) {
|
|
504
|
-
return { memory: parseMemoryRow(activeRows[0]), outcome: "created" };
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const aliasRows = await args.db
|
|
508
|
-
.select({ supersededById: juniorMemoryMemories.supersededById })
|
|
509
|
-
.from(juniorMemoryMemories)
|
|
510
|
-
.where(
|
|
511
|
-
and(
|
|
512
|
-
eq(juniorMemoryMemories.scope, args.scope.scope),
|
|
513
|
-
eq(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
|
|
514
|
-
eq(juniorMemoryMemories.idempotencyKey, args.idempotencyKey),
|
|
515
|
-
isNull(juniorMemoryMemories.archivedAtMs),
|
|
516
|
-
isNotNull(juniorMemoryMemories.supersededAtMs),
|
|
517
|
-
isNotNull(juniorMemoryMemories.supersededById),
|
|
518
|
-
or(
|
|
519
|
-
isNull(juniorMemoryMemories.expiresAtMs),
|
|
520
|
-
gt(juniorMemoryMemories.expiresAtMs, args.nowMs),
|
|
521
|
-
),
|
|
522
|
-
),
|
|
523
|
-
)
|
|
524
|
-
.orderBy(
|
|
525
|
-
desc(juniorMemoryMemories.createdAtMs),
|
|
526
|
-
asc(juniorMemoryMemories.id),
|
|
527
|
-
);
|
|
528
|
-
for (const alias of aliasRows) {
|
|
529
|
-
if (!alias.supersededById) {
|
|
530
|
-
continue;
|
|
531
|
-
}
|
|
532
|
-
const rows = await args.db
|
|
533
|
-
.select()
|
|
534
|
-
.from(juniorMemoryMemories)
|
|
535
|
-
.where(
|
|
536
|
-
and(
|
|
537
|
-
eq(juniorMemoryMemories.id, alias.supersededById),
|
|
538
|
-
eq(juniorMemoryMemories.scope, args.scope.scope),
|
|
539
|
-
eq(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
|
|
540
|
-
isNull(juniorMemoryMemories.archivedAtMs),
|
|
541
|
-
isNull(juniorMemoryMemories.supersededAtMs),
|
|
542
|
-
isNull(juniorMemoryMemories.supersededById),
|
|
543
|
-
or(
|
|
544
|
-
isNull(juniorMemoryMemories.expiresAtMs),
|
|
545
|
-
gt(juniorMemoryMemories.expiresAtMs, args.nowMs),
|
|
546
|
-
),
|
|
547
|
-
),
|
|
548
|
-
)
|
|
549
|
-
.limit(1);
|
|
550
|
-
if (rows[0]) {
|
|
551
|
-
return { memory: parseMemoryRow(rows[0]), outcome: "duplicate" };
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
return undefined;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* Archive a bounded batch of expired active rows and remove their derived vectors.
|
|
559
|
-
*/
|
|
560
|
-
export async function archiveExpiredMemoryBatch(args: {
|
|
561
|
-
db: MemoryDb;
|
|
562
|
-
idempotencyKey?: string;
|
|
563
|
-
limit?: number;
|
|
564
|
-
nowMs: number;
|
|
565
|
-
scopes: ResolvedMemoryScope[];
|
|
566
|
-
}): Promise<ArchiveExpiredMemoriesResult> {
|
|
567
|
-
const scopePredicate = visibleScopePredicate(args.scopes);
|
|
568
|
-
if (!scopePredicate) {
|
|
569
|
-
return { archivedCount: 0 };
|
|
570
|
-
}
|
|
571
|
-
const predicates: SQL[] = [
|
|
572
|
-
scopePredicate,
|
|
573
|
-
isNull(juniorMemoryMemories.archivedAtMs),
|
|
574
|
-
isNull(juniorMemoryMemories.supersededAtMs),
|
|
575
|
-
isNull(juniorMemoryMemories.supersededById),
|
|
576
|
-
lte(juniorMemoryMemories.expiresAtMs, args.nowMs),
|
|
577
|
-
];
|
|
578
|
-
if (args.idempotencyKey !== undefined) {
|
|
579
|
-
predicates.push(
|
|
580
|
-
eq(juniorMemoryMemories.idempotencyKey, args.idempotencyKey),
|
|
581
|
-
);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
const archivedIds = await args.db.transaction(async (tx) => {
|
|
585
|
-
const expired = await tx
|
|
586
|
-
.select({ id: juniorMemoryMemories.id })
|
|
587
|
-
.from(juniorMemoryMemories)
|
|
588
|
-
.where(and(...predicates))
|
|
589
|
-
.orderBy(
|
|
590
|
-
asc(juniorMemoryMemories.expiresAtMs),
|
|
591
|
-
asc(juniorMemoryMemories.id),
|
|
592
|
-
)
|
|
593
|
-
.limit(boundedLimit(args.limit, DEFAULT_EXPIRED_ARCHIVE_LIMIT));
|
|
594
|
-
const ids = expired.map((row) => row.id);
|
|
595
|
-
if (ids.length === 0) {
|
|
596
|
-
return [];
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
const archived = await tx
|
|
600
|
-
.update(juniorMemoryMemories)
|
|
601
|
-
.set({
|
|
602
|
-
archivedAtMs: args.nowMs,
|
|
603
|
-
archiveReason: "expired",
|
|
604
|
-
})
|
|
605
|
-
.where(and(inArray(juniorMemoryMemories.id, ids), ...predicates))
|
|
606
|
-
.returning({ id: juniorMemoryMemories.id });
|
|
607
|
-
const idsToClean = archived.map((row) => row.id);
|
|
608
|
-
if (idsToClean.length > 0) {
|
|
609
|
-
await tx
|
|
610
|
-
.delete(juniorMemoryEmbeddings)
|
|
611
|
-
.where(inArray(juniorMemoryEmbeddings.memoryId, idsToClean));
|
|
612
|
-
}
|
|
613
|
-
return idsToClean;
|
|
614
|
-
});
|
|
615
|
-
return { archivedCount: archivedIds.length };
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
function denseRanks<T>(
|
|
619
|
-
values: T[],
|
|
620
|
-
key: (value: T) => string | number,
|
|
621
|
-
): number[] {
|
|
622
|
-
let previous: string | number | undefined;
|
|
623
|
-
let rank = 0;
|
|
624
|
-
return values.map((value, index) => {
|
|
625
|
-
const current = key(value);
|
|
626
|
-
if (index === 0 || current !== previous) {
|
|
627
|
-
rank = index + 1;
|
|
628
|
-
previous = current;
|
|
629
|
-
}
|
|
630
|
-
return rank;
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
async function embedOne(
|
|
635
|
-
embedder: MemoryEmbeddingProvider,
|
|
636
|
-
text: string,
|
|
637
|
-
): Promise<MemoryEmbedding> {
|
|
638
|
-
const normalized = normalizeContent(text);
|
|
639
|
-
if (!normalized) {
|
|
640
|
-
throw new Error("Embedding text is required.");
|
|
641
|
-
}
|
|
642
|
-
const result = embeddingResultSchema.parse(
|
|
643
|
-
await embedder.embedTexts({ texts: [normalized] }),
|
|
644
|
-
);
|
|
645
|
-
if (result.vectors.length !== 1) {
|
|
646
|
-
throw new Error("Embedding provider returned an unexpected vector count.");
|
|
647
|
-
}
|
|
648
|
-
return {
|
|
649
|
-
model: result.model,
|
|
650
|
-
provider: result.provider,
|
|
651
|
-
vector: result.vectors[0],
|
|
652
|
-
};
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
/** Store the derived vector index; failures must not block memory persistence. */
|
|
656
|
-
async function storeEmbedding(args: {
|
|
657
|
-
content: string;
|
|
658
|
-
db: MemoryDb;
|
|
659
|
-
embedder: MemoryEmbeddingProvider | undefined;
|
|
660
|
-
embedding?: MemoryEmbedding;
|
|
661
|
-
memoryId: string;
|
|
662
|
-
nowMs: number;
|
|
663
|
-
}): Promise<void> {
|
|
664
|
-
if (!args.embedder && !args.embedding) {
|
|
665
|
-
return;
|
|
666
|
-
}
|
|
667
|
-
try {
|
|
668
|
-
const existing = await args.db
|
|
669
|
-
.select({ memoryId: juniorMemoryEmbeddings.memoryId })
|
|
670
|
-
.from(juniorMemoryEmbeddings)
|
|
671
|
-
.where(eq(juniorMemoryEmbeddings.memoryId, args.memoryId))
|
|
672
|
-
.limit(1);
|
|
673
|
-
if (existing[0]) {
|
|
674
|
-
return;
|
|
675
|
-
}
|
|
676
|
-
} catch {
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
let embedding: Awaited<ReturnType<typeof embedOne>>;
|
|
680
|
-
if (args.embedding) {
|
|
681
|
-
embedding = args.embedding;
|
|
682
|
-
} else {
|
|
683
|
-
const embedder = args.embedder;
|
|
684
|
-
if (!embedder) {
|
|
685
|
-
return;
|
|
686
|
-
}
|
|
687
|
-
try {
|
|
688
|
-
embedding = await embedOne(embedder, args.content);
|
|
689
|
-
} catch {
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
try {
|
|
694
|
-
await args.db
|
|
695
|
-
.insert(juniorMemoryEmbeddings)
|
|
696
|
-
.values({
|
|
697
|
-
contentHash: hashEmbeddedContent(args.content),
|
|
698
|
-
createdAtMs: args.nowMs,
|
|
699
|
-
dimensions: MEMORY_EMBEDDING_DIMENSIONS,
|
|
700
|
-
embedding: embedding.vector,
|
|
701
|
-
memoryId: args.memoryId,
|
|
702
|
-
metric: EMBEDDING_METRIC,
|
|
703
|
-
model: embedding.model,
|
|
704
|
-
provider: embedding.provider,
|
|
705
|
-
})
|
|
706
|
-
.onConflictDoNothing();
|
|
707
|
-
} catch {
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
function activeScopedSubjectPredicate(args: {
|
|
713
|
-
kind: MemoryRecord["kind"];
|
|
714
|
-
nowMs: number;
|
|
715
|
-
scope: ResolvedMemoryScope;
|
|
716
|
-
subject: ResolvedMemorySubject;
|
|
717
|
-
}): SQL {
|
|
718
|
-
const predicate = and(
|
|
719
|
-
eq(juniorMemoryMemories.scope, args.scope.scope),
|
|
720
|
-
eq(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
|
|
721
|
-
eq(juniorMemoryMemories.kind, args.kind),
|
|
722
|
-
eq(juniorMemoryMemories.subjectType, args.subject.subjectType),
|
|
723
|
-
eq(juniorMemoryMemories.subjectKey, args.subject.subjectKey),
|
|
724
|
-
isNull(juniorMemoryMemories.archivedAtMs),
|
|
725
|
-
isNull(juniorMemoryMemories.supersededAtMs),
|
|
726
|
-
isNull(juniorMemoryMemories.supersededById),
|
|
727
|
-
or(
|
|
728
|
-
isNull(juniorMemoryMemories.expiresAtMs),
|
|
729
|
-
gt(juniorMemoryMemories.expiresAtMs, args.nowMs),
|
|
730
|
-
),
|
|
731
|
-
);
|
|
732
|
-
if (!predicate) {
|
|
733
|
-
throw new Error("Memory duplicate predicate is empty.");
|
|
734
|
-
}
|
|
735
|
-
return predicate;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
async function findExactDuplicateMemory(args: {
|
|
739
|
-
content: string;
|
|
740
|
-
db: MemoryDb;
|
|
741
|
-
kind: MemoryRecord["kind"];
|
|
742
|
-
nowMs: number;
|
|
743
|
-
scope: ResolvedMemoryScope;
|
|
744
|
-
subject: ResolvedMemorySubject;
|
|
745
|
-
}): Promise<MemoryRecord | undefined> {
|
|
746
|
-
const rows = await args.db
|
|
747
|
-
.select()
|
|
748
|
-
.from(juniorMemoryMemories)
|
|
749
|
-
.where(
|
|
750
|
-
and(
|
|
751
|
-
activeScopedSubjectPredicate(args),
|
|
752
|
-
eq(juniorMemoryMemories.content, args.content),
|
|
753
|
-
),
|
|
754
|
-
)
|
|
755
|
-
.orderBy(
|
|
756
|
-
desc(juniorMemoryMemories.createdAtMs),
|
|
757
|
-
asc(juniorMemoryMemories.id),
|
|
758
|
-
)
|
|
759
|
-
.limit(1);
|
|
760
|
-
return rows[0] ? parseMemoryRow(rows[0]) : undefined;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
async function rememberDuplicateIdempotency(args: {
|
|
764
|
-
content: string;
|
|
765
|
-
db: MemoryDb;
|
|
766
|
-
duplicate: MemoryRecord;
|
|
767
|
-
idempotencyKey?: string;
|
|
768
|
-
nowMs: number;
|
|
769
|
-
runtimeContext: MemoryRuntimeContext;
|
|
770
|
-
scope: ResolvedMemoryScope;
|
|
771
|
-
subject: ResolvedMemorySubject;
|
|
772
|
-
}): Promise<void> {
|
|
773
|
-
if (args.idempotencyKey === undefined) {
|
|
774
|
-
return;
|
|
775
|
-
}
|
|
776
|
-
await args.db
|
|
777
|
-
.insert(juniorMemoryMemories)
|
|
778
|
-
.values({
|
|
779
|
-
content: args.content,
|
|
780
|
-
createdAtMs: args.nowMs,
|
|
781
|
-
expiresAtMs: args.duplicate.expiresAtMs,
|
|
782
|
-
id: idempotencyAliasId({
|
|
783
|
-
idempotencyKey: args.idempotencyKey,
|
|
784
|
-
scope: args.scope,
|
|
785
|
-
targetId: args.duplicate.id,
|
|
786
|
-
}),
|
|
787
|
-
idempotencyKey: args.idempotencyKey,
|
|
788
|
-
locationId: args.runtimeContext.locationId,
|
|
789
|
-
observedAtMs: args.nowMs,
|
|
790
|
-
scope: args.scope.scope,
|
|
791
|
-
scopeKey: args.scope.scopeKey,
|
|
792
|
-
sourceKey: sourceKey(args.runtimeContext),
|
|
793
|
-
sourcePlatform: storedMemorySource(args.runtimeContext.source),
|
|
794
|
-
subjectKey: args.subject.subjectKey,
|
|
795
|
-
subjectType: args.subject.subjectType,
|
|
796
|
-
supersededAtMs: args.nowMs,
|
|
797
|
-
supersededById: args.duplicate.id,
|
|
798
|
-
kind: args.duplicate.kind,
|
|
799
|
-
})
|
|
800
|
-
.onConflictDoNothing();
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
/** Select semantic preferences, then fill the window by recency for unembedded records. */
|
|
804
|
-
async function listPreferenceAdjudicationCandidates(args: {
|
|
805
|
-
db: MemoryDb;
|
|
806
|
-
embedding?: MemoryEmbedding;
|
|
807
|
-
nowMs: number;
|
|
808
|
-
scope: ResolvedMemoryScope;
|
|
809
|
-
subject: ResolvedMemorySubject;
|
|
810
|
-
}): Promise<MemoryRecord[]> {
|
|
811
|
-
const vectorCandidates = args.embedding
|
|
812
|
-
? await listVectorPreferenceAdjudicationCandidates({
|
|
813
|
-
db: args.db,
|
|
814
|
-
embedding: args.embedding,
|
|
815
|
-
nowMs: args.nowMs,
|
|
816
|
-
scope: args.scope,
|
|
817
|
-
subject: args.subject,
|
|
818
|
-
})
|
|
819
|
-
: [];
|
|
820
|
-
const recentCandidates = (
|
|
821
|
-
await args.db
|
|
822
|
-
.select()
|
|
823
|
-
.from(juniorMemoryMemories)
|
|
824
|
-
.where(
|
|
825
|
-
activeScopedSubjectPredicate({
|
|
826
|
-
...args,
|
|
827
|
-
kind: "preference",
|
|
828
|
-
}),
|
|
829
|
-
)
|
|
830
|
-
.orderBy(
|
|
831
|
-
desc(juniorMemoryMemories.createdAtMs),
|
|
832
|
-
asc(juniorMemoryMemories.id),
|
|
833
|
-
)
|
|
834
|
-
.limit(PREFERENCE_ADJUDICATION_CANDIDATE_LIMIT)
|
|
835
|
-
).map(parseMemoryRow);
|
|
836
|
-
return [
|
|
837
|
-
...new Map(
|
|
838
|
-
[...vectorCandidates, ...recentCandidates].map((memory) => [
|
|
839
|
-
memory.id,
|
|
840
|
-
memory,
|
|
841
|
-
]),
|
|
842
|
-
).values(),
|
|
843
|
-
].slice(0, PREFERENCE_ADJUDICATION_CANDIDATE_LIMIT);
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
async function listVectorPreferenceAdjudicationCandidates(args: {
|
|
847
|
-
db: MemoryDb;
|
|
848
|
-
embedding: MemoryEmbedding;
|
|
849
|
-
nowMs: number;
|
|
850
|
-
scope: ResolvedMemoryScope;
|
|
851
|
-
subject: ResolvedMemorySubject;
|
|
852
|
-
}): Promise<MemoryRecord[]> {
|
|
853
|
-
const distance = cosineDistance(
|
|
854
|
-
juniorMemoryEmbeddings.embedding,
|
|
855
|
-
args.embedding.vector,
|
|
856
|
-
);
|
|
857
|
-
const rows = await args.db
|
|
858
|
-
.select({
|
|
859
|
-
contentHash: juniorMemoryEmbeddings.contentHash,
|
|
860
|
-
distance,
|
|
861
|
-
memory: juniorMemoryMemories,
|
|
862
|
-
})
|
|
863
|
-
.from(juniorMemoryMemories)
|
|
864
|
-
.innerJoin(
|
|
865
|
-
juniorMemoryEmbeddings,
|
|
866
|
-
eq(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id),
|
|
867
|
-
)
|
|
868
|
-
.where(
|
|
869
|
-
and(
|
|
870
|
-
activeScopedSubjectPredicate({ ...args, kind: "preference" }),
|
|
871
|
-
eq(juniorMemoryEmbeddings.provider, args.embedding.provider),
|
|
872
|
-
eq(juniorMemoryEmbeddings.model, args.embedding.model),
|
|
873
|
-
eq(juniorMemoryEmbeddings.dimensions, MEMORY_EMBEDDING_DIMENSIONS),
|
|
874
|
-
eq(juniorMemoryEmbeddings.metric, EMBEDDING_METRIC),
|
|
875
|
-
),
|
|
876
|
-
)
|
|
877
|
-
.orderBy(
|
|
878
|
-
distance,
|
|
879
|
-
desc(juniorMemoryMemories.createdAtMs),
|
|
880
|
-
asc(juniorMemoryMemories.id),
|
|
881
|
-
)
|
|
882
|
-
.limit(PREFERENCE_ADJUDICATION_VECTOR_LIMIT);
|
|
883
|
-
return rows.flatMap((row) => {
|
|
884
|
-
if (hashEmbeddedContent(row.memory.content) !== row.contentHash) {
|
|
885
|
-
return [];
|
|
886
|
-
}
|
|
887
|
-
return [parseMemoryRow(row.memory)];
|
|
888
|
-
});
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
type PreferenceAdjudicationResult =
|
|
892
|
-
| { decision: "create" }
|
|
893
|
-
| { decision: "duplicate"; memory: MemoryRecord }
|
|
894
|
-
| { decision: "supersede"; ids: [string, ...string[]] };
|
|
895
|
-
|
|
896
|
-
/**
|
|
897
|
-
* Normalize a preference decision to known duplicate or supersession targets.
|
|
898
|
-
* Uncertainty, invalid ids, and model failure leave existing memories active.
|
|
899
|
-
*/
|
|
900
|
-
async function adjudicatePreferenceCandidate(args: {
|
|
901
|
-
candidates: MemoryRecord[];
|
|
902
|
-
content: string;
|
|
903
|
-
decider: MemorySupersessionDecider;
|
|
904
|
-
runtimeContext: MemoryRuntimeContext;
|
|
905
|
-
}): Promise<PreferenceAdjudicationResult> {
|
|
906
|
-
const [firstCandidate, ...remainingCandidates] = args.candidates;
|
|
907
|
-
if (!firstCandidate) {
|
|
908
|
-
return { decision: "create" };
|
|
909
|
-
}
|
|
910
|
-
const existingMemories = [
|
|
911
|
-
{ content: firstCandidate.content, id: firstCandidate.id },
|
|
912
|
-
...remainingCandidates.map((memory) => ({
|
|
913
|
-
content: memory.content,
|
|
914
|
-
id: memory.id,
|
|
915
|
-
})),
|
|
916
|
-
];
|
|
917
|
-
const candidateIds = new Set(args.candidates.map((memory) => memory.id));
|
|
918
|
-
try {
|
|
919
|
-
const decision = await args.decider.adjudicateSupersession({
|
|
920
|
-
candidate: {
|
|
921
|
-
content: args.content,
|
|
922
|
-
kind: "preference",
|
|
923
|
-
},
|
|
924
|
-
existingMemories,
|
|
925
|
-
runtimeContext: args.runtimeContext,
|
|
926
|
-
});
|
|
927
|
-
if (decision.decision === "duplicate") {
|
|
928
|
-
const memory = args.candidates.find(
|
|
929
|
-
(candidate) => candidate.id === decision.duplicateId,
|
|
930
|
-
);
|
|
931
|
-
return memory
|
|
932
|
-
? { decision: "duplicate", memory }
|
|
933
|
-
: { decision: "create" };
|
|
934
|
-
}
|
|
935
|
-
if (decision.decision === "supersedes_old") {
|
|
936
|
-
const ids = decision.supersededIds.filter((id) => candidateIds.has(id));
|
|
937
|
-
const [firstId, ...remainingIds] = ids;
|
|
938
|
-
return firstId
|
|
939
|
-
? { decision: "supersede", ids: [firstId, ...remainingIds] }
|
|
940
|
-
: { decision: "create" };
|
|
941
|
-
}
|
|
942
|
-
return { decision: "create" };
|
|
943
|
-
} catch {
|
|
944
|
-
return { decision: "create" };
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
/** List active records for the runtime-derived visible scopes. */
|
|
949
|
-
async function listVisibleMemories(args: {
|
|
950
|
-
db: MemoryDb;
|
|
951
|
-
limit?: number;
|
|
952
|
-
nowMs: number;
|
|
953
|
-
scopes: ResolvedMemoryScope[];
|
|
954
|
-
}): Promise<MemoryRecord[]> {
|
|
955
|
-
const predicate = activeVisiblePredicate(args);
|
|
956
|
-
if (!predicate) {
|
|
957
|
-
return [];
|
|
958
|
-
}
|
|
959
|
-
const limit = boundedLimit(args.limit, DEFAULT_LIST_LIMIT);
|
|
960
|
-
const rows = await args.db
|
|
961
|
-
.select()
|
|
962
|
-
.from(juniorMemoryMemories)
|
|
963
|
-
.where(predicate)
|
|
964
|
-
.orderBy(
|
|
965
|
-
desc(juniorMemoryMemories.createdAtMs),
|
|
966
|
-
asc(juniorMemoryMemories.id),
|
|
967
|
-
)
|
|
968
|
-
.limit(limit);
|
|
969
|
-
return rows.map(parseMemoryRow);
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
function normalizeRetrievalQuery(query: string): string {
|
|
973
|
-
const normalized = query.replace(/\s+/g, " ").trim();
|
|
974
|
-
if (normalized.length <= MAX_RETRIEVAL_QUERY_CHARS) {
|
|
975
|
-
return normalized;
|
|
976
|
-
}
|
|
977
|
-
return normalized.slice(0, MAX_RETRIEVAL_QUERY_CHARS).trimEnd();
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
function retrievalLegLimit(limit: number, overfetch: number): number {
|
|
981
|
-
const requested = Math.max(1, limit);
|
|
982
|
-
const withOverfetch = requested * Math.max(1, overfetch);
|
|
983
|
-
// Never return fewer candidates than the caller asked for. A hard overfetch
|
|
984
|
-
// cap below `limit` under-fills when one modality is empty or both overlap.
|
|
985
|
-
return Math.min(
|
|
986
|
-
MAX_RETRIEVAL_LEG_CANDIDATES,
|
|
987
|
-
Math.max(requested, withOverfetch),
|
|
988
|
-
);
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
/** Search a bounded active candidate set with PostgreSQL full-text ranking. */
|
|
992
|
-
async function searchVisibleLexicalMemories(args: {
|
|
993
|
-
db: MemoryDb;
|
|
994
|
-
limit: number;
|
|
995
|
-
nowMs: number;
|
|
996
|
-
query: string;
|
|
997
|
-
scopes: ResolvedMemoryScope[];
|
|
998
|
-
}): Promise<MemoryMatch[]> {
|
|
999
|
-
const predicate = activeVisiblePredicate(args);
|
|
1000
|
-
if (!predicate) {
|
|
1001
|
-
return [];
|
|
1002
|
-
}
|
|
1003
|
-
const query = normalizeRetrievalQuery(args.query);
|
|
1004
|
-
if (!query) {
|
|
1005
|
-
return [];
|
|
1006
|
-
}
|
|
1007
|
-
const queryVector = sql`to_tsvector('english', ${query})`;
|
|
1008
|
-
const tsquery = sql`(
|
|
1009
|
-
SELECT COALESCE(
|
|
1010
|
-
string_agg(quote_literal(term), ' | ')::tsquery,
|
|
1011
|
-
''::tsquery
|
|
1012
|
-
)
|
|
1013
|
-
FROM unnest(tsvector_to_array(${queryVector})) AS query_terms(term)
|
|
1014
|
-
)`;
|
|
1015
|
-
// GIN filter first, then rank only a bounded recent match window.
|
|
1016
|
-
const candidateLimit = Math.min(
|
|
1017
|
-
MAX_LEXICAL_RANK_CANDIDATES,
|
|
1018
|
-
args.limit * LEXICAL_RANK_WINDOW_MULTIPLIER,
|
|
1019
|
-
);
|
|
1020
|
-
const candidates = args.db
|
|
1021
|
-
.select()
|
|
1022
|
-
.from(juniorMemoryMemories)
|
|
1023
|
-
.where(
|
|
1024
|
-
and(predicate, sql`${juniorMemoryMemories.searchVector} @@ ${tsquery}`),
|
|
1025
|
-
)
|
|
1026
|
-
.orderBy(
|
|
1027
|
-
desc(juniorMemoryMemories.observedAtMs),
|
|
1028
|
-
asc(juniorMemoryMemories.id),
|
|
1029
|
-
)
|
|
1030
|
-
.limit(candidateLimit)
|
|
1031
|
-
.as("lexical_candidates");
|
|
1032
|
-
const textRank = sql<number>`ts_rank_cd(${candidates.searchVector}, ${tsquery})`;
|
|
1033
|
-
const rows = await args.db
|
|
1034
|
-
.select({
|
|
1035
|
-
memory: {
|
|
1036
|
-
archiveReason: candidates.archiveReason,
|
|
1037
|
-
archivedAtMs: candidates.archivedAtMs,
|
|
1038
|
-
content: candidates.content,
|
|
1039
|
-
createdAtMs: candidates.createdAtMs,
|
|
1040
|
-
expiresAtMs: candidates.expiresAtMs,
|
|
1041
|
-
id: candidates.id,
|
|
1042
|
-
idempotencyKey: candidates.idempotencyKey,
|
|
1043
|
-
kind: candidates.kind,
|
|
1044
|
-
observedAtMs: candidates.observedAtMs,
|
|
1045
|
-
scope: candidates.scope,
|
|
1046
|
-
scopeKey: candidates.scopeKey,
|
|
1047
|
-
searchVector: candidates.searchVector,
|
|
1048
|
-
sourceKey: candidates.sourceKey,
|
|
1049
|
-
sourcePlatform: candidates.sourcePlatform,
|
|
1050
|
-
subjectKey: candidates.subjectKey,
|
|
1051
|
-
subjectType: candidates.subjectType,
|
|
1052
|
-
supersededAtMs: candidates.supersededAtMs,
|
|
1053
|
-
supersededById: candidates.supersededById,
|
|
1054
|
-
},
|
|
1055
|
-
textRank,
|
|
1056
|
-
})
|
|
1057
|
-
.from(candidates)
|
|
1058
|
-
.orderBy(desc(textRank), desc(candidates.observedAtMs), asc(candidates.id))
|
|
1059
|
-
.limit(args.limit);
|
|
1060
|
-
const ranks = denseRanks(rows, (row) => Number(row.textRank));
|
|
1061
|
-
return rows.map((row, index) => ({
|
|
1062
|
-
lexical: { rank: ranks[index] },
|
|
1063
|
-
memory: parseMemoryRow(row.memory),
|
|
1064
|
-
}));
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
/** Search active visible records with pgvector cosine distance. */
|
|
1068
|
-
async function searchVisibleVectorMemories(args: {
|
|
1069
|
-
db: MemoryDb;
|
|
1070
|
-
embedding: MemoryEmbedding;
|
|
1071
|
-
limit: number;
|
|
1072
|
-
maxDistance?: number;
|
|
1073
|
-
nowMs: number;
|
|
1074
|
-
scopes: ResolvedMemoryScope[];
|
|
1075
|
-
}): Promise<MemoryMatch[]> {
|
|
1076
|
-
const predicate = activeVisiblePredicate(args);
|
|
1077
|
-
if (!predicate) {
|
|
1078
|
-
return [];
|
|
1079
|
-
}
|
|
1080
|
-
const embedding = args.embedding;
|
|
1081
|
-
const distance = cosineDistance(
|
|
1082
|
-
juniorMemoryEmbeddings.embedding,
|
|
1083
|
-
embedding.vector,
|
|
1084
|
-
);
|
|
1085
|
-
// Push distance cutoff into SQL so recall does not overfetch weak neighbors.
|
|
1086
|
-
const distancePredicate =
|
|
1087
|
-
args.maxDistance === undefined
|
|
1088
|
-
? undefined
|
|
1089
|
-
: sql`${distance} <= ${args.maxDistance}`;
|
|
1090
|
-
const rows = await args.db
|
|
1091
|
-
.select({
|
|
1092
|
-
contentHash: juniorMemoryEmbeddings.contentHash,
|
|
1093
|
-
distance,
|
|
1094
|
-
memory: juniorMemoryMemories,
|
|
1095
|
-
})
|
|
1096
|
-
.from(juniorMemoryMemories)
|
|
1097
|
-
.innerJoin(
|
|
1098
|
-
juniorMemoryEmbeddings,
|
|
1099
|
-
eq(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id),
|
|
1100
|
-
)
|
|
1101
|
-
.where(
|
|
1102
|
-
and(
|
|
1103
|
-
predicate,
|
|
1104
|
-
eq(juniorMemoryEmbeddings.provider, embedding.provider),
|
|
1105
|
-
eq(juniorMemoryEmbeddings.model, embedding.model),
|
|
1106
|
-
eq(juniorMemoryEmbeddings.dimensions, MEMORY_EMBEDDING_DIMENSIONS),
|
|
1107
|
-
eq(juniorMemoryEmbeddings.metric, EMBEDDING_METRIC),
|
|
1108
|
-
...(distancePredicate ? [distancePredicate] : []),
|
|
1109
|
-
),
|
|
1110
|
-
)
|
|
1111
|
-
.orderBy(
|
|
1112
|
-
distance,
|
|
1113
|
-
desc(juniorMemoryMemories.createdAtMs),
|
|
1114
|
-
asc(juniorMemoryMemories.id),
|
|
1115
|
-
)
|
|
1116
|
-
.limit(args.limit);
|
|
1117
|
-
const ranks = denseRanks(rows, (row) => Number(row.distance));
|
|
1118
|
-
return rows.flatMap((row, index) => {
|
|
1119
|
-
const distanceValue = Number(row.distance);
|
|
1120
|
-
if (
|
|
1121
|
-
row.distance === null ||
|
|
1122
|
-
!Number.isFinite(distanceValue) ||
|
|
1123
|
-
hashEmbeddedContent(row.memory.content) !== row.contentHash
|
|
1124
|
-
) {
|
|
1125
|
-
return [];
|
|
1126
|
-
}
|
|
1127
|
-
return [
|
|
1128
|
-
{
|
|
1129
|
-
memory: parseMemoryRow(row.memory),
|
|
1130
|
-
vector: {
|
|
1131
|
-
rank: ranks[index],
|
|
1132
|
-
},
|
|
1133
|
-
},
|
|
1134
|
-
];
|
|
1135
|
-
});
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
/** Create a context-bound SQL-backed store for explicit memory operations. */
|
|
1139
|
-
export function createMemoryStore(
|
|
1140
|
-
db: MemoryDb,
|
|
1141
|
-
context: MemoryRuntimeContext,
|
|
1142
|
-
options: MemoryStoreOptions = {},
|
|
1143
|
-
): MemoryStore {
|
|
1144
|
-
const runtimeContext = memoryRuntimeContextSchema.parse(context);
|
|
1145
|
-
const parsedOptions = memoryStoreOptionsSchema.parse({ now: options.now });
|
|
1146
|
-
const embedder = options.embedder;
|
|
1147
|
-
const supersessionDecider = options.supersessionDecider;
|
|
1148
|
-
const getNowMs = parsedOptions.now ?? Date.now;
|
|
1149
|
-
|
|
1150
|
-
async function archiveExpiredVisibleMemories(
|
|
1151
|
-
input: ArchiveExpiredMemoriesInput | undefined,
|
|
1152
|
-
nowMs: number,
|
|
1153
|
-
): Promise<ArchiveExpiredMemoriesResult> {
|
|
1154
|
-
input = archiveExpiredMemoriesInputSchema.parse(input ?? {});
|
|
1155
|
-
return await archiveExpiredMemoryBatch({
|
|
1156
|
-
db,
|
|
1157
|
-
limit: input.limit,
|
|
1158
|
-
nowMs,
|
|
1159
|
-
scopes: deriveVisibleMemoryScopes(runtimeContext),
|
|
1160
|
-
});
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
async function reuseDuplicateMemory(args: {
|
|
1164
|
-
content: string;
|
|
1165
|
-
duplicate: MemoryRecord;
|
|
1166
|
-
idempotencyKey?: string;
|
|
1167
|
-
nowMs: number;
|
|
1168
|
-
scope: ResolvedMemoryScope;
|
|
1169
|
-
subject: ResolvedMemorySubject;
|
|
1170
|
-
}): Promise<CreateMemoryResult> {
|
|
1171
|
-
await rememberDuplicateIdempotency({
|
|
1172
|
-
...args,
|
|
1173
|
-
db,
|
|
1174
|
-
runtimeContext,
|
|
1175
|
-
});
|
|
1176
|
-
await storeEmbedding({
|
|
1177
|
-
content: args.duplicate.content,
|
|
1178
|
-
db,
|
|
1179
|
-
embedder,
|
|
1180
|
-
memoryId: args.duplicate.id,
|
|
1181
|
-
nowMs: args.nowMs,
|
|
1182
|
-
});
|
|
1183
|
-
return { created: false, memory: args.duplicate };
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
/** Persist a memory under the plugin-derived scope and subject. */
|
|
1187
|
-
async function createScopedMemory(
|
|
1188
|
-
rawInput: CreateMemoryInput,
|
|
1189
|
-
subjectType: ResolvedMemorySubject["subjectType"],
|
|
1190
|
-
): Promise<CreateMemoryResult> {
|
|
1191
|
-
const input = createMemoryInputSchema.parse(rawInput);
|
|
1192
|
-
const nowMs = getNowMs();
|
|
1193
|
-
const content = normalizeContent(input.content);
|
|
1194
|
-
const scope = deriveMemoryScope(runtimeContext);
|
|
1195
|
-
const subject = deriveMemorySubject(runtimeContext, subjectType);
|
|
1196
|
-
if (content.length > MAX_MEMORY_CONTENT_CHARS) {
|
|
1197
|
-
throw new Error("Memory content exceeds the maximum length.");
|
|
1198
|
-
}
|
|
1199
|
-
await archiveExpiredMemoryBatch({
|
|
1200
|
-
db,
|
|
1201
|
-
nowMs,
|
|
1202
|
-
scopes: [scope],
|
|
1203
|
-
});
|
|
1204
|
-
await archiveExpiredMemoryBatch({
|
|
1205
|
-
db,
|
|
1206
|
-
idempotencyKey: input.idempotencyKey,
|
|
1207
|
-
limit: 1,
|
|
1208
|
-
nowMs,
|
|
1209
|
-
scopes: [scope],
|
|
1210
|
-
});
|
|
1211
|
-
if (input.idempotencyKey !== undefined) {
|
|
1212
|
-
const idempotent = await findByIdempotencyKey({
|
|
1213
|
-
db,
|
|
1214
|
-
idempotencyKey: input.idempotencyKey,
|
|
1215
|
-
nowMs,
|
|
1216
|
-
scope,
|
|
1217
|
-
});
|
|
1218
|
-
if (idempotent) {
|
|
1219
|
-
await storeEmbedding({
|
|
1220
|
-
content: idempotent.memory.content,
|
|
1221
|
-
db,
|
|
1222
|
-
embedder,
|
|
1223
|
-
memoryId: idempotent.memory.id,
|
|
1224
|
-
nowMs,
|
|
1225
|
-
});
|
|
1226
|
-
return idempotent.outcome === "created"
|
|
1227
|
-
? { created: false, idempotent: true, memory: idempotent.memory }
|
|
1228
|
-
: { created: false, memory: idempotent.memory };
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
const exactDuplicate = await findExactDuplicateMemory({
|
|
1233
|
-
content,
|
|
1234
|
-
db,
|
|
1235
|
-
kind: input.kind,
|
|
1236
|
-
nowMs,
|
|
1237
|
-
scope,
|
|
1238
|
-
subject,
|
|
1239
|
-
});
|
|
1240
|
-
if (exactDuplicate) {
|
|
1241
|
-
return await reuseDuplicateMemory({
|
|
1242
|
-
content,
|
|
1243
|
-
duplicate: exactDuplicate,
|
|
1244
|
-
idempotencyKey: input.idempotencyKey,
|
|
1245
|
-
nowMs,
|
|
1246
|
-
scope,
|
|
1247
|
-
subject,
|
|
1248
|
-
});
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
let candidateEmbedding: MemoryEmbedding | undefined;
|
|
1252
|
-
if (embedder) {
|
|
1253
|
-
try {
|
|
1254
|
-
candidateEmbedding = await embedOne(embedder, content);
|
|
1255
|
-
} catch {
|
|
1256
|
-
candidateEmbedding = undefined;
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
let supersededIds: string[] = [];
|
|
1260
|
-
if (
|
|
1261
|
-
subjectType === "user" &&
|
|
1262
|
-
input.kind === "preference" &&
|
|
1263
|
-
supersessionDecider &&
|
|
1264
|
-
(input.expiresAtMs === undefined || input.expiresAtMs > nowMs)
|
|
1265
|
-
) {
|
|
1266
|
-
const preferenceCandidates = await listPreferenceAdjudicationCandidates({
|
|
1267
|
-
db,
|
|
1268
|
-
...(candidateEmbedding ? { embedding: candidateEmbedding } : undefined),
|
|
1269
|
-
nowMs,
|
|
1270
|
-
scope,
|
|
1271
|
-
subject,
|
|
1272
|
-
});
|
|
1273
|
-
const adjudication = await adjudicatePreferenceCandidate({
|
|
1274
|
-
candidates: preferenceCandidates,
|
|
1275
|
-
content,
|
|
1276
|
-
decider: supersessionDecider,
|
|
1277
|
-
runtimeContext,
|
|
1278
|
-
});
|
|
1279
|
-
if (adjudication.decision === "duplicate") {
|
|
1280
|
-
return await reuseDuplicateMemory({
|
|
1281
|
-
content,
|
|
1282
|
-
duplicate: adjudication.memory,
|
|
1283
|
-
idempotencyKey: input.idempotencyKey,
|
|
1284
|
-
nowMs,
|
|
1285
|
-
scope,
|
|
1286
|
-
subject,
|
|
1287
|
-
});
|
|
1288
|
-
}
|
|
1289
|
-
if (adjudication.decision === "supersede") {
|
|
1290
|
-
supersededIds = adjudication.ids;
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
const id = randomUUID();
|
|
1295
|
-
const write = await db.transaction(async (tx) => {
|
|
1296
|
-
const inserted = await tx
|
|
1297
|
-
.insert(juniorMemoryMemories)
|
|
1298
|
-
.values({
|
|
1299
|
-
content,
|
|
1300
|
-
createdAtMs: nowMs,
|
|
1301
|
-
expiresAtMs: input.expiresAtMs,
|
|
1302
|
-
id,
|
|
1303
|
-
idempotencyKey: input.idempotencyKey,
|
|
1304
|
-
locationId: runtimeContext.locationId,
|
|
1305
|
-
observedAtMs: nowMs,
|
|
1306
|
-
scope: scope.scope,
|
|
1307
|
-
scopeKey: scope.scopeKey,
|
|
1308
|
-
sourceKey: sourceKey(runtimeContext),
|
|
1309
|
-
sourcePlatform: storedMemorySource(runtimeContext.source),
|
|
1310
|
-
subjectKey: subject.subjectKey,
|
|
1311
|
-
subjectType: subject.subjectType,
|
|
1312
|
-
kind: input.kind,
|
|
1313
|
-
})
|
|
1314
|
-
.onConflictDoNothing({
|
|
1315
|
-
target: [
|
|
1316
|
-
juniorMemoryMemories.scope,
|
|
1317
|
-
juniorMemoryMemories.scopeKey,
|
|
1318
|
-
juniorMemoryMemories.idempotencyKey,
|
|
1319
|
-
],
|
|
1320
|
-
where: sql`${juniorMemoryMemories.idempotencyKey} IS NOT NULL AND ${juniorMemoryMemories.archivedAtMs} IS NULL AND ${juniorMemoryMemories.supersededAtMs} IS NULL AND ${juniorMemoryMemories.supersededById} IS NULL`,
|
|
1321
|
-
})
|
|
1322
|
-
.returning();
|
|
1323
|
-
const insertedMemory = inserted[0];
|
|
1324
|
-
if (!insertedMemory || supersededIds.length === 0) {
|
|
1325
|
-
return { inserted, supersededIds: [] };
|
|
1326
|
-
}
|
|
1327
|
-
const superseded = await tx
|
|
1328
|
-
.update(juniorMemoryMemories)
|
|
1329
|
-
.set({
|
|
1330
|
-
supersededAtMs: nowMs,
|
|
1331
|
-
supersededById: insertedMemory.id,
|
|
1332
|
-
})
|
|
1333
|
-
.where(
|
|
1334
|
-
and(
|
|
1335
|
-
inArray(juniorMemoryMemories.id, supersededIds),
|
|
1336
|
-
activeScopedSubjectPredicate({
|
|
1337
|
-
kind: input.kind,
|
|
1338
|
-
nowMs,
|
|
1339
|
-
scope,
|
|
1340
|
-
subject,
|
|
1341
|
-
}),
|
|
1342
|
-
),
|
|
1343
|
-
)
|
|
1344
|
-
.returning({ id: juniorMemoryMemories.id });
|
|
1345
|
-
const idsToClean = superseded.map((row) => row.id);
|
|
1346
|
-
if (idsToClean.length > 0) {
|
|
1347
|
-
await tx
|
|
1348
|
-
.delete(juniorMemoryEmbeddings)
|
|
1349
|
-
.where(inArray(juniorMemoryEmbeddings.memoryId, idsToClean));
|
|
1350
|
-
}
|
|
1351
|
-
return { inserted, supersededIds: idsToClean };
|
|
1352
|
-
});
|
|
1353
|
-
if (write.inserted[0]) {
|
|
1354
|
-
const memory = parseMemoryRow(write.inserted[0]);
|
|
1355
|
-
await storeEmbedding({
|
|
1356
|
-
content: memory.content,
|
|
1357
|
-
db,
|
|
1358
|
-
embedder,
|
|
1359
|
-
embedding: candidateEmbedding,
|
|
1360
|
-
memoryId: memory.id,
|
|
1361
|
-
nowMs,
|
|
1362
|
-
});
|
|
1363
|
-
return {
|
|
1364
|
-
created: true,
|
|
1365
|
-
memory,
|
|
1366
|
-
...(write.supersededIds.length > 0
|
|
1367
|
-
? { supersededIds: write.supersededIds }
|
|
1368
|
-
: undefined),
|
|
1369
|
-
};
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
const idempotent = await findByIdempotencyKey({
|
|
1373
|
-
db,
|
|
1374
|
-
idempotencyKey: input.idempotencyKey,
|
|
1375
|
-
nowMs,
|
|
1376
|
-
scope,
|
|
1377
|
-
});
|
|
1378
|
-
if (!idempotent) {
|
|
1379
|
-
throw new Error("Memory idempotency conflict did not resolve.");
|
|
1380
|
-
}
|
|
1381
|
-
await storeEmbedding({
|
|
1382
|
-
content: idempotent.memory.content,
|
|
1383
|
-
db,
|
|
1384
|
-
embedder,
|
|
1385
|
-
memoryId: idempotent.memory.id,
|
|
1386
|
-
nowMs,
|
|
1387
|
-
});
|
|
1388
|
-
return idempotent.outcome === "created"
|
|
1389
|
-
? { created: false, idempotent: true, memory: idempotent.memory }
|
|
1390
|
-
: { created: false, memory: idempotent.memory };
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
/**
|
|
1394
|
-
* Hybrid retrieval for both automatic recall and explicit search.
|
|
1395
|
-
*
|
|
1396
|
-
* Keep both legs parallel and fuse ranks with RRF. Never skip lexical when
|
|
1397
|
-
* vectors already hit: that drops exact/token memories and serializes the
|
|
1398
|
-
* miss path. Each leg is a hard-capped top-k probe so Postgres work stays
|
|
1399
|
-
* bounded even on broad queries.
|
|
1400
|
-
*
|
|
1401
|
-
* Automatic recall also searches private memory by itself. This keeps newer
|
|
1402
|
-
* public memory with common words from hiding older private memory.
|
|
1403
|
-
*/
|
|
1404
|
-
async function retrieveVisibleMemories(
|
|
1405
|
-
rawInput: SearchMemoriesInput,
|
|
1406
|
-
vectorMaxDistance: number | undefined,
|
|
1407
|
-
): Promise<MemoryRecord[]> {
|
|
1408
|
-
const input = searchMemoriesInputSchema.parse(rawInput);
|
|
1409
|
-
const nowMs = getNowMs();
|
|
1410
|
-
const scopes = deriveVisibleMemoryScopes(runtimeContext);
|
|
1411
|
-
await archiveExpiredMemoryBatch({
|
|
1412
|
-
db,
|
|
1413
|
-
nowMs,
|
|
1414
|
-
scopes,
|
|
1415
|
-
});
|
|
1416
|
-
const limit = boundedLimit(input.limit, DEFAULT_SEARCH_LIMIT);
|
|
1417
|
-
const overfetch =
|
|
1418
|
-
vectorMaxDistance === undefined
|
|
1419
|
-
? SEARCH_RETRIEVAL_OVERFETCH
|
|
1420
|
-
: RECALL_RETRIEVAL_OVERFETCH;
|
|
1421
|
-
const candidateLimit = retrievalLegLimit(limit, overfetch);
|
|
1422
|
-
const privateScopes = scopes.filter((scope) => scope.scope === "private");
|
|
1423
|
-
// Search private memory by itself during recall so public results cannot
|
|
1424
|
-
// fill both search windows.
|
|
1425
|
-
const probePrivate =
|
|
1426
|
-
vectorMaxDistance !== undefined && privateScopes.length > 0;
|
|
1427
|
-
const query = normalizeRetrievalQuery(input.query);
|
|
1428
|
-
let queryEmbedding: MemoryEmbedding | undefined;
|
|
1429
|
-
if (embedder && query) {
|
|
1430
|
-
try {
|
|
1431
|
-
queryEmbedding = await embedOne(embedder, query);
|
|
1432
|
-
} catch {
|
|
1433
|
-
queryEmbedding = undefined;
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
const emptyMatches = Promise.resolve([] as MemoryMatch[]);
|
|
1437
|
-
const lexicalArgs = {
|
|
1438
|
-
db,
|
|
1439
|
-
limit: candidateLimit,
|
|
1440
|
-
nowMs,
|
|
1441
|
-
query: input.query,
|
|
1442
|
-
};
|
|
1443
|
-
// Always run both legs in parallel. Conditional lexical skip is unsafe:
|
|
1444
|
-
// one in-threshold vector distractor can hide a stronger lexical hit.
|
|
1445
|
-
// Embed once up front; vector probes only run when that embedding exists.
|
|
1446
|
-
const matches = await Promise.all([
|
|
1447
|
-
queryEmbedding
|
|
1448
|
-
? searchVisibleVectorMemories({
|
|
1449
|
-
db,
|
|
1450
|
-
embedding: queryEmbedding,
|
|
1451
|
-
limit: candidateLimit,
|
|
1452
|
-
...(vectorMaxDistance !== undefined
|
|
1453
|
-
? { maxDistance: vectorMaxDistance }
|
|
1454
|
-
: undefined),
|
|
1455
|
-
nowMs,
|
|
1456
|
-
scopes,
|
|
1457
|
-
})
|
|
1458
|
-
: emptyMatches,
|
|
1459
|
-
searchVisibleLexicalMemories({
|
|
1460
|
-
...lexicalArgs,
|
|
1461
|
-
scopes,
|
|
1462
|
-
}),
|
|
1463
|
-
queryEmbedding && probePrivate
|
|
1464
|
-
? searchVisibleVectorMemories({
|
|
1465
|
-
db,
|
|
1466
|
-
embedding: queryEmbedding,
|
|
1467
|
-
limit: candidateLimit,
|
|
1468
|
-
maxDistance: vectorMaxDistance,
|
|
1469
|
-
nowMs,
|
|
1470
|
-
scopes: privateScopes,
|
|
1471
|
-
})
|
|
1472
|
-
: emptyMatches,
|
|
1473
|
-
probePrivate
|
|
1474
|
-
? searchVisibleLexicalMemories({
|
|
1475
|
-
...lexicalArgs,
|
|
1476
|
-
scopes: privateScopes,
|
|
1477
|
-
})
|
|
1478
|
-
: emptyMatches,
|
|
1479
|
-
]);
|
|
1480
|
-
return rankMemoryMatches(matches.flat(), {
|
|
1481
|
-
nowMs,
|
|
1482
|
-
// Slight lexical preference protects exact ids/names/timezones on ties.
|
|
1483
|
-
...(vectorMaxDistance === undefined
|
|
1484
|
-
? undefined
|
|
1485
|
-
: { lexicalWeight: 1, vectorWeight: 0.85 }),
|
|
1486
|
-
})
|
|
1487
|
-
.slice(0, limit)
|
|
1488
|
-
.map(({ memory }) => memory);
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
return {
|
|
1492
|
-
async archiveExpiredMemories(input) {
|
|
1493
|
-
return await archiveExpiredVisibleMemories(input, getNowMs());
|
|
1494
|
-
},
|
|
1495
|
-
|
|
1496
|
-
async createMemory(input) {
|
|
1497
|
-
return await createScopedMemory(input, "user");
|
|
1498
|
-
},
|
|
1499
|
-
|
|
1500
|
-
async createConversationMemory(input) {
|
|
1501
|
-
return await createScopedMemory(input, "conversation");
|
|
1502
|
-
},
|
|
1503
|
-
|
|
1504
|
-
async listMemories(input) {
|
|
1505
|
-
input = listMemoriesInputSchema.parse(input);
|
|
1506
|
-
const nowMs = getNowMs();
|
|
1507
|
-
const scopes = deriveVisibleMemoryScopes(runtimeContext);
|
|
1508
|
-
await archiveExpiredMemoryBatch({
|
|
1509
|
-
db,
|
|
1510
|
-
nowMs,
|
|
1511
|
-
scopes,
|
|
1512
|
-
});
|
|
1513
|
-
return await listVisibleMemories({
|
|
1514
|
-
db,
|
|
1515
|
-
limit: input.limit,
|
|
1516
|
-
nowMs,
|
|
1517
|
-
scopes,
|
|
1518
|
-
});
|
|
1519
|
-
},
|
|
1520
|
-
|
|
1521
|
-
async recallMemories(input) {
|
|
1522
|
-
return await retrieveVisibleMemories(input, RECALL_MAX_VECTOR_DISTANCE);
|
|
1523
|
-
},
|
|
1524
|
-
|
|
1525
|
-
async searchMemories(input) {
|
|
1526
|
-
return await retrieveVisibleMemories(input, undefined);
|
|
1527
|
-
},
|
|
1528
|
-
|
|
1529
|
-
async archiveMemory(input) {
|
|
1530
|
-
input = archiveMemoryInputSchema.parse(input);
|
|
1531
|
-
const nowMs = getNowMs();
|
|
1532
|
-
// Public memory is shared and has no single user owner.
|
|
1533
|
-
const scopes = deriveVisibleMemoryScopes(runtimeContext).filter(
|
|
1534
|
-
(scope) => scope.scope === "private",
|
|
1535
|
-
);
|
|
1536
|
-
const predicate = activeVisiblePredicate({ nowMs, scopes });
|
|
1537
|
-
const idPrefix = input.id.trim();
|
|
1538
|
-
if (!idPrefix) {
|
|
1539
|
-
throw new Error("Memory id is required.");
|
|
1540
|
-
}
|
|
1541
|
-
const rows = predicate
|
|
1542
|
-
? await db
|
|
1543
|
-
.select()
|
|
1544
|
-
.from(juniorMemoryMemories)
|
|
1545
|
-
.where(
|
|
1546
|
-
and(
|
|
1547
|
-
predicate,
|
|
1548
|
-
or(
|
|
1549
|
-
eq(juniorMemoryMemories.id, idPrefix),
|
|
1550
|
-
like(juniorMemoryMemories.id, `${idPrefix}%`),
|
|
1551
|
-
),
|
|
1552
|
-
),
|
|
1553
|
-
)
|
|
1554
|
-
.orderBy(asc(juniorMemoryMemories.id))
|
|
1555
|
-
.limit(2)
|
|
1556
|
-
: [];
|
|
1557
|
-
if (rows.length === 0) {
|
|
1558
|
-
throw new Error("Memory was not found in the current context.");
|
|
1559
|
-
}
|
|
1560
|
-
if (rows.length > 1) {
|
|
1561
|
-
throw new Error("Memory id prefix is ambiguous.");
|
|
1562
|
-
}
|
|
1563
|
-
const memory = parseMemoryRow(rows[0]);
|
|
1564
|
-
const updated = await db
|
|
1565
|
-
.update(juniorMemoryMemories)
|
|
1566
|
-
.set({
|
|
1567
|
-
archivedAtMs: nowMs,
|
|
1568
|
-
archiveReason: input.reason ?? "user_removed",
|
|
1569
|
-
})
|
|
1570
|
-
.where(eq(juniorMemoryMemories.id, memory.id))
|
|
1571
|
-
.returning();
|
|
1572
|
-
await db
|
|
1573
|
-
.delete(juniorMemoryEmbeddings)
|
|
1574
|
-
.where(eq(juniorMemoryEmbeddings.memoryId, memory.id));
|
|
1575
|
-
return parseMemoryRow(updated[0]);
|
|
1576
|
-
},
|
|
1577
|
-
};
|
|
1578
|
-
}
|