Skip to content

External .cs changes remain pending after the focus-return Auto Refresh hold is released #2575

Description

@nyoro-wrl

Summary

When the Unity Editor is unfocused, Unity CLI Loop holds Auto Refresh with
AssetDatabase.DisallowAutoRefresh(). In the reproduction below, an external git pull
changed existing C# files during that interval. Returning focus to Unity released the hold,
but the pending script changes were not imported. No compilation or Domain Reload occurred
until an explicit refresh through uloop compile was run.

This reproduced with Unity's Auto Refresh and Domain Reload enabled, no active uloop
hot-reload patches, and no other hot-reload package installed.

I understand that Unity CLI Loop is primarily designed around an LLM-driven Unity workflow.
In practice, however, the same Editor session is often shared by an agent and a developer
working manually. It would be helpful if external IDE edits, git pull, git checkout, or
similar human-driven operations could still flow through Unity's normal refresh behavior on
focus return, without the developer needing to know that a later uloop compile is required.

Environment

  • macOS 26.5.1 (25F80)
  • Unity 6000.3.23f1
  • Installed Unity CLI Loop package: 3.1.0, Git dependency lock
    d6fa8bf679cc31563c7b58e7da95a15f3dd07d14
  • Edit Mode (EditorApplication.isPlaying == false)
  • kAutoRefreshMode=1
  • AssetDatabase.IsDirectoryMonitoringEnabled() == false (Directory Monitoring is not
    available on macOS)
  • uloop hot-reload --status: ActivePatchTotal: 0

I also checked current main at d0340b3b577b86f71ccb05139ddaf062d3998f9d
(package version 3.2.1). The relevant hold/release implementation is unchanged there, and
still does not request an AssetDatabase refresh after releasing the hold.

Reproduction

  1. Open a Unity project containing an existing C# type, focus the Editor once, then move
    focus to Finder or a terminal. The loaded Scenes and Prefab Stage were clean in this
    reproduction.

  2. Confirm the focus-return tracker has armed its hold:

    uloop execute-dynamic-code --code 'using UnityEditor; return $"focused={EditorApplication.isFocused};held={SessionState.GetBool("io.github.hatayama.UnityCliLoop.ExternalSceneChangeTracker.AutoRefreshHeld", false)}";'

    Observed: focused=False;held=True.

  3. Change the existing C# file outside Unity. In the observed case, git pull changed two
    existing scripts and added a private static field to each type.

  4. Bring Unity to the foreground once. I used uloop focus-window, which reported that it
    focused the real Editor process, and then waited for the normal Auto Refresh/compilation.

  5. Inspect EditorApplication.isCompiling, the loaded type through reflection, the
    Assembly-CSharp.dll timestamp, and Editor.log.

Actual result

Eight seconds after focus return:

focused=True
held=False
compiling=False
updating=False
new fields present in loaded types=False
  • The source files had a modification time of 22:56:16, while
    Library/ScriptAssemblies/Assembly-CSharp.dll remained at 20:04:12.
  • Editor.log contained no new script import, compilation, or Domain Reload after focus
    return.
  • Therefore, the hold was released successfully, but the pending C# changes remained
    unimported and Unity kept running the previous assembly.

Running plain uloop compile immediately afterward produced zero errors and warnings,
imported both scripts, updated Assembly-CSharp.dll, logged domain reloads=1, and made both
new fields visible in the loaded types. This confirms the files were valid and that an
explicit refresh resolves the stale state.

I reproduced the same failure independently with a newly created C# probe under Assets/.
Forty-five seconds after focus return, AssetDatabase.AssetPathToGUID(path) was still empty,
no .meta file had been created, and Editor.log showed no import, compilation, or Domain
Reload. This second reproduction did not depend on Git or on the contents of the pulled
commit.

Expected result

After the focus-return preflight safely handles open Scene/Prefab changes and releases its
Auto Refresh hold, Unity should resume the normal Auto Refresh behavior for changes that
accumulated while the hold was active. Pending C# changes should be imported, compiled, and
cause the normal Domain Reload without requiring an agent-specific uloop compile command.

This matters in mixed human/agent workflows where source changes arrive through Git or an
external IDE while Unity is running in the background. Although uloop's primary use case is
LLM-driven operation, it would be helpful if a developer working manually in the same project
could continue to receive Unity's normal Auto Refresh behavior simply by returning to the
Editor.

Suspected cause

ExternalSceneChangeTracker supplies AssetDatabase.DisallowAutoRefresh and
AssetDatabase.AllowAutoRefresh to ExternalAssetFocusReturnService. On focus gain,
HandleFocusChanged(true) performs the preflight and only then calls
ReleaseAutoRefreshIfHeld().

Unity documents focus regain as an Auto Refresh trigger, while it documents
AllowAutoRefresh() as decrementing the internal hold counter; it does not document that the
call schedules AssetDatabase.Refresh().

This timing suggests, but does not yet prove, that Unity processes the focus-triggered refresh
while the hold is still active and that releasing the hold later in the callback does not
queue another refresh. The hold/release path was introduced in #1757 and shipped through
#1760. I have not run an A/B test without that tracker, and #2480 observed a changed Scene
being imported on focus return with Unity 6000.3.15f1, so this may be script-specific or
Unity-version-dependent.

The current unit tests verify the call order preflight -> allow, but do not verify the
end-to-end result of an external .cs change being imported and compiled after real focus
return.

Documentation and related issues

Possible fix and regression coverage

Possible direction, pending confirmation: after the Scene/Prefab preflight and successful
hold release, ensure that changes accumulated while the hold was active are handed back to
Unity's refresh pipeline. This might require scheduling one refresh, but the exact mechanism
should preserve the existing native-dialog/crash protection and avoid unnecessary refreshes.

If reliably restoring the automatic focus-return behavior is difficult because of Unity's
refresh or callback constraints, a useful fallback would be a clearly visible button in the
Unity CLI Loop Editor UI that lets a developer manually refresh assets and compile pending
scripts. Automatic handling would still be preferable, but an Editor button would support
mixed human/agent workflows without requiring the developer to open a terminal or know the
uloop compile command.

Please add regression coverage for the hold/release-to-refresh handoff:

  1. Editor unfocused and Auto Refresh held.
  2. Existing .cs file changed externally.
  3. Editor receives focus and the hold is released.
  4. The changed script is imported, compilation completes, and a Domain Reload occurs without
    calling uloop compile.

If a real focus/import/Domain Reload integration test is unsafe in Edit Mode, cover the
refresh-scheduling decision with a pure unit test and retain a documented manual or external
E2E gate for the complete sequence.

Activity

  1. hatayama commented on Sep 4, 2026

    @hatayama
    Owner

    Thank you for the detailed report and reproduction steps.

    I reproduced it on 2022.3.62f3 and 6000.3.15f1, and also ran the A/B you mentioned (hold on/off, focus-return preflight on/off) with an open Scene changed externally:

    Auto Refresh hold focus-return preflight External .cs External Scene change
    on on not imported (your report) no dialog
    off on imported and compiled no dialog
    off off imported and compiled native "modified externally" dialog

    So the hold never contributed to avoiding the native dialog. Editor.log shows the same order on both versions: Unity's focus refresh (import) runs first, then the C# focusChanged callback (our preflight), and the Scene-changed-on-disk check that raises the dialog runs on a later tick. The preflight alone gets there first.

    The fix will remove the hold entirely (no DisallowAutoRefresh on focus loss) and keep only the preflight, rather than adding an extra AssetDatabase.Refresh() after release, so Unity's normal Auto Refresh behavior and the user's Auto Refresh preference stay untouched. A regression harness for the focus-return case will be added alongside it. It will ship in the next release.

  2. hatayama commented on Sep 4, 2026

    @hatayama
    Owner

    The fix has shipped in v3.3.0 (released 2026-09-04).

    Please update the CLI and the package to 3.3.0 and give it a try. If the pending-changes symptom still shows up on your side, let me know here and I will reopen the investigation.

  3. nyoro-wrl commented on Sep 5, 2026

    @nyoro-wrl
    Author

    I was able to resolve it with version 3.3.0. Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions