Skip to content

feat(relay): multi-platform-per-agent — list identity, N-hello, per-frame egress (Phase 1.5) - #52830

Merged
benbarclay merged 1 commit into
mainfrom
feat/relay-multiplatform-agent
Jun 26, 2026
Merged

feat(relay): multi-platform-per-agent — list identity, N-hello, per-frame egress (Phase 1.5)#52830
benbarclay merged 1 commit into
mainfrom
feat/relay-multiplatform-agent

Conversation

@benbarclay

Copy link
Copy Markdown
Collaborator

Multi-platform-per-agent — the gateway half (Phase 1.5, Shape A)

One hosted agent now fronts a set of platforms (e.g. Discord and Telegram) over a single relay WebSocket, instead of being pinned to one. This is the hermes-agent half of the cross-repo cut-over; it pairs with the connector's per-frame egress (gateway-gateway #75, already merged) and the NAS deploy-config stamp (nous-account-service feat/relay-multiplatform-stamp).

What changed (gateway/relay/)

  • relay_platform_identities() parses the env-scoped deploy config into the ordered (platform, bot_id) set:
    • GATEWAY_RELAY_PLATFORMS — comma-sep list (e.g. discord,telegram)
    • GATEWAY_RELAY_BOT_IDS — JSON keyed map {"discord":{"botId":"…"},"telegram":{"botId":"…","username":"…"}}
    • Cut over from the scalar GATEWAY_RELAY_PLATFORM/_BOT_ID — no fallback path retained (per the design decision). A malformed map degrades to empty bot ids (boot never crashes).
  • self_provision_relay() loops one /relay/provision POST per platform under one gatewayId + one secret, partial-failure-tolerant (one platform's bad config doesn't sink the rest).
  • WebSocketRelayTransport holds the identity set, sends one hello per identity (the connector accumulates them into its advertised set), and stamps the per-frame OutboundFrame.platform + its matching advertised botId on every outbound.
  • RelayAdapter remembers each chat's underlying source.platform on inbound (mirroring the existing guild/DM scope-capture) and tags the reply's egress platform — so a reply routes back through the right sender.
  • send_relay_policy() declares one relevance policy per fronted platform (the connector keys policy by (tenant, platform, instanceId)).

Back-compat

A single-platform deploy passes a 1-element list and emits no per-frame tag → the connector falls back to the session-default platform → the wire shape is byte-identical to before.

Verification

  • ruff clean, tsc-equivalent (pyright) clean on changed files.
  • Relay unit suite: 221 pass / 1 skip (211 baseline + 10 new multi-platform tests: list/map parse, provision-loop multi + partial-failure, per-frame egress tag).
  • All 15 cross-repo E2E drivers green against the connector at origin/main (with fix(whatsapp): multi-user session isolation and bridge message handling #75) — proving the N-hello + per-frame-egress wire interop on real sockets, plus the single-platform back-compat path.

⚠️ Atomic cut-over — no scalar fallback. This must land together with the NAS stamp PR and the prod/staging env-var migration so a recreated container never gets a half-old/half-new env. See the relay-platform-parity spec (D-Q1.5c).

Infographic

multi-platform-relay-agent

…, N-hello, per-frame egress (Phase 1.5)

Cut over the agent half of Shape A (D-Q1.5a/b.1/c) to front a SET of platforms on
one relay WS:

- relay_platform_identities() parses GATEWAY_RELAY_PLATFORMS (list) +
  GATEWAY_RELAY_BOT_IDS (JSON keyed map {platform:{botId,username?}}). Cut over
  from the scalar GATEWAY_RELAY_PLATFORM/_BOT_ID (no fallback, D-Q1.5c).
- self_provision_relay() loops one /relay/provision per platform under one
  gatewayId+secret, partial-failure-tolerant.
- WebSocketRelayTransport takes the identity SET, sends one hello per identity
  (connector accumulates the advertised set), and stamps the per-frame
  OutboundFrame.platform + its matching advertised botId on outbound.
- RelayAdapter remembers each chat's underlying source.platform (mirroring the
  existing guild/dm scope capture) and tags the reply's egress platform.
- send_relay_policy() declares one relevance policy per fronted platform (the
  connector keys policy by (tenant,platform,instanceId)).

Single-platform deploys are byte-identical on the wire (1-element list, no per-frame
tag -> connector session-default fallback). typecheck/ruff clean; relay unit 221 pass
(+10 new); all 15 cross-repo E2E drivers green vs connector origin/main.
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Lint report: feat/relay-multiplatform-agent vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 11365 on HEAD, 11364 on base (🆕 +1)

🆕 New issues (3):

Rule Count
unresolved-import 1
invalid-argument-type 1
invalid-assignment 1
First entries
tests/gateway/relay/test_relay_multiplatform.py:19: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/gateway/relay/test_relay_multiplatform.py:226: [invalid-argument-type] invalid-argument-type: Argument to `RelayAdapter.__init__` is incorrect: Expected `RelayTransport | None`, found `StubConnector`
tests/run_agent/test_credits_notices_toggle.py:76: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to attribute `_credits_session_start_micros` of type `int`

✅ Fixed issues (2):

Rule Count
unresolved-attribute 2
First entries
tests/run_agent/test_credits_notices_toggle.py:76: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_credits_session_start_micros` on type `AIAgent`
run_agent.py:2989: [unresolved-attribute] unresolved-attribute: Object of type `Self@get_credits_spent_micros` has no attribute `_credits_session_start_micros`

Unchanged: 5992 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@alt-glitch alt-glitch added type/feature New feature or request comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Jun 26, 2026

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Approved

Phase 1.5 multi-platform-per-agent relay feature. Adds the ability for one gateway to front N platforms on a single WS connection, with per-frame egress tagging.

Looks Good

  • relay_platform_identities() cleanly parses GATEWAY_RELAY_PLATFORMS + GATEWAY_RELAY_BOT_IDS with graceful degradation for malformed JSON
  • self_provision_relay() loop is partial-failure-tolerant: one platform failing does not block others
  • Per-frame egress tagging via platform kwarg on send_outbound/send_follow_up is backward-compatible (single-platform gateways emit the same frame shape as before)
  • _bot_id_for() correctly resolves the matching botId for each platform's advertised set
  • Multi-hello handshake (one hello per identity) is clean
  • Comprehensive test suite: identity parsing, provision loop, partial failure, adapter egress tagging, untagged fallback
  • Transport stub updated correctly for tests

Reviewed by Hermes Agent

@benbarclay
benbarclay merged commit 6e4e596 into main Jun 26, 2026
27 checks passed
@benbarclay
benbarclay deleted the feat/relay-multiplatform-agent branch June 26, 2026 07:32
pai-scaffolde pushed a commit to pai-scaffolde/hermes-agent that referenced this pull request Jun 28, 2026
…, N-hello, per-frame egress (Phase 1.5) (NousResearch#52830)

Cut over the agent half of Shape A (D-Q1.5a/b.1/c) to front a SET of platforms on
one relay WS:

- relay_platform_identities() parses GATEWAY_RELAY_PLATFORMS (list) +
  GATEWAY_RELAY_BOT_IDS (JSON keyed map {platform:{botId,username?}}). Cut over
  from the scalar GATEWAY_RELAY_PLATFORM/_BOT_ID (no fallback, D-Q1.5c).
- self_provision_relay() loops one /relay/provision per platform under one
  gatewayId+secret, partial-failure-tolerant.
- WebSocketRelayTransport takes the identity SET, sends one hello per identity
  (connector accumulates the advertised set), and stamps the per-frame
  OutboundFrame.platform + its matching advertised botId on outbound.
- RelayAdapter remembers each chat's underlying source.platform (mirroring the
  existing guild/dm scope capture) and tags the reply's egress platform.
- send_relay_policy() declares one relevance policy per fronted platform (the
  connector keys policy by (tenant,platform,instanceId)).

Single-platform deploys are byte-identical on the wire (1-element list, no per-frame
tag -> connector session-default fallback). typecheck/ruff clean; relay unit 221 pass
(+10 new); all 15 cross-repo E2E drivers green vs connector origin/main.
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…, N-hello, per-frame egress (Phase 1.5) (NousResearch#52830)

Cut over the agent half of Shape A (D-Q1.5a/b.1/c) to front a SET of platforms on
one relay WS:

- relay_platform_identities() parses GATEWAY_RELAY_PLATFORMS (list) +
  GATEWAY_RELAY_BOT_IDS (JSON keyed map {platform:{botId,username?}}). Cut over
  from the scalar GATEWAY_RELAY_PLATFORM/_BOT_ID (no fallback, D-Q1.5c).
- self_provision_relay() loops one /relay/provision per platform under one
  gatewayId+secret, partial-failure-tolerant.
- WebSocketRelayTransport takes the identity SET, sends one hello per identity
  (connector accumulates the advertised set), and stamps the per-frame
  OutboundFrame.platform + its matching advertised botId on outbound.
- RelayAdapter remembers each chat's underlying source.platform (mirroring the
  existing guild/dm scope capture) and tags the reply's egress platform.
- send_relay_policy() declares one relevance policy per fronted platform (the
  connector keys policy by (tenant,platform,instanceId)).

Single-platform deploys are byte-identical on the wire (1-element list, no per-frame
tag -> connector session-default fallback). typecheck/ruff clean; relay unit 221 pass
(+10 new); all 15 cross-repo E2E drivers green vs connector origin/main.
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
…, N-hello, per-frame egress (Phase 1.5) (NousResearch#52830)

Cut over the agent half of Shape A (D-Q1.5a/b.1/c) to front a SET of platforms on
one relay WS:

- relay_platform_identities() parses GATEWAY_RELAY_PLATFORMS (list) +
  GATEWAY_RELAY_BOT_IDS (JSON keyed map {platform:{botId,username?}}). Cut over
  from the scalar GATEWAY_RELAY_PLATFORM/_BOT_ID (no fallback, D-Q1.5c).
- self_provision_relay() loops one /relay/provision per platform under one
  gatewayId+secret, partial-failure-tolerant.
- WebSocketRelayTransport takes the identity SET, sends one hello per identity
  (connector accumulates the advertised set), and stamps the per-frame
  OutboundFrame.platform + its matching advertised botId on outbound.
- RelayAdapter remembers each chat's underlying source.platform (mirroring the
  existing guild/dm scope capture) and tags the reply's egress platform.
- send_relay_policy() declares one relevance policy per fronted platform (the
  connector keys policy by (tenant,platform,instanceId)).

Single-platform deploys are byte-identical on the wire (1-element list, no per-frame
tag -> connector session-default fallback). typecheck/ruff clean; relay unit 221 pass
(+10 new); all 15 cross-repo E2E drivers green vs connector origin/main.
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
…, N-hello, per-frame egress (Phase 1.5) (NousResearch#52830)

Cut over the agent half of Shape A (D-Q1.5a/b.1/c) to front a SET of platforms on
one relay WS:

- relay_platform_identities() parses GATEWAY_RELAY_PLATFORMS (list) +
  GATEWAY_RELAY_BOT_IDS (JSON keyed map {platform:{botId,username?}}). Cut over
  from the scalar GATEWAY_RELAY_PLATFORM/_BOT_ID (no fallback, D-Q1.5c).
- self_provision_relay() loops one /relay/provision per platform under one
  gatewayId+secret, partial-failure-tolerant.
- WebSocketRelayTransport takes the identity SET, sends one hello per identity
  (connector accumulates the advertised set), and stamps the per-frame
  OutboundFrame.platform + its matching advertised botId on outbound.
- RelayAdapter remembers each chat's underlying source.platform (mirroring the
  existing guild/dm scope capture) and tags the reply's egress platform.
- send_relay_policy() declares one relevance policy per fronted platform (the
  connector keys policy by (tenant,platform,instanceId)).

Single-platform deploys are byte-identical on the wire (1-element list, no per-frame
tag -> connector session-default fallback). typecheck/ruff clean; relay unit 221 pass
(+10 new); all 15 cross-repo E2E drivers green vs connector origin/main.
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
…, N-hello, per-frame egress (Phase 1.5) (NousResearch#52830)

Cut over the agent half of Shape A (D-Q1.5a/b.1/c) to front a SET of platforms on
one relay WS:

- relay_platform_identities() parses GATEWAY_RELAY_PLATFORMS (list) +
  GATEWAY_RELAY_BOT_IDS (JSON keyed map {platform:{botId,username?}}). Cut over
  from the scalar GATEWAY_RELAY_PLATFORM/_BOT_ID (no fallback, D-Q1.5c).
- self_provision_relay() loops one /relay/provision per platform under one
  gatewayId+secret, partial-failure-tolerant.
- WebSocketRelayTransport takes the identity SET, sends one hello per identity
  (connector accumulates the advertised set), and stamps the per-frame
  OutboundFrame.platform + its matching advertised botId on outbound.
- RelayAdapter remembers each chat's underlying source.platform (mirroring the
  existing guild/dm scope capture) and tags the reply's egress platform.
- send_relay_policy() declares one relevance policy per fronted platform (the
  connector keys policy by (tenant,platform,instanceId)).

Single-platform deploys are byte-identical on the wire (1-element list, no per-frame
tag -> connector session-default fallback). typecheck/ruff clean; relay unit 221 pass
(+10 new); all 15 cross-repo E2E drivers green vs connector origin/main.
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
…, N-hello, per-frame egress (Phase 1.5) (NousResearch#52830)

Cut over the agent half of Shape A (D-Q1.5a/b.1/c) to front a SET of platforms on
one relay WS:

- relay_platform_identities() parses GATEWAY_RELAY_PLATFORMS (list) +
  GATEWAY_RELAY_BOT_IDS (JSON keyed map {platform:{botId,username?}}). Cut over
  from the scalar GATEWAY_RELAY_PLATFORM/_BOT_ID (no fallback, D-Q1.5c).
- self_provision_relay() loops one /relay/provision per platform under one
  gatewayId+secret, partial-failure-tolerant.
- WebSocketRelayTransport takes the identity SET, sends one hello per identity
  (connector accumulates the advertised set), and stamps the per-frame
  OutboundFrame.platform + its matching advertised botId on outbound.
- RelayAdapter remembers each chat's underlying source.platform (mirroring the
  existing guild/dm scope capture) and tags the reply's egress platform.
- send_relay_policy() declares one relevance policy per fronted platform (the
  connector keys policy by (tenant,platform,instanceId)).

Single-platform deploys are byte-identical on the wire (1-element list, no per-frame
tag -> connector session-default fallback). typecheck/ruff clean; relay unit 221 pass
(+10 new); all 15 cross-repo E2E drivers green vs connector origin/main.
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 P2 Medium — degraded but workaround exists sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants