Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MAINTENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
24 changes: 24 additions & 0 deletions maintenance/slack-status-legacy.md
Original file line number Diff line number Diff line change
@@ -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.
25 changes: 19 additions & 6 deletions plugins/platforms/slack/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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


Expand Down
26 changes: 16 additions & 10 deletions tests/gateway/test_slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down
Loading