The gateway now ships in the
praisonai-bot package. praisonai serve gateway still works exactly as documented here; for a standalone install see praisonai-bot Migration.Quick Start
How It Works
When a client reconnects, the gateway checks whether the session has queued inbox messages or an in-flight execution. If so, it sends astatus frame, marks the session as executing before spawning the queue task (preventing duplicate workers), and processes pending messages in FIFO order.
Graceful Drain on Shutdown
Graceful drain is opt-in and default off. Withoutdrain_timeout, shutdown cancels in-flight turns immediately — today’s behaviour is preserved.
When enabled, shutdown runs two layers sequentially, sharing a single budget:
Layer 1 — Channel-Bot Drain (BotOS)
BotOS.drain() quiesces ingress and waits for running agent turns to finish.
| Phase | What happens |
|---|---|
| Drain begins | accepting → False (no new turns dispatched), is_draining → True, INFO log |
| While in flight | Polls _running_turns every ~0.5 s up to drain_timeout; uses DrainTimeoutPolicy.should_keep_draining() |
| Drain ends cleanly | All turns finished → outbox flush() called if the delivery router supports it → INFO log |
| Drain timeout | Remaining turns are abandoned (count returned), WARN log with reason |
| Property | Type | Description |
|---|---|---|
accepting | bool | False once drain begins — gate new ingress on this |
is_draining | bool | True while actively waiting for turns to finish |
Layer 2 — WebSocket Session Drain
After channel-bot drain,WebSocketGateway._drain_active_sessions persists sessions with pending inbox or in-flight execution — and now receives only the remaining budget from the total window.
| Phase | Behaviour |
|---|---|
| Within budget | Sessions that finish are persisted via the configured session store |
| After budget | Remaining sessions are force-persisted with pending work; a SESSION_END event is emitted |
SESSION_END payload:
How the Budget is Shared
drain_timeout bounds total shutdown time, not per-phase or per-bot.
Previously, the WebSocket-session drain received the full
drain_timeout again after stop_channels already consumed up to that window — total shutdown could reach 2 × drain_timeout. The budget is now tracked across both phases so the ceiling is always honoured.Reconnect and Auto-Resume
On resume, clients receive:mark_executing(True) before launching asyncio.create_task(_run_session_queue(...)), so a race between reconnect and shutdown cannot spawn duplicate queue processors.
Persisted Shape
Gateway session snapshots include pending work:Configuration
| Option | Type | Default | Description |
|---|---|---|---|
drain_timeout (YAML / CLI / BotOS kwarg) | float | str | None (disabled) | Total shutdown drain budget in seconds. 0/unset = immediate cancel. |
drain_timeout on WebSocketGateway.stop() | float | inherits from above | Per-call override for programmatic shutdown. |
drain_timeout accepts numbers or numeric strings from YAML/env (e.g. "30" is coerced to 30.0). Non-finite or non-numeric values log a warning and disable drain — no user action required.
Bypass / Disable
To keep today’s immediate-cancel behaviour, simply don’t setdrain_timeout. No configuration change is needed.
drain_timeout: 0 or remove the key.
External Drain Trigger
drain_timeout bounds the drain phase but only fires when something calls gateway.stop(). For hosted / containerised deployments where an operator or deploy step needs to ask a running gateway to drain — without exposing an inbound control port and without a stale signal wedging a restarted instance — pair it with the port-less drain trigger.
See Gateway Drain Trigger for the marker-file contract and the DrainMarkerPolicy / current_epoch() API.
Best Practices
Set drain_timeout to at least your longest expected agent turn
Set drain_timeout to at least your longest expected agent turn
A 30-second turn needs at least
drain_timeout: 30 to drain cleanly. Short values cause turns to be abandoned.Configure a session store
Configure a session store
Without persistence, drained sessions cannot be resumed after restart — only logged.
Monitor SESSION_END events
Monitor SESSION_END events
Listen for
had_pending_work: true or was_executing: true in audit or observability hooks to detect force-closed sessions.Test reconnect under load
Test reconnect under load
Send messages faster than the agent processes them, then drop the WebSocket — pending messages should resume in order on reconnect.
Gate new ingress on accepting for custom adapters
Gate new ingress on accepting for custom adapters
If you build a custom channel adapter, check
bot_os.accepting before dispatching a new turn. When False, the gateway is draining and new turns should be rejected.External drain trigger
drain_timeout bounds the drain phase but only fires when something calls gateway.stop(). For hosted / containerised deployments where an operator or deploy step needs to ask a running gateway to drain — without exposing an inbound control port and without a stale signal wedging a restarted instance — pair it with the port-less drain trigger.
See Gateway Drain Trigger for the marker-file contract and the DrainMarkerPolicy / current_epoch() API.
Scale-to-Zero and session continuity work together. When the scale-to-zero policy quiesces the gateway (stops transports), session continuity is what ensures the conversation history is preserved so users can pick up exactly where they left off after the gateway wakes. See Scale-to-Zero Gateway.
Related
Gateway
WebSocket control plane overview
Session Persistence
Persistent sessions and event replay
Error Handling
Reconnect and error recovery
Session Protocol
Session message format
Scale to Zero
Suspend when idle — session continuity makes resume work
Drain Trigger
Port-less external drain signal for hosted deployments

