@sentry/junior-memory 0.106.0 → 0.107.1

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.js CHANGED
@@ -3,8 +3,41 @@ import { defineJuniorPlugin } from "@sentry/junior-plugin-api";
3
3
 
4
4
  // src/agent.ts
5
5
  import { actorSchema } from "@sentry/junior-plugin-api";
6
+ import { z as z3 } from "zod";
7
+
8
+ // src/store.ts
9
+ import { createHash, randomUUID } from "crypto";
10
+ import {
11
+ and,
12
+ asc,
13
+ desc,
14
+ eq,
15
+ gt,
16
+ ilike,
17
+ inArray,
18
+ isNull,
19
+ isNotNull,
20
+ like,
21
+ lte,
22
+ or,
23
+ sql as sql2
24
+ } from "drizzle-orm";
25
+ import { cosineDistance } from "drizzle-orm/sql/functions";
6
26
  import { z as z2 } from "zod";
7
27
 
28
+ // src/db/schema.ts
29
+ import { sql } from "drizzle-orm";
30
+ import {
31
+ bigint,
32
+ check,
33
+ index,
34
+ integer,
35
+ pgTable,
36
+ text,
37
+ uniqueIndex,
38
+ vector
39
+ } from "drizzle-orm/pg-core";
40
+
8
41
  // src/types.ts
9
42
  import {
10
43
  localActorSchema,
@@ -42,1606 +75,1021 @@ var memoryRuntimeContextSchema = z.union([
42
75
  localMemoryRuntimeContextSchema
43
76
  ]);
44
77
 
45
- // src/agent.ts
46
- var memoryKindSchema = z2.enum(MEMORY_KINDS);
47
- var memoryRejectReasonSchema = z2.enum([
48
- "not_public_shareable",
49
- "secret_or_credential",
50
- "sensitive_personal",
51
- "third_party_personal",
52
- "vague_or_not_self_contained",
53
- "not_durable",
54
- "assistant_or_system_detail",
55
- "unsupported_scope"
56
- ]);
57
- var createMemoryRequestSchema = z2.object({
58
- content: z2.string().min(1),
59
- expiresAtMs: z2.number().finite().optional(),
60
- runtimeContext: memoryRuntimeContextSchema,
61
- sourceContext: z2.object({
62
- currentUserText: z2.string().min(1).optional()
63
- }).strict().optional()
64
- }).strict();
65
- var transcriptProvenanceSchema = z2.object({
66
- authority: z2.enum(["instruction", "context"]),
67
- actor: actorSchema.optional()
68
- }).strict();
69
- var evidenceMessageIndicesSchema = z2.array(z2.number().int().nonnegative()).min(1).max(10).describe("Indices from <run-transcript> that directly support this memory.");
70
- var extractSessionRequestSchema = z2.object({
71
- existingMemories: z2.array(
72
- z2.object({
73
- content: z2.string().min(1)
74
- }).strict()
75
- ).max(10).default([]),
76
- actors: z2.array(actorSchema),
77
- runtimeContext: memoryRuntimeContextSchema,
78
- transcript: z2.array(
79
- z2.discriminatedUnion("type", [
80
- z2.object({
81
- type: z2.literal("message"),
82
- role: z2.enum(["user", "assistant"]),
83
- text: z2.string().min(1),
84
- provenance: transcriptProvenanceSchema.optional(),
85
- isRunActor: z2.boolean().optional()
86
- }).strict(),
87
- z2.object({
88
- type: z2.literal("toolResult"),
89
- toolName: z2.string().min(1),
90
- isError: z2.boolean(),
91
- text: z2.string().min(1)
92
- }).strict()
93
- ])
94
- ).min(1)
95
- }).strict();
96
- var supersessionRequestSchema = z2.object({
97
- candidate: z2.object({
98
- content: z2.string().min(1),
99
- kind: z2.literal("preference")
100
- }).strict(),
101
- existingMemories: z2.array(
102
- z2.object({
103
- content: z2.string().min(1),
104
- id: z2.string().min(1)
105
- }).strict()
106
- ).min(1).max(10),
107
- runtimeContext: memoryRuntimeContextSchema
108
- }).strict();
109
- var expiresAtMsSchema = z2.number().finite().nullable().describe(
110
- "Expiration timestamp when the fact should expire, otherwise null."
78
+ // src/db/schema.ts
79
+ var juniorMemoryMemories = pgTable(
80
+ "junior_memory_memories",
81
+ {
82
+ id: text("id").primaryKey(),
83
+ scope: text("scope", { enum: MEMORY_SCOPES }).notNull(),
84
+ scopeKey: text("scope_key").notNull(),
85
+ kind: text("type", { enum: MEMORY_KINDS }).notNull(),
86
+ subjectType: text("subject_type", { enum: MEMORY_SUBJECT_TYPES }).notNull(),
87
+ subjectKey: text("subject_key"),
88
+ content: text("content").notNull(),
89
+ sourcePlatform: text("source_platform", {
90
+ enum: MEMORY_SOURCE_PLATFORMS
91
+ }).notNull(),
92
+ sourceKey: text("source_key").notNull(),
93
+ idempotencyKey: text("idempotency_key"),
94
+ observedAtMs: bigint("observed_at_ms", { mode: "number" }).notNull(),
95
+ createdAtMs: bigint("created_at_ms", { mode: "number" }).notNull(),
96
+ expiresAtMs: bigint("expires_at_ms", { mode: "number" }),
97
+ supersededAtMs: bigint("superseded_at_ms", { mode: "number" }),
98
+ supersededById: text("superseded_by_id"),
99
+ archivedAtMs: bigint("archived_at_ms", { mode: "number" }),
100
+ archiveReason: text("archive_reason")
101
+ },
102
+ (table) => [
103
+ index("junior_memory_memories_visible_idx").on(table.scope, table.scopeKey, table.createdAtMs.desc(), table.id).where(
104
+ sql`${table.archivedAtMs} IS NULL AND ${table.supersededAtMs} IS NULL AND ${table.supersededById} IS NULL`
105
+ ),
106
+ index("junior_memory_memories_expiration_idx").on(table.expiresAtMs).where(
107
+ sql`${table.archivedAtMs} IS NULL AND ${table.expiresAtMs} IS NOT NULL`
108
+ ),
109
+ uniqueIndex("junior_memory_memories_idempotency_idx").on(table.scope, table.scopeKey, table.idempotencyKey).where(
110
+ sql`${table.idempotencyKey} IS NOT NULL AND ${table.archivedAtMs} IS NULL AND ${table.supersededAtMs} IS NULL AND ${table.supersededById} IS NULL`
111
+ ),
112
+ check(
113
+ "junior_memory_memories_scope_check",
114
+ sql`${table.scope} IN ('personal', 'conversation')`
115
+ ),
116
+ check(
117
+ "junior_memory_memories_kind_check",
118
+ sql`${table.kind} IN (
119
+ 'preference',
120
+ 'procedure',
121
+ 'knowledge'
122
+ )`
123
+ ),
124
+ check(
125
+ "junior_memory_memories_subject_type_check",
126
+ sql`${table.subjectType} IN ('user', 'conversation', 'general')`
127
+ ),
128
+ check(
129
+ "junior_memory_memories_subject_key_check",
130
+ sql`(${table.subjectType} = 'general' AND ${table.subjectKey} IS NULL) OR (${table.subjectType} IN ('user', 'conversation') AND ${table.subjectKey} IS NOT NULL AND length(${table.subjectKey}) > 0)`
131
+ ),
132
+ check(
133
+ "junior_memory_memories_source_platform_check",
134
+ sql`${table.sourcePlatform} IN ('slack', 'local')`
135
+ )
136
+ ]
111
137
  );
112
- var memoryReviewDecisionSchema = z2.discriminatedUnion("decision", [
113
- z2.object({
114
- decision: z2.literal("store"),
115
- kind: memoryKindSchema,
116
- content: z2.string().min(1),
117
- expiresAtMs: z2.number().finite().optional()
118
- }).strict(),
119
- z2.object({
120
- decision: z2.literal("reject"),
121
- reason: memoryRejectReasonSchema
122
- }).strict()
123
- ]);
124
- var memoryReviewResponseSchema = z2.discriminatedUnion("decision", [
125
- z2.object({
126
- decision: z2.literal("store"),
127
- kind: memoryKindSchema.describe(
128
- "Use preference only for actor-owned personal preferences, opinions, habits, or workflows. Use procedure for reusable task or process instructions. Use knowledge for shared project, channel, operational, or runbook facts."
138
+ var juniorMemoryEmbeddings = pgTable(
139
+ "junior_memory_embeddings",
140
+ {
141
+ memoryId: text("memory_id").primaryKey().references(() => juniorMemoryMemories.id, { onDelete: "cascade" }),
142
+ provider: text("provider").notNull(),
143
+ model: text("model").notNull(),
144
+ dimensions: integer("dimensions").notNull(),
145
+ metric: text("metric", { enum: MEMORY_EMBEDDING_METRICS }).notNull(),
146
+ contentHash: text("content_hash").notNull(),
147
+ embedding: vector("embedding", {
148
+ dimensions: MEMORY_EMBEDDING_DIMENSIONS
149
+ }).notNull(),
150
+ createdAtMs: bigint("created_at_ms", { mode: "number" }).notNull()
151
+ },
152
+ (table) => [
153
+ index("junior_memory_embeddings_model_idx").on(
154
+ table.provider,
155
+ table.model,
156
+ table.dimensions,
157
+ table.metric
129
158
  ),
130
- canonicalFact: z2.string().min(1).describe(
131
- "Stored memory text. It must be self-contained and must not include actor names, actor/user labels, source labels, or first- or second-person wording."
159
+ check(
160
+ "junior_memory_embeddings_metric_check",
161
+ sql`${table.metric} IN ('cosine')`
132
162
  ),
133
- expiresAtMs: expiresAtMsSchema
134
- }).strict(),
135
- z2.object({
136
- decision: z2.literal("reject"),
137
- reason: memoryRejectReasonSchema
138
- }).strict()
139
- ]);
140
- var extractedMemorySchema = z2.object({
141
- kind: memoryKindSchema.describe(
142
- "Use preference only for actor-owned personal preferences, opinions, habits, or workflows. Use procedure for reusable task or process instructions. Use knowledge for shared project, channel, operational, or runbook facts."
143
- ),
144
- canonicalFact: z2.string().min(1).describe(
145
- "Stored memory text as one self-contained fact. It must not include actor names, actor/user labels, source labels, or first- or second-person wording."
146
- ),
147
- expiresAtMs: expiresAtMsSchema,
148
- evidenceMessageIndices: evidenceMessageIndicesSchema
149
- }).strict();
150
- var extractedMemoryResultSchema = z2.object({
151
- content: z2.string().min(1),
152
- expiresAtMs: expiresAtMsSchema,
153
- kind: memoryKindSchema,
154
- evidenceMessageIndices: evidenceMessageIndicesSchema
155
- }).strict();
156
- var extractMemoriesResponseSchema = z2.object({
157
- memories: z2.array(extractedMemorySchema).max(5).describe(
158
- "Accepted public/shareable durable memories from the completed run. Return one object per distinct source assertion and classify it with kind."
159
- )
160
- }).strict();
161
- var supersessionDecisionResponseSchema = z2.discriminatedUnion("decision", [
162
- z2.object({
163
- decision: z2.literal("supersedes_old"),
164
- supersededIds: z2.array(z2.string().min(1)).min(1).max(10)
165
- }).strict(),
166
- z2.object({
167
- decision: z2.enum(["distinct", "uncertain"])
168
- }).strict()
169
- ]);
170
- var MEMORY_REVIEW_SYSTEM = [
171
- "You are Junior's memory review agent.",
172
- "Review one memory candidate and return one structured review decision.",
173
- "Store only public/shareable, self-contained facts that are useful beyond this turn.",
174
- "Reject secrets, credentials, private or sensitive personal details, gossip, speculative claims about other people, assistant/system implementation details, vague references, and low-durability chatter.",
175
- "Use the runtime context only for authority and scope; do not accept model-provided actor ids, scope ids, aliases, or arbitrary subjects."
176
- ].join("\n");
177
- var MEMORY_EXTRACTION_SYSTEM = [
178
- "You are Junior's passive memory extraction agent. Return only structured memories worth storing.",
179
- "Use the completed run transcript as source evidence, including user-authored messages and tool results.",
180
- "Assistant text is context for interpreting the run, not independent evidence for new facts.",
181
- "Reject secrets, credentials, private or sensitive personal details, gossip, speculative claims about other people, assistant/system implementation details, vague references, and low-durability chatter.",
182
- "If no public, durable, self-contained memory remains after rewriting, return an empty memories array."
183
- ].join("\n");
184
- var MEMORY_SUPERSESSION_SYSTEM = [
185
- "You are Junior's memory supersession agent.",
186
- "Decide whether a new actor preference clearly replaces existing active actor preferences.",
187
- "Return supersedes_old only for obvious changed preferences about the same mutable slot.",
188
- "If the facts are additive, different topics, duplicate, broader/narrower without direct replacement, or uncertain, do not supersede."
189
- ].join("\n");
190
- var CANONICAL_CONTENT_RULES = [
191
- "- Stored memory text must be a rewritten fact, not copied user wording or a sentence about who said it.",
192
- "- Store the minimum useful assertion supported by source evidence; do not add adjacent steps, caveats, or generalized advice.",
193
- "- Do not return both concise and expanded variants of the same source assertion; keep the shortest self-contained canonical memory.",
194
- "- Put ownership in structured fields, not prose.",
195
- "- For actor memories, omit the subject and write a stable fact such as 'Prefers X', 'Uses Y', or 'Thinks Z'.",
196
- "- Drop perspective/provenance markers while preserving useful context.",
197
- "- Remove actor names, display names, actor/user labels, first- or second-person wording, thread labels, channel labels, and source labels."
198
- ];
199
- function escapeXml(value) {
200
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
201
- }
202
- function runtimeDescription(request) {
203
- const runtime = request.runtimeContext;
204
- const actor = runtime.actor?.platform === "slack" ? `slack:${runtime.actor.teamId}:${runtime.actor.userId}` : runtime.actor?.platform === "local" ? `local:${runtime.actor.userId}` : "none";
205
- const source = runtime.source.platform === "slack" ? `slack:${runtime.source.teamId}:${runtime.source.channelId}` : `local:${runtime.source.conversationId}`;
206
- const lines = [
207
- `- actor: ${escapeXml(actor)}`,
208
- `- source: ${escapeXml(source)}`,
209
- `- has_conversation: ${runtime.conversationId ? "true" : "false"}`,
210
- `- expires_at: ${request.expiresAtMs === void 0 ? "never" : escapeXml(new Date(request.expiresAtMs).toISOString())}`
211
- ];
212
- return ["<runtime>", ...lines, "</runtime>"].join("\n");
213
- }
214
- function sourceContext(request) {
215
- const currentUserText = request.sourceContext?.currentUserText?.trim();
216
- if (!currentUserText) {
163
+ check(
164
+ "junior_memory_embeddings_dimensions_check",
165
+ sql`${table.dimensions} = ${sql.raw(String(MEMORY_EMBEDDING_DIMENSIONS))}`
166
+ )
167
+ ]
168
+ );
169
+
170
+ // src/scope.ts
171
+ import { isPrivateSource } from "@sentry/junior-plugin-api";
172
+ function sourceConversationKey(ctx) {
173
+ if (ctx.source.platform === "local") {
174
+ return ctx.source.conversationId;
175
+ }
176
+ if (!isPrivateSource(ctx.source)) {
177
+ return `slack:${ctx.source.teamId}`;
178
+ }
179
+ const threadKey = ctx.source.threadTs ?? ctx.source.messageTs;
180
+ if (!threadKey) {
217
181
  return void 0;
218
182
  }
219
- return [
220
- "<source-context>",
221
- "The current user-authored text is source evidence for explicit memory requests. Use it to recover the concrete fact when the candidate is incomplete, vague, or over-personalized. Store only rewritten, self-contained memory content.",
222
- "<current-user-message>",
223
- escapeXml(currentUserText),
224
- "</current-user-message>",
225
- "</source-context>"
226
- ].join("\n");
183
+ return `slack:${ctx.source.teamId}:${ctx.source.channelId}:${threadKey}`;
227
184
  }
228
- function existingMemoriesContext(request) {
229
- if (request.existingMemories.length === 0) {
230
- return "<existing-memories>[]</existing-memories>";
185
+ function actorScopeKey(ctx) {
186
+ const actor = ctx.actor;
187
+ if (!actor?.userId) {
188
+ return void 0;
231
189
  }
232
- return [
233
- "<existing-memories>",
234
- "Use these only to skip memories that are already covered or semantically redundant. They are not source evidence for new memories.",
235
- escapeXml(JSON.stringify(request.existingMemories)),
236
- "</existing-memories>"
237
- ].join("\n");
190
+ if (actor.platform === "slack") {
191
+ return `slack:${actor.teamId}:${actor.userId}`;
192
+ }
193
+ return `local:${actor.userId}`;
238
194
  }
239
- function allowedExtractionKinds(actorCount) {
240
- return actorCount === 1 ? new Set(MEMORY_KINDS) : /* @__PURE__ */ new Set(["procedure", "knowledge"]);
195
+ function deriveMemoryScope(ctx, scope) {
196
+ if (scope === "personal") {
197
+ const scopeKey2 = actorScopeKey(ctx);
198
+ if (!scopeKey2) {
199
+ throw new Error("Personal memory requires actor context.");
200
+ }
201
+ return { scope, scopeKey: scopeKey2 };
202
+ }
203
+ const scopeKey = sourceConversationKey(ctx);
204
+ if (!scopeKey) {
205
+ throw new Error("Conversation memory requires conversation context.");
206
+ }
207
+ return { scope, scopeKey };
241
208
  }
242
- function memoryKindsContext(allowedKinds) {
243
- const lines = ["<memory-kinds>"];
244
- if (allowedKinds.has("preference")) {
245
- lines.push(
246
- "- preference: a durable first-person personal preference, opinion, habit, or workflow owned by the current actor. Stored as actor memory."
209
+ function deriveMemorySubject(ctx, scope) {
210
+ if (scope.scope === "personal") {
211
+ const subjectKey2 = actorScopeKey(ctx);
212
+ if (!subjectKey2) {
213
+ throw new Error("User-subject memory requires actor context.");
214
+ }
215
+ return { subjectType: "user", subjectKey: subjectKey2 };
216
+ }
217
+ const subjectKey = sourceConversationKey(ctx);
218
+ if (!subjectKey) {
219
+ throw new Error(
220
+ "Conversation-subject memory requires conversation context."
247
221
  );
248
222
  }
249
- lines.push(
250
- "- procedure: reusable instructions for how a task, lookup, investigation, process, triage flow, or runbook should be done. Store the method, source-of-truth, prerequisite, or decision path when it took effort to discover. Stored as conversation memory.",
251
- "- knowledge: stable shared project, channel, operational, or runbook fact that is not a personal actor preference. Direct answers to user inquiries qualify only when they are durable beyond this run. Stored as conversation memory.",
252
- "</memory-kinds>"
253
- );
254
- return lines.join("\n");
223
+ return { subjectType: "conversation", subjectKey };
255
224
  }
256
- function reviewPrompt(request) {
257
- const sections = [
258
- "<memory-review-input>",
259
- "Review the candidate memory using the runtime-owned context below.",
260
- "",
261
- runtimeDescription(request),
262
- "",
263
- sourceContext(request),
264
- "",
265
- "<candidate>",
266
- escapeXml(request.content),
267
- "</candidate>",
268
- "",
269
- "<rules>",
270
- "- Return store only when the candidate is public/shareable, durable, and self-contained.",
271
- "- First classify the memory kind: preference, procedure, or knowledge.",
272
- "- Use kind=preference only for first-person facts authored by the current actor about their own preference, opinion, habit, identity, or workflow.",
273
- "- Reject named third-person personal facts such as another person's preference, opinion, habit, identity, relationship, or workflow. Do not assume a named person is the current actor.",
274
- "- Use kind=procedure for reusable task/process/runbook instructions.",
275
- "- Use kind=knowledge for shared project, channel, operational, or runbook facts.",
276
- "- When current-user-message contains an explicit memory request with a concrete fact or procedure, extract from current-user-message even if the candidate is vague, incomplete, or phrased as an instruction.",
277
- "- A candidate may be badly phrased by an outer assistant or extraction pass. When current-user-message contains the actor's own first-person memory fact, treat that as actor-authored source evidence and canonicalize the fact instead of rejecting for third-person wording.",
278
- "- When candidate wording personalizes a shared task, process, runbook, project, channel, or operational fact, use current-user-message to recover the shared fact and classify it as procedure or knowledge.",
279
- "- Explicit procedure requests are valid when the source text contains both task context and action. Canonicalize them as shared procedure facts instead of rejecting them as vague.",
280
- "- Store content as person-less, source-less canonical knowledge. Ownership and source live in structured metadata, not prose.",
281
- "- For actor memories, omit the subject and write the content as a stable fact such as 'Prefers X', 'Uses Y', or 'Thinks Z'.",
282
- "- Remove actor names, display names, actor/user labels, first- or second-person wording, thread labels, channel labels, and source labels from stored content.",
283
- "- Reject third-party personal profile facts, even if they mention a name.",
284
- "- Reject vague content such as 'remember this' unless the candidate or current-user-message contains the concrete fact.",
285
- "- Preserve the requested expiration when one exists; otherwise set expiresAtMs to null.",
286
- "- If unsure, reject.",
287
- "</rules>",
288
- "</memory-review-input>"
289
- ].filter((section) => section !== void 0);
290
- return sections.join("\n");
225
+ function deriveVisibleMemoryScopes(ctx) {
226
+ const scopes = [];
227
+ try {
228
+ scopes.push(deriveMemoryScope(ctx, "personal"));
229
+ } catch {
230
+ }
231
+ try {
232
+ scopes.push(deriveMemoryScope(ctx, "conversation"));
233
+ } catch {
234
+ }
235
+ return scopes;
291
236
  }
292
- function transcriptActorLabel(actor) {
293
- if (!actor) {
294
- return "none";
237
+
238
+ // src/store.ts
239
+ var DEFAULT_LIST_LIMIT = 50;
240
+ var DEFAULT_SEARCH_LIMIT = 10;
241
+ var DEFAULT_EXPIRED_ARCHIVE_LIMIT = 100;
242
+ var HIGH_CONFIDENCE_DUPLICATE_DISTANCE = 0.015;
243
+ var PREFERENCE_ADJUDICATION_CANDIDATE_LIMIT = 10;
244
+ var PREFERENCE_ADJUDICATION_VECTOR_LIMIT = 5;
245
+ var VECTOR_SEARCH_OVERFETCH = 4;
246
+ var MAX_MEMORY_CONTENT_CHARS = 4e3;
247
+ var EMBEDDING_METRIC = "cosine";
248
+ var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
249
+ var RELEVANCE_NEAR_TIE_DELTA = 0.01;
250
+ var SOURCE_CHANNEL_BOOST = 0.05;
251
+ var nonEmptyStringSchema2 = z2.string().min(1);
252
+ var memoryContentSchema = z2.string().refine((content) => content.trim().length > 0, {
253
+ message: "Memory content is required."
254
+ });
255
+ var numberSchema = z2.number().finite();
256
+ var createMemoryInputSchema = z2.object({
257
+ content: memoryContentSchema,
258
+ expiresAtMs: numberSchema.optional(),
259
+ idempotencyKey: nonEmptyStringSchema2,
260
+ kind: z2.enum(MEMORY_KINDS)
261
+ }).strict();
262
+ var listMemoriesInputSchema = z2.object({
263
+ limit: numberSchema.optional()
264
+ }).strict();
265
+ var searchMemoriesInputSchema = z2.object({
266
+ limit: numberSchema.optional(),
267
+ query: nonEmptyStringSchema2
268
+ }).strict();
269
+ var archiveMemoryInputSchema = z2.object({
270
+ id: nonEmptyStringSchema2,
271
+ reason: nonEmptyStringSchema2.optional()
272
+ }).strict();
273
+ var archiveExpiredMemoriesInputSchema = z2.object({
274
+ limit: numberSchema.optional()
275
+ }).strict();
276
+ var clockSchema = z2.function({ input: [], output: numberSchema }).optional();
277
+ var memoryStoreOptionsSchema = z2.object({
278
+ now: clockSchema
279
+ }).strict();
280
+ var optionalNumberSchema = z2.preprocess(
281
+ (value) => value === null ? void 0 : value,
282
+ z2.coerce.number().optional()
283
+ );
284
+ var optionalStringSchema = z2.preprocess(
285
+ (value) => value === null ? void 0 : value,
286
+ z2.string().optional()
287
+ );
288
+ var optionalNonEmptyStringSchema = z2.preprocess(
289
+ (value) => value === null ? void 0 : value,
290
+ z2.string().min(1).optional()
291
+ );
292
+ var memoryRowSchema = z2.object({
293
+ archivedAtMs: optionalNumberSchema,
294
+ archiveReason: optionalStringSchema,
295
+ content: memoryContentSchema,
296
+ createdAtMs: z2.coerce.number(),
297
+ expiresAtMs: optionalNumberSchema,
298
+ id: z2.string().min(1),
299
+ idempotencyKey: optionalStringSchema,
300
+ observedAtMs: z2.coerce.number(),
301
+ scope: z2.enum(MEMORY_SCOPES),
302
+ scopeKey: z2.string().min(1),
303
+ sourceKey: z2.string().min(1),
304
+ sourcePlatform: z2.enum(MEMORY_SOURCE_PLATFORMS),
305
+ subjectKey: optionalNonEmptyStringSchema,
306
+ subjectType: z2.enum(MEMORY_SUBJECT_TYPES),
307
+ supersededAtMs: optionalNumberSchema,
308
+ supersededById: optionalStringSchema,
309
+ kind: z2.enum(MEMORY_KINDS)
310
+ }).strict().superRefine((row, ctx) => {
311
+ if (row.subjectType === "general") {
312
+ if (row.subjectKey !== void 0) {
313
+ ctx.addIssue({
314
+ code: "custom",
315
+ message: "General-subject memory rows must not have a subject key.",
316
+ path: ["subjectKey"]
317
+ });
318
+ }
319
+ return;
295
320
  }
296
- if (actor.platform === "system") {
297
- return `system:${actor.name}`;
321
+ if (row.subjectKey === void 0) {
322
+ ctx.addIssue({
323
+ code: "custom",
324
+ message: "User and conversation memory rows require a subject key.",
325
+ path: ["subjectKey"]
326
+ });
298
327
  }
299
- return actor.platform === "slack" ? `slack:${actor.teamId}:${actor.userId}` : `local:${actor.userId}`;
328
+ });
329
+ var memoryRecordSchema = z2.object({
330
+ archivedAtMs: numberSchema.optional(),
331
+ archiveReason: nonEmptyStringSchema2.optional(),
332
+ content: memoryContentSchema,
333
+ createdAtMs: numberSchema,
334
+ expiresAtMs: numberSchema.optional(),
335
+ id: nonEmptyStringSchema2,
336
+ observedAtMs: numberSchema,
337
+ scope: z2.enum(MEMORY_SCOPES),
338
+ subjectType: z2.enum(MEMORY_SUBJECT_TYPES),
339
+ supersededAtMs: numberSchema.optional(),
340
+ supersededById: nonEmptyStringSchema2.optional(),
341
+ kind: z2.enum(MEMORY_KINDS)
342
+ }).strict();
343
+ var embeddingVectorSchema = z2.array(numberSchema).length(MEMORY_EMBEDDING_DIMENSIONS);
344
+ var embeddingResultSchema = z2.object({
345
+ dimensions: z2.literal(MEMORY_EMBEDDING_DIMENSIONS),
346
+ model: nonEmptyStringSchema2,
347
+ provider: nonEmptyStringSchema2,
348
+ vectors: z2.array(embeddingVectorSchema)
349
+ }).strict();
350
+ var memorySupersessionCandidateSchema = z2.object({
351
+ content: z2.string().min(1),
352
+ id: z2.string().min(1)
353
+ }).strict();
354
+ var memorySupersessionCandidatesSchema = z2.array(memorySupersessionCandidateSchema).min(1).max(PREFERENCE_ADJUDICATION_CANDIDATE_LIMIT);
355
+ var supersededIdsSchema = z2.array(z2.string().min(1)).min(1).max(PREFERENCE_ADJUDICATION_CANDIDATE_LIMIT);
356
+ var memorySupersessionInputSchema = z2.object({
357
+ candidate: z2.object({
358
+ content: z2.string().min(1),
359
+ kind: z2.literal("preference")
360
+ }).strict(),
361
+ existingMemories: memorySupersessionCandidatesSchema,
362
+ runtimeContext: memoryRuntimeContextSchema
363
+ }).strict();
364
+ var memorySupersessionDecisionSchema = z2.discriminatedUnion(
365
+ "decision",
366
+ [
367
+ z2.object({
368
+ decision: z2.literal("duplicate"),
369
+ duplicateId: z2.string().min(1)
370
+ }).strict(),
371
+ z2.object({
372
+ decision: z2.literal("supersedes_old"),
373
+ supersededIds: supersededIdsSchema
374
+ }).strict(),
375
+ z2.object({
376
+ decision: z2.enum(["distinct", "uncertain"])
377
+ }).strict()
378
+ ]
379
+ );
380
+ function normalizeContent(content) {
381
+ return content.replace(/\s+/g, " ").trim();
300
382
  }
301
- function runTranscriptContext(request) {
302
- return [
303
- "<run-transcript>",
304
- ...request.transcript.map((entry, index2) => {
305
- if (entry.type === "toolResult") {
306
- return [
307
- `<tool-result index="${index2}" tool="${escapeXml(entry.toolName)}" is_error="${entry.isError ? "true" : "false"}">`,
308
- escapeXml(entry.text),
309
- "</tool-result>"
310
- ].join("\n");
311
- }
312
- const authority = entry.provenance?.authority ?? "context";
313
- const isRunActor = entry.isRunActor === true;
314
- const actor = transcriptActorLabel(entry.provenance?.actor);
315
- return [
316
- `<message index="${index2}" role="${entry.role}" authority="${authority}" is_run_actor="${isRunActor ? "true" : "false"}" actor="${escapeXml(actor)}">`,
317
- escapeXml(entry.text),
318
- "</message>"
319
- ].join("\n");
320
- }),
321
- "</run-transcript>"
322
- ].join("\n");
383
+ function hashEmbeddedContent(content) {
384
+ return createHash("sha256").update(content, "utf8").digest("hex");
323
385
  }
324
- function sessionExtractionPrompt(request) {
325
- const allowedKinds = allowedExtractionKinds(request.actors.length);
326
- const allowsPreference = allowedKinds.has("preference");
327
- return [
328
- "<memory-extraction-input>",
329
- "Extract durable memories from this completed agent run using the runtime-owned context below.",
330
- "",
331
- runtimeDescription({
332
- runtimeContext: request.runtimeContext
333
- }),
334
- "",
335
- existingMemoriesContext(request),
336
- "",
337
- memoryKindsContext(allowedKinds),
338
- "",
339
- runTranscriptContext(request),
340
- "",
341
- "<rules>",
342
- "- Return at most five memories.",
343
- "- Every returned memory must cite one or more evidenceMessageIndices from <run-transcript>.",
344
- "- Cite only indices that directly support the stored fact; do not cite assistant messages as independent evidence.",
345
- "- Each transcript message exposes authority (instruction or context), is_run_actor, and an actor id. Use these to classify evidence.",
346
- ...allowsPreference ? [
347
- "- For a preference, cite only messages with authority=instruction and is_run_actor=true; a preference must be the run actor's own first-person fact."
348
- ] : [],
349
- "- For a procedure or knowledge memory, cite run-actor instruction messages, public context messages, or successful tool results.",
350
- "- Use user messages and successful tool results as source evidence for storable facts.",
351
- "- Use failed tool results only when the failure reveals durable process knowledge, not transient errors.",
352
- "- Use assistant messages only as context; do not store the assistant's claims unless supported by user messages or tool results.",
353
- "- Return one memory per distinct fact.",
354
- "- Prefer storing how to achieve a result: stable source-of-truth, query location, workflow, prerequisite, caveat, or reusable decision path that took effort to discover.",
355
- "- Store direct answers to user inquiries only when they are stable operational/project knowledge, not values that naturally change over time.",
356
- "- Do not store point-in-time analytics, search, issue, metric, incident, availability, or status answers just because a tool produced them.",
357
- "- Do not store the fact that the user asked for advice, search, recall, planning, listing, inspection, or removal. Store only stable knowledge discovered in response, such as a reusable method or source-of-truth.",
358
- "- A user question asking how, what, where, or whether to do something is not source evidence for the answer. Store the answer only when supported by a user-authored factual statement or a tool result.",
359
- "- Set kind=procedure for reusable task/process/runbook instructions.",
360
- "- Set kind=knowledge for shared team, project, channel, runbook, or operational facts.",
361
- ...allowsPreference ? [
362
- "- Set kind=preference only for clear durable first-person facts authored by the current actor about their own preference, opinion, habit, identity, or workflow.",
363
- "- A single task request or ask-for-help is never a durable preference, even when phrased as an ongoing action for this run (for example 'help me capture takeaways as we go'). Do not convert a one-off ask into a 'Prefers ...' memory.",
364
- "- A durable preference requires explicitly stated, generalizable first-person phrasing such as 'I prefer ...', 'I always ...', or 'I never ...' that describes how the actor wants things done in general, not just for the current task."
365
- ] : [
366
- "- This completed run has multiple run actors. Return only conversation-scoped procedure or knowledge memories.",
367
- "- Do not return personal preferences, opinions, habits, identity facts, or workflow preferences from any actor in this run.",
368
- "- Do not convert a personal first-person statement into shared knowledge or procedure. Statements like 'I prefer ...', 'I use ...', 'I always ...', or 'I never ...' are not memory evidence in this run.",
369
- "- Shared team, channel, repository, or operational norms are eligible only when the source states them as collective practice or durable operational fact, not as one individual's preference."
370
- ],
371
- "- Reject named third-person personal facts such as another person's preference, opinion, habit, identity, relationship, or workflow. Do not assume a named person is the current actor.",
372
- "- User-authored task instructions are procedures, not preferences, unless they explicitly describe the actor's personal preference or habit.",
373
- "- Procedural statements such as 'for X, do Y', 'when X, do Y', and 'to accomplish X, do Y' belong in procedures.",
374
- ...CANONICAL_CONTENT_RULES,
375
- "- Skip a candidate when existing-memories already cover the same durable fact.",
376
- "- Reject third-party personal profile facts, even if they mention a name.",
377
- "- If unsure, return no memory for that candidate.",
378
- "</rules>",
379
- "</memory-extraction-input>"
380
- ].join("\n");
386
+ function idempotencyAliasId(args) {
387
+ return `alias:${createHash("sha256").update(args.scope.scope).update("\0").update(args.scope.scopeKey).update("\0").update(args.idempotencyKey).update("\0").update(args.targetId).digest("hex")}`;
381
388
  }
382
- function supersessionPrompt(request) {
383
- return [
384
- "<memory-supersession-input>",
385
- "Decide whether the candidate preference clearly replaces one or more existing active preferences.",
386
- "",
387
- runtimeDescription({
388
- runtimeContext: request.runtimeContext
389
- }),
390
- "",
391
- "<candidate>",
392
- escapeXml(JSON.stringify(request.candidate)),
393
- "</candidate>",
394
- "",
395
- "<existing-memories>",
396
- escapeXml(JSON.stringify(request.existingMemories)),
397
- "</existing-memories>",
398
- "",
399
- "<rules>",
400
- "- Return supersedes_old only when the candidate and old memory describe the same mutable preference slot and the candidate is the newer value.",
401
- "- Examples of same mutable slot: preferred programming language, preferred review style, preferred notification cadence, preferred tool for a task.",
402
- "- Do not supersede when the candidate is just more specific, an additional preference, a different task/context, or the same value phrased differently.",
403
- "- Do not supersede memories from different topics even if they are both preferences.",
404
- "- Only return ids that appear in existing-memories.",
405
- "- If unsure, return uncertain.",
406
- "</rules>",
407
- "</memory-supersession-input>"
408
- ].join("\n");
389
+ function boundedLimit(value, fallback) {
390
+ if (typeof value !== "number" || !Number.isFinite(value)) {
391
+ return fallback;
392
+ }
393
+ return Math.min(200, Math.max(1, Math.floor(value)));
409
394
  }
410
- function createMemoryAgent(model) {
411
- return {
412
- async adjudicateSupersession(rawRequest) {
413
- const request = supersessionRequestSchema.parse(
414
- rawRequest
415
- );
416
- const result = await model.completeObject({
417
- schema: supersessionDecisionResponseSchema,
418
- system: MEMORY_SUPERSESSION_SYSTEM,
419
- prompt: supersessionPrompt(request),
420
- maxTokens: 400
421
- });
422
- return supersessionDecisionResponseSchema.parse(
423
- result.object
424
- );
425
- },
426
- async extractSessionMemories(rawRequest) {
427
- const request = extractSessionRequestSchema.parse(rawRequest);
428
- const result = await model.completeObject({
429
- schema: extractMemoriesResponseSchema,
430
- system: MEMORY_EXTRACTION_SYSTEM,
431
- prompt: sessionExtractionPrompt(request),
432
- maxTokens: 1e3
433
- });
434
- return extractedMemoriesFromResponse(
435
- extractMemoriesResponseSchema.parse(result.object)
436
- );
437
- },
438
- async reviewCreateRequest(rawRequest) {
439
- const request = parseCreateMemoryRequest(rawRequest);
440
- const result = await model.completeObject({
441
- schema: memoryReviewResponseSchema,
442
- system: MEMORY_REVIEW_SYSTEM,
443
- prompt: reviewPrompt(request),
444
- maxTokens: 700
445
- });
446
- const response = memoryReviewResponseSchema.parse(result.object);
447
- return memoryReviewFromResponse(response);
448
- }
449
- };
395
+ function sourceKey(ctx) {
396
+ if (ctx.source.platform === "local") {
397
+ return ctx.source.conversationId;
398
+ }
399
+ const threadKey = ctx.source.threadTs ?? ctx.source.messageTs;
400
+ if (!threadKey) {
401
+ throw new Error(
402
+ "Memory source requires a Slack message or thread timestamp."
403
+ );
404
+ }
405
+ return `slack:${ctx.source.teamId}:${ctx.source.channelId}:${threadKey}`;
450
406
  }
451
- function memoryReviewFromResponse(response) {
452
- if (response.decision === "store") {
453
- return parseMemoryReview({
454
- decision: "store",
455
- kind: response.kind,
456
- content: response.canonicalFact,
457
- ...response.expiresAtMs !== null ? { expiresAtMs: response.expiresAtMs } : {}
458
- });
407
+ function sourceChannelPrefix(ctx) {
408
+ if (ctx.source.platform !== "slack") {
409
+ return void 0;
459
410
  }
460
- return parseMemoryReview({
461
- decision: "reject",
462
- reason: response.reason
411
+ return `slack:${ctx.source.teamId}:${ctx.source.channelId}:`;
412
+ }
413
+ function parseMemoryRow(row) {
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 !== void 0 ? { expiresAtMs: parsed.expiresAtMs } : {},
424
+ ...parsed.supersededAtMs !== void 0 ? { supersededAtMs: parsed.supersededAtMs } : {},
425
+ ...parsed.supersededById ? { supersededById: parsed.supersededById } : {},
426
+ ...parsed.archivedAtMs !== void 0 ? { archivedAtMs: parsed.archivedAtMs } : {},
427
+ ...parsed.archiveReason ? { archiveReason: parsed.archiveReason } : {}
463
428
  });
464
429
  }
465
- function extractedMemoriesFromResponse(response) {
466
- const toMemory = (memory) => parseExtractedMemory({
467
- content: memory.canonicalFact,
468
- expiresAtMs: memory.expiresAtMs,
469
- kind: memory.kind,
470
- evidenceMessageIndices: memory.evidenceMessageIndices
430
+ function visibleScopePredicate(scopes) {
431
+ if (scopes.length === 0) {
432
+ return void 0;
433
+ }
434
+ return or(
435
+ ...scopes.map(
436
+ (scope) => and(
437
+ eq(juniorMemoryMemories.scope, scope.scope),
438
+ eq(juniorMemoryMemories.scopeKey, scope.scopeKey)
439
+ )
440
+ )
441
+ );
442
+ }
443
+ function activeVisiblePredicate(args) {
444
+ const scopePredicate = visibleScopePredicate(args.scopes);
445
+ if (!scopePredicate) {
446
+ return void 0;
447
+ }
448
+ return and(
449
+ scopePredicate,
450
+ isNull(juniorMemoryMemories.archivedAtMs),
451
+ isNull(juniorMemoryMemories.supersededAtMs),
452
+ isNull(juniorMemoryMemories.supersededById),
453
+ or(
454
+ isNull(juniorMemoryMemories.expiresAtMs),
455
+ gt(juniorMemoryMemories.expiresAtMs, args.nowMs)
456
+ )
457
+ );
458
+ }
459
+ async function findByIdempotencyKey(args) {
460
+ const activeRows = await args.db.select().from(juniorMemoryMemories).where(
461
+ and(
462
+ eq(juniorMemoryMemories.scope, args.scope.scope),
463
+ eq(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
464
+ eq(juniorMemoryMemories.idempotencyKey, args.idempotencyKey),
465
+ isNull(juniorMemoryMemories.archivedAtMs),
466
+ isNull(juniorMemoryMemories.supersededAtMs),
467
+ isNull(juniorMemoryMemories.supersededById),
468
+ or(
469
+ isNull(juniorMemoryMemories.expiresAtMs),
470
+ gt(juniorMemoryMemories.expiresAtMs, args.nowMs)
471
+ )
472
+ )
473
+ ).limit(1);
474
+ if (activeRows[0]) {
475
+ return parseMemoryRow(activeRows[0]);
476
+ }
477
+ const aliasRows = await args.db.select({ supersededById: juniorMemoryMemories.supersededById }).from(juniorMemoryMemories).where(
478
+ and(
479
+ eq(juniorMemoryMemories.scope, args.scope.scope),
480
+ eq(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
481
+ eq(juniorMemoryMemories.idempotencyKey, args.idempotencyKey),
482
+ isNull(juniorMemoryMemories.archivedAtMs),
483
+ isNotNull(juniorMemoryMemories.supersededAtMs),
484
+ isNotNull(juniorMemoryMemories.supersededById),
485
+ or(
486
+ isNull(juniorMemoryMemories.expiresAtMs),
487
+ gt(juniorMemoryMemories.expiresAtMs, args.nowMs)
488
+ )
489
+ )
490
+ ).orderBy(
491
+ desc(juniorMemoryMemories.createdAtMs),
492
+ asc(juniorMemoryMemories.id)
493
+ );
494
+ for (const alias of aliasRows) {
495
+ if (!alias.supersededById) {
496
+ continue;
497
+ }
498
+ const rows = await args.db.select().from(juniorMemoryMemories).where(
499
+ and(
500
+ eq(juniorMemoryMemories.id, alias.supersededById),
501
+ eq(juniorMemoryMemories.scope, args.scope.scope),
502
+ eq(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
503
+ isNull(juniorMemoryMemories.archivedAtMs),
504
+ isNull(juniorMemoryMemories.supersededAtMs),
505
+ isNull(juniorMemoryMemories.supersededById),
506
+ or(
507
+ isNull(juniorMemoryMemories.expiresAtMs),
508
+ gt(juniorMemoryMemories.expiresAtMs, args.nowMs)
509
+ )
510
+ )
511
+ ).limit(1);
512
+ if (rows[0]) {
513
+ return parseMemoryRow(rows[0]);
514
+ }
515
+ }
516
+ return void 0;
517
+ }
518
+ async function archiveExpiredMemoryBatch(args) {
519
+ const scopePredicate = visibleScopePredicate(args.scopes);
520
+ if (!scopePredicate) {
521
+ return { archivedCount: 0 };
522
+ }
523
+ const predicates = [
524
+ scopePredicate,
525
+ isNull(juniorMemoryMemories.archivedAtMs),
526
+ isNull(juniorMemoryMemories.supersededAtMs),
527
+ isNull(juniorMemoryMemories.supersededById),
528
+ lte(juniorMemoryMemories.expiresAtMs, args.nowMs)
529
+ ];
530
+ if (args.idempotencyKey !== void 0) {
531
+ predicates.push(
532
+ eq(juniorMemoryMemories.idempotencyKey, args.idempotencyKey)
533
+ );
534
+ }
535
+ const archivedIds = await args.db.transaction(async (tx) => {
536
+ const expired = await tx.select({ id: juniorMemoryMemories.id }).from(juniorMemoryMemories).where(and(...predicates)).orderBy(
537
+ asc(juniorMemoryMemories.expiresAtMs),
538
+ asc(juniorMemoryMemories.id)
539
+ ).limit(boundedLimit(args.limit, DEFAULT_EXPIRED_ARCHIVE_LIMIT));
540
+ const ids = expired.map((row) => row.id);
541
+ if (ids.length === 0) {
542
+ return [];
543
+ }
544
+ const archived = await tx.update(juniorMemoryMemories).set({
545
+ archivedAtMs: args.nowMs,
546
+ archiveReason: "expired"
547
+ }).where(and(inArray(juniorMemoryMemories.id, ids), ...predicates)).returning({ id: juniorMemoryMemories.id });
548
+ const idsToClean = archived.map((row) => row.id);
549
+ if (idsToClean.length > 0) {
550
+ await tx.delete(juniorMemoryEmbeddings).where(inArray(juniorMemoryEmbeddings.memoryId, idsToClean));
551
+ }
552
+ return idsToClean;
471
553
  });
472
- return response.memories.map(toMemory);
554
+ return { archivedCount: archivedIds.length };
473
555
  }
474
- function parseExtractedMemory(memory) {
475
- return extractedMemoryResultSchema.parse(memory);
556
+ function searchScore(memory, terms) {
557
+ const haystack = memory.content.toLowerCase();
558
+ return terms.reduce(
559
+ (score, term) => score + (haystack.includes(term) ? 1 : 0),
560
+ 0
561
+ );
476
562
  }
477
- function parseMemoryReview(result) {
478
- return memoryReviewDecisionSchema.parse(result);
563
+ function searchTerms(query) {
564
+ return [
565
+ ...new Set(
566
+ query.toLowerCase().split(/[^a-z0-9_'-]+/).map((term) => term.trim()).filter((term) => term.length >= 2)
567
+ )
568
+ ];
479
569
  }
480
- function parseCreateMemoryRequest(request) {
481
- return createMemoryRequestSchema.parse(request);
570
+ async function embedOne(embedder, text2) {
571
+ const normalized = normalizeContent(text2);
572
+ if (!normalized) {
573
+ throw new Error("Embedding text is required.");
574
+ }
575
+ const result = embeddingResultSchema.parse(
576
+ await embedder.embedTexts({ texts: [normalized] })
577
+ );
578
+ if (result.vectors.length !== 1) {
579
+ throw new Error("Embedding provider returned an unexpected vector count.");
580
+ }
581
+ return {
582
+ model: result.model,
583
+ provider: result.provider,
584
+ vector: result.vectors[0]
585
+ };
482
586
  }
483
-
484
- // src/cli/search.ts
485
- import { InvalidArgumentError, Option } from "commander";
486
- import { and, desc, eq, gt, ilike, isNull, or } from "drizzle-orm";
487
-
488
- // src/db/schema.ts
489
- import { sql } from "drizzle-orm";
490
- import {
491
- bigint,
492
- check,
493
- index,
494
- integer,
495
- pgTable,
496
- text,
497
- uniqueIndex,
498
- vector
499
- } from "drizzle-orm/pg-core";
500
- var juniorMemoryMemories = pgTable(
501
- "junior_memory_memories",
502
- {
503
- id: text("id").primaryKey(),
504
- scope: text("scope", { enum: MEMORY_SCOPES }).notNull(),
505
- scopeKey: text("scope_key").notNull(),
506
- kind: text("type", { enum: MEMORY_KINDS }).notNull(),
507
- subjectType: text("subject_type", { enum: MEMORY_SUBJECT_TYPES }).notNull(),
508
- subjectKey: text("subject_key"),
509
- content: text("content").notNull(),
510
- sourcePlatform: text("source_platform", {
511
- enum: MEMORY_SOURCE_PLATFORMS
512
- }).notNull(),
513
- sourceKey: text("source_key").notNull(),
514
- idempotencyKey: text("idempotency_key"),
515
- observedAtMs: bigint("observed_at_ms", { mode: "number" }).notNull(),
516
- createdAtMs: bigint("created_at_ms", { mode: "number" }).notNull(),
517
- expiresAtMs: bigint("expires_at_ms", { mode: "number" }),
518
- supersededAtMs: bigint("superseded_at_ms", { mode: "number" }),
519
- supersededById: text("superseded_by_id"),
520
- archivedAtMs: bigint("archived_at_ms", { mode: "number" }),
521
- archiveReason: text("archive_reason")
522
- },
523
- (table) => [
524
- index("junior_memory_memories_visible_idx").on(table.scope, table.scopeKey, table.createdAtMs.desc(), table.id).where(
525
- sql`${table.archivedAtMs} IS NULL AND ${table.supersededAtMs} IS NULL AND ${table.supersededById} IS NULL`
526
- ),
527
- index("junior_memory_memories_expiration_idx").on(table.expiresAtMs).where(
528
- sql`${table.archivedAtMs} IS NULL AND ${table.expiresAtMs} IS NOT NULL`
529
- ),
530
- uniqueIndex("junior_memory_memories_idempotency_idx").on(table.scope, table.scopeKey, table.idempotencyKey).where(
531
- sql`${table.idempotencyKey} IS NOT NULL AND ${table.archivedAtMs} IS NULL AND ${table.supersededAtMs} IS NULL AND ${table.supersededById} IS NULL`
532
- ),
533
- check(
534
- "junior_memory_memories_scope_check",
535
- sql`${table.scope} IN ('personal', 'conversation')`
536
- ),
537
- check(
538
- "junior_memory_memories_kind_check",
539
- sql`${table.kind} IN (
540
- 'preference',
541
- 'procedure',
542
- 'knowledge'
543
- )`
544
- ),
545
- check(
546
- "junior_memory_memories_subject_type_check",
547
- sql`${table.subjectType} IN ('user', 'conversation', 'general')`
548
- ),
549
- check(
550
- "junior_memory_memories_subject_key_check",
551
- sql`(${table.subjectType} = 'general' AND ${table.subjectKey} IS NULL) OR (${table.subjectType} IN ('user', 'conversation') AND ${table.subjectKey} IS NOT NULL AND length(${table.subjectKey}) > 0)`
552
- ),
553
- check(
554
- "junior_memory_memories_source_platform_check",
555
- sql`${table.sourcePlatform} IN ('slack', 'local')`
556
- )
557
- ]
558
- );
559
- var juniorMemoryEmbeddings = pgTable(
560
- "junior_memory_embeddings",
561
- {
562
- memoryId: text("memory_id").primaryKey().references(() => juniorMemoryMemories.id, { onDelete: "cascade" }),
563
- provider: text("provider").notNull(),
564
- model: text("model").notNull(),
565
- dimensions: integer("dimensions").notNull(),
566
- metric: text("metric", { enum: MEMORY_EMBEDDING_METRICS }).notNull(),
567
- contentHash: text("content_hash").notNull(),
568
- embedding: vector("embedding", {
569
- dimensions: MEMORY_EMBEDDING_DIMENSIONS
570
- }).notNull(),
571
- createdAtMs: bigint("created_at_ms", { mode: "number" }).notNull()
572
- },
573
- (table) => [
574
- index("junior_memory_embeddings_model_idx").on(
575
- table.provider,
576
- table.model,
577
- table.dimensions,
578
- table.metric
579
- ),
580
- check(
581
- "junior_memory_embeddings_metric_check",
582
- sql`${table.metric} IN ('cosine')`
583
- ),
584
- check(
585
- "junior_memory_embeddings_dimensions_check",
586
- sql`${table.dimensions} = ${sql.raw(String(MEMORY_EMBEDDING_DIMENSIONS))}`
587
- )
588
- ]
589
- );
590
-
591
- // src/cli/format.ts
592
- function formatDate(ms) {
593
- return ms === null ? "-" : new Date(ms).toISOString();
594
- }
595
- function formatMemory(row, args) {
596
- const lines = [
597
- `id=${row.id}`,
598
- `scope=${row.scope}`,
599
- `scope_key=${row.scopeKey}`,
600
- `subject_type=${row.subjectType}`,
601
- ...row.subjectKey ? [`subject_key=${row.subjectKey}`] : [],
602
- `kind=${row.kind}`,
603
- `created_at=${formatDate(row.createdAtMs)}`,
604
- `observed_at=${formatDate(row.observedAtMs)}`,
605
- `expires_at=${formatDate(row.expiresAtMs)}`,
606
- `archived_at=${formatDate(row.archivedAtMs)}`
607
- ];
608
- if (args.showContent) {
609
- lines.push(`content=${row.content}`);
610
- }
611
- return lines.join("\n");
612
- }
613
-
614
- // src/cli/search.ts
615
- function parseLimit(value) {
616
- const parsed = Number(value);
617
- if (!Number.isFinite(parsed)) {
618
- throw new InvalidArgumentError("--limit must be a number");
587
+ async function storeEmbedding(args) {
588
+ if (!args.embedder && !args.embedding) {
589
+ return;
619
590
  }
620
- return Math.min(100, Math.max(1, Math.floor(parsed)));
621
- }
622
- async function runSearch(ctx, queryParts, options) {
623
- const query = (queryParts ?? []).join(" ").trim();
624
- const nowMs = Date.now();
625
- const terms = [
626
- ...new Set(
627
- query.toLowerCase().split(/[^a-z0-9_'-]+/).map((term) => term.trim()).filter((term) => term.length >= 2)
628
- )
629
- ];
630
- const db = ctx.db;
631
- const activeExpirationPredicate = or(
632
- isNull(juniorMemoryMemories.expiresAtMs),
633
- gt(juniorMemoryMemories.expiresAtMs, nowMs)
634
- );
635
- const predicates = [
636
- eq(juniorMemoryMemories.scope, options.scope),
637
- eq(juniorMemoryMemories.scopeKey, options.scopeKey),
638
- isNull(juniorMemoryMemories.archivedAtMs),
639
- isNull(juniorMemoryMemories.supersededAtMs),
640
- isNull(juniorMemoryMemories.supersededById)
641
- ];
642
- if (activeExpirationPredicate) {
643
- predicates.push(activeExpirationPredicate);
591
+ try {
592
+ const existing = await args.db.select({ memoryId: juniorMemoryEmbeddings.memoryId }).from(juniorMemoryEmbeddings).where(eq(juniorMemoryEmbeddings.memoryId, args.memoryId)).limit(1);
593
+ if (existing[0]) {
594
+ return;
595
+ }
596
+ } catch {
597
+ return;
644
598
  }
645
- if (terms.length > 0) {
646
- const termPredicate = or(
647
- ...terms.map((term) => ilike(juniorMemoryMemories.content, `%${term}%`))
648
- );
649
- if (termPredicate) {
650
- predicates.push(termPredicate);
599
+ let embedding;
600
+ if (args.embedding) {
601
+ embedding = args.embedding;
602
+ } else {
603
+ const embedder = args.embedder;
604
+ if (!embedder) {
605
+ return;
606
+ }
607
+ try {
608
+ embedding = await embedOne(embedder, args.content);
609
+ } catch {
610
+ return;
651
611
  }
652
612
  }
653
- const rows = await db.select().from(juniorMemoryMemories).where(and(...predicates)).orderBy(desc(juniorMemoryMemories.createdAtMs)).limit(options.limit);
654
- if (rows.length === 0) {
655
- await ctx.io.writeOutput("No memories matched.\n");
656
- return 0;
613
+ try {
614
+ await args.db.insert(juniorMemoryEmbeddings).values({
615
+ contentHash: hashEmbeddedContent(args.content),
616
+ createdAtMs: args.nowMs,
617
+ dimensions: MEMORY_EMBEDDING_DIMENSIONS,
618
+ embedding: embedding.vector,
619
+ memoryId: args.memoryId,
620
+ metric: EMBEDDING_METRIC,
621
+ model: embedding.model,
622
+ provider: embedding.provider
623
+ }).onConflictDoNothing();
624
+ } catch {
625
+ return;
657
626
  }
658
- await ctx.io.writeOutput(
659
- `${rows.map(
660
- (row) => formatMemory(row, { showContent: Boolean(options.showContent) })
661
- ).join("\n\n")}
662
- `
663
- );
664
- return 0;
665
627
  }
666
- function configureMemorySearchCommand(parent, junior) {
667
- parent.command("search").description("Search visible memories").argument("[query...]", "Search query").addOption(
668
- new Option("--scope <scope>", "Memory scope").choices([...MEMORY_SCOPES]).makeOptionMandatory()
669
- ).requiredOption("--scope-key <key>", "Scope key").addOption(
670
- new Option("--limit <n>", "Maximum rows").argParser(parseLimit).default(20)
671
- ).option("--show-content", "Print raw memory content").action(
672
- junior.action(async (ctx, queryParts, options) => {
673
- return await runSearch(
674
- ctx,
675
- queryParts,
676
- options
677
- );
678
- })
628
+ function activeScopedSubjectPredicate(args) {
629
+ const predicate = and(
630
+ eq(juniorMemoryMemories.scope, args.scope.scope),
631
+ eq(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
632
+ eq(juniorMemoryMemories.kind, args.kind),
633
+ eq(juniorMemoryMemories.subjectType, args.subject.subjectType),
634
+ args.subject.subjectKey === void 0 ? isNull(juniorMemoryMemories.subjectKey) : eq(juniorMemoryMemories.subjectKey, args.subject.subjectKey),
635
+ isNull(juniorMemoryMemories.archivedAtMs),
636
+ isNull(juniorMemoryMemories.supersededAtMs),
637
+ isNull(juniorMemoryMemories.supersededById),
638
+ or(
639
+ isNull(juniorMemoryMemories.expiresAtMs),
640
+ gt(juniorMemoryMemories.expiresAtMs, args.nowMs)
641
+ )
679
642
  );
680
- }
681
-
682
- // src/cli/show.ts
683
- import { eq as eq2 } from "drizzle-orm";
684
- async function runShow(ctx, id) {
685
- const db = ctx.db;
686
- const rows = await db.select().from(juniorMemoryMemories).where(eq2(juniorMemoryMemories.id, id)).limit(1);
687
- if (!rows[0]) {
688
- await ctx.io.writeError(`Memory not found: ${id}
689
- `);
690
- return 1;
643
+ if (!predicate) {
644
+ throw new Error("Memory duplicate predicate is empty.");
691
645
  }
692
- await ctx.io.writeOutput(`${formatMemory(rows[0], { showContent: true })}
693
- `);
694
- return 0;
646
+ return predicate;
695
647
  }
696
- function configureMemoryShowCommand(parent, junior) {
697
- parent.command("show").description("Show one memory").argument("<id>", "Memory id").action(
698
- junior.action(async (ctx, id) => {
699
- return await runShow(ctx, id);
700
- })
648
+ async function findExactDuplicateMemory(args) {
649
+ const rows = await args.db.select().from(juniorMemoryMemories).where(
650
+ and(
651
+ activeScopedSubjectPredicate(args),
652
+ eq(juniorMemoryMemories.content, args.content)
653
+ )
654
+ ).orderBy(
655
+ desc(juniorMemoryMemories.createdAtMs),
656
+ asc(juniorMemoryMemories.id)
657
+ ).limit(1);
658
+ return rows[0] ? parseMemoryRow(rows[0]) : void 0;
659
+ }
660
+ async function findVectorDuplicateMemory(args) {
661
+ const distance = cosineDistance(
662
+ juniorMemoryEmbeddings.embedding,
663
+ args.embedding.vector
701
664
  );
665
+ const rows = await args.db.select({
666
+ contentHash: juniorMemoryEmbeddings.contentHash,
667
+ distance,
668
+ memory: juniorMemoryMemories
669
+ }).from(juniorMemoryMemories).innerJoin(
670
+ juniorMemoryEmbeddings,
671
+ eq(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id)
672
+ ).where(
673
+ and(
674
+ activeScopedSubjectPredicate(args),
675
+ eq(juniorMemoryEmbeddings.provider, args.embedding.provider),
676
+ eq(juniorMemoryEmbeddings.model, args.embedding.model),
677
+ eq(juniorMemoryEmbeddings.dimensions, MEMORY_EMBEDDING_DIMENSIONS),
678
+ eq(juniorMemoryEmbeddings.metric, EMBEDDING_METRIC),
679
+ lte(distance, HIGH_CONFIDENCE_DUPLICATE_DISTANCE)
680
+ )
681
+ ).orderBy(
682
+ distance,
683
+ desc(juniorMemoryMemories.createdAtMs),
684
+ asc(juniorMemoryMemories.id)
685
+ ).limit(1);
686
+ const row = rows[0];
687
+ if (!row || hashEmbeddedContent(row.memory.content) !== row.contentHash) {
688
+ return void 0;
689
+ }
690
+ return parseMemoryRow(row.memory);
702
691
  }
703
-
704
- // src/cli/index.ts
705
- function createMemoryCliCommand() {
706
- return {
707
- name: "memory",
708
- summary: "Inspect Junior memory state",
709
- configure(command, junior) {
710
- configureMemorySearchCommand(command, junior);
711
- configureMemoryShowCommand(command, junior);
692
+ async function rememberDuplicateIdempotency(args) {
693
+ if (args.idempotencyKey === void 0) {
694
+ return;
695
+ }
696
+ await args.db.insert(juniorMemoryMemories).values({
697
+ content: args.content,
698
+ createdAtMs: args.nowMs,
699
+ expiresAtMs: args.duplicate.expiresAtMs,
700
+ id: idempotencyAliasId({
701
+ idempotencyKey: args.idempotencyKey,
702
+ scope: args.scope,
703
+ targetId: args.duplicate.id
704
+ }),
705
+ idempotencyKey: args.idempotencyKey,
706
+ observedAtMs: args.nowMs,
707
+ scope: args.scope.scope,
708
+ scopeKey: args.scope.scopeKey,
709
+ sourceKey: sourceKey(args.runtimeContext),
710
+ sourcePlatform: args.runtimeContext.source.platform,
711
+ subjectKey: args.subject.subjectKey,
712
+ subjectType: args.subject.subjectType,
713
+ supersededAtMs: args.nowMs,
714
+ supersededById: args.duplicate.id,
715
+ kind: args.duplicate.kind
716
+ }).onConflictDoNothing();
717
+ }
718
+ async function listPreferenceAdjudicationCandidates(args) {
719
+ const vectorCandidates = args.embedding ? await listVectorPreferenceAdjudicationCandidates({
720
+ db: args.db,
721
+ embedding: args.embedding,
722
+ nowMs: args.nowMs,
723
+ scope: args.scope,
724
+ subject: args.subject
725
+ }) : [];
726
+ const recentCandidates = (await args.db.select().from(juniorMemoryMemories).where(
727
+ activeScopedSubjectPredicate({
728
+ ...args,
729
+ kind: "preference"
730
+ })
731
+ ).orderBy(
732
+ desc(juniorMemoryMemories.createdAtMs),
733
+ asc(juniorMemoryMemories.id)
734
+ ).limit(PREFERENCE_ADJUDICATION_CANDIDATE_LIMIT)).map(parseMemoryRow);
735
+ return [
736
+ ...new Map(
737
+ [...vectorCandidates, ...recentCandidates].map((memory) => [
738
+ memory.id,
739
+ memory
740
+ ])
741
+ ).values()
742
+ ].slice(0, PREFERENCE_ADJUDICATION_CANDIDATE_LIMIT);
743
+ }
744
+ async function listVectorPreferenceAdjudicationCandidates(args) {
745
+ const distance = cosineDistance(
746
+ juniorMemoryEmbeddings.embedding,
747
+ args.embedding.vector
748
+ );
749
+ const rows = await args.db.select({
750
+ contentHash: juniorMemoryEmbeddings.contentHash,
751
+ distance,
752
+ memory: juniorMemoryMemories
753
+ }).from(juniorMemoryMemories).innerJoin(
754
+ juniorMemoryEmbeddings,
755
+ eq(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id)
756
+ ).where(
757
+ and(
758
+ activeScopedSubjectPredicate({ ...args, kind: "preference" }),
759
+ eq(juniorMemoryEmbeddings.provider, args.embedding.provider),
760
+ eq(juniorMemoryEmbeddings.model, args.embedding.model),
761
+ eq(juniorMemoryEmbeddings.dimensions, MEMORY_EMBEDDING_DIMENSIONS),
762
+ eq(juniorMemoryEmbeddings.metric, EMBEDDING_METRIC)
763
+ )
764
+ ).orderBy(
765
+ distance,
766
+ desc(juniorMemoryMemories.createdAtMs),
767
+ asc(juniorMemoryMemories.id)
768
+ ).limit(PREFERENCE_ADJUDICATION_VECTOR_LIMIT);
769
+ return rows.flatMap((row) => {
770
+ if (hashEmbeddedContent(row.memory.content) !== row.contentHash) {
771
+ return [];
712
772
  }
713
- };
773
+ return [parseMemoryRow(row.memory)];
774
+ });
714
775
  }
715
-
716
- // src/tools.ts
717
- import { Type } from "@sinclair/typebox";
718
- import { Value } from "@sinclair/typebox/value";
719
- import {
720
- definePluginTool,
721
- getSourceKey,
722
- PluginToolInputError,
723
- pluginToolResultSchema
724
- } from "@sentry/junior-plugin-api";
725
- import { z as z4 } from "zod";
726
-
727
- // src/store.ts
728
- import { createHash, randomUUID } from "crypto";
729
- import {
730
- and as and2,
731
- asc,
732
- desc as desc2,
733
- eq as eq3,
734
- gt as gt2,
735
- ilike as ilike2,
736
- inArray,
737
- isNull as isNull2,
738
- isNotNull,
739
- like,
740
- lte,
741
- or as or2,
742
- sql as sql2
743
- } from "drizzle-orm";
744
- import { cosineDistance } from "drizzle-orm/sql/functions";
745
- import { z as z3 } from "zod";
746
-
747
- // src/scope.ts
748
- import { isPrivateSource } from "@sentry/junior-plugin-api";
749
- function sourceConversationKey(ctx) {
750
- if (ctx.source.platform === "local") {
751
- return ctx.source.conversationId;
752
- }
753
- if (!isPrivateSource(ctx.source)) {
754
- return `slack:${ctx.source.teamId}`;
755
- }
756
- const threadKey = ctx.source.threadTs ?? ctx.source.messageTs;
757
- if (!threadKey) {
758
- return void 0;
776
+ async function adjudicatePreferenceCandidate(args) {
777
+ const [firstCandidate, ...remainingCandidates] = args.candidates;
778
+ if (!firstCandidate) {
779
+ return { decision: "create" };
780
+ }
781
+ const existingMemories = [
782
+ { content: firstCandidate.content, id: firstCandidate.id },
783
+ ...remainingCandidates.map((memory) => ({
784
+ content: memory.content,
785
+ id: memory.id
786
+ }))
787
+ ];
788
+ const candidateIds = new Set(args.candidates.map((memory) => memory.id));
789
+ try {
790
+ const decision = await args.decider.adjudicateSupersession({
791
+ candidate: {
792
+ content: args.content,
793
+ kind: "preference"
794
+ },
795
+ existingMemories,
796
+ runtimeContext: args.runtimeContext
797
+ });
798
+ if (decision.decision === "duplicate") {
799
+ const memory = args.candidates.find(
800
+ (candidate) => candidate.id === decision.duplicateId
801
+ );
802
+ return memory ? { decision: "duplicate", memory } : { decision: "create" };
803
+ }
804
+ if (decision.decision === "supersedes_old") {
805
+ const ids = decision.supersededIds.filter((id) => candidateIds.has(id));
806
+ const [firstId, ...remainingIds] = ids;
807
+ return firstId ? { decision: "supersede", ids: [firstId, ...remainingIds] } : { decision: "create" };
808
+ }
809
+ return { decision: "create" };
810
+ } catch {
811
+ return { decision: "create" };
759
812
  }
760
- return `slack:${ctx.source.teamId}:${ctx.source.channelId}:${threadKey}`;
761
813
  }
762
- function actorScopeKey(ctx) {
763
- const actor = ctx.actor;
764
- if (!actor?.userId) {
765
- return void 0;
766
- }
767
- if (actor.platform === "slack") {
768
- return `slack:${actor.teamId}:${actor.userId}`;
814
+ async function listVisibleMemories(args) {
815
+ const predicate = activeVisiblePredicate(args);
816
+ if (!predicate) {
817
+ return [];
769
818
  }
770
- return `local:${actor.userId}`;
819
+ const limit = boundedLimit(args.limit, DEFAULT_LIST_LIMIT);
820
+ const rows = await args.db.select().from(juniorMemoryMemories).where(predicate).orderBy(
821
+ desc(juniorMemoryMemories.createdAtMs),
822
+ asc(juniorMemoryMemories.id)
823
+ ).limit(limit);
824
+ return rows.map(parseMemoryRow);
771
825
  }
772
- function deriveMemoryScope(ctx, scope) {
773
- if (scope === "personal") {
774
- const scopeKey2 = actorScopeKey(ctx);
775
- if (!scopeKey2) {
776
- throw new Error("Personal memory requires actor context.");
777
- }
778
- return { scope, scopeKey: scopeKey2 };
826
+ async function searchVisibleMemories(args) {
827
+ const terms = searchTerms(args.query);
828
+ if (terms.length === 0) {
829
+ return [];
779
830
  }
780
- const scopeKey = sourceConversationKey(ctx);
781
- if (!scopeKey) {
782
- throw new Error("Conversation memory requires conversation context.");
831
+ const predicate = activeVisiblePredicate(args);
832
+ if (!predicate) {
833
+ return [];
783
834
  }
784
- return { scope, scopeKey };
835
+ const rows = await args.db.select().from(juniorMemoryMemories).where(
836
+ and(
837
+ predicate,
838
+ or(
839
+ ...terms.map(
840
+ (term) => ilike(juniorMemoryMemories.content, `%${term}%`)
841
+ )
842
+ )
843
+ )
844
+ );
845
+ return rows.map((row) => ({
846
+ memory: parseMemoryRow(row),
847
+ score: 0,
848
+ sourceKey: row.sourceKey
849
+ }));
785
850
  }
786
- function deriveMemorySubject(ctx, scope) {
787
- if (scope.scope === "personal") {
788
- const subjectKey2 = actorScopeKey(ctx);
789
- if (!subjectKey2) {
790
- throw new Error("User-subject memory requires actor context.");
791
- }
792
- return { subjectType: "user", subjectKey: subjectKey2 };
851
+ async function searchVisibleVectorMemories(args) {
852
+ if (!args.embedder) {
853
+ return [];
793
854
  }
794
- const subjectKey = sourceConversationKey(ctx);
795
- if (!subjectKey) {
796
- throw new Error(
797
- "Conversation-subject memory requires conversation context."
798
- );
855
+ const predicate = activeVisiblePredicate(args);
856
+ if (!predicate) {
857
+ return [];
799
858
  }
800
- return { subjectType: "conversation", subjectKey };
801
- }
802
- function deriveVisibleMemoryScopes(ctx) {
803
- const scopes = [];
859
+ let embedding;
804
860
  try {
805
- scopes.push(deriveMemoryScope(ctx, "personal"));
861
+ embedding = await embedOne(args.embedder, args.query);
806
862
  } catch {
863
+ return [];
807
864
  }
808
- try {
809
- scopes.push(deriveMemoryScope(ctx, "conversation"));
810
- } catch {
865
+ const distance = cosineDistance(
866
+ juniorMemoryEmbeddings.embedding,
867
+ embedding.vector
868
+ );
869
+ const rows = await args.db.select({
870
+ contentHash: juniorMemoryEmbeddings.contentHash,
871
+ distance,
872
+ memory: juniorMemoryMemories
873
+ }).from(juniorMemoryMemories).innerJoin(
874
+ juniorMemoryEmbeddings,
875
+ eq(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id)
876
+ ).where(
877
+ and(
878
+ predicate,
879
+ eq(juniorMemoryEmbeddings.provider, embedding.provider),
880
+ eq(juniorMemoryEmbeddings.model, embedding.model),
881
+ eq(juniorMemoryEmbeddings.dimensions, MEMORY_EMBEDDING_DIMENSIONS),
882
+ eq(juniorMemoryEmbeddings.metric, EMBEDDING_METRIC)
883
+ )
884
+ ).orderBy(
885
+ distance,
886
+ desc(juniorMemoryMemories.createdAtMs),
887
+ asc(juniorMemoryMemories.id)
888
+ ).limit(args.limit);
889
+ return rows.flatMap((row) => {
890
+ const distanceValue = Number(row.distance);
891
+ if (row.distance === null || !Number.isFinite(distanceValue) || hashEmbeddedContent(row.memory.content) !== row.contentHash) {
892
+ return [];
893
+ }
894
+ if (args.maxDistance !== void 0 && distanceValue > args.maxDistance) {
895
+ return [];
896
+ }
897
+ return [
898
+ {
899
+ memory: parseMemoryRow(row.memory),
900
+ score: 1 / (1 + Math.max(0, distanceValue)),
901
+ sourceKey: row.memory.sourceKey
902
+ }
903
+ ];
904
+ });
905
+ }
906
+ function sourceBoost(candidate, currentChannelPrefix) {
907
+ if (!currentChannelPrefix) {
908
+ return 0;
811
909
  }
812
- return scopes;
910
+ return candidate.sourceKey.startsWith(currentChannelPrefix) ? SOURCE_CHANNEL_BOOST : 0;
813
911
  }
814
-
815
- // src/store.ts
816
- var DEFAULT_LIST_LIMIT = 50;
817
- var DEFAULT_SEARCH_LIMIT = 10;
818
- var DEFAULT_EXPIRED_ARCHIVE_LIMIT = 100;
819
- var HIGH_CONFIDENCE_DUPLICATE_DISTANCE = 0.015;
820
- var SUPERSESSION_CANDIDATE_LIMIT = 10;
821
- var VECTOR_SEARCH_OVERFETCH = 4;
822
- var MAX_MEMORY_CONTENT_CHARS = 4e3;
823
- var EMBEDDING_METRIC = "cosine";
824
- var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
825
- var RELEVANCE_NEAR_TIE_DELTA = 0.01;
826
- var SOURCE_CHANNEL_BOOST = 0.05;
827
- var SUPERSESSION_STOP_TERMS = /* @__PURE__ */ new Set([
828
- "and",
829
- "for",
830
- "prefer",
831
- "prefers",
832
- "preference",
833
- "the",
834
- "use",
835
- "uses",
836
- "with"
837
- ]);
838
- var nonEmptyStringSchema2 = z3.string().min(1);
839
- var memoryContentSchema = z3.string().refine((content) => content.trim().length > 0, {
840
- message: "Memory content is required."
841
- });
842
- var numberSchema = z3.number().finite();
843
- var createMemoryInputSchema = z3.object({
844
- content: memoryContentSchema,
845
- expiresAtMs: numberSchema.optional(),
846
- idempotencyKey: nonEmptyStringSchema2,
847
- kind: z3.enum(MEMORY_KINDS)
848
- }).strict();
849
- var listMemoriesInputSchema = z3.object({
850
- limit: numberSchema.optional()
851
- }).strict();
852
- var searchMemoriesInputSchema = z3.object({
853
- limit: numberSchema.optional(),
854
- query: nonEmptyStringSchema2
855
- }).strict();
856
- var archiveMemoryInputSchema = z3.object({
857
- id: nonEmptyStringSchema2,
858
- reason: nonEmptyStringSchema2.optional()
859
- }).strict();
860
- var archiveExpiredMemoriesInputSchema = z3.object({
861
- limit: numberSchema.optional()
862
- }).strict();
863
- var clockSchema = z3.function({ input: [], output: numberSchema }).optional();
864
- var memoryStoreOptionsSchema = z3.object({
865
- now: clockSchema
866
- }).strict();
867
- var optionalNumberSchema = z3.preprocess(
868
- (value) => value === null ? void 0 : value,
869
- z3.coerce.number().optional()
870
- );
871
- var optionalStringSchema = z3.preprocess(
872
- (value) => value === null ? void 0 : value,
873
- z3.string().optional()
874
- );
875
- var optionalNonEmptyStringSchema = z3.preprocess(
876
- (value) => value === null ? void 0 : value,
877
- z3.string().min(1).optional()
878
- );
879
- var memoryRowSchema = z3.object({
880
- archivedAtMs: optionalNumberSchema,
881
- archiveReason: optionalStringSchema,
882
- content: memoryContentSchema,
883
- createdAtMs: z3.coerce.number(),
884
- expiresAtMs: optionalNumberSchema,
885
- id: z3.string().min(1),
886
- idempotencyKey: optionalStringSchema,
887
- observedAtMs: z3.coerce.number(),
888
- scope: z3.enum(MEMORY_SCOPES),
889
- scopeKey: z3.string().min(1),
890
- sourceKey: z3.string().min(1),
891
- sourcePlatform: z3.enum(MEMORY_SOURCE_PLATFORMS),
892
- subjectKey: optionalNonEmptyStringSchema,
893
- subjectType: z3.enum(MEMORY_SUBJECT_TYPES),
894
- supersededAtMs: optionalNumberSchema,
895
- supersededById: optionalStringSchema,
896
- kind: z3.enum(MEMORY_KINDS)
897
- }).strict().superRefine((row, ctx) => {
898
- if (row.subjectType === "general") {
899
- if (row.subjectKey !== void 0) {
900
- ctx.addIssue({
901
- code: "custom",
902
- message: "General-subject memory rows must not have a subject key.",
903
- path: ["subjectKey"]
904
- });
905
- }
906
- return;
907
- }
908
- if (row.subjectKey === void 0) {
909
- ctx.addIssue({
910
- code: "custom",
911
- message: "User and conversation memory rows require a subject key.",
912
- path: ["subjectKey"]
913
- });
914
- }
915
- });
916
- var memoryRecordSchema = z3.object({
917
- archivedAtMs: numberSchema.optional(),
918
- archiveReason: nonEmptyStringSchema2.optional(),
919
- content: memoryContentSchema,
920
- createdAtMs: numberSchema,
921
- expiresAtMs: numberSchema.optional(),
922
- id: nonEmptyStringSchema2,
923
- observedAtMs: numberSchema,
924
- scope: z3.enum(MEMORY_SCOPES),
925
- subjectType: z3.enum(MEMORY_SUBJECT_TYPES),
926
- supersededAtMs: numberSchema.optional(),
927
- supersededById: nonEmptyStringSchema2.optional(),
928
- kind: z3.enum(MEMORY_KINDS)
929
- }).strict();
930
- var embeddingVectorSchema = z3.array(numberSchema).length(MEMORY_EMBEDDING_DIMENSIONS);
931
- var embeddingResultSchema = z3.object({
932
- dimensions: z3.literal(MEMORY_EMBEDDING_DIMENSIONS),
933
- model: nonEmptyStringSchema2,
934
- provider: nonEmptyStringSchema2,
935
- vectors: z3.array(embeddingVectorSchema)
936
- }).strict();
937
- function normalizeContent(content) {
938
- return content.replace(/\s+/g, " ").trim();
939
- }
940
- function hashEmbeddedContent(content) {
941
- return createHash("sha256").update(content, "utf8").digest("hex");
942
- }
943
- function idempotencyAliasId(args) {
944
- return `alias:${createHash("sha256").update(args.scope.scope).update("\0").update(args.scope.scopeKey).update("\0").update(args.idempotencyKey).update("\0").update(args.targetId).digest("hex")}`;
945
- }
946
- function boundedLimit(value, fallback) {
947
- if (typeof value !== "number" || !Number.isFinite(value)) {
948
- return fallback;
949
- }
950
- return Math.min(200, Math.max(1, Math.floor(value)));
951
- }
952
- function sourceKey(ctx) {
953
- if (ctx.source.platform === "local") {
954
- return ctx.source.conversationId;
955
- }
956
- const threadKey = ctx.source.threadTs ?? ctx.source.messageTs;
957
- if (!threadKey) {
958
- throw new Error(
959
- "Memory source requires a Slack message or thread timestamp."
960
- );
961
- }
962
- return `slack:${ctx.source.teamId}:${ctx.source.channelId}:${threadKey}`;
963
- }
964
- function sourceChannelPrefix(ctx) {
965
- if (ctx.source.platform !== "slack") {
966
- return void 0;
912
+ function observedAgeBoost(memory, nowMs) {
913
+ const ageMs = Math.max(0, nowMs - memory.observedAtMs);
914
+ if (ageMs <= 7 * ONE_DAY_MS) {
915
+ return 0.15;
967
916
  }
968
- return `slack:${ctx.source.teamId}:${ctx.source.channelId}:`;
969
- }
970
- function parseMemoryRow(row) {
971
- const parsed = memoryRowSchema.parse(row);
972
- return memoryRecordSchema.parse({
973
- id: parsed.id,
974
- scope: parsed.scope,
975
- kind: parsed.kind,
976
- subjectType: parsed.subjectType,
977
- content: parsed.content,
978
- observedAtMs: parsed.observedAtMs,
979
- createdAtMs: parsed.createdAtMs,
980
- ...parsed.expiresAtMs !== void 0 ? { expiresAtMs: parsed.expiresAtMs } : {},
981
- ...parsed.supersededAtMs !== void 0 ? { supersededAtMs: parsed.supersededAtMs } : {},
982
- ...parsed.supersededById ? { supersededById: parsed.supersededById } : {},
983
- ...parsed.archivedAtMs !== void 0 ? { archivedAtMs: parsed.archivedAtMs } : {},
984
- ...parsed.archiveReason ? { archiveReason: parsed.archiveReason } : {}
985
- });
986
- }
987
- function visibleScopePredicate(scopes) {
988
- if (scopes.length === 0) {
989
- return void 0;
917
+ if (ageMs <= 30 * ONE_DAY_MS) {
918
+ return 0.1;
990
919
  }
991
- return or2(
992
- ...scopes.map(
993
- (scope) => and2(
994
- eq3(juniorMemoryMemories.scope, scope.scope),
995
- eq3(juniorMemoryMemories.scopeKey, scope.scopeKey)
996
- )
997
- )
998
- );
999
- }
1000
- function activeVisiblePredicate(args) {
1001
- const scopePredicate = visibleScopePredicate(args.scopes);
1002
- if (!scopePredicate) {
1003
- return void 0;
920
+ if (ageMs <= 90 * ONE_DAY_MS) {
921
+ return 0.05;
1004
922
  }
1005
- return and2(
1006
- scopePredicate,
1007
- isNull2(juniorMemoryMemories.archivedAtMs),
1008
- isNull2(juniorMemoryMemories.supersededAtMs),
1009
- isNull2(juniorMemoryMemories.supersededById),
1010
- or2(
1011
- isNull2(juniorMemoryMemories.expiresAtMs),
1012
- gt2(juniorMemoryMemories.expiresAtMs, args.nowMs)
1013
- )
1014
- );
923
+ return 0;
1015
924
  }
1016
- async function findByIdempotencyKey(args) {
1017
- const activeRows = await args.db.select().from(juniorMemoryMemories).where(
1018
- and2(
1019
- eq3(juniorMemoryMemories.scope, args.scope.scope),
1020
- eq3(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
1021
- eq3(juniorMemoryMemories.idempotencyKey, args.idempotencyKey),
1022
- isNull2(juniorMemoryMemories.archivedAtMs),
1023
- isNull2(juniorMemoryMemories.supersededAtMs),
1024
- isNull2(juniorMemoryMemories.supersededById),
1025
- or2(
1026
- isNull2(juniorMemoryMemories.expiresAtMs),
1027
- gt2(juniorMemoryMemories.expiresAtMs, args.nowMs)
1028
- )
1029
- )
1030
- ).limit(1);
1031
- if (activeRows[0]) {
1032
- return parseMemoryRow(activeRows[0]);
1033
- }
1034
- const aliasRows = await args.db.select({ supersededById: juniorMemoryMemories.supersededById }).from(juniorMemoryMemories).where(
1035
- and2(
1036
- eq3(juniorMemoryMemories.scope, args.scope.scope),
1037
- eq3(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
1038
- eq3(juniorMemoryMemories.idempotencyKey, args.idempotencyKey),
1039
- isNull2(juniorMemoryMemories.archivedAtMs),
1040
- isNotNull(juniorMemoryMemories.supersededAtMs),
1041
- isNotNull(juniorMemoryMemories.supersededById),
1042
- or2(
1043
- isNull2(juniorMemoryMemories.expiresAtMs),
1044
- gt2(juniorMemoryMemories.expiresAtMs, args.nowMs)
1045
- )
1046
- )
1047
- ).orderBy(
1048
- desc2(juniorMemoryMemories.createdAtMs),
1049
- asc(juniorMemoryMemories.id)
1050
- );
1051
- for (const alias of aliasRows) {
1052
- if (!alias.supersededById) {
925
+ function mergeSearchCandidates(candidates, nowMs, currentChannelKey) {
926
+ const byId = /* @__PURE__ */ new Map();
927
+ for (const candidate of candidates) {
928
+ const existing = byId.get(candidate.memory.id);
929
+ if (existing) {
930
+ existing.score += candidate.score;
1053
931
  continue;
1054
932
  }
1055
- const rows = await args.db.select().from(juniorMemoryMemories).where(
1056
- and2(
1057
- eq3(juniorMemoryMemories.id, alias.supersededById),
1058
- eq3(juniorMemoryMemories.scope, args.scope.scope),
1059
- eq3(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
1060
- isNull2(juniorMemoryMemories.archivedAtMs),
1061
- isNull2(juniorMemoryMemories.supersededAtMs),
1062
- isNull2(juniorMemoryMemories.supersededById),
1063
- or2(
1064
- isNull2(juniorMemoryMemories.expiresAtMs),
1065
- gt2(juniorMemoryMemories.expiresAtMs, args.nowMs)
1066
- )
1067
- )
1068
- ).limit(1);
1069
- if (rows[0]) {
1070
- return parseMemoryRow(rows[0]);
1071
- }
1072
- }
1073
- return void 0;
1074
- }
1075
- async function archiveExpiredMemoryBatch(args) {
1076
- const scopePredicate = visibleScopePredicate(args.scopes);
1077
- if (!scopePredicate) {
1078
- return { archivedCount: 0 };
1079
- }
1080
- const predicates = [
1081
- scopePredicate,
1082
- isNull2(juniorMemoryMemories.archivedAtMs),
1083
- isNull2(juniorMemoryMemories.supersededAtMs),
1084
- isNull2(juniorMemoryMemories.supersededById),
1085
- lte(juniorMemoryMemories.expiresAtMs, args.nowMs)
1086
- ];
1087
- if (args.idempotencyKey !== void 0) {
1088
- predicates.push(
1089
- eq3(juniorMemoryMemories.idempotencyKey, args.idempotencyKey)
1090
- );
933
+ byId.set(candidate.memory.id, {
934
+ memory: candidate.memory,
935
+ score: candidate.score,
936
+ sourceKey: candidate.sourceKey
937
+ });
1091
938
  }
1092
- const archivedIds = await args.db.transaction(async (tx) => {
1093
- const expired = await tx.select({ id: juniorMemoryMemories.id }).from(juniorMemoryMemories).where(and2(...predicates)).orderBy(
1094
- asc(juniorMemoryMemories.expiresAtMs),
1095
- asc(juniorMemoryMemories.id)
1096
- ).limit(boundedLimit(args.limit, DEFAULT_EXPIRED_ARCHIVE_LIMIT));
1097
- const ids = expired.map((row) => row.id);
1098
- if (ids.length === 0) {
1099
- return [];
939
+ return [...byId.values()].sort((left, right) => {
940
+ const scoreDelta = right.score - left.score;
941
+ if (Math.abs(scoreDelta) > RELEVANCE_NEAR_TIE_DELTA) {
942
+ return scoreDelta;
1100
943
  }
1101
- const archived = await tx.update(juniorMemoryMemories).set({
1102
- archivedAtMs: args.nowMs,
1103
- archiveReason: "expired"
1104
- }).where(and2(inArray(juniorMemoryMemories.id, ids), ...predicates)).returning({ id: juniorMemoryMemories.id });
1105
- const idsToClean = archived.map((row) => row.id);
1106
- if (idsToClean.length > 0) {
1107
- await tx.delete(juniorMemoryEmbeddings).where(inArray(juniorMemoryEmbeddings.memoryId, idsToClean));
944
+ const sourceDelta = sourceBoost(right, currentChannelKey) - sourceBoost(left, currentChannelKey);
945
+ if (sourceDelta !== 0) {
946
+ return sourceDelta;
1108
947
  }
1109
- return idsToClean;
1110
- });
1111
- return { archivedCount: archivedIds.length };
1112
- }
1113
- function searchScore(memory, terms) {
1114
- const haystack = memory.content.toLowerCase();
1115
- return terms.reduce(
1116
- (score, term) => score + (haystack.includes(term) ? 1 : 0),
1117
- 0
1118
- );
1119
- }
1120
- function searchTerms(query) {
1121
- return [
1122
- ...new Set(
1123
- query.toLowerCase().split(/[^a-z0-9_'-]+/).map((term) => term.trim()).filter((term) => term.length >= 2)
1124
- )
1125
- ];
948
+ return observedAgeBoost(right.memory, nowMs) - observedAgeBoost(left.memory, nowMs) || right.memory.observedAtMs - left.memory.observedAtMs || left.memory.id.localeCompare(right.memory.id);
949
+ }).map((candidate) => candidate.memory);
1126
950
  }
1127
- async function embedOne(embedder, text2) {
1128
- const normalized = normalizeContent(text2);
1129
- if (!normalized) {
1130
- throw new Error("Embedding text is required.");
951
+ function createMemoryStore(db, context, options = {}) {
952
+ const runtimeContext = memoryRuntimeContextSchema.parse(context);
953
+ const parsedOptions = memoryStoreOptionsSchema.parse({ now: options.now });
954
+ const embedder = options.embedder;
955
+ const maxVectorDistance = options.maxVectorDistance;
956
+ const supersessionDecider = options.supersessionDecider;
957
+ const getNowMs = parsedOptions.now ?? Date.now;
958
+ async function archiveExpiredVisibleMemories(input, nowMs) {
959
+ input = archiveExpiredMemoriesInputSchema.parse(input ?? {});
960
+ return await archiveExpiredMemoryBatch({
961
+ db,
962
+ limit: input.limit,
963
+ nowMs,
964
+ scopes: deriveVisibleMemoryScopes(runtimeContext)
965
+ });
1131
966
  }
1132
- const result = embeddingResultSchema.parse(
1133
- await embedder.embedTexts({ texts: [normalized] })
1134
- );
1135
- if (result.vectors.length !== 1) {
1136
- throw new Error("Embedding provider returned an unexpected vector count.");
967
+ async function reuseDuplicateMemory(args) {
968
+ await rememberDuplicateIdempotency({
969
+ ...args,
970
+ db,
971
+ runtimeContext
972
+ });
973
+ await storeEmbedding({
974
+ content: args.duplicate.content,
975
+ db,
976
+ embedder,
977
+ memoryId: args.duplicate.id,
978
+ nowMs: args.nowMs
979
+ });
980
+ return { created: false, memory: args.duplicate };
1137
981
  }
1138
- return {
1139
- model: result.model,
1140
- provider: result.provider,
1141
- vector: result.vectors[0]
1142
- };
1143
- }
1144
- async function storeEmbedding(args) {
1145
- if (!args.embedder && !args.embedding) {
1146
- return;
1147
- }
1148
- try {
1149
- const existing = await args.db.select({ memoryId: juniorMemoryEmbeddings.memoryId }).from(juniorMemoryEmbeddings).where(eq3(juniorMemoryEmbeddings.memoryId, args.memoryId)).limit(1);
1150
- if (existing[0]) {
1151
- return;
982
+ async function createScopedMemory(rawInput, scopeKind) {
983
+ const input = createMemoryInputSchema.parse(rawInput);
984
+ const nowMs = getNowMs();
985
+ const content = normalizeContent(input.content);
986
+ const scope = deriveMemoryScope(runtimeContext, scopeKind);
987
+ const subject = deriveMemorySubject(runtimeContext, scope);
988
+ if (content.length > MAX_MEMORY_CONTENT_CHARS) {
989
+ throw new Error("Memory content exceeds the maximum length.");
1152
990
  }
1153
- } catch {
1154
- return;
1155
- }
1156
- let embedding;
1157
- if (args.embedding) {
1158
- embedding = args.embedding;
1159
- } else {
1160
- const embedder = args.embedder;
1161
- if (!embedder) {
1162
- return;
991
+ await archiveExpiredMemoryBatch({
992
+ db,
993
+ nowMs,
994
+ scopes: [scope]
995
+ });
996
+ await archiveExpiredMemoryBatch({
997
+ db,
998
+ idempotencyKey: input.idempotencyKey,
999
+ limit: 1,
1000
+ nowMs,
1001
+ scopes: [scope]
1002
+ });
1003
+ if (input.idempotencyKey !== void 0) {
1004
+ const idempotent2 = await findByIdempotencyKey({
1005
+ db,
1006
+ idempotencyKey: input.idempotencyKey,
1007
+ nowMs,
1008
+ scope
1009
+ });
1010
+ if (idempotent2) {
1011
+ await storeEmbedding({
1012
+ content: idempotent2.content,
1013
+ db,
1014
+ embedder,
1015
+ memoryId: idempotent2.id,
1016
+ nowMs
1017
+ });
1018
+ return { created: false, memory: idempotent2 };
1019
+ }
1163
1020
  }
1164
- try {
1165
- embedding = await embedOne(embedder, args.content);
1166
- } catch {
1167
- return;
1021
+ const exactDuplicate = await findExactDuplicateMemory({
1022
+ content,
1023
+ db,
1024
+ kind: input.kind,
1025
+ nowMs,
1026
+ scope,
1027
+ subject
1028
+ });
1029
+ if (exactDuplicate) {
1030
+ return await reuseDuplicateMemory({
1031
+ content,
1032
+ duplicate: exactDuplicate,
1033
+ idempotencyKey: input.idempotencyKey,
1034
+ nowMs,
1035
+ scope,
1036
+ subject
1037
+ });
1168
1038
  }
1169
- }
1170
- try {
1171
- await args.db.insert(juniorMemoryEmbeddings).values({
1172
- contentHash: hashEmbeddedContent(args.content),
1173
- createdAtMs: args.nowMs,
1174
- dimensions: MEMORY_EMBEDDING_DIMENSIONS,
1175
- embedding: embedding.vector,
1176
- memoryId: args.memoryId,
1177
- metric: EMBEDDING_METRIC,
1178
- model: embedding.model,
1179
- provider: embedding.provider
1180
- }).onConflictDoNothing();
1181
- } catch {
1182
- return;
1183
- }
1184
- }
1185
- function activeScopedSubjectPredicate(args) {
1186
- const predicate = and2(
1187
- eq3(juniorMemoryMemories.scope, args.scope.scope),
1188
- eq3(juniorMemoryMemories.scopeKey, args.scope.scopeKey),
1189
- eq3(juniorMemoryMemories.kind, args.kind),
1190
- eq3(juniorMemoryMemories.subjectType, args.subject.subjectType),
1191
- args.subject.subjectKey === void 0 ? isNull2(juniorMemoryMemories.subjectKey) : eq3(juniorMemoryMemories.subjectKey, args.subject.subjectKey),
1192
- isNull2(juniorMemoryMemories.archivedAtMs),
1193
- isNull2(juniorMemoryMemories.supersededAtMs),
1194
- isNull2(juniorMemoryMemories.supersededById),
1195
- or2(
1196
- isNull2(juniorMemoryMemories.expiresAtMs),
1197
- gt2(juniorMemoryMemories.expiresAtMs, args.nowMs)
1198
- )
1199
- );
1200
- if (!predicate) {
1201
- throw new Error("Memory duplicate predicate is empty.");
1202
- }
1203
- return predicate;
1204
- }
1205
- async function findExactDuplicateMemory(args) {
1206
- const rows = await args.db.select().from(juniorMemoryMemories).where(
1207
- and2(
1208
- activeScopedSubjectPredicate(args),
1209
- eq3(juniorMemoryMemories.content, args.content)
1210
- )
1211
- ).orderBy(
1212
- desc2(juniorMemoryMemories.createdAtMs),
1213
- asc(juniorMemoryMemories.id)
1214
- ).limit(1);
1215
- return rows[0] ? parseMemoryRow(rows[0]) : void 0;
1216
- }
1217
- async function findVectorDuplicateMemory(args) {
1218
- const distance = cosineDistance(
1219
- juniorMemoryEmbeddings.embedding,
1220
- args.embedding.vector
1221
- );
1222
- const rows = await args.db.select({
1223
- contentHash: juniorMemoryEmbeddings.contentHash,
1224
- distance,
1225
- memory: juniorMemoryMemories
1226
- }).from(juniorMemoryMemories).innerJoin(
1227
- juniorMemoryEmbeddings,
1228
- eq3(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id)
1229
- ).where(
1230
- and2(
1231
- activeScopedSubjectPredicate(args),
1232
- eq3(juniorMemoryEmbeddings.provider, args.embedding.provider),
1233
- eq3(juniorMemoryEmbeddings.model, args.embedding.model),
1234
- eq3(juniorMemoryEmbeddings.dimensions, MEMORY_EMBEDDING_DIMENSIONS),
1235
- eq3(juniorMemoryEmbeddings.metric, EMBEDDING_METRIC),
1236
- lte(distance, HIGH_CONFIDENCE_DUPLICATE_DISTANCE)
1237
- )
1238
- ).orderBy(
1239
- distance,
1240
- desc2(juniorMemoryMemories.createdAtMs),
1241
- asc(juniorMemoryMemories.id)
1242
- ).limit(1);
1243
- const row = rows[0];
1244
- if (!row || hashEmbeddedContent(row.memory.content) !== row.contentHash) {
1245
- return void 0;
1246
- }
1247
- return parseMemoryRow(row.memory);
1248
- }
1249
- async function rememberDuplicateIdempotency(args) {
1250
- if (args.idempotencyKey === void 0) {
1251
- return;
1252
- }
1253
- await args.db.insert(juniorMemoryMemories).values({
1254
- content: args.content,
1255
- createdAtMs: args.nowMs,
1256
- expiresAtMs: args.duplicate.expiresAtMs,
1257
- id: idempotencyAliasId({
1258
- idempotencyKey: args.idempotencyKey,
1259
- scope: args.scope,
1260
- targetId: args.duplicate.id
1261
- }),
1262
- idempotencyKey: args.idempotencyKey,
1263
- observedAtMs: args.nowMs,
1264
- scope: args.scope.scope,
1265
- scopeKey: args.scope.scopeKey,
1266
- sourceKey: sourceKey(args.runtimeContext),
1267
- sourcePlatform: args.runtimeContext.source.platform,
1268
- subjectKey: args.subject.subjectKey,
1269
- subjectType: args.subject.subjectType,
1270
- supersededAtMs: args.nowMs,
1271
- supersededById: args.duplicate.id,
1272
- kind: args.duplicate.kind
1273
- }).onConflictDoNothing();
1274
- }
1275
- async function listSupersessionCandidates(args) {
1276
- const predicate = activeScopedSubjectPredicate(args);
1277
- const terms = searchTerms(args.content).filter(
1278
- (term) => !SUPERSESSION_STOP_TERMS.has(term)
1279
- );
1280
- const lexicalRows = terms.length > 0 ? await args.db.select().from(juniorMemoryMemories).where(
1281
- and2(
1282
- predicate,
1283
- or2(
1284
- ...terms.map(
1285
- (term) => ilike2(juniorMemoryMemories.content, `%${term}%`)
1286
- )
1287
- )
1288
- )
1289
- ) : [];
1290
- const lexicalCandidates = lexicalRows.map(parseMemoryRow).map((memory) => ({
1291
- memory,
1292
- score: searchScore(memory, terms)
1293
- })).filter((candidate) => candidate.score > 1).sort(
1294
- (left, right) => right.score - left.score || right.memory.createdAtMs - left.memory.createdAtMs || left.memory.id.localeCompare(right.memory.id)
1295
- ).map((candidate) => candidate.memory);
1296
- const vectorCandidates = args.embedding ? await listVectorSupersessionCandidates({
1297
- db: args.db,
1298
- embedding: args.embedding,
1299
- kind: args.kind,
1300
- nowMs: args.nowMs,
1301
- scope: args.scope,
1302
- subject: args.subject
1303
- }) : [];
1304
- const byId = /* @__PURE__ */ new Map();
1305
- for (const memory of [...lexicalCandidates, ...vectorCandidates]) {
1306
- if (byId.size >= SUPERSESSION_CANDIDATE_LIMIT) {
1307
- break;
1039
+ let candidateEmbedding;
1040
+ if (embedder) {
1041
+ try {
1042
+ candidateEmbedding = await embedOne(embedder, content);
1043
+ } catch {
1044
+ candidateEmbedding = void 0;
1045
+ }
1308
1046
  }
1309
- byId.set(memory.id, memory);
1310
- }
1311
- return [...byId.values()];
1312
- }
1313
- async function listVectorSupersessionCandidates(args) {
1314
- const distance = cosineDistance(
1315
- juniorMemoryEmbeddings.embedding,
1316
- args.embedding.vector
1317
- );
1318
- const rows = await args.db.select({
1319
- contentHash: juniorMemoryEmbeddings.contentHash,
1320
- distance,
1321
- memory: juniorMemoryMemories
1322
- }).from(juniorMemoryMemories).innerJoin(
1323
- juniorMemoryEmbeddings,
1324
- eq3(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id)
1325
- ).where(
1326
- and2(
1327
- activeScopedSubjectPredicate(args),
1328
- eq3(juniorMemoryEmbeddings.provider, args.embedding.provider),
1329
- eq3(juniorMemoryEmbeddings.model, args.embedding.model),
1330
- eq3(juniorMemoryEmbeddings.dimensions, MEMORY_EMBEDDING_DIMENSIONS),
1331
- eq3(juniorMemoryEmbeddings.metric, EMBEDDING_METRIC)
1332
- )
1333
- ).orderBy(
1334
- distance,
1335
- desc2(juniorMemoryMemories.createdAtMs),
1336
- asc(juniorMemoryMemories.id)
1337
- ).limit(SUPERSESSION_CANDIDATE_LIMIT);
1338
- return rows.flatMap((row) => {
1339
- if (hashEmbeddedContent(row.memory.content) !== row.contentHash) {
1340
- return [];
1047
+ const vectorDuplicate = candidateEmbedding ? await findVectorDuplicateMemory({
1048
+ db,
1049
+ embedding: candidateEmbedding,
1050
+ kind: input.kind,
1051
+ nowMs,
1052
+ scope,
1053
+ subject
1054
+ }) : void 0;
1055
+ if (vectorDuplicate) {
1056
+ return await reuseDuplicateMemory({
1057
+ content,
1058
+ duplicate: vectorDuplicate,
1059
+ idempotencyKey: input.idempotencyKey,
1060
+ nowMs,
1061
+ scope,
1062
+ subject
1063
+ });
1341
1064
  }
1342
- return [parseMemoryRow(row.memory)];
1343
- });
1344
- }
1345
- async function decideSupersededIds(args) {
1346
- if (args.kind !== "preference" || !args.decider || args.candidates.length === 0) {
1347
- return [];
1348
- }
1349
- const existingMemories = args.candidates.map((memory) => ({
1350
- content: memory.content,
1351
- id: memory.id
1352
- }));
1353
- const candidateIds = new Set(args.candidates.map((memory) => memory.id));
1354
- try {
1355
- const decision = await args.decider.adjudicateSupersession({
1356
- candidate: {
1357
- content: args.content,
1358
- kind: "preference"
1359
- },
1360
- existingMemories,
1361
- runtimeContext: args.runtimeContext
1362
- });
1363
- if (decision.decision !== "supersedes_old") {
1364
- return [];
1365
- }
1366
- return decision.supersededIds.filter((id) => candidateIds.has(id));
1367
- } catch {
1368
- return [];
1369
- }
1370
- }
1371
- async function listVisibleMemories(args) {
1372
- const predicate = activeVisiblePredicate(args);
1373
- if (!predicate) {
1374
- return [];
1375
- }
1376
- const limit = boundedLimit(args.limit, DEFAULT_LIST_LIMIT);
1377
- const rows = await args.db.select().from(juniorMemoryMemories).where(predicate).orderBy(
1378
- desc2(juniorMemoryMemories.createdAtMs),
1379
- asc(juniorMemoryMemories.id)
1380
- ).limit(limit);
1381
- return rows.map(parseMemoryRow);
1382
- }
1383
- async function searchVisibleMemories(args) {
1384
- const terms = searchTerms(args.query);
1385
- if (terms.length === 0) {
1386
- return [];
1387
- }
1388
- const predicate = activeVisiblePredicate(args);
1389
- if (!predicate) {
1390
- return [];
1391
- }
1392
- const rows = await args.db.select().from(juniorMemoryMemories).where(
1393
- and2(
1394
- predicate,
1395
- or2(
1396
- ...terms.map(
1397
- (term) => ilike2(juniorMemoryMemories.content, `%${term}%`)
1398
- )
1399
- )
1400
- )
1401
- );
1402
- return rows.map((row) => ({
1403
- memory: parseMemoryRow(row),
1404
- score: 0,
1405
- sourceKey: row.sourceKey
1406
- }));
1407
- }
1408
- async function searchVisibleVectorMemories(args) {
1409
- if (!args.embedder) {
1410
- return [];
1411
- }
1412
- const predicate = activeVisiblePredicate(args);
1413
- if (!predicate) {
1414
- return [];
1415
- }
1416
- let embedding;
1417
- try {
1418
- embedding = await embedOne(args.embedder, args.query);
1419
- } catch {
1420
- return [];
1421
- }
1422
- const distance = cosineDistance(
1423
- juniorMemoryEmbeddings.embedding,
1424
- embedding.vector
1425
- );
1426
- const rows = await args.db.select({
1427
- contentHash: juniorMemoryEmbeddings.contentHash,
1428
- distance,
1429
- memory: juniorMemoryMemories
1430
- }).from(juniorMemoryMemories).innerJoin(
1431
- juniorMemoryEmbeddings,
1432
- eq3(juniorMemoryEmbeddings.memoryId, juniorMemoryMemories.id)
1433
- ).where(
1434
- and2(
1435
- predicate,
1436
- eq3(juniorMemoryEmbeddings.provider, embedding.provider),
1437
- eq3(juniorMemoryEmbeddings.model, embedding.model),
1438
- eq3(juniorMemoryEmbeddings.dimensions, MEMORY_EMBEDDING_DIMENSIONS),
1439
- eq3(juniorMemoryEmbeddings.metric, EMBEDDING_METRIC)
1440
- )
1441
- ).orderBy(
1442
- distance,
1443
- desc2(juniorMemoryMemories.createdAtMs),
1444
- asc(juniorMemoryMemories.id)
1445
- ).limit(args.limit);
1446
- return rows.flatMap((row) => {
1447
- const distanceValue = Number(row.distance);
1448
- if (row.distance === null || !Number.isFinite(distanceValue) || hashEmbeddedContent(row.memory.content) !== row.contentHash) {
1449
- return [];
1450
- }
1451
- if (args.maxDistance !== void 0 && distanceValue > args.maxDistance) {
1452
- return [];
1453
- }
1454
- return [
1455
- {
1456
- memory: parseMemoryRow(row.memory),
1457
- score: 1 / (1 + Math.max(0, distanceValue)),
1458
- sourceKey: row.memory.sourceKey
1459
- }
1460
- ];
1461
- });
1462
- }
1463
- function sourceBoost(candidate, currentChannelPrefix) {
1464
- if (!currentChannelPrefix) {
1465
- return 0;
1466
- }
1467
- return candidate.sourceKey.startsWith(currentChannelPrefix) ? SOURCE_CHANNEL_BOOST : 0;
1468
- }
1469
- function observedAgeBoost(memory, nowMs) {
1470
- const ageMs = Math.max(0, nowMs - memory.observedAtMs);
1471
- if (ageMs <= 7 * ONE_DAY_MS) {
1472
- return 0.15;
1473
- }
1474
- if (ageMs <= 30 * ONE_DAY_MS) {
1475
- return 0.1;
1476
- }
1477
- if (ageMs <= 90 * ONE_DAY_MS) {
1478
- return 0.05;
1479
- }
1480
- return 0;
1481
- }
1482
- function mergeSearchCandidates(candidates, nowMs, currentChannelKey) {
1483
- const byId = /* @__PURE__ */ new Map();
1484
- for (const candidate of candidates) {
1485
- const existing = byId.get(candidate.memory.id);
1486
- if (existing) {
1487
- existing.score += candidate.score;
1488
- continue;
1489
- }
1490
- byId.set(candidate.memory.id, {
1491
- memory: candidate.memory,
1492
- score: candidate.score,
1493
- sourceKey: candidate.sourceKey
1494
- });
1495
- }
1496
- return [...byId.values()].sort((left, right) => {
1497
- const scoreDelta = right.score - left.score;
1498
- if (Math.abs(scoreDelta) > RELEVANCE_NEAR_TIE_DELTA) {
1499
- return scoreDelta;
1500
- }
1501
- const sourceDelta = sourceBoost(right, currentChannelKey) - sourceBoost(left, currentChannelKey);
1502
- if (sourceDelta !== 0) {
1503
- return sourceDelta;
1504
- }
1505
- return observedAgeBoost(right.memory, nowMs) - observedAgeBoost(left.memory, nowMs) || right.memory.observedAtMs - left.memory.observedAtMs || left.memory.id.localeCompare(right.memory.id);
1506
- }).map((candidate) => candidate.memory);
1507
- }
1508
- function createMemoryStore(db, context, options = {}) {
1509
- const runtimeContext = memoryRuntimeContextSchema.parse(context);
1510
- const parsedOptions = memoryStoreOptionsSchema.parse({ now: options.now });
1511
- const embedder = options.embedder;
1512
- const maxVectorDistance = options.maxVectorDistance;
1513
- const supersessionDecider = options.supersessionDecider;
1514
- const getNowMs = parsedOptions.now ?? Date.now;
1515
- async function archiveExpiredVisibleMemories(input, nowMs) {
1516
- input = archiveExpiredMemoriesInputSchema.parse(input ?? {});
1517
- return await archiveExpiredMemoryBatch({
1518
- db,
1519
- limit: input.limit,
1520
- nowMs,
1521
- scopes: deriveVisibleMemoryScopes(runtimeContext)
1522
- });
1523
- }
1524
- async function createScopedMemory(rawInput, scopeKind) {
1525
- const input = createMemoryInputSchema.parse(rawInput);
1526
- const nowMs = getNowMs();
1527
- const content = normalizeContent(input.content);
1528
- const scope = deriveMemoryScope(runtimeContext, scopeKind);
1529
- const subject = deriveMemorySubject(runtimeContext, scope);
1530
- if (content.length > MAX_MEMORY_CONTENT_CHARS) {
1531
- throw new Error("Memory content exceeds the maximum length.");
1532
- }
1533
- await archiveExpiredMemoryBatch({
1534
- db,
1535
- nowMs,
1536
- scopes: [scope]
1537
- });
1538
- await archiveExpiredMemoryBatch({
1539
- db,
1540
- idempotencyKey: input.idempotencyKey,
1541
- limit: 1,
1542
- nowMs,
1543
- scopes: [scope]
1544
- });
1545
- if (input.idempotencyKey !== void 0) {
1546
- const idempotent2 = await findByIdempotencyKey({
1547
- db,
1548
- idempotencyKey: input.idempotencyKey,
1549
- nowMs,
1550
- scope
1551
- });
1552
- if (idempotent2) {
1553
- await storeEmbedding({
1554
- content: idempotent2.content,
1555
- db,
1556
- embedder,
1557
- memoryId: idempotent2.id,
1558
- nowMs
1559
- });
1560
- return { created: false, memory: idempotent2 };
1561
- }
1562
- }
1563
- const exactDuplicate = await findExactDuplicateMemory({
1564
- content,
1565
- db,
1566
- kind: input.kind,
1567
- nowMs,
1568
- scope,
1569
- subject
1570
- });
1571
- if (exactDuplicate) {
1572
- await rememberDuplicateIdempotency({
1573
- content,
1574
- db,
1575
- duplicate: exactDuplicate,
1576
- idempotencyKey: input.idempotencyKey,
1577
- nowMs,
1578
- runtimeContext,
1579
- scope,
1580
- subject
1581
- });
1582
- await storeEmbedding({
1583
- content: exactDuplicate.content,
1584
- db,
1585
- embedder,
1586
- memoryId: exactDuplicate.id,
1587
- nowMs
1588
- });
1589
- return { created: false, memory: exactDuplicate };
1590
- }
1591
- let candidateEmbedding;
1592
- if (embedder) {
1593
- try {
1594
- candidateEmbedding = await embedOne(embedder, content);
1595
- } catch {
1596
- candidateEmbedding = void 0;
1597
- }
1598
- }
1599
- const vectorDuplicate = candidateEmbedding ? await findVectorDuplicateMemory({
1600
- db,
1601
- embedding: candidateEmbedding,
1602
- kind: input.kind,
1603
- nowMs,
1604
- scope,
1605
- subject
1606
- }) : void 0;
1607
- if (vectorDuplicate) {
1608
- await rememberDuplicateIdempotency({
1609
- content,
1610
- db,
1611
- duplicate: vectorDuplicate,
1612
- idempotencyKey: input.idempotencyKey,
1613
- nowMs,
1614
- runtimeContext,
1615
- scope,
1616
- subject
1617
- });
1618
- await storeEmbedding({
1619
- content: vectorDuplicate.content,
1620
- db,
1621
- embedder,
1622
- memoryId: vectorDuplicate.id,
1623
- nowMs
1624
- });
1625
- return { created: false, memory: vectorDuplicate };
1626
- }
1627
- let supersededIds = [];
1628
- if (scopeKind === "personal" && input.kind === "preference" && supersessionDecider && (input.expiresAtMs === void 0 || input.expiresAtMs > nowMs)) {
1629
- const supersessionCandidates = await listSupersessionCandidates({
1630
- content,
1631
- db,
1632
- ...candidateEmbedding ? { embedding: candidateEmbedding } : {},
1633
- kind: input.kind,
1634
- nowMs,
1635
- scope,
1636
- subject
1637
- });
1638
- supersededIds = await decideSupersededIds({
1639
- candidates: supersessionCandidates,
1640
- content,
1641
- decider: supersessionDecider,
1642
- kind: input.kind,
1643
- runtimeContext
1644
- });
1065
+ let supersededIds = [];
1066
+ if (scopeKind === "personal" && input.kind === "preference" && supersessionDecider && (input.expiresAtMs === void 0 || input.expiresAtMs > nowMs)) {
1067
+ const preferenceCandidates = await listPreferenceAdjudicationCandidates({
1068
+ db,
1069
+ ...candidateEmbedding ? { embedding: candidateEmbedding } : {},
1070
+ nowMs,
1071
+ scope,
1072
+ subject
1073
+ });
1074
+ const adjudication = await adjudicatePreferenceCandidate({
1075
+ candidates: preferenceCandidates,
1076
+ content,
1077
+ decider: supersessionDecider,
1078
+ runtimeContext
1079
+ });
1080
+ if (adjudication.decision === "duplicate") {
1081
+ return await reuseDuplicateMemory({
1082
+ content,
1083
+ duplicate: adjudication.memory,
1084
+ idempotencyKey: input.idempotencyKey,
1085
+ nowMs,
1086
+ scope,
1087
+ subject
1088
+ });
1089
+ }
1090
+ if (adjudication.decision === "supersede") {
1091
+ supersededIds = adjudication.ids;
1092
+ }
1645
1093
  }
1646
1094
  const id = randomUUID();
1647
1095
  const rows = await db.transaction(async (tx) => {
@@ -1675,7 +1123,7 @@ function createMemoryStore(db, context, options = {}) {
1675
1123
  supersededAtMs: nowMs,
1676
1124
  supersededById: insertedMemory.id
1677
1125
  }).where(
1678
- and2(
1126
+ and(
1679
1127
  inArray(juniorMemoryMemories.id, supersededIds),
1680
1128
  activeScopedSubjectPredicate({
1681
1129
  kind: input.kind,
@@ -1703,122 +1151,676 @@ function createMemoryStore(db, context, options = {}) {
1703
1151
  });
1704
1152
  return { created: true, memory };
1705
1153
  }
1706
- const idempotent = await findByIdempotencyKey({
1707
- db,
1708
- idempotencyKey: input.idempotencyKey,
1709
- nowMs,
1710
- scope
1154
+ const idempotent = await findByIdempotencyKey({
1155
+ db,
1156
+ idempotencyKey: input.idempotencyKey,
1157
+ nowMs,
1158
+ scope
1159
+ });
1160
+ if (!idempotent) {
1161
+ throw new Error("Memory idempotency conflict did not resolve.");
1162
+ }
1163
+ await storeEmbedding({
1164
+ content: idempotent.content,
1165
+ db,
1166
+ embedder,
1167
+ memoryId: idempotent.id,
1168
+ nowMs
1169
+ });
1170
+ return { created: false, memory: idempotent };
1171
+ }
1172
+ return {
1173
+ async archiveExpiredMemories(input) {
1174
+ return await archiveExpiredVisibleMemories(input, getNowMs());
1175
+ },
1176
+ async createMemory(input) {
1177
+ return await createScopedMemory(input, "personal");
1178
+ },
1179
+ async createConversationMemory(input) {
1180
+ return await createScopedMemory(input, "conversation");
1181
+ },
1182
+ async listMemories(input) {
1183
+ input = listMemoriesInputSchema.parse(input);
1184
+ const nowMs = getNowMs();
1185
+ const scopes = deriveVisibleMemoryScopes(runtimeContext);
1186
+ await archiveExpiredMemoryBatch({
1187
+ db,
1188
+ nowMs,
1189
+ scopes
1190
+ });
1191
+ return await listVisibleMemories({
1192
+ db,
1193
+ limit: input.limit,
1194
+ nowMs,
1195
+ scopes
1196
+ });
1197
+ },
1198
+ async searchMemories(input) {
1199
+ input = searchMemoriesInputSchema.parse(input);
1200
+ const nowMs = getNowMs();
1201
+ const scopes = deriveVisibleMemoryScopes(runtimeContext);
1202
+ await archiveExpiredMemoryBatch({
1203
+ db,
1204
+ nowMs,
1205
+ scopes
1206
+ });
1207
+ const limit = boundedLimit(input.limit, DEFAULT_SEARCH_LIMIT);
1208
+ const vectorCandidates = await searchVisibleVectorMemories({
1209
+ db,
1210
+ embedder,
1211
+ limit: limit * VECTOR_SEARCH_OVERFETCH,
1212
+ ...maxVectorDistance !== void 0 ? { maxDistance: maxVectorDistance } : {},
1213
+ nowMs,
1214
+ query: input.query,
1215
+ scopes
1216
+ });
1217
+ const candidates = await searchVisibleMemories({
1218
+ db,
1219
+ nowMs,
1220
+ query: input.query,
1221
+ scopes
1222
+ });
1223
+ const terms = searchTerms(input.query);
1224
+ const lexicalCandidates = candidates.map((candidate) => ({
1225
+ ...candidate,
1226
+ score: searchScore(candidate.memory, terms)
1227
+ })).filter((item) => item.score > 0);
1228
+ return mergeSearchCandidates(
1229
+ [...vectorCandidates, ...lexicalCandidates],
1230
+ nowMs,
1231
+ sourceChannelPrefix(runtimeContext)
1232
+ ).slice(0, limit);
1233
+ },
1234
+ async archiveMemory(input) {
1235
+ input = archiveMemoryInputSchema.parse(input);
1236
+ const nowMs = getNowMs();
1237
+ const scopes = deriveVisibleMemoryScopes(runtimeContext);
1238
+ const predicate = activeVisiblePredicate({ nowMs, scopes });
1239
+ const idPrefix = input.id.trim();
1240
+ if (!idPrefix) {
1241
+ throw new Error("Memory id is required.");
1242
+ }
1243
+ const rows = predicate ? await db.select().from(juniorMemoryMemories).where(
1244
+ and(
1245
+ predicate,
1246
+ or(
1247
+ eq(juniorMemoryMemories.id, idPrefix),
1248
+ like(juniorMemoryMemories.id, `${idPrefix}%`)
1249
+ )
1250
+ )
1251
+ ).orderBy(asc(juniorMemoryMemories.id)).limit(2) : [];
1252
+ if (rows.length === 0) {
1253
+ throw new Error("Memory was not found in the current context.");
1254
+ }
1255
+ if (rows.length > 1) {
1256
+ throw new Error("Memory id prefix is ambiguous.");
1257
+ }
1258
+ const memory = parseMemoryRow(rows[0]);
1259
+ const updated = await db.update(juniorMemoryMemories).set({
1260
+ archivedAtMs: nowMs,
1261
+ archiveReason: input.reason ?? "user_removed"
1262
+ }).where(eq(juniorMemoryMemories.id, memory.id)).returning();
1263
+ await db.delete(juniorMemoryEmbeddings).where(eq(juniorMemoryEmbeddings.memoryId, memory.id));
1264
+ return parseMemoryRow(updated[0]);
1265
+ }
1266
+ };
1267
+ }
1268
+
1269
+ // src/agent.ts
1270
+ var memoryKindSchema = z3.enum(MEMORY_KINDS);
1271
+ var memoryRejectReasonSchema = z3.enum([
1272
+ "not_public_shareable",
1273
+ "secret_or_credential",
1274
+ "sensitive_personal",
1275
+ "third_party_personal",
1276
+ "vague_or_not_self_contained",
1277
+ "not_durable",
1278
+ "assistant_or_system_detail",
1279
+ "unsupported_scope"
1280
+ ]);
1281
+ var createMemoryRequestSchema = z3.object({
1282
+ content: z3.string().min(1),
1283
+ expiresAtMs: z3.number().finite().optional(),
1284
+ runtimeContext: memoryRuntimeContextSchema,
1285
+ sourceContext: z3.object({
1286
+ currentUserText: z3.string().min(1).optional()
1287
+ }).strict().optional()
1288
+ }).strict();
1289
+ var transcriptProvenanceSchema = z3.object({
1290
+ authority: z3.enum(["instruction", "context"]),
1291
+ actor: actorSchema.optional()
1292
+ }).strict();
1293
+ var evidenceMessageIndicesSchema = z3.array(z3.number().int().nonnegative()).min(1).max(10).describe("Indices from <run-transcript> that directly support this memory.");
1294
+ var extractSessionRequestSchema = z3.object({
1295
+ existingMemories: z3.array(
1296
+ z3.object({
1297
+ content: z3.string().min(1)
1298
+ }).strict()
1299
+ ).max(10).default([]),
1300
+ actors: z3.array(actorSchema),
1301
+ runtimeContext: memoryRuntimeContextSchema,
1302
+ transcript: z3.array(
1303
+ z3.discriminatedUnion("type", [
1304
+ z3.object({
1305
+ type: z3.literal("message"),
1306
+ role: z3.enum(["user", "assistant"]),
1307
+ text: z3.string().min(1),
1308
+ provenance: transcriptProvenanceSchema.optional(),
1309
+ isRunActor: z3.boolean().optional()
1310
+ }).strict(),
1311
+ z3.object({
1312
+ type: z3.literal("toolResult"),
1313
+ toolName: z3.string().min(1),
1314
+ isError: z3.boolean(),
1315
+ text: z3.string().min(1)
1316
+ }).strict()
1317
+ ])
1318
+ ).min(1)
1319
+ }).strict();
1320
+ var expiresAtMsSchema = z3.number().finite().nullable().describe(
1321
+ "Expiration timestamp when the fact should expire, otherwise null."
1322
+ );
1323
+ var memoryReviewDecisionSchema = z3.discriminatedUnion("decision", [
1324
+ z3.object({
1325
+ decision: z3.literal("store"),
1326
+ kind: memoryKindSchema,
1327
+ content: z3.string().min(1),
1328
+ expiresAtMs: z3.number().finite().optional()
1329
+ }).strict(),
1330
+ z3.object({
1331
+ decision: z3.literal("reject"),
1332
+ reason: memoryRejectReasonSchema
1333
+ }).strict()
1334
+ ]);
1335
+ var memoryReviewResponseSchema = z3.discriminatedUnion("decision", [
1336
+ z3.object({
1337
+ decision: z3.literal("store"),
1338
+ kind: memoryKindSchema.describe(
1339
+ "Use preference only for actor-owned personal preferences, opinions, habits, or workflows. Use procedure for reusable task or process instructions. Use knowledge for shared project, channel, operational, or runbook facts."
1340
+ ),
1341
+ canonicalFact: z3.string().min(1).describe(
1342
+ "Stored memory text. It must be self-contained and must not include actor names, actor/user labels, source labels, or first- or second-person wording."
1343
+ ),
1344
+ expiresAtMs: expiresAtMsSchema
1345
+ }).strict(),
1346
+ z3.object({
1347
+ decision: z3.literal("reject"),
1348
+ reason: memoryRejectReasonSchema
1349
+ }).strict()
1350
+ ]);
1351
+ var extractedMemorySchema = z3.object({
1352
+ kind: memoryKindSchema.describe(
1353
+ "Use preference only for actor-owned personal preferences, opinions, habits, or workflows. Use procedure for reusable task or process instructions. Use knowledge for shared project, channel, operational, or runbook facts."
1354
+ ),
1355
+ canonicalFact: z3.string().min(1).describe(
1356
+ "Stored memory text as one self-contained fact. It must not include actor names, actor/user labels, source labels, or first- or second-person wording."
1357
+ ),
1358
+ expiresAtMs: expiresAtMsSchema,
1359
+ evidenceMessageIndices: evidenceMessageIndicesSchema
1360
+ }).strict();
1361
+ var extractedMemoryResultSchema = z3.object({
1362
+ content: z3.string().min(1),
1363
+ expiresAtMs: expiresAtMsSchema,
1364
+ kind: memoryKindSchema,
1365
+ evidenceMessageIndices: evidenceMessageIndicesSchema
1366
+ }).strict();
1367
+ var extractMemoriesResponseSchema = z3.object({
1368
+ memories: z3.array(extractedMemorySchema).max(5).describe(
1369
+ "Accepted public/shareable durable memories from the completed run. Return one object per distinct source assertion and classify it with kind."
1370
+ )
1371
+ }).strict();
1372
+ var MEMORY_REVIEW_SYSTEM = [
1373
+ "You are Junior's memory review agent.",
1374
+ "Review one memory candidate and return one structured review decision.",
1375
+ "Store only public/shareable, self-contained facts that are useful beyond this turn.",
1376
+ "Reject secrets, credentials, private or sensitive personal details, gossip, speculative claims about other people, assistant/system implementation details, vague references, and low-durability chatter.",
1377
+ "Use the runtime context only for authority and scope; do not accept model-provided actor ids, scope ids, aliases, or arbitrary subjects."
1378
+ ].join("\n");
1379
+ var MEMORY_EXTRACTION_SYSTEM = [
1380
+ "You are Junior's passive memory extraction agent. Return only structured memories worth storing.",
1381
+ "Use the completed run transcript as source evidence, including user-authored messages and tool results.",
1382
+ "Assistant text is context for interpreting the run, not independent evidence for new facts.",
1383
+ "Reject secrets, credentials, private or sensitive personal details, gossip, speculative claims about other people, assistant/system implementation details, vague references, and low-durability chatter.",
1384
+ "If no public, durable, self-contained memory remains after rewriting, return an empty memories array."
1385
+ ].join("\n");
1386
+ var MEMORY_PREFERENCE_ADJUDICATION_SYSTEM = [
1387
+ "You are Junior's memory preference adjudication agent.",
1388
+ "Classify how one new actor preference relates to existing active actor preferences.",
1389
+ "Return duplicate when the same durable preference is merely phrased differently.",
1390
+ "Return supersedes_old only for an obvious changed value in the same mutable preference slot.",
1391
+ "Return distinct for additive preferences or different topics, and uncertain when the relationship is unclear."
1392
+ ].join("\n");
1393
+ var CANONICAL_CONTENT_RULES = [
1394
+ "- Stored memory text must be a rewritten fact, not copied user wording or a sentence about who said it.",
1395
+ "- Store the minimum useful assertion supported by source evidence; do not add adjacent steps, caveats, or generalized advice.",
1396
+ "- Do not return both concise and expanded variants of the same source assertion; keep the shortest self-contained canonical memory.",
1397
+ "- Put ownership in structured fields, not prose.",
1398
+ "- For actor memories, omit the subject and write a stable fact such as 'Prefers X', 'Uses Y', or 'Thinks Z'.",
1399
+ "- Drop perspective/provenance markers while preserving useful context.",
1400
+ "- Remove actor names, display names, actor/user labels, first- or second-person wording, thread labels, channel labels, and source labels."
1401
+ ];
1402
+ function escapeXml(value) {
1403
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
1404
+ }
1405
+ function runtimeDescription(request) {
1406
+ const runtime = request.runtimeContext;
1407
+ const actor = runtime.actor?.platform === "slack" ? `slack:${runtime.actor.teamId}:${runtime.actor.userId}` : runtime.actor?.platform === "local" ? `local:${runtime.actor.userId}` : "none";
1408
+ const source = runtime.source.platform === "slack" ? `slack:${runtime.source.teamId}:${runtime.source.channelId}` : `local:${runtime.source.conversationId}`;
1409
+ const lines = [
1410
+ `- actor: ${escapeXml(actor)}`,
1411
+ `- source: ${escapeXml(source)}`,
1412
+ `- has_conversation: ${runtime.conversationId ? "true" : "false"}`,
1413
+ `- expires_at: ${request.expiresAtMs === void 0 ? "never" : escapeXml(new Date(request.expiresAtMs).toISOString())}`
1414
+ ];
1415
+ return ["<runtime>", ...lines, "</runtime>"].join("\n");
1416
+ }
1417
+ function sourceContext(request) {
1418
+ const currentUserText = request.sourceContext?.currentUserText?.trim();
1419
+ if (!currentUserText) {
1420
+ return void 0;
1421
+ }
1422
+ return [
1423
+ "<source-context>",
1424
+ "The current user-authored text is source evidence for explicit memory requests. Use it to recover the concrete fact when the candidate is incomplete, vague, or over-personalized. Store only rewritten, self-contained memory content.",
1425
+ "<current-user-message>",
1426
+ escapeXml(currentUserText),
1427
+ "</current-user-message>",
1428
+ "</source-context>"
1429
+ ].join("\n");
1430
+ }
1431
+ function existingMemoriesContext(request) {
1432
+ if (request.existingMemories.length === 0) {
1433
+ return "<existing-memories>[]</existing-memories>";
1434
+ }
1435
+ return [
1436
+ "<existing-memories>",
1437
+ "Use these only to skip memories that are already covered or semantically redundant. They are not source evidence for new memories.",
1438
+ escapeXml(JSON.stringify(request.existingMemories)),
1439
+ "</existing-memories>"
1440
+ ].join("\n");
1441
+ }
1442
+ function allowedExtractionKinds(actorCount) {
1443
+ return actorCount === 1 ? new Set(MEMORY_KINDS) : /* @__PURE__ */ new Set(["procedure", "knowledge"]);
1444
+ }
1445
+ function memoryKindsContext(allowedKinds) {
1446
+ const lines = ["<memory-kinds>"];
1447
+ if (allowedKinds.has("preference")) {
1448
+ lines.push(
1449
+ "- preference: a durable first-person personal preference, opinion, habit, or workflow owned by the current actor. Stored as actor memory."
1450
+ );
1451
+ }
1452
+ lines.push(
1453
+ "- procedure: reusable instructions for how a task, lookup, investigation, process, triage flow, or runbook should be done. Store the method, source-of-truth, prerequisite, or decision path when it took effort to discover. Stored as conversation memory.",
1454
+ "- knowledge: stable shared project, channel, operational, or runbook fact that is not a personal actor preference. Direct answers to user inquiries qualify only when they are durable beyond this run. Stored as conversation memory.",
1455
+ "</memory-kinds>"
1456
+ );
1457
+ return lines.join("\n");
1458
+ }
1459
+ function reviewPrompt(request) {
1460
+ const sections = [
1461
+ "<memory-review-input>",
1462
+ "Review the candidate memory using the runtime-owned context below.",
1463
+ "",
1464
+ runtimeDescription(request),
1465
+ "",
1466
+ sourceContext(request),
1467
+ "",
1468
+ "<candidate>",
1469
+ escapeXml(request.content),
1470
+ "</candidate>",
1471
+ "",
1472
+ "<rules>",
1473
+ "- Return store only when the candidate is public/shareable, durable, and self-contained.",
1474
+ "- First classify the memory kind: preference, procedure, or knowledge.",
1475
+ "- Use kind=preference only for first-person facts authored by the current actor about their own preference, opinion, habit, identity, or workflow.",
1476
+ "- Reject named third-person personal facts such as another person's preference, opinion, habit, identity, relationship, or workflow. Do not assume a named person is the current actor.",
1477
+ "- Use kind=procedure for reusable task/process/runbook instructions.",
1478
+ "- Use kind=knowledge for shared project, channel, operational, or runbook facts.",
1479
+ "- When current-user-message contains an explicit memory request with a concrete fact or procedure, extract from current-user-message even if the candidate is vague, incomplete, or phrased as an instruction.",
1480
+ "- A candidate may be badly phrased by an outer assistant or extraction pass. When current-user-message contains the actor's own first-person memory fact, treat that as actor-authored source evidence and canonicalize the fact instead of rejecting for third-person wording.",
1481
+ "- When candidate wording personalizes a shared task, process, runbook, project, channel, or operational fact, use current-user-message to recover the shared fact and classify it as procedure or knowledge.",
1482
+ "- Explicit procedure requests are valid when the source text contains both task context and action. Canonicalize them as shared procedure facts instead of rejecting them as vague.",
1483
+ "- Store content as person-less, source-less canonical knowledge. Ownership and source live in structured metadata, not prose.",
1484
+ "- For actor memories, omit the subject and write the content as a stable fact such as 'Prefers X', 'Uses Y', or 'Thinks Z'.",
1485
+ "- Remove actor names, display names, actor/user labels, first- or second-person wording, thread labels, channel labels, and source labels from stored content.",
1486
+ "- Reject third-party personal profile facts, even if they mention a name.",
1487
+ "- Reject vague content such as 'remember this' unless the candidate or current-user-message contains the concrete fact.",
1488
+ "- Preserve the requested expiration when one exists; otherwise set expiresAtMs to null.",
1489
+ "- If unsure, reject.",
1490
+ "</rules>",
1491
+ "</memory-review-input>"
1492
+ ].filter((section) => section !== void 0);
1493
+ return sections.join("\n");
1494
+ }
1495
+ function transcriptActorLabel(actor) {
1496
+ if (!actor) {
1497
+ return "none";
1498
+ }
1499
+ if (actor.platform === "system") {
1500
+ return `system:${actor.name}`;
1501
+ }
1502
+ return actor.platform === "slack" ? `slack:${actor.teamId}:${actor.userId}` : `local:${actor.userId}`;
1503
+ }
1504
+ function runTranscriptContext(request) {
1505
+ return [
1506
+ "<run-transcript>",
1507
+ ...request.transcript.map((entry, index2) => {
1508
+ if (entry.type === "toolResult") {
1509
+ return [
1510
+ `<tool-result index="${index2}" tool="${escapeXml(entry.toolName)}" is_error="${entry.isError ? "true" : "false"}">`,
1511
+ escapeXml(entry.text),
1512
+ "</tool-result>"
1513
+ ].join("\n");
1514
+ }
1515
+ const authority = entry.provenance?.authority ?? "context";
1516
+ const isRunActor = entry.isRunActor === true;
1517
+ const actor = transcriptActorLabel(entry.provenance?.actor);
1518
+ return [
1519
+ `<message index="${index2}" role="${entry.role}" authority="${authority}" is_run_actor="${isRunActor ? "true" : "false"}" actor="${escapeXml(actor)}">`,
1520
+ escapeXml(entry.text),
1521
+ "</message>"
1522
+ ].join("\n");
1523
+ }),
1524
+ "</run-transcript>"
1525
+ ].join("\n");
1526
+ }
1527
+ function sessionExtractionPrompt(request) {
1528
+ const allowedKinds = allowedExtractionKinds(request.actors.length);
1529
+ const allowsPreference = allowedKinds.has("preference");
1530
+ return [
1531
+ "<memory-extraction-input>",
1532
+ "Extract durable memories from this completed agent run using the runtime-owned context below.",
1533
+ "",
1534
+ runtimeDescription({
1535
+ runtimeContext: request.runtimeContext
1536
+ }),
1537
+ "",
1538
+ existingMemoriesContext(request),
1539
+ "",
1540
+ memoryKindsContext(allowedKinds),
1541
+ "",
1542
+ runTranscriptContext(request),
1543
+ "",
1544
+ "<rules>",
1545
+ "- Return at most five memories.",
1546
+ "- Every returned memory must cite one or more evidenceMessageIndices from <run-transcript>.",
1547
+ "- Cite only indices that directly support the stored fact; do not cite assistant messages as independent evidence.",
1548
+ "- Each transcript message exposes authority (instruction or context), is_run_actor, and an actor id. Use these to classify evidence.",
1549
+ ...allowsPreference ? [
1550
+ "- For a preference, cite only messages with authority=instruction and is_run_actor=true; a preference must be the run actor's own first-person fact."
1551
+ ] : [],
1552
+ "- For a procedure or knowledge memory, cite run-actor instruction messages, public context messages, or successful tool results.",
1553
+ "- Use user messages and successful tool results as source evidence for storable facts.",
1554
+ "- Use failed tool results only when the failure reveals durable process knowledge, not transient errors.",
1555
+ "- Use assistant messages only as context; do not store the assistant's claims unless supported by user messages or tool results.",
1556
+ "- Return one memory per distinct fact.",
1557
+ "- Prefer storing how to achieve a result: stable source-of-truth, query location, workflow, prerequisite, caveat, or reusable decision path that took effort to discover.",
1558
+ "- Store direct answers to user inquiries only when they are stable operational/project knowledge, not values that naturally change over time.",
1559
+ "- Do not store point-in-time analytics, search, issue, metric, incident, availability, or status answers just because a tool produced them.",
1560
+ "- Do not store the fact that the user asked for advice, search, recall, planning, listing, inspection, or removal. Store only stable knowledge discovered in response, such as a reusable method or source-of-truth.",
1561
+ "- A user question asking how, what, where, or whether to do something is not source evidence for the answer. Store the answer only when supported by a user-authored factual statement or a tool result.",
1562
+ "- Set kind=procedure for reusable task/process/runbook instructions.",
1563
+ "- Set kind=knowledge for shared team, project, channel, runbook, or operational facts.",
1564
+ ...allowsPreference ? [
1565
+ "- Set kind=preference only for clear durable first-person facts authored by the current actor about their own preference, opinion, habit, identity, or workflow.",
1566
+ "- A single task request or ask-for-help is never a durable preference, even when phrased as an ongoing action for this run (for example 'help me capture takeaways as we go'). Do not convert a one-off ask into a 'Prefers ...' memory.",
1567
+ "- A durable preference requires explicitly stated, generalizable first-person phrasing such as 'I prefer ...', 'I always ...', or 'I never ...' that describes how the actor wants things done in general, not just for the current task."
1568
+ ] : [
1569
+ "- This completed run has multiple run actors. Return only conversation-scoped procedure or knowledge memories.",
1570
+ "- Do not return personal preferences, opinions, habits, identity facts, or workflow preferences from any actor in this run.",
1571
+ "- Do not convert a personal first-person statement into shared knowledge or procedure. Statements like 'I prefer ...', 'I use ...', 'I always ...', or 'I never ...' are not memory evidence in this run.",
1572
+ "- Shared team, channel, repository, or operational norms are eligible only when the source states them as collective practice or durable operational fact, not as one individual's preference."
1573
+ ],
1574
+ "- Reject named third-person personal facts such as another person's preference, opinion, habit, identity, relationship, or workflow. Do not assume a named person is the current actor.",
1575
+ "- User-authored task instructions are procedures, not preferences, unless they explicitly describe the actor's personal preference or habit.",
1576
+ "- Procedural statements such as 'for X, do Y', 'when X, do Y', and 'to accomplish X, do Y' belong in procedures.",
1577
+ ...CANONICAL_CONTENT_RULES,
1578
+ "- Skip a candidate when existing-memories already cover the same durable fact.",
1579
+ "- Reject third-party personal profile facts, even if they mention a name.",
1580
+ "- If unsure, return no memory for that candidate.",
1581
+ "</rules>",
1582
+ "</memory-extraction-input>"
1583
+ ].join("\n");
1584
+ }
1585
+ function preferenceAdjudicationPrompt(request) {
1586
+ return [
1587
+ "<memory-preference-adjudication-input>",
1588
+ "Classify the candidate preference against the related active preferences.",
1589
+ "",
1590
+ runtimeDescription({
1591
+ runtimeContext: request.runtimeContext
1592
+ }),
1593
+ "",
1594
+ "<candidate>",
1595
+ escapeXml(JSON.stringify(request.candidate)),
1596
+ "</candidate>",
1597
+ "",
1598
+ "<existing-memories>",
1599
+ escapeXml(JSON.stringify(request.existingMemories)),
1600
+ "</existing-memories>",
1601
+ "",
1602
+ "<rules>",
1603
+ "- Return duplicate when the candidate and one existing memory express the same durable preference or value with different wording.",
1604
+ "- Return supersedes_old only when the candidate and old memory describe the same mutable preference slot and the candidate is the newer value.",
1605
+ "- Examples of same mutable slot: preferred programming language, preferred review style, preferred notification cadence, preferred tool for a task.",
1606
+ "- Return distinct when the candidate is an additional preference or belongs to a different task or topic.",
1607
+ "- Return uncertain when broader or narrower wording makes equivalence or replacement unclear.",
1608
+ "- Do not supersede memories from different topics even if they are both preferences.",
1609
+ "- duplicateId and supersededIds may contain only ids from existing-memories.",
1610
+ "- If unsure, return uncertain.",
1611
+ "</rules>",
1612
+ "</memory-preference-adjudication-input>"
1613
+ ].join("\n");
1614
+ }
1615
+ function createMemoryAgent(model) {
1616
+ return {
1617
+ async adjudicateSupersession(rawRequest) {
1618
+ const request = memorySupersessionInputSchema.parse(rawRequest);
1619
+ const result = await model.completeObject({
1620
+ schema: memorySupersessionDecisionSchema,
1621
+ system: MEMORY_PREFERENCE_ADJUDICATION_SYSTEM,
1622
+ prompt: preferenceAdjudicationPrompt(request),
1623
+ maxTokens: 400
1624
+ });
1625
+ return memorySupersessionDecisionSchema.parse(result.object);
1626
+ },
1627
+ async extractSessionMemories(rawRequest) {
1628
+ const request = extractSessionRequestSchema.parse(rawRequest);
1629
+ const result = await model.completeObject({
1630
+ schema: extractMemoriesResponseSchema,
1631
+ system: MEMORY_EXTRACTION_SYSTEM,
1632
+ prompt: sessionExtractionPrompt(request),
1633
+ maxTokens: 1e3
1634
+ });
1635
+ return extractedMemoriesFromResponse(
1636
+ extractMemoriesResponseSchema.parse(result.object)
1637
+ );
1638
+ },
1639
+ async reviewCreateRequest(rawRequest) {
1640
+ const request = parseCreateMemoryRequest(rawRequest);
1641
+ const result = await model.completeObject({
1642
+ schema: memoryReviewResponseSchema,
1643
+ system: MEMORY_REVIEW_SYSTEM,
1644
+ prompt: reviewPrompt(request),
1645
+ maxTokens: 700
1646
+ });
1647
+ const response = memoryReviewResponseSchema.parse(result.object);
1648
+ return memoryReviewFromResponse(response);
1649
+ }
1650
+ };
1651
+ }
1652
+ function memoryReviewFromResponse(response) {
1653
+ if (response.decision === "store") {
1654
+ return parseMemoryReview({
1655
+ decision: "store",
1656
+ kind: response.kind,
1657
+ content: response.canonicalFact,
1658
+ ...response.expiresAtMs !== null ? { expiresAtMs: response.expiresAtMs } : {}
1711
1659
  });
1712
- if (!idempotent) {
1713
- throw new Error("Memory idempotency conflict did not resolve.");
1660
+ }
1661
+ return parseMemoryReview({
1662
+ decision: "reject",
1663
+ reason: response.reason
1664
+ });
1665
+ }
1666
+ function extractedMemoriesFromResponse(response) {
1667
+ const toMemory = (memory) => parseExtractedMemory({
1668
+ content: memory.canonicalFact,
1669
+ expiresAtMs: memory.expiresAtMs,
1670
+ kind: memory.kind,
1671
+ evidenceMessageIndices: memory.evidenceMessageIndices
1672
+ });
1673
+ return response.memories.map(toMemory);
1674
+ }
1675
+ function parseExtractedMemory(memory) {
1676
+ return extractedMemoryResultSchema.parse(memory);
1677
+ }
1678
+ function parseMemoryReview(result) {
1679
+ return memoryReviewDecisionSchema.parse(result);
1680
+ }
1681
+ function parseCreateMemoryRequest(request) {
1682
+ return createMemoryRequestSchema.parse(request);
1683
+ }
1684
+
1685
+ // src/cli/search.ts
1686
+ import { InvalidArgumentError, Option } from "commander";
1687
+ import { and as and2, desc as desc2, eq as eq2, gt as gt2, ilike as ilike2, isNull as isNull2, or as or2 } from "drizzle-orm";
1688
+
1689
+ // src/cli/format.ts
1690
+ function formatDate(ms) {
1691
+ return ms === null ? "-" : new Date(ms).toISOString();
1692
+ }
1693
+ function formatMemory(row, args) {
1694
+ const lines = [
1695
+ `id=${row.id}`,
1696
+ `scope=${row.scope}`,
1697
+ `scope_key=${row.scopeKey}`,
1698
+ `subject_type=${row.subjectType}`,
1699
+ ...row.subjectKey ? [`subject_key=${row.subjectKey}`] : [],
1700
+ `kind=${row.kind}`,
1701
+ `created_at=${formatDate(row.createdAtMs)}`,
1702
+ `observed_at=${formatDate(row.observedAtMs)}`,
1703
+ `expires_at=${formatDate(row.expiresAtMs)}`,
1704
+ `archived_at=${formatDate(row.archivedAtMs)}`
1705
+ ];
1706
+ if (args.showContent) {
1707
+ lines.push(`content=${row.content}`);
1708
+ }
1709
+ return lines.join("\n");
1710
+ }
1711
+
1712
+ // src/cli/search.ts
1713
+ function parseLimit(value) {
1714
+ const parsed = Number(value);
1715
+ if (!Number.isFinite(parsed)) {
1716
+ throw new InvalidArgumentError("--limit must be a number");
1717
+ }
1718
+ return Math.min(100, Math.max(1, Math.floor(parsed)));
1719
+ }
1720
+ async function runSearch(ctx, queryParts, options) {
1721
+ const query = (queryParts ?? []).join(" ").trim();
1722
+ const nowMs = Date.now();
1723
+ const terms = [
1724
+ ...new Set(
1725
+ query.toLowerCase().split(/[^a-z0-9_'-]+/).map((term) => term.trim()).filter((term) => term.length >= 2)
1726
+ )
1727
+ ];
1728
+ const db = ctx.db;
1729
+ const activeExpirationPredicate = or2(
1730
+ isNull2(juniorMemoryMemories.expiresAtMs),
1731
+ gt2(juniorMemoryMemories.expiresAtMs, nowMs)
1732
+ );
1733
+ const predicates = [
1734
+ eq2(juniorMemoryMemories.scope, options.scope),
1735
+ eq2(juniorMemoryMemories.scopeKey, options.scopeKey),
1736
+ isNull2(juniorMemoryMemories.archivedAtMs),
1737
+ isNull2(juniorMemoryMemories.supersededAtMs),
1738
+ isNull2(juniorMemoryMemories.supersededById)
1739
+ ];
1740
+ if (activeExpirationPredicate) {
1741
+ predicates.push(activeExpirationPredicate);
1742
+ }
1743
+ if (terms.length > 0) {
1744
+ const termPredicate = or2(
1745
+ ...terms.map((term) => ilike2(juniorMemoryMemories.content, `%${term}%`))
1746
+ );
1747
+ if (termPredicate) {
1748
+ predicates.push(termPredicate);
1714
1749
  }
1715
- await storeEmbedding({
1716
- content: idempotent.content,
1717
- db,
1718
- embedder,
1719
- memoryId: idempotent.id,
1720
- nowMs
1721
- });
1722
- return { created: false, memory: idempotent };
1723
1750
  }
1751
+ const rows = await db.select().from(juniorMemoryMemories).where(and2(...predicates)).orderBy(desc2(juniorMemoryMemories.createdAtMs)).limit(options.limit);
1752
+ if (rows.length === 0) {
1753
+ await ctx.io.writeOutput("No memories matched.\n");
1754
+ return 0;
1755
+ }
1756
+ await ctx.io.writeOutput(
1757
+ `${rows.map(
1758
+ (row) => formatMemory(row, { showContent: Boolean(options.showContent) })
1759
+ ).join("\n\n")}
1760
+ `
1761
+ );
1762
+ return 0;
1763
+ }
1764
+ function configureMemorySearchCommand(parent, junior) {
1765
+ parent.command("search").description("Search visible memories").argument("[query...]", "Search query").addOption(
1766
+ new Option("--scope <scope>", "Memory scope").choices([...MEMORY_SCOPES]).makeOptionMandatory()
1767
+ ).requiredOption("--scope-key <key>", "Scope key").addOption(
1768
+ new Option("--limit <n>", "Maximum rows").argParser(parseLimit).default(20)
1769
+ ).option("--show-content", "Print raw memory content").action(
1770
+ junior.action(async (ctx, queryParts, options) => {
1771
+ return await runSearch(
1772
+ ctx,
1773
+ queryParts,
1774
+ options
1775
+ );
1776
+ })
1777
+ );
1778
+ }
1779
+
1780
+ // src/cli/show.ts
1781
+ import { eq as eq3 } from "drizzle-orm";
1782
+ async function runShow(ctx, id) {
1783
+ const db = ctx.db;
1784
+ const rows = await db.select().from(juniorMemoryMemories).where(eq3(juniorMemoryMemories.id, id)).limit(1);
1785
+ if (!rows[0]) {
1786
+ await ctx.io.writeError(`Memory not found: ${id}
1787
+ `);
1788
+ return 1;
1789
+ }
1790
+ await ctx.io.writeOutput(`${formatMemory(rows[0], { showContent: true })}
1791
+ `);
1792
+ return 0;
1793
+ }
1794
+ function configureMemoryShowCommand(parent, junior) {
1795
+ parent.command("show").description("Show one memory").argument("<id>", "Memory id").action(
1796
+ junior.action(async (ctx, id) => {
1797
+ return await runShow(ctx, id);
1798
+ })
1799
+ );
1800
+ }
1801
+
1802
+ // src/cli/index.ts
1803
+ function createMemoryCliCommand() {
1724
1804
  return {
1725
- async archiveExpiredMemories(input) {
1726
- return await archiveExpiredVisibleMemories(input, getNowMs());
1727
- },
1728
- async createMemory(input) {
1729
- return await createScopedMemory(input, "personal");
1730
- },
1731
- async createConversationMemory(input) {
1732
- return await createScopedMemory(input, "conversation");
1733
- },
1734
- async listMemories(input) {
1735
- input = listMemoriesInputSchema.parse(input);
1736
- const nowMs = getNowMs();
1737
- const scopes = deriveVisibleMemoryScopes(runtimeContext);
1738
- await archiveExpiredMemoryBatch({
1739
- db,
1740
- nowMs,
1741
- scopes
1742
- });
1743
- return await listVisibleMemories({
1744
- db,
1745
- limit: input.limit,
1746
- nowMs,
1747
- scopes
1748
- });
1749
- },
1750
- async searchMemories(input) {
1751
- input = searchMemoriesInputSchema.parse(input);
1752
- const nowMs = getNowMs();
1753
- const scopes = deriveVisibleMemoryScopes(runtimeContext);
1754
- await archiveExpiredMemoryBatch({
1755
- db,
1756
- nowMs,
1757
- scopes
1758
- });
1759
- const limit = boundedLimit(input.limit, DEFAULT_SEARCH_LIMIT);
1760
- const vectorCandidates = await searchVisibleVectorMemories({
1761
- db,
1762
- embedder,
1763
- limit: limit * VECTOR_SEARCH_OVERFETCH,
1764
- ...maxVectorDistance !== void 0 ? { maxDistance: maxVectorDistance } : {},
1765
- nowMs,
1766
- query: input.query,
1767
- scopes
1768
- });
1769
- const candidates = await searchVisibleMemories({
1770
- db,
1771
- nowMs,
1772
- query: input.query,
1773
- scopes
1774
- });
1775
- const terms = searchTerms(input.query);
1776
- const lexicalCandidates = candidates.map((candidate) => ({
1777
- ...candidate,
1778
- score: searchScore(candidate.memory, terms)
1779
- })).filter((item) => item.score > 0);
1780
- return mergeSearchCandidates(
1781
- [...vectorCandidates, ...lexicalCandidates],
1782
- nowMs,
1783
- sourceChannelPrefix(runtimeContext)
1784
- ).slice(0, limit);
1785
- },
1786
- async archiveMemory(input) {
1787
- input = archiveMemoryInputSchema.parse(input);
1788
- const nowMs = getNowMs();
1789
- const scopes = deriveVisibleMemoryScopes(runtimeContext);
1790
- const predicate = activeVisiblePredicate({ nowMs, scopes });
1791
- const idPrefix = input.id.trim();
1792
- if (!idPrefix) {
1793
- throw new Error("Memory id is required.");
1794
- }
1795
- const rows = predicate ? await db.select().from(juniorMemoryMemories).where(
1796
- and2(
1797
- predicate,
1798
- or2(
1799
- eq3(juniorMemoryMemories.id, idPrefix),
1800
- like(juniorMemoryMemories.id, `${idPrefix}%`)
1801
- )
1802
- )
1803
- ).orderBy(asc(juniorMemoryMemories.id)).limit(2) : [];
1804
- if (rows.length === 0) {
1805
- throw new Error("Memory was not found in the current context.");
1806
- }
1807
- if (rows.length > 1) {
1808
- throw new Error("Memory id prefix is ambiguous.");
1809
- }
1810
- const memory = parseMemoryRow(rows[0]);
1811
- const updated = await db.update(juniorMemoryMemories).set({
1812
- archivedAtMs: nowMs,
1813
- archiveReason: input.reason ?? "user_removed"
1814
- }).where(eq3(juniorMemoryMemories.id, memory.id)).returning();
1815
- await db.delete(juniorMemoryEmbeddings).where(eq3(juniorMemoryEmbeddings.memoryId, memory.id));
1816
- return parseMemoryRow(updated[0]);
1805
+ name: "memory",
1806
+ summary: "Inspect Junior memory state",
1807
+ configure(command, junior) {
1808
+ configureMemorySearchCommand(command, junior);
1809
+ configureMemoryShowCommand(command, junior);
1817
1810
  }
1818
1811
  };
1819
1812
  }
1820
1813
 
1821
1814
  // src/tools.ts
1815
+ import { Type } from "@sinclair/typebox";
1816
+ import { Value } from "@sinclair/typebox/value";
1817
+ import {
1818
+ definePluginTool,
1819
+ getSourceKey,
1820
+ PluginToolInputError,
1821
+ pluginToolResultSchema
1822
+ } from "@sentry/junior-plugin-api";
1823
+ import { z as z4 } from "zod";
1822
1824
  var MAX_TOOL_CONTENT_CHARS = 4e3;
1823
1825
  var DEFAULT_RESULT_LIMIT = 20;
1824
1826
  var DEFAULT_SEARCH_LIMIT2 = 10;