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
Restart-Safety Story
The entire point of this feature: a marker stamped withcurrent_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:- Default marker path (convention from PraisonAI #2390, not yet enforced in code):
~/.praisonai/gateway/.drain_request.json actiondefaults to"drain"if absent. Any other value (including a non-string) is ignored.- Missing / empty / non-string
epochis ignored unlessrequire_epoch=Falseis passed to the policy.
Configuration Options
DrainMarkerPolicy constructor
| Param | Type | Default | Description |
|---|---|---|---|
require_epoch | bool | True | When 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
| Param | Type | Default | Description |
|---|---|---|---|
marker | dict | None | — | Parsed marker contents, or None when no marker file is present. Non-dicts return False. |
current_epoch | str | — | The current instantiation epoch — pass the return value of current_epoch(). |
now | float | — | A monotonic timestamp. Unused by the default policy, but keeps the call site stable when subclasses add TTL/debounce. |
last_handled_epoch | str | None | None (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
| Signal | Source | Notes |
|---|---|---|
| Kernel boot id | /proc/sys/kernel/random/boot_id | Changes on every reboot. |
| PID-1 start time | Field 22 of /proc/1/stat | Changes on every boot / container (re)start. |
| Fail-closed default | "" (empty string) when either signal is unavailable | Mac / Windows / sandboxed containers without /proc return "". Every marker is foreign to an empty epoch unless require_epoch=False. |
Common Patterns
1. Operator-side: write the marker
<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
3. Accepting legacy markers without an epoch (advanced)
Best Practices
Always stamp markers with current_epoch()
Always stamp markers with current_epoch()
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 the marker atomically
Write the marker atomically
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.Pair with drain_timeout
Pair with drain_timeout
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.Don't store secrets in the marker
Don't store secrets in the marker
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.
Related
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

