diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index 966a1320d08..04080f35fe8 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -1709,7 +1709,7 @@ func normalizeCodexInstructions(body []byte) []byte { if !instructions.Exists() || instructions.Type == gjson.Null { body, _ = sjson.SetBytes(body, "instructions", "") } - return body + return stripCodexIntermediaryUpdatesFromPayload(body) } var imageGenToolJSON = []byte(`{"type":"image_generation","output_format":"png"}`) diff --git a/internal/runtime/executor/codex_prompt_patch.go b/internal/runtime/executor/codex_prompt_patch.go new file mode 100644 index 00000000000..88a328d5a68 --- /dev/null +++ b/internal/runtime/executor/codex_prompt_patch.go @@ -0,0 +1,87 @@ +package executor + +import ( + "bytes" + "encoding/json" + "strings" +) + +const codexIntermediaryUpdatesHeading = "## Intermediary updates" + +func stripCodexIntermediaryUpdatesFromPayload(body []byte) []byte { + if !bytes.Contains(body, []byte(codexIntermediaryUpdatesHeading)) { + return body + } + + var payload map[string]any + decoder := json.NewDecoder(bytes.NewReader(body)) + decoder.UseNumber() + if err := decoder.Decode(&payload); err != nil { + return body + } + + instructions, ok := payload["instructions"].(string) + if !ok { + return body + } + stripped, changed := stripCodexIntermediaryUpdatesText(instructions) + if !changed { + return body + } + payload["instructions"] = stripped + + var out bytes.Buffer + encoder := json.NewEncoder(&out) + encoder.SetEscapeHTML(false) + if err := encoder.Encode(payload); err != nil { + return body + } + return bytes.TrimSpace(out.Bytes()) +} + +func stripCodexIntermediaryUpdatesText(text string) (string, bool) { + if !strings.Contains(text, codexIntermediaryUpdatesHeading) { + return text, false + } + + lines := strings.SplitAfter(text, "\n") + out := make([]string, 0, len(lines)) + skipping := false + changed := false + + for _, line := range lines { + trimmed := strings.TrimSpace(strings.TrimSuffix(strings.TrimSuffix(line, "\n"), "\r")) + if !skipping && trimmed == codexIntermediaryUpdatesHeading { + skipping = true + changed = true + continue + } + + if skipping { + if isCodexIntermediaryUpdatesStopHeading(trimmed) { + skipping = false + out = append(out, line) + } + continue + } + + out = append(out, line) + } + + if !changed { + return text, false + } + return strings.Join(out, ""), true +} + +func isCodexIntermediaryUpdatesStopHeading(trimmedLine string) bool { + if trimmedLine == codexIntermediaryUpdatesHeading { + return false + } + + headingLevel := 0 + for headingLevel < len(trimmedLine) && headingLevel < 6 && trimmedLine[headingLevel] == '#' { + headingLevel++ + } + return headingLevel > 0 && len(trimmedLine) > headingLevel && trimmedLine[headingLevel] == ' ' +} diff --git a/internal/runtime/executor/codex_prompt_patch_test.go b/internal/runtime/executor/codex_prompt_patch_test.go new file mode 100644 index 00000000000..e997810da3f --- /dev/null +++ b/internal/runtime/executor/codex_prompt_patch_test.go @@ -0,0 +1,92 @@ +package executor + +import ( + "strings" + "testing" + + "github.com/tidwall/gjson" +) + +func TestStripCodexIntermediaryUpdatesFromPayloadTopLevelInstructions(t *testing.T) { + payload := []byte(`{"instructions":"before\n\n## Intermediary updates \n- noisy updates\n- more noise\n## Final answer instructions\nkeep this","input":[{"role":"user","content":"hello"}]}`) + + out := stripCodexIntermediaryUpdatesFromPayload(payload) + instructions := gjson.GetBytes(out, "instructions").String() + + if strings.Contains(instructions, "Intermediary updates") || strings.Contains(instructions, "noisy updates") { + t.Fatalf("intermediary section was not stripped: %q", instructions) + } + if !strings.Contains(instructions, "before") || !strings.Contains(instructions, "## Final answer instructions") { + t.Fatalf("surrounding instructions were not preserved: %q", instructions) + } +} + +func TestStripCodexIntermediaryUpdatesFromPayloadLeavesNestedInputText(t *testing.T) { + payload := []byte(`{"instructions":"keep","input":[{"content":[{"type":"input_text","text":"alpha\n## Intermediary updates\nremove me"}]}],"reasoning":{"effort":"high"}}`) + + out := stripCodexIntermediaryUpdatesFromPayload(payload) + text := gjson.GetBytes(out, "input.0.content.0.text").String() + + if !strings.Contains(text, "Intermediary updates") || !strings.Contains(text, "remove me") { + t.Fatalf("nested user text was stripped unexpectedly: %q", text) + } + if !strings.Contains(text, "alpha") { + t.Fatalf("nested prompt prefix was not preserved: %q", text) + } + if got := gjson.GetBytes(out, "reasoning.effort").String(); got != "high" { + t.Fatalf("unrelated fields changed: reasoning.effort=%q", got) + } +} + +func TestStripCodexIntermediaryUpdatesFromPayloadOnlyTouchesInstructions(t *testing.T) { + payload := []byte(`{"instructions":"keep\n## Intermediary updates\nremove me\n## Final answer instructions\nanswer","input":[{"content":[{"type":"input_text","text":"alpha\n## Intermediary updates\nkeep this user text"}]}]}`) + + out := stripCodexIntermediaryUpdatesFromPayload(payload) + instructions := gjson.GetBytes(out, "instructions").String() + text := gjson.GetBytes(out, "input.0.content.0.text").String() + + if strings.Contains(instructions, "Intermediary updates") || strings.Contains(instructions, "remove me") { + t.Fatalf("instructions intermediary section was not stripped: %q", instructions) + } + if !strings.Contains(instructions, "## Final answer instructions") { + t.Fatalf("following instruction section was not preserved: %q", instructions) + } + if !strings.Contains(text, "Intermediary updates") || !strings.Contains(text, "keep this user text") { + t.Fatalf("nested user text was stripped unexpectedly: %q", text) + } +} + +func TestStripCodexIntermediaryUpdatesFromPayloadLeavesUnrelatedPayload(t *testing.T) { + payload := []byte(`{"instructions":"inline mention of ## Intermediary updates should stay","input":[]}`) + + out := stripCodexIntermediaryUpdatesFromPayload(payload) + if string(out) != string(payload) { + t.Fatalf("payload changed unexpectedly:\ngot %s\nwant %s", string(out), string(payload)) + } +} + +func TestStripCodexIntermediaryUpdatesTextRemovesLastSection(t *testing.T) { + out, changed := stripCodexIntermediaryUpdatesText("before\n## Intermediary updates\n- remove me") + if !changed { + t.Fatal("expected text to change") + } + if strings.Contains(out, "Intermediary updates") || strings.Contains(out, "remove me") { + t.Fatalf("last section was not stripped: %q", out) + } + if !strings.Contains(out, "before") { + t.Fatalf("prefix was not preserved: %q", out) + } +} + +func TestStripCodexIntermediaryUpdatesTextStopsAtMarkdownHeading(t *testing.T) { + out, changed := stripCodexIntermediaryUpdatesText("before\n## Intermediary updates\n- remove me\n## Intermediary updates\n- remove me too\n### Keep this\nkeep") + if !changed { + t.Fatal("expected text to change") + } + if strings.Contains(out, "Intermediary updates") || strings.Contains(out, "remove me") { + t.Fatalf("intermediary sections were not stripped: %q", out) + } + if !strings.Contains(out, "before") || !strings.Contains(out, "### Keep this\nkeep") { + t.Fatalf("surrounding markdown was not preserved: %q", out) + } +}