{"repo":"NousResearch/hermes-agent","public_id":"18980","display_label":"Prevent zombie Slack Socket Mode connections on reconnect","summary":"These issues and PRs address a bug where calling connect() in Slack Socket Mode leaves previous handlers or tasks open, creating zombie connections. Related work also covers hardening socket recovery and deduplicating restart behavior on Windows.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":19181,"merged_at":"2026-05-03T10:47:49+00:00"},"resolution":{"state":"resolved","by":"pr-19181"},"freshness":{"state":"verified","last_verified_at":"2026-05-03T10:47:49+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":18980,"pr_number":19181,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-05-03T10:47:49+00:00"}],"members":[{"kind":"issue","number":18980,"title":"fix(slack): close previous handler/task in connect() to prevent zombie Socket Mode connections","url":null,"state":"closed"},{"kind":"pr","number":18982,"title":"fix(slack): close previous handler in connect() to prevent zombie Socket Mode connections","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#19181","number":19181}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":19181,"title":"fix(slack): close previous handler in connect() to prevent zombie Socket Mode connections","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/platforms/slack.py","scripts/release.py","tests/gateway/test_slack.py"],"fix_hunks":[{"file":"gateway/platforms/slack.py","hunks":["@@ -528,6 +528,21 @@ async def connect(self) -> bool:\n                 return False\n             lock_acquired = True\n \n+            # Close any previous handler before creating a new one so that\n+            # calling connect() a second time (e.g. during a gateway restart or\n+            # in-process reconnect attempt) does not leave a zombie Socket Mode\n+            # connection alive.  Both the old and new connections would otherwise\n+            # receive every Slack event and dispatch it twice, producing double\n+            # responses — the same bug that affected DiscordAdapter (#18187).\n+            if self._handler is not None:\n+                try:\n+                    await self._handler.close_async()\n+                except Exception:\n+                    logger.debug(\"[%s] Failed to close previous Slack handler\", self.name)\n+                finally:\n+                    self._handler = None\n+                    self._app = None\n+\n             # First token is the primary — used for AsyncApp / Socket Mode\n             primary_token = [redacted:secret-assignment]\n             self._app = AsyncApp(token=[redacted:secret-assignment]"]},{"file":"scripts/release.py","hunks":["@@ -506,6 +506,7 @@\n     \"[redacted:email]\": \"gnanam1990\",\n     \"[redacted:email]\": \"0xyg3n\",\n     \"7093928+0xyg3n@users.noreply.github.com\": \"0xyg3n\",\n+    \"[redacted:email]\": \"nftpoetrist\",  # PR #18982\n     \"[redacted:email]\": \"Hypn0sis\",\n     \"[redacted:email]\": \"OwenYWT\",\n     \"[redacted:email]\": \"v1k22\","]},{"file":"tests/gateway/test_slack.py","hunks":["@@ -231,6 +231,55 @@ async def test_releases_platform_lock_when_auth_fails(self):\n         mock_release.assert_called_once_with(\"slack-app-token\", \"xapp-fake\")\n         assert adapter._platform_lock_identity is None\n \n+    @pytest.mark.asyncio\n+    async def test_reconnect_closes_previous_handler_to_prevent_zombie_socket(self):\n+        \"\"\"Regression for #18980: calling connect() on an adapter that already has\n+        a live handler (e.g. during a gateway restart) must close the old\n+        AsyncSocketModeHandler before creating a new one.  Without this guard,\n+        the old Socket Mode websocket stays alive and both connections dispatch\n+        every Slack event, producing double responses — the same bug that\n+        affected DiscordAdapter (#18187).\n+        \"\"\"\n+        config = PlatformConfig(enabled=True, token=\"[redacted:secret-assignment]\")\n+        adapter = SlackAdapter(config)\n+\n+        # Simulate state left over from a prior connect() call.\n+        first_handler = AsyncMock()\n+        first_handler.close_async = AsyncMock()\n+        adapter._handler = first_handler\n+\n+        mock_app = MagicMock()\n+        def _noop_decorator(event_type):\n+            def decorator(fn): return fn\n+            return decorator\n+        mock_app.event = _noop_decorator\n+        mock_app.command = _noop_decorator\n+        mock_app.action = _noop_decorator\n+        mock_app.client = AsyncMock()\n+\n+        mock_web_client = AsyncMock()\n+        mock_web_client.auth_test = AsyncMock(return_value={\n+            \"user_id\": \"U_BOT\",\n+            \"user\": \"testbot\",\n+            \"team_id\": \"T_FAKE\",\n+            \"team\": \"FakeTeam\",\n+        })\n+\n+        second_handler = MagicMock()\n+\n+        with patch.object(_slack_mod, \"AsyncApp\", return_value=mock_app), \\\n+             patch.object(_slack_mod, \"AsyncWebClient\", return_value=mock_web_client), \\\n+             patch.object(_slack_mod, \"AsyncSocketModeHandler\", return_value=second_handler), \\\n+             patch.dict(os.environ, {\"SLACK_APP_TOKEN\": \"[redacted:secret-assignment]\"}), \\\n+             patch(\"gateway.status.acquire_scoped_lock\", return_value=(True, None)), \\\n+             patch(\"gateway.status.release_scoped_lock\"), \\\n+             patch(\"asyncio.create_task\"):\n+            result = await adapter.connect()\n+\n+        assert result is True\n+        first_handler.close_async.assert_awaited_once_with()\n+        assert adapter._handler is second_handler\n+\n \n # ---------------------------------------------------------------------------\n # TestSlackProxyBehavior\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}