@makefinks/daemon 0.7.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "module": "src/index.tsx",
30
30
  "type": "module",
31
- "version": "0.7.2",
31
+ "version": "0.9.0",
32
32
  "bin": {
33
33
  "daemon": "dist/cli.js"
34
34
  },
@@ -73,6 +73,7 @@
73
73
  "typescript": "^5.9.2"
74
74
  },
75
75
  "dependencies": {
76
+ "@ai-sdk/mcp": "^1.0.14",
76
77
  "@ai-sdk/openai": "^3.0.0",
77
78
  "@openrouter/ai-sdk-provider": "^2.1.0",
78
79
  "@opentui-ui/toast": "^0.0.3",
@@ -40,6 +40,7 @@ export class AgentTurnRunner {
40
40
  this.abortController = new AbortController();
41
41
 
42
42
  const isActive = () => runId === this.activeRunId && this.abortController !== null;
43
+ const isCurrent = () => runId === this.activeRunId;
43
44
 
44
45
  let result: AgentTurnResult | null = null;
45
46
  let error: Error | null = null;
@@ -93,6 +94,10 @@ export class AgentTurnRunner {
93
94
  if (!isActive()) return;
94
95
  callbacks.onStepUsage?.(usage);
95
96
  },
97
+ onMemorySaved: (preview) => {
98
+ if (!isCurrent()) return;
99
+ callbacks.onMemorySaved?.(preview);
100
+ },
96
101
  onComplete: (fullText, responseMessages, usage, finalText) => {
97
102
  if (!isActive()) return;
98
103
  result = { fullText, responseMessages, usage, finalText };
@@ -21,6 +21,8 @@ import type {
21
21
  ToolApprovalRequest,
22
22
  ToolApprovalResponse,
23
23
  TranscriptionResult,
24
+ MemoryToastPreview,
25
+ MemoryToastOperation,
24
26
  } from "../types";
25
27
  import { debug, toolDebug } from "../utils/debug-logger";
26
28
  import { getOpenRouterReportedCost } from "../utils/openrouter-reported-cost";
@@ -166,7 +168,7 @@ export async function generateResponse(
166
168
 
167
169
  // Include relevant memories in the system prompt if available
168
170
  let memoryInjection: string | undefined;
169
- if (isMemoryAvailable()) {
171
+ if (getDaemonManager().memoryEnabled && isMemoryAvailable()) {
170
172
  const injection = await buildMemoryInjection(userMessage);
171
173
  if (injection) {
172
174
  memoryInjection = injection;
@@ -176,29 +178,6 @@ export async function generateResponse(
176
178
  // Add the user message
177
179
  messages.push({ role: "user" as const, content: userMessage });
178
180
 
179
- const userTextForMemory = userMessage.trim();
180
- if (userTextForMemory) {
181
- void (async () => {
182
- if (!isMemoryAvailable()) return;
183
- const memoryManager = getMemoryManager();
184
- await memoryManager.initialize();
185
- if (!memoryManager.isAvailable) return;
186
- try {
187
- await memoryManager.add(
188
- [{ role: "user", content: userTextForMemory }],
189
- {
190
- timestamp: new Date().toISOString(),
191
- source: "conversation",
192
- },
193
- true
194
- );
195
- } catch (error) {
196
- const err = error instanceof Error ? error : new Error(String(error));
197
- debug.error("memory-auto-add-failed", { message: err.message });
198
- }
199
- })();
200
- }
201
-
202
181
  // Stream response from the agent with mode-specific system prompt
203
182
  const agent = await createDaemonAgent(interactionMode, reasoningEffort, memoryInjection);
204
183
 
@@ -317,6 +296,11 @@ export async function generateResponse(
317
296
  }
318
297
 
319
298
  callbacks.onComplete?.(fullText, allResponseMessages, undefined, finalText);
299
+
300
+ void persistConversationMemory(userMessage, finalText ?? fullText).then((preview) => {
301
+ if (!preview) return;
302
+ callbacks.onMemorySaved?.(preview);
303
+ });
320
304
  } catch (error) {
321
305
  // Check if this was an abort - don't treat as error
322
306
  if (abortSignal?.aborted) {
@@ -334,6 +318,72 @@ export async function generateResponse(
334
318
  }
335
319
  }
336
320
 
321
+ async function persistConversationMemory(
322
+ userMessage: string,
323
+ assistantMessage: string
324
+ ): Promise<string | null> {
325
+ const userTextForMemory = userMessage.trim();
326
+ const assistantTextForMemory = assistantMessage.trim();
327
+
328
+ if (!userTextForMemory || !assistantTextForMemory) return null;
329
+ if (!getDaemonManager().memoryEnabled) return null;
330
+ if (!isMemoryAvailable()) return null;
331
+
332
+ const memoryManager = getMemoryManager();
333
+ await memoryManager.initialize();
334
+ if (!memoryManager.isAvailable) return null;
335
+
336
+ try {
337
+ const memoryMessages = [
338
+ { role: "user", content: `<user>${userTextForMemory}</user>` },
339
+ { role: "assistant", content: `<assistant>${assistantTextForMemory}</assistant>` },
340
+ ];
341
+ const result = await memoryManager.add(
342
+ memoryMessages,
343
+ {
344
+ timestamp: new Date().toISOString(),
345
+ source: "conversation",
346
+ },
347
+ true
348
+ );
349
+ return buildMemoryToastPreview(result.results);
350
+ } catch (error) {
351
+ const err = error instanceof Error ? error : new Error(String(error));
352
+ debug.error("memory-auto-add-failed", { message: err.message });
353
+ return null;
354
+ }
355
+ }
356
+
357
+ function buildMemoryToastPreview(
358
+ results: Array<{ memory: string; event: "ADD" | "UPDATE" | "DELETE" | "NONE" }>
359
+ ): MemoryToastPreview | null {
360
+ if (results.length === 0) return null;
361
+
362
+ const saved = results.filter((entry) => entry.event === "ADD" || entry.event === "UPDATE");
363
+ if (saved.length === 0) return null;
364
+
365
+ const previewEntries = saved.length > 2 ? saved.slice(-2) : saved;
366
+ const lines = previewEntries.map((entry) => `• ${truncatePreview(entry.memory, 52)}`);
367
+ if (saved.length > previewEntries.length) {
368
+ lines.push(`• +${saved.length - previewEntries.length} more`);
369
+ }
370
+
371
+ const hasUpdate = saved.some((entry) => entry.event === "UPDATE");
372
+ const operation: MemoryToastOperation = hasUpdate ? "UPDATE" : "ADD";
373
+
374
+ return {
375
+ operation,
376
+ description: lines.join("\n"),
377
+ };
378
+ }
379
+
380
+ function truncatePreview(text: string, maxChars: number): string {
381
+ const trimmed = text.trim();
382
+ if (trimmed.length <= maxChars) return trimmed;
383
+ if (maxChars <= 1) return "…";
384
+ return `${trimmed.slice(0, maxChars - 1)}…`;
385
+ }
386
+
337
387
  /**
338
388
  * Generate a short descriptive title for a session based on the first user message.
339
389
  * Uses the currently selected model.
@@ -0,0 +1,348 @@
1
+ import { createHash } from "node:crypto";
2
+ import { EventEmitter } from "node:events";
3
+
4
+ import { type MCPClient, createMCPClient } from "@ai-sdk/mcp";
5
+ import type { ToolSet } from "ai";
6
+
7
+ import { type McpServerConfig, type McpTransportType, loadManualConfig } from "../../utils/config";
8
+ import { debug } from "../../utils/debug-logger";
9
+
10
+ export type McpServerLifecycleStatus = "idle" | "loading" | "ready" | "error";
11
+
12
+ export interface McpServerStatus {
13
+ id: string;
14
+ type: McpTransportType;
15
+ url: string;
16
+ status: McpServerLifecycleStatus;
17
+ toolCount: number;
18
+ error?: string;
19
+ }
20
+
21
+ export interface McpToolMeta {
22
+ internalName: string;
23
+ serverId: string;
24
+ originalToolName: string;
25
+ }
26
+
27
+ type McpServerResolvedConfig = {
28
+ id: string;
29
+ type: McpTransportType;
30
+ url: string;
31
+ };
32
+
33
+ const MAX_TOOL_NAME_LENGTH = 64;
34
+
35
+ function sanitizeNamePart(raw: string): string {
36
+ const cleaned = raw
37
+ .replace(/[^a-zA-Z0-9_-]/g, "_")
38
+ .replace(/_+/g, "_")
39
+ .replace(/^_+/, "")
40
+ .replace(/_+$/, "");
41
+ return cleaned.length > 0 ? cleaned : "x";
42
+ }
43
+
44
+ function shortHash(input: string): string {
45
+ return createHash("sha1").update(input).digest("hex").slice(0, 8);
46
+ }
47
+
48
+ function deriveServerIdFromUrl(url: string): string | null {
49
+ try {
50
+ const parsed = new URL(url);
51
+ const host = parsed.hostname;
52
+ if (!host) return null;
53
+ const port = parsed.port ? `_${parsed.port}` : "";
54
+ return `${host}${port}`;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ function ensureUniqueId(base: string, used: Set<string>): string {
61
+ let candidate = base;
62
+ let i = 2;
63
+ while (used.has(candidate)) {
64
+ candidate = `${base}-${i}`;
65
+ i++;
66
+ }
67
+ used.add(candidate);
68
+ return candidate;
69
+ }
70
+
71
+ function buildInternalToolName(serverId: string, toolName: string, used: Set<string>): string {
72
+ const serverPart = sanitizeNamePart(serverId);
73
+ const toolPart = sanitizeNamePart(toolName);
74
+ const base = `mcp_${serverPart}__${toolPart}`;
75
+ if (base.length <= MAX_TOOL_NAME_LENGTH && !used.has(base)) {
76
+ used.add(base);
77
+ return base;
78
+ }
79
+
80
+ const hash = shortHash(`${serverId}\0${toolName}`);
81
+ let left = serverPart;
82
+ let right = toolPart;
83
+ let candidate = `mcp_${left}__${right}__${hash}`;
84
+ if (candidate.length > MAX_TOOL_NAME_LENGTH) {
85
+ const maxRight = Math.max(8, MAX_TOOL_NAME_LENGTH - `mcp_${left}____${hash}`.length);
86
+ right = right.slice(0, maxRight);
87
+ candidate = `mcp_${left}__${right}__${hash}`;
88
+ }
89
+
90
+ if (candidate.length > MAX_TOOL_NAME_LENGTH) {
91
+ candidate = candidate.slice(0, MAX_TOOL_NAME_LENGTH);
92
+ }
93
+
94
+ if (!used.has(candidate)) {
95
+ used.add(candidate);
96
+ return candidate;
97
+ }
98
+
99
+ let counter = 2;
100
+ let next = candidate;
101
+ while (used.has(next)) {
102
+ const counterHash = shortHash(`${serverId}\0${toolName}\0${counter}`);
103
+ next = `mcp_${left}__${right}__${counterHash}`;
104
+ if (next.length > MAX_TOOL_NAME_LENGTH) {
105
+ next = next.slice(0, MAX_TOOL_NAME_LENGTH);
106
+ }
107
+ counter++;
108
+ }
109
+ used.add(next);
110
+ return next;
111
+ }
112
+
113
+ function resolveServerConfigs(raw: McpServerConfig[] | undefined): McpServerResolvedConfig[] {
114
+ if (!raw || raw.length === 0) return [];
115
+ const usedIds = new Set<string>();
116
+ const out: McpServerResolvedConfig[] = [];
117
+
118
+ for (const entry of raw) {
119
+ if (!entry || typeof entry !== "object") continue;
120
+ const type = entry.type;
121
+ const url = entry.url;
122
+ if ((type !== "http" && type !== "sse") || typeof url !== "string" || url.trim().length === 0) continue;
123
+ const derivedId = entry.id?.trim() ? entry.id.trim() : deriveServerIdFromUrl(url.trim());
124
+ const id = derivedId
125
+ ? ensureUniqueId(derivedId, usedIds)
126
+ : ensureUniqueId(`server-${out.length + 1}`, usedIds);
127
+ out.push({ id, type, url: url.trim() });
128
+ }
129
+
130
+ return out;
131
+ }
132
+
133
+ class McpManager extends EventEmitter {
134
+ private started = false;
135
+ private loadRunId = 0;
136
+
137
+ private servers: McpServerStatus[] = [];
138
+ private mergedTools: ToolSet = {};
139
+ private toolMetaByName = new Map<string, McpToolMeta>();
140
+ private internalNamesByServer = new Map<string, Set<string>>();
141
+
142
+ private clientsByServer = new Map<string, MCPClient>();
143
+ private toolsByServer = new Map<string, ToolSet>();
144
+
145
+ start(): void {
146
+ if (this.started) return;
147
+ this.started = true;
148
+ setImmediate(() => {
149
+ void this.loadFromConfig();
150
+ });
151
+ }
152
+
153
+ getServersSnapshot(): McpServerStatus[] {
154
+ return this.servers.map((s) => ({ ...s }));
155
+ }
156
+
157
+ getToolsSnapshot(): ToolSet {
158
+ return this.mergedTools;
159
+ }
160
+
161
+ getToolMeta(toolName: string): McpToolMeta | null {
162
+ return this.toolMetaByName.get(toolName) ?? null;
163
+ }
164
+
165
+ async closeAll(): Promise<void> {
166
+ const clients = [...this.clientsByServer.values()];
167
+ this.clientsByServer.clear();
168
+ await Promise.allSettled(clients.map((client) => client.close()));
169
+ }
170
+
171
+ private emitUpdate(): void {
172
+ this.emit("update");
173
+ }
174
+
175
+ private rebuildMergedTools(): void {
176
+ const merged: ToolSet = {};
177
+ for (const tools of this.toolsByServer.values()) {
178
+ Object.assign(merged, tools);
179
+ }
180
+ this.mergedTools = merged;
181
+ }
182
+
183
+ private clearServerTools(serverId: string): void {
184
+ const internalNames = this.internalNamesByServer.get(serverId);
185
+ if (internalNames) {
186
+ for (const internalName of internalNames) {
187
+ this.toolMetaByName.delete(internalName);
188
+ }
189
+ }
190
+ this.internalNamesByServer.delete(serverId);
191
+ this.toolsByServer.delete(serverId);
192
+ }
193
+
194
+ private setServerStatus(next: McpServerStatus): void {
195
+ const idx = this.servers.findIndex((s) => s.id === next.id);
196
+ if (idx >= 0) {
197
+ const copy = [...this.servers];
198
+ copy[idx] = next;
199
+ this.servers = copy;
200
+ } else {
201
+ this.servers = [...this.servers, next];
202
+ }
203
+ this.emitUpdate();
204
+ }
205
+
206
+ private async loadFromConfig(): Promise<void> {
207
+ const runId = ++this.loadRunId;
208
+ const config = loadManualConfig();
209
+ const servers = resolveServerConfigs(config.mcpServers);
210
+
211
+ this.servers = servers.map((server) => ({
212
+ id: server.id,
213
+ type: server.type,
214
+ url: server.url,
215
+ status: "loading" as const,
216
+ toolCount: 0,
217
+ }));
218
+ this.emitUpdate();
219
+
220
+ if (servers.length === 0) {
221
+ this.mergedTools = {};
222
+ this.toolMetaByName.clear();
223
+ this.internalNamesByServer.clear();
224
+ this.toolsByServer.clear();
225
+ this.emitUpdate();
226
+ return;
227
+ }
228
+
229
+ await Promise.allSettled(
230
+ servers.map(async (server) => {
231
+ if (runId !== this.loadRunId) return;
232
+ await this.loadSingleServer(server, runId);
233
+ })
234
+ );
235
+ }
236
+
237
+ private async loadSingleServer(server: McpServerResolvedConfig, runId: number): Promise<void> {
238
+ const startedAt = Date.now();
239
+ const currentStatus = this.servers.find((s) => s.id === server.id);
240
+ if (!currentStatus || runId !== this.loadRunId) return;
241
+
242
+ this.setServerStatus({
243
+ id: server.id,
244
+ type: server.type,
245
+ url: server.url,
246
+ status: "loading",
247
+ toolCount: 0,
248
+ });
249
+
250
+ let client: MCPClient | null = null;
251
+ try {
252
+ client = await createMCPClient({
253
+ transport: {
254
+ type: server.type,
255
+ url: server.url,
256
+ },
257
+ });
258
+
259
+ const tools = await client.tools();
260
+ const usedNames = new Set<string>(this.toolMetaByName.keys());
261
+ const remapped: Record<string, unknown> = {};
262
+ const internalNames = new Set<string>();
263
+
264
+ for (const [toolName, toolValue] of Object.entries(tools)) {
265
+ const internalName = buildInternalToolName(server.id, toolName, usedNames);
266
+ (remapped as Record<string, unknown>)[internalName] = toolValue;
267
+ internalNames.add(internalName);
268
+ this.toolMetaByName.set(internalName, {
269
+ internalName,
270
+ serverId: server.id,
271
+ originalToolName: toolName,
272
+ });
273
+ }
274
+
275
+ // Replace existing server registration atomically
276
+ this.clearServerTools(server.id);
277
+ this.internalNamesByServer.set(server.id, internalNames);
278
+ this.toolsByServer.set(server.id, remapped as ToolSet);
279
+ this.clientsByServer
280
+ .get(server.id)
281
+ ?.close()
282
+ .catch(() => {});
283
+ this.clientsByServer.set(server.id, client);
284
+ client = null;
285
+
286
+ this.rebuildMergedTools();
287
+ this.setServerStatus({
288
+ id: server.id,
289
+ type: server.type,
290
+ url: server.url,
291
+ status: "ready",
292
+ toolCount: internalNames.size,
293
+ });
294
+ debug.info("mcp-server-ready", {
295
+ id: server.id,
296
+ type: server.type,
297
+ url: server.url,
298
+ toolCount: internalNames.size,
299
+ ms: Date.now() - startedAt,
300
+ });
301
+ } catch (error) {
302
+ const err = error instanceof Error ? error : new Error(String(error));
303
+ this.clearServerTools(server.id);
304
+ this.rebuildMergedTools();
305
+ this.setServerStatus({
306
+ id: server.id,
307
+ type: server.type,
308
+ url: server.url,
309
+ status: "error",
310
+ toolCount: 0,
311
+ error: err.message,
312
+ });
313
+ debug.warn("mcp-server-error", {
314
+ id: server.id,
315
+ type: server.type,
316
+ url: server.url,
317
+ message: err.message,
318
+ });
319
+ if (client) {
320
+ try {
321
+ await client.close();
322
+ } catch {
323
+ // Ignore close failures
324
+ }
325
+ }
326
+ }
327
+ }
328
+ }
329
+
330
+ let singleton: McpManager | null = null;
331
+
332
+ export function getMcpManager(): McpManager {
333
+ if (!singleton) {
334
+ singleton = new McpManager();
335
+ }
336
+ return singleton;
337
+ }
338
+
339
+ export function startMcpManager(): void {
340
+ getMcpManager().start();
341
+ }
342
+
343
+ export function destroyMcpManager(): void {
344
+ if (!singleton) return;
345
+ void singleton.closeAll();
346
+ singleton.removeAllListeners();
347
+ singleton = null;
348
+ }
@@ -11,6 +11,7 @@ import { getAppConfigDir } from "../../utils/preferences";
11
11
  import { getMemoryModel } from "../model-config";
12
12
 
13
13
  const MEMORY_USER_ID = "daemon_global";
14
+ const MAX_MEMORY_INPUT_CHARS = 10_000;
14
15
  /** Raw memory entry from mem0 API */
15
16
  interface Mem0RawEntry {
16
17
  id: string;
@@ -106,6 +107,71 @@ class MemoryManager {
106
107
 
107
108
  this.memory = new Memory({
108
109
  version: "v1.1",
110
+ customPrompt: `You are a Personal Information Organizer, specialized in extracting **enduring** facts, user memories, and preferences.
111
+ Your role is to extract **only** information that would be useful to recall in a conversation two weeks from now.
112
+
113
+ # [IMPORTANT]: GENERATE FACTS SOLELY BASED ON THE USER'S MESSAGES.
114
+ # [IMPORTANT]: DO NOT INCLUDE INFORMATION FROM ASSISTANT OR SYSTEM MESSAGES.
115
+
116
+ ### WHAT TO STORE (The "Two-Week Test"):
117
+ 1. **Biographical Details:** Names, age, job title, company, location.
118
+ 2. **Relationships:** Names of partners, family members, pets, or colleagues.
119
+ 3. **Enduring Preferences:** Strong likes/dislikes (e.g., food, hobbies, style).
120
+ 4. **Long-term Plans:** Upcoming trips, long-term projects, or goals.
121
+ 5. **Direct Instructions:** How the user wants to be addressed or formatted (e.g., "Call me X").
122
+ 6. **Multi-True Facts:** If multiple preferences or details can all be true (e.g., likes multiple languages, foods, hobbies), store each as a separate fact rather than updating/overwriting an existing one.
123
+
124
+ ### WHAT TO IGNORE (Do NOT store these):
125
+ 1. **Transient Commands & Questions:** Do not store that the user asked to "summarize a PDF," "translate a sentence," or "write code."
126
+ 2. **Immediate Context:** Do not store "User said 'continue'" or "User said 'yes'."
127
+ 3. **General Opinions on News/Politics:** Unless the user explicitly identifies with a stance, avoid summarizing general questions (e.g., ignore "What is the capital of France?").
128
+ 4. **Meta-Commentary:** Do not store compliments or insults to the bot (e.g., "You are smart") unless it alters how you should behave.
129
+
130
+ ### Examples:
131
+
132
+ User: Hi there.
133
+ Assistant: Hello! How can I help you today?
134
+ Output: {{"facts" : []}}
135
+
136
+ User: Can you summarize this article for me?
137
+ Assistant: Sure, please paste the text.
138
+ Output: {{"facts" : []}}
139
+ (Reasoning: This is a transient task, not a fact about the user.)
140
+
141
+ User: I am a vegetarian, so please don't suggest any meat dishes.
142
+ Assistant: Noted, I will provide vegetarian options only.
143
+ Output: {{"facts" : ["Is a vegetarian", "Does not eat meat"]}}
144
+
145
+ User: I'm planning a hiking trip to Patagonia next November.
146
+ Assistant: That sounds amazing!
147
+ Output: {{"facts" : ["Planning a hiking trip to Patagonia in November"]}}
148
+
149
+ User: Who is the president of the US?
150
+ Assistant: The current president is...
151
+ Output: {{"facts" : []}}
152
+
153
+ User: My dog's name is Buster. He's a golden retriever.
154
+ Assistant: Buster sounds adorable.
155
+ Output: {{"facts" : ["Has a dog named Buster", "Dog is a golden retriever"]}}
156
+
157
+ User: Actually, I moved. I live in Chicago now, not New York.
158
+ Assistant: Got it, updated your location.
159
+ Output: {{"facts" : ["Lives in Chicago", "No longer lives in New York"]}}
160
+
161
+ User: I hate Python, I prefer coding in Rust.
162
+ Assistant: Understood.
163
+ Output: {{"facts" : ["Dislikes Python", "Prefers coding in Rust"]}}
164
+
165
+ User: test
166
+ Assistant: System operational.
167
+ Output: {{"facts" : []}}
168
+
169
+ Return the facts in JSON format as shown above.
170
+
171
+ Rules:
172
+ - If no *enduring* facts are found, return an empty list for "facts".
173
+ - Detect the language of the user input and record facts in that same language.
174
+ - Write fully self-contained facts (e.g., "Lives in Chicago" instead of "Lives there").`,
109
175
  embedder: {
110
176
  provider: "openai",
111
177
  config: {
@@ -200,6 +266,28 @@ class MemoryManager {
200
266
  throw new Error("Memory system not available");
201
267
  }
202
268
 
269
+ const sanitizedMessages = messages.map((message) => {
270
+ if (message.role !== "user") return message;
271
+ if (message.content.length <= MAX_MEMORY_INPUT_CHARS) return message;
272
+ return {
273
+ ...message,
274
+ content: message.content.slice(0, MAX_MEMORY_INPUT_CHARS),
275
+ };
276
+ });
277
+
278
+ if (sanitizedMessages !== messages) {
279
+ const truncated = sanitizedMessages.some((message, index) => {
280
+ return message.role === "user" && messages[index]?.content.length !== message.content.length;
281
+ });
282
+ if (truncated) {
283
+ memoryDebug.info("memory-add-truncate", {
284
+ maxChars: MAX_MEMORY_INPUT_CHARS,
285
+ originalLengths: messages.map((message) => message.content.length),
286
+ truncatedLengths: sanitizedMessages.map((message) => message.content.length),
287
+ });
288
+ }
289
+ }
290
+
203
291
  const startTime = Date.now();
204
292
  memoryDebug.info("memory-add-input", {
205
293
  infer,
@@ -207,7 +295,7 @@ class MemoryManager {
207
295
  messages,
208
296
  });
209
297
 
210
- const result = (await this.memory.add(messages, {
298
+ const result = (await this.memory.add(sanitizedMessages, {
211
299
  userId: MEMORY_USER_ID,
212
300
  metadata,
213
301
  infer,
@@ -236,7 +324,7 @@ class MemoryManager {
236
324
  rawResults: result.results,
237
325
  durationMs,
238
326
  });
239
- return result;
327
+ return { results: extracted };
240
328
  }
241
329
 
242
330
  /** Get all memories */
@@ -100,7 +100,7 @@ export function buildOpenRouterChatSettings(
100
100
  export const TRANSCRIPTION_MODEL = "gpt-4o-mini-transcribe-2025-12-15";
101
101
 
102
102
  // Default model for memory operations (cheap & fast)
103
- export const DEFAULT_MEMORY_MODEL = "deepseek/deepseek-v3.2";
103
+ export const DEFAULT_MEMORY_MODEL = "x-ai/grok-4.1-fast";
104
104
 
105
105
  /**
106
106
  * Get the model ID for memory operations (deduplication, extraction).