claude-code-cache-fix 1.1.0 → 1.2.0
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/README.md +25 -0
- package/package.json +1 -1
- package/preload.mjs +164 -0
package/README.md
CHANGED
|
@@ -92,6 +92,27 @@ This keeps images in the last 3 user messages and replaces older ones with a tex
|
|
|
92
92
|
|
|
93
93
|
Set to `0` (default) to disable.
|
|
94
94
|
|
|
95
|
+
## Prefix lock (resume cache hit)
|
|
96
|
+
|
|
97
|
+
Even with the block relocation fix, the first API call after `--resume` triggers a full cache rebuild because CC reassembles messages with different system-reminder blocks, changing the prefix bytes. On a 300k token context at Opus rates, that's ~$2.80 per resume.
|
|
98
|
+
|
|
99
|
+
The prefix lock eliminates this by saving the exact `messages[0]` content after all fixes are applied, then replaying it on the next resume to produce a byte-identical prefix.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
export CACHE_FIX_PREFIX_LOCK=1
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Safety guards — the lock only fires when ALL of these match:
|
|
106
|
+
- System prompt hash (same project, no CLAUDE.md changes)
|
|
107
|
+
- Tools hash (no MCP/plugin changes)
|
|
108
|
+
- User message text (same conversation)
|
|
109
|
+
- User content hash (no substantive context changes)
|
|
110
|
+
- Not a post-compaction conversation
|
|
111
|
+
|
|
112
|
+
If any guard fails, the lock skips and falls back to normal behavior. The worst case is a skip — the lock cannot increase costs or cause context loss.
|
|
113
|
+
|
|
114
|
+
Set to `0` (default) to disable.
|
|
115
|
+
|
|
95
116
|
## Monitoring
|
|
96
117
|
|
|
97
118
|
The interceptor includes monitoring for several additional issues identified by the community:
|
|
@@ -131,6 +152,8 @@ Logs are written to `~/.claude/cache-fix-debug.log`. Look for:
|
|
|
131
152
|
- `BUDGET WARNING: tool result chars at N / 200,000 threshold` — approaching budget cap
|
|
132
153
|
- `FALSE RATE LIMIT: synthetic model detected` — client-side false rate limit
|
|
133
154
|
- `GROWTHBOOK FLAGS: {...}` — server-controlled feature flags on first call
|
|
155
|
+
- `PREFIX LOCK: APPLIED — replayed saved messages[0]` — resume cache hit achieved
|
|
156
|
+
- `PREFIX LOCK: skipped — <reason>` — guard prevented lock (expected, safe)
|
|
134
157
|
- `SKIPPED: resume relocation (not a resume or already correct)` — no fix needed
|
|
135
158
|
|
|
136
159
|
### Prefix diff mode
|
|
@@ -150,6 +173,7 @@ Snapshots are saved to `~/.claude/cache-fix-snapshots/` and diff reports are gen
|
|
|
150
173
|
| `CACHE_FIX_DEBUG` | `0` | Enable debug logging to `~/.claude/cache-fix-debug.log` |
|
|
151
174
|
| `CACHE_FIX_PREFIXDIFF` | `0` | Enable prefix snapshot diffing |
|
|
152
175
|
| `CACHE_FIX_IMAGE_KEEP_LAST` | `0` | Keep images in last N user messages (0 = disabled) |
|
|
176
|
+
| `CACHE_FIX_PREFIX_LOCK` | `0` | Replay saved messages[0] on resume for cache hit (0 = disabled) |
|
|
153
177
|
|
|
154
178
|
## Limitations
|
|
155
179
|
|
|
@@ -170,6 +194,7 @@ Snapshots are saved to `~/.claude/cache-fix-snapshots/` and diff reports are gen
|
|
|
170
194
|
## Related research
|
|
171
195
|
|
|
172
196
|
- **[@ArkNill/claude-code-hidden-problem-analysis](https://github.com/ArkNill/claude-code-hidden-problem-analysis)** — Systematic proxy-based analysis of 7 bugs including microcompact, budget enforcement, false rate limiter, and extended thinking quota impact. The monitoring features in v1.1.0 are informed by this research.
|
|
197
|
+
- **[@Renvect/X-Ray-Claude-Code-Interceptor](https://github.com/Renvect/X-Ray-Claude-Code-Interceptor)** — Diagnostic HTTPS proxy with real-time dashboard, system prompt section diffing, per-tool stripping thresholds, and multi-stream JSONL logging. Works with any Claude client that supports `ANTHROPIC_BASE_URL` (CLI, VS Code extension, desktop app), complementing this package's CLI-only `NODE_OPTIONS` approach.
|
|
173
198
|
|
|
174
199
|
## Contributors
|
|
175
200
|
|
package/package.json
CHANGED
package/preload.mjs
CHANGED
|
@@ -365,6 +365,156 @@ function stripOldToolResultImages(messages, keepLast) {
|
|
|
365
365
|
return { messages: strippedCount > 0 ? result : messages, stats };
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
// --------------------------------------------------------------------------
|
|
369
|
+
// Prefix lock — replay saved messages[0] on resume for cache hit
|
|
370
|
+
// --------------------------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
// CACHE_FIX_PREFIX_LOCK=1 — save messages[0] on every call and replay it on
|
|
373
|
+
// resume to avoid a cache rebuild. Disabled by default.
|
|
374
|
+
//
|
|
375
|
+
// On resume, CC reassembles messages with blocks in different positions and
|
|
376
|
+
// injects fresh system-reminders, changing the prefix bytes. Even after our
|
|
377
|
+
// relocation fix corrects the blocks, the prefix differs from what the server
|
|
378
|
+
// cached on the last pre-exit call, causing a full cache rebuild.
|
|
379
|
+
//
|
|
380
|
+
// This feature saves the exact messages[0] content after all fixes are applied.
|
|
381
|
+
// On the first call of a new process (resume), if system prompt hash and tools
|
|
382
|
+
// hash match the saved snapshot, and the real user message text matches, we
|
|
383
|
+
// replay the saved messages[0] to produce a byte-identical prefix → cache hit.
|
|
384
|
+
|
|
385
|
+
const PREFIX_LOCK = process.env.CACHE_FIX_PREFIX_LOCK === "1";
|
|
386
|
+
const PREFIX_LOCK_FILE = join(homedir(), ".claude", "cache-fix-prefix-lock.json");
|
|
387
|
+
|
|
388
|
+
let _prefixLockFirstCall = true;
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Compute hashes for prefix lock comparison.
|
|
392
|
+
*/
|
|
393
|
+
function computePrefixHashes(system, tools) {
|
|
394
|
+
const sysHash = system
|
|
395
|
+
? createHash("sha256").update(JSON.stringify(system)).digest("hex").slice(0, 16)
|
|
396
|
+
: "none";
|
|
397
|
+
const toolHash = tools
|
|
398
|
+
? createHash("sha256").update(JSON.stringify(tools.map(t => t.name).sort())).digest("hex").slice(0, 16)
|
|
399
|
+
: "none";
|
|
400
|
+
return { sysHash, toolHash };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Extract the real user message text from messages[0] (skipping system-reminders).
|
|
405
|
+
*/
|
|
406
|
+
function extractUserTextFromFirstMsg(msg) {
|
|
407
|
+
if (!msg || !Array.isArray(msg.content)) return "";
|
|
408
|
+
for (const block of msg.content) {
|
|
409
|
+
if (block.type === "text" && typeof block.text === "string" &&
|
|
410
|
+
!block.text.startsWith("<system-reminder>") &&
|
|
411
|
+
!block.text.startsWith("<local-command")) {
|
|
412
|
+
return block.text.slice(0, 200); // enough to identify, not too much to compare
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return "";
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Hash all non-system-reminder user content in messages[0] to detect
|
|
420
|
+
* substantive changes that the userText check (first 200 chars) might miss.
|
|
421
|
+
*/
|
|
422
|
+
function hashUserContent(msg) {
|
|
423
|
+
if (!msg || !Array.isArray(msg.content)) return "empty";
|
|
424
|
+
const userBlocks = msg.content.filter(b =>
|
|
425
|
+
b.type === "text" && typeof b.text === "string" &&
|
|
426
|
+
!b.text.startsWith("<system-reminder>") &&
|
|
427
|
+
!b.text.startsWith("<local-command")
|
|
428
|
+
);
|
|
429
|
+
if (userBlocks.length === 0) return "empty";
|
|
430
|
+
return createHash("sha256")
|
|
431
|
+
.update(userBlocks.map(b => b.text).join("\n"))
|
|
432
|
+
.digest("hex").slice(0, 16);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* On resume: try to replay saved messages[0] for cache hit.
|
|
437
|
+
* Returns the locked messages array or the original if lock doesn't apply.
|
|
438
|
+
*/
|
|
439
|
+
function applyPrefixLock(messages, system, tools) {
|
|
440
|
+
if (!PREFIX_LOCK || !Array.isArray(messages) || messages.length < 2) return messages;
|
|
441
|
+
|
|
442
|
+
const firstUserIdx = messages.findIndex(m => m.role === "user");
|
|
443
|
+
if (firstUserIdx === -1) return messages;
|
|
444
|
+
|
|
445
|
+
const { sysHash, toolHash } = computePrefixHashes(system, tools);
|
|
446
|
+
const currentUserText = extractUserTextFromFirstMsg(messages[firstUserIdx]);
|
|
447
|
+
const currentContentHash = hashUserContent(messages[firstUserIdx]);
|
|
448
|
+
|
|
449
|
+
// Skip if this looks like a compacted conversation (system-reminder as first block
|
|
450
|
+
// with compaction summary markers)
|
|
451
|
+
const firstBlock = messages[firstUserIdx]?.content?.[0];
|
|
452
|
+
if (firstBlock?.text?.includes("CompactBoundary") || firstBlock?.text?.includes("compacted")) {
|
|
453
|
+
debugLog("PREFIX LOCK: skipped — compacted conversation detected");
|
|
454
|
+
return messages;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (_prefixLockFirstCall) {
|
|
458
|
+
_prefixLockFirstCall = false;
|
|
459
|
+
|
|
460
|
+
// Try to load and apply saved prefix
|
|
461
|
+
try {
|
|
462
|
+
const saved = JSON.parse(readFileSync(PREFIX_LOCK_FILE, "utf8"));
|
|
463
|
+
|
|
464
|
+
if (saved.sysHash !== sysHash) {
|
|
465
|
+
debugLog("PREFIX LOCK: skipped — system prompt changed");
|
|
466
|
+
} else if (saved.toolHash !== toolHash) {
|
|
467
|
+
debugLog("PREFIX LOCK: skipped — tools changed");
|
|
468
|
+
} else if (saved.userText !== currentUserText) {
|
|
469
|
+
debugLog("PREFIX LOCK: skipped — user message text changed");
|
|
470
|
+
} else if (saved.contentHash && saved.contentHash !== currentContentHash) {
|
|
471
|
+
debugLog("PREFIX LOCK: skipped — user content hash changed (substantive context change)");
|
|
472
|
+
} else if (!saved.content || !Array.isArray(saved.content)) {
|
|
473
|
+
debugLog("PREFIX LOCK: skipped — saved content invalid");
|
|
474
|
+
} else {
|
|
475
|
+
// Apply the saved messages[0] content
|
|
476
|
+
const result = [...messages];
|
|
477
|
+
result[firstUserIdx] = { ...result[firstUserIdx], content: saved.content };
|
|
478
|
+
debugLog(`PREFIX LOCK: APPLIED — replayed saved messages[0] (${saved.content.length} blocks)`);
|
|
479
|
+
return result;
|
|
480
|
+
}
|
|
481
|
+
} catch {
|
|
482
|
+
debugLog("PREFIX LOCK: no saved prefix found (first run or file missing)");
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return messages;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Save current messages[0] content for future resume replay.
|
|
491
|
+
* Called after all fixes are applied, before the request is sent.
|
|
492
|
+
*/
|
|
493
|
+
function savePrefixLock(messages, system, tools) {
|
|
494
|
+
if (!PREFIX_LOCK || !Array.isArray(messages)) return;
|
|
495
|
+
|
|
496
|
+
const firstUserIdx = messages.findIndex(m => m.role === "user");
|
|
497
|
+
if (firstUserIdx === -1) return;
|
|
498
|
+
|
|
499
|
+
const { sysHash, toolHash } = computePrefixHashes(system, tools);
|
|
500
|
+
const userText = extractUserTextFromFirstMsg(messages[firstUserIdx]);
|
|
501
|
+
const contentHash = hashUserContent(messages[firstUserIdx]);
|
|
502
|
+
const content = messages[firstUserIdx].content;
|
|
503
|
+
|
|
504
|
+
try {
|
|
505
|
+
writeFileSync(PREFIX_LOCK_FILE, JSON.stringify({
|
|
506
|
+
timestamp: new Date().toISOString(),
|
|
507
|
+
sysHash,
|
|
508
|
+
toolHash,
|
|
509
|
+
userText,
|
|
510
|
+
contentHash,
|
|
511
|
+
content,
|
|
512
|
+
}));
|
|
513
|
+
} catch (e) {
|
|
514
|
+
debugLog("PREFIX LOCK: failed to save:", e?.message);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
368
518
|
// --------------------------------------------------------------------------
|
|
369
519
|
// Tool schema stabilization (Bug 2 secondary cause)
|
|
370
520
|
// --------------------------------------------------------------------------
|
|
@@ -694,6 +844,15 @@ globalThis.fetch = async function (url, options) {
|
|
|
694
844
|
}
|
|
695
845
|
}
|
|
696
846
|
|
|
847
|
+
// Prefix lock: replay saved messages[0] on resume for cache hit
|
|
848
|
+
if (payload.messages && payload.system) {
|
|
849
|
+
const locked = applyPrefixLock(payload.messages, payload.system, payload.tools);
|
|
850
|
+
if (locked !== payload.messages) {
|
|
851
|
+
payload.messages = locked;
|
|
852
|
+
modified = true;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
697
856
|
// Bug 2a: Stabilize tool ordering
|
|
698
857
|
if (payload.tools) {
|
|
699
858
|
const sorted = stabilizeToolOrder(payload.tools);
|
|
@@ -726,6 +885,11 @@ globalThis.fetch = async function (url, options) {
|
|
|
726
885
|
debugLog("Request body rewritten");
|
|
727
886
|
}
|
|
728
887
|
|
|
888
|
+
// Save prefix lock after all fixes applied
|
|
889
|
+
if (payload.messages && payload.system) {
|
|
890
|
+
savePrefixLock(payload.messages, payload.system, payload.tools);
|
|
891
|
+
}
|
|
892
|
+
|
|
729
893
|
// Monitor for microcompact / budget enforcement degradation
|
|
730
894
|
if (payload.messages) {
|
|
731
895
|
monitorContextDegradation(payload.messages);
|