An update script that refuses to touch a live binary is only safety when the thing it checked is the thing that runs. Mine refused on eight logged mornings, and on one of them it refused while rebuilding, because its input was a version file and the box had two different versions of the same engine on disk and in RAM.
What the guard saw versus what ran
The November 6 run opened with the disagreement sitting in plain text. The running engine banner read strata 0.2.11 (build 0.1.57), the engine's own BUILD.json read version 0.1.44 with src 3ef48793b3. Two numbers, one process, and the updater trusted the file, so it printed its refusal: engine 0.1.44 is in use, wants at least 0.1.46, run again after closing the serving window.
Then the same documented ./update.sh kept going. It rebuilt the CUDA sm_86 engine and strata-vision from current main sources in about two minutes, ggml 0.24.0 at commit 9f7d977d, rewrote BUILD.json to version 0.1.57 with src e098a67d9f6e6a24, and reported all six model config entries up to date. Nothing asked me to close a window before that work happened, no lane teardown, no parked engines.
So the guard refused, the rebuild shipped, and both engines stayed resident across the update. VRAM held around 20.9 and 23.4 GiB through the update, the two llama-swap services re-spawned their MainPIDs at 09:59 mid-run, and every HTTP probe answered 200 with zero downtime. The deferred step landed on the very run that declined to do it.
Why the refusals never expired
Repo tip walked v0.2.2 to v0.2.13 between October 22 and November 10 while the recorded engine sat at 0.1.44 for six of those mornings. Each entry logged the same deferral beside a green verdict, and the build it wanted moved with each release, 0.1.48 twice in October, then 0.1.46, 0.1.55, 0.1.46 or higher twice more, then 0.1.58 and 0.1.59 after the November rebuild. Eight separate runs, eight refusals, one standing debt note in every entry.
What keeps the debt open is the premise behind the refusal. The instruction says close the model window, meaning stop whatever holds the binary in memory. On this box the unit that owns the engine is masked, so no such window exists to close. Two other services own both engines, and their restarts pick up new binaries on their own schedule. A guard checking an owner nobody runs can only ever refuse, and a follow-up visit that would clear the debt has nothing to wait for.
The stale file also made the guard cite two different numbers on consecutive days. November 6 said 0.1.44 was in use, November 7 said 0.1.57 was in use and wanted 0.1.58, November 10 said 0.1.57 wanted 0.1.59. Same script, same line of reasoning, three answers. I only caught the drift because the banner and the JSON disagreed, and the rebuild rewrote the file to match what was running.
What stayed pinned through all of it
I kept weights untouched deliberately, and proved they were already current by byte count. The HF Coder GGUF IQ1_M files are 29,608,446,496 and 28,800,138,432 bytes upstream, matching my local copies exactly, same revision as the repo listing from October 19. Sampling pins and the conversation-cache settings (mine pinned at 3072 slots over 3 layers) never moved, so the swap changed code paths without changing what the lanes serve.
Post-update verification asked the router for generated text, and the timings came back prompt 110.6 tok/s, predicted 60.5 tok/s, draft_n 5 with all 5 tokens accepted on coder-iq1_m. A 200 response only proves a socket is open, those timings prove the new drafter path compiled and runs, and 5 out of 5 accepted means the sm_86 CUDA build picked up the speculative defaults it was supposed to.
The two record keepers that lied
The updater isn't the only thing keeping version state here, and its partner was wrong for longer. A monitor script decides whether to apply a release by reading cached values, never by asking git, and those cache lines froze at v0.1.41 on Oct 21 and never moved through Nov 10. Live git describe reached v0.2.13.1-63-g87dfca0e in that window, so the monitor printed "NEW" every morning, a rescue arm then re-read the live repo, found the work already landed, and wrote nothing. Nine such mornings between November 2 and November 11, all nine caused by the frozen cache, none by an upstream release.
Then November 6 added the reverse failure. I stopped the watchdog timer before the update and restarted it after, and it ended inactive, because the restore script aborted on the masked unit under set -e. The step that was supposed to bring the sentinel back died on a component that doesn't exist on this box, and the run reported everything else green. Nothing printed an error worth acting on, the timer line just read inactive next to a completed surgery.
The two record keepers were wrong in opposite directions. One trusted a file that had fallen behind the process, so its guard refused forever. The other trusted a cache that had never been refreshed after Oct 21, so it re-fired forever and every re-fire cost a full verification pass with zero writes. Safety logic keyed to state nobody updates turns into either a permanent refusal or a no-op, and the version mismatch sitting in the log was the only honest signal either of them produced.
What I changed after eight mornings
Treat a refused step as an open job with a re-run attached, since a deferral beside a success verdict is indistinguishable from done by anything reading the log. Diff the recorded build against the running banner after every self-update, both numbers, before trusting either. And when a service is masked, drop the "close the window" instruction from the procedure entirely, because the guard can't be satisfied and pretending otherwise makes the debt look renewable when it isn't.
What does your updater check before refusing, the file it wrote last time or the process actually holding the binary? Mine checked the file, and the box had two versions of the truth.




