@@ -179,22 +179,14 @@ function originSocketId(origin: unknown): string | null {
179179
180180/**
181181 * The transaction origin stamped on an agent-streamed frame (a {@link FILE_DOC_MESSAGE_TYPE.SYNC_NO_PERSIST}
182- * apply). It carries the emitting socket id for broadcast exclusion, but is deliberately NOT a plain
183- * string — so `originSocketId` returns `null` for it and the update never triggers `edited`/`schedulePersist`
184- * (the copilot's final `edit_content` write is the durable persist).
182+ * apply). A non-string sentinel, so `originSocketId` returns `null` for it and the update never triggers
183+ * `edited`/`schedulePersist` (the copilot's final `edit_content` write is the durable persist). Unlike a
184+ * client edit, an agent frame is broadcast to the WHOLE room (its originating socket is NOT excluded), so a
185+ * second {@link FileDocProvider} on the same socket — e.g. the chat preview alongside the Files editor —
186+ * also receives the mid-stream ops. The emitting provider no-ops on its own echo (the ops are already
187+ * applied locally), so broadcasting back to the sender is harmless.
185188 */
186- interface AgentSyncOrigin {
187- readonly agentSocketId : string
188- }
189-
190- function isAgentSyncOrigin ( origin : unknown ) : origin is AgentSyncOrigin {
191- return typeof origin === 'object' && origin !== null && 'agentSocketId' in origin
192- }
193-
194- /** The socket id to exclude when relaying an update — a client socket edit OR an agent-streamed frame. */
195- function excludeSocketId ( origin : unknown ) : string | null {
196- return originSocketId ( origin ) ?? ( isAgentSyncOrigin ( origin ) ? origin . agentSocketId : null )
197- }
189+ const AGENT_SYNC_ORIGIN = Symbol ( 'file-doc-agent-sync' )
198190
199191/**
200192 * Broadcast an AWARENESS frame to the room ACROSS tasks via the Socket.IO Redis adapter. Awareness
@@ -706,7 +698,15 @@ function getOrCreateRoom(io: Server, ref: RoomRef): FileDocRoom {
706698 // Fan out to THIS task's clients only (excluding the origin socket if local — a user edit OR an
707699 // agent-streamed frame). Cross-task delivery rides the shared stream — every task's tailer applies +
708700 // runs its own local fan-out.
709- broadcastLocal ( io , name , encoding . toUint8Array ( encoder ) , excludeSocketId ( origin ) )
701+ // A client edit excludes its own sender socket (echo suppression). An agent frame broadcasts to the
702+ // WHOLE room — no socket excluded — so a same-socket sibling provider (chat preview + Files editor)
703+ // stays live mid-stream; the emitting provider no-ops on its own echo.
704+ broadcastLocal (
705+ io ,
706+ name ,
707+ encoding . toUint8Array ( encoder ) ,
708+ origin === AGENT_SYNC_ORIGIN ? null : originSocketId ( origin )
709+ )
710710 // Share every locally-originated update to the stream so peers converge. Skip updates that already
711711 // came FROM the stream (REDIS_ORIGIN / REDIS_SNAPSHOT_ORIGIN) and SEED_ORIGIN — the seed is published
712712 // EXPLICITLY and AWAITED under the seed lock (so it lands before the lock releases), which a
@@ -720,7 +720,7 @@ function getOrCreateRoom(io: Server, ref: RoomRef): FileDocRoom {
720720 // fresh task catching up purely from it must not treat the doc as unedited. The seed transition
721721 // itself is never counted, so a seeded-but-unedited doc is never projected back over the file. NOTE:
722722 // an agent-streamed frame ({@link FILE_DOC_MESSAGE_TYPE.SYNC_NO_PERSIST}) is not counted on the
723- // ORIGINATING task (it applies under an AgentSyncOrigin — see the handler), but in the multi-replica
723+ // ORIGINATING task (it applies under { @link AGENT_SYNC_ORIGIN} — see the handler), but in the multi-replica
724724 // path it is published to the stream and PEER tasks apply it as REDIS_ORIGIN, indistinguishable from a
725725 // peer edit, so it marks `edited` there. That only ever causes an extra idempotent persist of content
726726 // the copilot's final `edit_content` write persists durably anyway (safe over-persist, never a lost
@@ -804,16 +804,15 @@ function handleMessage(socket: AuthenticatedSocket, data: unknown) {
804804 break
805805 }
806806 case FILE_DOC_MESSAGE_TYPE . SYNC_NO_PERSIST : {
807- // An agent-streamed frame: apply + fan out to peers (so a collaborator sees the stream live) but
807+ // An agent-streamed frame: apply + fan out to the room (so a collaborator sees the stream live) but
808808 // do NOT treat it as a durable user edit. Unlike SYNC, we do NOT set `lastEditorUserId`, and the
809- // apply uses an {@link AgentSyncOrigin } (not the bare socket id ) so `originSocketId` is `null` in
810- // `doc.on('update')` — skipping `edited`/`schedulePersist`. `excludeSocketId` still reads the
811- // carried socket id , so the sender is excluded from the relay fan-out . The copilot's final
809+ // apply uses {@link AGENT_SYNC_ORIGIN } (a non-string sentinel ) so `originSocketId` is `null` in
810+ // `doc.on('update')` — skipping `edited`/`schedulePersist`, and broadcasting to the WHOLE room
811+ // (including the sender socket , so a same-socket sibling provider stays live) . The copilot's final
812812 // `edit_content` write remains the authoritative durable persist.
813813 const encoder = encoding . createEncoder ( )
814814 encoding . writeVarUint ( encoder , FILE_DOC_MESSAGE_TYPE . SYNC )
815- const agentOrigin : AgentSyncOrigin = { agentSocketId : socket . id }
816- syncProtocol . readSyncMessage ( decoder , encoder , room . doc , agentOrigin )
815+ syncProtocol . readSyncMessage ( decoder , encoder , room . doc , AGENT_SYNC_ORIGIN )
817816 if ( encoding . length ( encoder ) > 1 ) {
818817 socket . emit ( FILE_DOC_EVENTS . MESSAGE , encoding . toUint8Array ( encoder ) )
819818 }
0 commit comments