fix: batch release v0.50.228 — renderer, model race, tool card, empty session, .env#1179
fix: batch release v0.50.228 — renderer, model race, tool card, empty session, .env#1179nesquena-hermes merged 8 commits intomasterfrom
Conversation
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]>
e15f471 to
bc51651
Compare
|
Updated: folded in reviewer commit |
nesquena
left a comment
There was a problem hiding this comment.
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 POSIXfsyncbefore rename — durabilitychmod 0600before rename — readers never see 0644 windowos.replace(_tmp_path, env_path)— atomicexcept BaseExceptioncleanup so stray.env_*.tmpdon't accumulate- Regression test (
test_providers_write_env_uses_atomic_rename) asserts:tempfile,os.replace(, and noO_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 andhermes_cli.config.save_env_valueuse the same atomic-rename pattern. - JS syntax:
node --checkpasses 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_filehelper inapi/onboarding.pyis now unused (the duplicate_write_env_filewas 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.
…, .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)!
Batch release v0.50.228 — 5 bug fixes.
PRs included
<pre>blocks preserved inrenderMd().envfile corruption from concurrent writes (#1164)Test results
2643 tests passing.
Notes