fix: accept minimax apply-patch add delete quirks

This commit is contained in:
Codex
2026-06-03 09:58:20 +00:00
parent fef55cafb1
commit 1d542d82b5
2 changed files with 68 additions and 61 deletions
+36 -48
View File
@@ -16,6 +16,7 @@ export interface UpdateChunk {
export interface PatchParseResult {
patch: string;
hunks: PatchHunk[];
hints: string[];
}
export interface PatchUpdateResult {
@@ -119,7 +120,8 @@ export function applyPatchV2HelpPayload() {
"Add File has no @@ hunk marker: put content immediately after `*** Add File: <path>` and prefix every content line with +.",
"A blank line in Add File is a line containing only +.",
"Update File uses @@ or @@ context markers, followed by context lines starting with space and changed lines starting with - or +.",
"Prefer `trans <route> apply-patch < /tmp/patch.diff` for long patches, Windows paths, or quoting-sensitive content."
"Prefer `trans <route> apply-patch < /tmp/patch.diff` for long patches, Windows paths, or quoting-sensitive content.",
"MiniMax compatibility: stray @@ or unprefixed content inside Add File, and extra hunk/body lines after Delete File, are accepted with stderr hints."
],
examples: {
addFile: [
@@ -159,6 +161,7 @@ export function parseApplyPatchV2(patchText: string): PatchParseResult {
if (last !== endMarker) throw new ApplyPatchV2Error(`invalid patch: last line must be '${endMarker}'`);
const hunks: PatchHunk[] = [];
const hints: string[] = [];
let index = 1;
const environmentLine = lines[index]?.trimStart() ?? "";
if (environmentLine.startsWith(environmentIdMarker)) {
@@ -178,10 +181,30 @@ export function parseApplyPatchV2(patchText: string): PatchParseResult {
const added: string[] = [];
while (index < lines.length - 1 && !isFileHeader(lines[index] ?? "")) {
const addLine = lines[index] ?? "";
if (!addLine.startsWith("+")) {
throw invalidAddFileLineError(addLine, index + 1, filePath);
if (addLine.startsWith("+")) {
if (addLine.length > 1 && addLine.slice(1).trim().length === 0) {
hints.push(`apply-patch hint: accepted MiniMax-style whitespace-only Add File line in ${filePath} on line ${index + 1}; prefer a line containing only + for blank lines.`);
added.push("");
index += 1;
continue;
}
added.push(addLine.slice(1));
index += 1;
continue;
}
added.push(addLine.slice(1));
if (addLine.trimStart().startsWith("@@")) {
hints.push(`apply-patch hint: accepted MiniMax-style @@ inside Add File ${filePath} on line ${index + 1}; Add File does not need @@.`);
index += 1;
continue;
}
if (addLine.trim().length === 0) {
hints.push(`apply-patch hint: accepted a bare blank line inside Add File ${filePath} on line ${index + 1}; prefer a line containing only +.`);
added.push("");
index += 1;
continue;
}
hints.push(`apply-patch hint: accepted unprefixed Add File content in ${filePath} on line ${index + 1}; prefix new-file content lines with +.`);
added.push(addLine);
index += 1;
}
hunks.push({ kind: "add", path: filePath, content: joinLinesWithFinalNewline(added) });
@@ -190,9 +213,13 @@ export function parseApplyPatchV2(patchText: string): PatchParseResult {
if (line.startsWith(deleteFileMarker)) {
const filePath = validatePatchPath(line.slice(deleteFileMarker.length), index + 1);
index += 1;
const nextContentIndex = nextNonBlankLineIndex(lines, index, lines.length - 1);
if (nextContentIndex < lines.length - 1 && !isFileHeader(lines[nextContentIndex] ?? "")) {
throw invalidDeleteFileLineError(lines[nextContentIndex] ?? "", nextContentIndex + 1, filePath);
const extraLines: number[] = [];
while (index < lines.length - 1 && !isFileHeader(lines[index] ?? "")) {
if ((lines[index] ?? "").trim().length > 0) extraLines.push(index + 1);
index += 1;
}
if (extraLines.length > 0) {
hints.push(`apply-patch hint: ignored extra MiniMax-style hunk/body lines after Delete File ${filePath} on line ${extraLines[0]}; Delete File only needs the header.`);
}
hunks.push({ kind: "delete", path: filePath });
continue;
@@ -222,47 +249,7 @@ export function parseApplyPatchV2(patchText: string): PatchParseResult {
throw new ApplyPatchV2Error("invalid hunk header", { line: index + 1, text: line });
}
return { patch, hunks };
}
function invalidAddFileLineError(lineText: string, line: number, filePath: string): ApplyPatchV2Error {
const details = { line, path: filePath, text: lineText };
if (lineText.trimStart().startsWith("@@")) {
return new ApplyPatchV2Error(
"invalid add file line: remove @@ for Add File. Add File sections do not use hunk markers; put content directly after the header and prefix every content line with +.",
details,
);
}
if (lineText.trim().length === 0) {
return new ApplyPatchV2Error(
"invalid add file line: blank lines in Add File must be written as a line containing only +.",
details,
);
}
return new ApplyPatchV2Error(
"invalid add file line: every Add File content line must start with +. Add File has no @@ marker; use @@ only under Update File.",
details,
);
}
function invalidDeleteFileLineError(lineText: string, line: number, filePath: string): ApplyPatchV2Error {
const details = { line, path: filePath, text: lineText };
if (lineText.trimStart().startsWith("@@")) {
return new ApplyPatchV2Error(
"invalid delete file line: remove @@ for Delete File. Delete File sections contain only the `*** Delete File: <path>` header; do not include hunk markers or file content.",
details,
);
}
return new ApplyPatchV2Error(
"invalid delete file line: Delete File sections contain only the `*** Delete File: <path>` header. Remove old file content lines such as -old line.",
details,
);
}
function nextNonBlankLineIndex(lines: string[], start: number, endExclusive: number): number {
let index = start;
while (index < endExclusive && (lines[index] ?? "").trim().length === 0) index += 1;
return index;
return { patch, hunks, hints };
}
export function deriveUpdatedContent(filePath: string, originalContent: string, chunks: UpdateChunk[]): PatchUpdateResult {
@@ -291,6 +278,7 @@ export async function runApplyPatchV2(options: ApplyPatchV2Options): Promise<num
try {
const parsed = parseApplyPatchV2(patchText);
const plan = await applyPatchV2Hunks(options.executor, parsed.hunks);
for (const hint of parsed.hints) stderr.write(`${hint}\n`);
options.stdout.write("Success. Updated the following files:\n");
for (const item of plan.changed) options.stdout.write(`${item}\n`);
return 0;
+32 -13
View File
@@ -913,15 +913,35 @@ export async function runSshArgvGuidanceContract(): Promise<JsonRecord> {
"*** End Patch",
"",
].join("\n"), {}, { stderrOutput: true });
assertCondition(addFileWithHunkMarkerV2.exitCode === 1 && addFileWithHunkMarkerV2.error === null, "v2 CLI path should return a visible parse error for Add File @@ misuse", addFileWithHunkMarkerV2);
assertCondition(addFileWithHunkMarkerV2.stdout === "", "v2 Add File parse failures should keep Codex-style empty stdout", addFileWithHunkMarkerV2);
assertCondition(addFileWithHunkMarkerV2.exitCode === 0 && addFileWithHunkMarkerV2.error === null, "v2 should accept MiniMax-style Add File @@ misuse with a hint", addFileWithHunkMarkerV2);
assertCondition(addFileWithHunkMarkerV2.stdout.includes("A bad-add.txt"), "v2 MiniMax-style Add File should still apply the new file", addFileWithHunkMarkerV2);
assertCondition(
addFileWithHunkMarkerV2.stderr.includes("remove @@ for Add File")
&& addFileWithHunkMarkerV2.stderr.includes("prefix every content line with +"),
"v2 Add File @@ misuse should tell agents exactly how to repair the patch",
addFileWithHunkMarkerV2.stderr.includes("accepted MiniMax-style @@ inside Add File bad-add.txt")
&& addFileWithHunkMarkerV2.stderr.includes("Add File does not need @@"),
"v2 MiniMax-style Add File compatibility should still hint the canonical syntax",
addFileWithHunkMarkerV2.stderr,
);
assertCondition(!addFileWithHunkMarkerV2.commands.some((command) => command.startsWith("write-b64") || command.startsWith("delete")), "v2 Add File parse failures must not touch remote files", addFileWithHunkMarkerV2.commands);
assertCondition(addFileWithHunkMarkerV2.files["bad-add.txt"] === "content\n", "v2 MiniMax-style Add File compatibility should preserve content", addFileWithHunkMarkerV2);
const addFileBareLinesV2 = await applyPatchV2FixtureAttempt([
"*** Begin Patch",
"*** Add File: loose-add.txt",
"first",
"",
"+ ",
"+third",
"*** End Patch",
"",
].join("\n"), {}, { stderrOutput: true });
assertCondition(addFileBareLinesV2.exitCode === 0 && addFileBareLinesV2.error === null, "v2 should accept MiniMax-style Add File bare content and blank lines with hints", addFileBareLinesV2);
assertCondition(addFileBareLinesV2.files["loose-add.txt"] === "first\n\n\nthird\n", "v2 MiniMax-style Add File loose lines should preserve intended content", addFileBareLinesV2);
assertCondition(
addFileBareLinesV2.stderr.includes("accepted unprefixed Add File content in loose-add.txt")
&& addFileBareLinesV2.stderr.includes("accepted a bare blank line inside Add File loose-add.txt")
&& addFileBareLinesV2.stderr.includes("accepted MiniMax-style whitespace-only Add File line in loose-add.txt"),
"v2 MiniMax-style Add File loose lines should emit canonical syntax hints",
addFileBareLinesV2.stderr,
);
const deleteFileWithHunkMarkerV2 = await applyPatchV2FixtureAttempt([
"*** Begin Patch",
@@ -931,16 +951,15 @@ export async function runSshArgvGuidanceContract(): Promise<JsonRecord> {
"*** End Patch",
"",
].join("\n"), { "bad-delete.txt": "content\n" }, { stderrOutput: true });
assertCondition(deleteFileWithHunkMarkerV2.exitCode === 1 && deleteFileWithHunkMarkerV2.error === null, "v2 CLI path should return a visible parse error for Delete File @@ misuse", deleteFileWithHunkMarkerV2);
assertCondition(deleteFileWithHunkMarkerV2.stdout === "", "v2 Delete File parse failures should keep Codex-style empty stdout", deleteFileWithHunkMarkerV2);
assertCondition(deleteFileWithHunkMarkerV2.exitCode === 0 && deleteFileWithHunkMarkerV2.error === null, "v2 should accept MiniMax-style Delete File @@ misuse with a hint", deleteFileWithHunkMarkerV2);
assertCondition(deleteFileWithHunkMarkerV2.stdout.includes("D bad-delete.txt"), "v2 MiniMax-style Delete File should still delete the file", deleteFileWithHunkMarkerV2);
assertCondition(
deleteFileWithHunkMarkerV2.stderr.includes("remove @@ for Delete File")
&& deleteFileWithHunkMarkerV2.stderr.includes("do not include hunk markers or file content"),
"v2 Delete File @@ misuse should tell agents exactly how to repair the patch",
deleteFileWithHunkMarkerV2.stderr.includes("ignored extra MiniMax-style hunk/body lines after Delete File bad-delete.txt")
&& deleteFileWithHunkMarkerV2.stderr.includes("Delete File only needs the header"),
"v2 MiniMax-style Delete File compatibility should still hint the canonical syntax",
deleteFileWithHunkMarkerV2.stderr,
);
assertCondition(deleteFileWithHunkMarkerV2.files["bad-delete.txt"] === "content\n", "v2 Delete File parse failures must not delete remote files", deleteFileWithHunkMarkerV2);
assertCondition(!deleteFileWithHunkMarkerV2.commands.some((command) => command.startsWith("delete")), "v2 Delete File parse failures must not issue remote delete operations", deleteFileWithHunkMarkerV2.commands);
assertCondition(deleteFileWithHunkMarkerV2.files["bad-delete.txt"] === undefined, "v2 MiniMax-style Delete File compatibility should delete the file", deleteFileWithHunkMarkerV2);
const sequentialCompoundV2 = await applyPatchV2Fixture([
"*** Begin Patch",