fix(gateway): relay thread-rename must carry the parent-channel discriminator - #76465
Merged
Conversation
…iminator Live staging (2026-08-01, on a fresh instance where title generation finally succeeded): the rename lane fired end to end, but the connector declined the op with "discord egress declined: target not routed to an onboarded tenant". The trace logs added earlier pinpointed it: discord auto-thread rename: thread=... lane=relay new_title='...' relay thread_rename declined ...: target not routed to an onboarded tenant discord auto-thread rename result: thread=... applied=False Root cause: the connector's routedEgressGuard resolves the owning tenant from the outbound metadata's scope_id (guild) or user_id (author). The adapter builds those via _with_scope(chat_id), reading per-chat caches keyed by the PARENT channel chat_id learned at inbound. The relay rename lane called rename_thread WITHOUT parent_chat_id, so chat_id defaulted to the THREAD id — a key the caches never held — and the op shipped with no discriminator. resolveTenant returned undefined and egress was declined before the op ever reached the (now-durable) no-clobber guard. This was the true terminal blocker: every earlier fix (send-result feedback, registration/poll ordering, connector-owned guard, durable Redis store) was correct but sat DOWNSTREAM of this egress-routing decline, so none of them could take effect. Fix: the relay lane passes parent_chat_id=source.chat_id (the relay source's chat_id IS the parent channel; the thread came from send-result feedback). _with_scope then resolves scope_id/user_id from the parent-channel caches and the connector routes the op to the tenant. Scoped to the relay lane only (use_connector_guard); the native lane renames via the direct Discord API and needs no discriminator. Tests: adapter-level — a rename passing parent_chat_id carries the cached scope_id, one keyed on the thread id alone does not (the regression shape); lane-level — the late-feedback test now asserts parent_chat_id flows through as the parent channel. Relay suite 150 passed; ruff + footguns clean. Connector-compatible with the deployed egress guard; no gateway-gateway change needed.
Contributor
૮ >ﻌ< ა ci reviewran on adf9c08 ℹ️ InfoDesktop E2E visual evidence · View test artifacts · View job3 visual diffs. inline evidence upload failed. Failed to upload diff-1508682a2ae8-boot-ready-diff.png with gh image (exit code 1): Error uploading /home/runner/work/_temp/e2e-evidence/diff-1508682a2ae8-boot-ready-diff.png: step 0 (get upload token): uploadToken not found on repo page — do you have write access to NousResearch/hermes-agent? (or, if NousResearch enforces SAML SSO, authorize at https://github.com/orgs/NousResearch/sso) |
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
…iminator (NousResearch#76465) Live staging (2026-08-01, on a fresh instance where title generation finally succeeded): the rename lane fired end to end, but the connector declined the op with "discord egress declined: target not routed to an onboarded tenant". The trace logs added earlier pinpointed it: discord auto-thread rename: thread=... lane=relay new_title='...' relay thread_rename declined ...: target not routed to an onboarded tenant discord auto-thread rename result: thread=... applied=False Root cause: the connector's routedEgressGuard resolves the owning tenant from the outbound metadata's scope_id (guild) or user_id (author). The adapter builds those via _with_scope(chat_id), reading per-chat caches keyed by the PARENT channel chat_id learned at inbound. The relay rename lane called rename_thread WITHOUT parent_chat_id, so chat_id defaulted to the THREAD id — a key the caches never held — and the op shipped with no discriminator. resolveTenant returned undefined and egress was declined before the op ever reached the (now-durable) no-clobber guard. This was the true terminal blocker: every earlier fix (send-result feedback, registration/poll ordering, connector-owned guard, durable Redis store) was correct but sat DOWNSTREAM of this egress-routing decline, so none of them could take effect. Fix: the relay lane passes parent_chat_id=source.chat_id (the relay source's chat_id IS the parent channel; the thread came from send-result feedback). _with_scope then resolves scope_id/user_id from the parent-channel caches and the connector routes the op to the tenant. Scoped to the relay lane only (use_connector_guard); the native lane renames via the direct Discord API and needs no discriminator. Tests: adapter-level — a rename passing parent_chat_id carries the cached scope_id, one keyed on the thread id alone does not (the regression shape); lane-level — the late-feedback test now asserts parent_chat_id flows through as the parent channel. Relay suite 150 passed; ruff + footguns clean. Connector-compatible with the deployed egress guard; no gateway-gateway change needed.
33hodl
pushed a commit
to 33hodl/hermes-agent
that referenced
this pull request
Aug 12, 2026
…iminator (NousResearch#76465) Live staging (2026-08-01, on a fresh instance where title generation finally succeeded): the rename lane fired end to end, but the connector declined the op with "discord egress declined: target not routed to an onboarded tenant". The trace logs added earlier pinpointed it: discord auto-thread rename: thread=... lane=relay new_title='...' relay thread_rename declined ...: target not routed to an onboarded tenant discord auto-thread rename result: thread=... applied=False Root cause: the connector's routedEgressGuard resolves the owning tenant from the outbound metadata's scope_id (guild) or user_id (author). The adapter builds those via _with_scope(chat_id), reading per-chat caches keyed by the PARENT channel chat_id learned at inbound. The relay rename lane called rename_thread WITHOUT parent_chat_id, so chat_id defaulted to the THREAD id — a key the caches never held — and the op shipped with no discriminator. resolveTenant returned undefined and egress was declined before the op ever reached the (now-durable) no-clobber guard. This was the true terminal blocker: every earlier fix (send-result feedback, registration/poll ordering, connector-owned guard, durable Redis store) was correct but sat DOWNSTREAM of this egress-routing decline, so none of them could take effect. Fix: the relay lane passes parent_chat_id=source.chat_id (the relay source's chat_id IS the parent channel; the thread came from send-result feedback). _with_scope then resolves scope_id/user_id from the parent-channel caches and the connector routes the op to the tenant. Scoped to the relay lane only (use_connector_guard); the native lane renames via the direct Discord API and needs no discriminator. Tests: adapter-level — a rename passing parent_chat_id carries the cached scope_id, one keyed on the thread id alone does not (the regression shape); lane-level — the late-feedback test now asserts parent_chat_id flows through as the parent channel. Relay suite 150 passed; ruff + footguns clean. Connector-compatible with the deployed egress guard; no gateway-gateway change needed.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Relay thread-rename must carry the parent-channel discriminator
Live staging (2026-08-01), on a fresh instance where title generation finally succeeded: the rename lane fired end-to-end for the first time — and the trace logging added in the prior PRs paid off by naming the exact failure:
Root cause — the true terminal blocker. The connector's
routedEgressGuardresolves the owning tenant from the outbound metadata'sscope_id(guild) oruser_id(author). The adapter builds that metadata via_with_scope(chat_id), which reads per-chat caches keyed by the parent channelchat_idlearned at inbound. The relay rename lane calledrename_threadwithoutparent_chat_id, sochat_iddefaulted to the thread id — a key those caches never held — and the op shipped with no discriminator.resolveTenantreturnedundefined, egress was declined, and the op never reached the (now-durable) no-clobber guard at all.This is why every earlier fix was necessary but not sufficient: send-result feedback (#188/#74482), registration/poll ordering (#75581), connector-owned guard (#192), durable Redis store (#193) — all correct, all sitting downstream of this egress-routing decline. Nothing downstream could ever run.
Fix: the relay lane passes
parent_chat_id=source.chat_id(the relay source'schat_idis the parent channel; the thread came from send-result feedback)._with_scopethen resolvesscope_id/user_idfrom the parent-channel caches and the connector routes the op to the tenant. Scoped to the relay lane only (use_connector_guard); the native lane renames via the direct Discord API and needs no discriminator.Tests
parent_chat_idcarries the cachedscope_id; one keyed on the thread id alone does not (the exact regression shape).parent_chat_idflows through as the parent channel.Relay suite 150 passed · ruff + footguns clean.
Connector-compatible with the deployed egress guard — no gateway-gateway change needed. This is a gateway-only fix.
Deploy + verify: after the fleet image carries this, the trace logs should read
applied=Trueand the connector's decision logdecision=apply_connector_created— I'll confirm mechanically from the logs before calling it fixed.