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

> One durable, fail-safe operator brake — halt every new-work admission point instantly, without killing in-flight runs, and resume without a restart

One shared brake every new-work seam can consult — halt new admissions instantly, keep in-flight runs, resume without a restart.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Global Operator Brake"
        Op[👤 Operator] -->|engage reason, actor| Sentinel[💾 Durable Sentinel<br/>gateway.pause]
        Sentinel -->|is_engaged| WS[📥 WebSocket Inbound]
        Sentinel -->|is_engaged| KB[📥 Kanban Dispatch]
        Sentinel -->|is_engaged| SCH[📥 Scheduler Tick]
        WS -->|held| Held[🚫 New work refused]
        KB -->|held| Held
        SCH -->|held| Held
        InFlight[🤖 In-flight runs] -.->|untouched| Finish[✅ Finish normally]
    end

    classDef op fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef store fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef seam fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef held fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff

    class Op op
    class Sentinel store
    class WS,KB,SCH seam
    class Held held
    class InFlight,Finish ok
```

## Quick Start

**1. Default — no config, behaviour unchanged**

A gateway with no `control` block defaults to `backend: "off"`, a no-op brake that is byte-for-byte today's behaviour.

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

agent = Agent(name="Support", instructions="Help users")
WebSocketGateway(agent=agent).start()   # gateway.control defaults to "off" -> NullEmergencyStop
```

**2. Enable the durable file brake**

Selecting `backend: "file"` instantiates a durable sentinel that survives a restart and is shared across replicas that point at the same `path`.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonaiagents.gateway import EmergencyStopConfig, GatewayConfig
from praisonai_bot.gateway import WebSocketGateway

agent = Agent(name="Support", instructions="Help users")

gateway = WebSocketGateway(
    agent=agent,
    config=GatewayConfig(
        control=EmergencyStopConfig(
            backend="file",
            path="~/.praisonai/state/gateway.pause",
        ),
    ),
)
gateway.start()
```

**3. Or configure it in gateway.yaml**

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
gateway:
  control:
    backend: file      # "off" (default, no-op) | "file" (durable sentinel)
    path: ~/.praisonai/state/gateway.pause
```

**4. Drive the brake from Python (works today)**

Engage, inspect, and release the durable brake directly — the sentinel behaviour is available today.

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

brake = EmergencyStopConfig(
    backend="file",
    path="/var/lib/praisonai/gateway.pause",
).to_estop()

brake.engage(reason="cost spike", actor="mervin")
assert brake.is_engaged() is True

state = brake.state()
# state.engaged, state.reason, state.actor, state.at

brake.disengage()
assert brake.is_engaged() is False
```

***

## How It Works

The operator engages one durable sentinel; every admission seam consults it to refuse new work, while runs already in flight are untouched and finish normally.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Op as Operator
    participant Stop as FileEmergencyStop<br/>(sentinel)
    participant Seam as Admission Seam
    participant Run as In-flight Run

    Op->>Stop: engage(reason, actor)
    Note over Stop: writes durable sentinel
    Seam->>Stop: is_engaged()
    Stop-->>Seam: True
    Note over Seam: new work refused
    Note over Run: existing run keeps going -> finishes
    Op->>Stop: disengage()
    Seam->>Stop: is_engaged()
    Stop-->>Seam: False
    Note over Seam: admissions resume — no restart
```

| Piece | Role |
| - | - |
| `EmergencyStopConfig` | User-facing knob — selects `off` or `file`, wired as `control=…` |
| `EmergencyStopProtocol` | Pure contract: `is_engaged` / `engage` / `disengage` / `state` |
| `NullEmergencyStop` | Default backend for `"off"` — never engaged, inert |
| `FileEmergencyStop` | Durable backend for `"file"` — JSON sentinel, atomic write, fail-safe reads |
| `EmergencyStopState` | Frozen audit snapshot: `engaged`, `reason`, `actor`, `at` |

`engage` and `disengage` are idempotent — engaging twice or disengaging a missing sentinel is safe.

***

## Fail-Safe Behaviour

An ambiguous brake holds new work rather than letting it run freely: any sentinel that cannot be read with confidence counts as **engaged**.

> [!NOTE]
>
> `gateway stop` and `gateway stop --force` fail safely too: if the target process can't be signalled (e.g. it's owned by another user), the stop is reported as "not confirmed" and the PID lock is **left intact**, so the still-running gateway stays protected against a duplicate start. See [Gateway CLI — stop](https://praison.ai/docs/features/gateway-cli.md#commands).

| Sentinel state | `is_engaged()` | `state().engaged` | `state().reason` |
| - | - | - | - |
| Missing | `False` | `False` | `""` |
| Present, valid JSON object | `True` | `True` | as written |
| Present, unreadable / corrupt / permission error | `True` | `True` | `"unreadable-sentinel"` |
| Valid JSON object, non-numeric `at` (e.g. `[]`) | `True` | `True` | `"unreadable-sentinel"` |

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from pathlib import Path
from praisonaiagents.gateway import FileEmergencyStop

sentinel = Path("/tmp/demo.pause")
sentinel.write_text("{ not valid json")
brake = FileEmergencyStop(str(sentinel))
assert brake.is_engaged() is True     # ambiguous sentinel -> holds new work
assert brake.state().reason == "unreadable-sentinel"
```

***

## Configuration Options

`EmergencyStopConfig` from `praisonaiagents.gateway`.

| Option | Type | Default | Description |
| - | - | - | - |
| `backend` | `str` | `"off"` | `"off"` (no-op, default) or `"file"` (durable sentinel). Any other value raises `ValueError`. |
| `path` | `Optional[str]` | `None` | Sentinel file location. **Required** when `backend="file"`, else `ValueError`. |

Properties and methods:

* `enabled` → `bool` — `True` when `backend != "off"`.
* `to_estop()` → the concrete brake (`NullEmergencyStop` for `"off"`, `FileEmergencyStop(path)` for `"file"`).
* `to_dict()` / `from_dict()` — YAML/JSON roundtrip; `from_dict(None)` tolerates a missing block.

Wired into `GatewayConfig` as `control` and surfaces in `GatewayConfig.to_dict()` as `{"control": {"backend": "off", "path": None}}` when defaults hold.

> [card]
>
> Full field, type, and default reference for `EmergencyStopConfig`, `EmergencyStopProtocol`, `NullEmergencyStop`, `FileEmergencyStop`, and `EmergencyStopState`
>
> [Open](https://praison.ai/docs/sdk/reference/praisonaiagents/modules/feature_configs.md)

***

## When to Enable

Match the backend to whether an on-call operator needs an instant, durable hold.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Start{Do operators need an<br/>instant, durable hold?} -->|Single-node dev| Off1[backend='off'<br/>default, no config]
    Start -->|Production, no operator control| Off2[backend='off'<br/>default, no config]
    Start -->|Production with on-call<br/>needing an instant hold| File[backend='file'<br/>durable sentinel]

    classDef q fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef off fill:#7C90A0,stroke:#7C90A0,color:#fff
    classDef opt fill:#10B981,stroke:#7C90A0,color:#fff

    class Start q
    class Off1,Off2 off
    class File opt
```

***

## Backward Compatibility

`off` is the default and reproduces today's behaviour byte-for-byte — no brake is engaged, `is_engaged()` is always `False`, and no new dependency is added.

***

## Roadmap (what this ships vs. what comes next)

> [!NOTE]
>
> Kept intentionally minimal per the lightweight-and-powerful mandate: this PR ships the **shared core contract + fail-safe policy + config selector** only. The heavy bot-side wiring (WS inbound, kanban dispatch, scheduler seams) and the `praisonai gateway pause/resume` CLI belong in the wrapper/bot packages and are a natural follow-up that consumes this contract.

Today's release ships the pure protocol, the `off`/`file` selector, and the audit-safe state snapshot. Selecting `backend: "file"` instantiates the durable brake and wires it into `GatewayConfig.control`; you can already engage it and inspect `state()` from Python. Consultation at the WebSocket inbound, kanban dispatch, and scheduler seams — plus a `praisonai gateway pause/resume` CLI — is follow-up work in the bot/wrapper packages.

***

## Best Practices

> [accordion] Put the sentinel on durable, shared storage
>
> Point `path` at a location that survives a restart and is visible to every replica — a mounted volume, not a container's ephemeral `/tmp`. That is what makes the hold outlast a crash and apply fleet-wide.

> [accordion] Always pass reason and actor when engaging
>
> `engage(reason="cost spike", actor="mervin")` records who paused and why in the sentinel, surfaced by `state()` for `/health` and audit. An unlabelled hold is hard to explain later.

> [accordion] Trust the fail-safe, but alert on it
>
> An unreadable or corrupt sentinel counts as engaged on purpose — new work stays held. Treat `state().reason == "unreadable-sentinel"` as a signal to inspect the file, not as a normal engaged state.

> [accordion] Leave it off unless an operator needs the brake
>
> Single-node dev and deployments without on-call control gain nothing from a durable sentinel. Keep `backend: "off"` there for zero cost and unchanged behaviour.

***

## Related

> [card] [Gateway Turn Lock](https://praison.ai/docs/features/gateway-turn-lock.md)
>
> Serialise turns cluster-wide.

> [card] [Gateway Admission Control](https://praison.ai/docs/features/gateway-admission-control.md)
>
> Concurrency ceiling & backpressure.

> [card] [Gateway Graceful Drain](https://praison.ai/docs/features/gateway-graceful-drain.md)
>
> Drain toward shutdown — orthogonal to the brake.

> [card] [Gateway Channel Supervision](https://praison.ai/docs/features/gateway-channel-supervision.md)
>
> Per-channel pause vs. this fleet-wide brake.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.