diff --git a/tests/tools/test_file_tools_cwd_resolution.py b/tests/tools/test_file_tools_cwd_resolution.py index 24e23477962..88d6265b6f9 100644 --- a/tests/tools/test_file_tools_cwd_resolution.py +++ b/tests/tools/test_file_tools_cwd_resolution.py @@ -394,3 +394,27 @@ def test_unknown_owner_keeps_prior_single_session_behavior(tmp_path, monkeypatch ) assert ft._get_live_tracking_cwd("default") == str(ws) assert ft._get_live_tracking_cwd("any-session") == str(ws) + + +def test_preserved_cwd_does_not_override_non_owning_sessions_worktree( + _two_worktree_sessions, monkeypatch +): + """#26211 belt-and-suspenders must not break worktree isolation. + + The owner (session B) doing an owned live read mirrors wt_b into the shared + _last_known_cwd['default'] registry. Session A — which does NOT own the env + but HAS its own registered worktree (wt_a) — must still resolve into wt_a, + not inherit B's preserved cwd through the shared-container key. The + session-specific registered override must beat the durable shared anchor. + """ + wt_a, wt_b, _main = _two_worktree_sessions + monkeypatch.setattr(ft, "_last_known_cwd", {}) + + # Owner B resolves first — this mirrors wt_b into _last_known_cwd['default']. + assert ft._resolve_path_for_task("target.py", task_id="sess-b") == (wt_b / "target.py") + assert ft._last_known_cwd.get("default") == str(wt_b) + + # A still routes to its own registered worktree despite the shared anchor. + resolved_a = ft._resolve_path_for_task("target.py", task_id="sess-a") + assert resolved_a == (wt_a / "target.py") + assert not str(resolved_a).startswith(str(wt_b)) diff --git a/tools/file_tools.py b/tools/file_tools.py index cf47322b16f..9b0f9681dba 100644 --- a/tools/file_tools.py +++ b/tools/file_tools.py @@ -244,6 +244,15 @@ def _authoritative_workspace_root(task_id: str = "default") -> str | None: live = _get_live_tracking_cwd(task_id) if live: return live + # A session-specific registered override (TUI/Desktop/ACP workspace cwd) + # is more authoritative than the shared last-known anchor: it is keyed by + # the raw session id, so when two worktree sessions share the single + # "default" terminal env, a NON-owning session must resolve against its OWN + # registered worktree — never the other session's leftover cwd. (Checked + # before _last_known_cwd, which is keyed by the shared container id.) + registered = _registered_task_cwd_override(task_id) + if registered: + return registered # When the terminal env was cleaned up mid-conversation, the live cwd is # gone but the directory the agent navigated to is still recorded in the # durable _last_known_cwd registry. Prefer it over the config/process @@ -254,9 +263,6 @@ def _authoritative_workspace_root(task_id: str = "default") -> str | None: preserved = _last_known_cwd_for(task_id) if preserved: return preserved - registered = _registered_task_cwd_override(task_id) - if registered: - return registered return _configured_terminal_cwd()