diff --git a/gateway/status.py b/gateway/status.py index 1616d109342cf..8b44cc441a956 100644 --- a/gateway/status.py +++ b/gateway/status.py @@ -996,6 +996,14 @@ def write_runtime_status( payload = _read_json_file(path) or _build_runtime_status_record() previous_payload = copy.deepcopy(payload) current_record = _build_pid_record() + + # Runtime status belongs to one gateway process. A fresh process must not + # inherit fatal adapters, active work, or restart flags from its predecessor. + previous_identity = (payload.get("pid"), payload.get("start_time")) + current_identity = (current_record.get("pid"), current_record.get("start_time")) + if gateway_state == "starting" and previous_identity != current_identity: + payload = _build_runtime_status_record() + payload.setdefault("platforms", {}) payload["kind"] = current_record["kind"] payload["pid"] = current_record["pid"] diff --git a/tests/gateway/test_status.py b/tests/gateway/test_status.py index eea8e05a0a48c..9acc31648cdbe 100644 --- a/tests/gateway/test_status.py +++ b/tests/gateway/test_status.py @@ -192,6 +192,42 @@ def test_write_runtime_status_overwrites_stale_pid_on_restart(self, tmp_path, mo assert payload["pid"] == os.getpid(), "PID should be overwritten, not preserved via setdefault" assert payload["start_time"] != 1000.0, "start_time should be overwritten on restart" + def test_starting_new_process_resets_stale_runtime_fields(self, tmp_path, monkeypatch): + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + state_path = tmp_path / "gateway_state.json" + state_path.write_text(json.dumps({ + "pid": 111, + "start_time": 1000, + "kind": "hermes-gateway", + "gateway_state": "running", + "restart_requested": True, + "active_agents": 3, + "served_profiles": ["default", "coder"], + "platforms": { + "coder:slack": { + "state": "fatal", + "error_code": "duplicate_credential", + } + }, + "updated_at": "2025-01-01T00:00:00Z", + })) + monkeypatch.setattr(status, "_build_pid_record", lambda: { + "pid": 222, + "kind": "hermes-gateway", + "argv": ["hermes", "gateway", "run"], + "start_time": 2000, + "hermes_home": str(tmp_path), + }) + + status.write_runtime_status(gateway_state="starting", exit_reason=None) + + payload = status.read_runtime_status() + assert payload["pid"] == 222 + assert payload["platforms"] == {} + assert payload["active_agents"] == 0 + assert payload["restart_requested"] is False + assert "served_profiles" not in payload + def test_runtime_status_running_pid_rejects_pid_reused_by_other_profile(self, monkeypatch): """Regression (user report): a stale profile's recycled PID must not be @@ -1219,4 +1255,3 @@ def _runtime_pid(runtime, *, expected_home=None): # expected_home is what stops a recycled PID belonging to another # profile's live gateway from being reported as this profile's. assert seen["expected_home"] == profile_dir -