improve: text_editor add mtime checks and prompts for patch read validation

linuztx committed Feb 28, 2026 at 01:55 UTC a5a1e02bad341b6f2f3118844579245dd76be082
3 files changed +43 -1
plugins/text_editor/prompts/fw.text_editor.patch_need_read.md new
+1
@@ -0,0 +1 @@
1 +error patching {{path}}: read file before patching - line numbers unknown
plugins/text_editor/prompts/fw.text_editor.patch_stale_read.md new
+1
@@ -0,0 +1 @@
1 +error patching {{path}}: file changed since last read - re-read to get current line numbers
plugins/text_editor/tools/text_editor.py
+41 -1
@@ -10,6 +10,10 @@ from plugins.text_editor.helpers.file_ops import (
10 apply_patch,
11 )
12
13 +# Key used in agent.data to store file mtimes
14 +_MTIME_KEY = "_text_editor_mtimes"
15 +
16 +
17
18 class TextEditor(Tool):
19
@@ -49,6 +53,8 @@ class TextEditor(Tool):
53 if result.error:
54 return self._error("read", path, result.error)
55
56 + _record_mtime(self.agent, os.path.expanduser(path))
57 +
58 # Extension point
59 ext_data = {"content": result.content, "warnings": result.warnings}
60 await call_extensions(
@@ -89,6 +95,8 @@ class TextEditor(Tool):
95 )
96
97 expanded = os.path.expanduser(path)
98 + _record_mtime(self.agent, expanded)
99 +
100 cfg = _get_config(self.agent)
101 read_result = read_file(
102 expanded,
@@ -117,6 +125,11 @@ class TextEditor(Tool):
125 if not os.path.isfile(expanded):
126 return self._error("patch", path, "file not found")
127
128 + stale_err = _check_mtime(self.agent, expanded)
129 + if stale_err:
130 + return self._error("patch", path, stale_err)
131 +
132 +
133 parsed, err = validate_edits(edits)
134 if err:
135 return self._error("patch", path, err)
@@ -138,6 +151,8 @@ class TextEditor(Tool):
151 data={"path": expanded, "total_lines": total_lines},
152 )
153
154 + _record_mtime(self.agent, expanded)
155 +
156 patch_content = _read_patch_region(
157 expanded, ext_data["edits"], total_lines, _get_config(self.agent)
158 )
@@ -168,7 +183,6 @@ class TextEditor(Tool):
183 def _read_patch_region(
184 path: str, edits: list[dict], total_lines: int, cfg: dict
185 ) -> str:
171 - """Read back the affected region after a patch so the LLM sees the result."""
186 if not edits:
187 return ""
188
@@ -194,6 +208,32 @@ def _read_patch_region(
208 )
209 return result.content
210
211 +
212 +def _record_mtime(agent, path: str):
213 + mtimes = agent.data.setdefault(_MTIME_KEY, {})
214 + try:
215 + mtimes[os.path.realpath(path)] = os.path.getmtime(path)
216 + except OSError:
217 + pass
218 +
219 +
220 +def _check_mtime(agent, path: str) -> str:
221 + mtimes = agent.data.get(_MTIME_KEY, {})
222 + real = os.path.realpath(path)
223 + if real not in mtimes:
224 + return agent.read_prompt(
225 + "fw.text_editor.patch_need_read.md", path=path
226 + )
227 + try:
228 + current = os.path.getmtime(path)
229 + except OSError:
230 + return ""
231 + if current != mtimes[real]:
232 + return agent.read_prompt(
233 + "fw.text_editor.patch_stale_read.md", path=path
234 + )
235 + return ""
236 +
237 # ------------------------------------------------------------------
238 # Config
239 # ------------------------------------------------------------------