fix(gateway): let an explicit workspace move win for a running session

session.workspace.move refused a running live session with 4009
(session busy), but the desktop's Move-to-project flow calls exactly
this RPC — so the UI updated its local grouping while state.db kept the
old cwd and the agent's tools kept running in the old workspace. Two
sources of truth disagreed (#86626).

An explicit move now wins: the stored row and the live session re-anchor
together. In-flight tool calls keep the cwd they were launched with; the
next tool call uses the new workspace.
This commit is contained in:
Ayush Nangia
2026-08-15 13:58:02 +05:30
committed by Teknium
parent 8fc4189edd
commit be8b58dcbc
2 changed files with 57 additions and 4 deletions
+52
View File
@@ -19458,3 +19458,55 @@ def test_persist_live_session_system_prompt_restores_pre_existing_override(tmp_p
finally:
reset_hermes_home_override(outer_token)
assert get_hermes_home_override() is None
def test_workspace_move_rehomes_running_session(monkeypatch, tmp_path):
"""An explicit Move-to-project must win for a RUNNING session: the stored
row and the live runtime session re-anchor together, never a UI-vs-db
disagreement (#86626)."""
target = "stored-running-session"
new_cwd = tmp_path / "dest-project"
new_cwd.mkdir()
captured = {}
class FakeDB:
def get_session(self, session_id):
return {"id": session_id}
def update_session_cwd(self, session_id, cwd, branch=None, root=None, replace_git_meta=True):
captured["row_update"] = (session_id, cwd)
def close(self):
pass
import contextlib
@contextlib.contextmanager
def _fake_db(_params):
yield FakeDB()
monkeypatch.setattr(server, "_profile_db", _fake_db)
monkeypatch.setattr(
server,
"_git_branch_for_cwd",
lambda cwd: "main",
)
monkeypatch.setattr(
server,
"_git_common_repo_root_for_cwd",
lambda cwd: str(new_cwd),
)
live = {"session_key": target, "running": True, "cwd": str(tmp_path / "old-project")}
server._sessions["live-sid"] = live
monkeypatch.setattr(server, "_register_session_cwd", lambda _session: None)
res = server._methods["session.workspace.move"](
"rid",
{"session_key": target, "cwd": str(new_cwd)},
)
assert "error" not in res, res
assert captured["row_update"] == (target, str(new_cwd))
assert live["cwd"] == str(new_cwd)
assert live.get("explicit_cwd") is True
+5 -4
View File
@@ -920,8 +920,11 @@ def _(rid, params: dict) -> dict:
stale ``git_repo_root`` would keep it grouped under the project it left.
A live agent bound to the row follows through the runtime path too, so its
terminal/file tools re-anchor immediately; a mid-turn session refuses the
move rather than yanking the workspace out from under its tools.
terminal/file tools re-anchor immediately. An explicit move wins even
mid-turn: refusing a running session made the desktop's "Move to project"
claim success in the UI while ``state.db`` kept the old cwd — two sources
of truth disagreeing (#86626). In-flight tool calls keep the cwd they were
launched with; the NEXT tool call uses the new workspace.
"""
target = str(params.get("session_key") or "").strip()
if not target:
@@ -944,8 +947,6 @@ def _(rid, params: dict) -> dict:
if sess.get("session_key") == target:
live, live_sid = sess, sid
break
if live is not None and live.get("running"):
return _err(rid, 4009, "session busy")
branch = _git_branch_for_cwd(resolved)
root = _git_common_repo_root_for_cwd(resolved)