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.

python
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.

Diagram

Supervisor Actions

Gateway Exit Codes

transient error

fatal config error

clean shutdown

🛰️ Gateway

75 EX_TEMPFAIL\n🔄 Restart me

78 EX_CONFIG\n🛑 Stop — fix config first

0\n✅ OK

systemd/k8s\nrestarts

systemd stops\n RestartPreventExitStatus=78

No action

Quick Start

  1. Start the gateway normally
    bash
    praisonai gateway start --agents my_agents.yaml

    The process exits with code 0 on clean shutdown, 75 on transient failure (supervisor should restart), and 78 on fatal config error (supervisor should alert, not restart).

  2. 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
  3. 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 CodeConstantMeaningSupervisor action
0GATEWAY_OK_EXIT_CODEClean shutdown (signal received or graceful stop)None
75GATEWAY_RESTART_EXIT_CODETransient failure — network blip, upstream timeoutRestart
78GATEWAY_FATAL_CONFIG_EXIT_CODEFatal misconfiguration — fix config before restartingStop

Exit codes 75 (EX_TEMPFAIL) and 78 (EX_CONFIG) follow the BSD sysexits.h standard, which most Linux supervisors understand natively.

  • Exit 75 is also emitted by LoopWatchdog when a wedged event loop is detected.
  • Startup-phase wedge (PR #5266 / issue #5265): exit 75 is also emitted by StartupWatchdog when the startup deadline expires before confirm_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 Python arm(deadline_s=…) in a custom entry point (no CLI/YAML knob yet). See Startup Watchdog.
  • Shutdown-phase deadline (PR #5084 / issue #5079): the LoopWatchdog shutdown-phase deadline fires os._exit(75) if graceful drain does not complete within drain_timeout + gateway.watchdog.shutdown_grace. The stack-dump header reads “graceful shutdown did not complete within ~Ns” instead of the loop-wedge header. Configured via gateway.watchdog.shutdown_grace (YAML) or stop(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 meant telegram:. Previously warned and started; now fails closed with exit 78.
  • Missing token: on a tokenful platform — the token: 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 via praisonai.channels / praisonai.bots entry points, and isn’t registered at runtime via register_platform() fails closed. To validate against your custom platform, register it before calling WebSocketGateway.start_with_config().
  • --agents file with non-mapping entries
  • Unresolved tool in the config (strict tool pre-flight) — a tool named in gateway.yaml that cannot be resolved (typo, uninstalled optional package, or a gated local tools.py). Strict is the default; pass --no-strict-tools or set strict_tools: false to warn-and-continue instead. See Pre-flight tool check.
  • FatalConfigError raised by a plugin or adapter during startup

Transient failures (exit 75)

Everything else — network errors, upstream timeouts, unexpected exceptions during operation — exits 75.

Diagram

FatalConfigError\nor config/dep issue

Everything else

Exception type

Exit 78\nStop supervisor

Exit 75\nRestart supervisor

Startup wedge

Loop wedge

Shutdown-phase deadline


Common Patterns

Raise FatalConfigError from a custom plugin

python
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

bash
#!/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
fi

Classify exits programmatically

python
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

ini
[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.target

Reload and enable:

bash
systemctl daemon-reload
systemctl enable --now praisonai-gateway

How It Works

Diagram
ClassifierGatewaySupervisorClassifierGatewaySupervisoralt[Config is broken][Transient failure][Clean stop]Start processLoad gateway.yaml / –agentsFatalConfigError / ValueErrorExit code 78exit(78)Stop — do not restartOSError / RuntimeError / etc.Exit code 75exit(75)Restart after RestartSecKeyboardInterrupt / SIGTERMexit(0)Mark success — do not restart

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

ConstantValuesysexits.hWhenSupervisor should
GATEWAY_OK_EXIT_CODE0EX_OKClean shutdown (SIGTERM, Ctrl+C, normal stop)Not restart
GATEWAY_RESTART_EXIT_CODE75EX_TEMPFAILTransient failure (network blip, dependency restart)Restart
GATEWAY_FATAL_CONFIG_EXIT_CODE78EX_CONFIGMisconfiguration (bad YAML, missing --agents, missing deps)Stop and alert
python
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):

  • exc is None
  • exc is KeyboardInterrupt (includes SIGTERM mapped to KeyboardInterrupt)
  • exc is SystemExit(0) or SystemExit(None)
  • exc is SystemExit(n) where n is any integer — passes n through unchanged

Fatal (78 — do not restart):

  • FatalConfigError raised explicitly anywhere in the start path
  • gateway.yaml missing, empty, or schema-invalid (load_gateway_config raises ValueError)
  • --agents file missing or unreadable
  • agents: key absent or falsy in the agents YAML
  • agents: is not a list (e.g. agents: {name: bad})
  • agents: list contains a non-mapping entry (e.g. agents: ["bad"]) — previously triggered AttributeError and crash-looped at code 75; now correctly raises FatalConfigError and exits 78
  • Unknown or typo’d channel platform (e.g. telegran:) — previously warned and started; now raises ValueError from 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, not register_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-tools or strict_tools: false
  • Missing required gateway dependencies at boot (e.g. pip install praisonai[api] not run)

Transient (75 — ask supervisor to restart):

  • Any Exception not 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:

ini
[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=78

After 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:

xml
<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:

bat
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

ini
[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.target

With --config for multi-bot mode:

ini
[Service]
ExecStart=/usr/local/bin/praisonai serve gateway --config /etc/praisonai/gateway.yaml
Restart=on-failure
RestartSec=2
RestartPreventExitStatus=78

Kubernetes

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:

yaml
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-config

s6 / runit

Place this finish script alongside your run script. $1 is the exit code from run:

sh
#!/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 0

For runit (finish):

sh
#!/bin/sh
# $1 = exit code, $2 = signal
if [ "$1" = "78" ]; then
    # Signal the runsvdir to down this service
    sv down .
fi

Embedding the Classifier

Use classify_exit_reason directly when you run the gateway start path from your own code:

python
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:

python
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.gateway is 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

bash
#!/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

yaml
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 75

RestartForceExitStatus=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

yaml
spec:
  restartPolicy: OnFailure
  containers:
    - name: gateway
      image: praisonai/gateway:latest
      command: ["praisonai", "gateway", "start"]
      env:
        - name: PRAISONAI_AGENTS_FILE
          value: /config/agents.yaml

Add a liveness probe alert on exit code 78:

yaml
livenessProbe:
  exec:
    command: ["test", "-f", "/tmp/gateway.ready"]
  initialDelaySeconds: 10
  periodSeconds: 30

s6 / runit

bash
#!/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
fi

Best 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.

ini
RestartPreventExitStatus=78
Treat 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.

sh
# 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.

bash
# In your CI pipeline, before deploying:
praisonai serve gateway --config gateway.yaml
# Exit 78 will fail the pipeline step
Don't swallow FatalConfigError without re-raising

Catching FatalConfigError and not re-raising converts exit 78 back to 75 and re-enables the crash-loop.

python
# 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.

ini
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=5
Use 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