Skip to content

fix: CLI commands recover reliably after Unity reloads#1136

Merged
hatayama merged 8 commits into
v3-betafrom
feature/hatayama/investigate-csharp-server-stability
May 16, 2026
Merged

fix: CLI commands recover reliably after Unity reloads#1136
hatayama merged 8 commits into
v3-betafrom
feature/hatayama/investigate-csharp-server-stability

Conversation

@hatayama
Copy link
Copy Markdown
Owner

@hatayama hatayama commented May 16, 2026

Summary

  • CLI commands now wait on a single recovery state signal during Unity startup, compilation, and domain reload.
  • Stale recovery state now produces actionable guidance instead of leaving commands waiting until timeout.

User Impact

  • Before this change, CLI commands could misread Unity recovery progress, wait too long, or fail to resume cleanly after a domain reload.
  • After this change, reload and startup recovery are tracked consistently, readiness is confirmed before commands continue, and stale state points users toward uloop fix or uloop launch.

Changes

  • Replace scattered recovery checks with a project readiness state file that records starting, compiling, reloading, recovering, ready, stopped, and failed phases.
  • Gate ready state publication on an internal readiness probe that does not depend on user-toggleable tools.
  • Update compile wait, command dispatch, and fix behavior to use the readiness state and avoid stale lock-file waits.
  • Harden atomic state writes and keep recovery-focused tests isolated from the real project Temp state.

Verification

  • Packages/src/Cli~/dist/darwin-arm64/uloop compile --project-path /Users/a12115/ghq/hatayama/unity-cli-loop --wait-for-domain-reload
  • Focused EditMode recovery tests: 29 passed
  • scripts/check-go-cli.sh
  • git diff --check origin/v3-beta..HEAD
  • ~/.codex/skills/codex-review/scripts/codex-review --mode branch v3-beta

Overview

This PR centralizes CLI command recovery after Unity domain reloads by introducing a persistent server readiness state file and a shared recovery signal mechanism. Instead of scattered checks across lock files and ad-hoc waiting, all CLI commands now wait on a single ServerReadinessStateStore that tracks server lifecycle phases atomically.

Architecture & Key Components

Readiness State System

A new ServerReadinessStateStore (C#) persists server state to Temp/UnityCliLoop/server-state.json with the following lifecycle phases:

  • starting - server initialization
  • compiling - compilation in progress
  • reloading - domain reload in progress
  • recovering - recovery after reload
  • ready - server operational
  • stopped - server shut down
  • failed - error occurred

Each state record includes Phase, GenerationId, UpdatedAt, Reason, Endpoint, and LastError fields, written atomically via AtomicFileWriter with .tmp.write.tmp → final-file rotation to ensure consistency across domain reloads.

CLI-Side Recovery (Go)

Go-side code now reads server state from the shared location and implements:

  • isUnityBusyByServerState(): Determines busy status from persisted state instead of filesystem lock-file scanning
  • waitForRecoveringServerIfNeeded(): Waits only when server is in a busy phase (starting/compiling/reloading/recovering/stopping)
  • Stale state detection: Identifies when a busy-state file is stale (no running Unity process) and returns immediate guidance (uloop fix) instead of waiting until timeout

Compilation & Domain Reload Integration

  • CompilationLockFileService: Now records pre-compilation readiness state and restores it after compilation finishes, ensuring no-reload or compiler-error paths don't leave commands stuck
  • DomainReloadDetectionFileService: Persists domain reload lifecycle (before/after transitions) with generation IDs and markers to coordinate recovery across the reload boundary
  • UnityCliLoopServerController: Centralized recovery coordinator that:
    • Manages readiness state transitions during startup, reload, and shutdown
    • Requires readiness probe success before publishing ready state
    • Writes failed state on recovery errors instead of leaving commands in limbo

Readiness Probing

  • New IUnityCliLoopServerReadinessProbe interface enables pluggable readiness checks
  • UnityCliLoopFirstPartyServerLifecycleBinding implements the probe via an internal get-version IPC request (not user-facing execute-dynamic-code) that includes CLI metadata and rejects JSON-RPC error responses
  • Bounded timeout (30s) prevents indefinite probe waits

Command Dispatch Changes

RunProjectLocal() (Go) now conditionally waits for server readiness before tool execution:

  • Skips wait for fix and focus-window commands (safe to run in any state)
  • Waits on readiness state for all other commands

Error Handling Improvements

Two new failure classifications in error envelope:

  1. serverStoppedError: Server stopped before readiness; guidance is to restart bridge
  2. staleServerStateError: Stale recovery state file detected; guidance is to run uloop fix

The fix command now cleans recovery state files (server-state.json and variants) in addition to legacy lock files.

Test Coverage

C# Test Isolation: Tests create isolated ServerReadinessStateStore instances with unique temp directories, preventing cross-test interference:

  • AtomicFileWriterTests: Sidecar file recovery behavior
  • CompilationLockFileServiceTests: Readiness state restoration after compilation
  • DomainReloadDetectionServiceTests: State store integration
  • DomainReloadRecoveryUseCaseTests: Recovery without server instances
  • ProjectIpcWarmupClientTests: JSON-RPC error response validation
  • UnityCliLoopServerControllerStartupLockTests: Readiness probe timeout and success paths
  • UnityCliLoopFirstPartyServerLifecycleBindingTests: CLI metadata in readiness requests

Go Test Coverage:

  • compile_wait_test.go: Server state-based completion vs lock-file clearing
  • error_envelope_test.go: New server-stopped and stale-state classifications
  • fix_test.go: Recovery state cleanup (state file + .tmp + legacy locks)
  • server_state_test.go: (11 tests) State reading, sidecar recovery, busy detection, failure handling, stale detection
  • tool_readiness_test.go: Stopped-phase detection and stale-busy identification

Breaking Changes

  • RestoreServerStateIfNeeded()RestoreServerStateIfNeededAsync() in SessionRecoveryService
  • ExecuteAfterDomainReloadAsync() now async in DomainReloadRecoveryUseCase
  • UnityCliLoopBridgeServerInstanceFactory.ServerStarted / ServerStopping events changed to no-op accessors

Wire Format & Constants

  • New constant UnityCliLoopConstants.SERVER_STATE_FILE_NAME = "server-state.json"
  • New config UnityCliLoopServerConfig.READINESS_PROBE_TIMEOUT_MS = 30000 (30 seconds)
  • State JSON schema: { phase, generationId, updatedAt, reason, endpoint, lastError }

Documentation

Updated SKILL.md troubleshooting to distinguish:

  • Stale recovery state (Editor running, CLI sees stale state) → uloop fix
  • Stale lock files (leftover temp artifacts) → cleanup via uloop fix

Review Change Stack

hatayama added 7 commits May 16, 2026 11:40
Persist server readiness through a shared state file so CLI callers can distinguish startup, reload, recovery, ready, stopped, and failed states instead of relying on stale lock files.

- centralize domain reload recovery behind tracked tasks and readiness probes
- make CLI preflight and compile waits honor recovery state without infinite waits
- keep recovery tests isolated from the real project IPC server
Ensure the internal project IPC readiness probe uses the same CLI metadata contract as real native CLI requests, rejects JSON-RPC error responses, and fails with a bounded timeout instead of leaving startup or recovery state stuck. Also let the native CLI read atomic state sidecars so it does not dispatch through a write or crash recovery window.
Use the internal get-version bridge command for server readiness so startup and recovery do not fail when execute-dynamic-code is disabled by project settings. Also publish stopped state after domain reloads that have no server to recover, preventing CLI waiters from observing a permanent recovering phase.
Keep compile-finish handling from publishing ready before the server readiness probe succeeds, and make recovery waits exit promptly when the server transitions to stopped.
Capture the pre-compile readiness snapshot so compiler-error or no-reload compiles do not leave CLI waiters stuck in the compiling phase, while startup and reload paths keep ownership of ready publication.
Return immediate recovery guidance when a busy server-state file remains after the target Unity process is gone, so normal CLI commands do not wait for the full readiness timeout.
Write atomic files through an in-progress sidecar before publishing the completed temp file so external CLI recovery never reads a partially written state. Isolate domain reload tests from the real project readiness state to avoid polluting local CLI readiness between test runs.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 16, 2026

Warning

Rate limit exceeded

@hatayama has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 23 minutes and 18 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c107128d-d216-497a-87af-e0a938222e0a

📥 Commits

Reviewing files that changed from the base of the PR and between 71983f5 and a0854a0.

⛔ Files ignored due to path filters (4)
  • Assets/Tests/Editor/ServerReadinessStateStoreTests.cs.meta is excluded by none and included by none
  • Packages/src/Cli~/dist/darwin-amd64/uloop is excluded by !**/dist/** and included by none
  • Packages/src/Cli~/dist/darwin-arm64/uloop is excluded by !**/dist/** and included by none
  • Packages/src/Cli~/dist/windows-amd64/uloop.exe is excluded by !**/dist/**, !**/*.exe and included by none
📒 Files selected for processing (12)
  • Assets/Tests/Editor/DomainReloadRecoveryUseCaseTests.cs
  • Assets/Tests/Editor/ServerReadinessStateStoreTests.cs
  • Packages/src/Cli~/internal/cli/compile_wait_test.go
  • Packages/src/Cli~/internal/cli/fix.go
  • Packages/src/Cli~/internal/cli/fix_test.go
  • Packages/src/Cli~/internal/cli/server_state.go
  • Packages/src/Cli~/internal/cli/tool_readiness.go
  • Packages/src/Cli~/internal/cli/tool_readiness_test.go
  • Packages/src/Editor/Application/SessionRecoveryService.cs
  • Packages/src/Editor/CompositionRoot/UnityCliLoopFirstPartyServerLifecycleBinding.cs
  • Packages/src/Editor/Infrastructure/Server/ServerReadinessStateStore.cs
  • Packages/src/Editor/Infrastructure/Settings/AtomicFileWriter.cs
📝 Walkthrough

Walkthrough

This PR adds comprehensive server readiness state tracking and lifecycle management to the Unity CLI Loop system. It persists server phase transitions (Starting, Ready, Compiling, Reloading, Recovering, Stopping, Stopped, Failed) to a shared JSON file with sidecar recovery, introduces readiness probing via IPC before marking servers ready, and replaces lock-file–based busy detection with state-driven readiness waits on both C# and Go sides.

Changes

Server Readiness State Infrastructure

Layer / File(s) Summary
Readiness state data model and store
Packages/src/Editor/Infrastructure/Server/ServerReadinessStateStore.cs
ServerReadinessPhase enum, ServerReadinessState DTO with JSON properties, and ServerReadinessStateStore that reads/writes state from Temp/UnityCliLoop/server-state.json with .tmp/.bak sidecar recovery via AtomicFileWriter.
Atomic file write with temp sidecar handling
Packages/src/Editor/Infrastructure/Settings/AtomicFileWriter.cs
Write path now uses in-progress .tmp.write temp file before rotation through completed .tmp, with explicit cleanup and shared suffix constants for sidecar recovery.
Readiness configuration and constants
Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs, Packages/src/Editor/ToolContracts/UnityCliLoopServerConfig.cs
New constants SERVER_STATE_FILE_NAME and READINESS_PROBE_TIMEOUT_MS added to shared configs.

CLI Server State Reading & Classification

Layer / File(s) Summary
Server state reading and recovery
Packages/src/Cli~/internal/cli/server_state.go
New module reads persisted server state from Temp/UnityCliLoop/server-state.json with fallback to .tmp and .bak sidecars (ignoring .tmp.write), classifies phases as busy or stopped, and defines error types for stopped/stale server states.
Readiness waiting with recovery check
Packages/src/Cli~/internal/cli/server_state.go
waitForRecoveringServerIfNeeded reads state, skips wait for ready/missing/non-busy states, and delegates to readiness callback when server is busy.

Readiness Probe Interface & First-Party Implementation

Layer / File(s) Summary
Readiness probe interface contract
Packages/src/Editor/Application/UnityCliLoopServerReadinessProbe.cs
New IUnityCliLoopServerReadinessProbe interface defines async ProbeAsync(CancellationToken) method for verifying project IPC responsiveness.
First-party readiness probe implementation
Packages/src/Editor/CompositionRoot/UnityCliLoopFirstPartyServerLifecycleBinding.cs
Refactored to implement IUnityCliLoopServerReadinessProbe via constructor-injected ProjectIpcWarmupClient, sending "get-version" JSON-RPC request with CLI version instead of execute-dynamic-code probes.
JSON-RPC response validation in warmup client
Packages/src/Editor/Infrastructure/ProjectIpcWarmupClient.cs
Response validation added via new ValidateJsonRpcSuccessResponse method that throws on JSON-RPC error fields or missing results, returning captured response payload.

Server Controller Lifecycle & Readiness Integration

Layer / File(s) Summary
Controller constructor and state dependencies
Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs
Constructor now accepts ServerReadinessStateStore and IUnityCliLoopServerReadinessProbe with null assertions.
Lifecycle event subscription and publishing
Packages/src/Editor/Application/UnityCliLoopServerApplicationService.cs, Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs
Added PublishServerStarted() and PublishServerStopping() methods; controller explicitly manages editor lifecycle subscriptions and unsubscriptions.
Manual start/stop readiness state transitions
Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs
Start writes Starting → probes readiness → writes Ready or Failed; stop writes StoppingStopped or Failed on completion.
Assembly reload and domain-reload recovery
Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs
Pre-reload writes Reloading state; post-reload schedules tracked async recovery task, writes failed state on exception, throws on failure.
Server restoration and startup-protection flows
Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs
RestoreServerStateIfNeeded async early-return when bridge running; StartRecoveryIfNeededAsync respects startup protection (mark ready if running, fail if protected but missing), writes Recovering or Starting state.
Readiness probing with timeout and state persistence
Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs
MarkServerReadyAsync probes with timeout, writes Ready state and publishes ServerStarted; WriteServerState persists phase/reason/error/endpoint.
Editor quitting and error-case state writes
Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs
On quit writes StoppingStopped; bind failures write detailed Failed state with message and throw.

Compilation & Domain Reload State Tracking

Layer / File(s) Summary
Compilation lock service with state restoration
Packages/src/Editor/Infrastructure/Server/CompilationLockFileService.cs
Stores ServerReadinessStateStore dependency; on compile start saves prior state and generation id, writes Compiling state; on finish asynchronously restores prior state if generation and phase match.
Domain reload detection with state markers
Packages/src/Editor/Infrastructure/Server/DomainReloadDetectionFileService.cs
Accepts optional ServerReadinessStateStore; instance beforeAssemblyReload writes Reloading state with marker; StartDomainReload and CompleteDomainReload write corresponding state transitions; RollbackDomainReloadStart writes Failed state.
Session recovery async refactor
Packages/src/Editor/Application/SessionRecoveryService.cs, Packages/src/Editor/Application/UseCases/DomainReloadRecoveryUseCase.cs
RestoreServerStateIfNeeded becomes async RestoreServerStateIfNeededAsync; domain-reload use-case now awaits it directly instead of wrapping in Task.FromResult.

CLI Compile-Wait & Recovery Integration

Layer / File(s) Summary
Compile-wait state-driven busy checking
Packages/src/Cli~/internal/cli/compile_wait.go
Switches from lock-file–based isUnityBusyByCompileLocks to state-based isUnityBusyByServerState, reading persisted server state to determine busy status.
Tool readiness wait mode and stale-busy detection
Packages/src/Cli~/internal/cli/tool_readiness.go
Refactors readiness loop to be state-driven; introduces toolReadinessWaitMode to control stopped-server behavior and isBusyServerStateStale to detect stale-busy by checking for running Unity process.
Conditional readiness wait before command dispatch
Packages/src/Cli~/internal/cli/run.go
RunProjectLocal adds conditional readiness wait before most commands via shouldWaitForServerReadinessBeforeCommand, skipping wait for "fix" and "focus-window".
Recovery state cleanup in fix command
Packages/src/Cli~/internal/cli/fix.go
runFix invokes cleanupStaleRecoveryState to remove server-state files and variants plus legacy lock files.

Error Handling & Server Readiness Failures

Layer / File(s) Summary
Server stopped and stale state error classification
Packages/src/Cli~/internal/cli/error_envelope.go
classifyError detects serverStoppedError and staleServerStateError, returning UNITY_NOT_REACHABLE with retry flags and tailored NextActions (restart for stopped, run uloop fix for stale).
Command registry and troubleshooting docs update
Packages/src/Cli~/internal/cli/command_registry.go, Packages/src/Editor/FirstPartyTools/Compile/Skill/SKILL.md
fix description updated to reference "recovery state"; SKILL.md split into distinct stale-recovery-state and stale-lock-files subsections.

Composition Root & Wiring

Layer / File(s) Summary
Application registration with state store and probe
Packages/src/Editor/CompositionRoot/UnityCliLoopApplicationRegistration.cs
Creates ServerReadinessStateStore from project root and serverReadinessProbe from ProjectIpcWarmupClient, passing them to domain-reload, compilation, and controller services.
Bootstrapper and first-party startup simplification
Packages/src/Editor/CompositionRoot/UnityCliLoopEditorBootstrapper.cs
Removes direct creation and initialization of UnityCliLoopFirstPartyServerLifecycleBinding; probe is now wired via composition root.
Bridge server event accessor and shutdown refactor
Packages/src/Editor/Infrastructure/UnityCliLoopBridgeServer.cs
ServerStarted/ServerStopping change from auto-events to explicit empty add/remove accessors; StopServer and Dispose use atomic-exchange helper for cancellation-token cleanup instead of direct disposal.
Test infrastructure assembly visibility
Packages/src/Editor/CompositionRoot/AssemblyInfo.cs
Adds InternalsVisibleTo("uLoopMCP.Tests.Editor") to allow test access to internal members.

Test Coverage

Layer / File(s) Summary
Atomic file writer sidecar recovery tests
Assets/Tests/Editor/AtomicFileWriterTests.cs
Verifies in-progress .tmp.write sidecars are not promoted during recovery, and that completed writes remove sidecar and publish target.
Compilation lock service readiness tests
Assets/Tests/Editor/CompilationLockFileServiceTests.cs
Verifies MarkCompilationFinished restores prior phase when phase matches, preserving state across compilation cycles.
Domain reload detection and recovery tests
Assets/Tests/Editor/DomainReloadDetectionServiceTests.cs, Assets/Tests/Editor/DomainReloadRecoveryUseCaseTests.cs
Integrate ServerReadinessStateStore, verify state transitions, test lightweight TestServerInstance.
Server controller startup lock and protection tests
Assets/Tests/Editor/UnityCliLoopServerControllerStartupLockTests.cs, Assets/Tests/Editor/UnityCliLoopServerStartupProtectionTests.cs
Inject ServerReadinessStateStore and TestReadinessProbe, verify state publication and timeout behavior.
First-party readiness probe test
Assets/Tests/Editor/UnityCliLoopFirstPartyServerLifecycleBindingTests.cs
Verifies CreateGetVersionReadinessRequestJson generates correct method and CLI version.
Project IPC warmup client validation tests
Assets/Tests/Editor/ProjectIpcWarmupClientTests.cs
Covers ValidateJsonRpcSuccessResponse on JSON-RPC error and success responses.
CLI server state reading and classification tests
Packages/src/Cli~/internal/cli/server_state_test.go
Covers missing-file handling, failure-error embedding, phase classification, sidecar recovery, and waitForRecoveringServerIfNeeded control flow.
CLI compile-wait and recovery state tests
Packages/src/Cli~/internal/cli/compile_wait_test.go
Replaces lock-based test with state-based completion test; adds failed-state early return test; provides writeServerStateForTest helper.
CLI error classification and readiness failure tests
Packages/src/Cli~/internal/cli/error_envelope_test.go
Covers serverStoppedError and staleServerStateError classification with retry flags and NextActions guidance.
CLI recovery state cleanup test
Packages/src/Cli~/internal/cli/fix_test.go
Tests cleanupStaleRecoveryState removing server-state files and legacy locks.
CLI tool readiness timeout and stale-state tests
Packages/src/Cli~/internal/cli/tool_readiness_test.go
Tests stopped-state detection and stale-busy state when Unity process is missing.

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • hatayama/unity-cli-loop#1027: Main PR replaces compile-wait/lock-based busy checks with server-state + readiness-probe logic; retrieved PR focuses on making uloop launch/compile waits finish reliably via the same state-driven approach.
  • hatayama/unity-cli-loop#1121: Both PRs modify the domain-reload recovery/shutdown flow—main PR refactors DomainReloadRecoveryUseCase and adds scoped-service reset, while retrieved PR focuses on bridge server's pre-domain-reload behavior and tests it.
  • hatayama/unity-cli-loop#1079: Both PRs modify domain-reload recovery/testing wiring for DomainReloadDetectionServiceTests by adding ServerReadinessStateStore dependency and updating test fixture setup.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/hatayama/investigate-csharp-server-stability

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 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/Cli`~/internal/cli/compile_wait_test.go:
- Around line 104-107: The goroutine in the test discards errors from
writeServerStateForTest, causing flakiness; change it to send the error back to
the test and fail deterministically. Replace the anonymous goroutine with one
that writes the result of writeServerStateForTest into a channel (or uses
sync/errgroup), then in the main test goroutine read the channel and call
t.Fatalf/t.Fatal if an error is returned; keep the same 20ms sleep and the call
to writeServerStateForTest so you only add error propagation around that call.

In `@Packages/src/Cli`~/internal/cli/tool_readiness.go:
- Around line 58-59: The call sites pass the parent ctx into
isBusyServerStateStale which can outlive the readiness timeout; change those
calls to use the timeout-scoped context (timeoutContext) instead of ctx so the
probe I/O is bounded. Locate the calls to isBusyServerStateStale (e.g., the
invocation currently using ctx at the first occurrence and the similar call
around the 86-88 region) and replace the ctx argument with timeoutContext,
ensuring timeoutContext is in scope where used.

In `@Packages/src/Editor/Application/SessionRecoveryService.cs`:
- Around line 74-79: The method currently awaits
_recoveryCoordinator.StartRecoveryIfNeededAsync but then unconditionally returns
ValidationResult.Success(); instead re-check _recoveryCoordinator.CurrentServer
(or the coordinator's state) after the await and, if CurrentServer is null or
!CurrentServer.IsRunning, return a failed ValidationResult (with an explanatory
message) rather than Success; update ExecuteAfterDomainReloadAsync to inspect
_recoveryCoordinator.CurrentServer after calling StartRecoveryIfNeededAsync and
convert the non-running outcome into ValidationResult.Failure.

In
`@Packages/src/Editor/CompositionRoot/UnityCliLoopFirstPartyServerLifecycleBinding.cs`:
- Line 15: Update the stale XML summary on the
UnityCliLoopFirstPartyServerLifecycleBinding class: replace the phrase
"execute-dynamic-code readiness" with the correct probe description "get-version
readiness" (or similar wording referencing the get-version probe) so the
class-level XML comment matches the current probing behavior.

In `@Packages/src/Editor/Infrastructure/Server/ServerReadinessStateStore.cs`:
- Around line 136-144: Delete() currently removes only the main file and the
".bak" sidecar, leaving ".tmp" / ".tmp.write" files that RecoverSidecarFiles in
Read() can promote and resurrect deleted state; update Delete() (the method
named Delete in ServerReadinessStateStore) to also remove the temp sidecars by
invoking the AtomicFileWriter cleanup for temp files (or explicitly File.Delete
_stateFilePath + ".tmp" and _stateFilePath + ".tmp.write" if no cleanup helper
exists) so both ".tmp" and ".tmp.write" are removed alongside the ".bak".

In `@Packages/src/Editor/Infrastructure/UnityCliLoopBridgeServer.cs`:
- Around line 24-34: UnityCliLoopBridgeServer's ServerStarted and ServerStopping
accessors are empty so subscriptions are dropped; implement the add/remove to
forward handlers to the actual runtime server (e.g., call
underlyingServer.ServerStarted += value and underlyingServer.ServerStopping +=
value in add, and -= in remove) or, if the underlying server may be null when
subscribers attach, maintain a private delegate/list (e.g.,
_pendingServerStarted/_pendingServerStopping) and attach those when the runtime
server is assigned; reference the UnityCliLoopBridgeServer class and its
ServerStarted/ServerStopping events (or refactor
UnityCliLoopServerApplicationService to subscribe directly to the concrete
server instance exposed by IUnityCliLoopServerLifecycleSource) so subscriptions
are not silently ignored.
🪄 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: c73e171b-8d2c-4945-9a28-6b674e346566

📥 Commits

Reviewing files that changed from the base of the PR and between d9fc0e7 and 71983f5.

⛔ Files ignored due to path filters (9)
  • Assets/Tests/Editor/AtomicFileWriterTests.cs.meta is excluded by none and included by none
  • Assets/Tests/Editor/CompilationLockFileServiceTests.cs.meta is excluded by none and included by none
  • Assets/Tests/Editor/UnityCliLoopFirstPartyServerLifecycleBindingTests.cs.meta is excluded by none and included by none
  • Packages/src/Cli~/dist/darwin-amd64/uloop is excluded by !**/dist/** and included by none
  • Packages/src/Cli~/dist/darwin-arm64/uloop is excluded by !**/dist/** and included by none
  • Packages/src/Cli~/dist/windows-amd64/uloop.exe is excluded by !**/dist/**, !**/*.exe and included by none
  • Packages/src/Editor/Application/UnityCliLoopServerReadinessProbe.cs.meta is excluded by none and included by none
  • Packages/src/Editor/CompositionRoot/AssemblyInfo.cs.meta is excluded by none and included by none
  • Packages/src/Editor/Infrastructure/Server/ServerReadinessStateStore.cs.meta is excluded by none and included by none
📒 Files selected for processing (38)
  • Assets/Tests/Editor/AtomicFileWriterTests.cs
  • Assets/Tests/Editor/CompilationLockFileServiceTests.cs
  • Assets/Tests/Editor/DomainReloadDetectionServiceTests.cs
  • Assets/Tests/Editor/DomainReloadRecoveryUseCaseTests.cs
  • Assets/Tests/Editor/ProjectIpcWarmupClientTests.cs
  • Assets/Tests/Editor/UnityCliLoopFirstPartyServerLifecycleBindingTests.cs
  • Assets/Tests/Editor/UnityCliLoopServerControllerStartupLockTests.cs
  • Assets/Tests/Editor/UnityCliLoopServerStartupProtectionTests.cs
  • Packages/src/Cli~/internal/cli/command_registry.go
  • Packages/src/Cli~/internal/cli/compile_wait.go
  • Packages/src/Cli~/internal/cli/compile_wait_test.go
  • Packages/src/Cli~/internal/cli/error_envelope.go
  • Packages/src/Cli~/internal/cli/error_envelope_test.go
  • Packages/src/Cli~/internal/cli/fix.go
  • Packages/src/Cli~/internal/cli/fix_test.go
  • Packages/src/Cli~/internal/cli/run.go
  • Packages/src/Cli~/internal/cli/server_state.go
  • Packages/src/Cli~/internal/cli/server_state_test.go
  • Packages/src/Cli~/internal/cli/tool_readiness.go
  • Packages/src/Cli~/internal/cli/tool_readiness_test.go
  • Packages/src/Editor/Application/SessionRecoveryService.cs
  • Packages/src/Editor/Application/UnityCliLoopServerApplicationService.cs
  • Packages/src/Editor/Application/UnityCliLoopServerReadinessProbe.cs
  • Packages/src/Editor/Application/UseCases/DomainReloadRecoveryUseCase.cs
  • Packages/src/Editor/CompositionRoot/AssemblyInfo.cs
  • Packages/src/Editor/CompositionRoot/UnityCliLoopApplicationRegistration.cs
  • Packages/src/Editor/CompositionRoot/UnityCliLoopEditorBootstrapper.cs
  • Packages/src/Editor/CompositionRoot/UnityCliLoopFirstPartyServerLifecycleBinding.cs
  • Packages/src/Editor/FirstPartyTools/Compile/Skill/SKILL.md
  • Packages/src/Editor/Infrastructure/ProjectIpcWarmupClient.cs
  • Packages/src/Editor/Infrastructure/Server/CompilationLockFileService.cs
  • Packages/src/Editor/Infrastructure/Server/DomainReloadDetectionFileService.cs
  • Packages/src/Editor/Infrastructure/Server/ServerReadinessStateStore.cs
  • Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs
  • Packages/src/Editor/Infrastructure/Settings/AtomicFileWriter.cs
  • Packages/src/Editor/Infrastructure/UnityCliLoopBridgeServer.cs
  • Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs
  • Packages/src/Editor/ToolContracts/UnityCliLoopServerConfig.cs
💤 Files with no reviewable changes (1)
  • Packages/src/Editor/CompositionRoot/UnityCliLoopEditorBootstrapper.cs

Comment thread Packages/src/Cli~/internal/cli/compile_wait_test.go
Comment thread Packages/src/Cli~/internal/cli/tool_readiness.go Outdated
Comment thread Packages/src/Editor/Application/SessionRecoveryService.cs
Comment on lines +24 to +34
public event Action ServerStarted
{
add { }
remove { }
}

public event Action ServerStopping
{
add { }
remove { }
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 16, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for any code subscribing to ServerStarted/ServerStopping on the factory type
rg -nP --type=cs '\bUnityCliLoopBridgeServerInstanceFactory\b' -A 10 | rg -P '\.(ServerStarted|ServerStopping)\s*\+='
rg -nP --type=cs 'IUnityCliLoopServerLifecycleSource' -A 10 | rg -P '\.(ServerStarted|ServerStopping)\s*\+='

Repository: hatayama/unity-cli-loop

Length of output: 349


Remove empty event accessors or ensure subscriptions are rerouted to the functional server instance.

External code in UnityCliLoopServerApplicationService.cs (lines 202, 207) subscribes to ServerStarted and ServerStopping events on the factory via IUnityCliLoopServerLifecycleSource. With empty accessors, these subscriptions are silently ignored and the handlers will never fire—breaking the application service's event handling logic. Either implement the event accessors to forward to the underlying server instance, or refactor the application service to subscribe to the server instance directly instead of the factory.

🤖 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/Infrastructure/UnityCliLoopBridgeServer.cs` around lines
24 - 34, UnityCliLoopBridgeServer's ServerStarted and ServerStopping accessors
are empty so subscriptions are dropped; implement the add/remove to forward
handlers to the actual runtime server (e.g., call underlyingServer.ServerStarted
+= value and underlyingServer.ServerStopping += value in add, and -= in remove)
or, if the underlying server may be null when subscribers attach, maintain a
private delegate/list (e.g., _pendingServerStarted/_pendingServerStopping) and
attach those when the runtime server is assigned; reference the
UnityCliLoopBridgeServer class and its ServerStarted/ServerStopping events (or
refactor UnityCliLoopServerApplicationService to subscribe directly to the
concrete server instance exposed by IUnityCliLoopServerLifecycleSource) so
subscriptions are not silently ignored.

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.

Not changed in a0854a0f.

I do not want to forward these raw factory events in this PR because the controller intentionally publishes server lifecycle after the readiness probe has passed. Forwarding the bridge instance event directly here would risk marking the server as started before the external CLI readiness contract is true.

A safer follow-up would be to split the loop-exit source contract from the ready-after-probe lifecycle publisher, but direct forwarding would weaken the recovery/readiness model this PR is trying to stabilize.

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.

@hatayama, understood — the empty accessors are an intentional guard. Since UnityCliLoopServerControllerService is the authoritative publisher of ServerStarted/ServerStopping (gated on the readiness probe), forwarding the raw bridge instance events from the factory would violate the readiness contract this PR is stabilizing. The empty accessors are the correct way to satisfy the IUnityCliLoopServerLifecycleSource interface without leaking premature signals.

I'll record this design intent and withdraw the concern. Would you like me to open a follow-up issue to track the proposed split of the loop-exit source contract from the probe-gated lifecycle publisher?


✏️ Learnings added
Learnt from: hatayama
Repo: hatayama/unity-cli-loop PR: 1136
File: Packages/src/Editor/Infrastructure/UnityCliLoopBridgeServer.cs:24-34
Timestamp: 2026-05-16T06:00:48.765Z
Learning: In `Packages/src/Editor/Infrastructure/UnityCliLoopBridgeServer.cs`, the `ServerStarted` and `ServerStopping` events on `UnityCliLoopBridgeServerInstanceFactory` intentionally have empty `add`/`remove` accessors. This is by design: `UnityCliLoopServerControllerService` is the authoritative publisher of these lifecycle events, and it gates publication on a readiness probe passing. Forwarding the raw bridge-instance events from the factory would bypass probe-gating and mark the server as started before the external CLI readiness contract is satisfied. Do not flag these empty accessors as a bug.

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.


🧠 Learnings used
Learnt from: hatayama
Repo: hatayama/uLoopMCP PR: 662
File: Packages/src/Editor/UI/McpEditorWindow.cs:229-238
Timestamp: 2026-02-22T13:32:37.251Z
Learning: In C# codebases (e.g., Packages/src/Editor/UI/McpEditorWindow.cs), apply a Fail Fast policy: avoid broad defensive try-catch for unexpected exceptions. Only catch expected, domain-specific exceptions (e.g., Win32Exception for missing executables) at the appropriate layer, and let genuine bugs surface rather than being silently swallowed. This improves visibility of defects and maintains early failure behavior across the codebase.

Learnt from: hatayama
Repo: hatayama/uLoopMCP PR: 662
File: Packages/src/Editor/UI/McpEditorWindow.cs:599-683
Timestamp: 2026-02-22T13:32:34.654Z
Learning: In C# code within the uLoopMCP repository, apply the policy that async void methods should avoid try-catch for control flow and resource release. Use try-finally blocks to ensure resource cleanup (e.g., resetting state flags, refreshing UI) and allow exceptions to propagate (Fail Fast) to surface unexpected errors immediately. This guideline should be followed for files under Packages/src/Editor/**/*.cs and, where applicable, similar async void methods in this project.

Learnt from: hatayama
Repo: hatayama/uLoopMCP PR: 698
File: Packages/src/Editor/Config/ToolSettings.cs:41-42
Timestamp: 2026-02-28T15:10:41.524Z
Learning: In Unity Editor C# code (e.g., ToolSettings.cs, ULoopSettings.cs), use Debug.Assert for internal invariants and preconditions as part of Contract Programming and Fail Fast. These assertions validate internal contracts and are expected to halt in Unity Editor during development builds; do not replace them with runtime exceptions for external input validation. Apply this guidance to editor-related C# files under Packages/src/Editor and similar internal code paths.

Learnt from: hatayama
Repo: hatayama/uLoopMCP PR: 698
File: Packages/src/Editor/Config/ToolSkillSynchronizer.cs:115-144
Timestamp: 2026-02-28T15:10:54.346Z
Learning: In uLoopMCP's Editor code, background tasks triggered by UI (e.g., ToolSkillSynchronizer.RunSkillsInstall) should emit Debug.LogWarning on failures instead of throwing exceptions to avoid crashing the Editor window. This is an intentional exception to the general Fail Fast policy, applied specifically to non-critical background tasks. Apply this pattern to editor-related C# files under the Packages/src/Editor directory. When failures occur, log a warning with contextual information (task name, identifiers) instead of propagating exceptions; use try-catch if needed to convert errors to log warnings.

Learnt from: hatayama
Repo: hatayama/unity-cli-loop PR: 806
File: Packages/src/Runtime/SimulateMouseInput/SimulateMouseInputOverlay.cs:131-145
Timestamp: 2026-03-19T13:09:52.378Z
Learning: In the hatayama/unity-cli-loop repository (and related uLoopMCP), follow the contract-programming convention documented in CLAUDE.md: use Debug.Assert to document and enforce programmer preconditions (fail-fast during development). Treat null/invalid inputs caused by incorrect usage as programmer errors, not recoverable runtime conditions. Do not recommend replacing Debug.Assert precondition checks with throw/InvalidOperationException (or similar explicit exceptions) when the intent is to fail-fast via assert in development builds.

Learnt from: hatayama
Repo: hatayama/unity-cli-loop PR: 887
File: AGENTS.md:5-5
Timestamp: 2026-04-04T11:51:37.434Z
Learning: In Unity projects, directory names that intentionally end with `~` (e.g., `TypeScriptServer~`, `Documentation~`) should be treated as valid. Unity’s asset pipeline ignores folders/files ending with `~`, so code/docs should not be flagged for using trailing tildes in Unity folder/directory names.

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.

No additional code change is needed beyond a0854a0f.

Thanks for confirming the design intent. I do not need a follow-up issue from this PR right now because the behavior is already intentional: lifecycle publication remains owned by UnityCliLoopServerControllerService after readiness probing, while the factory only forwards ServerLoopExited.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 47 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/src/Cli~/internal/cli/tool_readiness.go">

<violation number="1" location="Packages/src/Cli~/internal/cli/tool_readiness.go:58">
P2: Use the readiness timeout context for stale-state checks; passing the parent context can bypass the 180s readiness deadline during process lookup.</violation>
</file>

<file name="Packages/src/Editor/Infrastructure/Server/ServerReadinessStateStore.cs">

<violation number="1" location="Packages/src/Editor/Infrastructure/Server/ServerReadinessStateStore.cs:132">
P2: Handle file read/parse failures in `Read()` so a corrupted readiness file does not crash recovery flow.</violation>

<violation number="2" location="Packages/src/Editor/Infrastructure/Server/ServerReadinessStateStore.cs:140">
P2: Treat state-file deletion as best-effort by catching lock/permission exceptions during cleanup.

(Based on your team's feedback about best-effort temp cleanup during shutdown/reload.) [FEEDBACK_USED]</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Re-trigger cubic

Comment thread Packages/src/Cli~/internal/cli/tool_readiness.go Outdated
{
if (File.Exists(_stateFilePath))
{
File.Delete(_stateFilePath);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Treat state-file deletion as best-effort by catching lock/permission exceptions during cleanup.

(Based on your team's feedback about best-effort temp cleanup during shutdown/reload.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/src/Editor/Infrastructure/Server/ServerReadinessStateStore.cs, line 140:

<comment>Treat state-file deletion as best-effort by catching lock/permission exceptions during cleanup.

(Based on your team's feedback about best-effort temp cleanup during shutdown/reload.) </comment>

<file context>
@@ -0,0 +1,156 @@
+        {
+            if (File.Exists(_stateFilePath))
+            {
+                File.Delete(_stateFilePath);
+            }
+
</file context>
Suggested change
File.Delete(_stateFilePath);
try
{
File.Delete(_stateFilePath);
}
catch (IOException)
{
}
catch (UnauthorizedAccessException)
{
}

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.

Not changed in a0854a0f.

Cleanup remains fail-fast here. Swallowing IOException or UnauthorizedAccessException would hide the exact condition that prevents the CLI/server from clearing stale readiness state. The applied fix removes all recoverable sidecars while still surfacing real filesystem failures clearly.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for the feedback! I've saved this as a new learning to improve future reviews.

return null;
}

string content = File.ReadAllText(_stateFilePath);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Handle file read/parse failures in Read() so a corrupted readiness file does not crash recovery flow.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/src/Editor/Infrastructure/Server/ServerReadinessStateStore.cs, line 132:

<comment>Handle file read/parse failures in `Read()` so a corrupted readiness file does not crash recovery flow.</comment>

<file context>
@@ -0,0 +1,156 @@
+                return null;
+            }
+
+            string content = File.ReadAllText(_stateFilePath);
+            return JsonConvert.DeserializeObject<ServerReadinessState>(content);
+        }
</file context>

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.

Not changed in a0854a0f.

I am keeping corrupted readiness state as a visible failure instead of treating it as absent. The CLI side already reports unreadable state explicitly, and a C# self-healing path should be designed as an explicit result/logging flow rather than catch-and-continue in Read().

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for the feedback! I already have a similar learning that covers this, so I'm keeping the existing one.

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.

No additional code change is needed beyond a0854a0f.

Keeping this as-is intentionally preserves the fail-fast behavior for corrupted readiness state. If we later want self-healing here, I would treat it as a separate design change with an explicit result/logging path rather than catch-and-continue inside Read().

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for the feedback! I've saved this as a new learning to improve future reviews.

Tighten recovery reporting and state cleanup so CLI waiters do not treat incomplete recovery as success or resurrect deleted readiness state from atomic-write sidecars.

- fail session recovery when no running server exists after recovery
- remove all server-state sidecars from cleanup paths
- bound stale-state process lookup by the readiness timeout context
@hatayama hatayama merged commit 7e45f1e into v3-beta May 16, 2026
50 checks passed
@hatayama hatayama deleted the feature/hatayama/investigate-csharp-server-stability branch May 16, 2026 06:21
@github-actions github-actions Bot mentioned this pull request May 16, 2026
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