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:
3 files changed
+82
-3
No files matched your search
+5
-1
@@ -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
@@ -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,
|
||||
|
||||
@@ -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": [
|
||||
|
||||
Reference in new issue
Block a user