diff --git a/internal/runtime/executor/claude_executor_cloaking.go b/internal/runtime/executor/claude_executor_cloaking.go index e482119ce..14bcd5b3d 100644 --- a/internal/runtime/executor/claude_executor_cloaking.go +++ b/internal/runtime/executor/claude_executor_cloaking.go @@ -1876,6 +1876,12 @@ func enforceCacheControlLimit(payload []byte, maxBlocks int) []byte { return payload } + thread := gjson.GetBytes(payload, "thread") + if thread.Exists() && thread.Type != gjson.Null && maxBlocks > 0 { + // Anthropic reserves one cache breakpoint for thread continuation. + maxBlocks-- + } + total := countCacheControls(payload) if total <= maxBlocks { return payload diff --git a/internal/runtime/executor/claude_executor_test.go b/internal/runtime/executor/claude_executor_test.go index 666a63ab3..da361df6b 100644 --- a/internal/runtime/executor/claude_executor_test.go +++ b/internal/runtime/executor/claude_executor_test.go @@ -3240,6 +3240,27 @@ func TestEnforceCacheControlLimit_PreservesKeyOrderWhenModified(t *testing.T) { } } +func TestEnforceCacheControlLimit_ReservesThreadMarker(t *testing.T) { + payload := []byte(`{ + "thread": {"type":"create"}, + "tools": [{"name":"t1","cache_control":{"type":"ephemeral"}}], + "system": [ + {"type":"text","text":"s1","cache_control":{"type":"ephemeral"}}, + {"type":"text","text":"s2","cache_control":{"type":"ephemeral"}} + ], + "messages": [{"role":"user","content":[ + {"type":"text","text":"u1","cache_control":{"type":"ephemeral"}}, + {"type":"text","text":"u2","cache_control":{"type":"ephemeral"}} + ]}] + }`) + + out := enforceCacheControlLimit(payload, 4) + + if got := countCacheControls(out); got != 3 { + t.Fatalf("cache_control count = %d, want 3 when thread is present", got) + } +} + func TestEnforceCacheControlLimit_ToolOnlyPayloadStillRespectsLimit(t *testing.T) { payload := []byte(`{ "tools": [