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

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

```

## 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
```

***

## Restart Scope

| Changed section                        | Effect                                                                |
| -------------------------------------- | --------------------------------------------------------------------- |
| `agents.*`                             | Recreate agents only — channels keep running                          |
| `channels.<name>.*`                    | Restart only that channel                                             |
| `channels.<name>.unknown_user_policy`  | Re-read policy and rebuild `BotConfig` — re-fires the startup warning |
| `channels.<name>.owner_user_id`        | Re-read owner and rebuild `BotConfig`                                 |
| `provider.*`, `guardrails.*`           | Recreate agents                                                       |
| `scheduler.*`, `routes.*`, `routing.*` | Full channel restart                                                  |
| Entire `channels` section              | 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>
  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`.              |

### 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"`.

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

***

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