Skip to content

fix: address mem0 deep review issues from PR #59#63

Merged
BillChirico merged 33 commits intomainfrom
fix/mem0-review-fixes
Feb 16, 2026
Merged

fix: address mem0 deep review issues from PR #59#63
BillChirico merged 33 commits intomainfrom
fix/mem0-review-fixes

Conversation

@BillChirico
Copy link
Collaborator

Summary

Fixes 9 bugs and design issues identified in the deep review of PR #59.

Changes (one commit per fix)

Bugs Fixed

  1. handleForgetTopic silently drops results without IDs — empty string is falsy, so m.id filter dropped valid falsy IDs. Now uses explicit m.id !== undefined && m.id !== null && m.id !== '' check.
  2. formatRelations writes undefined into system prompt — missing source/relationship/target properties caused undefined → works at → Google in prompts. Now filters incomplete relations.
  3. buildMemoryContext has no timeout — hanging mem0 blocked all AI responses. Now wrapped with Promise.race and a 5s timeout.
  4. Fire-and-forget extractAndStoreMemories calls markUnavailable — background failure for one user disabled memory for all. Now only logs errors, no system-wide side effects.
  5. checkMem0Health blocks startup with no timeout — hanging health check prevented bot from starting. Now wrapped with Promise.race and 10s timeout.

Design Issues Fixed

  1. isMemoryAvailable() was a getter with write side effects — renamed auto-recovery version to checkAndRecoverMemory(), kept isMemoryAvailable() as a pure side-effect-free getter.
  2. /memory forget topic hardcoded limit of 10 — now loops with batch size 100, up to 10 iterations (1000 memories max), deleting all matches.
  3. No size budget for memory context — added 2000-char limit with truncation to prevent bloated system prompts.
  4. Username baked into memory content — now passes username as metadata only, keeping memory content clean.

Testing

  • All 734 tests pass ✅
  • Coverage: 93.45% statements, 83.98% branches, 86.86% functions, 94.03% lines
  • Lint clean (Biome) ✅
  • 12 new tests added for the fixes

Ref: PR #59 review comment

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 16, 2026

Warning

Rate limit exceeded

@BillChirico has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 17 minutes and 40 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between afbc7c74024a898fb85f2c4085da44615c9d9413 and ab04cc6.

📒 Files selected for processing (8)
  • src/commands/memory.js
  • src/index.js
  • src/modules/ai.js
  • src/modules/memory.js
  • tests/commands/memory.test.js
  • tests/index.test.js
  • tests/modules/ai.test.js
  • tests/modules/memory.test.js
📝 Walkthrough

Walkthrough

Implemented resilience mechanisms for memory operations by introducing timeout protections, recovery workflows, and availability state management. Added auto-recovery logic with cooldown periods, batch deletion for memory cleanup, and safety checks during startup and AI response generation.

Changes

Cohort / File(s) Summary
Memory Command Processing
src/commands/memory.js
Replaced memory availability check with checkAndRecoverMemory() in command entry points. Reworked /memory forget <topic> to perform batched deletion (100 items per batch, max 10 iterations) with aggregated result reporting.
Memory Module Core
src/modules/memory.js
Introduced markUnavailable() and checkAndRecoverMemory() functions for managing memory state with cooldown-based auto-recovery. Made isMemoryAvailable() pure. Updated checkMem0Health() to accept optional AbortSignal. Added character budget enforcement (2000 chars) in context building. Exposed test hooks: _setMem0Available, _getRecoveryCooldownMs, _isTransientError, _setClient.
Startup and Health Checks
src/index.js
Wrapped mem0 health check during startup with 10-second timeout via AbortController. On timeout or failure, calls markUnavailable() and logs warning; startup continues without memory features.
AI Response Generation
src/modules/ai.js
Added 5-second timeout race condition for memory context retrieval using Promise.race(). Timeout rejection treated as failed lookup; response generation continues without memory context.
Command Tests
tests/commands/memory.test.js
Updated to mock and use checkAndRecoverMemory() instead of isMemoryAvailable(). Added coverage for batch deletion behavior, edge cases with falsy IDs, and adjusted batch size assertions to 100.
Startup Tests
tests/index.test.js
Added memory health check mocking infrastructure and checkMem0HealthImpl parameter to test harness. Added tests verifying markUnavailable() is called on health check rejection and startup continues successfully.
Memory Module Tests
tests/modules/memory.test.js
Expanded coverage for checkAndRecoverMemory() auto-recovery flows and cooldown-based recovery. Added tests for markUnavailable() behavior, health-check abort signal handling, context truncation at 2000 characters, and falsy-but-valid ID preservation across operations.
AI Tests
tests/modules/ai.test.js
Added test case verifying AI response generation falls back gracefully when memory context lookup times out within 5-second window.
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ⚠️ Unable to check for merge conflicts: Failed to fetch base branch: From https://github.com/BillChirico/bills-bot
! [rejected] main -> main (non-fast-forward)
+ 3b21951...9237ff3 main -> origin/main (forced update)
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main objective: fixing mem0 issues identified in a deep review of a prior PR.
Description check ✅ Passed The description is well-structured, detailed, and directly related to the changeset, providing context, bug fixes, design improvements, and test coverage information.
Docstring Coverage ✅ Passed Docstring coverage is 94.74% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/mem0-review-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link

claude bot commented Feb 16, 2026

Claude finished @BillChirico's task in 1m 19s —— View job


Review Complete

  • Read AGENTS.md for project conventions
  • Analyze PR changes and context
  • Review code quality and patterns
  • Check for security issues
  • Verify testing coverage
  • Submit review verdict

All 9 fixes from the deep review are correctly implemented:

  1. Falsy ID filtering - Uses explicit checks (m.id !== undefined && m.id !== null && m.id !== '') and nullish coalescing (m.id ?? '')
  2. formatRelations undefined - Filters incomplete relations with .filter((r) => r.source && r.relationship && r.target)
  3. buildMemoryContext timeout - Uses Promise.race with 5s timeout
  4. Fire-and-forget extractAndStoreMemories - Only logs errors, doesn't call markUnavailable()
  5. checkMem0Health timeout - Uses Promise.race with 10s timeout and AbortController to prevent late resolution race
  6. isMemoryAvailable pure - Renamed recovery version to checkAndRecoverMemory()
  7. Forget topic batching - Loops with batch size 100, max 10 iterations (1000 memories max)
  8. Memory context size budget - 2000-char limit with truncation
  9. Username in metadata - Passes username as metadata only

Previous review issues addressed:

  • Cursor bot's falsy ID coercion issue: Fixed with m.id ?? '' at lines 365/399
  • Cursor bot's health check timeout issue: Fixed with markUnavailable() and AbortSignal

Approved

claude[bot]
claude bot previously approved these changes Feb 16, 2026
Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - Clean implementation of all 9 fixes from the deep review.

@AnExiledDev
Copy link
Contributor

Deep Review: PR #63

Stats: 7 files, +339/-60, 12 new tests (123 total pass ✅)

Verdict

8 of 9 fixes are complete and correct. One fix (#1: falsy ID filtering) is partially undermined by a pre-existing coercion in searchMemories that wasn't updated — exactly what Cursor bot flagged.


Fix Verification

Fix Original Issue Verdict
1. Falsy ID filter #1 — empty IDs dropped ⚠️ Partial — see below
2. formatRelations undefined #2 — undefined in prompt ✅ Complete
3. buildMemoryContext timeout #3 — hanging API blocks responses ✅ Complete
4. Fire-and-forget markUnavailable #4 — background failure disables system ✅ Complete
5. checkMem0Health timeout #5 — blocks startup ✅ Complete
6. isMemoryAvailable side effects #8 — getter with write effects ✅ Complete
7. Forget topic limit 10 #9 — only deletes first 10 ✅ Complete
8. Memory context size budget #11 — no size limit ✅ Complete
9. Username in content #10 — baked into memory text ✅ Complete

⚠️ Fix 1 gap: || '' coercion still undermines falsy ID fix

Cursor bot was right. The fix in handleForgetTopic correctly uses explicit checks (m.id !== undefined && m.id !== null && m.id !== ''), but searchMemories (line 357) and getMemories (line 391) still use m.id || '', which coerces falsy-but-valid IDs (like numeric 0) to empty string before the consumer sees them:

SDK returns:         { id: 0, memory: "..." }
searchMemories maps: { id: '', memory: "..." }   ← || '' coerces 0 to ''
handleForgetTopic:   m.id !== '' → filtered OUT   ← fix doesn't help

The test at commands/memory.test.js:567 mocks searchMemories directly with { id: 0 }, bypassing the real implementation — so it passes but doesn't catch the production path.

Fix: m.id || ''m.id ?? '' at lines 357 and 391 of memory.js.

Test Quality

12 new tests are well-written. Notable strengths:

  • isMemoryAvailable purity test correctly verifies no auto-recovery side effect
  • Timeout test uses fake timers properly with advanceTimersByTimeAsync
  • Batch deletion loop verifies multi-batch with 100+1 pattern
  • Fire-and-forget safety test asserts system stays available after extraction failure

One gap: the falsy ID test mocks at the boundary and doesn't exercise the real searchMemories coercion.

Recommendation

Approve with one change request: m.id || ''m.id ?? '' at memory.js lines 357 and 391.

Everything else is clean, focused, and well-tested.

— Generated by Claude Code

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 16, 2026
claude[bot]
claude bot previously approved these changes Feb 16, 2026
Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - All 9 fixes from the deep review are correctly implemented and well-tested. The falsy ID coercion issue (raised by Cursor bot) has been addressed using nullish coalescing (m.id ?? '') at lines 357 and 391 of memory.js.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 16, 2026
claude[bot]
claude bot previously approved these changes Feb 16, 2026
Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - All 9 fixes correctly implemented. Health check timeout now properly calls markUnavailable() for auto-recovery. Falsy ID coercion fixed with nullish coalescing. Tests are comprehensive.

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

- Add src/modules/memory.js — mem0 REST API client wrapper with
  addMemory, searchMemories, getMemories, deleteMemory, deleteAllMemories,
  buildMemoryContext, and extractAndStoreMemories
- Add /memory command (view, forget, forget <topic>) for user data control
- Integrate memory into AI pipeline: pre-response context injection and
  post-response memory extraction (fire-and-forget)
- Add mem0 health check on startup with graceful fallback (AI works
  without mem0)
- Memory scoping: user_id=Discord ID, app_id=bills-bot
- Config: memory.{enabled, maxContextMemories, autoExtract, extractModel}
- 55 new tests (44 memory module + 11 command), all 668 tests passing
- Update .env.example with MEM0_API_URL

Closes #24
- Replace raw HTTP fetch calls with mem0ai SDK (v2.2.2)
- Switch from self-hosted mem0 REST API to hosted platform (api.mem0.ai)
- Enable graph memory (enable_graph: true) on all add/search/getAll calls
- Add graph relation context to buildMemoryContext() for richer AI prompts
- Update searchMemories() to return { memories, relations } with graph data
- Add formatRelations() helper for readable relation context
- Replace MEM0_API_URL env var with MEM0_API_KEY for hosted platform auth
- Update health check to verify API key config and SDK client initialization
- Add _setClient() test helper for SDK mock injection
- Rewrite all tests to mock SDK instead of global fetch
- Update /memory forget command to destructure new searchMemories format

Closes #24
…s in memory command

- Use splitMessage() utility instead of manual substring truncation for
  Discord's 2000-char limit, properly handling multi-byte characters (#1)
- Include memory IDs in searchMemories results and use them directly in
  handleForgetTopic instead of fragile text equality matching (#2)
- Parallelize deleteMemory calls with Promise.allSettled instead of
  sequential for loop (#3)
- Verify deferReply is called in all forget test variants (#7)
Replace permanent mem0Available = false on API errors with a cooldown-
based recovery mechanism. After RECOVERY_COOLDOWN_MS (60s), the next
request is allowed through to check if the service recovered. If it
fails again, the cooldown resets.

This prevents a single transient network error from permanently
disabling the memory system for all users until restart.

Resolves review threads #4, #8, #12.
…reation

The health check now performs a lightweight search against the mem0
platform to verify the SDK client actually works, rather than just
checking that the client object was created. This properly validates
connectivity with the hosted mem0 platform.

Also fixes misleading test name that asserted success but was named
as a failure case.

Resolves review threads #5, #11.
Add privacy documentation to the memory module explaining that user
messages are sent to the mem0 hosted platform for processing. Update
the /memory command description to indicate external storage. Users
can view and delete their data via /memory view and /memory forget.

Resolves review thread #6.
…c API

- Remove extractModel from getMemoryConfig() and config.json since it
  was never consumed by any function (#10)
- Document addMemory as part of the public API, exported for direct use
  by other modules/plugins that need to store memories (#9)
- Add /memory optout subcommand to toggle memory collection per user
  - In-memory Set with JSON file persistence (data/optout.json)
  - extractAndStoreMemories and buildMemoryContext skip opted-out users
  - Toggle behavior: running again opts back in

- Add confirmation prompt on /memory forget (all)
  - Discord ButtonBuilder with Confirm (Danger) and Cancel buttons
  - 30-second timeout with auto-cancel
  - Only the command user can interact with buttons

- Add /memory admin view and /memory admin clear subcommands
  - Requires ManageGuild or Administrator permission
  - Admin view shows target user's memories with opt-out status
  - Admin clear includes confirmation prompt before deletion

- Comprehensive test coverage for all new features
  - 17 new optout module tests
  - 32 total memory command tests (was 12)
  - 54 total memory module tests (was 52)
  - All metrics above 80% threshold
- Add memory_optouts table (user_id TEXT PK, created_at TIMESTAMPTZ) to db.js schema
- Rewrite optout.js: remove all filesystem code, use DB pool for persistence
- Keep in-memory Set for fast lookups (isOptedOut called on every message)
- Best-effort DB persistence: log warning on failure, keep in-memory state
- loadOptOuts now async, loads from DB on startup with graceful fallback
- toggleOptOut now async, writes INSERT/DELETE to DB
- Export _setPool for test injection, keep _resetOptouts for testing
- Update memory.js to await async toggleOptOut
- Rewrite tests to mock DB pool instead of filesystem
- All 714 tests passing, lint clean
Opt-out preferences were never loaded from the database on startup,
causing all users to appear opted-in after a restart. Now loadOptOuts()
is awaited during the startup sequence before the mem0 health check.
Previously the catch fallback in getMemoryConfig() defaulted to
enabled: true, meaning a config failure would silently enable memory
collection. Now defaults to enabled: false and autoExtract: false
so memory is safely disabled if config is unavailable.
deleteAllMemories and deleteMemory were missing markUnavailable()
calls in their catch blocks, inconsistent with all other API methods
(addMemory, searchMemories, getMemories, extractAndStoreMemories).
Now transient failures in delete operations trigger the same
cooldown/recovery mechanism.
handleView and handleAdminView had identical truncation logic for
fitting memory lists within Discord's 2000-char limit. Extracted
into a shared formatMemoryList() helper to reduce duplication.
Test 'should return defaults when getConfig throws' renamed to
'should return safe disabled fallback when getConfig throws' to
match the new behavior where config failure disables memory.
Updated assertions to expect enabled: false and autoExtract: false.
The auto-recovery test had an unawaited .then() chain which meant
assertions inside the callback could silently pass or fail without
affecting the test result. Refactored to use async/await with a
simpler, more reliable test structure.
Not every API error should disable the entire memory system. Added
isTransientError() helper that distinguishes:

- Transient (retryable): network errors (ECONNREFUSED, ETIMEDOUT, etc.),
  5xx server errors, 429 rate limits, timeout/fetch-failed messages
- Permanent (disable system): 4xx client errors (401/403 auth failures),
  unknown errors

Only permanent errors now call markUnavailable(). Transient errors log a
warning and return safe defaults without disabling the system.

Updated all 6 API operation catch blocks: addMemory, searchMemories,
getMemories, deleteAllMemories, deleteMemory, extractAndStoreMemories.

Added 8 tests covering error classification and behavior verification.
Explains why setting false does a hard disable without cooldown (test
helper for instant state toggling) while setting true calls markAvailable()
to clear cooldown. Production code uses markUnavailable() which enables
timed auto-recovery.
Previously, vi.useRealTimers() was called at the end of each test using
fake timers. If a test failed mid-way, the call would be skipped and
fake timers would leak into subsequent tests. Moved to an afterEach
block in the isMemoryAvailable describe block so it always runs.
Replaced fragile test that relied on the auto-created mock client lacking
a search method. Now explicitly creates a mock client with a search method
that throws ECONNREFUSED, clearly testing the scenario where a client is
created but cannot reach the mem0 platform. Also asserts that search was
actually called.
…ForgetTopic

Empty string is falsy in JS, so `m.id` filter was silently dropping
memories with falsy-but-valid IDs (e.g. numeric 0). Now uses explicit
check: m.id !== undefined && m.id !== null && m.id !== ''
Relations with undefined or null properties were being rendered as
'undefined → works at → Google' in the system prompt. Now filters
out incomplete relations before formatting.
A hanging mem0 call would block all AI responses indefinitely.
Now uses Promise.race with a 5-second timeout so the bot continues
responding even if memory lookup hangs.
…emories

Background memory extraction failures were disabling the memory system
for all users. Now only logs the error — background failures should not
have system-wide side effects. Also passes username as metadata instead
of only baking it into the content string.
A hanging mem0 health check would block bot startup indefinitely.
Now uses Promise.race with a 10-second timeout — if it times out,
logs a warning and continues without memory features.
…emory

isMemoryAvailable() was a getter with write side effects (auto-recovery
called markAvailable). Now:
- isMemoryAvailable() — pure, no side effects, just reads state
- checkAndRecoverMemory() — checks availability with cooldown-based
  auto-recovery (the side-effectful version)

All internal memory operations and command handlers use
checkAndRecoverMemory() for the recovery behavior.
Was hardcoded to search limit of 10, silently leaving remaining
memories. Now searches in batches of 100 and loops until no more
results, with a safety cap of 10 iterations (1000 memories max).
Memory context had no size limit — with many memories and relations,
it could bloat the system prompt significantly. Now truncates with
'...' if the context exceeds 2000 characters.
Username was being prepended to the user message content as
'username: message', polluting stored memories with the username
string. Now passes username only as metadata, keeping memory
content clean and username available for querying via metadata.
Change m.id || '' to m.id ?? '' in searchMemories and getMemories
to preserve falsy-but-valid IDs like 0. The || operator coerces
0 to empty string before consumers see it; ?? only coerces
null/undefined.

Add tests verifying falsy ID preservation in both functions.
…covery

When checkMem0Health times out during startup, the catch block now calls
markUnavailable() to set mem0UnavailableSince to Date.now(). This enables
checkAndRecoverMemory() to auto-recover after the cooldown period expires.

Previously, the timeout left mem0Available=false with mem0UnavailableSince=0,
which permanently disabled memory features since the recovery condition
requires mem0UnavailableSince > 0.

Changes:
- Export markUnavailable() from src/modules/memory.js
- Import and call markUnavailable() in index.js catch block
- Add tests verifying markUnavailable is called on health check failure
- Add tests verifying auto-recovery works after markUnavailable

Resolves PR #63 review thread PRRT_kwDORICdSM5uwjHN
…ortSignal

The Promise.race timeout in startup doesn't cancel the underlying
checkMem0Health() call. If the health check takes longer than 10s but
eventually succeeds, it calls markAvailable(), silently overriding the
markUnavailable() set by the timeout catch block.

Fix: pass an AbortController signal to checkMem0Health(). When the
timeout fires, it aborts the signal. checkMem0Health checks
signal.aborted before calling markAvailable(), preventing the late
success from re-enabling memory features after startup logged them
as disabled.

Adds test verifying checkMem0Health returns false and does not mark
available when signal is already aborted.
@BillChirico BillChirico force-pushed the fix/mem0-review-fixes branch from 0f763f5 to ab04cc6 Compare February 16, 2026 16:19
@BillChirico BillChirico merged commit 439104a into main Feb 16, 2026
3 of 4 checks passed
@BillChirico BillChirico deleted the fix/mem0-review-fixes branch February 16, 2026 16:20
Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.


const matchesWithIds = matches.filter(
(m) => m.id !== undefined && m.id !== null && m.id !== '',
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filter null/undefined checks are dead code after upstream coercion

Low Severity

The handleForgetTopic filter checks m.id !== undefined && m.id !== null but these conditions can never trigger because searchMemories already coerces null/undefined IDs to '' via m.id ?? ''. The effective filter is just m.id !== ''. This makes the fix for falsy ID handling (fix #1) appear more robust than it actually is — the null/undefined guards are dead code. Either searchMemories should stop coercing IDs (use m.id directly) to let the downstream filter handle all cases, or the filter should be simplified to match what searchMemories actually produces.

Additional Locations (1)

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants