From 7707ea1d2a5463ca351b4173a836da70f4b3fe71 Mon Sep 17 00:00:00 2001 From: ai-ag2026 Date: Wed, 15 Jul 2026 09:56:54 +0200 Subject: [PATCH 1/2] fix(webui): stop password managers autofilling the search box, and keep unsaved sessions startable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two independent defects that chain into "new conversations cannot be started": every POST /api/session/draft and /api/chat/start 404s a few seconds after POST /api/session/new returned 200. 1. Search inputs are parsed as username fields. index.html contains no
at all, but does contain password inputs (the Settings "Access Password" panel). Chromium therefore groups the document's unowned fields into one synthetic password form and picks the first text input in the DOM — #sessionSearch, the sidebar conversation filter — as that form's username field, then autofills the saved account name into it on load. autocomplete="off" was already present; it is ignored for credential heuristics. The autofill fires oninput -> filterSessions() -> GET /api/sessions/search ?content=1&depth=5: a full content search the user never asked for, repeated on every page load. Fix: give the four search inputs type="search" plus the per-manager opt-outs (data-1p-ignore / data-lpignore / data-bwignore / data-form-type), and give the Settings password inputs a real owner with proper autocomplete tokens so Chromium scopes the credential form to them instead of to the whole document. The form uses display:contents, so layout is unchanged. CSS suppresses WebKit's native search clear button, since these fields ship their own (#sessionSearchClear). 2. A never-persisted session is treated as evictable. new_session() intentionally keeps a session in RAM until its first message (#1171), so the SESSIONS cache is its only copy. _session_is_evictable() (#4765) short-circuited on zero messages, reasoning that an empty shell "is recreated on next access". It is not: get_session() has no recreate path and raises KeyError, so both routes 404 and the session can never be started. The content search from (1) pulls every hit through get_session(), which blows past sessions_cache_max (default 300) and evicts the session being composed. That is why this reproduces within seconds on an install with ~1700 sessions, and why it grew worse as the session count rose. Fix: require proof of persistence before evicting, for empty sessions too — which is what the function's own docstring already promised ("Its full state is already persisted to the JSON sidecar"). The zero-message branch was the single path that bypassed it. #4765's memory bound is unaffected: sessions loaded from disk remain evictable, and its existing tests still pass. Trade-off: an unsaved empty session now stays resident for the process lifetime (one per "New Conversation" click, an empty object each). That is a deliberate application of the function's stated invariant — "slightly more RAM for a session we are unsure about is strictly better than evicting an unsaved session". Bounding it by age instead would also work if maintainers prefer. Validation: pytest tests/test_issue4765_sessions_lru_eviction.py -> 9 passed The added regression test fails on the unpatched predicate (verified by reverting api/models.py alone) and passes with the fix. Note: (1) fixes what triggers the churn, (2) fixes what turns that churn into an unstartable session. Either fix alone leaves a real defect in place, so they are submitted together. Co-Authored-By: Claude Opus 4.8 --- api/models.py | 16 +++++--- static/index.html | 24 ++++++++---- static/style.css | 7 +++- tests/test_issue4765_sessions_lru_eviction.py | 38 +++++++++++++++++++ 4 files changed, 72 insertions(+), 13 deletions(-) diff --git a/api/models.py b/api/models.py index 75c22b8e..78aebbfe 100644 --- a/api/models.py +++ b/api/models.py @@ -4056,6 +4056,14 @@ def _session_is_evictable(s) -> bool: on-disk ``message_count`` being at least the in-memory message count. A metadata-only stub is inherently backed by disk, so it is evictable. + The persistence requirement holds even for a session with ZERO messages. + ``new_session()`` deliberately does not touch disk until the first message + (#1171), so between "New Conversation" and the first send, this cache is the + session's ONLY copy. Evicting it does not discard a recreatable empty shell: + ``get_session()`` has no recreate path and raises ``KeyError``, so the very + next ``/api/session/draft`` or ``/api/chat/start`` 404s and the session can + never be started. Sessions the user is still composing must stay resident. + Anything we cannot positively prove is safe stays resident. Using slightly more RAM for a session we are unsure about is strictly better than evicting an active or unsaved session (task safety invariant: a half-done memory fix @@ -4077,14 +4085,12 @@ def _session_is_evictable(s) -> bool: if getattr(s, '_loaded_metadata_only', False): return True in_memory_count = len(getattr(s, 'messages', None) or []) - if in_memory_count == 0: - # A zero-message session has nothing to lose. If it was never persisted - # (brand new, no sidecar) dropping it only discards an empty shell; the - # next access recreates it. If it is persisted, it is trivially clean. - return True disk_count = _persisted_message_count(sid) if disk_count is None: return False # cannot prove it is on disk → keep it resident + if in_memory_count == 0: + # Persisted and empty → trivially clean, nothing to lose. + return True return disk_count >= in_memory_count diff --git a/static/index.html b/static/index.html index 0ca1bc78..8f9c625f 100644 --- a/static/index.html +++ b/static/index.html @@ -200,7 +200,7 @@ - +
@@ -225,7 +225,7 @@
- + @@ -253,7 +253,7 @@
- +
Loading...
@@ -353,8 +353,9 @@ - +
@@ -1541,11 +1542,20 @@
Enter a new password to set or change it. Leave blank to keep current setting.
- + + + +