{"record":{"id":"67a08bd61fec1545","repo":"MemPalace/mempalace","slug":"writable-daemon-startup-refused-another-writer-ow","errorCode":null,"errorMessage":"writable daemon startup refused: another writer owns local backend {resolved_backend!r} for {palace_path!r}; stop the existing writable MCP/direct/daemon owner, or route all writes through that owner","messagePattern":"writable daemon startup refused: another writer owns local backend (.+?) for (.+?); stop the existing writable MCP/direct/daemon owner, or route all writes through that owner","errorType":"exception","errorClass":"DaemonError","httpStatus":null,"severity":"critical","filePath":"mempalace/daemon.py","lineNumber":898,"sourceCode":"    if backend:\n        os.environ[\"MEMPALACE_BACKEND_EXPLICIT\"] = backend\n        os.environ[\"MEMPALACE_BACKEND\"] = backend\n    # Privacy by architecture: tighten the umask to owner-only BEFORE the queue\n    # DB is created. SQLite's WAL/SHM sidecars hold un-checkpointed verbatim\n    # payloads and are (re)created with the process umask on every open/close\n    # cycle, so the umask must already be tight when DaemonRuntime builds the\n    # QueueStore (its _init_db opens the DB in WAL mode) — not only once the HTTP\n    # server starts. Restored in the finally at the end of run_server.\n    prev_umask = os.umask(0o077)\n    runtime = None\n    writer_lease = contextlib.ExitStack()\n    try:\n        resolved_backend = resolve_backend_name(palace_path, explicit=backend)\n        if backend_requires_single_writer(resolved_backend):\n            try:\n                writer_lease.enter_context(mine_palace_lock(palace_path))\n            except MineAlreadyRunning as exc:\n                raise DaemonError(\n                    \"writable daemon startup refused: another writer owns \"\n                    f\"local backend {resolved_backend!r} for {palace_path!r}; \"\n                    \"stop the existing writable MCP/direct/daemon owner, or \"\n                    \"route all writes through that owner\"\n                ) from exc\n\n        token = ensure_token(palace_path)\n        # Backend resolution above is only the ownership decision. Preserve\n        # the caller's explicit/implicit distinction in queued payloads:\n        # DaemonRuntime historically injects a backend only when one was\n        # explicitly selected.\n        runtime = DaemonRuntime(palace_path, backend=backend)\n    except BaseException:\n        writer_lease.close()\n        _restore_server_process_state(previous_env, prev_umask)\n        raise\n\n    class _Handler(BaseHTTPRequestHandler):","sourceCodeStart":880,"sourceCodeEnd":916,"githubUrl":"https://github.com/MemPalace/mempalace/blob/06cb6987f02610784fefbad4b2bd5d026d164ba6/mempalace/daemon.py#L880-L916","documentation":"DaemonError raised in run_server (mempalace/daemon.py:898) when the resolved backend requires single-writer ownership (backend_requires_single_writer) and mine_palace_lock raises MineAlreadyRunning — i.e. another writable process (MCP server, direct CLI writer, or daemon) already holds the palace lock. The daemon refuses to start rather than risk concurrent writers corrupting a local backend like ChromaDB/SQLite.","triggerScenarios":"Starting a second daemon (mempalace daemon start / start_daemon) while a writable MCP server or a direct write command holds the palace lock; a previous daemon that didn't shut down cleanly (orphan holding the lock); running CLI writes concurrently with daemon startup; a stale lock from a crashed process that still looks alive by pid.","commonSituations":"Claude Code MCP server running with write access while the user manually starts the daemon; CI jobs racing on the same palace directory; a forgotten background daemon; a wedged process after a laptop sleep/restore.","solutions":["Find and stop the existing writer: check the daemon pid file and MCP processes (ps aux | grep mempalace), stop the writable MCP/direct owner, then retry daemon startup.","Route all writes through the single owner instead of starting a second one — point hooks/CLI at the running daemon.","If the owner is a zombie/orphan, kill its pid (verify it is really a mempalace process first) and retry.","If you genuinely need concurrent processes, use a backend that doesn't require single-writer ownership."],"exampleFix":"# before: second writer while MCP owns the palace\nclient = start_daemon(palace_path)  # DaemonError: startup refused\n\n# after: stop the MCP/direct writer first, or reuse the running daemon\nclient = get_client_if_running(palace_path)\nif client is None:\n    client = start_daemon(palace_path)","handlingStrategy":"try-catch","validationCode":"from mempalace.daemon import get_client_if_running\n\n# reuse the running owner instead of starting a second writer\nclient = get_client_if_running(palace_path)\nif client is None:\n    # safe to attempt startup only when no owner answers\n    ...","typeGuard":null,"tryCatchPattern":"from mempalace.daemon import DaemonError\n\ntry:\n    client = start_daemon(palace_path)\nexcept DaemonError as exc:\n    if \"another writer owns\" in str(exc):\n        # stop the other owner or route writes through it; do not force a second daemon\n        client = get_client_if_running(palace_path)\n    else:\n        raise","preventionTips":["Designate exactly one writable owner per palace (daemon or MCP), enforce it in startup scripts.","On startup races, retry get_client_if_running before attempting to start again.","Clean up orphaned mempalace processes after crashes before restarting."],"tags":["daemon","locking","concurrency","single-writer","startup"],"backgroundTag":null,"analyzedSha":"06cb6987f02610784fefbad4b2bd5d026d164ba6","analyzedAt":"2026-08-15T03:03:36.213Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}