{"repo":"NousResearch/hermes-agent","public_id":"40677","display_label":"Active profile not propagated to desktop/TUI subsystems","summary":"Multiple bugs and fixes around desktop, TUI, and dashboard components ignoring the active profile and falling back to global or default settings. Affected areas include TTS/voice config, audio endpoints, chat workers, skills resolution, and slash-command subprocesses.","subsystem":"apps/desktop","counts":{"members":15,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":6,"merged_prs":2,"attempt_prs":7},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":60180},"resolution":{"state":"partially_resolved","by":"pr-60180,pr-73521"},"freshness":{"state":"verified","last_verified_at":"2026-07-28T18:58:03+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.10628571428571428},"fix_edges":[{"issue_number":40677,"pr_number":60180,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":40896,"pr_number":73521,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":53441,"pr_number":73521,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":58908,"pr_number":60180,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":70955,"pr_number":73521,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":72207,"pr_number":73521,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":40677,"title":"[Bug]: Profile-local skills are unavailable in Dashboard/TUI/Desktop GUI because child processes use the root HERMES_HOME","url":null,"state":"closed"},{"kind":"issue","number":41517,"title":"Desktop/Dashboard chat worker loses selected profile and falls back to default","url":null,"state":"closed"},{"kind":"issue","number":45506,"title":"Desktop voice mode uses default profile TTS config for non-default profiles","url":null,"state":"closed"},{"kind":"issue","number":53441,"title":"Desktop audio endpoints ignore active profile config","url":null,"state":"closed"},{"kind":"issue","number":64057,"title":"[Bug]: TTS voice selection ignores mismatched global vs profile config on desktop","url":null,"state":"closed"},{"kind":"issue","number":66012,"title":"[Bug]: Hermes Desktop ignores per-profile tts/voice config; always uses global/default TTS provider","url":null,"state":"closed"},{"kind":"pr","number":40959,"title":"fix(tui): pass profile_home to slash_worker subprocess (#40677)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#60180","number":60180},{"kind":"pr","value":"#36847","number":36847},{"kind":"pr","value":"#56689","number":56689},{"kind":"pr","value":"#40677","number":40677}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":41526,"title":"fix(tui_gateway): pass profile to slash worker subprocess","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"4a99571d54e0e0ca0dddd7918ba32446ac115362","number":0},{"kind":"pr","value":"#40677","number":40677}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":45522,"title":"fix(desktop): route TTS through active profile","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#53470","number":53470}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":53470,"title":"fix(desktop): scope audio endpoints to the active profile","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"097f0d0167","number":0},{"kind":"pr","value":"#73521","number":73521}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":53472,"title":"fix(desktop): scope audio helpers to active profile","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"097f0d0167","number":0},{"kind":"pr","value":"#73521","number":73521},{"kind":"pr","value":"#53470","number":53470},{"kind":"pr","value":"#69491","number":69491},{"kind":"pr","value":"#73521","number":73521}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":53488,"title":"fix: scope desktop audio requests to profile","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"097f0d0167","number":0},{"kind":"pr","value":"#73521","number":73521},{"kind":"pr","value":"#53470","number":53470},{"kind":"pr","value":"#69491","number":69491},{"kind":"pr","value":"#73521","number":73521}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":60180,"title":"fix(skills): profile-aware skills resolution in long-lived runtimes (#40677)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["tests/test_tui_gateway_server.py","tests/tools/test_skills_tool_profile_scope.py","tests/tui_gateway/test_slash_worker_profile_home.py","tools/skill_manager_tool.py","tools/skills_tool.py","tui_gateway/server.py"],"fix_hunks":[{"file":"tests/test_tui_gateway_server.py","hunks":["@@ -1947,7 +1947,7 @@ def test_init_session_fires_reset_hook(monkeypatch):\n     hooks = []\n \n     class _FakeWorker:\n-        def __init__(self, key, model):\n+        def __init__(self, key, model, profile_home=None):\n             self.key = key\n \n         def close(self):","@@ -5453,7 +5453,7 @@ def test_session_create_close_race_does_not_orphan_worker(monkeypatch):\n     unregistered_keys: list[str] = []\n \n     class _FakeWorker:\n-        def __init__(self, key, model):\n+        def __init__(self, key, model, profile_home=None):\n             self.key = key\n             self._closed = False\n ","@@ -5574,7 +5574,7 @@ def test_session_create_no_race_keeps_worker_alive(monkeypatch):\n     unregistered_keys: list[str] = []\n \n     class _FakeWorker:\n-        def __init__(self, key, model):\n+        def __init__(self, key, model, profile_home=None):\n             self.key = key\n \n         def close(self):","@@ -5678,7 +5678,7 @@ def __init__(self):\n \n def test_session_create_continues_when_state_db_is_unavailable(monkeypatch):\n     class _FakeWorker:\n-        def __init__(self, key, model):\n+        def __init__(self, key, model, profile_home=None):\n             self.key = key\n \n         def close(self):","@@ -5726,7 +5726,7 @@ def test_session_create_lazy_info_reports_desktop_contract(monkeypatch):\n     date\" on every launch even against a current backend.\"\"\"\n \n     class _FakeWorker:\n-        def __init__(self, key, model):\n+        def __init__(self, key, model, profile_home=None):\n             self.key = key\n \n         def close(self):"]},{"file":"tests/tools/test_skills_tool_profile_scope.py","hunks":["@@ -0,0 +1,105 @@\n+\"\"\"Regression tests for profile-scoped skills_tool path resolution.\"\"\"\n+\n+import importlib\n+import json\n+from pathlib import Path\n+\n+\n+def _write_skill(root: Path, category: str, name: str, description: str) -> Path:\n+    skill_dir = root / \"skills\" / category / name\n+    skill_dir.mkdir(parents=True)\n+    (skill_dir / \"SKILL.md\").write_text(\n+        f\"---\\n\"\n+        f\"name: {name}\\n\"\n+        f\"description: {description}\\n\"\n+        f\"---\\n\\n\"\n+        f\"# {name}\\n\\n\"\n+        f\"Loaded from {description}.\\n\",\n+        encoding=\"utf-8\",\n+    )\n+    return skill_dir\n+\n+\n+def _reload_skills_tool(import_home: Path, monkeypatch):\n+    monkeypatch.setenv(\"HERMES_HOME\", str(import_home))\n+    import tools.skills_tool as skills_tool\n+\n+    return importlib.reload(skills_tool)\n+\n+\n+def test_skill_view_uses_live_profile_home_after_module_import(tmp_path, monkeypatch):\n+    \"\"\"skill_view should not stay pinned to HERMES_HOME from import time.\"\"\"\n+    default_home = tmp_path / \"default-home\"\n+    profile_home = tmp_path / \"profiles\" / \"orchestrator\"\n+    _write_skill(default_home, \"autonomous-ai-agents\", \"default-only\", \"default home\")\n+    profile_skill_dir = _write_skill(\n+        profile_home,\n+        \"software-development\",\n+        \"kanban-orchestrator-operations\",\n+        \"orchestrator profile\",\n+    )\n+\n+    skills_tool = _reload_skills_tool(default_home, monkeypatch)\n+    assert skills_tool.SKILLS_DIR == default_home / \"skills\"\n+\n+    monkeypatch.setenv(\"HERMES_HOME\", str(profile_home))\n+\n+    result = json.loads(\n+        skills_tool.skill_view(\"kanban-orchestrator-operations\", preprocess=False)\n+    )\n+\n+    assert result[\"success\"] is True\n+    assert result[\"name\"] == \"kanban-orchestrator-operations\"\n+    assert Path(result[\"skill_dir\"]) == profile_skill_dir\n+    assert \"orchestrator profile\" in result[\"content\"]\n+\n+\n+def test_skills_list_uses_live_profile_home_after_module_import(tmp_path, monkeypatch):\n+    \"\"\"skills_list should list the active profile skills, not the import-time root.\"\"\"\n+    default_home = tmp_path / \"default-home\"\n+    profile_home = tmp_path / \"profiles\" / \"orchestrator\"\n+    _write_skill(default_home, \"autonomous-ai-agents\", \"default-only\", \"default home\")\n+    _write_skill(\n+        profile_home,\n+        \"software-development\",\n+        \"kanban-orchestrator-operations\",\n+        \"orchestrator profile\",\n+    )\n+\n+    skills_tool = _reload_skills_tool(default_home, monkeypatch)\n+    monkeypatch.setenv(\"HERMES_HOME\", str(profile_home))\n+\n+    result = json.loads(skills_tool.skills_list())\n+    names = {skill[\"name\"] for skill in result[\"skills\"]}\n+\n+    assert result[\"success\"] is True\n+    assert \"kanban-orchestrator-operations\" in names\n+    assert \"default-only\" not in names\n+\n+\n+def test_explicit_skills_dir_monkeypatch_still_wins(tmp_path, monkeypatch):\n+    \"\"\"Existing tests can still override tools.skills_tool.SKILLS_DIR directly.\"\"\"\n+    default_home = tmp_path / \"default-home\"\n+    profile_home = tmp_path / \"profiles\" / \"orchestrator\"\n+    patched_root = tmp_path / \"patched\"\n+    patched_skill_dir = _write_skill(\n+        patched_root,\n+        \"software-development\",\n+        \"patched-skill\",\n+        \"patched skills dir\",\n+    )\n+    _write_skill(\n+        profile_home,\n+        \"software-development\",\n+        \"profile-skill\",\n+        \"orchestrator profile\",\n+    )\n+\n+    skills_tool = _reload_skills_tool(default_home, monkeypatch)\n+    monkeypatch.setenv(\"HERMES_HOME\", str(profile_home))\n+    monkeypatch.setattr(skills_tool, \"SKILLS_DIR\", patched_root / \"skills\")\n+\n+    result = json.loads(skills_tool.skill_view(\"patched-skill\", preprocess=False))\n+\n+    assert result[\"success\"] is True\n+    assert Path(result[\"skill_dir\"]) == patched_skill_dir"]},{"file":"tests/tui_gateway/test_slash_worker_profile_home.py","hunks":["@@ -0,0 +1,124 @@\n+\"\"\"Tests for TUI gateway slash_worker profile_home propagation (#40677).\"\"\"\n+\n+import os\n+import subprocess\n+import sys\n+from unittest.mock import MagicMock, patch, call\n+\n+import pytest\n+\n+\n+def test_slash_worker_accepts_profile_home():\n+    \"\"\"_SlashWorker.__init__ accepts profile_home parameter.\"\"\"\n+    with patch.dict(\"sys.modules\", {\n+        \"hermes_constants\": MagicMock(get_hermes_home=MagicMock(return_value=\"/tmp/hermes_test\")),\n+    }):\n+        with patch(\"subprocess.Popen\") as mock_popen:\n+            mock_popen.return_value.stdout = MagicMock()\n+            mock_popen.return_value.stderr = MagicMock()\n+            \n+            from tui_gateway.server import _SlashWorker\n+            \n+            # Test initialization with profile_home\n+            worker = _SlashWorker(\n+                session_key=\"test_key\",\n+                model=\"test-model\",\n+                profile_home=\"/home/luke/.hermes/profiles/work\"\n+            )\n+            \n+            # Verify Popen was called\n+            assert mock_popen.called\n+            \n+            # Check that HERMES_HOME was set in the environment\n+            call_kwargs = mock_popen.call_args[1]\n+            assert \"env\" in call_kwargs\n+            assert call_kwargs[\"env\"][\"HERMES_HOME\"] == \"/home/luke/.hermes/profiles/work\"\n+\n+\n+def test_slash_worker_without_profile_home():\n+    \"\"\"_SlashWorker works without profile_home parameter (backward compatible).\"\"\"\n+    with patch.dict(\"sys.modules\", {\n+        \"hermes_constants\": MagicMock(get_hermes_home=MagicMock(return_value=\"/tmp/hermes_test\")),\n+    }):\n+        with patch(\"subprocess.Popen\") as mock_popen:\n+            mock_popen.return_value.stdout = MagicMock()\n+            mock_popen.return_value.stderr = MagicMock()\n+            \n+            from tui_gateway.server import _SlashWorker\n+            \n+            # Test initialization without profile_home (backward compatible)\n+            worker = _SlashWorker(\n+                session_key=\"test_key\",\n+                model=\"test-model\"\n+            )\n+            \n+            # Verify Popen was called\n+            assert mock_popen.called\n+            \n+            # Check that HERMES_HOME was NOT overridden\n+            call_kwargs = mock_popen.call_args[1]\n+            assert \"env\" in call_kwargs\n+            # HERMES_HOME should be from parent env or undefined (inherited from os.environ)\n+            # The key is that it's not explicitly set when profile_home is None\n+            env = call_kwargs[\"env\"]\n+            # Verify env is a copy of os.environ\n+            assert \"PATH\" in env\n+\n+\n+def test_slash_worker_with_none_profile_home():\n+    \"\"\"_SlashWorker with explicit profile_home=None works.\"\"\"\n+    with patch.dict(\"sys.modules\", {\n+        \"hermes_constants\": MagicMock(get_hermes_home=MagicMock(return_value=\"/tmp/hermes_test\")),\n+    }):\n+        with patch(\"subprocess.Popen\") as mock_popen:\n+            mock_popen.return_value.stdout = MagicMock()\n+            mock_popen.return_value.stderr = MagicMock()\n+            \n+            from tui_gateway.server import _SlashWorker\n+            \n+            # Test initialization with explicit None\n+            worker = _SlashWorker(\n+                session_key=\"test_key\",\n+                model=\"test-model\",\n+                profile_home=None\n+            )\n+            \n+            # Verify Popen was called\n+            assert mock_popen.called\n+            \n+            # Check that HERMES_HOME was NOT set\n+            call_kwargs = mock_popen.call_args[1]\n+            env = call_kwargs[\"env\"]\n+            # When profile_home is None, HERMES_HOME should come from parent env only\n+            if \"HERMES_HOME\" in env:\n+                # This is from os.environ at test time, not from our code\n+                pass\n+\n+\n+def test_slash_worker_inherits_argv_correctly():\n+    \"\"\"_SlashWorker passes correct argv to Popen.\"\"\"\n+    with patch.dict(\"sys.modules\", {\n+        \"hermes_constants\": MagicMock(get_hermes_home=MagicMock(return_value=\"/tmp/hermes_test\")),\n+    }):\n+        with patch(\"subprocess.Popen\") as mock_popen:\n+            mock_popen.return_value.stdout = MagicMock()\n+            mock_popen.return_value.stderr = MagicMock()\n+            \n+            from tui_gateway.server import _SlashWorker\n+            \n+            # Test that argv is correct\n+            worker = _SlashWorker(\n+                session_key=\"my_session\",\n+                model=\"gpt-4\"\n+            )\n+            \n+            call_args = mock_popen.call_args[0][0]\n+            \n+            # Verify argv structure\n+            assert sys.executable in call_args\n+            assert \"-m\" in call_args\n+            assert \"tui_gateway.slash_worker\" in call_args\n+            assert \"--session-key\" in call_args\n+            assert \"my_session\" in call_args\n+            assert \"--model\" in call_args\n+            assert \"gpt-4\" in call_args"]},{"file":"tools/skill_manager_tool.py","hunks":["@@ -150,6 +150,22 @@ def _security_scan_skill(skill_dir: Path) -> Optional[str]:\n # All skills live in ~/.hermes/skills/ (single source of truth)\n HERMES_HOME = get_hermes_home()\n SKILLS_DIR = HERMES_HOME / \"skills\"\n+_SKILLS_DIR_AT_IMPORT = SKILLS_DIR\n+\n+\n+def _skills_dir() -> Path:\n+    \"\"\"Return the active profile's skills directory at call time.\n+\n+    Long-lived multi-profile runtimes (Dashboard/TUI/Desktop backend, cron,\n+    kanban workers) import this module once under the launch HERMES_HOME and\n+    later bind a different profile per session (#40677). Honor an explicitly\n+    patched module-level ``SKILLS_DIR`` (tests), otherwise resolve from the\n+    live profile-scoped HERMES_HOME on every call.\n+    \"\"\"\n+    configured = Path(SKILLS_DIR)\n+    if configured != _SKILLS_DIR_AT_IMPORT:\n+        return configured\n+    return get_hermes_home() / \"skills\"\n \n MAX_NAME_LENGTH = 64\n MAX_DESCRIPTION_LENGTH = 1024","@@ -174,7 +190,7 @@ def _containing_skills_root(skill_path: Path) -> Path:\n             return root\n         except (ValueError, OSError):\n             continue\n-    return SKILLS_DIR\n+    return _skills_dir()\n \n \n def _is_path_redirect(path: Path) -> bool:","@@ -562,8 +578,8 @@ def _validate_content_size(content: str, label: str = \"SKILL.md\") -> Optional[st\n def _resolve_skill_dir(name: str, category: str = None) -> Path:\n     \"\"\"Build the directory path for a new skill, optionally under a category.\"\"\"\n     if category:\n-        return SKILLS_DIR / category / name\n-    return SKILLS_DIR / name\n+        return _skills_dir() / category / name\n+    return _skills_dir() / name\n \n \n def _find_skill(name: str) -> Optional[Dict[str, Any]]:","@@ -608,8 +624,9 @@ def _find_skill_in_other_profiles(name: str) -> List[Tuple[str, Path]]:\n         return matches\n \n     # Collect (profile_name, skills_dir) for every profile EXCEPT the\n-    # one whose SKILLS_DIR we already searched in _find_skill().\n-    active_dir = SKILLS_DIR.resolve() if SKILLS_DIR.exists() else SKILLS_DIR\n+    # one whose skills dir we already searched in _find_skill().\n+    _active = _skills_dir()\n+    active_dir = _active.resolve() if _active.exists() else _active\n     candidates: List[Tuple[str, Path]] = []\n \n     # Default profile (~/.hermes/skills) — only consider when active is non-default.","@@ -828,7 +845,7 @@ def _create_skill(name: str, content: str, category: str = None) -> Dict[str, An\n     result = {\n         \"success\": True,\n         \"message\": f\"Skill '{name}' created.\",\n-        \"path\": str(skill_dir.relative_to(SKILLS_DIR)),\n+        \"path\": str(skill_dir.relative_to(_skills_dir())),\n         \"skill_md\": str(skill_md),\n         \"_change\": {\"description\": _desc},\n     }"]},{"file":"tools/skills_tool.py","hunks":["@@ -92,6 +92,22 @@\n # skills all coexist here without polluting the git repo.\n HERMES_HOME = get_hermes_home()\n SKILLS_DIR = HERMES_HOME / \"skills\"\n+_SKILLS_DIR_AT_IMPORT = SKILLS_DIR\n+\n+\n+def _skills_dir() -> Path:\n+    \"\"\"Return the active profile's skills directory at call time.\n+\n+    Some long-lived runtimes import this module before the active profile has\n+    set HERMES_HOME. Keep the legacy SKILLS_DIR module attribute for tests and\n+    external patchers, but when it has not been patched, resolve from the live\n+    profile-scoped HERMES_HOME on every call.\n+    \"\"\"\n+    configured = Path(SKILLS_DIR)\n+    if configured != _SKILLS_DIR_AT_IMPORT:\n+        return configured\n+    return get_hermes_home() / \"skills\"\n+\n \n # Anthropic-recommended limits for progressive disclosure efficiency\n MAX_NAME_LENGTH = 64","@@ -501,9 +517,9 @@ def _get_category_from_path(skill_path: Path) -> Optional[str]:\n     For paths like: ~/.hermes/skills/mlops/axolotl/SKILL.md -> \"mlops\"\n     Also works for external skill dirs configured via skills.external_dirs.\n     \"\"\"\n-    # Try the module-level SKILLS_DIR first (respects monkeypatching in tests),\n+    # Try the active profile skills dir first (respects monkeypatching in tests),\n     # then fall back to external dirs from config.\n-    dirs_to_check = [SKILLS_DIR]\n+    dirs_to_check = [_skills_dir()]\n     try:\n         from agent.skill_utils import get_external_skills_dirs\n         dirs_to_check.extend(get_external_skills_dirs())","@@ -622,8 +638,9 @@ def _find_all_skills(*, skip_disabled: bool = False) -> List[Dict[str, Any]]:\n \n     # Scan local dir first, then external dirs (local takes precedence)\n     dirs_to_scan = []\n-    if SKILLS_DIR.exists():\n-        dirs_to_scan.append(SKILLS_DIR)\n+    active_skills_dir = _skills_dir()\n+    if active_skills_dir.exists():\n+        dirs_to_scan.append(active_skills_dir)\n     dirs_to_scan.extend(get_external_skills_dirs())\n \n     for scan_dir in dirs_to_scan:","@@ -701,8 +718,9 @@ def skills_list(category: str = None, task_id: str = None) -> str:\n         JSON string with minimal skill info: name, description, category\n     \"\"\"\n     try:\n-        if not SKILLS_DIR.exists():\n-            SKILLS_DIR.mkdir(parents=True, exist_ok=True)\n+        active_skills_dir = _skills_dir()\n+        if not active_skills_dir.exists():\n+            active_skills_dir.mkdir(parents=True, exist_ok=True)\n             return json.dumps(\n                 {\n                     \"success\": True,","@@ -984,8 +1002,9 @@ def skill_view(\n \n         # Build list of all skill directories to search\n         all_dirs = []\n-        if SKILLS_DIR.exists():\n-            all_dirs.append(SKILLS_DIR)\n+        active_skills_dir = _skills_dir()\n+        if active_skills_dir.exists():\n+            all_dirs.append(active_skills_dir)\n         all_dirs.extend(get_external_skills_dirs())\n \n         if not all_dirs:","@@ -1135,7 +1154,7 @@ def _record(sd: Optional[Path], smd: Path) -> None:\n         # Security: warn if skill is loaded from outside trusted directories\n         # (local skills dir + configured external_dirs are all trusted)\n         _outside_skills_dir = True\n-        _trusted_dirs = [SKILLS_DIR.resolve()]\n+        _trusted_dirs = [active_skills_dir.resolve()]\n         try:\n             _trusted_dirs.extend(d.resolve() for d in all_dirs[1:])\n         except Exception:","@@ -1375,7 +1394,7 @@ def _record(sd: Optional[Path], smd: Path) -> None:\n             linked_files[\"scripts\"] = script_files\n \n         try:\n-            rel_path = str(skill_md.relative_to(SKILLS_DIR))\n+            rel_path = str(skill_md.relative_to(active_skills_dir))\n         except ValueError:\n             # External skill — use path relative to the skill's own parent dir\n             rel_path = str(skill_md.relative_to(skill_md.parent.parent)) if skill_md.parent.parent else skill_md.name"]},{"file":"tui_gateway/server.py","hunks":["@@ -275,7 +275,7 @@ def close(self) -> None:\n class _SlashWorker:\n     \"\"\"Persistent HermesCLI subprocess for slash commands.\"\"\"\n \n-    def __init__(self, session_key: str, model: str):\n+    def __init__(self, session_key: str, model: str, profile_home: str | None = None):\n         self._lock = threading.Lock()\n         self._seq = 0\n         self.stderr_tail: list[str] = []","@@ -294,6 +294,15 @@ def __init__(self, session_key: str, model: str):\n         self._closed = False\n         from hermes_cli._subprocess_compat import windows_hide_flags\n \n+        # slash_worker runs the Hermes agent → needs provider credentials.\n+        # Tier-1 secrets (gateway/GitHub/infra) are still stripped (#29157).\n+        env = hermes_subprocess_env(inherit_credentials=True)\n+        if profile_home:\n+            # Global-remote / multi-profile sessions: the worker must resolve\n+            # config/skills/state against the session's profile home, not the\n+            # gateway's launch HERMES_HOME (#40677).\n+            env[\"HERMES_HOME\"] = str(profile_home)\n+\n         # start_new_session=True detaches the slash worker into its own\n         # process group / session. Without this, the worker inherits the\n         # gateway's pgid (= TUI parent PID). When mcp_tool's","@@ -310,9 +319,7 @@ def __init__(self, session_key: str, model: str):\n             text=True,\n             bufsize=1,\n             cwd=os.getcwd(),\n-            # slash_worker runs the Hermes agent → needs provider credentials.\n-            # Tier-1 secrets (gateway/GitHub/infra) are still stripped (#29157).\n-            env=hermes_subprocess_env(inherit_credentials=[redacted:secret-assignment]\n+            env=env,\n             creationflags=windows_hide_flags(),\n             start_new_session=True,\n         )","@@ -1299,7 +1306,11 @@ def _build() -> None:\n             current[\"config_model_seen\"] = _config_model_target()\n \n             try:\n-                worker = _SlashWorker(key, getattr(agent, \"model\", _resolve_model()))\n+                worker = _SlashWorker(\n+                    key,\n+                    getattr(agent, \"model\", _resolve_model()),\n+                    profile_home=current.get(\"profile_home\"),\n+                )\n                 _attach_worker(sid, current, worker)\n             except Exception:\n                 pass","@@ -2628,6 +2639,7 @@ def _restart_slash_worker(sid: str, session: dict):\n         new_worker = _SlashWorker(\n             session[\"session_key\"],\n             getattr(session.get(\"agent\"), \"model\", _resolve_model()),\n+            profile_home=session.get(\"profile_home\"),\n         )\n     except Exception:\n         session[\"slash_worker\"] = None","@@ -4514,7 +4526,11 @@ def _init_session(\n         _attach_worker(\n             sid,\n             _sessions[sid],\n-            _SlashWorker(key, getattr(agent, \"model\", _resolve_model())),\n+            _SlashWorker(\n+                key,\n+                getattr(agent, \"model\", _resolve_model()),\n+                profile_home=_sessions[sid].get(\"profile_home\"),\n+            ),\n         )\n     except Exception:\n         # Defer hard-failure to slash.exec; chat still works without slash worker.","@@ -12710,6 +12726,7 @@ def _(rid, params: dict) -> dict:\n             worker = _SlashWorker(\n                 session[\"session_key\"],\n                 getattr(session.get(\"agent\"), \"model\", _resolve_model()),\n+                profile_home=session.get(\"profile_home\"),\n             )\n             _attach_worker(params.get(\"session_id\", \"\"), session, worker)\n         except Exception as e:\n"]}]},{"kind":"pr","number":60257,"title":"fix(skills): invalidate slash-command cache on profile change in long-lived runtimes","url":null,"state":"closed","close_reason":{"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":73521,"title":"fix(desktop): voice profile scoping, remote media playback, stop-button state, read-aloud content","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["apps/desktop/src/app/chat/composer/hooks/use-voice-conversation.ts","apps/desktop/src/components/assistant-ui/markdown-text.media.test.tsx","apps/desktop/src/components/assistant-ui/markdown-text.tsx","apps/desktop/src/hermes-profile-scope.test.ts","apps/desktop/src/hermes.test.ts","apps/desktop/src/hermes.ts","apps/desktop/src/lib/media.remote.test.ts","apps/desktop/src/lib/media.ts","apps/desktop/src/lib/speech-text.test.ts","apps/desktop/src/lib/speech-text.ts","apps/desktop/src/lib/voice-playback.ts","contributors/emails/[redacted:email]","contributors/emails/[redacted:email]","contributors/emails/[redacted:email]","hermes_cli/web_server.py","tests/hermes_cli/test_web_server_profile_unification.py"]}],"trust":{"default_tier":"verified","includes_suggested":false}}