From f0a3a279b7c48d28b9e7e4aea1ed9caed846906b Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 21 Apr 2026 14:28:09 +0200 Subject: [PATCH] Always provide full activation message upon calling activate_project This is necessary, because some clients (e.g. Claude Desktop) will reuse a single session across chats. Resolves #1384 --- CHANGELOG.md | 2 ++ src/serena/agent.py | 8 ++++++-- test/serena/test_serena_agent.py | 29 ++++++++++++++++++++++------- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b3453c7..0c5fbe38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ Status of the `main` branch. Changes prior to the next official version change w `initial_instructions`). Now they are passed directly in the activation message (and excluded from a subsequent `initial_instructions` call). - Fix: Project activation message was provided more than once for case of dynamic project activation followed by `initial_instructions` #1372 + - Always provide full activation message upon calling `activate_project` (even if project was already active in the same session) #1384 + This is necessary, because some clients (e.g. Claude Desktop) will reuse a single session across chats. - Security: Forbid `".."` in memory names to disallow accessing files outside dedicated memory directories - Security: Add check for tool being read-only in the project server (previously only checked in `query_project` tool, i.e. client side) - Usage reporting now also includes the name of the Serena context that is used diff --git a/src/serena/agent.py b/src/serena/agent.py index 3abc69be..d0c6b65c 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -800,9 +800,13 @@ class SerenaAgent: proj = self._active_project assert proj is not None, "A project must be active before calling this." - # if activation message was already provided in the current session, don't provide it again + # Note: The activation message is always returned in full, even if it was already provided in the current session, + # because some clients (e.g. Claude Desktop) will use the same session across multiple chats. + # So while we don't want the activation message to be additionally included in the system prompt + # (initial_instructions), an explicit project activation should always return it. + # (The check below deliberately left in place for documentation purposes, preventing a regression) if self._project_prompt_status.is_project_activation_message_already_provided(session_id): - return "Project was already activated." + pass # no special handling # provide basic project information (name, location, languages, encoding) if proj.is_newly_created: diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index c529fa2e..896b030f 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -728,14 +728,16 @@ class TestPromptProvision: but not on subsequent calls within the same session. #1372 """ project_name = "test_repo_python" + session1 = "session1" + session2 = "session2" - result1 = self._call_tool(serena_agent, InitialInstructionsTool, session_id="session1") + result1 = self._call_tool(serena_agent, InitialInstructionsTool, session_id=session1) self._assert_activation_message(result1, project_name, present=True) - result2 = self._call_tool(serena_agent, InitialInstructionsTool, session_id="session2") + result2 = self._call_tool(serena_agent, InitialInstructionsTool, session_id=session2) self._assert_activation_message(result2, project_name, present=True) - result3 = self._call_tool(serena_agent, InitialInstructionsTool, session_id="session1") + result3 = self._call_tool(serena_agent, InitialInstructionsTool, session_id=session1) self._assert_activation_message(result3, project_name, present=False) @pytest.mark.parametrize("serena_agent", [Language.PYTHON], indirect=True) @@ -747,16 +749,18 @@ class TestPromptProvision: """ project_name1 = "test_repo_python" project_name2 = "test_repo_java" + session1 = "session1" + session2 = "session2" # the initial instructions must contain the project activation message for the first project - result1 = self._call_tool(serena_agent, InitialInstructionsTool, session_id="session1") + result1 = self._call_tool(serena_agent, InitialInstructionsTool, session_id=session1) self._assert_activation_message(result1, project_name1, present=True) # now activate another project which dynamically enables a new mode (no-onboarding) reg_project = serena_agent.serena_config.get_registered_project(project_name2) reg_project.project_config.default_modes = ["no-onboarding"] expected_new_mode_message = "The onboarding process is not applied." - result2 = self._call_tool(serena_agent, ActivateProjectTool, project=project_name2, session_id="session1") + result2 = self._call_tool(serena_agent, ActivateProjectTool, project=project_name2, session_id=session1) # the new mode's prompt must be included in the activation message self._assert_activation_message(result2, project_name2, present=True) @@ -765,16 +769,27 @@ class TestPromptProvision: ) # the mode prompt must not be included in subsequent calls to the initial instructions tool within the same session - result3 = self._call_tool(serena_agent, InitialInstructionsTool, session_id="session1") + result3 = self._call_tool(serena_agent, InitialInstructionsTool, session_id=session1) assert expected_new_mode_message not in result3, ( f"Expected new mode message '{expected_new_mode_message}' to not be included in subsequent calls, but it was found in result:\n{result3}" ) # the mode prompt must be included in the initial instructions of a new session - result4 = self._call_tool(serena_agent, InitialInstructionsTool, session_id="session2") + result4 = self._call_tool(serena_agent, InitialInstructionsTool, session_id=session2) assert expected_new_mode_message in result4, ( f"Expected new mode message '{expected_new_mode_message}' to be included in new session, but it was not found in result:\n{result4}" ) # the initial instructions for the new session must also include the activation message for the project self._assert_activation_message(result4, project_name2, present=True) + + @pytest.mark.parametrize("serena_agent", [Language.PYTHON], indirect=True) + def test_activate_project_tool_always_returns_activation_message(self, serena_agent: SerenaAgent) -> None: + project_name = "test_repo_python" + session = "session1" + + result1 = self._call_tool(serena_agent, ActivateProjectTool, project=project_name, session_id=session) + self._assert_activation_message(result1, project_name, present=True) + + result2 = self._call_tool(serena_agent, ActivateProjectTool, project=project_name, session_id=session) + self._assert_activation_message(result2, project_name, present=True)