Bug Description
The POSIX stdio MCP watchdog can classify a live Hermes parent as orphaned and terminate the MCP server process group.
The watchdog records psutil.Process(parent_pid).create_time() when it starts and later requires exact float equality. The public Process.create_time() value is not a stable process-identity contract when the system clock or Linux boot-time basis changes.
This was observed on Windows 11 with WSL2 Ubuntu while both Hermes Gateway parent processes remained alive. The incorrect decision is also reproducible deterministically by holding the PPID constant while returning a different public create-time value.
Steps to Reproduce
-
Run this deterministic probe against the current implementation:
from unittest.mock import patch
from tools import mcp_stdio_watchdog as watchdog
with (
patch.object(watchdog.psutil, "pid_exists", return_value=True),
patch.object(watchdog.psutil, "Process") as process,
):
process.return_value.create_time.return_value = 101.0
print(watchdog._is_orphaned(4242, 100.0, getppid=lambda: 4242))
-
The result is True even though the direct parent PID is unchanged and still exists.
-
On WSL2, run a Gateway with a stdio MCP server and sample the Gateway's public create_time() using fresh psutil.Process(pid) instances over time.
-
If the public value drifts, the next watchdog poll follows the same deterministic branch, sends SIGTERM to the MCP process group, and Hermes reconnects the server with a new generation.
Expected Behavior
A watchdog that remains a direct child of the same live Hermes process should not terminate its MCP child process group.
Actual Behavior
In one observed failure window:
- profile A's stdio MCP connection advanced from generation 1 to generation 2;
- profile B's stdio MCP connection advanced from generation 1 to generation 2;
- the generation-1 watchdog exited with
returncode=-15, signal=15, parent_alive=False;
- both Gateway MainPIDs remained active;
- profile A's same MainPID create time changed from
1783705305.82 to 1783705306.82;
- profile B's same MainPID create time changed from
1783705305.76 to 1783705306.76.
The resulting chain was:
create_time mismatch -> false orphan -> MCP SIGTERM -> closed stdio transport -> reconnect and generation increment.
Root Cause Analysis
As of upstream main 02063ece119648eeb8e7603c857b03aceda22ffc, tools/mcp_stdio_watchdog.py compares a freshly read public Process.create_time() value with the epoch value captured by tools/mcp_tool.py.
psutil issue giampaolo/psutil#2526 and merged PR giampaolo/psutil#2527 explain that the public create_time() value may change when the system clock changes. The psutil fix made its internal process identity monotonic; it did not make the public epoch timestamp immutable.
For this watchdog, the process is a direct POSIX child of Hermes. The kernel-maintained relationship is sufficient:
def _is_orphaned(original_ppid: int, getppid=os.getppid) -> bool:
return getppid() != original_ppid
When the Hermes parent exits, the existing watchdog is reparented and its PPID changes. Reusing the old numeric PID cannot attach the existing watchdog to a new process.
Proposed Fix
- Remove the internal
--create-time watchdog argument.
- Stop sampling and comparing
psutil.Process.create_time() in the MCP watchdog path.
- Use the original direct PPID as the parent-liveness contract.
- Keep the existing process-group termination, signal forwarding, stdio pass-through, and non-POSIX behavior unchanged.
I have a focused patch and regression tests ready and am willing to submit a PR.
Environment
- Hermes Agent 0.18.2
- Windows 11 + WSL2 Ubuntu
- Python 3.11.15
- psutil 7.2.2
- Affected component: Tools / MCP stdio transport and Gateway usage
Related Work
- Hermes PR #42198 handles an unreadable create-time sentinel in the slash worker, not runtime drift in the MCP watchdog.
- Hermes PR #60211 addresses MCP stream ownership and event-loop shutdown.
- Hermes PR #32962 is a broader WSL2 subprocess-resilience change and does not remove this exact comparison.
- Hermes PR #61364 covers desktop backend parent death and currently uses the same public create-time assumption on a different surface.
Bug Description
The POSIX stdio MCP watchdog can classify a live Hermes parent as orphaned and terminate the MCP server process group.
The watchdog records
psutil.Process(parent_pid).create_time()when it starts and later requires exact float equality. The publicProcess.create_time()value is not a stable process-identity contract when the system clock or Linux boot-time basis changes.This was observed on Windows 11 with WSL2 Ubuntu while both Hermes Gateway parent processes remained alive. The incorrect decision is also reproducible deterministically by holding the PPID constant while returning a different public create-time value.
Steps to Reproduce
Run this deterministic probe against the current implementation:
The result is
Trueeven though the direct parent PID is unchanged and still exists.On WSL2, run a Gateway with a stdio MCP server and sample the Gateway's public
create_time()using freshpsutil.Process(pid)instances over time.If the public value drifts, the next watchdog poll follows the same deterministic branch, sends SIGTERM to the MCP process group, and Hermes reconnects the server with a new generation.
Expected Behavior
A watchdog that remains a direct child of the same live Hermes process should not terminate its MCP child process group.
Actual Behavior
In one observed failure window:
returncode=-15,signal=15,parent_alive=False;1783705305.82to1783705306.82;1783705305.76to1783705306.76.The resulting chain was:
create_timemismatch -> false orphan -> MCP SIGTERM -> closed stdio transport -> reconnect and generation increment.Root Cause Analysis
As of upstream main
02063ece119648eeb8e7603c857b03aceda22ffc,tools/mcp_stdio_watchdog.pycompares a freshly read publicProcess.create_time()value with the epoch value captured bytools/mcp_tool.py.psutil issue giampaolo/psutil#2526 and merged PR giampaolo/psutil#2527 explain that the public
create_time()value may change when the system clock changes. The psutil fix made its internal process identity monotonic; it did not make the public epoch timestamp immutable.For this watchdog, the process is a direct POSIX child of Hermes. The kernel-maintained relationship is sufficient:
When the Hermes parent exits, the existing watchdog is reparented and its PPID changes. Reusing the old numeric PID cannot attach the existing watchdog to a new process.
Proposed Fix
--create-timewatchdog argument.psutil.Process.create_time()in the MCP watchdog path.I have a focused patch and regression tests ready and am willing to submit a PR.
Environment
Related Work