Skip to content

Comments

Fix stdio bridge stalls when Unity is backgrounded during domain reload#787

Merged
dsarno merged 5 commits intoCoplayDev:betafrom
dsarno:fix/stdio-bridge-zombie-785
Feb 20, 2026
Merged

Fix stdio bridge stalls when Unity is backgrounded during domain reload#787
dsarno merged 5 commits intoCoplayDev:betafrom
dsarno:fix/stdio-bridge-zombie-785

Conversation

@dsarno
Copy link
Collaborator

@dsarno dsarno commented Feb 20, 2026

Summary

  • QueuePlayerLoopUpdate after command enqueue so ProcessCommands fires even when Unity is backgrounded (mirrors HTTP RequestMainThreadPump pattern)
  • Keep ProcessCommands permanently registered on EditorApplication.update to eliminate the registration gap between Stop and Start during domain reload
  • Replace blocking retry loop in Start() (Thread.Sleep x 10) with single attempt + port-switch fallback; async retries handled by reload handler
  • Async retry with backoff in StdioBridgeReloadHandler — replaces fire-and-forget TryStartBridgeImmediate with 6-attempt retry loop (0s, 1s, 3s, 5s, 10s, 30s), matching the HTTP reload handler pattern. Retries are cancelled via CancellationToken on editor quit or subsequent domain reloads.
  • Fix macOS port conflict — use ExclusiveAddressUse in both the real listener (CreateConfiguredListener) and the port probe (IsPortAvailable) to prevent AssetImportWorkers from binding the same port via SO_REUSEADDR
  • Add PortManager unit tests (5 tests including macOS SO_REUSEADDR detection)
  • Add resource URI reference table to unity-mcp-skill to prevent LLMs from guessing incorrect URIs

Root Causes

  1. Stdio never called QueuePlayerLoopUpdate — when Unity is backgrounded, the main loop stops iterating, so ProcessCommands never fires and queued commands time out
  2. ProcessCommands deregistered during Stop() — during domain reload, Stop() removes the EditorApplication.update callback and Start() re-adds it, but all delegates are cleared on reload, creating a gap where commands queue but never process
  3. Single-shot reload handler — TryStartBridgeImmediate gave up after one failure, leaving the bridge dead if the port was temporarily busy
  4. AssetImportWorkers stealing TCP connections — on macOS, SO_REUSEADDR allows multiple processes to bind the same port; workers loaded the same assemblies and bound the bridge port, intercepting connections meant for the main editor
  5. IsPortAvailable used permissive test bind — PortManager.IsPortAvailable() tested with a plain TcpListener (which defaults to SO_REUSEADDR on macOS), so DiscoverNewPort() returned the same occupied port

Test plan

  • StdioBridgeReconnectTests: 2/2 passed on stdio (with Unity backgrounded)
  • Full EditMode suite: 668 tests, 0 failures (HTTP mode)
  • Python tests: 685 passed
  • PortManager tests: 5/5 passed (including macOS SO_REUSEADDR case)
  • Live stress test on stdio: 6 domain reloads, 20+ MCP operations (create/edit/delete scripts, create/delete GameObjects, refresh, console reads) all succeeded without touching Unity
  • HTTP smoke test: 3 creates, 2 parallel edits, 2 compiles, GameObject with components, cleanup — no regressions

Fixes #785

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Enhanced port conflict detection on macOS to prevent binding failures
    • Improved Stdio bridge recovery after domain reloads with intelligent retry scheduling
    • Fixed stale command handling to prevent timeout accumulation
    • Improved client connection management to close disconnected clients
  • New Features

    • Added automatic port fallback when the default port is in use
  • Tests

    • Added comprehensive port availability tests
    • Added client reconnection scenario tests
  • Documentation

    • Added guidance on Resource URI discovery and usage

dsarno and others added 3 commits February 19, 2026 16:06
After domain reload, the bridge could accept TCP connections but never
process commands. Three defensive fixes address the plausible failure
vectors:

1. Stale client cleanup: when a new client connects, close all other
   active clients (in stdio there is only one server). Forces hung
   ReadFrameAsUtf8Async to throw and exit cleanly.

2. ProcessCommands self-healing: track consecutive TCS timeouts. After
   2+ consecutive, force re-register ProcessCommands on
   EditorApplication.update and reset the reentrancy guard.

3. Stale command eviction: commands stuck with IsExecuting=true for
   more than 60s are evicted from the queue with an error result.

Also adds always-on diagnostic logging at client connect/disconnect
with active client counts.

Includes E2E reconnection tests that verify both abrupt disconnect
recovery and stale client cleanup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add QueuePlayerLoopUpdate after command enqueue so ProcessCommands fires
  even when Unity is backgrounded (mirrors HTTP RequestMainThreadPump)
- Keep ProcessCommands permanently registered on EditorApplication.update
  to eliminate the registration gap between Stop and Start during reload
- Replace blocking retry loop in Start() (Thread.Sleep x 10) with single
  attempt + port-switch fallback; async retries handled by reload handler
- Replace fire-and-forget TryStartBridgeImmediate with async retry loop
  (6 attempts with backoff: 0s, 1s, 3s, 5s, 10s, 30s) matching HTTP
- Fix macOS port conflict: use ExclusiveAddressUse in both the real
  listener (CreateConfiguredListener) and the port probe (IsPortAvailable)
  to prevent AssetImportWorkers from binding the same port via SO_REUSEADDR
- Add PortManager unit tests (5 tests including macOS SO_REUSEADDR case)

Fixes CoplayDev#785

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a prominent 'Do NOT Guess' section with a URI lookup table near
the top of the skill, so LLMs use exact URIs instead of fabricating
similar-looking ones.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 20, 2026

Reviewer's Guide

Refactors the stdio transport startup/reload and port management to avoid stalls and macOS port conflicts, adds robust reconnect/retry behavior and stale-command cleanup, and documents resource URIs for the Unity MCP skill, backed by new edit-mode tests.

Sequence diagram for stdio bridge resume with retry after domain reload

sequenceDiagram
    actor User
    participant UnityEditor
    participant AssemblyReloadEvents
    participant StdioBridgeReloadHandler
    participant EditorConfigurationCache
    participant MCPServiceLocator
    participant TransportManager
    participant StdioBridgeHost

    User->>UnityEditor: Trigger code change causing domain reload
    UnityEditor->>AssemblyReloadEvents: beforeAssemblyReload
    AssemblyReloadEvents->>StdioBridgeReloadHandler: OnBeforeAssemblyReload()
    StdioBridgeReloadHandler->>UnityEditor: Set ResumeStdioAfterReload flag
    StdioBridgeReloadHandler->>StdioBridgeHost: Stop()

    UnityEditor->>AssemblyReloadEvents: afterAssemblyReload
    AssemblyReloadEvents->>StdioBridgeReloadHandler: OnAfterAssemblyReload()
    StdioBridgeReloadHandler->>UnityEditor: Read ResumeStdioAfterReload flag
    alt flag not set
        StdioBridgeReloadHandler-->>AssemblyReloadEvents: Return (no resume)
    else flag set
        StdioBridgeReloadHandler->>UnityEditor: Check EditorApplication.isCompiling
        alt not compiling
            StdioBridgeReloadHandler->>StdioBridgeReloadHandler: ResumeStdioWithRetriesAsync()
        else compiling
            StdioBridgeReloadHandler->>UnityEditor: EditorApplication.delayCall(ResumeStdioWithRetriesAsync)
        end

        loop attempts over ResumeRetrySchedule
            StdioBridgeReloadHandler->>EditorConfigurationCache: Read Instance.UseHttpTransport
            alt UseHttpTransport is true
                StdioBridgeReloadHandler->>UnityEditor: Delete ResumeStdioAfterReload flag
                StdioBridgeReloadHandler-->>UnityEditor: Abort retries
                StdioBridgeReloadHandler-->>StdioBridgeReloadHandler: Break loop
            else
                StdioBridgeReloadHandler->>StdioBridgeReloadHandler: Wait delay for this attempt
                StdioBridgeReloadHandler->>MCPServiceLocator: TransportManager
                MCPServiceLocator->>TransportManager: StartAsync(TransportMode.Stdio)
                TransportManager->>StdioBridgeHost: Start()
                alt listener binds successfully
                    StdioBridgeHost-->>TransportManager: started = true
                    TransportManager-->>StdioBridgeReloadHandler: true
                    StdioBridgeReloadHandler->>UnityEditor: Delete ResumeStdioAfterReload flag
                    StdioBridgeReloadHandler->>UnityEditor: MCPForUnityEditorWindow.RequestHealthVerification()
                    StdioBridgeReloadHandler-->>UnityEditor: Resume succeeded
                    StdioBridgeReloadHandler-->>StdioBridgeReloadHandler: Break loop
                else listener fails (port busy or error)
                    StdioBridgeHost-->>TransportManager: started = false or exception
                    TransportManager-->>StdioBridgeReloadHandler: false or exception
                    StdioBridgeReloadHandler->>TransportManager: GetState(TransportMode.Stdio)
                    StdioBridgeReloadHandler-->>StdioBridgeReloadHandler: Log failure and continue loop
                end
            end
        end

        alt all attempts exhausted and not started
            StdioBridgeReloadHandler->>UnityEditor: Delete ResumeStdioAfterReload flag
            StdioBridgeReloadHandler-->>UnityEditor: Log final failure
        end
    end
Loading

Class diagram for updated stdio bridge, reload handler, and port manager

classDiagram
    class StdioBridgeHost {
        <<static>>
        +bool isRunning
        +bool isAutoConnectMode
        +ulong MaxFrameBytes
        +int FrameIOTimeoutMs
        +Stopwatch _uptime
        +int _consecutiveTimeouts
        +bool _processCommandsHooked
        +void Start()
        +void Stop()
        +TcpListener CreateConfiguredListener(int port)
        +Task ListenerLoopAsync(CancellationToken token)
        +Task HandleClientAsync(TcpClient client, CancellationToken token)
        +void ProcessCommands()
    }

    class QueuedCommand {
        +string CommandJson
        +TaskCompletionSource~string~ Tcs
        +bool IsExecuting
        +long EnqueuedAtMs
    }

    class PortManager {
        <<static>>
        +bool IsPortAvailable(int port)
        +int DiscoverNewPort()
    }

    class StdioBridgeReloadHandler {
        <<static>>
        +TimeSpan[] ResumeRetrySchedule
        +void OnBeforeAssemblyReload()
        +void OnAfterAssemblyReload()
        +Task ResumeStdioWithRetriesAsync()
    }

    class MCPServiceLocator {
        <<static>>
        +TransportManager TransportManager
    }

    class TransportManager {
        +Task~bool~ StartAsync(TransportMode mode)
        +TransportState GetState(TransportMode mode)
    }

    class EditorConfigurationCache {
        <<singleton>>
        +EditorConfigurationCache Instance
        +bool UseHttpTransport
    }

    StdioBridgeHost --> QueuedCommand : uses
    StdioBridgeHost --> PortManager : uses for ports
    StdioBridgeHost --> MCPServiceLocator : uses to integrate transport
    StdioBridgeReloadHandler --> MCPServiceLocator : restarts stdio bridge
    StdioBridgeReloadHandler --> EditorConfigurationCache : checks transport mode
    StdioBridgeReloadHandler --> StdioBridgeHost : indirectly via TransportManager
    PortManager ..> TcpListener : uses for IsPortAvailable
Loading

File-Level Changes

Change Details Files
Make stdio bridge more robust across domain reloads, backgrounded editor, and client reconnects.
  • Record enqueue time on queued commands and periodically evict long-stuck executing commands with an error response.
  • Ensure ProcessCommands stays permanently registered on EditorApplication.update using a _processCommandsHooked guard instead of deregistering on Stop().
  • After enqueuing a command, call EditorApplication.QueuePlayerLoopUpdate() so ProcessCommands runs even when Unity is backgrounded.
  • Track consecutive TCS timeouts and log when they occur to aid diagnosis.
  • Improve client handling so new connections log active counts, close stale prior clients, and log remaining clients on exit.
MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs
Change stdio bridge startup and reload behavior to avoid blocking and add async retry with backoff.
  • Replace Start()’s blocking port retry loop with a single bind attempt plus one port-switch fallback, leaving further retries to the reload handler.
  • Reduce listener shutdown wait time in Stop() and keep the ProcessCommands hook registered through reloads.
  • Introduce StdioBridgeReloadHandler.ResumeStdioWithRetriesAsync with a 6-step backoff schedule (0s–30s) that retries TransportManager.StartAsync(Stdio) and aborts if HTTP transport is enabled.
  • Handle compilation state when scheduling resume logic, using CompilationPipeline.isCompiling via reflection and EditorApplication.delayCall.
  • On successful resume, clear the ResumeStdioAfterReload flag and request editor-window health verification; on failure, log detailed debug/warn messages.
MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs
MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs
Harden port allocation to avoid macOS SO_REUSEADDR conflicts and validate with tests.
  • Configure CreateConfiguredListener on macOS to use ExclusiveAddressUse instead of SO_REUSEADDR, with comments explaining AssetImportWorker conflicts.
  • Update PortManager.IsPortAvailable to set ExclusiveAddressUse on the probe listener on macOS so it correctly reports ports held by other processes as unavailable.
  • Add PortManagerTests covering occupied/free ports, macOS SO_REUSEADDR behavior, and DiscoverNewPort behavior including skipping the default port when held.
MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs
MCPForUnity/Editor/Helpers/PortManager.cs
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PortManagerTests.cs
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PortManagerTests.cs.meta
Add regression tests for stdio bridge reconnection behavior.
  • Create StdioBridgeReconnectTests to validate that a new client can connect and exchange ping/pong after an abrupt disconnect from the previous client.
  • Add a test that connecting a second client while the first is still connected causes the first to be closed, using framed protocol helpers for read/write.
  • Implement local frame-protocol helpers (handshake read, framed send/receive) shared within the test to mirror production framing.
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/StdioBridgeReconnectTests.cs
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/StdioBridgeReconnectTests.cs.meta
Document Unity MCP resource URIs to discourage LLMs from guessing invalid values.
  • Extend SKILL.md with a 'Resource URIs: Do NOT Guess' section and a table of common resource URIs such as editor state, project info, scene gameobject API, tags, layers, instances, and custom tools.
  • Instruct consumers to call ListMcpResourcesTool(server="UnityMCP") when unsure about URIs.
unity-mcp-skill/SKILL.md

Assessment against linked issues

Issue Objective Addressed Explanation
#785 Ensure that after the Python MCP stdio client disconnects or restarts, StdioBridgeHost can accept a new TCP connection on the same port and correctly process commands from the new client instead of entering a zombie state.
#785 Harden the stdio bridge’s lifecycle and socket management so stale connections and stuck command-processing state (e.g., across domain reloads or abrupt disconnects) are cleaned up automatically without requiring a manual End Session / Start Session in Unity.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 20, 2026

📝 Walkthrough
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.00% 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 describes the main fix: resolving stdio bridge stalls during domain reload when Unity is backgrounded.
Description check ✅ Passed The description comprehensively documents root causes, specific changes, test results, and addresses the required template sections with detailed explanations.
Linked Issues check ✅ Passed All code changes address the core objectives from issue #785: ProcessCommands registration gap fixed, async retry logic implemented, port binding improved, and zombie state prevented through proper connection/command handling.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the zombie state and command processing issues; documentation update to unity-mcp-skill is complementary and within scope of the PR objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link
Contributor

@sourcery-ai sourcery-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.

Hey - I've found 2 issues, and left some high level feedback:

  • In HandleClientAsync, you take two separate locks on clientsLock just to compute the active client count; consider collapsing these into the existing add/remove critical sections to avoid extra locking and reduce the chance of transiently inconsistent counts in logs.
  • The retry loop in ResumeStdioWithRetriesAsync uses Task.Delay without any cancellation, so the background task can keep running across editor quit or subsequent reloads; consider threading a cancellation token from assembly reload/quitting events into this method so retries can be aborted cleanly.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `HandleClientAsync`, you take two separate locks on `clientsLock` just to compute the active client count; consider collapsing these into the existing add/remove critical sections to avoid extra locking and reduce the chance of transiently inconsistent counts in logs.
- The retry loop in `ResumeStdioWithRetriesAsync` uses `Task.Delay` without any cancellation, so the background task can keep running across editor quit or subsequent reloads; consider threading a cancellation token from assembly reload/quitting events into this method so retries can be aborted cleanly.

## Individual Comments

### Comment 1
<location> `MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs:401` </location>
<code_context>
             using (NetworkStream stream = client.GetStream())
             {
                 lock (clientsLock) { activeClients.Add(client); }
+                int clientCount;
+                lock (clientsLock) { clientCount = activeClients.Count; }
                 try
                 {
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Avoid locking `clientsLock` twice for add + count; both operations can be done under a single lock.

In `HandleClientAsync`, `activeClients.Add(client)` and `clientCount = activeClients.Count` are guarded by two separate `lock (clientsLock)` blocks. This allows another thread to modify `activeClients` between these operations and adds extra locking overhead. Wrap both statements in a single `lock (clientsLock)` and compute `clientCount` immediately after the add.

```suggestion
            try
            using (NetworkStream stream = client.GetStream())
            {
                int clientCount;
                lock (clientsLock)
                {
                    activeClients.Add(client);
                    clientCount = activeClients.Count;
                }
                try
                {
```
</issue_to_address>

### Comment 2
<location> `unity-mcp-skill/SKILL.md:26` </location>
<code_context>
+|----------|-----|
+| Editor state | `mcpforunity://editor/state` |
+| Project info | `mcpforunity://project/info` |
+| Scene gameobject API | `mcpforunity://scene/gameobject-api` |
+| Tags | `mcpforunity://project/tags` |
+| Layers | `mcpforunity://project/layers` |
</code_context>

<issue_to_address>
**suggestion (typo):** Consider using Unity’s canonical spelling "GameObject" in the label.

To stay consistent with Unity terminology and the rest of the docs, please change the label to "Scene GameObject API" while keeping the URI as-is.

```suggestion
| Scene GameObject API | `mcpforunity://scene/gameobject-api` |
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
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: 4

🧹 Nitpick comments (1)
MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs (1)

466-468: Two consecutive lock acquisitions can be combined

Lines 467–468 acquire clientsLock twice in a row (once to add, once to read count). Same pattern at lines 640–642 (remove + read count). Each pair can be merged into a single lock scope.

♻️ Proposed refactor
-            lock (clientsLock) { activeClients.Add(client); }
-            int clientCount;
-            lock (clientsLock) { clientCount = activeClients.Count; }
+            int clientCount;
+            lock (clientsLock) { activeClients.Add(client); clientCount = activeClients.Count; }
-            lock (clientsLock) { activeClients.Remove(client); }
-            int remaining;
-            lock (clientsLock) { remaining = activeClients.Count; }
+            int remaining;
+            lock (clientsLock) { activeClients.Remove(client); remaining = activeClients.Count; }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs` around
lines 466 - 468, The code currently acquires clientsLock twice in a row—once to
modify activeClients and again to read its Count—so merge each pair into a
single lock scope: inside the same lock(clientsLock) block perform the mutation
(Add or Remove) and then immediately read activeClients.Count into clientCount;
update both occurrences (the add at methods referencing activeClients.Add and
the remove at activeClients.Remove) to use a single lock to reduce contention
and avoid redundant locking.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs`:
- Around line 114-124: There is no guard preventing multiple concurrent retry
loops from ResumeStdioWithRetriesAsync, so simultaneous domain reloads can start
overlapping loops that both call StdioBridgeHost.StartAsync and flip the
EditorPrefs resume flag; add a static concurrency guard (e.g., a static volatile
bool or an Interlocked-backed static flag named something like
s_resumeLoopRunning) at the top of ResumeStdioWithRetriesAsync to check-and-set
before starting the loop and clear it when the loop completes (including on all
early returns/exceptions), and use that flag to skip starting a second loop;
reference ResumeStdioWithRetriesAsync, StartAsync and the EditorPrefs resume
flag, and keep startStopLock in StdioBridgeHost.Start() unchanged.

In `@MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs`:
- Line 57: The _consecutiveTimeouts counter is incremented but never triggers
the promised self-healing; add a threshold check (e.g., const int
TIMEOUT_THRESHOLD = 3) where timeouts are counted (in HandleClientAsync or the
timeout handler) and when exceeded marshal back to the main thread via
EditorApplication.delayCall to perform corrective actions: remove and re-add the
ProcessCommands delegate from EditorApplication.update and set
processingCommands = 0, then reset _consecutiveTimeouts = 0; ensure this uses
the same ProcessCommands method reference and only mutates
EditorApplication.update on the main thread.

In
`@TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PortManagerTests.cs`:
- Around line 66-106: Tests call PortManager.DiscoverNewPort(), which invokes
SavePort and mutates persistent on-disk port files (unity-mcp-port-<hash>.json
and legacy file), breaking isolation and causing order-dependent failures; fix
by preventing disk writes during tests: either stub/mock SavePort via dependency
injection into PortManager (introduce an ISavePort or IFileSystem interface and
have DiscoverNewPort use it) so tests can inject a no-op saver, or implement
test setup/teardown that snapshots the existing port file contents before
running tests and fully restores them in a [TearDown] method (also delete files
created by the test) to ensure GetPortWithFallback and the running bridge see
the original state.

In
`@TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/StdioBridgeReconnectTests.cs`:
- Around line 22-23: ConnectTimeoutMs is declared but never enforced, so
client1.Connect(...) and client2.Connect(...) can block indefinitely; update the
tests to enforce a connect timeout by replacing blocking Connect calls with
ConnectAsync combined with a Task.Wait/WaitAsync timeout using ConnectTimeoutMs,
or set the socket timeouts (SendTimeout/ReceiveTimeout) prior to connecting and
use the async connect to abort if it exceeds ConnectTimeoutMs; modify all
occurrences of client1.Connect and client2.Connect (and any other Connect calls
in this test) to use this pattern and ensure ConnectTimeoutMs is actually
referenced rather than left unused.

---

Nitpick comments:
In `@MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs`:
- Around line 466-468: The code currently acquires clientsLock twice in a
row—once to modify activeClients and again to read its Count—so merge each pair
into a single lock scope: inside the same lock(clientsLock) block perform the
mutation (Add or Remove) and then immediately read activeClients.Count into
clientCount; update both occurrences (the add at methods referencing
activeClients.Add and the remove at activeClients.Remove) to use a single lock
to reduce contention and avoid redundant locking.

private const ulong MaxFrameBytes = 64UL * 1024 * 1024;
private const int FrameIOTimeoutMs = 30000;
private static readonly Stopwatch _uptime = Stopwatch.StartNew();
private static volatile int _consecutiveTimeouts = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

_consecutiveTimeouts tracking has no corrective action — self-healing feature is incomplete

The commit description promises: "force re-registering ProcessCommands after repeated timeouts; reset reentrancy guard." The counter is incremented and logged, but there is no code that:

  • Checks _consecutiveTimeouts against a threshold
  • Removes and re-adds ProcessCommands to EditorApplication.update
  • Resets the processingCommands reentrancy guard (processingCommands = 0)

Without the corrective action, a stuck processingCommands = 1 guard (which could occur if a previous ProcessCommands invocation somehow exited without hitting the finally) will never be cleared by this mechanism, leaving the bridge permanently unable to process commands without a restart.

The intended fix would look like:

🔧 Suggested completion of the self-healing logic
+        private const int ConsecutiveTimeoutThreshold = 3;

         // In HandleClientAsync, on TCS timeout:
         int timeouts = Interlocked.Increment(ref _consecutiveTimeouts);
         McpLog.Warn($"Command TCS timed out ({timeouts} consecutive)");
+        if (timeouts >= ConsecutiveTimeoutThreshold)
+        {
+            McpLog.Warn("[Self-heal] Resetting ProcessCommands hook after repeated timeouts");
+            Interlocked.Exchange(ref processingCommands, 0); // reset stuck reentrancy guard
+            if (_processCommandsHooked)
+            {
+                EditorApplication.update -= ProcessCommands;
+                _processCommandsHooked = false;
+            }
+            if (!_processCommandsHooked)
+            {
+                _processCommandsHooked = true;
+                EditorApplication.update += ProcessCommands;
+            }
+            Interlocked.Exchange(ref _consecutiveTimeouts, 0);
+        }

Note: EditorApplication.update must only be modified on the main thread. If the corrective action is triggered from HandleClientAsync (which runs on a thread-pool thread), it must be marshalled to the main thread via EditorApplication.delayCall.

Also applies to: 569-574

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs` at line
57, The _consecutiveTimeouts counter is incremented but never triggers the
promised self-healing; add a threshold check (e.g., const int TIMEOUT_THRESHOLD
= 3) where timeouts are counted (in HandleClientAsync or the timeout handler)
and when exceeded marshal back to the main thread via
EditorApplication.delayCall to perform corrective actions: remove and re-add the
ProcessCommands delegate from EditorApplication.update and set
processingCommands = 0, then reset _consecutiveTimeouts = 0; ensure this uses
the same ProcessCommands method reference and only mutates
EditorApplication.update on the main thread.

- Collapse double lock in HandleClientAsync (add + count in one lock)
- Add CancellationToken to ResumeStdioWithRetriesAsync so retries abort
  on editor quit or subsequent domain reloads
- Fix 'gameobject' → 'GameObject' capitalization in skill URI table

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
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.

🧹 Nitpick comments (2)
MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs (1)

116-124: Duplicated IsCompiling reflection logic across three locations.

This reflection-based CompilationPipeline.isCompiling check is duplicated here, in HttpBridgeReloadHandler.OnAfterAssemblyReload, and in StdioBridgeHost.IsCompiling(). Consider extracting it into a shared helper (e.g., on a utility class) to reduce maintenance burden.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs` around lines 116 -
124, Extract the duplicated reflection logic that checks
UnityEditor.Compilation.CompilationPipeline.isCompiling into a single shared
static helper method (e.g., EditorCompilationHelper.IsCompilationInProgress) and
replace the three in-place implementations in StdioBridgeReloadHandler (the
block using Type.GetType and prop.GetValue),
HttpBridgeReloadHandler.OnAfterAssemblyReload, and StdioBridgeHost.IsCompiling()
to call that helper; the helper should perform the safe
Type.GetType("UnityEditor.Compilation.CompilationPipeline, UnityEditor") lookup,
reflect the static "isCompiling" property with binding flags, return the
combined boolean (including EditorApplication.isCompiling), and swallow/refine
exceptions consistently so callers keep the same behavior.
MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs (1)

642-645: Double lock acquisition on clientsLock — combine into one.

The Remove on line 642 and the Count on line 644 each acquire clientsLock separately. Since they're adjacent, combining them avoids the redundant lock round-trip and gives an atomic remove-and-count.

♻️ Suggested fix
-                    lock (clientsLock) { activeClients.Remove(client); }
-                    int remaining;
-                    lock (clientsLock) { remaining = activeClients.Count; }
+                    int remaining;
+                    lock (clientsLock)
+                    {
+                        activeClients.Remove(client);
+                        remaining = activeClients.Count;
+                    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs` around
lines 642 - 645, Combine the two adjacent locks into a single critical section:
inside one lock(clientsLock) perform activeClients.Remove(client) and int
remaining = activeClients.Count; then call McpLog.Info($"Client handler exited
(remaining clients: {remaining})") using that local remaining variable outside
the lock; reference the existing clientsLock, activeClients, and McpLog.Info in
StdioBridgeHost.cs when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs`:
- Around line 572-577: The _consecutiveTimeouts counter is only logged but never
causes corrective action; add a threshold check after Interlocked.Increment(ref
_consecutiveTimeouts) (use the same block where McpLog.Warn is called) and, when
exceeded (e.g. 3-5 timeouts), perform a safe self-healing step: reset the
processingCommands reentrancy guard using Interlocked.Exchange(ref
processingCommands, 0) and re-register or invoke ProcessCommands as appropriate,
then log the recovery action via McpLog.Info/Error; ensure the reset is done
atomically and only when the guard is stuck to avoid breaking normal concurrent
behavior.

---

Nitpick comments:
In `@MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs`:
- Around line 116-124: Extract the duplicated reflection logic that checks
UnityEditor.Compilation.CompilationPipeline.isCompiling into a single shared
static helper method (e.g., EditorCompilationHelper.IsCompilationInProgress) and
replace the three in-place implementations in StdioBridgeReloadHandler (the
block using Type.GetType and prop.GetValue),
HttpBridgeReloadHandler.OnAfterAssemblyReload, and StdioBridgeHost.IsCompiling()
to call that helper; the helper should perform the safe
Type.GetType("UnityEditor.Compilation.CompilationPipeline, UnityEditor") lookup,
reflect the static "isCompiling" property with binding flags, return the
combined boolean (including EditorApplication.isCompiling), and swallow/refine
exceptions consistently so callers keep the same behavior.

In `@MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs`:
- Around line 642-645: Combine the two adjacent locks into a single critical
section: inside one lock(clientsLock) perform activeClients.Remove(client) and
int remaining = activeClients.Count; then call McpLog.Info($"Client handler
exited (remaining clients: {remaining})") using that local remaining variable
outside the lock; reference the existing clientsLock, activeClients, and
McpLog.Info in StdioBridgeHost.cs when making the change.

- PortManagerTests: save/restore on-disk port files in SetUp/TearDown
  to prevent DiscoverNewPort from mutating persistent state
- StdioBridgeReconnectTests: replace blocking Connect with ConnectAsync
  + Wait(ConnectTimeoutMs) so CI tests cannot hang indefinitely

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dsarno dsarno merged commit 79e6c91 into CoplayDev:beta Feb 20, 2026
1 of 2 checks passed
Copy link
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.

🧹 Nitpick comments (6)
unity-mcp-skill/SKILL.md (1)

22-31: mcpforunity://scene/gameobject/{instance_id} is absent from the table but referenced in the doc.

Line 181 uses mcpforunity://scene/gameobject/{instance_id} as the standard way to read full per-object data — a very common operation. Omitting it from the reference table may prompt an LLM to guess the parameterized form, which is precisely the behavior this section aims to suppress.

📝 Suggested table addition
 | Custom tools | `mcpforunity://custom-tools` |
+| Scene GameObject (by ID) | `mcpforunity://scene/gameobject/{instance_id}` |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@unity-mcp-skill/SKILL.md` around lines 22 - 31, The reference table in
SKILL.md is missing the per-object resource URI
mcpforunity://scene/gameobject/{instance_id} even though the doc later
references it; add a new row to the existing "Resource | URI" table (near the
"Scene GameObject API" / "Scene GameObject" entries) with a Resource name like
"Scene GameObject (per-instance)" and the URI
mcpforunity://scene/gameobject/{instance_id}, so readers and LLMs have the exact
parameterized form documented.
MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs (2)

642-645: Double lock acquisition on clientsLock can be consolidated.

Lines 642 and 644 each acquire clientsLock independently. The remaining count could become stale between the two acquisitions (another client could connect/disconnect). Consolidate into one block:

♻️ Suggested fix
-                    lock (clientsLock) { activeClients.Remove(client); }
-                    int remaining;
-                    lock (clientsLock) { remaining = activeClients.Count; }
+                    int remaining;
+                    lock (clientsLock)
+                    {
+                        activeClients.Remove(client);
+                        remaining = activeClients.Count;
+                    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs` around
lines 642 - 645, Consolidate the two separate lock acquisitions on clientsLock
into a single critical section: inside one lock(clientsLock) call remove the
client from activeClients and read remaining as activeClients.Count (i.e.,
perform activeClients.Remove(client) and set remaining within the same lock) to
avoid the count becoming stale; then call McpLog.Info($"Client handler exited
(remaining clients: {remaining})") (you can log outside the lock) — this touches
the code manipulating activeClients, clientsLock, and the McpLog.Info call.

29-29: Stale-eviction threshold is measured from enqueue time, not execution start — intentional?

EnqueuedAtMs is captured when the command is queued (line 554), and the eviction check on line 799 compares nowMs - EnqueuedAtMs against staleThresholdMs. This means the threshold covers the entire lifecycle (wait + execution), not just execution time. A command that sat idle in the queue for most of the threshold and only recently began executing would be evicted immediately.

Given that staleThresholdMs = 2 × FrameIOTimeoutMs = 60 s and there's already a 30 s TCS timeout in HandleClientAsync, this is probably fine in practice — but the comment on line 793 says "stuck with IsExecuting=true for too long", which is slightly misleading since the clock started at enqueue time.

If the intent is truly to measure execution duration, consider capturing a separate ExecutionStartedAtMs when IsExecuting is set to true (line 822). Otherwise, updating the comment to clarify the total-lifecycle semantics would prevent future confusion.

Also applies to: 553-554, 793-815

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs` at line
29, The eviction check measures from enqueue time (EnqueuedAtMs) rather than
execution start, which can evict commands that only recently began executing; to
fix, add a new field ExecutionStartedAtMs on the same command type, set
ExecutionStartedAtMs when IsExecuting is set to true (in the code path around
HandleClientAsync / where IsExecuting is flipped), and change the stale eviction
logic (the check that compares nowMs - EnqueuedAtMs against staleThresholdMs) to
use nowMs - ExecutionStartedAtMs when IsExecuting==true (fall back to
EnqueuedAtMs if ExecutionStartedAtMs is unset); also update the comment that
referenced "stuck with IsExecuting=true for too long" to reflect that the
eviction now measures execution duration.
MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs (2)

38-41: CancellationTokenSource is cancelled but never disposed.

Each call to ResumeStdioWithRetriesAsync creates a new CancellationTokenSource (line 143), and CancelRetries only calls Cancel() on the old one. The old CTS is never disposed. While CTS is lightweight and the GC will eventually collect it, the finalizer may delay cleanup of the internal timer (from CancelAfter / Task.Delay).

Consider disposing the old CTS in CancelRetries:

♻️ Suggested fix
         private static void CancelRetries()
         {
-            try { _retryCts?.Cancel(); } catch { }
+            var old = _retryCts;
+            _retryCts = null;
+            try { old?.Cancel(); } catch { }
+            try { old?.Dispose(); } catch { }
         }

Also applies to: 142-143

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs` around lines 38 -
41, CancelRetries currently only calls _retryCts.Cancel() leaving the old
CancellationTokenSource undisposed; update CancelRetries to safely Cancel and
Dispose the existing _retryCts (e.g., call _retryCts.Cancel();
_retryCts.Dispose(); _retryCts = null) to avoid leaking the CTS timer, and
ensure thread-safety with the same synchronization used around _retryCts in
ResumeStdioWithRetriesAsync so you don't race disposing a newly-created CTS;
swallow or log ObjectDisposedException if needed.

116-124: Extract shared compilation check helper — duplicated across reload handlers.

The compilation state check using Type.GetType("UnityEditor.Compilation.CompilationPipeline") is duplicated across StdioBridgeReloadHandler.cs (lines 118-124), HttpBridgeReloadHandler.cs (lines 90-92), and already encapsulated in StdioBridgeHost.IsCompiling() (lines 234-251). Create a shared utility method (e.g., in a CompilationHelper class) to centralize this logic and ensure consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs` around lines 116 -
124, Extract the duplicated compilation-state logic into a single static helper
(e.g., class CompilationHelper with a public static bool IsCompiling()) that
implements the current two-step check (EditorApplication.isCompiling plus the
reflection lookup of "UnityEditor.Compilation.CompilationPipeline" and its
static isCompiling property, with safe exception handling), update
StdioBridgeReloadHandler to call CompilationHelper.IsCompiling() instead of the
inline reflection block, update HttpBridgeReloadHandler to call the same helper,
and remove or refactor StdioBridgeHost.IsCompiling() to delegate to
CompilationHelper.IsCompiling() so all three places use the centralized method
for consistency.
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PortManagerTests.cs (1)

22-45: GetRegistryDirectory() is private, so the test cannot call it directly.

The test's SetUp duplicates the path construction logic from PortManager.GetRegistryDirectory() (which is private static). While this duplication is currently unavoidable due to the access modifier, consider making GetRegistryDirectory() internal so tests can call it directly. This would ensure the test and production code always target the same directory and prevent silent divergence if the path ever changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PortManagerTests.cs`
around lines 22 - 45, Change PortManager.GetRegistryDirectory() from private
static to internal static and update the test to call
PortManager.GetRegistryDirectory() instead of duplicating path logic; if the
test assembly is separate, add an InternalsVisibleTo attribute for the test
assembly name so the internal method is accessible. Locate
GetRegistryDirectory() in the PortManager class and modify its access modifier,
then replace the duplicated path construction in PortManagerTests.SetUp with a
call to PortManager.GetRegistryDirectory() and reuse the returned directory for
_legacyFilePath and _portFilePath handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs`:
- Line 57: _consecutiveTimeouts is incremented on timeout but never acted upon;
either document it as purely diagnostic or implement self-healing: add a
threshold constant (e.g., MAX_CONSECUTIVE_TIMEOUTS), check _consecutiveTimeouts
in the timeout handling path inside ProcessCommands (or the method that
increments it), and when the threshold is exceeded perform corrective actions
such as logging an error with the count, resetting the reentrancy guard,
re-registering or restarting ProcessCommands (or re-initializing the transport),
and resetting _consecutiveTimeouts to 0; alternatively, add a clear comment next
to the _consecutiveTimeouts declaration stating it is only diagnostic if you
choose not to implement the self-healing path.

In
`@TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PortManagerTests.cs`:
- Around line 48-57: TearDown currently only restores pre-existing files when
_savedLegacyFileContent/_savedPortFileContent are non-null, leaving files
created during tests on disk; update the TearDown method (referencing TearDown,
_savedLegacyFileContent, _legacyFilePath, _savedPortFileContent, _portFilePath
and the test flow that calls DiscoverNewPort) so that for each tracked path: if
the saved content is non-null, write it back to the file; otherwise if the path
is non-null and the file exists, delete the file to remove artifacts created
during the test run.

---

Nitpick comments:
In `@MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs`:
- Around line 38-41: CancelRetries currently only calls _retryCts.Cancel()
leaving the old CancellationTokenSource undisposed; update CancelRetries to
safely Cancel and Dispose the existing _retryCts (e.g., call _retryCts.Cancel();
_retryCts.Dispose(); _retryCts = null) to avoid leaking the CTS timer, and
ensure thread-safety with the same synchronization used around _retryCts in
ResumeStdioWithRetriesAsync so you don't race disposing a newly-created CTS;
swallow or log ObjectDisposedException if needed.
- Around line 116-124: Extract the duplicated compilation-state logic into a
single static helper (e.g., class CompilationHelper with a public static bool
IsCompiling()) that implements the current two-step check
(EditorApplication.isCompiling plus the reflection lookup of
"UnityEditor.Compilation.CompilationPipeline" and its static isCompiling
property, with safe exception handling), update StdioBridgeReloadHandler to call
CompilationHelper.IsCompiling() instead of the inline reflection block, update
HttpBridgeReloadHandler to call the same helper, and remove or refactor
StdioBridgeHost.IsCompiling() to delegate to CompilationHelper.IsCompiling() so
all three places use the centralized method for consistency.

In `@MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs`:
- Around line 642-645: Consolidate the two separate lock acquisitions on
clientsLock into a single critical section: inside one lock(clientsLock) call
remove the client from activeClients and read remaining as activeClients.Count
(i.e., perform activeClients.Remove(client) and set remaining within the same
lock) to avoid the count becoming stale; then call McpLog.Info($"Client handler
exited (remaining clients: {remaining})") (you can log outside the lock) — this
touches the code manipulating activeClients, clientsLock, and the McpLog.Info
call.
- Line 29: The eviction check measures from enqueue time (EnqueuedAtMs) rather
than execution start, which can evict commands that only recently began
executing; to fix, add a new field ExecutionStartedAtMs on the same command
type, set ExecutionStartedAtMs when IsExecuting is set to true (in the code path
around HandleClientAsync / where IsExecuting is flipped), and change the stale
eviction logic (the check that compares nowMs - EnqueuedAtMs against
staleThresholdMs) to use nowMs - ExecutionStartedAtMs when IsExecuting==true
(fall back to EnqueuedAtMs if ExecutionStartedAtMs is unset); also update the
comment that referenced "stuck with IsExecuting=true for too long" to reflect
that the eviction now measures execution duration.

In
`@TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PortManagerTests.cs`:
- Around line 22-45: Change PortManager.GetRegistryDirectory() from private
static to internal static and update the test to call
PortManager.GetRegistryDirectory() instead of duplicating path logic; if the
test assembly is separate, add an InternalsVisibleTo attribute for the test
assembly name so the internal method is accessible. Locate
GetRegistryDirectory() in the PortManager class and modify its access modifier,
then replace the duplicated path construction in PortManagerTests.SetUp with a
call to PortManager.GetRegistryDirectory() and reuse the returned directory for
_legacyFilePath and _portFilePath handling.

In `@unity-mcp-skill/SKILL.md`:
- Around line 22-31: The reference table in SKILL.md is missing the per-object
resource URI mcpforunity://scene/gameobject/{instance_id} even though the doc
later references it; add a new row to the existing "Resource | URI" table (near
the "Scene GameObject API" / "Scene GameObject" entries) with a Resource name
like "Scene GameObject (per-instance)" and the URI
mcpforunity://scene/gameobject/{instance_id}, so readers and LLMs have the exact
parameterized form documented.

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.

StdioBridgeHost enters zombie state after Python server restart — accepts connections but never processes commands

1 participant