Skip to content

fix(package): Stop tools from hanging when a pause point fires mid-command#1685

Merged
hatayama merged 7 commits into
v3-betafrom
docs/rewrite-pause-point-skill
Jul 11, 2026
Merged

fix(package): Stop tools from hanging when a pause point fires mid-command#1685
hatayama merged 7 commits into
v3-betafrom
docs/rewrite-pause-point-skill

Conversation

@hatayama

@hatayama hatayama commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Summary

  • Rewrites the uloop-wait-for-pause-point skill to describe the source-based (file:line) pause point workflow, replacing stale wording left over from the old debugger-style approach.
  • Fixes a real bug found while writing the E2E verification for that skill: several tools (most notably simulate-mouse-ui) hung forever if a pause point fired while the tool call was still in flight.

User Impact

  • Before: if Play Mode paused mid-command (e.g. clicking a button that hits a pause point), the calling tool could hang indefinitely instead of returning. Recovering required restarting the Unity Editor.
  • After: any tool interrupted by a pause point returns promptly with InterruptedByPausePoint: true and identifies which pause point(s) fired, so callers can tell "the click landed, only the follow-up animation was cut short" apart from "the click never reached the target."
  • BUSY responses now also report how long ago the main thread last ticked, so a caller can distinguish "Editor is alive but the tool is genuinely slow" from "Editor is frozen" without extra probing.

Changes

  • MouseUiEditorFrameWaiter gains an explicit Paused outcome (distinct from Completed/TimedOut), since Time.realtimeSinceStartup freezes while Play Mode is paused but EditorApplication.update keeps ticking — duration-based wait loops (overlay animations, LongPress hold, drag interpolation) would otherwise spin forever.
  • All simulate-mouse-ui actions (Click, LongPress, Drag, DragStart, DragMove, DragEnd) now report InterruptedByPausePoint/PausePointId/PausePointHitCount/PausePointHits on the same pattern already used by simulate-keyboard/simulate-mouse-input.
  • Root cause of the hang itself: the outermost await useCase.ExecuteAsync(...) in several tool wrappers lacked ConfigureAwait(false), so it captured Unity's SynchronizationContext. That context's pump is tied to the player loop and does not run while paused, so a continuation posted back to it after a pause-triggered completion never executed. Added ConfigureAwait(false) to all affected wrappers (ControlPlayMode, Compile, GetLogs, GetHierarchy, FindGameObjects, ReplayInput, RunTests, SimulateMouseInput, SimulateKeyboard, SimulateMouseUi) and documented the invariant once on the shared UnityCliLoopTool.ExecuteAsync base method.
  • Wired the existing EditorMainThreadLivenessTracker into ServerBusyErrorData/JsonRpcResponseFactory so BUSY responses include secondsSinceLastMainThreadTick.
  • Updated the pause point skill's "Line Placement" guidance and added two "Requirements & Safety" notes learned during this investigation (package-ID paths under Packages/, and "No sequence point found" meaning an assembly was built without sequence points).

Verification

  • uloop compile: 0 errors / 0 warnings.
  • uloop run-tests (full unfiltered suite): all tests passing, including the updated SkillInstallLayoutTests expectation.
  • Real E2E repro (simulate-mouse-ui --action Click), pause point hit mid-command: InterruptedByPausePoint: true, ~0.17s response, Editor fully responsive afterward (control-play-mode --action Step in ~0.07s, no BUSY, no Editor restart needed). Repeated after all three fix commits landed as a final check against the diagnostic-instrumentation removal.
  • Real E2E repro (simulate-keyboard --action Press), pause point on a key-guarded line hit while the press was already in flight (not before it started): InterruptedByPausePoint: true, PressEdgeObserved: true, ~0.39s response, Editor fully responsive afterward.

Review in cubic

hatayama added 7 commits July 11, 2026 12:18
The skill previously documented the hand-written UloopPausePoint.Pause("id")
marker workflow: edit source to add a marker, recompile, then enable the
pause point by id. That workflow is now superseded by source-based pause
points (enable-pause-point --file <path> --line <N>), which require no
source edit and no recompile, and work mid-PlayMode.

Rewrite the skill to document only the source-based workflow, plus:
- the new CapturedVariables snapshot response (locals, parameters, and
  instance fields, UnityObject classification handles, truncation
  semantics)
- the Debug code-optimization requirement
- domain-reload patch lifetime
- the Mono JIT inlining limitation

The generated copies under .claude/ and .agents/ were regenerated via
`uloop skills install --claude --agents` and are byte-identical to the
source (verified).

This is PR 6, commit 1 of the uloop Source Pause Point plan.
Source pause points and captured variables are new ubiquitous-language
terms introduced by the file/line pause-point feature; the glossary
needs entries so docs, commit messages, and reviews use them
consistently with the existing pause-point marker terminology.
EditorApplication.update keeps ticking while paused, but Time.realtimeSinceStartup
freezes, so every duration-based wait loop (overlay animations, LongPress hold,
drag interpolation) spun forever once a Pause Point fired mid-command. Give
MouseUiEditorFrameWaiter an explicit Paused outcome and thread it through every
mouse UI action so a mid-command pause returns InterruptedByPausePoint (with the
same PausePointId/PausePointHitCount/PausePointHits fields SimulateKeyboard and
SimulateMouseInput already expose) instead of hanging the tool call.
The same captured-SynchronizationContext hang that hit simulate-mouse-ui was
latent in nine other tool wrappers: their outermost `await useCase.ExecuteAsync(...)`
still captured UnitySynchronizationContext, so any of them could hang forever if
a Pause Point fired while the call was in flight. Add ConfigureAwait(false) to
all of them and document the invariant once on the shared base class instead of
per wrapper, so new tools inherit the rule from where they implement ExecuteAsync.
Investigating the mouse-ui hang needed a way to tell "main thread still ticking,
tool genuinely running long" apart from a frozen Editor without native stack
sampling. EditorMainThreadLivenessTracker already tracked this; wire its
SecondsSinceLastMainThreadTick() into ServerBusyErrorData so BUSY responses
carry it too, and cover the wire shape with a test.
…ontmatter

The skill rewrite changed the wait-for-pause-point frontmatter description
prefix from "Pauses Unity's playback" to "Pauses Unity playback", but
SkillInstallLayoutTests still pinned the old prefix and failed.
…kill

Pausing on a line that handles simulated input no longer risks hanging the
command: simulate-* now returns promptly with InterruptedByPausePoint=true,
so the Line Placement caution is replaced with the fixed behavior and when
to still prefer post-consumption lines.

Also add two enable-failure findings from E2E validation to Requirements &
Safety: scripts under Packages/ resolve only by the package-id path form,
and a "No sequence point found" error on every line means the assembly
lacks debug sequence points.
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR revises pause-point documentation, adds main-thread liveness diagnostics, prevents paused-editor continuation hangs, and propagates paused or timed-out frame outcomes through mouse UI simulation responses.

Changes

Pause-point guidance

Layer / File(s) Summary
Pause-point workflow and safety guidance
.agents/skills/..., .claude/skills/..., Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md
Updates pause-point workflows, captured-variable semantics, timeout interpretation, line placement, and runtime requirements.
Pause-point glossary definitions
docs/glossary.md
Adds definitions for pause points, source pause points, and captured variables.

Async execution and busy diagnostics

Layer / File(s) Summary
Non-capturing tool continuations
Packages/src/Editor/FirstPartyTools/*, Packages/src/Editor/ToolContracts/UnityCliLoopTool.cs
Applies ConfigureAwait(false) to tool execution awaits and documents the continuation requirement.
Busy-response liveness diagnostics
Packages/src/Editor/Infrastructure/Api/*, Assets/Tests/Editor/JsonRpcResponseFactoryWireShapeCharacterizationTests.cs
Adds secondsSinceLastMainThreadTick to busy responses and verifies the serialized field.

Mouse UI pause interruption handling

Layer / File(s) Summary
Structured editor-frame wait outcomes
Packages/src/Editor/FirstPartyTools/SimulateMouseUi/*
Replaces boolean frame readiness with completed, paused, and timed-out outcomes across waits and animations.
Pause-aware click and drag execution
Packages/src/Editor/FirstPartyTools/SimulateMouseUi/*
Distinguishes pauses from timeouts during clicks, long presses, and drag phases while cleaning up overlays.
Pause-point response metadata
Packages/src/Editor/FirstPartyTools/SimulateMouseUi/*, Packages/src/Runtime/PausePoints/AssemblyInfo.cs
Adds interruption and pause-point hit fields to mouse UI responses and enables required internal access.

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

Sequence Diagram(s)

sequenceDiagram
  participant SimulateMouseUi
  participant MouseUiEditorFrameWaiter
  participant MouseUiGestureExecutor
  participant MouseUiSimulationResponseFactory
  SimulateMouseUi->>MouseUiGestureExecutor: execute click or drag
  MouseUiGestureExecutor->>MouseUiEditorFrameWaiter: wait for editor frame
  MouseUiEditorFrameWaiter-->>MouseUiGestureExecutor: Completed, Paused, or TimedOut
  MouseUiGestureExecutor->>MouseUiSimulationResponseFactory: create outcome response
  MouseUiSimulationResponseFactory-->>SimulateMouseUi: return pause-point metadata
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly summarizes the main fix: preventing tools from hanging when a pause point interrupts a command.
Description check ✅ Passed The description is directly related to the changeset and accurately summarizes the pause-point hang fixes and related updates.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/rewrite-pause-point-skill

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.

coderabbitai[bot]
coderabbitai Bot previously requested changes Jul 11, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Packages/src/Editor/ToolContracts/UnityCliLoopTool.cs (1)

61-61: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Propagate ConfigureAwait(false) through the delegated use cases. CompileUseCase, RunTestsUseCase, and ExecuteDynamicCodeUseCase still have plain awaits, so the deadlock risk extends beyond the tool wrapper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Packages/src/Editor/ToolContracts/UnityCliLoopTool.cs` at line 61, Update
CompileUseCase, RunTestsUseCase, and ExecuteDynamicCodeUseCase so every await on
their delegated asynchronous operations uses ConfigureAwait(false), propagating
the context-free await behavior beyond the tool wrapper.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiSimulationResponseFactory.cs`:
- Around line 148-196: Marshal pause-point registry access to the main thread
before invoking AttachPausePointHit from CreateInterruptedResult, ensuring both
GetLatestHitSnapshot and GetHitSnapshots are read on the owning thread. Preserve
the existing response construction and pause-point population behavior while
routing the continuation through the project’s standard main-thread dispatch
mechanism; do not make unsynchronized registry reads from background
continuations.

---

Outside diff comments:
In `@Packages/src/Editor/ToolContracts/UnityCliLoopTool.cs`:
- Line 61: Update CompileUseCase, RunTestsUseCase, and ExecuteDynamicCodeUseCase
so every await on their delegated asynchronous operations uses
ConfigureAwait(false), propagating the context-free await behavior beyond the
tool wrapper.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f3c54c14-cc37-47e1-a5f9-3c327e630066

📥 Commits

Reviewing files that changed from the base of the PR and between 272edb3 and a19f440.

⛔ Files ignored due to path filters (1)
  • Packages/src/Editor/FirstPartyTools/SimulateMouseUi/UnityCLILoop.FirstPartyTools.SimulateMouseUi.Editor.asmdef is excluded by none and included by none
📒 Files selected for processing (28)
  • .agents/skills/uloop-wait-for-pause-point/SKILL.md
  • .claude/skills/uloop-wait-for-pause-point/SKILL.md
  • Assets/Tests/Editor/JsonRpcResponseFactoryWireShapeCharacterizationTests.cs
  • Assets/Tests/Editor/SkillInstallLayoutTests.cs
  • Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md
  • Packages/src/Editor/FirstPartyTools/Compile/CompileTool.cs
  • Packages/src/Editor/FirstPartyTools/ControlPlayMode/ControlPlayModeTool.cs
  • Packages/src/Editor/FirstPartyTools/FindGameObjects/FindGameObjectsTool.cs
  • Packages/src/Editor/FirstPartyTools/GetHierarchy/GetHierarchyTool.cs
  • Packages/src/Editor/FirstPartyTools/GetLogs/GetLogsTool.cs
  • Packages/src/Editor/FirstPartyTools/ReplayInput/ReplayInputTool.cs
  • Packages/src/Editor/FirstPartyTools/RunTests/RunTestsTool.cs
  • Packages/src/Editor/FirstPartyTools/SimulateKeyboard/SimulateKeyboardTool.cs
  • Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputTool.cs
  • Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiDragEventExecutor.cs
  • Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiEditorFrameWaiter.cs
  • Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiIncrementalDragExecutor.cs
  • Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiOneShotDragExecutor.cs
  • Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiOverlayAnimator.cs
  • Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiPressActionExecutor.cs
  • Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiSimulationResponseFactory.cs
  • Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiResponse.cs
  • Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiTool.cs
  • Packages/src/Editor/Infrastructure/Api/JsonRpcResponseFactory.cs
  • Packages/src/Editor/Infrastructure/Api/ServerBusyErrorData.cs
  • Packages/src/Editor/ToolContracts/UnityCliLoopTool.cs
  • Packages/src/Runtime/PausePoints/AssemblyInfo.cs
  • docs/glossary.md

Comment on lines +148 to +196
private static void AttachPausePointHit(SimulateMouseUiResponse result)
{
if (result == null)
{
Debug.Assert(false, "result must not be null");
return;
}

UloopPausePointSnapshot? snapshot = UloopPausePointRegistry.GetLatestHitSnapshot();
if (snapshot == null)
{
return;
}

if (!snapshot.IsHit)
{
return;
}

string? snapshotId = snapshot.Id;
if (string.IsNullOrEmpty(snapshotId))
{
return;
}

result.PausePointId = snapshotId;
result.PausePointHitCount = snapshot.HitCount;
result.PausePointHits = CollectPausePointHits();
}

// One input can hit several markers in the same frame; the representative
// PausePointId alone forced agents into extra status calls to find the others.
private static List<UnityCliLoopPausePointHit> CollectPausePointHits()
{
List<UnityCliLoopPausePointHit> hits = new();
foreach (UloopPausePointSnapshot snapshot in UloopPausePointRegistry.GetHitSnapshots())
{
if (!snapshot.IsHit || string.IsNullOrEmpty(snapshot.Id))
{
continue;
}
hits.Add(new UnityCliLoopPausePointHit
{
Id = snapshot.Id,
HitCount = snapshot.HitCount
});
}
return hits;
}

@coderabbitai coderabbitai Bot Jul 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate the registry and inspect snapshot accessors + backing storage for thread-safety.
fd -tf 'UloopPausePointRegistry*.cs' --exec sh -c 'echo "== {} =="; cat -n "{}"'
rg -nP --type=cs -C3 'GetLatestHitSnapshot|GetHitSnapshots|class UloopPausePointRegistry'

Repository: hatayama/unity-cli-loop

Length of output: 11600


🏁 Script executed:

#!/bin/bash
# Inspect the call flow around CreateInterruptedResult and any thread-affinity assumptions.
fd -tf 'MouseUiSimulationResponseFactory.cs' --exec sh -c 'echo "== {} =="; sed -n "1,260p" "{}"'
rg -n --type=cs 'CreateInterruptedResult|ConfigureAwait\(false\)|SwitchToMainThread|GetLatestHitSnapshot|GetHitSnapshots' Packages/src/Editor/FirstPartyTools/SimulateMouseUi Packages/src/Runtime/PausePoints

Repository: hatayama/unity-cli-loop

Length of output: 21962


🏁 Script executed:

#!/bin/bash
# Inspect the awaited helpers and the paused branches that call CreateInterruptedResult.
sed -n '1,220p' Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiOverlayAnimator.cs
printf '\n====\n'
sed -n '1,220p' Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiEditorFrameWaiter.cs
printf '\n====\n'
sed -n '1,240p' Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiPressActionExecutor.cs
printf '\n====\n'
sed -n '1,240p' Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiOneShotDragExecutor.cs

Repository: hatayama/unity-cli-loop

Length of output: 27304


Marshal pause-point snapshot reads back to the main thread CreateInterruptedResult can resume on a non-main-thread continuation here, but UloopPausePointRegistry keeps _latestHitSnapshot and _hitSnapshots as main-thread-only state. Switch threads before calling AttachPausePointHit, or make those registry reads synchronized.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiSimulationResponseFactory.cs`
around lines 148 - 196, Marshal pause-point registry access to the main thread
before invoking AttachPausePointHit from CreateInterruptedResult, ensuring both
GetLatestHitSnapshot and GetHitSnapshots are read on the owning thread. Preserve
the existing response construction and pause-point population behavior while
routing the continuation through the project’s standard main-thread dispatch
mechanism; do not make unsynchronized registry reads from background
continuations.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AttachPausePointHit is only reachable from CreateInterruptedResult, which is only called inside MouseUiFrameWaitOutcome.Paused branches (e.g. MouseUiPressActionExecutor.cs:59-65,77-83,141-147,166-173,195-201, and the equivalent branches in the drag executors) — the branch-to-factory-call path has no further await, it's synchronous.

MouseUiEditorFrameWaiter only produces Paused in two places (MouseUiEditorFrameWaiter.cs:35-38,49-53): the entry check (which runs on whatever thread the previous loop iteration's MainThreadSwitcher.SwitchToMainThread call left us on) or the check immediately after MainThreadSwitcher.SwitchToMainThread. So a Paused return always happens on the main thread.

The ConfigureAwait(false) here only opts the continuation out of being posted to UnitySynchronizationContext — it does not force a thread hop. Since the awaited operation is already on the main thread when it completes (per the above), the continuation runs inline on the main thread, not on a threadpool thread.

On the write side, UloopPausePointRegistry's hit-recording (HitCore) is likewise main-thread-only — the Harmony patch target is Play Mode game code, and off-thread hits are already marshaled to the main thread before reaching the registry (established in an earlier PR in this series).

So reads and writes of _hitSnapshots/_latestHitSnapshot are serialized on the main thread by construction; there's no race here. This is the same invariant the shipped simulate-keyboard/simulate-mouse-input response factories already rely on (KeyboardInputSimulationResponseFactory.AttachPausePointHit follows the identical pattern).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
sed -n '1,80p' Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiEditorFrameWaiter.cs
echo "===MainThreadSwitcher==="
fd -tf 'MainThreadSwitcher*.cs' --exec sh -c 'echo "== {} =="; cat -n "{}"'
echo "===Registry HitCore==="
fd -tf 'UloopPausePointRegistry*.cs' --exec sh -c 'echo "== {} =="; cat -n "{}"'
echo "===Keyboard factory pattern==="
fd -tf 'KeyboardInputSimulationResponseFactory*.cs' --exec sh -c 'echo "== {} =="; cat -n "{}"'

Length of output: 26161


Confirmed — the trace holds up: MouseUiEditorFrameWaiter only returns Paused from the pre-check or right after await MainThreadSwitcher.SwitchToMainThread(ct), both of which are main-thread-only completions, and SwitchToMainThreadAwaitable.Awaiter.OnCompleted inlines the continuation synchronously when already on the main thread. ConfigureAwait(false) on the earlier EditorFrameWaiter.WaitFramesOrTimeoutAsync await doesn't change that, since it only affects whether SynchronizationContext.Post is used, not which thread the continuation runs on. Combined with HitCore being main-thread-only on the write side, _latestHitSnapshot/_hitSnapshots reads and writes are indeed serialized on the main thread here. Withdrawing the concern — thanks for the detailed walkthrough.


🧬 Code Graph Analysis Results

Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiResponse.cs (relevant snippet)

Lines 12-30

public class SimulateMouseUiResponse : UnityCliLoopToolResponse
    {
        public bool Success { get; set; }
        public string Message { get; set; } = "";
        public string Action { get; set; } = "";
        public string? HitGameObjectName { get; set; }
        public float PositionX { get; set; }
        public float PositionY { get; set; }
        public float? EndPositionX { get; set; }
        public float? EndPositionY { get; set; }
        public bool InterruptedByPausePoint { get; set; }
        public string? PausePointId { get; set; }
        public int? PausePointHitCount { get; set; }
        public List<UnityCliLoopPausePointHit>? PausePointHits { get; set; }

        public SimulateMouseUiResponse()
        {
        }
    }

✏️ Learnings added
Learnt from: hatayama
Repo: hatayama/unity-cli-loop PR: 1685
File: Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiSimulationResponseFactory.cs:148-196
Timestamp: 2026-07-11T06:59:48.161Z
Learning: In the unity-cli-loop repo, `MouseUiEditorFrameWaiter.WaitForEditorFrameAndSwitchToMainThreadAsync` (Packages/src/Editor/FirstPartyTools/SimulateMouseUi/MouseUiEditorFrameWaiter.cs) only returns `MouseUiFrameWaitOutcome.Paused` from two points: the entry check before any await, or immediately after `await MainThreadSwitcher.SwitchToMainThread(ct)`. `MainThreadSwitcher.SwitchToMainThreadAwaitable.Awaiter.OnCompleted` (Packages/src/Editor/ToolContracts/MainThreadSwitcher.cs) inlines the continuation synchronously if already on the main thread, so a `Paused` outcome is always observed on the main thread. `ConfigureAwait(false)` on the earlier `EditorFrameWaiter.WaitFramesOrTimeoutAsync` await only opts out of `SynchronizationContext.Post`; it does not force a thread hop, so if that await completes on the main thread the continuation still runs there. Therefore, code paths that reach `MouseUiSimulationResponseFactory.CreateInterruptedResult`/`AttachPausePointHit` (and the analogous `KeyboardInputSimulationResponseFactory.AttachPausePointHit`) from a `Paused` branch are guaranteed to run on the main thread, matching `UloopPausePointRegistry`'s main-thread-only invariant for `_latestHitSnapshot`/`_hitSnapshots` (writes happen in `HitCore`, which is only reached via the main-thread-marshaled Harmony patch path). No synchronization is needed for these registry reads in this pattern.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@hatayama

Copy link
Copy Markdown
Owner Author

Re: the outside-diff comment about propagating ConfigureAwait(false) through CompileUseCase, RunTestsUseCase, and ExecuteDynamicCodeUseCase on UnityCliLoopTool.cs:61:

  • ExecuteDynamicCodeUseCase is a false positive — every await there already uses ConfigureAwait(false) (confirmed at every call site, including the ones that wrap across lines: WarmForegroundExecutionPathIfNeededAsync, ExecuteRequestAsync, RetryMissingReturnIfNeeded, ShouldWaitAsync, TryExecuteIfIdleAsync, RunForegroundSequenceAsync). This is consistent with it being the one tool (alongside the wrapper tools this PR fixed) that already returned correctly under a paused mid-command repro.
  • CompileUseCase.cs:106,154,170 and RunTestsUseCase.cs:95,99,102,154 genuinely have plain awaits and are a real instance of the same bug class. However, mechanically adding ConfigureAwait(false) here is not safe: unlike the simulate-mouse-ui/simulate-keyboard wait loops (which pair every ConfigureAwait(false) with an explicit main-thread switch-back before touching Unity APIs again), these awaits are currently relying on the implicit context capture to resume on the main thread, and the code immediately after them reads main-thread-only APIs (e.g. EditorApplication.isPlaying). A mechanical fix would trade a rare pause-triggered hang for a frequent threadpool-touches-Unity-API bug — it needs a real design change (porting the mouse-ui waiter pattern, or a preflight rejection for run-tests) plus a measured repro, not a find-and-replace.

Filed as #1686 with the affected lines, the mechanism, the "don't just add ConfigureAwait(false)" warning, candidate fixes, and verification requirements. Left the UnityCliLoopTool.ExecuteAsync invariant comment as-is — it's still the correct rule, and this is a known, tracked violation rather than a reason to weaken the documented invariant.

@hatayama hatayama dismissed coderabbitai[bot]’s stale review July 11, 2026 06:59

Both points addressed: (1) replied with the structural main-thread-serialization argument for AttachPausePointHit — see inline reply; (2) CompileUseCase/RunTestsUseCase plain-await gap confirmed valid but tracked as follow-up #1686 with a warning against a mechanical ConfigureAwait(false) fix, since it would trade a rare hang for a frequent Unity-API-off-main-thread bug; ExecuteDynamicCodeUseCase confirmed a false positive. Dismissing to unblock merge; CI is fully green and Fable 5 (final reviewer for this workflow) is doing the human review pass.

@hatayama hatayama changed the title docs(package): Rewrite pause point skill for the source-based workflow fix(package): Stop tools from hanging when a pause point fires mid-command Jul 11, 2026
@hatayama hatayama merged commit 442f9bf into v3-beta Jul 11, 2026
10 checks passed
@hatayama hatayama deleted the docs/rewrite-pause-point-skill branch July 11, 2026 10:30
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