Skip to content

feat(gateway): Discord tool progress bubbles auto-delete when job is done - #18306

Closed
nazirulhafiy wants to merge 23 commits into
NousResearch:mainfrom
nazirulhafiy:feat/delete-progress-bubble
Closed

feat(gateway): Discord tool progress bubbles auto-delete when job is done#18306
nazirulhafiy wants to merge 23 commits into
NousResearch:mainfrom
nazirulhafiy:feat/delete-progress-bubble

Conversation

@nazirulhafiy

@nazirulhafiy nazirulhafiy commented May 1, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Discord progress bubbles — the ephemeral messages showing tool icons (💻, 🐍, 📖, 🖼️, 🔍, 🌐, etc.) — were left behind after each tool finished executing. Over time, channels accumulated orphan bubbles that served no purpose once their job was done.

Root cause: The progress message system in send_progress_messages() edited bubbles to show progress but did not reliably delete them on completion, interruption, restart/drain, or final cleanup paths.

Fix: Add Discord message deletion support and wire progress-bubble cleanup through the gateway progress loop. Cleanup now covers normal completion, reset/interruption handling, final edit drains, and persisted restart-survivable cleanup. Deletion is capability-gated so only adapters that implement delete_message() are affected. Controlled by display.auto_delete_tool_progress (enabled by default).

Related Issue

N/A. Discovered during real-world Discord usage — channels accumulated stale progress bubbles after every tool call.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

Primary progress-bubble changes:

  • gateway/platforms/discord.py (+19/-0): Added delete_message() method that fetches and deletes a message by ID. Handles both channel and thread messages. Failures are non-fatal.
  • gateway/run.py (+243/-3): Wired auto-delete into the progress message lifecycle, including:
    • normal completion
    • __reset__ / interruption paths
    • final edit drain
    • persisted cleanup for bubbles that need deletion after restart/drain recovery
    • independent delete capability gating so deletion is not blocked by edit capability state
  • tests/gateway/test_progress_auto_delete.py (+169/-0): Added coverage for capability checks, adapter behavior, config resolution, cleanup persistence, and progress bubble deletion paths.
  • tests/gateway/test_run_cleanup_progress.py (+13/-0): Synced cleanup tests with the global display.auto_delete_tool_progress config source.

CI/test stabilization added while rebasing onto current main:

  • tests/e2e/conftest.py (+8/-0): Replaced Discord exception mocks with real Exception subclasses so Python 3.11+ except discord.Forbidden paths work under tests.
  • tests/agent/test_context_compressor_summary_continuity.py (+3/-2): Kept the handoff fixture above the compression threshold after merging current main.
  • tests/run_agent/test_compression_feasibility.py (+1/-0): Added missing _custom_providers fixture state expected by current compression logic.
  • agent/auxiliary_client.py (+1/-1), tests/agent/test_auxiliary_client.py (+1/-0), run_agent.py (+1/-0): Small test/cleanup fixes needed to keep current main’s auxiliary-client and background-review tests isolated and green.

How to Test

Manual verification:

  1. Configure Hermes with a Discord gateway profile.
  2. Send any message that triggers tools, e.g. what time is it in Tokyo.
  3. Observe that each progress bubble (💻, 🐍, 🔍, 🌐, 📖, etc.) is deleted after its tool completes.
  4. Set display.auto_delete_tool_progress: false in config.yaml and verify progress bubbles persist (old behavior).

Targeted tests:

pytest tests/gateway/test_progress_auto_delete.py -v
pytest tests/gateway/test_run_cleanup_progress.py -q
pytest tests/e2e -q
pytest tests/agent/test_auxiliary_client.py tests/run_agent/test_provider_parity.py tests/run_agent/test_background_review.py -q

Checklist

Code

Documentation & Housekeeping

  • N/A — feature flag is self-documenting via display.auto_delete_tool_progress config key.
  • N/A — no new user-facing config schema beyond the runtime flag.
  • N/A — no workflow or architecture changes.
  • Cross-platform impact considered: delete_message only fires on platforms whose adapter overrides it. No impact on unsupported platforms.
  • N/A — tool schemas unchanged.

Verification Evidence

Production-verified during real Discord sessions — progress bubbles auto-delete after tool completion. Tested with terminal (💻), code (🐍), search (🔍), web (🌐), and read (📖) tools.

Config override works as expected: display.auto_delete_tool_progress: false preserves old orphan-bubble behavior.

Latest PR GitHub Actions status after rebasing onto current main:

test: passed in 14m37s
e2e: passed in 51s
Windows footguns (blocking): passed
ruff + ty diff: passed
ruff enforcement (blocking): passed
nix (macos-latest): passed
nix (ubuntu-latest): passed
build-amd64: passed
build-arm64: passed
check-attribution: passed
supply-chain scan: passed

@alt-glitch alt-glitch added type/feature New feature or request comp/gateway Gateway runner, session dispatch, delivery platform/discord Discord bot adapter P3 Low — cosmetic, nice to have labels May 1, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Likely duplicate of #14877 — same feature (auto-delete progress bubbles on Discord), same approach (delete_message + reset handler wiring). Also related to #4767 and #4882.

@alt-glitch

Copy link
Copy Markdown
Collaborator

Likely duplicate of #14877 — same feature (auto-delete progress bubbles on Discord), same approach (delete_message + reset handler wiring).

@nazirulhafiy
nazirulhafiy force-pushed the feat/delete-progress-bubble branch 2 times, most recently from 089d667 to 8d64179 Compare May 2, 2026 16:46
@nazirulhafiy nazirulhafiy changed the title feat(gateway): auto-delete tool progress bubbles on Discord feat(gateway): Discord tool progress bubbles auto-delete when job is done May 2, 2026
@nazirulhafiy
nazirulhafiy force-pushed the feat/delete-progress-bubble branch 2 times, most recently from 3d74d6c to a103414 Compare May 5, 2026 00:35
Adds delete_message() to the Discord adapter and wires it into both
__reset__ handlers in the progress message loop, plus the final edit
drain path. When a content bubble lands after a tool batch, the
progress bubble (🔧, 📖, 💻) is now deleted instead of left orphaned
in the channel, reducing chat clutter.

- gateway/platforms/discord.py: new delete_message() method
- gateway/run.py: delete in normal __reset__, CancelledError drain,
  and final-edit path — with capability gate via
  'type(adapter).delete_message is not BasePlatformAdapter.delete_message'
…o-delete

- Final-drain path (3rd location) now logs delete attempts and errors,
  matching the __reset__ handlers — so lingering bubbles are diagnosable.
- Adds config key `display.auto_delete_tool_progress` (default: true).
  Set to `false` in config.yaml to disable progress bubble auto-deletion.
- All three delete locations respect the toggle.
…eanup

Bug: The __reset__ delete handler gated deletion on can_edit,
which flips to False on any edit failure (e.g. Discord 429 rate
limit on rapid tool calls). Once can_edit went False, every
subsequent progress bubble became permanent.

Fix:
- Add can_delete flag alongside can_edit
- Use can_delete in the __reset__ delete gate instead of can_edit
- Track can_delete = False only when delete_message() actually
  fails, not when edit fails
- Apply same failure tracking to CancelledError drain and final
  drain delete paths

This ensures Discord rate-limiting an edit mid-session no longer
disables progress bubble deletion.
…o-delete

Adds 8 tests covering:
- Capability check: overridden vs base adapter delete_message detection
- Adapter behavior: success, failure, and multiple sequential calls
- Config resolution: auto_delete_tool_progress default, explicit off,
  and per-platform overrides via display_config

Pragmatic approach: tests verify the component pieces directly
(capability checks, adapter contract, config resolver) rather than
requiring a full _run_agent integration test, since the delete code
path requires an active Discord adapter + live agent loop that doesn't
map cleanly to the mock-based test harness.

Part of PR #18306.
…nterrupt

Bug: When the agent finishes normally (not interrupted/cancelled),
_run_still_current() returned False and send_progress_messages()
exited via early return at the top of the main loop body. This
bypassed the CancelledError handler where the delete calls live,
so tool progress bubbles (e.g. ⏰ cronjob) lingered permanently
in Discord.

Fix: Added delete logic at the early-return site (normal completion)
matching the existing pattern in the CancelledError drain handler.
All three exit paths now clean up: normal completion, __reset__
drain, and final-drain.

Part of PR #18306.
- Add delete to first _run_still_current() check (race: run completes
  before __reset__ signal arrives). Tagged (run-completed).
- Move __reset__ delete outside can_edit gate (Discord supports
  delete even when edit fails). Fixes nested-gate bug May 2026.
@nazirulhafiy
nazirulhafiy force-pushed the feat/delete-progress-bubble branch from a103414 to 9838993 Compare May 5, 2026 00:51
@nazirulhafiy

Copy link
Copy Markdown
Contributor Author

Restart-resilient progress bubble cleanup

This PR introduced auto-delete for tool progress bubbles. Here is an extension that fixes the restart gap:

Problem: If the gateway restarts mid-session, the cleanup hook never runs → stale progress bubbles persist in Discord.

Fix: Persist the pending cleanup queue to ~/.hermes/gateway/pending_progress_cleanup.json at bubble creation time. On gateway startup, drain stale records before processing new messages.

  • Record (platform, chat_id, message_id) on bubble creation
  • Clear record on successful delete (existing 4 paths + startup drain)
  • Retry failed deletes up to 5 attempts
  • Gateway startup calls _drain_pending_progress_cleanups() after adapters connect

Commit on nazirulhafiy/hermes-agent@hermies/patches:
173533683 gateway: persist progress bubble cleanup across restarts

Tests added in tests/gateway/test_progress_auto_delete.py (11/11 passing).

The restart-drain logic was verified live: startup log showed 404 Not Found: Unknown Message for a stale bubble — drain ran, bubble was already gone, confirming the code path executes correctly.

nazirulhafiy and others added 10 commits May 7, 2026 17:14
- Add pending_progress_cleanup.json queue at ~/.hermes/gateway/
- Record (platform, chat_id, message_id) on bubble creation
- Clear record on successful delete (4 existing paths + startup drain)
- Retry failed deletes up to 5 attempts
- GatewayRunner.start() drains stale records after adapter connect
- Add TestPersistedProgressCleanup (3 tests: round-trip, startup success,
  startup failure retries)

Extends PR #18306 (auto-delete tool progress bubbles).
hermes config set model.aliases.xxx commands write to the model.aliases
nested key, but _load_direct_aliases() only read from the top-level
model_aliases key. This meant aliases set via hermes config set were
invisible to the /model command, and unrecognised inputs fell through
to the DeepSeek normaliser which mapped everything to deepseek-chat.

Add a second pass in _load_direct_aliases() that reads model.aliases
and converts string-value entries (provider/model format) into
DirectAlias objects. The provider is parsed from the slash prefix;
if no slash, the current default provider from config is used.

Also prevent simple aliases from overriding explicit model_aliases
dict entries when both exist.
The Telegram/Discord /model pickers currently call
list_authenticated_providers(), which returns every provider whose
credentials resolve locally and every model in its curated snapshot.
Two failure modes fall out:

- OpenRouter rows can include IDs the live catalog no longer carries.
- Provider rows can surface with zero callable models (e.g. a slug
  whose credential pool entry exists but has nothing behind it).

list_picker_providers() wraps the base function and post-processes the
result so the interactive picker only shows models the user can
actually select:

- OpenRouter's models come from fetch_openrouter_models() (live-catalog
  filtered against the curated OPENROUTER_MODELS snapshot).
- Rows with an empty models list are dropped, except custom endpoints
  (is_user_defined=True with an api_url) where the user may enter
  model ids manually.
- All other fields pass through unchanged.

The gateway /model handler switches to the new helper for the
interactive picker payload only. Typed /model <name> and the text
fallback list stay on list_authenticated_providers() so nothing is
hidden from power users or platforms without a picker.

Covered by nine focused unit tests in
tests/hermes_cli/test_list_picker_providers.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ative providers (#20802)

OpenCode Go and OpenCode Zen are flat-namespace model resellers — their
/v1/models returns bare IDs (deepseek-v4-flash, minimax-m2.7), and the
inference API rejects vendor-prefixed names with HTTP 401 'Model not
supported'. Two bugs fixed:

1. `switch_model` in hermes_cli/model_switch.py was silently switching the
   user off opencode-go to native deepseek when they typed
   `/model deepseek-v4-flash`. Step d found the model in opencode-go's live
   catalog, but step e (detect_provider_for_model) still ran and matched
   the bare name against deepseek's static catalog. Fix: track whether
   the live catalog resolved it; skip step e when it did.

2. `normalize_model_for_provider` in hermes_cli/model_normalize.py only
   stripped the exact `opencode-zen/` prefix, leaving arbitrary vendor
   prefixes like `minimax/minimax-m2.7` (commonly copied from aggregator
   slugs into fallback_model configs) intact — causing HTTP 401s when
   the fallback chain activated. Fix: opencode-go/opencode-zen strip ANY
   leading vendor prefix because their APIs are flat-namespace.

Tests: 11 new cases in tests/hermes_cli/test_opencode_go_flat_namespace.py
covering both normalization (prefix stripping, regression guards for
opencode-zen Claude hyphenation and openrouter vendor-prepending) and
switch_model (bare-name resolution on opencode-go's live catalog must
not trigger cross-provider hijack).

Reported by @UFOnik via Discord; Kimi K2.6 always worked because moonshotai
has no overlapping entry in a native provider's static catalog. Deepseek
and minimax failed because their v4/v2.7 names existed in the native
deepseek/minimax catalogs.
…ential resolution

When no explicit delegation.model/provider is configured, _resolve_delegation_credentials()
now consults smart-route's resolve_child_model() to pick the correct child model from
the parent's model family (e.g. deepseek-v4-pro → deepseek-v4-flash via opencode-go).

Graceful degradation: if smart-route plugin is not loaded (CLI/cron context), falls back
to inheriting parent's exact model. Explicit delegation config still takes priority.
…ion credential resolution"

This reverts commit 7132caa.
…t fixture

The test _install_fakes only mocked _load_gateway_config (which controls
_cleanup_progress), but the progress consumer also reads
auto_delete_tool_progress from a separate config source
(hermes_cli.config.load_config). This defaults to True, causing progress
bubbles to be deleted even when cleanup_on=False.

Monkeypatch hermes_cli.config.load_config to return
auto_delete_tool_progress matching the test's cleanup_on parameter.
loop.add_signal_handler is wrapped in try/except NotImplementedError on
line 245-250, but the footgun checker flags it by line. Suppress with
inline comment.
Python 3.11+ raises TypeError when code tries to catch a MagicMock
in an except clause (magic mocks don't inherit from BaseException).
The main branch's new _fetch_channel_context uses 'except discord.Forbidden'
which triggers this on CI where discord is mocked.

Define Forbidden, HTTPException, NotFound, DiscordException, and
InvalidArgument as real Exception subclasses so except clauses work
in mocked environments.
Summary of fixes:
- test_plugin_discovery: bump profile count 33→34 (google-gemini-cli added)
- test_context_compressor_summary_continuity: move handoff message
  after protected head region so _find_latest_context_summary finds it
- test_update_autostash: mock _refresh_active_lazy_features to prevent
  lazy backend pip installs from polluting the command assertion
- test_compression_feasibility: add _custom_providers=[] to _make_agent
  and update 3 mock_ctx_len assertions to include custom_providers=[]
@nazirulhafiy

Copy link
Copy Markdown
Contributor Author

Closing in favor of the cleaned replacement PR: #26055\n\nThat branch was rebuilt to remove unrelated history and keep the change focused on Discord tool-progress bubble cleanup.

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 P3 Low — cosmetic, nice to have platform/discord Discord bot adapter type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants