You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
After a successful webhook agent run, Hermes requests one durable handoff of the exact webhook session, creates one thread under Discord's configured home channel, and atomically moves routing ownership from the webhook key to the destination thread key. The session ID and role-aware transcript are preserved, the webhook source route is removed, and the next Discord thread event resolves to the same session.
Handoff mode is exclusive: it suppresses the legacy webhook delivery path and rejects deliver_only: true. Routes without handoff_to retain their existing behavior.
The implementation extends the existing durable handoff/session infrastructure without a database migration or new dependency. It adds durable delivery-ID idempotency, compare-and-swap routing/finalization, restart recovery, default-profile safety, and cold-relay routing priming.
#31566 is adjacent active-run clarify-routing work; this PR adds post-completion session continuation and does not claim to resolve that separate interaction path.
Companion Discord participation-tracker fix: #90348. That tracker fix is intentionally not included here. #63459 is the older, broader related Discord anchoring PR.
Overlap was checked against open and merged work. In particular, #90304 and #90236 are adjacent webhook/profile or intake work, but neither implements durable exact-session handoff.
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
gateway/platforms/webhook.py: validates the trusted handoff_to: discord route setting, suppresses legacy delivery, durably claims stable delivery IDs, requests the exact session once, and finalizes failures/cancellations atomically. Successful media-only turns are recognized through the runner's explicit success marker; pre-agent early exits cannot create empty handoff threads.
hermes_state.py and gateway/session.py: add no-migration transactional metadata claims, running-state terminal transitions, exact-owner routing CAS move/removal, compression-lineage-aware cleanup, and exact-session transcript preservation.
gateway/run.py: generalizes the durable watcher, moves webhook route ownership, reconciles cancellation during off-loop claims/moves, derives webhook destination keys from global gateway per-user settings, preserves CLI/TUI behavior, fails synthetic destination-agent errors visibly, validates default-profile continuity, and primes cold relay routing.
gateway/config.py and gateway/platforms/base.py: preserve authenticated Discord home provenance across the legacy environment overlay and generalize CLI-specific handoff wording.
cli-config.yaml.example and website/docs/user-guide/messaging/webhooks.md: document the trusted route contract, exclusive delivery, stable-header requirement, durable versus legacy deduplication, asynchronous 202 Accepted behavior, and failure visibility.
tests/gateway/test_webhook_session_handoff.py, tests/gateway/test_webhook_handoff_watcher.py, tests/gateway/test_session.py, tests/test_hermes_state.py, tests/gateway/test_config.py, tests/gateway/test_handoff_watcher_async_db.py, and tests/hermes_cli/test_session_handoff.py: add integration and concurrency coverage for success, media-only output, pre-agent early exit, duplicate delivery, exact ID/transcript movement, route ownership, failure, cancellation, invalid/missing destinations, per-user/profile routing, relay restart, compression, restart recovery, and unchanged CLI/TUI behavior.
How to Test
All commands below were run through the repository test wrapper with HOME and HERMES_HOME pointed at an isolated temporary directory.
scripts/check-windows-footguns.py passed on all six changed source files.
git diff --check upstream/main...HEAD passed.
upstream/main at a72c9ca248a051b8c7e8a69ff422c7be5066cdc4 is an ancestor of the tested head.
Total targeted verification on the published branch tip: 1,114 passed, 2 skipped, 0 failed.
A full pytest tests/ -q run was not completed in this isolated worktree: optional acp/anthropic packages are absent and the macOS process-guard tests require a different host setup. The full-suite checkbox therefore remains intentionally unchecked.
No live Discord, gateway, profile, state database, or Hermes installation was exercised or modified under the worktree-only constraint. The PR remains draft pending exact-head CI and disposable maintainer end-to-end validation.
AI code review — automated review for reference; please use your judgment.
gateway/platforms/webhook.py:on_processing_complete / _process_handoff — Maintainability flag: the runner↔adapter contract now rides on a private attribute convention (event._agent_run_failed is False distinguishing media-only false negatives from real failures). It's well-commented, but a dunder-ish named constant or small typed field on MessageEvent would keep a future rename from silently turning every media-only handoff into webhook_handoff_failed. Same for _cron_extra_prompt-style underscore keys already in flight elsewhere.
gateway/platforms/webhook.py:_handle_webhook duplicate-recovery path — Edge worth a decision: if an operator retargets a route's handoff_to (say discord → a future telegram) while providers still replay old delivery ids, the stored durable state fails the platform != handoff_to check, raises, and every replay of those ids gets 503 "Webhook handoff state unavailable" until someone deletes meta rows by hand. Suggestion: treat marker/platform mismatch as not-mine (fall through to fresh claim under a namespaced key that includes the target) rather than a hard failure, or document the manual cleanup step.
gateway/platforms/base.py:create_handoff_thread fallback removal for routed sources — Positive, and the best decision in the PR: refusing to fall back to the parent/home channel when thread creation fails prevents a webhook handoff from silently taking ownership of whatever conversation lives there. Combined with the durable set_meta_if_absent claim (restart-surviving idempotency), expected-session CAS on route moves, shielded SQLite writes with cancel-then-reconcile semantics, and destination-profile validation before thread creation, the failure taxonomy here is unusually rigorous for a first cut of a handoff feature.
Review scope note: 191KB/15 files — the above covers the webhook/handoff core; I did not line-review the i18n/docs portions or any test files outside the head window. If there are e2e tests exercising the duplicate-recovery and cancelled-claim paths against a real state.db, they'd be the main remaining confidence lever — worth pointing reviewers at them explicitly in the PR description.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Adds a trusted webhook-route contract:
After a successful webhook agent run, Hermes requests one durable handoff of the exact webhook session, creates one thread under Discord's configured home channel, and atomically moves routing ownership from the webhook key to the destination thread key. The session ID and role-aware transcript are preserved, the webhook source route is removed, and the next Discord thread event resolves to the same session.
Handoff mode is exclusive: it suppresses the legacy webhook delivery path and rejects
deliver_only: true. Routes withouthandoff_toretain their existing behavior.The implementation extends the existing durable handoff/session infrastructure without a database migration or new dependency. It adds durable delivery-ID idempotency, compare-and-swap routing/finalization, restart recovery, default-profile safety, and cold-relay routing priming.
Related Issue
Related to #31565.
#31566 is adjacent active-run clarify-routing work; this PR adds post-completion session continuation and does not claim to resolve that separate interaction path.
Companion Discord participation-tracker fix: #90348. That tracker fix is intentionally not included here. #63459 is the older, broader related Discord anchoring PR.
Overlap was checked against open and merged work. In particular, #90304 and #90236 are adjacent webhook/profile or intake work, but neither implements durable exact-session handoff.
Type of Change
Changes Made
gateway/platforms/webhook.py: validates the trustedhandoff_to: discordroute setting, suppresses legacy delivery, durably claims stable delivery IDs, requests the exact session once, and finalizes failures/cancellations atomically. Successful media-only turns are recognized through the runner's explicit success marker; pre-agent early exits cannot create empty handoff threads.hermes_state.pyandgateway/session.py: add no-migration transactional metadata claims, running-state terminal transitions, exact-owner routing CAS move/removal, compression-lineage-aware cleanup, and exact-session transcript preservation.gateway/run.py: generalizes the durable watcher, moves webhook route ownership, reconciles cancellation during off-loop claims/moves, derives webhook destination keys from global gateway per-user settings, preserves CLI/TUI behavior, fails synthetic destination-agent errors visibly, validates default-profile continuity, and primes cold relay routing.gateway/config.pyandgateway/platforms/base.py: preserve authenticated Discord home provenance across the legacy environment overlay and generalize CLI-specific handoff wording.cli-config.yaml.exampleandwebsite/docs/user-guide/messaging/webhooks.md: document the trusted route contract, exclusive delivery, stable-header requirement, durable versus legacy deduplication, asynchronous202 Acceptedbehavior, and failure visibility.tests/gateway/test_webhook_session_handoff.py,tests/gateway/test_webhook_handoff_watcher.py,tests/gateway/test_session.py,tests/test_hermes_state.py,tests/gateway/test_config.py,tests/gateway/test_handoff_watcher_async_db.py, andtests/hermes_cli/test_session_handoff.py: add integration and concurrency coverage for success, media-only output, pre-agent early exit, duplicate delivery, exact ID/transcript movement, route ownership, failure, cancellation, invalid/missing destinations, per-user/profile routing, relay restart, compression, restart recovery, and unchanged CLI/TUI behavior.How to Test
All commands below were run through the repository test wrapper with
HOMEandHERMES_HOMEpointed at an isolated temporary directory.fb63aa508d85994f2955c8a8172d70bfa2f03190: 437 passed, 2 skipped, 0 failed.scripts/check-windows-footguns.pypassed on all six changed source files.git diff --check upstream/main...HEADpassed.upstream/mainata72c9ca248a051b8c7e8a69ff422c7be5066cdc4is an ancestor of the tested head.Total targeted verification on the published branch tip: 1,114 passed, 2 skipped, 0 failed.
A full
pytest tests/ -qrun was not completed in this isolated worktree: optionalacp/anthropicpackages are absent and the macOS process-guard tests require a different host setup. The full-suite checkbox therefore remains intentionally unchecked.No live Discord, gateway, profile, state database, or Hermes installation was exercised or modified under the worktree-only constraint. The PR remains draft pending exact-head CI and disposable maintainer end-to-end validation.
Checklist
Code
fix(scope):,feat(scope):, etc.); upstream synchronization merge commits retain Git-generated subjectspytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) —website/docs/user-guide/messaging/webhooks.mdand handoff docstringscli-config.yaml.exampleif I added/changed config keysCONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/A; no contributor workflow or project architecture policy changedScreenshots / Logs
Not applicable. This is a backend routing/state change; verification results are listed above.