Repository navigation
Releases: german-krasnikov/unity-biome-mcp
Release list
v2.1.0
[v2.1.0] — 2026-09-25
Changed
- CI secret gating: Unity and Sonar steps skip with notice on fork/Dependabot PRs (no secrets); on push/schedule/dispatch, missing secrets cause exit 1 error — distinguishes "unavailable by design" from "secrets lost" per event type and job.
- Python CI on every PR: Lint/Test/README check now run on every pull request — the path filter on
pull_requestwas removed, so docs-only PRs also report these checks (prerequisite for required status checks). - Server dependencies via uv and Dependabot: Switched from pip to
uvecosystem.server/uv.lockis enforced source of truth — Lint job installsruffdirectly from lock (viauv export --locked --extra dev), Test/README/badge jobs install frompyproject.tomlranges. Dependabot keeps both files in sync weekly. Lock validated viauv lock --checkin Lint. - Dev dependency group unified:
[dependency-groups].devnow self-references[project.optional-dependencies].dev(single list) —uv sync/uv runno longer silently drop hypothesis/pytest-cov/pyyaml/ruff. - SonarCloud action pinned by SHA: SonarSource/sonarcloud-github-action pinned to v5.0.0 (ffc3010...) instead of floating tag.
- Unity-compat summary accuracy: No longer reports "Compile check passed" when compile was skipped due to missing secrets.
Fixed
- SIGTERM supervisor test race: Child Python process now inherits SIG_IGN from parent (set before fork/exec) instead of racing to set it within 0.1s timeout.
- Import-linter synthetic fixtures: Grimp cache now isolated via
--no-cacheflag (was caching by package name + mtime in shared CWD, polluting parallel test runs on Windows). - Ruff version pinned and SIM117 fixed: CI was installing uncontrolled
ruff; pinned to 0.16.8 inserver/uv.lockand installed from lock in Lint job. Ruff 0.16.8 found two real SIM117 violations (nestedasync with) in the codebase — fixed.
v2.0.0
[v2.0.0] — 2026-09-08
Added
- Offline Freshness and Reload Contract Validation (N0a/Reload): Two new NUnit dotnet projects (
Tests~/AssemblyFreshness/andTests~/SourcePatchReadiness/) validate bytecode freshness detection and reload-readiness state transitions without Editor. Both use~folder convention to remain invisible to asset importer. CoversAssemblySourceFreshnessbyte comparison andSourcePatchReloadAcklease lifecycle. - Offline Adapter Compile-Contract (C1): Pure dotnet test project
Tests~/MutationAdapterContract/validates the SourcePatch seam contract and adapter Apply outcomes without Unity Editor. Executes on every build viaci-pure-dotnet.yml. Script-fetched adapter sources verified by SHA256 atomic lock inscripts/gauntlet/fetch_adapter_sources.py; seam-drift negative control confirms renamed seam members break the build. - Mutation Regression Test Lane (A5–A8): Comprehensive off-live Python test suite (
server/tests/mutation/) covering FSR provider lifecycle, source-patch adapter, and reload recovery in isolation. Stories S11–S16c validate explicit disable, source restore, provider absence, re-add, and removal workflows; harness awaits Editor ready state before guard-vulnerable calls. Run viapytest tests/mutation -m mutation_liveorscripts/run_mutation_regression_cell.py --mode full(driver 1800 s budget). CI lane.github/workflows/mutation-regression.ymlsupersedes legacyfsr-qualification.yml. - Provider Lifecycle Regression Tests (S16b–S16c): Remove/re-add FastScriptReload provider package in a live session; verify compile clean, provider state transitions, no orphan port/state files, epoch monotonicity, and port persistence after reload.
- SDK Qualification Tests for Playtest Outcomes (SDK-E04): New
server/tests/live/test_sdk_playtest_outcomes.pyandtest_sdk_scene_cache.pywithsdk_toolsandsdk_runtimefixtures validaterun_playtestverdict accuracy across sync/async routes and cache invalidation after scenario terminal events.
Fixed
- Batch Body-Line Normalization: Every successful
batchcommand now emits[N] ok: <data>instead of a bare[N] okwhen there was no data to report — Python consumers that already parse theok:prefix now see a consistent format on every body line. Error lines ([N] err: ...) are unchanged. - Playtest Play/Stop Reconciliation After a Lost Acknowledgement: If the acknowledgement for an
editor play/stoprequest is lost (connection drops right as the write lands), the outcome is now reconciled by polling the retry-safeget_statusread until the expectedplaying=state is observed, instead of surfacing the connection error to the caller — the play/stop command itself is never resent. This relies on the bridge's reload guard already letting read-only status probes (get_status,compile_status,sync_status) through during a domain reload, so reconciliation can complete without waiting out the full reload-expiry window. - N1b — sync/force_refresh Registered as Mutating, Non-Batchable: The
syncandforce_refreshwire commands defaultedmutating/notBatchableto false at registration, so the read-only gate, Play Mode mutation gate, and batch safety check never saw them as unsafe — they executed and triggeredRefresh/RequestScriptCompilationfrom live Play Mode or read-only state instead of being refused pre-dispatch. Both are now registeredmutating: true, notBatchable: true; a batch line naming either is refused instead of executed, and inaskchat mode they are blocked bySessionAuthorizationas a secondary defence (the Pythonsync_unitybroker already blocks direct calls as a write).sync_statusis unaffected and stays a read allowed during compile. - A8 — sync_unity Compile-Guard Absorption: When Editor auto-refresh starts compiling before
synccommand arrives, the C# guard rejection "Unity is compiling. Retry in 5s." is now absorbed bysync_unity. Instead of surfacing as an unhandled error,_sync_unityrecognizes the guard text (constantSYNC_COMPILE_GUARD_TEXT), adopts the sync epoch, and runs the same wait/verdict path as a normal acknowledged sync. Any other error text propagates unchanged. Affects agent visibility: no more unhandled "Unity is compiling" when auto-refresh started compiling first. - N0b — Playtest Outcome Raising Contract:
run_playtestnow raisesToolErrorwith the full report text for ANY non-pass outcome (fail/aborted/malformed/empty) on both sync and async routes. Pass outcome returns normally (text or JSON format). Eliminates silent failure absorption and provides consistent error signal across all dispatch paths. - N0a — Cache Trust Guard During Async Playtest (Middleware._scenario_uncertain): Set on
start_playtestack, cleared only by terminal evidence (is_scenario_terminal) or Edit Mode transition. While set, PrefetchCache lookups are bypassed and background prefetch is suppressed. Survives reconnect andreset_session()(reconnect does not prove playtest stopped). Fallback_force_scene_invalidateclears caches but not the guard. - N4a — Chat Ask Atomicity:
PendingAskRegistry.Completenow performs atomicTryRemovewith TCS captured beforeonAskEvent. Fail-soft try/catch added toChatBackendProbe.IsChatBackendRunningandChatSettingsHookmethods to prevent stale probe exceptions from blocking ask dispatch.
Breaking
- N1a — Plugin Registration Guards: A Python plugin declaring a reserved builtin name, a name colliding with a host tool, or a name it does not own (declared via
register_read_cmds/register_write_cmdsbut never registered as a tool) is now rejected ENTIRELY — zero commands, gating entries, or budget features survive. Previously such a duplicate/foreign name was silently skipped, letting the rest of the plugin load. On the C# side,CommandRegistry.Registerrefuses a duplicate command from a different plugin instance andPluginRegistry.RegisterAllPluginsrolls back that plugin's entire registration; a plugin ID conflict (PluginRegistry.Register) refuses a different instance under an existing ID while re-registering the same instance stays idempotent. Failures are queryable viaget_failed_plugins()(Python and C#, mirrored) and logged at registration time. Plugin command inventory is now attributed by the registering plugin's identity instead of name prefix, soAdditionalCommandsand prefix-free commands are correctly grouped under their owning plugin indiscover_tools/build_helpoutput. Plugins that previously loaded with a silently-skipped duplicate must be fixed to use non-colliding, owned names.
Changed
- CI Unity Version Matrix Refresh: Canonical worker baseline bumped 6000.0.65f1 → 6000.0.83f1 (LTS latest patch). Compat matrix (
unity-compat.yml) gains a 6000.6 compile-check cell (previously missing) and its 6.7 alpha pin moves 6000.7.0a4 → 6000.7.0a6. FSR/mutation qualification lock (scripts/fsr_qualification_lock.jsonu_min) stays frozen at 6000.0.65f1 for this release's already-qualified mutation evidence; re-qualification on the new baseline is tracked as follow-up. - UPM Package Dependency:
unity-plugin/package.jsonnow declarescom.unity.nuget.mono-cecil 1.11.5as required dependency. Ensure project registry resolvescom.unity.nuget.*packages (standard Unity configurations include this by default). Used internally for assembly analysis during reload and compile verification.
Known Issues
- FSR Fork Adapter Object Leak: The FastScriptReload fork adapter (
BiomeSourcePatchDispatcher) leaks oneHideAndDontSaveGameObject per enable→patch→disable cycle in mutation-mode sessions. No functional impact; the leak is confined to disposable mutation workers. Fix pending upstream in the provider fork (tracked as an imperativexfail, S16). sync_unityPost-Resolve Settle Time: After a Package Manager resolve,sync_unitymay take up to ~80 s to settle while Unity finishes asset-importing before the compile/reload verdict is available. Latency only, not a correctness issue.AssetDatabaseHelper.WriteTextUTF-8 BOM: Text written via thewrite_textMCP tool (.cs/.shader/.txtfiles) is encoded with a UTF-8 BOM instead ofJsonHelper.Utf8NoBom. Unity and most tooling handle a BOM-prefixed file correctly; no functional impact observed.
v1.54.0
[v1.54.0] — 2026-09-06
Added
- Modular Architecture Phase 2 — Plugin Registration Atomicity (F3): Failed plugin registration no longer leaves stale callable commands, gating entries, or budget features. Python:
_atomic.pysnapshots and restores all registries on failure. C#:PluginRegistry.CaptureForTest/RestoreForTestper-plugin;CommandRegistry.Registerthrows on duplicate whenCallerIsPlugin. Invariant: a plugin that throws duringregister()leaves zero observable side effects - Modular Architecture Phase 2 — Self-Contained Module Pilot (PR-04):
WatchModuleowns its registration metadata via declarativeSPEC_KWARGSmerged at import time. Instance-scoped_send/_argsinjection proves module isolation. C#CommandRegistry.EntrygainsMutatingArgsPolicydelegate andNotBatchablebool;uitk_filehardcoded name-switch migrated to registration site. 36 policy-vector tests cover read/write, batch, and retry-safety for 5 representative commands. Invariant: adding a command requires only the module's registration — no central list edits - Modular Architecture Phase 2 — Reload Module Facade with Replaceable Algorithm (PR-04R):
SyncHelper.IsMainAssemblyCompilingis now an injectable predicate (the identified architectural seam).SourcePatchModePolicy.RequestDisable()delegates viaISourcePatchReloadPortinstead of directSyncHelper.TriggerSynccall. Import-linter forbids diagnose/reload_ladder from importing sync.py. Invariant: Reload algorithm can be swapped via composition without edits to core, transport, Chat, or SourcePatch - Modular Architecture Phase 2 — Chat Off as Real Disconnection (PR-05): Removed stale reflection probes in
ChatBackendProbe/ChatSettingsHook; Chat assemblies now wire owned delegates via[InitializeOnLoad].ask_userreturns immediately when no Chat subscriber exists (no 300s hang).search_contextthrowsProviderUnavailableException(classified UNAVAILABLE, logged as warning) when Chat.CLI absent. Nine Chat-dependent test files moved from base to Chat.Tests.CLI/View; base asmdef no longer references Chat. Invariant: Chat Off/absent leaves MCP tools fully operational; no stale process, pending-ask leak, or false-positive error - Modular Architecture Phase 2 — SourcePatch Off/Absence Proof & Reload Boundary (PR-06):
ForceUnreconciledForTests()seam onSourcePatchHostenables contract tests. Two C# tests prove real lazy-reconciliation to Off and concurrent-sync epoch drift resolves to Recovery (not false-Off). Python boundary scan locksSyncHelper.TriggerSyncout of SourcePatch control files except sanctioned adapter. Invariant: SourcePatch Off leaves normal writes and domain reload unintercepted; provider/algorithm swaps without core changes - DSL Conformance Oracle Strengthened (PR-07): Strict error classification in DSL harness. Fixed ci-conformance.yml lane-generation to use
gen_lane_args.py. Add zero-selection fail-closed guard. Documented reserved header fields (NeedsPlaymode,ExpectSteps,ExpectFailed,SuiteOnly) as parsed but unenforced. NewPreparedPlaytestimmutable container for data isolation.ConsoleErroredfield serialized in receiptToJson(additive, backward-compatible). Invariant: DSL conformance tests classify errors specifically; prepared playtest data is immutable; lane selection cannot silently produce empty set - Coverage Hotspots and Clone Detection (PR-08): New
opencover_parse.py(OpenCover XML → method-level cyclomatic complexity + coverage),coverage_hotspots.py(changed-aware top-20 report with scenario map),cochange_report.py(manual cochange analysis). Configurejscpdv5.1.2 with baseline-from-ref and fail-on-new-clones gate in ci-python.yml. Coverage-hotspots CI job reports todocs/quality/on master push. Invariant: new substantive code duplicates fail CI; coverage hotspots ranked by changed × complexity × uncovered
Fixed
- F7 — Honest Terminal Outcome for Playtest Verdict:
PlaytestStepReceipt.Oknow accounts for console errors detected after the step passes. Both text and JSON verdict consumers reject ABORTED runs, receipts with contradictory aggregate/step ledgers, and empty/malformed required receipts. Invariant: abort, console-error-on-passing-step, cleanup failure, and empty receipt are non-PASS in all formats (text/JSON) and all paths (sync/async start-poll) - F8 — Unified Read-Only Policy Across All Routes: Remove
_INTERNALcategory filter fromWRITE_CMDSthat allowedstart_playtestandsource_patch_writeto bypass read-only gate. Reclassify ping/get_version as explicit reads. Addunknown_is_writefail-closed fallback at both authorization call sites. Mirrorstart_playtestintoSCENE_STATE_NEUTRAL_WRITESto matchrun_playtest. Invariant: read-only blocks mutating commands regardless of sync/async route, warmup state, or category - F1 — Player Profile Preflight Rejects Unsupported DSL Before Execution:
PlayerProfilePreflight.Validate(ParseResult)in engine-free Playtest.Core checks fatal parse errors,@needs editmodeheader, SETUP/TEARDOWN blocks, EXPECT_FAIL, compound WAIT_UNTIL AND/OR, ASSERT...TIMEOUT retry semantics, and empty main section.PlayerPlaytestRunner.Run()wraps load+parse in try/catch and delegates gate decision to preflight. Invariant: unsupported Player scripts are rejected with terminal receipt before any body action executes - F2 — Conservative Scene Cache Invalidation After Playtest Scenario: Split
SCENE_STATE_NEUTRAL_WRITESinto two policies: "counts as blind write for advisory guard" (unchanged) vs. "can make caches stale" (newis_scenario_terminalpredicate).Middleware.invalidate_scene_caches()uses_scene_generationcounter to fence late in-flight background prefetches from re-poisoning cleared caches. Cover sync, async start/poll, suite, wire-exception, and poll-give-up paths. Invariant: a public read immediately after playtest scenario completion returns fresh scene state
Changed
- Architectural Boundaries Established: Import-linter contracts mechanically prevent tools→server, tools→middleware, bridge→session, plugin_api→server imports. Static asmdef boundary checks for Playtest.Core (
noEngineReferences) and SourcePatch (no provider refs). CI validates architectural integrity on every build - Hygiene: Replaced blocking
.ResultwithawaitinCommandRouterAskUserTests(test-only)
Test Results
- Modular architecture phase 2 suite: All policy vectors green; plugin atomicity verified; chat/sourcepatch boundaries confirmed
- DSL conformance: Reserved field parsing confirmed; Player preflight gate validated
v1.53.0
[v1.53.0] — 2026-09-05
Added
- Async playtest dispatch (Wave E, E02–E04): Non-blocking
start_playtest+ pollingget_playtest_runfor scripts withtimeout > 120s; threshold_RUN_PLAYTEST_SYNC_CEILING_S(Python) andRunPlaytestTimeoutSeconds(C#) prevent single blocking TCP calls from timeout. Transparent routing inrun_playtest()viaplaytest_async.run_via_start_poll()(R-04); run ID persisted inPlaytestReceiptStoreacross domain reloads - PlayMode corpus and CI job (Wave E, E06a–E06b): 3 Play-bound
.playtestfiles inunity-test-project/Assets/Tests/PlayMode/viaPlaytestCorpusPlayModeTests; CI laneunity-tests.ymlPlayMode job with-testPlatform PlayMode --filter PlaytestCorpusPlayModeTestsfilter; validates runtime coroutines and Play-only DSL operations - Player fan-out runner (Wave E, E07–E09): New
scripts/run_player_playtests.pywith--jobs Nparallelism; filters.playtestby@needs playertag; builds standalone Player for Linux/macOS/Windows; runs sequentially or in parallel; returns matrix report. 6.playtestfiles inStreamingAssets/Playtests/under CI nightly-full lane - Honest test coverage (Wave E, E11): Updated taxonomy and CI lanes: 9 EditMode corpus files + 3 PlayMode corpus files + 6 Player files = 18/22 total coverage. Python
.suitelane stress-tests stateful A→B→C runner viarun_playtest_suite(tag="@suite-only") - MCP verb DSL steps:
MCP <cmd> key=value [INTO $var]executes MCP commands from playtest scripts; $RUN_ID injected as parallel-safe unique identifier per run; EXPECT_FAIL modifier inverts pass/fail for single step; format=json outputs structured per-step receipt objects with command, status, and response data persisted for archival - EditMode DSL execution:
# @needs editmodeheader opt-out of Play Mode gate;PlaytestRunner.Run(..., requiresPlayMode: false)runs steps throughEditorApplication.updateticks (not delayCall) via centralizedMainThreadDispatcher; MCP mutations validated at dispatch time - Playtest corpus carriers: 9 Edit-capable MCPFeedbackFixture files run through
PlaytestCorpusEditModeTests(B21); 3 Play-bound-only files (C_shared_finish,DSL_types,I3_independent_pass) run throughPlaytestCorpusPlayModeTests(B22) for runtime coroutine validation; Python.suitelane stress-tests stateful A/B/C chain viarun_playtest_suite(tag="@suite-only") - Test taxonomy and lanes (data-driven):
Tests/taxonomy-map.json(C13) canonical cross-language taxonomy (pytest markers, C# TestCategories, DSL @needs values);Tests/biome-test-lanes.json(C15) 4 lanes (pr-python-core, pr-unity-core, master-conformance, nightly-full) with filter configs;scripts/check_test_metadata.py(C18) enforces unknown category/tag = CI failure - Test selection flags in runner:
run_unity_tests.pynow accepts--categoryand--assemblyfor targeted test runs;--tests-fileallows filtering from a newline-delimited file with validation and deduplication by canonical form - Test run retention and pruning:
TestRunStore.PruneOldRunsimplements 50-run cap with 7-day staleness floor; runs older than 7 days are retained only if under 50 total - Dispatch-failure visibility:
TestRunReconcilernow surfaces the exact failure reason indispatch_issuefield instead of generic dispatch status - Tag filtering for playtest suites:
run_playtest_suite(tag=...)filters files by# @tagsheader value; zero matches fail-closed - Cassette recorder for protocol testing: New
bridge_cassette.pymodule records and replays TCP exchanges for deterministic wire protocol testing - Full baseline migration:
full-baseline.jsonreplaces hardcoded 6001 Unity version constant - Engine-free Core assembly (Wave D): New
UnityMCP.Playtest.Coreruntime assembly withnoEngineReferences: true(D16) extracts parser, Compare logic, and utilities from Editor; consumed by both Editor playtest runner and Player runtime without Unity dependency. Enables pure dotnet testing lane and player builds. - Unified playtest parser (Wave D, D14, D18):
PlaytestParser.csmoved from Editor to Core and split into logical files (Directives, Internals, Mcp, Subroutines). NewPlaytestHeaderScannerparses DSL headers at load time. UnifiedCompare()logic serves Editor DSL, Player runtime, and pure dotnet tests. - Pure dotnet test lane (Wave D, D15): New
unity-plugin/Tests~/Pure/UnityMCP.Playtest.Core.Tests.csprojruns NUnit tests outside Unity usingdotnet testwith zero dependency on Editor or runtime. Tilde folder prevents Microsoft.NET.Test.Sdk from breaking Editor compilation. Validates parser correctness and Compare parity. - Player runtime playtest support (Wave D, D17):
PlayerPlaytestRunnerreuses Core parser and validates scripts via pre-scan gate that rejects unsupported verbs (MCP, MOVE, CLICK) and# @needs editmodebefore execution. Supports 9 core step types: ASSERT, ASSERT_BATCH, ASSERT_NEAR, ASSERT_CONSOLE_CLEAN, WAIT, WAIT_UNTIL, LOG, SNAPSHOT, INVOKE. - Numeric utilities (Wave D): New
Float3.csimmutable 3D vector type andNumericParsing.csutilities for decimal/float parsing with culture-invariant rounding.IAliasSourceinterface (D20) allows custom alias providers to inject VAL definitions at runtime.
Changed
- PR-lane Python test speed: 262s → 35s via selective test-class filtering and focused pytest markers (M-wave-a)
- Test run polling compaction:
get_test_runresponses omit full detail for non-terminal states, keeping compact snapshots until terminal verdict - CI timeout consistency: Timeout-minutes now explicitly set across all job lanes; Linux-only Unity compile on PR lanes (v1.17.0 contracts)
- Library/.meta cache strategy: Library artifacts cached across runs with mtime validation; .meta GUID conflicts pre-checked on import
Test Results
- C# EditMode: 9,310 passed
- Python PR-lane: 35s (35× speedup)
v1.52.0
[v1.52.0] — 2026-09-03
Fixed
- Test run timeout on Editor focus loss or domain reload:
run_tests_wait/get_test_runno longer hang indefinitely when the Editor loses focus or undergoes a domain reload. Implemented disk-fallback protocol and health-value gates to detect stalled test dispatch and recover via direct test-run store reads; durable protocol now includes explicithealthfield andexpected_countfor safer gate validation - Zero-match filter produces silent failure or stuck
Finalizingstate: Running tests with a filter that matches no tests (e.g., typo in filter name) now returns an explicitZERO_TEST_MATCHissue in the result, allowing the next run to proceed instead of silently hanging or remaining inFinalizingstate; added 180-second ceiling onFinalizingexpiry tied to execution boundary events await_compileandget_compile_errorsreturn stale cached results: Removedget_compile_errorsfrom_READ_CACHEABLEandrecompilefromGATE_PRIORSso each call after a C# edit immediately polls live compilation status instead of returning outdated[CACHED]responses- Ghost Unity process appears alive to status checks: Compile paths (
get_compile_errors,await_compile,sync_unity) now return explicitUNITY-UNREACHABLEerror when the Editor process is hung or dead;mcp_statusexposesunity_status=unreachablevia improved liveness detection (~30-second ping intervals) - After Editor restart, Python client loops on connection refused when port changes: Added fast-path port rediscovery with explicit
[port changed]warning when the Editor binds to a different TCP port after restart, eliminating repeatedConnectionRefusedcycles; corrected off-by-one error in MCPServer.StartAsync bind-retry loop boundary so the final allowed retry actually attempts the original port instead of immediately falling back to free-port allocation - "Level Up!" button in Setup Wizard hangs on double-click and shows
Unable to add packageerror: UPM operations now guarded by a single in-flight-state checker that disables the button during updates; safe to re-click immediately.UpmErrorClassifierandUpmOperationGuardprevent phantom "package add already running" errors - Plugin or server update overwrites manual changes to
.env, custom config keys, or TOML sections: Configuration files now use atomic swap (File.Replace) instead of delete-then-move, preserving concurrent edits even under file-locking scenarios (Windows OneDrive/antivirus). Deep-merge strategy for TOML config; per-targetLastSyncedVersiontracking prevents unnecessary rewrites - Random HTTP/TLS probes on the TCP port flood the console with repeating desync warnings: Added
DesyncWarnLimiterto classify known foreign protocol probes (HTTP, TLS handshakes) and emit at most one warning per 30 seconds, reducing console noise uitk_fileaction on Windows fails reading.uxml/.ussfiles withescapes Assets/path validation error: Path escaping now correctly handles Windows backslashes (\) as part of the canonical path, not as escape sequences; fixedToAbsPathresolution for relative UXML/USS includes- Setup Wizard enables configurations for all detected agent tools (VS Code, Codex) even if not installed: Wizard now only enables agent configs for tools that are actually installed, preventing stale or phantom agent entries in the MCP configuration
Changed
- Main-thread dispatcher refactored to eliminate timeout windows: Replaced
EditorTickOncewith a centralizedMainThreadDispatcherthat usesEditorApplication.updatedirectly (notdelayCall), making it immune to focus loss and domain reload hangups. Dispatcher runs snapshot-bounded Drain cycles with per-action exception handling;delayCallnow restricted to GUI-only contexts (Chat, Wizard, menus) - Atomic file writes for all config and state files:
MCP_Port.json,{pid}.port, editor state, wizard config, and durable test-run store now useAtomicFile.Swap(temp file +File.Replace) to prevent data loss under concurrent file-locking (Windows OneDrive/antivirus/network paths) - Durable test-run protocol gates for recovery: Added
health(no_test_progress, editor_unresponsive) andexpected_countfields to test-run results for safer terminal-state detection;ZERO_TEST_MATCHwarning flags empty-filter matches. Disk fallback and lifecycle-gate self-heal allow runs to recover without stalling - Error classification on unreachable Editor: Compile paths return explicit
UNITY-UNREACHABLEerror instead of timeout;mcp_statusreportsunity_status=unreachablealongside liveness values (connected,connected-stalled, etc.)
Documentation
- Updated
AI/architecture.mdto documentMainThreadDispatcher,AtomicFile.Swap, durable test-run protocol gates, and error classification - Updated
AI/structure.mdwith new C# modules:AtomicFile.cs,MainThreadDispatcher.cs(replacingEditorTickOnce), hygiene test guards, and desync limiters
Test Results
- C# NUnit: 9,276 passed / 0 failed / 34 skipped (×2 runs, run-6bf0491c, run-9b12374c; health=healthy, build_coherent)
- Python live: 278 passed / 0 failed
- Python conformance/seams: 290 passed / 42 failed (pre-existing; byte-identical to master):
- 41 failures: seams batch completeness/surface tests —
BatchHelper.csv1.41.0 (d3fc6c9) writes[N] {result}while test expects[N] ok:; unrelated to this fix - 1 failure:
test_screenshot_succeeds— empty screenshot on background Editor; unrelated to this fix - Note: seams live suite does not run in CI (
ci-conformance:-m "not live")
- 41 failures: seams batch completeness/surface tests —
- Repository unit: 1,180 passed / 1 failed (pre-existing:
test_installed_runtime_lease.py::test_installed_entrypoint_serves_public_stdio— environment venv issue without mcp SDK, byte-identical to master) - Install unit: 89 passed / 1 skipped
- Server non-live unit: 6,857 passed / 1 skipped / 634 deselected
- Wire protocol: 26 passed
- Code quality (ruff): All checks passed
v1.51.0 — Mutation Mode (Source Patch MVP)
v1.51.0 — Mutation Mode (Source Patch MVP)
This release is dedicated to Mutation Mode — optional hot method-body patching for the Unity Editor. An AI agent (or you) can edit the body of a C# method and see the change live, with no domain reload, no lost state, no waiting.
What it does
While Mutation Mode is ON, asset(action="write_text") on a .cs file routes through an optional in-memory patching provider (a pinned fork of the excellent open-source FastScriptReload). The edited method's new body takes effect immediately in the running Editor: statics survive, scene state survives, iteration cost drops from a full reload cycle to effectively zero.
Quick start
- Install the provider (optional package; the base plugin never depends on it) — add to
Packages/manifest.json:"com.handzlikchris.fastscriptreload": "https://github.com/german-krasnikov/FastScriptReload.git?path=/Assets#b90a5c3fd7cfa452f23e8a807cc7bd61dc934bbf"
- Enable — either
editor(action="mutation_mode", enable=true)via MCP, or the new Mutation Mode (experimental) checkbox in MCP Settings Hub → General. - Edit method bodies via
asset(action="write_text")— applied instantly. Disable withenable=false(exactly one script reload, files stay as written).
Without the provider the checkbox is disabled with a tooltip and enabling returns a clear error — fail-closed by design.
Safety model
- Fail-closed everywhere: invalid edits are rejected before any file effect; any uncertainty transitions to a Recovery state instead of guessing. Recovery now has a legal exit (
enable=false→ one reload). - Path validation: traversal (
..), absolute paths,Packages/, non-.cstargets are rejected pre-effect (SourcePatchPathGuard). - State machine:
Unavailable → Off ↔ OnReady/Busy ↔ Recovery, receipt-based reconciliation after every domain reload (mode auto-resets to Off — no stale sessions). - Transport: internal read-only wire commands are now correctly retry-safe; unsafe commands are never resent after uncertain delivery.
Qualified scope (MVP)
Existing sync non-generic methods in Assets/, body-only edits, single file at a time, plain C# classes (POCO), Edit Mode, Mono backend, Unity 6000.0.65f1.
Platforms: macOS ARM64 + Linux x64 (CI-qualified — two unchanged Aggregate-PASS runs of the qualification matrix); Windows x64 engineering-supported (CI qualification blocked by headed-GUI unavailability on hosted runners).
Not supported (yet): MonoBehaviour-derived method mutation, async/iterator/lambda/generic/overloaded methods, Play Mode mutations, multi-file edits. All are fail-closed, not silent.
Also in this release
- UI: Settings Hub toggle with live state polling (Provider Absent / Busy / Disabling / Recovery / Play Mode disabled states, recovery warning).
- Observability:
mcp_statusexposessource_patch_intent,source_patch_provider,source_patch_state,source_patch_op,source_patch_recovery. - Fixes: ON-path writes no longer trigger spurious
[ScriptCompilation]requests (raw write instead of ImportAsset); AutoRefresh lease no longer leaks on Recovery entry; retry-safe classification for internal wire commands (get_status,sync_status). - Docs: new user guide
docs/features/mutation-mode.md(workflow, provider install, limitations, FAQ) plus updated architecture/testing internals docs.
Verification
Release evidence: local qualification cycles + CI matrix runs (see AI/source-patch-release-evidence.md), live lane 278/0/0, server non-live lane 6630 passed, cross-project fault injection 4/4, full SourcePatch C# namespace 147/148 (1 pre-existing guard skip).
v1.50.8
[v1.50.8] — 2026-08-23
Fixed
- NoTestsMatched outcome rank:
TestRunFinalizationCoordinator.OutcomeRank()and
TestRunStore.IsTerminalOutcome()now recogniseNoTestsMatchedas a terminal outcome.
Zero-test filter runs no longer downgrade toIncompleteand throwTestRunStoreException.
v1.50.7
[v1.50.7] — 2026-08-23
Changed
- Mutation Mode scope clarified (MCPAUDIT-007, MCPAUDIT-009): Error messages in
BatchHelper.csandCommandRouter.csnow redirect Play Mode mutation blocks to
execute_code(persist_as=...).EditorStateHelper.csno_hot_reload_packagewarning
now states: Mutation Mode controls WHEN reload happens, not WHETHER — .cs edits still
trigger a full domain reload without the Hot Reload package.
v1.50.6
[v1.50.6] — 2026-08-23
Fixed
- sync_unity false-green on warn: response (MCPAUDIT-015): Unity-side
warn:anderr:ack
strings now raiseToolErrorimmediately instead of silently falling through to_parse_ack.
Prevents false "sync clean" when compilation is blocked (e.g. Play Mode script edit). - Non-string ack guard:
sync_unityraisesToolErrorif bridge returnsNone(timeout
sentinel) instead of propagatingAttributeErrorfrom_parse_ack. - editor() schema exposes all actions (MCPAUDIT-004):
actionparameter now uses
Literal["state","play","pause","stop","select","project_path","fast_play_mode","mutation_mode"]
so schema-validating providers see the full enum.
v1.50.5
[v1.50.5] — 2026-08-23
Fixed
- Status conflation resolved (WS-MCP-249):
mcp_status.mutation_modenow reports
MCPSettings.GetMutationMode()directly instead ofHotReloadDetector.IsActive()— external
AutoRefresh changes no longer falsely report Mutation Mode active. - FastPlay ownership (WS-MCP-249):
FastPlayOwnerflags enum (User=1,Mutation=2) tracks
who owns the Play Mode setting — Mutation OFF no longer disables user-owned Fast Play Mode. - Python cache removed (WS-MCP-249):
_mm_cachedeliminated fromsync.pyand
code_intel.py— every call re-readsget_statusfresh.