@sentry/junior-memory 0.179.0 → 0.181.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 +25 -24
- package/dist/agent.d.ts +4 -0
- package/dist/api.d.ts +3 -4
- package/dist/db/schema.d.ts +19 -2
- package/dist/events.d.ts +3 -3
- package/dist/index.js +527 -706
- package/dist/index.js.map +1 -1
- package/dist/process-session.d.ts +3 -4
- package/dist/ranking.d.ts +0 -2
- package/dist/recall.d.ts +9 -2
- package/dist/scope.d.ts +11 -15
- package/dist/store.d.ts +6 -7
- package/dist/tools.d.ts +8 -1
- package/dist/types.d.ts +4 -2
- package/dist/user-pages.d.ts +1 -1
- package/dist/viewer.d.ts +85 -0
- package/migrations/0009_faithful_whizzer.sql +114 -0
- package/migrations/meta/0009_snapshot.json +411 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +4 -3
- package/src/agent.ts +9 -11
- package/src/api.ts +36 -27
- package/src/db/schema.ts +3 -1
- package/src/events.ts +30 -8
- package/src/operational-report.ts +20 -20
- package/src/plugin.ts +19 -7
- package/src/process-session.ts +23 -37
- package/src/ranking.ts +11 -28
- package/src/recall.ts +16 -6
- package/src/scope.ts +34 -135
- package/src/store.ts +43 -100
- package/src/tools.ts +37 -21
- package/src/types.ts +5 -2
- package/src/user-pages.ts +6 -8
- package/src/viewer.ts +393 -0
- package/dist/personal-store.d.ts +0 -92
- package/dist/personal.d.ts +0 -31
- package/src/personal-store.ts +0 -421
- package/src/personal.ts +0 -140
package/src/tools.ts
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
type PluginToolOutput,
|
|
8
8
|
type Source,
|
|
9
9
|
type Actor,
|
|
10
|
+
type Identity,
|
|
11
|
+
type User,
|
|
10
12
|
pluginToolOutputSchema,
|
|
11
13
|
} from "@sentry/junior-plugin-api";
|
|
12
14
|
import { z } from "zod";
|
|
@@ -43,8 +45,8 @@ const KNOWN_TOOL_INPUT_ERROR_MESSAGES = new Set([
|
|
|
43
45
|
"Memory id is required.",
|
|
44
46
|
"Memory was not found in the current context.",
|
|
45
47
|
"Memory id prefix is ambiguous.",
|
|
46
|
-
"
|
|
47
|
-
"User
|
|
48
|
+
"Private memory requires a User.",
|
|
49
|
+
"User memory requires a User.",
|
|
48
50
|
]);
|
|
49
51
|
|
|
50
52
|
/** Runtime-owned context used to bind memory tools to visible scopes. */
|
|
@@ -53,8 +55,12 @@ export interface MemoryToolContext {
|
|
|
53
55
|
conversationId?: string;
|
|
54
56
|
db: MemoryDb;
|
|
55
57
|
embedder?: MemoryEmbeddingProvider;
|
|
58
|
+
locationId?: string;
|
|
56
59
|
actor?: Actor;
|
|
57
60
|
source: Source;
|
|
61
|
+
users: {
|
|
62
|
+
resolveActor(): Promise<{ identity: Identity; user?: User } | undefined>;
|
|
63
|
+
};
|
|
58
64
|
userText?: string;
|
|
59
65
|
}
|
|
60
66
|
|
|
@@ -79,27 +85,31 @@ function asToolInputError(error: unknown): never {
|
|
|
79
85
|
throw error;
|
|
80
86
|
}
|
|
81
87
|
|
|
82
|
-
function memoryRuntimeContext(
|
|
88
|
+
async function memoryRuntimeContext(
|
|
83
89
|
context: MemoryToolContext,
|
|
84
|
-
): MemoryRuntimeContext {
|
|
90
|
+
): Promise<MemoryRuntimeContext> {
|
|
91
|
+
const actorUser = (await context.users.resolveActor())?.user;
|
|
85
92
|
return memoryRuntimeContextSchema.parse({
|
|
86
93
|
...(context.conversationId
|
|
87
94
|
? { conversationId: context.conversationId }
|
|
88
|
-
:
|
|
89
|
-
...(context.actor ? { actor: context.actor } :
|
|
95
|
+
: undefined),
|
|
96
|
+
...(context.actor ? { actor: context.actor } : undefined),
|
|
97
|
+
...(context.locationId ? { locationId: context.locationId } : undefined),
|
|
90
98
|
source: context.source,
|
|
99
|
+
...(actorUser ? { userId: actorUser.id } : undefined),
|
|
91
100
|
});
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
function memoryStore(
|
|
95
104
|
context: MemoryToolContext,
|
|
105
|
+
runtimeContext: MemoryRuntimeContext,
|
|
96
106
|
options: { supersessionDecider?: MemorySupersessionDecider } = {},
|
|
97
107
|
) {
|
|
98
|
-
return createMemoryStore(context.db,
|
|
108
|
+
return createMemoryStore(context.db, runtimeContext, {
|
|
99
109
|
embedder: context.embedder,
|
|
100
110
|
...(options.supersessionDecider
|
|
101
111
|
? { supersessionDecider: options.supersessionDecider }
|
|
102
|
-
:
|
|
112
|
+
: undefined),
|
|
103
113
|
});
|
|
104
114
|
}
|
|
105
115
|
|
|
@@ -237,7 +247,7 @@ const createMemoryInputSchema = z
|
|
|
237
247
|
.min(1)
|
|
238
248
|
.max(MAX_TOOL_CONTENT_CHARS)
|
|
239
249
|
.describe(
|
|
240
|
-
"Self-contained
|
|
250
|
+
"Self-contained memory candidate. Include the subject in natural language when it matters; do not rely on surrounding chat context.",
|
|
241
251
|
),
|
|
242
252
|
expires_at: z
|
|
243
253
|
.string()
|
|
@@ -355,7 +365,7 @@ function createInput(
|
|
|
355
365
|
kind: input.kind,
|
|
356
366
|
...(input.expiresAtMs !== undefined
|
|
357
367
|
? { expiresAtMs: input.expiresAtMs }
|
|
358
|
-
:
|
|
368
|
+
: undefined),
|
|
359
369
|
} satisfies CreateMemoryInput;
|
|
360
370
|
}
|
|
361
371
|
|
|
@@ -375,7 +385,7 @@ function compactMemory(memory: MemoryRecord): MemoryToolProjection {
|
|
|
375
385
|
observedAtMs: memory.observedAtMs,
|
|
376
386
|
...(memory.expiresAtMs !== undefined
|
|
377
387
|
? { expiresAtMs: memory.expiresAtMs }
|
|
378
|
-
:
|
|
388
|
+
: undefined),
|
|
379
389
|
});
|
|
380
390
|
}
|
|
381
391
|
|
|
@@ -400,7 +410,7 @@ export function createMemoryCreateTool(context: MemoryCreateToolContext) {
|
|
|
400
410
|
readOnlyHint: false,
|
|
401
411
|
},
|
|
402
412
|
description:
|
|
403
|
-
"Explicit memory-write tool. Use only when the latest user message directly asks Junior to remember, store, save, or forget-and-replace a
|
|
413
|
+
"Explicit memory-write tool. Use only when the latest user message directly asks Junior to remember, store, save, or forget-and-replace a fact. Do not use for ordinary statements like 'I prefer X', 'I use Y', or 'X goes before Y' unless the user also asks you to remember/store/save it; passive memory learning handles those after the visible reply. Pass one self-contained natural-language candidate preserving the user's explicit memory intent. Do not ask the user to rephrase ordinary first-person facts, and do not rewrite them into display-name or third-person wording. Do not include secrets, private personal details, medical/legal/financial/sensitive facts, or another person's personal preference, opinion, habit, identity, relationship, workflow, or private life. Junior sets access, Location, Source, and subject. The memory agent rewrites the content and sets the memory kind.",
|
|
404
414
|
executionMode: "sequential",
|
|
405
415
|
inputSchema: createMemoryInputSchema,
|
|
406
416
|
outputSchema: memoryCreateOutputSchema,
|
|
@@ -408,8 +418,8 @@ export function createMemoryCreateTool(context: MemoryCreateToolContext) {
|
|
|
408
418
|
const parsedInput = parseMemoryToolInput(createMemoryInputSchema, input);
|
|
409
419
|
const toolCallId = requireToolCallId(options.toolCallId);
|
|
410
420
|
const requestedExpiresAtMs = parseExpiresAt(parsedInput.expires_at);
|
|
411
|
-
const runtimeContext = memoryRuntimeContext(context);
|
|
412
|
-
const store = memoryStore(context, {
|
|
421
|
+
const runtimeContext = await memoryRuntimeContext(context);
|
|
422
|
+
const store = memoryStore(context, runtimeContext, {
|
|
413
423
|
supersessionDecider: context.supersessionDecider,
|
|
414
424
|
});
|
|
415
425
|
const review = await (async () => {
|
|
@@ -420,7 +430,7 @@ export function createMemoryCreateTool(context: MemoryCreateToolContext) {
|
|
|
420
430
|
content: requireMemoryContent(parsedInput.content),
|
|
421
431
|
...(requestedExpiresAtMs !== undefined
|
|
422
432
|
? { expiresAtMs: requestedExpiresAtMs }
|
|
423
|
-
:
|
|
433
|
+
: undefined),
|
|
424
434
|
runtimeContext,
|
|
425
435
|
...(context.userText?.trim()
|
|
426
436
|
? {
|
|
@@ -428,7 +438,7 @@ export function createMemoryCreateTool(context: MemoryCreateToolContext) {
|
|
|
428
438
|
currentUserText: context.userText.trim(),
|
|
429
439
|
},
|
|
430
440
|
}
|
|
431
|
-
:
|
|
441
|
+
: undefined),
|
|
432
442
|
}),
|
|
433
443
|
),
|
|
434
444
|
);
|
|
@@ -492,15 +502,16 @@ export function createMemoryRemoveTool(context: MemoryToolContext) {
|
|
|
492
502
|
readOnlyHint: false,
|
|
493
503
|
},
|
|
494
504
|
description:
|
|
495
|
-
"Forget one memory
|
|
505
|
+
"Forget one private memory owned by the current User. Public memories are read-only. Use only ids or short id prefixes returned by listMemories or searchMemories. Never remove memories by hidden Actor, provider, scope, or subject ids.",
|
|
496
506
|
executionMode: "sequential",
|
|
497
507
|
inputSchema: removeMemoryInputSchema,
|
|
498
508
|
outputSchema: memorySingleOutputSchema,
|
|
499
509
|
execute: async (input) => {
|
|
500
510
|
const parsedInput = parseMemoryToolInput(removeMemoryInputSchema, input);
|
|
511
|
+
const runtimeContext = await memoryRuntimeContext(context);
|
|
501
512
|
const memory = await (async () => {
|
|
502
513
|
try {
|
|
503
|
-
return await memoryStore(context).archiveMemory({
|
|
514
|
+
return await memoryStore(context, runtimeContext).archiveMemory({
|
|
504
515
|
id: parsedInput.id,
|
|
505
516
|
reason: "tool_removed",
|
|
506
517
|
});
|
|
@@ -530,7 +541,8 @@ export function createMemoryListTool(context: MemoryToolContext) {
|
|
|
530
541
|
outputSchema: memoryManyOutputSchema,
|
|
531
542
|
execute: async (input) => {
|
|
532
543
|
const parsedInput = parseMemoryToolInput(listMemoriesInputSchema, input);
|
|
533
|
-
const
|
|
544
|
+
const runtimeContext = await memoryRuntimeContext(context);
|
|
545
|
+
const memories = await memoryStore(context, runtimeContext).listMemories({
|
|
534
546
|
limit: boundedLimit(parsedInput.limit, DEFAULT_RESULT_LIMIT),
|
|
535
547
|
});
|
|
536
548
|
return memoryToolResult("listMemories", {
|
|
@@ -544,7 +556,7 @@ export function createMemoryListTool(context: MemoryToolContext) {
|
|
|
544
556
|
export function createMemorySearchTool(context: MemoryToolContext) {
|
|
545
557
|
return definePluginTool({
|
|
546
558
|
description:
|
|
547
|
-
"Search active memories visible in the current context. Use when the model needs targeted memory recall.
|
|
559
|
+
"Search active memories visible in the current context. Use when the model needs targeted memory recall. Public memories are visible everywhere. Private memories belong to the current User.",
|
|
548
560
|
annotations: {
|
|
549
561
|
destructiveHint: false,
|
|
550
562
|
idempotentHint: true,
|
|
@@ -558,7 +570,11 @@ export function createMemorySearchTool(context: MemoryToolContext) {
|
|
|
558
570
|
searchMemoriesInputSchema,
|
|
559
571
|
input,
|
|
560
572
|
);
|
|
561
|
-
const
|
|
573
|
+
const runtimeContext = await memoryRuntimeContext(context);
|
|
574
|
+
const memories = await memoryStore(
|
|
575
|
+
context,
|
|
576
|
+
runtimeContext,
|
|
577
|
+
).searchMemories({
|
|
562
578
|
query: parsedInput.query,
|
|
563
579
|
limit: boundedLimit(parsedInput.limit, DEFAULT_SEARCH_LIMIT),
|
|
564
580
|
});
|
package/src/types.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
|
|
4
4
|
export const MEMORY_KINDS = ["preference", "procedure", "knowledge"] as const;
|
|
5
5
|
|
|
6
|
-
export const MEMORY_SCOPES = ["
|
|
6
|
+
export const MEMORY_SCOPES = ["private", "public"] as const;
|
|
7
7
|
export const MEMORY_SUBJECT_TYPES = [
|
|
8
8
|
"user",
|
|
9
9
|
"conversation",
|
|
@@ -22,12 +22,15 @@ export type MemoryEmbeddingMetric = (typeof MEMORY_EMBEDDING_METRICS)[number];
|
|
|
22
22
|
|
|
23
23
|
const nonEmptyStringSchema = z.string().min(1);
|
|
24
24
|
|
|
25
|
-
/**
|
|
25
|
+
/** Host data used to set memory access, subject, and source. */
|
|
26
26
|
export const memoryRuntimeContextSchema = z
|
|
27
27
|
.object({
|
|
28
28
|
conversationId: nonEmptyStringSchema.optional(),
|
|
29
|
+
locationId: nonEmptyStringSchema.optional(),
|
|
29
30
|
actor: actorSchema.optional(),
|
|
30
31
|
source: sourceSchema,
|
|
32
|
+
/** User linked to the active Actor. */
|
|
33
|
+
userId: nonEmptyStringSchema.optional(),
|
|
31
34
|
})
|
|
32
35
|
.strict();
|
|
33
36
|
|
package/src/user-pages.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Render memory in Junior's User page format. */
|
|
2
2
|
import type { PluginUserPageDefinition } from "@sentry/junior-plugin-api";
|
|
3
|
-
import {
|
|
4
|
-
import type { MemoryVisibility, PersonalMemoryRecord } from "./personal-store";
|
|
3
|
+
import { listMemories, type MemoryVisibility, type MemoryView } from "./viewer";
|
|
5
4
|
import type { MemoryDb } from "./store";
|
|
6
5
|
|
|
7
6
|
function titleCase(value: string): string {
|
|
@@ -16,7 +15,7 @@ function rememberedDate(createdAtMs: number): string {
|
|
|
16
15
|
}).format(new Date(createdAtMs));
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
function originLabel(origin:
|
|
18
|
+
function originLabel(origin: MemoryView["origin"]): string {
|
|
20
19
|
if (origin === "automatic") return "Automatic";
|
|
21
20
|
if (origin === "explicit") return "Explicit";
|
|
22
21
|
return "Other";
|
|
@@ -50,17 +49,16 @@ export function createMemoryUserPage(): PluginUserPageDefinition {
|
|
|
50
49
|
description:
|
|
51
50
|
"Personal and public memories Junior can use across conversations.",
|
|
52
51
|
async read(ctx, input) {
|
|
53
|
-
const
|
|
54
|
-
const page = await memories.list({
|
|
52
|
+
const page = await listMemories(ctx.db as MemoryDb, ctx.viewer.id, {
|
|
55
53
|
cursor: input.cursor,
|
|
56
54
|
...pageFilter(input.filter),
|
|
57
55
|
limit: input.limit,
|
|
58
|
-
...(input.query ? { query: input.query } :
|
|
56
|
+
...(input.query ? { query: input.query } : undefined),
|
|
59
57
|
});
|
|
60
58
|
return {
|
|
61
59
|
type: "list",
|
|
62
60
|
emptyText: pageEmptyText(input),
|
|
63
|
-
...(page.nextCursor ? { nextCursor: page.nextCursor } :
|
|
61
|
+
...(page.nextCursor ? { nextCursor: page.nextCursor } : undefined),
|
|
64
62
|
searchPlaceholder: "Search memories",
|
|
65
63
|
records: page.memories.map((memory) => ({
|
|
66
64
|
actions:
|
package/src/viewer.ts
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory access for an authenticated User.
|
|
3
|
+
*
|
|
4
|
+
* Every User can read public memory and private memory that they own.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
and,
|
|
8
|
+
asc,
|
|
9
|
+
desc,
|
|
10
|
+
eq,
|
|
11
|
+
gt,
|
|
12
|
+
ilike,
|
|
13
|
+
isNull,
|
|
14
|
+
like,
|
|
15
|
+
lt,
|
|
16
|
+
or,
|
|
17
|
+
sql,
|
|
18
|
+
} from "drizzle-orm";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import { juniorMemoryEmbeddings, juniorMemoryMemories } from "./db/schema";
|
|
21
|
+
import { publicMemoryScope } from "./scope";
|
|
22
|
+
import { parseMemoryRow, type MemoryDb, type MemoryRecord } from "./store";
|
|
23
|
+
import { MEMORY_KINDS, type MemorySourcePlatform } from "./types";
|
|
24
|
+
|
|
25
|
+
const DAY_MS = 24 * 60 * 60 * 1_000;
|
|
26
|
+
const nonEmptyStringSchema = z.string().min(1);
|
|
27
|
+
const memoryVisibilitySchema = z.enum(["private", "public"]);
|
|
28
|
+
const cursorSchema = z
|
|
29
|
+
.object({
|
|
30
|
+
createdAtMs: z.number().finite(),
|
|
31
|
+
id: nonEmptyStringSchema,
|
|
32
|
+
kind: z.enum(MEMORY_KINDS).optional(),
|
|
33
|
+
origin: z.enum(["automatic", "explicit"]).optional(),
|
|
34
|
+
query: z.string().max(200).optional(),
|
|
35
|
+
version: z.literal(1),
|
|
36
|
+
visibility: memoryVisibilitySchema.optional(),
|
|
37
|
+
})
|
|
38
|
+
.strict();
|
|
39
|
+
const pageInputSchema = z
|
|
40
|
+
.object({
|
|
41
|
+
cursor: z.string().min(1).max(1_000).optional(),
|
|
42
|
+
kind: z.enum(MEMORY_KINDS).optional(),
|
|
43
|
+
limit: z.number().int().min(1).max(50),
|
|
44
|
+
origin: z.enum(["automatic", "explicit"]).optional(),
|
|
45
|
+
query: z.string().max(200).optional(),
|
|
46
|
+
visibility: memoryVisibilitySchema.optional(),
|
|
47
|
+
})
|
|
48
|
+
.strict();
|
|
49
|
+
const timelineDaysSchema = z.number().int().min(1).max(365);
|
|
50
|
+
|
|
51
|
+
/** Access label returned by dashboard and REST memory views. */
|
|
52
|
+
export type MemoryVisibility = z.output<typeof memoryVisibilitySchema>;
|
|
53
|
+
|
|
54
|
+
/** Memory fields returned to an authenticated User. */
|
|
55
|
+
export type MemoryView = MemoryRecord & {
|
|
56
|
+
origin: "automatic" | "explicit" | "other";
|
|
57
|
+
sourcePlatform: MemorySourcePlatform;
|
|
58
|
+
visibility: MemoryVisibility;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
interface MemoryPage {
|
|
62
|
+
memories: MemoryView[];
|
|
63
|
+
nextCursor?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type MemoryPageInput = z.output<typeof pageInputSchema>;
|
|
67
|
+
|
|
68
|
+
/** Expected error for a malformed or mismatched page cursor. */
|
|
69
|
+
export class InvalidMemoryCursorError extends Error {
|
|
70
|
+
constructor() {
|
|
71
|
+
super("Memory cursor is invalid.");
|
|
72
|
+
this.name = "InvalidMemoryCursorError";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Expected error when the current User cannot access a memory. */
|
|
77
|
+
export class MemoryNotFoundError extends Error {
|
|
78
|
+
constructor() {
|
|
79
|
+
super("Memory was not found for this user.");
|
|
80
|
+
this.name = "MemoryNotFoundError";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function publicScopePredicate() {
|
|
85
|
+
return and(
|
|
86
|
+
eq(juniorMemoryMemories.scope, publicMemoryScope.scope),
|
|
87
|
+
eq(juniorMemoryMemories.scopeKey, publicMemoryScope.scopeKey),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function privateScopePredicate(userId: string) {
|
|
92
|
+
return and(
|
|
93
|
+
eq(juniorMemoryMemories.scope, "private"),
|
|
94
|
+
eq(juniorMemoryMemories.scopeKey, userId),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function visibleScopePredicate(userId: string, visibility?: MemoryVisibility) {
|
|
99
|
+
if (visibility === "public") return publicScopePredicate();
|
|
100
|
+
if (visibility === "private") return privateScopePredicate(userId);
|
|
101
|
+
return or(publicScopePredicate(), privateScopePredicate(userId));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function activeMemoryPredicate(
|
|
105
|
+
userId: string,
|
|
106
|
+
nowMs: number,
|
|
107
|
+
visibility?: MemoryVisibility,
|
|
108
|
+
) {
|
|
109
|
+
return and(
|
|
110
|
+
visibleScopePredicate(userId, visibility),
|
|
111
|
+
isNull(juniorMemoryMemories.archivedAtMs),
|
|
112
|
+
isNull(juniorMemoryMemories.supersededAtMs),
|
|
113
|
+
isNull(juniorMemoryMemories.supersededById),
|
|
114
|
+
or(
|
|
115
|
+
isNull(juniorMemoryMemories.expiresAtMs),
|
|
116
|
+
gt(juniorMemoryMemories.expiresAtMs, nowMs),
|
|
117
|
+
),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function utcDate(ms: number): string {
|
|
122
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function searchTerms(query: string): string[] {
|
|
126
|
+
return [
|
|
127
|
+
...new Set(
|
|
128
|
+
query
|
|
129
|
+
.toLowerCase()
|
|
130
|
+
.split(/[^a-z0-9_'-]+/)
|
|
131
|
+
.map((term) => term.trim())
|
|
132
|
+
.filter((term) => term.length >= 2),
|
|
133
|
+
),
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function memoryOrigin(idempotencyKey: string | null): MemoryView["origin"] {
|
|
138
|
+
if (idempotencyKey?.startsWith("session:")) return "automatic";
|
|
139
|
+
if (idempotencyKey?.startsWith("tool:")) return "explicit";
|
|
140
|
+
return "other";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function toMemoryView(
|
|
144
|
+
row: typeof juniorMemoryMemories.$inferSelect,
|
|
145
|
+
): MemoryView {
|
|
146
|
+
const memory = parseMemoryRow(row);
|
|
147
|
+
return {
|
|
148
|
+
...memory,
|
|
149
|
+
origin: memoryOrigin(row.idempotencyKey),
|
|
150
|
+
sourcePlatform: row.sourcePlatform,
|
|
151
|
+
visibility: memory.scope,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function cursorFilters(input: MemoryPageInput) {
|
|
156
|
+
return {
|
|
157
|
+
kind: input.kind,
|
|
158
|
+
origin: input.origin,
|
|
159
|
+
query: input.query?.trim() || undefined,
|
|
160
|
+
visibility: input.visibility,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function decodeCursor(
|
|
165
|
+
value: string | undefined,
|
|
166
|
+
filters: ReturnType<typeof cursorFilters>,
|
|
167
|
+
) {
|
|
168
|
+
if (!value) return undefined;
|
|
169
|
+
try {
|
|
170
|
+
const parsed = cursorSchema.parse(
|
|
171
|
+
JSON.parse(Buffer.from(value, "base64url").toString("utf8")),
|
|
172
|
+
);
|
|
173
|
+
if (
|
|
174
|
+
parsed.query !== filters.query ||
|
|
175
|
+
parsed.kind !== filters.kind ||
|
|
176
|
+
parsed.origin !== filters.origin ||
|
|
177
|
+
parsed.visibility !== filters.visibility
|
|
178
|
+
) {
|
|
179
|
+
throw new InvalidMemoryCursorError();
|
|
180
|
+
}
|
|
181
|
+
return { createdAtMs: parsed.createdAtMs, id: parsed.id };
|
|
182
|
+
} catch {
|
|
183
|
+
throw new InvalidMemoryCursorError();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function encodeCursor(
|
|
188
|
+
createdBefore: { createdAtMs: number; id: string },
|
|
189
|
+
filters: ReturnType<typeof cursorFilters>,
|
|
190
|
+
): string {
|
|
191
|
+
return Buffer.from(
|
|
192
|
+
JSON.stringify({ ...createdBefore, ...filters, version: 1 }),
|
|
193
|
+
"utf8",
|
|
194
|
+
).toString("base64url");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Archive one active private memory owned by the authenticated User. */
|
|
198
|
+
export async function archiveMemory(db: MemoryDb, userId: string, id: string) {
|
|
199
|
+
const memoryId = nonEmptyStringSchema.parse(id);
|
|
200
|
+
const nowMs = Date.now();
|
|
201
|
+
const updated = await db
|
|
202
|
+
.update(juniorMemoryMemories)
|
|
203
|
+
.set({
|
|
204
|
+
archivedAtMs: nowMs,
|
|
205
|
+
archiveReason: "user_removed",
|
|
206
|
+
})
|
|
207
|
+
.where(
|
|
208
|
+
and(
|
|
209
|
+
activeMemoryPredicate(userId, nowMs, "private"),
|
|
210
|
+
eq(juniorMemoryMemories.id, memoryId),
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
.returning();
|
|
214
|
+
if (!updated[0]) throw new MemoryNotFoundError();
|
|
215
|
+
await db
|
|
216
|
+
.delete(juniorMemoryEmbeddings)
|
|
217
|
+
.where(eq(juniorMemoryEmbeddings.memoryId, memoryId));
|
|
218
|
+
return parseMemoryRow(updated[0]);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Read one active memory visible to the authenticated User. */
|
|
222
|
+
export async function getMemory(
|
|
223
|
+
db: MemoryDb,
|
|
224
|
+
userId: string,
|
|
225
|
+
id: string,
|
|
226
|
+
): Promise<MemoryView> {
|
|
227
|
+
const memoryId = nonEmptyStringSchema.parse(id);
|
|
228
|
+
const nowMs = Date.now();
|
|
229
|
+
const rows = await db
|
|
230
|
+
.select()
|
|
231
|
+
.from(juniorMemoryMemories)
|
|
232
|
+
.where(
|
|
233
|
+
and(
|
|
234
|
+
activeMemoryPredicate(userId, nowMs),
|
|
235
|
+
eq(juniorMemoryMemories.id, memoryId),
|
|
236
|
+
),
|
|
237
|
+
)
|
|
238
|
+
.limit(1);
|
|
239
|
+
if (!rows[0]) throw new MemoryNotFoundError();
|
|
240
|
+
return toMemoryView(rows[0]);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** List one stable page of active memory visible to the authenticated User. */
|
|
244
|
+
export async function listMemories(
|
|
245
|
+
db: MemoryDb,
|
|
246
|
+
userId: string,
|
|
247
|
+
input: MemoryPageInput,
|
|
248
|
+
): Promise<MemoryPage> {
|
|
249
|
+
input = pageInputSchema.parse(input);
|
|
250
|
+
const filters = cursorFilters(input);
|
|
251
|
+
const cursor = decodeCursor(input.cursor, filters);
|
|
252
|
+
const active = activeMemoryPredicate(userId, Date.now(), input.visibility);
|
|
253
|
+
const createdBefore = cursor
|
|
254
|
+
? or(
|
|
255
|
+
lt(juniorMemoryMemories.createdAtMs, cursor.createdAtMs),
|
|
256
|
+
and(
|
|
257
|
+
eq(juniorMemoryMemories.createdAtMs, cursor.createdAtMs),
|
|
258
|
+
gt(juniorMemoryMemories.id, cursor.id),
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
: undefined;
|
|
262
|
+
const terms = input.query ? searchTerms(input.query) : [];
|
|
263
|
+
const search =
|
|
264
|
+
input.query === undefined
|
|
265
|
+
? undefined
|
|
266
|
+
: terms.length === 0
|
|
267
|
+
? sql`false`
|
|
268
|
+
: or(
|
|
269
|
+
...terms.map((term) =>
|
|
270
|
+
ilike(juniorMemoryMemories.content, `%${term}%`),
|
|
271
|
+
),
|
|
272
|
+
);
|
|
273
|
+
const kind = input.kind
|
|
274
|
+
? eq(juniorMemoryMemories.kind, input.kind)
|
|
275
|
+
: undefined;
|
|
276
|
+
const origin =
|
|
277
|
+
input.origin === "automatic"
|
|
278
|
+
? like(juniorMemoryMemories.idempotencyKey, "session:%")
|
|
279
|
+
: input.origin === "explicit"
|
|
280
|
+
? like(juniorMemoryMemories.idempotencyKey, "tool:%")
|
|
281
|
+
: undefined;
|
|
282
|
+
const rows = await db
|
|
283
|
+
.select()
|
|
284
|
+
.from(juniorMemoryMemories)
|
|
285
|
+
.where(and(active, createdBefore, search, kind, origin))
|
|
286
|
+
.orderBy(
|
|
287
|
+
desc(juniorMemoryMemories.createdAtMs),
|
|
288
|
+
asc(juniorMemoryMemories.id),
|
|
289
|
+
)
|
|
290
|
+
.limit(input.limit + 1);
|
|
291
|
+
const memories = rows.slice(0, input.limit).map(toMemoryView);
|
|
292
|
+
const last = memories.at(-1);
|
|
293
|
+
if (rows.length <= input.limit || !last) return { memories };
|
|
294
|
+
const next = { createdAtMs: last.createdAtMs, id: last.id };
|
|
295
|
+
return { memories, nextCursor: encodeCursor(next, filters) };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Summarize active memory visible to the authenticated User. */
|
|
299
|
+
export async function getMemoryStats(db: MemoryDb, userId: string) {
|
|
300
|
+
const nowMs = Date.now();
|
|
301
|
+
const [counts] = await db
|
|
302
|
+
.select({
|
|
303
|
+
active: sql<number>`count(*)`.mapWith(Number),
|
|
304
|
+
automatic:
|
|
305
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.idempotencyKey} like 'session:%')`.mapWith(
|
|
306
|
+
Number,
|
|
307
|
+
),
|
|
308
|
+
createdThirtyDays:
|
|
309
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.createdAtMs} >= ${nowMs - 30 * DAY_MS})`.mapWith(
|
|
310
|
+
Number,
|
|
311
|
+
),
|
|
312
|
+
embedded: sql<number>`count(${juniorMemoryEmbeddings.memoryId})`.mapWith(
|
|
313
|
+
Number,
|
|
314
|
+
),
|
|
315
|
+
explicit:
|
|
316
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.idempotencyKey} like 'tool:%')`.mapWith(
|
|
317
|
+
Number,
|
|
318
|
+
),
|
|
319
|
+
knowledge:
|
|
320
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'knowledge')`.mapWith(
|
|
321
|
+
Number,
|
|
322
|
+
),
|
|
323
|
+
private:
|
|
324
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.scope} = 'private')`.mapWith(
|
|
325
|
+
Number,
|
|
326
|
+
),
|
|
327
|
+
preference:
|
|
328
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'preference')`.mapWith(
|
|
329
|
+
Number,
|
|
330
|
+
),
|
|
331
|
+
procedure:
|
|
332
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.kind} = 'procedure')`.mapWith(
|
|
333
|
+
Number,
|
|
334
|
+
),
|
|
335
|
+
public:
|
|
336
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.scope} = 'public')`.mapWith(
|
|
337
|
+
Number,
|
|
338
|
+
),
|
|
339
|
+
})
|
|
340
|
+
.from(juniorMemoryMemories)
|
|
341
|
+
.leftJoin(
|
|
342
|
+
juniorMemoryEmbeddings,
|
|
343
|
+
eq(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id),
|
|
344
|
+
)
|
|
345
|
+
.where(activeMemoryPredicate(userId, nowMs));
|
|
346
|
+
if (!counts) throw new Error("Memory stats query returned no row.");
|
|
347
|
+
return counts;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Read daily memory creation totals visible to the authenticated User in UTC. */
|
|
351
|
+
export async function getMemoryTimeline(
|
|
352
|
+
db: MemoryDb,
|
|
353
|
+
userId: string,
|
|
354
|
+
days: number,
|
|
355
|
+
) {
|
|
356
|
+
days = timelineDaysSchema.parse(days);
|
|
357
|
+
const todayMs = Date.parse(`${utcDate(Date.now())}T00:00:00.000Z`);
|
|
358
|
+
const startMs = todayMs - (days - 1) * DAY_MS;
|
|
359
|
+
const rows = await db
|
|
360
|
+
.select({
|
|
361
|
+
date: sql<string>`to_char(to_timestamp(${juniorMemoryMemories.createdAtMs} / 1000.0) AT TIME ZONE 'UTC', 'YYYY-MM-DD')`.as(
|
|
362
|
+
"date",
|
|
363
|
+
),
|
|
364
|
+
private:
|
|
365
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.scope} = 'private')`.mapWith(
|
|
366
|
+
Number,
|
|
367
|
+
),
|
|
368
|
+
public:
|
|
369
|
+
sql<number>`count(*) filter (where ${juniorMemoryMemories.scope} = 'public')`.mapWith(
|
|
370
|
+
Number,
|
|
371
|
+
),
|
|
372
|
+
})
|
|
373
|
+
.from(juniorMemoryMemories)
|
|
374
|
+
.where(
|
|
375
|
+
and(
|
|
376
|
+
visibleScopePredicate(userId),
|
|
377
|
+
gt(juniorMemoryMemories.createdAtMs, startMs - 1),
|
|
378
|
+
),
|
|
379
|
+
)
|
|
380
|
+
.groupBy(
|
|
381
|
+
sql`to_char(to_timestamp(${juniorMemoryMemories.createdAtMs} / 1000.0) AT TIME ZONE 'UTC', 'YYYY-MM-DD')`,
|
|
382
|
+
);
|
|
383
|
+
const byDate = new Map(rows.map((row) => [row.date, row]));
|
|
384
|
+
return Array.from({ length: days }, (_, index) => {
|
|
385
|
+
const date = utcDate(startMs + index * DAY_MS);
|
|
386
|
+
const row = byDate.get(date);
|
|
387
|
+
return {
|
|
388
|
+
date,
|
|
389
|
+
private: row?.private ?? 0,
|
|
390
|
+
public: row?.public ?? 0,
|
|
391
|
+
};
|
|
392
|
+
});
|
|
393
|
+
}
|