diff --git a/internal/runtime/executor/claude_executor_cloaking.go b/internal/runtime/executor/claude_executor_cloaking.go
index e74347e5..db09a6f9 100644
--- a/internal/runtime/executor/claude_executor_cloaking.go
+++ b/internal/runtime/executor/claude_executor_cloaking.go
@@ -207,7 +207,7 @@ func checkSystemInstructionsWithMode(payload []byte, strictMode bool) []byte {
}
// checkSystemInstructionsWithSigningMode keeps the top-level system in Claude
-// Code's minimal CLI shape. A caller's complete system text is preserved as a
+// Code's minimal CLI shape. Each caller system block is preserved as a separate
// mid-conversation system message after the first user turn, where supported
// Claude models give it operator-level authority without changing the cached
// top-level prefix.
@@ -227,25 +227,25 @@ func checkSystemInstructionsWithSigningModeAt(payload []byte, strictMode bool, c
return injectClaudeCodeCurrentDate(payload, now)
}
- forwardedSystem := collectForwardedClaudeSystemPrompt(system)
- if strings.TrimSpace(forwardedSystem) == "" {
+ forwardedSystemBlocks := collectForwardedClaudeSystemPromptBlocks(system)
+ if len(forwardedSystemBlocks) == 0 {
return injectClaudeCodeCurrentDate(payload, now)
}
if claudeUsesLegacySystemReminder(payload) {
- payload = prependClaudeSystemReminderToFirstUserMessage(payload, forwardedSystem)
+ payload = prependClaudeSystemRemindersToFirstUserMessage(payload, forwardedSystemBlocks)
} else {
// Unknown and future model IDs optimistically use the authoritative
// mid-conversation system role. Only empirically unsupported legacy IDs
// stay on the user-reminder compatibility path.
- payload = insertClaudeMidConversationSystemMessage(payload, forwardedSystem)
+ payload = insertClaudeMidConversationSystemMessages(payload, forwardedSystemBlocks)
}
return injectClaudeCodeCurrentDate(payload, now)
}
// relocateClaudeSystemPromptForCountTokens keeps a cloaked count_tokens request
// in Claude Code's measured shape, which carries only model, messages and tools.
-// The Claude Code system blocks are therefore not installed here, but a caller's
-// system prompt still has to be accounted for, so it is relocated into messages
+// The Claude Code system blocks are therefore not installed here, but each caller
+// system block still has to be accounted for, so it is relocated into messages
// using the same positional mapping as the Messages path. That keeps the counted
// tokens aligned with the request the caller is about to send while preventing a
// third-party system prompt from reaching Anthropic in the system slot.
@@ -256,22 +256,22 @@ func relocateClaudeSystemPromptForCountTokens(payload []byte, strictMode bool) [
}
// Strict mode drops caller prompts on the Messages path, so it must not
// reintroduce them here either.
- forwardedSystem := ""
+ var forwardedSystemBlocks []string
if !strictMode {
- forwardedSystem = collectForwardedClaudeSystemPrompt(system)
+ forwardedSystemBlocks = collectForwardedClaudeSystemPromptBlocks(system)
}
updated, errDelete := sjson.DeleteBytes(payload, "system")
if errDelete != nil {
return payload
}
payload = updated
- if strings.TrimSpace(forwardedSystem) == "" {
+ if len(forwardedSystemBlocks) == 0 {
return payload
}
if claudeUsesLegacySystemReminder(payload) {
- return prependClaudeSystemReminderToFirstUserMessage(payload, forwardedSystem)
+ return prependClaudeSystemRemindersToFirstUserMessage(payload, forwardedSystemBlocks)
}
- return insertClaudeMidConversationSystemMessage(payload, forwardedSystem)
+ return insertClaudeMidConversationSystemMessages(payload, forwardedSystemBlocks)
}
// claudeLegacySystemReminderModels lists the official Anthropic model IDs and
@@ -309,13 +309,13 @@ func claudeUsesLegacySystemReminder(payload []byte) bool {
return legacy
}
-func collectForwardedClaudeSystemPrompt(system gjson.Result) string {
- var parts []string
+func collectForwardedClaudeSystemPromptBlocks(system gjson.Result) []string {
+ var blocks []string
appendText := func(text string) {
if strings.TrimSpace(text) == "" || util.IsClaudeCodeAttributionSystemText(text) || text == claudeCodeCLIIdentity {
return
}
- parts = append(parts, text)
+ blocks = append(blocks, text)
}
if system.IsArray() {
@@ -328,7 +328,7 @@ func collectForwardedClaudeSystemPrompt(system gjson.Result) string {
} else if system.Type == gjson.String {
appendText(system.String())
}
- return strings.Join(parts, "\n\n")
+ return blocks
}
// buildTextBlock constructs a JSON text block with JSON.stringify-compatible
@@ -354,42 +354,62 @@ func marshalJSONStringWithoutHTMLEscape(value string) string {
return strings.TrimSuffix(encoded.String(), "\n")
}
-func prependClaudeSystemReminderToFirstUserMessage(payload []byte, text string) []byte {
+func prependClaudeSystemRemindersToFirstUserMessage(payload []byte, texts []string) []byte {
firstUserIdx := firstClaudeUserMessageIndex(payload)
- if firstUserIdx < 0 {
+ if firstUserIdx < 0 || len(texts) == 0 {
return payload
}
- reminderText := claudeCallerSystemReminder(text)
- reminderBlock := buildTextBlock(reminderText, nil)
+ reminderTexts := make([]string, 0, len(texts))
+ for _, text := range texts {
+ reminderTexts = append(reminderTexts, claudeCallerSystemReminder(text))
+ }
+
contentPath := fmt.Sprintf("messages.%d.content", firstUserIdx)
content := gjson.GetBytes(payload, contentPath)
if content.IsArray() {
blocks := content.Array()
+ existing := make(map[string]int, len(blocks))
for _, block := range blocks {
- if block.Get("type").String() == "text" && block.Get("text").String() == reminderText {
- return payload
+ if block.Get("type").String() == "text" {
+ existing[block.Get("text").String()]++
+ }
+ }
+
+ reminderBlocks := make([]string, 0, len(reminderTexts))
+ for _, reminderText := range reminderTexts {
+ if existing[reminderText] > 0 {
+ existing[reminderText]--
+ continue
}
+ reminderBlocks = append(reminderBlocks, buildTextBlock(reminderText, nil))
+ }
+ if len(reminderBlocks) == 0 {
+ return payload
}
insertAt := 0
for insertAt < len(blocks) && blocks[insertAt].Get("type").String() == "tool_result" {
insertAt++
}
- rawBlocks := make([]string, 0, len(blocks)+1)
+ rawBlocks := make([]string, 0, len(blocks)+len(reminderBlocks))
for idx, block := range blocks {
if idx == insertAt {
- rawBlocks = append(rawBlocks, reminderBlock)
+ rawBlocks = append(rawBlocks, reminderBlocks...)
}
rawBlocks = append(rawBlocks, block.Raw)
}
if insertAt == len(blocks) {
- rawBlocks = append(rawBlocks, reminderBlock)
+ rawBlocks = append(rawBlocks, reminderBlocks...)
}
payload, _ = sjson.SetRawBytes(payload, contentPath, []byte("["+strings.Join(rawBlocks, ",")+"]"))
} else if content.Type == gjson.String {
- userBlock := buildTextBlock(content.String(), nil)
- payload, _ = sjson.SetRawBytes(payload, contentPath, []byte("["+reminderBlock+","+userBlock+"]"))
+ rawBlocks := make([]string, 0, len(reminderTexts)+1)
+ for _, reminderText := range reminderTexts {
+ rawBlocks = append(rawBlocks, buildTextBlock(reminderText, nil))
+ }
+ rawBlocks = append(rawBlocks, buildTextBlock(content.String(), nil))
+ payload, _ = sjson.SetRawBytes(payload, contentPath, []byte("["+strings.Join(rawBlocks, ",")+"]"))
}
return payload
}
@@ -405,9 +425,9 @@ func claudeCallerSystemReminder(text string) string {
return reminder.String()
}
-func insertClaudeMidConversationSystemMessage(payload []byte, text string) []byte {
+func insertClaudeMidConversationSystemMessages(payload []byte, texts []string) []byte {
firstUserIdx := firstClaudeUserMessageIndex(payload)
- if firstUserIdx < 0 {
+ if firstUserIdx < 0 || len(texts) == 0 {
return payload
}
@@ -415,28 +435,39 @@ func insertClaudeMidConversationSystemMessage(payload []byte, text string) []byt
if !messages.IsArray() {
return payload
}
- for _, message := range messages.Array() {
- if message.Get("role").String() == "system" && claudeMessageContentText(message.Get("content")) == text {
- return payload
- }
- }
-
- content := "[" + buildTextBlock(text, map[string]string{"type": "ephemeral"}) + "]"
- systemMessage := `{"role":"system","content":` + content + "}"
messageBlocks := messages.Array()
insertAt := firstUserIdx + 1
for insertAt < len(messageBlocks) && messageBlocks[insertAt].Get("role").String() == "user" {
insertAt++
}
- rawMessages := make([]string, 0, len(messageBlocks)+1)
+ if len(messageBlocks)-insertAt >= len(texts) {
+ matches := true
+ for idx, text := range texts {
+ message := messageBlocks[insertAt+idx]
+ if message.Get("role").String() != "system" || claudeMessageContentText(message.Get("content")) != text {
+ matches = false
+ break
+ }
+ }
+ if matches {
+ return payload
+ }
+ }
+
+ systemMessages := make([]string, 0, len(texts))
+ for _, text := range texts {
+ content := "[" + buildTextBlock(text, map[string]string{"type": "ephemeral"}) + "]"
+ systemMessages = append(systemMessages, `{"role":"system","content":`+content+"}")
+ }
+ rawMessages := make([]string, 0, len(messageBlocks)+len(systemMessages))
for idx, message := range messageBlocks {
if idx == insertAt {
- rawMessages = append(rawMessages, systemMessage)
+ rawMessages = append(rawMessages, systemMessages...)
}
rawMessages = append(rawMessages, message.Raw)
}
if insertAt == len(messageBlocks) {
- rawMessages = append(rawMessages, systemMessage)
+ rawMessages = append(rawMessages, systemMessages...)
}
payload, _ = sjson.SetRawBytes(payload, "messages", []byte("["+strings.Join(rawMessages, ",")+"]"))
return payload
@@ -578,22 +609,9 @@ func injectClaudeCodeCurrentDate(payload []byte, now time.Time) []byte {
rawBlocks = append(rawBlocks, block.Raw)
}
- insertAt := 0
- for insertAt < len(rawBlocks) {
- block := gjson.Parse(rawBlocks[insertAt])
- if block.Get("type").String() == "tool_result" {
- insertAt++
- continue
- }
- if block.Get("type").String() == "text" && isClaudeCodeContextReminder(block.Get("text").String()) {
- insertAt++
- continue
- }
- break
- }
rawBlocks = append(rawBlocks, "")
- copy(rawBlocks[insertAt+1:], rawBlocks[insertAt:])
- rawBlocks[insertAt] = dateBlock
+ copy(rawBlocks[1:], rawBlocks)
+ rawBlocks[0] = dateBlock
payload, _ = sjson.SetRawBytes(payload, contentPath, []byte("["+strings.Join(rawBlocks, ",")+"]"))
return payload
}
diff --git a/internal/runtime/executor/claude_executor_test.go b/internal/runtime/executor/claude_executor_test.go
index d8c4448d..d27c4991 100644
--- a/internal/runtime/executor/claude_executor_test.go
+++ b/internal/runtime/executor/claude_executor_test.go
@@ -3639,24 +3639,29 @@ func assertClaudeLegacySystemReminderLayout(t *testing.T, body []byte, wantSyste
}
content := gjson.GetBytes(body, "messages.0.content").Array()
if len(content) != 3 {
- t.Fatalf("user content has %d blocks, want caller reminder, currentDate, and user text", len(content))
+ t.Fatalf("user content has %d blocks, want currentDate, caller reminder, and user text", len(content))
}
- if got := content[0].Get("text").String(); got != claudeCallerSystemReminder(wantSystem) {
+ assertClaudeCodeCurrentDateBlock(t, content[0])
+ if got := content[1].Get("text").String(); got != claudeCallerSystemReminder(wantSystem) {
t.Fatalf("caller reminder lost system prompt: got len %d, want len %d", len(got), len(wantSystem))
}
- if content[0].Get("cache_control").Exists() {
- t.Fatalf("caller reminder unexpectedly has cache_control: %s", content[0].Raw)
+ if content[1].Get("cache_control").Exists() {
+ t.Fatalf("caller reminder unexpectedly has cache_control: %s", content[1].Raw)
}
- assertClaudeCodeCurrentDateBlock(t, content[1])
assertEphemeralUserTextBlock(t, content[2], wantUser)
}
func assertClaudeCodeCurrentDateBlock(t *testing.T, block gjson.Result) {
+ t.Helper()
+ assertClaudeCodeCurrentDateBlockAt(t, block, time.Now())
+}
+
+func assertClaudeCodeCurrentDateBlockAt(t *testing.T, block gjson.Result, now time.Time) {
t.Helper()
if got := block.Get("type").String(); got != "text" {
t.Fatalf("currentDate block type = %q, want text", got)
}
- if got, want := block.Get("text").String(), claudeCodeCurrentDateReminder(time.Now()); got != want {
+ if got, want := block.Get("text").String(), claudeCodeCurrentDateReminder(now); got != want {
t.Fatalf("currentDate reminder = %q, want %q", got, want)
}
if block.Get("cache_control").Exists() {
@@ -3753,29 +3758,38 @@ func TestInjectClaudeCodeCurrentDateIsIdempotentAndAlignsFirstUserCache(t *testi
assertEphemeralUserTextBlock(t, content[1], "hello")
}
-func TestInjectClaudeCodeCurrentDateFollowsLeadingRemindersAndToolResults(t *testing.T) {
+func TestInjectClaudeCodeCurrentDateMovesExistingCopyToFirstBlock(t *testing.T) {
+ fixed := time.Date(2026, time.August, 1, 9, 0, 0, 0, time.FixedZone("UTC+8", 8*60*60))
+ dateBlock := buildTextBlock(claudeCodeCurrentDateReminder(fixed), nil)
+ payload := []byte(`{"messages":[{"role":"user","content":[` +
+ `{"type":"text","text":"hello"},` + dateBlock + `]}]}`)
+
+ out := injectClaudeCodeCurrentDate(payload, fixed)
+ content := gjson.GetBytes(out, "messages.0.content").Array()
+ if len(content) != 2 {
+ t.Fatalf("content has %d blocks, want one currentDate and user text: %s", len(content), out)
+ }
+ assertClaudeCodeCurrentDateBlockAt(t, content[0], fixed)
+ assertEphemeralUserTextBlock(t, content[1], "hello")
+}
+
+func TestInjectClaudeCodeCurrentDatePrecedesExistingReminder(t *testing.T) {
fixed := time.Date(2026, time.August, 1, 9, 0, 0, 0, time.FixedZone("UTC+8", 8*60*60))
reminder := "\ncaller instructions\n"
payload := []byte(`{"messages":[{"role":"user","content":[` +
- `{"type":"tool_result","tool_use_id":"toolu_1","content":"ok"},` +
buildTextBlock(reminder, nil) + `,` +
`{"type":"text","text":"continue","cache_control":{"type":"ephemeral","ttl":"1h"}}]}]}`)
out := injectClaudeCodeCurrentDate(payload, fixed)
content := gjson.GetBytes(out, "messages.0.content").Array()
- if len(content) != 4 {
- t.Fatalf("content has %d blocks, want tool_result, reminder, currentDate, user text: %s", len(content), out)
- }
- if got := content[0].Get("type").String(); got != "tool_result" {
- t.Fatalf("content[0].type = %q, want tool_result", got)
+ if len(content) != 3 {
+ t.Fatalf("content has %d blocks, want currentDate, reminder, and user text: %s", len(content), out)
}
+ assertClaudeCodeCurrentDateBlockAt(t, content[0], fixed)
if got := content[1].Get("text").String(); got != reminder {
t.Fatalf("content[1].text = %q, want standalone reminder", got)
}
- if got := content[2].Get("text").String(); got != claudeCodeCurrentDateReminder(fixed) {
- t.Fatalf("content[2].text = %q, want currentDate after reminder", got)
- }
- assertEphemeralUserTextBlock(t, content[3], "continue")
+ assertEphemeralUserTextBlock(t, content[2], "continue")
}
// Test case 1: String system prompt becomes an authoritative mid-conversation
@@ -3865,18 +3879,42 @@ func TestCheckSystemInstructionsWithMode_LegacyModelUsesSystemReminder(t *testin
}
content := gjson.GetBytes(out, "messages.0.content").Array()
if len(content) != 3 {
- t.Fatalf("user content has %d blocks, want caller reminder, currentDate, and user text", len(content))
+ t.Fatalf("user content has %d blocks, want currentDate, caller reminder, and user text", len(content))
}
- if got := content[0].Get("text").String(); got != claudeCallerSystemReminder("legacy instructions") {
+ assertClaudeCodeCurrentDateBlock(t, content[0])
+ if got := content[1].Get("text").String(); got != claudeCallerSystemReminder("legacy instructions") {
t.Fatalf("caller system reminder = %q", got)
}
- if content[0].Get("cache_control").Exists() {
- t.Fatalf("caller system reminder unexpectedly has cache_control: %s", content[0].Raw)
+ if content[1].Get("cache_control").Exists() {
+ t.Fatalf("caller system reminder unexpectedly has cache_control: %s", content[1].Raw)
}
- assertClaudeCodeCurrentDateBlock(t, content[1])
assertEphemeralUserTextBlock(t, content[2], "hi")
}
+func TestCheckSystemInstructionsWithMode_LegacyModelKeepsSystemBlocksSeparate(t *testing.T) {
+ payload := []byte(`{"model":"claude-opus-4-6","system":[` +
+ `{"type":"text","text":"first guidance","cache_control":{"type":"ephemeral","ttl":"1h"}},` +
+ `{"type":"text","text":"second guidance"}],` +
+ `"messages":[{"role":"user","content":"hi"}]}`)
+
+ out := checkSystemInstructionsWithMode(payload, false)
+ content := gjson.GetBytes(out, "messages.0.content").Array()
+ if len(content) != 4 {
+ t.Fatalf("user content has %d blocks, want currentDate, two caller reminders, and user text: %s", len(content), out)
+ }
+ assertClaudeCodeCurrentDateBlock(t, content[0])
+ for idx, want := range []string{"first guidance", "second guidance"} {
+ block := content[idx+1]
+ if got := block.Get("text").String(); got != claudeCallerSystemReminder(want) {
+ t.Fatalf("content[%d].text = %q, want separate caller reminder %q", idx+1, got, want)
+ }
+ if block.Get("cache_control").Exists() {
+ t.Fatalf("content[%d] caller reminder unexpectedly has cache_control: %s", idx+1, block.Raw)
+ }
+ }
+ assertEphemeralUserTextBlock(t, content[3], "hi")
+}
+
// Test case 2: Strict mode keeps only the injected Claude Code system blocks.
func TestCheckSystemInstructionsWithMode_StringSystemStrict(t *testing.T) {
payload := []byte(`{"system":"You are a helpful assistant.","messages":[{"role":"user","content":"hi"}]}`)
@@ -3932,6 +3970,72 @@ func TestCheckSystemInstructionsWithMode_ArraySystemStillWorks(t *testing.T) {
assertClaudeMidConversationSystemMessage(t, out, 1, "Be concise.")
}
+func TestCheckSystemInstructionsWithMode_ArraySystemKeepsBlocksAsSeparateMessages(t *testing.T) {
+ payload := []byte(`{"model":"claude-opus-5","system":[` +
+ `{"type":"text","text":"first guidance","cache_control":{"type":"ephemeral","ttl":"1h"}},` +
+ `{"type":"text","text":"second guidance"}],` +
+ `"messages":[{"role":"user","content":"hi"}]}`)
+
+ out := checkSystemInstructionsWithMode(payload, false)
+ if got := gjson.GetBytes(out, "messages.#").Int(); got != 3 {
+ t.Fatalf("message count = %d, want user and two separate system messages: %s", got, out)
+ }
+ content := gjson.GetBytes(out, "messages.0.content").Array()
+ if len(content) != 2 {
+ t.Fatalf("user content has %d blocks, want currentDate and user text: %s", len(content), out)
+ }
+ assertClaudeCodeCurrentDateBlock(t, content[0])
+ assertEphemeralUserTextBlock(t, content[1], "hi")
+ assertClaudeMidConversationSystemMessage(t, out, 1, "first guidance")
+ assertClaudeMidConversationSystemMessage(t, out, 2, "second guidance")
+}
+
+func TestRelocateClaudeSystemPromptForCountTokensKeepsBlocksSeparate(t *testing.T) {
+ tests := []struct {
+ name string
+ model string
+ legacy bool
+ }{
+ {name: "mid-system model", model: "claude-opus-5"},
+ {name: "legacy model", model: "claude-opus-4-6", legacy: true},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ payload := []byte(`{"model":"` + test.model + `","system":[` +
+ `{"type":"text","text":"first guidance"},` +
+ `{"type":"text","text":"second guidance"}],` +
+ `"messages":[{"role":"user","content":"hi"}]}`)
+
+ out := relocateClaudeSystemPromptForCountTokens(payload, false)
+ if gjson.GetBytes(out, "system").Exists() {
+ t.Fatalf("count_tokens system must be absent: %s", out)
+ }
+ if test.legacy {
+ content := gjson.GetBytes(out, "messages.0.content").Array()
+ if len(content) != 3 {
+ t.Fatalf("legacy content has %d blocks, want two reminders and user text: %s", len(content), out)
+ }
+ if got := content[0].Get("text").String(); got != claudeCallerSystemReminder("first guidance") {
+ t.Fatalf("first caller reminder = %q", got)
+ }
+ if got := content[1].Get("text").String(); got != claudeCallerSystemReminder("second guidance") {
+ t.Fatalf("second caller reminder = %q", got)
+ }
+ if got := content[2].Get("text").String(); got != "hi" {
+ t.Fatalf("user text = %q, want hi", got)
+ }
+ return
+ }
+ if got := gjson.GetBytes(out, "messages.#").Int(); got != 3 {
+ t.Fatalf("message count = %d, want user and two system messages: %s", got, out)
+ }
+ assertClaudeMidConversationSystemMessage(t, out, 1, "first guidance")
+ assertClaudeMidConversationSystemMessage(t, out, 2, "second guidance")
+ })
+ }
+}
+
// Test case 5: Special characters survive the mid-conversation system move.
func TestCheckSystemInstructionsWithMode_StringWithSpecialChars(t *testing.T) {
payload := []byte(`{"model":"claude-opus-5","system":"Use tags & \"quotes\" in output.","messages":[{"role":"user","content":"hi"}]}`)
@@ -4925,42 +5029,42 @@ func TestClaudeExecutor_ExecuteStreamOAuthCustomToolMCPAliasRoundTrip(t *testing
}
}
-func TestPrependClaudeSystemReminder_FollowsToolResultsAndIsIdempotent(t *testing.T) {
- fixed := time.Date(2026, time.August, 1, 9, 0, 0, 0, time.FixedZone("UTC+8", 8*60*60))
+func TestPrependClaudeSystemReminders_FollowsToolResultsAndIsIdempotent(t *testing.T) {
payload := []byte(`{"messages":[` +
`{"role":"assistant","content":[{"type":"tool_use","id":"toolu_1","name":"Read","input":{}}]},` +
`{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"ok"},{"type":"text","text":"continue"}]}` +
`]}`)
- first := prependClaudeSystemReminderToFirstUserMessage(payload, "legacy guidance")
- second := prependClaudeSystemReminderToFirstUserMessage(first, "legacy guidance")
+ texts := []string{"first guidance", "second guidance"}
+ first := prependClaudeSystemRemindersToFirstUserMessage(payload, texts)
+ second := prependClaudeSystemRemindersToFirstUserMessage(first, texts)
if !bytes.Equal(first, second) {
t.Fatalf("caller reminder insertion is not idempotent:\nfirst: %s\nsecond: %s", first, second)
}
- out := injectClaudeCodeCurrentDate(first, fixed)
- content := gjson.GetBytes(out, "messages.1.content").Array()
+ content := gjson.GetBytes(first, "messages.1.content").Array()
if len(content) != 4 {
- t.Fatalf("content has %d blocks, want tool_result, caller reminder, currentDate, and user text", len(content))
+ t.Fatalf("content has %d blocks, want tool_result, two caller reminders, and user text", len(content))
}
if got := content[0].Get("type").String(); got != "tool_result" {
t.Fatalf("content[0].type = %q, want tool_result", got)
}
- if got := content[1].Get("text").String(); got != claudeCallerSystemReminder("legacy guidance") {
- t.Fatalf("content[1].text = %q, want caller reminder", got)
+ for idx, text := range texts {
+ if got := content[idx+1].Get("text").String(); got != claudeCallerSystemReminder(text) {
+ t.Fatalf("content[%d].text = %q, want caller reminder %q", idx+1, got, text)
+ }
}
- if got := content[2].Get("text").String(); got != claudeCodeCurrentDateReminder(fixed) {
- t.Fatalf("content[2].text = %q, want currentDate", got)
+ if got := content[3].Get("text").String(); got != "continue" {
+ t.Fatalf("content[3].text = %q, want user text", got)
}
- assertEphemeralUserTextBlock(t, content[3], "continue")
}
-func TestInsertClaudeMidConversationSystemMessage_FollowsToolResultUserTurn(t *testing.T) {
+func TestInsertClaudeMidConversationSystemMessages_FollowsToolResultUserTurn(t *testing.T) {
payload := []byte(`{"messages":[` +
`{"role":"assistant","content":[{"type":"tool_use","id":"toolu_1","name":"Read","input":{}}]},` +
`{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"ok"}]}` +
`]}`)
- out := insertClaudeMidConversationSystemMessage(payload, "guidance")
+ out := insertClaudeMidConversationSystemMessages(payload, []string{"guidance"})
if got := gjson.GetBytes(out, "messages.#").Int(); got != 3 {
t.Fatalf("message count = %d, want 3: %s", got, out)
}
@@ -4974,14 +5078,14 @@ func TestInsertClaudeMidConversationSystemMessage_FollowsToolResultUserTurn(t *t
assertClaudeMidConversationSystemMessage(t, out, 2, "guidance")
}
-func TestInsertClaudeMidConversationSystemMessage_PrecedesExistingAssistantTurn(t *testing.T) {
+func TestInsertClaudeMidConversationSystemMessages_PrecedesExistingAssistantTurn(t *testing.T) {
payload := []byte(`{"messages":[` +
`{"role":"user","content":"hello"},` +
`{"role":"assistant","content":"answer"},` +
`{"role":"user","content":"continue"}` +
`]}`)
- out := insertClaudeMidConversationSystemMessage(payload, "guidance")
+ out := insertClaudeMidConversationSystemMessages(payload, []string{"guidance"})
roles := gjson.GetBytes(out, "messages.#.role").Array()
wantRoles := []string{"user", "system", "assistant", "user"}
if len(roles) != len(wantRoles) {
@@ -4995,14 +5099,14 @@ func TestInsertClaudeMidConversationSystemMessage_PrecedesExistingAssistantTurn(
assertClaudeMidConversationSystemMessage(t, out, 1, "guidance")
}
-func TestInsertClaudeMidConversationSystemMessage_FollowsConsecutiveUserRun(t *testing.T) {
+func TestInsertClaudeMidConversationSystemMessages_FollowsConsecutiveUserRun(t *testing.T) {
payload := []byte(`{"messages":[` +
`{"role":"user","content":"first"},` +
`{"role":"user","content":"second"},` +
`{"role":"assistant","content":"answer"}` +
`]}`)
- out := insertClaudeMidConversationSystemMessage(payload, "guidance")
+ out := insertClaudeMidConversationSystemMessages(payload, []string{"guidance"})
roles := gjson.GetBytes(out, "messages.#.role").Array()
wantRoles := []string{"user", "user", "system", "assistant"}
if len(roles) != len(wantRoles) {
@@ -5016,13 +5120,19 @@ func TestInsertClaudeMidConversationSystemMessage_FollowsConsecutiveUserRun(t *t
assertClaudeMidConversationSystemMessage(t, out, 2, "guidance")
}
-func TestInsertClaudeMidConversationSystemMessage_IsIdempotent(t *testing.T) {
+func TestInsertClaudeMidConversationSystemMessages_IsIdempotent(t *testing.T) {
payload := []byte(`{"messages":[{"role":"user","content":"hello"}]}`)
- first := insertClaudeMidConversationSystemMessage(payload, "guidance")
- second := insertClaudeMidConversationSystemMessage(first, "guidance")
+ texts := []string{"first guidance", "second guidance"}
+ first := insertClaudeMidConversationSystemMessages(payload, texts)
+ second := insertClaudeMidConversationSystemMessages(first, texts)
if !bytes.Equal(first, second) {
t.Fatalf("mid-conversation system insertion is not idempotent:\nfirst: %s\nsecond: %s", first, second)
}
+ if got := gjson.GetBytes(first, "messages.#").Int(); got != 3 {
+ t.Fatalf("message count = %d, want user and two system messages: %s", got, first)
+ }
+ assertClaudeMidConversationSystemMessage(t, first, 1, texts[0])
+ assertClaudeMidConversationSystemMessage(t, first, 2, texts[1])
}
// TestClaudeCodeCLIBetas_MatchesObservedClientMatrix pins the Anthropic-Beta