Skip to main content
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.
from praisonaiagents import Agent

agent = Agent(name="assistant", instructions="Be helpful.")
# Session continuity keeps thread context across reconnects
agent.start("Continue where we left off")
Gateway sessions preserve pending messages and in-flight executions across disconnects, restarts, and graceful shutdowns. The user continues a chat after reconnect; session continuity restores context on the same thread.
Resumed sessions also restore the handshake’s negotiated protocol_version and the client’s advertised capabilities — see Server-side state after handshake.

Quick Start

1

Start gateway with an agent

from praisonaiagents import Agent

agent = Agent(
    name="assistant",
    instructions="Answer user messages in order.",
)

# Default: immediate cancel on shutdown — no drain_timeout needed
# praisonai gateway start --config gateway.yaml
2

Enable graceful drain (opt-in)

from praisonai.bots.botos import BotOS

bot_os = BotOS(drain_timeout=30)   # opt in; 0 or None = immediate cancel
await bot_os.start()
# ... on shutdown:
await bot_os.stop()                 # waits up to 30 s for in-flight turns

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 a status 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. Without drain_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.
PhaseWhat happens
Drain beginsaccepting → False (no new turns dispatched), is_draining → True, INFO log
While in flightPolls _running_turns every ~0.5 s up to drain_timeout; uses DrainTimeoutPolicy.should_keep_draining()
Drain ends cleanlyAll turns finished → outbox flush() called if the delivery router supports it → INFO log
Drain timeoutRemaining turns are abandoned (count returned), WARN log with reason
from praisonaiagents.gateway import DrainTimeoutPolicy, DrainDecision

policy = DrainTimeoutPolicy(drain_timeout_seconds=30)
decision: DrainDecision = policy.should_keep_draining(
    running_turns=2, seconds_elapsed=5.0,
)
# DrainDecision(keep_draining=True, reason="2 turn(s) in flight; draining")
BotOS drain properties (adapter authors):
PropertyTypeDescription
acceptingboolFalse once drain begins — gate new ingress on this
is_drainingboolTrue 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.
PhaseBehaviour
Within budgetSessions that finish are persisted via the configured session store
After budgetRemaining sessions are force-persisted with pending work; a SESSION_END event is emitted
Force-closed SESSION_END payload:
{
  "session_id": "...",
  "reason": "Force-closed during shutdown after timeout",
  "had_pending_work": true,
  "was_executing": true
}

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:
{
  "type": "status",
  "message": "Resuming processing (2 pending messages)..."
}
The gateway sets 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:
{
  "session_id": "...",
  "messages": [],
  "events": [],
  "event_cursor": 42,
  "pending_inbox": ["queued message 1", "queued message 2"],
  "is_executing": true
}
The inbox is snapshotted non-destructively: items are drained into a list and put back so live processing is unaffected.

Configuration

OptionTypeDefaultDescription
drain_timeout (YAML / CLI / BotOS kwarg)float | strNone (disabled)Total shutdown drain budget in seconds. 0/unset = immediate cancel.
drain_timeout on WebSocketGateway.stop()floatinherits from abovePer-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 set drain_timeout. No configuration change is needed.
from praisonaiagents import Agent

agent = Agent(
    name="assistant",
    instructions="Answer user messages.",
)
# No drain_timeout → SIGTERM cancels in-flight turns immediately (default)
To disable drain after enabling it in YAML, set 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

A 30-second turn needs at least drain_timeout: 30 to drain cleanly. Short values cause turns to be abandoned.
Without persistence, drained sessions cannot be resumed after restart — only logged.
Listen for had_pending_work: true or was_executing: true in audit or observability hooks to detect force-closed sessions.
Send messages faster than the agent processes them, then drop the WebSocket — pending messages should resume in order on reconnect.
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.

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