NousResearch/hermes-agent

Gateway MEDIA tag Windows file path parsing fixes

Cluster 35270 · gateway. Data as of

Nothing open on GitHub. Fix merged in #35388 on 30 May 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #35388. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #35388. That is separate from the fix event.
Still open
Nothing open on GitHub.

Multiple bug reports and fixes addressing failures in gateway regex logic to correctly parse Windows-style absolute paths, including drive letters and backslashes, in MEDIA tags.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #35388 fix(gateway): support Windows absolute paths in MEDIA tag regexes

    merged to main

    files: gateway/platforms/base.py, gateway/run.py, tests/gateway/test_extract_local_files.py, tests/gateway/test_platform_base.py, tests/gateway/test_run_tool_media_re.py

    Show the diff
    • gateway/platforms/base.py
      @@ -1191,10 +1191,12 @@ def _log_safe_path(path: str) -> str:
       # bare-path detector (extract_local_files) downstream rather than silently
       # deleted. Shared by the non-streaming dispatch path and the streaming
       # consumer so both behave identically.
      +# Path anchors: ``~/`` (Unix home-relative), ``/`` (Unix absolute),
      +# ``X:\\`` or ``X:/`` (Windows drive-letter absolute — #34632).
       MEDIA_TAG_CLEANUP_RE = re.compile(
           r'''[`"']?MEDIA:\s*'''
           r'''(?P<path>`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|'''
      -    r'''(?:~/|/)\S+(?:[^\S\n]+\S+)*?\.(?:''' + _MEDIA_EXT_ALTERNATION + r'''))'''
      +    r'''(?:~/|/|[A-Za-z]:[/\\])\S+(?:[^\S\n]+\S+)*?\.(?:''' + _MEDIA_EXT_ALTERNATION + r'''))'''
           r'''(?=[\s`"',;:)\]}]|$)[`"']?''',
           re.IGNORECASE,
       )
      @@ -2665,9 +2667,10 @@ def extract_local_files(content: str) -> Tuple[List[str], str]:
       
               # (?<![/:\w.]) prevents matching inside URLs (e.g. https://…/img.png)
               #             and relative paths (./foo.png)
      -        # (?:~/|/)    anchors to absolute or home-relative paths
      +        # (?:~/|/)    anchors to absolute or home-relative Unix paths
      +        # (?:[A-Za-z]:[/\\]) anchors to Windows drive-letter paths (#34632)
               path_re = re.compile(
      -            r'(?<![/:\w.])(?:~/|/)(?:[\w.\-]+/)*[\w.\-]+\.(?:' + ext_part + r')\b',
      +            r'(?<![/:\w.])(?:~/|/|[A-Za-z]:[/\\])(?:[\w.\-]+[/\\])*[\w.\-]+\.(?:' + ext_part + r')\b',
                   re.IGNORECASE,
               )
       
    • gateway/run.py
      @@ -17281,7 +17281,7 @@ def _clarify_callback_sync(question: str, choices) -> str:
                           _hc = _hm.get("content", "")
                           if "MEDIA:" in _hc:
                               _TOOL_MEDIA_RE = re.compile(
      -                            r'MEDIA:((?:/|~\/)\S+\.(?:png|jpe?g|gif|webp|'
      +                            r'MEDIA:((?:[A-Za-z]:[/\\]|/|~\/)\S+\.(?:png|jpe?g|gif|webp|'
                                   r'mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|'
                                   r'flac|epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|'
                                   r'txt|csv|apk|ipa))',
      @@ -17607,7 +17607,7 @@ def _approval_notify_sync(approval_data: dict) -> None:
                               content = msg.get("content", "")
                               if "MEDIA:" in content:
                                   _TOOL_MEDIA_RE = re.compile(
      -                                r'MEDIA:((?:/|~\/)\S+\.(?:png|jpe?g|gif|webp|'
      +                                r'MEDIA:((?:[A-Za-z]:[/\\]|/|~\/)\S+\.(?:png|jpe?g|gif|webp|'
                                       r'mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|'
                                       r'flac|epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|'
                                       r'txt|csv|apk|ipa))',
    • tests/gateway/test_extract_local_files.py
      @@ -336,9 +336,35 @@ def test_path_with_spaces_not_matched(self):
               paths, _ = _extract("File at /tmp/my file.png here")
               assert paths == []
       
      -    def test_windows_path_not_matched(self):
      -        """Windows-style paths should not match."""
      -        paths, _ = _extract("See C:\\Users\\test\\image.png")
      +    @pytest.mark.parametrize(
      +        "content,expected",
      +        [
      +            # Backslash separators (native Windows style)
      +            ("See C:\\Users\\test\\image.png here", "C:\\Users\\test\\image.png"),
      +            # Forward slashes with drive letter (common in cross-platform code)
      +            ("See C:/Users/test/image.png here", "C:/Users/test/image.png"),
      +            # Non-C: drive
      +            ("Video at D:/data/clip.mp4 ready", "D:/data/clip.mp4"),
      +            # Lowercase drive letter
      +            ("Path e:/audio/track.mp3 done", "e:/audio/track.mp3"),
      +        ],
      +    )
      +    def test_windows_drive_letter_paths_matched(self, content, expected):
      +        """Windows drive-letter paths (C:/..., C:\\...) must be detected (#34632).
      +
      +        Prior behavior anchored on (?:~/|/) only, which silently dropped
      +        Windows absolute paths so the agent's bare-path references were
      +        sent as text instead of native uploads.
      +        """
      +        paths, cleaned = _extract(content)
      +        assert paths == [expected]
      +        assert expected not in cleaned
      +
      +    def test_relative_windows_path_not_matched(self):
      +        """A bare Windows-style filename without a drive letter must still
      +        not match (e.g. ``foo\\bar.png`` is treated as relative, like its
      +        Unix sibling ``foo/bar.png``)."""
      +        paths, _ = _extract("File at foo\\bar.png here")
               assert paths == []
       
           def test_relative_path_not_matched(self):
    • tests/gateway/test_platform_base.py
      @@ -361,6 +361,45 @@ def test_both_directives_can_coexist(self):
               assert "[[audio_as_voice]]" not in cleaned
               assert "[[as_document]]" not in cleaned
       
      +    # Windows path support — regression coverage for #34632
      +
      +    def test_media_tag_windows_backslash_path(self):
      +        """extract_media should recognise Windows backslash paths."""
      +        media, cleaned = BasePlatformAdapter.extract_media(
      +            r"MEDIA:C:\Users\kotsu\file.pdf"
      +        )
      +        assert len(media) == 1
      +        assert media[0][0].endswith("file.pdf")
      +
      +    def test_media_tag_windows_forward_slash_path(self):
      +        """extract_media should recognise Windows forward-slash paths."""
      +        media, cleaned = BasePlatformAdapter.extract_media(
      +            "MEDIA:C:/Users/kotsu/file.pdf"
      +        )
      +        assert len(media) == 1
      +        assert media[0][0].endswith("file.pdf")
      +
      +    def test_media_tag_windows_drive_root(self):
      +        """extract_media should recognise a path at the drive root."""
      +        media, cleaned = BasePlatformAdapter.extract_media(
      +            r"MEDIA:D:\report.md"
      +        )
      +        assert len(media) == 1
      +        assert media[0][0].endswith("report.md")
      +
      +    def test_media_tag_unix_paths_still_work(self):
      +        """Unix absolute and tilde paths must still extract after Windows change."""
      +        for content in ["MEDIA:/tmp/audio.ogg", r"MEDIA:~/docs/notes.md"]:
      +            media, _ = BasePlatformAdapter.extract_media(content)
      +            assert len(media) == 1, f"Failed for: {content}"
      +
      +    def test_relative_path_still_ignored(self):
      +        """Relative Windows-style paths (no drive letter) must not match."""
      +        media, _ = BasePlatformAdapter.extract_media(
      +            r"MEDIA:Users\kotsu\file.pdf"
      +        )
      +        assert media == []
      +
       
       class TestMediaExtensionAllowlistParity:
           """Regression coverage for issue #34517 — the MEDIA: extension black hole.
    • tests/gateway/test_run_tool_media_re.py
      @@ -0,0 +1,147 @@
      +r"""Tests for _TOOL_MEDIA_RE regex patterns in gateway/run.py.
      +
      +Issue #34632: The _TOOL_MEDIA_RE patterns in GatewayRunner used (?:/|~\/) to
      +anchor paths, which only matched Unix-style absolute and home-relative paths.
      +Windows absolute paths (C:\\Users\\..., D:/...) were silently ignored, causing
      +MEDIA directive delivery to fail on Windows.
      +
      +Fix: Add [A-Za-z]:[/\\\\] as a third anchor alternative in both patterns.
      +
      +Two identical _TOOL_MEDIA_RE patterns exist in run.py:
      +1. History scanning (~L17223): collects already-seen media paths
      +2. Result scanning (~L17549): extracts new media tags from agent output
      +
      +This test file validates that both equivalent regex patterns correctly match
      +Windows paths while preserving existing Unix path matching behavior.
      +"""
      +
      +import re
      +
      +import pytest
      +
      +
      +# Reconstruct the exact _TOOL_MEDIA_RE pattern from gateway/run.py
      +# The pattern is built by concatenating raw string parts:
      +#   r'MEDIA:((?:[A-Za-z]:[/\\]|/|~\/)\S+\.(?:png|...))'
      +_TOOL_MEDIA_RE = re.compile(
      +    r'MEDIA:((?:[A-Za-z]:[/\\]|/|~\/)\S+\.(?:png|jpe?g|gif|webp|'
      +    r'mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|'
      +    r'flac|epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|'
      +    r'txt|csv|apk|ipa))',
      +    re.IGNORECASE,
      +)
      +
      +
      +# Reconstruct the pre-fix pattern (without Windows anchor) for regression proof
      +_TOOL_MEDIA_RE_PRE_FIX = re.compile(
      +    r'MEDIA:((?:/|~\/)\S+\.(?:png|jpe?g|gif|webp|'
      +    r'mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|'
      +    r'flac|epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|'
      +    r'txt|csv|apk|ipa))',
      +    re.IGNORECASE,
      +)
      +
      +
      +class TestToolMediaReWindowsPaths:
      +    """Issue #34632: _TOOL_MEDIA_RE must match Windows absolute paths."""
      +
      +    # ── Positive: Windows paths now match ──────────────────────────
      +
      +    @pytest.mark.parametrize("media_tag, expected_path", [
      +        # Windows backslash paths
      +        ("MEDIA:C:\\Users\\test\\image.png", "C:\\Users\\test\\image.png"),
      +        ("MEDIA:D:\\data\\report.pdf", "D:\\data\\report.pdf"),
      +        ("MEDIA:E:\\Photos\\vacation.jpg", "E:\\Photos\\vacation.jpg"),
      +        # Windows forward-slash paths
      +        ("MEDIA:C:/Users/test/image.png", "C:/Users/test/image.png"),
      +        ("MEDIA:D:/data/report.pdf", "D:/data/report.pdf"),
      +        # Mixed separators
      +        ("MEDIA:C:\\Users/test\\image.webp", "C:\\Users/test\\image.webp"),
      +        # Various extensions
      +        ("MEDIA:F:\\videos\\clip.mp4", "F:\\videos\\clip.mp4"),
      +        ("MEDIA:G:\\audio\\song.mp3", "G:\\audio\\song.mp3"),
      +        ("MEDIA:H:\\docs\\sheet.xlsx", "H:\\docs\\sheet.xlsx"),
      +        ("MEDIA:Z:\\archive\\backup.zip", "Z:\\archive\\backup.zip"),
      +    ])
      +    def test_windows_paths_match(self, media_tag, expected_path):
      +        """Windows absolute paths with drive letters are matched."""
      +        match = _TOOL_MEDIA_RE.search(media_tag)
      +        assert match is not None, f"Should match: {media_tag}"
      +        assert match.group(1) == expected_path
      +
      +    # ── Positive: Unix paths still match ───────────────────────────
      +
      +    @pytest.mark.parametrize("media_tag, expected_path", [
      +        ("MEDIA:/tmp/output.png", "/tmp/output.png"),
      +        ("MEDIA:/var/log/report.pdf", "/var/log/report.pdf"),
      +        ("MEDIA:/home/user/docs/file.txt", "/home/user/docs/file.txt"),
      +        # Home-relative
      +        ("MEDIA:~/Downloads/image.jpg", "~/Downloads/image.jpg"),
      +        ("MEDIA:~/Documents/report.pdf", "~/Documents/report.pdf"),
      +    ])
      +    def test_unix_paths_still_match(self, media_tag, expected_path):
      +        """Unix-style absolute and home-relative paths still match."""
      +        match = _TOOL_MEDIA_RE.search(media_tag)
      +        assert match is not None, f"Should match: {media_tag}"
      +        assert match.group(1) == expected_path
      +
      +    # ── Negative: invalid paths don't match ────────────────────────
      +
      +    @pytest.mark.parametrize("text", [
      +        "No MEDIA tag here",
      +        "MEDIA:relative/path/file.png",       # relative path, no anchor
      +        "MEDIA:file.png",                      # no directory
      +        "MEDIA:C:file.png",                    # drive letter but no separator
      +        "MEDIA:/path/to/file.unknown",         # unsupported extension
      +        "MEDIA:/path/to/file",                 # no extension
      +        "MEDIA:",                               # empty path
      +    ])
      +    def test_invalid_paths_dont_match(self, text):
      +        """Non-MEDIA text, relative paths, and unsupported extensions are ignored."""
      +        match = _TOOL_MEDIA_RE.search(text)
      +        assert match is None, f"Should NOT match: {text}"
      +
      +    # ── Negative/preserved: old pattern rejects Windows paths ──────
      +
      +    @pytest.mark.parametrize("media_tag", [
      +        "MEDIA:C:\\Users\\test\\image.png",
      +        "MEDIA:D:/data/report.pdf",
      +        "MEDIA:C:\\path\\file.jpg",
      +    ])
      +    def test_pre_fix_pattern_rejects_windows(self, media_tag):
      +        """The pre-fix pattern (without Windows anchor) does NOT match Windows paths.
      +        This proves the fix is necessary — without it, these paths are silently ignored."""
      +        match = _TOOL_MEDIA_RE_PRE_FIX.search(media_tag)
      +        assert match is None, f"Pre-fix pattern should NOT match: {media_tag}"
      +
      +    # ── Edge cases ─────────────────────────────────────────────────
      +
      +    def test_multiple_media_tags_in_content(self):
      +        """Multiple MEDIA tags in the same content are all found."""
      +        content = (
      +            "Some text MEDIA:C:\\path\\img.png and more MEDIA:/tmp/out.pdf trailing"
      +        )
      +        matches = list(_TOOL_MEDIA_RE.finditer(content))
      +        assert len(matches) == 2
      +        paths = [m.group(1) for m in matches]
      +        assert "C:\\path\\img.png" in paths
      +        assert "/tmp/out.pdf" in paths
      +
      +    def test_case_insensitive_drive_letter(self):
      +        """Drive letters are case-insensitive due to re.IGNORECASE."""
      +        match_lower = _TOOL_MEDIA_RE.search("MEDIA:c:\\path\\file.png")
      +        match_upper = _TOOL_MEDIA_RE.search("MEDIA:C:\\path\\file.png")
      +        assert match_lower is not None
      +        assert match_upper is not None
      +        assert match_lower.group(1).lower() == match_upper.group(1).lower()
      +
      +    @pytest.mark.parametrize("media_tag", [
      +        "MEDIA:C:\\path\\file.jpeg",
      +        "MEDIA:C:\\path\\file.JPG",
      +        "MEDIA:C:\\path\\file.GIF",
      +        "MEDIA:C:\\path\\file.MP4",
      +    ])
      +    def test_case_insensitive_extensions(self, media_tag):
      +        """File extensions are matched case-insensitively."""
      +        match = _TOOL_MEDIA_RE.search(media_tag)
      +        assert match is not None, f"Should match: {media_tag}"

1 PR closed without merging

From the close comments: done elsewhere 1

  • #35328 fix(gateway): support Windows drive letters and backslashes in file path regex (fixes #35270) · done elsewhere → #35388, #35161
Closed issues · 1
  • Closed#35270[Bug]: Gateway extract_media and extract_local_files fail to parse Windows file paths
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
gateway
Merged PRs
#35388
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
30 May 2026

For your agent

The same facts, typed.