From 7d3057c7c951b8009a95a409fc6ad0efa67803c1 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Mon, 6 Jul 2026 02:33:45 +0000 Subject: [PATCH] release: reload config cache on profile switch (#5645, #5619) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root fix for the #5619 profile-switch config-cache bug: split the reload gate so a profile-PATH change forces reload unconditionally (was wrongly suppressed by in-memory overrides → stale default-profile config/models/keys), while a file-mtime change still respects overrides. Folded in a test-isolation fix (Codex): the stage302 regression test leaked _cfg_cache/_cfg_mtime/_cfg_path/_cfg_fingerprint/cfg globals (deepcopy snapshot + clear/update restore, since reload_config mutates _cfg_cache in place). Full gate: Codex SAFE (2 rounds), suite 12178/0, 121 profile/provider tests. Co-authored-by: Rod Boev --- CHANGELOG.md | 1 + ...est_stage302_config_override_regression.py | 55 ++++++++++++++----- 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index efe4f512..e5230e3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ ### Fixed +- **Switching profiles now serves that profile's models and providers — not the previous one's.** A profile switch could keep serving the default (or previously-active) profile's model catalog and provider config: for example, a llama.cpp `custom_providers` profile showed the wrong models/keys. The config cache treated a profile-path change the same as a file-modification-time change, so an active in-memory override wrongly suppressed the reload on switch. A profile switch now forces a config reload unconditionally (while a mere file-mtime change still respects in-memory overrides). Thanks @rodboev. (#5645, #5619) - **The sidebar now shows a newly created session immediately.** Creating a new conversation from inside the WebUI left the sidebar list stale on some code paths until a manual refresh; `newSession()` now refreshes the list (coalesced, so no double-repaint). Thanks @nankingjing. (#5632, #3874) - **No more stray composer divider when the left button group is fully hidden.** If you hid all four left composer controls (attach, saved prompts, mic, voice mode), the vertical divider to their right was left orphaned; it's now hidden with them. The divider visibility is also recomputed after the voice-mode preference is applied on both the settings-load boot path and the voice-mode toggle, so it no longer boots against a stale button state. Thanks @silent-reader-cn. (#5499, #5451) - **Mobile: scrolling up into history during a streaming reply no longer jumps the viewport to the top.** On touch devices, a recent change applied `content-visibility: auto` with a flat 1px intrinsic-size estimate to every off-screen message row; tall assistant rows (long answers / large tool results) collapsed to 1px, `scrollHeight` lurched down by tens of thousands of pixels, and the browser clamped `scrollTop` back to the top. `content-visibility` is now kept only for the short, size-predictable user rows (assistant rows keep layout/style containment without the height-collapse), and when you've scrolled up and the semantic anchor can't be restored, the view now holds position via native scroll-anchoring instead of snapping to a stale offset. Thanks @allenliang2022. (#5638, #5637) diff --git a/tests/test_stage302_config_override_regression.py b/tests/test_stage302_config_override_regression.py index 4c7ebb3f..4ed5ae14 100644 --- a/tests/test_stage302_config_override_regression.py +++ b/tests/test_stage302_config_override_regression.py @@ -87,19 +87,48 @@ def test_get_config_does_not_reload_when_only_in_memory_override_same_path_mtime encoding="utf-8", ) os.utime(config_path, (base_mtime, base_mtime)) - monkeypatch.setattr(config, "_get_config_path", lambda: config_path) - config.reload_config() - - test_override = { - "model": {"provider": "openai", "default": "gpt-test"}, - "providers": {}, + # reload_config() mutates module globals that monkeypatch does not restore + # (_cfg_cache/_cfg_mtime/_cfg_path/_cfg_fingerprint/cfg); snapshot + restore + # them so this test can't leak the temp config.yaml path into later tests + # (order-dependent flake — matches the cleanup in test_profile_switch_1200.py). + # NOTE: _cfg_cache is mutated IN PLACE by reload_config() (clear+repopulate the + # same dict object), so it must be snapshotted BY VALUE (deepcopy) and restored + # via clear()+update() — a bare reference would restore the already-mutated dict. + import copy as _copy + _saved_cache = _copy.deepcopy(getattr(config, "_cfg_cache", {}) or {}) + _saved = { + "_cfg_mtime": getattr(config, "_cfg_mtime", None), + "_cfg_path": getattr(config, "_cfg_path", None), + "_cfg_fingerprint": getattr(config, "_cfg_fingerprint", None), + "cfg": getattr(config, "cfg", None), } - monkeypatch.setattr(config, "cfg", test_override, raising=False) + try: + monkeypatch.setattr(config, "_get_config_path", lambda: config_path) + config.reload_config() - os.utime(config_path, (base_mtime + 1.0, base_mtime + 1.0)) + test_override = { + "model": {"provider": "openai", "default": "gpt-test"}, + "providers": {}, + } + monkeypatch.setattr(config, "cfg", test_override, raising=False) - # The same_path mtime-only reload would normally trigger reload, but the - # override-detection should suppress it. - result = config.get_config() - assert result is test_override - assert result["model"]["provider"] == "openai" + os.utime(config_path, (base_mtime + 1.0, base_mtime + 1.0)) + + # The same_path mtime-only reload would normally trigger reload, but the + # override-detection should suppress it. + result = config.get_config() + assert result is test_override + assert result["model"]["provider"] == "openai" + finally: + # Restore _cfg_cache contents in place (it's mutated in place by reload). + try: + if isinstance(getattr(config, "_cfg_cache", None), dict): + config._cfg_cache.clear() + config._cfg_cache.update(_saved_cache) + except Exception: + pass + for _name, _val in _saved.items(): + try: + setattr(config, _name, _val) + except Exception: + pass