ADR-017: The daemon's background lifecycle lives under memcastle daemon, and serve stays the foreground server¶
Status¶
Accepted, amends ADR-011, ADR-012 and ADR-014 (the commands they name moved)
Context¶
Running the daemon was spread over four top-level words with unequal footing:
serveran it in the foreground, anddaemonwas an alias for it.stopasked it to shut down.restartstopped it, spawned a detachedserveand waited until that was serving, and quietly doubled as the only way to start a daemon in the background.- Nothing started one in the background on purpose, so a person who only wanted their terminal back had to learn that "restart" was the answer.
The alias made daemon read as a synonym for the foreground server, while the word is the natural name for the
background process and for the group of commands that manage it.
db already shows the shape (db start, db stop, db status), so a flat stop and restart beside a daemon
alias were the odd ones out.
The constraints are the invariants:
- The CLI has no business logic MCP and HTTP cannot reuse (invariant 1).
Starting a process is not business logic, but it must stay clear of
storeandjobs. - One daemon per embedded palace (invariant 4): a second
serveon a held palace only dies on the file lock, after a failed startup. - A supervisor needs a foreground process.
systemd
Type=simple, launchd and containers all want to own the process, not a parent that exits.
Decision¶
daemonis a command group:daemon start,daemon stopanddaemon restart.servestays what it was, minus itsdaemonalias: the daemon in the foreground, logging to standard error, which is what a supervisor runs.stopandrestartare removed from the top level, with no alias. The project is pre-release, so this is a breaking change made once, as in ADR-011 and ADR-015, not a compatibility layer kept for good. A barememcastle daemonnames no action and prints help instead of starting something.daemon startspawnsservedetached and waits until it is serving. "Serving" is the registry file appearing with a live daemon behind it, the signal every other command discovers the daemon by, not the process merely existing. It is the spawn-and-wait half of whatrestartalready did, now reachable on its own.daemon startfails when a daemon already answers for the palace. The error ismemcastle::client::already_running, naming the address and pointing atdaemon restartandstatus. A second daemon would die on the palace lock after the cost of a failed startup, and "start" succeeding without having started anything would hide that the flags it was given were ignored.daemon restartis unchanged in behaviour, including starting a daemon when none is running.daemon startanddaemon restartshareserve's flags and pass them,--configand the resolved--palaceon.- A background daemon is detached for real.
Standard streams go to null, and the process leaves the launching terminal's group (its own process group on Unix,
DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUPon Windows). Otherwise a Ctrl-C in the terminal that randaemon startwould kill the daemon it had just started. - The CLI's reach does not grow.
daemon startanddaemon restartuseserver::lifecycleandDaemonClient, and touch neitherstorenorjobs;daemon stopis only aDaemonClientcall. Invariant 1 now namesdaemon startanddaemon restartwhere it namedrestart, and thestore-isolationhook still holds without change. statusstays top-level. It is the command a script runs without knowing whether a daemon exists, anddaemon statuswould only be a second spelling of it.
Alternatives rejected¶
- Keep
stopandrestartas top-level commands, or as hidden aliases. Two spellings for every action, and--helpeither still lists the flat words or teaches one thing while accepting another. A hidden alias is a compatibility layer for a pre-release interface with no users to protect. daemon start --foregroundinstead ofserve. One entry point, but every systemd unit, test and debugging recipe would change, and "the command that starts the background process" would have a flag meaning "do not".serveas the foreground server anddaemonas the background one say which is which.daemon startas a no-op that succeeds when a daemon is running. Idempotent, which scripts like, but flags such as--port 9000would be silently ignored by the daemon that was already there, and the caller would believe the daemon listens where it does not.daemon restartis the idempotent spelling for "make it what I asked for".- A real daemonizing
serve(double fork,setsid, pidfile). More machinery than a spawn needs, cannot work on Windows, and breaks the supervisor case thatserveexists for. - Redirecting the background daemon's log to a file.
Useful, but it needs a location, rotation and a place in the configuration, and the documentation already sends
anyone who needs the log to
servein the foreground or to a supervisor. Left for a later decision, not smuggled in with this one.
Consequences¶
- Scripts and notes that said
memcastle stop,memcastle restartormemcastle daemon(forserve) break and have to change, and the old words now fail as unknown commands. statuspoints atmemcastle daemon start,daemon restartanddaemon stop, andnot_runningpoints atdaemon start(orservein the foreground).daemon startanddaemon restartare a best-effort detached spawn, not a supervisor: nothing restarts the daemon if it dies, and its log output is discarded.daemon stopstill returns once the shutdown request is accepted, without waiting for the process to exit.- A daemon started this way no longer shares its launching terminal's process group, so signals sent to that group
(Ctrl-C, closing the terminal on most shells) leave it running until
daemon stop. - The detach on Windows is code a Linux CI run does not exercise.