Skip to content

Commit 045023f

Browse files
fix: save hook auto-mines transcript without MEMPAL_DIR (#840)
TDD: test written first, failed, then fixed. Problem: save hook says "saved in background" but MEMPAL_DIR defaults to empty, so nothing actually mines. Users get no auto-save despite the hook firing every 15 messages. Fix: use TRANSCRIPT_PATH (received from Claude Code in the hook's JSON input) to discover the session directory. Mine that directory automatically. MEMPAL_DIR is still supported as override but no longer required. Also fixed: bare python3 → $(command -v python3) for nohup safety. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 52392ad commit 045023f

2 files changed

Lines changed: 81 additions & 4 deletions

File tree

hooks/mempal_save_hook.sh

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,20 @@ if [ "$SINCE_LAST" -ge "$SAVE_INTERVAL" ] && [ "$EXCHANGE_COUNT" -gt 0 ]; then
133133

134134
echo "[$(date '+%H:%M:%S')] TRIGGERING SAVE at exchange $EXCHANGE_COUNT" >> "$STATE_DIR/hook.log"
135135

136-
# Optional: run mempalace ingest in background if MEMPAL_DIR is set
136+
# Auto-mine the transcript. Two paths:
137+
# 1. TRANSCRIPT_PATH (from Claude Code) — mine the directory it lives in
138+
# 2. MEMPAL_DIR (user-configured) — mine that directory
139+
# At least one should work. If neither is set, nothing mines.
140+
PYTHON="$(command -v python3)"
141+
MINE_DIR=""
142+
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
143+
MINE_DIR="$(dirname "$TRANSCRIPT_PATH")"
144+
fi
137145
if [ -n "$MEMPAL_DIR" ] && [ -d "$MEMPAL_DIR" ]; then
138-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
139-
REPO_DIR="$(dirname "$SCRIPT_DIR")"
140-
python3 -m mempalace mine "$MEMPAL_DIR" >> "$STATE_DIR/hook.log" 2>&1 &
146+
MINE_DIR="$MEMPAL_DIR"
147+
fi
148+
if [ -n "$MINE_DIR" ]; then
149+
"$PYTHON" -m mempalace mine "$MINE_DIR" >> "$STATE_DIR/hook.log" 2>&1 &
141150
fi
142151

143152
# Notify the AI that a checkpoint happened — but do NOT ask it to write

tests/test_save_hook_mines.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""TDD: save hook must actually mine conversations without MEMPAL_DIR.
2+
3+
The save hook should auto-discover the conversation transcript and mine it
4+
without the user needing to set MEMPAL_DIR. Currently MEMPAL_DIR defaults
5+
to empty, which means the mining block is skipped and nothing is saved
6+
despite the hook telling the agent "saved in background."
7+
8+
Written BEFORE the fix.
9+
"""
10+
11+
import os
12+
13+
14+
class TestSaveHookAutoMines:
15+
"""The save hook must mine the active transcript automatically."""
16+
17+
def test_hook_mines_transcript_path(self):
18+
"""The hook receives TRANSCRIPT_PATH from Claude Code.
19+
It should use that to mine the conversation, not depend on MEMPAL_DIR."""
20+
hook_path = os.path.join(
21+
os.path.dirname(os.path.dirname(__file__)),
22+
"hooks",
23+
"mempal_save_hook.sh",
24+
)
25+
src = open(hook_path).read()
26+
27+
# The hook ALREADY receives TRANSCRIPT_PATH in the JSON input.
28+
# It should use this to mine the current session's transcript
29+
# regardless of whether MEMPAL_DIR is set.
30+
# The hook must have a path that uses TRANSCRIPT_PATH to determine
31+
# what to mine, separate from the MEMPAL_DIR path.
32+
uses_transcript = "TRANSCRIPT_PATH" in src
33+
has_mine = "mempalace mine" in src
34+
# TRANSCRIPT_PATH must appear in the mining logic, not just the parse block
35+
transcript_drives_mine = "MINE_DIR" in src and "dirname" in src and "TRANSCRIPT_PATH" in src
36+
37+
assert uses_transcript and has_mine and transcript_drives_mine, (
38+
"Save hook only mines when MEMPAL_DIR is set (defaults to empty). "
39+
"The hook receives TRANSCRIPT_PATH from Claude Code — it should "
40+
"mine that file automatically so conversations are saved without "
41+
"the user setting an env var. Currently the hook says 'saved in "
42+
"background' but nothing actually saves."
43+
)
44+
45+
def test_mempal_dir_default_not_empty(self):
46+
"""If MEMPAL_DIR is still used, it should have a sensible default,
47+
not an empty string that silently disables mining."""
48+
hook_path = os.path.join(
49+
os.path.dirname(os.path.dirname(__file__)),
50+
"hooks",
51+
"mempal_save_hook.sh",
52+
)
53+
src = open(hook_path).read()
54+
55+
# Check if MEMPAL_DIR defaults to empty
56+
has_empty_default = 'MEMPAL_DIR=""' in src
57+
58+
# If it defaults to empty, mining is silently disabled
59+
if has_empty_default:
60+
# There must be an alternative mining path that doesn't need MEMPAL_DIR
61+
has_alternative = (
62+
src.count("mempalace mine") > 1
63+
or "TRANSCRIPT_PATH" in src.split("mempalace mine")[0]
64+
)
65+
assert has_alternative, (
66+
'MEMPAL_DIR defaults to "" which silently disables mining. '
67+
"Either set a default path or add transcript-based mining."
68+
)

0 commit comments

Comments
 (0)