fix(gateway): only follow settles into same-repo worktrees; never override an explicit cwd
Builds on #72787's current_root guard (cherry-picked with authorship preserved). Two further hardenings for #72776: - require the settled cwd and the workspace to share the same common .git dir (the shape 'git worktree add' produces), so a git workspace visiting an UNRELATED repo is a browsing visit, not a re-home (repro'd by Johnny-xuan in the issue thread); - never reconcile over an explicitly chosen workspace (explicit_cwd), while a settle-adopted cwd stays followable via a cwd_from_settle marker cleared by _set_session_cwd / project switch. Fixes #72776
This commit is contained in:
parent
a22d6516df
commit
8babfe95b4
|
|
@ -118,6 +118,52 @@ def test_a_deleted_directory_is_not_a_move(session, repo_with_worktree, tmp_path
|
|||
assert session["cwd"] == str(repo)
|
||||
|
||||
|
||||
def test_an_unrelated_repo_is_not_a_move(session, repo_with_worktree, tmp_path):
|
||||
"""Git workspace A visiting unrelated git repo B is a visit, not a re-home.
|
||||
|
||||
Only checkouts sharing the same common .git dir (the shape `git worktree
|
||||
add` produces) count as a relocation; `cd ~/other-project && git log`
|
||||
must not re-anchor the chat onto the foreign repo.
|
||||
"""
|
||||
repo, _ = repo_with_worktree
|
||||
other = tmp_path / "other"
|
||||
other.mkdir()
|
||||
_git(other, "init", "-b", "main")
|
||||
_git(other, "config", "user.email", "t@example.com")
|
||||
_git(other, "config", "user.name", "t")
|
||||
(other / "x.txt").write_text("x\n", encoding="utf-8")
|
||||
_git(other, "add", ".")
|
||||
_git(other, "commit", "-m", "init")
|
||||
terminal_tool.record_session_cwd(session["session_key"], str(other))
|
||||
|
||||
assert server._reconcile_session_cwd_from_terminal(session) is False
|
||||
assert session["cwd"] == str(repo)
|
||||
|
||||
|
||||
def test_an_explicit_workspace_is_never_overridden(session, repo_with_worktree):
|
||||
"""A user-chosen cwd must survive even a legitimate same-repo worktree move."""
|
||||
repo, worktree = repo_with_worktree
|
||||
session["explicit_cwd"] = True
|
||||
terminal_tool.record_session_cwd(session["session_key"], str(worktree))
|
||||
|
||||
assert server._reconcile_session_cwd_from_terminal(session) is False
|
||||
assert session["cwd"] == str(repo)
|
||||
|
||||
|
||||
def test_a_settle_adopted_cwd_can_keep_following(session, repo_with_worktree):
|
||||
"""The settle marker keeps a session following the agent across worktrees."""
|
||||
repo, worktree = repo_with_worktree
|
||||
terminal_tool.record_session_cwd(session["session_key"], str(worktree))
|
||||
assert server._reconcile_session_cwd_from_terminal(session) is True
|
||||
assert session["explicit_cwd"] is True
|
||||
assert session["cwd_from_settle"] is True
|
||||
|
||||
# Agent moves back to the primary checkout: still follows.
|
||||
terminal_tool.record_session_cwd(session["session_key"], str(repo))
|
||||
assert server._reconcile_session_cwd_from_terminal(session) is True
|
||||
assert session["cwd"] == str(repo)
|
||||
|
||||
|
||||
def test_remote_backends_do_not_reanchor(session, repo_with_worktree, monkeypatch):
|
||||
"""A remote cwd names a path on the host, not one this gateway can probe."""
|
||||
repo, worktree = repo_with_worktree
|
||||
|
|
|
|||
|
|
@ -2346,8 +2346,11 @@ def _reconcile_session_cwd_from_terminal(session: dict | None) -> bool:
|
|||
A plain `cd` is deliberately NOT a workspace move (see
|
||||
``_apply_project_workspace``): browsing to /tmp to read a log must not
|
||||
re-home the chat. What we adopt here is narrower — the session's recorded
|
||||
cwd is in a DIFFERENT git working tree than its workspace. That is a
|
||||
relocation by any reading, and it is the only shape this reconciles.
|
||||
cwd is in a DIFFERENT working tree of the SAME repository (the shape
|
||||
``git worktree add`` produces). Everything else — a non-git workspace
|
||||
stepping into a repo, or a git workspace visiting an unrelated repo — is
|
||||
a browsing visit, and a user's explicitly chosen workspace is never
|
||||
overridden at all.
|
||||
|
||||
Local backends only: a remote/SSH cwd names a path on the host, which this
|
||||
gateway can neither stat nor probe with git.
|
||||
|
|
@ -2355,6 +2358,14 @@ def _reconcile_session_cwd_from_terminal(session: dict | None) -> bool:
|
|||
if not session or not _is_local_terminal_backend():
|
||||
return False
|
||||
|
||||
# A workspace the user (or GUI) explicitly chose is never overridden by
|
||||
# where the agent's terminal happened to settle — only another explicit
|
||||
# action (`_set_session_cwd`, a project switch) moves it. A cwd this very
|
||||
# function adopted is marked `cwd_from_settle` so a session can keep
|
||||
# following the agent through successive worktrees.
|
||||
if session.get("explicit_cwd") and not session.get("cwd_from_settle"):
|
||||
return False
|
||||
|
||||
try:
|
||||
from tools.terminal_tool import get_session_cwd
|
||||
|
||||
|
|
@ -2383,10 +2394,23 @@ def _reconcile_session_cwd_from_terminal(session: dict | None) -> bool:
|
|||
if not landed or not current_root or landed == current_root:
|
||||
return False
|
||||
|
||||
# And only between checkouts of the SAME repository — the shape a real
|
||||
# `git worktree add` produces (linked worktrees share the common .git
|
||||
# dir). Settling in an UNRELATED repo (`cd ~/other-project && git log`)
|
||||
# is likewise a visit: adopting it would re-home the chat onto whatever
|
||||
# foreign repo the terminal last touched.
|
||||
landed_common = _git_common_repo_root_for_cwd(resolved)
|
||||
current_common = _git_common_repo_root_for_cwd(current)
|
||||
if not landed_common or landed_common != current_common:
|
||||
return False
|
||||
|
||||
session["cwd"] = resolved
|
||||
# The session works here now, so this is its workspace — a desktop chat
|
||||
# whose cwd was an unpersisted launch artifact earns a real row.
|
||||
# whose cwd was an unpersisted launch artifact earns a real row. The
|
||||
# settle marker keeps this adoption overridable by the NEXT settle while
|
||||
# still yielding to a user's explicit choice (see the guard above).
|
||||
session["explicit_cwd"] = True
|
||||
session["cwd_from_settle"] = True
|
||||
_register_session_cwd(session)
|
||||
|
||||
with _session_db(session) as db:
|
||||
|
|
@ -2684,6 +2708,9 @@ def _set_session_cwd(session: dict, cwd: str) -> str:
|
|||
# An explicit user choice — persist it as the workspace (and let a later
|
||||
# lazy row creation persist it too, not the launch-dir fallback).
|
||||
session["explicit_cwd"] = True
|
||||
# A user's choice supersedes any earlier settle-adopted cwd: from here on
|
||||
# the terminal wandering must not move the workspace again.
|
||||
session["cwd_from_settle"] = False
|
||||
_register_session_cwd(session)
|
||||
with _session_db(session) as db:
|
||||
if db is not None:
|
||||
|
|
@ -5549,6 +5576,8 @@ def _apply_project_workspace(task_id: str, path: str, _name: str = "") -> None:
|
|||
|
||||
session["cwd"] = resolved
|
||||
session["explicit_cwd"] = True
|
||||
# An explicit project switch supersedes any earlier settle-adopted cwd.
|
||||
session["cwd_from_settle"] = False
|
||||
_register_session_cwd(session)
|
||||
|
||||
with _session_db(session) as db:
|
||||
|
|
|
|||
Loading…
Reference in New Issue