{"record":{"id":"492028e1ffb21611","repo":"MemPalace/mempalace","slug":"daemon-endpoint-missing-port","errorCode":null,"errorMessage":"daemon endpoint missing port","messagePattern":"daemon endpoint missing port","errorType":"exception","errorClass":"DaemonError","httpStatus":null,"severity":"error","filePath":"mempalace/daemon.py","lineNumber":1110,"sourceCode":"    for stale in (endpoint_path(palace_path), pid_path(palace_path)):\n        try:\n            stale.unlink()\n        except OSError:\n            pass\n    for key, value in previous_env.items():\n        if value is None:\n            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.","sourceCodeStart":1092,"sourceCodeEnd":1128,"githubUrl":"https://github.com/MemPalace/mempalace/blob/06cb6987f02610784fefbad4b2bd5d026d164ba6/mempalace/daemon.py#L1092-L1128","documentation":"DaemonError from DaemonClient.__init__ (mempalace/daemon.py:1110): endpoint.json was read successfully but contains no 'port' key. The endpoint file records where the daemon listens; a port-less file means it was written by an incompatible/older version, was hand-edited, or was truncated in a way that still parses as JSON.","triggerScenarios":"A stale endpoint.json from an older daemon version that recorded only host/pid; manual editing of the file; a JSON object like {} left behind by a failed write or test; concurrent format change after upgrade without cleanup.","commonSituations":"Upgrading MemPalace while an old endpoint.json persists in the state dir; test fixtures writing minimal endpoint files; state dirs shared/copied between machines.","solutions":["Stop the daemon if any is running, delete the stale endpoint.json in the state dir, and restart so it is rewritten in the current format.","If you control the endpoint file (tests), always include both 'port' and 'pid' keys.","Check the daemon log for the startup that produced the file and confirm version match between client and daemon."],"exampleFix":"# before: hand-written endpoint\n{\"host\": \"127.0.0.1\"}\n\n# after: let the daemon write it (delete stale file and restart)\n# {\"host\": \"127.0.0.1\", \"port\": 52341, \"pid\": 12345}","handlingStrategy":"validation","validationCode":"import json\nfrom mempalace import daemon\n\ndef endpoint_has_port(palace_path: str) -> bool:\n    try:\n        return \"port\" in json.loads(daemon.endpoint_path(palace_path).read_text())\n    except (OSError, json.JSONDecodeError):\n        return False","typeGuard":"def is_valid_endpoint(ep: object) -> bool:\n    return isinstance(ep, dict) and isinstance(ep.get(\"port\"), int)","tryCatchPattern":"from mempalace.daemon import DaemonError\n\ntry:\n    client = DaemonClient(palace_path)\nexcept DaemonError:\n    # stale/incompatible endpoint — clear it and restart the daemon\n    daemon.endpoint_path(palace_path).unlink(missing_ok=True)\n    client = start_daemon(palace_path)","preventionTips":["Never hand-edit endpoint.json; delete and let the daemon rewrite it.","In tests, always write endpoint fixtures with both 'port' and 'pid'.","Delete state-dir endpoint files after version upgrades."],"tags":["daemon","discovery","endpoint","startup"],"backgroundTag":null,"analyzedSha":"06cb6987f02610784fefbad4b2bd5d026d164ba6","analyzedAt":"2026-08-15T03:03:36.213Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}