claude-code-cache-fix 2.0.3 → 2.0.5
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/preload.mjs +41 -9
- package/tools/MANUAL-COMPACT.md +138 -0
- package/tools/manual-compact.sh +212 -0
package/package.json
CHANGED
package/preload.mjs
CHANGED
|
@@ -590,7 +590,17 @@ function isBookkeepingReminder(text) {
|
|
|
590
590
|
// correctly.
|
|
591
591
|
// --------------------------------------------------------------------------
|
|
592
592
|
|
|
593
|
-
|
|
593
|
+
// Detected per-request from existing markers. Default 1h; downgraded to 5m
|
|
594
|
+
// if any existing block already carries ttl="5m" (Q5h=100% tier).
|
|
595
|
+
// The API rejects 1h markers after 5m markers, so all injected markers
|
|
596
|
+
// must match the lowest existing tier.
|
|
597
|
+
let _detectedTtlTier = "1h";
|
|
598
|
+
|
|
599
|
+
function getCanonicalMarker() {
|
|
600
|
+
return { type: "ephemeral", ttl: _detectedTtlTier };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const CACHE_CONTROL_CANONICAL_MARKER_LEGACY = { type: "ephemeral", ttl: "1h" };
|
|
594
604
|
|
|
595
605
|
/**
|
|
596
606
|
* Strip every cache_control marker from a single user message's content
|
|
@@ -777,7 +787,9 @@ const CACHE_CONTROL_STICKY_DIR = join(homedir(), ".claude", "cache-fix-state");
|
|
|
777
787
|
// CC uses 1 on system[2] + cache_control_normalize places 1 on last user msg = 2 reserved.
|
|
778
788
|
// Sticky can use at most 2 historical positions to stay within the 4-marker cap.
|
|
779
789
|
const CACHE_CONTROL_STICKY_MAX_POSITIONS = 2;
|
|
780
|
-
|
|
790
|
+
function getCacheControlStickyDefaultMarker() {
|
|
791
|
+
return { type: "ephemeral", ttl: _detectedTtlTier };
|
|
792
|
+
}
|
|
781
793
|
|
|
782
794
|
/**
|
|
783
795
|
* Build the absolute state-file path for a given project key. Exported so
|
|
@@ -850,7 +862,7 @@ function readCacheControlStickyState(key) {
|
|
|
850
862
|
marker:
|
|
851
863
|
p.marker && typeof p.marker === "object" && typeof p.marker.type === "string"
|
|
852
864
|
? { ...p.marker }
|
|
853
|
-
: { ...
|
|
865
|
+
: { ...getCacheControlStickyDefaultMarker() },
|
|
854
866
|
});
|
|
855
867
|
}
|
|
856
868
|
return { version: 1, positions };
|
|
@@ -1800,6 +1812,24 @@ globalThis.fetch = async function (url, options) {
|
|
|
1800
1812
|
debugLog("CACHE_FIX_DISABLED=1 — all bug fixes bypassed, monitoring active");
|
|
1801
1813
|
}
|
|
1802
1814
|
|
|
1815
|
+
// Detect existing TTL tier from the payload. If any block already has
|
|
1816
|
+
// ttl="5m" (Q5h=100% tier), all injected markers must use 5m too —
|
|
1817
|
+
// the API rejects 1h after 5m in processing order (tools → system → messages).
|
|
1818
|
+
_detectedTtlTier = "1h";
|
|
1819
|
+
const allBlocks = [
|
|
1820
|
+
...(Array.isArray(payload.system) ? payload.system : []),
|
|
1821
|
+
...(Array.isArray(payload.messages) ? payload.messages.flatMap(m => Array.isArray(m.content) ? m.content : []) : []),
|
|
1822
|
+
];
|
|
1823
|
+
for (const block of allBlocks) {
|
|
1824
|
+
if (block?.cache_control?.ttl === "5m") {
|
|
1825
|
+
_detectedTtlTier = "5m";
|
|
1826
|
+
break;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
if (_detectedTtlTier === "5m") {
|
|
1830
|
+
debugLog("TTL TIER DETECT: existing 5m markers found — all injected markers will use 5m");
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1803
1833
|
debugLog("--- API call to", urlStr);
|
|
1804
1834
|
debugLog("message count:", payload.messages?.length);
|
|
1805
1835
|
|
|
@@ -2350,15 +2380,15 @@ globalThis.fetch = async function (url, options) {
|
|
|
2350
2380
|
const existingCC = targetBlock?.cache_control;
|
|
2351
2381
|
const canonicalAlreadyCorrect =
|
|
2352
2382
|
existingCC &&
|
|
2353
|
-
existingCC.type ===
|
|
2354
|
-
existingCC.ttl ===
|
|
2383
|
+
existingCC.type === getCanonicalMarker().type &&
|
|
2384
|
+
existingCC.ttl === getCanonicalMarker().ttl;
|
|
2355
2385
|
|
|
2356
2386
|
if (!(canonicalAlreadyCorrect && countUserCacheControlMarkers(payload) === 1)) {
|
|
2357
2387
|
// Strip all markers from user messages, then place canonical.
|
|
2358
2388
|
for (const msg of payload.messages) stripCacheControlMarkers(msg);
|
|
2359
2389
|
const tm = payload.messages[targetMsgIdx];
|
|
2360
2390
|
const newContent = tm.content.slice();
|
|
2361
|
-
newContent[targetBlockIdx] = { ...newContent[targetBlockIdx], cache_control: { ...
|
|
2391
|
+
newContent[targetBlockIdx] = { ...newContent[targetBlockIdx], cache_control: { ...getCanonicalMarker() } };
|
|
2362
2392
|
payload.messages[targetMsgIdx] = { ...tm, content: newContent };
|
|
2363
2393
|
ccMutated = true;
|
|
2364
2394
|
}
|
|
@@ -2423,7 +2453,8 @@ globalThis.fetch = async function (url, options) {
|
|
|
2423
2453
|
debugLog(`SKIPPED: TTL injection (${requestType} set to 'none' — pass-through)`);
|
|
2424
2454
|
recordFixResult("ttl", "skipped");
|
|
2425
2455
|
} else {
|
|
2426
|
-
|
|
2456
|
+
// Respect detected tier: if existing blocks have 5m, never inject 1h
|
|
2457
|
+
const ttlParam = ttlValue === "5m" || _detectedTtlTier === "5m" ? "5m" : "1h";
|
|
2427
2458
|
let ttlInjected = 0;
|
|
2428
2459
|
payload.system = payload.system.map((block) => {
|
|
2429
2460
|
if (block.cache_control?.type === "ephemeral" && !block.cache_control.ttl) {
|
|
@@ -2835,7 +2866,8 @@ export {
|
|
|
2835
2866
|
isBookkeepingReminder,
|
|
2836
2867
|
stripCacheControlMarkers,
|
|
2837
2868
|
countUserCacheControlMarkers,
|
|
2838
|
-
|
|
2869
|
+
CACHE_CONTROL_CANONICAL_MARKER_LEGACY,
|
|
2870
|
+
getCanonicalMarker,
|
|
2839
2871
|
normalizeToolUseInputsInBody,
|
|
2840
2872
|
computeStickyMessageHash,
|
|
2841
2873
|
cacheControlStickyStatePath,
|
|
@@ -2844,6 +2876,6 @@ export {
|
|
|
2844
2876
|
readCacheControlStickyState,
|
|
2845
2877
|
writeCacheControlStickyState,
|
|
2846
2878
|
CACHE_CONTROL_STICKY_MAX_POSITIONS,
|
|
2847
|
-
|
|
2879
|
+
getCacheControlStickyDefaultMarker,
|
|
2848
2880
|
_pinnedBlocks, // exported so tests can reset between runs
|
|
2849
2881
|
};
|
|
@@ -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."
|