From 3999418b0e46a2ca2369abf2d42e77bfc770f4f0 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Fri, 3 Jul 2026 22:58:21 +0000 Subject: [PATCH] gate-fix #5467: serialize full rows in render-skip signature (close false-skips) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex gate found the render-skip signature omitted fields the sidebar renders: - CORE: pending/running (active_stream_id/has_pending_user_message/pending_started_at) - SILENT: attention dots - SILENT: source/read-only/worktree/lineage/child/model/profile + _sidebarReferenceSessions Any omitted field = a false-skip = stale sidebar. Root-cause fix (most reliable): serialize the FULL applied row objects + _sidebarReferenceSessions instead of a hand-picked allowlist — inherently complete, can't rot when a new rendered field is added. Rewrote the 'ignores non-display fields' test into positive full-serialization + no-strip regression assertions. --- static/sessions.js | 34 +++++-------- tests/test_webui_external_refresh_frontend.py | 48 +++++++++++++++---- 2 files changed, 52 insertions(+), 30 deletions(-) diff --git a/static/sessions.js b/static/sessions.js index bf82d935..4d80fbbe 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -4346,32 +4346,24 @@ function _syncSessionAttentionSoundState(sessions){ next.forEach((sig,sid)=>_sessionAttentionSoundState.set(sid,sig)); } -// Signature of everything the sidebar render reads from the applied payload + -// the coarse display state. Used to skip the full DOM rebuild when a poll -// returns data identical to what is already on screen (the common idle case). -// A streaming row's fields advance each poll so its signature changes and it -// still renders; client-only display toggles (search, active, density, select, -// lineage expansion) render through their own paths and are intentionally light -// here. Serialization failure returns null → never skip. (#5455 WS2.4) +// Signature of everything the sidebar render reads. Used to skip the full DOM +// rebuild when a poll returns data identical to what is already on screen (the +// common idle case). We serialize the FULL applied row objects (not a curated +// field subset) plus the reference/nesting rows and the coarse display state, so +// ANY server- or client-visible field the render helpers read (streaming/pending +// state, attention dots, source/read-only/worktree/lineage/child/model/profile +// meta, etc.) is covered — a narrow allowlist silently false-skips the moment a +// new rendered field is added (Codex #5467 gate: it omitted pending/running, +// attention, and the source/lineage cluster). A streaming/pending row's fields +// advance each poll so its signature changes and it still renders. Serialization +// failure returns null → never skip (fail-open). (#5455 WS2.4) let _lastSessionListRenderSig = null; function _sessionListRenderSignature(){ try{ const search=($('sessionSearch')&&$('sessionSearch').value)||''; - const sessionKeys = [ - 'session_id','id','display_name','title','created_at','started_at', - 'updated_at','last_message_at','last_activity','message_count', - 'actual_message_count','is_streaming','streaming','status','pinned', - 'archived','project_id','source','session_source','generation', - '_lineage_root_id','_lineage_tip_id','_compression_segment_count', - ]; - const sessionsSlim = Array.isArray(_allSessions) ? _allSessions.map(s => { - if(!s || typeof s !== 'object') return s; - const out={}; - sessionKeys.forEach(k=>{ if(Object.prototype.hasOwnProperty.call(s,k)) out[k]=s[k]; }); - return out; - }) : _allSessions; return JSON.stringify([ - sessionsSlim, + _allSessions, + _sidebarReferenceSessions, _allProjects, _activeSessionIdForSidebar(), search, diff --git a/tests/test_webui_external_refresh_frontend.py b/tests/test_webui_external_refresh_frontend.py index d37f253c..03816568 100644 --- a/tests/test_webui_external_refresh_frontend.py +++ b/tests/test_webui_external_refresh_frontend.py @@ -133,17 +133,47 @@ def test_session_event_profile_filter_tolerates_default_root_aliases(): assert "S.activeProfileIsDefault = !!data.is_default;" in PANELS_JS -def test_session_list_render_signature_ignores_non_display_session_fields(): +def test_session_list_render_signature_serializes_full_rows_not_a_narrow_allowlist(): + """The render-skip signature must serialize the FULL applied rows (+ reference + rows), not a curated field subset — a narrow allowlist silently false-skips + when a rendered field it omits changes (Codex #5467: it dropped pending/running + streaming state, attention dots, and the source/lineage cluster, so an + approval/clarify transition or a row starting to stream could keep a stale + sidebar). Serializing the whole row objects covers every field the render + helpers read, present and future. + """ start = SESSIONS_JS.find("function _sessionListRenderSignature()") assert start != -1 - block = SESSIONS_JS[start:start + 1500] - assert "const sessionKeys = [" in block - assert "const sessionsSlim = Array.isArray(_allSessions)" in block - assert "sessionKeys.forEach" in block - assert "_allSessions," not in block - assert "message_count" in block - assert "last_message_at" in block - assert "_compression_segment_count" in block + block = SESSIONS_JS[start:start + 900] + # Full-object serialization, not a hand-picked allowlist. + assert "const sessionKeys = [" not in block, \ + "signature must not use a narrow field allowlist (it false-skips on omitted fields)" + assert "sessionKeys.forEach" not in block + assert "_allSessions," in block, "signature must serialize the full applied rows" + assert "_sidebarReferenceSessions," in block, \ + "signature must include the hidden reference/nesting rows the sidebar renders" + # Fail-open on serialization failure (never skip on null). + assert "catch(_){ return null; }" in block + + +def test_session_list_render_signature_changes_on_pending_running_and_attention(): + """Regression for the Codex #5467 CORE/SILENT false-skips: because the + signature serializes whole rows, a change to pending/running state or the + attention dot changes the signature (so the sidebar repaints, not skips). + This is a structural guarantee of full-row serialization — assert the + signature does not strip those fields back out. + """ + start = SESSIONS_JS.find("function _sessionListRenderSignature()") + block = SESSIONS_JS[start:start + 900] + # None of the previously-omitted, render-consumed fields may be filtered out. + for stripped in ( + "delete out.active_stream_id", + "delete out.attention", + "delete out.has_pending_user_message", + ): + assert stripped not in block, f"signature must not strip {stripped!r}" + # The whole-object arrays are passed straight to JSON.stringify. + assert "JSON.stringify([" in block def test_pwa_pull_to_refresh_refreshes_session_list_not_page_when_available():