Skip to content

fix(diff): repair truncated Grok diffs with missing markers (#186)#230

Open
proyectoauraorg wants to merge 2 commits into
Zoo-Code-Org:mainfrom
proyectoauraorg:fix/diff-parser-grok-truncation
Open

fix(diff): repair truncated Grok diffs with missing markers (#186)#230
proyectoauraorg wants to merge 2 commits into
Zoo-Code-Org:mainfrom
proyectoauraorg:fix/diff-parser-grok-truncation

Conversation

@proyectoauraorg
Copy link
Copy Markdown
Contributor

@proyectoauraorg proyectoauraorg commented May 21, 2026

Summary

Fixes #186. Grok (4.3) frequently truncates streamed diffs mid-output, so a <<<<<<< SEARCH block can arrive without its ======= separator and/or >>>>>>> REPLACE closer. The parser then rejects the whole edit with "Expected '=======' was not found" and the user sees "Unable to apply diff".

This adds a small recovery layer, repairTruncatedDiff(), called at the start of applyDiff() that reconstructs the missing markers so a salvageable diff can still be applied.

How it works

For each <<<<<<< SEARCH block the repair step:

  • leaves complete blocks (both ======= and >>>>>>> REPLACE) untouched, verbatim;
  • if only >>>>>>> REPLACE is missing, appends it;
  • if both markers are missing, treats the first line after SEARCH as the search content and the rest as the replacement, then inserts ======= and >>>>>>> REPLACE;
  • preserves escaped markers via (?<!\\) look-behinds (won't touch \<<<<<<< content);
  • when a repaired block is followed by more blocks, re-adds an inter-block separator so the appended >>>>>>> REPLACE never gets glued to the next <<<<<<< SEARCH.

Complete, valid diffs pass through unchanged, so behavior for well-formed input is identical.

Testing

Added coverage in multi-search-replace.spec.ts (complete-diff passthrough, missing closer, missing both markers, empty-search edge case, multi-block truncation, no spurious trailing newline).

vitest run core/diff/strategies/__tests__/multi-search-replace.spec.ts
# Test Files 1 passed (1) · Tests 65 passed (65)

Files

  • src/core/diff/strategies/multi-search-replace.tsrepairTruncatedDiff() + call in applyDiff()
  • src/core/diff/strategies/__tests__/multi-search-replace.spec.ts — tests

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced handling of incomplete or truncated diffs. The system now automatically detects and repairs missing merge-conflict markers and separators. Truncated or incomplete outputs are reconstructed into complete, valid diff structures, ensuring they are correctly processed and applied reliably across all use cases.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 21, 2026

📝 Walkthrough

Walkthrough

This PR adds automatic repair of truncated merge-conflict diffs to handle AI model output that cuts off mid-stream. A new private repairTruncatedDiff() method detects incomplete diff blocks and reconstructs them by appending missing markers. The repair step is integrated into applyDiff() before validation, with comprehensive unit tests and regression tests for issue #186.

Changes

Truncated Diff Repair

Layer / File(s) Summary
Repair mechanism implementation
src/core/diff/strategies/multi-search-replace.ts
repairTruncatedDiff() private method splits diff content by <<<<<<< SEARCH markers, detects missing ======= or >>>>>>> REPLACE closers, and reconstructs incomplete blocks by extracting SEARCH/REPLACE content and appending the missing markers.
Integration into diff application
src/core/diff/strategies/multi-search-replace.ts
applyDiff() preprocesses diffContent by calling repairTruncatedDiff() before validateMarkerSequencing(), ensuring truncated diffs are repaired before validation.
Unit tests for repair scenarios
src/core/diff/strategies/__tests__/multi-search-replace.spec.ts
Test suite validates the repair mechanism handles complete diffs unchanged, repairs missing markers, handles partial truncation with complete blocks following, manages empty search content, and preserves existing trailing newlines.
Regression tests for Grok truncation (#186)
src/core/diff/strategies/__tests__/multi-search-replace.spec.ts
End-to-end tests via applyDiff() confirm that diffs truncated at the closing >>>>>>> REPLACE marker and before the ======= separator are successfully applied, and well-formed multi-block diffs remain unchanged.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

A truncated diff came tumbling down,
AI models left things broken in town.
But a repair beast stitched the markers right,
Healing lost diffs from dawn to night! 🐰✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding repair logic for truncated Grok diffs with missing markers, directly addressing the issue number.
Description check ✅ Passed The description is comprehensive and well-structured, covering the problem, solution approach, implementation details, testing approach, and affected files. It directly addresses the required template sections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 21, 2026

Codecov Report

❌ Patch coverage is 96.00000% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/core/diff/strategies/multi-search-replace.ts 96.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

@edelauna edelauna left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a fixture based e2e test which reproduces the issue / acts as a regression guard.

proyectoauraorg and others added 2 commits May 21, 2026 09:34
…Zoo-Code-Org#186)

Grok frequently truncates streamed diffs, leaving SEARCH blocks without the
======= separator and/or the >>>>>>> REPLACE closer, which makes applyDiff
fail with 'Expected ======= was not found'. repairTruncatedDiff() detects
incomplete blocks and reinserts the missing markers while preserving valid
blocks and escaped markers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…fs (Zoo-Code-Org#186)

Per review feedback: end-to-end applyDiff() regression guards using realistic
truncated-Grok fixtures (missing >>>>>>> REPLACE, missing ======= separator),
plus a well-formed multi-block diff that must pass through unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@proyectoauraorg
Copy link
Copy Markdown
Contributor Author

Thanks @edelauna — added fixture-based regression tests that run through the full applyDiff() path with realistic truncated-Grok diffs: one missing the closing >>>>>>> REPLACE, one truncated before ======= (the exact "Expected '=======' was not found" case), and a well-formed multi-block diff that must pass through untouched. All pass; pushed in the latest commit.

@proyectoauraorg proyectoauraorg force-pushed the fix/diff-parser-grok-truncation branch from af855d3 to 8c86955 Compare May 21, 2026 15:37
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/core/diff/strategies/multi-search-replace.ts (1)

324-332: 💤 Low value

Reassigning the diffContent parameter.

Minor: prefer a local const repaired = this.repairTruncatedDiff(diffContent) and use repaired downstream rather than mutating the parameter — keeps the original input observable for logging/diagnostics later.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/diff/strategies/multi-search-replace.ts` around lines 324 - 332, The
applyDiff method currently reassigns the diffContent parameter with
this.repairTruncatedDiff(diffContent); instead, call
this.repairTruncatedDiff(diffContent) into a new local const (e.g., const
repairedDiff = this.repairTruncatedDiff(diffContent)) and use repairedDiff for
all downstream logic inside applyDiff (while leaving the original diffContent
parameter untouched) so the original input remains available for
logging/diagnostics; update all references within applyDiff that currently use
diffContent after the repair point to use the new local variable (refer to
applyDiff and repairTruncatedDiff).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/diff/strategies/multi-search-replace.ts`:
- Around line 294-318: The repair branch mis-handles blocks that contain a
closer without a separator and blocks that include Grok header directives;
update the logic in multi-search-replace.ts around the variables
hasSeparator/hasCloser/block/repaired so that if hasCloser && !hasSeparator you
do not synthesize a second ">>>>>>> REPLACE" but instead insert a "======="
immediately before the existing closer (i.e., locate the existing ">>>>>>>
REPLACE" and splice in "=======\n" before it); additionally, before applying the
firstNewlineIdx / searchMatch heuristic (the code that computes content,
firstNewlineIdx, searchContent, replaceContent) strip any leading directive
lines like ":start_line:\d+", ":end_line:\d+" and lines of "-------" so the
first-line-is-SEARCH heuristic sees only real content and not header metadata.

---

Nitpick comments:
In `@src/core/diff/strategies/multi-search-replace.ts`:
- Around line 324-332: The applyDiff method currently reassigns the diffContent
parameter with this.repairTruncatedDiff(diffContent); instead, call
this.repairTruncatedDiff(diffContent) into a new local const (e.g., const
repairedDiff = this.repairTruncatedDiff(diffContent)) and use repairedDiff for
all downstream logic inside applyDiff (while leaving the original diffContent
parameter untouched) so the original input remains available for
logging/diagnostics; update all references within applyDiff that currently use
diffContent after the repair point to use the new local variable (refer to
applyDiff and repairTruncatedDiff).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ed8cc68c-09f7-4fd2-993c-cf40d9920974

📥 Commits

Reviewing files that changed from the base of the PR and between 166bc3f and 8c86955.

📒 Files selected for processing (2)
  • src/core/diff/strategies/__tests__/multi-search-replace.spec.ts
  • src/core/diff/strategies/multi-search-replace.ts

Comment on lines +294 to +318
if (hasSeparator && !hasCloser) {
// Has ======= but missing >>>>>>> REPLACE — append closing marker
const body = block.replace(/\s+$/, "")
repaired += body + "\n>>>>>>> REPLACE" + separator
} else {
// Missing both ======= and >>>>>>> REPLACE
const searchMatch = block.match(/^<<<<<<< SEARCH\n?([\s\S]*)$/)
const content = (searchMatch?.[1] ?? "").replace(/\s+$/, "")
const firstNewlineIdx = content.indexOf("\n")
if (firstNewlineIdx !== -1) {
// First line is SEARCH content, rest is REPLACE content
const searchContent = content.substring(0, firstNewlineIdx)
const replaceContent = content.substring(firstNewlineIdx + 1)
repaired +=
"<<<<<<< SEARCH\n" +
searchContent +
"\n=======\n" +
replaceContent +
"\n>>>>>>> REPLACE" +
separator
} else {
// Single line — treat as empty SEARCH with content as REPLACE
repaired += "<<<<<<< SEARCH\n=======\n" + content + "\n>>>>>>> REPLACE" + separator
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Repair branch can mis-handle blocks that drop ======= but keep >>>>>>> REPLACE, or contain :start_line: / ------- directives.

Two edge cases in the "needs repair" branch are worth a guard:

  1. !hasSeparator && hasCloser (block has >>>>>>> REPLACE but no =======) falls into the else branch at line 298, which appends another >>>>>>> REPLACE. The original closer ends up inside the synthesized REPLACE content, producing a doubly-closed block downstream.
  2. When both markers are missing and the block still includes :start_line:N and/or ------- directives (a partial structured SEARCH header that Grok had emitted before truncation), the "first newline = search content" heuristic at lines 302–306 treats :start_line:5 as the SEARCH body, dropping ------- and the real content into REPLACE. The repaired diff then fails the marker validation or matches the wrong location.

Consider stripping any :start_line: / :end_line: / ------- header lines before applying the first-newline heuristic, and explicitly handling the hasCloser && !hasSeparator case (e.g., insert ======= just before the existing >>>>>>> REPLACE instead of synthesizing a new closer).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/diff/strategies/multi-search-replace.ts` around lines 294 - 318, The
repair branch mis-handles blocks that contain a closer without a separator and
blocks that include Grok header directives; update the logic in
multi-search-replace.ts around the variables
hasSeparator/hasCloser/block/repaired so that if hasCloser && !hasSeparator you
do not synthesize a second ">>>>>>> REPLACE" but instead insert a "======="
immediately before the existing closer (i.e., locate the existing ">>>>>>>
REPLACE" and splice in "=======\n" before it); additionally, before applying the
firstNewlineIdx / searchMatch heuristic (the code that computes content,
firstNewlineIdx, searchContent, replaceContent) strip any leading directive
lines like ":start_line:\d+", ":end_line:\d+" and lines of "-------" so the
first-line-is-SEARCH heuristic sees only real content and not header metadata.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Grok 4.3 "Unable to apply diff" - Expected '=======' was not found.

2 participants