Repository navigation
fix: uloop says the Unity Editor is not running instead of telling agents to wait and retry - #3400
Conversation
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.
|
No actionable comments were generated in the recent review.
|
| 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:
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
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: 
4 · 
1
-
Failed checks: 1 warning Passed checks: 4 passedFull 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.
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.
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_REACHABLEwithRetryable: trueand "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
UNITY_NOT_RUNNING(Retryable: false). The next step it gives isuloop launch. This covers a dial that never reached Unity and a connection that dropped after the request was dispatched (SafeToRetrythen follows the command, as for other dispatched failures).Temp/UnityLockfileis 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.logfirst, thenEditor-prev.log. With several Editors open,Editor.logusually belongs to whichever started last, so when neither log names this project no log step is given.uloop statusNotRunning uses the same wording.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, an Editor exit during startup is worded as "Unity exited before it finished starting". It staysSafeToRetry: truelike every other launch failure.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.-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.uloop launchpasses 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,launchand 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 keepUNITY_NOT_REACHABLE, waits keep waiting, andstatusreportsUnreachablewith the reason.launchand 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_RUNNINGexists only in the CLI's stdout error envelope, and the IPC wire format is unchanged. The shared-inputs stamps are regenerated for thecli/commonchanges.Verification
scripts/check-go-cli.sh(format, vet, lint, tests, build),scripts/check-code-complexity.sh, andscripts/check-file-length.shpass.statusis Ready andcompilesucceeds.