# NousResearch/hermes-agent · Video attachment caching and handling across platforms

## Open now

- Nothing open on GitHub.

These issues and pull requests concern inbound video attachments not being cached or accepted on Discord and Telegram, along with proposed fixes for both platforms including mp4 upload support.

## TL;DR

- 5 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 1 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #13009. Verify it addresses your case.
- **Recommended:** read PR #13009 (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: `partially_resolved` · deciding PR(s): `pr-13009`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): 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.34`: 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

- #4204: Telegram inbound videos are not cached and .mp4 file uploads are rejected as unsupported documents
- #13149: Discord: inbound video attachments are rejected instead of cached

## Prior & competing PRs

- **#13009: fix(telegram): cache inbound videos and accept mp4 uploads**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/platforms/base.py`, `gateway/platforms/telegram.py`, `scripts/release.py`, `tests/gateway/test_telegram_documents.py`
  - fix hunks:
  - `gateway/platforms/base.py`

    ```diff
    @@ -552,6 +552,39 @@ async def cache_audio_from_url(url: str, ext: str = ".ogg", retries: int = 2) ->
         raise last_exc
     
     
    +# ---------------------------------------------------------------------------
    +# Video cache utilities
    +#
    +# Same pattern as image/audio cache -- videos from platforms are downloaded
    +# here so the agent can reference them by local file path.
    +# ---------------------------------------------------------------------------
    +
    +VIDEO_CACHE_DIR = get_hermes_dir("cache/videos", "video_cache")
    +
    +SUPPORTED_VIDEO_TYPES = {
    +    ".mp4": "video/mp4",
    +    ".mov": "video/quicktime",
    +    ".webm": "video/webm",
    +    ".mkv": "video/x-matroska",
    +    ".avi": "video/x-msvideo",
    +}
    +
    +
    +def get_video_cache_dir() -> Path:
    +    """Return the video cache directory, creating it if it doesn't exist."""
    +    VIDEO_CACHE_DIR.mkdir(parents=True, exist_ok=True)
    +    return VIDEO_CACHE_DIR
    +
    +
    +def cache_video_from_bytes(data: bytes, ext: str = ".mp4") -> str:
    +    """Save raw video bytes to the cache and return the absolute file path."""
    +    cache_dir = get_video_cache_dir()
    +    filename = f"video_{uuid.uuid4().hex[:12]}{ext}"
    +    filepath = cache_dir / filename
    +    filepath.write_bytes(data)
    +    return str(filepath)
    +
    +
     # ---------------------------------------------------------------------------
     # Document cache utilities
     #
    ```
  - `gateway/platforms/telegram.py`

    ```diff
    @@ -71,8 +71,10 @@ class _MockContextTypes:
         SendResult,
         cache_image_from_bytes,
         cache_audio_from_bytes,
    +    cache_video_from_bytes,
         cache_document_from_bytes,
         resolve_proxy_url,
    +    SUPPORTED_VIDEO_TYPES,
         SUPPORTED_DOCUMENT_TYPES,
         utf16_len,
         _prefix_within_utf16_limit,
    ```

    ```diff
    @@ -2628,6 +2630,23 @@ async def _handle_media_message(self, update: Update, context: ContextTypes.DEFA
                 except Exception as e:
                     logger.warning("[Telegram] Failed to cache audio: %s", e, exc_info=True)
     
    +        elif msg.video:
    +            try:
    +                file_obj = await msg.video.get_file()
    +                video_bytes = await file_obj.download_as_bytearray()
    +                ext = ".mp4"
    +                if getattr(file_obj, "file_path", None):
    +                    for candidate in SUPPORTED_VIDEO_TYPES:
    +                        if file_obj.file_path.lower().endswith(candidate):
    +                            ext = candidate
    +                            break
    +                cached_path = cache_video_from_bytes(bytes(video_bytes), ext=ext)
    +                event.media_urls = [cached_path]
    +                event.media_types = [SUPPORTED_VIDEO_TYPES.get(ext, "video/mp4")]
    +                logger.info("[Telegram] Cached user video at %s", cached_path)
    +            except Exception as e:
    +                logger.warning("[Telegram] Failed to cache video: %s", e, exc_info=True)
    +
             # Download document files to cache for agent processing
             elif msg.document:
                 doc = msg.document
    ```

    ```diff
    @@ -2644,6 +2663,21 @@ async def _handle_media_message(self, update: Update, context: ContextTypes.DEFA
                         mime_to_ext = {v: k for k, v in SUPPORTED_DOCUMENT_TYPES.items()}
                         ext = mime_to_ext.get(doc.mime_type, "")
     
    +                if not ext and doc.mime_type:
    +                    video_mime_to_ext = {v: k for k, v in SUPPORTED_VIDEO_TYPES.items()}
    +                    ext = video_mime_to_ext.get(doc.mime_type, "")
    +
    +                if ext in SUPPORTED_VIDEO_TYPES:
    +                    file_obj = await doc.get_file()
    +                    video_bytes = await file_obj.download_as_bytearray()
    +                    cached_path = cache_video_from_bytes(bytes(video_bytes), ext=ext)
    +                    event.media_urls = [cached_path]
    +                    event.media_types = [SUPPORTED_VIDEO_TYPES[ext]]
    +                    event.message_type = MessageType.VIDEO
    +                    logger.info("[Telegram] Cached user video document at %s", cached_path)
    +                    await self.handle_message(event)
    +                    return
    +
                     # Check if supported
                     if ext not in SUPPORTED_DOCUMENT_TYPES:
                         supported_list = ", ".join(sorted(SUPPORTED_DOCUMENT_TYPES.keys()))
    ```
  - `scripts/release.py`

    ```diff
    @@ -77,6 +77,7 @@
         "[redacted:email]": "yyq4193",
         "[redacted:email]": "Asunfly",
         "[redacted:email]": "honghua",
    +    "[redacted:email]": "jplew",
         "[redacted:email]": "nish3451",
         "[redacted:email]": "Mibayy",
         "[redacted:email]": "sgaofen",
    ```
  - `tests/gateway/test_telegram_documents.py`

    ```diff
    @@ -23,6 +23,7 @@
         MessageType,
         SendResult,
         SUPPORTED_DOCUMENT_TYPES,
    +    SUPPORTED_VIDEO_TYPES,
     )
     
     
    ```

    ```diff
    @@ -117,6 +118,12 @@ def _make_update(msg):
         return update
     
     
    +def _make_video(file_obj=None):
    +    video = MagicMock()
    +    video.get_file = AsyncMock(return_value=file_obj or _make_file_obj(b"video-bytes"))
    +    return video
    +
    +
     # ---------------------------------------------------------------------------
     # Fixtures
     # ---------------------------------------------------------------------------
    ```

    ```diff
    @@ -132,10 +139,13 @@ def adapter():
     
     @pytest.fixture(autouse=True)
     def _redirect_cache(tmp_path, monkeypatch):
    -    """Point document cache to tmp_path so tests don't touch ~/.hermes."""
    +    """Point document/video cache to tmp_path so tests don't touch ~/.hermes."""
         monkeypatch.setattr(
             "gateway.platforms.base.DOCUMENT_CACHE_DIR", tmp_path / "doc_cache"
         )
    +    monkeypatch.setattr(
    +        "gateway.platforms.base.VIDEO_CACHE_DIR", tmp_path / "video_cache"
    +    )
     
     
     # ---------------------------------------------------------------------------
    ```

    ```diff
    @@ -348,6 +358,37 @@ async def test_download_exception_handled(self, adapter):
             adapter.handle_message.assert_called_once()
     
     
    +class TestVideoDownloadBlock:
    +    @pytest.mark.asyncio
    +    async def test_native_video_is_cached(self, adapter):
    +        file_obj = _make_file_obj(b"fake-mp4")
    +        file_obj.file_path = "videos/clip.mp4"
    +        msg = _make_message()
    +        msg.video = _make_video(file_obj)
    +        update = _make_update(msg)
    +
    +        await adapter._handle_media_message(update, MagicMock())
    +        event = adapter.handle_message.call_args[0][0]
    +        assert event.message_type == MessageType.VIDEO
    +        assert len(event.media_urls) == 1
    +        assert os.path.exists(event.media_urls[0])
    +        assert event.media_types == [SUPPORTED_VIDEO_TYPES[".mp4"]]
    +
    +    @pytest.mark.asyncio
    +    async def test_mp4_document_is_treated_as_video(self, adapter):
    +        file_obj = _make_file_obj(b"fake-mp4-doc")
    +        doc = _make_document(file_name="good.mp4", mime_type="video/mp4", file_size=1024, file_obj=file_obj)
    +        msg = _make_message(document=doc)
    +        update = _make_update(msg)
    +
    +        await adapter._handle_media_message(update, MagicMock())
    +        event = adapter.handle_message.call_args[0][0]
    +        assert event.message_type == MessageType.VIDEO
    +        assert len(event.media_urls) == 1
    +        assert os.path.exists(event.media_urls[0])
    +        assert event.media_types == [SUPPORTED_VIDEO_TYPES[".mp4"]]
    +
    +
     # ---------------------------------------------------------------------------
     # TestMediaGroups — media group (album) buffering
     # ---------------------------------------------------------------------------
    ```
- **#13150: fix(discord): handle inbound video attachments**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `4314d451ca961cb50c3430197a3a2c7a8575fd0e`, `cc8e5ec2afbfd10a3cff4e710210dd9ecae64a33`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#4205: fix(telegram): cache inbound videos and accept mp4 uploads**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#13009`

## Fix status

- Merged to main in PR #13009 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-04-20`. This confirmation is old and may be out of date (a revert/reopen would not have invalidated it here); re-verify.
- Subsystem: `gateway`
