My pi setup had a problem. total-recall indexed my sessions and collected exactly 7 facts across the whole history. Seven. That is not memory, that is a sticky note someone forgot to throw away.
I wanted what claude-mem actually delivers: facts and knowledge collected automatically, available across sessions, and ideally shared between pi and Claude Code so I stop re-explaining my own projects to two different agents.
Why bother with Claude Code at all? I run it on a $1000 credit I am not in a hurry to lose, and pushing pi through that same account feels like a reliable way to get it banned. So pi talks to my own model provider and Claude Code talks to the credited one. The catch is they then know nothing about each other. claude-mem is the bridge: one worker, two agents feeding it, neither borrowing the other’s auth.
Here is how I wired it in, what broke, and the two fixes that mattered.
The package
pi-agent-memory is the pi extension that bridges pi to the claude-mem worker. It is an ArtemisAI fork of thedotmack/claude-mem, the same engine Claude Code runs.
pi install npm:pi-agent-memoryIt drops in five lifecycle hooks (SessionStart, UserPromptSubmit, PostToolUse, Stop, SessionEnd). Those hooks observe tool calls, build semantic summaries, and inject them into future sessions. That is the auto-collect behavior I was after.
I looked at jo-inc/pi-mem too. Plain Markdown, no SQLite, git-backable, works with pi and Claude Code. Tempting. But it is not cross-engine by default. My priority was a single store both agents read from, and claude-mem’s worker gives you that out of the box. So I went with the worker.
The worker was already running
Claude Code had already installed claude-mem. Its worker daemon was alive, backed by SQLite + FTS5 + Chroma, sitting in ~/.claude-mem/. It was just managed by CC’s plugin, so it died when I closed CC.
I wanted it independent of Claude Code’s plugin lifecycle, so the worker runs on its own. claude-mem ships the worker as a script you launch directly (this is the same line the ensure script fires when the port is empty):
bun ~/.claude/plugins/marketplaces/thedotmack/plugin/scripts/worker-service.cjs startRun it once and the worker stays up on its own. CC connects to the same instance. One store, two agents. Single source of truth, confirmed.
The one bug that ate an hour
claude-mem writes its worker port into ~/.claude-mem/settings.json as a string. The ArtemisAI pi-mem reader checks typeof port === "number". String fails the check, so it silently falls back to the default port, which has nothing listening. Result: pi loads the extension, reaches for the worker, gets fetch failed, and you get zero memory.
The fix is the official override the extension checks first:
export CLAUDE_MEM_PORT=37701 # whatever your worker actually listens onpi-only. No shared CC config touched. After that, pi -p and a /memory-status check connected clean.
Backfilling 262 sessions
Fresh install means an empty store. I had 262 pi sessions on disk as JSONL. claude-mem captures live, it has no pi-JSONL importer, but it exposes a Worker HTTP API. So I scripted the backfill: read each session, post turns as observations, let the worker summarize.
Condensed version of what ran:
import json, urllib.request, os, glob
BASE = "http://127.0.0.1:37701"SESSIONS = os.path.expanduser("~/.pi/agent/sessions")
def post(path, body): req = urllib.request.Request( BASE + path, data=json.dumps(body).encode(), headers={"Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(req, timeout=15) as r: return r.status
for path in glob.glob(f"{SESSIONS}/**/*.jsonl", recursive=True): sid = "pi-backfill-" + os.path.basename(path) post("/api/sessions/init", { "contentSessionId": sid, "project": "pi-backfill", "prompt": "historical session", "platformSource": "pi-agent", }) turns = [] with open(path) as fh: for line in fh: try: o = json.loads(line) except Exception: continue msg = o.get("message", o) for part in msg.get("content", []) if isinstance(msg.get("content"), list) else []: if isinstance(part, dict) and part.get("type") == "text": turns.append(part["text"]) for chunk in turns: post("/api/sessions/observations", { "contentSessionId": sid, "tool_name": "UserPrompt", "tool_input": chunk[:4000], }) post("/api/sessions/summarize", {"contentSessionId": sid})I also pushed the 7 existing facts into the dominant project buckets so preferences recall across projects. End state: 269 pi-agent sessions in the worker DB, AI summarization queue draining in the background.
Dropping total-recall
Once claude-mem was live, total-recall was dead weight. Removed npm:pi-total-recall from settings.json, deleted the cluster (pi-total-recall, pi-session-search, pi-knowledge-search) and its SQLite dirs. Its session_search tool is replaced by claude-mem’s memory_recall.
Backed up first, always:
BK=~/.pi/agent-backup-$(date +%Y%m%dT%H%M%S)mkdir -p "$BK"cp ~/.pi/agent/settings.json "$BK"/Making it self-healing
A manual daemon dies on reboot, so I stack three things. Cheapest first.
First, an idempotent ensure script at ~/.local/bin/ensure-claude-mem-worker.sh. It checks whether anything is already listening on the port; if so it exits, otherwise it logs and starts the worker. Full contents:
#!/usr/bin/env bash# Ensure the claude-mem worker is listening on :37701. Idempotent + safe to call often.# Used by: launchd (RunAtLoad + StartInterval) and the `pi` zsh hook.set -uPORT=37701ROOT="${CLAUDE_MEM_ROOT:-/Users/rezhapradana/.claude/plugins/marketplaces/thedotmack}"LOG_DIR="$HOME/.claude-mem/logs"LOG="$LOG_DIR/ensure-$(date +%Y-%m-%d).log"export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/sbin:/usr/bin:/bin:$HOME/.bun/bin:$HOME/.local/bin:$PATH"
mkdir -p "$LOG_DIR"
# Already listening? -> nothing to do.if lsof -iTCP:"$PORT" -sTCP:LISTEN -n -P >/dev/null 2>&1; then exit 0fi
echo "$(date '+%H:%M:%S') ensure: worker not on :$PORT, starting…" >>"$LOG"
if [ ! -x "$ROOT/plugin/scripts/worker-service.cjs" ]; then echo "$(date '+%H:%M:%S') ensure: worker-service.cjs missing at $ROOT" >>"$LOG" exit 1fi
cd "$ROOT" || exit 1bun plugin/scripts/worker-service.cjs start >>"$LOG" 2>&1echo "$(date '+%H:%M:%S') ensure: start issued (exit $?)" >>"$LOG"Safe to call on every launch.
Second, a launchd agent at ~/Library/LaunchAgents/com.claude-mem.worker.plist with RunAtLoad and StartInterval 300. It self-heals after a crash or reboot and serves both pi and CC, so neither app needs its own hook. Worst case there is a five-minute gap between checks, which is fine here.
Third, a pi() zsh hook so the terminal never blocks waiting:
pi() { ~/.local/bin/ensure-claude-mem-worker.sh >/dev/null 2>&1 & command pi "$@"}Verify
Installing is easy. The real question is whether it actually recalls.
pi -p # triggers session_start hook -> worker connect/memory-statusmemory_recall for the godot game I am building returned 5 results. airflow db sync returned 5 results. Those are backfilled sessions, live, and cross-engine. Done.
Caveats
- The worker is a daemon. Bun + Node +
uv+ SQLite. More moving parts than a Markdown file in your dotfiles. Worth it for cross-engine sharing, not free. - Git-backing is a JSON snapshot via
export-memories.ts, not a live diffable store. If you want version control, cron the export and commit it. - The string-vs-number port mismatch is the kind of bug that stays silent until you actually query memory. Set
CLAUDE_MEM_PORTand move on.
Net result: pi now remembers across sessions, shares a brain with Claude Code, and the worker keeps itself alive. Seven facts became 262 sessions of context, plus whatever Claude Code already stashed in the same store.