release: reload config cache on profile switch (#5645, #5619)

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 <rod.boev@gmail.com>
This commit is contained in:
nesquena-hermesandRod Boev committed 2026-07-06 02:33:45 +00:00
1 parent f7fe895c63
commit 7d3057c7c9
2 files changed
+43 -13

No files matched your search

+1
View File
@@ -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)
@@ -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