From c655a2bf8758ef04f84caed01b2bbf83df242ba8 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 1 May 2025 15:37:37 -0700 Subject: [PATCH] fix: infinite loop in TS library We observed that GPT 4.1 sometimes omits the space afte `@@` in edit chunks. This causes an infinite loop in the TS library. This change make the parser a little more robust to this case. --- codex-cli/src/utils/agent/apply-patch.ts | 22 ++++++++++------------ codex-cli/tests/apply-patch.test.ts | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/codex-cli/src/utils/agent/apply-patch.ts b/codex-cli/src/utils/agent/apply-patch.ts index 525404c30..b9f940870 100644 --- a/codex-cli/src/utils/agent/apply-patch.ts +++ b/codex-cli/src/utils/agent/apply-patch.ts @@ -95,6 +95,8 @@ export class DiffError extends Error {} // Parser (patch text -> Patch) // ----------------------------------------------------------------------------- +const CHUNK_DELIMITER = '@@'; + class Parser { current_files: Record; lines: Array; @@ -200,16 +202,12 @@ class Parser { END_OF_FILE_PREFIX, ]) ) { - const defStr = this.read_str("@@ "); - let sectionStr = ""; - if (!defStr && this.lines[this.index] === "@@") { - sectionStr = this.lines[this.index]!; - this.index += 1; - } - if (!(defStr || sectionStr || index === 0)) { - throw new DiffError(`Invalid Line:\n${this.lines[this.index]}`); - } - if (defStr.trim()) { + const sectionStr = this.read_str("@@", true); + const defStr = sectionStr.slice(CHUNK_DELIMITER.length).trim(); + if (!(sectionStr || index === 0)) { + throw new DiffError(`Invalid Line:\n${this.lines[this.index]}`); + } + if (defStr) { let found = false; // ------------------------------------------------------------------ // Equality helpers using the canonicalisation from find_context_core. @@ -261,11 +259,11 @@ class Parser { !found && !fileLines .slice(0, index) - .some((s) => canonLocal(s.trim()) === canonLocal(defStr.trim())) + .some((s) => canonLocal(s.trim()) === canonLocal(defStr)) ) { for (let i = index; i < fileLines.length; i++) { if ( - canonLocal(fileLines[i]!.trim()) === canonLocal(defStr.trim()) + canonLocal(fileLines[i]!.trim()) === canonLocal(defStr) ) { index = i + 1; this.fuzz += 1; diff --git a/codex-cli/tests/apply-patch.test.ts b/codex-cli/tests/apply-patch.test.ts index e1532c01b..ffbe90e97 100644 --- a/codex-cli/tests/apply-patch.test.ts +++ b/codex-cli/tests/apply-patch.test.ts @@ -344,3 +344,21 @@ test("apply_commit correctly performs move / rename operations", () => { expect(writes).toEqual({ "new.txt": "new" }); expect(removals).toEqual(["old.txt"]); }); + +test("text_to_patch + patch_to_commit tolerant to missing edit space", () => { + const originalFiles = { + "a.txt": "section1\n[old code1]\nsection2\n[old code2]", + }; + + const patch = `*** Begin Patch\n*** Update File: a.txt\n@@section1\n-[old code1]\n+[new code1}\n@@section2\n-[old code2]\n+[new code2}\n*** End Patch`; + + const [parsedPatch] = text_to_patch(patch, originalFiles); + const commit = patch_to_commit(parsedPatch, originalFiles).changes; + + expect(commit["a.txt"]).toEqual({ + type: ActionType.UPDATE, + move_path: undefined, + old_content: "section1\n[old code1]\nsection2\n[old code2]", + new_content: "section1\n[new code1}\nsection2\n[new code2}", + }); +});