release: stamp v0.50.288 — 3-PR batch (#1569 + #1571 + #1572) (4053 \u2192 4094 tests)

This commit is contained in:
Hermes Bot committed 2026-05-03 22:54:34 +00:00
1 parent c07999f0ce
commit 59afbdb3ce
3 files changed
+32 -3

No files matched your search

+29
View File
@@ -1,5 +1,34 @@
# Hermes Web UI -- Changelog
## [v0.50.288] — 2026-05-03
### Fixed (3 PRs — picker symmetry + cron profile isolation — closes #1567, #1568, #1573)
- **Nous Portal endpoint disagreement + featured-set cap** (#1569; closes #1567) — reporter (Deor, Discord, relayed by @AvidFuturist) saw Settings → Providers card showing `"Nous Portal — 396 models · OAuth"` while the in-conversation model picker dropdown listed only the 4 hardcoded curated entries (Claude Opus 4.6, Claude Sonnet 4.6, GPT-5.4 Mini, Gemini 3.1 Pro Preview). Two related root-shape bugs bundled. **(1)** Asymmetric auth detection — `api/providers.py:get_providers` iterates ALL OAuth providers regardless of authentication state and unconditionally live-fetches the catalog, while `api/config.py:_build_available_models_uncached` only iterates providers in `detected_providers`, gated on `hermes_cli.models.list_available_providers().authenticated`. That flag can disagree with `hermes_cli.auth.get_auth_status(<id>).logged_in`, so when the disagreement happens for Nous, the picker silently falls through to the curated 4-entry static list while the providers card keeps showing the live catalog. **Fix:** added explicit `get_auth_status("nous").logged_in` check after the existing `list_available_providers()` loop — picker now includes Nous whenever the providers card would. **(2)** UX cap — even with the disagreement fixed, dumping a 397-model catalog into a flat dropdown is unusable. New `_build_nous_featured_set()` helper at `api/config.py:965` runs the same algorithm in both `/api/models` and `/api/models/live` so background enrichment doesn't undo the trim. Selection rules (deterministic): sticky-selection always pinned, every curated flagship preserved, vendor round-robin via `_NOUS_VENDOR_PRIORITY` for top-up to 15. Disclosure pattern: optgroup label `"Nous Portal (15 of 397)"`, new `extra_models` field on the API surface, slash command + `_dynamicModelLabels` map hydrated from both halves so a model selected outside the featured slice still renders with its proper label, providers card uses `models_total` for the header count + small `+N more` disclosure pill at the end of the rendered pill list. **(3)** Stale-fallback poisoning — when authenticated AND live-fetch returns `[]` (transient hermes_cli failure, OAuth refresh in flight, cache miss), omit the Nous group entirely rather than falling back to stale-4 (which actively contradicts the providers card instead of self-healing). Static fallback only when `hermes_cli` is unavailable or raises (test envs, package mismatches). 20 new tests in `tests/test_issue1567_nous_picker_capacity_and_symmetry.py` covering selection helper invariants, large-catalog cap behavior, detection symmetry, live-fetch-empty handling, providers/picker symmetry, frontend extras contract.
- **Cron Scheduled Jobs panel respects per-request active profile** (#1571 by @kowenhaoai; closes #1573) — `/api/crons*` endpoints called into `cron.jobs` (from `hermes-agent`), whose path resolver reads `HERMES_HOME` from `os.environ` at call time. The WebUI's per-request profile isolation (#798) is thread-local — set per-request from the `hermes_profile` cookie in `server.py`, cleared after the request — so those two mechanisms didn't talk to each other and `cron.jobs` always saw the process-default `HERMES_HOME` no matter which profile the request belonged to. CRUD operations silently wrote to the wrong `jobs.json`. **Fix:** two new context managers in `api/profiles.py:139-260`, both holding a module-level `_cron_env_lock`. `cron_profile_context()` is the HTTP-side variant (resolves home via `get_active_hermes_home()` which honors the TLS cookie, swaps `os.environ['HERMES_HOME']`, re-patches the cached `cron.jobs.HERMES_DIR/CRON_DIR/JOBS_FILE/OUTPUT_DIR` module constants, restores everything on exit). `cron_profile_context_for_home(home)` is the thread-side variant (worker threads have no TLS context, so the HTTP handler captures the active home at dispatch time and passes it explicitly). All 12 cron endpoints wrapped (6 GET + 6 POST). `_handle_cron_run` additionally captures the TLS-active home at dispatch and forwards it into `_run_cron_tracked(job, profile_home)` so cron output files land in the correct profile directory. Pre-release reviewer pushed test-skip-on-missing-agent fix so machines without `~/hermes-agent` run the suite cleanly. Post-review tightening: removed an over-broad `except Exception` around `get_active_hermes_home()` in `_handle_cron_run` (silent fallback to `_profile_home=None` would have re-introduced the exact bug the PR fixes — let any unexpected exception 500 the request rather than risk silent cross-profile state corruption); added thread-safety note on `os.environ` mutation explaining why `_cron_env_lock` is sufficient given CPython GIL semantics + `subprocess.Popen` env inheritance at fork time. 4 regression tests in `tests/test_scheduled_jobs_profile_isolation.py`. Two follow-up issues filed for architectural concerns (#1574 lock granularity, #1575 in-process scheduler bypass) — both deferred as out of scope. **Verified end-to-end via real browser test on isolated environment** (12 sessions, 3 projects, 6 default crons + 1 work-only-cron, 2 profiles): UI profile switch → cron tab auto-refreshes to show only target profile's jobs, both directions; on-disk verification confirmed perfect isolation in `~/.hermes/cron/jobs.json` (default profile) vs `~/.hermes/profiles/work/cron/jobs.json`.
- **Collapse duplicate provider groups + guard provider-id-as-model.default** (#1572; closes #1568) — reporter (Deor, Discord, relayed by @AvidFuturist) saw the Settings → Default Model dropdown rendering OpenCode Go provider as TWO separate optgroups: `"OpenCode Go"` (canonical, with all 14 catalog models) and `"Opencode_Go"` (phantom group containing one self-referential entry). Three structural causes (all in `api/config.py:_build_available_models_uncached`). **(1)** Detection-path id leakage — `cfg["providers"]` keys are read verbatim, so a config with `providers.opencode_go.api_key` (underscore variant) AND another path adding the canonical `opencode-go` (e.g. via `active_provider`) end up with both in `detected_providers`, creating two distinct provider groups with the second labelled via `pid.title()` fallback as `"Opencode_Go"`. **(2)** Injection-block rogue model — the default-model injection block puts ANY `model.default` string into the picker as a fake option, so a stray `model.default: opencode_go` (provider id mistakenly used as a model id) surfaces as a phantom model labelled `"Opencode GO"`. **(3)** Empty-group bleed — when a non-canonical provider id makes it into `detected_providers` but has no entry in `_PROVIDER_MODELS`, the build loop creates an optgroup with zero models. **Fix:** new `_canonicalise_provider_id()` helper folds underscores to hyphens, lowercases, applies alias resolution only when the alias target is itself canonical in `_PROVIDER_DISPLAY` (the constraint that prevents `x-ai` from round-tripping through the alias table to `xai`). Detection-path canonicalises before adding to `detected_providers`; same treatment in the `only_show_configured` intersection. Post-collection dedup pass re-canonicalises every entry (belt-and-braces against future regressions in any of the ~25 `detected_providers.add(...)` callsites). Provider-id guard on the model.default injection block — when the injected value matches a known provider display name or alias (after underscore/case normalization), skip the injection and emit a `logger.warning`. Real unknown model IDs (newly released models, custom endpoints) still get injected — only provider-shaped values are rejected. Empty-group filter at end of build (drops optgroups with zero models, with `custom:` exemption since users may want an empty card visible as a reminder). 17 new tests in `tests/test_issue1568_duplicate_provider_groups.py` covering the helper unit, dedup E2E, model.default guard, empty-group filter. Plus one structural test fix in `tests/test_issue604_all_providers_model_picker.py:test_cfg_providers_only_adds_known` — widened the regex window from 500 → 1500 chars so the new documentation comment block doesn't push `_PROVIDER_MODELS` past the substring slice (pre-existing brittle-window pattern, not a new issue).
### Tests
4053 → **4094 passing** (+41 net: +20 from #1569 Nous featured-set, +17 from #1572 dedup, +4 from #1571 cron isolation). 0 regressions. Full suite in 108s.
### Pre-release verification
- All 41 PR-related tests pass standalone.
- All 4094 tests pass in the full suite (clean state, no pre-existing flakes triggered).
- Browser sanity (HTTP API checks against port 8789): 11/11 endpoints verified.
- All modified JS files (`static/commands.js`, `static/panels.js`, `static/ui.js`) pass `node -c`.
- **Real-world browser testing** on isolated test environment (12 sessions, 3 projects, 6 default crons + 1 work cron, 4 skills, 2 profiles): profile switch via UI updates the chip, sidebar re-renders, **cron tab auto-refreshes to show only target profile's jobs**. On-disk verification confirms perfect isolation. Profile chip + cron tab UI confirmed by vision-model.
- Pre-release Opus advisor: SHIP AS-IS — no MUST-FIX. All 5 verification questions check out (conflict-free merge, no deadlock between `_cron_env_lock` and `_available_models_cache_lock`, subprocess env inheritance under lock verified, `_canonicalise_provider_id` dedup-pass idempotent, stale-fallback handling correct under partial network failure). One non-blocking symmetry nit on `_run_cron_tracked` worker-side broad-except flagged as a follow-up issue.
### Maintainer in-stage actions
- **PR rebase verified clean** (REBASE-DEFAULT rule applied). All 3 PR branches were on or near current master; rebase was no-op.
- **#1571 post-review fix combination**: contributor's `df03055` (post-review tightening) was on `pull/1571/head` while reviewer's `d83e1d8` (test-skip-on-missing-agent) was on `origin/fix/scheduled-jobs-profile-isolation`. Cherry-picked the test-skip commit onto the contributor branch to combine both fixes before merging into stage.
## [v0.50.287] — 2026-05-03
### Fixed (1 PR — closes another vector for the pending-message-loss class)
+1 -1
View File
@@ -2,7 +2,7 @@
> Web companion to the Hermes Agent CLI. Same workflows, browser-native.
>
> Last updated: v0.50.287 (May 03, 2026) — 4053 tests collected
> Last updated: v0.50.288 (May 03, 2026) — 4094 tests collected
> Test source: `pytest tests/ --collect-only -q`
> Per-version detail: see [CHANGELOG.md](./CHANGELOG.md)
+2 -2
View File
@@ -1835,8 +1835,8 @@ Bridged CLI sessions:
---
*Last updated: v0.50.287, May 03, 2026*
*Total automated tests collected: 4053*
*Last updated: v0.50.288, May 03, 2026*
*Total automated tests collected: 4094*
*Regression gate: tests/test_regressions.py*
*Run: pytest tests/ -v --timeout=60*
*Source: <repo>/*