fix(gateway): self-heal state file PID on server startup

After /restart on Windows, the gateway process gets a new PID (os.execv
creates a new process on Windows), but the state file at
run/gateway.<suffix>.json still contains the old PID from the initial
background spawn.  Nothing rewrites it, leaving state inconsistent.

Add GatewayRuntime.refresh_state_pid() — a classmethod that reads the
existing state file, updates the PID and identity to the current
process, and writes atomically.  Call it early in _run_gateway() so the
state file is always correct regardless of how the process was started
(initial spawn, os.execv, or subprocess.Popen).

On POSIX os.execv preserves the PID, so this is a no-op in normal
operation there, but still beneficial after any unusual restart path.

Fixes #4511
@
This commit is contained in:
dajiaohuang
2026-07-06 15:29:11 +08:00
committed by Xubin Ren
parent 105230cc34
commit 67b56cba74
2 changed files with 30 additions and 0 deletions
+11
View File
@@ -1306,6 +1306,17 @@ def _run_gateway(
raise typer.Exit(1) from exc
session_manager = SessionManager(config.workspace_path)
# Self-heal the gateway state file with the current PID after any restart.
from nanobot.gateway.runtime import GatewayRuntime, GatewayRuntimePaths
GatewayRuntime.refresh_state_pid(
paths=GatewayRuntimePaths.for_instance(
workspace=str(config.workspace_path)
if not is_default_workspace(config.workspace_path)
else None,
config_path=config.config_path,
)
)
# Preserve existing single-workspace installs, but keep custom workspaces clean.
if is_default_workspace(config.workspace_path):
_migrate_cron_store(config)
+19
View File
@@ -127,6 +127,25 @@ class GatewayRuntime:
self._subprocess_run = subprocess_run
self._sleep = sleep
@classmethod
def refresh_state_pid(cls, *, paths: GatewayRuntimePaths) -> None:
"""Update the PID in an existing state file to ``os.getpid()``.
Called early in gateway server startup so the state file self-heals
after any restart, regardless of platform or restart mechanism.
"""
if not paths.state_path.exists():
return
try:
state = json.loads(paths.state_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return
state["pid"] = os.getpid()
rt = cls(paths=paths)
state["identity"] = rt._process_identity(os.getpid())
state["started_at"] = _utc_now()
rt._write_state(state)
def start_background(self, options: GatewayStartOptions) -> RuntimeResult:
"""Start gateway as a detached background process."""
current = self.status()