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;