diff --git a/MAINTENANCE.md b/MAINTENANCE.md index 3b3971ba714c6..7018239e7ec0e 100644 --- a/MAINTENANCE.md +++ b/MAINTENANCE.md @@ -82,6 +82,7 @@ as `0xble/hermes-agent-archived`; its history is not the replacement's baseline. | Camofox accounts and vault | Preserve named accounts, Connect/secret-safe fills, shadow-DOM login forms | Browser account, vault, or 1Password backend changes | [Camofox and vault](maintenance/camofox-vault.md) | | Camofox navigation titles | Return the exact owned tab's title without crossing account boundaries | Camofox navigation or tab-list changes | [Camofox navigation titles](maintenance/camofox-navigation-titles.md) | | Browser upload | Attach local files to the page's upload control, including cross-origin iframes, with safe path checks and staging | Camofox tab actions, upload route, or `uploads_dir` changes | [Browser upload](maintenance/browser-upload.md) | +| Slack status on the legacy API | Thread status and its clear stay on `assistant.threads.setStatus`; titles may use Agent Sessions | Slack status/title calls or upstream Agent Sessions changes | [Slack status legacy](maintenance/slack-status-legacy.md) | | Agent secret entry | The agent may type a self-fetched password when the vault has no item for the origin, and a self-fetched code when the vault cannot mint one; values shown on a page or in chat never count | Vault tool descriptions, the browser input vault note, or upstream vault prompt changes | [Agent secret entry](maintenance/agent-secret-entry.md) | | Security guidance plugin | Keep bounded path-aware security pattern guidance and explicit warning/block semantics | Security-guidance pattern, plugin wiring, or focused-test changes | [Security guidance plugin](maintenance/security-guidance.md) | | Essential skill opt-out | Let one home opt out of seeding and protecting the essential `hermes-agent` skill | Bundled or essential skill seeding, disabled-list, or delete-guard changes | [Essential skill opt-out](maintenance/essential-skill-opt-out.md) | diff --git a/maintenance/slack-status-legacy.md b/maintenance/slack-status-legacy.md new file mode 100644 index 0000000000000..93d1b1b658bd0 --- /dev/null +++ b/maintenance/slack-status-legacy.md @@ -0,0 +1,24 @@ +# Slack status on the legacy API + +Load this unit when changing Slack thread status or title calls (`_session_status_method`, `_session_title_method`, `send_typing`, `stop_typing`), or when syncing an upstream change to Slack's Agent Sessions support. + +## Required behavior + +Slack thread status (`is thinking...`, live tool phrases, the elapsed-time heartbeat, `typing_status_text`) and its empty-string clear go through `assistant.threads.setStatus`, even when the installed slack-sdk exposes `agents_sessions_setStatus`. Thread titles still use `agents.sessions.rename` when available. + +`agents.sessions.setStatus` accepts only `active`, `processing`, `suspended` or `closed`. It rejects every free-text status and the empty clear, and it does not clear when the app replies. Hermes debug-logs status failures, so routing free text there hides the indicator with no visible error. + +## Provenance and patches + +- Fork patch identity: `slack-status-legacy`. +- Cherry-picked from upstream [NousResearch/hermes-agent#110391](https://github.com/NousResearch/hermes-agent/pull/110391) by Baris Sencan, authorship preserved. Upstream issue: [#110374](https://github.com/NousResearch/hermes-agent/issues/110374). +- Introduced by upstream `a5522f69c036`, which routed status through Agent Sessions whenever slack-sdk 3.44+ is installed. The fork's slack-sdk 3.44.1 bump activated it, and LPG's Io showed no Slack indicator from then on. +- Alternative upstream fix [#123457](https://github.com/NousResearch/hermes-agent/pull/123457) keeps Agent Sessions and maps to `processing`/`active`. That loses custom status text and needs an explicit `active` on every exit path, so it is a product decision rather than a defect repair. + +## Verification + +`scripts/run_tests.sh tests/gateway/test_slack.py` (`TestAgentSessionsApiRouting`). Live check: mention the bot in a Slack channel thread and confirm the status appears during the turn and clears after the reply. + +## Retirement and rollback + +Retire when upstream merges #110391, #123457 or an equivalent fix that stops sending free text to `agents.sessions.setStatus`. Before Slack's February 2027 removal of `assistant.threads.setStatus`, migrate to the lifecycle enum deliberately. To roll back, revert the cherry-picked commit and this unit. diff --git a/plugins/platforms/slack/adapter.py b/plugins/platforms/slack/adapter.py index 6d6af7566c814..052ac8f10853a 100644 --- a/plugins/platforms/slack/adapter.py +++ b/plugins/platforms/slack/adapter.py @@ -265,7 +265,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 @@ -281,11 +283,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 fe2c8ed13e477..f41823e03b61d 100644 --- a/tests/gateway/test_slack.py +++ b/tests/gateway/test_slack.py @@ -5631,9 +5631,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): @@ -5644,35 +5646,39 @@ 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_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):