Skip to content

Commit 0197940

Browse files
committed
fix(files): gate the settle apply locally, not on a settle-time re-election
Cursor round 5 (High): the settle recomputed leadership from live awareness and the leader cleared its announcement immediately, so a straggler peer that settled afterward became the sole announcer, self-elected, and applied finalBody through its base-seeded shadow — re-inserting the whole doc as a duplicate. Fix: gate the settle apply on a LOCAL didApplyStreamRef (set only when this client actually applied a mid-stream frame — i.e. it was the mid-stream leader whose shadow is up to date), not on a settle-time re-election. A client that never applied (non-leader, a held `update`, or a pre-seed stream) skips the final apply and converges via Yjs + the durable write. The mid-stream leader election (isAgentStreamLeader) is unchanged, so exactly one client's didApplyStreamRef is ever true.
1 parent 59a10a7 commit 0197940

1 file changed

Lines changed: 23 additions & 11 deletions

File tree

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

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,15 @@ export function LoadedRichMarkdownEditor({
390390
streamOperationRef.current = streamOperation
391391
/** The live agent-stream shadow replica, held for the current stream and freed on settle/unmount. */
392392
const agentStreamSessionRef = useRef<AgentStreamSession | null>(null)
393+
/**
394+
* True once THIS client has applied at least one mid-stream frame for the current stream — i.e. it was
395+
* the elected leader whose shadow is up to date. Gates the settle apply LOCALLY (not on a settle-time
396+
* re-election, which is racy: a straggler that settles after the leader clears its announcement would
397+
* self-elect and re-insert the whole doc via its base-seeded shadow). A client that never applied
398+
* (non-leader, a held `update`, or a pre-seed stream) converges to the final state via Yjs + the
399+
* durable write instead. Reset on settle.
400+
*/
401+
const didApplyStreamRef = useRef(false)
393402
const router = useRouter()
394403
const routerRef = useRef(router)
395404
routerRef.current = router
@@ -883,6 +892,7 @@ export function LoadedRichMarkdownEditor({
883892
// single-writer election so only one tab/window actually applies this stream (see the tick).
884893
if (agentStreamSessionRef.current === null) {
885894
agentStreamSessionRef.current = beginAgentStream(editor)
895+
didApplyStreamRef.current = false
886896
if (collaboration) announceAgentApplying(collaboration.awareness)
887897
}
888898
const session = agentStreamSessionRef.current
@@ -930,6 +940,7 @@ export function LoadedRichMarkdownEditor({
930940
streamRafRef.current = null
931941
return
932942
}
943+
didApplyStreamRef.current = true
933944
streamRafRef.current = null
934945
lastStreamedBodyRef.current = pending
935946
lastStreamParseAtRef.current = performance.now()
@@ -948,21 +959,22 @@ export function LoadedRichMarkdownEditor({
948959
// survive); otherwise open one on demand. The durable server write then lands as a noop diff.
949960
if (wasStreamingRef.current && collabReady) {
950961
wasStreamingRef.current = false
951-
// Only the elected leader applies the final body — a non-leader never applied mid-stream, so
952-
// reconciling its base-seeded shadow to the final body would re-insert the whole doc as a
953-
// duplicate; it converges to the final state via Yjs + the durable server write instead. Compute
954-
// leadership BEFORE clearing our announcement, then stop announcing.
955-
const wasLeader =
956-
!collaboration || isAgentStreamLeader(collaboration.awareness, collaboration.doc.clientID)
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.
967+
const didApply = didApplyStreamRef.current
968+
didApplyStreamRef.current = false
957969
if (collaboration) clearAgentApplying(collaboration.awareness)
958970
lastStreamedBodyRef.current = null
959-
const finalBody = splitFrontmatter(content).body
960-
const session = wasLeader
961-
? (agentStreamSessionRef.current ?? beginAgentStream(editor))
962-
: agentStreamSessionRef.current
971+
const session = agentStreamSessionRef.current
963972
agentStreamSessionRef.current = null
964973
if (session) {
965-
if (wasLeader) runOffRender(() => applyAgentStreamFrame(editor, session, finalBody))
974+
if (didApply) {
975+
const finalBody = splitFrontmatter(content).body
976+
runOffRender(() => applyAgentStreamFrame(editor, session, finalBody))
977+
}
966978
// Free the shadow with an UNGUARDED microtask (not `runOffRender`): a rapid follow-up stream
967979
// can supersede the run token and drop the apply above, but the shadow must always be
968980
// destroyed. Queued after the apply, so it frees the shadow only once that has had its chance.

0 commit comments

Comments
 (0)