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="drain-agent", instructions="Drain the gateway queue gracefully.")
agent.start("Drain all pending messages before shutting down the gateway.")
Ask a running gateway to finish in-flight turns and exit — without exposing any inbound port, and without a left-over signal wedging a restarted instance. The user drops a drain marker file; the gateway finishes in-flight turns and exits without exposing an inbound drain port.

Quick Start

1

Import the policy and epoch function

from praisonaiagents.gateway import DrainMarkerPolicy, current_epoch
2

Build the policy once at startup

policy = DrainMarkerPolicy()
epoch = current_epoch()
3

Run a watcher loop

import asyncio
import json
import os
from time import monotonic
from praisonaiagents.gateway import DrainMarkerPolicy, current_epoch

policy = DrainMarkerPolicy()
last_handled = None
path = os.path.expanduser("~/.praisonai/gateway/.drain_request.json")

async def watch(gateway):
    global last_handled
    while True:
        if os.path.exists(path):
            with open(path) as f:
                marker = json.load(f)
        else:
            marker = None
        if policy.drain_requested(
            marker,
            current_epoch(),
            monotonic(),
            last_handled_epoch=last_handled,
        ):
            last_handled = marker.get("epoch")
            await gateway.stop(drain_timeout=30)
            return
        await asyncio.sleep(1)
The built-in marker watcher and praisonai gateway drain CLI are not yet merged — write your own watcher loop for now (pattern above). This page will be updated when the follow-up PR ships.

How It Works

Restart-Safety Story

The entire point of this feature: a marker stamped with current_epoch() is silently ignored by any other instance of the gateway — even if the marker file survives a reboot on a durable volume.

Decision Tree — When Is a Marker Honoured?


The Drain Marker Contract

Write this JSON to the marker path:
{
  "epoch": "<output of current_epoch() at write time>",
  "action": "drain"
}
  • Default marker path (convention from PraisonAI #2390, not yet enforced in code): ~/.praisonai/gateway/.drain_request.json
  • action defaults to "drain" if absent. Any other value (including a non-string) is ignored.
  • Missing / empty / non-string epoch is ignored unless require_epoch=False is passed to the policy.

Configuration Options

DrainMarkerPolicy constructor

from praisonaiagents.gateway import DrainMarkerPolicy

policy = DrainMarkerPolicy(require_epoch=True)
ParamTypeDefaultDescription
require_epochboolTrueWhen True, a marker without a non-empty string epoch is ignored (fail-safe). Set False only when intentionally accepting hand-rolled or legacy markers.

DrainMarkerPolicy.drain_requested() parameters

ParamTypeDefaultDescription
markerdict | NoneParsed marker contents, or None when no marker file is present. Non-dicts return False.
current_epochstrThe current instantiation epoch — pass the return value of current_epoch().
nowfloatA monotonic timestamp. Unused by the default policy, but keeps the call site stable when subclasses add TTL/debounce.
last_handled_epochstr | NoneNone (kwarg-only)If equal to the marker’s epoch, the request is treated as already handled and ignored — a polling watcher fires only once per instantiation.

current_epoch() — what it returns

SignalSourceNotes
Kernel boot id/proc/sys/kernel/random/boot_idChanges on every reboot.
PID-1 start timeField 22 of /proc/1/statChanges on every boot / container (re)start.
Fail-closed default"" (empty string) when either signal is unavailableMac / Windows / sandboxed containers without /proc return "". Every marker is foreign to an empty epoch unless require_epoch=False.
from praisonaiagents.gateway import current_epoch

epoch = current_epoch()
# Linux: "550e8400-e29b-41d4-a716-446655440000:12345"
# Mac / Windows / no /proc: ""

Common Patterns

1. Operator-side: write the marker

import json
import os
from praisonaiagents.gateway import current_epoch

path = os.path.expanduser("~/.praisonai/gateway/.drain_request.json")
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path + ".tmp", "w") as f:
    json.dump({"epoch": current_epoch(), "action": "drain"}, f)
os.replace(path + ".tmp", path)
Writing to <path>.tmp then os.replace() makes the write atomic — a half-written JSON file is treated as malformed and ignored.

2. Gateway-side: minimal watcher loop

import asyncio
import json
import os
from time import monotonic
from praisonaiagents.gateway import DrainMarkerPolicy, current_epoch

policy = DrainMarkerPolicy()
last_handled = None
path = os.path.expanduser("~/.praisonai/gateway/.drain_request.json")

async def watch(gateway):
    global last_handled
    while True:
        if os.path.exists(path):
            with open(path) as f:
                marker = json.load(f)
        else:
            marker = None
        if policy.drain_requested(
            marker,
            current_epoch(),
            monotonic(),
            last_handled_epoch=last_handled,
        ):
            last_handled = marker.get("epoch")
            await gateway.stop(drain_timeout=30)
            return
        await asyncio.sleep(1)

3. Accepting legacy markers without an epoch (advanced)

from praisonaiagents.gateway import DrainMarkerPolicy

policy = DrainMarkerPolicy(require_epoch=False)
Only do this if you own both ends and have a different staleness story.

Best Practices

The whole restart-safety guarantee depends on it. Leave require_epoch=True (the default). An unstamped marker is silently ignored — this is intentional and protects newly started instances from stale files.
Write to <path>.tmp then os.replace() — a half-written JSON file is parsed as malformed and ignored, leaving the previous request still active. Atomic rename is the only safe pattern on most filesystems.
DrainMarkerPolicy only decides when to drain; the actual bounded wait still goes through gateway.stop(drain_timeout=...) documented on the Session Continuity page. Without a drain_timeout, shutdown cancels in-flight turns immediately.
The epoch is non-secret and opaque. Treat the marker file as world-readable convention metadata, not a control-plane secret. Anyone who can write to the marker path can trigger a drain.

The praisonai gateway drain CLI command and the built-in marker watcher in gateway/server.py are mentioned as the wrapper-side companions to this predicate in PraisonAI #2390 but are not yet merged. Until they land, the predicate is consumed by writing your own watcher (pattern 2 above). This page will be updated to document the CLI + YAML gateway.drain block when the follow-up PR ships.

Scale to Zero

ScaleToZeroPolicy — sibling idle-policy predicate

Session Continuity

drain_timeout and in-process drain

Bot Gateway

Bot Gateway overview

Gateway

Gateway and Control Plane top-level page