Skip to content

fix: Windows self-update restart fails due to os.execv behavior - #3647

Closed
jja881 wants to merge 5 commits into
nesquena:masterfrom
jja881:fix/windows-self-update-restart
Closed

fix: Windows self-update restart fails due to os.execv behavior#3647
jja881 wants to merge 5 commits into
nesquena:masterfrom
jja881:fix/windows-self-update-restart

Conversation

@jja881

@jja881 jja881 commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Problem

On Windows, does NOT replace the current process - it spawns a new process while the old one keeps running. This causes:

  1. Old process continues holding the port (8787)
  2. New process fails to bind the same port ()
  3. WebUI shows 'Update failed' after 15-second timeout

Solution

On Windows (), use with flag to start the new process, then to terminate the old one and release the port.

This matches the behavior on Linux where properly replaces the process image.

Testing

Tested on Windows 10 with Python 3.11. Self-update now successfully restarts the WebUI.

…te restart

os.execv() on Windows does NOT replace the current process - it spawns
a new process while the old one keeps running. This causes 'address
already in use' when the new process tries to bind the same port.

On Windows, use subprocess.Popen() with DETACHED_PROCESS flag to start
the new process, then os._exit(0) to terminate the old one and release
the port.
@greptile-apps

greptile-apps Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the Windows self-update restart failure caused by os.execv() not replacing the process image on Windows, and bundles the sprint-29 security hardening for CSRF forwarded-host and TTS forwarded-IP/prosody validation that shipped alongside it.

  • Windows restart (api/updates.py, server.py): On win32, the restart now uses subprocess.Popen with DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP and all stdio redirected to DEVNULL, then calls os._exit(0) immediately so the old process releases the port. server.py adds a 10 s bind-retry loop using SO_EXCLUSIVEADDRUSE to tolerate the brief OS socket-teardown window. Previously flagged issues (1 s sleep before exit, inherited file handles) are both addressed.
  • CSRF hardening (api/routes.py): X-Forwarded-Host / X-Real-Host are no longer trusted by default in the same-origin check; they require HERMES_WEBUI_TRUST_FORWARDED_HOST=1. Existing reverse-proxy tests updated accordingly.
  • TTS security (api/routes.py): Rate-limit bucket now keys on the real client IP by default (X-Forwarded-For trusted only via HERMES_WEBUI_TRUST_FORWARDED_FOR=1); rate and pitch prosody values are validated against ±N% / ±NHz grammar before being passed to edge_tts.Communicate.

Confidence Score: 5/5

Safe to merge; the Windows restart path is well-constructed and the previously flagged broken-pipe and port-hold issues are both resolved.

The core Windows restart change (subprocess.Popen + os._exit(0), DEVNULL streams, bind-retry loop) is correct and addresses all previously raised concerns. The bundled security changes are guarded by explicit env-var opt-ins and have matching test coverage. The only gap is a missing CHANGELOG entry for the Windows fix itself.

CHANGELOG.md is missing a Fixed entry for the Windows self-update restart fix; all code files look correct.

Important Files Changed

Filename Overview
api/updates.py Windows restart path replaced: uses subprocess.Popen with DETACHED_PROCESS + CREATE_NEW_PROCESS_GROUP and DEVNULL streams, then os._exit(0) to release the port immediately. Previous issues (sleep before exit, inherited file handles) are fully addressed.
server.py Added SO_EXCLUSIVEADDRUSE bind-retry loop on Windows (20 × 0.5 s = 10 s window) to tolerate the brief port-teardown period after the old process calls os._exit(0). time is already imported; allow_reuse_address=False prevents SO_REUSEADDR from conflicting with SO_EXCLUSIVEADDRUSE on retry iterations.
api/routes.py CSRF check now only trusts X-Forwarded-Host under explicit opt-in env var; TTS rate limiter likewise opts in to X-Forwarded-For; new _normalize_tts_prosody validates rate/pitch against ±N%/±NHz grammar before passing to edge_tts.
CHANGELOG.md Adds v0.51.267 Security section for CSRF/TTS hardening, but omits a Fixed entry for the Windows self-update restart fix that is the stated subject of this PR.
tests/test_issue2931_edge_tts_endpoint.py New tests cover: prosody validation rejects injected markup and out-of-range values, valid prosody round-trips to edge_tts kwargs, and rate-limit bucket uses real client IP by default with forwarded-for opt-in path also tested.
tests/test_issue1909_csrf_token.py Adds test confirming X-Forwarded-Host is ignored without opt-in env var; updates existing reverse-proxy test to set the opt-in flag it now requires.
tests/test_issue3635_profile_chip_active.py Adds a standing cross-file invariant guard ensuring the profile chip (ui.js) and dropdown active-row (panels.js) always read S.activeProfile as the same source of truth.
tests/test_sprint29.py Updates forwarded-host CSRF port-normalization tests to set the HERMES_WEBUI_TRUST_FORWARDED_HOST opt-in flag, which is now required for X-Forwarded-Host to participate in the origin check.

Sequence Diagram

sequenceDiagram
    participant OldProcess as Old Process (Windows)
    participant OS as Windows OS
    participant NewProcess as New Process

    OldProcess->>OldProcess: self-update triggered
    OldProcess->>NewProcess: "subprocess.Popen(args, DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP, stdio=DEVNULL)"
    OldProcess->>OS: os._exit(0)
    OS-->>OS: socket teardown begins (port released)
    NewProcess->>NewProcess: Python interpreter + imports start
    loop Retry up to 20× (0.5 s each)
        NewProcess->>OS: socket.bind(port 8787) with SO_EXCLUSIVEADDRUSE
        alt Port still releasing
            OS-->>NewProcess: WSAEADDRINUSE (10048)
            NewProcess->>NewProcess: time.sleep(0.5)
        else Port free
            OS-->>NewProcess: bind() success
            NewProcess->>NewProcess: serve_forever()
        end
    end
Loading

Reviews (3): Last reviewed commit: "fix: add bind retry loop on Windows to h..." | Re-trigger Greptile

Comment thread api/updates.py Outdated
Comment thread api/updates.py
nesquena-hermes and others added 3 commits June 5, 2026 17:06
…rebased from nesquena#3639) (nesquena#3644)

Test-only. Adds TestProfileSwitcherSourceOfTruthInvariant generalizing the nesquena#3635
fix so the chip + dropdown can't re-split their source of truth (both must read
S.activeProfile). Rebased onto current master — the original nesquena#3639 branch was
stacked on the pre-squash nesquena#3637 and would have reverted ~5 shipped releases
(IF/IG/IH) if merged as-is; this carries ONLY the +74-line test delta.

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: nesquena <nesquena@users.noreply.github.com>
## Release v0.51.267 — Release II (stage-r17)

Security hardening cluster — 3 @zapabob PRs (forwarded-header trust + TTS prosody validation).

### Security
| Issue/PR | Author | Hardening |
|----------|--------|-----------|
| nesquena#3640 | @zapabob | `/api/tts` per-client throttle no longer trusts `X-Forwarded-For` by default (can't spoof to evade the rate limit); forwarded IP honored only behind a trusted-proxy opt-in. |
| nesquena#3642 | @zapabob | CSRF same-origin check no longer trusts `X-Forwarded-Host`/`X-Real-Host` by default (closes a forwarded-host CSRF bypass); opt-in keeps legit reverse-proxy deploys working; default uses the real `Host`. |
| nesquena#3643 | @zapabob | Browser-provided TTS prosody (rate/pitch/volume) validated against the `±N%` / `±NHz` grammar before `edge_tts.Communicate`. |

### Attribution
Each contributor branch was **rebased onto current master and pushed back to @zapabob's fork** (native authorship preserved), so the source PRs are current/mergeable. Shipped here as one release because all three add a `[Unreleased]` CHANGELOG entry at the same location (merging individually would force a rebase-cascade). Source PRs nesquena#3640/nesquena#3642/nesquena#3643 closed as merged-via-release with credit.

### Gate
- Full pytest suite: **7779 passed, 0 failed**
- ruff: CLEAN
- revert-guard: PASS (all 3 branches rebased; master is an ancestor)
- Codex (regression): **SAFE TO SHIP** — each hardening is **default-secure AND opt-in-compatible** (no legit reverse-proxy/tunnel deploy breaks on update): CSRF forwarded-host default-off + opt-in works + normal same-origin still passes; TTS prosody rejects out-of-grammar input, legit `+N%` passes; TTS throttle ignores spoofed XFF by default.

Co-authored-by: zapabob <1920071390@campus.ouj.ac.jp>
- Remove time.sleep(1) before os._exit(0) to release port immediately
- Add stdin/stdout/stderr=DEVNULL to subprocess.Popen to avoid broken-pipe errors

Reviewed-by: greptile-apps[bot]
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Read the api/updates.py change against _schedule_restart on master (1048-1083) and, importantly, the Windows bind path in server.py. The diagnosis is right — os.execv on Windows spawns rather than replaces, so the old process lingers — but the proposed Popen-then-os._exit ordering collides with how this server binds its port on Windows, and I think it'll fail the same way the bug it's fixing does.

(Heads up on the diff: the origin/master...HEAD range also shows the v0.51.267 CSRF/TTS work because the branch re-created those release commits with new SHAs on top of a v0.51.266 base — fd2e2868 vs master's merged f1211e1f for the same #3648. The only novel change here is the api/updates.py Windows branch; the CSRF/TTS/profile-chip files are already on master. A rebase onto current origin/master will shrink this to the one file.)

The ordering races against SO_EXCLUSIVEADDRUSE

server.py:208-213 deliberately binds with exclusive-use on Windows:

def server_bind(self):
    if sys.platform == 'win32':
        self.allow_reuse_address = False
        SO_EXCLUSIVEADDRUSE = getattr(socket, 'SO_EXCLUSIVEADDRUSE', -5)
        self.socket.setsockopt(socket.SOL_SOCKET, SO_EXCLUSIVEADDRUSE, 1)
    super().server_bind()

The PR spawns the replacement before the parent releases the socket:

subprocess.Popen(args, ...)   # child starts, races to bind()
os._exit(0)                    # parent only now releases the port

With SO_EXCLUSIVEADDRUSE, the OS refuses a second bind to that port while the parent still holds it. The child will typically reach bind() within milliseconds — well before the parent's interpreter tears down and the kernel reclaims the socket — so the child hits "address already in use" and dies. That's the exact failure the PR is trying to eliminate, just moved from execv to Popen. On a fast machine this will reproduce reliably.

Two ways to make it robust

Option A — parent exits first, then a tiny launcher spawns the child. Order it so the port is provably free before the new process tries to bind. A small detached relauncher (e.g. cmd /c "timeout /t 1 >nul & <python> server.py"), or having the child retry bind() with backoff, both close the window. A bind retry in server.py is the most defensive — it'd help any restart path, not just Windows self-update.

Option B — child waits for the port. Have the spawned process poll /health (or attempt-and-retry the bind) until the parent is gone. This mirrors the client-side _waitForServerThenReload readiness contract that PR #3654 is adding; the two are complementary — #3654 keeps the browser from reloading early, this would keep the new server from binding early.

Option A with a bind() retry loop is my recommendation since it's the smallest change that's correct regardless of timing.

Lock interaction is fine

os._exit(0) inside with _apply_lock: (1049) skips normal lock release, but that's harmless here — the process is dying, the OS reclaims everything, and master already relies on the same "threads die when the process image is replaced" property for os.execv. No deadlock concern.

Test gap

The novel change has no Windows-specific coverage (understandably hard to exercise in CI). At minimum, a unit test that monkeypatches sys.platform = 'win32' and asserts _schedule_restart calls subprocess.Popen with DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP (and does not call os.execv) would lock in the platform branch. The bind-race itself is timing-dependent, so the retry loop from Option A is what actually needs the integration coverage — worth a note in the PR that it was manually verified on the overlap window, not just a single happy-path restart.

Solid root-cause analysis; the fix just needs the bind ordering sorted before it'll hold on fast Windows hosts.

On Windows with SO_EXCLUSIVEADDRUSE, when a self-update restarts the
server the new process may attempt to bind the port before the old
process's socket teardown completes. Add a retry loop (up to 10s) in
server_bind() to wait for the port to become available.

Addresses review feedback from nesquena on PR nesquena#3647.
nesquena-hermes added a commit that referenced this pull request Jun 5, 2026
fix #3647) (#3687)

* fix(updates): Windows self-update restart via detached Popen + bind-retry (os.execv doesn't replace proc on Windows) (#3647)

Co-authored-by: jja881 <jja881@users.noreply.github.com>

* docs(changelog): v0.51.280 — Release IV (stage-p3i)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: jja881 <jja881@users.noreply.github.com>
eleboucher pushed a commit to eleboucher/homelab that referenced this pull request Jun 6, 2026
…➔ 0.51.293) (#856)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/nesquena/hermes-webui](https://github.com/nesquena/hermes-webui) | patch | `0.51.277` → `0.51.293` |

---

### Release Notes

<details>
<summary>nesquena/hermes-webui (ghcr.io/nesquena/hermes-webui)</summary>

### [`v0.51.293`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051293--2026-06-06--Release-JI-stage-s5--thinking-card-no-longer-renders-twice)

[Compare Source](nesquena/hermes-webui@v0.51.292...v0.51.293)

##### Fixed

- **The "Thinking" card no longer renders twice on a settled turn.** For a turn that had both a tool call and reasoning (e.g. think → call a tool → answer), the thinking card could appear once inside the collapsed **Activity** group at the top of the turn and again as a stranded second card below the answer and the `Done in …` footer. The thinking-only inline render path (added in v0.51.258 for [#&#8203;3592](nesquena/hermes-webui#3592)) now only fires when the turn has no Activity group of its own, and when it does render inline it inserts the card **above** the answer body instead of after the footer. Thinking that echoes the visible answer on a trailing reasoning-only message is also de-duplicated against the whole turn's answer text now, not just the same message's body. Genuinely thinking-only turns still show their thinking inline (the [#&#8203;3592](nesquena/hermes-webui#3592) fix is preserved, not reverted). ([#&#8203;3709](nesquena/hermes-webui#3709); supersedes [#&#8203;3708](nesquena/hermes-webui#3708))

### [`v0.51.292`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051292--2026-06-06--Release-JH-stage-s4--compression-exhausted-turns-surface-as-errors-not-fake-completions)

[Compare Source](nesquena/hermes-webui@v0.51.291...v0.51.292)

##### Fixed

- **Context-compression-exhausted turns are no longer finalized as a falsely "completed" response.** When Hermes Agent exhausts context compression in a long tool-heavy turn, the streamed result can end on a tool result or an assistant `tool_calls` turn with no final assistant answer. WebUI previously rendered that as a settled, completed reply. It now classifies a persisted transcript that ends in a tool/tool-call/empty-assistant tail (or an internal `[CONTEXT COMPACTION — REFERENCE ONLY]` marker) — and `compression_exhausted`/`failed`/`partial` agent results — as a terminal failure and surfaces a clear error instead. The compression session-id migration and pre-compression snapshot now run **before** the terminal-failure path returns, so frontend/backend session state stays consistent when exhaustion fires after the agent rotates `session_id`. ([#&#8203;3316](nesquena/hermes-webui#3316), [@&#8203;franksong2702](https://github.com/franksong2702); fixes [#&#8203;3315](nesquena/hermes-webui#3315))

### [`v0.51.291`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051291--2026-06-06--Release-JG-stage-s2--preserve-live-turn-content-when-switching-away-mid-stream)

[Compare Source](nesquena/hermes-webui@v0.51.290...v0.51.291)

##### Fixed

- **Switching away from a streaming session no longer loses the in-progress thinking/tool content.** When you clicked to another chat while a session was streaming during a quiet window (mid tool-execution or silent reasoning, between content events) and then switched back, the live turn's tool cards and thinking could disappear permanently — only the elapsed-time clock survived — until the response finished and the transcript re-rendered from the server. Cause: the live-turn DOM snapshot was only captured on content/`tool_complete` SSE events, so the switch-away teardown could run with a stale-or-absent snapshot, and the switch-back fallback rebuilt an empty thinking card. `closeLiveStream()` now snapshots the live turn **before** tearing the stream down, so switching back restores the exact state shown at switch-away. ([#&#8203;3668](nesquena/hermes-webui#3668))

### [`v0.51.290`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051290--2026-06-06--Release-JF-stage-s1--profile-providermodel-now-respected-in-session-resolution)

[Compare Source](nesquena/hermes-webui@v0.51.289...v0.51.290)

##### Fixed

- **Profile-bound sessions now resolve their provider and model from the profile** instead of silently falling back to the global active provider. Previously, when a chat was started under a profile and the model string was not `@provider:`-qualified (and no explicit provider was sent), the backend used the catalog's global active provider — so a profile wired to one provider/key could silently run on a different one, causing **wrong credentials/billing** and **silent context truncation** (the global default model's advertised context window could differ from what the provider actually served, so the provider dropped the oldest messages and long chats "forgot" earlier content). Resolution is now authoritative from the profile across all four runtime entry points (chat start, streaming worker incl. background/btw runs, and both deferred `/api/session` display resolvers); stale models are still repaired under the profile provider — including the `openai-codex` profile + stale `openai/…` slash-model case — while native slash IDs on OpenRouter/custom providers are preserved and explicit `@provider:` qualifiers still win. ([#&#8203;3448](nesquena/hermes-webui#3448), [@&#8203;rodboev](https://github.com/rodboev); fixes [#&#8203;3405](nesquena/hermes-webui#3405))

### [`v0.51.289`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051289--2026-06-06--Release-JE-hotfix--sidebar-ReferenceError-3696--scope-undef-prevention-gate)

[Compare Source](nesquena/hermes-webui@v0.51.288...v0.51.289)

##### Fixed

- **Sidebar no longer crashes with `ReferenceError: _sessionAttentionState is not defined`.** The session-attention helper was declared *inside* `renderSessionListFromCache()` and relied on function hoisting, but the top-level `_sidebarRowHasVisibleMessages` (reached via `renderSessionListFromCache` → `_partitionSidebarSessionRows`) called it bare — and hoisting is scoped to the enclosing function, so every sidebar cache-render threw and the session list went blank. `_sessionAttentionState` is now a top-level function reachable by both call sites. Regressed in [#&#8203;3672](nesquena/hermes-webui#3672) (v0.51.269). ([#&#8203;3696](nesquena/hermes-webui#3696))
- **Stale-stream terminal events no longer risk a `ReferenceError: source is not defined`.** `_bailOutOfTerminalEventsFromStaleStream` (declared inside `attachLiveStream`) called `_closeSource(source)` against a `source` that was not in its lexical scope — it would have thrown on the late-finalizing-stream path when the user is back in an active session. `source` is now threaded as an explicit parameter. Found by the new scope gate below during review. ([#&#8203;3696](nesquena/hermes-webui#3696))

##### Internal

- **New static-JS scope/undefined-reference gate (`scripts/scope_undef_gate.py`).** Models the WebUI's classic-`<script>` shared global scope and runs ESLint `no-undef` per file, flagging a function that is defined only *nested* but called from a sibling/top-level scope — the brick class behind [#&#8203;3696](nesquena/hermes-webui#3696) that `node --check`, source-presence tests, and the existing `no-const-assign` runtime gate all miss. Wired into the CI `lint` job alongside the `no-const-assign`/`no-import-assign` runtime gate, with an in-suite test (`tests/test_static_js_scope_undef.py`) and a focused structural regression test (`tests/test_issue3696_session_attention_scope.py`). ([#&#8203;3696](nesquena/hermes-webui#3696))

### [`v0.51.288`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051288--2026-06-06--Release-JD-stage-r24--collapsible-approval-card)

[Compare Source](nesquena/hermes-webui@v0.51.287...v0.51.288)

##### Added

- **The tool-call approval card can be collapsed to a thin header strip.** A chevron toggle in the approval-card header shrinks the card to just its "Approval required" heading so the tool-call rationale and transcript scrolled above it stay readable; clicking again re-expands it. Includes full ARIA (`aria-expanded`/`aria-controls`/`aria-label`), an icon swap, and transcript reflow that preserves a near-bottom scroll position. State resets to expanded for each new approval, so a fresh approval is never hidden. ([#&#8203;3515](nesquena/hermes-webui#3515), [@&#8203;rodboev](https://github.com/rodboev); closes [#&#8203;3007](nesquena/hermes-webui#3007))

### [`v0.51.287`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051287--2026-06-06--Release-JC-stage-r22--WeCom-session-classification--worker-profile-picker-hiding)

[Compare Source](nesquena/hermes-webui@v0.51.286...v0.51.287)

##### Fixed

- **WeCom gateway sessions are now classified as messaging conversations.** Rows arriving with raw sources `wecom` / `wecom_callback` are normalized into the messaging category (alongside weixin/telegram/discord/slack/email) and given proper "WeCom" / "WeCom Callback" display names, so they group and surface correctly in the sidebar. ([#&#8203;3653](nesquena/hermes-webui#3653), [@&#8203;franksong2702](https://github.com/franksong2702))

##### Changed

- **Worker profiles are hidden from the chat profile picker.** Worker profiles (used for orchestrator/Kanban dispatch) are no longer offered as normal human chat targets in the picker, while still appearing in the profile management view with a "Hidden from chat" badge. The active profile is never hidden. ([#&#8203;3662](nesquena/hermes-webui#3662), [@&#8203;Chukwuebuka-20](https://github.com/Chukwuebuka-20))

### [`v0.51.286`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051286--2026-06-06--Release-JB-stage-r21--sidebar-tab-reordering)

[Compare Source](nesquena/hermes-webui@v0.51.285...v0.51.286)

##### Added

- **Drag-reorder for sidebar tabs.** In Settings → Appearance, the "Sidebar tabs" chips (Tasks, Kanban, Skills, Memory, Spaces, Profiles, Todos, Insights, Logs) can be dragged to reorder how they appear in the left rail and sidebar nav, persisted via a sanitized `tab_order` setting (collapses duplicates, rejects `chat`/`settings`, strips non-strings). Chat and Settings stay fixed. Reorder is pointer/desktop-based (consistent with the existing Kanban drag-and-drop); the chips remain tappable for show/hide on touch. ([#&#8203;3067](nesquena/hermes-webui#3067), [@&#8203;ai-ag2026](https://github.com/ai-ag2026))

### [`v0.51.285`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051285--2026-06-06--Release-JA-stage-r19--update-reload-server-identity-race-fix)

[Compare Source](nesquena/hermes-webui@v0.51.284...v0.51.285)

##### Fixed

- **Don't reload the page until the *replacement* server is actually up after an update.** The post-update reload previously compared raw `/health` uptime, which couldn't distinguish a still-running old process from the restarted one (it could reload against the old process or hang). The client now reads a stable `server_started_at` identity before the update POST and reloads only once `/health` reports a *different* identity (with a null-baseline fallback). Both the force-update and regular apply paths read and pass the baseline. ([#&#8203;3654](nesquena/hermes-webui#3654), [@&#8203;franksong2702](https://github.com/franksong2702); [#&#8203;874](nesquena/hermes-webui#874))

### [`v0.51.284`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051284--2026-06-05--Release-IZ-stage-w4--sidebar-status-labels--cron-sessions-toggle)

[Compare Source](nesquena/hermes-webui@v0.51.283...v0.51.284)

##### Added

- **Manual session status labels (Todo / In Progress / Done).** Tag any session from its row's ⋯ menu with a colored status badge (blue Todo / amber In Progress / green Done), stored per-session in localStorage. The badge renders inline on the sidebar row and uses theme variables so it adapts to light/dark and skins. ([#&#8203;3570](nesquena/hermes-webui#3570), [@&#8203;rodboev](https://github.com/rodboev))
- **"Show cron sessions" preference** (Settings → Preferences). Surfaces cron-job output as conversations in the sidebar. Off by default and gated under "Show non-WebUI sessions" — only active once non-WebUI sessions are enabled — with a note that high-frequency jobs can flood the sidebar. ([#&#8203;3514](nesquena/hermes-webui#3514), [@&#8203;rodboev](https://github.com/rodboev); closes [#&#8203;2841](nesquena/hermes-webui#2841))

### [`v0.51.283`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051283--2026-06-05--Release-IY-stage-w2--composer-queue-hint-during-auto-compaction)

[Compare Source](nesquena/hermes-webui@v0.51.282...v0.51.283)

##### Fixed

- **The composer now tells you a message will queue during auto-compaction instead of looking dead.** While automatic compression runs, the send button previously went `disabled` with only a "Waiting for compression to finish" tooltip. It now shows a `queue` action with the placeholder + tooltip "Type a message — it will queue and send after compression", so you can type and have it sent automatically when compaction completes. ([#&#8203;3512](nesquena/hermes-webui#3512), [@&#8203;rodboev](https://github.com/rodboev); closes [#&#8203;3079](nesquena/hermes-webui#3079))

### [`v0.51.282`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051282--2026-06-05--Release-IX-stage-3544--surface-memoryskill-saves-in-Activity-summary)

[Compare Source](nesquena/hermes-webui@v0.51.281...v0.51.282)

##### Added

- **The collapsed Activity summary now shows when the agent saved a memory or updated a skill** — e.g. "Activity: 2 tools, 1 memory saved, 1 skill updated" — so persistent-state changes are visible at a glance without expanding the group. Detection matches the real tool action vocabularies (`memory`: add/replace count as saves, `remove` excluded; `skill_manage`: create/patch/edit/write\_file count as updates, delete/remove\_file excluded), and only completed, non-errored calls are counted. The memory/skill counts are subtracted from the tool count so it reflects only non-memory/skill tools. Classification is stamped as durable `data-*` attributes so the suffix survives the live tool-call group's HTML snapshot/restore on session switch. Sessions with no memory/skill writes render the unchanged "Activity: N tools" label. ([#&#8203;3544](nesquena/hermes-webui#3544), [@&#8203;rodboev](https://github.com/rodboev); closes [#&#8203;3340](nesquena/hermes-webui#3340))

### [`v0.51.281`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051281--2026-06-05--Release-IW-stage-verdigris--Verdigris-emeraldbronze-skin)

[Compare Source](nesquena/hermes-webui@v0.51.280...v0.51.281)

##### Added

- **New "Verdigris" appearance skin** — a dark-only emerald/forest-green palette (`#&#8203;0F1714` background, `#&#8203;121D18` sidebar) with bronze-gold accents (`#C89A5A`), named for the green-bronze patina on aged copper. Selectable in Settings → Appearance and via `/theme verdigris`. Fully scoped under `:root.dark[data-skin="verdigris"]` (no bleed into the default appearance or other skins), with component-level accents for the new-chat button, scrollbar, tool cards, tree viewer, session badges/tags, diff blocks, MCP status, and image lightbox. ([#&#8203;3602](nesquena/hermes-webui#3602), [@&#8203;rodboev](https://github.com/rodboev); closes [#&#8203;3357](nesquena/hermes-webui#3357))

### [`v0.51.280`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051280--2026-06-05--Release-IV-stage-p3i--Windows-self-update-restart-fix)

[Compare Source](nesquena/hermes-webui@v0.51.279...v0.51.280)

##### Fixed

- **Self-update now restarts correctly on Windows.** `os.execv` does not replace the current process on Windows (it spawns a new one while the old keeps running), so the old process held port 8787 and the new process failed to bind ("address already in use"), surfacing as "Update failed" after the timeout. On Windows the restart now launches a detached new process (`subprocess.Popen` with `DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP`) and exits the old one immediately to release the port, plus a bounded bind-retry loop in `server_bind()` (up to 10s) to ride out the `SO_EXCLUSIVEADDRUSE` teardown window. POSIX behavior is unchanged (still `os.execv`). ([#&#8203;3647](nesquena/hermes-webui#3647), [@&#8203;jja881](https://github.com/jja881))

### [`v0.51.279`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051279--2026-06-05--Release-IU-stage-p3h--preserve-Activitystreaming-turn-on-mid-stream-scroll)

[Compare Source](nesquena/hermes-webui@v0.51.278...v0.51.279)

##### Fixed

- **Loading earlier messages during an active stream no longer wipes the Activity panel or the current streaming turn.** Two causes: (1) the message merge/dedup keys didn't include `tool_calls`, so assistant messages invoking *different* tools with identical empty content and same-second timestamps collapsed into one — dropping every state.db tool-call after the first the sidecar registered; (2) `_syncToolCallsForLoadedMessages` cleared `S.toolCalls` while `S.busy` blocked the `renderMessages` rebuild. `tool_calls` is now part of the merge/dedup/visible keys (with a preservation branch so distinct tool invocations within the sidecar timestamp window aren't skipped), and the frontend keeps the live tool-call/streaming state when paging in history. ([#&#8203;3665](nesquena/hermes-webui#3665), [@&#8203;mysoul12138](https://github.com/mysoul12138); fixes [#&#8203;3346](nesquena/hermes-webui#3346))

### [`v0.51.278`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051278--2026-06-05--Release-IT-stage-p3g--repair-inline-PDF-preview)

[Compare Source](nesquena/hermes-webui@v0.51.277...v0.51.278)

##### Fixed

- **Inline PDF preview in chat now renders again.** The PDF.js loader previously created a `<script>` with both `src` and `textContent` set (the latter is ignored when `src` is present), so PDF.js never initialized and the preview hung on the spinner before degrading to a download link. It now loads PDF.js via a blob module script that sets the worker source, passes `isEvalSupported:false` to harden the parser, and revokes the blob URL on load. CSP gains `blob:` in `script-src` and a scoped `worker-src blob: 'self' https://cdn.jsdelivr.net` to permit the worker. ([#&#8203;3652](nesquena/hermes-webui#3652), [@&#8203;xx77yy](https://github.com/xx77yy); closes [#&#8203;3649](nesquena/hermes-webui#3649))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDEuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEwMS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19-->

Reviewed-on: https://git.erwanleboucher.dev/eleboucher/homelab/pulls/856
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
fix nesquena#3647) (nesquena#3687)

* fix(updates): Windows self-update restart via detached Popen + bind-retry (os.execv doesn't replace proc on Windows) (nesquena#3647)

Co-authored-by: jja881 <jja881@users.noreply.github.com>

* docs(changelog): v0.51.280 — Release IV (stage-p3i)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: jja881 <jja881@users.noreply.github.com>
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.

2 participants