@@ -892,12 +892,24 @@ export default class SyncProcess {
892892 } , ACTION_CONCURRENCY )
893893
894894 const findChainCacheForCreations = { }
895+ // Each target-side concurrent creation may be merged with at most ONE source creation. Without
896+ // this, two source creations that both canMergeWith the same target creation (e.g. duplicate
897+ // titles/urls produced by fuzzing or by a previous reset) each call addMapping against the same
898+ // target id; the second add evicts the first ("X will become unmapped"), orphaning a real item
899+ // which is then silently dropped. Claim a target creation synchronously the moment it is matched
900+ // (before any await) so a concurrent task cannot claim it too.
901+ const claimedTargetCreations = new Set ( )
895902 await Parallel . each ( sourceScanResult . CREATE . getActions ( ) , async ( action ) => {
896903 const concurrentCreation = targetCreations . find ( a => (
904+ ! claimedTargetCreations . has ( a ) &&
897905 action . payload . parentId === Mappings . mapParentId ( mappingsSnapshot , a . payload , action . payload . location ) &&
898- action . payload . canMergeWith ( a . payload )
906+ action . payload . canMergeWith ( a . payload ) &&
907+ // Don't bind to a target creation that is already mapped to a *different* item — that bind
908+ // would evict the existing mapping and orphan its counterpart.
909+ ! Mappings . wouldEvictUnrelatedMapping ( mappingsSnapshot , action . payload , a . payload )
899910 ) )
900911 if ( concurrentCreation ) {
912+ claimedTargetCreations . add ( concurrentCreation )
901913 // created on both the target and sourcely, try to reconcile
902914 const subScanner = new Scanner (
903915 this . mappings ,
0 commit comments