diff --git a/CHANGELOG.md b/CHANGELOG.md index d98ffd62..1a45e656 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ ### Fixed +- **The profile dropdown no longer serves stale rows sooner than intended.** The 4-second cache behind the profile list stamped its timestamp *before* the profile-row build (an `os.walk` across every profile's skill tree) rather than after, so the TTL clock started early — the cache effectively expired sooner than 4s by however long the build took, and a build slower than the TTL could produce an entry that was already stale on arrival. The timestamp is now taken after the rows are built (still inside the single-flight lock), so the full TTL measures from when the data actually became valid. Thanks @starship-s. (#6846) + - **Switching the workspace mid-session no longer rewrites the system prompt or invalidates the LLM prefix cache.** The active workspace (`s.workspace`) updates on every turn when you switch workspaces via the WebUI header dropdown, and that live value was interpolated into the system prompt (`msg[0]`) — so a mid-session switch mutated `msg[0]` and invalidated the model's prefix cache (APC / Radix Tree) for the entire transcript, silently re-billing and slowing every subsequent turn. Sessions now freeze an immutable `created_workspace` snapshot at creation and interpolate that (never the live workspace) into the system prompt, keeping `msg[0]` byte-stable across switches; the active workspace still reaches the model through the `[Workspace::v1: /path]` tag on the current user turn, so file operations continue to target the workspace you switched to. Legacy sessions with no persisted snapshot fall back to their recorded workspace. Thanks @webtecnica. (#6683, #6672) - **You can now scroll up to read earlier content while a response is still streaming.** During an in-flight programmatic (auto-follow) scroll, the programmatic-scroll guard returned before the native scroll listener could see the event, so a gentle upward wheel (a small `deltaY` above the ordinary −30 sticky-unpin threshold) was swallowed and live-tail follow kept yanking the reader back to the bottom. A `_freshProgrammaticScrollActive()` guard (a 150 ms validity window on the programmatic-scroll flag, refreshed by every programmatic scroll writer) now lets even a small upward wheel during a fresh programmatic scroll immediately stop auto-follow, so scrolling up mid-stream sticks. The ordinary −30 direct threshold and the touch/moved-up branches are unchanged, and the re-pin-only-at-true-bottom (#4295), gentle-wheel-intent (#4970), and footer-jitter (#4346) behaviors are preserved. Thanks @franksong2702. (#6453, #6414) diff --git a/api/profiles.py b/api/profiles.py index 22668048..337f2b9e 100644 --- a/api/profiles.py +++ b/api/profiles.py @@ -2114,7 +2114,7 @@ def list_profiles_api() -> list: else: rows = _build_profile_rows_fast() if rows is not None: - _LIST_PROFILES_CACHE = (rows, now) + _LIST_PROFILES_CACHE = (rows, time.time()) if rows is None: # Fallback: cheap helpers unavailable — use the original (slow) path, diff --git a/tests/test_issue5364_skills_stats_thundering_herd.py b/tests/test_issue5364_skills_stats_thundering_herd.py index dc70d4a8..12a9f6b4 100644 --- a/tests/test_issue5364_skills_stats_thundering_herd.py +++ b/tests/test_issue5364_skills_stats_thundering_herd.py @@ -301,6 +301,30 @@ class TestListProfilesSingleFlight: assert len(results) == n_threads assert all(r and r[0]["name"] == "default" for r in results) + def test_slow_build_gets_full_cache_ttl(self, mod, monkeypatch): + build_calls = [] + clock = [0.0] + + def _slow_build(): + build_calls.append(1) + clock[0] += 0.08 + return [{"name": "default", "path": "/x"}] + + with ( + patch.object(time, "time", side_effect=lambda: clock[0]), + patch.object(mod, "_is_isolated_profile_mode", return_value=False), + patch.object(mod, "get_active_profile_name", return_value="default"), + patch.object(mod, "_build_profile_rows_fast", side_effect=_slow_build), + ): + monkeypatch.setattr(mod, "_LIST_PROFILES_CACHE_TTL", 0.05) + mod._LIST_PROFILES_CACHE = None + mod.list_profiles_api() + mod.list_profiles_api() + + assert len(build_calls) == 1, ( + "cache age must start after a slow build completes, not before it starts" + ) + # --------------------------------------------------------------------------- # 4. #4783 contract preserved: the cheap mtime probe still runs on every call