NousResearch/hermes-agent

Prevent zombie Slack Socket Mode connections on reconnect

Cluster 18980. Data as of

Nothing open on GitHub. Fix merged in #19181 on 3 May 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #19181. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #19181. That is separate from the fix event.
Still open
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.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #19181 fix(slack): close previous handler in connect() to prevent zombie Socket Mode connections

    merged to main

    files: gateway/platforms/slack.py, scripts/release.py, tests/gateway/test_slack.py

    Show the diff
    • gateway/platforms/slack.py
      @@ -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
      @@ -506,6 +506,7 @@
           "[redacted:email]": "gnanam1990",
           "[redacted:email]": "0xyg3n",
           "[email protected]": "0xyg3n",
      +    "[redacted:email]": "nftpoetrist",  # PR #18982
           "[redacted:email]": "Hypn0sis",
           "[redacted:email]": "OwenYWT",
           "[redacted:email]": "v1k22",
    • tests/gateway/test_slack.py
      @@ -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

1 PR closed without merging

From the close comments: merged via another change 1

  • #18982 fix(slack): close previous handler in connect() to prevent zombie Socket Mode connections · merged via another change → #19181
Closed issues · 1
  • Closed#18980fix(slack): close previous handler/task in connect() to prevent zombie Socket Mode connections
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Merged PRs
#19181
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
3 May 2026

For your agent

The same facts, typed.