@@ -99,7 +99,7 @@ let client = null;
9999 * After RECOVERY_COOLDOWN_MS, the next request will be allowed through
100100 * to check if the service has recovered.
101101 */
102- function markUnavailable ( ) {
102+ export function markUnavailable ( ) {
103103 mem0Available = false ;
104104 mem0UnavailableSince = Date . now ( ) ;
105105}
@@ -154,15 +154,30 @@ export function getMemoryConfig() {
154154}
155155
156156/**
157- * Check if memory feature is enabled and mem0 is available .
158- * Supports auto-recovery: if mem0 was marked unavailable due to a transient
159- * error and the cooldown period has elapsed, it will be tentatively re-enabled
160- * so the next request can check if the service has recovered .
157+ * Pure availability check — no side effects .
158+ * Returns true only if memory is both enabled in config and currently marked available.
159+ * Does NOT trigger auto-recovery. Use { @link checkAndRecoverMemory} when you want
160+ * the cooldown-based recovery logic .
161161 * @returns {boolean }
162162 */
163163export function isMemoryAvailable ( ) {
164164 const memConfig = getMemoryConfig ( ) ;
165165 if ( ! memConfig . enabled ) return false ;
166+ return mem0Available ;
167+ }
168+
169+ /**
170+ * Check if memory feature is enabled and mem0 is available, with auto-recovery.
171+ * If mem0 was marked unavailable due to a transient error and the cooldown period
172+ * has elapsed, this will tentatively re-enable it so the next request can check
173+ * if the service has recovered.
174+ *
175+ * Use this instead of {@link isMemoryAvailable} when you want the recovery side effect.
176+ * @returns {boolean }
177+ */
178+ export function checkAndRecoverMemory ( ) {
179+ const memConfig = getMemoryConfig ( ) ;
180+ if ( ! memConfig . enabled ) return false ;
166181
167182 if ( mem0Available ) return true ;
168183
@@ -231,9 +246,12 @@ export function _setClient(newClient) {
231246 * Run a health check against the mem0 platform on startup.
232247 * Verifies the API key is configured and the SDK client can actually
233248 * communicate with the hosted platform by performing a lightweight search.
249+ * @param {object } [options]
250+ * @param {AbortSignal } [options.signal] - When aborted, prevents a late-resolving
251+ * check from calling {@link markAvailable} (guards against race with startup timeout).
234252 * @returns {Promise<boolean> } true if mem0 is ready
235253 */
236- export async function checkMem0Health ( ) {
254+ export async function checkMem0Health ( { signal } = { } ) {
237255 const memConfig = getMemoryConfig ( ) ;
238256 if ( ! memConfig . enabled ) {
239257 info ( 'Memory module disabled via config' ) ;
@@ -262,6 +280,11 @@ export async function checkMem0Health() {
262280 limit : 1 ,
263281 } ) ;
264282
283+ // Guard against late resolution after a startup timeout has already
284+ // called markUnavailable(). If the caller's AbortSignal has fired,
285+ // the timeout won the race and we must not flip availability back on.
286+ if ( signal ?. aborted ) return false ;
287+
265288 markAvailable ( ) ;
266289 info ( 'mem0 health check passed (SDK connectivity verified)' ) ;
267290 return true ;
@@ -286,7 +309,7 @@ export async function checkMem0Health() {
286309 * @returns {Promise<boolean> } true if stored successfully
287310 */
288311export async function addMemory ( userId , text , metadata = { } ) {
289- if ( ! isMemoryAvailable ( ) ) return false ;
312+ if ( ! checkAndRecoverMemory ( ) ) return false ;
290313
291314 try {
292315 const c = getClient ( ) ;
@@ -318,7 +341,7 @@ export async function addMemory(userId, text, metadata = {}) {
318341 * @returns {Promise<{memories: Array<{memory: string, score?: number}>, relations: Array}> }
319342 */
320343export async function searchMemories ( userId , query , limit ) {
321- if ( ! isMemoryAvailable ( ) ) return { memories : [ ] , relations : [ ] } ;
344+ if ( ! checkAndRecoverMemory ( ) ) return { memories : [ ] , relations : [ ] } ;
322345
323346 const memConfig = getMemoryConfig ( ) ;
324347 const maxResults = limit ?? memConfig . maxContextMemories ;
@@ -339,7 +362,7 @@ export async function searchMemories(userId, query, limit) {
339362 const relations = result ?. relations || [ ] ;
340363
341364 const memories = rawMemories . map ( ( m ) => ( {
342- id : m . id || '' ,
365+ id : m . id ?? '' ,
343366 memory : m . memory || m . text || m . content || '' ,
344367 score : m . score ?? null ,
345368 } ) ) ;
@@ -358,7 +381,7 @@ export async function searchMemories(userId, query, limit) {
358381 * @returns {Promise<Array<{id: string, memory: string}>> } All user memories
359382 */
360383export async function getMemories ( userId ) {
361- if ( ! isMemoryAvailable ( ) ) return [ ] ;
384+ if ( ! checkAndRecoverMemory ( ) ) return [ ] ;
362385
363386 try {
364387 const c = getClient ( ) ;
@@ -373,7 +396,7 @@ export async function getMemories(userId) {
373396 const memories = Array . isArray ( result ) ? result : result ?. results || [ ] ;
374397
375398 return memories . map ( ( m ) => ( {
376- id : m . id || '' ,
399+ id : m . id ?? '' ,
377400 memory : m . memory || m . text || m . content || '' ,
378401 } ) ) ;
379402 } catch ( err ) {
@@ -389,7 +412,7 @@ export async function getMemories(userId) {
389412 * @returns {Promise<boolean> } true if deleted successfully
390413 */
391414export async function deleteAllMemories ( userId ) {
392- if ( ! isMemoryAvailable ( ) ) return false ;
415+ if ( ! checkAndRecoverMemory ( ) ) return false ;
393416
394417 try {
395418 const c = getClient ( ) ;
@@ -411,7 +434,7 @@ export async function deleteAllMemories(userId) {
411434 * @returns {Promise<boolean> } true if deleted successfully
412435 */
413436export async function deleteMemory ( memoryId ) {
414- if ( ! isMemoryAvailable ( ) ) return false ;
437+ if ( ! checkAndRecoverMemory ( ) ) return false ;
415438
416439 try {
417440 const c = getClient ( ) ;
@@ -435,21 +458,29 @@ export async function deleteMemory(memoryId) {
435458export function formatRelations ( relations ) {
436459 if ( ! relations || relations . length === 0 ) return '' ;
437460
438- const lines = relations . map ( ( r ) => `- ${ r . source } → ${ r . relationship } → ${ r . target } ` ) ;
461+ const lines = relations
462+ . filter ( ( r ) => r . source && r . relationship && r . target )
463+ . map ( ( r ) => `- ${ r . source } → ${ r . relationship } → ${ r . target } ` ) ;
464+
465+ if ( lines . length === 0 ) return '' ;
439466
440467 return `\nRelationships:\n${ lines . join ( '\n' ) } ` ;
441468}
442469
470+ /** Maximum characters for memory context injected into system prompt */
471+ const MAX_MEMORY_CONTEXT_CHARS = 2000 ;
472+
443473/**
444474 * Build a context string from user memories to inject into the system prompt.
445475 * Includes both regular memories and graph relations for richer context.
476+ * Enforces a character budget to prevent oversized system prompts.
446477 * @param {string } userId - Discord user ID
447478 * @param {string } username - Display name
448479 * @param {string } query - The user's current message (for relevance search)
449480 * @returns {Promise<string> } Context string or empty string
450481 */
451482export async function buildMemoryContext ( userId , username , query ) {
452- if ( ! isMemoryAvailable ( ) ) return '' ;
483+ if ( ! checkAndRecoverMemory ( ) ) return '' ;
453484 if ( isOptedOut ( userId ) ) return '' ;
454485
455486 const { memories, relations } = await searchMemories ( userId , query ) ;
@@ -468,6 +499,11 @@ export async function buildMemoryContext(userId, username, query) {
468499 context += relationsContext ;
469500 }
470501
502+ // Enforce character budget to prevent oversized system prompts
503+ if ( context . length > MAX_MEMORY_CONTEXT_CHARS ) {
504+ context = `${ context . substring ( 0 , MAX_MEMORY_CONTEXT_CHARS ) } ...` ;
505+ }
506+
471507 return context ;
472508}
473509
@@ -482,7 +518,7 @@ export async function buildMemoryContext(userId, username, query) {
482518 * @returns {Promise<boolean> } true if any memories were stored
483519 */
484520export async function extractAndStoreMemories ( userId , username , userMessage , assistantReply ) {
485- if ( ! isMemoryAvailable ( ) ) return false ;
521+ if ( ! checkAndRecoverMemory ( ) ) return false ;
486522 if ( isOptedOut ( userId ) ) return false ;
487523
488524 const memConfig = getMemoryConfig ( ) ;
@@ -493,13 +529,14 @@ export async function extractAndStoreMemories(userId, username, userMessage, ass
493529 if ( ! c ) return false ;
494530
495531 const messages = [
496- { role : 'user' , content : ` ${ username } : ${ userMessage } ` } ,
532+ { role : 'user' , content : userMessage } ,
497533 { role : 'assistant' , content : assistantReply } ,
498534 ] ;
499535
500536 await c . add ( messages , {
501537 user_id : userId ,
502538 app_id : APP_ID ,
539+ metadata : { username } ,
503540 enable_graph : true ,
504541 } ) ;
505542
@@ -510,8 +547,10 @@ export async function extractAndStoreMemories(userId, username, userMessage, ass
510547 } ) ;
511548 return true ;
512549 } catch ( err ) {
550+ // Only log — do NOT call markUnavailable() here.
551+ // This runs fire-and-forget in the background; a failure for one user's
552+ // extraction should not disable the memory system for all other users.
513553 logWarn ( 'Memory extraction failed' , { userId, error : err . message } ) ;
514- if ( ! isTransientError ( err ) ) markUnavailable ( ) ;
515554 return false ;
516555 }
517556}
0 commit comments