Repository navigation
fix: Send the compile again when Unity lost the request or rejected it as already compiling - #3192
Merged
hatayama merged 6 commits intoOct 6, 2026
Conversation
The fresh compile path is about to send the compile again when Unity lost the request or rejected it as busy, which needs a single attempt that reports how it ended. Nothing changes yet: every return reports a final outcome and the existing entry calls the attempt once.
Unity can lose a compile request that arrives just before a domain reload, and it rejects a compile that arrives while it is already compiling or updating. The CLI then either waited out the whole timeout or returned the rejection after waiting for the Editor. These tests pin the resend behavior. The scaffold they need (a shorter status poll for tests, the resending entry, and the server-gone check) does not resend yet, so the resend cases fail.
A compile request that reaches Unity just before a domain reload can be acknowledged and then dropped before the main thread registers it. The CLI then polled Ready answers without a result until its wait ran out. A compile that arrives while Unity is already compiling or updating is rejected, and the CLI returned that rejection after waiting for the Editor to become Ready. The fresh compile entry now sends the request again with a new request ID in both cases, at most three sends within the one wait the caller asked for: - A request counts as lost only after the server was seen recreated (the send dropped, a status query found the server gone, or an answer reported a domain reload) and three Ready answers without a result followed. Without that, a live request answers the same way until its compile starts. - A busy rejection is resent once the wait has seen the Editor Ready. - Nothing is resent with less than 10 seconds of the wait left, and the last attempt behaves exactly as before. The first attempt keeps the exact --timeout-seconds wait, and pause-point recovery's own entry still never resends.
The CLI now waits for Unity and sends the compile again when Unity rejects it as compiling or updating, so these codes reach the caller only after the resends ran out. The skill tells agents to run the compile again rather than treat the codes as an immediate collision.
Contributor
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configuration
📒 Files selected for processing (9)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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 |
Field verification tells a lost request from a busy rejection only by the reason on the resend log entry, so a swapped reason must fail a test rather than mislead that check.
hatayama
merged commit Oct 6, 2026
927db18
into
feature/hot-reload-large-project-feedback
6 of 7 checks passed
This was referenced Oct 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
uloop compileno longer waits out its whole timeout (600 s by default) when Unity loses the compile request in a domain reload. It notices the loss within a few seconds and sends the compile again.uloop compileno longer fails withCOMPILE_ALREADY_IN_PROGRESS/COMPILE_EDITOR_UPDATINGafter it has already waited for a compile it did not start (for example one started by an asset refresh). Once the Editor is idle it sends the compile again and returns the real result.User Impact
Problem 1 (lost request). If the compile request reaches Unity in the few tens of milliseconds just before a domain reload, the IPC thread accepts it, but the reload starts before the main thread handles it. Unity keeps no record of it. After the reload, every
get-compile-statusanswer isReady=true/HasResult=false. The CLI then waited the full timeout and ended withCOMPILE_WAIT_TIMEOUT, and it kept the Editor in front for about 590 s of that wait. The hot-reload fallback compile is sent right after the stale-assembly failure, so it hits this window most often (reproduced twice).Problem 2 (busy rejection). If Unity is already compiling or updating when the request arrives, it rejects the compile and stores that rejection as the request's result. The CLI waited for the Editor to become Ready (the running compile plus its domain reload, about 19 s on a large project). It then returned the stored rejection with exit 1. The CLI had waited, but no compile ran.
After this change the CLI sends the compile again in both cases, with a new request ID. It sends at most 3 times in total, and all the sends share the one wait the user asked for.
How a lost request is recognized
Facts about Unity's
get-compile-status(CompileStatusBridgeCommand.BuildResponse):Readyis Editor-wide: not compiling, not updating, and not reloading.Ready=true/HasResult=falsefor a few seconds before its compile starts (the Play Mode exit wait, the compile-start wait, or an unfocused Editor). Resending then would be rejected as busy, because the first request still holds the single-flight slot.So a request counts as lost only when both of these hold:
The server was seen to be recreated. Only three things count as evidence:
IsDomainReloadInProgress=true.These do not count, because each can happen while the server is alive:
3 Ready answers without a result in a row after that evidence. This is the same streak the reattach wait already uses. A busy answer resets the streak.
Changes
Only the fresh-compile path behind
runCompileWithReattachPolicyresends. That path servesuloop compile, the hot-reload fallback compile, and the run-tests implicit compile.Each resend uses a new
RequestId. Unity keeps the old rejection under the old ID, so a query with that ID would return it at once.At most 3 sends. The last attempt detects nothing and behaves exactly as before:
COMPILE_WAIT_TIMEOUT.Reaching the limit never creates a new kind of failure, and no new error code is added.
No resend with less than 10 s of the wait left. The resent compile would start in Unity just as the CLI times out, which adds a compile and turns a rejection into a timeout. In that case a rejection is returned and a lost request keeps waiting, both as before.
The first attempt's wait limit is still exactly
--timeout-seconds. That value appears intimeout_msand intimed out after …ms. A resent attempt waits only for the time that is left.Each resend writes a
cli_compile_request_resendCLI VibeLog entry, whenULOOP_DEBUGis set, with these fields:reason:request_missingoreditor_busyattemptrequest_id: the ID being replacedPreparatory refactor (behavior unchanged): the body of the fresh compile was moved into one-attempt function. The busy-rejection check was moved next to the new code, renamed, and is now shared with pause-point recovery.
The compile skill's
ErrorCodeline now says the busy codes are what remains after the CLI resent twice. The generated.claude/and.agents/copies were regenerated withskills install.Unchanged:
cli/common, so no shared release inputs change.Input space
missing≥ 3 in a rowCOMPILE_WAIT_TIMEOUTmissingmissing×3IsDomainReloadInProgress=true, thenmissing×3missingcontinuesmissing×2, then the resultmissing×2, compiling,missing×2, then the resultCOMPILE_ALREADY_IN_PROGRESS/COMPILE_EDITOR_UPDATINGafter Readymissingon all 3 attemptsmissingcontinuesmissingcontinues, less than 10 s left--timeout-seconds)TestRunFreshCompileReportsCancellationWhileWaitingAfterDisconnectcompile_attachtests)--timeout-secondsInputs not used as axes:
--force-recompile: the resend reuses the same params. A forced request that spans a reload gets aCOMPILE_RESULT_UNKNOWNresult, which is row 8.WaitForDomainReload: always true on this path.ReloadExternalSceneChanges: it only changes the result's content (row 8).nullor omittedResult: the completion check is unchanged.Verification
This PR targets the integration branch, so the Go CI (
build-cli) does not run on it. Everything below was run locally on macOS (Apple silicon).New tests (
compile_fresh_recovery_test.go): 16 test functions, 22 cases.Red, before the change: these failed:
These passed as guards:
Green: all of them pass.
-race -count=20is stable.Each test finishes in about 10 ms.
T1 and T15 also check the single
cli_compile_request_resendentry. T1 expectsreasonrequest_missing,attempt1, and the first request's ID; T15 expectsreasoneditor_busy.The status query fake counts only queries made before the test cancels the context. Go 1.26 tickers are synchronous, so
selectcan pick a tick overctx.Done(). Counting only pre-cancel queries keeps exact count asserts from flaking.Mutations: 14 of 14 caught. Each was applied, the new tests run, and the change reverted;
git statuswas clean afterwards.Regression.
go test ./... -count=1incli/project-runner: 921 PASS, 1 FAIL. The baseline at the branch point is 896 PASS and the same FAIL; the 25 new PASS lines are the 22 cases plus 3 parent tests.TestSendWithTransientConnectionRetryAbortsOnRefusedConnect. The agent sandbox deniesbindon a Unix socket there (bind: operation not permitted), and it fails the same way on the base.scripts/check-go-cli.sh.cli/commonatipcendpoint, where the sandbox deniedmkdirunder/tmp. Everything else incli/commonpassed, and this PR does not touchcli/common.cli/dispatcher,cli/project-runner, andcli/release-automation:gofmt -l: emptygo vet: cleango build.Other checks.
runFreshCompileAttempt13,waitForCompileCompletionWithDeps13,runFreshCompileRecoveringWithDeps5,isServerGoneError4.run.gois 441).cli/project-runneris 95.23%; the baseline is 95.2.check-skill-sizepasses andscripts/sync-tool-docs.sh --checkreports no drift. The regenerated.claude/and.agents/compile skill copies match the source byte for byte.On a real Editor (Unity 2022.3, dev binaries,
ULOOP_DEBUG=1):AssetDatabase.Refresh()throughexecute-dynamic-code, and ranuloop compileright after it returned.is_compiling=trueand stored "Compilation is already in progress".cli_compile_request_resend reason=editor_busy attempt=1and sent a new request with the remaining 594,964 ms as its wait.Success: true, exit 0, about 9 s after the command started. During the investigation, the same sequence without this change returnedCOMPILE_ALREADY_IN_PROGRESSwith exit 1.uloop compileabout 200 ms after it changed.domain_reload_start, and its send ended with EOF 5 ms later. Unity never loggedcompile_request_receivedfor that ID, and after the reload it answeredready=true has_result=falsethree times.cli_compile_request_resend reason=request_missing attempt=1and sent a new ID. Unity received it at once.Success: true, exit 0, about 9.5 s after the first send. Before this change the wait had no other way out of these answers, so it would have run the full 600 s, as in the two fallback-compile reproductions.uloop compilereturnedSuccess: true.Not verified:
main.Not covered
RequestIdwhen it resends after a busy rejection.COMPILE_WAIT_TIMEOUT, and the second ID's pending record stays, so the nextuloop compilereattaches to it. This is accepted by design.Notes
the compile request has no record in Unity, in lowercase for staticcheck ST1005. It is never shown to users.