mem0 Plugin Ghost Writes: memory_add Success with Empty Database
The openclaw-mem0 plugin reports successful memory writes but SQLite database remains empty due to duplicate plugin registration or uncommitted transactions.
π Symptoms
Primary Manifestation
The memory_add tool executes without throwing exceptions and returns a success message, yet no data persists to the SQLite database.
# User observes successful return from memory_add
$ openclaw run "Remember my favorite color is blue"
β Memory stored successfully (v1.2.3)
# But database remains empty
$ sqlite3 ~/.mem0/vector_store.db 'SELECT count(*) FROM vectors;'
0
Diagnostic CLI Commands
bash
Check database file existence and size
$ ls -la ~/.mem0/vector_store.db -rw-rw-r– 1 user user 0 Jan 15 10:30:00 ~/.mem0/vector_store.db
Verify database schema integrity
$ sqlite3 ~/.mem0/vector_store.db ‘SELECT name FROM sqlite_master WHERE type=“table”;’ (empty result - no tables exist)
Cross-check the active memory store
$ openclaw debug –show-active-stores [ERROR] No stores configured. Using in-memory fallback.
Secondary Indicators
- Repeated `memory_add` calls produce identical `count(*)` results (always 0)
- No exception or error message during write operations
- Migration scripts report transfer completion without actual data movement
- Plugin logs show "Stored in memory" without "Committed to disk" confirmation
π§ Root Cause
Architectural Analysis
The ghost write behavior stems from a state synchronization failure between the plugin’s in-memory representation and the persistent storage layer. The issue manifests through two distinct failure modes:
Failure Mode 1: Duplicate Registration Desynchronization
When register() is invoked multiple times (a known issue related to #77822), the plugin creates multiple closure-scoped instances with divergent state:
// Simplified representation of the bug
let memoryInstance = null; // Module-level singleton
function register(config) {
// First call: memoryInstance = new Mem0Manager()
// Second call: memoryInstance = new Mem0Manager() // NEW instance
// Writes go to SECOND instance (orphaned on garbage collection)
memoryInstance = new Mem0Manager(config);
}
function memory_add(content) {
// This instance never gets committed to disk
return memoryInstance.add(content);
}
The first register() call initializes an instance bound to ~/.mem0/, but a subsequent register() call creates a second instance. All subsequent writes target the second instance, which may operate on a different database path or hold an uncommitted transaction buffer.
Failure Mode 2: Transaction Isolation Without Commit
The mem0 SQLite backend may execute writes within an uncommitted transaction context:
// Transaction opened but never committed
await db.transaction(async (tx) => {
await tx.insert('vectors', { content, embedding });
// Missing: await tx.commit();
});
// Data exists in transaction log but not in main database file
Root Cause Vectors
| Vector | Description | Impact |
|---|---|---|
Duplicate register() calls | Multiple plugin registration events during initialization | Writes target orphaned instance |
| Transaction scope leak | Write operations wrapped in uncommitted transactions | Data invisible until rollback |
| Path resolution divergence | ~/.mem0/ vs /tmp/mem0/ vs absolute path | Silent writes to wrong location |
| Process lifecycle mismatch | In-memory buffer flushed to different DB connection | Ghost data in disconnected context |
Environment-Specific Triggers
- Docker: Volume mount conflicts cause separate processes to write to different database files
- macOS: `~/Library/Application Support/mem0/` vs `~/.mem0/` path discrepancy
- Hot reload: Development servers re-register plugins without resetting state
π οΈ Step-by-Step Fix
Phase 1: Isolate the Plugin Instance
Before proceeding, ensure only one instance of the mem0 plugin is active:
bash
Kill all existing openclaw processes
$ pkill -f “openclaw” || true
Verify clean slate
$ ps aux | grep openclaw (empty output)
Phase 2: Clear Corrupted State
bash
Remove potentially corrupted database and instance directories
$ rm -rf ~/.mem0/ $ rm -rf /tmp/mem0/ $ rm -rf ~/.cache/openclaw/plugins/mem0/
Clear any plugin cache
$ openclaw plugin uninstall @mem0/openclaw-mem0 β Plugin uninstalled
$ openclaw plugin install @mem0/openclaw-mem0 β Plugin installed (v1.2.3)
Phase 3: Verify Single Registration
Inspect your configuration file to ensure single plugin registration:
yaml
openclaw.config.yaml (BEFORE - problematic)
plugins:
- name: mem0 package: @mem0/openclaw-mem0 config: api_key: ${MEM0_API_KEY}
Remove duplicate entries below
- name: mem0-again package: @mem0/openclaw-mem0 config: api_key: ${MEM0_API_KEY}
yaml
openclaw.config.yaml (AFTER - corrected)
plugins:
- name: mem0 package: @mem0/openclaw-mem0 config: api_key: ${MEM0_API_KEY} db_path: ~/.mem0/vector_store.db # Explicit path
Phase 4: Programmatic Fix for Plugin Code
If modifying the plugin directly, add instance deduplication:
javascript // In your plugin’s index.js let registeredInstance = null; let registrationCount = 0;
export function register(config) { registrationCount++;
if (registrationCount > 1) {
console.warn(`[mem0] Duplicate registration detected (#${registrationCount}). Reusing existing instance.`);
return registeredInstance;
}
registeredInstance = new Mem0Plugin(config);
return registeredInstance;
}
Phase 5: Force Synchronous Commit (Temporary Workaround)
If the issue persists, force synchronous writes:
bash
Set environment variable to enable synchronous mode
$ export MEM0_SYNC_WRITE=1 $ export MEM0_COMMIT_INTERVAL_MS=0
Verify environment is set
$ env | grep MEM0 MEM0_SYNC_WRITE=1 MEM0_COMMIT_INTERVAL_MS=0
Phase 6: Manual Database Verification
After implementing fixes, manually verify database state:
bash
Create database directory with proper permissions
$ mkdir -p ~/.mem0 $ chmod 755 ~/.mem0
Verify database creation
$ openclaw run “Test memory” $ sqlite3 ~/.mem0/vector_store.db ‘SELECT count(*) FROM vectors;’
Should now return 1 or more
π§ͺ Verification
Verification Test Suite
Execute the following sequence to confirm the fix:
bash
Step 1: Fresh process initialization
$ openclaw reset –force β State cleared
Step 2: Verify database initialization
$ sqlite3 ~/.mem0/vector_store.db ‘SELECT name FROM sqlite_master WHERE type=“table”;’ vectors embeddings
Step 3: Add test memory
$ openclaw run “Remember that my birthday is March 15th” β Memory stored successfully
Step 4: Immediate database verification
$ sqlite3 ~/.mem0/vector_store.db ‘SELECT content FROM vectors LIMIT 1;’ March 15th
Step 5: Cross-instance verification
$ openclaw run “Recall my birthday” Birthday is March 15th
Step 6: Persistent verification (new process)
$ openclaw run “What did I ask you to remember?” Birthday is March 15th
Expected Outputs
| Command | Expected Result |
|---|---|
sqlite3 ~/.mem0/vector_store.db 'SELECT count(*)' | Integer β₯ 1 |
sqlite3 ~/.mem0/vector_store.db '.tables' | vectors embeddings |
openclaw run "recall" | Retrieves previously stored content |
openclaw restart && openclaw run "recall" | Same result after process restart |
Regression Detection Script
bash #!/bin/bash
test-mem0-persistence.sh
DB_PATH="$HOME/.mem0/vector_store.db" TEST_CONTENT=“mem0-persistence-test-$(date +%s)”
Clean start
rm -f “$DB_PATH”
Add memory
openclaw run “Remember: $TEST_CONTENT”
Check database
COUNT=$(sqlite3 “$DB_PATH” “SELECT count(*) FROM vectors WHERE content LIKE ‘%$TEST_CONTENT%’;”)
if [ “$COUNT” -ge 1 ]; then echo “β PASS: Memory persisted correctly” exit 0 else echo “β FAIL: Memory not found in database” exit 1 fi
β οΈ Common Pitfalls
Pitfall 1: Hidden Duplicate Registrations
Many users include the plugin in both ~/.openclawrc and project-level .openclaw/config.yaml, causing double registration.
Solution: Audit all configuration files: bash $ find ~ -name “.openclaw” -o -name “openclaw*.yaml” 2>/dev/null | xargs grep -l mem0 ~/.openclawrc /projects/myapp/.openclaw/config.yaml
Pitfall 2: Path Tilde Expansion in Different Contexts
The shell expands ~ but the plugin may use os.homedir() or receive raw string without expansion.
javascript // Plugin receives raw path without expansion config.db_path = “~/.mem0/db.sqlite” // WRONG - creates directory named ~
// Correct approach config.db_path = path.join(os.homedir(), ‘.mem0’, ‘db.sqlite’);
Solution: Always use absolute paths in configuration: yaml db_path: /home/username/.mem0/vector_store.db
Pitfall 3: Docker Volume Mount Timing
In containerized environments, the database file may be created inside the container before volume mount, causing writes to an ephemeral layer.
Solution: Pre-create database directory with correct permissions: dockerfile RUN mkdir -p /root/.mem0 && chmod 777 /root/.mem0 VOLUME /root/.mem0
Pitfall 4: Concurrent Write Access
Multiple openclaw processes writing simultaneously can lock the SQLite database.
Solution: Implement write queue or use WAL mode: bash $ sqlite3 ~/.mem0/vector_store.db “PRAGMA journal_mode=WAL;”
Pitfall 5: macOS Gatekeeper Path Isolation
On macOS, sandboxed applications may use ~/Library/Application Support/ instead of ~/.mem0/.
Solution: Set explicit path: bash export MEM0_DB_PATH="$HOME/Library/Application Support/mem0/vector_store.db"
π Related Errors
| Error Code | Description | Connection |
|---|---|---|
| #77822 | Duplicate register() calls causing closure variable desynchronization | Primary root cause - same underlying mechanism |
| E2BIG | Memory store buffer overflow during bulk migration | Related - transaction scope issues |
| SQLITE_BUSY | Database locked during concurrent writes | Symptom of multi-instance writes |
| ENOTDIR | Invalid database path resolution | Contributes to ghost writes to wrong location |
| ECONNREFUSED | Plugin fails to connect to vector store | Downstream failure when DB never initialized |
Additional Related Issues
- #78103 - "mem0 memory_search returns stale results after plugin reload"
- #77456 - "Plugin singleton pattern broken by hot module replacement"
- #76912 - "SQLite WAL mode causing read-your-own-writes inconsistency"