Skip to content

fix: uloop says the Unity Editor is not running instead of telling agents to wait and retry - #3400

Merged
hatayama merged 12 commits into
mainfrom
fix/unity-not-running-diagnosis
Oct 27, 2026
Merged

hatayama merged 12 commits into
mainfrom
fix/unity-not-running-diagnosis

Conversation

@hatayama

@hatayama hatayama commented Oct 27, 2026 •

Copy link
Copy Markdown
Owner

Summary

AI agents kept missing that Unity had crashed or been closed. The CLI looked up the process list, found no Unity Editor, and threw that answer away. Commands against a closed Editor returned UNITY_NOT_REACHABLE with Retryable: true and "If Unity is starting, compiling, or reloading scripts, wait and retry". Waits that were already running (compile, launch readiness and hot-reload, 10 minutes each; PlayMode changes; pause points) kept polling a server that could not come back until they timed out.

What changes

  • Commands: when the process lookup answers and finds no Editor for the project, a command fails with the new UNITY_NOT_RUNNING (Retryable: false). The next step it gives is uloop launch. This covers a dial that never reached Unity and a connection that dropped after the request was dispatched (SafeToRetry then follows the command, as for other dispatched failures).
  • Crash evidence: when Temp/UnityLockfile is left behind, the message says the Editor did not shut down normally and may have crashed. It also names the Editor's own log to read before relaunching, because the next Editor start moves or replaces it. A log counts as the Editor's own when the project path Unity records near its top names this project: Editor.log first, then Editor-prev.log. With several Editors open, Editor.log usually belongs to whichever started last, so when neither log names this project no log step is given. uloop status NotRunning uses the same wording.
  • Waits: the compile, compile attach, launch readiness, dynamic-code reload, control-play-mode, hot-reload editor-ready and busy, and pause point waits stop with UNITY_NOT_RUNNING. They do this once the server is gone and two process lookups at least 2 s apart both find no Editor. A failed lookup, a successful poll, or an error that a live server also produces resets the count. Lookups are throttled to one per interval.
  • Launch: for launch, an Editor exit during startup is worded as "Unity exited before it finished starting". It stays SafeToRetry: true like every other launch failure.
  • Compile waits: a compile wait that stops on an exit writes a terminal vibe log entry (cli_compile_status_poll_unity_exited). An attach wait that stops on an exit drops its pending record, because the result lived in the exited Editor.
  • Projects named only by -createProject: the process lookup also reads -createProject <path> as the project an Editor has open. The Editor that creates a project opens it in the same session.
  • Paths through a symlink: an Editor whose command-line path differs from the project root as a string still matches when both paths name the same directory. A command's project root has its symlinks evaluated, while uloop launch passes the path as given. Before, such an Editor was never found for its own project.

Detection boundary

Liveness is decided by the Editor command-line match that status, launch and focus already use (-projectPath, and now -createProject). An Editor whose command line names no project is now reported as unidentified instead of being dropped. While one runs, every path that would conclude "not running" falls back to the behavior from before this PR: commands keep UNITY_NOT_REACHABLE, waits keep waiting, and status reports Unreachable with the reason. launch and focus keep the plain lookup, so their behavior is unchanged.

Not in this PR

Pushing a notification when the Editor exits, for example through a background watch command, will follow in a separate PR.

Compatibility

No protocol version bump. UNITY_NOT_RUNNING exists only in the CLI's stdout error envelope, and the IPC wire format is unchanged. The shared-inputs stamps are regenerated for the cli/common changes.

Verification

  • scripts/check-go-cli.sh (format, vet, lint, tests, build), scripts/check-code-complexity.sh, and scripts/check-file-length.sh pass.
  • Mutation checks: the compile wait exit test fails without the exit watch, the send-path test fails without the post-dispatch exit check, and the default-lookup tests fail when a default goes back to the plain lookup.
  • The dist binary was run end to end against a project with no Editor (with and without a lockfile left behind) and against a fake Unity process whose command line names no project. It was also run against a live Editor: status is Ready and compile succeeds.

The compile wait's check for "Unity's server went away" is about to be
needed outside the project runner: the readiness wait in clicore will use
it to decide when to look for the Editor process. Moving it next to the
other transport predicates keeps one definition for both modules.
Agents kept missing that Unity had crashed or closed. A command against a
closed Editor answered UNITY_NOT_REACHABLE with Retryable true and "if
Unity is starting, compiling, or reloading scripts, wait and retry", even
though the retry loop had just looked up the process list and found no
Editor. The agent was told to wait for an Editor that was not there.

- When that lookup answers without an Editor, the command now fails with
  UNITY_NOT_RUNNING, Retryable false, and `uloop launch` as the step. A
  lookup that failed still leaves the dial error as it was.
- A connection that drops after the request reached Unity is checked the
  same way, so an Editor that exits mid-command is no longer reported as
  a domain reload to wait out.
- When Temp/UnityLockfile is left behind, the message says the Editor did
  not shut down normally and may have crashed, and names the Editor log
  to read before launching, since the next launch renames it.
  `uloop status` NotRunning uses the same wording.

The new error neither unwraps to its cause nor repeats it in Error(), so
the callers that wait out a disconnect do not mistake it for one.
When Unity crashed during a compile, launch, PlayMode change, hot reload,
or pause point wait, the CLI kept polling a server that could not come
back until its own timeout: ten minutes for compile, launch, and hot
reload. The agent sat through that wait and then got a timeout that did
not say Unity was gone.

Each of those waits now feeds its poll errors to a UnityExitWatch, which
looks up the Editor process while the server stays gone and ends the wait
with UNITY_NOT_RUNNING once two lookups at least two seconds apart both
find no Editor. Why two spaced lookups: an Editor that is tearing down can
still be listed for a moment, and one empty listing is thin evidence for
ending a ten-minute wait. A failed lookup, a successful poll, or an error
a live server also produces restarts the confirmation, and lookups are
throttled to one per interval so a 50ms pause point poll does not list
processes on every poll.
The readiness wait behind `uloop launch` now ends with UNITY_NOT_RUNNING
when the Editor exits, but the envelope was worded for a command that
reached a running Editor: SafeToRetry false, a warning that the command
may have partly run, and "run `uloop launch`, then run the command
again", which for launch itself is circular.

For launch the error now says Unity exited before it finished starting,
stays SafeToRetry like every other launch failure, since another launch
starts a new Editor and cannot double-apply anything, and tells the
caller to run launch again after the Editor log step.
A compile status wait that stopped because the Editor exited returned
without a terminal vibe log entry, leaving a status poll start with no
matching end: the exact signature docs/vibe-logs.md tells readers to
look for behind a hang, in the one case where nothing hung. The wait now
logs cli_compile_status_poll_unity_exited before returning.

The attach path also kept its pending record after such an exit. That
record points at a compile result held in the Editor's memory, which went
away with the Editor, so the next compile after a relaunch would spend
its attach probes looking for a result that cannot come back. It is now
removed when the wait ended with UNITY_NOT_RUNNING; other failures keep
it as before.
When the request reached Unity and the connection dropped, a process
lookup that failed left the disconnect in place without any trace, while
the same failure before dispatch writes cli_unity_process_probe_failed.
Both now log it, so a disconnect that was not upgraded to
UNITY_NOT_RUNNING can be told apart from one where an Editor was found.
The log message is reworded to cover both paths.

The test helper that makes the default lookup find an Editor also notes
that the default focus is still real, so a test ending in an undispatched
dial failure should stub it.
The Editor process lookup ties a Unity process to a project only through
-projectPath on its command line. The Editor that creates a project opens
it in the same session, and that session's command line may name the
project only through -createProject. Such an Editor was invisible to the
lookup, which now ends compile and other waits with UNITY_NOT_RUNNING,
and a new user is likely to install uloop in exactly that session.
The process lookup dropped every Editor whose command line names no
project, so "nothing found" also covered an Editor that may be this
project's. That was harmless while it only chose whether to focus, but
UNITY_NOT_RUNNING, the exit watch that ends compile and other waits, and
the crash wording in `uloop status` now conclude from it that Unity is
gone. Such an Editor would have its compile wait cut short by an ordinary
domain reload.

The lookup now keeps those Editors as unidentified. A new
FindRunningUnityProcessDefinitively returns ErrUnidentifiedUnityEditor
instead of nil while one runs, and every caller that concludes Unity is
not running uses it, so they treat it as a lookup that could not tell:
commands keep UNITY_NOT_REACHABLE, waits keep waiting, and status reports
Unreachable with the reason. Launch and focus keep the plain lookup.
A command's project root has its symlinks evaluated, while an Editor's
command line keeps the path it was started with; `uloop launch` itself
passes the unevaluated path. The lookup compared the two as strings, so an
Editor opened through a symlinked directory was never found for its own
project. That used to cost only a NotRunning from `uloop status`, but the
lookup now decides UNITY_NOT_RUNNING and ends compile and other waits, so
every domain reload in such a project would have been reported as an exit.

A candidate whose path differs as a string now still matches when both
paths stat to the same directory, which also covers a case-insensitive
file system accepting either spelling.
The abnormal-exit step named Unity's default Editor.log, but every Editor
start renames Editor.log to Editor-prev.log and replaces the previous
one. With several Editors open, Editor.log usually belongs to whichever
started last, so the step sent the caller to another project's log for
the reason this Editor exited.

The log now counts as this Editor's only when the project path Unity
writes near its top ("Successfully changed project path to: ...") names
the project, checking Editor.log and then Editor-prev.log. When neither
is, no log step is given. The project path comparison is shared with the
process lookup, so a path through a symlink matches here too.
@coderabbitai

coderabbitai Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 6b67b07 and cb82a53.

Files selected for processing: 9
  • cli/common/clicore/tool_readiness_test.go
  • cli/common/unityprocess/process.go
  • cli/common/unityprocess/process_branches_test.go
  • cli/dispatcher/shared-inputs-stamp.json
  • cli/project-runner/internal/projectrunner/control_play_mode_wait.go
  • cli/project-runner/internal/projectrunner/status_command.go
  • cli/project-runner/internal/projectrunner/unity_exit_scripted_server_unix_test.go
  • cli/project-runner/internal/projectrunner/unity_process_lookup_helpers_test.go
  • cli/project-runner/shared-inputs-stamp.json
Files skipped from review as they are similar to previous changes: 1
  • cli/dispatcher/shared-inputs-stamp.json

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.

Walkthrough

The change adds definitive Unity process discovery and Editor-exit detection. It reports confirmed exits through a shared CLI error and applies the detection to readiness, compile, play-mode, hot-reload, pause-point, connection-retry, and status flows.

Changes: Unity Editor exit handling

Layer / File(s) Summary
Process and project-file discovery
cli/common/unityprocess/*, cli/common/clicore/focus.go, cli/dispatcher/internal/dispatcher/launch.go, cli/dispatcher/shared-inputs-stamp.json
Process discovery retains Editors whose project path is unidentified, recognizes -createProject, and compares project paths, including symlinked paths. New helpers locate the project lockfile and a matching Editor log. Launch lockfile path construction uses the shared helper.
Exit confirmation and error reporting
cli/common/errors/*, cli/common/clicore/unity_exit.go, cli/common/clicore/unity_exit_test.go, cli/common/clicore/tool_readiness.go, cli/common/clicore/tool_readiness_test.go
UnityExitWatch checks for a server-gone error and confirms Editor absence through two process lookups at least two seconds apart. UnityNotRunningError carries exit, lockfile, and log details and maps them to CLI error output. Readiness polling returns the error when exit is confirmed.
Unity exit checks in project-runner waits
cli/project-runner/internal/projectrunner/compile_*, cli/project-runner/internal/projectrunner/control_play_mode_wait*, cli/project-runner/internal/projectrunner/hot_reload_*, cli/project-runner/internal/projectrunner/pause_point_wait_poll.go, cli/project-runner/internal/projectrunner/unity_*
Compile, play-mode, hot-reload, and pause-point waits now check for confirmed Editor exit. Compile waits log the exit, and attached compile waits clear the pending record for UnityNotRunningError.
Connection outcomes and status reports
cli/project-runner/internal/projectrunner/connection_retry*, cli/project-runner/internal/projectrunner/status_command*, cli/project-runner/shared-inputs-stamp.json, cli/dispatcher/internal/dispatcher/launch_test.go
Connection flows return UnityNotRunningError when a successful process lookup finds no Editor. Status distinguishes an unidentified running Editor from no running Editor and uses the shared error details for the latter.

Priority: Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant ProjectRunnerWait
  participant UnityExitWatch
  participant UnityProcessLookup
  ProjectRunnerWait->>UnityExitWatch: Observe failed status poll
  UnityExitWatch->>UnityProcessLookup: Check for project Editor
  UnityProcessLookup-->>UnityExitWatch: Return process lookup result
  UnityExitWatch-->>ProjectRunnerWait: Continue polling or return UnityNotRunningError
Loading

Possibly related PRs

  • hatayama/unity-cli-loop#3394: Both changes use Unity lockfile state in launch or Editor-exit handling; this change also uses it to select exit diagnostics.
Merge Risk: Minimal · up to cb82a

No actionable merge-blocking issue is identified; the change appears ready for normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 79.49% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Documentation-only edits and deletions within surv… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly summarizes the main change: report that the Unity Editor is not running instead of advising agents to wait and retry.
Description check Passed The description directly explains the Unity-not-running behavior, exit detection, wait handling, process matching, and verification for this changeset.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 79.49% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 117 functions across 40 files. (2 skipped: 2 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The play mode state wait fed only failed polls to its exit watch, so a
status answer between two lost connections did not restart the watch's
count of lookups that found no Editor. Every other wait observes each
poll, an answer included, and ends only after two lookups in a row find
nothing; this one could end after two lookups in total.
Three parts of the UNITY_NOT_RUNNING path could be undone with every
test still passing: the send path's check for an Editor that exited
after dispatch, the mapping of an Editor of unknown project to
ErrUnidentifiedUnityEditor, and the defaults that pick the definitive
lookup. The last one carries the guarantee that an unidentified Editor
falls back to the behavior from before.

- A scripted server now drops a dispatched request through the full
  send path and expects UnityNotRunningError.
- The mapping moves into selectProjectProcessDefinitively and is tested
  for each answer.
- `uloop status` now goes through the same package lookup as the other
  defaults, and tests pin that lookup and the readiness wait's default
  to the definitive one.
@hatayama
hatayama merged commit fb876df into main Oct 27, 2026
16 checks passed
@hatayama
hatayama deleted the fix/unity-not-running-diagnosis branch October 27, 2026 06:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant