# NousResearch/hermes-agent · Fuzzy match deletes boundary space in replacements

## Open now

- Nothing open on GitHub.

A bug where whitespace-normalized fuzzy matching silently drops the boundary space after a match during str_replace or patch operations. Multiple fix attempts address preserving that boundary space in fuzzy replacements.

## TL;DR

- 5 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 3 PR(s) closed without merging.
- A fix was merged to main in PR #52678. Verify it addresses your case.
- **Recommended:** read PR #52678 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-52678`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.40`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #52491: \[Bug\]: Fuzzy whitespace-normalized edit silently deletes the boundary space after a match (str_replace / patch corrupts file)

## Prior & competing PRs

- **#52502: fix(fuzzy-match): preserve boundary space after whitespace-normalized match (#52491)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#52678`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#52678: fix(fuzzy-match): preserve boundary space after whitespace-normalized match (#52491)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `tests/tools/test_fuzzy_match.py`, `tools/fuzzy_match.py`
  - fix hunks:
  - `tests/tools/test_fuzzy_match.py`

    ```diff
    @@ -43,6 +43,47 @@ def test_extra_spaces_match(self):
             assert count == 1
             assert "bar" in new
     
    +    def test_boundary_space_preserved_after_match(self):
    +        """Regression: whitespace_normalized match ending with a non-space
    +        character must NOT consume the word-boundary space that follows.
    +        https://github.com/NousResearch/hermes-agent/issues/52491"""
    +        # Case 1 — simple word boundary
    +        new, count, strategy, err = fuzzy_find_and_replace(
    +            "foo   bar baz", "foo bar", "XY",
    +        )
    +        assert err is None
    +        assert count == 1
    +        assert strategy == "whitespace_normalized"
    +        assert new == "XY baz", f"Boundary space deleted: {new!r}"
    +
    +    def test_boundary_space_preserved_in_code_edit(self):
    +        """Regression: real-world code-edit scenario where the space before
    +        the next operator must survive a whitespace-normalized match."""
    +        content = "result = compute(a,  b) + tail"
    +        new, count, strategy, err = fuzzy_find_and_replace(
    +            content, "compute(a, b)", "compute(a, b, c)",
    +        )
    +        assert err is None
    +        assert count == 1
    +        assert strategy == "whitespace_normalized"
    +        assert new == "result = compute(a, b, c) + tail", f"Boundary space deleted: {new!r}"
    +
    +    def test_trailing_ws_still_consumed_when_match_ends_with_space(self):
    +        """When the normalized match itself ends with whitespace (pattern has
    +        trailing space), the expansion must still consume the full whitespace
    +        run in the original."""
    +        # Use a pattern with trailing space where the boundary is clear:
    +        # content has "foo   " then "bar", pattern is "foo " — the match
    +        # should cover all 3 original spaces (the trailing ws run).
    +        new, count, strategy, err = fuzzy_find_and_replace(
    +            "a = foo   + bar", "foo +", "XY",
    +        )
    +        assert err is None
    +        assert count == 1
    +        # "foo   +" normalized to "foo +" matches; trailing spaces consumed
    +        # Result: "a = XY bar"
    +        assert "XY" in new and "bar" in new
    +
     
     class TestIndentDifference:
         def test_different_indentation(self):
    ```
  - `tools/fuzzy_match.py`

    ```diff
    @@ -768,9 +768,14 @@ def _map_normalized_positions(original: str, normalized: str,
             else:
                 orig_end = orig_start + (norm_end - norm_start)
             
    -        # Expand to include trailing whitespace that was normalized
    -        while orig_end < len(original) and original[orig_end] in ' \t':
    -            orig_end += 1
    +        # Expand to include trailing whitespace that was normalized,
    +        # but only when the normalized match itself ended with whitespace.
    +        # When the match ends with a non-space character, the first
    +        # whitespace in the original is a word boundary and must not be
    +        # consumed.  See https://github.com/NousResearch/hermes-agent/issues/52491
    +        if norm_end < len(normalized) and normalized[norm_end - 1] == ' ':
    +            while orig_end < len(original) and original[orig_end] in ' \t':
    +                orig_end += 1
             
             original_matches.append((orig_start, min(orig_end, len(original))))
         
    ```
- **#52513: fix(tools): preserve word-boundary space in whitespace-normalized edits**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#52678`, `#52502`
- **#52861: fix(tools): preserve boundary spaces in fuzzy replacements**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#52678`, `#52491`

## Fix status

- Merged to main in PR #52678 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-26`.
