claude-code-cache-fix 2.0.3 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/tools/MANUAL-COMPACT.md +138 -0
- package/tools/manual-compact.sh +212 -0
package/package.json
CHANGED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# manual-compact.sh — Manual Compaction for 1M Context Hack Sessions
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
When using the 1M context window hack (`DISABLE_COMPACT=1` + `CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000`), the `/compact` command is disabled by CC. This tool provides a manual compaction alternative: extract the conversation, summarize it via Claude, and restore context after `/clear`.
|
|
6
|
+
|
|
7
|
+
**This tool is specifically for sessions running the 1M hack.** If you have `/compact` available, use that instead — it's built-in, integrated, and handles the full compaction lifecycle automatically.
|
|
8
|
+
|
|
9
|
+
## How It Works
|
|
10
|
+
|
|
11
|
+
1. Extracts conversation turns from the session JSONL transcript
|
|
12
|
+
2. Splits turns into three weighted segments:
|
|
13
|
+
- **Foundational** (first 20%) — truncated to 200 chars each
|
|
14
|
+
- **Working** (middle 40%) — truncated to 400 chars each
|
|
15
|
+
- **Active** (last 40%) — preserved up to 2000 chars each
|
|
16
|
+
3. Sends the weighted extract to Claude Sonnet for summarization
|
|
17
|
+
4. Produces a structured summary optimized for agent handoff
|
|
18
|
+
|
|
19
|
+
The weighting ensures recent active work (the part you're most likely to need) gets full detail, while earlier completed work is compressed.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# By project directory (recommended) — auto-finds the most recent session
|
|
25
|
+
manual-compact.sh ~/git_repos/myproject
|
|
26
|
+
|
|
27
|
+
# By project directory with user context
|
|
28
|
+
manual-compact.sh ~/git_repos/myproject /tmp/context.txt
|
|
29
|
+
|
|
30
|
+
# By direct JSONL path (if you know the exact session)
|
|
31
|
+
manual-compact.sh ~/.claude/projects/-home-user-git-repos-myproject/abc123.jsonl
|
|
32
|
+
|
|
33
|
+
# By direct JSONL path with user context
|
|
34
|
+
manual-compact.sh ~/.claude/projects/-home-user-git-repos-myproject/abc123.jsonl /tmp/context.txt
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
When you pass a project directory, the tool:
|
|
38
|
+
1. Converts it to CC's internal project path format
|
|
39
|
+
2. Finds the most recently modified session JSONL
|
|
40
|
+
3. Shows you the session details (modified date, size)
|
|
41
|
+
4. **Asks for confirmation** before proceeding
|
|
42
|
+
|
|
43
|
+
### WARNING: Wrong Session = Wrong Context
|
|
44
|
+
|
|
45
|
+
**If you select the wrong session JSONL, the summary will be from a completely different conversation.** Loading that summary after `/clear` will inject false context — the agent will confidently act on information from another session, another project, or another agent's work.
|
|
46
|
+
|
|
47
|
+
Always:
|
|
48
|
+
- Verify the session timestamp matches your active session
|
|
49
|
+
- Review the summary output before feeding it to an agent
|
|
50
|
+
- When in doubt, check the last few lines of the JSONL to confirm it's the right conversation
|
|
51
|
+
|
|
52
|
+
### Example: Basic Compaction
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
./tools/manual-compact.sh ~/git_repos/kanfei-nowcast-e3b
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
Project directory: /home/manager/git_repos/kanfei_nowcast_e3b
|
|
60
|
+
Auto-detected session: db11f377-4ca8-4fc3-9b6d-1069da58c1b2.jsonl
|
|
61
|
+
Modified: 2026-04-19 13:26:42
|
|
62
|
+
Size: 4.8M
|
|
63
|
+
|
|
64
|
+
Is this the correct session? [Y/n]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Output: `/tmp/db11f377-...-compact-summary.txt`
|
|
68
|
+
|
|
69
|
+
### Example: With User Context
|
|
70
|
+
|
|
71
|
+
If there's specific context you know the summary might miss:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
echo "The MR2 OOM debugging took 3 days. The PR #75 architectural recommendation
|
|
75
|
+
was max(dualpol_lr, hail_lr) for correlation grouping." > /tmp/context.txt
|
|
76
|
+
|
|
77
|
+
./tools/manual-compact.sh ~/git_repos/kanfei-nowcast-e3b /tmp/context.txt
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The user context is injected into the summarization prompt, ensuring those details appear in the output.
|
|
81
|
+
|
|
82
|
+
### Restoring Context After /clear
|
|
83
|
+
|
|
84
|
+
In the CC session:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
/clear
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Then as your first message:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
Read /tmp/<session-id>-compact-summary.txt for context on where we left off.
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Limitations
|
|
97
|
+
|
|
98
|
+
### This tool is a workaround, not a replacement for /compact
|
|
99
|
+
|
|
100
|
+
- `/compact` operates inside CC with full access to the internal message array, system prompt, tool schemas, and session state. This tool only sees the JSONL transcript, which is a subset.
|
|
101
|
+
- `/compact` preserves CC's internal state (tool registration, MCP connections, plugin state). This tool + `/clear` resets all of that. The agent must re-establish any stateful connections.
|
|
102
|
+
- `/compact` is atomic — one command, seamless continuation. This tool requires `/clear` + paste, which is a hard context boundary.
|
|
103
|
+
|
|
104
|
+
### Summary fidelity
|
|
105
|
+
|
|
106
|
+
Tested at ~95% fidelity for active work resumption, ~70% for broader project context. Gaps typically include:
|
|
107
|
+
|
|
108
|
+
- **Operational debugging history** — multi-day debugging sagas compress away
|
|
109
|
+
- **Timeline information** — the summary doesn't indicate when things happened or how long they took
|
|
110
|
+
- **Depth of architectural discussions** — detailed technical recommendations get compressed to one-liners
|
|
111
|
+
- **Background process context** — overnight watchers, cron monitoring, polling patterns
|
|
112
|
+
|
|
113
|
+
Use the user context file to fill known gaps.
|
|
114
|
+
|
|
115
|
+
### Token cost
|
|
116
|
+
|
|
117
|
+
The summarization call costs tokens against your Q5h quota. At ~50K extract tokens through Sonnet, expect ~1-2% Q5h per compaction. This is comparable to what `/compact` costs.
|
|
118
|
+
|
|
119
|
+
### Requires Claude Sonnet access
|
|
120
|
+
|
|
121
|
+
The tool uses `claude --print --model claude-sonnet-4-6` for summarization. Sonnet is used instead of Opus to minimize Q5h impact. If Sonnet is unavailable, change the model in the script.
|
|
122
|
+
|
|
123
|
+
## Why the 1M Hack Disables /compact
|
|
124
|
+
|
|
125
|
+
The 1M context hack works by setting `DISABLE_COMPACT=1`, which CC reads as "disable all compaction." CC's code uses a single env var to control both:
|
|
126
|
+
- The context window calculation (`ff()` returns 1M when `DISABLE_COMPACT=1`)
|
|
127
|
+
- The `/compact` command availability (`isEnabled: () => !DISABLE_COMPACT`)
|
|
128
|
+
|
|
129
|
+
These are coupled in CC's source — there is no way to get 1M context AND `/compact` simultaneously without CC code changes. The coupling is in the CC binary, not in our interceptor.
|
|
130
|
+
|
|
131
|
+
We attempted to toggle `DISABLE_COMPACT` via the interceptor (set during API calls, unset between turns), but CC registers available commands at startup before any API call, so the toggle cannot re-enable `/compact` after session start.
|
|
132
|
+
|
|
133
|
+
## Requirements
|
|
134
|
+
|
|
135
|
+
- Claude Code v2.1.112 (the last Node.js version — v2.1.113+ uses Bun)
|
|
136
|
+
- The cache-fix interceptor loaded via `NODE_OPTIONS=--import`
|
|
137
|
+
- `DISABLE_COMPACT=1` and `CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000` set
|
|
138
|
+
- `claude` CLI available in PATH (used for summarization)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# manual-compact.sh — Generate a compaction summary for a CC session
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# manual-compact.sh <project-dir-or-session-jsonl> [user-context-file]
|
|
6
|
+
#
|
|
7
|
+
# Accepts either:
|
|
8
|
+
# - A project working directory (e.g. ~/git_repos/myproject)
|
|
9
|
+
# → auto-finds the most recent session JSONL
|
|
10
|
+
# - A direct path to a session JSONL file
|
|
11
|
+
#
|
|
12
|
+
# Produces a summary at /tmp/<session-id>-compact-summary.txt
|
|
13
|
+
# that can be pasted or referenced after /clear.
|
|
14
|
+
#
|
|
15
|
+
# The optional user-context-file is additional context the user wants
|
|
16
|
+
# preserved in the summary (equivalent to /compact <instructions>).
|
|
17
|
+
#
|
|
18
|
+
# WARNING: Using the wrong session JSONL will produce a summary from
|
|
19
|
+
# a DIFFERENT conversation. Loading that into your session after /clear
|
|
20
|
+
# will inject completely wrong context. Always verify the output before
|
|
21
|
+
# feeding it to an agent.
|
|
22
|
+
|
|
23
|
+
set -euo pipefail
|
|
24
|
+
|
|
25
|
+
if [ $# -lt 1 ]; then
|
|
26
|
+
echo "Usage: $0 <project-dir-or-session-jsonl> [user-context-file]"
|
|
27
|
+
echo ""
|
|
28
|
+
echo "Generates a compaction summary from a CC session JSONL transcript."
|
|
29
|
+
echo "After /clear, reference the output file to restore context."
|
|
30
|
+
echo ""
|
|
31
|
+
echo "Arguments:"
|
|
32
|
+
echo " <project-dir> Working directory of the CC session (e.g. ~/git_repos/myproject)"
|
|
33
|
+
echo " Auto-detects the most recent session JSONL."
|
|
34
|
+
echo " <session-jsonl> Direct path to a session JSONL file."
|
|
35
|
+
echo " [user-context] Optional file with additional context to preserve."
|
|
36
|
+
echo ""
|
|
37
|
+
echo "WARNING: Verify the output summary before loading it after /clear."
|
|
38
|
+
echo " A wrong session JSONL = wrong context = confused agent."
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
INPUT="$1"
|
|
43
|
+
USER_CONTEXT_FILE="${2:-}"
|
|
44
|
+
|
|
45
|
+
# Determine if input is a directory or a JSONL file
|
|
46
|
+
if [ -d "$INPUT" ]; then
|
|
47
|
+
# Convert project directory to CC's project path format
|
|
48
|
+
REAL_PATH=$(realpath "$INPUT")
|
|
49
|
+
# CC replaces / with - and prepends -
|
|
50
|
+
PROJECT_KEY=$(echo "$REAL_PATH" | sed 's|/|-|g')
|
|
51
|
+
PROJECT_DIR="$HOME/.claude/projects/${PROJECT_KEY}"
|
|
52
|
+
|
|
53
|
+
if [ ! -d "$PROJECT_DIR" ]; then
|
|
54
|
+
echo "ERROR: No CC project found for directory: $INPUT"
|
|
55
|
+
echo " Expected: $PROJECT_DIR"
|
|
56
|
+
echo ""
|
|
57
|
+
echo "Available projects:"
|
|
58
|
+
ls -d ~/.claude/projects/*/ 2>/dev/null | head -10
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# Find the most recent JSONL (exclude subdirectories like subagents/)
|
|
63
|
+
JSONL=$(find "$PROJECT_DIR" -maxdepth 1 -name "*.jsonl" -type f -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2-)
|
|
64
|
+
|
|
65
|
+
if [ -z "$JSONL" ]; then
|
|
66
|
+
echo "ERROR: No session JSONL found in: $PROJECT_DIR"
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
echo "Project directory: $INPUT"
|
|
71
|
+
echo "Auto-detected session: $(basename "$JSONL")"
|
|
72
|
+
echo " Modified: $(stat -c '%y' "$JSONL" | cut -d'.' -f1)"
|
|
73
|
+
echo " Size: $(du -h "$JSONL" | cut -f1)"
|
|
74
|
+
echo ""
|
|
75
|
+
read -p "Is this the correct session? [Y/n] " CONFIRM
|
|
76
|
+
if [[ "${CONFIRM:-Y}" =~ ^[Nn] ]]; then
|
|
77
|
+
echo ""
|
|
78
|
+
echo "Available sessions in $PROJECT_DIR:"
|
|
79
|
+
ls -lt "$PROJECT_DIR"/*.jsonl 2>/dev/null | awk '{print " " $6, $7, $8, $NF}'
|
|
80
|
+
echo ""
|
|
81
|
+
echo "Re-run with the specific JSONL path."
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
elif [ -f "$INPUT" ]; then
|
|
85
|
+
JSONL="$INPUT"
|
|
86
|
+
else
|
|
87
|
+
echo "ERROR: $INPUT is not a directory or file."
|
|
88
|
+
exit 1
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
SESSION_ID=$(basename "$JSONL" .jsonl)
|
|
92
|
+
OUTPUT="/tmp/${SESSION_ID}-compact-summary.txt"
|
|
93
|
+
EXTRACT="/tmp/${SESSION_ID}-conv-extract.txt"
|
|
94
|
+
|
|
95
|
+
echo ""
|
|
96
|
+
echo "Extracting conversation from: $JSONL"
|
|
97
|
+
|
|
98
|
+
# Extract conversation turns, keeping more detail for recent turns
|
|
99
|
+
python3 << PYEOF
|
|
100
|
+
import json, sys
|
|
101
|
+
|
|
102
|
+
conversation = []
|
|
103
|
+
with open("$JSONL") as f:
|
|
104
|
+
for line in f:
|
|
105
|
+
try:
|
|
106
|
+
d = json.loads(line.strip())
|
|
107
|
+
if d.get('type') == 'user':
|
|
108
|
+
msg = d.get('message', {})
|
|
109
|
+
content = msg.get('content', '')
|
|
110
|
+
if isinstance(content, str) and len(content.strip()) > 0:
|
|
111
|
+
if content.startswith('<local-command') or content.startswith('<command-name>'):
|
|
112
|
+
continue
|
|
113
|
+
conversation.append(('user', content))
|
|
114
|
+
elif isinstance(content, list):
|
|
115
|
+
texts = []
|
|
116
|
+
for b in content:
|
|
117
|
+
if isinstance(b, dict):
|
|
118
|
+
if b.get('type') == 'text' and b.get('text'):
|
|
119
|
+
t = b['text']
|
|
120
|
+
if not t.startswith('<local-command') and not t.startswith('<command-name>'):
|
|
121
|
+
texts.append(t)
|
|
122
|
+
elif b.get('type') == 'tool_result' and b.get('content'):
|
|
123
|
+
c = b['content']
|
|
124
|
+
if isinstance(c, str):
|
|
125
|
+
texts.append(c)
|
|
126
|
+
elif isinstance(c, list):
|
|
127
|
+
for tb in c:
|
|
128
|
+
if isinstance(tb, dict) and tb.get('text'):
|
|
129
|
+
texts.append(tb['text'])
|
|
130
|
+
if texts:
|
|
131
|
+
conversation.append(('user', ' '.join(texts)))
|
|
132
|
+
elif d.get('type') == 'assistant':
|
|
133
|
+
msg = d.get('message', {})
|
|
134
|
+
content = msg.get('content', [])
|
|
135
|
+
if isinstance(content, list):
|
|
136
|
+
texts = [b.get('text', '') for b in content if isinstance(b, dict) and b.get('type') == 'text' and b.get('text')]
|
|
137
|
+
if texts:
|
|
138
|
+
conversation.append(('assistant', ' '.join(texts)))
|
|
139
|
+
except:
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
total = len(conversation)
|
|
143
|
+
if total == 0:
|
|
144
|
+
print("No conversation found.", file=sys.stderr)
|
|
145
|
+
sys.exit(1)
|
|
146
|
+
|
|
147
|
+
# Split into three segments with different detail levels:
|
|
148
|
+
# - First 20%: truncate to 200 chars each (foundational context)
|
|
149
|
+
# - Middle 40%: truncate to 400 chars each (working context)
|
|
150
|
+
# - Last 40%: full text up to 2000 chars each (active work — most important)
|
|
151
|
+
seg1_end = int(total * 0.2)
|
|
152
|
+
seg2_end = int(total * 0.6)
|
|
153
|
+
|
|
154
|
+
with open("$EXTRACT", 'w') as f:
|
|
155
|
+
f.write("=== FOUNDATIONAL CONTEXT (early session) ===\n\n")
|
|
156
|
+
for role, text in conversation[:seg1_end]:
|
|
157
|
+
f.write(f"[{role}]: {text[:200]}\n\n")
|
|
158
|
+
|
|
159
|
+
f.write("\n=== WORKING CONTEXT (mid session) ===\n\n")
|
|
160
|
+
for role, text in conversation[seg1_end:seg2_end]:
|
|
161
|
+
f.write(f"[{role}]: {text[:400]}\n\n")
|
|
162
|
+
|
|
163
|
+
f.write("\n=== ACTIVE WORK (recent — preserve in full detail) ===\n\n")
|
|
164
|
+
for role, text in conversation[seg2_end:]:
|
|
165
|
+
f.write(f"[{role}]: {text[:2000]}\n\n")
|
|
166
|
+
|
|
167
|
+
import os
|
|
168
|
+
size = os.path.getsize("$EXTRACT")
|
|
169
|
+
print(f"Extracted {total} turns ({size:,} bytes, ~{size//4:,} est. tokens)")
|
|
170
|
+
print(f" Foundational: {seg1_end} turns (truncated to 200 chars)")
|
|
171
|
+
print(f" Working: {seg2_end - seg1_end} turns (truncated to 400 chars)")
|
|
172
|
+
print(f" Active: {total - seg2_end} turns (up to 2000 chars)")
|
|
173
|
+
PYEOF
|
|
174
|
+
|
|
175
|
+
# Build the summarization prompt
|
|
176
|
+
USER_CONTEXT=""
|
|
177
|
+
if [ -n "$USER_CONTEXT_FILE" ] && [ -f "$USER_CONTEXT_FILE" ]; then
|
|
178
|
+
USER_CONTEXT=$(cat "$USER_CONTEXT_FILE")
|
|
179
|
+
echo "User context loaded from: $USER_CONTEXT_FILE"
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
PROMPT="Summarize this conversation for context continuity after a /clear.
|
|
183
|
+
|
|
184
|
+
CRITICAL PRIORITIES (in order):
|
|
185
|
+
1. ACTIVE WORK STATE — What is the agent doing RIGHT NOW? What branch, what uncommitted changes, what task is in progress, what was the last action taken? This is the most important section. Be precise about exactly where things stand — do not understate progress.
|
|
186
|
+
2. RECENT DECISIONS — Key decisions made in the last ~20% of the conversation and their rationale.
|
|
187
|
+
3. PENDING NEXT STEPS — What was about to happen next? What was queued?
|
|
188
|
+
4. COMPLETED WORK — PRs merged, issues closed, features shipped. Brief — the git history has the details.
|
|
189
|
+
5. FOUNDATIONAL CONTEXT — Agent identity, repo location, key collaborators, infrastructure. Brief.
|
|
190
|
+
|
|
191
|
+
FORMAT: Use headers and bullet points. Be specific about file paths, branch names, commit SHAs, function names. The agent reading this will have zero prior context — every detail that matters must be explicit.
|
|
192
|
+
|
|
193
|
+
DO NOT understate progress on in-flight work. If the last 20% of the conversation shows implementation was done, say it was done — do not say 'investigation started'."
|
|
194
|
+
|
|
195
|
+
if [ -n "$USER_CONTEXT" ]; then
|
|
196
|
+
PROMPT="$PROMPT
|
|
197
|
+
|
|
198
|
+
ADDITIONAL USER CONTEXT TO PRESERVE:
|
|
199
|
+
$USER_CONTEXT"
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
echo ""
|
|
203
|
+
echo "Sending to Claude for summarization..."
|
|
204
|
+
|
|
205
|
+
cat "$EXTRACT" | claude --print --model claude-sonnet-4-6 "$PROMPT" > "$OUTPUT" 2>/dev/null
|
|
206
|
+
|
|
207
|
+
SIZE=$(wc -c < "$OUTPUT")
|
|
208
|
+
echo ""
|
|
209
|
+
echo "Summary generated: $OUTPUT ($SIZE bytes)"
|
|
210
|
+
echo ""
|
|
211
|
+
echo "To restore context after /clear:"
|
|
212
|
+
echo " Read $OUTPUT for context on where we left off."
|