Skip to content

fix: batch release v0.50.228 — renderer, model race, tool card, empty session, .env#1179

Merged
nesquena-hermes merged 8 commits intomasterfrom
stage/batch-228
Apr 27, 2026
Merged

fix: batch release v0.50.228 — renderer, model race, tool card, empty session, .env#1179
nesquena-hermes merged 8 commits intomasterfrom
stage/batch-228

Conversation

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Batch release v0.50.228 — 5 bug fixes.

PRs included

PR Author Fix
#1172 (#1150) @bsgdigital Raw <pre> blocks preserved in renderMd()
#1174 @nesquena-hermes Live model race that silently overwrites session model (#1169)
#1175 @nesquena-hermes Tool card output truncated at 220 chars and unscrollable (#1170)
#1176 @nesquena-hermes New Conversation creates empty session when already empty (#1171)
#1178 @bergeouss .env file corruption from concurrent writes (#1164)

Test results

2643 tests passing.

Notes

bsgdigital and others added 7 commits April 27, 2026 22:07
Stash raw pre blocks before inline code-tag normalization so multiline code content is not rewritten into stray backticks that can corrupt subsequent code box rendering. Add a JS-renderer behavior test that guards balanced pre/code output across multiple raw code blocks.

Made-with: Cursor
Rename the raw pre stash variable so static substring checks for _pre_stash continue to target the intended code-block stash section.

Made-with: Cursor
… model (#1169)

When a user has a provider-specific model configured (e.g. Kimi K2 via Nous)
that isn't in the static model list, syncTopbar() fires a setTimeout callback
that finds the model absent from the dropdown and persists the wrong fallback
model (first static option, e.g. Opus 4.6) to disk — before _fetchLiveModels()
returns. This corrupts the session on every load.

Fix has two parts:
1. Track _liveModelFetchPending per provider. While a live fetch is in flight,
   syncTopbar()'s 'model not found' branch skips both the sel.value override
   and the /api/session/update persist call entirely.
2. In _addLiveModelsToSelect(), after inserting new models, re-apply
   S.session.model if it is now resolvable in the updated dropdown. This
   restores the correct model even if syncTopbar() already ran.

Closes #1169
…1170)

Two separate bugs prevented users from reading tool output:

1. JS truncation at 220 chars was too aggressive — stack traces and file paths
   are regularly cut mid-line. Raised to 800 chars so most outputs display
   in full without needing 'Show more'.

2. .tool-card-detail had overflow:hidden in both closed and open states.
   While required for the max-height CSS transition animation when closed,
   overflow:hidden on the parent clips the inner <pre>'s scroll — the
   pre had overflow-y:auto but could never actually scroll. Fixed by
   adding overflow:auto on .tool-card.open .tool-card-detail.

Also raised the inner pre max-height from 180px → 360px and the card cap
from 520px → 600px to give long tool outputs more room.

Test updated to match the new max-height values and assert overflow:auto
on the open card state.

Closes #1170
…lready empty (#1171)

Clicking 'New Conversation' or pressing Cmd+K repeatedly when the current
session has zero messages created N empty 'Untitled' sessions that piled up
in the sidebar list until the 60-second cleanup filter hid them.

Added a message_count guard to both the btnNewChat onclick handler and the
Cmd+K keydown handler: if the current session is already empty, just focus
the composer and return rather than POSTing /api/session/new again.

Test updated to match the now multi-line btnNewChat handler and the extended
shortcut block slice.

Closes #1171
…am writes

Two-part fix for the race condition where WebUI onboarding corrupts
the shared .env file, breaking CLI/Telegram integrations:

Cause 1 — Missing lock on onboarding's _write_env_file:
  onboarding.py had a duplicate _write_env_file() without _ENV_LOCK,
  while providers.py's version held the lock. Concurrent writes from
  the WebUI (onboarding flow) and the Telegram bot could corrupt the
  shared ~/.hermes/.env file, causing API keys to vanish.

  Fix: Remove the duplicate from onboarding.py and import the locked
  version from providers.py. All monkey-patching tests continue to
  work since the name api.onboarding._write_env_file is preserved.

Cause 2 — _write_env_file destroyed comments and reordered keys:
  Both _write_env_file copies loaded the .env into a dict (dropping
  comments/blank lines) and rewrote it sorted alphabetically. This
  meant any comments in the user's .env were silently lost on every
  WebUI config save.

  Fix: Refactored providers._write_env_file to preserve comments, blank
  lines, and original key order. Updates happen in-place on existing
  lines; new keys are appended with a blank-line separator; deleted
  keys are removed cleanly.

Includes 8 tests: comment preservation, blank lines, key order, key
removal, empty file, import verification, and lock presence check.
…oss-process)

The contributor PR's within-process fix (_ENV_LOCK on the load→modify→write
cycle) is correct, but doesn't close the cross-process race that is the
actual root cause of #1164. The hermes-agent CLI/Telegram bot uses
hermes_cli.config.save_env_value, which writes .env atomically via
tempfile + os.replace. The WebUI's _write_env_file was using
os.open(..., O_TRUNC) — between the truncate and the complete write,
a concurrent reader sees a partial or empty file. The agent has a
_sanitize_env_lines workaround for the resulting corruption pattern
(concatenated KEY=VALUE pairs from interleaved writes / partial reads),
which is a strong signal that this race actually occurs in practice.

Fix: stage the write through tempfile.mkstemp() in the same directory
(same filesystem, so os.replace is atomic on POSIX), fsync before
rename, chmod 0600 before rename so readers never see a 0644 window,
and clean up the tempfile on any exception. Matches the exact pattern
hermes_cli/config.py:save_env_value uses.

Within-process safety from the PR's _ENV_LOCK is preserved (the whole
sequence still runs inside the with-lock block).

Also added a regression test asserting the new invariants:
- tempfile staging present
- os.replace called
- O_TRUNC pattern is gone

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@nesquena-hermes
Copy link
Copy Markdown
Collaborator Author

Updated: folded in reviewer commit 7e5cdfa from the #1178 review — adds atomic .env write via tempfile.mkstemp + os.replace to close the cross-process race (matches hermes_cli/config.py:save_env_value pattern). 2644 tests passing.

Copy link
Copy Markdown
Owner

@nesquena nesquena left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — end-to-end ✅ (approved after parity push for #1178)

What this ships

v0.50.228 batch — 5 fixes consolidated from individually-approved PRs:

PR Author Fix Prior review
#1172 (#1150) @bsgdigital Raw <pre> blocks preserved in renderMd() #1172
#1174 nesquena-hermes Live model race silently overwrites session model (#1169) #1174
#1175 nesquena-hermes Tool card output truncated/unscrollable (#1170) #1175
#1176 nesquena-hermes New Conversation creates empty session (#1171) #1176
#1178 @bergeouss .env file corruption from concurrent writes (#1164) #1178 ✅ (with my atomic-write follow-up)

What I caught — batch missed the approved atomic-write follow-up on #1178

The integration of #1178 absorbed @bergeouss's commit d4de119 but not my approved review-pushed follow-up at 7e5cdfa (atomic tempfile + os.replace). Verified by inspecting the absorbed commit fa23c905 — it only contains the within-process lock fix and comment preservation, but still uses os.open(..., O_WRONLY | O_CREAT | O_TRUNC) for the actual write.

This is a real gap: #1164's actual root cause is the cross-process race between WebUI and Telegram bot/CLI. The agent's hermes_cli/config.py:save_env_value writes atomically; without my fix the WebUI doesn't, leaving the cross-process race unfixed.

What I pushed — e15f471

Brought the batch to parity with the approved #1178 review state. Same patch as approved:

  • tempfile.mkstemp(dir=env_path.parent) — same FS, atomic rename on POSIX
  • fsync before rename — durability
  • chmod 0600 before rename — readers never see 0644 window
  • os.replace(_tmp_path, env_path) — atomic
  • except BaseException cleanup so stray .env_*.tmp don't accumulate
  • Regression test (test_providers_write_env_uses_atomic_rename) asserts: tempfile, os.replace(, and no O_TRUNC — locks the bug from regressing

CI re-ran green on e15f471: 3.11 ✅, 3.12 ✅, 3.13 ✅.

Spot-check — all 5 fix invariants present in final batch state

Verified each fix's signature is in the merged tree:

PR Invariant File:line Status
#1172 rawPreStash + \x00R token ui.js:831-839
#1174 _liveModelFetchPending = new Set() + liveStillPending early-return ui.js:171, 1859-1860
#1175 max-height:600px + overflow:auto on .tool-card.open .tool-card-detail style.css:1236
#1176 `(S.session.message_count 0)===0` guard
#1178 tempfile.mkstemp + os.replace (after my push) providers.py:158-185

CHANGELOG entry verified

Five ### Fixed entries under ## [v0.50.228] — 2026-04-27, one per absorbed PR, with author attribution for the contributor PRs (@bsgdigital, @bergeouss). Cleanly placed after v0.50.227. ✅

Tests

  • Local full suite: 2596 passed, 47 skipped, 1 PR-unrelated pre-existing failure (test_sprint3.py::test_workspace_add_rejects_system_paths — macOS-only quirk, fails on master).
  • CI on PR after my push: ✅ test (3.11), ✅ test (3.12), ✅ test (3.13).
  • PR's targeted tests: 9 env tests + all per-PR tests (renderer, model race, tool card, mobile layout) pass.

Other audit — confirmed correct

  • No regression in absorbed PRs: each individual fix's signature line is still present (no overzealous refactor during absorb).
  • Agent-side coupling: only #1178 touches a shared file (~/.hermes/.env). After my push, both WebUI and hermes_cli.config.save_env_value use the same atomic-rename pattern.
  • JS syntax: node --check passes on all touched files.

Minor observations (non-blocking)

  • The agent's batch absorb seems to have grabbed the contributor's commit at integration time, missing maintainer-edit follow-up pushes from review. Worth noting in the integration playbook so future absorbs explicitly check whether maintainer-edit pushes were applied to the contributor's branch before cherry-picking.
  • The _load_env_file helper in api/onboarding.py is now unused (the duplicate _write_env_file was its only caller). Harmless dead code; can be removed in a follow-up.

Recommendation

Approved (after parity push). All 5 fixes verified against their original PR reviews. The atomic-write addition I pushed brings the batch to parity with the approved #1178 review state — without it, #1164's actual root-cause symptom (cross-process race) wouldn't be closed even though the within-process fix shipped. CI green; CHANGELOG entry well-formed; per-fix invariants confirmed in final tree. Parked at approval — ready for the release agent's merge/tag pipeline.

@nesquena-hermes nesquena-hermes merged commit ef26d19 into master Apr 27, 2026
3 checks passed
@nesquena-hermes nesquena-hermes deleted the stage/batch-228 branch April 27, 2026 22:29
JKJameson pushed a commit to JKJameson/hermes-webui that referenced this pull request Apr 29, 2026
…, .env (nesquena#1179)

Merged as v0.50.228. 2644 tests passing. Browser QA 21/21 (desktop 1440×900 + mobile iPhone 14). All 5 fix invariants verified live in browser.

**Fix verifications:**
- nesquena#1172 (`renderMd` pre-stash): `rawPreStash` present in function, `<pre>` blocks pass through without content rewrite ✅
- nesquena#1174 (model race guard): `syncTopbar()` contains `liveStillPending` guard ✅
- nesquena#1175 (tool card): `.tool-card-result pre` max-height=360px, `.tool-card.open .tool-card-detail` overflow=auto, cap=600px ✅  
- nesquena#1176 (empty session guard): double-click New Conversation on empty session → stays on same session, composer focused ✅
- nesquena#1178 (`.env` atomic write): `tempfile.mkstemp + os.replace` in `providers.py`, 9/9 env tests pass ✅

Thanks @bsgdigital (nesquena#1150) and @bergeouss (nesquena#1178)!
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.

3 participants