From 9b90259fbb63424e155c11093042998e2233465a Mon Sep 17 00:00:00 2001 From: Baris Sencan Date: Mon, 14 Sep 2026 00:25:07 +0100 Subject: [PATCH] fix(slack): keep the thread status indicator on assistant.threads.setStatus a5522f69c0 routed status through agents.sessions.setStatus whenever the installed slack-sdk ships the typed method (>=3.44.0). That API accepts only the closed enum active|processing|suspended|closed, while every Hermes call site still sends the legacy free-text protocol: work-start phrases, per-tool live_status updates, typing_status_text, and an empty string to clear. All of them are rejected with invalid_arguments, and because status failures are debug-logged by design the indicator vanished silently on every turn (#110374). Status now always uses the legacy method, which takes free text and auto-clears on reply until its February 2027 deprecation. Titles keep using agents.sessions.rename, which takes free text and was never affected. Not migrated to the enum on purpose: processing does not auto-clear when the app replies, so a turn ending without an explicit active would leave the spinner up for up to an hour, re-opening the stuck-indicator class fixed by #64621 / #24117; and the new API has no equivalent for custom status text, so adopting it drops live_status and typing_status_text. That trade is a product decision, not a regression fix. Co-Authored-By: Claude Opus 5 --- plugins/platforms/slack/adapter.py | 25 +++++++++++++++++++------ tests/gateway/test_slack.py | 26 ++++++++++++++++---------- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/plugins/platforms/slack/adapter.py b/plugins/platforms/slack/adapter.py index 414ac6b0ef934..273ff98f71d07 100644 --- a/plugins/platforms/slack/adapter.py +++ b/plugins/platforms/slack/adapter.py @@ -264,7 +264,9 @@ def _sdk_supports_agent_sessions() -> bool: Slack is deprecating the Assistant messaging experience in February 2027: ``assistant.threads.setStatus`` / ``assistant.threads.setTitle`` are replaced by ``agents.sessions.setStatus`` / ``agents.sessions.rename`` - (typed methods landed in slack-sdk 3.44.0). Checked on the SDK class — + (typed methods landed in slack-sdk 3.44.0). Only ``rename`` is adopted — + see ``_session_status_method`` for why status stays on the legacy method. + Checked on the SDK class — never on a client instance, where mock auto-attributes would lie. """ global _AGENT_SESSIONS_SUPPORTED @@ -280,11 +282,22 @@ def _sdk_supports_agent_sessions() -> bool: def _session_status_method(client: Any): - """Return the status setter: Agent Sessions API when available, else legacy.""" - if _sdk_supports_agent_sessions(): - method = getattr(client, "agents_sessions_setStatus", None) - if method is not None: - return method + """Return the status setter: always legacy ``assistant.threads.setStatus``. + + Deliberately NOT routed through ``agents.sessions.setStatus`` (#110374). + That API takes a closed enum (``active|processing|suspended|closed``) while + every Hermes status payload is free text — live per-tool phrases, the + elapsed-time heartbeat, ``typing_status_text`` — plus an EMPTY string to + clear. All of those are rejected with ``invalid_arguments``, and because + status failures are debug-logged the indicator vanishes silently. + + The enum also has no auto-clear (``processing`` lingers up to an hour after + the app replies), which would re-open the stuck-indicator bug class of + #64621 / #24117. The legacy method keeps free text and auto-clear until its + February 2027 deprecation; migrating means mapping lifecycle states onto the + enum and accepting the loss of custom status text, which is a product call. + Titles are unaffected — ``agents.sessions.rename`` takes free text. + """ return client.assistant_threads_setStatus diff --git a/tests/gateway/test_slack.py b/tests/gateway/test_slack.py index 76f9515e177fb..61e5faa06f951 100644 --- a/tests/gateway/test_slack.py +++ b/tests/gateway/test_slack.py @@ -5968,9 +5968,11 @@ def test_block_kit_dump_still_describes_bot_ui_blocks(self): class TestAgentSessionsApiRouting: """slack-sdk 3.44.0 Agent Sessions API (assistant_view deprecation Feb 2027). - When the installed slack-sdk ships agents.sessions.* typed methods, status - and title calls route through them; older SDKs keep using the legacy - assistant.threads.* methods (compat bridge on Slack's side). + When the installed slack-sdk ships agents.sessions.* typed methods, TITLE + calls route through agents.sessions.rename; older SDKs keep using the legacy + assistant.threads.* methods (compat bridge on Slack's side). Status always + stays on assistant.threads.setStatus — the Agent Sessions status API takes a + closed enum and rejects Hermes' free-text phrases and empty clear (#110374). """ def _adapter(self): @@ -5981,18 +5983,20 @@ def _adapter(self): return a @pytest.mark.asyncio - async def test_typing_uses_agent_sessions_when_supported(self): + async def test_typing_stays_on_legacy_even_with_sdk_support(self): + """#110374: agents.sessions.setStatus takes a closed enum, so free-text + status phrases must keep going to assistant.threads.setStatus.""" _slack_mod._AGENT_SESSIONS_SUPPORTED = True a = self._adapter() a._app.client.agents_sessions_setStatus = AsyncMock() a._app.client.assistant_threads_setStatus = AsyncMock() await a.send_typing("C123", metadata={"thread_id": "parent_ts"}) - a._app.client.agents_sessions_setStatus.assert_called_once_with( + a._app.client.assistant_threads_setStatus.assert_called_once_with( channel_id="C123", thread_ts="parent_ts", status="is thinking...", ) - a._app.client.assistant_threads_setStatus.assert_not_called() + a._app.client.agents_sessions_setStatus.assert_not_called() @pytest.mark.asyncio async def test_typing_falls_back_to_legacy_without_sdk_support(self): @@ -6007,20 +6011,22 @@ async def test_typing_falls_back_to_legacy_without_sdk_support(self): ) @pytest.mark.asyncio - async def test_stop_typing_clears_via_agent_sessions(self): + async def test_stop_typing_clears_via_legacy_empty_status(self): + """The empty-string clear is rejected by the enum API (#110374), so the + clear path stays on assistant.threads.setStatus too.""" _slack_mod._AGENT_SESSIONS_SUPPORTED = True a = self._adapter() a._app.client.agents_sessions_setStatus = AsyncMock() a._app.client.assistant_threads_setStatus = AsyncMock() await a.send_typing("C123", metadata={"thread_id": "parent_ts"}) - a._app.client.agents_sessions_setStatus.reset_mock() + a._app.client.assistant_threads_setStatus.reset_mock() await a.stop_typing("C123", metadata={"thread_id": "parent_ts"}) - a._app.client.agents_sessions_setStatus.assert_called_once_with( + a._app.client.assistant_threads_setStatus.assert_called_once_with( channel_id="C123", thread_ts="parent_ts", status="", ) - a._app.client.assistant_threads_setStatus.assert_not_called() + a._app.client.agents_sessions_setStatus.assert_not_called() @pytest.mark.asyncio async def test_thread_title_uses_agents_sessions_rename(self):