fix(daemon): follow-active-buffer snapshots are semantic-sessions only

Round-2 regression (TUI could not attach): the follow path fired for
every crdt_replica session, and its first-tick send delivered a
GUARANTEED duplicate BufferSnapshot right after the attach sweep. The
grid TUI's BufferMirror is init-once -- the duplicate errored ('buffer
already has a CRDT snapshot applied') on every attach, and every TUI
buffer switch would have produced another. Display-follows-snapshot is
a grid-less-frontend concept: the GPU rebuilds its replica wholesale
on every snapshot and is the only consumer that needs the follow. The
send is now gated on negotiated_capabilities.semantic_render -- TUI
sessions get zero follow-sends, byte-identical wire behavior to main.

Verified: PTY-doubled real-binary attach tests pass with the gate;
the one failing PTY test (m10_11_doubled_pty_fixture_propagates_
keystrokes_from_both_sides, 'AXYB' interleaving) fails identically on
main 3/3 -- pre-existing, tracked separately, not this branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-07-07 21:11:32 -04:00
parent 5c5caa9482
commit 157358c57e
1 changed files with 22 additions and 8 deletions

View File

@ -992,15 +992,29 @@ fn dispatcher_loop(
// typing-into-a-buffer-you-can't-see hazard. Ship the
// now-active buffer's snapshot to THIS frontend only
// (its own view changed; nobody else's did).
let active_now = {
let core = editor.core.borrow();
core.active_window_for(*fid).map(|w| w.buffer_id)
};
if let Some(active_now) = active_now
&& last_active_buffer_sent.get(fid) != Some(&active_now)
//
// SEMANTIC sessions only: display-follows-snapshot is
// a grid-less-frontend concept, and the GPU rebuilds
// its replica wholesale on every snapshot. The grid
// TUI renders via CellDelta and its `BufferMirror` is
// init-once — a follow send there is a guaranteed
// duplicate that errors ("already has a CRDT snapshot
// applied") on every attach and every buffer switch
// (the PR #94 round-2 startup regression).
if session_registry
.session_state(*fid)
.is_some_and(|s| s.negotiated_capabilities.semantic_render)
{
send_buffer_snapshot_to_frontend(editor, active_now, *fid, &mut streams);
last_active_buffer_sent.insert(*fid, active_now);
let active_now = {
let core = editor.core.borrow();
core.active_window_for(*fid).map(|w| w.buffer_id)
};
if let Some(active_now) = active_now
&& last_active_buffer_sent.get(fid) != Some(&active_now)
{
send_buffer_snapshot_to_frontend(editor, active_now, *fid, &mut streams);
last_active_buffer_sent.insert(*fid, active_now);
}
}
}
#[cfg(not(feature = "crdt"))]