mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-13 11:10:37 +00:00
fix(streaming): target the parser-owned TAIL live segment in the #3877 re-attach (Codex CORE)
Codex caught an asymmetry in the first cut: the rebuilt side selected the tail live segment ([...].pop()) but the preserved side used querySelector() = the FIRST [data-live-assistant=1] segment. In a multi-live-segment turn (reconnect / post-tool activity boundaries — see messages.js ensureAssistantRow re-attaching to the LAST live segment), the smd parser writes into the tail segment, so swapping the first preserved segment would move the wrong node and leave the parser-owned tail detached — re-introducing the flicker for multi-segment turns. Fix: select the preserved segment from querySelectorAll, defaulting to the LAST (tail), and prefer the one whose data-live-segment-seq matches the rebuilt tail; compute _preservedLen from that segment. Added a structural regression test pinning the tail selection. Verified live: a 2-live-segment turn (seq 1 + seq 2, parser on seq 2) keeps the parser-owned tail connected across a mid-stream rebuild (was orphaned with the first-segment selection).
This commit is contained in:
+20
-4
@@ -8997,17 +8997,33 @@ function renderMessages(options){
|
||||
// streaming.
|
||||
if(_preservedLiveTurn){
|
||||
const _rebuilt=document.getElementById('liveAssistantTurn');
|
||||
const _preservedSeg=_preservedLiveTurn.querySelector('[data-live-assistant="1"]');
|
||||
// Pick the PARSER-OWNED live segment, not just the first one. On reconnect /
|
||||
// post-tool activity boundaries a live turn can carry MULTIPLE
|
||||
// [data-live-assistant="1"] segments, and the smd parser writes into the
|
||||
// LAST (tail) one (see ensureAssistantRow in messages.js — it re-attaches to
|
||||
// the last live segment). Prefer the preserved segment whose
|
||||
// data-live-segment-seq matches the rebuilt tail (same logical segment), then
|
||||
// fall back to the last preserved live segment. Using querySelector() (first)
|
||||
// here would move the wrong segment and leave the parser-owned tail detached
|
||||
// in a multi-segment turn.
|
||||
const _rebuiltSegs=_rebuilt?_rebuilt.querySelectorAll('[data-live-assistant="1"]'):null;
|
||||
const _rebuiltSeg=(_rebuiltSegs&&_rebuiltSegs.length)?_rebuiltSegs[_rebuiltSegs.length-1]:null;
|
||||
const _preservedSegs=_preservedLiveTurn.querySelectorAll('[data-live-assistant="1"]');
|
||||
let _preservedSeg=_preservedSegs.length?_preservedSegs[_preservedSegs.length-1]:null;
|
||||
const _rebuiltSeq=_rebuiltSeg?_rebuiltSeg.getAttribute('data-live-segment-seq'):null;
|
||||
if(_rebuiltSeq){
|
||||
for(const _seg of _preservedSegs){
|
||||
if(_seg.getAttribute('data-live-segment-seq')===_rebuiltSeq){_preservedSeg=_seg;break;}
|
||||
}
|
||||
}
|
||||
const _preservedLen=_liveAssistantSegmentTextLength(_preservedSeg||_preservedLiveTurn);
|
||||
if(_preservedLen>0){
|
||||
const _rebuiltSegs=_rebuilt?_rebuilt.querySelectorAll('[data-live-assistant="1"]'):null;
|
||||
const _rebuiltSeg=(_rebuiltSegs&&_rebuiltSegs.length)?_rebuiltSegs[_rebuiltSegs.length-1]:null;
|
||||
const _rebuiltLen=_rebuilt?_liveAssistantSegmentTextLength(_rebuiltSeg||_rebuilt):-1;
|
||||
if(_rebuiltLen<=_preservedLen){
|
||||
if(S.session) _preservedLiveTurn.dataset.sessionId=S.session.session_id;
|
||||
if(_rebuilt&&_rebuiltSeg&&_preservedSeg){
|
||||
// Segment-level swap: keep the rebuilt turn (and its other segments /
|
||||
// tool groups) but restore the parser-referenced live segment.
|
||||
// tool groups) but restore the parser-referenced (tail) live segment.
|
||||
_rebuiltSeg.replaceWith(_preservedSeg);
|
||||
}else if(_rebuilt){
|
||||
// Rebuilt turn has no live segment to target — replace the whole turn.
|
||||
|
||||
@@ -135,6 +135,29 @@ def test_reattach_swaps_at_segment_level_to_preserve_rebuilt_structure():
|
||||
assert "_preservedSeg" in reattach and "_rebuiltSeg" in reattach
|
||||
|
||||
|
||||
def test_reattach_targets_the_parser_owned_tail_segment_not_the_first():
|
||||
"""Multi-live-segment turns (reconnect / post-tool activity boundaries) can have
|
||||
several [data-live-assistant="1"] segments; the smd parser writes into the LAST
|
||||
(tail) one. The re-attach must select the preserved TAIL segment — preferring the
|
||||
one whose data-live-segment-seq matches the rebuilt tail — not the first via a bare
|
||||
querySelector(). Picking the first would move the wrong segment and leave the
|
||||
parser-owned tail detached (Codex CORE finding on the #3877-reopen fix)."""
|
||||
body = _function_body(UI_JS, "renderMessages")
|
||||
reattach = body[body.find("Re-attach the preserved live turn (#3877)") :]
|
||||
# Preserved segment is chosen from querySelectorAll (tail), not querySelector (first).
|
||||
assert "_preservedSegs=_preservedLiveTurn.querySelectorAll('[data-live-assistant=\"1\"]')" in reattach
|
||||
assert "_preservedSegs[_preservedSegs.length-1]" in reattach, (
|
||||
"must default to the LAST preserved live segment (the parser-owned tail)"
|
||||
)
|
||||
# And prefer the segment whose live-segment-seq matches the rebuilt tail.
|
||||
assert "data-live-segment-seq" in reattach and "_rebuiltSeq" in reattach, (
|
||||
"must prefer the preserved segment matching the rebuilt tail's "
|
||||
"data-live-segment-seq before falling back to the last segment"
|
||||
)
|
||||
# The first-only querySelector form must NOT be how _preservedSeg is derived.
|
||||
assert "_preservedSeg=_preservedLiveTurn.querySelector(" not in reattach
|
||||
|
||||
|
||||
def test_reattach_runs_after_rebuild_loop():
|
||||
"""The re-attach must run AFTER the rebuild (so a freshly-rebuilt live turn exists to
|
||||
compare against / replace) but is still inside renderMessages."""
|
||||
|
||||
Reference in New Issue
Block a user