Skip to content

Bug #12: batch_operations Edit Discarded File Content

Status: RESOLVED in v3.14.2 Category: Correctness / Data Loss Severity: High (silent data corruption on every batch edit) Resolution Date: 2026-02-26

Every batch_operations call containing an operation of type: "edit" silently corrupted the target file.

A user attempted a batch edit with special-character text (quotes, escaped newlines):

{
"operations": [
{
"type": "edit",
"path": "src/config.go",
"old_text": "const name = \"old\";",
"new_text": "const name = \"new\";"
}
]
}

After the operation, the file no longer contained its original content. Instead, it contained only the value of new_text — a single line — and everything else in the file was gone.

The symptom appeared to be an escaping problem (literal \" in the file, garbage lines, the compiler treating the whole file as line 1), but the real cause was deeper.

executeEdit in core/batch_operations.go was an unfinished placeholder. The function read the file into content but immediately discarded it, then wrote op.NewText as the entire file:

// BEFORE (broken)
func (m *BatchOperationManager) executeEdit(op FileOperation, result *OperationResult) error {
content, err := os.ReadFile(op.Path)
if err != nil {
return err
}
originalSize := len(content)
// TODO: Implement more sophisticated edit logic
// For now, simple replacement
newContent := []byte(op.NewText) // ← op.OldText ignored, content discarded
err = os.WriteFile(op.Path, newContent, 0644)
...
}
  • content (the actual file) was read and never used
  • op.OldText was never checked or matched
  • The entire file was replaced with the raw value of op.NewText
  • No error was returned — the operation reported success

The // TODO: comment in the source was the only hint that the implementation was incomplete.

Every single batch_operations call with type: "edit", regardless of content. The function was broken by design — it was a stub left in place.

Why the Symptom Looked Like an Escaping Bug

Section titled “Why the Symptom Looked Like an Escaping Bug”

Because new_text was often a single expression or line (e.g. const name = "new";), the resulting file was one short line instead of the original hundreds of lines. When the compiler then read the file, all the original code was missing and it emitted cascading errors that superficially resembled encoding or escape issues.


Replaced the placeholder body with a proper find-and-replace on the existing file content:

// AFTER (fixed)
func (m *BatchOperationManager) executeEdit(op FileOperation, result *OperationResult) error {
content, err := os.ReadFile(op.Path)
if err != nil {
return err
}
original := string(content)
newContent := strings.Replace(original, op.OldText, op.NewText, 1)
if newContent == original {
return fmt.Errorf("old_text not found in file: %s", op.Path)
}
err = os.WriteFile(op.Path, []byte(newContent), 0644)
if err == nil {
result.BytesAffected = int64(len(newContent) - len(original))
}
return err
}

Key changes:

  • original is preserved and used as the base for the replacement
  • strings.Replace(..., 1) replaces the first occurrence of op.OldText with op.NewText
  • If op.OldText is not present in the file, an explicit error is returned instead of silently writing incomplete content
  • BytesAffected now correctly reflects the net byte delta (can be negative)

JSON escapes (\", \n, \\) were already handled correctly by json.Unmarshal in the tool handler — there was no secondary escaping bug.


FileChange
core/batch_operations.goexecuteEdit: replaced TODO placeholder with strings.Replace; added "strings" import

go build ./... → OK (no errors)
go test ./tests/... → PASS
go test ./core/... → PASS

  1. TODO stubs that return success are dangerous: The function reported success: true and even computed BytesAffected — nothing in the caller signaled that the edit was wrong. A stub that returns fmt.Errorf("not implemented") would have surfaced the bug immediately.
  2. Symptoms can mask root causes: What appeared to be a character-encoding issue was actually a full file replacement. Checking the file size or line count after the operation would have revealed the real problem instantly.
  3. batch_operations is not a thin wrapper: Each operation type needs its own correct implementation — it cannot simply delegate to op.NewText as content.