> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Gateway Hot-Reload

> Edit gateway.yaml live — restart only what changed

<Note>
  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](/docs/guides/praisonai-bot-migration).
</Note>

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

agent = Agent(name="reload-agent", instructions="Hot-reload gateway configuration without downtime.")
agent.start("Reload the gateway configuration without restarting the service.")
```

The gateway diffs `gateway.yaml` against the running config and restarts only affected agents or channels. The WebSocket server keeps running.

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# gateway.yaml
agents:
  assistant:
    instructions: "You are a helpful assistant."
```

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai gateway run gateway.yaml
# Edit agents.assistant.instructions and save — only agents reload (~5s)
```

The user edits `gateway.yaml` on disk; the watcher diffs changes and reloads only affected agents or channels while the WebSocket server stays up.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    Edit[✏️ Edit gateway.yaml] --> Watcher[👁️ File watcher]
    Watcher -->|watchdog available| Event[⚡ Event-driven]
    Watcher -->|fallback| Poll[🔄 mtime polling]
    Event --> Debounce[⏱️ Debounce 1s]
    Poll --> Debounce
    Debounce --> Diff[🔍 Diff paths]
    Diff -->|HOT_APPLIABLE_KEYS| Hot[⚡ Hot-apply in place]
    Diff -->|agents.*| Agents[🔄 Recreate agents]
    Diff -->|channels.name.*| Channel[🔄 Restart one channel]
    Diff -->|routes/scheduler| Full[🔄 Full channel restart]

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef tool fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef warn fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef good fill:#10B981,stroke:#7C90A0,color:#fff

    class Edit agent
    class Watcher,Debounce,Diff tool
    class Event,Poll,Agents,Channel,Full warn
    class Hot good

```

## How It Works

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Operator
    participant Watcher
    participant Gateway as Gateway Hot-Reload
    participant Health

    Operator->>Watcher: Edit gateway.yaml
    Watcher->>Gateway: Diff + apply reload
    Gateway->>Gateway: Record ReloadStatus
    Gateway->>Health: applied_config_revision + drift
    Operator->>Health: gateway status / health()
    Health-->>Operator: reload outcome + drift
```

## Quick Start

<Steps>
  <Step title="Run the gateway">
    ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    praisonai gateway run gateway.yaml
    ```
  </Step>

  <Step title="Edit live">
    Change agent instructions or a single channel token in `gateway.yaml` and save. The watcher applies a selective reload within \~5 seconds (1s debounce). Credential fields can also be [secret references](/docs/features/gateway-secret-references) (`{source, id}`) instead of plaintext.
  </Step>

  <Step title="Trigger reload manually with SIGHUP">
    Send `SIGHUP` to reload without editing a file — useful for orchestration scripts and systemd:

    ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # Find the gateway pid, then:
    kill -HUP $(pgrep -f "praisonai gateway")

    # Or under systemd:
    systemctl reload praisonai-gateway
    ```
  </Step>
</Steps>

***

## Event-driven vs Polling

The watcher **prefers** event-driven file notifications via the optional `watchdog` package and **falls back gracefully** to mtime polling when `watchdog` is unavailable or an observer cannot start.

| Mode          | How it works                         | When used                                  |
| ------------- | ------------------------------------ | ------------------------------------------ |
| Event-driven  | OS file-system events via `watchdog` | `watchdog>=3.0.0` installed                |
| mtime polling | Periodic stat check every 5s         | `watchdog` not installed or observer fails |

Both modes apply the same 1s debounce to coalesce rapid saves.

**Install `watchdog` for faster reload detection:**

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
pip install "praisonai[gateway]"
# or
pip install "praisonai[all]"
```

<Note>
  `watchdog` is **optional** — without it, polling continues exactly as before. Install it only when faster reaction times matter.
</Note>

***

## Operator-triggered Reload via SIGHUP

`start_with_config` installs a `SIGHUP` handler that runs the same `reload_config` path as a file-change reload — no shutdown, no dropped connections.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    Sig[📡 SIGHUP] --> Handler[🔧 SIGHUP handler]
    Handler --> Reload[reload_config]
    Reload --> Diff[🔍 Diff + apply]
    Diff --> Drain[⏳ Drain in-flight turns]
    Drain --> Restart[🔄 Restart affected channels]

    classDef signal fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff

    class Sig signal
    class Handler,Reload,Diff,Drain process
    class Restart result
```

Reload via `SIGHUP` is **best-effort** — it is silently skipped on platforms without `SIGHUP` support (e.g. Windows).

***

## Drain-coordinated Channel Restart

When a reload triggers a channel restart, the gateway drains in-flight turns before bouncing the channel — no mid-conversation cuts.

The drain window for reload-triggered restarts is controlled by a new YAML key:

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
gateway:
  reload_drain_timeout: 10   # seconds; falls back to drain_timeout if unset
```

***

## Hot-Apply (no restart)

Some config paths are safe to apply *in place* — the gateway mutates the live subsystem without restarting agents or channels. When the diff only touches these paths, no drain runs, no client sees a blip.

An operator raising `gateway.reload_drain_timeout` at 3am does **not** disconnect a live conversation:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

# The agent whose transport is served by the gateway
agent = Agent(
    name="ops-bot",
    instructions="Answer ops questions. Stay connected — I'll be tuning drain timings live.",
)

agent.start("Under the hood: operators can now raise gateway.reload_drain_timeout "
            "from 10s to 30s in gateway.yaml without dropping this connection.")
```

Editing the key in `gateway.yaml` triggers the hot-apply on the next SIGHUP / file save — no restart, no client disconnect, no drain:

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# gateway.yaml — before
gateway:
  reload_drain_timeout: 10

# gateway.yaml — after (hot-applied on the next SIGHUP / file save)
gateway:
  reload_drain_timeout: 30
```

The health check confirms the change landed **without a restart** — the only changed path is hot-applied:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
payload = gateway.health()
print(payload["reload"]["changed_paths"])
# ('gateway.reload_drain_timeout',)  ← the only changed path, hot-applied
```

The full registry of hot-appliable paths:

| Path                           | Live effect                                                               | What restarts |
| ------------------------------ | ------------------------------------------------------------------------- | ------------- |
| `gateway.logging.level`        | Root/gateway logger level applied immediately                             | Nothing       |
| `gateway.drain_timeout`        | New drain window used on the next graceful drain                          | Nothing       |
| `gateway.reload_drain_timeout` | New reload-drain window used on the next reload-triggered channel restart | Nothing       |

Anything **not** in this registry falls through to the existing restart-based plans — restart is always the safe default for unknown or structural changes. Invalid values (e.g. a non-numeric timeout) are ignored so one bad key never aborts an otherwise-good reload; the applied paths appear in the reload summary line as `hot[...]` and in `health().reload.changed_paths`.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Change[✏️ Config change] --> Q{classify_reload#40;path#41;?}
    Q -->|HOT| Hot[⚡ Hot-apply in place]
    Q -->|AGENTS| Agents[🔄 Recreate agents only]
    Q -->|CHANNEL| Channel[🔄 Restart one channel]
    Q -->|FULL| Full[🔄 Full channel restart]
    Hot --> None[✅ Nothing restarts]

    classDef change fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef check fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef restart fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef good fill:#10B981,stroke:#7C90A0,color:#fff

    class Change change
    class Q check
    class Hot,None good
    class Agents,Channel,Full restart
```

***

## Restart Scope

Each *Effect* is prefixed with the canonical [`ReloadScope`](#programmatic-reload-scope-classification) value that `classify_reload(path)` returns for that section.

| Changed section                                                                  | Effect                                                                                                                                                             |
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `gateway.logging.level`, `gateway.drain_timeout`, `gateway.reload_drain_timeout` | **HOT** — hot-apply in place, no channel restart, no agent recreate (see [Hot-Apply](#hot-apply-no-restart))                                                       |
| `agents.*`                                                                       | **AGENTS** — recreate agents only; channels keep running                                                                                                           |
| `provider.*`, `guardrails.*`                                                     | **AGENTS** — recreate agents                                                                                                                                       |
| `channels.<name>.*`                                                              | **CHANNEL** — restart only that channel                                                                                                                            |
| `channels.<name>.unknown_user_policy`                                            | **CHANNEL** — re-read policy and rebuild `BotConfig` — re-fires the startup warning                                                                                |
| `channels.<name>.owner_user_id`                                                  | **CHANNEL** — re-read owner and rebuild `BotConfig`                                                                                                                |
| `scheduler.*`, `routes.*`, `routing.*`                                           | **FULL** — full channel restart                                                                                                                                    |
| `lifecycle.*`                                                                    | Rebuild scale-to-zero / drain-marker policies; cancel or relaunch only the loops whose enablement changed (`_reconcile_lifecycle`) — no channel or process restart |
| Entire `channels` section (bare)                                                 | **FULL** — full channel restart                                                                                                                                    |
| Invalid YAML on save                                                             | Keep last-known-good config                                                                                                                                        |

Full restart stops and starts all channels but **does not** restart the WebSocket server — connected clients stay connected.

<Note>
  Token-free channels survive a reload cleanly. The `local` channel is on the gateway's token-free allowlist alongside `email`, `agentmail`, and `signal`, so a `gateway reload` keeps it healthy instead of dropping it into a degraded state. Before this fix the hot-reload path (`_start_single_channel`) required a token and marked `local` degraded on every reload. See [Local (Terminal) Channel](/docs/features/local-channel) and [Degraded Channel Isolation](/docs/features/gateway-degraded-channels).
</Note>

<Note>
  Changing `unknown_user_policy` on a channel with an empty `allowed_users` re-fires the startup warning with the new policy's text ([PR #2856](https://github.com/MervinPraison/PraisonAI/pull/2856)). Existing sessions are unaffected — the change applies to inbound DMs after the reload commits.
</Note>

***

## Observability

The gateway records every reload outcome so operators can confirm the last edit took effect without scraping logs.

Reloads still log a concise summary line on completion:

```
reload applied: agents; restart[telegram]
```

The format is `reload applied: <changed-sections>; restart[<channels>]`. Grep for `reload applied` to trace all reloads in your log stream.

### Reload status in `health()`

`health()` surfaces the reload outcome and config revisions when the gateway runs from a config file:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

# Operator polls health from a running gateway
payload = gateway.health()
print(payload["reload"])
# {'watcher': 'active', 'last_result': 'ok', 'last_at': 1731801234.5,
#  'changed_paths': ['agents.assistant.instructions'], 'error': None}
print(payload["applied_config_revision"], payload["on_disk_config_revision"])
print(payload["config_drift"])   # True if a restart is still owed
```

These four keys are additive — they only appear when the gateway runs from a config file, so existing `health()` consumers see no change:

| Key                       | Type            | Description                                                                    |
| ------------------------- | --------------- | ------------------------------------------------------------------------------ |
| `reload`                  | `dict`          | Full reload status from `ReloadStatus.to_dict()` — outcome + watcher liveness. |
| `applied_config_revision` | `str` (12-char) | Revision id of the config the gateway is *actually running*.                   |
| `on_disk_config_revision` | `str` (12-char) | Revision id of the current `gateway.yaml` on disk.                             |
| `config_drift`            | `bool`          | `True` when `applied_config_revision != on_disk_config_revision`.              |

The [`pressure`](/docs/features/gateway-pressure-telemetry) block is a sibling additive `health()` key that follows this exact `to_dict()` pattern — present only when the back-pressure machinery is wired.

### ReloadStatus fields

`ReloadStatus` is a frozen dataclass describing the most recent reload attempt:

| Field           | Type                                                | Default      | Meaning                                                                                                     |
| --------------- | --------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------- |
| `watcher`       | `"active"` \| `"disabled"`                          | `"disabled"` | `"active"` while the watcher runs; `"disabled"` once it has genuinely given up. Detects silent degradation. |
| `last_result`   | `"ok"` \| `"failed"` \| `"no_changes"` \| `"never"` | `"never"`    | Outcome of the most recent reload attempt.                                                                  |
| `last_at`       | `float` \| `None`                                   | `None`       | Unix timestamp of the last reload attempt.                                                                  |
| `changed_paths` | `tuple[str, ...]`                                   | `()`         | Config paths that changed on the last successful reload.                                                    |
| `error`         | `str` \| `None`                                     | `None`       | Human-readable reason on `"failed"`; `None` otherwise.                                                      |

### Compare configs offline

`compute_config_revision` returns the same 12-character revision id used by `health()`, so you can check an on-disk config before deploying it:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.gateway import compute_config_revision
import yaml

with open("gateway.yaml") as f:
    revision = compute_config_revision(yaml.safe_load(f))
print(revision)   # e.g. 'a1b2c3d4e5f6'
```

Identical logical configs produce identical revisions regardless of key order or whitespace; an empty or `None` config returns the sentinel `"000000000000"`.

### Inspect the hot-apply registry

`HOT_APPLIABLE_KEYS` and `is_hot_appliable` are public exports from `praisonaiagents.gateway`, so you can query which paths are zero-restart before an edit:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.gateway import HOT_APPLIABLE_KEYS, is_hot_appliable

# The whole registry (frozenset[str], safe to iterate)
print(sorted(HOT_APPLIABLE_KEYS))
# ['gateway.drain_timeout', 'gateway.logging.level', 'gateway.reload_drain_timeout']

# Ask about a specific path — matches exact paths AND leaves under a registered key
is_hot_appliable("gateway.logging.level")          # True
is_hot_appliable("gateway.logging.level.extra")    # True  (leaf under a registered key)
is_hot_appliable("agents.assistant.instructions")  # False → falls through to restart plan
```

The `SupportsHotReload` protocol is `runtime_checkable` — a gateway implementation opts in by defining an `apply_hot_reload(paths, new_config) -> None` method. Third-party gateway subclasses that want the wrapper's hot-apply behaviour implement this method; core owns the classification so every runtime reloads identically.

### Programmatic reload-scope classification

`ReloadScope` and `classify_reload` are public exports from `praisonaiagents.gateway` that tell you exactly what an edit to `gateway.yaml` will do — before you save it.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonaiagents.gateway import classify_reload, ReloadScope

agent = Agent(
    name="ops-bot",
    instructions="Answer ops questions about safe gateway reloads.",
)

# Before saving gateway.yaml, ask what the edit will actually do:
scope = classify_reload("gateway.logging.level")
if scope == ReloadScope.HOT:
    agent.start("The change is hot-apply — no channel restart, no client disconnect.")
elif scope == ReloadScope.CHANNEL:
    agent.start("Only the affected channel will restart. Other channels stay connected.")
elif scope == ReloadScope.AGENTS:
    agent.start("Agents recreate in place. Channels keep running.")
else:  # ReloadScope.FULL
    agent.start("Full channel restart — expect a brief drain across all channels.")
```

The four canonical scopes are plain strings, so wrapper and runtime code can compare against them without importing the class:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.gateway import ReloadScope, classify_reload

ReloadScope.HOT       # "hot"     — apply in place, no restart
ReloadScope.CHANNEL   # "channel" — restart only channels.<name>
ReloadScope.AGENTS    # "agents"  — recreate agents only, channels keep running
ReloadScope.FULL      # "full"    — full restart (fail-safe default)

# Ask what a specific path would do before saving:
classify_reload("gateway.logging.level")            # 'hot'
classify_reload("channels.telegram.token")          # 'channel'
classify_reload("agents.assistant.instructions")    # 'agents'
classify_reload("guardrails.jailbreak.enabled")     # 'agents'
classify_reload("routes.discord")                   # 'full' — structural fallback
classify_reload("channels")                         # 'full' — bare section, fail-safe
```

`classify_reload` is canonical and side-effect-free — every runtime (core, wrapper, third-party gateway subclasses) uses the same classifier, so a pre-flight check reflects exactly what happens at reload time. Anything not explicitly recognised falls through to `ReloadScope.FULL`, keeping full restart the fail-safe default.

### Reload observability flow

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    Edit[✏️ Edit gateway.yaml] --> Watch[👁️ Watcher]
    Watch --> Diff{🔍 Diff}
    Diff -->|changed| Apply[🔄 Apply reload]
    Diff -->|unchanged| NoChange[⏸️ no_changes]
    Apply -->|success| Ok[✅ last_result=ok]
    Apply -->|error| Failed[❌ last_result=failed]
    Ok --> Rev[💾 applied_config_revision]
    Rev --> Health[📊 health.reload / drift]
    Failed --> Health
    NoChange --> Health

    classDef edit fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef watch fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef good fill:#10B981,stroke:#7C90A0,color:#fff
    classDef bad  fill:#F59E0B,stroke:#7C90A0,color:#fff

    class Edit edit
    class Watch,Diff,Apply,Rev watch
    class Ok,NoChange,Health good
    class Failed bad
```

### Inspect reload from the CLI

`praisonai gateway status` prints the reload result, watcher state, and config drift alongside the existing status:

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai gateway status
```

```
  Reload: OK  2m ago  changed=agents.assistant.instructions
  Watcher: active
  Config: a1b2c3d4e5f6 (up to date)
```

When the running config no longer matches disk, the drift is shown with both revisions:

```
  Reload: FAILED  30s ago  invalid YAML at agents.assistant
  Watcher: active
  Config: on-disk 9f8e7d6c5b4a  !=  applied a1b2c3d4e5f6   (change not in effect)
```

### When `config_drift` is true

A drift means the config on disk has not taken effect — walk this path to recover:

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    Drift{config_drift=true} --> Retry[📡 Retry SIGHUP]
    Retry --> Recheck{Still drifted?}
    Recheck -->|no| Done[✅ In effect]
    Recheck -->|yes| Reason[🔍 Check reload.error]
    Reason --> FixYaml[✏️ Fix gateway.yaml]
    FixYaml --> Restart[🔄 Full restart]

    classDef cfg fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef proc fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef good fill:#10B981,stroke:#7C90A0,color:#fff
    classDef warn fill:#F59E0B,stroke:#7C90A0,color:#fff

    class Drift,FixYaml cfg
    class Retry,Reason,Recheck proc
    class Done good
    class Restart warn
```

<Note>
  The reload machinery is additive: `ReloadStatus` and `compute_config_revision` are new exports from `praisonaiagents.gateway`, and the existing `reload applied: …` log line is unchanged. Pre-existing `health()` consumers are unaffected.
</Note>

***

## Tuning

| Setting                        | Default                               | Description                                                                                                                                                                                                    |
| ------------------------------ | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Poll interval                  | `5.0`s                                | How often the mtime watcher checks the file                                                                                                                                                                    |
| Debounce                       | `1.0`s                                | Wait after last write before applying                                                                                                                                                                          |
| `gateway.reload_drain_timeout` | falls back to `gateway.drain_timeout` | Bounded drain window before a reload-triggered channel restart. **Hot-appliable** — edits take effect on the next reload-triggered drain without restarting anything (see [Hot-Apply](#hot-apply-no-restart)). |

***

<Note>
  **Backward compatibility:** Leaving `reload_drain_timeout` unset preserves the prior immediate-restart behaviour. Not installing `watchdog` keeps polling as before. This is a fully additive change. The `HOT_APPLIABLE_KEYS` set is a closed core registry; unknown or structural changes still trigger the existing restart plans, so upgrading is drop-in and cannot silently skip a needed restart.
</Note>

***

## Best Practices

<AccordionGroup>
  <Accordion title="Prefer agent-only edits for prompt tweaks">
    Changing `agents.*` avoids dropping live Telegram/Discord sessions.
  </Accordion>

  <Accordion title="Scope channel edits narrowly">
    Edit one channel block to restart only that platform.
  </Accordion>

  <Accordion title="Validate YAML before saving">
    Invalid saves are ignored — the previous config keeps running.
  </Accordion>

  <Accordion title="Use SIGHUP in systemd for zero-downtime config pushes">
    Add `ExecReload=kill -HUP $MAINPID` to your systemd unit so `systemctl reload` triggers a drain-coordinated reload without stopping the process.
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="Bot Gateway" icon="server" href="/docs/features/bot-gateway">
    Gateway server overview
  </Card>

  <Card title="Gateway Channel Supervision" icon="shield" href="/docs/features/gateway-channel-supervision">
    Self-healing channels
  </Card>

  <Card title="Code-Skew Guard" icon="shield-halved" href="/docs/features/gateway-code-skew-guard">
    Detect in-place code updates and refuse hot operations until the process restarts.
  </Card>

  <Card title="Gateway Reliability Preset" icon="shield-check" href="/docs/features/gateway-reliability-preset">
    One switch to compose drain + admission control
  </Card>
</CardGroup>
