@@ -125,26 +125,40 @@ export async function notifyFolderResourceChanged(
125125 * streaming caller fires and forgets it. Bounded to {@link APPLY_EDIT_TIMEOUT_MS}, so it adds latency
126126 * only when the socket pod is unreachable.
127127 *
128- * `version` is the durable `contentUpdatedAt` (epoch ms) this markdown was written with, for a durable
129- * write. Omit it for a STREAMING intermediate merge (the copilot stream mid-flight): intermediate
130- * content advances the live doc for viewers but is not a durable checkpoint, so the relay leaves its
131- * synced version pinned to the last durable write — which is exactly the copilot tool's final
132- * `edit_content` write, carrying the real version, that reconciles the durable file.
128+ * `order` positions this merge on the file's monotonic version line so a stale write never regresses
129+ * the doc — the relay drops any merge not NEWER than the version the doc already incorporates:
130+ * - `version` — a DURABLE write's `contentUpdatedAt` (epoch ms). Both orders the merge AND is recorded
131+ * as the doc's synced version (the persist If-Match guard), so a later persist treats this write as
132+ * synced rather than an out-of-band conflict.
133+ * - `streamedAt` — the wall-clock time (epoch ms) a STREAMING snapshot was produced (the copilot stream
134+ * mid-flight). Orders the merge so a delayed snapshot older than a newer durable write — possibly from
135+ * another app process — is dropped, but is NEVER recorded: the synced version stays pinned to the last
136+ * durable write, which is exactly the copilot tool's final `edit_content` write that reconciles the file.
133137 *
134- * Merges for a file run on a single serialized chain: each is chained after the current tail and
135- * applies strictly after it, so ordering can never regress the doc — a DURABLE (versioned) write
136- * always applies after any in-flight streaming merge AND after every earlier durable write, never
137- * concurrently. The final durable write is therefore always the last merge applied and cannot be
138- * clobbered by a late straggler. The copilot streaming caller uses {@link isLiveDocMergeInFlight} to
139- * skip redundant snapshots while one is in flight, so a slow relay can't backlog stale snapshots.
138+ * Pass one or the other, never both. Passing neither applies the merge without ordering it (legacy).
139+ *
140+ * Ordering is enforced at two scales. Within this process, merges for a file run on a single serialized
141+ * chain — each chained after the current tail — so a durable write applies after any in-flight streaming
142+ * merge and after every earlier durable write, never concurrently. Across processes, the per-process
143+ * chain does not apply, so the relay orders merges by the monotonic version above (durable version /
144+ * streaming `streamedAt`) under a cluster-wide lock. The copilot streaming caller uses
145+ * {@link isLiveDocMergeInFlight} to skip redundant snapshots while one is in flight, so a slow relay
146+ * can't backlog stale snapshots.
140147 */
148+ export interface LiveFileDocMergeOrder {
149+ /** A durable write's `contentUpdatedAt` (epoch ms): orders the merge AND is recorded as the synced version. */
150+ version ?: number
151+ /** A streaming snapshot's production time (epoch ms): orders the merge only — never recorded as a checkpoint. */
152+ streamedAt ?: number
153+ }
154+
141155export async function mergeEditIntoLiveFileDoc (
142156 fileId : string ,
143157 markdown : string ,
144- version ?: number
158+ order : LiveFileDocMergeOrder = { }
145159) : Promise < void > {
146160 const tail = liveDocMergeChain . get ( fileId ) ?? Promise . resolve ( )
147- const run = tail . then ( ( ) => applyLiveFileDocMerge ( fileId , markdown , version ) )
161+ const run = tail . then ( ( ) => applyLiveFileDocMerge ( fileId , markdown , order ) )
148162 liveDocMergeChain . set ( fileId , run )
149163 try {
150164 await run
@@ -171,15 +185,21 @@ export function isLiveDocMergeInFlight(fileId: string): boolean {
171185async function applyLiveFileDocMerge (
172186 fileId : string ,
173187 markdown : string ,
174- version ?: number
188+ order : LiveFileDocMergeOrder
175189) : Promise < void > {
176190 try {
177191 const response = await fetch ( `${ getSocketServerUrl ( ) } /api/file-doc/apply-edit` , {
178192 method : 'POST' ,
179193 headers : { 'Content-Type' : 'application/json' , 'x-api-key' : env . INTERNAL_API_SECRET } ,
180- // A durable `version` (the durable `updatedAt` epoch ms) records the version the live doc now
181- // incorporates (the persist If-Match guard); omitted for a streaming intermediate merge.
182- body : JSON . stringify ( { fileId, markdown, version } ) ,
194+ // `version` (durable `contentUpdatedAt`) records the synced version the live doc now incorporates
195+ // (the persist If-Match guard); `streamedAt` orders a streaming snapshot without recording it.
196+ // JSON.stringify drops whichever is undefined, so the wire shape is unchanged for durable writes.
197+ body : JSON . stringify ( {
198+ fileId,
199+ markdown,
200+ version : order . version ,
201+ streamedAt : order . streamedAt ,
202+ } ) ,
183203 signal : AbortSignal . timeout ( APPLY_EDIT_TIMEOUT_MS ) ,
184204 } )
185205 if ( ! response . ok ) {
0 commit comments