fix(file-ops): a V4A patch keeps terminal escape bytes on lines it never touched

read_file_raw feeds the V4A write-back, and it ran the whole file through the
terminal fence-leak cleanup, which deletes every OSC sequence and BEL byte. A
prompt script's title escape or a beep line was silently rewritten on apply, and
the diff and post-write hash both started from the stripped text, so nothing
showed it. A leaked wrapper always carries the fence marker: clean only those
lines when the content is going to be written back.
This commit is contained in:
John Paul Soliva
2026-09-24 02:37:59 +09:00
committed by Austin Pickett
parent ac2ffe60d0
commit 0a8c4f8540
3 changed files with 31 additions and 3 deletions

View File

@@ -371,6 +371,26 @@ class TestBomHandling:
assert b"print('world')" in raw
def test_v4a_update_keeps_terminal_escape_bytes_on_untouched_lines(self, ops, tmp_path: Path):
# read_file_raw feeds the V4A write-back; its leak cleanup must not eat the file's own
# OSC title escapes and BEL bytes on lines the patch never touched.
target = tmp_path / "prompt.sh"
original = (b'set_title() { printf "\x1b]0;%s\x07" "$1"; }\n'
b'beep() { printf "\x07"; }\n'
b'VERSION=1\n')
target.write_bytes(original)
patch = (
"*** Begin Patch\n"
f"*** Update File: {target}\n"
"@@\n"
"-VERSION=1\n"
"+VERSION=2\n"
"*** End Patch"
)
res = ops.patch_v4a(patch)
assert res.success, res.error
assert target.read_bytes() == original.replace(b"VERSION=1", b"VERSION=2")
class TestProtectedInstructionFiles:
"""Writes to agent-instruction files ALWAYS require approval.

View File

@@ -1085,7 +1085,8 @@ class ShellFileOperations(LintMixin, SearchMixin, FileOperations):
return ReadResult(error=f"Failed to read file: {cat_result.stdout}")
# Strip a leading BOM (a phantom U+FEFF defeats an exact first-line match);
# write_file re-probes disk and restores it.
raw_content, _ = _strip_bom(_strip_terminal_fence_leaks(cat_result.stdout))
# V4A writes this back, so only lines carrying a leaked fence are cleaned.
raw_content, _ = _strip_bom(_strip_terminal_fence_leaks(cat_result.stdout, fenced_lines_only=True))
return ReadResult(content=raw_content, file_size=file_size)
def read_file_bytes(self, path: str, max_bytes: Optional[int] = None) -> ReadResult:

View File

@@ -204,13 +204,20 @@ def count_conflict_blocks(formatted_content: str) -> int:
return min(opens, len(_CONFLICT_CLOSE.findall(formatted_content))) if opens else 0
def _strip_terminal_fence_leaks(text: str) -> str:
def _strip_terminal_fence_leaks(text: str, *, fenced_lines_only: bool = False) -> str:
"""Strip leaked terminal fence wrappers (OSC sequences, fence markers) from
command output; drops lines that were nothing but wrapper."""
command output; drops lines that were nothing but wrapper.
``fenced_lines_only`` is for file content that gets written back: a leak always carries the
fence marker, so a line without one is the file's own bytes (a prompt script's ``\x1b]0;``
title escape, a BEL) and is kept verbatim."""
if not text:
return text
cleaned_lines: List[str] = []
for line in text.splitlines(keepends=True):
if fenced_lines_only and "__HERMES_FENCE_" not in line:
cleaned_lines.append(line)
continue
had_terminal_wrapper = "__HERMES_FENCE_" in line or "\x1b]" in line
cleaned = _FENCE_MARKER_RE.sub("", _OSC_SEQUENCE_RE.sub("", line)).replace("\x07", "")
if had_terminal_wrapper and cleaned.strip("'\r\n\t ") == "":