NousResearch/hermes-agent

Windows path handling bugs in MEDIA tag parsing

Cluster 24032 · gateway. Data as of

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

The short answer

0open issues
1open PR
11items grouped
Known?
Yes. 11 related items grouped here, 5 issues 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 unresolved. That is separate from the fix event.
Still open
0 issues and 1 PR.

Multiple bug reports and fixes addressing failures to parse or deliver MEDIA directive tags when paths contain Windows drive letters, spaces, or GIS file extensions. The issues span regex matching, local file extraction, and silent delivery failures on Windows.

Open issues0

No open issues.

Open PRs1

  • PR#24217fix(gateway): preserve spaced MEDIA paths

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}"

4 PRs closed without merging

From the close comments: done elsewhere 1 · merged via another change 1 · superseded by another PR 1 · other 1

  • #24132 fix(gateway): preserve spaced MEDIA file paths · other
  • #28991 fix(gateway): recognize Windows drive-letter paths in extract_local_files() bare-path uploads · done elsewhere → #35388, #35161
  • #29234 fix(gateway): parse MEDIA: tags with Windows paths (#28989) · superseded by another PR → #28991
  • #35161 fix(gateway): support Windows absolute paths in all MEDIA regex locations (#34632) · merged via another change → 51d165a, #35388
Closed issues · 5
  • Closed#24032[BUG] extract_media regex truncates Windows spaced paths and rejects GIS extensions (.kmz/.kml/.geojson/.gpx)
  • Closed#28989[Bug]: MEDIA: tags with Windows paths not parsed — images sent as plain text
  • Closed#34632Bug: MEDIA directive silently fails on Windows
  • Closed#35380fix: Windows drive-letter paths not matched by MEDIA: tag and local file regex
  • Closed#35546Windows: MEDIA: file delivery silently fails for unquoted drive-letter paths (extract_media)
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.17: 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
5
Attempts
4 PRs closed without merging
Fix link checked
30 May 2026

For your agent

The same facts, typed.