mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-26 17:38:36 +00:00
fix(slack): preserve typed command integrity across enrichment paths
Commands typed in Slack could be mangled by every enrichment layer the adapter applies to normal messages: - Block Kit / unfurl / attachment-notice / text-file injection could prepend or append content around a command, moving the command token away from character zero or polluting its arguments. Commands are now restored from canonical authored input after all enrichment (final is_command_text guard before MessageEvent construction). - @bot /cmd (typed slash behind a mention) was never classified as a command; the mention-strip branch now re-probes for both slash and bang forms. - The Slack Agent-view context label ([Slack app context: ...]) was prepended to command events too; now command-exempt. - Native slash payload arguments were strip()ed, destroying meaningful spacing inside/after arguments; only the command delimiter is nonsemantic now. - Slash payload thread identity (thread_ts/message_ts, top-level or nested in message/container) is preserved onto SessionSource so session-scoped commands hit the same thread session. - /queue and /steer queued fallbacks now propagate channel_context so a command that triggered first-entry thread backfill doesn't lose the history when re-queued. Adapted from PR #66310 to the post-#69320 channel_context design (thread history already rides MessageEvent.channel_context, never text).
This commit is contained in:
parent
e7ef57f8c3
commit
c2d306c4c2
5 changed files with 279 additions and 16 deletions
|
|
@ -4079,13 +4079,19 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
# Re-run command normalization against the canonical Slack text,
|
||||
# not the block-augmented agent text. Otherwise quoted/forwarded
|
||||
# rich-text payload can become accidental command arguments.
|
||||
# Handles both ``@bot !cmd`` (bang hidden behind the mention when
|
||||
# the first probe ran) and ``@bot /cmd`` (typed slash addressed
|
||||
# at the bot).
|
||||
mention_stripped = original_text.replace(f"<@{bot_uid}>", "").strip()
|
||||
command_text = _rewrite_known_bang_command(mention_stripped)
|
||||
if command_text != mention_stripped:
|
||||
if mention_stripped.startswith("/"):
|
||||
command_text = mention_stripped
|
||||
else:
|
||||
command_text = _rewrite_known_bang_command(mention_stripped)
|
||||
if command_text.startswith("/"):
|
||||
original_text = command_text
|
||||
text = command_text
|
||||
# Refresh command classification: the bang was hidden behind
|
||||
# the leading mention when the first probe ran.
|
||||
# Refresh command classification: the command token was
|
||||
# hidden behind the leading mention on the first probe.
|
||||
command_probe_text = command_text
|
||||
is_command_text = True
|
||||
# Register this thread so all future messages auto-trigger the bot.
|
||||
|
|
@ -4491,6 +4497,16 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
else:
|
||||
msg_type = MessageType.DOCUMENT
|
||||
|
||||
# Every enrichment path above (blocks, unfurls, attachment notices,
|
||||
# text-file injection, thread history) is deliberately allowed for
|
||||
# normal messages. Commands are restored from canonical authored
|
||||
# input only: the gateway parser requires the command token at
|
||||
# character zero, and enrichment must never mutate a command's
|
||||
# arguments.
|
||||
if is_command_text:
|
||||
text = command_probe_text
|
||||
msg_type = MessageType.COMMAND
|
||||
|
||||
# Resolve user display name (cached after first lookup)
|
||||
user_name = await self._resolve_user_name(
|
||||
user_id, chat_id=channel_id, team_id=team_id
|
||||
|
|
@ -4619,7 +4635,11 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
# the user switches views and would let stale context bleed into later
|
||||
# turns. The agent receives an inert label, never a fetched channel body.
|
||||
context_channel_id = agent_context.get("context_channel_id", "")
|
||||
if context_channel_id and context_channel_id != channel_id:
|
||||
if (
|
||||
context_channel_id
|
||||
and context_channel_id != channel_id
|
||||
and msg_event.message_type != MessageType.COMMAND
|
||||
):
|
||||
msg_event.text = (
|
||||
f"[Slack app context: user is viewing channel {context_channel_id}]\n\n"
|
||||
f"{msg_event.text}"
|
||||
|
|
@ -5762,7 +5782,8 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
message).
|
||||
"""
|
||||
slash_name = (command.get("command") or "").lstrip("/").strip()
|
||||
text = command.get("text", "").strip()
|
||||
raw_text = str(command.get("text") or "")
|
||||
text = raw_text
|
||||
user_id = command.get("user_id", "")
|
||||
channel_id = command.get("channel_id", "")
|
||||
team_id = command.get("team_id", "")
|
||||
|
|
@ -5775,29 +5796,32 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
# Legacy /hermes <subcommand> [args] routing + free-form questions.
|
||||
# Empty slash_name falls into this branch for backward compat
|
||||
# with any caller that didn't populate command["command"].
|
||||
legacy_text = raw_text.strip()
|
||||
from hermes_cli.commands import slack_subcommand_map
|
||||
|
||||
subcommand_map = slack_subcommand_map()
|
||||
subcommand_map["compact"] = "/compress"
|
||||
# Guard against whitespace-only text where ``text`` is truthy but
|
||||
# ``text.split()`` returns ``[]`` (e.g. user sends ``/hermes ``).
|
||||
parts = text.split() if text else []
|
||||
parts = legacy_text.split() if legacy_text else []
|
||||
first_word = parts[0] if parts else ""
|
||||
if first_word in subcommand_map:
|
||||
rest = text[len(first_word) :].strip()
|
||||
rest = legacy_text[len(first_word) :].strip()
|
||||
text = (
|
||||
f"{subcommand_map[first_word]} {rest}".strip()
|
||||
if rest
|
||||
else subcommand_map[first_word]
|
||||
)
|
||||
elif text:
|
||||
pass # Treat as a regular question
|
||||
elif legacy_text:
|
||||
text = legacy_text # Treat as a regular question
|
||||
else:
|
||||
text = "/help"
|
||||
else:
|
||||
# Native slash — /<slash_name> [args]. Route directly through the
|
||||
# gateway command dispatcher by prepending the slash.
|
||||
text = f"/{slash_name} {text}".strip()
|
||||
# gateway command dispatcher by prepending the slash. Only the
|
||||
# command delimiter is nonsemantic: preserve Slack's raw argument
|
||||
# payload, including meaningful internal/trailing spacing.
|
||||
text = f"/{slash_name}" if not raw_text else f"/{slash_name} {raw_text}"
|
||||
|
||||
# Slack slash commands can originate from DMs or shared channels.
|
||||
# Preserve DM semantics only for DM channel IDs; shared channels must
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue