Files
mempalace/hooks/README.md
T

190 lines
8.0 KiB
Markdown
Raw Normal View History

# MemPalace Hooks — Auto-Save for Terminal AI Tools
These hook scripts make MemPalace save automatically. No manual "save" commands needed.
2026-05-09 10:52:25 -05:00
This deployment ships only the **remote** hook variants — the palace runs as a Docker container on a server (e.g. Unraid), and hooks `curl` the active session transcript to the server's `/ingest/transcript` endpoint over HTTPS with bearer auth. Server-side, the existing `mine_convos` pipeline handles entity detection, room assignment, dedup, and idempotency. See [`deploy/unraid/README.md`](../deploy/unraid/README.md) for the server side.
## What They Do
| Hook | When It Fires | What Happens |
2026-05-09 10:52:25 -05:00
|---|---|---|
| **Save Hook** (`mempal_save_hook_remote.sh`) | Every 15 user messages (configurable via `SAVE_INTERVAL`) | Backgrounded `curl` POSTs the active transcript. Returns immediately so the AI doesn't stall. Idempotent — failed retries are safe. |
| **PreCompact Hook** (`mempal_precompact_hook_remote.sh`) | Right before context compaction | Synchronous `curl` POST. Blocks until the upload completes (or the hook timeout fires) so memory is durable before context shrinks. |
**Two-layer capture.** The save hook ships the JSONL transcript directly to the server (capturing raw tool output — Bash results, search findings, build errors), where the miner files it verbatim into the palace. Tool output gets stored even if the AI summarizes instead of quoting.
## Env-var contract
The scripts read all configuration from environment variables. There is no script-level config to edit; the same script works against any number of machines.
| Variable | Required | Purpose |
|---|---|---|
| `MEMPAL_REMOTE_URL` | yes | Base URL of the MemPalace server, e.g. `https://unraid.local:8443`. |
| `MEMPAL_REMOTE_TOKEN` | yes | Bearer token shared with the server's `MEMPAL_TOKEN`. |
| `MEMPAL_REMOTE_INSECURE` | no | Set to `1` to skip TLS verification. Use only when the server uses Caddy's `tls internal` self-signed cert and the client hasn't trusted the root CA. |
| `MEMPAL_REMOTE_WING` | no | Force a specific wing for this client's transcripts. Default: server derives wing from the session id. |
| `SAVE_INTERVAL` | no | Override the default of 15 user messages. |
| `MEMPAL_PYTHON` | no | Path to a Python 3 interpreter. Only needs `json` + `sys` from stdlib — mempalace does not need to be installed in it. Used to parse the hook's stdin JSON. |
2026-05-09 10:52:25 -05:00
Set these persistently:
**PowerShell (Windows):**
```powershell
[Environment]::SetEnvironmentVariable("MEMPAL_REMOTE_URL", "https://unraid.local:8443", "User")
[Environment]::SetEnvironmentVariable("MEMPAL_REMOTE_TOKEN", "<the-token>", "User")
[Environment]::SetEnvironmentVariable("MEMPAL_REMOTE_INSECURE", "1", "User") # if self-signed
```
**Bash/Zsh:** add the same exports to `~/.zshrc` / `~/.bashrc`.
If `MEMPAL_REMOTE_URL` or `MEMPAL_REMOTE_TOKEN` is unset, the scripts no-op and log a one-liner — they never block the AI from stopping. Safe to install on a machine that doesn't have a remote configured yet.
## Install — Claude Code
2026-05-09 10:52:25 -05:00
Make the scripts executable:
```bash
chmod +x hooks/mempal_save_hook_remote.sh hooks/mempal_precompact_hook_remote.sh
```
Add to `.claude/settings.local.json`:
```json
{
"hooks": {
"Stop": [{
"matcher": "*",
"hooks": [{
"type": "command",
2026-05-09 10:52:25 -05:00
"command": "/absolute/path/to/hooks/mempal_save_hook_remote.sh",
"timeout": 30
}]
}],
"PreCompact": [{
"hooks": [{
"type": "command",
2026-05-09 10:52:25 -05:00
"command": "/absolute/path/to/hooks/mempal_precompact_hook_remote.sh",
"timeout": 60
}]
}]
}
}
```
## Install — Codex CLI (OpenAI)
Add to `.codex/hooks.json`:
```json
{
"Stop": [{
"type": "command",
2026-05-09 10:52:25 -05:00
"command": "/absolute/path/to/hooks/mempal_save_hook_remote.sh",
"timeout": 30
}],
"PreCompact": [{
"type": "command",
2026-05-09 10:52:25 -05:00
"command": "/absolute/path/to/hooks/mempal_precompact_hook_remote.sh",
"timeout": 60
}]
}
```
2026-05-09 10:52:25 -05:00
## How it works
### Save Hook (Stop event)
```
User sends message → AI responds → Claude Code fires Stop hook
2026-05-09 10:52:25 -05:00
Hook counts user messages in JSONL transcript
2026-05-09 10:52:25 -05:00
┌─── < SAVE_INTERVAL since last save ──→ echo "{}" (let AI stop)
2026-05-09 10:52:25 -05:00
└─── ≥ SAVE_INTERVAL since last save
2026-05-09 10:52:25 -05:00
Background curl POST → server /ingest/transcript
2026-05-09 10:52:25 -05:00
Hook returns {} immediately (AI stops normally)
2026-05-09 10:52:25 -05:00
Server-side miner runs in background, files drawers
```
### PreCompact Hook
```
Context window getting full → Claude Code fires PreCompact
2026-05-09 10:52:25 -05:00
Synchronous curl POST → server /ingest/transcript
2026-05-09 10:52:25 -05:00
Wait for 200 OK (or hook timeout)
2026-05-09 10:52:25 -05:00
echo "{}" → Compaction proceeds
```
2026-05-09 10:52:25 -05:00
Synchronous on PreCompact is intentional — this is the safety net before context shrinks. The Claude Code hook timeout (set in `settings.local.json`) bounds how long we'll wait.
## Debugging
```bash
2026-05-09 10:52:25 -05:00
tail -f ~/.mempalace/hook_state/hook.log
```
2026-05-09 10:52:25 -05:00
Example:
```
[14:30:15] Session abc123: 12 exchanges, 12 since last save
[14:35:22] Session abc123: 15 exchanges, 15 since last save
2026-05-09 10:52:25 -05:00
[14:35:22] ingest ok
[14:50:18] PRE-COMPACT triggered for session abc123
[14:50:19] PRE-COMPACT ingest ok
```
2026-05-09 10:52:25 -05:00
A 401 response means the bearer token is wrong. A connection error means the URL/cert is wrong (or the server is down). All curl output goes to the same log.
2026-05-09 10:52:25 -05:00
## Known limitations
2026-05-09 10:52:25 -05:00
**Hooks require session restart after install.** Claude Code loads hooks from `settings.json` at session start only. If you edit hook config mid-session, restart Claude Code to pick up changes.
2026-05-09 10:52:25 -05:00
**Python interpreter resolution.** The scripts parse hook stdin JSON with `python3`. When Claude Code is launched from a GUI on macOS (Spotlight, dock, `open -a`), its `PATH` is the minimal `/usr/bin:/bin:/usr/sbin:/sbin` inherited from `launchd` rather than your shell PATH. If `python3` isn't there, set `MEMPAL_PYTHON` to a known-good interpreter:
```bash
2026-05-09 10:52:25 -05:00
export MEMPAL_PYTHON="/usr/bin/python3"
# or:
export MEMPAL_PYTHON="$HOME/.venvs/x/bin/python"
```
2026-05-09 10:52:25 -05:00
Resolution priority: `$MEMPAL_PYTHON``$(command -v python3)` → bare `python3`. The interpreter only needs `json` and `sys` — mempalace itself does not need to be installed.
2026-05-09 10:52:25 -05:00
**`MineAlreadyRunning` collisions.** If two clients ingest simultaneously, the second one's request returns 500 because the server-side `mine_lock` is held. The save hook is idempotent — the next save catches up. If you see this constantly in the log, raise `SAVE_INTERVAL` on the chattier client.
2026-05-09 10:52:25 -05:00
## Backfilling past conversations
2026-05-09 10:52:25 -05:00
The hooks only capture sessions going forward. To mine **past** sessions into the remote palace, loop `curl` over them:
```bash
2026-05-09 10:52:25 -05:00
# Claude Code sessions
for f in ~/.claude/projects/**/*.jsonl; do
curl -k -X POST \
-H "Authorization: Bearer $MEMPAL_REMOTE_TOKEN" \
-H "X-Session-Id: $(basename "$f" .jsonl)" \
--data-binary @"$f" \
"$MEMPAL_REMOTE_URL/ingest/transcript"
done
# Codex CLI sessions
for f in ~/.codex/sessions/**/*.jsonl; do
curl -k -X POST \
-H "Authorization: Bearer $MEMPAL_REMOTE_TOKEN" \
-H "X-Session-Id: $(basename "$f" .jsonl)" \
--data-binary @"$f" \
"$MEMPAL_REMOTE_URL/ingest/transcript"
done
```
2026-05-09 10:52:25 -05:00
The server-side miner is idempotent — re-uploading the same transcript won't double-file. Drop `-k` once Caddy's root CA is trusted on the client.
## Cost
2026-05-09 10:52:25 -05:00
**Zero extra tokens.** The hooks save in the background — the AI doesn't need to write anything in the chat window. All filing happens server-side after the upload returns.