9.7 KiB
Gateway / API Server / OpenWebUI Restart & Recovery Runbook
Purpose
Use this runbook when code changes affecting gateway, api_server, managed downloads, or the OpenWebUI runs proxy need to go live safely.
This document answers four operator questions:
- Do these changes need a restart?
- How do we restart without causing restart loops or half-broken state?
- What should we verify before and after restart?
- How do we diagnose and recover when restart fails or keeps flapping?
Does this work require restart?
Yes for the current workstream.
These changes modify live Python code paths:
gateway/platforms/api_server.pyhermes_cli/managed_downloads.py/Users/hermes/.hermes/scripts/openwebui_runs_proxy.py
If the gateway and/or the OpenWebUI runs proxy are already running, their existing Python processes will keep serving the old code until they are restarted.
Live findings from this session
Verified during runtime inspection:
http://127.0.0.1:8642/healthreturned200http://127.0.0.1:8653/healthreturned200http://127.0.0.1:8646/line/webhook/healthreturned200launchctl print gui/$(id -u)/ai.hermes.gatewayshowed the active LaunchAgent still running:gateway run --replaceruns = 41forks = 2400last terminating signal = Killed: 9
Interpretation:
- the service is currently healthy
- but the long-lived LaunchAgent configuration is risky because
--replacecan create self-replacement churn in a supervised service - this should be corrected before or as part of the next controlled reload
Critical launchd rule learned from this rollout
When the LaunchAgent plist itself changes, launchctl kickstart -k <label> is not enough to guarantee the live job adopts the new ProgramArguments.
Use these rules instead:
- use
kickstart -kwhen you only want to restart the already-loaded job definition - use
bootout ...plist+bootstrap ...plistwhen you changed the plist and need launchd to load the new definition
Real evidence from this rollout:
- after removing
--replacefrom~/Library/LaunchAgents/ai.hermes.gateway.plist, a plainkickstart -k gui/$(id -u)/ai.hermes.gatewaystill left the live arguments ongateway run --replace - only after
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/ai.hermes.gateway.plistpluslaunchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.hermes.gateway.plistdidlaunchctl printshow live arguments updated to plaingateway run
Safe rollout sequence
Phase A — preflight (do not restart yet)
- Confirm modified code compiles.
- Run targeted tests for:
tests/gateway/test_api_server.pytests/gateway/test_session_api.pytests/gateway/test_openwebui_runs_proxy_handoff.py
- Confirm current health endpoints before touching launchd:
8642/health8653/health8646/line/webhook/health
- Record current listeners / PIDs:
- port
8642 - port
8653
- port
- Confirm whether the gateway LaunchAgent still contains
--replace.
Completion criteria:
- tests pass
- current runtime is healthy
- we know whether launchd config itself needs correction
Phase B — fix the launchd steady-state configuration
For a long-lived LaunchAgent, prefer:
gateway run
Avoid in steady-state launchd config:
gateway run --replace
Reason:
--replaceis appropriate for explicit operator takeover flows- but in a persistent
RunAtLoad + KeepAliveLaunchAgent it increases the risk of self-replacement races, SIGTERM churn, and restart flapping
Phase C — reload in a controlled order
- Update the LaunchAgent plist if it still contains
--replace. - If the plist changed, reload the LaunchAgent definition with
bootout + bootstrapinstead of relying onkickstart -k. - Verify the new gateway process command line no longer includes
--replace. - Re-check
8642/healthbefore treating OpenWebUI as recovered. - Re-check
8653/healthand run a real OpenWebUI proxy smoke request.
Completion criteria:
- gateway healthy on
8642 - runs proxy healthy on
8653 - live command line matches expected startup arguments
Phase C1 — exact commands for plist-changing reloads
Gateway:
uid=$(id -u)
launchctl bootout gui/${uid} ~/Library/LaunchAgents/ai.hermes.gateway.plist
launchctl bootstrap gui/${uid} ~/Library/LaunchAgents/ai.hermes.gateway.plist
OpenWebUI runs proxy:
uid=$(id -u)
launchctl bootout gui/${uid} ~/Library/LaunchAgents/ai.hermes.openwebui-runs-proxy.plist
launchctl bootstrap gui/${uid} ~/Library/LaunchAgents/ai.hermes.openwebui-runs-proxy.plist
Recommended health wait loops:
for i in {1..30}; do
if curl -fsS http://127.0.0.1:8642/health; then
echo "gateway healthy on try ${i}"
break
fi
sleep 1
done
for i in {1..30}; do
if curl -fsS http://127.0.0.1:8653/health; then
echo "proxy healthy on try ${i}"
break
fi
sleep 1
done
Observed in this real rollout:
- gateway recovered on try
13 - runs proxy recovered on try
2
Post-restart verification checklist
Core health
8642/healthreturns2008653/healthreturns2008646/line/webhook/healthreturns200lsofshows one expected listener on8642lsofshows one expected listener on8653
Launchd / process state
launchctl print gui/$(id -u)/ai.hermes.gatewayshowsgateway runwithout--replace- PID changed only once during the controlled restart
- no immediate follow-up SIGTERM / Killed:9 churn appears in logs
Functional checks
- non-streaming
/v1/responsesstill rewrites local artifact paths to managed download URLs - streaming
/v1/responsesno longer leaks/Users/...orC:\...in live deltas /v1/chat/completionsstreaming still emits normal assistant text and keepalives- OpenWebUI proxy rewrites local file paths to clickable managed download links
download_link_mode=private/publicstill propagates correctly
Failure modes and how to diagnose them
1. Service does not come back at all
Symptoms:
8642/healthfails- no listener on
8642 - launchd job exists but no stable PID
Check first:
gateway.error.loggateway.loglaunchctl print gui/$(id -u)/ai.hermes.gateway- command line / working directory / venv path in the plist
Most likely causes:
- syntax/import error in changed Python code
- bad venv path or stale interpreter path
- environment mismatch
- port bind failure
2. Service comes back, then keeps restarting
Symptoms:
runs/forksclimb quickly inlaunchctl print- repeated
Received SIGTERM last terminating signal = Killed: 9or similar
Check first:
- whether the LaunchAgent still contains
--replace - whether another helper job is repeatedly kickstarting the service
- whether a watchdog or restart helper is racing the main LaunchAgent
Most likely causes:
- supervised launchd service using
--replace - duplicate restart authority (helper + launchd + watchdog)
- repeated crash on startup with KeepAlive immediately reviving it
3. Gateway is healthy but OpenWebUI still broken
Symptoms:
8642/healthis healthy8653is unhealthy or returns bad stream behavior
Check first:
- runs proxy process / listener on
8653 - runs proxy logs
- whether the proxy is still serving old code
- whether it can reach upstream
8642
Most likely causes:
- proxy not restarted after code change
- upstream base URL mismatch
- missing dependency / import in the proxy process
- old proxy process still running
4. Health endpoints are fine but behavior is stale
Symptoms:
- routes work, but old behavior remains
- new download-link mode / streaming rewrite behavior not visible
Check first:
- live PID start times
- whether the file on disk matches the process that launchd is running
- whether only one process instance exists per port
Most likely causes:
- service not actually restarted
- edited one file, but launchd runs another path
- stale long-lived proxy/gateway process
- the job was
kickstart -krestarted but the changed plist definition was never reloaded, so launchd kept the old arguments
Recommended diagnostic order when restart fails
- Confirm health endpoints and listeners.
- Inspect launchd runtime state (
launchctl print). - If the plist changed, confirm whether the job was truly reloaded with
bootout + bootstraprather than merelykickstart -krestarted. - Inspect recent gateway stdout/stderr logs with timestamps around the restart.
- Verify the exact source file on disk contains the expected fix.
- If needed, stop auto-restart pressure and run the gateway/proxy once in foreground with the same venv and env vars.
- Only after the foreground process is healthy, restore launchd supervision.
Anti-flap rules
- Do not combine multiple independent restart authorities unless one is clearly primary.
- Do not leave
--replaceinside a steady-state launchd job. - Do not restart blindly when health endpoints are already good; first confirm whether the issue is stale behavior vs runtime outage.
- Do not declare success from static code inspection alone; always verify the live listener and health endpoint.
Rollback rule
If restart introduces instability:
- stop the restart loop source
- revert to the last known-good commit / script version
- start the service once in foreground or via a controlled launchd reload
- verify health and behavior before restoring automatic supervision
Current recommendation for this environment
Before the next production restart of the gateway:
- remove
--replacefrom~/Library/LaunchAgents/ai.hermes.gateway.plist - keep the gateway in
RunAtLoad + KeepAlive, but with plaingateway run - restart in a controlled sequence
- verify both
8642and8653 - smoke-test streaming and managed-download rewriting immediately after restart