{"record":{"id":"a230fa3e2eab61bd","repo":"MemPalace/mempalace","slug":"daemon-endpoint-pid-is-not-alive","errorCode":null,"errorMessage":"daemon endpoint pid is not alive","messagePattern":"daemon endpoint pid is not alive","errorType":"exception","errorClass":"DaemonError","httpStatus":null,"severity":"error","filePath":"mempalace/daemon.py","lineNumber":1117,"sourceCode":"            os.environ.pop(key, None)\n        else:\n            os.environ[key] = value\n\n\nclass DaemonClient:\n    def __init__(self, palace_path: str):\n        self.palace_path = canonical_palace_path(palace_path)\n        endpoint = _read_endpoint(self.palace_path)\n        port = endpoint.get(\"port\")\n        if port is None:\n            raise DaemonError(\"daemon endpoint missing port\")\n        # Don't read the token until we trust the endpoint points at a live\n        # process we started: a stale endpoint whose pid is dead may have its\n        # port reused by an unrelated process, and we must not send our bearer\n        # token there.\n        pid = endpoint.get(\"pid\")\n        if pid is not None and not _pid_alive(int(pid)):\n            raise DaemonError(\"daemon endpoint pid is not alive\")\n        self.token = read_token(self.palace_path)\n        self.host = endpoint.get(\"host\") or HOST\n        self.port = int(port)\n        # The daemon is always on 127.0.0.1, so a request must never go through\n        # an HTTP proxy. Building an opener with an empty ProxyHandler bypasses\n        # urllib's proxy discovery entirely. On macOS that discovery\n        # (urllib.request._scproxy, via the SystemConfiguration framework) runs\n        # on the first request to any host and is NOT bounded by the per-request\n        # timeout — on a CI runner with no network it can hang for tens of\n        # seconds, which looks exactly like the daemon never came up. A no-proxy\n        # opener is the correct production choice here and also removes that hang.\n        self._opener = urlrequest.build_opener(urlrequest.ProxyHandler({}))\n\n    @property\n    def base_url(self) -> str:\n        return f\"http://{self.host}:{self.port}\"\n\n    def request(","sourceCodeStart":1099,"sourceCodeEnd":1135,"githubUrl":"https://github.com/MemPalace/mempalace/blob/06cb6987f02610784fefbad4b2bd5d026d164ba6/mempalace/daemon.py#L1099-L1135","documentation":"DaemonError from DaemonClient.__init__ (mempalace/daemon.py:1117): endpoint.json records a pid, but _pid_alive() reports that process no longer exists. The daemon is dead; the endpoint is stale. Critically, the client refuses to proceed because the dead daemon's port may since have been reused by an unrelated process, and sending the bearer token there would leak it — so the token is deliberately not read before this check.","triggerScenarios":"Daemon crashed or was killed after writing endpoint.json; machine rebooted leaving a stale endpoint; pid recycled by the OS to another process is the danger case being guarded; constructing DaemonClient long after the daemon exited.","commonSituations":"Hooks firing after a laptop sleep/reboot where the daemon died; CI runners between jobs; daemon OOM-killed; get_client_if_running normally swallows this as None, so a direct DaemonClient() call exposes it.","solutions":["Remove the stale endpoint.json and start a fresh daemon via start_daemon/ensure_client.","Prefer ensure_client(palace_path) or get_client_if_running() over raw DaemonClient() — they handle restart and None-fallback correctly.","If the daemon should be alive, check the pid file, daemon log, and port to diagnose the crash."],"exampleFix":"# before\nclient = DaemonClient(palace_path)  # stale endpoint, pid dead\n\n# after\nclient = get_client_if_running(palace_path) or start_daemon(palace_path)","handlingStrategy":"fallback","validationCode":"import json\nfrom mempalace import daemon\n\ndef daemon_alive(palace_path: str) -> bool:\n    try:\n        ep = json.loads(daemon.endpoint_path(palace_path).read_text())\n    except (OSError, json.JSONDecodeError):\n        return False\n    pid = ep.get(\"pid\")\n    return pid is not None and daemon._pid_alive(int(pid))","typeGuard":null,"tryCatchPattern":"from mempalace.daemon import DaemonError, get_client_if_running, start_daemon\n\nclient = get_client_if_running(palace_path)  # None when pid is dead\nif client is None:\n    daemon.endpoint_path(palace_path).unlink(missing_ok=True)\n    client = start_daemon(palace_path)","preventionTips":["After crashes/reboots, sweep stale endpoint files before constructing clients.","Use get_client_if_running/ensure_client instead of DaemonClient directly.","Never send the bearer token to an endpoint whose pid you haven't verified."],"tags":["daemon","stale-state","pid","security","startup"],"backgroundTag":null,"analyzedSha":"06cb6987f02610784fefbad4b2bd5d026d164ba6","analyzedAt":"2026-08-15T03:03:36.213Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}