Skip to content

feat(relay): Phase 5 §5.3 going-idle / buffered-flip primitive (gateway side) - #51572

Merged
benbarclay merged 1 commit into
mainfrom
feat/phase5-b-going-idle
Jun 23, 2026
Merged

feat(relay): Phase 5 §5.3 going-idle / buffered-flip primitive (gateway side)#51572
benbarclay merged 1 commit into
mainfrom
feat/phase5-b-going-idle

Conversation

@benbarclay

Copy link
Copy Markdown
Collaborator

Phase 5 §5.3 — going-idle / buffered-flip primitive (gateway side)

The gateway half of the going-idle/buffered-flip primitive. This is a scale-to-zero PRIMITIVE, not the behaviour — nothing here decides to sleep or suspends a machine. It integrates with the gateway's EXISTING drain transition rather than introducing a parallel relay-only idle path.

What it does

When the gateway drains, it tells the connector to flip its destination to buffered-only so inbound that arrives while it's gone is buffered durably and replayed (in order, no loss/dup) when it reconnects — instead of being pushed at a closing socket.

  • ws_transport.py
    • go_idle() — sends going_idle, awaits the connector's going_idle_ack (connector-authoritative flip-then-ack, Q-5.3c: stays serving until the ack, so an event in the flip window is delivered live, not lost). Returns False on timeout/not-connected (caller closes anyway — no regression).
    • Buffered inbound (a delivery carrying a bufferId) is acked via inbound_ack after the handler runs → drain-without-dup on the delivery leg.
    • NET-NEW reconnect loop — re-dials + re-handshakes after an unexpected close (capped backoff), so a gateway that went idle re-establishes its socket, which triggers the connector's buffered-flip drain. Off by default; register_relay_adapter turns it on in production. A deliberate disconnect() never reconnects.
  • adapter.py — emits going_idle from its existing disconnect() drain seam before tearing down the socket; best-effort + guarded (a missing go_idle, or a failed/timed-out ack, never blocks shutdown).
  • transport.py Protocol + docs/relay-connector-contract.md §3.2 document the three new frames.

Not in scope (deferred behaviour)

The autonomous idle timer that decides to drain, the actual machine suspend (Fly autostop:"suspend"), and the NAS suspended-health model. A future workstream consumes these primitives.

Verification

  • +6 relay unit tests (tests/gateway/relay/test_relay_going_idle.py); full relay suite 124 pass.
  • Cross-repo E2E: the connector's new gateway_going_idle_driver.py drives this production transport through go-idle → ack → flip → buffer-while-disconnected → reconnect → drain-replay-in-order → no loss / no dup → unflip over a real socket. All 11 drivers green.

Merge ordering: this is the FIRST half. The connector PR (NousResearch/gateway-gateway feat/phase5-b-going-idle) adds an E2E driver that imports go_idle/reconnect from this branch, so its cross-repo CI stays red until this lands. Land this, then the connector.

Ben's relay-adapter solo lane.

Infographic

phase5-going-idle-spine

…ay side)

The gateway half of the going-idle/buffered-flip primitive (scale-to-zero
PRIMITIVE, not the behaviour). Integrates with the EXISTING drain transition:

- ws_transport: `go_idle()` sends `going_idle` + awaits the connector's
  `going_idle_ack` (connector-authoritative flip-then-ack, Q-5.3c — stays
  serving until the ack so nothing is lost in the flip window); acks a buffered
  inbound (bufferId present) via `inbound_ack` after the handler runs
  (drain-without-dup on the delivery leg); NET-NEW reconnect loop re-dials +
  re-handshakes after an unexpected close (off by default, on in production).
- adapter: emits `going_idle` from its existing `disconnect()` drain seam before
  tearing down the socket; best-effort + guarded (never blocks shutdown).
- transport Protocol + contract doc §3.2 document the 3 new frames.

+6 relay tests (124 pass). NOT in scope: the autonomous idle timer / machine
suspend / NAS health model (deferred behaviour). Ben's relay-adapter solo lane.
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Lint report: feat/phase5-b-going-idle 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: 11140 on HEAD, 11132 on base (🆕 +8)

🆕 New issues (8):

Rule Count
invalid-argument-type 5
unresolved-import 3
First entries
tests/gateway/relay/test_relay_going_idle.py:23: [unresolved-import] unresolved-import: Cannot resolve imported module `websockets`
tests/gateway/relay/test_relay_going_idle.py:15: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/gateway/relay/test_relay_roundtrip_telegram.py:80: [invalid-argument-type] invalid-argument-type: Argument to `RelayAdapter.__init__` is incorrect: Expected `RelayTransport | None`, found `StubConnector`
tests/gateway/relay/test_relay_follow_up.py:46: [invalid-argument-type] invalid-argument-type: Argument to `RelayAdapter.__init__` is incorrect: Expected `RelayTransport | None`, found `StubConnector`
tests/gateway/relay/test_relay_going_idle.py:16: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest_asyncio`
tests/gateway/relay/test_relay_roundtrip.py:58: [invalid-argument-type] invalid-argument-type: Argument to `RelayAdapter.__init__` is incorrect: Expected `RelayTransport | None`, found `StubConnector`
tests/gateway/relay/test_relay_interrupt.py:37: [invalid-argument-type] invalid-argument-type: Argument to `RelayAdapter.__init__` is incorrect: Expected `RelayTransport | None`, found `StubConnector`
tests/gateway/relay/test_relay_passthrough.py:44: [invalid-argument-type] invalid-argument-type: Argument to `RelayAdapter.__init__` is incorrect: Expected `RelayTransport | None`, found `StubConnector`

✅ Fixed issues: none

Unchanged: 5873 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 23, 2026
@benbarclay
benbarclay merged commit 40fddc9 into main Jun 23, 2026
27 checks passed
@benbarclay
benbarclay deleted the feat/phase5-b-going-idle branch June 23, 2026 23:50
T02200059 pushed a commit to T02200059/hermes-agent that referenced this pull request Jun 24, 2026
…ay side) (NousResearch#51572)

The gateway half of the going-idle/buffered-flip primitive (scale-to-zero
PRIMITIVE, not the behaviour). Integrates with the EXISTING drain transition:

- ws_transport: `go_idle()` sends `going_idle` + awaits the connector's
  `going_idle_ack` (connector-authoritative flip-then-ack, Q-5.3c — stays
  serving until the ack so nothing is lost in the flip window); acks a buffered
  inbound (bufferId present) via `inbound_ack` after the handler runs
  (drain-without-dup on the delivery leg); NET-NEW reconnect loop re-dials +
  re-handshakes after an unexpected close (off by default, on in production).
- adapter: emits `going_idle` from its existing `disconnect()` drain seam before
  tearing down the socket; best-effort + guarded (never blocks shutdown).
- transport Protocol + contract doc §3.2 document the 3 new frames.

+6 relay tests (124 pass). NOT in scope: the autonomous idle timer / machine
suspend / NAS health model (deferred behaviour). Ben's relay-adapter solo lane.
kpadilha pushed a commit to kpadilha/hermes-agent that referenced this pull request Jun 24, 2026
…ay side) (NousResearch#51572)

The gateway half of the going-idle/buffered-flip primitive (scale-to-zero
PRIMITIVE, not the behaviour). Integrates with the EXISTING drain transition:

- ws_transport: `go_idle()` sends `going_idle` + awaits the connector's
  `going_idle_ack` (connector-authoritative flip-then-ack, Q-5.3c — stays
  serving until the ack so nothing is lost in the flip window); acks a buffered
  inbound (bufferId present) via `inbound_ack` after the handler runs
  (drain-without-dup on the delivery leg); NET-NEW reconnect loop re-dials +
  re-handshakes after an unexpected close (off by default, on in production).
- adapter: emits `going_idle` from its existing `disconnect()` drain seam before
  tearing down the socket; best-effort + guarded (never blocks shutdown).
- transport Protocol + contract doc §3.2 document the 3 new frames.

+6 relay tests (124 pass). NOT in scope: the autonomous idle timer / machine
suspend / NAS health model (deferred behaviour). Ben's relay-adapter solo lane.
pai-scaffolde pushed a commit to pai-scaffolde/hermes-agent that referenced this pull request Jun 28, 2026
…ay side) (NousResearch#51572)

The gateway half of the going-idle/buffered-flip primitive (scale-to-zero
PRIMITIVE, not the behaviour). Integrates with the EXISTING drain transition:

- ws_transport: `go_idle()` sends `going_idle` + awaits the connector's
  `going_idle_ack` (connector-authoritative flip-then-ack, Q-5.3c — stays
  serving until the ack so nothing is lost in the flip window); acks a buffered
  inbound (bufferId present) via `inbound_ack` after the handler runs
  (drain-without-dup on the delivery leg); NET-NEW reconnect loop re-dials +
  re-handshakes after an unexpected close (off by default, on in production).
- adapter: emits `going_idle` from its existing `disconnect()` drain seam before
  tearing down the socket; best-effort + guarded (never blocks shutdown).
- transport Protocol + contract doc §3.2 document the 3 new frames.

+6 relay tests (124 pass). NOT in scope: the autonomous idle timer / machine
suspend / NAS health model (deferred behaviour). Ben's relay-adapter solo lane.
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…ay side) (NousResearch#51572)

The gateway half of the going-idle/buffered-flip primitive (scale-to-zero
PRIMITIVE, not the behaviour). Integrates with the EXISTING drain transition:

- ws_transport: `go_idle()` sends `going_idle` + awaits the connector's
  `going_idle_ack` (connector-authoritative flip-then-ack, Q-5.3c — stays
  serving until the ack so nothing is lost in the flip window); acks a buffered
  inbound (bufferId present) via `inbound_ack` after the handler runs
  (drain-without-dup on the delivery leg); NET-NEW reconnect loop re-dials +
  re-handshakes after an unexpected close (off by default, on in production).
- adapter: emits `going_idle` from its existing `disconnect()` drain seam before
  tearing down the socket; best-effort + guarded (never blocks shutdown).
- transport Protocol + contract doc §3.2 document the 3 new frames.

+6 relay tests (124 pass). NOT in scope: the autonomous idle timer / machine
suspend / NAS health model (deferred behaviour). Ben's relay-adapter solo lane.
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
…ay side) (NousResearch#51572)

The gateway half of the going-idle/buffered-flip primitive (scale-to-zero
PRIMITIVE, not the behaviour). Integrates with the EXISTING drain transition:

- ws_transport: `go_idle()` sends `going_idle` + awaits the connector's
  `going_idle_ack` (connector-authoritative flip-then-ack, Q-5.3c — stays
  serving until the ack so nothing is lost in the flip window); acks a buffered
  inbound (bufferId present) via `inbound_ack` after the handler runs
  (drain-without-dup on the delivery leg); NET-NEW reconnect loop re-dials +
  re-handshakes after an unexpected close (off by default, on in production).
- adapter: emits `going_idle` from its existing `disconnect()` drain seam before
  tearing down the socket; best-effort + guarded (never blocks shutdown).
- transport Protocol + contract doc §3.2 document the 3 new frames.

+6 relay tests (124 pass). NOT in scope: the autonomous idle timer / machine
suspend / NAS health model (deferred behaviour). Ben's relay-adapter solo lane.
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
…ay side) (NousResearch#51572)

The gateway half of the going-idle/buffered-flip primitive (scale-to-zero
PRIMITIVE, not the behaviour). Integrates with the EXISTING drain transition:

- ws_transport: `go_idle()` sends `going_idle` + awaits the connector's
  `going_idle_ack` (connector-authoritative flip-then-ack, Q-5.3c — stays
  serving until the ack so nothing is lost in the flip window); acks a buffered
  inbound (bufferId present) via `inbound_ack` after the handler runs
  (drain-without-dup on the delivery leg); NET-NEW reconnect loop re-dials +
  re-handshakes after an unexpected close (off by default, on in production).
- adapter: emits `going_idle` from its existing `disconnect()` drain seam before
  tearing down the socket; best-effort + guarded (never blocks shutdown).
- transport Protocol + contract doc §3.2 document the 3 new frames.

+6 relay tests (124 pass). NOT in scope: the autonomous idle timer / machine
suspend / NAS health model (deferred behaviour). Ben's relay-adapter solo lane.
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
…ay side) (NousResearch#51572)

The gateway half of the going-idle/buffered-flip primitive (scale-to-zero
PRIMITIVE, not the behaviour). Integrates with the EXISTING drain transition:

- ws_transport: `go_idle()` sends `going_idle` + awaits the connector's
  `going_idle_ack` (connector-authoritative flip-then-ack, Q-5.3c — stays
  serving until the ack so nothing is lost in the flip window); acks a buffered
  inbound (bufferId present) via `inbound_ack` after the handler runs
  (drain-without-dup on the delivery leg); NET-NEW reconnect loop re-dials +
  re-handshakes after an unexpected close (off by default, on in production).
- adapter: emits `going_idle` from its existing `disconnect()` drain seam before
  tearing down the socket; best-effort + guarded (never blocks shutdown).
- transport Protocol + contract doc §3.2 document the 3 new frames.

+6 relay tests (124 pass). NOT in scope: the autonomous idle timer / machine
suspend / NAS health model (deferred behaviour). Ben's relay-adapter solo lane.
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
…ay side) (NousResearch#51572)

The gateway half of the going-idle/buffered-flip primitive (scale-to-zero
PRIMITIVE, not the behaviour). Integrates with the EXISTING drain transition:

- ws_transport: `go_idle()` sends `going_idle` + awaits the connector's
  `going_idle_ack` (connector-authoritative flip-then-ack, Q-5.3c — stays
  serving until the ack so nothing is lost in the flip window); acks a buffered
  inbound (bufferId present) via `inbound_ack` after the handler runs
  (drain-without-dup on the delivery leg); NET-NEW reconnect loop re-dials +
  re-handshakes after an unexpected close (off by default, on in production).
- adapter: emits `going_idle` from its existing `disconnect()` drain seam before
  tearing down the socket; best-effort + guarded (never blocks shutdown).
- transport Protocol + contract doc §3.2 document the 3 new frames.

+6 relay tests (124 pass). NOT in scope: the autonomous idle timer / machine
suspend / NAS health model (deferred behaviour). Ben's relay-adapter solo lane.
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.

2 participants