Skip to content

fix(discord): send tool-using responses (api_calls>=2)#33842

Closed
sweetcornna wants to merge 1 commit into
NousResearch:mainfrom
sweetcornna:fix/29346-discord-tool-response-drop
Closed

fix(discord): send tool-using responses (api_calls>=2)#33842
sweetcornna wants to merge 1 commit into
NousResearch:mainfrom
sweetcornna:fix/29346-discord-tool-response-drop

Conversation

@sweetcornna

@sweetcornna sweetcornna commented May 28, 2026

Copy link
Copy Markdown
Contributor

Problem

On Discord, tool-using replies (api_calls >= 2) were silently dropped. The log showed response ready: ... api_calls=2 response=NNNN chars and then nothing — no Sending response line, no error — and the answer never reached the channel. Single-call replies (api_calls == 1) were largely unaffected because their typically smaller content rarely tripped the strip path.

Root cause

In BasePlatformAdapter._process_message_background (gateway/platforms/base.py), the response passes through an extract pipeline: extract_media -> extract_images -> inline-directive strips -> extract_local_files. When that pipeline reduced a non-empty reply to "" while producing no native attachment (no image / media / local file), the downstream if text_content: send guard was skipped, so the message was dropped with nothing logged.

Fix

Recover the stripped reply on Discord and make the drop observable.

  • Snapshot the pre-extract response into _response_pre_extract before the pipeline runs.
  • After the extract steps, add a recovery branch gated on self.platform == Platform.DISCORD and not text_content and not images and not local_files and not media_files and _response_pre_extract.strip().
  • When that branch fires, sanitize the snapshot — remove [[audio_as_voice]], [[as_document]], and MEDIA:<path> directives (re.sub(r"MEDIA:\s*\S+", ...)) — and, if anything remains, assign it to text_content so the existing send path delivers it.
  • Emit a WARNING (Response stripped to empty after extract pipeline (orig=%d chars). Sending raw response as fallback.) including the original char count, so the silent-drop pattern is visible next time.
  • The recovery is intentionally scoped to Discord because that is the confirmed reproducer; other adapters keep current behavior. It also does nothing when a real attachment was extracted (image/media/local file), avoiding a duplicate text echo alongside the native attachment.

Testing

New regression suite tests/gateway/test_discord_tool_response_drop.py (4 cases) added in the diff. The extract helpers are monkeypatched to deterministically reduce text to empty, isolating the send-path bug from the strip heuristics:

  • test_response_reduced_to_empty_is_recovered_and_sent: a multi-hundred-char tool reply (the issue's repeated sample string, assert len > 500) stripped to empty is still sent (exactly one send) and a stripped to empty WARNING is logged.
  • test_directives_stripped_from_fallback_text: [[audio_as_voice]], [[as_document]], and MEDIA: directives are removed from the fallback text while the real answer survives.
  • test_no_fallback_when_attachment_produced: when an image attachment is extracted (empty text is intentional), no text fallback is sent, so the attachment content is not duplicated.
  • test_fallback_is_discord_scoped: a Telegram adapter with the same empty-after-strip condition sends nothing, confirming the fallback is Discord-only.

Per the PR description, the author also ran pytest tests/gateway/test_platform_base.py tests/gateway/test_base_topic_sessions.py tests/gateway/test_delivery.py tests/gateway/test_discord_tool_response_drop.py (138 passed, 2 skipped) and confirmed the recovery tests fail (0 sends) when the base.py change is reverted. These run results are reported by the author and not reproduced here.

Risk & impact

  • Behavior change is limited to Discord and only triggers on the previously-broken path (non-empty reply reduced to empty with no attachment); the normal send path is untouched.
  • Other platforms are explicitly excluded, so their delivery pipelines are unaffected.
  • The fallback re-uses the original response only after sanitizing internal directives, so directives cannot leak to users; the attachment guard prevents duplicate delivery.
  • Adds one WARNING log line when recovery fires, improving observability of this class of failure.

Closes #29346

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery platform/discord Discord bot adapter labels May 28, 2026
When the agent returns a non-empty reply that the extract pipeline
(extract_media / extract_images / extract_local_files plus the inline
directive strips) happens to reduce to an empty string, the
``if text_content:`` guard in ``_process_message_background`` bypassed
the send entirely. The symptom on Discord was a ``response ready`` log
followed by silence — no ``Sending response`` line, no error in
``errors.log``, and the final answer never reached the channel. The
failure is most visible on tool-using turns (api_calls>=2) where the
larger, more varied response content is more likely to trip an
over-eager strip path.

Preserve the pre-extract response and, when no native attachment was
produced to deliver in its place, sanitize the directive tags and send
it as a fallback so the user actually receives the answer. Log a
``WARNING`` so future occurrences of the silent-drop pattern are
observable. Scoped to Discord because that is the confirmed reproducer;
other adapters keep current behavior to avoid unintended regressions in
their delivery pipelines.

Closes NousResearch#29346
@sweetcornna sweetcornna force-pushed the fix/29346-discord-tool-response-drop branch from 40f2c28 to 34e2a6e Compare June 1, 2026 03:13
@sweetcornna

Copy link
Copy Markdown
Contributor Author

CI note — the red checks are inherited from main, not introduced by this PR.

This branch was just rebased onto the latest main to clear a merge conflict. The failing test (N) jobs reproduce on main itself and live entirely in files this PR does not touch:

  • tests/hermes_cli/test_model_catalog.py::TestManifestMatchesInRepoLists::test_in_repo_lists_match_manifestwebsite/static/api/model-catalog.json is out of sync with the in-repo model lists after a8526a415 ("bump minimax to minimax-m3"), which edited hermes_cli/models.py without regenerating the catalog. Fix on main: python scripts/build_model_catalog.py && git add website/static/api/model-catalog.json.
  • tests/hermes_cli/test_gui_command.py::test_gui_installs_packages_and_launches_desktop_app and ::test_gui_forwards_desktop_environment_overridesStopIteration, also failing on main independent of this PR.

This PR only changes gateway/platforms/base.py (Discord empty-response recovery) and its regression test, both of which pass.

Happy to rebase again once main is green.

banditburai added a commit to banditburai/hermes-agent that referenced this pull request Jun 1, 2026
NousResearch#29346)

The extract pipeline (extract_media/extract_images/extract_local_files +
directive strips) can reduce a non-empty tool-using response to empty
text_content with no deliverable attachment. The 'if text_content' send
guard then silently skips delivery: a 'response ready' log with no
'Sending response', no error, and the answer never reaches the user.

- A2: snapshot the pre-extract response; when extraction yields empty text
  and no image/local/media attachment, deliver the recovered original from
  the post-extract_media body (so a spaced MEDIA path can't leak). Applies
  on ALL platforms (supersedes the Discord-only NousResearch#33842 and the unsafe
  raw-fallback NousResearch#29499).
- A3: loud delivery invariant - a non-empty response that produces nothing
  deliverable logs response_delivery_dropped at ERROR; every recovery logs
  response_delivery_recovered. No silent drop survives.
- Factor a _strip_media_directives helper for the [[...]] strips; MEDIA
  stripping stays owned by extract_media, whose grammar handles spaced and
  quoted paths.
- Salvaged + de-scoped the NousResearch#33842 test harness to all platforms; added
  unrecoverable-drop and no-leak regression tests.
teknium1 pushed a commit that referenced this pull request Jun 2, 2026
#29346)

The extract pipeline (extract_media/extract_images/extract_local_files +
directive strips) can reduce a non-empty tool-using response to empty
text_content with no deliverable attachment. The 'if text_content' send
guard then silently skips delivery: a 'response ready' log with no
'Sending response', no error, and the answer never reaches the user.

- A2: snapshot the pre-extract response; when extraction yields empty text
  and no image/local/media attachment, deliver the recovered original from
  the post-extract_media body (so a spaced MEDIA path can't leak). Applies
  on ALL platforms (supersedes the Discord-only #33842 and the unsafe
  raw-fallback #29499).
- A3: loud delivery invariant - a non-empty response that produces nothing
  deliverable logs response_delivery_dropped at ERROR; every recovery logs
  response_delivery_recovered. No silent drop survives.
- Factor a _strip_media_directives helper for the [[...]] strips; MEDIA
  stripping stays owned by extract_media, whose grammar handles spaced and
  quoted paths.
- Salvaged + de-scoped the #33842 test harness to all platforms; added
  unrecoverable-drop and no-leak regression tests.
@sweetcornna

Copy link
Copy Markdown
Contributor Author

Closing as redundant. After rebasing onto current main, the #29346/#33842 fix already landed upstream in a superior, generalized form: main now recovers an extract-pipeline-emptied response on every platform (the in-tree comment notes "#33842 was Discord-only") via _strip_media_directives(response). After resolving the rebase, this PR base.py diff against main is empty (fully subsumed) and its remaining Discord-only test contradicts main improved all-platform behavior. Nothing left to merge.

@sweetcornna sweetcornna closed this Jun 2, 2026
changman pushed a commit to changman/hermes-agent that referenced this pull request Jun 10, 2026
NousResearch#29346)

The extract pipeline (extract_media/extract_images/extract_local_files +
directive strips) can reduce a non-empty tool-using response to empty
text_content with no deliverable attachment. The 'if text_content' send
guard then silently skips delivery: a 'response ready' log with no
'Sending response', no error, and the answer never reaches the user.

- A2: snapshot the pre-extract response; when extraction yields empty text
  and no image/local/media attachment, deliver the recovered original from
  the post-extract_media body (so a spaced MEDIA path can't leak). Applies
  on ALL platforms (supersedes the Discord-only NousResearch#33842 and the unsafe
  raw-fallback NousResearch#29499).
- A3: loud delivery invariant - a non-empty response that produces nothing
  deliverable logs response_delivery_dropped at ERROR; every recovery logs
  response_delivery_recovered. No silent drop survives.
- Factor a _strip_media_directives helper for the [[...]] strips; MEDIA
  stripping stays owned by extract_media, whose grammar handles spaced and
  quoted paths.
- Salvaged + de-scoped the NousResearch#33842 test harness to all platforms; added
  unrecoverable-drop and no-leak regression tests.
alt-glitch pushed a commit that referenced this pull request Jun 14, 2026
#29346)

The extract pipeline (extract_media/extract_images/extract_local_files +
directive strips) can reduce a non-empty tool-using response to empty
text_content with no deliverable attachment. The 'if text_content' send
guard then silently skips delivery: a 'response ready' log with no
'Sending response', no error, and the answer never reaches the user.

- A2: snapshot the pre-extract response; when extraction yields empty text
  and no image/local/media attachment, deliver the recovered original from
  the post-extract_media body (so a spaced MEDIA path can't leak). Applies
  on ALL platforms (supersedes the Discord-only #33842 and the unsafe
  raw-fallback #29499).
- A3: loud delivery invariant - a non-empty response that produces nothing
  deliverable logs response_delivery_dropped at ERROR; every recovery logs
  response_delivery_recovered. No silent drop survives.
- Factor a _strip_media_directives helper for the [[...]] strips; MEDIA
  stripping stays owned by extract_media, whose grammar handles spaced and
  quoted paths.
- Salvaged + de-scoped the #33842 test harness to all platforms; added
  unrecoverable-drop and no-leak regression tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/discord Discord bot adapter type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Discord: tool-using responses (api_calls≥2) silently dropped — no Sending response log after response ready

2 participants