Skip to content

Commit 3ef00a1

Browse files
committed
fix(files): open the agent-stream shadow lazily on lead (no stale handoff)
Greptile round 6 (P1): the leader race — (a) a mid-stream leadership handoff could apply from a stale pre-stream shadow, and (b) two tabs starting the same stream before awareness converges could both lead briefly. - (a) fixed: the shadow is now opened LAZILY in the tick, only when this client actually leads, seeded from the CURRENT doc — so a handoff successor diffs against the prior leader's ops (never a stale base) and a non-leader builds no shadow at all. Announce candidacy via a dedicated ref (decoupled from the shadow); settle still gates the final apply on didApplyStreamRef (leader-only). - (b) the pure startup race is inherent to eventually-consistent election. It is now the only residual: bounded to two tabs starting the SAME stream within the awareness-propagation window, transient (converges in a frame or two), and never persisted (SYNC_NO_PERSIST + the durable edit_content reconcile). Resumes are sequential, so the common multi-tab case elects cleanly. Documented inline; a server-granted lease would close it fully but at a round-trip cost on the common single-tab path, which isn't worth it.
1 parent 0197940 commit 3ef00a1

1 file changed

Lines changed: 31 additions & 22 deletions

File tree

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.tsx

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,8 @@ export function LoadedRichMarkdownEditor({
399399
* durable write instead. Reset on settle.
400400
*/
401401
const didApplyStreamRef = useRef(false)
402+
/** True once this client has announced candidacy in the agent-stream election for the current stream. */
403+
const agentAnnouncedRef = useRef(false)
402404
const router = useRouter()
403405
const routerRef = useRef(router)
404406
routerRef.current = router
@@ -886,16 +888,15 @@ export function LoadedRichMarkdownEditor({
886888
// re-runs and applies once it lands (the read-only placeholder shows the base content meanwhile —
887889
// see `showPlaceholder`).
888890
if (!collabReady) return
889-
// Open the stream's shadow on the FIRST ready frame so it captures the pre-stream base (immune to
890-
// later peer edits) — including for an `update`, whose frames are all held until settle, so settle
891-
// still has a shadow through which to apply the final rewrite. Announce candidacy in the
892-
// single-writer election so only one tab/window actually applies this stream (see the tick).
893-
if (agentStreamSessionRef.current === null) {
894-
agentStreamSessionRef.current = beginAgentStream(editor)
891+
// Announce candidacy in the single-writer election (see the tick) so only one tab/window applies
892+
// this stream. The shadow is opened lazily in the tick, only when THIS client actually leads — so a
893+
// non-leader builds none, and a client that takes leadership mid-stream (a handoff) seeds its shadow
894+
// from the CURRENT doc, already carrying the prior leader's ops, never a stale base.
895+
if (!agentAnnouncedRef.current) {
896+
agentAnnouncedRef.current = true
895897
didApplyStreamRef.current = false
896898
if (collaboration) announceAgentApplying(collaboration.awareness)
897899
}
898-
const session = agentStreamSessionRef.current
899900
const body = splitFrontmatter(content).body
900901
if (body === lastStreamedBodyRef.current) return
901902
pendingStreamBodyRef.current = body
@@ -914,10 +915,15 @@ export function LoadedRichMarkdownEditor({
914915
streamRafRef.current = null
915916
return
916917
}
917-
// Single-writer election: only the leader (min clientID among clients applying this stream)
918-
// writes it into the shared doc, so multiple tabs/windows watching the same live copilot stream
919-
// don't each insert it and duplicate content. A non-leader renders the leader's ops via Yjs;
920-
// re-checked each frame, so it converges to one writer the moment awareness propagates.
918+
// Single-writer election: only the leader (min clientID among clients announcing they apply this
919+
// stream) writes it into the shared doc, so multiple tabs/windows watching the same live copilot
920+
// stream don't each insert it and duplicate content. A non-leader renders the leader's ops via
921+
// Yjs; re-checked each frame, so it converges to one writer the moment awareness propagates.
922+
// Bounded residual (accepted): if two tabs start the SAME stream within the awareness-propagation
923+
// window they briefly both lead and duplicate a frame or two — a rare, transient, never-persisted
924+
// glitch (SYNC_NO_PERSIST keeps it out of storage; the durable edit_content write reconciles the
925+
// final doc). Resumes are sequential (the second tab sees the first's announcement), so the common
926+
// multi-tab case elects cleanly.
921927
if (
922928
collaboration &&
923929
!isAgentStreamLeader(collaboration.awareness, collaboration.doc.clientID)
@@ -934,8 +940,11 @@ export function LoadedRichMarkdownEditor({
934940
}
935941
const el = containerRef.current
936942
const pinnedToBottom = el ? el.scrollHeight - el.scrollTop - el.clientHeight < 80 : false
937-
// Defensive: a ready collab editor always has a ySync binding, so this applies; if one is
938-
// somehow absent, bail this frame without advancing rather than looping.
943+
// Open the shadow lazily HERE — only when THIS client actually leads — seeded from the CURRENT
944+
// doc, so a handoff successor diffs against the prior leader's ops (no stale base) and a
945+
// non-leader never builds one. Defensive: a ready collab editor always has a ySync binding.
946+
agentStreamSessionRef.current ??= beginAgentStream(editor)
947+
const session = agentStreamSessionRef.current
939948
if (!session || !applyAgentStreamFrame(editor, session, pending)) {
940949
streamRafRef.current = null
941950
return
@@ -953,19 +962,17 @@ export function LoadedRichMarkdownEditor({
953962
cancelAnimationFrame(streamRafRef.current)
954963
streamRafRef.current = null
955964
}
956-
// Settle: apply the FINAL body once so the Y.Doc exactly equals the streamed result — even when no
957-
// frame applied mid-stream (an `update` is held until settle, or the stream finished before the
958-
// seed). Reuse the stream's shadow when it exists (seeded from the pre-stream base, so peer edits
959-
// survive); otherwise open one on demand. The durable server write then lands as a noop diff.
965+
// Settle: only a client that actually applied mid-stream (the elected leader, `didApplyStreamRef`)
966+
// applies the FINAL body — its shadow is up to date, so this just catches a throttled last frame, so
967+
// the Y.Doc exactly equals the streamed result. A client that never applied (a non-leader, a held
968+
// `update`, or a pre-seed stream) has no shadow and skips — it converges via Yjs + the durable write.
969+
// This is a LOCAL decision (no settle-time re-election), so a straggler can't self-elect after the
970+
// leader clears its announcement.
960971
if (wasStreamingRef.current && collabReady) {
961972
wasStreamingRef.current = false
962-
// Only a client that actually applied mid-stream (the elected leader, `didApplyStreamRef`) applies
963-
// the final body — its shadow is up to date, so this just catches a throttled last frame. A client
964-
// that never applied has a base-seeded shadow; reconciling it to the final body would re-insert the
965-
// whole doc as a duplicate, so it skips and converges via Yjs + the durable write. This is a LOCAL
966-
// decision (no settle-time re-election), so a straggler can't self-elect after the leader clears.
967973
const didApply = didApplyStreamRef.current
968974
didApplyStreamRef.current = false
975+
agentAnnouncedRef.current = false
969976
if (collaboration) clearAgentApplying(collaboration.awareness)
970977
lastStreamedBodyRef.current = null
971978
const session = agentStreamSessionRef.current
@@ -1088,6 +1095,8 @@ export function LoadedRichMarkdownEditor({
10881095
agentStreamSessionRef.current = null
10891096
}
10901097
lastStreamedBodyRef.current = null
1098+
didApplyStreamRef.current = false
1099+
agentAnnouncedRef.current = false
10911100
},
10921101
[]
10931102
)

0 commit comments

Comments
 (0)