fix(updates): report a plugin build installed beside the running one, never "up to date" on a failed check

A `claude plugin update` run from a terminal (or with only the Claude desktop
app open) unpacks the new build and starts nothing: no SessionStart fires, so
the dashboard kept running the old cache dir across reboots, and with the
forge unreachable the sidebar read "Up to date" (seen on a machine stuck at
1.8.10).

- pluginStatus asks supersededBy for its own root: a recorded sibling install
  sets `installed_not_running: {root, version}` and `update_available`, with
  no network needed
- the sidebar reads "<version> installed, not running"; the update modal keys
  on it, so Update now is offered even with no release tag or sha
- plugin-update-helper hands over to the recorded install first, the newest
  sibling dir only as a fallback
- a plugin install whose release lookup failed reads "Could not check for
  updates" instead of "Up to date"
- docs/PLUGINS.md: the case and the manual hand-over command
This commit is contained in:
2026-10-01 10:09:12 +07:00
parent 969b06253c
commit 058c574138
14 changed files with 208 additions and 27 deletions
+28
View File
@@ -185,6 +185,34 @@ key, which holds without parsing a cache path or trusting a name. An entry
pointing at a directory that no longer exists is ignored — a phantom must never
wedge a bootstrap shut.
### A build installed while the server kept running
The same record answers the opposite case. `claude plugin update` run from a
terminal (or with only the Claude desktop app open) starts **nothing**: no
SessionStart fires, so no bootstrap hands the server over, and the dashboard
keeps running the old directory — across reboots too, since
`agenthub.service` still names it and it still exists. `agenthub open` only
opens the browser.
So `getUpdatesStatus` (`server/lib/update-check.js`) asks `supersededBy` for
its own root: when a sibling build is the installed one, the status carries
`installed_not_running: {root, version}` and `update_available: true` — with
no network needed, so an unreachable forge can no longer hide it. The sidebar
reads **`<version>` installed, not running**, and the update modal's **Update
now** runs the usual chain; `scripts/plugin-update-helper.js` hands over to the
recorded install first (`supersededBy(oldRoot)`), falling back to the newest
sibling directory only when there is no record.
Separately, a plugin install whose release lookup failed (`release_check` not
`ok`) now reads **Could not check for updates** in the sidebar instead of
**Up to date** — a plugin install has no commit count to fall back on.
To hand over by hand without the dashboard:
```bash
node "$(python3 -c 'import json,os;d=json.load(open(os.path.expanduser("~/.claude/plugins/installed_plugins.json")));print([e["installPath"] for k,v in d["plugins"].items() if k.startswith("agenthub@") for e in v][0])')/scripts/plugin-bootstrap.js" --force
```
### Pruning the plugin cache
Claude Code unpacks every update beside the last one and does not reliably