{"repo":"NousResearch/hermes-agent","public_id":"40334","display_label":"Profile switch CWD and terminal.cwd priority fixes","summary":"A cluster of bugs and fixes around the Desktop app failing to apply the correct working directory when switching profiles, specifically that terminal.cwd from the target profile was being ignored in favor of stale workspace or previous-profile paths.","subsystem":"apps/desktop","counts":{"members":9,"open_issues":0,"open_issues_distinct":0,"open_prs":3,"closed_issues":3,"merged_prs":2,"attempt_prs":4},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":122310,"merged_at":"2026-09-25T05:38:16+00:00"},"resolution":{"state":"partially_resolved","by":"pr-40892,pr-122310"},"freshness":{"state":"verified","last_verified_at":"2026-09-25T05:38:16+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.19666666666666666},"fix_edges":[{"issue_number":40334,"pr_number":40892,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-10T00:45:30+00:00"},{"issue_number":54990,"pr_number":122310,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-09-25T05:38:16+00:00"}],"members":[{"kind":"issue","number":40334,"title":"Desktop: profile switch ignores terminal.cwd, uses wrong profile's working directory","url":null,"state":"closed"},{"kind":"issue","number":52589,"title":"Desktop app-global workspace overrides profile-specific terminal.cwd","url":null,"state":"closed"},{"kind":"issue","number":54990,"title":"[Bug]: Profile switch in Desktop app leaves shell CWD stuck in previous profile's workspace","url":null,"state":"closed"},{"kind":"pr","number":40892,"title":"fix(tui_gateway): honor target profile's terminal.cwd on desktop profile switch","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["tests/test_tui_gateway_server.py","tui_gateway/server.py"],"fix_hunks":[{"file":"tests/test_tui_gateway_server.py","hunks":["@@ -60,6 +60,64 @@ def test_session_context_explicit_cwd_for_ephemeral_task(monkeypatch, tmp_path):\n         server._clear_session_context(tokens)\n \n \n+def _write_profile_cfg(home: Path, cwd: str | None) -> Path:\n+    import yaml\n+\n+    home.mkdir(parents=True, exist_ok=True)\n+    cfg = {\"terminal\": {\"cwd\": cwd}} if cwd is not None else {}\n+    (home / \"config.yaml\").write_text(yaml.safe_dump(cfg), encoding=\"utf-8\")\n+    return home\n+\n+\n+def test_profile_configured_cwd_reads_target_profile(tmp_path):\n+    \"\"\"A profile's own terminal.cwd is read from its config.yaml.\"\"\"\n+    project = tmp_path / \"proj\"\n+    project.mkdir()\n+    home = _write_profile_cfg(tmp_path / \"home\", str(project))\n+    assert server._profile_configured_cwd(home) == str(project)\n+\n+\n+def test_profile_configured_cwd_skips_placeholders_and_missing(tmp_path):\n+    \"\"\"Placeholder values, missing config, and bad paths fall through to None.\"\"\"\n+    assert server._profile_configured_cwd(None) is None\n+    assert server._profile_configured_cwd(tmp_path / \"nope\") is None\n+    for placeholder in (\".\", \"auto\", \"cwd\", \"\"):\n+        home = _write_profile_cfg(tmp_path / placeholder.strip(\".\"), placeholder)\n+        assert server._profile_configured_cwd(home) is None\n+    home = _write_profile_cfg(tmp_path / \"ghost\", str(tmp_path / \"does-not-exist\"))\n+    assert server._profile_configured_cwd(home) is None\n+\n+\n+def test_completion_cwd_prefers_profile_over_stale_env(monkeypatch, tmp_path):\n+    \"\"\"Issue #40334: a new session bound to another profile must use THAT\n+    profile's terminal.cwd, not the launch profile's stale TERMINAL_CWD.\"\"\"\n+    profile_b = tmp_path / \"ef-design\"\n+    profile_b.mkdir()\n+    home = _write_profile_cfg(tmp_path / \"home-b\", str(profile_b))\n+    stale = tmp_path / \"mahjong\"\n+    stale.mkdir()\n+\n+    monkeypatch.setenv(\"TERMINAL_CWD\", str(stale))\n+    monkeypatch.setattr(server, \"_profile_home\", lambda name: home if name else None)\n+\n+    assert server._completion_cwd({\"profile\": \"ef-design\"}) == str(profile_b)\n+    # No profile → unchanged fallback to the launch env var.\n+    assert server._completion_cwd({}) == str(stale)\n+\n+\n+def test_completion_cwd_explicit_cwd_wins_over_profile(monkeypatch, tmp_path):\n+    \"\"\"An explicit client-provided cwd still beats the profile config.\"\"\"\n+    explicit = tmp_path / \"explicit\"\n+    explicit.mkdir()\n+    profile_b = tmp_path / \"configured\"\n+    profile_b.mkdir()\n+    home = _write_profile_cfg(tmp_path / \"home-c\", str(profile_b))\n+\n+    monkeypatch.setattr(server, \"_profile_home\", lambda name: home if name else None)\n+    result = server._completion_cwd({\"cwd\": str(explicit), \"profile\": \"ef-design\"})\n+    assert result == str(explicit)\n+\n+\n def test_terminal_task_cwd_local_backend_uses_session_cwd(monkeypatch, tmp_path):\n     \"\"\"A local terminal backend must keep host-validated session cwd behaviour.\"\"\"\n     project = tmp_path / \"project\""]},{"file":"tui_gateway/server.py","hunks":["@@ -488,6 +488,40 @@ def _profile_home(profile: str | None) -> Path | None:\n     return home if (home / \"state.db\").exists() or home.exists() else None\n \n \n+# Placeholder ``terminal.cwd`` values that don't name a real directory — the\n+# gateway resolves these to the home dir at runtime, so they must NOT be treated\n+# as an explicit workspace (mirrors gateway/run.py's config bridge).\n+_CWD_PLACEHOLDERS = {\".\", \"auto\", \"cwd\"}\n+\n+\n+def _profile_configured_cwd(profile_home: Path | None) -> str | None:\n+    \"\"\"Resolve a non-launch profile's ``terminal.cwd`` from its own config.yaml.\n+\n+    The desktop's app-global remote mode serves every profile from one backend,\n+    so the process-global ``TERMINAL_CWD`` belongs to the *launch* profile. A new\n+    session bound to another profile must take its workspace from THAT profile's\n+    config, not the stale env var (issue #40334). Returns an absolute, existing\n+    directory, or None for placeholders / missing / invalid paths.\n+    \"\"\"\n+    if profile_home is None:\n+        return None\n+    try:\n+        import yaml\n+\n+        p = Path(profile_home) / \"config.yaml\"\n+        if not p.exists():\n+            return None\n+        with open(p, encoding=\"utf-8\") as f:\n+            data = yaml.safe_load(f) or {}\n+        raw = str((data.get(\"terminal\") or {}).get(\"cwd\") or \"\").strip()\n+        if not raw or raw in _CWD_PLACEHOLDERS:\n+            return None\n+        resolved = os.path.abspath(os.path.expanduser(raw))\n+        return resolved if os.path.isdir(resolved) else None\n+    except Exception:\n+        return None\n+\n+\n def write_json(obj: dict) -> bool:\n     \"\"\"Emit one JSON frame. Routes via the most-specific transport available.\n ","@@ -798,9 +832,13 @@ def _normalize_completion_path(path_part: str) -> str:\n \n \n def _completion_cwd(params: dict | None = None) -> str:\n+    params = params or {}\n     raw = (\n-        (params or {}).get(\"cwd\")\n-        or _sessions.get((params or {}).get(\"session_id\") or \"\", {}).get(\"cwd\")\n+        params.get(\"cwd\")\n+        or _sessions.get(params.get(\"session_id\") or \"\", {}).get(\"cwd\")\n+        # A session bound to another profile resolves its workspace from THAT\n+        # profile's config before falling back to the launch profile's env var.\n+        or _profile_configured_cwd(_profile_home(params.get(\"profile\")))\n         or os.environ.get(\"TERMINAL_CWD\")\n         or os.getcwd()\n     )\n"]}]},{"kind":"pr","number":56869,"title":"fix(tui_gateway): prefer profile terminal.cwd over stale client-sent cwd on profile switch","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"93340fa3c","number":0},{"kind":"commit","value":"f3af7930c","number":0}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":61653,"title":"fix: profile terminal.cwd overrides desktop app-global workspace (#52589)","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":63976,"title":"fix(desktop): clear project scope on profile switch","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"65c2b249345e","number":0},{"kind":"pr","value":"#40892","number":40892},{"kind":"pr","value":"#43082","number":43082},{"kind":"pr","value":"#56869","number":56869},{"kind":"pr","value":"#61653","number":61653},{"kind":"pr","value":"#63976","number":63976},{"kind":"pr","value":"#40334","number":40334},{"kind":"pr","value":"#54990","number":54990},{"kind":"pr","value":"#374151","number":374151},{"kind":"pr","value":"#52589","number":52589}],"category":"partial_fix","implication":"needs_llm"},"merged":false},{"kind":"pr","number":122310,"title":"fix(desktop): leave the entered project when switching profile or backend","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["apps/desktop/src/app/chat/sidebar/chat-sidebar.integration.test.tsx","apps/desktop/src/app/chat/sidebar/index.tsx","apps/desktop/src/app/session/hooks/default-new-session.test.tsx","apps/desktop/src/app/session/hooks/use-session-actions.test.tsx","apps/desktop/src/app/session/hooks/use-session-actions/index.ts","apps/desktop/src/app/session/new-session-route.ts","apps/desktop/src/app/session/workspace-session-target.test.ts","apps/desktop/src/store/coding-status.ts","apps/desktop/src/store/gateway-switch.test.ts","apps/desktop/src/store/gateway-switch.ts","apps/desktop/src/store/profile.test.ts","apps/desktop/src/store/profile.ts","apps/desktop/src/store/project-scope.ts","apps/desktop/src/store/projects.test.ts","apps/desktop/src/store/projects.ts"]},{"kind":"pr","number":122898,"title":"fix: profile terminal.cwd beats the desktops inherited workspace cwd","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"a9972dc3f9","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}