Issue files (.scratch/issues/20-29): retrospective specs for all work done in the current sprint — hardening, route-timeout, start-layer protocol, heartbeat stats, availability map, rolling RPM, smart assignment, throughput routing, routing tests, relay outbound client. ADRs (docs/adr/0011-0014): 0011 — Auto-shard from memory budget and tracker network assignment 0012 — X-Meshnet-Start-Layer overlapping shard execution protocol 0013 — Rolling RPM statistics, smart assignment scoring, throughput routing 0014 — Relay outbound client for NAT/internet pipeline hops prd.json: US-020 through US-029 added, all marked done. ralph_progress.py now shows 29/29 complete (100%). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2.2 KiB
US-029 — Outbound relay client: NAT/internet pipeline hops
Status: done Priority: Critical Stage: Implemented
Context
Nodes behind NAT (WSL2 with 172.x.x.x addresses, 5G mobile, home routers) register with the
tracker and include a relay_addr (wss://relay/rpc/{peer_id}). When the head node needs to
forward activations to such a peer, it currently fails because the direct HTTP endpoint is
unreachable.
The relay server (US-017) is already running and the node already opens a persistent outbound
WebSocket (RelayHttpBridge). What is missing is the outbound caller side: given a relay_addr,
open a per-hop WebSocket to the relay's /rpc/{peer_id} endpoint and send the activation through it.
Protocol
Node A → WS connect wss://relay/rpc/{peer_id_B}
→ send JSON: {request_id, method, path, headers, body_base64}
Relay → forward as relay-http-request envelope to Node B's persistent WS
Node B → process /forward locally
→ send relay-http-response envelope back
Relay → resolve future, send response JSON to Node A
Node A ← {request_id, status, headers, body_base64}
Binary activations (bfloat16) are base64-encoded. No precision loss.
Acceptance criteria
_relay_hop(relay_addr, path, body, headers, timeout)intorch_server.py— opens WS, sends, receives, returns(status, headers_lower, body_bytes)_get_remaining_routereturnslist[dict]withrelay_addrfield (waslist[tuple])_run_downstream_pipelinedispatches via_relay_hopwhen hop hasrelay_addr; falls back to direct HTTP if relay connection fails- Tracker
_handle_proxy_chatincludesrelay_addrin downstream hop dicts when node has one relay_bridge._handle_requestdecodesbody_base64; response usesbody_base64foroctet-streamcontent- All 157 tests pass (
python -m pytest) - QUICKSTART.md updated with relay NAT/internet architecture and test scenario
WSL2 test scenario
Start two nodes in WSL2 pointing at the public tracker. Both get 172.x.x.x endpoints (unreachable from outside). Both connect to relay automatically. Send an inference request through the tracker — activations flow via relay. No --advertise-host needed.