Operations
Upgrades#
ClawMetry ships many releases a day. An observability sidecar that lags the fleet by weeks is worse than useless: it renders old shapes, misses fixes for the very bugs it is supposed to surface, and quietly makes the dashboard look broken. So the daemon keeps itself current by default, on rails, with a hard kill switch.
If you would rather drive it by hand, skip to Turning it off.
The three ways to upgrade#
| Way | Command | When |
|---|---|---|
| Automatic | none — the daemon does it | Default. Within minutes of a release. |
| In-app | clawmetry update | You want it now, from the terminal. |
| Package manager | pip install -U clawmetry | You manage the environment yourself. |
All three land on the same wheel from PyPI. The automatic path is the only one that also restarts the daemon for you.
`clawmetry update` from inside a source checkout
If your shell is sitting in a ClawMetry git checkout, dashboard.py in the
current directory shadows the installed package and the command reports a
false downgrade ("0.12.565 → 0.12.900" backwards, or a version you have
never installed). Run it from anywhere else — cd ~ && clawmetry update.
What the daemon actually does#
The update-check worker (routes/update_check.py) runs inside the sync daemon
and does four things on a loop:
- Poll PyPI every
CLAWMETRY_UPDATE_CHECK_SECSseconds (default 60,
clamped to [30, 86400]). This is one small CDN-cached GET per node per
minute.
- Pick a target. Not necessarily the absolute latest — the newest release
that is newer than what you run and has survived the stability window (see below).
- Install it with pip, if
auto_updateis on for this node. - Restart, so the new wheel is actually the code that runs.
Only the daemon role installs. A dashboard process — often a foreground
terminal a human is watching — will never pip-install and exit underneath you
unless you explicitly set CLAWMETRY_AUTO_UPDATE=1 yourself.
The stability window#
CLAWMETRY_AUTOUPDATE_MIN_AGE_HOURS is how many hours a release must have sat
on PyPI before an unattended install will take it. The default is 0 — the
fleet tracks the absolute latest.
That default is deliberate. An earlier 48-hour gate meant that during an active release run every published version was too fresh, so nodes never found an installable target and froze on an old build for days. The safety net for a bad wheel is the rollback guard below, not a stale window.
Set it if you want a more conservative fleet:
CLAWMETRY_AUTOUPDATE_MIN_AGE_HOURS=24 # only install releases 24h+ oldThe selection logic honours it: with a window set, the daemon installs the newest release that has aged in, which keeps the node at latest-minus-window rather than frozen.
Failed installs back off#
A pip install that fails is not retried on the next 60-second tick. The failed version is recorded with a deadline and skipped until it passes:
| Situation | Backoff |
|---|---|
| PyPI propagation lag (the version is not on every mirror yet) | ~2 minutes |
| A real install failure | CLAWMETRY_AUTOUPDATE_RETRY_SECS, default 1800 (30 min) |
Newer releases are still considered during the backoff — only the specific version that failed is held down.
Restart behaviour#
How the daemon comes back depends on whether something is supervising it.
| Node | Behaviour |
|---|---|
| Supervised (launchd plist, systemd unit) | Install, exit, supervisor respawns on the new wheel. |
Unsupervised (container, kubectl exec, a hand-run python -m clawmetry.sync) | Install, then re-exec its own process image so the new build runs. |
| Windows | Install and defer to the next start. |
The re-exec exists because an unsupervised node used to install the wheel and
then keep the old code in memory indefinitely — a containerised node stayed
stale until somebody bounced the pod. Disable it with
CLAWMETRY_AUTOUPDATE_EXEC_RESTART=0.
The rollback guard#
clawmetry/update_guard.py is a firmware-OTA-style crash-loop guard. A
self-update arms it right after pip succeeds; the daemon checks it at
every boot.
| Constant | Value | Meaning |
|---|---|---|
WINDOW_S | 3600 | A guard older than an hour is stale — the update clearly applied. |
MAX_BOOTS | 3 | This many boots inside the window means a crash loop. |
CONFIRM_AFTER_S | 300 | A run that stays up this long confirms the new version and clears the guard. |
On a crash loop the guard pip-installs the previous version and exits; the
supervisor respawns on the rolled-back build. The rollback is written to
~/.clawmetry/update_rollback.json so the dashboard and the heartbeat can say
what happened instead of leaving you with a mysteriously old version.
What the guard cannot catch
An import-time crash of clawmetry.sync happens before the guard itself runs.
That band is covered by the stability window — which is why an operator running
an air-gapped or high-stakes fleet should set
CLAWMETRY_AUTOUPDATE_MIN_AGE_HOURS rather than relying on the guard alone.
Plans turn it on#
When the daemon's heartbeat resolves an entitled plan — Trial, Starter, Pro or
Enterprise — it enables auto_update for that node, so "I pay for this,
the node should stay current" is true without a manual step.
Three properties of that behaviour matter:
- It only ever enables. A downgrade never silently re-disables what you
chose, and it never disables a setting you turned on.
- It respects an explicit opt-out: with
CLAWMETRY_AUTO_UPDATEset to
0 / false / no / off, the plan sync does nothing.
- It no-ops for free and inactive plans.
The same heartbeat also provisions clawmetry-pro immediately on an upgrade,
so paid runtimes start syncing within a cycle or two rather than waiting for the
next entitlement watcher pass. See
Plans and entitlements.
Turning it off#
CLAWMETRY_AUTO_UPDATE=0That is the hard kill switch. It is checked before anything else and it beats the stored config, the plan sync, and the default-on policy. Put it in the daemon's environment — the launchd plist, the systemd unit, the container spec — not just in an interactive shell, or the daemon will never see it.
To turn it off for one node without an env var, use the config endpoint:
curl -X POST localhost:8900/api/update-check/config \
-H 'Content-Type: application/json' \
-d '{"auto_update": false}'Inspecting the state#
| Endpoint | What it answers |
|---|---|
GET /api/update-check/status | Current version, latest seen, whether an update is available. |
GET /api/update-check/config | The stored flags for this node. |
POST /api/update-check/config | Change them. |
POST /api/update-check/check-now | Poll PyPI immediately instead of waiting for the tick. |
POST /api/update-check/dismiss | Suppress the dashboard banner for a version. |
GET /api/update-check/history | Past checks, so you can see when a node last saw a release. |
Across a fleet, the same picture is in the nodes view — a node stuck several
versions behind is usually one whose daemon is not running at all, which
clawmetry status will tell you in one line.
Environment reference#
| Variable | Default | Effect |
|---|---|---|
CLAWMETRY_AUTO_UPDATE | unset | 0/false/no/off is a hard kill switch. 1 opts a non-daemon process in. |
CLAWMETRY_UPDATE_CHECK_SECS | 60 | Daemon poll cadence, clamped to [30, 86400]. |
CLAWMETRY_AUTOUPDATE_MIN_AGE_HOURS | 0 | Stability window before an unattended install. |
CLAWMETRY_AUTOUPDATE_RETRY_SECS | 1800 | Backoff after a failed install of a specific version. |
CLAWMETRY_AUTOUPDATE_EXEC_RESTART | unset | 0 disables the unsupervised-daemon re-exec. |