gate-fix #5467: serialize full rows in render-skip signature (close false-skips)
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.
This commit is contained in:
1 parent
10e5e941d3
commit
3999418b0e
2 files changed
+52
-30
No files matched your search
+13
-21
@@ -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,
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in new issue
Block a user