Video attachment caching and handling across platforms

NousResearch/hermes-agent · cluster 4204 · gateway

Data as of

Nothing open on GitHub. Fix merged in #13009. Check it covers your case.

The short answer

0open issues
0open PRs
5items grouped
Known?
Yes. 5 related items grouped here, 2 issues already closed.
Fix event
A fix was merged to main in PR #13009. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #13009. That is separate from the fix event.
Still open
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.

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.

  • #13009 fix(telegram): cache inbound videos and accept mp4 uploads

    merged to main

    files: gateway/platforms/base.py, gateway/platforms/telegram.py, scripts/release.py, tests/gateway/test_telegram_documents.py

    Show the diff
    • gateway/platforms/base.py
      @@ -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
      @@ -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,
      @@ -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
      @@ -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
      @@ -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
      @@ -23,6 +23,7 @@
           MessageType,
           SendResult,
           SUPPORTED_DOCUMENT_TYPES,
      +    SUPPORTED_VIDEO_TYPES,
       )
       
       
      @@ -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
       # ---------------------------------------------------------------------------
      @@ -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"
      +    )
       
       
       # ---------------------------------------------------------------------------
      @@ -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
       # ---------------------------------------------------------------------------

2 PRs closed without merging

From the close comments: done elsewhere 1 · merged via another change 1

  • #4205 fix(telegram): cache inbound videos and accept mp4 uploads · merged via another change → #13009
  • #13150 fix(discord): handle inbound video attachments · done elsewhere → 4314d45, cc8e5ec
Closed issues · 2
  • Closed#4204Telegram inbound videos are not cached and .mp4 file uploads are rejected as unsupported documents
  • Closed#13149Discord: inbound video attachments are rejected instead of cached
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.34: 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
#13009
Closed issues
2
Attempts
2 PRs closed without merging
Fix link checked
20 Apr 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.

For your agent

The same facts, typed.