Gateway Liveness

Reap half-open gateway connections with ping/pong heartbeats

Liveness is on out-of-box: the gateway pings peers on an interval, refreshes activity on every inbound frame, and reaps any session — or any stalled handshake — that misses too many beats.

Diagram

Liveness

PING

PONG

no PONG

REAP

🖥️ Gateway

💻 Peer

🔍 evaluate

🔒 LIVENESS_TIMEOUT

Quick Start

  1. On by default
    python
    from praisonaiagents import Agent
    from praisonai_bot.gateway import WebSocketGateway
    
    # Liveness runs at the config defaults — no LivenessConfig needed.
    gateway = WebSocketGateway()
    gateway.register_agent(Agent(name="Support", instructions="Reply to users"))
  2. Tune the cadence
    python
    from praisonaiagents.gateway import GatewayConfig, LivenessConfig
    from praisonai_bot.gateway import WebSocketGateway
    
    gateway = WebSocketGateway(
        config=GatewayConfig(
            liveness=LivenessConfig(interval_ms=15_000, missed_beats_before_reap=3)
        )
    )
  3. Opt out with interval_ms=0
    python
    from praisonaiagents.gateway import GatewayConfig, LivenessConfig
    from praisonai_bot.gateway import WebSocketGateway
    
    # The only supported way to fully disable liveness at the runtime.
    gateway = WebSocketGateway(
        config=GatewayConfig(liveness=LivenessConfig(enabled=False, interval_ms=0))
    )

How It Works

The gateway emits a PING on each interval; any inbound frame (a PONG, a peer PING, or a normal message) refreshes activity. A silent peer misses beats until the reaper closes it.

Diagram
PeerLivenessPolicyGateway(reaper task)PeerLivenessPolicyGateway(reaper task)last_activity refreshedpeer goes silentno PONG forinterval × missed_beatsPINGPONGevaluate(last_activity, now)KEEPPINGevaluate(last_activity, now)REAPclose LIVENESS_TIMEOUT
PieceOwnerRole
EventType.PING / EventType.PONGProtocolWire frame both peers agree on
LivenessPolicy.evaluate(last_activity, now)Core (pure)Returns KEEP or REAP
WebSocketGateway._liveness_loopWrapper serverEmits PING, evaluates policy, closes reaped peers
WebSocketGateway._client_last_seen[client_id]Wrapper serverPer-connection fallback clock so sessionless peers still age out
WebSocketClient._heartbeat_loopReference clientSends PING, force-closes the socket after 2× silence for backoff/resume to reconnect
GatewayCloseCode.LIVENESS_TIMEOUTProtocolTyped close code the client can recognise

A connection is reaped once now > last_activity + interval_seconds × missed_beats_before_reap. Half-open peers with no bound session yet (a stalled pre-hello/join handshake) are evaluated against a per-connection _client_last_seen clock, so silent handshake sockets age out instead of leaking forever.

The runtime is on by default: the config’s enabled=False no longer disables it. Only interval_ms=0 yields a disabled policy, after which the loop stays inert and nothing is reaped.


Health Output

WebSocketGateway.health() surfaces the reaper’s state when the task is running:

FieldTypeDescription
liveness.enabledboolTrue while the reaper is emitting PING and evaluating
liveness.interval_msintCadence being used (same value advertised as heartbeat_ms)
liveness.missed_beats_before_reapintHow many intervals of silence trigger a reap
liveness.reaped_connectionsintCumulative half-open peers closed with LIVENESS_TIMEOUT
json
"liveness": {
  "enabled": true,
  "interval_ms": 30000,
  "missed_beats_before_reap": 2,
  "reaped_connections": 4
}

LIVENESS_TIMEOUT close code

The reaped peer sees a WebSocket close with:

  • Code: 4002 (application-defined, private-use 4000–4999 range)
  • Reason: "liveness_timeout" (equal to GatewayCloseCode.LIVENESS_TIMEOUT.value)

Custom clients should treat this as expected and reconnect. The reference client’s heartbeat loop usually force-reconnects first — after 2 × heartbeat_ms of silence it closes its own socket so the backoff/resume path re-establishes the connection before the server reaps it.


Configuration Options

LivenessConfig is the user-facing config; to_policy() bridges it to the pure LivenessPolicy the reaper consumes.

Card

Full field, type, and default reference


Common Patterns

Liveness runs at the defaults; pick a cadence from the peer’s network profile, or opt out entirely with interval_ms=0.

Diagram

Keep defaults

Yes

Mobile / NAT

LAN / low-latency

Mixed

Why disable?

Tune the cadence?

interval_ms=30000
missed_beats=2

Peer network?

interval_ms=5000
missed_beats=2

interval_ms=15000
missed_beats=3

Explicit opt-out
interval_ms=0

Out-of-box defaults

python
from praisonai_bot.gateway import WebSocketGateway

gateway = WebSocketGateway()  # liveness on: 30s × 2 beats

Aggressive reaping for high-turnover realtime

python
from praisonaiagents.gateway import GatewayConfig, LivenessConfig
from praisonai_bot.gateway import WebSocketGateway

gateway = WebSocketGateway(
    config=GatewayConfig(
        liveness=LivenessConfig(interval_ms=5_000, missed_beats_before_reap=2)
    )
)

Fully disabled

python
from praisonaiagents.gateway import GatewayConfig, LivenessConfig
from praisonai_bot.gateway import WebSocketGateway

gateway = WebSocketGateway(
    config=GatewayConfig(liveness=LivenessConfig(enabled=False, interval_ms=0))
)
# PING/reap is off, but the housekeeping tick still runs so pressure eviction stays active.

Best Practices

Only opt out with interval_ms=0

enabled=False alone does not disable liveness — the runtime overrides it and synthesises an enabled policy from the config’s window. To fully turn liveness off, set interval_ms=0, which yields a disabled policy and keeps the reaper loop inert.

Alert on liveness.reaped_connections

health() exposes a cumulative liveness.reaped_connections counter. A sudden burst of reaps signals an upstream network problem (flaky peers, NAT timeouts, or a proxy dropping idle sockets) — watch the rate, not just the total.

Honour the advertised interval on custom clients

The server advertises heartbeat_ms in hello_ok; when liveness is enabled (the default) this equals the policy’s interval_ms. The reference client already runs _heartbeat_loop: it pings every interval and force-closes the socket after 2 × heartbeat_ms of silence so backoff/resume reconnects. Custom clients should derive the same 2× silence window from the advertised value — miss it and the server closes you with 4002.

Treat LIVENESS_TIMEOUT as expected

A reaped peer sees GatewayCloseCode.LIVENESS_TIMEOUT (value "liveness_timeout", close code 4002) in the WebSocket close reason. Match on it to reconnect and resume — don’t surface it as a fatal error to the user.


Related