-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpre-compact.sh
More file actions
executable file
·147 lines (119 loc) · 6.01 KB
/
Copy pathpre-compact.sh
File metadata and controls
executable file
·147 lines (119 loc) · 6.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#!/bin/bash
# pre-compact.sh — Runs on PreCompact to generate the recall transcript.
# Parses the session transcript and condenses if too large (single Sonnet call).
# Writes to both /tmp/ (for manual /recall) and .claude/recall-context.md
# (for automatic injection via CLAUDE.md @-reference after compaction).
#
# Output targets 15-20K tokens (~10% of Claude Code's 200K context window).
#
# Hook input (stdin JSON): { "session_id": "...", "cwd": "...", "trigger": "auto|manual", ... }
# Hook output: None (PreCompact stdout is not injected into context).
# Recursion guard: if claude -p is called for condensation, it might trigger
# its own compaction, which would fire this hook again. Prevent infinite loops.
if [ -n "$RECALL_HOOK_ACTIVE" ]; then
exit 0
fi
export RECALL_HOOK_ACTIVE=1
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INPUT=$(cat)
SESSION_ID=$(echo "$INPUT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('session_id',''))" 2>/dev/null)
CWD=$(echo "$INPUT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('cwd',''))" 2>/dev/null)
if [ -z "$SESSION_ID" ]; then
exit 0
fi
OUTPUT_FILE="/tmp/recall-${SESSION_ID}.md"
# Step 1: Run the parser (stderr goes to log, stdout to file)
python3 "${SCRIPT_DIR}/parse-transcript.py" "$SESSION_ID" --cwd "$CWD" > "$OUTPUT_FILE" 2>/tmp/recall-precompact.log
if [ ! -f "$OUTPUT_FILE" ] || [ ! -s "$OUTPUT_FILE" ]; then
exit 0
fi
# Step 2: Condense if needed (>20K tokens)
# Keeps ~15K tokens of recent exchanges verbatim, summarizes older context
# with a single claude -p --model sonnet call (~30-40s).
CONDENSE_EXIT=0
python3 "${SCRIPT_DIR}/condense-tail.py" split "$OUTPUT_FILE" "$SESSION_ID" 2>>/tmp/recall-precompact.log || CONDENSE_EXIT=$?
if [ "$CONDENSE_EXIT" -eq 0 ]; then
SID_PREFIX="${SESSION_ID:0:8}"
# Must unset CLAUDECODE to allow claude -p from within Claude Code
unset CLAUDECODE
cat "/tmp/recall-older-${SID_PREFIX}.md" | \
claude -p --model sonnet --no-session-persistence \
"$(cat /tmp/recall-prompt-${SID_PREFIX}.txt)" \
> "/tmp/recall-summary-${SID_PREFIX}.md" 2>>/tmp/recall-precompact.log
python3 "${SCRIPT_DIR}/condense-tail.py" combine "$OUTPUT_FILE" "$SESSION_ID" 2>>/tmp/recall-precompact.log
fi
# Step 3: Write to $CWD/.claude/recall-context.md for automatic injection.
# After compaction, Claude Code re-reads CLAUDE.md from disk. If the project's
# CLAUDE.md (or AGENTS.md) contains @.claude/recall-context.md, the recall content
# is pulled into context automatically — no SessionStart stdout needed.
if [ -n "$CWD" ] && [ -d "$CWD/.claude" ]; then
CONTEXT_FILE="$CWD/.claude/recall-context.md"
SID_PREFIX="${SESSION_ID:0:8}"
STATS_FILE="/tmp/recall-stats-${SID_PREFIX}.json"
# Build the header with stats from condense-tail.py
if [ -f "$STATS_FILE" ]; then
# Read all stats in a single python3 call for efficiency
# Values with commas must be quoted for bash eval
eval "$(python3 -c "
import json
d = json.load(open('$STATS_FILE'))
print(f\"CONDENSED={'yes' if d['condensed'] else 'no'}\")
print(f\"ORIGINAL_TOKENS='{d['original_tokens']:,}'\")
print(f\"FINAL_TOKENS='{d.get('final_tokens', d['original_tokens']):,}'\")
print(f\"TOTAL_EXCHANGES={d['total_exchanges']}\")
print(f\"TAIL_EXCHANGES={d['tail_exchanges']}\")
print(f\"VERBATIM_PCT={d['verbatim_pct']}\")
print(f\"SUMMARIZED_PCT={d['summarized_pct']}\")
print(f\"DROPPED_PCT={d['dropped_pct']}\")
print(f\"TAIL_TOKENS='{d.get('tail_tokens', d.get('final_tokens', d['original_tokens'])):,}'\")
" 2>/dev/null)"
if [ "$CONDENSED" = "yes" ]; then
STATS_BLOCK="## Recall Stats
- **Session:** ${SESSION_ID}
- **Original transcript:** ~${ORIGINAL_TOKENS} tokens (${TOTAL_EXCHANGES} exchanges)
- **Condensation:** YES — older context summarized by Sonnet
- Verbatim tail: ${VERBATIM_PCT}% of original (last ${TAIL_EXCHANGES} exchanges, ~${TAIL_TOKENS} tokens)
- Summarized: ${SUMMARIZED_PCT}% of original (older exchanges condensed by Sonnet)
- Dropped: ${DROPPED_PCT}% (earliest exchanges beyond context cap)
- **Final size:** ~${FINAL_TOKENS} tokens
- **Full transcript:** /tmp/recall-${SESSION_ID}.md"
else
STATS_BLOCK="## Recall Stats
- **Session:** ${SESSION_ID}
- **Original transcript:** ~${ORIGINAL_TOKENS} tokens (${TOTAL_EXCHANGES} exchanges)
- **Condensation:** NO — full transcript preserved (100% verbatim)
- **Final size:** ~${FINAL_TOKENS} tokens
- **Full transcript:** /tmp/recall-${SESSION_ID}.md"
fi
# Clean up stats file
rm -f "$STATS_FILE"
else
# Fallback if stats file missing
STATS_BLOCK="## Recall Stats
- **Session:** ${SESSION_ID}
- *(Stats unavailable — condensation status unknown)*"
fi
cat > "$CONTEXT_FILE" << HEADER
<!-- Recall: Pre-compaction transcript (auto-generated, auto-cleaned) -->
${STATS_BLOCK}
NOTE: This file (.claude/recall-context.md) is auto-cleaned on next session start.
The full transcript persists at the path above until system reboot.
INSTRUCTIONS: This is the detailed conversation from before compaction — far richer than
the compaction summary. BEFORE doing anything else — before responding to ANY user message,
before acting on any request — you MUST:
1. Print a status block as the VERY FIRST thing you output:
"Recall loaded: ~X tokens (Y% verbatim, Z% summarized)"
using the RECALL STATS above. If 100% verbatim, say "Recall loaded: ~X tokens (full transcript)".
On the next line, print the full transcript path as a markdown link for transparency:
"Full transcript: [recall-SESSION_ID.md](file:///tmp/recall-SESSION_ID.md)"
This applies whether compaction was automatic OR triggered manually by the user via /compact.
2. UNDERSTAND the conversation arc and identify the last pending task
3. RE-READ files needed for pending work (check FILES TOUCHED section)
4. RE-LOAD skills from the SKILLS LOADED section if needed
5. CONTINUE working on the pending task seamlessly
If no work is pending, briefly summarize what was accomplished and ask what to do next.
---
HEADER
cat "$OUTPUT_FILE" >> "$CONTEXT_FILE"
fi
exit 0