Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
502ee76
fix: uloop compile returns as soon as the Editor answers instead of w…
hatayama Oct 7, 2026
3f1b517
fix: uloop compile no longer brings the Editor to the front while it …
hatayama Oct 7, 2026
2061e4f
fix: Pause point timeout details carry the wait's timeout and the mar…
hatayama Oct 7, 2026
f3daae7
feat: Hot reload writes a timing detail vibe entry that breaks down t…
hatayama Oct 7, 2026
ee6cc4b
fix: A request that Unity keeps answering busy no longer brings the E…
hatayama Oct 7, 2026
d872b6d
fix: Hot reload's call-site cache is bounded by bytes instead of a co…
hatayama Oct 7, 2026
ea9d692
fix: The Debug-switch warning says the setting goes back to the start…
hatayama Oct 7, 2026
00b4a89
feat: Hot reload patches a Multiplayer Play Mode Virtual Player by re…
hatayama Oct 8, 2026
14ecf1a
chore: Regenerate the pause-point skill copies that fell behind their…
hatayama Oct 8, 2026
731c2f7
perf: Hot reload asks the compilation pipeline for the assembly list …
hatayama Oct 8, 2026
084e1d1
perf: Hot reload compares a sibling with its snapshot again only when…
hatayama Oct 8, 2026
5614e38
perf: Hot reload keeps the PDB document list of an assembly across do…
hatayama Oct 8, 2026
0dc1632
docs: Explain why a Multiplayer Play Mode player can quit with a Fata…
hatayama Oct 8, 2026
c328223
fix: Hot reload in a Virtual Player patches only the edited method of…
hatayama Oct 8, 2026
63881ae
perf: Hot reload records each source's length and write time with its…
hatayama Oct 8, 2026
62b1236
perf: Hot reload keeps its compilation assembly list across the impor…
hatayama Oct 8, 2026
9665068
perf: Hot reload's caller scan reads an assembly's MemberRef table be…
hatayama Oct 8, 2026
1ed1980
perf: Hot reload keeps each assembly's referenced-method set on disk,…
hatayama Oct 8, 2026
7daed1a
perf: Hot reload builds the PDB document list with System.Reflection.…
hatayama Oct 8, 2026
4479359
fix: A tool request that Unity never acknowledged is reported as UNIT…
hatayama Oct 8, 2026
e61f285
feat: Hot reload writes vibe entries that split a cold run's analysis…
hatayama Oct 8, 2026
1bcad3b
perf: The first dynamic code run and hot reload after a domain reload…
hatayama Oct 8, 2026
4fa78e1
perf: The first hot reload after a domain reload finds the edited ass…
hatayama Oct 8, 2026
b200c1a
perf: The first hot reload after a compile no longer spends most of a…
hatayama Oct 9, 2026
19f7381
fix: Hot reload no longer mistakes an edit made right after compile f…
hatayama Oct 9, 2026
57bbb47
fix: Hot reload no longer silently skips a file saved while the compi…
hatayama Oct 9, 2026
0b9ded3
docs: Note that a package's own AssetDatabase.Refresh compiles saved …
hatayama Oct 9, 2026
88d0ff2
perf: The first hot reload shim compile after a server reset finds th…
hatayama Oct 9, 2026
7a5711d
fix: The package compiles on Unity 6000.5 and newer, whose Editor shi…
hatayama Oct 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,34 @@ main Editor) do not block each other.

## Hot reload

- A hot-reload patch lives in the Editor process it was applied to. A patch applied to the
main Editor does not reach the Virtual Players, and a player's own
`uloop --project-path <PROJECT_ROOT>/Library/VP/mppm<id> hot-reload --status` reports no
active patch.
- Hot reload cannot patch a Virtual Player yet: a player loads the main project's
`Library/ScriptAssemblies` and has none under its own root. `hot-reload --files ...` sent
to a player reports the file as `Failed`. Whether the CLI then compiles in that player
follows `--compile-on-skip`, as for any unapplied edit: when it compiles, the edit comes in
(`Outcome` is `ReplacedByCompile`); when the compile is held (`CompileFallback` is
`HeldForPlayMode`: `auto`, the default, while that player is in Play Mode), the edit has not
reached the player.
- A hot-reload patch lives in the Editor process it was applied to. To patch a Virtual
Player, send the command to that player:
`uloop --project-path <PROJECT_ROOT>/Library/VP/mppm<id> hot-reload --files ...`.
- A player reads the main project's `Library/ScriptAssemblies` and keeps its own hot-reload
state (the `ActivePatchTotal` that `--status` reports, and the source snapshots) under its own
`Library/UloopHotReload/`.
- A patch applied to the main Editor does not reach the players, and a patch applied to a player
does not reach the main Editor or the other players. A compile of the main Editor's project
reaches every player, and clears the patches a player holds.
- When the main Editor's project has never compiled the edited assembly, a player reports the
file as `Failed` with `Compiled assembly not found ... Compile the main Editor's project
first.`

## When a player quits right after activation

A player whose Editor shows
`Fatal Error! Compilation Pipeline: Could not read file Packages/<package>/<path>.asmdef`
a few seconds after it was activated, and then quits, never loaded its packages. Multiplayer
Play Mode gives a player symlinks to `Assets` and `ProjectSettings` and an empty `Packages`
folder (that is normal), and starts its Editor with `-noUpm -upmRestorePackages`: the player
takes the package list from the main project's `Library/PackageManager/ProjectCache` instead
of resolving packages itself. When the main Editor showed `Project has invalid dependencies` at
startup (for example a `file:` dependency whose folder is missing), that file is absent and the
player finds no package. Fix the main project's dependencies, restart the main Editor until it
starts without that dialog and `<PROJECT_ROOT>/Library/PackageManager/ProjectCache` exists,
then activate the player again. Until then
`uloop --project-path <PROJECT_ROOT>/Library/VP/mppm<id> ...` fails at project resolution: the
player never loaded uloop's project runner.

## Known limitations

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ patch binds to the newest shim. Edit the file and reload again to apply new chan
default for projects created before Unity 6.6), `uloop compile`, and the compile `uloop run-tests` runs first unless given `--skip-compile`. `uloop control-play-mode --action Play` warns with
the counts when it is about to drop patches or pause points. There is no persistence
and no automatic re-apply.
A compile Unity starts on its own ends them too: the hold that keeps a returning focus from
recompiling (`AutoRefreshHeld`) stops only Unity's Auto Refresh, so an editor script or
package that calls `AssetDatabase.Refresh()` itself — some do after every domain reload —
still imports the edits saved since the last compile, and Unity compiles them. The compiled
code then contains those edits, so behavior still converges.
- Never reflected by hot reload: initializer changes on compiled fields and new
types. Those always need `uloop compile`. Signature changes — return type,
rename, parameter list — are handled through the added-member rules and the
Expand Down
13 changes: 12 additions & 1 deletion .agents/skills/uloop-hot-reload/references/scope-and-limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,17 @@ line each. A file with a declaration that hot reload refused to introduce (its
`Warnings` line says the type requires a compile) gets no such line, because that
compile also establishes its baseline. This also holds for an existing file that
gains such a declaration, for example a nested type or a delegate.
A source whose length and last write time still equal those recorded when its baseline
was captured is taken to match it without being read, in later Editor sessions as well; a
rewrite that keeps both (for example a copy that preserves timestamps) is noticed only once
its assembly compiles again, which captures the baseline anew. A baseline captured by an
earlier package version carries no such record and is compared by bytes once per Editor
session. This applies wherever a source is compared with its baseline: finding
drifted siblings, deciding which live patches to re-apply after a skip, and selecting
files when `--files` is omitted.
A file saved while the compile was running — after the compiler read it — is marked when
the snapshot is captured: a run without `--files` selects it, and its methods are all
patched with a warning, until the next compile.

Property getters with a body (including expression-bodied properties) are patched
like ordinary methods. Editing a compiled property's setter, init, or indexer accessor
Expand Down Expand Up @@ -430,7 +441,7 @@ source on disk. When a run skips a method it had patched before, `Warnings` name
| File does not belong to any compiled assembly | Per-file entry with `Method` = `(file)`; only `Assets/` and `Packages/` sources resolve |
| Resolved assembly name is missing from CompilationPipeline | Per-file entry with `Method` = `(file)`; Unity may have mapped a not-yet-imported `.asmdef` onto a predefined assembly. Run `uloop compile` first |
| Script is not in the last compiled assembly's source list and its assembly membership cannot be confirmed | Per-file entry with `Method` = `(file)`; a new file passed with `--files` is hot-reloadable when its membership in an existing, unchanged compiled assembly is confirmed (`.asmdef` / `.asmref` boundaries are checked when present; a predefined assembly with none also passes), but fails when the Editor is not ready or an `.asmdef` / `.asmref` on its path was added, deleted, or changed since the last import — run `uloop compile` first |
| The Editor is a Multiplayer Play Mode Virtual Player | Per-file entry with `Method` = `(file)`; a Virtual Player has no compiled assemblies under its own project root, so hot reload cannot patch it yet. The edit reaches that player only through a compile: the CLI's compile fallback brings it in when `--compile-on-skip` lets the compile run, and `auto` holds it while that player is in Play Mode (`CompileFallback` is `HeldForPlayMode`). A patch applied to the main Editor does not reach Virtual Players (each is a separate Editor process) |
| The Editor is a Multiplayer Play Mode Virtual Player and the main Editor's project has not compiled the assembly | Per-file entry with `Method` = `(file)`; a Virtual Player reads the main Editor's `Library/ScriptAssemblies`, and the reason says to compile the main Editor's project first. Until then the edit reaches that player only through a compile: the CLI's compile fallback brings it in when `--compile-on-skip` lets the compile run, and `auto` holds it while that player is in Play Mode (`CompileFallback` is `HeldForPlayMode`). A patch applied to the main Editor does not reach Virtual Players (each is a separate Editor process) |
| The Editor is compiling or importing when the request arrives, or starts to before the reload is applied | Per-file entry with `Method` = `(file)`; nothing in the source needs a change. The response sets `RetryAfterEditorReady`, and the CLI waits for the Editor to settle (up to 10 minutes) and applies the same request again once; if Unity's own compile already took the edit in, the second apply reports `NothingToApply`. A second refusal falls through to `--compile-on-skip`. |
| Another uloop command still holds the Editor when the request arrives (for example a `uloop compile` sent a moment earlier) | The request is not run; the CLI does not retry against the busy Editor and does not bring it to the front. It waits for that command to finish and the Editor to be ready (up to 10 minutes), then sends the same request once. `EditorReadyRetryNote` names the command it waited for and `Timing.EditorReadyWaitMs` is the wait. A busy answer to that one request is reported as `UNITY_SERVER_BUSY` without another wait. While a cancelled `execute-dynamic-code` request still holds the Editor, the wait sends the request again every 5 seconds, because the Editor takes such a request's slot back only when another request arrives. When the command that ran was a compile that already took the edit in, give `--files`: with no files the Editor selects changed files anew, finds none, and fails validation instead of reporting `NothingToApply` |
| Loaded assembly differs from the one on disk (pending compile) | Run `uloop compile` first, then retry |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,4 @@ interrupt the flow or ask the user about it mid-run. At the next stopping point
propose `uloop set-code-optimization debug --startup` (session-only without `--startup`),
and only apply it if the user approves.

The automatic Debug switch changes only the current project's code optimization for this Editor session; it reverts on every Editor restart, and each re-switch costs a full script recompile. `uloop set-code-optimization debug --startup` makes Debug the startup default through a machine-wide Unity preference that applies to every project. Only the project's C# scripts run slower, mainly in Play Mode - the Editor itself is not slowed.
The automatic Debug switch changes the code optimization of this Editor session only; on every Editor restart it goes back to the 'Code Optimization On Startup' preference (a per-user Unity preference, Release unless changed), and each re-switch costs a full script recompile. `uloop set-code-optimization debug --startup` makes Debug the startup default through a machine-wide Unity preference that applies to every project. Only the project's C# scripts run slower, mainly in Play Mode - the Editor itself is not slowed.
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,34 @@ main Editor) do not block each other.

## Hot reload

- A hot-reload patch lives in the Editor process it was applied to. A patch applied to the
main Editor does not reach the Virtual Players, and a player's own
`uloop --project-path <PROJECT_ROOT>/Library/VP/mppm<id> hot-reload --status` reports no
active patch.
- Hot reload cannot patch a Virtual Player yet: a player loads the main project's
`Library/ScriptAssemblies` and has none under its own root. `hot-reload --files ...` sent
to a player reports the file as `Failed`. Whether the CLI then compiles in that player
follows `--compile-on-skip`, as for any unapplied edit: when it compiles, the edit comes in
(`Outcome` is `ReplacedByCompile`); when the compile is held (`CompileFallback` is
`HeldForPlayMode`: `auto`, the default, while that player is in Play Mode), the edit has not
reached the player.
- A hot-reload patch lives in the Editor process it was applied to. To patch a Virtual
Player, send the command to that player:
`uloop --project-path <PROJECT_ROOT>/Library/VP/mppm<id> hot-reload --files ...`.
- A player reads the main project's `Library/ScriptAssemblies` and keeps its own hot-reload
state (the `ActivePatchTotal` that `--status` reports, and the source snapshots) under its own
`Library/UloopHotReload/`.
- A patch applied to the main Editor does not reach the players, and a patch applied to a player
does not reach the main Editor or the other players. A compile of the main Editor's project
reaches every player, and clears the patches a player holds.
- When the main Editor's project has never compiled the edited assembly, a player reports the
file as `Failed` with `Compiled assembly not found ... Compile the main Editor's project
first.`

## When a player quits right after activation

A player whose Editor shows
`Fatal Error! Compilation Pipeline: Could not read file Packages/<package>/<path>.asmdef`
a few seconds after it was activated, and then quits, never loaded its packages. Multiplayer
Play Mode gives a player symlinks to `Assets` and `ProjectSettings` and an empty `Packages`
folder (that is normal), and starts its Editor with `-noUpm -upmRestorePackages`: the player
takes the package list from the main project's `Library/PackageManager/ProjectCache` instead
of resolving packages itself. When the main Editor showed `Project has invalid dependencies` at
startup (for example a `file:` dependency whose folder is missing), that file is absent and the
player finds no package. Fix the main project's dependencies, restart the main Editor until it
starts without that dialog and `<PROJECT_ROOT>/Library/PackageManager/ProjectCache` exists,
then activate the player again. Until then
`uloop --project-path <PROJECT_ROOT>/Library/VP/mppm<id> ...` fails at project resolution: the
player never loaded uloop's project runner.

## Known limitations

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ patch binds to the newest shim. Edit the file and reload again to apply new chan
default for projects created before Unity 6.6), `uloop compile`, and the compile `uloop run-tests` runs first unless given `--skip-compile`. `uloop control-play-mode --action Play` warns with
the counts when it is about to drop patches or pause points. There is no persistence
and no automatic re-apply.
A compile Unity starts on its own ends them too: the hold that keeps a returning focus from
recompiling (`AutoRefreshHeld`) stops only Unity's Auto Refresh, so an editor script or
package that calls `AssetDatabase.Refresh()` itself — some do after every domain reload —
still imports the edits saved since the last compile, and Unity compiles them. The compiled
code then contains those edits, so behavior still converges.
- Never reflected by hot reload: initializer changes on compiled fields and new
types. Those always need `uloop compile`. Signature changes — return type,
rename, parameter list — are handled through the added-member rules and the
Expand Down
13 changes: 12 additions & 1 deletion .claude/skills/uloop-hot-reload/references/scope-and-limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,17 @@ line each. A file with a declaration that hot reload refused to introduce (its
`Warnings` line says the type requires a compile) gets no such line, because that
compile also establishes its baseline. This also holds for an existing file that
gains such a declaration, for example a nested type or a delegate.
A source whose length and last write time still equal those recorded when its baseline
was captured is taken to match it without being read, in later Editor sessions as well; a
rewrite that keeps both (for example a copy that preserves timestamps) is noticed only once
its assembly compiles again, which captures the baseline anew. A baseline captured by an
earlier package version carries no such record and is compared by bytes once per Editor
session. This applies wherever a source is compared with its baseline: finding
drifted siblings, deciding which live patches to re-apply after a skip, and selecting
files when `--files` is omitted.
A file saved while the compile was running — after the compiler read it — is marked when
the snapshot is captured: a run without `--files` selects it, and its methods are all
patched with a warning, until the next compile.

Property getters with a body (including expression-bodied properties) are patched
like ordinary methods. Editing a compiled property's setter, init, or indexer accessor
Expand Down Expand Up @@ -430,7 +441,7 @@ source on disk. When a run skips a method it had patched before, `Warnings` name
| File does not belong to any compiled assembly | Per-file entry with `Method` = `(file)`; only `Assets/` and `Packages/` sources resolve |
| Resolved assembly name is missing from CompilationPipeline | Per-file entry with `Method` = `(file)`; Unity may have mapped a not-yet-imported `.asmdef` onto a predefined assembly. Run `uloop compile` first |
| Script is not in the last compiled assembly's source list and its assembly membership cannot be confirmed | Per-file entry with `Method` = `(file)`; a new file passed with `--files` is hot-reloadable when its membership in an existing, unchanged compiled assembly is confirmed (`.asmdef` / `.asmref` boundaries are checked when present; a predefined assembly with none also passes), but fails when the Editor is not ready or an `.asmdef` / `.asmref` on its path was added, deleted, or changed since the last import — run `uloop compile` first |
| The Editor is a Multiplayer Play Mode Virtual Player | Per-file entry with `Method` = `(file)`; a Virtual Player has no compiled assemblies under its own project root, so hot reload cannot patch it yet. The edit reaches that player only through a compile: the CLI's compile fallback brings it in when `--compile-on-skip` lets the compile run, and `auto` holds it while that player is in Play Mode (`CompileFallback` is `HeldForPlayMode`). A patch applied to the main Editor does not reach Virtual Players (each is a separate Editor process) |
| The Editor is a Multiplayer Play Mode Virtual Player and the main Editor's project has not compiled the assembly | Per-file entry with `Method` = `(file)`; a Virtual Player reads the main Editor's `Library/ScriptAssemblies`, and the reason says to compile the main Editor's project first. Until then the edit reaches that player only through a compile: the CLI's compile fallback brings it in when `--compile-on-skip` lets the compile run, and `auto` holds it while that player is in Play Mode (`CompileFallback` is `HeldForPlayMode`). A patch applied to the main Editor does not reach Virtual Players (each is a separate Editor process) |
| The Editor is compiling or importing when the request arrives, or starts to before the reload is applied | Per-file entry with `Method` = `(file)`; nothing in the source needs a change. The response sets `RetryAfterEditorReady`, and the CLI waits for the Editor to settle (up to 10 minutes) and applies the same request again once; if Unity's own compile already took the edit in, the second apply reports `NothingToApply`. A second refusal falls through to `--compile-on-skip`. |
| Another uloop command still holds the Editor when the request arrives (for example a `uloop compile` sent a moment earlier) | The request is not run; the CLI does not retry against the busy Editor and does not bring it to the front. It waits for that command to finish and the Editor to be ready (up to 10 minutes), then sends the same request once. `EditorReadyRetryNote` names the command it waited for and `Timing.EditorReadyWaitMs` is the wait. A busy answer to that one request is reported as `UNITY_SERVER_BUSY` without another wait. While a cancelled `execute-dynamic-code` request still holds the Editor, the wait sends the request again every 5 seconds, because the Editor takes such a request's slot back only when another request arrives. When the command that ran was a compile that already took the edit in, give `--files`: with no files the Editor selects changed files anew, finds none, and fails validation instead of reporting `NothingToApply` |
| Loaded assembly differs from the one on disk (pending compile) | Run `uloop compile` first, then retry |
Expand Down
Loading
Loading