MemPalace: palace architecture, AAAK compression, knowledge graph
The memory system: - Palace structure: Wings (people/projects) → Rooms (topics) → Closets (AAAK compressed) → Drawers (verbatim transcripts) - Halls connect related rooms within a wing - Tunnels cross-reference rooms across wings - AAAK: 30x lossless compression dialect for AI agents - Knowledge graph: temporal entity-relationship triples (SQLite) - Palace graph: room-based navigation with tunnel detection - MCP server: 19 tools — search, graph traversal, agent diary, AAAK auto-teach - Onboarding: guided setup generates wing config + AAAK entity registry - Contradiction detection: catches wrong pronouns, names, ages - Auto-save hooks for Claude Code 96.6% Recall@5 on LongMemEval — highest zero-API score published. 100% with optional Haiku rerank (500/500). Local. Free. No API key required.
This commit is contained in:
+138
@@ -0,0 +1,138 @@
|
||||
# MemPalace Hooks — Auto-Save for Terminal AI Tools
|
||||
|
||||
These hook scripts make MemPalace save automatically. No manual "save" commands needed.
|
||||
|
||||
## What They Do
|
||||
|
||||
| Hook | When It Fires | What Happens |
|
||||
|------|--------------|-------------|
|
||||
| **Save Hook** | Every 15 human messages | Blocks the AI, tells it to save key topics/decisions/quotes to the palace |
|
||||
| **PreCompact Hook** | Right before context compaction | Emergency save — forces the AI to save EVERYTHING before losing context |
|
||||
|
||||
The AI does the actual filing — it knows the conversation context, so it classifies memories into the right wings/halls/closets. The hooks just tell it WHEN to save.
|
||||
|
||||
## Install — Claude Code
|
||||
|
||||
Add to `.claude/settings.local.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"Stop": [{
|
||||
"matcher": "*",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "/absolute/path/to/hooks/mempal_save_hook.sh",
|
||||
"timeout": 30
|
||||
}]
|
||||
}],
|
||||
"PreCompact": [{
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "/absolute/path/to/hooks/mempal_precompact_hook.sh",
|
||||
"timeout": 30
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Make them executable:
|
||||
```bash
|
||||
chmod +x hooks/mempal_save_hook.sh hooks/mempal_precompact_hook.sh
|
||||
```
|
||||
|
||||
## Install — Codex CLI (OpenAI)
|
||||
|
||||
Add to `.codex/hooks.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"Stop": [{
|
||||
"type": "command",
|
||||
"command": "/absolute/path/to/hooks/mempal_save_hook.sh",
|
||||
"timeout": 30
|
||||
}],
|
||||
"PreCompact": [{
|
||||
"type": "command",
|
||||
"command": "/absolute/path/to/hooks/mempal_precompact_hook.sh",
|
||||
"timeout": 30
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `mempal_save_hook.sh` to change:
|
||||
|
||||
- **`SAVE_INTERVAL=15`** — How many human messages between saves. Lower = more frequent saves, higher = less interruption.
|
||||
- **`STATE_DIR`** — Where hook state is stored (defaults to `~/.mempalace/hook_state/`)
|
||||
- **`MEMPAL_DIR`** — Optional. Set to a conversations directory to auto-run `mempalace mine <dir>` on each save trigger. Leave blank (default) to let the AI handle saving via the block reason message.
|
||||
|
||||
### mempalace CLI
|
||||
|
||||
The relevant commands are:
|
||||
|
||||
```bash
|
||||
mempalace mine <dir> # Mine all files in a directory
|
||||
mempalace mine <dir> --mode convos # Mine conversation transcripts only
|
||||
```
|
||||
|
||||
The hooks resolve the repo root automatically from their own path, so they work regardless of where you install the repo.
|
||||
|
||||
## How It Works (Technical)
|
||||
|
||||
### Save Hook (Stop event)
|
||||
|
||||
```
|
||||
User sends message → AI responds → Claude Code fires Stop hook
|
||||
↓
|
||||
Hook counts human messages in JSONL transcript
|
||||
↓
|
||||
┌─── < 15 since last save ──→ echo "{}" (let AI stop)
|
||||
│
|
||||
└─── ≥ 15 since last save ──→ {"decision": "block", "reason": "save..."}
|
||||
↓
|
||||
AI saves to palace
|
||||
↓
|
||||
AI tries to stop again
|
||||
↓
|
||||
stop_hook_active = true
|
||||
↓
|
||||
Hook sees flag → echo "{}" (let it through)
|
||||
```
|
||||
|
||||
The `stop_hook_active` flag prevents infinite loops: block once → AI saves → tries to stop → flag is true → we let it through.
|
||||
|
||||
### PreCompact Hook
|
||||
|
||||
```
|
||||
Context window getting full → Claude Code fires PreCompact
|
||||
↓
|
||||
Hook ALWAYS blocks
|
||||
↓
|
||||
AI saves everything
|
||||
↓
|
||||
Compaction proceeds
|
||||
```
|
||||
|
||||
No counting needed — compaction always warrants a save.
|
||||
|
||||
## Debugging
|
||||
|
||||
Check the hook log:
|
||||
```bash
|
||||
cat ~/.mempalace/hook_state/hook.log
|
||||
```
|
||||
|
||||
Example output:
|
||||
```
|
||||
[14:30:15] Session abc123: 12 exchanges, 12 since last save
|
||||
[14:35:22] Session abc123: 15 exchanges, 15 since last save
|
||||
[14:35:22] TRIGGERING SAVE at exchange 15
|
||||
[14:40:01] Session abc123: 18 exchanges, 3 since last save
|
||||
```
|
||||
|
||||
## Cost
|
||||
|
||||
**Zero extra tokens.** The hooks are bash scripts that run locally. They don't call any API. The only "cost" is the AI spending a few seconds organizing memories at each checkpoint — and it's doing that with context it already has loaded.
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/bin/bash
|
||||
# MEMPALACE PRE-COMPACT HOOK — Emergency save before compaction
|
||||
#
|
||||
# Claude Code "PreCompact" hook. Fires RIGHT BEFORE the conversation
|
||||
# gets compressed to free up context window space.
|
||||
#
|
||||
# This is the safety net. When compaction happens, the AI loses detailed
|
||||
# context about what was discussed. This hook forces one final save of
|
||||
# EVERYTHING before that happens.
|
||||
#
|
||||
# Unlike the save hook (which triggers every N exchanges), this ALWAYS
|
||||
# blocks — because compaction is always worth saving before.
|
||||
#
|
||||
# === INSTALL ===
|
||||
# Add to .claude/settings.local.json:
|
||||
#
|
||||
# "hooks": {
|
||||
# "PreCompact": [{
|
||||
# "hooks": [{
|
||||
# "type": "command",
|
||||
# "command": "/absolute/path/to/mempal_precompact_hook.sh",
|
||||
# "timeout": 30
|
||||
# }]
|
||||
# }]
|
||||
# }
|
||||
#
|
||||
# For Codex CLI, add to .codex/hooks.json:
|
||||
#
|
||||
# "PreCompact": [{
|
||||
# "type": "command",
|
||||
# "command": "/absolute/path/to/mempal_precompact_hook.sh",
|
||||
# "timeout": 30
|
||||
# }]
|
||||
#
|
||||
# === HOW IT WORKS ===
|
||||
#
|
||||
# Claude Code sends JSON on stdin with:
|
||||
# session_id — unique session identifier
|
||||
#
|
||||
# We always return decision: "block" with a reason telling the AI
|
||||
# to save everything. After the AI saves, compaction proceeds normally.
|
||||
#
|
||||
# === MEMPALACE CLI ===
|
||||
# This repo uses: mempalace mine <dir>
|
||||
# or: mempalace mine <dir> --mode convos
|
||||
# Set MEMPAL_DIR below if you want the hook to auto-ingest before compaction.
|
||||
# Leave blank to rely on the AI's own save instructions.
|
||||
|
||||
STATE_DIR="$HOME/.mempalace/hook_state"
|
||||
mkdir -p "$STATE_DIR"
|
||||
|
||||
# Optional: set to the directory you want auto-ingested before compaction.
|
||||
# Example: MEMPAL_DIR="$HOME/conversations"
|
||||
# Leave empty to skip auto-ingest (AI handles saving via the block reason).
|
||||
MEMPAL_DIR=""
|
||||
|
||||
# Read JSON input from stdin
|
||||
INPUT=$(cat)
|
||||
|
||||
SESSION_ID=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('session_id','unknown'))" 2>/dev/null)
|
||||
|
||||
echo "[$(date '+%H:%M:%S')] PRE-COMPACT triggered for session $SESSION_ID" >> "$STATE_DIR/hook.log"
|
||||
|
||||
# Optional: run mempalace ingest synchronously so memories land before compaction
|
||||
if [ -n "$MEMPAL_DIR" ] && [ -d "$MEMPAL_DIR" ]; then
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
python3 -m mempalace mine "$MEMPAL_DIR" >> "$STATE_DIR/hook.log" 2>&1
|
||||
fi
|
||||
|
||||
# Always block — compaction = save everything
|
||||
cat << 'HOOKJSON'
|
||||
{
|
||||
"decision": "block",
|
||||
"reason": "COMPACTION IMMINENT. Save ALL topics, decisions, quotes, code, and important context from this session to your memory system. Be thorough — after compaction, detailed context will be lost. Organize into appropriate categories. Use verbatim quotes where possible. Save everything, then allow compaction to proceed."
|
||||
}
|
||||
HOOKJSON
|
||||
Executable
+143
@@ -0,0 +1,143 @@
|
||||
#!/bin/bash
|
||||
# MEMPALACE SAVE HOOK — Auto-save every N exchanges
|
||||
#
|
||||
# Claude Code "Stop" hook. After every assistant response:
|
||||
# 1. Counts human messages in the session transcript
|
||||
# 2. Every SAVE_INTERVAL messages, BLOCKS the AI from stopping
|
||||
# 3. Returns a reason telling the AI to save structured diary + palace entries
|
||||
# 4. AI does the save (topics, decisions, code, quotes → organized into palace)
|
||||
# 5. Next Stop fires with stop_hook_active=true → lets AI stop normally
|
||||
#
|
||||
# The AI does the classification — it knows what wing/hall/closet to use
|
||||
# because it has context about the conversation. No regex needed.
|
||||
#
|
||||
# === INSTALL ===
|
||||
# Add to .claude/settings.local.json:
|
||||
#
|
||||
# "hooks": {
|
||||
# "Stop": [{
|
||||
# "matcher": "*",
|
||||
# "hooks": [{
|
||||
# "type": "command",
|
||||
# "command": "/absolute/path/to/mempal_save_hook.sh",
|
||||
# "timeout": 30
|
||||
# }]
|
||||
# }]
|
||||
# }
|
||||
#
|
||||
# For Codex CLI, add to .codex/hooks.json:
|
||||
#
|
||||
# "Stop": [{
|
||||
# "type": "command",
|
||||
# "command": "/absolute/path/to/mempal_save_hook.sh",
|
||||
# "timeout": 30
|
||||
# }]
|
||||
#
|
||||
# === HOW IT WORKS ===
|
||||
#
|
||||
# Claude Code sends JSON on stdin with these fields:
|
||||
# session_id — unique session identifier
|
||||
# stop_hook_active — true if AI is already in a save cycle (prevents infinite loop)
|
||||
# transcript_path — path to the JSONL transcript file
|
||||
#
|
||||
# When we block, Claude Code shows our "reason" to the AI as a system message.
|
||||
# The AI then saves to memory, and when it tries to stop again,
|
||||
# stop_hook_active=true so we let it through. No infinite loop.
|
||||
#
|
||||
# === MEMPALACE CLI ===
|
||||
# This repo uses: mempalace mine <dir>
|
||||
# or: mempalace mine <dir> --mode convos
|
||||
# Set MEMPAL_DIR below if you want the hook to auto-ingest after blocking.
|
||||
# Leave blank to rely on the AI's own save instructions.
|
||||
#
|
||||
# === CONFIGURATION ===
|
||||
|
||||
SAVE_INTERVAL=15 # Save every N human messages (adjust to taste)
|
||||
STATE_DIR="$HOME/.mempalace/hook_state"
|
||||
mkdir -p "$STATE_DIR"
|
||||
|
||||
# Optional: set to the directory you want auto-ingested on each save trigger.
|
||||
# Example: MEMPAL_DIR="$HOME/conversations"
|
||||
# Leave empty to skip auto-ingest (AI handles saving via the block reason).
|
||||
MEMPAL_DIR=""
|
||||
|
||||
# Read JSON input from stdin
|
||||
INPUT=$(cat)
|
||||
|
||||
# Parse fields from Claude Code's JSON
|
||||
SESSION_ID=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('session_id','unknown'))" 2>/dev/null)
|
||||
STOP_HOOK_ACTIVE=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('stop_hook_active', False))" 2>/dev/null)
|
||||
TRANSCRIPT_PATH=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('transcript_path',''))" 2>/dev/null)
|
||||
|
||||
# Expand ~ in path
|
||||
TRANSCRIPT_PATH="${TRANSCRIPT_PATH/#\~/$HOME}"
|
||||
|
||||
# If we're already in a save cycle, let the AI stop normally
|
||||
# This is the infinite-loop prevention: block once → AI saves → tries to stop again → we let it through
|
||||
if [ "$STOP_HOOK_ACTIVE" = "True" ] || [ "$STOP_HOOK_ACTIVE" = "true" ]; then
|
||||
echo "{}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Count human messages in the JSONL transcript
|
||||
if [ -f "$TRANSCRIPT_PATH" ]; then
|
||||
EXCHANGE_COUNT=$(python3 -c "
|
||||
import json, sys
|
||||
count = 0
|
||||
with open('$TRANSCRIPT_PATH') as f:
|
||||
for line in f:
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
msg = entry.get('message', {})
|
||||
if isinstance(msg, dict) and msg.get('role') == 'user':
|
||||
content = msg.get('content', '')
|
||||
# Skip system/command messages — only count real human input
|
||||
if isinstance(content, str) and '<command-message>' in content:
|
||||
continue
|
||||
count += 1
|
||||
except:
|
||||
pass
|
||||
print(count)
|
||||
" 2>/dev/null)
|
||||
else
|
||||
EXCHANGE_COUNT=0
|
||||
fi
|
||||
|
||||
# Track last save point for this session
|
||||
LAST_SAVE_FILE="$STATE_DIR/${SESSION_ID}_last_save"
|
||||
LAST_SAVE=0
|
||||
if [ -f "$LAST_SAVE_FILE" ]; then
|
||||
LAST_SAVE=$(cat "$LAST_SAVE_FILE")
|
||||
fi
|
||||
|
||||
SINCE_LAST=$((EXCHANGE_COUNT - LAST_SAVE))
|
||||
|
||||
# Log for debugging (check ~/.mempalace/hook_state/hook.log)
|
||||
echo "[$(date '+%H:%M:%S')] Session $SESSION_ID: $EXCHANGE_COUNT exchanges, $SINCE_LAST since last save" >> "$STATE_DIR/hook.log"
|
||||
|
||||
# Time to save?
|
||||
if [ "$SINCE_LAST" -ge "$SAVE_INTERVAL" ] && [ "$EXCHANGE_COUNT" -gt 0 ]; then
|
||||
# Update last save point
|
||||
echo "$EXCHANGE_COUNT" > "$LAST_SAVE_FILE"
|
||||
|
||||
echo "[$(date '+%H:%M:%S')] TRIGGERING SAVE at exchange $EXCHANGE_COUNT" >> "$STATE_DIR/hook.log"
|
||||
|
||||
# Optional: run mempalace ingest in background if MEMPAL_DIR is set
|
||||
if [ -n "$MEMPAL_DIR" ] && [ -d "$MEMPAL_DIR" ]; then
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
python3 -m mempalace mine "$MEMPAL_DIR" >> "$STATE_DIR/hook.log" 2>&1 &
|
||||
fi
|
||||
|
||||
# Block the AI and tell it to save
|
||||
# The "reason" becomes a system message the AI sees and acts on
|
||||
cat << 'HOOKJSON'
|
||||
{
|
||||
"decision": "block",
|
||||
"reason": "AUTO-SAVE checkpoint. Save key topics, decisions, quotes, and code from this session to your memory system. Organize into appropriate categories. Use verbatim quotes where possible. Continue conversation after saving."
|
||||
}
|
||||
HOOKJSON
|
||||
else
|
||||
# Not time yet — let the AI stop normally
|
||||
echo "{}"
|
||||
fi
|
||||
Reference in New Issue
Block a user