# Deployment Guide

This guide is for people who want to run `openclaw-mem` on a real OpenClaw install, not just try the local proof.

## Overview

A normal production setup has five parts:
1. **Auto-capture plugin** — captures tool results automatically
2. **Periodic ingestion** — imports captured observations into SQLite
3. **Log rotation** — keeps JSONL files from growing without bound
4. **AI compression** — optional summarization of daily notes
5. **Monitoring** — health checks and error alerts

## Pick your starting role

In most deployments, `openclaw-mem` starts as a **sidecar**.
That means it adds capture, local recall, and receipts **without** replacing your active OpenClaw memory backend.

Common starting points:

- **Stable baseline**: `memory-core` + `openclaw-mem` sidecar
- **Semantic-first**: `memory-lancedb` + `openclaw-mem` sidecar
- **Controlled migration**: keep both available, switch the active memory backend only after smoke tests

## Two install roles in this repo

This repo ships two different plugin roles:

1. **`openclaw-mem` sidecar plugin**
   - captures and harvests observations
   - does **not** own the OpenClaw memory backend
   - is the default path in this guide

2. **`openclaw-mem-engine` memory engine**
   - optional active memory backend
   - owns memory read/write behavior when selected
   - should be treated as a controlled upgrade, not the default first install

## Read-only behavior

- If you are using only the **sidecar**, read-only behavior is mostly enforced by your prompt/runner setup.
- If you are using **`openclaw-mem-engine`** as the active memory backend, set `readOnly: true` (or `OPENCLAW_MEM_ENGINE_READONLY=1`) to reject write-path operations at runtime.

For deeper ownership and migration notes, see [ecosystem fit](ecosystem-fit.md).

## 1. Plugin Installation

### System-Wide (Recommended)

```bash
# Clone repo to persistent location
sudo mkdir -p /opt/openclaw-mem
sudo git clone https://github.com/phenomenoner/openclaw-mem.git /opt/openclaw-mem
cd /opt/openclaw-mem
sudo uv sync --locked

# Symlink plugin
sudo ln -s /opt/openclaw-mem/extensions/openclaw-mem /usr/lib/openclaw/plugins/openclaw-mem
```

### Per-User

```bash
git clone https://github.com/phenomenoner/openclaw-mem.git ~/openclaw-mem
cd ~/openclaw-mem
uv sync --locked

ln -s ~/openclaw-mem/extensions/openclaw-mem ~/.openclaw/plugins/openclaw-mem
```

### Configuration

Add to `~/.openclaw/openclaw.json` (or `/etc/openclaw/openclaw.json` for system-wide):

```jsonc
{
  "plugins": {
    "entries": {
      "openclaw-mem": {
        "enabled": true,
        "config": {
          "outputPath": "~/.openclaw/memory/openclaw-mem-observations.jsonl",
          "captureMessage": false,
          "redactSensitive": true,

          // Recommended safety default: avoid persisting high-sensitivity tools.
          // This plugin captures *tool results* (not raw user inbound messages).
          // For user preferences / reminders, use `openclaw-mem store` explicitly.
          "excludeTools": ["exec", "read", "browser", "gateway", "message", "nodes", "canvas"],

          // Optional: keep watchdog / narrow cron lanes out of sidecar capture entirely.
          // This is the bounded `openclaw-mem` adaptation of per-agent exclude.
          "excludeAgents": ["cron-watchdog", "healthcheck" ]

          // Alternative (stricter): use includeTools allowlist instead.
          // "includeTools": ["web_search", "web_fetch"]
        }
      }
    }
  }
}
```

Note:
- If your OpenClaw uses a non-default state dir (e.g. `OPENCLAW_STATE_DIR=/some/dir`), set `outputPath` under that directory (e.g. `/some/dir/memory/openclaw-mem-observations.jsonl`).

### About `excludeTools` / `excludeAgents`

- This plugin listens to **`tool_result_persist`** events, so it captures **tool results**, not raw inbound user messages.
- Excluding `message` mainly avoids persisting **outbound sendMessage payloads** (which may include private info), and does **not** prevent you from storing user preferences.
- `excludeAgents` is the per-agent carve-out for noisy or intentionally read-only lanes.
  - Matching is **exact string match** against the OpenClaw agent id.
  - When an agent id matches `excludeAgents`, the sidecar skips observation capture on `tool_result_persist` and skips `agent_end` episodic spool writes for that lane.
  - The episodic-spool effect matters only when `plugins.entries.openclaw-mem.config.episodes.enabled=true`.
  - This is useful for watchdog / healthcheck / lint / smoke loops where durable memory should stay clean by default.
  - Rollback is one-line: remove the agent id from `excludeAgents` (or remove the key entirely).
- Recommended pattern for personal / task-like facts ("buy coffee this afternoon", preferences, etc.):
  - use explicit CLI write **`openclaw-mem store`** when the agent/user says “remember this”.

If you want broader capture for debugging, prefer **includeTools allowlist** over capturing everything.

## 2. Periodic Ingestion

### Recommended profile: always-fresh + controlled semantic refresh

Use two separate cadences:

- **Fast ingest lane (freshness):** every 5 minutes
  - `harvest --no-embed --no-update-index`
- **Slow semantic lane (quality):** every 60 minutes (or slower)
  - `harvest --embed --update-index`

Why split?
- Fast lane keeps recent recall near real-time.
- Slow lane avoids paying embedding/index cost on every small batch.
- Operationally, this reduced observed lag from ~118 minutes to sub-minute in field testing.

Cost note:
- **Cheapest:** run harvest via system scheduler (systemd/cron), no LLM wrapper tokens.
- **Convenient:** OpenClaw cron `agentTurn` orchestration, but each run still consumes model tokens.

### Option A: systemd Timer (Linux)

Create `~/.config/systemd/user/openclaw-mem-ingest.service`:

```ini
[Unit]
Description=OpenClaw Memory Ingestion
After=network.target

[Service]
Type=oneshot
WorkingDirectory=/home/YOUR_USER/openclaw-mem
Environment="PATH=/home/YOUR_USER/.local/bin:/usr/bin:/bin"
ExecStart=/usr/bin/uv run --python 3.13 -- python -m openclaw_mem harvest --source /home/YOUR_USER/.openclaw/memory/openclaw-mem-observations.jsonl --no-embed --no-update-index --json

[Install]
WantedBy=default.target
```

Create `~/.config/systemd/user/openclaw-mem-ingest.timer`:

```ini
[Unit]
Description=Run OpenClaw Memory Ingestion every 5 minutes

[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
Persistent=true

[Install]
WantedBy=timers.target
```

Enable and start:

```bash
systemctl --user daemon-reload
systemctl --user enable openclaw-mem-ingest.timer
systemctl --user start openclaw-mem-ingest.timer

# Check status
systemctl --user status openclaw-mem-ingest.timer
journalctl --user -u openclaw-mem-ingest.service -f
```

### Option B: OpenClaw Cron Job

Add to OpenClaw config:

```json
{
  "cron": {
    "jobs": [
      {
        "name": "openclaw-mem ingest (5m)",
        "schedule": { "kind": "every", "everyMs": 300000 },
        "sessionTarget": "isolated",
        "wakeMode": "next-heartbeat",
        "payload": {
          "kind": "agentTurn",
          "model": "google-antigravity/gemini-3-flash",
          "thinking": "minimal",
          "message": "Run exactly one exec, then output ONLY NO_REPLY:\n\ncd /opt/openclaw-mem && uv run --python 3.13 -- python -m openclaw_mem harvest --source $HOME/.openclaw/memory/openclaw-mem-observations.jsonl --no-embed --no-update-index --json"
        },
        "delivery": { "mode": "none" }
      },
      {
        "name": "openclaw-mem embed+index (hourly)",
        "schedule": { "kind": "every", "everyMs": 3600000 },
        "sessionTarget": "isolated",
        "wakeMode": "next-heartbeat",
        "payload": {
          "kind": "agentTurn",
          "model": "google-antigravity/gemini-3-flash",
          "thinking": "minimal",
          "message": "Run exactly one exec, then output ONLY NO_REPLY:\n\ncd /opt/openclaw-mem && uv run --python 3.13 -- python -m openclaw_mem harvest --source $HOME/.openclaw/memory/openclaw-mem-observations.jsonl --embed --update-index --json"
        },
        "delivery": { "mode": "none" }
      }
    ]
  }
}
```

### Option C: Traditional Cron

```bash
# Edit crontab
crontab -e

# Add line (runs every 5 minutes)
*/5 * * * * cd /opt/openclaw-mem && uv run --python 3.13 -- python -m openclaw_mem harvest --source $HOME/.openclaw/memory/openclaw-mem-observations.jsonl --no-embed --no-update-index --json >> $HOME/.openclaw/logs/openclaw-mem-harvest.log 2>&1
```

## 2A. Optional governed optimization assist lane

If you want a bounded maintenance write lane, keep it separate from heartbeat/healthcheck jobs.
The recommended shape is a small scheduled worker that runs the full packet chain through the dedicated runner:

- `python tools/optimize_assist_runner.py`

Start with `--dry-run` until you have reviewed receipts on fixture or low-risk real data.

### OpenClaw cron example

```json
{
  "cron": {
    "jobs": [
      {
        "name": "openclaw-mem optimize assist canary (dry-run)",
        "schedule": { "kind": "every", "everyMs": 21600000 },
        "sessionTarget": "isolated",
        "wakeMode": "next-heartbeat",
        "payload": {
          "kind": "agentTurn",
          "model": "google-antigravity/gemini-3-flash",
          "thinking": "minimal",

          "message": "Run exactly one exec, then output ONLY NO_REPLY:\n\ncd /opt/openclaw-mem && uv run --python 3.13 -- python tools/optimize_assist_runner.py --json"
        },
        "delivery": { "mode": "none" }
      }
    ]
  }
}
```

Ready-to-paste snippet file:
- `docs/snippets/openclaw-cron.optimize-assist-canary.json`

Recommended next-step controller packet after Phase 8-11 code lands, while live receipts are still empty:

```json
{
  "cron": {
    "jobs": [
      {
        "name": "openclaw-mem optimize assist controller (dry-run, challenger-gated)",
        "schedule": { "kind": "every", "everyMs": 21600000 },
        "sessionTarget": "isolated",
        "wakeMode": "next-heartbeat",
        "payload": {
          "kind": "agentTurn",
          "model": "google-antigravity/gemini-3-flash",
          "thinking": "minimal",
          "message": "Run exactly one exec, then output ONLY NO_REPLY:\n\ncd /opt/openclaw-mem && uv run --python 3.13 -- python tools/optimize_assist_runner.py --controller-mode dry_run --challenger-enforce-quarantine --challenger-require-agreement-for-promotion --challenger-max-disagreements-for-promotion 0 --json"
        },
        "delivery": { "mode": "none" }
      }
    ]
  }
}
```

Ready-to-paste snippet file:
- `docs/snippets/openclaw-cron.optimize-assist-controller-dry-run.json`

### Promotion rules

- Keep the job **isolated** from watchdog/healthcheck lanes.
- Keep `assist-apply` on **approved packets only**.
- Remove dry-run only after you have verified before/after + rollback receipts and you are comfortable with the row/day caps. With the runner, that means adding `--allow-apply`.
- Do not use this lane for config mutation, routing changes, or broad memory gardening.

### Systemd example

Create `~/.config/systemd/user/openclaw-mem-optimize-assist.service`:

```ini
[Unit]
Description=OpenClaw Memory governed optimize assist lane
After=network.target

[Service]
Type=oneshot
WorkingDirectory=/home/YOUR_USER/openclaw-mem
Environment="PATH=/home/YOUR_USER/.local/bin:/usr/bin:/bin"
ExecStart=/usr/bin/uv run --python 3.13 -- python tools/optimize_assist_runner.py --json

[Install]
WantedBy=default.target
```

Create `~/.config/systemd/user/openclaw-mem-optimize-assist.timer`:

```ini
[Unit]
Description=Run OpenClaw Memory optimize assist lane every 6 hours

[Timer]
OnBootSec=5min
OnUnitActiveSec=6h
Persistent=true

[Install]
WantedBy=timers.target
```

## 2B. Optional governed continuity side-car

If you want derived continuity receipts on top of the normal Store/Pack/Observe flow, you can enable the `continuity` control plane from the same checkout.
This is optional and stays derived-only. It does **not** replace your memory-of-record.
When enabled, it starts autonomous snapshot + receipt generation on the configured cadence and writes artifacts under `~/.openclaw/memory/openclaw-mem/self-model-sidecar/`.

Recommended activation path:

```bash
cd /opt/openclaw-mem

# Inspect current state first
uv run --python 3.13 -- python -m openclaw_mem continuity status --json

# Enable periodic continuity snapshots + receipts
uv run --python 3.13 -- python -m openclaw_mem continuity enable --cadence-seconds 300 --json

# Verify control plane status
uv run --python 3.13 -- python -m openclaw_mem continuity status --json

# Start the bounded 72h soak controller
python3 tools/self_model_sidecar_soak_controller.py \
  --repo-root /opt/openclaw-mem \
  --run-dir ~/.openclaw/memory/openclaw-mem/self-model-sidecar \
  --cadence-seconds 300 \
  --target-hours 72
```

The soak controller uses a fresh baseline under `~/.openclaw/memory/openclaw-mem/self-model-sidecar/soak/`, warns clearly on stale/gapped/suspicious drift, and is designed to disable its own Gateway cron job once the 72h window passes cleanly.

Recommended first operator checks after enablement:

```bash
uv run --python 3.13 -- python -m openclaw_mem continuity current --json
uv run --python 3.13 -- python -m openclaw_mem continuity attachment-map --snapshot <snapshot.json> --json
uv run --python 3.13 -- python -m openclaw_mem continuity adjudication --snapshot <snapshot.json> --json
uv run --python 3.13 -- python -m openclaw_mem continuity explain --snapshot <snapshot.json> --stance <id> --json
uv run --python 3.13 -- python -m openclaw_mem continuity sensitivity --snapshot <snapshot.json> --stance <id> --json
uv run --python 3.13 -- python -m openclaw_mem continuity public-summary --snapshot <snapshot.json> --json
uv run --python 3.13 -- python -m openclaw_mem continuity patterns --json
uv run --python 3.13 -- python -m openclaw_mem continuity triggers --snapshot <snapshot.json> --json
uv run --python 3.13 -- python -m openclaw_mem continuity interventions --snapshot <snapshot.json> --json
uv run --python 3.13 -- python -m openclaw_mem continuity wording-lint --snapshot <snapshot.json> --json
uv run --python 3.13 -- python -m openclaw_mem continuity release-history --json
```

Operator notes:
- `continuity adjudication` is the gate that decides how strong a continuity claim is allowed to be.
- `continuity explain` and `continuity sensitivity` are the minimum anti-delusion checks before stronger product copy or release actions.
- `continuity public-summary` is the bounded hedge-first surface for restrained external phrasing.
- `continuity release` now supports `weaken`, `rebind`, and `retire`, and `release-history` gives you replayable receipts.
- `continuity patterns`, `triggers`, and `interventions` convert persisted receipts into a governed operator loop.
- `continuity wording-lint` catches missing hedges and banned selfhood-inflation terms before text leaves the lane.
- If you do not want autonomous continuity receipts, leave the control plane disabled and run the read surfaces ad hoc.

Rollback:

```bash
uv run --python 3.13 -- python -m openclaw_mem continuity disable --json
```

## 2C. Episodic auto-mode ingestion (new)

When `plugins.entries["openclaw-mem"].config.episodes.enabled=true`, run extractor periodically and keep ingest in follow mode (daemon/tailer).

Auto-captured set includes:
- plugin lane: `tool.call`, `tool.result`, `ops.alert`
- extractor lane: `conversation.user`, `conversation.assistant`

Scope derivation for conversation text:
- leading `[SCOPE: x]` → `x`
- otherwise `global`

### Recommended: extractor cron + ingest follow service

```bash
# extractor (periodic)
*/2 * * * * cd /opt/openclaw-mem && uv run --python 3.13 -- python -m openclaw_mem episodes extract-sessions --sessions-root ~/.openclaw/sessions --file ~/.openclaw/memory/openclaw-mem-episodes.jsonl --state ~/.openclaw/memory/openclaw-mem/episodes-extract-state.json --payload-cap-bytes 4096 --json >/dev/null 2>&1

# ingest daemon (systemd/supervisor/pm2 screen/tmux)
cd /opt/openclaw-mem && uv run --python 3.13 -- python -m openclaw_mem episodes ingest --file ~/.openclaw/memory/openclaw-mem-episodes.jsonl --state ~/.openclaw/memory/openclaw-mem/episodes-ingest-state.json --conversation-payload-cap-bytes 4096 --follow --poll-interval-ms 1000 --rotate-on-idle-seconds 60 --rotate-min-bytes 1048576 --json
```

Follow mode notes:
- resumes from state offset across restarts
- safely resets offset when spool is truncated/replaced (inode/dev or size shrink detection)
- low CPU when idle (sleep-based polling)
- graceful stop on SIGINT/SIGTERM
- optional `--rotate-on-idle-seconds` keeps the JSONL spool bounded; rotation is coordinated via a sibling lock file (`<spool>.lock`)

### Legacy fallback: periodic ingest pump

```bash
*/2 * * * * cd /opt/openclaw-mem && uv run --python 3.13 -- python -m openclaw_mem episodes ingest --file ~/.openclaw/memory/openclaw-mem-episodes.jsonl --state ~/.openclaw/memory/openclaw-mem/episodes-ingest-state.json --conversation-payload-cap-bytes 4096 --json >/dev/null 2>&1
```

Optional daily spool rotate (batch mode only):

```bash
7 0 * * * cd /opt/openclaw-mem && uv run --python 3.13 -- python -m openclaw_mem episodes ingest --file ~/.openclaw/memory/openclaw-mem-episodes.jsonl --state ~/.openclaw/memory/openclaw-mem/episodes-ingest-state.json --rotate --json >/dev/null 2>&1
```

Verification:

```bash
uv run python -m openclaw_mem episodes query --global --limit 20 --json
uv run python -m openclaw_mem episodes replay <session_id> --global --json
```

Expected:
- `count` increases after activity
- query/replay are summary-only by default (payload appears only with `--include-payload`)

Rollback:
1. stop ingest follow service/process
2. switch ingest back to periodic cron pump (legacy command above)
3. if needed, set `plugins.entries.openclaw-mem.config.episodes.enabled=false`
4. restart gateway

## 3. Log Rotation

### logrotate (Linux)

Create `/etc/logrotate.d/openclaw-mem` (or `~/.logrotate.d/openclaw-mem`):

```
/home/*/.openclaw/memory/openclaw-mem-observations.jsonl
/home/*/.openclaw/logs/openclaw-mem-*.log {
    daily
    rotate 30
    size 50M
    compress
    delaycompress
    missingok
    notifempty
    create 0600 user user
}
```

Test rotation:

```bash
logrotate -d ~/.logrotate.d/openclaw-mem  # dry-run
logrotate -f ~/.logrotate.d/openclaw-mem  # force rotate
```

### Manual Script

```bash
#!/bin/bash
# ~/.openclaw/scripts/rotate-observations.sh

DATE=$(date +%Y-%m-%d)
SRC="$HOME/.openclaw/memory/openclaw-mem-observations.jsonl"
DST="$HOME/.openclaw/memory/archive/openclaw-mem-observations-$DATE.jsonl.gz"

if [ -f "$SRC" ]; then
    mkdir -p "$(dirname "$DST")"
    gzip -c "$SRC" > "$DST"
    > "$SRC"  # truncate original
    echo "Rotated to $DST"
fi
```

Add to cron:

```bash
# Run daily at midnight
0 0 * * * /bin/bash ~/.openclaw/scripts/rotate-observations.sh
```

## 4. AI Compression (Optional)

### Daily Compression (systemd)

Create `~/.config/systemd/user/openclaw-mem-compress.service`:

```ini
[Unit]
Description=OpenClaw Memory AI Compression

[Service]
Type=oneshot
WorkingDirectory=/home/YOUR_USER/openclaw-mem
Environment="OPENAI_API_KEY=sk-YOUR_KEY"
Environment="PATH=/home/YOUR_USER/.local/bin:/usr/bin:/bin"
ExecStart=/usr/bin/uv run --python 3.13 -- python -m openclaw_mem summarize --json

[Install]
WantedBy=default.target
```

Create `~/.config/systemd/user/openclaw-mem-compress.timer`:

```ini
[Unit]
Description=Run OpenClaw Memory AI Compression daily

[Timer]
OnCalendar=daily
OnCalendar=02:00
Persistent=true

[Install]
WantedBy=timers.target
```

Enable:

```bash
systemctl --user daemon-reload
systemctl --user enable openclaw-mem-compress.timer
systemctl --user start openclaw-mem-compress.timer
```

### OpenClaw Cron Job

```json
{
  "cron": {
    "jobs": [
      {
        "name": "Daily AI compression",
        "schedule": { "kind": "cron", "expr": "0 2 * * *", "tz": "UTC" },
        "sessionTarget": "isolated",
        "payload": {
          "kind": "agentTurn",
          "message": "Run: cd /opt/openclaw-mem && OPENAI_API_KEY=$OPENAI_API_KEY uv run --python 3.13 -- python -m openclaw_mem summarize --json",
          "deliver": false
        }
      }
    ]
  }
}
```

## 5. Monitoring

### Read-only carve-out for watchdog / healthcheck lanes

If you run monitoring via an LLM-wrapped cron agent (OpenClaw `agentTurn`), treat it as a **read-only** lane:
- allow **recall/docs** to interpret what “normal” means,
- but **do not store** routine results.

Use the drop-in read-only card:
- `skills/agent-memory-skill.readonly.md`

And keep delivery quiet on OK (emit `NO_REPLY`), only alert on anomalies.

#### OpenClaw cron example (agentTurn + read-only prompt)

This repo includes a copy/paste **watchdog lane** `payload.message` template:
- `docs/snippets/openclaw-agentturn-message.watchdog-readonly.md`

If your OpenClaw config is JSON, JSON-escape that multi-line message:

```bash
cd /opt/openclaw-mem
python3 scripts/json_escape.py docs/snippets/openclaw-agentturn-message.watchdog-readonly.md
```

Then use it as the `payload.message` value (no extra quoting needed):

```json
{
  "name": "openclaw-mem watchdog (read-only)",
  "schedule": { "kind": "cron", "expr": "*/10 * * * *", "tz": "UTC" },
  "sessionTarget": "isolated",
  "wakeMode": "next-heartbeat",
  "payload": {
    "kind": "agentTurn",
    "model": "google-antigravity/gemini-3-flash",
    "thinking": "minimal",
    "message": <PASTE_OUTPUT_OF_json_escape.py_HERE>
  },
  "delivery": { "mode": "none" }
}
```

### Health Check Script

```bash
#!/bin/bash
# ~/.openclaw/scripts/healthcheck-openclaw-mem.sh

DB="$HOME/.openclaw/memory/openclaw-mem.sqlite"
JSONL="$HOME/.openclaw/memory/openclaw-mem-observations.jsonl"

# Check DB exists and is writable
if [ ! -f "$DB" ] || [ ! -w "$DB" ]; then
    echo "ERROR: Database missing or not writable: $DB"
    exit 1
fi

# Check JSONL isn't too large (>50MB)
if [ -f "$JSONL" ]; then
    SIZE=$(stat -f%z "$JSONL" 2>/dev/null || stat -c%s "$JSONL" 2>/dev/null)
    if [ "$SIZE" -gt 52428800 ]; then
        echo "WARNING: JSONL file is large (${SIZE} bytes). Consider rotation."
    fi
fi

# Check observation count
cd /opt/openclaw-mem
COUNT=$(uv run --python 3.13 -- python -m openclaw_mem status --json | python3 -c 'import sys, json; print(json.load(sys.stdin).get("count", 0))')
if [ "$COUNT" -lt 10 ]; then
    echo "WARNING: Low observation count ($COUNT). Check plugin is capturing."
fi

echo "OK: $COUNT observations in DB"
```

Add to cron for daily checks:

```bash
0 6 * * * /bin/bash ~/.openclaw/scripts/healthcheck-openclaw-mem.sh
```

### Metrics Export (Optional)

```python
#!/usr/bin/env python3
# Export metrics for Prometheus/Grafana

import json
import sqlite3
from pathlib import Path

DB = Path.home() / ".openclaw/memory/openclaw-mem.sqlite"
conn = sqlite3.connect(DB)

# Total observations
count = conn.execute("SELECT COUNT(*) FROM observations").fetchone()[0]
print(f"openclaw_mem_observations_total {count}")

# Observations by tool
rows = conn.execute(
    "SELECT tool_name, COUNT(*) FROM observations GROUP BY tool_name"
).fetchall()
for tool, cnt in rows:
    print(f'openclaw_mem_observations_by_tool{{tool="{tool}"}} {cnt}')

conn.close()
```

## 6. Backup & Disaster Recovery

### Backup Script

```bash
#!/bin/bash
# Backup SQLite DB and JSONL to S3/rsync

DATE=$(date +%Y-%m-%d)
BACKUP_DIR="$HOME/.openclaw/backups/$DATE"
mkdir -p "$BACKUP_DIR"

# Copy DB (safe even while writes are happening due to WAL mode)
cp ~/.openclaw/memory/openclaw-mem.sqlite "$BACKUP_DIR/"
cp ~/.openclaw/memory/openclaw-mem-observations.jsonl "$BACKUP_DIR/" 2>/dev/null || true

# Compress
tar -czf "$HOME/.openclaw/backups/openclaw-mem-$DATE.tar.gz" -C "$HOME/.openclaw/backups" "$DATE"
rm -rf "$BACKUP_DIR"

# Upload to S3 (optional)
# aws s3 cp "$HOME/.openclaw/backups/openclaw-mem-$DATE.tar.gz" s3://my-bucket/openclaw-mem/

# Keep last 7 days
find "$HOME/.openclaw/backups" -name "openclaw-mem-*.tar.gz" -mtime +7 -delete

echo "Backup complete: openclaw-mem-$DATE.tar.gz"
```

### Recovery

```bash
# Extract backup
tar -xzf openclaw-mem-2026-02-05.tar.gz

# Restore DB
cp 2026-02-05/openclaw-mem.sqlite ~/.openclaw/memory/

# Verify
cd /opt/openclaw-mem
uv run --python 3.13 -- python -m openclaw_mem status --json
```

## 7. Security Hardening

### File Permissions

```bash
# Restrict DB to user only
chmod 600 ~/.openclaw/memory/openclaw-mem.sqlite

# Restrict JSONL
chmod 600 ~/.openclaw/memory/openclaw-mem-observations.jsonl
```

### Secrets Management

Don't hardcode `OPENAI_API_KEY` in config files. Use:

**systemd:**
```ini
[Service]
EnvironmentFile=/etc/secrets/openclaw-mem.env
```

**Cron:**
```bash
*/5 * * * * source ~/.openclaw/secrets.env && cd /opt/openclaw-mem && ...
```

**OpenClaw config:**
Use environment variable substitution:
```json
{
  "env": {
    "OPENAI_API_KEY": "${OPENAI_API_KEY}"
  }
}
```

### Network Isolation (Optional)

If running on a server, restrict OpenAI API access:

```bash
# Allow only OpenAI API
sudo iptables -A OUTPUT -p tcp -d api.openai.com --dport 443 -j ACCEPT
sudo iptables -A OUTPUT -p tcp --dport 443 -j DROP  # drop all other HTTPS
```

## 8. Troubleshooting

### Check Plugin Status

```bash
openclaw plugins list | grep openclaw-mem
```

### Check Capture Activity

```bash
# Tail JSONL (should show new lines as tools run)
tail -f ~/.openclaw/memory/openclaw-mem-observations.jsonl

# Check file modification time
stat ~/.openclaw/memory/openclaw-mem-observations.jsonl
```

### Check Ingestion Logs

```bash
# systemd
journalctl --user -u openclaw-mem-ingest.service -f

# cron
tail -f ~/.openclaw/logs/openclaw-mem-ingest.log
```

### Database Locked Errors

Ensure WAL mode is enabled:

```bash
sqlite3 ~/.openclaw/memory/openclaw-mem.sqlite "PRAGMA journal_mode;"
# Should output: wal
```

### High Memory Usage

Check DB size:

```bash
du -h ~/.openclaw/memory/openclaw-mem.sqlite
```

If >1GB, consider archiving old observations:

```sql
-- Archive observations older than 90 days
DELETE FROM observations WHERE ts < datetime('now', '-90 days');
VACUUM;
```

## Production Checklist

- [ ] Plugin installed and enabled in config
- [ ] Periodic ingestion set up (systemd/cron)
- [ ] Log rotation configured
- [ ] Health checks in place
- [ ] Backups automated
- [ ] File permissions secured
- [ ] Secrets stored securely (not in config files)
- [ ] Monitoring/alerting configured
- [ ] Documentation updated with custom paths

## Performance Tips

- Run ingestion every 5-10 minutes (not every minute)
- Use `captureMessage: false` in plugin config (saves ~80% space)
- Enable log rotation (keeps JSONL files <50MB)
- Archive old observations (DELETE + VACUUM quarterly)
- Use `gemini-3-flash` for AI compression (cheaper than GPT-4)

## Support

For deployment issues, see:
- [`docs/db-concurrency.md`](db-concurrency.md) — Database locking
- [`docs/auto-capture.md`](auto-capture.md) — Plugin troubleshooting
- GitHub Issues: https://github.com/phenomenoner/openclaw-mem/issues
