Skip to content

feat: add user memory feature for persisting preferences across conversations #146

Merged
bbsngg merged 5 commits intoOpenLAIR:mainfrom
HenryPengZou:feature/user-memory
Apr 8, 2026
Merged

feat: add user memory feature for persisting preferences across conversations #146
bbsngg merged 5 commits intoOpenLAIR:mainfrom
HenryPengZou:feature/user-memory

Conversation

@HenryPengZou
Copy link
Copy Markdown
Contributor

@HenryPengZou HenryPengZou commented Apr 8, 2026

Summary

  • Add user memory system similar to ChatGPT/Claude/Gemini memory
  • Users can save preferences/context via Settings > Memory tab (add/edit/delete/toggle)
  • Memories injected into system prompts across all providers (Claude SDK, OpenRouter, LocalGPU,
    Gemini)
  • Memories synced to ~/.claude/MEMORY.md for terminal-only (Option B) users [update: now synced to ~/.claude/MEMORY-{userId}.md (user-scoped) to avoid multi-user file conflict, e.g., ~/.claude/MEMORY-1.md]
  • Full CRUD API at /api/memory with global enable/disable toggle

Test plan

  • Start server, go to Settings > Memory, add/edit/delete/toggle memories
  • Start a chat session, verify AI acknowledges saved memories
  • Check ~/.claude/MEMORY-{userId}.md is synced after changes
  • Test across providers (Claude, OpenRouter, LocalGPU)

HenryPengZou and others added 2 commits April 7, 2026 20:13
…rsations

Add a memory system similar to ChatGPT/Claude/Gemini memory. Users can
save preferences, habits, and context via Settings > Memory tab. Enabled
memories are injected into system prompts across all providers (Claude SDK,
OpenRouter, LocalGPU, Gemini). Includes full CRUD API, global toggle, and
per-memory enable/disable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When memories are created/updated/deleted/toggled via the UI, they are
now synced to:
- ~/.claude/MEMORY.md — standalone file readable by terminal users
- ~/.claude/CLAUDE.md — managed section so Claude Code CLI loads
  memories automatically for all projects

Also injects memories into dr-claw chat (cli-chat.js) system prompt
by reading ~/.claude/MEMORY.md at startup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Zhang-Henry
Copy link
Copy Markdown
Collaborator

PR Review — @HenryPengZou

Overall clean feature with well-structured CRUD. A few issues from security to correctness need attention before merge.


🔴 CRITICAL — Must Fix

1. Global memory_enabled setting is per-server, not per-user
server/routes/memory.js:89-105appSettingsDb.get('memory_enabled') is a single global key. If User A disables memory, it's disabled for all users. This should be a per-user setting (e.g. column in user_memories or a separate user-settings table).

2. syncMemoryFiles writes to shared ~/.claude/MEMORY.md — multi-user conflict
server/utils/memoryPrompt.js:68-79 — On a shared server, the last user to save overwrites ~/.claude/MEMORY.md and ~/.claude/CLAUDE.md with their memories. The file path is not user-scoped. Either namespace the file (e.g. MEMORY-{userId}.md), or drop file sync and rely solely on system prompt injection. If this is strictly single-user, document that assumption explicitly.


🟠 HIGH — Strongly Recommend Fixing

3. req.params.id not validated as integer
server/routes/memory.js — PUT :id, PATCH :id/toggle, DELETE :id pass req.params.id directly to DB methods without parsing. Add parseInt() + isNaN guard to reject non-numeric IDs.

4. Memory content injected without sanitization into system prompts
server/utils/memoryPrompt.js:31 — User-supplied m.content is interpolated directly into the system prompt. A malicious memory like "Ignore all prior instructions..." is a prompt injection vector. Consider:

  • Limiting memory content length (e.g. 500 chars per entry)
  • Capping total memory count per user (e.g. 50)
  • Stripping markdown headings (#) that could confuse prompt structure

5. No content length validation on create/update
server/routes/memory.js:28,43 — Only checks !content.trim() but no max length. Users could store arbitrarily large memories, bloating every system prompt.

6. Delete has no confirmation
src/components/settings/MemoryContent.jsx:136handleDelete fires immediately on click. Add window.confirm() or a two-step delete to prevent accidental data loss.


🟡 MEDIUM — Suggested Improvements


✅ Looks Good

  • DB migration is idempotent (CREATE TABLE IF NOT EXISTS)
  • CRUD routes follow existing codebase patterns
  • authenticateToken middleware correctly protects all endpoints
  • syncMemoryFiles uses start/end markers to avoid clobbering existing CLAUDE.md
  • Frontend component is clean with proper state management
  • userId correctly threaded through all 4 providers in index.js

Verdict: Do not merge as-is. Fix #1 and #2 (critical design issues that will cause bugs with multiple users). #3#6 are straightforward hardening. The rest are quality improvements.

@bbsngg
Copy link
Copy Markdown
Contributor

bbsngg commented Apr 8, 2026

@HenryPengZou

HenryPengZou and others added 2 commits April 7, 2026 23:48
…r fixes

Fixes for PR OpenLAIR#146 review by @Zhang-Henry:

Critical:
- OpenLAIR#1: memory_enabled is now per-user (column on users table) instead of
  global app_settings. Each user controls their own memory toggle.
- OpenLAIR#2: ~/.claude/MEMORY.md namespaced as MEMORY-{userId}.md to prevent
  multi-user overwrites on shared servers.

High:
- OpenLAIR#3: req.params.id validated as positive integer with parseInt + isNaN guard
- OpenLAIR#4: Memory content sanitized (strip markdown headings) before prompt injection;
  content length capped at 500 chars; max 50 memories per user
- OpenLAIR#5: Content length validation (400 error) on create and update routes
- OpenLAIR#6: Delete now requires window.confirm() before executing

Medium:
- OpenLAIR#8: Removed no-op try/catch wrappers from all memoryDb methods
- OpenLAIR#9: Added comment explaining why Gemini injects memory into user prompt
  (CLI has no system instruction API)
- OpenLAIR#10: Changed index from (is_enabled) to composite (user_id, is_enabled)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ser conflict

The CLAUDE.md managed section was the last shared resource that could
cause multi-user overwrites. Removed entirely because:
- Option A (web UI) injects memories via buildMemoryBlock() into system
  prompts directly from the DB — never reads any file
- The CLAUDE.md section was only needed for Option B (terminal CLI),
  which is not the primary use case

What remains:
- ~/.claude/MEMORY-{userId}.md — user-scoped reference file (read-only)
- DB → system prompt injection for all web UI sessions (the main path)

Also removed dead MEMORY.md reading from cli-chat.js since it lacks
user auth context to resolve the user-scoped filename.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@HenryPengZou
Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review @Zhang-Henry! All critical and high-priority issues are addressed
in commits 2129a6c and 4f940e7.

🔴 CRITICAL

1 — memory_enabled is now per-user, not global

  • Added memory_enabled column to the users table (with migration for existing DBs)
  • Replaced appSettingsDb.get('memory_enabled') with memoryDb.getMemoryEnabled(userId) /
    setMemoryEnabled(userId, bool)
  • Each user now independently controls their own memory toggle

2 — Multi-user file conflict fully resolved

  • ~/.claude/MEMORY.md → renamed to ~/.claude/MEMORY-{userId}.md (user-scoped)
  • Removed ~/.claude/CLAUDE.md managed section entirely — this was the last shared resource
    that could cause overwrites. It's no longer needed because Option A (web UI) injects memories via
    buildMemoryBlock() directly from the DB into system prompts and never reads any file.

🟠 HIGH

3 — req.params.id validated as integer

  • Added parseId() helper with parseInt() + isNaN + positive check
  • All :id routes (PUT, PATCH, DELETE) return 400 for non-numeric IDs

4 — Memory content sanitized against prompt injection

  • Content is sanitized (markdown headings # stripped) before system prompt injection via
    sanitizeContent()
  • Max 500 characters per memory (enforced in DB layer + API validation)
  • Max 50 memories per user (enforced in memoryDb.create())

5 — Content length validation on create/update

  • API returns 400 error if content exceeds 500 characters
  • Frontend textareas have maxLength={500}

6 — Delete confirmation added

  • handleDelete now calls window.confirm() before executing

🟡 MEDIUM

8 — Removed no-op try/catch wrappers

  • All memoryDb methods now throw directly instead of try { ... } catch (err) { throw err; }

9 — Gemini injection documented

  • Added comment explaining why Gemini injects memory into user prompt (CLI has no system
    instruction API, only --prompt flag)

10 — Index fixed

  • Changed idx_user_memories_enabled(is_enabled) → composite
    idx_user_memories_user_enabled(user_id, is_enabled)

Not addressed (low priority)

  • 7 (i18n) — Deferred; would require adding translation keys across the codebase
  • 11 (debounce syncMemoryFiles) — Low impact for typical usage patterns; can add if needed

Both branches added imports and route mounts at the same location in
server/index.js. Kept both: memoryRoutes and communityToolsRoutes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bbsngg bbsngg merged commit 7f33bd1 into OpenLAIR:main Apr 8, 2026
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.

3 participants