@@ -66,13 +66,12 @@ def _normalize_scoped_path(
6666 return normalized
6767
6868
69- def _validate_read_bounds (offset : int , limit : int ) -> None :
69+ def _validate_read_bounds (offset : int , limit : int ) -> int :
7070 if offset < 0 :
7171 raise ModelRetry ("invalid_patch: offset must be >= 0" )
7272 if limit < 1 :
7373 raise ModelRetry ("invalid_patch: limit must be >= 1" )
74- if limit > READ_MAX_LIMIT :
75- raise ModelRetry (f"invalid_patch: limit must be <= { READ_MAX_LIMIT } " )
74+ return min (limit , READ_MAX_LIMIT )
7675
7776
7877def _format_numbered_line (line_number : int , content : str ) -> str :
@@ -229,7 +228,7 @@ async def updater_read_file(
229228 limit : int = READ_MAX_LIMIT ,
230229) -> str :
231230 """Read file content with absolute path guard and truncation."""
232- _validate_read_bounds (offset , limit )
231+ effective_limit = _validate_read_bounds (offset , limit )
233232 target = _normalize_scoped_path (ctx , file_path = file_path , allow_missing = False )
234233 if target .is_dir ():
235234 raise ModelRetry (f"not_found: path is a directory '{ target .as_posix ()} '" )
@@ -241,7 +240,7 @@ async def updater_read_file(
241240 total_lines += 1
242241 if total_lines <= offset :
243242 continue
244- if len (output_lines ) >= limit :
243+ if len (output_lines ) >= effective_limit :
245244 break
246245 output_lines .append (
247246 _format_numbered_line (total_lines , raw_line .rstrip ("\r \n " ))
@@ -294,15 +293,17 @@ async def updater_edit_file(
294293
295294 replace_count = expected_replacements if expected_replacements is not None else 1
296295 updated_content = existing_content .replace (old_string , new_string , replace_count )
296+ changed = updated_content != existing_content
297297
298- async with aiofiles .open (target , mode = "w" , encoding = "utf-8" ) as handle :
299- await handle .write (updated_content )
298+ if changed :
299+ async with aiofiles .open (target , mode = "w" , encoding = "utf-8" ) as handle :
300+ await handle .write (updated_content )
300301
301302 return {
302303 "file_path" : target .as_posix (),
303- "changed" : updated_content != existing_content ,
304+ "changed" : changed ,
304305 "replacements" : replace_count ,
305- "status" : "updated" ,
306+ "status" : "updated" if changed else "unchanged" ,
306307 }
307308
308309
@@ -314,6 +315,7 @@ async def updater_apply_patch(
314315 operations = _parse_patch_operations (patch_text )
315316
316317 planned_writes : dict [Path , str | None ] = {}
318+ skipped_noop_files = 0
317319 for operation in operations :
318320 scoped = _normalize_scoped_path (
319321 ctx ,
@@ -330,6 +332,16 @@ async def updater_apply_patch(
330332 continue
331333
332334 if operation .operation_type == "add" :
335+ if scoped .exists () and scoped .is_dir ():
336+ raise ModelRetry (
337+ f"not_found: add target is a directory '{ scoped .as_posix ()} '"
338+ )
339+ if scoped .exists ():
340+ async with aiofiles .open (scoped , mode = "r" , encoding = "utf-8" ) as handle :
341+ existing_content = await handle .read ()
342+ if existing_content == operation .add_content :
343+ skipped_noop_files += 1
344+ continue
333345 planned_writes [scoped ] = operation .add_content
334346 continue
335347
@@ -344,6 +356,9 @@ async def updater_apply_patch(
344356 existing_content ,
345357 chunks = operation .chunks ,
346358 )
359+ if updated_content == existing_content :
360+ skipped_noop_files += 1
361+ continue
347362 planned_writes [scoped ] = updated_content
348363 continue
349364
@@ -364,4 +379,5 @@ async def updater_apply_patch(
364379 "status" : "applied" ,
365380 "operations" : len (operations ),
366381 "changed_files" : changed_files ,
382+ "skipped_noop_files" : skipped_noop_files ,
367383 }
0 commit comments