with one click
hermes-slack-thread-context-debugging
// Diagnose why Hermes cannot read Slack thread root messages, especially AWS Chatbot / Amazon Q / CloudWatch alert roots delivered as bot_message attachments or blocks.
// Diagnose why Hermes cannot read Slack thread root messages, especially AWS Chatbot / Amazon Q / CloudWatch alert roots delivered as bot_message attachments or blocks.
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | hermes-slack-thread-context-debugging |
| description | Diagnose why Hermes cannot read Slack thread root messages, especially AWS Chatbot / Amazon Q / CloudWatch alert roots delivered as bot_message attachments or blocks. |
| version | 1.0.0 |
| author | Hermes Agent |
| license | MIT |
| metadata | {"hermes":{"tags":["hermes","slack","gateway","debugging","aws-chatbot","amazon-q","cloudwatch","attachments","thread-context"]}} |
Use this when Hermes is invoked inside a Slack thread but seems unable to see or reason over the thread root message.
Especially relevant when the root message was posted by:
attachments or blocksIf Hermes can reply in the thread but says it cannot see the root message, the failure is usually not missing LLM skill or missing reasoning. It is usually an ingress/gateway parsing problem in the Slack adapter.
There are 3 common causes in Hermes:
Bot messages are filtered out on ingress
gateway/platforms/slack.py_handle_slack_message(), bot senders are ignored unless allow_bots permits them.Thread context fetch skips bot messages
gateway/platforms/slack.py_fetch_thread_context(), fetched thread messages with bot_id or subtype == "bot_message" are skipped.Attachment/block text is not extracted
text.attachments[] or blocks[], not in text.gateway/platforms/slack.py
_handle_slack_message()_fetch_thread_context()event.get("files", [])tests/gateway/test_slack.pytests/gateway/test_slack_approval_buttons.pywebsite/docs/user-guide/messaging/slack.mdConfirm Hermes already calls conversations.replies
_fetch_thread_context and conversations_repliesConfirm the root message is bot-authored
bot_id or subtype == "bot_message"Confirm the real content is in attachments or blocks
text is empty/shortattachments[].title/text/fields/fallbackblocks[]Check current filtering behavior
_fetch_thread_context(), if all bot messages are skipped, root context will never reach the model.Separate:
For AWS Chatbot-style alerts, you usually want:
In _fetch_thread_context(), replace blanket bot filtering:
if msg.get("bot_id") or msg.get("subtype") == "bot_message":
continue
with logic that only suppresses Hermes-authored messages.
Pattern:
def _is_own_bot_message(self, msg: dict, team_id: str) -> bool:
bot_uid = self._team_bot_user_ids.get(team_id, self._bot_user_id)
if bot_uid and msg.get("user") == bot_uid:
return True
return False
Then:
if self._is_own_bot_message(msg, team_id):
continue
Create a helper such as:
def _extract_slack_message_text(self, msg: dict) -> str:
parts = []
text = (msg.get("text") or "").strip()
if text:
parts.append(text)
for att in msg.get("attachments", []) or []:
for key in ("pretext", "title", "text", "footer", "fallback"):
value = (att.get(key) or "").strip()
if value:
parts.append(value)
for field in att.get("fields", []) or []:
title = (field.get("title") or "").strip()
value = (field.get("value") or "").strip()
if title and value:
parts.append(f"{title}: {value}")
elif value:
parts.append(value)
parts.extend(self._extract_text_from_blocks(msg.get("blocks", []) or []))
seen = set()
cleaned = []
for p in parts:
p = re.sub(r"\s+", " ", p).strip()
if p and p not in seen:
seen.add(p)
cleaned.append(p)
return "\n".join(cleaned).strip()
Minimum block support:
section.text.textsection.fields[*].textheader.text.textcontext.elements[*].textrich_text recursive flatteningimage.alt_textSlack conversations.replies can return root/parent image attachments in messages[0].files, while the triggering reply event has no files. If these are only rendered as text, gateway vision enrichment never runs.
Recommended pattern:
# _fetch_thread_context stores both formatted text and cached media paths
_ThreadContextCache(
content=content,
media_urls=context_media_urls,
media_types=context_media_types,
)
In _handle_slack_message(), after _fetch_thread_context(...), merge the cached thread-context media into the current MessageEvent.media_urls/media_types before constructing the event. This lets gateway.run._prepare_inbound_message_text() auto-run vision on images attached to the thread parent/root.
Also add a textual marker such as [attached image: filename.jpg] to the thread context, so the model can tie the vision description back to the parent message.
Add a regression test where:
filesconversations.replies returns a parent/root message with files[].url_private_download_download_slack_file is awaitedMessageEvent.media_urls contains the cached parent image path_fetch_thread_context()Replace:
msg_text = msg.get("text", "").strip()
with:
msg_text = self._extract_slack_message_text(msg)
_handle_slack_message()Replace plain top-level text capture:
text = event.get("text", "")
with:
text = self._extract_slack_message_text(event) or event.get("text", "")
This makes inbound parsing consistent for human messages, app messages, and thread context fetches.
Even after patching and restarting the gateway, the same Slack thread may still appear unchanged if Hermes already has an active/restored session for that thread.
Reason:
_handle_slack_message() only calls _fetch_thread_context() when there is no active session for the threadPractical implication:
[Thread context — prior messages ...] was injectedAdditional pitfall discovered in Hermes:
SessionStore.suspend_recently_active() can mark a thread session suspended=Trueget_or_create_session() will auto-reset that session key to a fresh session_idsession_key in _entries, it may wrongly treat the thread as already warm and skip _fetch_thread_context()Concrete verification clues:
~/.hermes/logs/agent.log
[Thread context — prior messages ...], the fetch path definitely worked at least once_fetch_thread_context() likely did not run on those later turnsSuspended N in-flight session(s) from previous run, assume session restore may mask the fix~/.hermes/sessions/sessions.json
session_id is currently bound to the Slack session_key (agent:main:slack:group:<channel>:<thread_ts>)If the post-restart session transcript lacks the [Thread context ...] preamble, that usually means the fetch path did not run for that turn.
A skill only helps after the relevant content has already entered the prompt. If the Slack adapter discards the root message or fails to extract attachment text, no skill can recover it.
SLACK_ALLOW_BOTS=allSLACK_ALLOW_BOTS=all affects trigger acceptance in _handle_slack_message().
It does not fix _fetch_thread_context() skipping bot messages, and does not parse attachments/blocks.
Use SLACK_ALLOW_BOTS only if you intentionally want Hermes to process bot-authored messages as primary inbound events.
If you want conservative defaults, add flags such as:
platforms:
slack:
extra:
include_external_bot_thread_context: true
parse_attachments_in_thread_context: true
Default recommendation for AWS-alert-heavy Slack workspaces:
include_external_bot_thread_context: trueAWS Chatbot-like thread root
subtype = "bot_message"text empty or shortattachments[].title/text/fieldsHermes own previous replies are excluded
Attachment-only message with empty text
Regression: existing human-only thread behavior remains unchanged
search_files("_fetch_thread_context", path="gateway/platforms", file_glob="slack.py")read_file("gateway/platforms/slack.py", ...)subtype == "bot_message"msg.get("attachments")msg.get("blocks")conversations_repliesIf you must patch Hermes locally before upstreaming:
fix/slack-thread-root-contextmaingit format-patch -1 HEAD --stdout > ~/.hermes/patches/<name>.patchWhy this matters:
~/.hermes/hermes-agentmainPractical recovery options after hermes update:
git checkout <hotfix-branch>git cherry-pick <commit>git apply ~/.hermes/patches/<name>.patchIf a human asks Hermes to investigate a CloudWatch/AWS Chatbot alert inside a Slack thread and Hermes cannot see the root message, the most likely fix is:
That is a gateway adapter fix, not a prompt/skill fix.