Gateway Exit Codes
Restart-intent exit codes that let systemd, Kubernetes, and s6 distinguish a recoverable blip from a fatal misconfiguration
praisonai serve gateway exits with a specific code to tell your supervisor whether to restart or stop — a misconfigured gateway should not crash-loop forever.
from praisonaiagents import Agent
agent = Agent(name="assistant", instructions="Serve users via the gateway.")
agent.start("Start the gateway under systemd supervision.")The user runs the gateway under a supervisor; exit codes signal whether to restart, stop for config fixes, or exit cleanly.
Quick Start
- Start the gateway normally
bash praisonai gateway start --agents my_agents.yamlThe process exits with code
0on clean shutdown,75on transient failure (supervisor should restart), and78on fatal config error (supervisor should alert, not restart). - Systemd service with correct restart policy
ini [Unit] Description=PraisonAI Gateway After=network.target [Service] ExecStart=/usr/local/bin/praisonai gateway start --agents /etc/praisonai/agents.yaml Restart=on-failure RestartSec=5 # Restart on transient failure (75), but not on fatal config error (78) RestartForceExitStatus=75 SuccessExitStatus=0 [Install] WantedBy=multi-user.target - Kubernetes restart policy
yaml apiVersion: v1 kind: Pod spec: restartPolicy: OnFailure containers: - name: praisonai-gateway image: praisonai/gateway:latest command: ["praisonai", "gateway", "start", "--agents", "/etc/praisonai/agents.yaml"] lifecycle: postStart: exec: command: ["/bin/sh", "-c", "echo Gateway started"]For exit code 78, set an alert in your monitoring system and do not auto-restart.
Exit Code Reference
| Exit Code | Constant | Meaning | Supervisor action |
|---|---|---|---|
0 | GATEWAY_OK_EXIT_CODE | Clean shutdown (signal received or graceful stop) | None |
75 | GATEWAY_RESTART_EXIT_CODE | Transient failure — network blip, upstream timeout | Restart |
78 | GATEWAY_FATAL_CONFIG_EXIT_CODE | Fatal misconfiguration — fix config before restarting | Stop |
Exit codes 75 (EX_TEMPFAIL) and 78 (EX_CONFIG) follow the BSD sysexits.h standard, which most Linux supervisors understand natively.
- Exit
75is also emitted byLoopWatchdogwhen a wedged event loop is detected. - Startup-phase wedge (PR #5266 / issue #5265): exit
75is also emitted byStartupWatchdogwhen the startup deadline expires beforeconfirm_loop_live(). The stack-dump header reads “gateway startup did not confirm a live event loop within ~Ns” — distinct from both the loop-wedge and shutdown-phase headers. Armed via Pythonarm(deadline_s=…)in a custom entry point (no CLI/YAML knob yet). See Startup Watchdog. - Shutdown-phase deadline (PR #5084 / issue #5079): the
LoopWatchdogshutdown-phase deadline firesos._exit(75)if graceful drain does not complete withindrain_timeout + gateway.watchdog.shutdown_grace. The stack-dump header reads “graceful shutdown did not complete within ~Ns” instead of the loop-wedge header. Configured viagateway.watchdog.shutdown_grace(YAML) orstop(grace=…)(Python). See Loop Watchdog → Shutdown-phase deadline mode.
What Triggers Each Code
Fatal configuration (exit 78)
The gateway exits 78 when it detects a problem that restarting won’t fix:
- Missing required dependencies (e.g. platform SDK not installed)
- Missing or unreadable
gateway.yaml - Schema-invalid
gateway.yaml(type errors, unknown keys) - Unknown / typo’d channel platform — e.g.
telegran:when you meanttelegram:. Previously warned and started; now fails closed with exit 78. - Missing
token:on a tokenful platform — thetoken:key must be present; an empty${VAR}resolution is tolerated (the channel is skipped), but a missing key is fatal. - Plugin platform not on the registry — a
platform:name that isn’t built-in, isn’t published viapraisonai.channels/praisonai.botsentry points, and isn’t registered at runtime viaregister_platform()fails closed. To validate against your custom platform, register it before callingWebSocketGateway.start_with_config(). --agentsfile with non-mapping entries- Unresolved tool in the config (strict tool pre-flight) — a tool named in
gateway.yamlthat cannot be resolved (typo, uninstalled optional package, or a gated localtools.py). Strict is the default; pass--no-strict-toolsor setstrict_tools: falseto warn-and-continue instead. See Pre-flight tool check. FatalConfigErrorraised by a plugin or adapter during startup
Transient failures (exit 75)
Everything else — network errors, upstream timeouts, unexpected exceptions during operation — exits 75.
Common Patterns
Raise FatalConfigError from a custom plugin
from praisonaiagents.gateway.protocols import FatalConfigError
class MyPlugin:
def startup(self, config):
if not config.get("api_key"):
raise FatalConfigError("MyPlugin: 'api_key' is required in gateway.yaml")Detect exit code in a shell script
#!/bin/bash
praisonai serve gateway --config gateway.yaml
CODE=$?
if [ "$CODE" -eq 78 ]; then
echo "Fatal config error — alert on-call team" >&2
send_alert "gateway misconfigured" &
exit 1
elif [ "$CODE" -eq 75 ]; then
echo "Transient failure — will restart" >&2
fiClassify exits programmatically
from praisonaiagents.gateway import classify_exit_reason, FatalConfigError
try:
run_gateway()
except Exception as exc:
code = classify_exit_reason(exc)
raise SystemExit(code)Supervisor Configuration Reference
systemd
[Unit]
Description=PraisonAI Gateway
After=network.target
[Service]
ExecStart=/usr/local/bin/praisonai serve gateway
Restart=on-failure
RestartSec=2
# A typo in gateway.yaml exits 78 — don't crash-loop, page instead
RestartPreventExitStatus=78
SuccessExitStatus=0
[Install]
WantedBy=multi-user.targetReload and enable:
systemctl daemon-reload
systemctl enable --now praisonai-gatewayHow It Works
The classifier (classify_exit_reason) is a pure function with no side effects. It lives in the core package so the wrapper CLI (praisonai serve gateway) and the runtime entry point (python -m praisonai.runtime) share one source of truth.
Exit Code Reference
| Constant | Value | sysexits.h | When | Supervisor should |
|---|---|---|---|---|
GATEWAY_OK_EXIT_CODE | 0 | EX_OK | Clean shutdown (SIGTERM, Ctrl+C, normal stop) | Not restart |
GATEWAY_RESTART_EXIT_CODE | 75 | EX_TEMPFAIL | Transient failure (network blip, dependency restart) | Restart |
GATEWAY_FATAL_CONFIG_EXIT_CODE | 78 | EX_CONFIG | Misconfiguration (bad YAML, missing --agents, missing deps) | Stop and alert |
from praisonaiagents.gateway import (
GATEWAY_OK_EXIT_CODE, # 0
GATEWAY_RESTART_EXIT_CODE, # 75
GATEWAY_FATAL_CONFIG_EXIT_CODE, # 78
)Fatal vs Transient
classify_exit_reason(exc) applies these rules in order:
Clean exit (0):
excisNoneexcisKeyboardInterrupt(includes SIGTERM mapped toKeyboardInterrupt)excisSystemExit(0)orSystemExit(None)excisSystemExit(n)wherenis any integer — passesnthrough unchanged
Fatal (78 — do not restart):
FatalConfigErrorraised explicitly anywhere in the start pathgateway.yamlmissing, empty, or schema-invalid (load_gateway_configraisesValueError)--agentsfile missing or unreadableagents:key absent or falsy in the agents YAMLagents:is not a list (e.g.agents: {name: bad})agents:list contains a non-mapping entry (e.g.agents: ["bad"]) — previously triggeredAttributeErrorand crash-looped at code 75; now correctly raisesFatalConfigErrorand exits 78- Unknown or typo’d channel platform (e.g.
telegran:) — previously warned and started; now raisesValueErrorfrom the registry-aware validator and exits 78 - Missing
token:key on a tokenful platform — previously warned; now fatal (an empty${VAR}resolution is tolerated and the channel is skipped) - Plugin
platform:not on the registry (not built-in, not an entry point, notregister_platform()d) — exits 78 - Unresolved tool named in the config under the strict tool pre-flight (default
--strict-tools) —raise typer.Exit(78); opt out with--no-strict-toolsorstrict_tools: false - Missing required gateway dependencies at boot (e.g.
pip install praisonai[api]not run)
Transient (75 — ask supervisor to restart):
- Any
Exceptionnot matched by the rules above
Supervisor Integration
Installed daemons honour these exit codes automatically
praisonai-bot gateway install (and praisonai onboard) generate a unit that already stops on exit 78, so a broken gateway.yaml never crash-loops. Add RestartPreventExitStatus=78 yourself only when you hand-write the unit.
systemd (generated)
~/.config/systemd/user/praisonai-bot.service:
[Unit]
Description=PraisonAI Bot Service
After=network.target
StartLimitIntervalSec=60
StartLimitBurst=5
[Service]
Type=notify
NotifyAccess=main
WatchdogSec=30
Restart=on-failure
RestartSec=5
SuccessExitStatus=0
RestartPreventExitStatus=78After PR #6125, praisonai-bot gateway install writes a Type=notify unit so systemd gates start on real readiness and restarts a wedged event loop via WatchdogSec. See Gateway Systemd Readiness & Watchdog.
StartLimitIntervalSec / StartLimitBurst also bound restart storms on transient (75) failures.
launchd (generated)
~/Library/LaunchAgents/ai.praison.bot.plist:
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key><false/>
<key>Crashed</key><true/>
</dict>
<key>ThrottleInterval</key>
<integer>30</integer>launchd has no RestartPreventExitStatus equivalent — the exit-78 no-crash-loop guarantee comes from ThrottleInterval=30 plus the runtime’s RestartLoopGuard, not from launchd stopping the service outright.
Windows (generated)
The PraisonAIGateway scheduled task runs a generated .cmd wrapper that translates 78 to a clean exit:
if "%ERRORLEVEL%"=="78" (
echo PraisonAI gateway stopped: fatal config error 78. Fix the config and re-start.
exit /b 0
)Windows Task Scheduler has no exit-code-specific stop either — the wrapper maps 78 to 0 so the task’s re-run policy sees a clean exit.
Hand-written units
systemd
[Unit]
Description=PraisonAI Gateway
After=network.target
[Service]
ExecStart=/usr/local/bin/praisonai serve gateway
Restart=on-failure
RestartSec=2
# Exit 78 = fatal config; don't crash-loop, page the operator instead
RestartPreventExitStatus=78
SuccessExitStatus=0
# Optional: capture logs
StandardOutput=journal
StandardError=journal
SyslogIdentifier=praisonai-gateway
[Install]
WantedBy=multi-user.targetWith --config for multi-bot mode:
[Service]
ExecStart=/usr/local/bin/praisonai serve gateway --config /etc/praisonai/gateway.yaml
Restart=on-failure
RestartSec=2
RestartPreventExitStatus=78Kubernetes
Kubernetes restartPolicy: OnFailure restarts on any non-zero exit, so exit 78 will still restart by default. Surface the fatal-config state via an init container or a wrapper script:
apiVersion: v1
kind: Pod
spec:
restartPolicy: OnFailure
initContainers:
- name: validate-config
image: your-praisonai-image
command: ["sh", "-c"]
args:
- |
praisonai serve gateway --config /config/gateway.yaml --check-only
if [ $? -eq 78 ]; then
echo "FATAL: gateway.yaml is invalid — fix before deploying"
exit 1
fi
volumeMounts:
- name: gateway-config
mountPath: /config
containers:
- name: gateway
image: your-praisonai-image
command: ["praisonai", "serve", "gateway", "--config", "/config/gateway.yaml"]
volumeMounts:
- name: gateway-config
mountPath: /config
volumes:
- name: gateway-config
configMap:
name: gateway-configs6 / runit
Place this finish script alongside your run script. $1 is the exit code from run:
#!/bin/sh
# finish — called by s6/runit after 'run' exits
# $1 = exit code from run
if [ "$1" = "78" ]; then
# Fatal config — operator must fix gateway.yaml
# Tell s6 to stop this service permanently (don't auto-restart)
s6-svc -O .
fi
# For any other code (including 75), exit 0 to let the supervisor restart
exit 0For runit (finish):
#!/bin/sh
# $1 = exit code, $2 = signal
if [ "$1" = "78" ]; then
# Signal the runsvdir to down this service
sv down .
fiEmbedding the Classifier
Use classify_exit_reason directly when you run the gateway start path from your own code:
import sys
from praisonaiagents.gateway import classify_exit_reason, FatalConfigError
def run_my_gateway():
# ... your gateway startup logic ...
pass
try:
run_my_gateway()
except Exception as exc:
sys.exit(classify_exit_reason(exc))Signal “stop restarting me” from anywhere in a custom start path by raising FatalConfigError:
from praisonaiagents.gateway import FatalConfigError
def validate_my_config(path):
if not config_is_valid(path):
raise FatalConfigError(f"Invalid config at {path} — fix before restarting")Backward Compatibility
The wrapper (praisonai serve gateway) imports the exit-code symbols from praisonaiagents.gateway at startup. Two fallback rules apply:
- If
praisonaiagents.gatewayis absent (ModuleNotFoundError) or predates the protocol (missing symbols →AttributeError), the wrapper uses local sysexits.h values (0/75/78) and a minimal classifier with the same semantics. - Any other
ImportError(a broken core install) surfaces immediately — broken cores are no longer silently swallowed.
Pre-PR #2439 wrappers returned None from start(); the new int return is backward-compatible — callers that ignored the return value continue to work and see exit 0 semantics from the shell.
s6-overlay
#!/bin/execlineb -P
# /etc/s6-overlay/s6-rc.d/gateway/run
foreground { praisonai serve gateway --config /etc/praisonai/gateway.yaml }
importas -u ? ?
ifelse { test ${?} -eq 78 } { s6-svscanctl -t /var/run/s6/services }Docker Compose
services:
gateway:
image: praisonai/gateway:latest
command: serve gateway --config /config/gateway.yaml
restart: on-failure
volumes:
- ./gateway.yaml:/config/gateway.yaml
[Service]
ExecStart=praisonai gateway start --agents /etc/praisonai/agents.yaml
Restart=on-failure
RestartSec=5
RestartForceExitStatus=75
SuccessExitStatus=0 75RestartForceExitStatus=75 tells systemd to restart even when Restart=on-failure would not normally trigger (e.g. the process exits quickly). Exit code 78 is NOT in RestartForceExitStatus, so systemd will not restart on fatal config errors.
Kubernetes
spec:
restartPolicy: OnFailure
containers:
- name: gateway
image: praisonai/gateway:latest
command: ["praisonai", "gateway", "start"]
env:
- name: PRAISONAI_AGENTS_FILE
value: /config/agents.yamlAdd a liveness probe alert on exit code 78:
livenessProbe:
exec:
command: ["test", "-f", "/tmp/gateway.ready"]
initialDelaySeconds: 10
periodSeconds: 30s6 / runit
#!/bin/sh
# /etc/services.d/praisonai-gateway/run
exec praisonai gateway start --agents /etc/praisonai/agents.yaml
# /etc/services.d/praisonai-gateway/finish
#!/bin/sh
# $1 = exit code
if [ "$1" -eq 78 ]; then
echo "Fatal config error — alerting operator"
touch /tmp/praisonai-config-error
exit 125 # s6: do not restart
fiBest Practices
Pin RestartPreventExitStatus=78 in systemd (hand-written units only)
This applies only to units you write yourself — praisonai-bot gateway install and praisonai onboard already set it. Without it, a typo in gateway.yaml restarts a hand-written unit indefinitely, burning through CPU, log storage, and pager budget.
RestartPreventExitStatus=78Treat exit 78 as a paging event
Exit 75 is normal supervisor noise — the gateway will come back. Exit 78 means a human deployed a broken config and the gateway will never come back on its own.
# Example: alert on 78 from journald
journalctl -u praisonai-gateway -f | grep "Fatal config"Validate gateway.yaml in CI before rollout
A 78 at boot is cheap. A 78 caught mid-rollout after rolling out to 10 pods is not.
# In your CI pipeline, before deploying:
praisonai serve gateway --config gateway.yaml
# Exit 78 will fail the pipeline stepDon't swallow FatalConfigError without re-raising
Catching FatalConfigError and not re-raising converts exit 78 back to 75 and re-enables the crash-loop.
# Wrong — crash-loops forever on a bad config
try:
run_my_gateway()
except FatalConfigError:
pass # Don't do this
# Right — let the supervisor see 78
try:
run_my_gateway()
except FatalConfigError as exc:
sys.exit(78)Always set RestartPreventExitStatus=78 in hand-written systemd units
The installed unit (praisonai-bot gateway install / praisonai onboard) already sets this. In a hand-written unit without it, systemd restarts a misconfigured gateway in a tight crash loop, filling logs and burning CPU. Exit 78 is the standard way to say “don’t restart me”.
Raise FatalConfigError for unrecoverable plugin errors
If your plugin needs a required API key or a database connection that can’t be deferred, raise FatalConfigError at startup so the operator knows to fix the config before the gateway will run.
Monitor for exit 78 in your alerting
A 78 exit in production means a deployment broke the config. Wire it to your on-call alerting so it isn’t silently swallowed by a restart loop.
Exit 0 on clean shutdown
The gateway exits 0 when it receives a shutdown signal (SIGTERM, /drain endpoint). Supervisors that restart on any non-zero exit will leave the gateway alone after a graceful stop.
Never treat exit code 78 as a transient failure
Code 78 means the configuration is broken. Auto-restarting on 78 creates a restart loop that burns CPU without making progress. Always alert an operator first.
Add RestartSec to avoid restart storms
When restarting on exit code 75, add at least 5 seconds between restarts to avoid hammering a degraded upstream.
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=5Use FatalConfigError for missing secrets
At startup, validate that all required API keys and config values are present. Raise FatalConfigError immediately rather than letting the process crash mid-run.
Log the exit code in your monitoring system
Route stdout/stderr to your log aggregator and alert on exit_code=78 to catch config regressions in CI/CD pipelines.
Related
Architecture and startup sequence
Gateway CLICommand-line interface for the gateway
Gateway Code-Skew GuardDetect in-place updates and prompt a restart
Gateway Error HandlingHow the gateway handles runtime errors
Event-Loop WatchdogAnother source of EX_TEMPFAIL (75) — a wedged asyncio loop
Another source of EX_TEMPFAIL (75) — a wedged pre-loop startup