Persistent Memory Across Restarts for a Personal Agent
Make personal-agent memory survive systemctl restart, laptop reboots, and container recreates by writing the right state to durable storage - not by stuffing infinite chat into RAM.
Summary
Split memory into preferences, task state, and optional retrieval notes; flush on every meaningful change; keep secrets out of memory stores; restore on host boot before the first chat wake.
Recipe
- Inventory what must survive. Prefs (timezone, tone, quiet hours), open tasks, approval payloads, processed message ids. Explicitly exclude raw API keys and full mail bodies by default.
- Pick storage. Start with JSON files + SQLite on a mounted data volume. Add vector retrieval only when keyword/task notes fail.
- Define schemas with version fields so you can migrate after upgrades.
- Write-through on change. Do not rely on process shutdown hooks alone; crashes skip them.
- Load on startup before accepting chat events.
- Bound what enters the model. Inject a short prefs block + top-k relevant notes, not the entire database.
- Back up the data directory on a schedule; test restore once.
- Document wipe: delete data dir, rotate keys if compromise suspected, re-seed prefs from a clean template.
Working Example
File prefs + SQLite notes (illustrative):
import json
import sqlite3
from pathlib import Path
from datetime import datetime, timezone
DATA = Path.home() / ".personal-agent"
PREFS_PATH = DATA / "prefs.json"
DB_PATH = DATA / "memory.db"
DEFAULT_PREFS = {
"version": 1,
"timezone": "America/Los_Angeles",
"tone": "concise, no emojis",
"quiet_hours": ["22:00", "07:00"],
"vip_senders": [],
}
def ensure_store() -> None:
DATA.mkdir(parents=True, exist_ok=True)
if not PREFS_PATH.exists():
PREFS_PATH.write_text(json.dumps(DEFAULT_PREFS, indent=2))
con = sqlite3.connect(DB_PATH)
con.execute(
"""CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY,
kind TEXT NOT NULL,
content TEXT NOT NULL,
created_at TEXT NOT NULL
)"""
)
con.execute(
"""CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
status TEXT NOT NULL,
payload TEXT NOT NULL,
updated_at TEXT NOT NULL
)"""
)
con.commit()
con.close()
def load_prefs() -> dict:
ensure_store()
return json.loads(PREFS_PATH.read_text())
def save_prefs(prefs: dict) -> None:
ensure_store()
tmp = PREFS_PATH.with_suffix(".tmp")
tmp.write_text(json.dumps(prefs, indent=2))
tmp.replace(PREFS_PATH) # atomic on same filesystem
def add_note(kind: str, content: str) -> None:
ensure_store()
con = sqlite3.connect(DB_PATH)
con.execute(
"INSERT INTO notes (kind, content, created_at) VALUES (?, ?, ?)",
(kind, content[:2000], datetime.now(timezone.utc).isoformat()),
)
con.commit()
con.close()
def recent_notes(limit: int = 5) -> list[str]:
ensure_store()
con = sqlite3.connect(DB_PATH)
rows = con.execute(
"SELECT content FROM notes ORDER BY id DESC LIMIT ?", (limit,)
).fetchall()
con.close()
return [r[0] for r in rows]
def build_system_context() -> str:
prefs = load_prefs()
notes = recent_notes(5)
return (
"User preferences (durable):\n"
f"{json.dumps(prefs, indent=2)}\n\n"
"Recent notes:\n- " + "\n- ".join(notes or ["(none)"])
)On host boot:
def startup() -> None:
ensure_store()
prefs = load_prefs()
# optional: rehydrate in-flight tasks with status != done
assert prefs.get("version") == 1Docker/systemd: mount DATA outside the image so redeploys keep prefs.json and memory.db.
Deep Dive
Three memory layers
| Layer | Lifetime | Examples | Failure if only in RAM |
|---|---|---|---|
| Working | One wake | Tool traces, intermediate drafts | Expected to vanish |
| Task | Hours-days | Open "book dentist" thread, pending APPROVE payloads | Lost mid-approval |
| Durable prefs / facts | Months | Timezone, VIP list, "never call before 10" | Agent amnesia every restart |
Clawdbot/OpenClaw-style always-on hosts feel "personal" when durable layers actually reload after process death.
What not to persist by default
- Full email bodies and attachments
- Raw OAuth refresh tokens (use the OS secret store or encrypted secret files separate from "memory")
- Complete chat transcripts forever
- Shell command output that may contain secrets
If you need audit logs, store them as ops logs with retention and redaction - not as model memory.
Write-through and atomicity
Crash mid-write can corrupt JSON.
Write to a temp file, then replace().
For SQLite, use transactions around multi-row task updates.
Flush after approvals, pref edits, and task status changes - the moments you cannot afford to lose.
Injection into the prompt
Every wake should rebuild a small context block:
- System policy (code)
- Prefs snapshot (durable)
- Active task (if any)
- Retrieved notes (optional, top-k)
- Current chat message
Do not replay the entire SQLite history into the model. That burns money and confuses priorities.
Migrations
Include "version": 1 in prefs and a schema_migrations table for SQL.
On startup, run additive migrations before serving traffic.
Keep a backup before migrating on a VPS.
Backup and restore
| Asset | Backup? | Notes |
|---|---|---|
prefs.json | yes | Small and critical |
memory.db | yes | Task continuity |
| Vector index | optional | Rebuildable from notes if you keep source rows |
| Logs | optional | Privacy-sensitive |
| Secrets env | separate secure backup | Not inside memory DB |
Test restore on a spare directory before you need it.
Multi-device caution
Two hosts writing one networked volume without coordination will corrupt task state.
Prefer a single active writer (one VPS) or an explicit primary.
Gotchas
- Memory only in the model provider's "memory product". You still need local task/approval state the host controls.
- Container without a volume. Beautiful amnesia on every deploy.
- Embedding every email. Cost, privacy, and retrieval noise; start structured.
- Model-writable prefs without review. The agent can "remember" insecure defaults; log diffs and gate security keys.
- Huge notes. Cap length; summarize offline if needed.
- Clock and timezone bugs. Store UTC timestamps; render in prefs timezone.
- Forgetting processed message ids. Restarts re-handle the same chat update and double-act.
- Backing up unencrypted memory to random cloud folders. Encrypt or use a trust-appropriate target.
Alternatives
| Approach | Pros | Cons |
|---|---|---|
| JSON prefs only | Simple, inspectable | Weak for many tasks |
| SQLite tasks + notes | Good default for solo hosts | You own migrations |
| Postgres on the side | Multi-process friendly | Heavier for personal use |
| Vector DB retrieval | Semantic recall | Ops + over-retention risk |
| Provider-side memory features | Fast to try | Portability and privacy trade-offs |
FAQs
Is vector search required for a personal agent?
No. Prefs + a few structured notes handle most day-to-day continuity.
How much history should I keep?
Keep open tasks and recent notes. Archive or delete completed task payloads on a schedule you define.
Where do approvals live across restart?
In the task table as status=pending_approval with the exact payload hash. After reboot, chat can still APPROVE <id>.
Can I sync memory via git?
Prefs maybe; full memory with personal content usually no. Git history is a leaky backup.
How do I prevent secret leakage into notes?
Never pass env vars into note-writing tools. Redact token-like patterns before insert.
What happens if prefs JSON is corrupt?
Boot into safe mode with DEFAULT_PREFS, alert you in chat, and refuse high-risk tools until fixed.
Should each chat platform have separate memory?
Share prefs and tasks across adapters; keep platform ids in auth tables, not duplicate brains.
How does this interact with long-term memory products?
Treat enterprise "memory platforms" as optional retrieval backends. The host still needs local durability for control-plane state.
What is a good first week setup?
prefs.json + SQLite tasks/notes on a backed-up volume. No vectors yet.
How do Clawdbot/OpenClaw-style systems fit?
They emphasize memory that outlives process restarts. Implement that property with boring storage; do not assume a brand name equals a good schema.
Related
- Self-Hosted Personal Agents Basics - prefs file sketch
- The Self-Hosted Personal Agent Mental Model - where memory sits in the host
- Setting Up Clawdbot/OpenClaw on a VPS or Local Machine - data directory layout
- Self-Hosted Personal Agents Best Practices - retention and wipe habits
- What a Personal AI Agent Actually Does Day to Day - what is worth remembering
- Long-Term Memory - deeper memory patterns (section)
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.