Skip to content

Release v0.51.285 — Release JA (stage-r19 — update-reload server-identity race fix #3654) - #3693

Merged
nesquena-hermes merged 2 commits into
masterfrom
release/stage-r19
Jun 6, 2026
Merged

Release v0.51.285 — Release JA (stage-r19 — update-reload server-identity race fix #3654)#3693
nesquena-hermes merged 2 commits into
masterfrom
release/stage-r19

Conversation

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Release stage-r19 (v0.51.285) — update-reload server-identity race fix (#3654)

UN-HOLD re-gate. I previously HELD #3654 (this session) because its update-reload logic
compared raw /health uptime, which can't distinguish a still-running old process from the
restarted replacement — a chain of issues ending in a design-level concern that needed a
backend /health change. @franksong2702 reworked it to the requested approach.

The original hold finding (now reworked)

  • OLD: _readHealthUptimeSeconds() raw-uptime < comparison; null baseline defeated by
    Number(null)===0; force-update read the baseline AFTER the POST.
  • NEW (verify this is correct + complete): /health now exposes server_started_at
    (= SERVER_START_TIME, the process-start timestamp). Client reads
    _readHealthServerIdentity() BEFORE the update POST, stores it as baselineServerIdentity,
    and _waitForServerThenReload() reloads only when nextServerIdentity !== baselineServerIdentity
    (a genuinely new process), with a baselineServerIdentity===null → reload-anyway fallback.
    _normalizeHealthServerIdentity coerces string|numeric, returns null only on missing/empty
    (NOT on 0). BOTH reload paths (force-update forceUpdate + regular applyUpdate) read +
    pass the baseline — no sibling-path gap.

What to check

  • Codex (regression): confirm the original raw-uptime race is gone and the identity comparison
    can't false-positive (reload against the OLD process) or hang forever (never reload). Verify
    the null-baseline fallback and the deadline/maxMs timeout behave. Confirm no NEW regression in
    the update-apply flow. SAFE TO SHIP or MUST-FIX.
  • Opus (correctness): the server-identity logic correctly distinguishes restart-vs-same-process;
    both reload call sites pass baseline; server_started_at is stable across the process lifetime
    and changes on restart. Focused code read + one small targeted check only; do NOT write
    extensive reproduction harnesses.

Files: api/routes.py (+server_started_at in /health), static/ui.js (identity read/compare),
tests/test_update_apply_ui.py, tests/test_update_banner_fixes.py.

Frank Song and others added 2 commits June 6, 2026 00:56
…e reload (#3654)

Replaces the raw-uptime comparison (couldn't distinguish a fresh old process
from the restarted one) with a stable server_started_at identity read before
the update POST; reloads only when the identity changes. Both the force-update
and regular apply paths read + pass the baseline. (#874, #3654)

Co-authored-by: Frank Song <franksong2702@gmail.com>
@nesquena-hermes
nesquena-hermes merged commit e9bb354 into master Jun 6, 2026
11 checks passed
@nesquena-hermes
nesquena-hermes deleted the release/stage-r19 branch June 6, 2026 01:13
@greptile-apps

greptile-apps Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces the raw-uptime /health comparison used for post-update page reloads with a stable server_started_at process-identity check, fixing the race where the old process could answer the health poll and trigger an early reload against the still-running original server.

  • api/routes.py: Adds server_started_at: SERVER_START_TIME to the /health payload; SERVER_START_TIME is time.time() at module import — stable within a process, unique on restart.
  • static/ui.js: Introduces _readHealthServerIdentity() (pre-POST baseline read) and _normalizeHealthServerIdentity() (null-safe coercion); _waitForServerThenReload now reloads only when the polled identity differs from the baseline, with an explicit null-baseline fallback (reload-on-first-healthy) and a 15 s deadline; both applyUpdates and forceUpdate capture the baseline before their respective POSTs.
  • Tests: New subprocess-driven Node.js integration tests exercise the null-baseline fallback path and the identity-change-gating path directly against the extracted function source.

Confidence Score: 5/5

Safe to merge — the identity-gated reload correctly prevents the old-process false-positive, both update paths capture the baseline before their POST, and the null-baseline fallback preserves prior behaviour when the identity is unavailable.

The original race (reloading against the still-running old process) is cleanly eliminated. SERVER_START_TIME is a module-level float that is stable within a process and always changes on restart, making it a reliable identity token. _normalizeHealthServerIdentity correctly avoids returning null for a zero value and is idempotent when applied twice. The 15 s deadline prevents indefinite polling. Both applyUpdates and forceUpdate capture the baseline before their respective POSTs, and the null-baseline fallback reinstates the old reload-on-first-healthy behaviour for the degraded case. The Node.js integration tests directly exercise both the fallback path and the identity-change gate. No gaps found.

No files require special attention.

Important Files Changed

Filename Overview
static/ui.js Core logic change: adds _normalizeHealthServerIdentity, _readHealthServerIdentity, and rewires _waitForServerThenReload to gate reload on a process-identity change rather than any healthy response; both applyUpdates and forceUpdate correctly capture the baseline before their POST calls.
api/routes.py Adds server_started_at (=SERVER_START_TIME, a module-level time.time()) to the /health payload; the value is always a valid float, stable across the process lifetime, and guaranteed to change on restart.
tests/test_update_banner_fixes.py Adds four new tests: structural source assertions for baselineServerIdentity capture and comparison patterns, plus two subprocess/Node.js integration tests validating the null-baseline fallback and the same-identity-skip/new-identity-reload behaviour directly.
tests/test_update_apply_ui.py Narrow fix: relaxes the _waitForServerThenReload() source-text assertion from the exact call signature to the bare function name to accommodate the new {baselineServerIdentity} argument.
CHANGELOG.md Adds v0.51.285 release entry describing the server-identity reload fix; no code changes.

Sequence Diagram

sequenceDiagram
    participant UI as Browser (ui.js)
    participant H as /health
    participant API as /api/updates/*

    Note over UI: applyUpdates() or forceUpdate()
    UI->>H: GET /health (cache:no-store)
    H-->>UI: "{ server_started_at: T1, status: ok }"
    Note over UI: baselineServerIdentity = normalize(T1)

    UI->>API: POST /api/updates/apply (or /force)
    API-->>UI: 200 OK
    Note over UI: _waitForServerThenReload({baselineServerIdentity})

    loop Poll every 500 ms (max 15 s)
        UI->>H: GET /health
        alt Server still old process
            H-->>UI: "{ server_started_at: T1, status: ok }"
            Note over UI: nextIdentity == baseline, keep polling
        else Server restarted (new process)
            H-->>UI: "{ server_started_at: T2, status: ok }"
            Note over UI: nextIdentity != baseline, location.reload()
        else baseline was null (fallback)
            H-->>UI: "{ status: ok }"
            Note over UI: reload immediately on first healthy response
        end
    end
    Note over UI: Deadline reached, show manual-reload warning
Loading

Reviews (1): Last reviewed commit: "docs(changelog): v0.51.285 — Release JA ..." | Re-trigger Greptile

SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
…tity race fix nesquena#3654) (nesquena#3693)

* Fix update reload readiness race — poll /health server identity before reload (nesquena#3654)

Replaces the raw-uptime comparison (couldn't distinguish a fresh old process
from the restarted one) with a stable server_started_at identity read before
the update POST; reloads only when the identity changes. Both the force-update
and regular apply paths read + pass the baseline. (nesquena#874, nesquena#3654)

Co-authored-by: Frank Song <franksong2702@gmail.com>

* docs(changelog): v0.51.285 — Release JA (stage-r19)

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant