Skip to content

fix(transport): debounce orphan-session teardown so server restarts don't strand sessions#155

Open
dsarno wants to merge 26 commits into
betafrom
fix/orphan-session-teardown
Open

fix(transport): debounce orphan-session teardown so server restarts don't strand sessions#155
dsarno wants to merge 26 commits into
betafrom
fix/orphan-session-teardown

Conversation

@dsarno

@dsarno dsarno commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Problem

The HTTP-Local orphan detector in McpConnectionSection.UpdateConnectionStatus() ended the session the instant a single server-reachability probe failed. That teardown path (EndOrphanedSessionAsyncStopAsync) also tears down the WebSocket transport's reconnect loop — so a transient outage (most commonly a dev-server restart, but also a momentary local-port blip) permanently stranded the session until a manual Start Session.

This is the "trigger 2" failure mode behind CoplayDev/unity-mcp#1207 (false orphan teardown at reload/restart boundaries). In the field it took down a live editor with a single console line during a server restart:

Server no longer running; ending orphaned session.

Fix

Debounce the teardown. Track when the local server first became unreachable and only end the session once it has stayed unreachable continuously for OrphanGraceSeconds (10s). Any successful reachability check clears the timer, so a blip no longer trips it — the reconnect loop owns transient outages and recovers on its own.

  • New pure helper ShouldEndOrphanedSession(sessionRunning, localServerReachable, unreachableSinceTime, now, graceSeconds) — all the decision logic, no Unity state, fully unit-testable.
  • serverUnreachableSinceTime records the start of a continuous unreachable window; it resets to -1 whenever the server is reachable again, the session ends, or the controls aren't shown.

Test

Truth-table unit test McpConnectionSection_ShouldEndOrphanedSession_DebouncesTransientUnreachable (reflection-invoked, in Windows_Characterization.cs): ends only when running && unreachable && elapsed ≥ grace; never on a blip within grace, when the server is reachable, when the session isn't running, or with no recorded unreachable-start.

Scope

Standalone fix branched off beta, independent of the liveness-watchdog work in #151. The substantive change is the single fix(transport): debounce orphan-session teardown commit; the remaining commits are the usual beta-behind-upstream/beta delta (same artifact as #151).

Relates to CoplayDev/unity-mcp#1207.

🤖 Generated with Claude Code

DLSinnocence and others added 26 commits May 14, 2026 17:01
Unity 6 exposes MemoryProfiler.TakeSnapshot from UnityEngine.CoreModule with CaptureFlags overloads, so the old Unity.MemoryProfiler.Editor reflection path makes memory_take_snapshot report that com.unity.memoryprofiler is missing even when the package is installed. The profiler list and compare actions only inspect snapshot files, so they no longer depend on MemoryProfiler type discovery.

Constraint: Unity 6 moved MemoryProfiler APIs into UnityEngine.CoreModule.

Rejected: Require com.unity.memoryprofiler editor assembly type discovery for all memory actions | list and compare only need filesystem metadata.

Confidence: high

Scope-risk: narrow

Tested: uv run --extra dev pytest tests/test_manage_profiler.py -v

Tested: Reflected Unity 6 MemoryProfiler.TakeSnapshot overloads in GameClient editor.

Not-tested: Full Unity package test suite.
The MemoryProfiler reflection code now verifies the selected overload's trailing parameter type before invoking it. This keeps Unity 6 CaptureFlags overloads and legacy uint overloads from being mixed in environments where both type families can be discovered.

Constraint: Reflection overload selection spans Unity versions with different trailing argument types.

Confidence: high

Scope-risk: narrow

Tested: Patched GameClient PackageCache and verified memory_take_snapshot, memory_list_snapshots, memory_compare_snapshots.

Tested: Verified profiler_status, get_frame_timing, get_counters, frame_debugger_get_events, and compare error paths.

Not-tested: Full Unity package test suite.
…uilder EditMode test assumptions

UnityTypeResolver.FindCandidates treated a short name as ambiguous when an internal type shared it (richer assembly sets), so unqualified generics like List<T>/Dictionary<TKey,TValue> failed to resolve while fully-qualified names worked. Add DisambiguateByIdentity: de-dupe by FullName, then prefer a single public type, then a single core/BCL type, narrowing only to a unique survivor so genuine clashes still report ambiguous.

Fix EditMode tests that encoded built-in-pipeline/contract assumptions and only failed under URP + ProBuilder (the package test project uses the built-in pipeline and lacks ProBuilder, so these were never exercised):
- ManageGraphics negative tests read result["error"] (ErrorResponse has no "message" field); feature_add uses the handler's "type" param, not "feature_type"
- ManageMaterialProperties uses the pipeline-appropriate color property (_BaseColor on URP/HDRP)
- MCPToolParameterTests asserts pipeline-appropriate shader name and reads _Smoothness on URP/HDRP
- ManageProBuilder get_mesh_info test requests include:"faces"

Verified in TestbedMCP (URP 17.2 + ProBuilder 6.0.9): full EditMode suite 887 passed / 0 failed / 46 skipped.
… liveness polling

Convert the local MCP HTTP server launch from a visible-terminal model to a
headless background launch and make startup diagnosable and robust.

- Launch windowless via TerminalLauncher.CreateHeadlessProcessStartInfo;
  combined stdout/stderr redirected to a per-port log at
  Library/MCPForUnity/Logs/server-launch-{port}.log (truncated each launch)
- Replace the per-launch confirmation dialog with a one-time confirm gated on
  the new EditorPrefs key HttpServerLaunchConfirmed; the quiet auto-start path
  skips it and does not set the flag
- Prepend platform uv/uvx PATH entries so bare uvx/uv resolves under
  GUI-launched Unity's minimal non-login PATH (notably macOS)
- Replace the fixed ~30-attempt reachability waits with open-ended polling
  tied to the launched process's liveness (5-minute hard cap), emitting a
  tail-of-log failure report when the server dies — in both
  HttpAutoStartHandler and McpConnectionSection
- Simplify quit-time cleanup to a handshake-scoped StopManagedLocalHttpServer
  so headless servers are not left as invisible orphans
- Transport-aware UI button labels (Connect/Disconnect vs Start/End Session),
  a transient "Starting…" state, and clearer lifecycle logging

Tests: TerminalLauncherTests covers the headless start-info contract;
ServerManagementServiceCharacterizationTests covers the one-time-confirm gate,
quiet-path bypass, and per-port log redirection.
…ayDev#1120)

Kilo Code v7.0.33+ moved MCP config out of the VS Code extension's
globalStorage/mcp_settings.json to a CLI-style kilo.jsonc under ~/.config/kilo,
with a new schema (https://app.kilo.ai/config.json): an "mcp" container,
type:"remote" for HTTP servers (type:"local" for stdio), and an "enabled" flag.
Writing the legacy mcpServers / type:"http" / disabled config left the server
showing as stdio + disabled.

- KiloCodeConfigurator targets ~/.config/kilo/kilo.jsonc on every OS and declares
  the new format (mcp container, type:remote/local, enabled:true, $schema)
- Generalize the per-client HTTP "type" override: replace the UsesStreamableHttpType
  bool with McpClient.HttpTypeValue (Cline/Roo => "streamableHttp", Kilo => "remote",
  default "http"); add StdioTypeValue, ServerContainerKey and SchemaUrl fields
- ConfigJsonBuilder honors ServerContainerKey ("mcp") and writes a root $schema;
  JsonFileMcpConfigurator.CheckStatus/Unregister read/remove from the configured
  container so status detection and teardown work for Kilo
- Replace StreamableHttpTypeTests with ClientConfigFormatTests covering Kilo's
  remote/mcp/enabled format, Cline's streamableHttp, and generic http

Verified: 5 new tests + 13 existing config tests pass (EditMode, Unity 2021.3).
A one-command, no-API-key end-to-end gate for the Python<->Unity bridge,
runnable locally and in CI.

- tools/local_harness.py: boots a headless Hub-licensed Editor (or attaches
  with --reuse) and runs smoke + EditMode + PlayMode legs over the bridge,
  aggregating JUnit; exit codes 0-5 (pass / regression / unreachable /
  no-compile / no-license / no-editor)
- Server/tests/e2e/bridge_smoke.py: deterministic no-LLM contract driver over
  the real wire path; the no-LLM counterpart to claude-nl-suite.yml
- .github/workflows/e2e-bridge.yml: PR gate booting headless Unity in CI;
  self-skips (warns) when Unity license secrets are absent
- .github/workflows/python-tests.yml: run the hermetic harness unit tests and
  add tools/** to the path triggers
- tools/tests/test_local_harness.py: 69 hermetic unit tests for the harness's
  Unity-free decision logic (discovery, version resolution, exit-code mapping)
- Document the harness in CLAUDE.md and the contributor docs
- Server/tests/test_tool_test_symmetry.py discovers every module exposing an
  @mcp_for_unity_tool and fails CI if it is not referenced by any test
- A KNOWN_UNTESTED quarantine lets pre-existing gaps (execute_menu_item,
  manage_shader, manage_tools) skip rather than fail; a second test fails if a
  quarantine entry is stale, so the list can only shrink

Operationalizes the CLAUDE.md rule that every new tool ships with a test.
pyproject.toml is already at 9.7.1; the lockfile lagged at 9.6.6.
…roject runtime artifacts

The 8 EditMode/Tools test scripts were tracked without their .meta files, so
every contributor's Unity regenerated them with random GUIDs (perpetual untracked
churn). Commit the metas so Unity reuses a stable GUID. Also ignore the MCP
bridge / UIToolkit runtime artifacts (Assets/UnityMCP, Assets/UI) the test
project emits when run.
- local_harness: fail fast (exit 2) on empty --legs and on --ci without
  UNITY_IMAGE, instead of silently exiting green / crashing with an
  unhandled CalledProcessError
- local_harness: correct the nearest-patch `best` type annotation
  (key is (patch:int, suffix:str), not a 3-tuple)
- test_tool_test_symmetry: fix the module docstring to match behavior
  (only test_*.py files are scanned; non-test_*.py scripts like
  tests/e2e/bridge_smoke.py do not count toward coverage)

Deferred CodeRabbit's SHA-pin suggestion for e2e-bridge.yml: the repo pins
actions by tag (@v4/@v6), not SHA, so pinning one workflow would be
inconsistent — left as a repo-wide policy decision.
parse_legs stays a lenient normalizer (test_drops_unknown_legs), but main() now
validates the raw --legs input against the shared ALLOWED_LEGS and exits 2 on any
unknown value -- so a typo like `smoke,edimode` fails loudly instead of silently
dropping the bad leg and running a subset.
…kwins-and-e2e

Patch: headless http server launch, Kilo Code (CoplayDev#1120), e2e bridge harness
…eta.12-27528862114

chore: update Unity package to beta version 9.7.2-beta.12
…er-coremodule-api

Fix Memory Profiler snapshot actions on Unity 6
…eta.13-27529138874

chore: update Unity package to beta version 9.7.2-beta.13
…o-beta-27529986410

chore: sync main (v9.7.3) into beta
…on't strand sessions

The HTTP-Local orphan detector (McpConnectionSection.UpdateConnectionStatus)
ended the session on a single unreachable server probe. That path also tears
down the WebSocket transport's reconnect loop, so a transient blip — e.g. a
server restart — stranded the session until a manual Start Session (issue
CoplayDev#1207 "trigger 2"). Debounce it: only end the session after the local server
has stayed unreachable continuously for OrphanGraceSeconds (10s), leaving the
reconnect loop to own transient outages. Adds a pure ShouldEndOrphanedSession
helper + a truth-table unit test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5f59c5ff-bb51-46e2-8515-c59de7cd2ed5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/orphan-session-teardown

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 and usage tips.

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.

4 participants