# NousResearch/hermes-agent · Prevent zombie Slack Socket Mode connections on reconnect

## Open now

- Nothing open on GitHub.

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.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #19181. Verify it addresses your case.
- **Recommended:** read PR #19181 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-19181`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #18980: fix(slack): close previous handler/task in connect() to prevent zombie Socket Mode connections

## Prior & competing PRs

- **#19181: fix(slack): close previous handler in connect() to prevent zombie Socket Mode connections**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `gateway/platforms/slack.py`, `scripts/release.py`, `tests/gateway/test_slack.py`
  - fix hunks:
  - `gateway/platforms/slack.py`

    ```diff
    @@ -528,6 +528,21 @@ async def connect(self) -> bool:
                     return False
                 lock_acquired = True
     
    +            # Close any previous handler before creating a new one so that
    +            # calling connect() a second time (e.g. during a gateway restart or
    +            # in-process reconnect attempt) does not leave a zombie Socket Mode
    +            # connection alive.  Both the old and new connections would otherwise
    +            # receive every Slack event and dispatch it twice, producing double
    +            # responses — the same bug that affected DiscordAdapter (#18187).
    +            if self._handler is not None:
    +                try:
    +                    await self._handler.close_async()
    +                except Exception:
    +                    logger.debug("[%s] Failed to close previous Slack handler", self.name)
    +                finally:
    +                    self._handler = None
    +                    self._app = None
    +
                 # First token is the primary — used for AsyncApp / Socket Mode
                 primary_token = [redacted:secret-assignment]
                 self._app = AsyncApp(token=[redacted:secret-assignment]
    ```
  - `scripts/release.py`

    ```diff
    @@ -506,6 +506,7 @@
         "[redacted:email]": "gnanam1990",
         "[redacted:email]": "0xyg3n",
         "7093928+0xyg3n@users.noreply.github.com": "0xyg3n",
    +    "[redacted:email]": "nftpoetrist",  # PR #18982
         "[redacted:email]": "Hypn0sis",
         "[redacted:email]": "OwenYWT",
         "[redacted:email]": "v1k22",
    ```
  - `tests/gateway/test_slack.py`

    ```diff
    @@ -231,6 +231,55 @@ async def test_releases_platform_lock_when_auth_fails(self):
             mock_release.assert_called_once_with("slack-app-token", "xapp-fake")
             assert adapter._platform_lock_identity is None
     
    +    @pytest.mark.asyncio
    +    async def test_reconnect_closes_previous_handler_to_prevent_zombie_socket(self):
    +        """Regression for #18980: calling connect() on an adapter that already has
    +        a live handler (e.g. during a gateway restart) must close the old
    +        AsyncSocketModeHandler before creating a new one.  Without this guard,
    +        the old Socket Mode websocket stays alive and both connections dispatch
    +        every Slack event, producing double responses — the same bug that
    +        affected DiscordAdapter (#18187).
    +        """
    +        config = PlatformConfig(enabled=True, token="[redacted:secret-assignment]")
    +        adapter = SlackAdapter(config)
    +
    +        # Simulate state left over from a prior connect() call.
    +        first_handler = AsyncMock()
    +        first_handler.close_async = AsyncMock()
    +        adapter._handler = first_handler
    +
    +        mock_app = MagicMock()
    +        def _noop_decorator(event_type):
    +            def decorator(fn): return fn
    +            return decorator
    +        mock_app.event = _noop_decorator
    +        mock_app.command = _noop_decorator
    +        mock_app.action = _noop_decorator
    +        mock_app.client = AsyncMock()
    +
    +        mock_web_client = AsyncMock()
    +        mock_web_client.auth_test = AsyncMock(return_value={
    +            "user_id": "U_BOT",
    +            "user": "testbot",
    +            "team_id": "T_FAKE",
    +            "team": "FakeTeam",
    +        })
    +
    +        second_handler = MagicMock()
    +
    +        with patch.object(_slack_mod, "AsyncApp", return_value=mock_app), \
    +             patch.object(_slack_mod, "AsyncWebClient", return_value=mock_web_client), \
    +             patch.object(_slack_mod, "AsyncSocketModeHandler", return_value=second_handler), \
    +             patch.dict(os.environ, {"SLACK_APP_TOKEN": "[redacted:secret-assignment]"}), \
    +             patch("gateway.status.acquire_scoped_lock", return_value=(True, None)), \
    +             patch("gateway.status.release_scoped_lock"), \
    +             patch("asyncio.create_task"):
    +            result = await adapter.connect()
    +
    +        assert result is True
    +        first_handler.close_async.assert_awaited_once_with()
    +        assert adapter._handler is second_handler
    +
     
     # ---------------------------------------------------------------------------
     # TestSlackProxyBehavior
    ```
- **#18982: fix(slack): close previous handler in connect() to prevent zombie Socket Mode connections**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#19181`

## Fix status

- Merged to main in PR #19181 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-03`.
