mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-16 12:40:18 +00:00
gate-fix #5466: stamp anchor attrs on incremental node + teardown on all terminal paths
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.
This commit is contained in:
@@ -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();
|
||||
|
||||
+12
-6
@@ -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=`<div class="msg-body">${renderMd?renderMd(text):esc(text)}</div>`;
|
||||
}
|
||||
node=document.createElement('div');
|
||||
node.className='assistant-segment';
|
||||
node.setAttribute('data-anchor-scene-prose','1');
|
||||
node.dataset.rawText=text;
|
||||
node.innerHTML=`<div class="msg-body">${renderMd?renderMd(text):esc(text)}</div>`;
|
||||
}else if(row.role==='thinking'){
|
||||
if(window._showThinking===false) return null;
|
||||
const text=String(row.text||row.thinking&&row.thinking.text||'').trim();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user