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.

Diagram

Global Operator Brake

engage reason, actor

is_engaged

is_engaged

is_engaged

held

held

held

untouched

👤 Operator

💾 Durable Sentinel
gateway.pause

📥 WebSocket Inbound

📥 Kanban Dispatch

📥 Scheduler Tick

🚫 New work refused

🤖 In-flight runs

✅ Finish normally

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

Diagram
In-flight RunAdmission SeamFileEmergencyStop(sentinel)OperatorIn-flight RunAdmission SeamFileEmergencyStop(sentinel)Operatorwrites durable sentinelnew work refusedexisting run keeps going -> finishesadmissions resume — no restartengage(reason, actor)is_engaged()Truedisengage()is_engaged()False
PieceRole
EmergencyStopConfigUser-facing knob — selects off or file, wired as control=…
EmergencyStopProtocolPure contract: is_engaged / engage / disengage / state
NullEmergencyStopDefault backend for "off" — never engaged, inert
FileEmergencyStopDurable backend for "file" — JSON sentinel, atomic write, fail-safe reads
EmergencyStopStateFrozen 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.

Sentinel stateis_engaged()state().engagedstate().reason
MissingFalseFalse""
Present, valid JSON objectTrueTrueas written
Present, unreadable / corrupt / permission errorTrueTrue"unreadable-sentinel"
Valid JSON object, non-numeric at (e.g. [])TrueTrue"unreadable-sentinel"
python
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.

OptionTypeDefaultDescription
backendstr"off""off" (no-op, default) or "file" (durable sentinel). Any other value raises ValueError.
pathOptional[str]NoneSentinel 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


When to Enable

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

Diagram

Single-node dev

Production, no operator control

Production with on-call
needing an instant hold

Do operators need an
instant, durable hold?

backend=’off’
default, no config

backend=’off’
default, no config

backend=’file’
durable sentinel


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)

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

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.

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.

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.

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