Skip to content

fix: pause-point disconnect and expiry no longer leave Play Mode stuck paused#1754

Merged
hatayama merged 2 commits into
feature/pause-playmode-guardsfrom
feat/pause-point-auto-resume
Jul 13, 2026
Merged

fix: pause-point disconnect and expiry no longer leave Play Mode stuck paused#1754
hatayama merged 2 commits into
feature/pause-playmode-guardsfrom
feat/pause-point-auto-resume

Conversation

@hatayama

@hatayama hatayama commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • Play Mode no longer stays paused forever after a pause-point hit when the agent disconnects, clears markers, or the capture window expires
  • Frame-dependent tools (screenshot / simulate-*) can proceed again after those events instead of timing out on a leftover pause

User Impact

  • Before: A pause-point hit paused Unity without tying that pause to the CLI session. If the agent died or abandoned the session, Unity stayed paused and later tools failed with “PlayMode is paused”
  • After: Clear / ClearAll, capture-window expiry, mid-request CLI disconnect, and bridge DisconnectAllClients all resume Play Mode
  • Trade-off (approved Option B): those triggers also clear a manual Editor pause or control-play-mode Pause if one happens to be active—there is no ownership distinction

Changes

  • Add Resume() on IUloopPausePointPauseController
  • Registry resumes on Clear / ClearAll / newly expired capture windows (including SingleShot hits past ExpiresAtUtc)
  • While paused, Editor update applies capture-window expirations without requiring a CLI poll
  • Mid-request client disconnect monitor and DisconnectAllClients call ResumeEditorPauseForClientDisconnect
  • Short-lived per-command TCP closes do not resume (would break post-await-pause-point inspection)

Out of scope

  • Domain reload residual pause handling (reload normally resets isPaused)

Verification

  • uloop compile — 0 errors / 0 warnings
  • uloop run-tests filter PausePointTests — 53 passed
  • uloop run-tests filter PausePointCaptureModeTests|PausePointToolModeTests — 12 passed

Test plan

  • CI green on this PR
  • Manual: enable pause-point, hit, clear-pause-point → Play Mode resumes
  • Manual: hit, wait past enable timeout with Editor paused → resumes without Clear
  • Manual: while paused after hit, kill an in-flight long request mid-way → resumes
  • Manual: after await-pause-point returns Hit, Unity stays paused until Clear/expiry (short command disconnect must not resume)

Review in cubic

…nnects mid-request

Abandoned pause-point pauses left frame-dependent tools stuck after the agent died. Option B resumes unconditionally on Clear/ClearAll, capture-window expiry (including SingleShot hits), mid-request client disconnect, and bridge DisconnectAllClients—without treating every short-lived command close as a disconnect.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c0d7c660-1ef2-42e8-91e5-65ca27e28208

📥 Commits

Reviewing files that changed from the base of the PR and between bc1233a and 42b4229.

📒 Files selected for processing (16)
  • Assets/Tests/Editor/PausePointCaptureModeTests.cs
  • Assets/Tests/Editor/PausePointTests.cs
  • Assets/Tests/Editor/PausePointToolModeTests.cs
  • Assets/Tests/Editor/SourcePausePointCapture/SourcePausePointCaptureTests.cs
  • Assets/Tests/Editor/SourcePausePointPatcher/SourcePausePointPatcherTests.cs
  • Assets/Tests/PlayMode/KeyboardInputSimulationResponseFactoryTests.cs
  • Assets/Tests/PlayMode/MouseInputSimulationResponseFactoryTests.cs
  • Assets/Tests/PlayMode/SimulateKeyboardTests.cs
  • Assets/Tests/PlayMode/SimulateMouseInputTests.cs
  • Packages/src/Editor/Infrastructure/UnityCliLoopBridgeClientDisconnectMonitor.cs
  • Packages/src/Editor/Infrastructure/UnityCliLoopBridgeClientSessionManager.cs
  • Packages/src/Runtime/PausePoints/IUloopPausePointPauseController.cs
  • Packages/src/Runtime/PausePoints/UloopPausePointEntry.cs
  • Packages/src/Runtime/PausePoints/UloopPausePointRawCaptureLifecycle.cs
  • Packages/src/Runtime/PausePoints/UloopPausePointRegistry.cs
  • Packages/src/Runtime/PausePoints/UnityEditorPausePointPauseController.cs

📝 Walkthrough

Walkthrough

Pause-point handling now resumes the Unity Editor after clears, capture-window expiration, and client disconnects. The runtime adds main-thread processing for pending resume requests and expiration cleanup, while editor and play-mode test doubles implement the new resume contract.

Changes

Pause-point resume lifecycle

Layer / File(s) Summary
Resume controller contract
Packages/src/Runtime/PausePoints/IUloopPausePointPauseController.cs, Packages/src/Runtime/PausePoints/UnityEditorPausePointPauseController.cs
Adds Resume() to the pause controller interface and clears EditorApplication.isPaused in the Unity Editor implementation.
Expiration and registry resume orchestration
Packages/src/Runtime/PausePoints/UloopPausePointEntry.cs, Packages/src/Runtime/PausePoints/UloopPausePointRegistry.cs
Reports whether capture expiration occurred and resumes the editor after clears, expiration, and capture-window processing. Disconnect resume requests use an interlocked pending flag.
Main-thread expiration and disconnect handling
Packages/src/Runtime/PausePoints/UloopPausePointRawCaptureLifecycle.cs, Packages/src/Editor/Infrastructure/UnityCliLoopBridgeClientDisconnectMonitor.cs, Packages/src/Editor/Infrastructure/UnityCliLoopBridgeClientSessionManager.cs
Processes pending resume requests and paused capture expirations from EditorApplication.update; disconnect paths request resume before cancellation or teardown.
Resume behavior test coverage
Assets/Tests/Editor/PausePointTests.cs, Assets/Tests/Editor/*PausePoint*Tests.cs, Assets/Tests/PlayMode/*Tests.cs
Adds clear, expiration, and disconnect assertions and updates test pause controllers to implement resume behavior.

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

Sequence Diagram(s)

sequenceDiagram
  participant ClientDisconnectMonitor
  participant PausePointRegistry
  participant EditorUpdate
  participant UnityEditor
  ClientDisconnectMonitor->>PausePointRegistry: request pending resume
  EditorUpdate->>PausePointRegistry: apply pending resume
  PausePointRegistry->>UnityEditor: Resume()
  UnityEditor-->>PausePointRegistry: clear editor pause state
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.13% 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
Title check ✅ Passed The title clearly summarizes the main change: pause points now resume Play Mode after disconnects and expiry.
Description check ✅ Passed The description matches the PR changes and objectives, covering resume behavior, expiry, disconnects, and test verification.
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.
✨ 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 feat/pause-point-auto-resume

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.

@qua-hatayama

Copy link
Copy Markdown
Contributor

Review (Fable) — 1 required fix

Spec compliance (approved Option B, triggers = mid-request disconnect + Clear/expiry), the ExpireIfNeeded refactor (SingleShot hit windows now expire at ExpiresAtUtc), the paused-only update pump, and the test coverage all look good. One blocker:

Required fix: disconnect-path Resume calls a Unity API off the main thread

ResumeEditorPauseForClientDisconnect() reaches EditorApplication.isPaused = false (main-thread-only API), but both new call sites run off the main thread:

  1. UnityCliLoopBridgeClientDisconnectMonitor.MonitorClientDisconnectAsync — the poll loop runs on the thread pool (request handling needs MainThreadSwitcher.SwitchToMainThread before tool execution, and the await Task.Delay continuations stay on the pool).
  2. UnityCliLoopBridgeClientSessionManager.DisconnectAllClients — one caller is UnityCliLoopBridgeServer.CleanupAfterUnexpectedLoopExit, whose own doc comment says "this runs on the thread pool where Unity APIs are unsafe".

Recommended shape (no new dispatch infra needed): make ResumeEditorPauseForClientDisconnect() only set a thread-safe pending flag (Interlocked/volatile), and consume the flag at the top of the OnEditorUpdate hook this PR already adds — before the if (!EditorApplication.isPaused) return; gate (if not paused, just drop the flag). The Clear/ClearAll/GetStatus/HitCore/expiry paths already run on the main thread (post-SwitchToMainThread or editor update) and can keep calling Resume directly.

Please add two pure tests with the existing fake controller: disconnect sets the flag and the update-side application calls Resume exactly once; a stale flag while unpaused is dropped without Resume.

Observation (no change requested)

While manually paused, an enabled-but-never-hit pause point whose window lapses will now expire via the update pump and clear the manual pause. This is within approved Option B, but it is the one case that can look like a spontaneous resume; if it ever bothers users, restricting expiry-resume to entries that actually Hit is the future knob.

Will LGTM once the thread-boundary fix lands.

Disconnect monitor and DisconnectAllClients run on the thread pool, so
ResumeEditorPauseForClientDisconnect only sets a pending flag; OnEditorUpdate
applies it before the isPaused gate to avoid unsafe EditorApplication access.

Co-authored-by: Cursor <cursoragent@cursor.com>
@qua-hatayama

Copy link
Copy Markdown
Contributor

Addressed the required thread-safety fix:

  • ResumeEditorPauseForClientDisconnect now only sets an Interlocked pending flag (safe from the disconnect monitor / DisconnectAllClients thread-pool paths)
  • OnEditorUpdate consumes the flag via ApplyPendingClientDisconnectResume before the isPaused gate; if already unpaused the flag is discarded
  • Clear / ClearAll / expiry paths still call Resume directly (main thread)

Tests:

  • ResumeEditorPauseForClientDisconnect_WhenPaused_ShouldResumeOnMainThreadApply (flag → apply → Resume once)
  • ApplyPendingClientDisconnectResume_WhenNotPaused_ShouldDiscardPendingFlag

Verification: compile 0/0, PausePointTests 54 passed.

@qua-hatayama

Copy link
Copy Markdown
Contributor

LGTM. Verified commit 42b4229: ResumeEditorPauseForClientDisconnect now only sets an Interlocked pending flag (safe from the thread-pool call sites), OnEditorUpdate consumes it via ApplyPendingClientDisconnectResume before the isPaused gate, a stale flag while unpaused is discarded without calling Resume, and the test-reset path clears the flag. The two requested pure tests are in place (flag → apply resumes exactly once; unpaused apply discards), and the class doc comment correctly documents the new cross-thread entry point. Main-thread paths (Clear/ClearAll/expiry) keep direct Resume. Good to merge into the umbrella branch after CI is green.

@hatayama hatayama merged commit 48c0155 into feature/pause-playmode-guards Jul 13, 2026
2 checks passed
@hatayama hatayama deleted the feat/pause-point-auto-resume branch July 13, 2026 10:20
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.

2 participants