Skip to content

feat(files): stream copilot edits into the collaborative doc smoothly - #6122

Merged
waleedlatif1 merged 15 commits into
realtime-roomsfrom
files-smooth-collab-streaming
Jul 31, 2026
Merged

feat(files): stream copilot edits into the collaborative doc smoothly#6122
waleedlatif1 merged 15 commits into
realtime-roomsfrom
files-smooth-collab-streaming

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • Copilot/agent file edits now stream smoothly and collaboratively into the file editor — matching how streaming looks on main, without breaking collaboration.
  • The open editor applies the growing content into its live Yjs binding as minimal updateYFragment diffs (the same primitive TipTap runs per keystroke) instead of a full-doc setContent. Each diff renders smoothly locally and broadcasts to every peer via CRDT — so a collaborator watching /files sees the smooth stream for free.
  • Streamed ops run under a dedicated transaction origin, so they stay out of the user's undo stack.
  • The apply is gated on collabReady (synced + seeded), so diffs never land on an unseeded doc; the read-only placeholder stays visible until the seed swaps in.
  • Deleted the throttled server-side streaming merge and the baseVersion version-ordering machinery it needed (relay file-doc.ts/http.ts, notify.ts, session contract). The durable final write still reconciles open editors and seeds late joiners.

Why

On main file views are non-collaborative and stream via local setContent; on this branch the editor is collaborative (Yjs), and that smooth path was disabled because setContent would replace the shared doc and wipe peers. Content arrived only via throttled full-snapshot server merges — visibly chunky. Applying the stream as incremental CRDT diffs is smooth and collaborative.

Type of Change

  • Bug fix / improvement

Testing

  • New apply-streamed-markdown.test.ts (headless collaborative editor): incremental apply + broadcast, undo isolation (verified it fails when the tracked origin is used), concurrent-peer no-clobber merge.
  • Rewrote file-preview-adapter.test.ts / notify.test.ts / relay file-doc*.test.ts for the surviving contract.
  • bunx tsc clean (apps/sim + apps/realtime); affected vitest suites, check:api-validation, lint, monorepo-boundary and realtime-prune checks all pass.
  • Independent two-pass audit (correctness + cleanliness) run and findings addressed.
  • Still owed: live 2-browser smoke on dev.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

- apply the agent stream client-side into the live Yjs binding as minimal
  updateYFragment diffs (like main's setContent, but incremental) so it renders
  smoothly AND broadcasts to every peer via CRDT — a collaborator on /files sees
  the stream for free
- gate the apply on collabReady so diffs never land on an unseeded doc; keep the
  read-only placeholder visible until the seed swaps in
- run streamed ops under a dedicated tx origin so they stay out of the user's
  undo stack
- delete the throttled server-side streaming merge and the baseVersion ordering
  machinery it needed (relay + notify + session contract); the durable final
  write still reconciles open editors and seeds late joiners
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 31, 2026 7:12pm

Request Review

@cursor

cursor Bot commented Jul 31, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Touches collaborative persistence, multi-replica Redis stream compaction, and real-time relay semantics; incorrect agent vs real-edit labeling could lose edits or persist preview content.

Overview
Copilot file edits now stream into open collaborative editors as minimal Yjs diffs (via a private shadow + updateYFragment), instead of throttled full-snapshot server merges. Each frame renders smoothly locally and broadcasts to peers; the final durable edit_content write still reconciles storage.

Realtime relay: New SYNC_NO_PERSIST message type and REDIS_AGENT_ORIGIN so agent preview frames fan out (including to sibling providers on one socket) but never trigger persist or mark the doc edited. Redis stream entries can carry an agent marker; compaction stamps agent-only snapshots accordingly and realEdited is latched synchronously to avoid mislabeling real edits. Presence ownership is per (socket, clientId) so chat preview + Files editor on one connection both relay awareness for agent-stream leader election.

Removed: Server-side mid-stream mergeEditIntoLiveFileDoc / baseVersion ordering, isLiveDocMergeInFlight, and preview-session baseVersion — preview adapter only emits growing file_preview_content events.

Client: agent-stream-leader election, apply-streamed-markdown session, collaborative streaming in rich-markdown-editor (hold update until settle), streamOperation plumbed through embedded file UIs. Append previews fail closed until base content loads.

Reviewed by Cursor Bugbot for commit ca81494. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR moves copilot file-preview streaming into the collaborative Yjs document while retaining a durable final reconciliation.

  • Applies streamed markdown through private shadow replicas and relays minimal Yjs updates.
  • Adds awareness-based single-writer election and non-persisting agent frame origins.
  • Updates relay ownership, Redis compaction metadata, protocol contracts, and related tests.

Confidence Score: 4/5

The PR should not merge until the remaining simultaneous-start path can no longer place duplicated copilot content into the shared collaborative document.

The reply states that the startup race is bounded, and the revised relay correctly prevents its output from being persisted, but simultaneous viewers can still elect themselves from stale local awareness and apply duplicate frames that are immediately visible to collaborators.

Files Needing Attention: apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.tsx; apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/collaboration/agent-stream-leader.ts

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.tsx Integrates collaborative stream ticks, leader transitions, settle handling, and shadow-session cleanup; the acknowledged simultaneous-start dual-writer window remains.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/collaboration/apply-streamed-markdown.ts Introduces a private Yjs shadow that captures agent-only deltas and relays them into the live collaborative document.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/collaboration/agent-stream-leader.ts Elects the lowest announced awareness client as stream writer, with eventual-consistency behavior during simultaneous startup.
apps/realtime/src/handlers/file-doc.ts Adds non-persisting agent sync handling and supports multiple awareness client IDs per shared socket.
apps/realtime/src/handlers/file-doc-store.ts Tags agent stream entries across Redis replication and compaction so transient frames do not mark documents durable.
apps/sim/lib/copilot/tools/server/files/file-preview.ts Makes append previews fail closed when existing file content is unavailable, preventing fragment-only snapshots.

Sequence Diagram

sequenceDiagram
  participant Agent as Copilot stream
  participant Leader as Elected editor
  participant Shadow as Private Yjs shadow
  participant Shared as Shared Yjs document
  participant Relay as Realtime relay
  participant Peer as Collaborator
  Agent->>Leader: Growing markdown snapshot
  Leader->>Shadow: Reconcile snapshot
  Shadow-->>Leader: Minimal agent delta
  Leader->>Shared: Apply agent-origin update
  Shared->>Relay: SYNC_NO_PERSIST
  Relay->>Peer: Broadcast Yjs update
  Agent->>Relay: Durable final write
  Relay->>Shared: Reconcile final content
Loading

Reviews (14): Last reviewed commit: "fix(files): own presence per client id, ..." | Re-trigger Greptile

…snapshots

Review round 1 (Greptile P1s):
- apply the stream against a private shadow replica (seeded from the live doc at
  stream start) and relay only the agent's own delta into the shared doc, so a
  concurrent peer edit to a region the agent snapshot didn't include is no longer
  reverted (previously the whole-body reconcile deleted it)
- gate append snapshots on "must extend the base": a base-less append fragment
  (emitted before the base loads) can no longer reconcile the seeded doc to a wipe;
  patch still legitimately replaces a mid-region
- gate the apply on collabReady so diffs never land on an unseeded doc; keep the
  placeholder visible until the seed swaps in
- plumb streamOperation through the preview surfaces to drive the append gate
- add a peer-edit-preservation test (fails under whole-body reconcile) and refresh
  the undo-isolation + broadcast tests for the session API
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Cursor round 1 (Low): endAgentStream ran inside runOffRender, whose microtask is
dropped when a rapid follow-up stream bumps the run token — leaking the shadow
Y.Doc. Split it out into an unguarded microtask queued after the (droppable) final
apply, so the shadow is always destroyed.
Cursor round 1 (High): client-applied stream frames broadcast over the sync
channel, so the relay stamped a socket origin and ran schedulePersist — durably
writing partial agent content mid-stream, attributed to the watching user (the old
server-merge applied with no origin and never did). Restore that behavior:

- new FILE_DOC_MESSAGE_TYPE.SYNC_NO_PERSIST wire tag; the provider tags
  AGENT_STREAM_ORIGIN updates with it (normal user edits stay SYNC)
- the relay applies it under an AgentSyncOrigin (carries the socket id for
  broadcast exclusion, but is not a plain string) so originSocketId() is null →
  no edited/schedulePersist/lastEditorUserId; excludeSocketId() still excludes the
  sender, and the update still publishes to the stream so peers converge
- the copilot's final edit_content write remains the authoritative durable persist
- tests: relay applies+fans-out but never persists a SYNC_NO_PERSIST frame
  (verified it fails if applied as a socket edit); provider tags agent edits
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/realtime/src/handlers/file-doc.ts Outdated
Cursor round 2:
- High (settle skips apply without session): the stream shadow is now opened on
  the first ready frame, BEFORE the extend gate — so an `update` rewrite (whose
  every frame is gated out until settle) and a stream that finishes before seed
  still get a session, and settle applies the final body via the reused-or-on-demand
  shadow instead of leaving the doc stale until the durable reconcile.
- Medium (peer edits stall the stream): the extend gate now reads a private
  `lastStreamedBodyRef` (the agent's own last frame), snapshotted at stream start,
  not `lastSyncedBodyRef` which `onUpdate` clobbers on peer edits — so a collaborator
  typing can't make the growing snapshot stop prefixing the shown body and freeze it.
- Medium (multi-replica over-persist): pre-existing, documented "safe over-persist"
  (a peer task tails the frame as REDIS_ORIGIN and marks edited) — refreshed the
  stale comment to describe the SYNC_NO_PERSIST source; copilot's edit_content write
  remains the authoritative durable persist.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Cursor/Greptile round 3 (High + Medium) — remove the fragile string-prefix
"extend gate", which was the root of both findings:

- Server: `buildFilePreviewText` now fails closed for an `append` whose base
  content hasn't loaded (returns undefined, like patch/update), so a base-less
  fragment never reaches the client. This eliminates the base-less wipe at
  settle (Greptile P1) at the source; an empty file (existingContent === '')
  still previews normally.
- Client: the collab streaming tick no longer string-prefixes the raw preview
  against the editor's canonical markdown (the '*' vs '-' / emphasis mismatch
  that froze every append frame — Cursor). The mid-stream hold is now purely
  operation-based: `update` waits for settle; append/patch/create apply each
  frame via the (peer-safe) shadow reconcile. lastStreamedBodyRef is now a plain
  dedup guard, not a prefix baseline.

Keeps the shadow, durable write, and SYNC_NO_PERSIST unchanged.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Cursor round 4 (High): with the stream applied client-side, two tabs/windows on
the same chat could each derive streamingContent (the reconnect/resume path
re-consumes preview events) and each independently insert the stream under a
different Yjs clientID, duplicating content until the durable reconcile.

Fix — single-writer election via the file-doc awareness (new agent-stream-leader):
- a client applying an agent stream announces `agentApplying` on its own awareness
- only the leader (min clientID among announcers) applies mid-stream AND at settle;
  a non-leader renders the leader's ops via Yjs and does not apply (a non-leader
  applying the final body would re-insert the whole doc as a duplicate)
- re-checked each frame, so it converges to one writer the moment awareness
  propagates; the sub-frame startup race is reconciled by the durable write
- single-client (the common case) is unaffected: it is the only announcer, so it
  always leads

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit a49c118. Configure here.

…lings)

Cursor round 7 (Medium): SYNC_NO_PERSIST frames applied under an origin carrying
the sender socket id, and excludeSocketId dropped that whole socket from the
relay fan-out. A second FileDocProvider on the same socket (chat preview + Files
editor) then missed all mid-stream ops and stayed stale until the durable
reconcile — a regression from the old no-origin server merge, which reached both.

Fix: the agent origin is now a plain AGENT_SYNC_ORIGIN symbol, and agent frames
broadcast to the WHOLE room (no socket excluded), matching the old behavior — so a
same-socket sibling provider stays live; the emitting provider no-ops on its own
echo (the ops are already applied locally). originSocketId still returns null for
the symbol, so it keeps skipping edited/schedulePersist. Removed excludeSocketId
and the socket-carrying origin object. Updated the relay test to assert the
whole-room broadcast (verified it fails if the sender is excluded).
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit f3f7b7a. Configure here.

A peer task tailing an agent-streamed preview frame previously applied it
as REDIS_ORIGIN, marking the seeded room edited and making a transient
startup-race duplicate eligible for that task's last-disconnect flush. Mark
agent frames with a stream field so peers apply them as REDIS_AGENT_ORIGIN,
excluded from the edited/persist gate. The copilot's durable edit_content
write stays the sole authority over file bytes.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

Fixed the multi-replica persistence gap in 888496f.

Agent-streamed frames are now tagged no-persist across replicas, not just on the originating task:

  • `file-doc-store.ts`: added `REDIS_AGENT_ORIGIN` + an `a` stream-entry marker. `publish(name, update, agent)` writes the marker; the tailer/catch-up (`applyEntry`) reads it and applies agent entries with `REDIS_AGENT_ORIGIN`.
  • `file-doc.ts`: agent frames (`AGENT_SYNC_ORIGIN`) publish with the marker, and `REDIS_AGENT_ORIGIN` is excluded from the re-publish path and is absent from the edited/persist gate.

So a peer task now tails an agent frame as `REDIS_AGENT_ORIGIN` (previously `REDIS_ORIGIN`) and never marks the seeded room `edited` — a transient startup-race duplicate between two stream leaders can no longer become eligible for a peer's last-disconnect flush. The copilot's durable `edit_content` write stays the sole authority over file bytes.

Covered by a falsification-verified store test (`file-doc-store.test.ts`): a normal publish tails as `REDIS_ORIGIN`, an agent publish tails as `REDIS_AGENT_ORIGIN`, and only the agent entry carries the marker.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/realtime/src/handlers/file-doc-store.ts
Two multi-writer edge cases surfaced in review:

- rich-markdown-editor: a client that led, lost leadership, then regained it
  reused its stale shadow (which never saw the interim leader's ops), re-emitting
  ops for content already present. Tear the shadow down when a client observes it
  is not the leader, so a regain rebuilds fresh from the current doc.

- file-doc-store: compaction always stamped its snapshot REDIS_SNAPSHOT_ORIGIN
  (marks peers edited). A long agent-only stream crossing the threshold could
  fold preview content into a persist-eligible snapshot. Track whether a room
  integrated any real edit and stamp an agent-only snapshot REDIS_AGENT_ORIGIN
  so it stays no-persist.

Both covered by falsification-verified tests.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 96fd287. Configure here.

Independent audit surfaced two real gaps:

- file-doc-store: realEdited was latched AFTER appendUpdate's awaits, but the
  edit already sits in room.doc synchronously. A concurrent agent-frame
  compaction could read realEdited=false, snapshot that real content, and stamp
  it a no-persist agent frame — a lost edit. Latch it synchronously (same tick
  as the doc mutation) before any await. Deterministic falsifiable test added.

- rich-markdown-editor: at settle every tab applied the final body, and a
  non-leader's local microtask runs before the leader's final propagates, so
  both insert the tail (Yjs keeps both) -> duplicated tail. Elect a single
  settle writer (reliable — awareness is long converged by settle), reading
  leadership before clearing the announcement. Corrects the overclaiming
  idempotency comment and the handoff pick-up comment.

Adds a y-tiptap internals upgrade-guardrail test.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

The shared workspace socket hosts one collaborative provider per mounted view,
so the chat file preview and the standalone Files editor for the same file each
bind their own Yjs client id over ONE socket. The relay owned a single client id
per socket, so the later JOIN overwrote the earlier and dropped its awareness —
which silently broke the single-writer agent-stream election (a peer stopped
seeing the streaming provider's announcement and could self-elect, duplicating
streamed text for the whole stream).

Track ownership per (socket, client id): a socket owns a set of client ids; the
awareness gate accepts a frame only if every id it carries is owned; cleanup
drops all of a socket's ids; the roster stays one-entry-per-session. Reclaim and
the same-user reconnect path evict just the reclaimed id, dropping the old socket
only if it empties. Falsification-verified test added.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit ca81494. Configure here.

@waleedlatif1
waleedlatif1 merged commit 1a995ff into realtime-rooms Jul 31, 2026
5 checks passed
@waleedlatif1
waleedlatif1 deleted the files-smooth-collab-streaming branch August 1, 2026 04:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant