Release PE (v0.51.444): invalidate sidebar session-list cache on WAL + settings updates (#4268) (#4270)

* stage-4268: invalidate sidebar cache on WAL + settings updates (#4268)

* Release PE: invalidate sidebar session-list cache on WAL + settings updates (#4268)

rodboev's #4268 (the sidebar-cache fix split out of #4133). Adds state.db-wal +
SETTINGS_FILE to _session_list_cache_source_stamp so uncommitted-WAL writes and
settings changes refresh the cached /api/sessions list. Codex SAFE + Opus SHIP
+ 9 cache tests green.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-06-15 14:47:10 -07:00
1 parent 2a3baa71b8
commit b875db776e
3 files changed
+82 -3

No files matched your search

+5 -1
View File
@@ -3,7 +3,11 @@
## [Unreleased]
## [v0.51.443] — 2026-06-15 — Release PD (scope session by-id reads + exports to active profile)
## [v0.51.444] — 2026-06-15 — Release PE (refresh sidebar session list on uncommitted writes + settings changes)
### Fixed
- **The sidebar session list now refreshes when new sessions are written but not yet checkpointed, and when settings change.** When CLI-session visibility is on, the `/api/sessions` response is cached and invalidated by a fingerprint of its source files. That fingerprint stamped the main `state.db` and the session index but not the SQLite write-ahead log (`state.db-wal`) — so a freshly-written CLI/agent session that was still in the WAL (not yet checkpointed into the main db file) could be missed until the next checkpoint, leaving the sidebar stale. The fingerprint now also stamps `state.db-wal` and the settings file, so those changes invalidate the cache immediately. Invalidation stays scoped to the CLI-session cache keys. (#4268)
### Security
+13 -2
View File
@@ -1475,14 +1475,18 @@ 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]]:
def _session_list_cache_source_stamp(key: tuple) -> tuple[tuple[int, int], tuple[int, int], tuple[int, int], tuple[int, int], tuple[int, 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))
return ((0, 0), (0, 0), (0, 0), (0, 0), (0, 0))
try:
state_db_path = Path(_active_state_db_path())
except Exception:
state_db_path = None
try:
state_db_wal_path = state_db_path.with_name(f"{state_db_path.name}-wal") if state_db_path is not None else None
except Exception:
state_db_wal_path = None
try:
gateway_metadata_path = _gateway_session_metadata_path()
except Exception:
@@ -1491,10 +1495,16 @@ def _session_list_cache_source_stamp(key: tuple) -> tuple[tuple[int, int], tuple
session_index_path = SESSION_DIR / "_index.json"
except Exception:
session_index_path = None
try:
settings_file = SETTINGS_FILE
except Exception:
settings_file = None
return (
_session_list_cache_path_stamp(state_db_path),
_session_list_cache_path_stamp(state_db_wal_path),
_session_list_cache_path_stamp(gateway_metadata_path),
_session_list_cache_path_stamp(session_index_path),
_session_list_cache_path_stamp(settings_file),
)
@@ -1867,6 +1877,7 @@ from api.config import (
CUSTOM_MODELS_ENDPOINT_TIMEOUT_SECONDS,
load_settings,
save_settings,
SETTINGS_FILE,
set_hermes_default_model,
model_with_provider_context,
get_reasoning_status,
+64
View File
@@ -292,6 +292,70 @@ def test_session_list_cache_rebuild_retries_after_invalidation():
assert calls == ["build", "build"]
def test_session_list_cache_source_stamp_tracks_state_db_wal(tmp_path, monkeypatch):
state_db = tmp_path / "state.db"
state_db.write_text("db", encoding="utf-8")
state_db_wal = tmp_path / "state.db-wal"
state_db_wal.write_text("wal-1", encoding="utf-8")
gateway = tmp_path / "gateway-sessions.json"
gateway.write_text("{}", encoding="utf-8")
session_dir = tmp_path / "sessions"
session_dir.mkdir()
(session_dir / "_index.json").write_text("{}", encoding="utf-8")
settings_file = tmp_path / "settings.json"
settings_file.write_text("{}", encoding="utf-8")
monkeypatch.setattr(routes, "_active_state_db_path", lambda: str(state_db))
monkeypatch.setattr(routes, "_gateway_session_metadata_path", lambda: gateway)
monkeypatch.setattr(routes, "SESSION_DIR", session_dir)
monkeypatch.setattr(routes, "SETTINGS_FILE", settings_file)
key = routes._session_list_cache_key(
active_profile="default",
all_profiles=False,
show_cli_sessions=True,
show_previous_messaging_sessions=False,
show_cron_sessions=False,
)
before = routes._session_list_cache_source_stamp(key)
state_db_wal.write_text("wal-2-more", encoding="utf-8")
after = routes._session_list_cache_source_stamp(key)
assert after != before
def test_session_list_cache_source_stamp_tracks_settings_file(tmp_path, monkeypatch):
state_db = tmp_path / "state.db"
state_db.write_text("db", encoding="utf-8")
gateway = tmp_path / "gateway-sessions.json"
gateway.write_text("{}", encoding="utf-8")
session_dir = tmp_path / "sessions"
session_dir.mkdir()
(session_dir / "_index.json").write_text("{}", encoding="utf-8")
settings_file = tmp_path / "settings.json"
settings_file.write_text('{"show_cli_sessions": false}', encoding="utf-8")
monkeypatch.setattr(routes, "_active_state_db_path", lambda: str(state_db))
monkeypatch.setattr(routes, "_gateway_session_metadata_path", lambda: gateway)
monkeypatch.setattr(routes, "SESSION_DIR", session_dir)
monkeypatch.setattr(routes, "SETTINGS_FILE", settings_file)
key = routes._session_list_cache_key(
active_profile="default",
all_profiles=False,
show_cli_sessions=True,
show_previous_messaging_sessions=False,
show_cron_sessions=False,
)
before = routes._session_list_cache_source_stamp(key)
settings_file.write_text('{"show_cli_sessions": true}', encoding="utf-8")
after = routes._session_list_cache_source_stamp(key)
assert after != before
def test_session_list_payload_to_response_overlays_live_stream_runtime(monkeypatch):
payload = {
"sessions": [