fix: hybridize background profile env routing - #2368
2 commits merged into
Conversation
e90e9e3 to
de878a5
Compare
de878a5 to
9894157
Compare
|
Rebased this branch on current What changed in the conflict repair:
Verification on rebased head
CI is running again on the rebased head. |
|
Reviewing this as the "Option B" follow-up to the #2321 / reverted-#2323 conversation. Pulled Where the PR succeedsThe change correctly addresses Opus's catch from the #2321 reopen — that # api/profiles.py:716-720 (PR)
thread_env = dict(runtime_env)
thread_env["HERMES_HOME"] = str(profile_home_path)
...
_set_thread_env(**thread_env)
with _ENV_LOCK:
had_hermes_home = "HERMES_HOME" in os.environ
old_hermes_home = os.environ.get("HERMES_HOME")
...
os.environ["HERMES_HOME"] = str(profile_home_path)And the new integration test at Concern: the test doesn't cover provider-credential resolution
The problem is the same shape as Opus's original catch, just for a different code path. No production reader consults It is written and cleared, nothing reads it. For provider auth specifically, the chain is:
So when cookie-profile == session-profile (the simple single-tab case), the PR works because Suggested acceptance test extensionThe maintainer's acceptance criterion #1 said "calls def test_background_profile_env_routes_provider_credentials(...):
# Set os.environ['OPENROUTER_API_KEY']='default-key' (cookie's active profile)
# Write profile-home/.env with OPENROUTER_API_KEY='work-key'
# Enter profile_env_for_background_worker(session, ...)
# Call agent.auxiliary_client._try_openrouter() (or equivalent)
# Assert the client was constructed with 'work-key', not 'default-key'Today that test would fail under this PR, which is the regression-reveal the maintainer asked for in criterion #2. The fix is either (a) restore VerdictFor sessions where the cookie and session profile match, this PR is correct and the load_config test pins the desired contract. For concurrent multi-profile workloads — which is the original #2321 motivation — the runtime-env keys move from "racy os.environ" to "thread-local nobody reads", which trades one bug for another. I'd suggest either:
Either way, the current test surface gives more comfort than is warranted given the production reader gap. Happy to look at a follow-up that closes the credential path or scopes the contract down explicitly. |
|
Pushed a follow-up for the credential-reader gap you caught. What changed:
Verification on
CI is running on the new head now. |
SummaryReading the follow-up commit CI is green on the new head. Code referencesHybrid setup/restore —
|
3de4338
… 0.51.75) (#527) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/nesquena/hermes-webui](https://github.com/nesquena/hermes-webui) | patch | `0.51.74` → `0.51.75` | --- ### Release Notes <details> <summary>nesquena/hermes-webui (ghcr.io/nesquena/hermes-webui)</summary> ### [`v0.51.75`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05175--2026-05-16--Release-AY-stage-368--11-PR-safe-lane-batch--storage--i18n--run-journal-parity--attachments--compression-sidebar--restart-recovery--text-mode-images--tables--settings-i18n--German-labels) [Compare Source](nesquena/hermes-webui@v0.51.74...v0.51.75) ##### Test infrastructure - Stage-368 maintainer fix — pytest no longer self-loops on the `_schedule_restart` daemon thread. Several existing tests in `tests/test_update_banner_fixes.py` call `api.updates._schedule_restart()`, which spawns a daemon thread that eventually calls `os.execv()`. Those tests monkeypatch `os.execv` for the test scope, but monkeypatch teardown can win the race against the daemon thread, restoring the real `os.execv` before the thread fires it — at which point the daemon re-execs the entire pytest process with the original argv, looking from the outside like pytest hangs at 99 % then restarts the suite from 0 % in an infinite loop. `tests/conftest.py` now installs a permanent no-op wrapper on `os.execv` at module-import time so late-firing daemon threads cannot re-exec pytest. New `tests/test_pytest_execv_guard.py` pins the guard against future regressions. ##### Added - **PR [#​2377](nesquena/hermes-webui#2377 by [@​franksong2702](https://github.com/franksong2702) (refs [#​2283](nesquena/hermes-webui#2283), refs [#​2363](nesquena/hermes-webui#2363), refs [#​1925](nesquena/hermes-webui#1925)) — Run-journal replay timeline parity checks. After [#​2283](nesquena/hermes-webui#2283) shipped the first run-journal replay slice and [#​2363](nesquena/hermes-webui#2363) documented the cross-layer state consistency contract, this PR adds explicit parity assertions over the replayed timeline so divergences between the journal and the visible transcript (Thinking → tool calls → assistant text) surface as test failures instead of silent drift. ##### Fixed - **PR [#​2391](nesquena/hermes-webui#2391 by [@​Michaelyklam](https://github.com/Michaelyklam) (fixes [#​2389](nesquena/hermes-webui#2389)) — Reduce browser storage pressure during service-worker updates and over long-running sessions. `static/sw.js` now calls `deleteOldShellCaches()` BEFORE `caches.open(CACHE_NAME)` in the install handler so the new \~2.2 MB shell cache no longer overlaps the old one during a version bump (especially painful on shared-origin quota accounting). A new `_clearSessionViewedCount()` helper plus extended `_clearHandoffStorageForSession()` prune `hermes-session-viewed-counts`, `hermes-session-completion-unread`, and `hermes-session-observed-streaming` on every single-session delete and batch-delete so per-session tracking maps no longer grow unbounded. - **PR [#​2387](nesquena/hermes-webui#2387 by [@​Michaelyklam](https://github.com/Michaelyklam) (fixes [#​2386](nesquena/hermes-webui#2386)) — Guard `localStorage.setItem('hermes-webui-session', ...)` and workspace-panel runtime-state writes with `try { … } catch (_) {}` across `static/boot.js`, `static/sessions.js`, `static/commands.js`, and `static/messages.js`. These convenience writes were previously fatal UI operations on quota-exhausted browsers (especially Firefox public-domain setups where shared quota fills up after a service-worker shell rotation). - **PR [#​2368](nesquena/hermes-webui#2368 by [@​Michaelyklam](https://github.com/Michaelyklam) — Hybridize background profile env routing so background title generation, manual compression, and update-summary workers honor a session's non-default profile. The pure thread-local refactor for [#​2321](nesquena/hermes-webui#2321) was reverted because `hermes_cli.config.load_config()` still reads `HERMES_HOME` from process env. This PR keeps the thread-local layer for WebUI helpers and adds an `os.environ.update(runtime_env)` mirror under a narrow `_ENV_LOCK` for the worker body, with proper restore of prior values. New test asserts `OPENROUTER_API_KEY` is visible from the worker against a non-default profile. - **PR [#​2382](nesquena/hermes-webui#2382 by [@​Michaelyklam](https://github.com/Michaelyklam) (fixes [#​2380](nesquena/hermes-webui#2380)) — Serve raw chat attachments from the per-session inbox in addition to the session workspace. Chat uploads were intentionally moved out of workspaces into a per-session attachment inbox in an earlier release; the transcript renderer still emits stable `api/file/raw?session_id=...&path=<filename>` URLs, but `_handle_file_raw` only checked `session.workspace` so inbox-backed uploads rendered as broken images. The URL surface is preserved and a session-attachment fallback is added with path-traversal guards intact. - **PR [#​2385](nesquena/hermes-webui#2385 by [@​franksong2702](https://github.com/franksong2702) — Keep fuller compression snapshots reachable in the sidebar. The default behavior hides `pre_compression_snapshot: true` rows so archived compression segments do not duplicate the active continuation. A real long Kanban session exposed a narrower failure: the fuller transcript was still present on disk but remained marked as `pre_compression_snapshot`, so the sidebar surfaced a shorter row and the fuller transcript became unreachable. The fix preserves discoverability without re-introducing duplication in normal cases. - **PR [#​2371](nesquena/hermes-webui#2371 by [@​franksong2702](https://github.com/franksong2702) — Clarify interrupted turn recovery after a WebUI restart. WebUI executes browser-originated agent turns inside the WebUI process; if that process restarts mid-turn, the worker dies with it. Run journal replay can only replay events that were already emitted, so the stale-pending repair path is now annotated and refined to make the post-restart state explicit (interrupted, recoverable, or terminal) instead of leaving the user with a half-rendered turn and no signal. - **PR [#​2378](nesquena/hermes-webui#2378 by [@​Michaelyklam](https://github.com/Michaelyklam) — Strip historical images in text-only mode. Current-turn uploads already respect `agent.image_input_mode: text`, but saved conversation history still passed native `image_url` content parts back into later provider calls, breaking text-only providers on replayed turns. `_sanitize_messages_for_api()` gains a `cfg=` keyword argument so the API-history sanitizer can strip historical native image parts when the mode is text. Default `cfg=None` preserves prior behavior for callers that don't pass the new argument. - **PR [#​2375](nesquena/hermes-webui#2375 by [@​Michaelyklam](https://github.com/Michaelyklam) — Keep Markdown tables block-level. Pipe tables were already converted to `<table>` markup, but the final paragraph pass did not treat generated tables as block-level output, occasionally wrapping them in `<p>` and breaking the surrounding layout. The fix isolates generated tables and adds `table` to the paragraph-wrap skip list so valid CommonMark tables render predictably. - **PR [#​2372](nesquena/hermes-webui#2372 by [@​mccxj](https://github.com/mccxj) — Settings → Conversation page action buttons now respect locale selection. Pre-fix, the JSON export, MD export, and Copy buttons had hardcoded English labels/titles. Adds `data-i18n` / `data-i18n-title` attributes plus the missing translation keys so non-English locales no longer see English labels stuck in the middle of a translated screen. - **PR [#​2381](nesquena/hermes-webui#2381 by [@​Michaelyklam](https://github.com/Michaelyklam) (fixes [#​2379](nesquena/hermes-webui#2379)) — German relative session-time labels now interpolate the elapsed value instead of rendering the literal `{n}` placeholder in the sidebar/header. The German locale now uses function-valued translations for minutes, hours, and days, matching the other locale bundles. </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/527
Thinking Path
hermes_cli.config.load_config()still readsHERMES_HOMEfrom process env.os.getenv()directly._ENV_LOCKcritical section remains short: it serializes setup/restore of env and skill-home module caches, but does not wrap the whole worker body.What Changed
profile_env_for_background_worker()to set thread-local profile runtime env and mirror runtime env intoos.environfor the worker body.hermes_cli.config.load_config()andos.getenv()-style provider credential readers see the session profile.Why It Matters
Non-default profile background workers previously had two bad choices: broad process-env mutation with race risk, or thread-local-only env that production Hermes readers did not actually consult. This PR takes the current compatibility path: the worker sees the profile config and provider credentials used by existing Hermes Agent code, while setup/restore is bounded and covered by regressions.
Closes #2321.
Verification
Result on follow-up head
5bd1f14:CI is running on the new head.
UI media: not applicable; backend/profile-env behavior only.
Risks / Follow-ups
os.getenv().os.environ._thread_ctx.env.Model Used
AI-assisted change with repository inspection, targeted editing, GitHub issue/PR review, and shell-based test verification.