diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f4984dc..70d8b007 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.546] — 2026-06-21 — Release TE (fix a flaky gateway-sync CI test) + +### Fixed + +- **Eliminated a non-deterministic CI test flake (#4557).** `test_gateway_sync` could intermittently fail (a session missing from `/api/sessions`) when a settings-visibility toggle rewrote the settings file inside the same `mtime_ns` bucket, colliding two cache layers. The session-list cache source stamp now includes a monotonic per-process settings-write counter (so a same-millisecond rewrite can't reuse a stale cache entry), and the model-layer CLI-sessions cache is now flushed on the visibility toggle. Test-infrastructure reliability only — no runtime behavior change. Thanks @rodboev. + ## [v0.51.545] — 2026-06-20 — Release TD (extension manifest asset bundles) ### Added diff --git a/api/config.py b/api/config.py index 9f98ca7d..180c98ad 100644 --- a/api/config.py +++ b/api/config.py @@ -7372,6 +7372,9 @@ _SETTINGS_BOOL_KEYS = { # Language codes are validated as short alphanumeric BCP-47-like tags (e.g. 'en', 'zh', 'fr') _SETTINGS_LANG_RE = __import__("re").compile(r"^[a-zA-Z]{2,10}(-[a-zA-Z0-9]{2,8})?$") +_SETTINGS_WRITE_VERSION = 0 +_SETTINGS_WRITE_LOCK = __import__("threading").Lock() + def save_settings(settings: dict) -> dict: """Save settings to disk. Returns the merged settings. Ignores unknown keys.""" @@ -7481,6 +7484,9 @@ def save_settings(settings: dict) -> dict: json.dumps(persisted, ensure_ascii=False, indent=2), encoding="utf-8", ) + global _SETTINGS_WRITE_VERSION + with _SETTINGS_WRITE_LOCK: + _SETTINGS_WRITE_VERSION += 1 # Invalidate the in-memory password hash cache so the next call to # get_password_hash() picks up the new value from disk immediately. if _password_changed: diff --git a/api/routes.py b/api/routes.py index fa3b1fe8..6bffd474 100644 --- a/api/routes.py +++ b/api/routes.py @@ -1690,10 +1690,10 @@ def _session_list_cache_path_stamp(path: Path | None) -> tuple[int, int]: return (0, 0) -def _session_list_cache_source_stamp(key: tuple) -> tuple[tuple[int, int], tuple[int, int], tuple[int, int], tuple[int, int], tuple[int, int]]: +def _session_list_cache_source_stamp(key: tuple) -> tuple[tuple[int, int], tuple[int, int], tuple[int, int], tuple[int, int], tuple[int, int], object, int]: _cache_profile, _cache_all_profiles, cache_show_cli_sessions, *_rest = key if not cache_show_cli_sessions: - return ((0, 0), (0, 0), (0, 0), (0, 0), (0, 0)) + return ((0, 0), (0, 0), (0, 0), (0, 0), (0, 0), None, 0) try: state_db_path = Path(_active_state_db_path()) except Exception: @@ -1714,6 +1714,11 @@ def _session_list_cache_source_stamp(key: tuple) -> tuple[tuple[int, int], tuple settings_file = SETTINGS_FILE except Exception: settings_file = None + try: + from api.config import _SETTINGS_WRITE_VERSION + swv = _SETTINGS_WRITE_VERSION + except Exception: + swv = 0 return ( _session_list_cache_path_stamp(state_db_path), _session_list_cache_path_stamp(state_db_wal_path), @@ -1725,6 +1730,7 @@ def _session_list_cache_source_stamp(key: tuple) -> tuple[tuple[int, int], tuple # frame size), so without this a freshly-committed CLI/gateway session # could be served stale for the cache TTL. Mirrors the models-layer fix. _session_list_cache_state_db_fingerprint(state_db_path), + swv, ) @@ -11426,6 +11432,11 @@ def handle_post(handler, parsed) -> bool: _clear_session_list_cache() except Exception: pass + try: + from api.models import clear_cli_sessions_cache + clear_cli_sessions_cache() + except Exception: + pass auth_enabled_after = is_auth_enabled() auth_just_enabled = bool( diff --git a/tests/conftest.py b/tests/conftest.py index 8a52ae10..587f2ef1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1086,6 +1086,11 @@ def cleanup_test_sessions(): _post(TEST_BASE, "/api/settings", {"show_cli_sessions": False}) except Exception: pass + try: + from api.models import clear_cli_sessions_cache + clear_cli_sessions_cache() + except Exception: + pass yield created for sid in created: @@ -1111,6 +1116,11 @@ def cleanup_test_sessions(): _post(TEST_BASE, "/api/settings", {"show_cli_sessions": False}) except Exception: pass + try: + from api.models import clear_cli_sessions_cache + clear_cli_sessions_cache() + except Exception: + pass # ── Convenience helpers ────────────────────────────────────────────────────────