--- title: Upgrades description: How ClawMetry keeps itself current — the daemon update-check worker, the auto-update rails, the crash-loop rollback guard, and every switch that turns it off. keywords: clawmetry auto update, clawmetry upgrade, CLAWMETRY_AUTO_UPDATE, clawmetry update rollback, pip install -U clawmetry eyebrow: 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](#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. :::warning `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: 1. **Poll PyPI** every `CLAWMETRY_UPDATE_CHECK_SECS` seconds (default **60**, clamped to `[30, 86400]`). This is one small CDN-cached `GET` per node per minute. 2. **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). 3. **Install it** with pip, if `auto_update` is on for this node. 4. **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: ```bash CLAWMETRY_AUTOUPDATE_MIN_AGE_HOURS=24 # only install releases 24h+ old ``` The 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. :::note 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_UPDATE` set 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](/docs/ops/plans/). ## Turning it off ```bash CLAWMETRY_AUTO_UPDATE=0 ``` That 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: ```bash 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`](/docs/cli/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. | → [Uninstall](/docs/ops/uninstall/) · [Troubleshooting](/docs/ops/troubleshooting/)