fix(upgrade): /gstack-upgrade stops a stale daemon — deferring to a busy one (#2551)

A browse daemon started before an upgrade keeps serving the OLD binary's
code after `git reset --hard` + `./setup` — the running process holds the
old executable, so users on the "new" version kept getting pre-upgrade
behavior (and config-mismatch refusals against the new CLI) until they
happened to stop it by hand.

New unconditional Step 4.8 in gstack-upgrade/SKILL.md.tmpl (+ regen, same
commit): compare the running daemon's recorded binaryVersion (the
readVersionHash git-SHA the server stamps into its state file) against the
freshly built browse/dist/.version.

- Stale + responsive → `browse stop` (graceful), telling the user
  old→new hash; the next command boots a daemon on the new binary.
- Stale + BUSY → DEFER (decision 10): never kill a busy daemon during
  upgrade. Print the old→new hash and the escape hatch —
  `browse stop` when it finishes, or `browse --force-restart stop` now.
- Dead pid / matching hash / no state → silent no-op.

Tests: skill-validation + gen-skill-docs 731 pass after regen.

Fixes #2551.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-16 10:59:07 -07:00
co-authored by Claude Fable 5
parent 5a6dd6bf88
commit 908c5a6a69
2 changed files with 88 additions and 0 deletions
+44
View File
@@ -220,6 +220,50 @@ Migrations are idempotent bash scripts in `gstack-upgrade/migrations/`. Each is
`v{VERSION}.sh` and runs only when upgrading from an older version. See CONTRIBUTING.md
for how to add new migrations.
### Step 4.8: Stop any stale daemon (unconditional)
A browse daemon started before the upgrade keeps serving the OLD binary's code
until it is stopped — it survives `git reset --hard` and `./setup` because the
running process holds the old executable (#2551). Always run this step, using
the install directory detected in Step 2.
```bash
INSTALL_DIR_PLACEHOLDER="<install dir from Step 2>"
NEW_HASH=$(cat "$INSTALL_DIR_PLACEHOLDER/browse/dist/.version" 2>/dev/null || echo "")
_STATE_FILE="${BROWSE_STATE_FILE:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/browse.json}"
if [ -z "$NEW_HASH" ] || [ ! -f "$_STATE_FILE" ]; then
echo "DAEMON_CHECK=none (no state file or no fresh build hash)"
else
DAEMON_PID=$(sed -n 's/.*"pid"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$_STATE_FILE" | head -1)
DAEMON_PORT=$(sed -n 's/.*"port"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$_STATE_FILE" | head -1)
OLD_HASH=$(sed -n 's/.*"binaryVersion"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$_STATE_FILE" | head -1)
if [ -z "$DAEMON_PID" ] || ! kill -0 "$DAEMON_PID" 2>/dev/null; then
echo "DAEMON_CHECK=dead (no live daemon to stop)"
elif [ "$OLD_HASH" = "$NEW_HASH" ]; then
echo "DAEMON_CHECK=current (daemon already runs the new binary)"
elif curl -fsS --max-time 2 "http://127.0.0.1:$DAEMON_PORT/health" 2>/dev/null | grep -q '"status":"healthy"'; then
echo "DAEMON_CHECK=stale-responsive pid=$DAEMON_PID hash=${OLD_HASH:-unknown} -> $NEW_HASH"
"$INSTALL_DIR_PLACEHOLDER/browse/dist/browse" stop && echo "DAEMON_STOPPED=yes"
else
echo "DAEMON_CHECK=stale-busy pid=$DAEMON_PID hash=${OLD_HASH:-unknown} -> $NEW_HASH"
fi
fi
```
Replace `<install dir from Step 2>` with the actual install directory before
running. Interpret the `DAEMON_CHECK` result:
1. **`stale-responsive` + `DAEMON_STOPPED=yes`:** Tell the user "Stopped the
old browse daemon (binary {OLD_HASH} → {NEW_HASH}). The next browse command
starts a fresh daemon on the new binary."
2. **`stale-busy`:** The daemon runs the old binary but is mid-work — DEFER to
it, never kill a busy daemon during upgrade. Tell the user: "A browse daemon
is still running the pre-upgrade binary ({OLD_HASH} → {NEW_HASH}) but is
busy right now. When it finishes, stop it with `browse stop` — or force it
immediately with `browse --force-restart stop` (loses that session's
tabs/cookies/logins)."
3. **`none` / `dead` / `current`:** Nothing to do — say nothing.
### Step 5: Write marker + clear cache
```bash
+44
View File
@@ -217,6 +217,50 @@ Migrations are idempotent bash scripts in `gstack-upgrade/migrations/`. Each is
`v{VERSION}.sh` and runs only when upgrading from an older version. See CONTRIBUTING.md
for how to add new migrations.
### Step 4.8: Stop any stale daemon (unconditional)
A browse daemon started before the upgrade keeps serving the OLD binary's code
until it is stopped — it survives `git reset --hard` and `./setup` because the
running process holds the old executable (#2551). Always run this step, using
the install directory detected in Step 2.
```bash
INSTALL_DIR_PLACEHOLDER="<install dir from Step 2>"
NEW_HASH=$(cat "$INSTALL_DIR_PLACEHOLDER/browse/dist/.version" 2>/dev/null || echo "")
_STATE_FILE="${BROWSE_STATE_FILE:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/browse.json}"
if [ -z "$NEW_HASH" ] || [ ! -f "$_STATE_FILE" ]; then
echo "DAEMON_CHECK=none (no state file or no fresh build hash)"
else
DAEMON_PID=$(sed -n 's/.*"pid"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$_STATE_FILE" | head -1)
DAEMON_PORT=$(sed -n 's/.*"port"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$_STATE_FILE" | head -1)
OLD_HASH=$(sed -n 's/.*"binaryVersion"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$_STATE_FILE" | head -1)
if [ -z "$DAEMON_PID" ] || ! kill -0 "$DAEMON_PID" 2>/dev/null; then
echo "DAEMON_CHECK=dead (no live daemon to stop)"
elif [ "$OLD_HASH" = "$NEW_HASH" ]; then
echo "DAEMON_CHECK=current (daemon already runs the new binary)"
elif curl -fsS --max-time 2 "http://127.0.0.1:$DAEMON_PORT/health" 2>/dev/null | grep -q '"status":"healthy"'; then
echo "DAEMON_CHECK=stale-responsive pid=$DAEMON_PID hash=${OLD_HASH:-unknown} -> $NEW_HASH"
"$INSTALL_DIR_PLACEHOLDER/browse/dist/browse" stop && echo "DAEMON_STOPPED=yes"
else
echo "DAEMON_CHECK=stale-busy pid=$DAEMON_PID hash=${OLD_HASH:-unknown} -> $NEW_HASH"
fi
fi
```
Replace `<install dir from Step 2>` with the actual install directory before
running. Interpret the `DAEMON_CHECK` result:
1. **`stale-responsive` + `DAEMON_STOPPED=yes`:** Tell the user "Stopped the
old browse daemon (binary {OLD_HASH} → {NEW_HASH}). The next browse command
starts a fresh daemon on the new binary."
2. **`stale-busy`:** The daemon runs the old binary but is mid-work — DEFER to
it, never kill a busy daemon during upgrade. Tell the user: "A browse daemon
is still running the pre-upgrade binary ({OLD_HASH} → {NEW_HASH}) but is
busy right now. When it finishes, stop it with `browse stop` — or force it
immediately with `browse --force-restart stop` (loses that session's
tabs/cookies/logins)."
3. **`none` / `dead` / `current`:** Nothing to do — say nothing.
### Step 5: Write marker + clear cache
```bash