Constituent PRs: - #1768 (@franksong2702) serialize Anthropic env fallback reads. Closes #1736. - #1778 (@Michaelyklam) preserve CLI session tool metadata. Closes #1772. - #1779 (@Michaelyklam) reset model picker on session switch. Closes #1771. AUTO-FIX: Opus stage-310 caught a regression in the new !hasSessionModel branch — it dropped the deferModelCorrection guard that the parallel else-branch keeps. Fired spurious /api/session/update POSTs against imported/read-only CLI sessions whose model field reads 'unknown' (the exact surface #1778 introduces in this same release). Wrapped the new branch's _persistSessionModelCorrection call + state mutation in if(!deferModelCorrection). Added test_sync_topbar_does_not_persist_correction_while_model_resolution_deferred regression test covering both empty and 'unknown' fast-path interaction. Tests: 4694 → 4702 collected (+8). 4695 passed, 4 skipped, 3 xpassed, 0 failed in 141.29s. Pre-release verification: - All 3 PRs CI-green individually. - node -c clean on static/ui.js. - 11/11 browser API endpoints PASS. - Pre-stamp re-fetch: all PR heads match local rebases. - Opus advisor: SHIP #1768 + #1778, #1779 SHOULD-FIX before merge — auto-fix applied at stage with regression test, re-verified clean. Closes #1736, #1771, #1772.
This commit is contained in:
parent
db132b97db
commit
c38ee6c339
5 changed files with 72 additions and 8 deletions
24
CHANGELOG.md
24
CHANGELOG.md
|
|
@ -1,5 +1,29 @@
|
|||
# Hermes Web UI -- Changelog
|
||||
|
||||
## [v0.51.16] — 2026-05-07 — 3-PR contributor batch (anthropic env race close, CLI tool metadata, model picker reset)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **PR #1768** by @franksong2702 — Serialize Anthropic env fallback reads (closes #1736, the architectural follow-up filed in v0.51.8 sweep). Wraps `_clear_anthropic_env_values()` and the runtime-provider resolver behind `_ENV_LOCK` (the same `threading.Lock` already serializing env save/restore in `streaming.py`). New helper `resolve_runtime_provider_with_anthropic_env_lock()` in `api/oauth.py` is called from 3 sites in `api/routes.py` and 2 in `api/streaming.py`. Opus stage-310 verified: same-lock not a new lock (no ordering risk), nested acquires are sequential not nested (no deadlock), the lock is released before the agent runs (chat throughput unaffected). `api/oauth.py +36`, `api/routes.py +18`, `api/streaming.py +16`, +52 LOC test coverage in `tests/test_issue1362_codex_oauth_onboarding.py`. Race window in `_clear_anthropic_env_values` now closed for the chat hot path; remaining detector-style polls in `api/config.py` are UI-only and never bypass real credentials.
|
||||
- **PR #1778** by @Michaelyklam — Preserve CLI session tool metadata (closes #1772). The server's CLI session loader was reading only `role`, `content`, `timestamp` from `state.db.messages`, missing tool_calls/tool_results columns. `api/models.py +54` extends the loader to read those columns plus `reasoning_details`, `codex_reasoning_items`, `codex_message_items`, `reasoning_content`, `reasoning` and rehydrate them onto the message dicts. `PRAGMA table_info(messages)` check ensures legacy state.db schemas without the columns don't error. `_is_cli_tool_metadata_enrichment()` correctly rebuilds sidecars when message count is identical but new metadata is present, and uses `save(touch_updated_at=False)` to avoid bumping updated_at on passive enrichment. `api/routes.py +66`, 152 LOC test coverage in `tests/test_cli_session_tool_metadata.py` plus captured API evidence at `docs/pr-media/1772/cli-tool-metadata-api-evidence.json`.
|
||||
- **PR #1779** by @Michaelyklam — Reset model picker on session switch (closes #1771). Bug: switching sessions silently kept the previous chat's model selected in the composer (could route an inexpensive chat to an expensive model unnoticed — high-impact for users on premium-credit OAuth providers). Fix in `static/ui.js +88/-29`: when session model metadata is missing, `unknown`, or stale, fall back to configured default model/provider, with first-available dropdown option only as last resort. **Auto-fix applied at stage**: Opus stage-310 caught a regression in the new `!hasSessionModel` branch — it dropped the `deferModelCorrection` guard that the parallel else-branch keeps. Without the guard, every fast-path session view of an empty/unknown-model session fired a spurious `/api/session/update` POST that raced `_resolveSessionModelForDisplaySoon` and silently wrote to imported/read-only CLI sessions whose model field reads `"unknown"` (#1778 introduces exactly that surface in this same release). Wrapped the new branch's `_persistSessionModelCorrection` call + state mutation in `if(!deferModelCorrection)` mirroring the else-branch. Added `test_sync_topbar_does_not_persist_correction_while_model_resolution_deferred` regression test that exercises the fast-path interaction with `_modelResolutionDeferred=true` for both empty and `"unknown"` model values; asserts the visible `sel.value` still updates for UX but no POST is issued and no state mutation occurs. 192 LOC of original regression coverage in `tests/test_issue1771_session_model_switch_sync.py` (now 215 LOC with the new test), 7 LOC tweak to `test_provider_mismatch.py` and 1 LOC to `test_session_metadata_fast_path.py` to align existing tests with the new fallback helper.
|
||||
|
||||
### Tests
|
||||
|
||||
4694 → **4702 collected** (+8 across 2 new test files plus 1 stage auto-fix regression test). 4695 passed, 4 skipped (2 dev-only spawn from v0.51.15 + 2 prong-2 noise), 3 xpassed, 0 failed in 141.29s.
|
||||
|
||||
### Pre-release verification
|
||||
|
||||
- All 3 PRs CI-green individually.
|
||||
- File overlap on `api/routes.py` (#1768 + #1778) auto-merged cleanly (different functions: oauth env-lock helpers vs CLI session loader extension).
|
||||
- `node -c` clean on `static/ui.js`; Python compile clean on all 6 changed .py files.
|
||||
- pytest: 4695 passed, 0 failed.
|
||||
- `scripts/run-browser-tests.sh`: all 11 endpoints PASS on isolated port 8789.
|
||||
- Pre-stamp re-fetch: all 3 PR heads still match local rebases.
|
||||
- Opus advisor: SHIP #1768 + #1778, #1779 SHOULD-FIX before merge — auto-fix applied at stage with regression test, re-verified clean.
|
||||
|
||||
Closes #1736, #1771, #1772.
|
||||
|
||||
## [v0.51.15] — 2026-05-07 — 4-PR contributor batch + 1 self-built (cron spawn migration, context menu, codex quota, model prefix)
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
> Web companion to the Hermes Agent CLI. Same workflows, browser-native.
|
||||
>
|
||||
> Last updated: v0.51.15 (May 7, 2026) — 4694 tests collected — 4-PR contributor batch + 1 self-built (#1762, #1767, #1769, #1770)
|
||||
> Last updated: v0.51.16 (May 7, 2026) — 4702 tests collected — 3-PR contributor batch (#1768, #1778, #1779)
|
||||
> Test source: `pytest tests/ --collect-only -q`
|
||||
> Per-version detail: see [CHANGELOG.md](./CHANGELOG.md)
|
||||
|
||||
|
|
|
|||
|
|
@ -1835,8 +1835,8 @@ Bridged CLI sessions:
|
|||
|
||||
---
|
||||
|
||||
*Last updated: v0.51.15, May 7, 2026*
|
||||
*Total automated tests collected: 4694*
|
||||
*Last updated: v0.51.16, May 7, 2026*
|
||||
*Total automated tests collected: 4702*
|
||||
*Regression gate: tests/test_regressions.py*
|
||||
*Run: pytest tests/ -v --timeout=60*
|
||||
*Source: <repo>/*
|
||||
|
|
|
|||
19
static/ui.js
19
static/ui.js
|
|
@ -3711,10 +3711,21 @@ function syncTopbar(){
|
|||
// first, then the first available option only as an HTML fallback.
|
||||
const fallback=_applySessionModelFallback(modelSel);
|
||||
if(fallback){
|
||||
S.session.model=fallback.model;
|
||||
S.session.model_provider=fallback.model_provider||null;
|
||||
currentModel=fallback.model;
|
||||
_persistSessionModelCorrection(fallback.model,S.session.model_provider||null);
|
||||
// Defer state mutation + network write while the live model resolution
|
||||
// is in flight — sessions.js sets _modelResolutionDeferred=true between
|
||||
// the fast-path session render and the resolve_model=1 round-trip.
|
||||
// Persisting here would race that resolution and would also issue
|
||||
// silent /api/session/update POSTs against imported/read-only CLI
|
||||
// sessions whose model field reads "unknown" (#1779 stage-310 review).
|
||||
// The visible sel.value change still happens above for UX; only the
|
||||
// state mutation + persist defers.
|
||||
const deferModelCorrection=Boolean(S.session._modelResolutionDeferred);
|
||||
if(!deferModelCorrection){
|
||||
S.session.model=fallback.model;
|
||||
S.session.model_provider=fallback.model_provider||null;
|
||||
currentModel=fallback.model;
|
||||
_persistSessionModelCorrection(fallback.model,S.session.model_provider||null);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const applied=_applyModelToDropdown(currentModel,modelSel,S.session.model_provider||null);
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ var S = {
|
|||
model: args.sessionModel,
|
||||
model_provider: args.sessionProvider || null,
|
||||
messages: [],
|
||||
_modelResolutionDeferred: !!args.modelResolutionDeferred,
|
||||
},
|
||||
messages: [],
|
||||
activeProfile: 'default',
|
||||
|
|
@ -143,7 +144,7 @@ def driver_path(tmp_path_factory):
|
|||
return str(p)
|
||||
|
||||
|
||||
def _run_sync(driver_path, *, session_model, initial_value="@expensive:gpt-5.5", default_model="@safe:gpt-4o-mini", dropdown_open=False):
|
||||
def _run_sync(driver_path, *, session_model, initial_value="@expensive:gpt-5.5", default_model="@safe:gpt-4o-mini", dropdown_open=False, model_resolution_deferred=False):
|
||||
payload = {
|
||||
"sessionModel": session_model,
|
||||
"sessionProvider": None,
|
||||
|
|
@ -151,6 +152,7 @@ def _run_sync(driver_path, *, session_model, initial_value="@expensive:gpt-5.5",
|
|||
"defaultModel": default_model,
|
||||
"activeProvider": "safe",
|
||||
"dropdownOpen": dropdown_open,
|
||||
"modelResolutionDeferred": model_resolution_deferred,
|
||||
"options": [
|
||||
{"provider": "expensive", "value": "@expensive:gpt-5.5", "label": "GPT-5.5"},
|
||||
{"provider": "safe", "value": "@safe:gpt-4o-mini", "label": "GPT-4o mini"},
|
||||
|
|
@ -190,3 +192,30 @@ def test_sync_topbar_rerenders_open_visible_model_dropdown_after_session_model_c
|
|||
assert got["selectValue"] == "@safe:gpt-4o-mini"
|
||||
assert got["calls"]["renderModelDropdown"] >= 1
|
||||
assert got["calls"]["positionModelDropdown"] >= 1
|
||||
|
||||
|
||||
|
||||
def test_sync_topbar_does_not_persist_correction_while_model_resolution_deferred(driver_path):
|
||||
"""Regression for stage-310 Opus review: the !hasSessionModel branch must
|
||||
skip the network write + state mutation while sessions.js has set
|
||||
_modelResolutionDeferred=true between the fast-path session render and
|
||||
the resolve_model=1 round-trip.
|
||||
|
||||
Without this guard, every fast-path session view of an empty/unknown-model
|
||||
session fires a /api/session/update POST that races _resolveSessionModelForDisplaySoon
|
||||
and thrashes imported/read-only CLI sessions whose model field reads "unknown"
|
||||
(#1778 introduced exactly that surface in v0.51.16).
|
||||
"""
|
||||
got_empty = _run_sync(driver_path, session_model="", model_resolution_deferred=True)
|
||||
# Visible UX still happens (sel.value gets the safe default) ...
|
||||
assert got_empty["selectValue"] == "@safe:gpt-4o-mini"
|
||||
# ... but session state is NOT mutated and NO POST is issued.
|
||||
assert got_empty["sessionModel"] == "", "S.session.model must not be mutated while resolution is deferred"
|
||||
update_calls = [c for c in got_empty["calls"]["fetches"] if "session" in c["url"] and "update" in c["url"]]
|
||||
assert update_calls == [], f"no /api/session/update POSTs while deferred (got {update_calls})"
|
||||
|
||||
got_unknown = _run_sync(driver_path, session_model="unknown", model_resolution_deferred=True)
|
||||
assert got_unknown["selectValue"] == "@safe:gpt-4o-mini"
|
||||
assert got_unknown["sessionModel"] == "unknown"
|
||||
update_calls_u = [c for c in got_unknown["calls"]["fetches"] if "session" in c["url"] and "update" in c["url"]]
|
||||
assert update_calls_u == [], "imported/read-only CLI session with model=unknown must not be silently written"
|
||||
|
|
|
|||
Loading…
Reference in a new issue