{"repo":"NousResearch/hermes-agent","public_id":"10309","display_label":"Session-scoped working directory tracking and fixes","summary":"These issues and PRs center on per-session working directory management: fixing stale CWD values in CLI, TUI, runtime footer, and code execution, and adding features to set or switch the working directory per session across CLI, desktop, and gateway contexts. A small number of members touch unrelated skills/asset lookup behavior.","subsystem":"agent","counts":{"members":23,"open_issues":5,"open_issues_distinct":5,"open_prs":4,"closed_issues":3,"merged_prs":2,"attempt_prs":13},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":65213},"resolution":{"state":"partially_resolved","by":"pr-65487"},"freshness":{"state":"verified","last_verified_at":"2026-07-16T11:24:07+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.07067193675889326},"fix_edges":[{"issue_number":56047,"pr_number":65213,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":56047,"pr_number":65487,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":10309,"title":"Feature: session-scoped repo pinning to preserve repository identity across compression/resume","url":null,"state":"open"},{"kind":"issue","number":29531,"title":"Per-session working directory for gateway (OpenAI-compatible API) sessions","url":null,"state":"open"},{"kind":"issue","number":35745,"title":"[Bug]: Runtime footer displays stale TERMINAL_CWD instead of the agent's live working directory","url":null,"state":"open"},{"kind":"issue","number":37277,"title":"[Feature]: Add session-scoped workspace binding for gateway chats","url":null,"state":"open"},{"kind":"issue","number":42525,"title":"feat(desktop): allow changing workspace/directory from Hermes Desktop UI","url":null,"state":"closed"},{"kind":"issue","number":49637,"title":"[Bug]: TUI reuses stale HERMES_CWD instead of current launch cwd","url":null,"state":"closed"},{"kind":"issue","number":50195,"title":"feat: Add ability to switch working directory during session","url":null,"state":"open"},{"kind":"issue","number":56047,"title":"Bug: execute_code sandbox ignores the per-session cwd override, diverges from write_file/terminal","url":null,"state":"closed"},{"kind":"pr","number":35767,"title":"fix(gateway): show live cwd in runtime footer","url":null,"state":"open","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":35768,"title":"fix(skills): ignore nested assets in skill lookup","url":null,"state":"closed","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":36895,"title":"fix(skills): ignore asset markdown in skill_view collision scan","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"9137b86a5286e4ea420e9fb894bce34fee546b3d","number":0},{"kind":"pr","value":"#37164","number":37164}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":49658,"title":"fix(tui): always capture current CWD instead of inheriting stale HERMES_CWD","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#49662","number":49662},{"kind":"pr","value":"#49662","number":49662},{"kind":"pr","value":"#49662","number":49662},{"kind":"pr","value":"#49637","number":49637}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":49662,"title":"fix(cli): use live launch cwd for HERMES_CWD instead of stale env","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#49658","number":49658},{"kind":"pr","value":"#49658","number":49658}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":56055,"title":"fix(agent): resolve execute_code cwd from per-session override (#56047)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"298a94926","number":0},{"kind":"pr","value":"#65487","number":65487},{"kind":"pr","value":"#56803","number":56803},{"kind":"pr","value":"#65213","number":65213}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":56069,"title":"fix(code-exec): honor session cwd override","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#35513","number":35513},{"kind":"pr","value":"#56055","number":56055},{"kind":"pr","value":"#46843","number":46843},{"kind":"pr","value":"#56047","number":56047},{"kind":"pr","value":"#35513","number":35513}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":56406,"title":"fix(tools): honor per-session cwd override in execute_code project mode","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#35513","number":35513}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":56803,"title":"fix(execute-code): honor session cwd overrides","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"4d6686c18","number":0},{"kind":"pr","value":"#65487","number":65487},{"kind":"pr","value":"#56055","number":56055},{"kind":"pr","value":"#65213","number":65213}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":65213,"title":"refactor(terminal): per-session cwd records replace shared-env cwd tracking","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["tests/agent/test_file_safety_credentials.py","tests/tools/test_container_cwd_sanitize.py","tests/tools/test_file_staleness.py","tests/tools/test_file_tools.py","tests/tools/test_file_tools_cwd_resolution.py","tests/tools/test_file_tools_tilde_profile.py","tests/tools/test_resolve_path.py","tests/tools/test_session_cwd_store.py","tests/tools/test_terminal_task_cwd.py","tools/delegate_tool.py","tools/file_tools.py","tools/terminal_tool.py"],"fix_hunks":[{"file":"tests/agent/test_file_safety_credentials.py","hunks":["@@ -144,6 +144,7 @@ def test_read_file_tool_blocks_relative_path_under_terminal_cwd(\n     import json\n \n     import tools.file_tools as ft\n+    import tools.terminal_tool as terminal_tool\n \n     _create(fake_home, \"auth.json\")\n     # Force the file_tools resolver to anchor relative paths at HERMES_HOME","@@ -151,7 +152,7 @@ def test_read_file_tool_blocks_relative_path_under_terminal_cwd(\n     monkeypatch.setenv(\"TERMINAL_CWD\", str(fake_home))\n     monkeypatch.chdir(tmp_path)\n     monkeypatch.setattr(\n-        ft, \"_get_live_tracking_cwd\", lambda task_id=\"default\": None\n+        terminal_tool, \"_session_cwd\", {}\n     )\n \n     out = json.loads(ft.read_file_tool(\"auth.json\"))","@@ -166,6 +167,7 @@ def test_read_file_tool_blocks_nested_google_oauth_path(\n     import json\n \n     import tools.file_tools as ft\n+    import tools.terminal_tool as terminal_tool\n \n     oauth = _create(fake_home, Path(\"auth\") / \"google_oauth.json\")\n     oauth.write_text(","@@ -180,7 +182,7 @@ def test_read_file_tool_blocks_nested_google_oauth_path(\n     )\n     monkeypatch.chdir(tmp_path)\n     monkeypatch.setattr(\n-        ft, \"_get_live_tracking_cwd\", lambda task_id=\"default\": None\n+        terminal_tool, \"_session_cwd\", {}\n     )\n \n     out = json.loads(ft.read_file_tool(str(oauth), task_id=\"google-oauth-test\"))","@@ -195,6 +197,7 @@ def test_search_tool_blocks_direct_auth_json_path(fake_home, monkeypatch):\n     import json\n \n     import tools.file_tools as ft\n+    import tools.terminal_tool as terminal_tool\n \n     auth = _create(fake_home, \"auth.json\")\n     auth.write_text(\"SEARCH_DIRECT_AUTH_SECRET\", encoding=\"utf-8\")","@@ -223,6 +226,7 @@ def test_search_tool_filters_credential_results(fake_home, tmp_path, monkeypatch\n \n     from tools.file_operations import SearchMatch, SearchResult\n     import tools.file_tools as ft\n+    import tools.terminal_tool as terminal_tool\n \n     auth = _create(fake_home, \"auth.json\")\n     token = [redacted:secret-assignment] Path(\"mcp-tokens\") / \"provider.json\")","@@ -256,7 +260,7 @@ def search(self, **kwargs):\n     monkeypatch.chdir(tmp_path)\n     monkeypatch.setattr(ft, \"_get_file_ops\", lambda task_id=\"default\": FakeFileOps())\n     monkeypatch.setattr(\n-        ft, \"_get_live_tracking_cwd\", lambda task_id=\"default\": None\n+        terminal_tool, \"_session_cwd\", {}\n     )\n \n     search_response = ft.search_tool("]},{"file":"tests/tools/test_container_cwd_sanitize.py","hunks":["@@ -211,7 +211,7 @@ def fake_create_environment(env_type, image, cwd, timeout, **kwargs):\n         monkeypatch.setattr(tt, \"_active_environments\", {})\n         monkeypatch.setattr(tt, \"_last_activity\", {})\n         monkeypatch.setattr(ft, \"_file_ops_cache\", {})\n-        monkeypatch.setattr(ft, \"_last_known_cwd\", {})\n+        monkeypatch.setattr(tt, \"_session_cwd\", {})\n \n         task_id = \"sess-fileops-host-cwd\"\n         tt.register_task_env_overrides(task_id, {\"cwd\": override_cwd})"]},{"file":"tests/tools/test_file_staleness.py","hunks":["@@ -150,8 +150,8 @@ def test_different_task_isolated(self, mock_ops):\n         self.assertNotIn(\"_warning\", result)\n \n     @patch(\"tools.file_tools._get_file_ops\")\n-    def test_relative_path_uses_live_cwd_for_staleness_tracking(self, mock_ops):\n-        \"\"\"Relative-path stale tracking must follow the live terminal cwd.\"\"\"\n+    def test_relative_path_uses_recorded_session_cwd_for_staleness_tracking(self, mock_ops):\n+        \"\"\"Relative-path stale tracking must follow the session's recorded cwd.\"\"\"\n         start_dir = os.path.join(self._tmpdir, \"start\")\n         live_dir = os.path.join(self._tmpdir, \"worktree\")\n         os.makedirs(start_dir, exist_ok=True)","@@ -165,15 +165,12 @@ def test_relative_path_uses_live_cwd_for_staleness_tracking(self, mock_ops):\n             f.write(\"live copy\\n\")\n \n         fake_ops = _make_fake_ops(\"live copy\\n\", 10)\n-        fake_ops.env = SimpleNamespace(cwd=live_dir)\n-        fake_ops.cwd = start_dir\n         mock_ops.return_value = fake_ops\n \n-        from tools import file_tools\n+        from tools import terminal_tool\n \n-        with file_tools._file_ops_lock:\n-            previous = file_tools._file_ops_cache.get(\"live_task\")\n-            file_tools._file_ops_cache[\"live_task\"] = fake_ops\n+        # The session cd'd into the worktree (recorded by the completed command).\n+        terminal_tool.record_session_cwd(\"live_task\", live_dir)\n \n         try:\n             with patch.dict(os.environ, {\"TERMINAL_CWD\": start_dir}, clear=False):","@@ -187,11 +184,7 @@ def test_relative_path_uses_live_cwd_for_staleness_tracking(self, mock_ops):\n                     write_file_tool(\"shared.txt\", \"replacement\", task_id=\"live_task\")\n                 )\n         finally:\n-            with file_tools._file_ops_lock:\n-                if previous is None:\n-                    file_tools._file_ops_cache.pop(\"live_task\", None)\n-                else:\n-                    file_tools._file_ops_cache[\"live_task\"] = previous\n+            terminal_tool.clear_session_cwd(\"live_task\")\n \n         self.assertIn(\"_warning\", result)\n         self.assertIn(\"modified since you last read\", result[\"_warning\"])"]},{"file":"tests/tools/test_file_tools.py","hunks":["@@ -677,13 +677,16 @@ def test_no_anyof_required_stays_mode_only(self):\n \n \n # ---------------------------------------------------------------------------\n-# _last_known_cwd tests (#26211: silent file creation failure in long conversations)\n+# Session-cwd persistence across env recreation (#26211: silent file creation\n+# failure in long conversations). The durable anchor is the per-session cwd\n+# record in terminal_tool; env cleanup cannot lose it because it never lived\n+# on the env.\n # ---------------------------------------------------------------------------\n \n-class TestLastKnownCwd:\n+class TestSessionCwdSurvivesEnvRecreation:\n     \"\"\"\n     When the terminal environment is cleaned up and re-created during a long\n-    conversation, _last_known_cwd preserves the old environment's CWD so\n+    conversation, the session's cwd record preserves the working directory so\n     subsequent file writes with relative paths land in the right directory.\n \n     Regression guard for issue #26211.","@@ -693,12 +696,12 @@ class TestLastKnownCwd:\n     @patch(\"tools.file_tools._file_ops_cache\", new_callable=dict)\n     @patch(\"tools.terminal_tool._get_env_config\")\n     @patch(\"tools.terminal_tool._create_environment\")\n-    def test_last_known_cwd_preserved_across_env_recreation(\n+    def test_recorded_cwd_used_for_recreated_env(\n         self, mock_create_env, mock_config, mock_cache, mock_active\n     ):\n-        from tools.file_tools import _get_file_ops, _last_known_cwd\n+        import tools.terminal_tool as tt\n+        from tools.file_tools import _get_file_ops\n \n-        # Setup: create a mock env with a known CWD\n         mock_env = MagicMock()\n         mock_env.cwd = \"/Users/user/project\"\n         mock_create_env.return_value = mock_env","@@ -709,42 +712,35 @@ def test_last_known_cwd_preserved_across_env_recreation(\n         }\n \n         task_id = \"default\"\n+        # The session's record holds the directory (written by the last\n+        # completed terminal command before the env was cleaned up).\n+        tt.record_session_cwd(task_id, \"/Users/user/project\")\n+        try:\n+            _get_file_ops(task_id)\n+\n+            create_call = mock_create_env.call_args\n+            assert create_call is not None, \"_create_environment was not called\"\n+            kwargs = create_call.kwargs if create_call.kwargs else {}\n+            cwd_passed = kwargs.get(\"cwd\", None)\n+            if cwd_passed is None:\n+                args = create_call.args if create_call.args else []\n+                if len(args) >= 3:\n+                    cwd_passed = args[2]\n+\n+            assert cwd_passed == \"/Users/user/project\", \\\n+                f\"Expected cwd='/Users/user/project', got {cwd_passed!r}\"\n+        finally:\n+            tt.clear_session_cwd(task_id)\n \n-        # Preset _last_known_cwd to simulate a previous env's CWD\n-        _last_known_cwd[task_id] = \"/Users/user/project\"\n-\n-        # Call _get_file_ops - should use _last_known_cwd for the new env\n-        result = _get_file_ops(task_id)\n-\n-        # Verify the env was created with the saved CWD, not the default\n-        create_call = mock_create_env.call_args\n-        assert create_call is not None, \"_create_environment was not called\"\n-        \n-        # Find cwd in the kwargs\n-        kwargs = create_call.kwargs if create_call.kwargs else {}\n-        # cwd is passed as positional or keyword\n-        cwd_passed = kwargs.get(\"cwd\", None)\n-        if cwd_passed is None:\n-            # Try positional args\n-            args = create_call.args if create_call.args else []\n-            # Position: (env_type, image, cwd, timeout, ...)\n-            if len(args) >= 3:\n-                cwd_passed = args[2]\n-        \n-        assert cwd_passed == \"/Users/user/project\", \\\n-            f\"Expected cwd='/Users/user/project', got {cwd_passed!r}\"\n-        \n-        # Cleanup\n-        _last_known_cwd.pop(task_id, None)\n-        \n     @patch(\"tools.terminal_tool._active_environments\", new_callable=dict)\n     @patch(\"tools.file_tools._file_ops_cache\", new_callable=dict)\n     @patch(\"tools.terminal_tool._get_env_config\")\n     @patch(\"tools.terminal_tool._create_environment\")\n-    def test_last_known_cwd_falls_back_to_config_default_when_not_set(\n+    def test_falls_back_to_config_default_when_no_record(\n         self, mock_create_env, mock_config, mock_cache, mock_active\n     ):\n-        from tools.file_tools import _get_file_ops, _last_known_cwd\n+        import tools.terminal_tool as tt\n+        from tools.file_tools import _get_file_ops\n \n         mock_env = MagicMock()\n         mock_env.cwd = \"/default/path\"","@@ -755,85 +751,44 @@ def test_last_known_cwd_falls_back_to_config_default_when_not_set(\n             \"timeout\": 30,\n         }\n \n-        # _get_file_ops resolves to \"default\"\n         task_id = \"default\"\n-        \n-        # Ensure _last_known_cwd is empty for this task\n-        _last_known_cwd.pop(task_id, None)\n+        tt.clear_session_cwd(task_id)\n+\n+        _get_file_ops(task_id)\n \n-        result = _get_file_ops(task_id)\n-        \n         create_call = mock_create_env.call_args\n         assert create_call is not None, \"_create_environment was not called\"\n-        \n         kwargs = create_call.kwargs if create_call.kwargs else {}\n         cwd_passed = kwargs.get(\"cwd\", None)\n         if cwd_passed is None:\n             args = create_call.args if create_call.args else []\n             if len(args) >= 3:\n                 cwd_passed = args[2]\n-        \n-        # Should fall back to config default\n+\n         assert cwd_passed == \"/config/default/path\", \\\n             f\"Expected cwd='/config/default/path', got {cwd_passed!r}\"\n \n-    @patch(\"tools.terminal_tool._active_environments\", new_callable=dict)\n-    @patch(\"tools.file_tools._file_ops_cache\", new_callable=dict)\n-    def test_live_cwd_read_mirrors_into_last_known_cwd(self, mock_cache, mock_active):\n-        \"\"\"Belt-and-suspenders (#26211): every successful live-cwd read records\n-        the cwd in _last_known_cwd, so the durable anchor doesn't depend on the\n-        cleanup-detection branch of _get_file_ops firing.\"\"\"\n-        from tools.file_tools import _get_live_tracking_cwd, _last_known_cwd\n-\n-        task_id = \"default\"\n-        _last_known_cwd.pop(task_id, None)\n-\n-        cached = MagicMock()\n-        cached.env = MagicMock()\n-        cached.env.cwd = \"/Users/user/project\"\n-        cached.env.cwd_owner = \"default\"\n-        mock_cache[task_id] = cached\n-\n-        live = _get_live_tracking_cwd(task_id)\n-\n-        assert live == \"/Users/user/project\"\n-        # The read mirrored the live cwd into the durable registry.\n-        assert _last_known_cwd.get(task_id) == \"/Users/user/project\"\n-        _last_known_cwd.pop(task_id, None)\n-\n     @patch(\"tools.terminal_tool._active_environments\", new_callable=dict)\n     @patch(\"tools.file_tools._file_ops_cache\", new_callable=dict)\n     @patch(\"tools.terminal_tool._get_env_config\")\n     @patch(\"tools.terminal_tool._create_environment\")\n-    def test_mirrored_cwd_survives_when_cache_already_cleared(\n+    def test_stale_cache_cwd_rescued_into_record_on_cleanup_detection(\n         self, mock_create_env, mock_config, mock_cache, mock_active\n     ):\n-        \"\"\"The original save-old-cwd path only fires when _file_ops_cache still\n-        holds the stale entry. If the cleanup thread popped BOTH dicts first,\n-        _get_file_ops sees cached=None and never saves — but the proactive\n-        mirror from an earlier live read already populated _last_known_cwd, so\n-        the rebuilt env still restores the user's directory.\"\"\"\n-        from tools.file_tools import (\n-            _get_file_ops, _get_live_tracking_cwd, _last_known_cwd,\n-        )\n+        \"\"\"If the env died but the file-ops cache entry survived, its cwd is\n+        rescued into the session record before the cache entry is dropped —\n+        the recreated env starts where the user left off.\"\"\"\n+        import tools.terminal_tool as tt\n+        from tools.file_tools import _get_file_ops\n \n         task_id = \"default\"\n-        _last_known_cwd.pop(task_id, None)\n+        tt.clear_session_cwd(task_id)\n \n-        # 1) Env is alive and the agent has cd'd into the project. A live read\n-        #    (happens on every relative-path resolution) mirrors the cwd.\n+        # Stale cache entry: env was cleaned up, cache still holds the old cwd.\n         cached = MagicMock()\n-        cached.env = MagicMock()\n-        cached.env.cwd = \"/Users/user/project\"\n-        cached.env.cwd_owner = \"default\"\n+        cached.env = None\n+        cached.cwd = \"/Users/user/project\"\n         mock_cache[task_id] = cached\n-        assert _get_live_tracking_cwd(task_id) == \"/Users/user/project\"\n-        assert _last_known_cwd.get(task_id) == \"/Users/user/project\"\n-\n-        # 2) Cleanup thread kills the env AND clears the cache before the next\n-        #    file write — so _get_file_ops' save-old-cwd branch never runs.\n-        mock_cache.pop(task_id, None)\n-        mock_active.clear()\n \n         mock_env = MagicMock()\n         mock_env.cwd = \"/Users/user/project\"","@@ -844,21 +799,23 @@ def test_mirrored_cwd_survives_when_cache_already_cleared(\n             \"timeout\": 30,\n         }\n \n-        _get_file_ops(task_id)\n+        try:\n+            _get_file_ops(task_id)\n \n-        create_call = mock_create_env.call_args\n-        assert create_call is not None, \"_create_environment was not called\"\n-        kwargs = create_call.kwargs if create_call.kwargs else {}\n-        cwd_passed = kwargs.get(\"cwd\", None)\n-        if cwd_passed is None:\n-            args = create_call.args if create_call.args else []\n-            if len(args) >= 3:\n-                cwd_passed = args[2]\n+            create_call = mock_create_env.call_args\n+            assert create_call is not None, \"_create_environment was not called\"\n+            kwargs = create_call.kwargs if create_call.kwargs else {}\n+            cwd_passed = kwargs.get(\"cwd\", None)\n+            if cwd_passed is None:\n+                args = create_call.args if create_call.args else []\n+                if len(args) >= 3:\n+                    cwd_passed = args[2]\n \n-        # Rebuilt env restored the mirrored cwd, NOT the config default.\n-        assert cwd_passed == \"/Users/user/project\", \\\n-            f\"Expected restored cwd='/Users/user/project', got {cwd_passed!r}\"\n-        _last_known_cwd.pop(task_id, None)\n+            # Rebuilt env restored the rescued cwd, NOT the config default.\n+            assert cwd_passed == \"/Users/user/project\", \\\n+                f\"Expected restored cwd='/Users/user/project', got {cwd_passed!r}\"\n+        finally:\n+            tt.clear_session_cwd(task_id)\n \n \n class TestSilentFileMisplacementE2E:","@@ -868,8 +825,8 @@ class TestSilentFileMisplacementE2E:\n     agent cd's into a project, the cleanup thread kills the env, and a later\n     relative-path write must land in the project dir (not the config default).\n     Mocks miss this because resolution (_resolve_path_for_task) runs BEFORE\n-    _get_file_ops rebuilds the env — only the durable _last_known_cwd fallback\n-    in _authoritative_workspace_root makes the resolved path correct.\n+    _get_file_ops rebuilds the env — only the durable session-cwd record\n+    makes the resolved path correct.\n     \"\"\"\n \n     def test_relative_write_after_env_cleanup_lands_in_user_cwd(self, tmp_path, monkeypatch):","@@ -889,13 +846,13 @@ def test_relative_write_after_env_cleanup_lands_in_user_cwd(self, tmp_path, monk\n         )\n \n         task_id = \"default\"\n-        ft._last_known_cwd.pop(task_id, None)\n+        tt.clear_session_cwd(task_id)\n \n-        # 1) Env alive; agent has cd'd into the project. A relative write\n-        #    while alive mirrors the live cwd into the durable registry.\n+        # 1) Env alive; agent has cd'd into the project (the completed command\n+        #    recorded the session cwd — simulate that write here).\n         fo = ft._get_file_ops(task_id)\n         fo.env.cwd = str(project)\n-        fo.env.cwd_owner = \"default\"\n+        tt.record_session_cwd(task_id, str(project))\n         ft.write_file_tool(\"alive.txt\", \"1\\n\", task_id)\n         assert (project / \"alive.txt\").exists()\n ","@@ -913,4 +870,4 @@ def test_relative_write_after_env_cleanup_lands_in_user_cwd(self, tmp_path, monk\n         assert not (config_default / \"report.txt\").exists(), \\\n             \"file silently misplaced into config default (the #26211 bug)\"\n \n-        ft._last_known_cwd.pop(task_id, None)\n+        tt.clear_session_cwd(task_id)"]},{"file":"tests/tools/test_file_tools_cwd_resolution.py","hunks":["@@ -36,8 +36,8 @@ def _isolated_cwd(tmp_path, monkeypatch):\n     # Process cwd = decoy, analogous to \"main repo\" while the terminal is in\n     # the worktree.\n     monkeypatch.chdir(decoy)\n-    # No live-terminal-cwd tracking recorded yet (fresh-session condition).\n-    monkeypatch.setattr(ft, \"_get_live_tracking_cwd\", lambda task_id=\"default\": None)\n+    # No session cwd recorded yet (fresh-session condition).\n+    monkeypatch.setattr(terminal_tool, \"_session_cwd\", {})\n     return workspace, decoy\n \n ","@@ -72,7 +72,7 @@ def test_live_tracking_cwd_wins_over_relative_terminal_cwd(_isolated_cwd, monkey\n     \"\"\"\n     workspace, decoy = _isolated_cwd\n     monkeypatch.setenv(\"TERMINAL_CWD\", \".\")\n-    monkeypatch.setattr(ft, \"_get_live_tracking_cwd\", lambda task_id=\"default\": str(workspace))\n+    terminal_tool.record_session_cwd(\"default\", str(workspace))\n \n     resolved = ft._resolve_path_for_task(\"target.py\", task_id=\"default\")\n ","@@ -137,7 +137,7 @@ def test_container_relative_path_keeps_container_cwd_symlink(tmp_path, monkeypat\n     container_mount.symlink_to(host_project, target_is_directory=True)\n     monkeypatch.setattr(terminal_tool, \"_get_env_config\", lambda: {\"env_type\": \"docker\"})\n     monkeypatch.setattr(terminal_tool, \"_active_environments\", {})\n-    monkeypatch.setattr(ft, \"_get_live_tracking_cwd\", lambda task_id=\"default\": str(container_mount))\n+    terminal_tool.record_session_cwd(\"default\", str(container_mount))\n \n     resolved = ft._resolve_path_for_task(\"oilsands-sim/README.md\", task_id=\"default\")\n ","@@ -185,9 +185,9 @@ def test_warning_fires_when_relative_path_escapes_workspace(_isolated_cwd, monke\n     \"\"\"Relative path resolving outside the live workspace must warn.\"\"\"\n     workspace, decoy = _isolated_cwd\n     # Live cwd = workspace, but the relative path resolves to decoy (process cwd)\n-    # because TERMINAL_CWD is the poison '.'.  Simulate by pointing live tracking\n-    # at workspace while the resolved path is under decoy.\n-    monkeypatch.setattr(ft, \"_get_live_tracking_cwd\", lambda task_id=\"default\": str(workspace))\n+    # because TERMINAL_CWD is the poison '.'.  Simulate by recording workspace\n+    # as the session cwd while the resolved path is under decoy.\n+    terminal_tool.record_session_cwd(\"default\", str(workspace))\n     resolved_in_decoy = decoy / \"target.py\"\n \n     warn = ft._path_resolution_warning(\"target.py\", resolved_in_decoy, task_id=\"default\")","@@ -200,7 +200,7 @@ def test_warning_fires_when_relative_path_escapes_workspace(_isolated_cwd, monke\n \n def test_no_warning_when_relative_path_inside_workspace(_isolated_cwd, monkeypatch):\n     workspace, decoy = _isolated_cwd\n-    monkeypatch.setattr(ft, \"_get_live_tracking_cwd\", lambda task_id=\"default\": str(workspace))\n+    terminal_tool.record_session_cwd(\"default\", str(workspace))\n     resolved_in_workspace = workspace / \"target.py\"\n \n     warn = ft._path_resolution_warning(\"target.py\", resolved_in_workspace, task_id=\"default\")","@@ -210,7 +210,7 @@ def test_no_warning_when_relative_path_inside_workspace(_isolated_cwd, monkeypat\n \n def test_no_warning_for_absolute_input(_isolated_cwd, monkeypatch):\n     workspace, decoy = _isolated_cwd\n-    monkeypatch.setattr(ft, \"_get_live_tracking_cwd\", lambda task_id=\"default\": str(workspace))\n+    terminal_tool.record_session_cwd(\"default\", str(workspace))\n \n     warn = ft._path_resolution_warning(str(decoy / \"target.py\"), decoy / \"target.py\", task_id=\"default\")\n ","@@ -219,7 +219,7 @@ def test_no_warning_for_absolute_input(_isolated_cwd, monkeypatch):\n \n def test_no_warning_when_no_live_cwd(_isolated_cwd, monkeypatch):\n     workspace, decoy = _isolated_cwd\n-    monkeypatch.setattr(ft, \"_get_live_tracking_cwd\", lambda task_id=\"default\": None)\n+    monkeypatch.setattr(terminal_tool, \"_session_cwd\", {})\n     monkeypatch.delenv(\"TERMINAL_CWD\", raising=False)\n \n     warn = ft._path_resolution_warning(\"target.py\", decoy / \"target.py\", task_id=\"default\")","@@ -244,7 +244,7 @@ def test_sentinel_terminal_cwd_is_treated_as_unset(_isolated_cwd, monkeypatch, s\n     never resolved as a literal relative directory.\n     \"\"\"\n     workspace, decoy = _isolated_cwd\n-    monkeypatch.setattr(ft, \"_get_live_tracking_cwd\", lambda task_id=\"default\": None)\n+    monkeypatch.setattr(terminal_tool, \"_session_cwd\", {})\n     monkeypatch.setenv(\"TERMINAL_CWD\", sentinel)\n \n     assert ft._configured_terminal_cwd() is None","@@ -261,7 +261,7 @@ def test_relative_nonsentinel_terminal_cwd_rejected(_isolated_cwd, monkeypatch):\n     be joined onto it as a literal subdir.\n     \"\"\"\n     workspace, decoy = _isolated_cwd\n-    monkeypatch.setattr(ft, \"_get_live_tracking_cwd\", lambda task_id=\"default\": None)\n+    monkeypatch.setattr(terminal_tool, \"_session_cwd\", {})\n     monkeypatch.setenv(\"TERMINAL_CWD\", \"some/rel/path\")\n \n     assert ft._configured_terminal_cwd() is None","@@ -277,7 +277,7 @@ def test_absolute_terminal_cwd_anchors_with_empty_registry(_isolated_cwd, monkey\n     worktree — not the process cwd (main repo).\n     \"\"\"\n     workspace, decoy = _isolated_cwd\n-    monkeypatch.setattr(ft, \"_get_live_tracking_cwd\", lambda task_id=\"default\": None)\n+    monkeypatch.setattr(terminal_tool, \"_session_cwd\", {})\n     monkeypatch.setenv(\"TERMINAL_CWD\", str(workspace))\n \n     resolved = ft._resolve_path_for_task(\"target.py\", task_id=\"default\")","@@ -295,7 +295,7 @@ def test_registered_task_cwd_override_anchors_before_terminal_env_exists(_isolat\n     \"\"\"\n     workspace, decoy = _isolated_cwd\n     task_id = \"desktop-session-cwd\"\n-    monkeypatch.setattr(ft, \"_get_live_tracking_cwd\", lambda task_id=\"default\": None)\n+    monkeypatch.setattr(terminal_tool, \"_session_cwd\", {})\n     monkeypatch.delenv(\"TERMINAL_CWD\", raising=False)\n     monkeypatch.setattr(terminal_tool, \"_task_env_overrides\", {})\n ","@@ -317,7 +317,7 @@ def test_warning_fires_from_terminal_cwd_when_registry_empty(_isolated_cwd, monk\n     worktree is flagged on the very first write.\n     \"\"\"\n     workspace, decoy = _isolated_cwd\n-    monkeypatch.setattr(ft, \"_get_live_tracking_cwd\", lambda task_id=\"default\": None)\n+    monkeypatch.setattr(terminal_tool, \"_session_cwd\", {})\n     monkeypatch.setenv(\"TERMINAL_CWD\", str(workspace))\n \n     # Relative path that escapes the worktree into the decoy/main checkout.","@@ -336,8 +336,8 @@ def test_live_cwd_still_wins_over_absolute_terminal_cwd(_isolated_cwd, monkeypat\n     workspace, decoy = _isolated_cwd\n     other = decoy.parent / \"other\"\n     other.mkdir()\n-    # Live cwd = workspace; TERMINAL_CWD points elsewhere — live must win.\n-    monkeypatch.setattr(ft, \"_get_live_tracking_cwd\", lambda task_id=\"default\": str(workspace))\n+    # Recorded session cwd = workspace; TERMINAL_CWD points elsewhere — record wins.\n+    terminal_tool.record_session_cwd(\"default\", str(workspace))\n     monkeypatch.setenv(\"TERMINAL_CWD\", str(other))\n \n     resolved = ft._resolve_path_for_task(\"target.py\", task_id=\"default\")","@@ -351,7 +351,7 @@ def test_live_cwd_still_wins_over_absolute_terminal_cwd(_isolated_cwd, monkeypat\n def test_write_file_reports_resolved_absolute_path(_isolated_cwd, monkeypatch):\n     \"\"\"write_file_tool must put the absolute on-disk path in files_modified.\"\"\"\n     workspace, decoy = _isolated_cwd\n-    monkeypatch.setattr(ft, \"_get_live_tracking_cwd\", lambda task_id=\"default\": str(workspace))\n+    terminal_tool.record_session_cwd(\"t1\", str(workspace))\n \n     import json\n     out = json.loads(ft.write_file_tool(\"newfile.txt\", \"hello\\n\", task_id=\"t1\"))","@@ -365,7 +365,7 @@ def test_write_file_reports_resolved_absolute_path(_isolated_cwd, monkeypatch):\n def test_patch_reports_resolved_absolute_path(_isolated_cwd, monkeypatch):\n     \"\"\"patch_tool (replace mode) must put the absolute on-disk path in files_modified.\"\"\"\n     workspace, decoy = _isolated_cwd\n-    monkeypatch.setattr(ft, \"_get_live_tracking_cwd\", lambda task_id=\"default\": str(workspace))\n+    terminal_tool.record_session_cwd(\"t1\", str(workspace))\n \n     import json\n     out = json.loads(ft.patch_tool(","@@ -383,24 +383,16 @@ def test_patch_reports_resolved_absolute_path(_isolated_cwd, monkeypatch):\n     assert (decoy / \"target.py\").read_text() == \"DECOY_ORIGINAL\\n\"\n \n \n-# ── Fix D: shared terminal env must not leak its cwd across worktree sessions ─\n-# (June 2026: two desktop sessions, each on its own worktree, share the single\n-# \"default\" terminal environment. Its `cwd` tracks whichever session ran the\n-# last command, so a file edit from the OTHER session resolved against that\n-# foreign cwd and silently landed in the wrong worktree. terminal_tool now\n-# stamps env.cwd_owner with the driving session; file tools trust the shared\n-# env's live cwd only when the resolving session owns it.)\n-\n-\n-class _FakeOwnedEnv:\n-    def __init__(self, cwd: str, cwd_owner: str):\n-        self.cwd = cwd\n-        self.cwd_owner = cwd_owner\n+# ── Cross-session isolation: one session's cwd never leaks into another ──────\n+# (June 2026 bug class: two desktop sessions, each on its own worktree, shared\n+# the single \"default\" terminal environment and could inherit each other's cwd.\n+# The per-session record store solves this structurally: each session's cd\n+# state lives in its own record, keyed by the raw session id.)\n \n \n @pytest.fixture\n def _two_worktree_sessions(tmp_path, monkeypatch):\n-    \"\"\"Two worktree sessions sharing one terminal env owned by session B.\"\"\"\n+    \"\"\"Two worktree sessions: B has cd'd (record), both registered overrides.\"\"\"\n     wt_a = tmp_path / \"wt_a\"\n     wt_b = tmp_path / \"wt_b\"\n     main = tmp_path / \"main\"","@@ -410,78 +402,60 @@ def _two_worktree_sessions(tmp_path, monkeypatch):\n     monkeypatch.chdir(main)\n     monkeypatch.delenv(\"TERMINAL_CWD\", raising=False)\n     monkeypatch.setattr(terminal_tool, \"_task_env_overrides\", {})\n+    monkeypatch.setattr(terminal_tool, \"_session_cwd\", {})\n     monkeypatch.setattr(ft, \"_file_ops_cache\", {})\n-    # Both sessions register their worktree cwd (TUI/desktop registration path).\n+    # Both sessions register their worktree cwd (TUI/desktop registration path;\n+    # registration seeds each session's record).\n     terminal_tool.register_task_env_overrides(\"sess-a\", {\"cwd\": str(wt_a)})\n     terminal_tool.register_task_env_overrides(\"sess-b\", {\"cwd\": str(wt_b)})\n-    # The shared \"default\" env: session B ran the last command, so its live cwd\n-    # is wt_b and B owns it.\n+    # Session B ran the last command; the shared env's live cwd is wt_b but\n+    # only B's RECORD carries it.\n     monkeypatch.setattr(\n         terminal_tool,\n         \"_active_environments\",\n-        {\"default\": _FakeOwnedEnv(str(wt_b), \"sess-b\")},\n+        {\"default\": _FakeEnv(str(wt_b))},\n     )\n     return wt_a, wt_b, main\n \n \n-def test_live_cwd_ignored_for_non_owning_session(_two_worktree_sessions):\n-    wt_a, wt_b, _main = _two_worktree_sessions\n-    # Owner sees the live cwd; the other session must NOT inherit it.\n-    assert ft._get_live_tracking_cwd(\"sess-b\") == str(wt_b)\n-    assert ft._get_live_tracking_cwd(\"sess-a\") is None\n+class _FakeEnv:\n+    def __init__(self, cwd: str):\n+        self.cwd = cwd\n \n \n def test_resolution_routes_to_resolving_sessions_worktree(_two_worktree_sessions):\n     \"\"\"The wrong-worktree fix: A resolves into wt_a, not the shared env's wt_b.\"\"\"\n     wt_a, wt_b, _main = _two_worktree_sessions\n-    # Session A does not own the shared env → falls back to its own registered\n-    # worktree cwd instead of B's live cwd.\n     resolved_a = ft._resolve_path_for_task(\"target.py\", task_id=\"sess-a\")\n     assert resolved_a == (wt_a / \"target.py\")\n     assert not str(resolved_a).startswith(str(wt_b))\n \n \n-def test_owning_session_still_resolves_against_live_cwd(_two_worktree_sessions):\n-    \"\"\"No regression: the owner keeps resolving against the live cwd.\"\"\"\n+def test_session_with_cd_record_resolves_against_it(_two_worktree_sessions):\n+    \"\"\"B's record (its own cd state) is authoritative for B.\"\"\"\n     wt_a, wt_b, _main = _two_worktree_sessions\n     resolved_b = ft._resolve_path_for_task(\"target.py\", task_id=\"sess-b\")\n     assert resolved_b == (wt_b / \"target.py\")\n     assert not str(resolved_b).startswith(str(wt_a))\n \n \n-def test_unknown_owner_keeps_prior_single_session_behavior(tmp_path, monkeypatch):\n-    \"\"\"An env with no owner (CLI / legacy) still yields its live cwd.\"\"\"\n-    ws = tmp_path / \"ws\"\n-    ws.mkdir()\n-    monkeypatch.setattr(ft, \"_file_ops_cache\", {})\n-    monkeypatch.setattr(\n-        terminal_tool,\n-        \"_active_environments\",\n-        {\"default\": _FakeOwnedEnv(str(ws), \"\")},\n-    )\n-    assert ft._get_live_tracking_cwd(\"default\") == str(ws)\n-    assert ft._get_live_tracking_cwd(\"any-session\") == str(ws)\n+def test_sessions_cd_updates_only_its_own_resolution(_two_worktree_sessions, tmp_path):\n+    \"\"\"B cd's elsewhere → B's resolution follows, A's is untouched.\"\"\"\n+    wt_a, wt_b, _main = _two_worktree_sessions\n+    elsewhere = tmp_path / \"elsewhere\"\n+    elsewhere.mkdir()\n+    terminal_tool.record_session_cwd(\"sess-b\", str(elsewhere))\n+\n+    assert ft._resolve_path_for_task(\"f.py\", task_id=\"sess-b\") == (elsewhere / \"f.py\")\n+    assert ft._resolve_path_for_task(\"f.py\", task_id=\"sess-a\") == (wt_a / \"f.py\")\n \n \n-def test_preserved_cwd_does_not_override_non_owning_sessions_worktree(\n+def test_unregistered_session_never_inherits_another_sessions_record(\n     _two_worktree_sessions, monkeypatch\n ):\n-    \"\"\"#26211 belt-and-suspenders must not break worktree isolation.\n-\n-    The owner (session B) doing an owned live read mirrors wt_b into the shared\n-    _last_known_cwd['default'] registry. Session A — which does NOT own the env\n-    but HAS its own registered worktree (wt_a) — must still resolve into wt_a,\n-    not inherit B's preserved cwd through the shared-container key. The\n-    session-specific registered override must beat the durable shared anchor.\n-    \"\"\"\n-    wt_a, wt_b, _main = _two_worktree_sessions\n-    monkeypatch.setattr(ft, \"_last_known_cwd\", {})\n-\n-    # Owner B resolves first — this mirrors wt_b into _last_known_cwd['default'].\n-    assert ft._resolve_path_for_task(\"target.py\", task_id=\"sess-b\") == (wt_b / \"target.py\")\n-    assert ft._last_known_cwd.get(\"default\") == str(wt_b)\n-\n-    # A still routes to its own registered worktree despite the shared anchor.\n-    resolved_a = ft._resolve_path_for_task(\"target.py\", task_id=\"sess-a\")\n-    assert resolved_a == (wt_a / \"target.py\")\n-    assert not str(resolved_a).startswith(str(wt_b))\n+    \"\"\"Session C: no record, no override. Must NOT inherit A's or B's cwd.\"\"\"\n+    wt_a, wt_b, main = _two_worktree_sessions\n+    resolved = ft._resolve_path_for_task(\"target.py\", task_id=\"sess-c\")\n+    assert not str(resolved).startswith(str(wt_a))\n+    assert not str(resolved).startswith(str(wt_b))\n+    assert resolved == (main / \"target.py\").resolve()"]},{"file":"tests/tools/test_file_tools_tilde_profile.py","hunks":["@@ -21,6 +21,7 @@\n import pytest\n \n import tools.file_tools as ft\n+import tools.terminal_tool as terminal_tool\n \n \n # ---------------------------------------------------------------------------","@@ -82,7 +83,7 @@ def test_relative_tilde_resolves_to_profile_home(self, tmp_path, monkeypatch):\n         process_home.mkdir()\n \n         monkeypatch.setenv(\"HOME\", str(process_home))\n-        monkeypatch.setattr(ft, \"_get_live_tracking_cwd\", lambda task_id=\"default\": None)\n+        monkeypatch.setattr(terminal_tool, \"_session_cwd\", {})\n \n         with patch(\"hermes_constants.get_subprocess_home\", return_value=str(profile_home)):\n             resolved = ft._resolve_path_for_task(\"~/test_file.txt\", task_id=\"test\")","@@ -98,7 +99,7 @@ def test_absolute_tilde_in_workspace_root(self, tmp_path, monkeypatch):\n         process_home.mkdir()\n \n         monkeypatch.setenv(\"HOME\", str(process_home))\n-        monkeypatch.setattr(ft, \"_get_live_tracking_cwd\", lambda task_id=\"default\": None)\n+        monkeypatch.setattr(terminal_tool, \"_session_cwd\", {})\n \n         with patch(\"hermes_constants.get_subprocess_home\", return_value=str(profile_home)):\n             # _resolve_base_dir uses the workspace root from config; if it contains ~,"]},{"file":"tests/tools/test_resolve_path.py","hunks":["@@ -52,33 +52,23 @@ def test_result_is_resolved(self, monkeypatch, tmp_path):\n         assert \"..\" not in str(result)\n         assert result == (tmp_path / \"b\" / \"file.txt\")\n \n-    def test_relative_path_prefers_live_file_ops_cwd(self, monkeypatch, tmp_path):\n-        \"\"\"Live env.cwd must win after the terminal session changes directory.\"\"\"\n+    def test_relative_path_prefers_recorded_session_cwd(self, monkeypatch, tmp_path):\n+        \"\"\"The session's recorded cwd must win after the terminal changes directory.\"\"\"\n         start_dir = tmp_path / \"start\"\n         live_dir = tmp_path / \"worktree\"\n         start_dir.mkdir()\n         live_dir.mkdir()\n         monkeypatch.setenv(\"TERMINAL_CWD\", str(start_dir))\n \n-        from tools import file_tools\n+        from tools import file_tools, terminal_tool\n \n         task_id = \"live-cwd\"\n-        fake_ops = SimpleNamespace(\n-            env=SimpleNamespace(cwd=str(live_dir)),\n-            cwd=str(start_dir),\n-        )\n-\n-        with file_tools._file_ops_lock:\n-            previous = file_tools._file_ops_cache.get(task_id)\n-            file_tools._file_ops_cache[task_id] = fake_ops\n+        # The session's completed `cd` recorded the new directory.\n+        terminal_tool.record_session_cwd(task_id, str(live_dir))\n \n         try:\n             result = file_tools._resolve_path(\"nested/file.txt\", task_id=task_id)\n         finally:\n-            with file_tools._file_ops_lock:\n-                if previous is None:\n-                    file_tools._file_ops_cache.pop(task_id, None)\n-                else:\n-                    file_tools._file_ops_cache[task_id] = previous\n+            terminal_tool.clear_session_cwd(task_id)\n \n         assert result == live_dir / \"nested\" / \"file.txt\""]},{"file":"tests/tools/test_session_cwd_store.py","hunks":["@@ -0,0 +1,244 @@\n+\"\"\"Session-cwd record store (cwd rearchitecture, step 1: dual-write).\n+\n+The store is the future single source of truth for per-session working\n+directories. Step 1 only guarantees the WRITE side: every path that learns a\n+session's live cwd must record it under the raw session key. Readers still\n+use the legacy env.cwd ladder; these tests pin the invariants the later\n+read-side flip will rely on.\n+\"\"\"\n+\n+import pytest\n+\n+import tools.terminal_tool as tt\n+\n+\n[redacted:email](autouse=True)\n+def _clean_store(monkeypatch):\n+    monkeypatch.setattr(tt, \"_session_cwd\", {})\n+    monkeypatch.setattr(tt, \"_task_env_overrides\", {})\n+\n+\n+class TestRecordSemantics:\n+    def test_records_are_keyed_by_raw_session_key(self):\n+        tt.record_session_cwd(\"sess-a\", \"/wt/a\")\n+        tt.record_session_cwd(\"sess-b\", \"/wt/b\")\n+        # No cross-talk: each session reads back exactly its own record.\n+        assert tt.get_session_cwd(\"sess-a\") == \"/wt/a\"\n+        assert tt.get_session_cwd(\"sess-b\") == \"/wt/b\"\n+        assert tt.get_session_cwd(\"sess-c\") is None\n+\n+    def test_none_and_empty_keys_collapse_to_default(self):\n+        tt.record_session_cwd(None, \"/somewhere\")\n+        assert tt.get_session_cwd(None) == \"/somewhere\"\n+        assert tt.get_session_cwd(\"\") == \"/somewhere\"\n+        assert tt.get_session_cwd(\"default\") == \"/somewhere\"\n+\n+    def test_invalid_cwd_values_are_ignored(self):\n+        tt.record_session_cwd(\"sess-a\", None)\n+        tt.record_session_cwd(\"sess-a\", \"\")\n+        tt.record_session_cwd(\"sess-a\", \"   \")\n+        tt.record_session_cwd(\"sess-a\", 123)  # type: ignore[arg-type]\n+        assert tt.get_session_cwd(\"sess-a\") is None\n+\n+    def test_clear_drops_only_the_named_session(self):\n+        tt.record_session_cwd(\"sess-a\", \"/wt/a\")\n+        tt.record_session_cwd(\"sess-b\", \"/wt/b\")\n+        tt.clear_session_cwd(\"sess-a\")\n+        assert tt.get_session_cwd(\"sess-a\") is None\n+        assert tt.get_session_cwd(\"sess-b\") == \"/wt/b\"\n+\n+\n+class TestDualWriteSites:\n+    def test_register_cwd_override_seeds_the_session_record(self):\n+        \"\"\"A registered workspace cwd IS the session's cwd until a `cd`.\"\"\"\n+        tt.register_task_env_overrides(\"desktop-sess\", {\"cwd\": \"/wt/desktop\"})\n+        assert tt.get_session_cwd(\"desktop-sess\") == \"/wt/desktop\"\n+\n+    def test_register_without_cwd_does_not_touch_the_record(self):\n+        tt.register_task_env_overrides(\"rl-42\", {\"docker_image\": \"x:y\"})\n+        assert tt.get_session_cwd(\"rl-42\") is None\n+\n+    def test_clear_task_env_overrides_drops_the_record(self):\n+        tt.register_task_env_overrides(\"desktop-sess\", {\"cwd\": \"/wt/desktop\"})\n+        tt.clear_task_env_overrides(\"desktop-sess\")\n+        assert tt.get_session_cwd(\"desktop-sess\") is None\n+\n+    def test_reregistration_updates_the_record(self):\n+        \"\"\"ACP session/load switching project roots mid-session.\"\"\"\n+        tt.register_task_env_overrides(\"acp-sess\", {\"cwd\": \"/proj/one\"})\n+        tt.register_task_env_overrides(\"acp-sess\", {\"cwd\": \"/proj/two\"})\n+        assert tt.get_session_cwd(\"acp-sess\") == \"/proj/two\"\n+\n+\n+class TestPostCommandDualWrite:\n+    \"\"\"The env's post-command cwd tracking must mirror into the session record.\"\"\"\n+\n+    def _run(self, monkeypatch, task_id, env):\n+        import json\n+        monkeypatch.setattr(tt, \"_active_environments\", {task_id: env})\n+        monkeypatch.setattr(tt, \"_last_activity\", {})\n+        monkeypatch.setattr(\n+            tt, \"_get_env_config\",\n+            lambda: {\"env_type\": \"local\", \"cwd\": \"/default\", \"timeout\": 60,\n+                     \"lifetime_seconds\": 3600},\n+        )\n+        monkeypatch.setattr(\n+            tt, \"_check_all_guards\",\n+            lambda command, env_type, **kwargs: {\"approved\": True},\n+        )\n+        return json.loads(tt.terminal_tool(command=\"cd /new/dir\", task_id=task_id))\n+\n+    def test_cd_result_is_recorded_under_the_session_key(self, monkeypatch):\n+        class FakeEnv:\n+            env = {}\n+            cwd = \"/start\"\n+            def execute(self, command, **kwargs):\n+                # Simulate the env's own post-command tracking (marker parse).\n+                self.cwd = \"/new/dir\"\n+                return {\"output\": \"\", \"returncode\": 0}\n+\n+        result = self._run(monkeypatch, \"sess-a\", FakeEnv())\n+        assert result[\"exit_code\"] == 0\n+        assert tt.get_session_cwd(\"sess-a\") == \"/new/dir\"\n+        # And ONLY that session's record was touched.\n+        assert tt.get_session_cwd(\"sess-b\") is None\n+\n+    def test_envs_without_cwd_tracking_record_nothing(self, monkeypatch):\n+        class FakeEnv:\n+            env = {}\n+            def execute(self, command, **kwargs):\n+                return {\"output\": \"\", \"returncode\": 0}\n+\n+        result = self._run(monkeypatch, \"sess-a\", FakeEnv())\n+        assert result[\"exit_code\"] == 0\n+        assert tt.get_session_cwd(\"sess-a\") is None\n+\n+\n+class TestFileToolsReadTheRecord:\n+    \"\"\"Step 2: file-tool path resolution prefers the session's own record.\"\"\"\n+\n+    def test_two_sessions_resolve_into_their_own_recorded_cwds(self, tmp_path, monkeypatch):\n+        import tools.file_tools as ft\n+\n+        wt_a = tmp_path / \"wt_a\"\n+        wt_b = tmp_path / \"wt_b\"\n+        for d in (wt_a, wt_b):\n+            d.mkdir()\n+        monkeypatch.chdir(tmp_path)\n+        monkeypatch.delenv(\"TERMINAL_CWD\", raising=False)\n+        monkeypatch.setattr(ft, \"_file_ops_cache\", {})\n+        monkeypatch.setattr(tt, \"_active_environments\", {})\n+\n+        # Each session ran commands that recorded its own cwd. No env alive,\n+        # no registered overrides — just the records.\n+        tt.record_session_cwd(\"sess-a\", str(wt_a))\n+        tt.record_session_cwd(\"sess-b\", str(wt_b))\n+\n+        assert ft._resolve_path_for_task(\"f.py\", task_id=\"sess-a\") == (wt_a / \"f.py\")\n+        assert ft._resolve_path_for_task(\"f.py\", task_id=\"sess-b\") == (wt_b / \"f.py\")\n+\n+    def test_record_beats_foreign_env_cwd_without_ownership_metadata(self, tmp_path, monkeypatch):\n+        \"\"\"The leak-A scenario, solved structurally: the shared env's cwd is\n+        never consulted for path resolution — only the session's own record.\"\"\"\n+        import tools.file_tools as ft\n+\n+        wt_a = tmp_path / \"wt_a\"\n+        wt_b = tmp_path / \"wt_b\"\n+        for d in (wt_a, wt_b):\n+            d.mkdir()\n+        monkeypatch.chdir(tmp_path)\n+        monkeypatch.delenv(\"TERMINAL_CWD\", raising=False)\n+        monkeypatch.setattr(ft, \"_file_ops_cache\", {})\n+\n+        class _Env:\n+            cwd = str(wt_b)  # another session's leftover cd on the shared env\n+\n+        monkeypatch.setattr(tt, \"_active_environments\", {\"default\": _Env()})\n+        tt.record_session_cwd(\"sess-a\", str(wt_a))\n+\n+        resolved = ft._resolve_path_for_task(\"f.py\", task_id=\"sess-a\")\n+        assert resolved == (wt_a / \"f.py\")\n+        assert not str(resolved).startswith(str(wt_b))\n+\n+\n+class TestDelegateSeedsChildRecord:\n+    def test_child_record_seeded_from_parent_then_isolated(self):\n+        tt.record_session_cwd(\"parent-task\", \"/parent/worktree\")\n+        # what delegate_tool does at spawn:\n+        tt.record_session_cwd(\"child-1\", tt.get_session_cwd(\"parent-task\"))\n+\n+        assert tt.get_session_cwd(\"child-1\") == \"/parent/worktree\"\n+        # child cds somewhere; parent record must be untouched.\n+        tt.record_session_cwd(\"child-1\", \"/child/scratch\")\n+        assert tt.get_session_cwd(\"parent-task\") == \"/parent/worktree\"\n+        assert tt.get_session_cwd(\"child-1\") == \"/child/scratch\"\n+\n+\n+class TestCommandCwdReadsTheRecord:\n+    \"\"\"_resolve_command_cwd: workdir > session record > default. Nothing else.\"\"\"\n+\n+    def test_record_beats_default(self):\n+        tt.record_session_cwd(\"sess-a\", \"/my/worktree\")\n+        resolved = tt._resolve_command_cwd(\n+            workdir=None,\n+            default_cwd=\"/config/default\",\n+            session_key=\"sess-a\",\n+        )\n+        assert resolved == \"/my/worktree\"\n+\n+    def test_workdir_still_beats_the_record(self):\n+        tt.record_session_cwd(\"sess-a\", \"/my/worktree\")\n+        resolved = tt._resolve_command_cwd(\n+            workdir=\"/explicit/place\",\n+            default_cwd=\"/config/default\",\n+            session_key=\"sess-a\",\n+        )\n+        assert resolved == \"/explicit/place\"\n+\n+    def test_no_record_falls_back_to_default(self):\n+        resolved = tt._resolve_command_cwd(\n+            workdir=None,\n+            default_cwd=\"/config/default\",\n+            session_key=\"sess-a\",\n+        )\n+        assert resolved == \"/config/default\"\n+\n+    def test_other_sessions_record_is_not_consulted(self):\n+        tt.record_session_cwd(\"sess-b\", \"/other/worktree\")\n+        resolved = tt._resolve_command_cwd(\n+            workdir=None,\n+            default_cwd=\"/config/default\",\n+            session_key=\"sess-a\",\n+        )\n+        assert resolved == \"/config/default\"\n+\n+    def test_cd_then_next_command_runs_in_the_new_dir(self, monkeypatch):\n+        \"\"\"E2E through terminal_tool: the record round-trips cd state.\"\"\"\n+        import json\n+\n+        class FakeEnv:\n+            env = {}\n+            cwd = \"/start\"\n+            def execute(self, command, **kwargs):\n+                self.last_cwd_arg = kwargs.get(\"cwd\")\n+                if command.startswith(\"cd \"):\n+                    self.cwd = command[3:]\n+                return {\"output\": \"\", \"returncode\": 0}\n+\n+        fake = FakeEnv()\n+        monkeypatch.setattr(tt, \"_active_environments\", {\"sess-a\": fake})\n+        monkeypatch.setattr(tt, \"_last_activity\", {})\n+        monkeypatch.setattr(\n+            tt, \"_get_env_config\",\n+            lambda: {\"env_type\": \"local\", \"cwd\": \"/default\", \"timeout\": 60,\n+                     \"lifetime_seconds\": 3600},\n+        )\n+        monkeypatch.setattr(\n+            tt, \"_check_all_guards\",\n+            lambda command, env_type, **kwargs: {\"approved\": True},\n+        )\n+\n+        json.loads(tt.terminal_tool(command=\"cd /project\", task_id=\"sess-a\"))\n+        assert tt.get_session_cwd(\"sess-a\") == \"/project\"\n+        json.loads(tt.terminal_tool(command=\"pwd\", task_id=\"sess-a\"))\n+        assert fake.last_cwd_arg == \"/project\""]},{"file":"tests/tools/test_terminal_task_cwd.py","hunks":["@@ -76,8 +76,8 @@ def execute(self, command, **kwargs):\n     assert calls == [{\"timeout\": 60, \"cwd\": \"/explicit/workdir\", \"bounded_capture\": True}]\n \n \n-def test_foreground_command_prefers_live_env_cwd_over_init_time_cwd(monkeypatch):\n-    \"\"\"A prior `cd` updates env.cwd; terminal_tool must honor that live cwd.\"\"\"\n+def test_foreground_command_prefers_recorded_session_cwd_over_init_time_cwd(monkeypatch):\n+    \"\"\"A prior `cd` records the session cwd; terminal_tool must honor it.\"\"\"\n     calls = []\n \n     class FakeEnv:","@@ -91,6 +91,7 @@ def execute(self, command, **kwargs):\n     task_id = \"session-live-cwd\"\n     monkeypatch.setattr(terminal_tool, \"_active_environments\", {task_id: FakeEnv()})\n     monkeypatch.setattr(terminal_tool, \"_last_activity\", {})\n+    monkeypatch.setattr(terminal_tool, \"_session_cwd\", {})\n     monkeypatch.setattr(terminal_tool, \"_task_env_overrides\", {task_id: {\"cwd\": \"/workspace/init\"}})\n     monkeypatch.setattr(terminal_tool, \"_get_env_config\", lambda: _minimal_terminal_config(cwd=\"/workspace/init\"))\n     monkeypatch.setattr(terminal_tool, \"_start_cleanup_thread\", lambda: None)","@@ -100,6 +101,8 @@ def execute(self, command, **kwargs):\n         \"_check_all_guards\",\n         lambda command, env_type, **kwargs: {\"approved\": True},\n     )\n+    # The prior command's completed `cd` recorded the session cwd.\n+    terminal_tool.record_session_cwd(task_id, \"/workspace/live\")\n \n     result = json.loads(terminal_tool.terminal_tool(command=\"pwd\", task_id=task_id))\n ","@@ -107,8 +110,8 @@ def execute(self, command, **kwargs):\n     assert calls == [(\"pwd\", {\"timeout\": 60, \"cwd\": \"/workspace/live\", \"bounded_capture\": True})]\n \n \n-def test_background_command_prefers_live_env_cwd_over_init_time_cwd(monkeypatch):\n-    \"\"\"Background process launches must also use the live session cwd.\"\"\"\n+def test_background_command_prefers_recorded_session_cwd_over_init_time_cwd(monkeypatch):\n+    \"\"\"Background process launches must also use the recorded session cwd.\"\"\"\n \n     class FakeEnv:\n         env = {}","@@ -129,6 +132,7 @@ def spawn_local(self, **kwargs):\n     task_id = \"session-live-cwd-bg\"\n     monkeypatch.setattr(terminal_tool, \"_active_environments\", {task_id: FakeEnv()})\n     monkeypatch.setattr(terminal_tool, \"_last_activity\", {})\n+    monkeypatch.setattr(terminal_tool, \"_session_cwd\", {})\n     monkeypatch.setattr(terminal_tool, \"_task_env_overrides\", {task_id: {\"cwd\": \"/workspace/init\"}})\n     monkeypatch.setattr(terminal_tool, \"_get_env_config\", lambda: _minimal_terminal_config(cwd=\"/workspace/init\"))\n     monkeypatch.setattr(terminal_tool, \"_start_cleanup_thread\", lambda: None)","@@ -139,6 +143,7 @@ def spawn_local(self, **kwargs):\n         lambda command, env_type, **kwargs: {\"approved\": True},\n     )\n     monkeypatch.setattr(process_registry_mod, \"process_registry\", registry)\n+    terminal_tool.record_session_cwd(task_id, \"/workspace/live\")\n \n     result = json.loads(\n         terminal_tool.terminal_tool(","@@ -162,15 +167,13 @@ def spawn_local(self, **kwargs):\n     }]\n \n \n-def test_registering_cwd_override_updates_live_env_cwd(monkeypatch):\n+def test_registering_cwd_override_updates_session_record(monkeypatch):\n     \"\"\"An ACP ``update_cwd`` (re-)registered mid-session must win over a\n-    previously ``cd``-ed live ``env.cwd``.\n+    previously ``cd``-ed session cwd.\n \n-    Preferring live ``env.cwd`` (so session-local ``cd`` survives) means a\n-    freshly registered ``cwd`` override would otherwise sit *below* the\n-    already-set ``env.cwd`` and be silently ignored. ``register_task_env_overrides``\n-    syncs the new cwd onto the live cached env so an explicit ACP project-root\n-    change takes effect, as the editor client expects.\n+    Registration writes the session record directly, so an explicit ACP\n+    project-root change takes effect on the next command, as the editor\n+    client expects.\n     \"\"\"\n \n     class FakeEnv:","@@ -181,15 +184,18 @@ class FakeEnv:\n     fake_env = FakeEnv()\n     monkeypatch.setattr(terminal_tool, \"_active_environments\", {task_id: fake_env})\n     monkeypatch.setattr(terminal_tool, \"_task_env_overrides\", {})\n+    monkeypatch.setattr(terminal_tool, \"_session_cwd\", {})\n+    # The session had cd'd somewhere before the editor switched project roots.\n+    terminal_tool.record_session_cwd(task_id, \"/workspace/old\")\n \n     terminal_tool.register_task_env_overrides(task_id, {\"cwd\": \"/workspace/new\"})\n \n-    # The live env now reflects the editor's new project root.\n+    # The live env mirror still updates (legacy env seeding) …\n     assert fake_env.cwd == \"/workspace/new\"\n-\n-    # A subsequent command resolves to the new cwd (env.cwd precedence).\n+    # … and the session record — what commands actually resolve against — too.\n+    assert terminal_tool.get_session_cwd(task_id) == \"/workspace/new\"\n     assert terminal_tool._resolve_command_cwd(\n-        workdir=None, env=fake_env, default_cwd=\"/workspace/config\"\n+        workdir=None, default_cwd=\"/workspace/config\", session_key=task_id\n     ) == \"/workspace/new\"\n \n ","@@ -264,20 +270,14 @@ def execute(self, command, **kwargs):\n     assert calls == [(\"pwd\", {\"timeout\": 60, \"cwd\": \"/home/user/src/hermes-agent\", \"bounded_capture\": True})]\n \n \n-def test_same_session_env_cwd_is_trusted_after_first_claim(monkeypatch):\n-    \"\"\"Once a session has claimed the env, subsequent commands trust env.cwd.\n-\n-    The prev_owner check only rejects env.cwd when a DIFFERENT session owned it\n-    before this call.  After the first command (which claims ownership),\n-    subsequent calls in the same session should trust the live env.cwd so that\n-    in-session `cd` state survives.\n-    \"\"\"\n+def test_same_session_recorded_cwd_survives_across_commands(monkeypatch):\n+    \"\"\"In-session `cd` state survives: the record written by one command is\n+    used by the next command in the same session.\"\"\"\n     calls = []\n \n     class FakeEnv:\n         env = {}\n         cwd = \"/workspace/deep\"\n-        cwd_owner = \"session-X\"\n \n         def execute(self, command, **kwargs):\n             calls.append((command, kwargs))","@@ -288,6 +288,7 @@ def execute(self, command, **kwargs):\n     monkeypatch.setattr(terminal_tool, \"_active_environments\", {\"default\": env})\n     monkeypatch.setattr(terminal_tool, \"_last_activity\", {})\n     monkeypatch.setattr(terminal_tool, \"_task_env_overrides\", {})\n+    monkeypatch.setattr(terminal_tool, \"_session_cwd\", {})\n     monkeypatch.setattr(terminal_tool, \"_get_env_config\", lambda: _minimal_terminal_config(cwd=\"/workspace/config\"))\n     monkeypatch.setattr(terminal_tool, \"_start_cleanup_thread\", lambda: None)\n     monkeypatch.setattr(terminal_tool, \"_resolve_container_task_id\", lambda value: \"default\")","@@ -297,13 +298,17 @@ def execute(self, command, **kwargs):\n         lambda command, env_type, **kwargs: {\"approved\": True},\n     )\n \n-    # First call: env was owned by \"session-X\" (same session_key since\n-    # get_current_session_key falls back to task_id). prev_owner == current\n-    # session, so env.cwd is trusted.\n+    # First command runs in the config cwd (no record yet) and afterwards\n+    # mirrors the env's post-command cwd into the session record.\n     result = json.loads(terminal_tool.terminal_tool(command=\"pwd\", task_id=task_id))\n+    assert result[\"exit_code\"] == 0\n+    assert calls[0] == (\"pwd\", {\"timeout\": 60, \"cwd\": \"/workspace/config\", \"bounded_capture\": True})\n+    assert terminal_tool.get_session_cwd(task_id) == \"/workspace/deep\"\n \n+    # Second command in the same session trusts the record.\n+    result = json.loads(terminal_tool.terminal_tool(command=\"pwd\", task_id=task_id))\n     assert result[\"exit_code\"] == 0\n-    assert calls == [(\"pwd\", {\"timeout\": 60, \"cwd\": \"/workspace/deep\", \"bounded_capture\": True})]\n+    assert calls[1] == (\"pwd\", {\"timeout\": 60, \"cwd\": \"/workspace/deep\", \"bounded_capture\": True})\n \n \n def test_safe_getcwd_returns_real_cwd(monkeypatch):"]},{"file":"tools/delegate_tool.py","hunks":["@@ -1906,6 +1906,18 @@ def _heartbeat_loop():\n \n         child_task_id = _subagent_id or f\"subagent-{task_index}-{_uuid.uuid4().hex[:8]}\"\n         parent_task_id = getattr(parent_agent, \"_current_task_id\", None)\n+        # Seed the child's session-cwd record from the parent's (cwd rearch):\n+        # children share the parent's container, and today they inherit the\n+        # parent's live env.cwd implicitly. Seeding at spawn preserves that\n+        # starting directory while keeping the child's subsequent `cd`s\n+        # isolated in its own record (a child's cd no longer bleeds back into\n+        # the parent once readers flip to the record store).\n+        try:\n+            from tools.terminal_tool import get_session_cwd, record_session_cwd\n+\n+            record_session_cwd(child_task_id, get_session_cwd(parent_task_id))\n+        except Exception as e:\n+            logger.debug(\"Child cwd seed failed: %s\", e)\n         wall_start = time.time()\n         parent_reads_snapshot = (\n             list(file_state.known_reads(parent_task_id)) if parent_task_id else []"]},{"file":"tools/file_tools.py","hunks":["@@ -269,103 +269,37 @@ def _registered_task_cwd_override(task_id: str = \"default\") -> str | None:\n     return _sentinel_free_abs_cwd(overrides.get(\"cwd\"))\n \n \n-def _live_cwd_if_owned(env, task_id: str) -> str | None:\n-    \"\"\"The env's live cwd, but only when THIS session owns it.\n-\n-    The terminal env is shared (collapsed to the ``\"default\"`` container), so its\n-    ``cwd`` tracks the LAST session that ran a command. With two worktree\n-    sessions open, trusting it blindly routes one session's edits into the other\n-    session's checkout (the wrong-worktree-patch bug). ``terminal_tool`` stamps\n-    ``env.cwd_owner`` with the session that last drove the env; return its cwd\n-    only when that owner matches the resolving session, else ``None`` so the\n-    caller falls through to this session's own registered cwd override. Unknown\n-    owner / ``default`` keys keep the prior behavior (single-session / CLI).\n-    \"\"\"\n-    if env is None:\n-        return None\n-    live = getattr(env, \"cwd\", None)\n-    if not live:\n-        return None\n-    owner = str(getattr(env, \"cwd_owner\", \"\") or \"\")\n-    tid = str(task_id or \"\")\n-    if owner and tid and owner != \"default\" and tid != \"default\" and owner != tid:\n-        return None\n-    return live\n-\n-\n-def _get_live_tracking_cwd(task_id: str = \"default\") -> str | None:\n-    \"\"\"Return the task's live terminal cwd for bookkeeping when available.\"\"\"\n-    try:\n-        from tools.terminal_tool import _resolve_container_task_id\n-        container_key = _resolve_container_task_id(task_id)\n-    except Exception:\n-        container_key = task_id\n-\n-    with _file_ops_lock:\n-        cached = _file_ops_cache.get(container_key) or _file_ops_cache.get(task_id)\n-    if cached is not None:\n-        env = getattr(cached, \"env\", None)\n-        live_cwd = _live_cwd_if_owned(env, task_id)\n-        if live_cwd:\n-            _remember_last_known_cwd(container_key, live_cwd)\n-            return live_cwd\n-        # Legacy: a cache entry carrying its own cwd with no env to own it.\n-        if env is None and getattr(cached, \"cwd\", None):\n-            legacy_cwd = getattr(cached, \"cwd\", None)\n-            _remember_last_known_cwd(container_key, legacy_cwd)\n-            return legacy_cwd\n-\n-    try:\n-        from tools.terminal_tool import _active_environments, _env_lock\n-\n-        with _env_lock:\n-            env = _active_environments.get(container_key) or _active_environments.get(task_id)\n-        live_cwd = _live_cwd_if_owned(env, task_id)\n-        if live_cwd:\n-            _remember_last_known_cwd(container_key, live_cwd)\n-            return live_cwd\n-    except Exception:\n-        pass\n-\n-    return None\n-\n-\n def _authoritative_workspace_root(task_id: str = \"default\") -> str | None:\n     \"\"\"Best-effort absolute workspace root for divergence checks.\n \n-    Prefers the live terminal cwd (the directory the agent is actually working\n-    in). When no terminal command has run yet — so the live registry is empty —\n-    falls back to a registered task/session cwd override (TUI/Desktop/ACP\n-    sessions register a raw-keyed cwd before any tool runs), then to a\n-    sentinel-free absolute ``$TERMINAL_CWD``. This is what lets a worktree or\n-    Desktop session warn about (and resolve into) its workspace from the very\n-    first ``write_file``/``patch``, before any ``cd`` has populated the live cwd.\n+    Resolution:\n+\n+      1. The session's own cwd RECORD (``terminal_tool.get_session_cwd``) —\n+         written on every completed terminal command and seeded by workspace\n+         registration, keyed by the raw session id. Because the record is\n+         per-session, one session's ``cd`` can never leak into another\n+         session's resolution.\n+      2. A registered task/session cwd override (TUI/Desktop/ACP sessions\n+         register a raw-keyed cwd before any tool runs). Normally already\n+         mirrored into the record at registration; kept as a direct fallback\n+         so a cleared/never-written record still resolves the workspace.\n+      3. A sentinel-free absolute ``$TERMINAL_CWD`` (the worktree path set by\n+         ``cli.py``/``main.py`` for ``-w`` sessions).\n \n     Returns ``None`` only when there is genuinely no reliable anchor, in which\n     case callers fall back to the process cwd.\n     \"\"\"\n-    live = _get_live_tracking_cwd(task_id)\n-    if live:\n-        return live\n-    # A session-specific registered override (TUI/Desktop/ACP workspace cwd)\n-    # is more authoritative than the shared last-known anchor: it is keyed by\n-    # the raw session id, so when two worktree sessions share the single\n-    # \"default\" terminal env, a NON-owning session must resolve against its OWN\n-    # registered worktree — never the other session's leftover cwd. (Checked\n-    # before _last_known_cwd, which is keyed by the shared container id.)\n+    try:\n+        from tools.terminal_tool import get_session_cwd\n+\n+        recorded = get_session_cwd(task_id)\n+    except Exception:\n+        recorded = None\n+    if recorded:\n+        return recorded\n     registered = _registered_task_cwd_override(task_id)\n     if registered:\n         return registered\n-    # When the terminal env was cleaned up mid-conversation, the live cwd is\n-    # gone but the directory the agent navigated to is still recorded in the\n-    # durable _last_known_cwd registry. Prefer it over the config/process\n-    # fallback so a relative-path write resolved BEFORE the env is rebuilt\n-    # still lands in the user's directory (root cause of #26211: write happens\n-    # via _resolve_path_for_task -> here, which runs before _get_file_ops\n-    # rebuilds the env). Keyed by the resolved container id, same as the save.\n-    preserved = _last_known_cwd_for(task_id)\n-    if preserved:\n-        return preserved\n     return _configured_terminal_cwd()\n \n ","@@ -794,45 +728,6 @@ def _is_expected_write_exception(exc: Exception) -> bool:\n \n _file_ops_lock = threading.Lock()\n _file_ops_cache: dict = {}\n-# Per-task last-known CWD — preserved across env re-creation so\n-# relative-path file writes land in the right directory after the\n-# terminal environment is cleaned up and rebuilt (root cause of #26211).\n-_last_known_cwd: dict = {}\n-\n-\n-def _remember_last_known_cwd(task_id: str, cwd: str | None) -> None:\n-    \"\"\"Mirror a live terminal cwd into the durable ``_last_known_cwd`` registry.\n-\n-    Belt-and-suspenders for #26211: the cleanup thread can pop BOTH\n-    ``_file_ops_cache`` and ``_active_environments`` before ``_get_file_ops``\n-    reaches its stale-cache detection branch, in which case the old cwd is\n-    never saved and the rebuilt env falls back to the config default — exactly\n-    the silent-misplacement bug. By recording the cwd on every successful live\n-    read (which happens on every relative-path file resolution while the env is\n-    alive), the durable anchor no longer depends on the cleanup-detection\n-    branch firing, so it survives recreation regardless of pop ordering.\n-    \"\"\"\n-    if not cwd:\n-        return\n-    with _file_ops_lock:\n-        if _last_known_cwd.get(task_id) != cwd:\n-            _last_known_cwd[task_id] = cwd\n-\n-\n-def _last_known_cwd_for(task_id: str = \"default\") -> str | None:\n-    \"\"\"Read the durable last-known cwd for *task_id*, container-key aware.\n-\n-    The registry is keyed by the resolved container id (the same key used by\n-    the save sites in ``_get_file_ops`` / ``_get_live_tracking_cwd``), so look\n-    up the resolved key first and fall back to the raw task id.\n-    \"\"\"\n-    try:\n-        from tools.terminal_tool import _resolve_container_task_id\n-        container_key = _resolve_container_task_id(task_id)\n-    except Exception:\n-        container_key = task_id\n-    with _file_ops_lock:\n-        return _last_known_cwd.get(container_key) or _last_known_cwd.get(task_id)\n \n # Track files read per task to detect re-read loops and deduplicate reads.\n # Per task_id we store:","@@ -1069,13 +964,18 @@ def _get_file_ops(task_id: str = \"default\") -> ShellFileOperations:\n                 _last_activity[task_id] = time.time()\n                 return cached\n             else:\n-                # Environment was cleaned up -- preserve the old cwd before\n-                # invalidating the stale cache entry (fixes #26211: silent\n-                # file-creation failures in long-running conversations).\n+                # Environment was cleaned up -- preserve the old cwd in the\n+                # session record before invalidating the stale cache entry\n+                # (fixes #26211: silent file-creation failures in long-running\n+                # conversations). Usually a no-op: every completed command\n+                # already recorded its cwd.\n                 old_cwd = getattr(cached, \"cwd\", None)\n                 if old_cwd:\n-                    with _file_ops_lock:\n-                        _last_known_cwd[task_id] = old_cwd\n+                    try:\n+                        from tools.terminal_tool import record_session_cwd\n+                        record_session_cwd(raw_task_id, old_cwd)\n+                    except Exception:\n+                        pass\n                 with _file_ops_lock:\n                     _file_ops_cache.pop(task_id, None)\n ","@@ -1113,7 +1013,12 @@ def _get_file_ops(task_id: str = \"default\") -> ShellFileOperations:\n             else:\n                 image = \"\"\n \n-            cwd = overrides.get(\"cwd\") or _last_known_cwd.get(task_id) or config[\"cwd\"]\n+            try:\n+                from tools.terminal_tool import get_session_cwd\n+                recorded_cwd = get_session_cwd(raw_task_id)\n+            except Exception:\n+                recorded_cwd = None\n+            cwd = overrides.get(\"cwd\") or recorded_cwd or config[\"cwd\"]\n             # Re-apply the container cwd guard that _get_env_config() already\n             # ran on config[\"cwd\"] (see #50636).  A per-task cwd override\n             # registered by the gateway/TUI/ACP for workspace tracking is a"]},{"file":"tools/terminal_tool.py","hunks":["@@ -1069,6 +1069,58 @@ def _maybe_reap_docker_orphans(container_config: Dict[str, Any]) -> None:\n # Thread-safe because each task_id is unique per rollout.\n _task_env_overrides: Dict[str, Dict[str, Any]] = {}\n \n+# ── Per-session cwd records (cwd rearchitecture, step 1) ────────────────────\n+#\n+# The durable source of truth for \"which directory is THIS session working\n+# in\". Keyed by the raw session/task key (NOT the collapsed container id):\n+# the terminal env is shared across sessions, so any cwd state stored on the\n+# env is a global mutable timeshared between sessions — the root cause of the\n+# wrong-worktree bug class (env.cwd_owner stamping, _last_known_cwd, and the\n+# ownership ladder in file_tools are all patches over that misplacement).\n+#\n+# Step 1 (this change): dual-write only. Every site that learns a session's\n+# live cwd (post-command tracking, cwd-override registration) also records it\n+# here. Readers still use the legacy env.cwd ladder. Later steps flip\n+# file_tools and _resolve_command_cwd to read this store, then delete the\n+# env-side tracking + ownership guards.\n+_session_cwd: Dict[str, str] = {}\n+_session_cwd_lock = threading.Lock()\n+\n+\n+def record_session_cwd(session_key: Optional[str], cwd: Optional[str]) -> None:\n+    \"\"\"Record *cwd* as the working directory of *session_key*.\n+\n+    Called wherever a session's live cwd becomes known: after a terminal\n+    command completes (the env's post-command tracking has just parsed the\n+    resulting cwd) and when a surface registers a workspace cwd override.\n+    Empty/None session keys collapse to ``\"default\"`` (single-session CLI).\n+    Non-string / empty cwds are ignored.\n+    \"\"\"\n+    if not isinstance(cwd, str) or not cwd.strip():\n+        return\n+    key = str(session_key or \"default\")\n+    with _session_cwd_lock:\n+        if _session_cwd.get(key) != cwd:\n+            _session_cwd[key] = cwd\n+\n+\n+def get_session_cwd(session_key: Optional[str]) -> Optional[str]:\n+    \"\"\"Return the recorded working directory for *session_key*, if any.\n+\n+    No fallback chain here on purpose: callers decide what an absent record\n+    means (config default, TERMINAL_CWD seed, process cwd). ``None``/empty\n+    keys read the ``\"default\"`` record.\n+    \"\"\"\n+    key = str(session_key or \"default\")\n+    with _session_cwd_lock:\n+        return _session_cwd.get(key)\n+\n+\n+def clear_session_cwd(session_key: str) -> None:\n+    \"\"\"Drop a session's cwd record (session teardown).\"\"\"\n+    with _session_cwd_lock:\n+        _session_cwd.pop(session_key, None)\n+\n \n def register_task_env_overrides(task_id: str, overrides: Dict[str, Any]):\n     \"\"\"","@@ -1090,15 +1142,14 @@ def register_task_env_overrides(task_id: str, overrides: Dict[str, Any]):\n \n     # If a live environment already exists for this task, a freshly registered\n     # ``cwd`` override (e.g. the ACP client switching the editor's project root\n-    # mid-session via ``session/load`` / ``session/resume``) must take effect on\n-    # the cached env too. ``terminal_tool`` resolves the per-command cwd as\n-    # ``workdir > env.cwd > config/override cwd`` so that ordinary in-session\n-    # ``cd`` state is preserved; without syncing here the override would sit\n-    # below the (already-set) ``env.cwd`` and be silently ignored once any\n-    # command has run. Pushing it onto the live env keeps ``cd`` tracking intact\n-    # while letting an explicit ACP cwd change win, as the client expects.\n+    # mid-session via ``session/load`` / ``session/resume``) must take effect\n+    # immediately. The session record is what commands resolve against;\n+    # the live env's cwd is also updated so env-side seeding stays consistent.\n     new_cwd = overrides.get(\"cwd\")\n     if isinstance(new_cwd, str) and new_cwd.strip():\n+        # A registered workspace cwd IS the session's working directory until\n+        # a `cd` changes it.\n+        record_session_cwd(task_id, new_cwd)\n         # The live env is cached under the raw task_id for per-session surfaces\n         # (ACP/gateway/dashboard) and under the collapsed container id for\n         # isolation-keyed rollouts. Try the raw id first, then the container id,","@@ -1118,6 +1169,7 @@ def clear_task_env_overrides(task_id: str):\n     Called during cleanup to avoid stale entries accumulating.\n     \"\"\"\n     _task_env_overrides.pop(task_id, None)\n+    clear_session_cwd(task_id)\n \n \n def _resolve_container_task_id(task_id: Optional[str]) -> str:","@@ -1986,43 +2038,21 @@ def _resolve_notification_flag_conflict(\n def _resolve_command_cwd(\n     *,\n     workdir: Optional[str],\n-    env: Any,\n     default_cwd: str,\n-    prev_owner: Optional[str] = None,\n+    session_key: Optional[str] = None,\n ) -> str:\n-    \"\"\"Return the cwd for a command, preferring the live session cwd.\n-\n-    ``terminal_tool`` historically re-sent the init-time/config cwd on every\n-    call. That broke session-local ``cd`` state: the environment tracked the\n-    new directory in ``env.cwd``, but foreground/background calls kept forcing\n-    the old cwd back through ``env.execute(..., cwd=...)``. Explicit\n-    ``workdir=`` must still override everything.\n-\n-    When ``prev_owner`` is provided and differs from the current session,\n-    ``env.cwd`` was mutated by a *different* session's ``cd`` and must NOT be\n-    trusted — fall through to ``default_cwd`` (the config/override cwd) so\n-    the command runs in this session's own workspace, not the previous\n-    session's leftover checkout.  This mirrors the ``_live_cwd_if_owned``\n-    guard file_tools uses for the same shared-env problem.\n+    \"\"\"Return the cwd for a command. Explicit ``workdir=`` overrides everything.\n+\n+    Otherwise the session's own cwd RECORD (``get_session_cwd``) wins — it is\n+    written after every completed command for this session, so it IS the\n+    session's ``cd`` state, with no shared-env ambiguity: another session's\n+    ``cd`` lands in another record and can't affect us. A session with no\n+    record yet (first command) runs in ``default_cwd`` (config/override cwd),\n+    which is also what seeds a fresh environment.\n     \"\"\"\n     if workdir:\n         return workdir\n-\n-    live_cwd = getattr(env, \"cwd\", None)\n-    if isinstance(live_cwd, str) and live_cwd.strip():\n-        # The env is shared (collapsed to \"default\"); its cwd tracks the LAST\n-        # session that ran a command.  If a different session owned the env\n-        # before this call claimed it, env.cwd is that session's leftover `cd`\n-        # — not ours.  Don't use it.\n-        if prev_owner is not None:\n-            session_key = getattr(env, \"cwd_owner\", \"\")\n-            # cwd_owner was already overwritten to the current session at the\n-            # call site, so compare against the captured previous owner.\n-            if prev_owner and prev_owner != \"default\" and session_key != prev_owner:\n-                return default_cwd\n-        return live_cwd\n-\n-    return default_cwd\n+    return get_session_cwd(session_key) or default_cwd\n \n \n def terminal_tool(","@@ -2111,7 +2141,7 @@ def terminal_tool(\n         else:\n             image = \"\"\n \n-        cwd = overrides.get(\"cwd\") or config[\"cwd\"]\n+        cwd = overrides.get(\"cwd\") or get_session_cwd(task_id) or config[\"cwd\"]\n         # A per-task cwd override (registered by the gateway/TUI for workspace\n         # tracking, or by RL/benchmark envs) wins over config[\"cwd\"] — but\n         # config[\"cwd\"] was already sanitized for container backends in","@@ -2164,6 +2194,7 @@ def terminal_tool(\n         # Use a per-task creation lock so concurrent tool calls for the same\n         # task_id wait for the first one to finish creating the sandbox,\n         # instead of each creating their own (wasting Modal resources).\n+        env = None\n         with _env_lock:\n             # Prefer the collapsed container id, but fall back to an env cached\n             # under the raw task_id. Per-session surfaces (ACP/gateway/dashboard)","@@ -2265,6 +2296,16 @@ def terminal_tool(\n                         env = new_env\n                     logger.info(\"%s environment ready for task %s\", env_type, effective_task_id[:8])\n \n+        if env is None:\n+            # Unreachable in practice (either the cached branch or the creation\n+            # branch assigned env above); guard for type-safety and so a future\n+            # refactor of the branches can't fall through to an AttributeError.\n+            return json.dumps({\n+                \"output\": \"\",\n+                \"exit_code\": -1,\n+                \"error\": \"Terminal environment unavailable (creation raced cleanup)\",\n+            }, ensure_ascii=False)\n+\n         # Hard-block: gateway lifecycle commands (systemctl/launchctl/hermes\n         # restart|stop targeting hermes-gateway) must never run inside the\n         # gateway process itself. The restart would SIGTERM the gateway, which","@@ -2362,24 +2403,13 @@ def terminal_tool(\n                 \"EOF.\"\n             )\n \n-        # Claim the (shared \"default\") terminal env for the session driving this\n-        # command. File tools read env.cwd_owner to decide whether the env's live\n-        # cwd is THIS session's `cd` or a different worktree session's — without\n-        # it, two open worktree sessions sharing the env route each other's edits\n-        # to the wrong checkout. get_current_session_key()'s contextvar doesn't\n-        # cross tool-worker threads, so fall back to the raw task_id (which IS the\n-        # session_key for the top-level agent) — a stable, thread-safe anchor.\n+        # The session key that drives cwd records: get_current_session_key()'s\n+        # contextvar doesn't cross tool-worker threads, so fall back to the raw\n+        # task_id (which IS the session_key for the top-level agent) — a\n+        # stable, thread-safe anchor.\n         from tools.approval import get_current_session_key\n \n         session_key = get_current_session_key(default=\"\") or (task_id or \"\")\n-        # Capture the env's previous owner BEFORE claiming it — _resolve_command_cwd\n-        # needs to know whether env.cwd was left by a *different* session's `cd`\n-        # (in which case it's stale for this session and must be ignored).\n-        prev_cwd_owner = getattr(env, \"cwd_owner\", \"\") or \"\"\n-        try:\n-            env.cwd_owner = session_key\n-        except Exception:\n-            pass\n \n         if background:\n             # Spawn a tracked background process via the process registry.","@@ -2389,9 +2419,8 @@ def terminal_tool(\n \n             effective_cwd = _resolve_command_cwd(\n                 workdir=workdir,\n-                env=env,\n                 default_cwd=cwd,\n-                prev_owner=prev_cwd_owner,\n+                session_key=session_key,\n             )\n             try:\n                 if env_type == \"local\":","@@ -2650,9 +2679,8 @@ def terminal_tool(\n                 try:\n                     command_cwd = _resolve_command_cwd(\n                         workdir=workdir,\n-                        env=env,\n                         default_cwd=cwd,\n-                        prev_owner=prev_cwd_owner,\n+                        session_key=session_key,\n                     )\n                     execute_kwargs = {\n                         \"timeout\": effective_timeout,","@@ -2693,7 +2721,15 @@ def terminal_tool(\n                 \n                 # Got a result\n                 break\n-            \n+\n+            # Dual-write (cwd rearch step 1): the env's post-command tracking\n+            # (marker parse / local sync) has just updated env.cwd with the\n+            # directory this command finished in. That cwd belongs to THIS\n+            # session — record it under the session key so the durable record\n+            # never depends on the shared env surviving or on who drives the\n+            # env next.\n+            record_session_cwd(session_key, getattr(env, \"cwd\", None))\n+\n             # Extract output\n             output = result.get(\"output\", \"\")\n             returncode = result.get(\"returncode\", 0)\n"]}]},{"kind":"pr","number":65487,"title":"fix(execute-code): resolve project-mode cwd from the session's own directory","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["tests/tools/test_code_execution_modes.py","tools/code_execution_tool.py"],"fix_hunks":[{"file":"tests/tools/test_code_execution_modes.py","hunks":["@@ -220,6 +220,69 @@ def test_project_expands_tilde(self):\n         with patch.dict(os.environ, {\"TERMINAL_CWD\": \"~\"}):\n             self.assertEqual(_resolve_child_cwd(\"project\", \"/tmp/staging\"), home)\n \n+    def test_project_prefers_registered_task_cwd_override(self):\n+        import tempfile\n+        import tools.terminal_tool as terminal_tool\n+\n+        with tempfile.TemporaryDirectory() as td:\n+            task_id = \"session-cwd-test\"\n+            with patch.dict(os.environ, {\"TERMINAL_CWD\": \"/does/not/exist\"}):\n+                with patch.object(terminal_tool, \"_task_env_overrides\", {}, create=False):\n+                    terminal_tool.register_task_env_overrides(task_id, {\"cwd\": td})\n+                    self.assertEqual(_resolve_child_cwd(\"project\", \"/tmp/staging\", task_id=task_id), td)\n+\n+    def test_project_prefers_session_cwd_record_over_override(self):\n+        \"\"\"The session's cwd RECORD (its live `cd` state) outranks the\n+        registration-time workspace override — same ladder as file tools\n+        and the terminal, so a `cd` before execute_code is honored.\"\"\"\n+        import tempfile\n+        import tools.terminal_tool as terminal_tool\n+\n+        with tempfile.TemporaryDirectory() as reg, tempfile.TemporaryDirectory() as cded:\n+            task_id = \"session-record-test\"\n+            with patch.dict(os.environ, {\"TERMINAL_CWD\": \"/does/not/exist\"}):\n+                with patch.object(terminal_tool, \"_task_env_overrides\", {}, create=False), \\\n+                     patch.object(terminal_tool, \"_session_cwd\", {}, create=False):\n+                    terminal_tool.register_task_env_overrides(task_id, {\"cwd\": reg})\n+                    # Simulate a later `cd`: post-command tracking rewrites the record.\n+                    terminal_tool.record_session_cwd(task_id, cded)\n+                    self.assertEqual(\n+                        _resolve_child_cwd(\"project\", \"/tmp/staging\", task_id=task_id), cded\n+                    )\n+\n+    def test_project_uses_session_cwd_record_without_any_override(self):\n+        \"\"\"A session that only `cd`'d (no session.cwd.set registration) still\n+        resolves to its recorded directory.\"\"\"\n+        import tempfile\n+        import tools.terminal_tool as terminal_tool\n+\n+        with tempfile.TemporaryDirectory() as cded:\n+            task_id = \"record-only-test\"\n+            with patch.dict(os.environ, {\"TERMINAL_CWD\": \"/does/not/exist\"}):\n+                with patch.object(terminal_tool, \"_task_env_overrides\", {}, create=False), \\\n+                     patch.object(terminal_tool, \"_session_cwd\", {}, create=False):\n+                    terminal_tool.record_session_cwd(task_id, cded)\n+                    self.assertEqual(\n+                        _resolve_child_cwd(\"project\", \"/tmp/staging\", task_id=task_id), cded\n+                    )\n+\n+    def test_project_stale_record_falls_through_to_override(self):\n+        \"\"\"A recorded directory that no longer exists is skipped; the\n+        registered override is the next rung.\"\"\"\n+        import tempfile\n+        import tools.terminal_tool as terminal_tool\n+\n+        with tempfile.TemporaryDirectory() as reg:\n+            task_id = \"stale-record-test\"\n+            with patch.dict(os.environ, {\"TERMINAL_CWD\": \"/does/not/exist\"}):\n+                with patch.object(terminal_tool, \"_task_env_overrides\", {}, create=False), \\\n+                     patch.object(terminal_tool, \"_session_cwd\", {}, create=False):\n+                    terminal_tool.register_task_env_overrides(task_id, {\"cwd\": reg})\n+                    terminal_tool.record_session_cwd(task_id, \"/deleted/dir/gone\")\n+                    self.assertEqual(\n+                        _resolve_child_cwd(\"project\", \"/tmp/staging\", task_id=task_id), reg\n+                    )\n+\n \n # ---------------------------------------------------------------------------\n # Schema description","@@ -316,6 +379,28 @@ def test_project_mode_runs_in_session_cwd(self):\n                 os.path.realpath(td),\n             )\n \n+    def test_project_mode_uses_registered_session_cwd_override(self):\n+        \"\"\"Project mode must honor session.cwd.set-style overrides even when\n+        TERMINAL_CWD is absent or points elsewhere.\"\"\"\n+        import tempfile\n+        import tools.terminal_tool as terminal_tool\n+\n+        with tempfile.TemporaryDirectory() as td:\n+            task_id = \"session-cwd-test\"\n+            with patch.dict(os.environ, {\"TERMINAL_CWD\": \"/does/not/exist\"}):\n+                with patch.object(terminal_tool, \"_task_env_overrides\", {}, create=False):\n+                    terminal_tool.register_task_env_overrides(task_id, {\"cwd\": td})\n+                    with _mock_mode(\"project\"):\n+                        with patch(\"model_tools.handle_function_call\", side_effect=_mock_handle_function_call):\n+                            raw = execute_code(\n+                                code=\"import os; print(os.getcwd())\",\n+                                task_id=task_id,\n+                                enabled_tools=list(SANDBOX_ALLOWED_TOOLS),\n+                            )\n+            result = json.loads(raw)\n+            self.assertEqual(result[\"status\"], \"success\")\n+            self.assertEqual(os.path.realpath(result[\"output\"].strip()), os.path.realpath(td))\n+\n     def test_project_mode_interpreter_is_venv_python(self):\n         \"\"\"Project mode: sys.executable inside the child is the venv's python\n         when VIRTUAL_ENV is set to a real venv.\"\"\""]},{"file":"tools/code_execution_tool.py","hunks":["@@ -1339,7 +1339,7 @@ def execute_code(\n         # Env scrubbing and tool whitelist apply identically in both modes.\n         _mode = _get_execution_mode()\n         _child_python = _resolve_child_python(_mode)\n-        _child_cwd = _resolve_child_cwd(_mode, tmpdir)\n+        _child_cwd = _resolve_child_cwd(_mode, tmpdir, task_id=task_id or \"\")\n         _script_path = os.path.join(tmpdir, \"script.py\")\n \n         proc = subprocess.Popen(","@@ -1745,17 +1745,43 @@ def _resolve_child_python(mode: str) -> str:\n     return sys.executable\n \n \n-def _resolve_child_cwd(mode: str, staging_dir: str) -> str:\n+def _resolve_child_cwd(mode: str, staging_dir: str, task_id: str = \"\") -> str:\n     \"\"\"Resolve the working directory for the execute_code subprocess.\n \n     - ``strict``: the staging tmpdir (today's behavior).\n-    - ``project``: the session's TERMINAL_CWD (same as the terminal tool), or\n-      ``os.getcwd()`` if TERMINAL_CWD is unset or doesn't point at a real dir.\n-      Falls back to the staging tmpdir as a last resort so we never invoke\n-      Popen with a nonexistent cwd.\n+    - ``project``: the session's own cwd — its per-session cwd record\n+      (written after every completed terminal command), then the raw\n+      per-session cwd override registered via ``session.cwd.set`` /\n+      ``register_task_env_overrides``, then the session's TERMINAL_CWD\n+      (same as the terminal tool), or ``os.getcwd()`` if none points at a\n+      real dir. Falls back to the staging tmpdir as a last resort so we\n+      never invoke Popen with a nonexistent cwd.\n+\n+    This mirrors the resolution ladder file tools and the terminal use\n+    (record → registered override → TERMINAL_CWD), so all file-writing\n+    paths within a session agree on the working directory. (#56047)\n     \"\"\"\n     if mode != \"project\":\n         return staging_dir\n+    if task_id:\n+        # 1. The session's cwd record — IS the session's `cd` state.\n+        try:\n+            from tools.terminal_tool import get_session_cwd\n+\n+            recorded = get_session_cwd(task_id)\n+        except Exception:\n+            recorded = None\n+        if recorded and os.path.isdir(recorded):\n+            return recorded\n+        # 2. Registered workspace override (session.cwd.set → gateway/TUI/ACP).\n+        try:\n+            from tools.file_tools import _registered_task_cwd_override\n+\n+            session_cwd = _registered_task_cwd_override(task_id)\n+        except Exception:\n+            session_cwd = None\n+        if session_cwd and os.path.isdir(session_cwd):\n+            return session_cwd\n     raw = os.environ.get(\"TERMINAL_CWD\", \"\").strip()\n     if raw:\n         expanded = os.path.expanduser(raw)\n"]}]},{"kind":"pr","number":104615,"title":"fix(gateway): persist per-session workspaces","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#42577","number":42577},{"kind":"pr","value":"#37277","number":37277},{"kind":"pr","value":"#37275","number":37275},{"kind":"pr","value":"#42577","number":42577}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":105478,"title":"feat(api-server): bind per-run workspace cwd on /v1/runs","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"67481b6f37","number":0},{"kind":"pr","value":"#78923","number":78923},{"kind":"pr","value":"#109230","number":109230},{"kind":"pr","value":"#136998","number":136998},{"kind":"pr","value":"#29531","number":29531}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":105991,"title":"feat(tui): add /cd slash command and scope path completion to session cwd","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":109839,"title":"fix(tui): make the launch cwd win over an inherited HERMES_CWD","url":null,"state":"closed","close_reason":{"category":"merged_via","implication":"superseded"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}