From 777e8a9c7af4d91105629df728feda8efeecf5e7 Mon Sep 17 00:00:00 2001
From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com>
Date: Tue, 28 Jul 2026 12:05:30 -0400
Subject: [PATCH] fix: trust the pipeline flag when a domain reload is deferred
(#1276)
EditorApplication.isCompiling conflates three states: actually compiling,
compilation queued, and finished-but-reload-deferred. A project holding
EditorApplication.LockReloadAssemblies sits in the third state for as long
as the lock is held, with no compilation running, and isCompiling stays
true the whole time.
Eight call sites gated on that raw flag, so they refused work indefinitely:
the stdio bridge would not start, unity_reflect and manage_scriptable_object
returned "Unity is compiling", refresh_unity reported the wrong resulting
state and never completed its wait, the stdio reload handler deferred its
resume, and TestJobManager both mis-attributed its init timeout and reported
a bogus "compiling" block reason.
Route all eight through EditorStateCache.GetActualIsCompiling(), which falls
back to the event-tracked CompilationPipeline flag, and drop the isPlaying
gate that previously limited the workaround to play mode.
Verified live: with LockReloadAssemblies held after RequestScriptCompilation,
EditorApplication.isCompiling is true while the pipeline flag is false, so
CompilationPipeline.compilationFinished does fire while the reload is held.
---
.../Editor/Services/EditorStateCache.cs | 29 ++++++++-----------
.../Services/StdioBridgeReloadHandler.cs | 10 ++-----
MCPForUnity/Editor/Services/TestJobManager.cs | 4 +--
.../Transport/Transports/StdioBridgeHost.cs | 22 +++-----------
.../Editor/Tools/ManageScriptableObject.cs | 3 +-
MCPForUnity/Editor/Tools/RefreshUnity.cs | 4 +--
MCPForUnity/Editor/Tools/UnityReflect.cs | 3 +-
7 files changed, 26 insertions(+), 49 deletions(-)
diff --git a/MCPForUnity/Editor/Services/EditorStateCache.cs b/MCPForUnity/Editor/Services/EditorStateCache.cs
index 54625c2e4..d02b26528 100644
--- a/MCPForUnity/Editor/Services/EditorStateCache.cs
+++ b/MCPForUnity/Editor/Services/EditorStateCache.cs
@@ -260,8 +260,8 @@ static EditorStateCache()
EditorApplication.playModeStateChanged += _ => ForceUpdate("playmode");
// Tracks whether an assembly compilation is actually running, for
- // GetActualIsCompiling's Play-mode check. Statics reset on domain reload
- // and this [InitializeOnLoad] ctor re-subscribes, so the flag is per-domain.
+ // GetActualIsCompiling. Statics reset on domain reload and this
+ // [InitializeOnLoad] ctor re-subscribes, so the flag is per-domain.
UnityEditor.Compilation.CompilationPipeline.compilationStarted += _ => _pipelineCompilationRunning = true;
UnityEditor.Compilation.CompilationPipeline.compilationFinished += _ => _pipelineCompilationRunning = false;
@@ -288,7 +288,7 @@ private static void OnUpdate()
{
// Throttle to reduce overhead while keeping the snapshot fresh enough for polling clients.
double now = EditorApplication.timeSinceStartup;
- // Use GetActualIsCompiling() to avoid Play mode false positives (issue #582)
+ // Use GetActualIsCompiling() to avoid isCompiling false positives (issues #549, #1276)
bool isCompiling = GetActualIsCompiling();
// Check for compilation edge transitions (always update on these)
@@ -543,10 +543,12 @@ public static JObject GetSnapshot()
private static bool _pipelineCompilationRunning;
///
- /// Returns the actual compilation state, working around a known Unity quirk where
- /// EditorApplication.isCompiling can return false positives in Play mode (e.g. a
- /// recompile deferred by Recompile-After-Finished-Playing keeps it true for the
- /// whole play session). See: https://github.com/CoplayDev/unity-mcp/issues/549
+ /// Returns the actual compilation state, working around known Unity quirks where
+ /// EditorApplication.isCompiling reports false positives while no compilation is
+ /// running: a recompile deferred by Recompile-After-Finished-Playing keeps it true
+ /// for the whole play session (issue #549), and a project holding
+ /// EditorApplication.LockReloadAssemblies keeps it true until the lock is released
+ /// (issue #1276). In both cases the event-tracked pipeline flag is authoritative.
///
internal static bool GetActualIsCompiling()
{
@@ -556,16 +558,9 @@ internal static bool GetActualIsCompiling()
return false;
}
- // In Play mode, trust the event-tracked pipeline state instead: a deferred
- // recompile keeps EditorApplication.isCompiling true without any compilation
- // actually running.
- if (EditorApplication.isPlaying)
- {
- return _pipelineCompilationRunning;
- }
-
- // Outside Play mode the raw signal is reliable.
- return true;
+ // Otherwise trust the event-tracked pipeline state: isCompiling stays true for as
+ // long as an assembly reload is deferred, with no compilation actually running.
+ return _pipelineCompilationRunning;
}
}
}
diff --git a/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs b/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs
index cfa4d7286..4d9bb1ab1 100644
--- a/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs
+++ b/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs
@@ -114,14 +114,8 @@ private static void OnAfterAssemblyReload()
}
// If the editor is not compiling, attempt an immediate restart without relying on editor focus.
- bool isCompiling = EditorApplication.isCompiling;
- try
- {
- var pipeline = Type.GetType("UnityEditor.Compilation.CompilationPipeline, UnityEditor");
- var prop = pipeline?.GetProperty("isCompiling", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
- if (prop != null) isCompiling |= (bool)prop.GetValue(null);
- }
- catch { }
+ // Routed through EditorStateCache so a deferred reload (issue #1276) does not block resume.
+ bool isCompiling = EditorStateCache.GetActualIsCompiling();
if (!isCompiling)
{
diff --git a/MCPForUnity/Editor/Services/TestJobManager.cs b/MCPForUnity/Editor/Services/TestJobManager.cs
index d162476e8..bdf626036 100644
--- a/MCPForUnity/Editor/Services/TestJobManager.cs
+++ b/MCPForUnity/Editor/Services/TestJobManager.cs
@@ -502,7 +502,7 @@ internal static TestJob GetJob(string jobId)
{
long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
long initTimeout = job.InitTimeoutMs > 0 ? job.InitTimeoutMs : DefaultInitializationTimeoutMs;
- if (!EditorApplication.isCompiling && !EditorApplication.isUpdating && now - job.StartedUnixMs > initTimeout)
+ if (!EditorStateCache.GetActualIsCompiling() && !EditorApplication.isUpdating && now - job.StartedUnixMs > initTimeout)
{
McpLog.Warn($"[TestJobManager] Job {jobId} failed to initialize within {initTimeout}ms, auto-failing");
job.Status = TestJobStatus.Failed;
@@ -589,7 +589,7 @@ private static string GetBlockedReason(TestJob job)
return "editor_unfocused";
}
- if (EditorApplication.isCompiling)
+ if (EditorStateCache.GetActualIsCompiling())
{
return "compiling";
}
diff --git a/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs b/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs
index c4fb2438d..a6a0559d0 100644
--- a/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs
+++ b/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs
@@ -237,24 +237,10 @@ private static void EnsureStartedOnEditorIdle()
}
}
- private static bool IsCompiling()
- {
- if (EditorApplication.isCompiling)
- {
- return true;
- }
- try
- {
- Type pipeline = Type.GetType("UnityEditor.Compilation.CompilationPipeline, UnityEditor");
- var prop = pipeline?.GetProperty("isCompiling", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
- if (prop != null)
- {
- return (bool)prop.GetValue(null);
- }
- }
- catch { }
- return false;
- }
+ // Routed through EditorStateCache so a deferred domain reload (issue #1276) does not
+ // pin the bridge off: raw EditorApplication.isCompiling stays true for as long as the
+ // reload is held, and this gates bridge startup.
+ private static bool IsCompiling() => EditorStateCache.GetActualIsCompiling();
public static void Start()
{
diff --git a/MCPForUnity/Editor/Tools/ManageScriptableObject.cs b/MCPForUnity/Editor/Tools/ManageScriptableObject.cs
index c508e9fc2..2ddd021c3 100644
--- a/MCPForUnity/Editor/Tools/ManageScriptableObject.cs
+++ b/MCPForUnity/Editor/Tools/ManageScriptableObject.cs
@@ -4,6 +4,7 @@
using System.Linq;
using System.Text.RegularExpressions;
using MCPForUnity.Editor.Helpers;
+using MCPForUnity.Editor.Services;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
@@ -44,7 +45,7 @@ public static object HandleCommand(JObject @params)
return new ErrorResponse(CodeInvalidParams);
}
- if (EditorApplication.isCompiling || EditorApplication.isUpdating)
+ if (EditorStateCache.GetActualIsCompiling() || EditorApplication.isUpdating)
{
// Unity is transient; treat as retryable on the client side.
return new ErrorResponse(CodeCompilingOrReloading, new { hint = "retry" });
diff --git a/MCPForUnity/Editor/Tools/RefreshUnity.cs b/MCPForUnity/Editor/Tools/RefreshUnity.cs
index 537472ac0..a7736aa7e 100644
--- a/MCPForUnity/Editor/Tools/RefreshUnity.cs
+++ b/MCPForUnity/Editor/Tools/RefreshUnity.cs
@@ -109,7 +109,7 @@ await WaitForUnityReadyAsync(
}
}
- string resultingState = EditorApplication.isCompiling
+ string resultingState = EditorStateCache.GetActualIsCompiling()
? "compiling"
: (EditorApplication.isUpdating ? "asset_import" : "idle");
@@ -146,7 +146,7 @@ void Tick()
return;
}
- if (!EditorApplication.isCompiling
+ if (!EditorStateCache.GetActualIsCompiling()
&& !EditorApplication.isUpdating
&& !TestRunStatus.IsRunning
&& !EditorApplication.isPlayingOrWillChangePlaymode)
diff --git a/MCPForUnity/Editor/Tools/UnityReflect.cs b/MCPForUnity/Editor/Tools/UnityReflect.cs
index 06aee0d7f..236b74342 100644
--- a/MCPForUnity/Editor/Tools/UnityReflect.cs
+++ b/MCPForUnity/Editor/Tools/UnityReflect.cs
@@ -6,6 +6,7 @@
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using MCPForUnity.Editor.Helpers;
+using MCPForUnity.Editor.Services;
using MCPForUnity.Runtime.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor;
@@ -99,7 +100,7 @@ private static Dictionary GetAssemblyTypeCache()
public static object HandleCommand(JObject @params)
{
- if (EditorApplication.isCompiling)
+ if (EditorStateCache.GetActualIsCompiling())
return new ErrorResponse("Cannot reflect while Unity is compiling. Wait for domain reload to complete.");
if (@params == null)