fix: guide mxcx apply-patch stale context recovery

This commit is contained in:
Codex
2026-06-04 03:24:03 +00:00
parent e30c8cb1c3
commit 3d44acfb15
3 changed files with 56 additions and 3 deletions
+14 -1
View File
@@ -126,6 +126,7 @@ export function applyPatchV2HelpPayload() {
"A blank line in Add File is a line containing only +.",
"Update File uses @@ or @@ context markers, followed by context lines starting with one extra space prefix and changed lines starting with - or +; for a column-0 source line `const x`, write ` const x`, and for a two-space-indented source line write three spaces total. Unified-diff line-range headers are accepted with hints for MiniMax compatibility.",
"Prefer `trans <route> apply-patch < /tmp/patch.diff` for long patches, Windows paths, or quoting-sensitive content.",
"If `failed to find expected lines` reports stale or oversized context for a block/function replacement, re-read the exact current block and retry with a smaller Update File hunk or split hunks around unique anchors.",
"MiniMax compatibility: stray @@ or unprefixed content inside Add File, unprefixed Update File context lines, and extra hunk/body lines after Delete File, are accepted with stderr hints.",
"MiniMax/MXCX concatenated patch compatibility: repeated nested Begin Patch / End Patch markers are accepted with stderr hints, but the CLI still uses only the v2 engine and never auto-falls back to apply-patch-v1."
],
@@ -153,6 +154,7 @@ export function applyPatchV2HelpPayload() {
"Do not put @@ after `*** Add File:`; @@ is only for Update File.",
"Prefer canonical @@ or @@ context over unified diff headers such as `@@ -1,3 +1,4 @@`; v2 accepts those headers with a hint.",
"If multiple printf/heredoc fragments were concatenated, keep one outer Begin/End envelope or include complete nested envelopes; v2 will hint on nested markers instead of trying the legacy helper.",
"Do not recover from apply-patch context mismatch by switching to download/upload, remote Python/Perl/sed, or whole-file rewrites; retry with corrected v2 hunks.",
"Do not use remote Python/Perl/sed heredocs for text patches when `trans <route> apply-patch` is available."
],
note: "apply-patch reads patch text from stdin and uses the v2 engine by default. Use `apply-patch-v1` only for the legacy helper."
@@ -475,6 +477,9 @@ function appendExpectedLineDiagnostics(lines: string[], cause: Record<string, un
if (diagnostics.likelyMissingAddedPrefixes === true) {
lines.push("Hint: this hunk looks like a large insertion whose new lines were written as context. Prefix every inserted line with +, keep only a few real existing context lines before/after the insertion, and regenerate the patch instead of editing it with sed.");
}
if (diagnostics.likelyStaleOrOversizedContext === true) {
lines.push("Hint: this looks like stale or oversized context for a block/function replacement. Do not switch to download/upload, remote Python/Perl/sed, or a whole-file rewrite. Re-read the exact current block and retry apply-patch with a smaller hunk, or split the edit into hunks around unique anchors.");
}
}
function recordValue(value: unknown): Record<string, unknown> | null {
@@ -1044,13 +1049,15 @@ function expectedLineDiagnostics(originalLines: string[], chunk: UpdateChunk, pr
const firstExpectedLine = chunk.oldLines.find((line) => line.trim().length > 0) ?? "";
const firstExpectedLineCandidates = firstExpectedLine.length === 0 ? [] : candidateLineNumbers(originalLines, firstExpectedLine, 8);
const prefix = bestPrefixMatch(originalLines, chunk.oldLines, firstExpectedLine, preferredStart);
const missingAddedPrefixes = likelyMissingAddedPrefixes(chunk, prefix.matchedLines);
return {
firstExpectedLine,
firstExpectedLineCandidates,
firstExpectedLineCandidatesTruncated: firstExpectedLine.length > 0 && candidateLineNumbers(originalLines, firstExpectedLine, 9).length > firstExpectedLineCandidates.length,
bestPrefixMatchedLines: prefix.matchedLines,
bestPrefixStartLine: prefix.startLine,
likelyMissingAddedPrefixes: likelyMissingAddedPrefixes(chunk, prefix.matchedLines),
likelyMissingAddedPrefixes: missingAddedPrefixes,
likelyStaleOrOversizedContext: !missingAddedPrefixes && likelyStaleOrOversizedContext(chunk, prefix.matchedLines),
};
}
@@ -1093,6 +1100,12 @@ function likelyMissingAddedPrefixes(chunk: UpdateChunk, bestPrefixMatchedLines:
return bestPrefixMatchedLines > 0 && bestPrefixMatchedLines < chunk.oldLines.length;
}
function likelyStaleOrOversizedContext(chunk: UpdateChunk, bestPrefixMatchedLines: number): boolean {
if (chunk.oldLines.length < 4) return false;
if (bestPrefixMatchedLines < 2 || bestPrefixMatchedLines >= chunk.oldLines.length) return false;
return chunk.addedLineCount > 0 || chunk.deletedLineCount > 0 || chunk.contextLineCount >= 4;
}
function lineEquivalent(left: string, right: string): boolean {
return left === right || left.trimEnd() === right.trimEnd() || left.trim() === right.trim() || normalizeLine(left) === normalizeLine(right);
}
@@ -1078,6 +1078,46 @@ export async function runSshArgvGuidanceContract(): Promise<JsonRecord> {
missingPlusLargeInsertVisibleV2.stderr,
);
const staleBlockReplacementVisibleV2 = await applyPatchV2FixtureAttempt([
"*** Begin Patch",
"*** Update File: runner-trace.ts",
"@@",
" export async function waitForAgentResult(",
" initial: AgentChatResponse,",
" activityRef?: ActivityRefSource",
" ): Promise<AgentChatResponse> {",
" if (isTerminalStatus(initial.status)) {",
" return isTerminalStatus(initial.status) ? initial : null;",
" }",
"- return initial;",
"+ return pollAgentResult(initial, activityRef);",
" }",
"*** End Patch",
"",
].join("\n"), {
"runner-trace.ts": [
"export async function waitForAgentResult(",
" initial: AgentChatResponse,",
" activityRef?: ActivityRefSource",
"): Promise<AgentChatResponse> {",
" if (isTerminalStatus(initial.status)) return initial;",
" return pollAgentResult(initial);",
"}",
"",
].join("\n"),
}, { stderrOutput: true });
assertCondition(staleBlockReplacementVisibleV2.exitCode === 1 && staleBlockReplacementVisibleV2.error === null, "v2 should reject stale block-replacement hunks without falling back", staleBlockReplacementVisibleV2);
assertCondition(
staleBlockReplacementVisibleV2.stderr.includes("Best partial context match: 4 expected line(s) matched")
&& staleBlockReplacementVisibleV2.stderr.includes("stale or oversized context for a block/function replacement")
&& staleBlockReplacementVisibleV2.stderr.includes("Do not switch to download/upload")
&& staleBlockReplacementVisibleV2.stderr.includes("remote Python/Perl/sed")
&& staleBlockReplacementVisibleV2.stderr.includes("retry apply-patch with a smaller hunk")
&& staleBlockReplacementVisibleV2.stderr.includes("split the edit into hunks around unique anchors"),
"v2 stale block replacement failure should steer MiniMax back to smaller apply-patch hunks instead of file transfer or script rewrites",
staleBlockReplacementVisibleV2.stderr,
);
const fragmentedEnvelopeFailureV2 = await applyPatchV2FixtureAttempt([
"*** Begin Patch",
"*** Update File: fragment.txt",