* fix(profiles): start list cache TTL after row build Slow row builds currently consume the cache TTL before the result is stored. Timestamp the cache at completion so every successful build receives the full TTL. Refs #5364 Assisted-by: Hermes Agent:gpt-5.6-sol Assisted-by: Codex:gpt-5.6-luna * Release exp-v0.52.176: start profile list-cache TTL after row build (#6846) --------- Co-authored-by: starship-s <45587122+starship-s@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
This commit is contained in:
3 files changed
+27
-1
No files matched your search
@@ -25,6 +25,8 @@
|
|||||||
|
|
||||||
### Fixed
|
### 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)
|
- **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)
|
- **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)
|
||||||
|
|||||||
+1
-1
@@ -2114,7 +2114,7 @@ def list_profiles_api() -> list:
|
|||||||
else:
|
else:
|
||||||
rows = _build_profile_rows_fast()
|
rows = _build_profile_rows_fast()
|
||||||
if rows is not None:
|
if rows is not None:
|
||||||
_LIST_PROFILES_CACHE = (rows, now)
|
_LIST_PROFILES_CACHE = (rows, time.time())
|
||||||
|
|
||||||
if rows is None:
|
if rows is None:
|
||||||
# Fallback: cheap helpers unavailable — use the original (slow) path,
|
# Fallback: cheap helpers unavailable — use the original (slow) path,
|
||||||
|
|||||||
@@ -301,6 +301,30 @@ class TestListProfilesSingleFlight:
|
|||||||
assert len(results) == n_threads
|
assert len(results) == n_threads
|
||||||
assert all(r and r[0]["name"] == "default" for r in results)
|
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
|
# 4. #4783 contract preserved: the cheap mtime probe still runs on every call
|
||||||
|
|||||||
Reference in new issue
Block a user