{"repo":"NousResearch/hermes-agent","public_id":"7390","display_label":"Silent truncation of skill frontmatter content","summary":"A bug was identified where skill file frontmatter was being silently truncated at arbitrary character limits (2000 or 4000 chars) in multiple files. The members cover the bug report and several fix attempts targeting prompt_builder.py, skills_tool.py, and skills_sync.py.","subsystem":null,"counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":7454,"merged_at":"2026-04-11T00:04:38+00:00"},"resolution":{"state":"resolved","by":"pr-7454"},"freshness":{"state":"stale","last_verified_at":"2026-04-11T00:04:38+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":7358,"pr_number":7454,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-04-11T00:04:38+00:00"},{"issue_number":7390,"pr_number":7454,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-04-11T00:04:38+00:00"}],"members":[{"kind":"issue","number":7390,"title":"[Bug]: `prompt_builder.py` silently truncates `SKILL.md` to first 2000 chars; skills with larger YAML frontmatter lose all metadata","url":null,"state":"closed"},{"kind":"pr","number":7394,"title":"fix(prompt_builder): remove 2000-char slice that silently truncated SKILL.md frontmatter","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#7454","number":7454},{"kind":"pr","value":"#7391","number":7391}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":7454,"title":"fix(gateway): replace os.environ session state with contextvars + fix skill frontmatter truncation","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/prompt_builder.py","agent/skill_utils.py","gateway/run.py","gateway/session_context.py","tests/gateway/test_session_env.py","tools/cronjob_tools.py","tools/send_message_tool.py","tools/skills_tool.py","tools/terminal_tool.py","tools/tts_tool.py"],"fix_hunks":[{"file":"agent/prompt_builder.py","hunks":["@@ -487,7 +487,7 @@ def _parse_skill_file(skill_file: Path) -> tuple[bool, dict, str]:\n     (True, {}, \"\") to err on the side of showing the skill.\n     \"\"\"\n     try:\n-        raw = skill_file.read_text(encoding=\"utf-8\")[:2000]\n+        raw = skill_file.read_text(encoding=\"utf-8\")\n         frontmatter, _ = parse_frontmatter(raw)\n \n         if not skill_matches_platform(frontmatter):","@@ -495,7 +495,7 @@ def _parse_skill_file(skill_file: Path) -> tuple[bool, dict, str]:\n \n         return True, frontmatter, extract_skill_description(frontmatter)\n     except Exception as e:\n-        logger.debug(\"Failed to parse skill file %s: %s\", skill_file, e)\n+        logger.warning(\"Failed to parse skill file %s: %s\", skill_file, e)\n         return True, {}, \"\"\n \n ","@@ -558,9 +558,10 @@ def build_skills_system_prompt(\n     # ── Layer 1: in-process LRU cache ─────────────────────────────────\n     # Include the resolved platform so per-platform disabled-skill lists\n     # produce distinct cache entries (gateway serves multiple platforms).\n+    from gateway.session_context import get_session_env\n     _platform_hint = (\n         os.environ.get(\"HERMES_PLATFORM\")\n-        or os.environ.get(\"HERMES_SESSION_PLATFORM\")\n+        or get_session_env(\"HERMES_SESSION_PLATFORM\")\n         or \"\"\n     )\n     cache_key = ("]},{"file":"agent/skill_utils.py","hunks":["@@ -145,10 +145,11 @@ def get_disabled_skill_names(platform: str | None = None) -> Set[str]:\n     if not isinstance(skills_cfg, dict):\n         return set()\n \n+    from gateway.session_context import get_session_env\n     resolved_platform = (\n         platform\n         or os.getenv(\"HERMES_PLATFORM\")\n-        or os.getenv(\"HERMES_SESSION_PLATFORM\")\n+        or get_session_env(\"HERMES_SESSION_PLATFORM\")\n     )\n     if resolved_platform:\n         platform_disabled = (skills_cfg.get(\"platform_disabled\") or {}).get("]},{"file":"gateway/run.py","hunks":["@@ -2400,8 +2400,8 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str):\n         # Build session context\n         context = build_session_context(source, self.config, session_entry)\n         \n-        # Set environment variables for tools\n-        self._set_session_env(context)\n+        # Set session context variables for tools (task-local, concurrency-safe)\n+        _session_env_tokens = [redacted:secret-assignment]\n         \n         # Read privacy.redact_pii from config (re-read per message)\n         _redact_pii = False","@@ -3234,8 +3234,8 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str):\n                 \"Try again or use /reset to start a fresh session.\"\n             )\n         finally:\n-            # Clear session env\n-            self._clear_session_env()\n+            # Restore session context variables to their pre-handler state\n+            self._clear_session_env(_session_env_tokens)\n     \n     def _format_session_info(self) -> str:\n         \"\"\"Resolve current model config and return a formatted info block.","@@ -6120,20 +6120,27 @@ async def _send_update_notification(self) -> bool:\n \n         return True\n \n-    def _set_session_env(self, context: SessionContext) -> None:\n-        \"\"\"Set environment variables for the current session.\"\"\"\n-        os.environ[\"HERMES_SESSION_PLATFORM\"] = context.source.platform.value\n-        os.environ[\"HERMES_SESSION_CHAT_ID\"] = context.source.chat_id\n-        if context.source.chat_name:\n-            os.environ[\"HERMES_SESSION_CHAT_NAME\"] = context.source.chat_name\n-        if context.source.thread_id:\n-            os.environ[\"HERMES_SESSION_THREAD_ID\"] = str(context.source.thread_id)\n-    \n-    def _clear_session_env(self) -> None:\n-        \"\"\"Clear session environment variables.\"\"\"\n-        for var in [\"HERMES_SESSION_PLATFORM\", \"HERMES_SESSION_CHAT_ID\", \"HERMES_SESSION_CHAT_NAME\", \"HERMES_SESSION_THREAD_ID\"]:\n-            if var in os.environ:\n-                del os.environ[var]\n+    def _set_session_env(self, context: SessionContext) -> list:\n+        \"\"\"Set session context variables for the current async task.\n+\n+        Uses ``contextvars`` instead of ``os.environ`` so that concurrent\n+        gateway messages cannot overwrite each other's session state.\n+\n+        Returns a list of reset tokens; pass them to ``_clear_session_env``\n+        in a ``finally`` block.\n+        \"\"\"\n+        from gateway.session_context import set_session_vars\n+        return set_session_vars(\n+            platform=context.source.platform.value,\n+            chat_id=context.source.chat_id,\n+            chat_name=context.source.chat_name or \"\",\n+            thread_id=str(context.source.thread_id) if context.source.thread_id else \"\",\n+        )\n+\n+    def _clear_session_env(self, tokens: list) -> None:\n+        \"\"\"Restore session context variables to their pre-handler values.\"\"\"\n+        from gateway.session_context import clear_session_vars\n+        clear_session_vars(tokens)\n     \n     async def _enrich_message_with_vision(\n         self,"]},{"file":"gateway/session_context.py","hunks":["@@ -0,0 +1,113 @@\n+\"\"\"\n+Session-scoped context variables for the Hermes gateway.\n+\n+Replaces the previous ``os.environ``-based session state\n+(``HERMES_SESSION_PLATFORM``, ``HERMES_SESSION_CHAT_ID``, etc.) with\n+Python's ``contextvars.ContextVar``.\n+\n+**Why this matters**\n+\n+The gateway processes messages concurrently via ``asyncio``.  When two\n+messages arrive at the same time the old code did:\n+\n+    os.environ[\"HERMES_SESSION_THREAD_ID\"] = str(context.source.thread_id)\n+\n+Because ``os.environ`` is *process-global*, Message A's value was\n+silently overwritten by Message B before Message A's agent finished\n+running.  Background-task notifications and tool calls therefore routed\n+to the wrong thread.\n+\n+``contextvars.ContextVar`` values are *task-local*: each ``asyncio``\n+task (and any ``run_in_executor`` thread it spawns) gets its own copy,\n+so concurrent messages never interfere.\n+\n+**Backward compatibility**\n+\n+The public helper ``get_session_env(name, default=\"\")`` mirrors the old\n+``os.getenv(\"HERMES_SESSION_*\", ...)`` calls.  Existing tool code only\n+needs to replace the import + call site:\n+\n+    # before\n+    import os\n+    platform = os.getenv(\"HERMES_SESSION_PLATFORM\", \"\")\n+\n+    # after\n+    from gateway.session_context import get_session_env\n+    platform = get_session_env(\"HERMES_SESSION_PLATFORM\", \"\")\n+\"\"\"\n+\n+from contextvars import ContextVar\n+\n+# ---------------------------------------------------------------------------\n+# Per-task session variables\n+# ---------------------------------------------------------------------------\n+\n+_SESSION_PLATFORM: ContextVar[str] = ContextVar(\"HERMES_SESSION_PLATFORM\", default=\"\")\n+_SESSION_CHAT_ID: ContextVar[str] = ContextVar(\"HERMES_SESSION_CHAT_ID\", default=\"\")\n+_SESSION_CHAT_NAME: ContextVar[str] = ContextVar(\"HERMES_SESSION_CHAT_NAME\", default=\"\")\n+_SESSION_THREAD_ID: ContextVar[str] = ContextVar(\"HERMES_SESSION_THREAD_ID\", default=\"\")\n+\n+_VAR_MAP = {\n+    \"HERMES_SESSION_PLATFORM\": _SESSION_PLATFORM,\n+    \"HERMES_SESSION_CHAT_ID\": _SESSION_CHAT_ID,\n+    \"HERMES_SESSION_CHAT_NAME\": _SESSION_CHAT_NAME,\n+    \"HERMES_SESSION_THREAD_ID\": _SESSION_THREAD_ID,\n+}\n+\n+\n+def set_session_vars(\n+    platform: str = \"\",\n+    chat_id: str = \"\",\n+    chat_name: str = \"\",\n+    thread_id: str = \"\",\n+) -> list:\n+    \"\"\"Set all session context variables and return reset tokens.\n+\n+    Call ``clear_session_vars(tokens)`` in a ``finally`` block to restore\n+    the previous values when the handler exits.\n+\n+    Returns a list of ``Token`` objects (one per variable) that can be\n+    passed to ``clear_session_vars``.\n+    \"\"\"\n+    tokens = [\n+        _SESSION_PLATFORM.set(platform),\n+        _SESSION_CHAT_ID.set(chat_id),\n+        _SESSION_CHAT_NAME.set(chat_name),\n+        _SESSION_THREAD_ID.set(thread_id),\n+    ]\n+    return tokens\n+\n+\n+def clear_session_vars(tokens: list) -> None:\n+    \"\"\"Restore session context variables to their pre-handler values.\"\"\"\n+    if not tokens:\n+        return\n+    vars_in_order = [\n+        _SESSION_PLATFORM,\n+        _SESSION_CHAT_ID,\n+        _SESSION_CHAT_NAME,\n+        _SESSION_THREAD_ID,\n+    ]\n+    for var, token in zip(vars_in_order, tokens):\n+        var.reset(token)\n+\n+\n+def get_session_env(name: str, default: str = \"\") -> str:\n+    \"\"\"Read a session context variable by its legacy ``HERMES_SESSION_*`` name.\n+\n+    Drop-in replacement for ``os.getenv(\"HERMES_SESSION_*\", default)``.\n+\n+    Resolution order:\n+    1. Context variable (set by the gateway for concurrency-safe access)\n+    2. ``os.environ`` (used by CLI, cron scheduler, and tests)\n+    3. *default*\n+    \"\"\"\n+    import os\n+\n+    var = _VAR_MAP.get(name)\n+    if var is not None:\n+        value = var.get()\n+        if value:\n+            return value\n+    # Fall back to os.environ for CLI, cron, and test compatibility\n+    return os.getenv(name, default)"]},{"file":"tests/gateway/test_session_env.py","hunks":["@@ -3,9 +3,15 @@\n from gateway.config import Platform\n from gateway.run import GatewayRunner\n from gateway.session import SessionContext, SessionSource\n+from gateway.session_context import (\n+    get_session_env,\n+    set_session_vars,\n+    clear_session_vars,\n+)\n \n \n-def test_set_session_env_includes_thread_id(monkeypatch):\n+def test_set_session_env_sets_contextvars(monkeypatch):\n+    \"\"\"_set_session_env should populate contextvars, not os.environ.\"\"\"\n     runner = object.__new__(GatewayRunner)\n     source = SessionSource(\n         platform=Platform.TELEGRAM,","@@ -21,25 +27,93 @@ def test_set_session_env_includes_thread_id(monkeypatch):\n     monkeypatch.delenv(\"HERMES_SESSION_CHAT_NAME\", raising=False)\n     monkeypatch.delenv(\"HERMES_SESSION_THREAD_ID\", raising=False)\n \n-    runner._set_session_env(context)\n+    tokens = [redacted:secret-assignment]\n \n-    assert os.getenv(\"HERMES_SESSION_PLATFORM\") == \"telegram\"\n-    assert os.getenv(\"HERMES_SESSION_CHAT_ID\") == \"-1001\"\n-    assert os.getenv(\"HERMES_SESSION_CHAT_NAME\") == \"Group\"\n-    assert os.getenv(\"HERMES_SESSION_THREAD_ID\") == \"17585\"\n+    # Values should be readable via get_session_env (contextvar path)\n+    assert get_session_env(\"HERMES_SESSION_PLATFORM\") == \"telegram\"\n+    assert get_session_env(\"HERMES_SESSION_CHAT_ID\") == \"-1001\"\n+    assert get_session_env(\"HERMES_SESSION_CHAT_NAME\") == \"Group\"\n+    assert get_session_env(\"HERMES_SESSION_THREAD_ID\") == \"17585\"\n \n+    # os.environ should NOT be touched\n+    assert os.getenv(\"HERMES_SESSION_PLATFORM\") is None\n+    assert os.getenv(\"HERMES_SESSION_THREAD_ID\") is None\n+\n+    # Clean up\n+    runner._clear_session_env(tokens)\n \n-def test_clear_session_env_removes_thread_id(monkeypatch):\n+\n+def test_clear_session_env_restores_previous_state(monkeypatch):\n+    \"\"\"_clear_session_env should restore contextvars to their pre-handler values.\"\"\"\n     runner = object.__new__(GatewayRunner)\n \n-    monkeypatch.setenv(\"HERMES_SESSION_PLATFORM\", \"telegram\")\n-    monkeypatch.setenv(\"HERMES_SESSION_CHAT_ID\", \"-1001\")\n-    monkeypatch.setenv(\"HERMES_SESSION_CHAT_NAME\", \"Group\")\n-    monkeypatch.setenv(\"HERMES_SESSION_THREAD_ID\", \"17585\")\n+    monkeypatch.delenv(\"HERMES_SESSION_PLATFORM\", raising=False)\n+    monkeypatch.delenv(\"HERMES_SESSION_CHAT_ID\", raising=False)\n+    monkeypatch.delenv(\"HERMES_SESSION_CHAT_NAME\", raising=False)\n+    monkeypatch.delenv(\"HERMES_SESSION_THREAD_ID\", raising=False)\n+\n+    source = SessionSource(\n+        platform=Platform.TELEGRAM,\n+        chat_id=\"-1001\",\n+        chat_name=\"Group\",\n+        chat_type=\"group\",\n+        thread_id=\"17585\",\n+    )\n+    context = SessionContext(source=source, connected_platforms=[], home_channels={})\n+\n+    tokens = [redacted:secret-assignment]\n+    assert get_session_env(\"HERMES_SESSION_PLATFORM\") == \"telegram\"\n \n-    runner._clear_session_env()\n+    runner._clear_session_env(tokens)\n \n-    assert os.getenv(\"HERMES_SESSION_PLATFORM\") is None\n-    assert os.getenv(\"HERMES_SESSION_CHAT_ID\") is None\n-    assert os.getenv(\"HERMES_SESSION_CHAT_NAME\") is None\n-    assert os.getenv(\"HERMES_SESSION_THREAD_ID\") is None\n+    # After clear, contextvars should return to defaults (empty)\n+    assert get_session_env(\"HERMES_SESSION_PLATFORM\") == \"\"\n+    assert get_session_env(\"HERMES_SESSION_CHAT_ID\") == \"\"\n+    assert get_session_env(\"HERMES_SESSION_CHAT_NAME\") == \"\"\n+    assert get_session_env(\"HERMES_SESSION_THREAD_ID\") == \"\"\n+\n+\n+def test_get_session_env_falls_back_to_os_environ(monkeypatch):\n+    \"\"\"get_session_env should fall back to os.environ when contextvar is unset.\"\"\"\n+    monkeypatch.setenv(\"HERMES_SESSION_PLATFORM\", \"discord\")\n+\n+    # No contextvar set — should read from os.environ\n+    assert get_session_env(\"HERMES_SESSION_PLATFORM\") == \"discord\"\n+\n+    # Now set a contextvar — should prefer it\n+    tokens = [redacted:secret-assignment]\"telegram\")\n+    assert get_session_env(\"HERMES_SESSION_PLATFORM\") == \"telegram\"\n+\n+    # Restore — should fall back to os.environ again\n+    clear_session_vars(tokens)\n+    assert get_session_env(\"HERMES_SESSION_PLATFORM\") == \"discord\"\n+\n+\n+def test_get_session_env_default_when_nothing_set(monkeypatch):\n+    \"\"\"get_session_env returns default when neither contextvar nor env is set.\"\"\"\n+    monkeypatch.delenv(\"HERMES_SESSION_PLATFORM\", raising=False)\n+\n+    assert get_session_env(\"HERMES_SESSION_PLATFORM\") == \"\"\n+    assert get_session_env(\"HERMES_SESSION_PLATFORM\", \"fallback\") == \"fallback\"\n+\n+\n+def test_set_session_env_handles_missing_optional_fields():\n+    \"\"\"_set_session_env should handle None chat_name and thread_id gracefully.\"\"\"\n+    runner = object.__new__(GatewayRunner)\n+    source = SessionSource(\n+        platform=Platform.TELEGRAM,\n+        chat_id=\"-1001\",\n+        chat_name=None,\n+        chat_type=\"private\",\n+        thread_id=None,\n+    )\n+    context = SessionContext(source=source, connected_platforms=[], home_channels={})\n+\n+    tokens = [redacted:secret-assignment]\n+\n+    assert get_session_env(\"HERMES_SESSION_PLATFORM\") == \"telegram\"\n+    assert get_session_env(\"HERMES_SESSION_CHAT_ID\") == \"-1001\"\n+    assert get_session_env(\"HERMES_SESSION_CHAT_NAME\") == \"\"\n+    assert get_session_env(\"HERMES_SESSION_THREAD_ID\") == \"\"\n+\n+    runner._clear_session_env(tokens)"]},{"file":"tools/cronjob_tools.py","hunks":["@@ -64,14 +64,15 @@ def _scan_cron_prompt(prompt: str) -> str:\n \n \n def _origin_from_env() -> Optional[Dict[str, str]]:\n-    origin_platform = os.getenv(\"HERMES_SESSION_PLATFORM\")\n-    origin_chat_id = os.getenv(\"HERMES_SESSION_CHAT_ID\")\n+    from gateway.session_context import get_session_env\n+    origin_platform = get_session_env(\"HERMES_SESSION_PLATFORM\")\n+    origin_chat_id = get_session_env(\"HERMES_SESSION_CHAT_ID\")\n     if origin_platform and origin_chat_id:\n         return {\n             \"platform\": origin_platform,\n             \"chat_id\": origin_chat_id,\n-            \"chat_name\": os.getenv(\"HERMES_SESSION_CHAT_NAME\"),\n-            \"thread_id\": os.getenv(\"HERMES_SESSION_THREAD_ID\"),\n+            \"chat_name\": get_session_env(\"HERMES_SESSION_CHAT_NAME\") or None,\n+            \"thread_id\": get_session_env(\"HERMES_SESSION_THREAD_ID\") or None,\n         }\n     return None\n "]},{"file":"tools/send_message_tool.py","hunks":["@@ -212,7 +212,8 @@ def _handle_send(args):\n         if isinstance(result, dict) and result.get(\"success\") and mirror_text:\n             try:\n                 from gateway.mirror import mirror_to_session\n-                source_label = os.getenv(\"HERMES_SESSION_PLATFORM\", \"cli\")\n+                from gateway.session_context import get_session_env\n+                source_label = get_session_env(\"HERMES_SESSION_PLATFORM\", \"cli\")\n                 if mirror_to_session(platform_name, chat_id, mirror_text, source_label=source_label, thread_id=thread_id):\n                     result[\"mirrored\"] = True\n             except Exception:","@@ -1020,7 +1021,8 @@ async def _send_feishu(pconfig, chat_id, message, media_files=None, thread_id=No\n \n def _check_send_message():\n     \"\"\"Gate send_message on gateway running (always available on messaging platforms).\"\"\"\n-    platform = os.getenv(\"HERMES_SESSION_PLATFORM\", \"\")\n+    from gateway.session_context import get_session_env\n+    platform = get_session_env(\"HERMES_SESSION_PLATFORM\", \"\")\n     if platform and platform != \"local\":\n         return True\n     try:"]},{"file":"tools/skills_tool.py","hunks":["@@ -347,7 +347,8 @@ def _capture_required_environment_variables(\n def _is_gateway_surface() -> bool:\n     if os.getenv(\"HERMES_GATEWAY_SESSION\"):\n         return True\n-    return bool(os.getenv(\"HERMES_SESSION_PLATFORM\"))\n+    from gateway.session_context import get_session_env\n+    return bool(get_session_env(\"HERMES_SESSION_PLATFORM\"))\n \n \n def _get_terminal_backend_name() -> str:"]},{"file":"tools/terminal_tool.py","hunks":["@@ -1420,10 +1420,11 @@ def terminal_tool(\n                     # In gateway mode, auto-register a fast watcher so the\n                     # gateway can detect completion and trigger a new agent\n                     # turn.  CLI mode uses the completion_queue directly.\n-                    _gw_platform = os.getenv(\"HERMES_SESSION_PLATFORM\", \"\")\n+                    from gateway.session_context import get_session_env as _gse\n+                    _gw_platform = _gse(\"HERMES_SESSION_PLATFORM\", \"\")\n                     if _gw_platform and not check_interval:\n-                        _gw_chat_id = os.getenv(\"HERMES_SESSION_CHAT_ID\", \"\")\n-                        _gw_thread_id = os.getenv(\"HERMES_SESSION_THREAD_ID\", \"\")\n+                        _gw_chat_id = _gse(\"HERMES_SESSION_CHAT_ID\", \"\")\n+                        _gw_thread_id = _gse(\"HERMES_SESSION_THREAD_ID\", \"\")\n                         proc_session.watcher_platform = _gw_platform\n                         proc_session.watcher_chat_id = _gw_chat_id\n                         proc_session.watcher_thread_id = _gw_thread_id","@@ -1445,9 +1446,10 @@ def terminal_tool(\n                         result_data[\"check_interval_note\"] = (\n                             f\"Requested {check_interval}s raised to minimum 30s\"\n                         )\n-                    watcher_platform = os.getenv(\"HERMES_SESSION_PLATFORM\", \"\")\n-                    watcher_chat_id = os.getenv(\"HERMES_SESSION_CHAT_ID\", \"\")\n-                    watcher_thread_id = os.getenv(\"HERMES_SESSION_THREAD_ID\", \"\")\n+                    from gateway.session_context import get_session_env as _gse2\n+                    watcher_platform = _gse2(\"HERMES_SESSION_PLATFORM\", \"\")\n+                    watcher_chat_id = _gse2(\"HERMES_SESSION_CHAT_ID\", \"\")\n+                    watcher_thread_id = _gse2(\"HERMES_SESSION_THREAD_ID\", \"\")\n \n                     # Store on session for checkpoint persistence\n                     proc_session.watcher_platform = watcher_platform"]},{"file":"tools/tts_tool.py","hunks":["@@ -480,7 +480,8 @@ def text_to_speech_tool(\n     # Telegram voice bubbles require Opus (.ogg); OpenAI and ElevenLabs can\n     # produce Opus natively (no ffmpeg needed).  Edge TTS always outputs MP3\n     # and needs ffmpeg for conversion.\n-    platform = os.getenv(\"HERMES_SESSION_PLATFORM\", \"\").lower()\n+    from gateway.session_context import get_session_env\n+    platform = get_session_env(\"HERMES_SESSION_PLATFORM\", \"\").lower()\n     want_opus = (platform == \"telegram\")\n \n     # Determine output path\n"]}]},{"kind":"pr","number":8716,"title":"fix(skills): remove [:4000] frontmatter truncation in skills_tool.py and skills_sync.py","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}