fix(#894): re-derive the render gate on DOM-live signals; drop the busy conflation

Review round 5 step-back (fix-era critical): the r4 gate's busy term
conflated 'a turn is executing' with 'this DOM holds live turn state'.
_editAndResend flips busy BEFORE its POST and /rewind emits only
clear_ui (no state_change), so the busy term skipped the truncation
render the rewind exists to produce and appended the resent bubble onto
the PRE-rewind transcript; /retry's regenerated turn likewise raced its
own clear_ui refetch.  The seam was re-derived once against the caller
x state matrix; the gate reads DOM-live signals only, split by scope:

- UNIVERSAL: dispatch seq (refetchSeq — overlapping fetches resolve
  last-DISPATCH-wins; an older snapshot landing late can neither
  double-render nor clear the latch over newer truth) and the content
  refs (skipping always beats stranding a ref; seeded callers null
  theirs before fetching, so it never blocks them).
- SEEDLESS-ONLY (keyed on the seedCursor arg): the
  .conv-batch--running DOM marker for the tool phase (NOT activeBatch —
  that is the pending-APPROVAL tracker, set only for opts.pending
  batches; ruled at-site), coordSend's busySource === 'optimistic'
  flavor (the one busy that marks un-committed DOM), and
  stream-OPENness (CONNECTING keeps the handle with a frozen cursor
  and a pending replay; handle-existence was not liveness — also
  applied to the retry's fire guard).  Seedless-only because the
  SEEDED resync renders over these deliberately: after a node dies
  mid-batch the --running class is dead residue no result will ever
  strip, and the resync's render IS the recovery — a universal term
  wedged the coord-restart scenario outright (family-run find; the
  r5 finders missed the seeded-path interaction).

Backstop comment corrected (r5): fresh/truncated SSE replays DO carry
a synthetic state_change (replay_ok does not) — a latched pane pays one
refetch per reconnect, bounded by reconnect jitter/backoff and #884's
server single-flight; heal-caused triggers remain structurally
impossible.  Caller fire-time ref guards demoted to the efficiency
layer at-site.  Contract test re-pins the gate: term presence in
comment-stripped CODE, universal-vs-seedless placement, wipe between
failure guard and latch-clear, no plain busy.  The edit-resend commit
gap under a second actor's clear_ui is accepted at-site (re-appears at
settle heal).  G4's honesty note names the tool-phase branch it
behaviorally detects; G5 wording replay_ok-precise.  Full coord family
(C + G1-G5) green; 136 pins green; 5 static mutants + the G4
behavioral control caught.
This commit is contained in:
Patrick Buckley
2026-07-24 11:51:32 -07:00
parent 54631d3111
commit fac2393967
3 changed files with 275 additions and 151 deletions
+58 -54
View File
@@ -133,10 +133,10 @@ Scenario G5 (coord-hidden-retry, #894 r4): the retry's stream-liveness
fire guard. A retry armed before close-on-hide must NOT fetch while the
transport is down (a seedless render past the frozen ``lastEventId``
double-renders on the show-edge replay): ``history_requests`` UNCHANGED
across the hidden fire window (``hidden0``). A quiet reconnect delivers
no state_change edge, so post-show the latch stays closed (the accepted
liveness-lag residual) until the runner drives an ORGANIC settle with a
plain send; that idle edge fires the TRANSPORT-FREE backstop on the live
across the hidden fire window (``hidden0``). A replay_ok reconnect
carries no synthetic state_change (only fresh/truncated replays do), so
post-show the latch stays closed (the accepted liveness-lag residual)
until the runner drives an ORGANIC settle with a plain send; that idle edge fires the TRANSPORT-FREE backstop on the live
stream (exactly ONE new SSE open across show + heal — the user-driven
reconnect; the heal adds zero). Stamps
``RECOVERY-READY-COORDHIDDENRETRY-hidden0-heal1``.
@@ -958,6 +958,42 @@ COORD_PAGE_HTML = r"""<!doctype html>
userRows;
};
// G3 — the TRANSPORT-FREE idle-edge backstop. Mirrors
// __verifyStaleBackstop; the storm assertion is sseDelta === 0 (the
// heal opened ZERO EventSource connections — a reconnecting backstop
// bumps events_requests once per reconnect and self-triggers). The
// latch-cleared proof is the reopen POST (posts 2), not a field read:
// coord's latch is closure-private.
window.__verifyCoordStaleBackstop = function (
healed,
sseDelta,
histDelta,
gatedPosts,
posts,
) {
const userRows = _coordUserRows();
const ok =
healed &&
sseDelta === 0 &&
histDelta === 1 &&
gatedPosts === 1 &&
posts === 2;
document.title = ok
? "RECOVERY-READY-COORDSTALEBACKSTOP-heal1-sse0"
: "RECOVERY-FAILED-COORDSTALEBACKSTOP-heal" +
(healed ? 1 : 0) +
"-sse" +
sseDelta +
"-hist" +
histDelta +
"-gated" +
gatedPosts +
"-posts" +
posts +
"-rows" +
userRows;
};
// G4 — the render-time gate (#894 r4): a turn that STARTS during a
// heal's in-flight /history must SURVIVE its resolution (the gate
// skips the wipe; pre-gate code detached the live turn into a
@@ -1022,42 +1058,6 @@ COORD_PAGE_HTML = r"""<!doctype html>
userRows;
};
// G3 — the TRANSPORT-FREE idle-edge backstop. Mirrors
// __verifyStaleBackstop; the storm assertion is sseDelta === 0 (the
// heal opened ZERO EventSource connections — a reconnecting backstop
// bumps events_requests once per reconnect and self-triggers). The
// latch-cleared proof is the reopen POST (posts 2), not a field read:
// coord's latch is closure-private.
window.__verifyCoordStaleBackstop = function (
healed,
sseDelta,
histDelta,
gatedPosts,
posts,
) {
const userRows = _coordUserRows();
const ok =
healed &&
sseDelta === 0 &&
histDelta === 1 &&
gatedPosts === 1 &&
posts === 2;
document.title = ok
? "RECOVERY-READY-COORDSTALEBACKSTOP-heal1-sse0"
: "RECOVERY-FAILED-COORDSTALEBACKSTOP-heal" +
(healed ? 1 : 0) +
"-sse" +
sseDelta +
"-hist" +
histDelta +
"-gated" +
gatedPosts +
"-posts" +
posts +
"-rows" +
userRows;
};
window.__verifyCoordRestart = function () {
// Same contract as Scenario B, read off the coordinator's public
// chrome + the transport wrapper (idle is asserted SERVER-side by
@@ -1092,7 +1092,6 @@ COORD_PAGE_HTML = r"""<!doctype html>
</html>
"""
# ---------------------------------------------------------------------------
# Minimal dependency-free CDP client (WebSocket over a raw socket).
# ---------------------------------------------------------------------------
@@ -2808,17 +2807,19 @@ def run_coord_heal_midturn(chrome: str) -> str:
seeds two/three gone. Storm proof: zero SSE opens across the whole
episode; exactly TWO /history fetches (the skipped one + the heal).
Detector honesty: ``history_delta == 2`` is the DISCRIMINATING bit.
Detector honesty: ``history_delta == 2`` is the DISCRIMINATING bit,
and the skip it witnesses rides the ``.conv-batch--running`` DOM
marker — turn 5 is in its TOOL phase at resolution (content refs
null), so this scenario is the behavioral detector for the gate's
tool-phase branch (the content-ref branch and the liveness statement
carry structural pins; G5 covers the hidden-retry liveness path).
Gate-stripped code renders the held fetch early, which CLEARS the
latch, kills the backstop refire, and stamps ``hist1`` (plus
downstream posts/rows drift). The ``mid1`` bit alone cannot
discriminate here: the early render repaints all three user rows from
the snapshot (the rewind pre-dates turn 4, so /history already
carries turns 4 and 5's user rows), and turn 5's tool phase has null
content refs — its per-token loss surface is the TOOL rows, which
this scenario does not fingerprint. On gated code ``mid1`` asserts
the stronger continuous-visibility claim (nothing wiped at any
point)."""
discriminate: the early render repaints all three user rows from the
snapshot (the rewind pre-dates turn 4, so /history already carries
turns 4 and 5's user rows). On gated code ``mid1`` asserts the
stronger continuous-visibility claim (nothing wiped at any point)."""
from tests._sse_recovery_server import final_text_script, parallel_bash_script
paced5 = parallel_bash_script({"g4": "for i in $(seq 1 50); do echo g4-$i; sleep 0.05; done"})
@@ -2939,8 +2940,9 @@ def run_coord_hidden_retry(chrome: str) -> str:
(hiddenDelta 0); on show, the reconnect's synthetic idle re-fires the
TRANSPORT-FREE backstop, which heals on the live stream.
A quiet reconnect delivers NO state_change edge, so the latch stays
closed after __show until the next ORGANIC settle — exactly the
A replay_ok reconnect (frozen cursor, nothing lost) carries no
synthetic state_change — only fresh/truncated replays do — so the
latch stays closed after __show until the next ORGANIC settle — exactly the
accepted-residual ruling (heals ride organic edges; no timer may
shortcut the lag). The runner drives that settle with a plain send
(sends are never latch-gated), whose turn-settle idle edge fires the
@@ -2987,9 +2989,11 @@ def run_coord_hidden_retry(chrome: str) -> str:
# (the hidden fetch lands) and hiddenDelta stamps 1.
_poll_until(lambda: node.history_requests != hidden_baseline, 3.5, 0.1)
hidden_delta = node.history_requests - hidden_baseline
# Show: the reconnect presents the frozen cursor. A quiet
# reconnect delivers NO state_change edge (the latch-closed lag is
# the accepted residual), so wait for the reconnect itself, then
# Show: the reconnect presents the frozen cursor and replays
# replay_ok (nothing lost), which carries NO synthetic
# state_change (only fresh/truncated replays do — the
# latch-closed lag is the accepted residual). Wait for the
# reconnect itself, then
# drive an ORGANIC settle with a plain send — its idle edge fires
# the TRANSPORT-FREE backstop on the live stream and the heal
# renders the rewound (ONE) + sent (a second) transcript.
+90 -30
View File
@@ -600,6 +600,13 @@ def test_coordinator_history_stale_latch_contract():
6. destroy() cancels staleRetryTimer (terminal-only) while
closeStreamTransport does NOT (transport redials keep the heal
intent alive).
7. The render-time gate (r5-derived): refetchHistory re-checks
DOM-live-state (content refs / the --running batch marker / the optimistic-send
flavor of busy — never plain busy, the r5 critical), dispatch
currency (seq), and seedless stream-OPENness AFTER the await and
BEFORE the wipe — and the latch-clear sits below every skip
point, so a skipped render can never reopen the affordances over
a stale DOM.
"""
from pathlib import Path
@@ -704,21 +711,26 @@ def test_coordinator_history_stale_latch_contract():
"(mirrors interactive's !_replayQueue pin)."
)
assert "visHandler" in retry_fire, (
"the retry's fire guard must carry the teardown sentinel — it is "
"the SOLE protection for the coordCloseSession path, which nulls "
"visHandler but does not cancel the timer."
"the retry's fire guard must carry the teardown sentinel for the "
"coordCloseSession path, which nulls visHandler but does not "
"cancel the timer (evtSource is also null there post-suspend, "
"but the sentinel is the durable term)."
)
assert "!currentAssistantEl" in retry_fire and "!currentReasoningEl" in retry_fire, (
"the retry's ref guards are LOAD-BEARING (coord's refetchHistory "
"does not reset streaming refs) — must not be deleted to match "
"interactive's quiesce-protected shape."
"the retry's ref guards skip a pointless fetch whose payload the "
"render-time gate would discard (the chokepoint carries the "
"correctness; these are the efficiency layer — keep them)."
)
assert "visHandler &&\n evtSource\n ) {" in body, (
"the retry's fire guard must require a live stream (evtSource): "
"close-on-hide keeps this timer armed by design, and a seedless "
"heal on a dead stream renders past the frozen cursor (replay "
"double-render on the show edge). Exact-tail pin so a comment "
"mention cannot satisfy it; re-anchor if the guard reflows."
assert (
"visHandler &&\n evtSource &&\n"
" evtSource.readyState === EventSource.OPEN\n"
" ) {" in body
), (
"the retry's fire guard must require an OPEN stream — not handle "
"existence: CONNECTING keeps the handle with a frozen cursor and "
"a pending replay, and a seedless heal then double-renders when "
"the replay lands. Exact-tail pin so a comment mention cannot "
"satisfy it; re-anchor if the guard reflows."
)
assert "if (staleRetryTimer) clearTimeout(staleRetryTimer);" in body, (
"re-arming on a newer clear_ui must cancel the pending timer "
@@ -748,24 +760,6 @@ def test_coordinator_history_stale_latch_contract():
"the very window the yield guards protect."
)
# 7. Render-time gate: the post-await re-checks must sit between the
# failure guard and the wipe. The await is a real window — a turn
# can START mid-fetch (refs re-check; a wipe then would detach the
# live bubble and lose the turn into the dangling ref), and a
# seedless render must not paint past a frozen cursor when the
# stream died mid-fetch (hide/suspend) or the show-edge replay
# double-renders. Caller-side guards cannot see across the await;
# only this chokepoint can.
hist_guard = body.index("if (!hist) return;", fetch_start)
ref_gate = body.index("if (currentAssistantEl || currentReasoningEl) return;", fetch_start)
live_gate = body.index("if (!seedCursor && (busy || !evtSource)) return;", fetch_start)
wipe = body.index("messagesEl.replaceChildren();", fetch_start)
assert hist_guard < ref_gate < wipe and hist_guard < live_gate < wipe, (
"refetchHistory must re-check the live-turn refs AND seedless "
"stream-liveness AFTER the await and BEFORE the wipe — the "
"render-time correctness carrier for every caller."
)
# 6. Teardown: terminal cancel in destroy(); NOT in closeStreamTransport.
destroy_slice = body[body.index("function destroy()") :]
destroy_slice = destroy_slice[: destroy_slice.index("\n function ")]
@@ -780,6 +774,72 @@ def test_coordinator_history_stale_latch_contract():
"redials keep the pending heal intent (terminal-only cancel)."
)
# 7. Render-time gate (r5-derived, seedless-scoped after the
# coord-restart family find): the post-await re-checks sit between
# the failure guard and the wipe, so the latch-clear (below the
# wipe) sits below every skip point by construction. Universal
# terms: dispatch currency (seq) and the content refs (skipping
# always beats stranding a ref; seeded callers null theirs before
# fetching, so it never blocks them). SEEDLESS-only terms —
# keyed on the seedCursor ARG: the .conv-batch--running marker
# (on a live stream it means results are streaming into those
# rows; on the SEEDED resync it can be a dead turn's residue and
# the render IS the recovery — a universal term wedged
# coord-restart outright), the optimistic-send busySource flavor,
# and stream-OPENness (CONNECTING keeps the handle with a frozen
# cursor and a pending replay). Plain ``busy`` must not appear:
# it means a turn is EXECUTING, not that this DOM holds live
# state (the r5 critical).
hist_guard = body.index("if (!hist) return;", fetch_start)
wipe = body.index("messagesEl.replaceChildren();", fetch_start)
latch_clear = body.index("historyStale = false;", fetch_start)
assert hist_guard < wipe < latch_clear, (
"the wipe must sit between the failure guard and the latch-clear "
"— every gate skip above the wipe then leaves the latch set."
)
gate_code = "\n".join(
line for line in body[hist_guard:wipe].splitlines() if not line.lstrip().startswith("//")
)
for term, why in (
("if (seq !== refetchSeq) return;", "dispatch-currency (seq)"),
(
"if (currentAssistantEl || currentReasoningEl) return;",
"universal content-ref",
),
("!seedCursor &&", "seedless scoping"),
('busySource === "optimistic"', "optimistic-row"),
('messagesEl.querySelector(".conv-batch--running")', "executing-batch"),
("evtSource.readyState !== EventSource.OPEN", "stream-OPENness"),
):
assert term in gate_code, (
f"the render-time gate must carry the {why} term (in CODE, not a comment)."
)
seedless_at = gate_code.index("!seedCursor &&")
assert gate_code.index("if (seq !== refetchSeq) return;") < seedless_at, (
"seq currency must be checked before the seedless group."
)
assert gate_code.index("if (currentAssistantEl || currentReasoningEl) return;") < seedless_at, (
"the universal ref check must precede the seedless group."
)
for term in (
'busySource === "optimistic"',
".conv-batch--running",
"evtSource.readyState !== EventSource.OPEN",
):
assert seedless_at < gate_code.index(term), (
f"{term} must live INSIDE the !seedCursor group — the seeded "
"resync renders over a dead --running batch / an optimistic "
"row deliberately (blocking it wedges the coord-restart "
"recovery)."
)
assert not re.search(r"\bbusy\b(?!Source)", gate_code), (
"plain busy must not gate the render — busy means a turn is "
"EXECUTING, not that this DOM holds live state (the r5 critical: "
"_editAndResend flips busy before its POST and /rewind emits no "
"state_change, so a busy term skips the truncation render the "
"rewind exists to produce)."
)
def test_coordinator_js_early_paints_pending_tool_calls():
"""The coord chat frontend must render a committed tool call on
@@ -669,6 +669,14 @@ function createCoordinatorPane(root, wsId, opts) {
// ``if (!hist)`` is synchronous, so no timer or SSE handler can observe
// the counter mid-render.
let refetchesInFlight = 0;
// Monotonic refetchHistory dispatch stamp. Each call captures
// ``++refetchSeq`` before its await; the render-time gate discards any
// payload whose stamp is no longer current, so overlapping fetches
// (reachable via another actor's clear_ui racing a heal) resolve
// last-DISPATCH-wins deterministically instead of last-RESPONSE-wins —
// an older snapshot landing late can neither double-render nor clear
// the staleness latch over a newer truth.
let refetchSeq = 0;
// The cursor position a replay_truncated envelope was received AT — i.e.
// "a gap of lost events exists BELOW this cursor". Keep-oldest: set only
// when null (repeated envelopes for the same unrepaired gap must not
@@ -3532,31 +3540,41 @@ function createCoordinatorPane(root, wsId, opts) {
// with /history down that is a zero-backoff disconnect/refetch/
// reconnect storm against a recovering node (interactive.js's
// round-5 critical), self-sustaining until /history recovers.
// A plain REST refetch emits zero SSE events, so the trigger
// edge is only ever organic (turn-settle driven; the server
// pushes no state_change heartbeats) and the loop is
// structurally impossible. Stream death has its own owners
// (EventSource native auto-reconnect, the truncated resync in
// the branch above — whose heal legitimately IS a reconnect:
// its stream is dead, this arm's is alive) and the else-if
// keeps the truncated branch's reload from doubling up (its
// render clears this latch too).
// A plain REST refetch emits zero SSE events, so this heal
// never CAUSES its own trigger and the self-loop is
// structurally impossible. Precision on the trigger edges
// (r5): turn settles are the main one, but fresh/truncated
// SSE replays also carry a synthetic state_change (replay_ok
// does not) — so a latched pane pays one refetch per
// reconnect too. That edge is bounded by the reconnect
// machinery's own jitter/backoff, is user/transport-driven
// rather than heal-driven, and the restart-herd /history load
// is bounded server-side by #884's single-flight — ruled
// acceptable; do NOT bolt a client-side limiter onto this arm
// (and never a transport-touching one). Stream death has its
// own owners (EventSource native auto-reconnect, the
// truncated resync in the branch above — whose heal
// legitimately IS a reconnect: its stream is dead, this arm's
// is alive) and the else-if keeps the truncated branch's
// reload from doubling up (its render clears this latch too).
//
// The ref guards are a DELIBERATE divergence from the
// interactive template (which omits them on its backstop):
// interactive's replayHistory resets the streaming refs on
// every render, coord's refetchHistory does not — and this arm
// also serves state_change:error edges, where no stream_end ran
// finishAssistantStream and a live-bubble ref can survive.
// Firing then would replaceChildren the bubble out from under
// the dangling ref; skipping defers the heal to the next
// organic settle (that turn's stream_end nulls the refs).
// Correctness never rides this arm — the busy||historyStale
// gate carries it; heals are liveness only. Accepted liveness
// lag (ruled): if /history recovers while the pane sits idle
// untouched, rewind/edit stay closed until the next organic
// settle — strictly safer than the storm. Do not add ANY
// transport-touching timer to shorten it.
// also serves state_change:error edges, where no stream_end
// ran finishAssistantStream and a live-bubble ref can
// survive. Post-r5 the render-time gate in refetchHistory
// carries the wipe-safety correctness; these fire-time guards
// are the efficiency layer — they skip a fetch whose payload
// the gate would discard (a latched pane draining a K-deep
// send queue still pays one discarded fetch per settle edge
// it loses the race on; ruled acceptable — per-pane cost
// rides organic edges only and #884 coalesces the herd).
// Accepted liveness lag (ruled): if /history recovers while
// the pane sits idle untouched, rewind/edit stay closed until
// the next organic settle — strictly safer than the storm.
// Do not add ANY transport-touching timer to shorten it.
//
// Fire-and-forget, seedless: the stream is alive so lastEventId
// must not rewind; no composer state rides this heal (that is
@@ -3678,11 +3696,12 @@ function createCoordinatorPane(root, wsId, opts) {
// in flight (!refetchesInFlight — yield instead of stomping it
// with a same-snapshot double render), mid-turn (!busy — the
// idle-edge backstop owns the busy case, e.g. the edit-resend
// dispatched below), a live bubble (the ref guards are
// LOAD-BEARING: refetchHistory does not reset streaming refs,
// so a wipe would strand a dangling ref), or torn down
// (visHandler — destroy() also cancels this timer outright,
// but coordCloseSession only nulls visHandler).
// dispatched below), a live bubble (the ref guards skip a
// fetch whose payload refetchHistory's render-time gate would
// discard — the gate carries the wipe-safety correctness,
// these are the efficiency layer), or torn down (visHandler —
// destroy() also cancels this timer outright, but
// coordCloseSession only nulls visHandler).
//
// The ARM is visHandler-gated too: this .then can settle AFTER
// a teardown (destroy/close-session during the in-flight
@@ -3703,17 +3722,20 @@ function createCoordinatorPane(root, wsId, opts) {
!currentAssistantEl &&
!currentReasoningEl &&
visHandler &&
evtSource
evtSource &&
evtSource.readyState === EventSource.OPEN
) {
// evtSource: a seedless heal must not render past a
// frozen cursor (close-on-hide keeps this timer armed by
// design, so the fire can land with the transport down)
// — skip; the latch survives and the show-edge
// reconnect's synthetic idle hands the heal to the
// backstop. The backstop itself needs no such term: it
// runs inside SSE dispatch, so its stream is live by
// Stream must be OPEN, not merely present: close-on-hide
// keeps this timer armed by design (the fire can land
// with the transport down), and a CONNECTING source has
// a frozen cursor with a pending replay — a seedless
// fetch then would render past it (double-render when
// the replay lands). Skip instead; the latch survives
// and the next organic settle re-fires the backstop.
// The backstop itself needs no such term: it runs
// inside SSE dispatch, so its stream is live by
// construction. refetchHistory's render-time gate
// re-checks both invariants across the await window.
// re-checks every invariant across the await window.
// Fire-and-forget, seedless (live stream — lastEventId
// must not rewind); a render throw stays loud, as on the
// backstop.
@@ -5751,7 +5773,9 @@ function createCoordinatorPane(root, wsId, opts) {
// in-flight fetch (see refetchesInFlight's decl). Only the fetch
// needs bracketing: the render below runs synchronously after the
// await, so no timer or SSE handler can observe the counter
// mid-render.
// mid-render. The seq stamp makes overlapping dispatches resolve
// last-dispatch-wins at the render-time gate.
const seq = ++refetchSeq;
refetchesInFlight++;
try {
hist = await getJSON(
@@ -5775,41 +5799,77 @@ function createCoordinatorPane(root, wsId, opts) {
// refetch.) Success ordering is unchanged: the wipe always ran
// after the await, never as immediate feedback.
if (!hist) return;
// RENDER-TIME gate (#894 r4): the await above is a real window — pane
// state can change between a caller's fire-time checks and this
// render, and only THIS site can see across it (chokepoint, not
// per-caller guards). Two invariants must hold at the wipe itself:
// RENDER-TIME gate (#894 r4, re-derived r5): the await above is a
// real window — pane state can change between a caller's fire-time
// checks and this render, and only THIS site can see across it
// (chokepoint, not per-caller guards). The gate reads DOM-LIVE-STATE
// signals, never plain ``busy`` — that distinction is the r5 lesson:
// ``busy`` means "a turn is executing", not "this DOM holds live turn
// state", and the two diverge exactly where it hurt. _editAndResend
// flips busy BEFORE its POST and /rewind emits only clear_ui (no
// state_change), so a busy term skipped the truncation render the
// rewind exists to produce and appended the resent bubble onto the
// PRE-rewind transcript; /retry's regenerated turn likewise raced its
// own clear_ui refetch. A skipped render must only ever mean "the
// wipe would destroy live DOM" or "the paint would outrun the
// cursor":
//
// 1. No live turn mid-stream. A turn can START during the fetch (the
// server drains queued sends at exactly the idle edges the
// backstop rides; another operator on a shared coordinator can
// send any time; coordSend paints an optimistic user row). This
// render does not reset the streaming refs, so replaceChildren
// would detach the live bubble — every remaining token renders
// into the dangling ref (invisible), and the optimistic user row
// is destroyed with nothing to repaint it. Skip instead: if the
// staleness latch is set it stays set (clear is below), the gate
// stays closed, and the turn's own settle re-fires the backstop.
// 2. Seedless renders need an IDLE pane on a LIVE stream. busy: the
// ref check above only sees the CONTENT phase — a turn in its tool
// phase has null content refs but live tool rows, and the wipe +
// toolRows.clear() below would orphan them mid-stream identically.
// evtSource: a seedless render must not advance the DOM past a
// frozen lastEventId (hide/suspend can land mid-fetch) — the
// show-edge reconnect replays from the frozen cursor and every
// turn this render already painted would render twice. Seeded
// callers (init, loadHistoryThenReconnect) own their reconnect
// flow — they adopt hist.cursor below, and the truncated resync
// legitimately rebuilds MID-turn — so both requirements key on the
// seedCursor ARG, not caller identity. On skip the latch
// survives; the next organic settle (or the show-edge reconnect's
// synthetic idle) re-fires the backstop.
// - seq (universal): a newer dispatch supersedes this payload
// (last-DISPATCH-wins; see refetchSeq's decl).
// - content refs (universal): a live bubble ref means the stream is
// mid-CONTENT — wiping detaches the node every remaining token
// renders into. Universal because skipping always beats
// stranding a ref, and the seeded callers null their refs before
// fetching anyway (loadHistoryThenReconnect explicitly; init
// pre-connect), so this never blocks them.
// - SEEDLESS-only, because the seeded flows deliberately render
// over/instead-of these states (keying on the seedCursor ARG, not
// caller identity):
// . .conv-batch--running — an executing tool batch
// (_setBatchRunning / _unsetBatchRunningIfAllResults strip the
// class only when every row has its result). NOT activeBatch —
// that is the pending-APPROVAL tracker (set only for
// opts.pending batches; the r5 re-derivation initially made
// exactly that mistake). On a seedless caller the stream is
// live, so the class means results are still streaming into
// those rows — wiping orphans them. On the SEEDED resync the
// marker can be a DEAD turn's residue (node died mid-batch; no
// result will ever strip the class) and the render IS the
// recovery — the replay adopts hist.cursor and rebuilds any
// genuinely live turn. Blocking that render wedged the
// coord-restart recovery outright (r5 family-run find).
// . busySource === "optimistic" — coordSend's pre-POST flip, the
// one busy flavor that marks un-committed DOM (an optimistic
// row the snapshot may not carry; see setBusy's contract). The
// seeded resync renders through it: dead-stream recovery
// outranks a transient row, matching pre-#894 behavior. The
// clear_ui .then's edit-resend paints under plain "server" busy
// instead; its narrower wipe window (a second actor's clear_ui
// inside the resend's commit gap) is accepted — the resent
// message re-appears at its turn's settle heal, and pre-#894
// behavior there was an over-rewind.
// . stream-OPENness — CONNECTING is NOT live (native
// auto-reconnect keeps the handle with a frozen lastEventId and
// a pending replay; painting past the cursor double-renders
// when that replay lands; hide/suspend null the handle
// outright). The seeded flows own their reconnect and adopt
// hist.cursor below.
//
// The callers' fire-time ref/stream guards remain as the efficiency
// layer (skip the pointless fetch); THIS gate is the correctness
// carrier.
// On every skip the staleness latch survives (clear is below), the
// affordance gate stays closed, and the next organic settle re-fires
// the backstop. The callers' fire-time guards remain as the
// efficiency layer (skip the pointless fetch); THIS gate is the
// correctness carrier.
if (seq !== refetchSeq) return;
if (currentAssistantEl || currentReasoningEl) return;
if (!seedCursor && (busy || !evtSource)) return;
if (
!seedCursor &&
(busySource === "optimistic" ||
messagesEl.querySelector(".conv-batch--running") ||
!evtSource ||
evtSource.readyState !== EventSource.OPEN)
)
return;
messagesEl.replaceChildren();
// A full committed-history render repairs any recorded truncation gap —
// whether this render came from the truncated resync itself or from an