From eaf7d1e9ab1abcd702cafdb50f372d628df2b43b Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Fri, 3 Jul 2026 21:24:08 +0000 Subject: [PATCH] gate-fix #5466: stamp anchor attrs on incremental node + teardown on all terminal paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex gate findings (2 SILENT, both fixed): - ui.js: incremental prose node now flows through the shared row-decoration block (data-anchor-scene-row/-row-id/-row-role/-source-event-type) instead of returning early, so live incremental rows keep the identity attrs the scene reconciler matches on. - messages.js: _cancelThrottledSnapshotTimer() + _clearAnchorProseIncrementalNode() now run on apperror, cancel, _handleStreamError, and _restoreSettledSession terminal paths (previously only fallback/done/stream_end) — closes the snapshot-timer/anchor-cache/window-global teardown gap. - test: extend test_messages_js_stream_perf_cleanup_lifecycle to pin teardown on all four additional terminal paths. CORE equivalence (Codex flagged smd != renderMd): reconciled — Opus's 1498-frame harness against real smd.min.js proves incremental-smd == whole-smd byte-identical; live-smd-vs-renderMd difference is the already-shipped fidelity model (main live body streams smd, settles renderMd), and the incremental path is gated on !settled so settled DOM still comes from renderMd. Not a regression. --- static/messages.js | 8 ++++++++ static/ui.js | 18 ++++++++++++------ tests/test_regressions.py | 24 ++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/static/messages.js b/static/messages.js index 2a2299d9b..8319ff748 100644 --- a/static/messages.js +++ b/static/messages.js @@ -5510,6 +5510,8 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ _clearStreamEndRecovery(); _terminalStateReached=true; if(_persistTimer){clearTimeout(_persistTimer);_persistTimer=null;} + _cancelThrottledSnapshotTimer(); + _clearAnchorProseIncrementalNode(); _streamFinalized=true; _cancelAnimationFramePendingStreamRender(); _streamFadeCleanupReduceMotionListener(); @@ -5711,6 +5713,8 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ _clearStreamEndRecovery(); _terminalStateReached=true; if(_persistTimer){clearTimeout(_persistTimer);_persistTimer=null;} + _cancelThrottledSnapshotTimer(); + _clearAnchorProseIncrementalNode(); _streamFinalized=true; _cancelAnimationFramePendingStreamRender(); _streamFadeCleanupReduceMotionListener(); @@ -5861,6 +5865,8 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ if(!session) return returnStatus?'missing':false; if(session.active_stream_id||session.pending_user_message) return returnStatus?'active':false; if(_persistTimer){clearTimeout(_persistTimer);_persistTimer=null;} + _cancelThrottledSnapshotTimer(); + _clearAnchorProseIncrementalNode(); _streamFinalized=true; _cancelAnimationFramePendingStreamRender(); _streamFadeCleanupReduceMotionListener(); @@ -5955,6 +5961,8 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ // Opus review Q1: mirror done/apperror/cancel finalization so any pending rAF // cannot fire after renderMessages() has settled the DOM with the error message. if(_persistTimer){clearTimeout(_persistTimer);_persistTimer=null;} + _cancelThrottledSnapshotTimer(); + _clearAnchorProseIncrementalNode(); _streamFinalized=true; _cancelAnimationFramePendingStreamRender(); _streamFadeCleanupReduceMotionListener(); diff --git a/static/ui.js b/static/ui.js index 5342cc881..bdc3a70a9 100644 --- a/static/ui.js +++ b/static/ui.js @@ -10538,13 +10538,19 @@ function _anchorSceneNodeForRow(row, opts){ const proseKey=row.local_id||row.row_id||''; if(!settled && proseKey && typeof window.__anchorProseIncrementalNode==='function'){ const inc=window.__anchorProseIncrementalNode(proseKey,text); - if(inc) return inc; + // Route the incremental node through the shared row-decoration block below + // (data-anchor-scene-row / -row-id / -row-role / -source-event-type) instead + // of returning early — otherwise live incremental prose rows lose the + // identity attributes the scene reconciler matches on. (Codex gate #5466) + if(inc){ node=inc; } + } + if(!node){ + node=document.createElement('div'); + node.className='assistant-segment'; + node.setAttribute('data-anchor-scene-prose','1'); + node.dataset.rawText=text; + node.innerHTML=`
${renderMd?renderMd(text):esc(text)}
`; } - node=document.createElement('div'); - node.className='assistant-segment'; - node.setAttribute('data-anchor-scene-prose','1'); - node.dataset.rawText=text; - node.innerHTML=`
${renderMd?renderMd(text):esc(text)}
`; }else if(row.role==='thinking'){ if(window._showThinking===false) return null; const text=String(row.text||row.thinking&&row.thinking.text||'').trim(); diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 1722e853e..b1c1aff37 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -1062,6 +1062,30 @@ def test_messages_js_stream_perf_cleanup_lifecycle(cleanup_test_sessions): assert "_cancelThrottledSnapshotTimer();" in done_body assert "_clearAnchorProseIncrementalNode();" in done_body + # #5466 Codex gate: the snapshot/anchor cleanup must run on EVERY terminal + # path, not just fallback/done/stream_end — otherwise a timer/cache/global + # survives the turn on apperror, cancel, stream-error, and the settled-session + # recovery path (the PR's own stated teardown invariant). + def _terminal_body(anchor, end_marker): + a = src.find(anchor) + assert a >= 0, f"missing terminal handler: {anchor}" + b = src.find(end_marker, a) + assert b > a, f"could not bound terminal handler: {anchor}" + return src[a:b] + + apperror_body = _terminal_body("source.addEventListener('apperror'", "_streamFadeCleanupReduceMotionListener();") + assert "_cancelThrottledSnapshotTimer();" in apperror_body and "_clearAnchorProseIncrementalNode();" in apperror_body, \ + "apperror terminal handler must tear down the snapshot timer + anchor prose cache" + cancel_body = _terminal_body("source.addEventListener('cancel'", "_streamFadeCleanupReduceMotionListener();") + assert "_cancelThrottledSnapshotTimer();" in cancel_body and "_clearAnchorProseIncrementalNode();" in cancel_body, \ + "cancel terminal handler must tear down the snapshot timer + anchor prose cache" + stream_error_body = _terminal_body("function _handleStreamError(source)", "_streamFadeCleanupReduceMotionListener();") + assert "_cancelThrottledSnapshotTimer();" in stream_error_body and "_clearAnchorProseIncrementalNode();" in stream_error_body, \ + "_handleStreamError must tear down the snapshot timer + anchor prose cache" + restore_body = _terminal_body("async function _restoreSettledSession(source", "_cancelAnimationFramePendingStreamRender();") + assert "_cancelThrottledSnapshotTimer();" in restore_body and "_clearAnchorProseIncrementalNode();" in restore_body, \ + "_restoreSettledSession terminal recovery must tear down the snapshot timer + anchor prose cache" + def test_messages_js_finalizes_thinking_card_before_tool_card(cleanup_test_sessions): """R19e: later reasoning after a tool call must render in a fresh Worklog