diff --git a/module/scripts/clawde-service/supervisor_backend_herdr.py b/module/scripts/clawde-service/supervisor_backend_herdr.py index dc83b76..5bc4363 100644 --- a/module/scripts/clawde-service/supervisor_backend_herdr.py +++ b/module/scripts/clawde-service/supervisor_backend_herdr.py @@ -118,21 +118,29 @@ def create_agent_window( return False return self.run_wrapper_in_pane(pane_id, wrapper_command) + def discard_superseded_tab( + self, session_name: str, agent_name: str, tab_id: str + ) -> None: + close_result = self.run_herdr_command("tab", "close", tab_id) + if close_result.returncode == 0: + return + print( + f"Error: failed to close the superseded herdr tab {tab_id!r} of " + f"{agent_name!r} in workspace {session_name!r}: " + f"{close_result.stderr.strip()}", + file=sys.stderr, + ) + def relaunch_wrapper_in_window( self, session_name: str, agent_name: str, wrapper_command: str ) -> bool: - tab = self.find_agent_tab(session_name, agent_name) - if tab is None: + superseded_tab = self.find_agent_tab(session_name, agent_name) + if superseded_tab is None: return self.create_agent_window(session_name, agent_name, wrapper_command) - close_result = self.run_herdr_command("tab", "close", tab["tab_id"]) - if close_result.returncode != 0: - print( - f"Error: failed to replace herdr tab {agent_name!r} in workspace " - f"{session_name!r}: {close_result.stderr.strip()}", - file=sys.stderr, - ) + if not self.create_agent_window(session_name, agent_name, wrapper_command): return False - return self.create_agent_window(session_name, agent_name, wrapper_command) + self.discard_superseded_tab(session_name, agent_name, superseded_tab["tab_id"]) + return True def ensure_host_ready(self, session_name: str) -> bool: if not self.ensure_server_running(): diff --git a/module/scripts/tests/unit/test_clawde_service_herdr.py b/module/scripts/tests/unit/test_clawde_service_herdr.py index e887318..bf5ba4d 100644 --- a/module/scripts/tests/unit/test_clawde_service_herdr.py +++ b/module/scripts/tests/unit/test_clawde_service_herdr.py @@ -4,6 +4,7 @@ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent)) from herdr_backend_test_support import ( + CompletedProcessStub, TAB_CREATE_JSON, TAB_LIST_WP, TAB_LIST_WP_ONLY_BOOTSTRAP, @@ -130,7 +131,7 @@ def test_create_agent_window_creates_the_workspace_when_missing(): ) in issued -def test_relaunch_replaces_the_existing_tab_before_starting_the_wrapper(): +def test_relaunch_starts_the_replacement_tab_before_discarding_the_stale_one(): issued = [] backend = backend_with_responses( issued, @@ -143,8 +144,7 @@ def test_relaunch_replaces_the_existing_tab_before_starting_the_wrapper(): assert backend.relaunch_wrapper_in_window( "clawde", "bronze", "exec /nix/store/x-agent" ) - assert ("tab", "close", "wP:t7") in issued - assert ( + creation = ( "tab", "create", "--workspace", @@ -152,7 +152,8 @@ def test_relaunch_replaces_the_existing_tab_before_starting_the_wrapper(): "--label", "bronze", "--no-focus", - ) in issued + ) + assert issued.index(creation) < issued.index(("tab", "close", "wP:t7")) assert ( "pane", "run", @@ -162,6 +163,42 @@ def test_relaunch_replaces_the_existing_tab_before_starting_the_wrapper(): assert not any(command[:3] == ("pane", "run", "wP:p7") for command in issued) +def test_relaunch_revives_an_agent_whose_tab_is_the_last_one_in_its_workspace(): + issued = [] + backend = backend_with_responses( + issued, + [ + (("workspace", "list"), WORKSPACE_LIST_WITH_CLAWDE), + (("tab", "list", "--workspace"), TAB_LIST_WP), + (("tab", "create"), TAB_CREATE_JSON), + ], + ) + creating_run = backend.run_herdr_command + + def run_refusing_to_close_the_last_tab(*arguments): + if arguments[:2] == ("tab", "close"): + issued.append(arguments) + refusal = CompletedProcessStub(1, "") + refusal.stderr = ( + '{"error":{"code":"tab_close_failed",' + '"message":"cannot close the last tab in a workspace"}}' + ) + return refusal + return creating_run(*arguments) + + backend.run_herdr_command = run_refusing_to_close_the_last_tab + + assert backend.relaunch_wrapper_in_window( + "clawde", "bronze", "exec /nix/store/x-agent" + ) + assert ( + "pane", + "run", + "wZ:p9", + "CLAWDE_MULTIPLEXER=herdr exec /nix/store/x-agent", + ) in issued + + def test_remove_agent_window_closes_the_agent_tab(): issued = [] backend = backend_with_responses(