Skip to content

feat(webhook): hand off completed sessions to messaging platforms - #90385

Draft
ryanlatham wants to merge 14 commits into
NousResearch:mainfrom
ryanlatham:codex/feat-webhook-session-handoff
Draft

feat(webhook): hand off completed sessions to messaging platforms#90385
ryanlatham wants to merge 14 commits into
NousResearch:mainfrom
ryanlatham:codex/feat-webhook-session-handoff

Conversation

@ryanlatham

@ryanlatham ryanlatham commented Aug 20, 2026

Copy link
Copy Markdown

What does this PR do?

Adds a trusted webhook-route contract:

handoff_to: discord

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.

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

  • 🐛 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.

  1. Handoff/state/config matrix:
    scripts/run_tests.sh tests/gateway/test_webhook_session_handoff.py tests/gateway/test_webhook_handoff_watcher.py tests/gateway/test_session.py tests/hermes_cli/test_session_handoff.py tests/gateway/test_handoff_watcher_async_db.py tests/test_hermes_state.py tests/gateway/test_config.py -q
    Result on fb63aa508d85994f2955c8a8172d70bfa2f03190: 437 passed, 2 skipped, 0 failed.
  2. Webhook + CLI/TUI + destination compatibility matrix:
    scripts/run_tests.sh tests/gateway/test_webhook_adapter.py tests/gateway/test_webhook_deliver_only.py tests/gateway/test_webhook_session_close.py tests/gateway/test_webhook_integration.py tests/gateway/test_webhook_dynamic_routes.py tests/gateway/test_webhook_route_toolsets.py tests/gateway/test_handoff_thread_session_key.py tests/gateway/test_telegram_topic_mode.py tests/gateway/relay/test_handoff_relay_aliasing.py tests/cli/test_handoff_cleanup_race.py tests/test_tui_gateway_server.py -q
    Result on the same commit: 677 passed, 0 failed.
  3. Static checks:
    • Ruff passed on every changed Python file.
    • 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.

Checklist

Code

  • I've read the Contributing Guide
  • My substantive commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.); upstream synchronization merge commits retain Git-generated subjects
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated diff)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 26.5.2 arm64, Python 3.11.15

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — website/docs/user-guide/messaging/webhooks.md and handoff docstrings
  • I've updated cli-config.yaml.example if I added/changed config keys
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A; no contributor workflow or project architecture policy changed
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide; the Windows-footgun scan passes
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A; no model tool or schema changed

Screenshots / Logs

Not applicable. This is a backend routing/state change; verification results are listed above.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery platform/webhook Webhook / API server platform/discord Discord bot adapter area/config Config system, migrations, profiles sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 20, 2026
@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference; please use your judgment.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have platform/discord Discord bot adapter platform/webhook Webhook / API server sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants