Thirty-four lines of shell to keep a local model alive
Strata’s server belongs to its launcher, so a plain systemd unit does not work. A one-shot unit plus a small watchdog on a timer had the model answering again 32 seconds after the watchdog noticed I had killed it.
Mullet’s local rung only works while the local model answers. On 1 November I wanted Strata to start at boot and come back after a crash without me. It took a systemd unit, a watchdog, and one trap in testing it.
Why not a normal unit
strata launch --preset NAME --no-tui --yes --restart starts Strata’s runtime host and server, and returns once the model answers. The server is owned by that host, so a plain unit that runs the server command line does not work. The unit has to wrap the launcher instead.
Everything here is per user and uses systemctl --user. Replace the paths, the preset name and the port with your own. This is ~/.config/systemd/user/strata-coder.service, with my paths replaced and my comments left out:
[Unit]
Description=Strata: Qwen3.8-Flash-Next Coder (preset coder-128k) on 127.0.0.1:8080
[Service]
Type=oneshot
RemainAfterExit=yes
TimeoutStartSec=900
TimeoutStopSec=120
Nice=5
ExecStart=/path/to/strata launch --preset coder-128k --no-tui --yes --restart --data-dir /path/to/data
ExecStop=/path/to/strata launch --stop --preset coder-128k --data-dir /path/to/data
[Install]
WantedBy=default.target
The catch is that a one-shot stays “active” after the launcher returns. If the server dies, systemd does not notice. That is why there is a watchdog.
The watchdog
~/.local/bin/strata-watchdog, made executable. The real file is 34 lines; this is the same code with my comments left out:
#!/bin/sh
UNIT=strata-coder.service
URL=${STRATA_URL:-http://127.0.0.1:8080/v1/models}
STATE=${XDG_RUNTIME_DIR:-/tmp}/strata-watchdog.fails
state=$(systemctl --user is-active "$UNIT" 2>/dev/null)
[ "$state" = inactive ] && exit 0 # stopped on purpose
[ "$state" = activating ] && exit 0 # still starting
if curl -sf --max-time 15 "$URL" >/dev/null; then rm -f "$STATE"; exit 0; fi
if pgrep -f "serve.server --engine strata" >/dev/null; then
fails=$(( $(cat "$STATE" 2>/dev/null || echo 0) + 1 ))
if [ "$fails" -lt 3 ]; then echo "$fails" > "$STATE"; exit 0; fi
reason="the API did not answer for $fails checks in a row"
else
reason="the server process is gone"
fi
rm -f "$STATE"
logger -t strata-watchdog "restarting $UNIT: $reason"
systemctl --user restart "$UNIT"
The rules are short. If the unit is inactive, I stopped it on purpose, so do nothing. If it is activating, it is still starting, so do nothing. If the API answers, clear the failure count. If the server process is gone, restart at once. If the process exists but the API is silent, count the failure in a file and restart on the third, so a long request on a busy one-slot server is not mistaken for a hang.
A timer runs it. strata-coder-watchdog.service is a one-shot whose ExecStart is %h/.local/bin/strata-watchdog, and the timer is:
[Timer]
OnBootSec=4min
OnUnitActiveSec=2min
Unit=strata-coder-watchdog.service
[Install]
WantedBy=timers.target
Then systemctl --user daemon-reload, systemctl --user enable --now strata-coder.service strata-coder-watchdog.timer, and loginctl enable-linger "$USER". Linger was already on here, so the model starts at boot without a login.
The test
I killed the server and the engine with kill -9. The API returned nothing, and the unit still said active, which is exactly why the watchdog is needed. The watchdog logged “restarting strata-coder.service: the server process is gone”, and the model was answering again 32 seconds after the watchdog started (the journal has the watchdog starting at 19:42:29 and Strata reporting the model answering at 19:43:02 on 1 November). The model files were already in the page cache. That is one test, a sample of one.
The timer adds up to two minutes before the watchdog notices, so a crash costs up to about 2 minutes 32 seconds (my arithmetic). A hang takes up to about 6 minutes to notice: three failed checks two minutes apart, and the first may come up to two minutes after the hang starts. That is my estimate from the timings. I tested a crash, not a hang. With cold files, such as after a reboot, Strata warns that the start can take one to three minutes rather than 30 seconds or so.
A trap when testing it
pkill -f and pgrep -f match whole command lines. If a test script runs pkill -f "serve.server --engine strata" through a shell, that shell’s own command line contains the same text, so the pattern matches the shell as well. In my test script that killed the script halfway through, and a pgrep -f check reported the server as running for the same reason: it found the shell asking the question.
The fix is the bracket trick. Write [s]erve.server in the pattern. As a regular expression it still matches serve.server, but the literal text in the command line no longer does. The watchdog is safe without it, because its pattern lives in the script file, not on its command line.
The full units are in the public repo, in docs/STRATA_SERVICE.md, at https://github.com/tomgould/mullet-public.
More on Mullet
-
Where Mullet goes next
Mullet 2.5.0 is out. What I plan next: a guard that checks a step fits a model’s window, a price ceiling instead of a free switch, more settings and…
-
Eight rules from a game framework’s agent prompts, for web and print design
On 1 November I read the agent prompts of VibeGame, a framework that builds 2D web games, and kept the eight techniques that carry over to web and…
-
What I tried and binned, and what each one cost
Three local-model options I stopped using, with what each cost and the verdict: a 25 GB model that timed out in a job, a Blackwell-only fork ruled…
-
When your plan has no five-hour limit
Mullet 2.5.0 copes with a Claude plan whose usage data has no five-hour window: the HUD drops the five-hour figures and the budget guard paces on…
-
Very cheap is basically free: pricing a coding model per step
A free model id in my config silently had no hosts, and the real price of a cheap coding model depends on which hosts you will accept. The per-step…
-
Why a ‘mechanical’ step failed the risk gate
Mullet’s risk gate fails a step labelled mechanical when its diff changes a literal or a call. A small prompt edit that has to change both failed it…
-
Half a 125-billion-parameter coding model, running on a 12 GB laptop GPU
On 1 November a pruned, quantised coding model ran on my laptop’s 12 GB GPU, split across VRAM, RAM and the CPU. What it is, the speeds I can and…
-
Two local-model failures that point at the context window
On 1 November a local model failed one Mullet step and had two attempts aborted on another, and both point at the context window. What the harness…
Featured projects
-
Solar Calendar
A calendar of 13 identical 28-day months, with each year starting on the first Monday on or after the spring equinox.
-
Where Should I Live
Rank cities around the world by what you’d keep after tax, what a month costs, which visas you can get and how each place fits the way you live.
-
Apple News API
A PHP client for publishing articles to Apple News, with a document builder for the whole Apple News Format.
More writing
-
A Markdown version of every page, for AI agents
Why every page on this site now has a Markdown version for coding agents, the three ways an agent finds it, what goes into it, and how each version…
-
A TV remote from the command line
Pairing a Linux computer with a Google TV over the protocol phone remotes use, what the six-character code on the screen really checks, and why a…
-
The packages I kept on a Google TV
The other half of decluttering a TCL Google TV: a keep-list of 35 packages that debloat lists remove or that only look disposable, how each one got…
-
Letting an agent drive the TV
The rules a coding agent worked under while it changed a TV we were still watching: only changes that can be undone, small batches with a person…