Skip to content

fix(gateway): relay thread-rename must carry the parent-channel discriminator - #76465

Merged
benbarclay merged 1 commit into
mainfrom
fix/relay-rename-egress-discriminator
Aug 2, 2026
Merged

fix(gateway): relay thread-rename must carry the parent-channel discriminator#76465
benbarclay merged 1 commit into
mainfrom
fix/relay-rename-egress-discriminator

Conversation

@benbarclay

Copy link
Copy Markdown
Collaborator

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:

discord auto-thread rename: thread=… lane=relay new_title='200 Word Story Request'
relay thread_rename declined …: discord egress declined: target not routed to an onboarded tenant
discord auto-thread rename result: thread=… applied=False

Root cause — the true terminal blocker. The connector's routedEgressGuard resolves the owning tenant from the outbound metadata's scope_id (guild) or user_id (author). The adapter builds that metadata via _with_scope(chat_id), which reads 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 those caches never held — and the op shipped with no discriminator. resolveTenant returned undefined, 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'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 exact regression shape).
  • Lane-level: the late-feedback race 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 is a gateway-only fix.

Deploy + verify: after the fleet image carries this, the trace logs should read applied=True and the connector's decision log decision=apply_connector_created — I'll confirm mechanically from the logs before calling it fixed.

…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.
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on adf9c08

ℹ️ Info

Desktop E2E visual evidence · View test artifacts · View job

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

@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery platform/discord Discord bot adapter sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Aug 1, 2026
@benbarclay
benbarclay merged commit 3f497e2 into main Aug 2, 2026
41 checks passed
@benbarclay
benbarclay deleted the fix/relay-rename-egress-discriminator branch August 2, 2026 00:08
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.
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 sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants