214 lines
6.0 KiB
Markdown
214 lines
6.0 KiB
Markdown
# Migration Guide: OpenClaw sqlite-vec → clawhdf5
|
|
|
|
This guide walks through migrating an OpenClaw agent from its default
|
|
sqlite-vec + Markdown file memory to the `clawhdf5` HDF5 backend.
|
|
|
|
---
|
|
|
|
## Why migrate?
|
|
|
|
| Feature | sqlite-vec + Markdown | clawhdf5 |
|
|
|---------|----------------------|----------|
|
|
| Storage format | SQLite WAL + flat .md files | Single HDF5 binary file |
|
|
| Vector search | sqlite-vec (SQLite extension) | Pure-Rust SIMD (clawhdf5-accel) |
|
|
| Full-text search | External (FTS5 or plain string match) | Built-in BM25 |
|
|
| Hybrid search | Manual combination | Automatic RRF blend |
|
|
| Memory tiers | Flat | Working → Episodic → Semantic |
|
|
| Hebbian decay | Not built-in | Automatic activation weighting |
|
|
| Portability | SQLite binary required | Zero native deps (all Rust) |
|
|
| Crash recovery | SQLite WAL | clawhdf5 WAL |
|
|
| Compaction | Manual | Auto-threshold + session-end |
|
|
| Embedding dim change | New DB required | New file required (same) |
|
|
|
|
---
|
|
|
|
## Step 1: Install `@redclaw/clawhdf5`
|
|
|
|
```bash
|
|
npm install @redclaw/clawhdf5
|
|
```
|
|
|
|
Or, if building from the monorepo source:
|
|
|
|
```bash
|
|
npm install -g @napi-rs/cli
|
|
cd packages/clawhdf5-node
|
|
npm install
|
|
npm run build
|
|
```
|
|
|
|
---
|
|
|
|
## Step 2: Update your OpenClaw config
|
|
|
|
Change `backend` from `"sqlite-vec"` (or `"markdown"`) to `"clawhdf5"`:
|
|
|
|
```json
|
|
{
|
|
"memory": {
|
|
"backend": "clawhdf5",
|
|
"clawhdf5": {
|
|
"path": "./agent.brain",
|
|
"embeddingDim": 768
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
See [openclaw-config.md](openclaw-config.md) for the full schema.
|
|
|
|
---
|
|
|
|
## Step 3: Run the one-time migration
|
|
|
|
clawhdf5 ships a migration helper that reads your existing Markdown memory
|
|
files and ingests them via `ingestMarkdown()`.
|
|
|
|
### Automated migration script
|
|
|
|
```typescript
|
|
import { ClawhdfMemory } from '@redclaw/clawhdf5';
|
|
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
import { join, relative } from 'path';
|
|
|
|
async function migrate(
|
|
memoryDir: string,
|
|
brainPath: string,
|
|
embeddingDim: number = 768,
|
|
): Promise<void> {
|
|
const mem = ClawhdfMemory.create(brainPath, embeddingDim);
|
|
|
|
// Walk all .md files under memoryDir
|
|
function walk(dir: string): string[] {
|
|
return readdirSync(dir).flatMap((entry) => {
|
|
const full = join(dir, entry);
|
|
return statSync(full).isDirectory() ? walk(full) : [full];
|
|
});
|
|
}
|
|
|
|
const files = walk(memoryDir).filter((f) => f.endsWith('.md'));
|
|
let totalSections = 0;
|
|
|
|
for (const file of files) {
|
|
const content = readFileSync(file, 'utf8');
|
|
const relPath = relative(process.cwd(), file);
|
|
const count = mem.ingestMarkdown(relPath, content);
|
|
console.log(` ${relPath}: ${count} sections`);
|
|
totalSections += count;
|
|
}
|
|
|
|
// Force WAL merge after bulk import
|
|
mem.flushWal();
|
|
|
|
console.log(`\nMigration complete: ${files.length} files, ${totalSections} sections`);
|
|
const s = mem.stats();
|
|
console.log(` Total records: ${s.totalRecords}`);
|
|
console.log(` File size: ${(s.fileSizeBytes / 1024).toFixed(1)} KB`);
|
|
}
|
|
|
|
// Usage
|
|
migrate('./memory', './agent.brain', 768).catch(console.error);
|
|
```
|
|
|
|
### What the migration does
|
|
|
|
1. Walks all `.md` files under your memory directory.
|
|
2. Parses each file into sections using the same `MarkdownParser` used by
|
|
OpenClaw (splits on ATX headings `#`, `##`, `###`, …).
|
|
3. Stores each section as a separate record in the HDF5 file with the file
|
|
path as the `source_channel` (e.g. `memory/user.md::Goals`).
|
|
4. Flushes the WAL to merge everything into the `.brain` file.
|
|
|
|
After migration, **the original `.md` files are not modified or deleted**.
|
|
You can keep them as a backup or remove them once you have verified the
|
|
migrated data.
|
|
|
|
---
|
|
|
|
## Step 4: Verify
|
|
|
|
```typescript
|
|
import { ClawhdfMemory } from '@redclaw/clawhdf5';
|
|
|
|
const mem = ClawhdfMemory.open('./agent.brain');
|
|
const s = mem.stats();
|
|
console.log('Records after migration:', s.totalRecords);
|
|
|
|
// Spot-check: retrieve a known path
|
|
const userMd = mem.get('memory/MEMORY.md');
|
|
console.log(userMd?.slice(0, 200));
|
|
|
|
// Round-trip a file back to Markdown
|
|
const exported = mem.exportMarkdown('memory/MEMORY.md');
|
|
console.log(exported.slice(0, 500));
|
|
```
|
|
|
|
---
|
|
|
|
## Step 5: Update agent code
|
|
|
|
If your agent code reads memory files directly from disk, update it to use
|
|
the clawhdf5 API instead:
|
|
|
|
**Before (sqlite-vec + file reads):**
|
|
```typescript
|
|
const content = readFileSync('memory/user.md', 'utf8');
|
|
const sections = parseMarkdown(content);
|
|
const results = await vectorSearch(query, sections, k);
|
|
```
|
|
|
|
**After (clawhdf5):**
|
|
```typescript
|
|
import { ClawhdfMemory } from '@redclaw/clawhdf5';
|
|
|
|
const mem = ClawhdfMemory.openOrCreate('./agent.brain', 768);
|
|
const embedding = await embed(query); // your embedding function
|
|
const results = mem.search(query, new Float32Array(embedding), k);
|
|
```
|
|
|
|
---
|
|
|
|
## Step 6: Session lifecycle hooks
|
|
|
|
Add compaction at session end for best long-term memory health:
|
|
|
|
```typescript
|
|
// At the start of your agent process
|
|
const mem = ClawhdfMemory.openOrCreate('./agent.brain', 768);
|
|
|
|
// ... agent runs ...
|
|
|
|
// At the end of each session
|
|
mem.tickSession(); // decay activation weights
|
|
const stats = mem.runConsolidation(Date.now() / 1000); // promote memories
|
|
console.log('[memory] consolidation:', stats);
|
|
```
|
|
|
|
---
|
|
|
|
## Rollback
|
|
|
|
If you need to roll back to sqlite-vec:
|
|
|
|
1. Change `memory.backend` back to `"sqlite-vec"` in your config.
|
|
2. The original `.md` files are unchanged (if you kept them).
|
|
3. Delete `agent.brain` (and `agent.brain.wal` if present).
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### `Error: no records found for path: memory/user.md`
|
|
The path passed to `get()` or `exportMarkdown()` must exactly match the
|
|
relative path used during `ingestMarkdown()`. Check for leading `./`
|
|
differences.
|
|
|
|
### Memory is empty after reopening
|
|
Make sure `flushWal()` was called after bulk writes. Without it, entries
|
|
remain in the WAL and may be lost if the process exits abnormally.
|
|
|
|
### Embedding dimension mismatch
|
|
The `embeddingDim` passed to `create()` cannot be changed after the file is
|
|
created. If you switch embedding models, create a new `.brain` file and
|
|
re-run the migration script.
|