{"record":{"id":"061e3e053afc5e9d","repo":"oraios/serena","slug":"the-content-of-path-changed-while-replacing-occ","errorCode":null,"errorMessage":"The content of {path} changed while replacing (occurrence {e} no longer resolves); the file was NOT modified. Re-run with dry_run=True for current ids.","messagePattern":"The content of (.+?) changed while replacing \\(occurrence (.+?) no longer resolves\\); the file was NOT modified\\. Re-run with dry_run=True for current ids\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/serena/tools/file_tools.py","lineNumber":441,"sourceCode":"        needle: str,\n        repl: str,\n    ) -> str:\n        occurrences_by_file: dict[str, list[ReplacementOccurrence]] = {}\n        for occ in occurrences:\n            occurrences_by_file.setdefault(occ.relative_path, []).append(occ)\n        with self.DiagnosticsContext(self, *occurrences_by_file.keys()) as diagnostics_context:\n            code_editor = self.create_code_editor()\n            for path, file_occurrences in occurrences_by_file.items():\n                with EditedFileContext(path, code_editor) as context:\n                    original_content = context.get_original_content()\n                    if original_content != contents[path]:\n                        # the editor's view differs from what was scanned (e.g. line-ending normalization);\n                        # re-derive the occurrences from the authoritative content and re-validate by id\n                        fresh_by_id = {o.occurrence_id: o for o in replacer.find_occurrences([(path, original_content)], needle, repl)}\n                        try:\n                            file_occurrences = [fresh_by_id[o.occurrence_id] for o in file_occurrences]\n                        except KeyError as e:\n                            raise ValueError(\n                                f\"The content of {path} changed while replacing (occurrence {e} no longer resolves); \"\n                                f\"the file was NOT modified. Re-run with dry_run=True for current ids.\"\n                            ) from e\n                    context.set_updated_content(replacer.apply_to_content(original_content, file_occurrences))\n            per_file = \"\\n\".join(f\"  {path}: {len(occs)}\" for path, occs in occurrences_by_file.items())\n            summary = f\"Replaced {len(occurrences)} occurrence(s) in {len(occurrences_by_file)} file(s):\\n{per_file}\"\n            return diagnostics_context.format_result(summary)\n\n\nclass DeleteLinesTool(EditingToolWithDiagnostics, ToolMarkerOptional):\n    \"\"\"\n    Deletes a range of lines within a file.\n    \"\"\"\n\n    def apply(\n        self,\n        relative_path: str,\n        start_line: int,","sourceCodeStart":423,"sourceCodeEnd":459,"githubUrl":"https://github.com/oraios/serena/blob/7fcbca7e62555ec2287ddb2f083caee805848ea6/src/serena/tools/file_tools.py#L423-L459","documentation":"During _apply_occurrences, Serena re-derives occurrences from the authoritative file content (to tolerate line-ending normalization) and re-validates the previously selected occurrences by id. If an id no longer resolves, the file content changed concurrently; the tool raises this ValueError and leaves the file unmodified rather than producing a corrupted edit.","triggerScenarios":"The file was edited (manually, by an editor with autosave/format-on-save, or by another agent/process) between the scan and the apply; line-ending conversion altered the content beyond what normalization can reconcile; occurrence ids from a stale dry run are replayed against changed content.","commonSituations":"Agents running replacements while a language server or formatter rewrites files; concurrent automation pipelines touching the same file; applying replacements in a worktree while an IDE buffer is being saved.","solutions":["Re-run the whole operation with dry_run=True to obtain current occurrence ids, then apply with the fresh ids","Ensure no formatter/autosave or concurrent process modifies the file during the replacement","Retry after the external writer finishes; re-scan content first to confirm it is stable","Apply replacements serially per file instead of in parallel with other editors"],"exampleFix":"// before\nagent.replace_content(needle=\"foo\", repl=\"bar\", occurrence_ids=stale_ids)\n# ValueError: content changed while replacing...\n// after\ndry = agent.replace_content(needle=\"foo\", repl=\"bar\", dry_run=True)\nagent.replace_content(needle=\"foo\", repl=\"bar\",\n                      occurrence_ids=[o.id for o in dry.occurrences])","handlingStrategy":"retry","validationCode":"content_before = open(abs_path, 'rb').read()\n# ... run replacement ...\ncontent_after = open(abs_path, 'rb').read()\nassert content_before == content_after or replacement_succeeded","typeGuard":"def file_stable(path: str, settle_secs: float = 0.2) -> bool:\n    import time\n    a = open(path, 'rb').read(); time.sleep(settle_secs)\n    return open(path, 'rb').read() == a","tryCatchPattern":"try:\n    agent.replace_content(needle, repl, occurrence_ids=ids)\nexcept ValueError as e:\n    if \"changed while replacing\" in str(e):\n        dry = agent.replace_content(needle, repl, dry_run=True)\n        agent.replace_content(needle, repl, occurrence_ids=[o.id for o in dry.occurrences])\n    else:\n        raise","preventionTips":["Disable format-on-save/autosave during automated replacements","Serialize file edits across agents and processes","Always refresh occurrence ids from a dry run right before applying"],"tags":["race-condition","concurrency","replace"],"backgroundTag":"file-changed-during-edit","analyzedSha":"7fcbca7e62555ec2287ddb2f083caee805848ea6","analyzedAt":"2026-08-29T00:04:09.619Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}