Skip to content

Conversation

@whatevertogo
Copy link
Contributor

@whatevertogo whatevertogo commented Jan 16, 2026

#553

ActionTrace System - Feature Overview

Overview

ActionTrace is a comprehensive Unity Editor event tracking and replay system. It captures, stores, and queries editor operations for debugging, analysis, and undo/replay capabilities.


Core Functionality

1. Event Capture

  • Passive Monitoring: Hooks into Unity editor events without interfering with normal workflow
  • Event Types: Component changes, asset operations, GameObject modifications, property changes
  • Sampling: Configurable sampling middleware to reduce noise and overhead

2. Event Storage

  • In-Memory Store: Fast, circular buffer with configurable capacity
  • Event Enrichment: Adds semantic context (category, importance, intent)
  • Global IDs: Cross-session references using instance IDs and GUIDs

3. Query & Analysis

  • Filtering: By event type, time range, importance level
  • Aggregation: Transaction grouping (e.g., all operations within one Undo group)
  • Summarization: Human-readable descriptions of complex operations

4. Semantic Analysis

Component | Purpose -- | -- Categorizer | Classifies events (scene edit, asset import, etc.) Scorer | Assigns importance (critical, high, medium, low) Intent Inferrer | Derives user intent from action patterns

Design Principles

  1. Non-Invasive: Passive capture mode with zero impact on workflow
  2. Extensible: Plugin-based descriptors for custom event types
  3. Performant: Sampling middleware and circular buffer limit memory
  4. Context-Aware: VCS integration and nested context tracking
  5. LLM-Friendly: Structured events optimized for AI understanding

Use Cases

  • Debugging: "What changed since I opened this scene?"
  • Audit Trail: "Who modified this asset and when?"
  • Teaching: Step-by-step replay of complex operations
  • AI Assistance: LLMs can understand editor context for better help
  • Crash Recovery: Identify what operations preceded an issue

Here is the English translation of your architecture overview, maintaining the original formatting and structure:

Data Storage Location

Storage Architecture

┌─────────────────────────────────────────────────────────┐

│ In-Memory Storage │

│ ┌─────────────────────────────────────────────────┐ │

│ │ List<EditorEvent> _events │ │

│ │ - Latest 100: Full payload (Hot Data) │ │

│ │ - Older events: Dehydrated payload=null (Cold) │ │

│ └─────────────────────────────────────────────────┘ │

│ ┌─────────────────────────────────────────────────┐ │

│ │ List<ContextMapping> _contextMappings │ │

│ └─────────────────────────────────────────────────┘ │

└─────────────────────────────────────────────────────────┘

│ Delayed Save (delayCall)

┌─────────────────────────────────────────────────────────┐

│ Library/McpState_timeline_events.json │

│ (JSON Serialization, Persistent across Domain Reload)│

└─────────────────────────────────────────────────────────┘

Specific Locations

Type | Path | Description -- | -- | -- Runtime Memory | List | Thread-safe collection Persistence | Library/McpState_timeline_events.json | JSON format, auto-saved

Summary by CodeRabbit

  • New Features
    • Added ActionTrace editor window to view and inspect editor action history.
    • Added tools to query editor events with filtering options.
    • Added ability to record AI notes and undo to specific action trace sequences.
    • Improved dependency path detection for UVX and Python.

✏️ Tip: You can customize this high-level summary in your review settings.

whatevertogo and others added 23 commits January 11, 2026 19:30
Fixes CoplayDev#538

The System Requirements panel showed "UV Package Manager: Not Found" even
when a valid UV path override was configured in Advanced Settings.

Root cause: PlatformDetectorBase.DetectUv() only searched PATH with bare
command names ("uvx", "uv") and never consulted PathResolverService which
respects the user's override setting.

Changes:
- Refactor DetectUv() to use PathResolverService.GetUvxPath() which checks
  override path first, then system PATH, then falls back to "uvx"
- Add TryValidateUvExecutable() to verify executables by running --version
  instead of just checking File.Exists
- Prioritize PATH environment variable in EnumerateUvxCandidates() for
  better compatibility with official uv install scripts
- Fix process output read order (ReadToEnd before WaitForExit) to prevent
  potential deadlocks

Co-Authored-By: ChatGLM 4.7 <noreply@zhipuai.com>
- Read both stdout and stderr when validating uv/uvx executables
- Respect WaitForExit timeout return value instead of ignoring it
- Fix version parsing to handle extra tokens like "(Homebrew 2025-01-01)"
- Resolve bare commands ("uv"/"uvx") to absolute paths after validation
- Rename FindExecutableInPath to FindUvxExecutableInPath for clarity

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…s PATH augmentation

Replace direct Process.Start calls with ExecPath.TryRun across all platform detectors.
This change:
- Fixes potential deadlocks by using async output reading
- Adds proper timeout handling with process termination
- Removes redundant fallback logic and simplifies version parsing
- Adds Windows PATH augmentation with common uv, npm, and Python installation paths

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The version extraction logic now properly handles outputs like:
- "uvx 0.9.18" -> "0.9.18"
- "uvx 0.9.18 (hash date)" -> "0.9.18"
- "uvx 0.9.18 extra info" -> "0.9.18"

Uses Math.Min to find the first occurrence of either space or parenthesis.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add absolute path resolution in TryValidatePython and TryValidateUvWithPath for better UI display
- Fix BuildAugmentedPath to avoid PATH duplication
- Add comprehensive comments for version parsing logic
- Ensure cross-platform consistency across all three detectors
- Fix override path validation logic with clear state handling
- Fix platform detector path resolution and Python version detection
- Use UserProfile consistently in GetClaudeCliPath instead of Personal
- All platforms now use protected BuildAugmentedPath method

This change improves user experience by displaying full paths in the UI
while maintaining robust fallback behavior if path resolution fails.

Co-Authored-By: GLM4.7 <noreply@zhipuai.com>
- Rename TryValidateUvExecutable -> TryValidateUvxExecutable for consistency
- Add cross-platform FindInPath() helper in ExecPath.cs
- Remove platform-specific where/which implementations in favor of unified helper
- Add Windows-specific DetectUv() override with enhanced uv/uvx detection
- Add WinGet shim path support for Windows uvx installation
- Update UI labels: "UV Path" -> "UVX Path"
- Only show uvx path status when override is configured

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement a comprehensive ActionTrace system that captures, stores, and queries Unity editor events for debugging, analysis, and undo/replay capabilities.

**Core Features:**
- Event capture layer with hooks for Unity editor events
- Context tracking with stack and timeline support
- Event store with in-memory persistence and query capabilities
- Semantic analysis (categorization, scoring, intent inference)
- VCS integration for version control context
- Editor window with UI for visualizing events
- MCP tools for remote query and control

**Components Added:**
- Capture: ActionTraceEventEmitter, EventFilter, PropertyChangeTracker, UnityEventHooks
- Context: ContextStack, ContextTimeline, OperationContext, ContextMapping
- Core: EventStore, EditorEvent, EventTypes, ActionTraceSettings, GlobalIdHelper
- Query: ActionTraceQuery, EventSummarizer, TransactionAggregator
- Semantics: DefaultCategorizer, DefaultEventScorer, DefaultIntentInferrer
- UI: ActionTraceEditorWindow with UXML/USS styling
- MCP Tools: get_action_trace, get_action_trace_settings, add_action_trace_note, undo_to_sequence

**Server-side:**
- Python models and resources for ActionTrace
- MCP tools for querying events, managing settings, and undoing to sequence

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ility

- Extract EventStore.Context.cs for context mapping management
- Extract EventStore.Diagnostics.cs for memory diagnostics and dehydration
- Extract EventStore.Merging.cs for event deduplication logic
- Extract EventStore.Persistence.cs for save/load and domain reload survival
- Add PropertyEventPayloadBuilder helper for consistent payload structure
- Add PropertyFormatter helper to eliminate code duplication
- Adjust ActionTraceSettings defaults (MaxEvents: 800, HotEventCount: 150)

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

Sorry @whatevertogo, your pull request is larger than the review limit of 300000 diff characters

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 16, 2026

📝 Walkthrough

Walkthrough

This PR introduces a comprehensive ActionTrace system for MCPForUnity that captures, filters, stores, and queries editor events (component changes, asset operations, scene management, builds, script compilation) with configurable sampling, event merging, semantic analysis, and a Unity Editor window for visualization. It includes event capture hooks, context tracking, undo group management, Python tools for remote querying, and multi-layered settings with presets.

Changes

Cohort / File(s) Summary
Core Event Infrastructure
MCPForUnity/Editor/ActionTrace/Core/EditorEvent.cs, EventStore.cs, EventStore.Context.cs, EventStore.Diagnostics.cs, EventStore.Merging.cs, EventStore.Persistence.cs
Thread-safe, immutable EditorEvent model with payload sanitization. Multi-partial EventStore with deferred save coalescing, event dehydration, dynamic context mapping, merging logic, and persistence via McpJobStateStore with schema versioning.
Event Type Registry & Metadata
EventTypes.cs, EventMetadata.cs, EventCategory.cs, SamplingMode.cs
Centralized event type constants, configurable metadata with importance/templates/sampling, event categorization enum, and sampling mode definitions.
Settings & Presets
ActionTraceSettings.cs, ActionTraceSettings.Editor.cs, ActionTracePreset.cs
ScriptableObject-based settings with layered filtering/merging/storage/sampling configuration, preset system (DebugAll, Standard, Lean, AIFocused, Realtime, Performance), and custom inspector.
Event Capture Points
IEventCapturePoint.cs, ActionTraceEventEmitter.cs, UnityEventHooks.cs, AssetChangePostprocessor.cs, PropertyChangeTracker.cs, SelectionPropertyTracker.cs, UndoGroupManager.cs
Capture point framework with registry and lifecycle. Centralized event emission, Unity callback integration (hierarchy, scene, playmode, build), asset lifecycle tracking via postprocessor, debounced property/selection change tracking, and undo grouping for tool calls.
Filtering & Sampling
EventFilter.cs, SamplingMiddleware.cs
Rule-based event filtering with fast/slow path matching, junk path detection, default rules for common junk patterns. Three-stage sampling pipeline (blacklist → per-type → cache) with Throttle/Debounce/DebounceByKey modes.
Context & Operation Tracking
OperationContext.cs, ContextStack.cs, ContextTimeline.cs, ContextMapping.cs, ToolCallScope.cs
Immutable operation context with source (Human/AI/System) and metadata. Thread-local context stack with disposable push/pop semantics. Automatic context-to-event mapping via side-table, tool call scope hierarchy with structured lifecycle events.
Query & Semantic Analysis
ActionTraceQuery.cs, EventSummarizer.cs, TransactionAggregator.cs, ContextCompressor.cs, EventQueryBuilder.cs, EventStatistics.cs
Event projection with importance scoring, categorization, intent inference; semantic summarization with conditional templates; atomic transaction aggregation by tool/time; context compression with preservation rules; fluent query builder with filters/sorts/pagination; comprehensive statistics analysis.
Semantic Interfaces & Implementations
IEventScorer.cs, DefaultEventScorer.cs, IEventCategorizer.cs, DefaultCategorizer.cs, IIntentInferrer.cs, DefaultIntentInferrer.cs
Pluggable scoring/categorization/intent-inference interfaces with default implementations for importance computation, category mapping, and user-intent detection from event context.
Event Descriptors
IEventDescriptor.cs, ComponentEventDescriptor.cs
Event descriptor base class and concrete descriptors for component, hierarchy, asset, scene, and playmode events with summarization and payload extraction.
Helpers
ActionTraceHelper.cs, PropertyEventPayloadBuilder.cs, PropertyFormatter.cs, UndoReflectionHelper.cs, GameObjectTrackingHelper.cs, GlobalIdHelper.cs, BuildTargetUtility.cs
Format/utility helpers for tool names, durations, property payloads, property formatting; reflection-based undo property extraction; GameObject lifecycle tracking; cross-session stable object IDs; build target name mapping.
Integration & Bridges
ManageAssetBridge.cs
Forward asset events from ManageAsset system to ActionTrace via event emitter.
UI & Windows
ActionTraceEditorWindow.cs, ActionTraceEditorWindow.uxml, ActionTraceEditorWindow.uss
Editor window with ListView for events, detail panel, toolbar (search/filter/sort/settings), auto-refresh, and styled UI with importance badges.
Platform Detector Updates
LinuxPlatformDetector.cs, MacOSPlatformDetector.cs, PlatformDetectorBase.cs, WindowsPlatformDetector.cs
Centralized UV/Python path detection via ExecPath utilities, override-aware early exits, augmented PATH building, improved version validation.
Other Infrastructure Changes
ExecPath.cs, ConfigJsonBuilder.cs, IPathResolverService.cs, PathResolverService.cs, StdioBridgeHost.cs, McpClientConfiguratorBase.cs, MCPSetupWindow.uxml.meta
Cross-platform path lookup helper, HTTP transport type preservation for Claude Code, enhanced uvx/Claude CLI path resolution with detection/override methods, domain-reload listener for stdio bridge, error message correction, meta file GUID update.
MCP Tools
ActionTraceSettingsTool.cs, AddTimelineNoteTool.csAddActionTraceNoteTool, GetActionTraceTool.cs, UndoToSequenceTool.cs, ManageAsset.cs (event additions)
Tools to query settings, add AI notes with task/conversation context, retrieve action traces with filtering, undo to specific sequence, and new ManageAsset events (OnAssetModified/Created/Deleted).
MenuItems
MCPForUnityMenu.cs
New ShowActionTraceWindow menu action.
Python Server Resources
action_trace.py (models), resources/action_trace.py, tools/add_action_trace_note.py, tools/get_action_trace.py, tools/get_action_trace_settings.py, tools/undo_to_sequence.py
Pydantic models for events/settings/stats/transactions; resource endpoints for querying events/statistics/settings; tools for adding notes with context, retrieving traces, viewing settings, undoing to sequence.
Settings UI
McpSettingsSection.cs, McpSettingsSection.uxml
UVX path validation using pathService.TryValidateUvxExecutable; UI label update from "UV Path:" to "UVX Path:".

Sequence Diagram(s)

sequenceDiagram
    participant UnityEditor as Unity Editor
    participant CapturePoints as Capture Points<br/>(Hooks, Postprocessor,<br/>Property Tracker)
    participant EventFilter as EventFilter<br/>(Junk Detection)
    participant SamplingMiddleware as SamplingMiddleware<br/>(Rate Limiting)
    participant EventStore as EventStore<br/>(Storage)
    participant UI as ActionTrace<br/>Editor Window
    participant Query as Query/Semantics<br/>(Scorer, Summarizer)

    UnityEditor->>CapturePoints: Event triggered<br/>(Asset/Component/Scene)
    CapturePoints->>EventFilter: Check ShouldTrackAsset?
    alt Filtered (Junk)
        EventFilter-->>CapturePoints: Block
    else Allowed
        CapturePoints->>SamplingMiddleware: ShouldRecord?
        alt Sampled/Throttled
            SamplingMiddleware-->>CapturePoints: Block/Queue
        else Record
            SamplingMiddleware-->>CapturePoints: Allow
            CapturePoints->>EventStore: Record(EditorEvent)
            EventStore->>EventStore: Validate, Merge if enabled,<br/>Assign sequence, Dehydrate old
            EventStore-->>CapturePoints: Return sequence
            EventStore->>EventStore: Persist (deferred)
            EventStore-->>UI: Emit EventRecorded callback
        end
    end

    UI->>EventStore: Query(limit, since)
    EventStore-->>UI: Return events (snapshot)
    UI->>Query: Project(events)
    Query->>Query: Score, Categorize,<br/>Infer Intent
    Query-->>UI: ActionTraceViewItem[]
    UI->>UI: Render ListView,<br/>Display Summary
Loading
sequenceDiagram
    participant AIAgent as AI Agent (Python)
    participant Tool as get_action_trace Tool
    participant Unity as Unity Editor
    participant EventStore as EventStore
    participant Query as ActionTraceQuery
    participant Response as Response

    AIAgent->>Tool: get_action_trace(limit, min_importance, filters)
    Tool->>Unity: async_send_command(limit, min_importance)
    Unity->>EventStore: Query(limit, sinceSequence)
    EventStore-->>Unity: IReadOnlyList<EditorEvent>
    Unity->>Query: Project(events)
    Query->>Query: Compute scores, categories,<br/>context/intent
    Query-->>Unity: ActionTraceViewItem[]
    Unity-->>Tool: EventQueryResult(events, total_count)
    Tool->>Response: Normalize to MCPResponse
    Response-->>AIAgent: {success, events, metadata}
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

This is a substantial, multi-faceted system addition with high complexity across capture, filtering, storage, querying, and UI layers. Review requires understanding event flow, filtering logic, context tracking, semantic analysis, Python/C# integration, and UI binding patterns.

Possibly related issues

Possibly related PRs

Suggested reviewers

  • dsarno
  • msanatan
  • Scriptwonder

Poem

🐰 Hop, skip, and trace we go,
Events captured high and low,
Filtered, sampled, stored with care,
Context stacked through thin air,
Queries dance on semantics bright—
The ActionTrace shines its light!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Feature/action trace' is generic and vague, referring to a broad ActionTrace feature without highlighting the specific primary change or main objective. Consider updating the title to be more specific and descriptive, such as 'Add ActionTrace event capture and storage system for Unity editor' or 'Implement ActionTrace: editor event tracking with query and semantic analysis', to better convey the primary scope and purpose.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

@whatevertogo whatevertogo marked this pull request as draft January 16, 2026 09:50
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: 18

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs (1)

76-121: Claude Code stdio path leaves type stale or missing.

When switching from HTTP to stdio for Claude Code, type is never updated to "stdio" and the removal guard preserves any old "http" value, which can misconfigure the client.

🔧 Proposed fix (set type for Claude Code in both branches)
         private static void PopulateUnityNode(JObject unity, string uvPath, McpClient client, bool isVSCode)
         {
+            bool isClaudeCode = string.Equals(client?.name, "Claude Code", StringComparison.OrdinalIgnoreCase);
             // Get transport preference (default to HTTP)
             bool prefValue = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
             bool clientSupportsHttp = client?.SupportsHttpTransport != false;
             bool useHttpTransport = clientSupportsHttp && prefValue;
@@
-                if (isVSCode)
+                if (isVSCode || isClaudeCode)
                 {
                     unity["type"] = "http";
                 }
-                // Also add type for Claude Code (uses mcpServers layout but needs type field)
-                else if (client?.name == "Claude Code")
-                {
-                    unity["type"] = "http";
-                }
             }
             else
             {
@@
-                if (isVSCode)
+                if (isVSCode || isClaudeCode)
                 {
                     unity["type"] = "stdio";
                 }
             }
 
             // Remove type for non-VSCode clients (except Claude Code which needs it)
-            if (!isVSCode && client?.name != "Claude Code" && unity["type"] != null)
+            if (!isVSCode && !isClaudeCode && unity["type"] != null)
             {
                 unity.Remove("type");
             }
MCPForUnity/Editor/Helpers/ExecPath.cs (1)

245-273: Avoid adding current directory to PATH when prependPath is empty.

When prependPath is empty, the constructed PATH becomes :<existing>, which implicitly adds the current directory to PATH. That can cause which to resolve a project-local executable unintentionally.

🛠️ Safer PATH handling
                 string path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
-                psi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(path) ? prependPath : (prependPath + Path.PathSeparator + path);
+                if (!string.IsNullOrEmpty(prependPath))
+                {
+                    psi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(path)
+                        ? prependPath
+                        : (prependPath + Path.PathSeparator + path);
+                }
🤖 Fix all issues with AI agents
In `@MCPForUnity/Editor/ActionTrace/Capture/PropertyChangeTracker.cs`:
- Around line 44-69: The PropertyChangeTracker class is never initialized
because it lacks the Unity Editor initialization attribute; add
[InitializeOnLoad] above the PropertyChangeTracker class declaration so its
static constructor runs and subscribes Undo.postprocessModifications and calls
ScheduleNextFlush(); ensure the attribute is placed directly before the class
definition to mirror SelectionPropertyTracker's initialization behavior so the
static constructor, Undo.postprocessModifications += mods =>
ProcessModifications(mods), and ScheduleNextFlush() execute on editor startup.

In `@MCPForUnity/Editor/ActionTrace/Capture/SamplingMiddleware.cs`:
- Around line 47-148: ShouldRecord currently stores the latest event for
SamplingMode.Debounce/DebounceByKey but never emits it; add a trailing-edge
flush: create a scheduled flush (e.g., using
UnityEditor.EditorApplication.delayCall or EditorApplication.update) that calls
a new FlushExpiredPendingSamples method which iterates _pendingSamples, finds
entries whose TimestampMs + strategy.WindowMs <= now, removes them and
emits/records their PendingSample.Event (reuse existing CleanupExpiredSamples as
a template or extend it to return expired items), and ensure ScheduleNextFlush
is called whenever you add/update a PendingSample so a flush will run after the
earliest window expires; also add the UnityEditor using and update references to
PendingSample, _pendingSamples, ShouldRecord, SamplingMode.Debounce and
SamplingMode.DebounceByKey accordingly.

In `@MCPForUnity/Editor/ActionTrace/Capture/SelectionPropertyTracker.cs`:
- Around line 104-165: The event currently uses _currentSelectionGlobalId as the
EditorEvent targetId which hides the actual modified object's identity; change
the call to RecordSelectionPropertyModified to pass the targetGlobalId (add a
string targetGlobalId parameter), update the RecordSelectionPropertyModified
signature to accept that id, and use that passed targetGlobalId as the
EditorEvent targetId when constructing evt; keep the existing selection_context
payload entries unchanged so selection info remains in the payload while the
event targetId preserves the real modified-object id.

In `@MCPForUnity/Editor/ActionTrace/Core/EventStore.cs`:
- Around line 226-235: The Clear() method currently only clears
_events/_contextMappings and resets _sequenceCounter but must also reset merge
tracking and pending notifications to avoid dropped events; update Clear() to
set _lastRecordedEvent to null, reset any merge-tracking fields (e.g.,
_mergeState, _mergeCandidates or similar flags used by the merge logic) and
clear any pending notification collections (e.g., _pendingNotifications or
pending operation queues) so all merge/notification state is fully reset after
Clear().
- Around line 75-147: The bug is that _lastRecordedEvent and _lastRecordedTime
are set before the merge logic, causing ShouldMergeWithLast to compare the
incoming event to itself; update Record so that assignment to _lastRecordedEvent
and _lastRecordedTime happens inside the _queryLock after you either call
MergeWithLastEventLocked or add evtWithSequence to _events (i.e., move the two
assignments to immediately after the merge/add block and before releasing the
lock), ensuring ShouldMergeWithLast and MergeWithLastEventLocked see the true
previous event.

In `@MCPForUnity/Editor/ActionTrace/Helpers/ActionTraceHelper.cs`:
- Around line 171-182: GetPreviousValue currently returns the
PropertyModification wrapper because it checks "previousValue" before
"previousValue.value"; change the method so it returns the actual previous
value: first try GetNestedValue(undoMod, "previousValue.value"), then try
GetNestedValue(undoMod, "previousValue"), and if the latter returns a
PropertyModification wrapper (inspect its type/name), extract its inner value
(e.g., via GetNestedValue on the returned object for "value" or use reflection
to read the "value" field) so callers receive the raw previous value; update
GetPreviousValue and use GetNestedValue/PropertyModification handling
consistently for UndoPropertyModification results.

In `@MCPForUnity/Editor/ActionTrace/Query/TransactionAggregator.cs`:
- Around line 100-139: ShouldSplit currently only splits when current has tool
metadata and first lacks it, making boundaries asymmetric; update ShouldSplit
(referencing GetToolCallId and GetTriggeredByTool and the local variables
firstToolCallId, currentToolCallId, firstTool, currentTool) to treat any
mismatch (non-empty vs empty) as a split in both priority checks: if
firstToolCallId is non-empty and currentToolCallId is empty, return true (same
for the reverse which already exists), and likewise if firstTool is non-empty
and currentTool is empty, return true; keep the existing time-window fallback
(transactionWindowMs) unchanged.

In `@MCPForUnity/Editor/ActionTrace/Semantics/DefaultIntentInferrer.cs`:
- Around line 16-63: Infer currently calls IsReparenting(surrounding) and
IsBatchOperation(surrounding) but doesn't guard for surrounding being null
(ActionTraceQuery passes surrounding: null), causing a NullReferenceException;
normalize surrounding to an empty IReadOnlyList<EditorEvent> at the start of the
Infer method (before the switch) so downstream helpers IsReparenting and
IsBatchOperation always receive a non-null list, or alternatively add null
handling inside those helper methods, ensuring neither dereferences a null
reference.

In `@MCPForUnity/Editor/ActionTrace/VCS/VcsContextProvider.cs`:
- Around line 54-205: RefreshContext/QueryGitStatus currently invoke three
synchronous git calls on the Editor update loop via RunGitCommand, which can
block the editor; move Git polling off the main thread by executing
QueryGitStatus (or the git invocations inside RunGitCommand) on a background
Task/Thread and marshal the resulting VcsContext back to the main thread (e.g.,
via EditorApplication.delayCall or scheduling _currentContext update in
OnUpdate) so the Editor is never blocked. Also harden RunGitCommand to use a
Process wait-with-timeout (WaitForExit(timeoutMs)), kill the process on timeout,
and return null/error instead of blocking. Finally, change
VcsContext.ToDictionary so it does not return a cached mutable Dictionary
instance—return a new Dictionary copy or an
IReadOnlyDictionary/ReadOnlyDictionary wrapper each call to avoid shared-mutable
state.

In `@MCPForUnity/Editor/Helpers/AssetPathUtility.cs`:
- Line 164: Remove the debug logging that exposes potentially sensitive data by
deleting the McpLog.Info call that logs sourceOverride in AssetPathUtility (the
line containing McpLog.Info($"[DEBUG] Using override: {sourceOverride}"));
ensure no other statements reference or re-log sourceOverride in the same method
so the override value is not written to logs.
- Line 161: Remove the unconditional debug log that prints sensitive data:
delete the McpLog.Info call that logs EditorPrefKeys.GitUrlOverride and the
sourceOverride value in AssetPathUtility.cs (the line referencing
McpLog.Info($"[DEBUG] GitUrlOverride key: '{EditorPrefKeys.GitUrlOverride}',
value: '{sourceOverride}'")). Ensure no other code in the same method/class logs
the raw GitUrlOverride value; if you need retain diagnostics, replace with a
non-sensitive message or guard it behind a secure debug flag that never runs in
production.

In `@MCPForUnity/Editor/Resources/ActionTrace/ActionTraceViewResource.cs`:
- Around line 165-252: QueryWithContext currently ignores taskId and
conversationId when includeSemantics (and context-only) is true; modify
QueryWithContext to filter eventsWithContext by the provided taskId and/or
conversationId before calling ActionTraceQuery.ProjectWithContext or before
applying the DefaultEventScorer. Specifically, apply the task/conversation
predicates to the collection returned by EventStore.QueryWithContext(limit,
sinceSequence) (use the local variable eventsWithContext) so both the projected
variable (used by ProjectWithContext) and the filtered variable in the else
branch only contain events matching taskId/conversationId; keep the subsequent
semantic projection (ActionTraceQuery.ProjectWithContext) and scoring
(DefaultEventScorer.Score) logic unchanged.
- Around line 366-405: Apply a null-check for EditorEvent.Payload inside
ApplyTaskFilters to avoid NullReferenceException for dehydrated AINote events:
when e.Type == "AINote" and either taskId or conversationId is specified, first
verify e.Payload != null and treat a null payload as a non-match (i.e., return
false) before calling Payload.TryGetValue for "task_id" or "conversation_id";
ensure the existing logic for comparing eventTaskId and eventConvId remains the
same but only runs after the payload null guard.
- Around line 90-118: In QueryBasic replace the call to
EventSummarizer.Summarize(e) with the event's own retrieval method
(e.GetSummary()) so dehydrated events return their PrecomputedSummary; update
the projection in QueryBasic (the anonymous object construction) to use
e.GetSummary() for the summary field and apply the same change to any other
query paths that currently call EventSummarizer.Summarize to ensure precomputed
summaries are preserved.

In `@MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs`:
- Around line 193-207: OnBeforeAssemblyReload currently calls
WriteHeartbeat(true, "domain_reload") then Stop(), but Stop() unconditionally
deletes the heartbeat/status file; fix by setting an explicit "reloading" flag
(e.g. a new static bool or a static property like s_isReloading) before calling
Stop() in OnBeforeAssemblyReload, update the ResumeStdioAfterReload EditorPref
deletion logic to check both EditorApplication.isCompiling and this new
s_isReloading flag, and guard the status-file deletion in Stop() (or the helper
that removes the file) to skip deletion when s_isReloading is true so the
heartbeat remains across the domain reload detection window; ensure symbol names
referenced: OnBeforeAssemblyReload, WriteHeartbeat, Stop,
ResumeStdioAfterReload, and EditorApplication.isCompiling.
- Around line 318-395: The CleanupZombieListeners method currently treats read
timeouts as fatal because stream.Read can throw IOException on ReceiveTimeout;
modify CleanupZombieListeners to wrap the stream.Read(...) call in a
try-catch(IOException) and treat that path the same as “no handshake” (log as
zombie and continue cleanup), and ensure you check bytesRead > 0 before decoding
the buffer; also avoid blocking the editor by reducing/blocking waits (replace
connectTask.Wait(500) and subsequent Thread.Sleep loops with short non-blocking
attempts or shorter timeouts when calling ConnectAsync on testClient/forceClient
and bail earlier) — update references in the method around
testClient.ConnectAsync, testClient.ReceiveTimeout, stream.Read, and the
forceClient ConnectAsync loop.

In `@MCPForUnity/Editor/Tools/AddTimelineNoteTool.cs`:
- Around line 29-30: The McpForUnityTool attribute on the AddActionTraceNoteTool
class lacks a Description parameter required for MCP clients; update the
attribute declaration on the AddActionTraceNoteTool class to include a
descriptive Description="..." argument (e.g., a short summary of the tool's
purpose) so MCP can parse the tool reliably, ensuring the Description is concise
and accurately reflects the tool's behavior.

In `@MCPForUnity/Editor/Tools/GetActionTraceTool.cs`:
- Around line 14-15: The McpForUnityTool attribute on the GetActionTraceTool
class is missing a Description property needed for MCP client parsing; update
the attribute usage on the GetActionTraceTool class (the
[McpForUnityTool("get_action_trace")] declaration) to include a Description =
"Get action trace for a specific entity/action" (or similar brief description)
so the attribute becomes [McpForUnityTool("get_action_trace", Description = "Get
action trace for a specific entity/action")].
♻️ Duplicate comments (3)
MCPForUnity/Editor/Resources/ActionTrace/ActionTraceViewResource.cs (3)

126-159: Same summary handling applies here.
Switch to p.Event.GetSummary() to preserve dehydrated summaries.


411-445: Same null‑payload risk applies here.
Guard p.Event.Payload before accessing task/conversation IDs.


450-484: Same null‑payload risk applies here.
Guard x.Event.Payload before accessing task/conversation IDs.

🟡 Minor comments (25)
Server/src/services/tools/add_action_trace_note.py-113-122 (1)

113-122: Persist auto-generated task/conversation IDs to context.

Right now, if callers omit IDs, each call gets a new task_id/conversation_id, which defeats implicit grouping across a session. If the intended behavior is “generate once, then reuse,” set the ContextVars when you auto-generate.

🔧 Suggested fix
     if not effective_task_id:
         effective_task_id = f"task-{uuid.uuid4().hex[:8]}"
+        _current_task_id.set(effective_task_id)
     if not effective_conv_id:
         effective_conv_id = f"conv-{uuid.uuid4().hex[:8]}"
+        _current_conversation_id.set(effective_conv_id)
Server/src/services/tools/add_action_trace_note.py-151-151 (1)

151-151: Preserve MCPResponse error details instead of stringifying.

async_send_command_with_retry can return an MCPResponse which has error, hint, and data fields. The current code converts it to a string, losing these structured fields. Use model_dump() to convert to dict instead.

🔧 Suggested fix
-    return response if isinstance(response, dict) else {"success": False, "message": str(response)}
+    if isinstance(response, dict):
+        return response
+    if hasattr(response, "model_dump"):
+        return response.model_dump()
+    return {"success": False, "message": str(response)}
Server/src/services/tools/undo_to_sequence.py-91-99 (1)

91-99: Preserve structured error details from non-dict responses.

When Unity returns an MCPResponse, collapsing it to {"message": str(response)} loses structured fields like error/hint. Consider normalizing it to a dict to keep a consistent schema.

🩹 Suggested normalization
-from transport.unity_transport import send_with_unity_instance
+from transport.unity_transport import send_with_unity_instance, normalize_unity_response
...
-    return response if isinstance(response, dict) else {"success": False, "message": str(response)}
+    return response if isinstance(response, dict) else normalize_unity_response(response)
Server/src/services/tools/undo_to_sequence.py-58-61 (1)

58-61: Update the docstring example to use the correct tool name.

Line 60 references get_actionTrace, but the actual tool name is get_action_trace (snake_case). Update the example to:

result = await get_action_trace(ctx, limit=1)
Server/src/services/tools/undo_to_sequence.py-72-78 (1)

72-78: Add validation to guard against negative sequence_id values.

coerce_int accepts negative values, but ActionTrace sequences are monotonically increasing and therefore must be non-negative. The C# handler's EventStore query may behave unexpectedly or cause issues with negative sinceSequence values. Add a validation check before sending to the C# handler.

🩹 Suggested validation
-    if coerced_sequence is None:
+    if coerced_sequence is None or coerced_sequence < 0:
         return {
             "success": False,
-            "message": "sequence_id parameter is required and must be a number."
+            "message": "sequence_id parameter is required and must be a non-negative number."
         }
MCPForUnity/Editor/ActionTrace/Integration/ManageAssetBridge.cs-37-74 (1)

37-74: Include full exception details in warnings.

Logging only ex.Message drops stack traces and inner exception details, which makes ActionTrace failures harder to diagnose.

🛠️ Proposed fix
-                McpLog.Warn($"[ManageAssetBridge] Failed to record asset modification: {ex.Message}");
+                McpLog.Warn($"[ManageAssetBridge] Failed to record asset modification: {ex}");
...
-                McpLog.Warn($"[ManageAssetBridge] Failed to record asset creation: {ex.Message}");
+                McpLog.Warn($"[ManageAssetBridge] Failed to record asset creation: {ex}");
...
-                McpLog.Warn($"[ManageAssetBridge] Failed to record asset deletion: {ex.Message}");
+                McpLog.Warn($"[ManageAssetBridge] Failed to record asset deletion: {ex}");
MCPForUnity/Editor/ActionTrace/Descriptors/IEventDescriptor.cs-67-82 (1)

67-82: Make GetTargetName null-safe for payload values.

If a payload key exists but the value is null, ToString() will throw. Use null-conditional conversion to avoid a hard crash.

🔧 Proposed fix
-            if (evt.Payload.TryGetValue("name", out var name))
-                return name.ToString();
+            if (evt.Payload.TryGetValue("name", out var name))
+                return name?.ToString() ?? string.Empty;

-            if (evt.Payload.TryGetValue("game_object", out var goName))
-                return goName.ToString();
+            if (evt.Payload.TryGetValue("game_object", out var goName))
+                return goName?.ToString() ?? string.Empty;

-            if (evt.Payload.TryGetValue("scene_name", out var sceneName))
-                return sceneName.ToString();
+            if (evt.Payload.TryGetValue("scene_name", out var sceneName))
+                return sceneName?.ToString() ?? string.Empty;

-            if (evt.Payload.TryGetValue("component_type", out var componentType))
-                return componentType.ToString();
+            if (evt.Payload.TryGetValue("component_type", out var componentType))
+                return componentType?.ToString() ?? string.Empty;

-            if (evt.Payload.TryGetValue("path", out var path))
-                return path.ToString();
+            if (evt.Payload.TryGetValue("path", out var path))
+                return path?.ToString() ?? string.Empty;
MCPForUnity/Editor/ActionTrace/Semantics/IIntentInferrer.cs-11-20 (1)

11-20: Fix return type to match documented nullable contract.

The method documentation states it may return null, but the signature returns non-nullable string. The implementation (DefaultIntentInferrer) explicitly returns null when unable to infer. Update the signature to string? to align with the documented contract and actual behavior.

🔧 Proposed fix
-        string Infer(EditorEvent evt, IReadOnlyList<EditorEvent> surrounding);
+        string? Infer(EditorEvent evt, IReadOnlyList<EditorEvent> surrounding);
MCPForUnity/Editor/ActionTrace/Core/GlobalIdHelper.cs-94-100 (1)

94-100: Guard against empty scene paths for unsaved scenes.
Line 95–99: For unsaved scenes, go.scene.path is empty, producing IDs like Scene:@Foo``. ParseFallbackId then calls `GetSceneByPath("")`, which will fail, so the ID can’t resolve even within the same session. Consider falling back to Asset/Instance IDs when the scene path is empty.

🔧 Suggested fix
-            if (obj is GameObject go && go.scene.IsValid())
+            if (obj is GameObject go && go.scene.IsValid() && !string.IsNullOrEmpty(go.scene.path))
             {
                 // Reuse GameObjectLookup.GetGameObjectPath()
                 string hierarchyPath = GameObjectLookup.GetGameObjectPath(go);
                 return $"{ScenePrefix}{go.scene.path}{PathSeparator}{hierarchyPath}";
             }
MCPForUnity/Editor/ActionTrace/Helpers/ActionTraceHelper.cs-28-39 (1)

28-39: Fix FormatToolName output to match the documented examples.
Line 33–38: The current regex removes underscores without inserting spaces and doesn’t title-case the first segment, so "manage_gameobject" becomes "manageGameobject" instead of "Manage GameObject" (per docs). Consider splitting and joining with spaces.

🔧 Suggested fix
-            return System.Text.RegularExpressions.Regex.Replace(
-                toolName,
-                "_([a-z])",
-                match => match.Groups[1].Value.ToUpper()
-            );
+            var parts = toolName.Split('_', StringSplitOptions.RemoveEmptyEntries);
+            for (int i = 0; i < parts.Length; i++)
+            {
+                var part = parts[i];
+                if (part.Length == 0) continue;
+                parts[i] = char.ToUpperInvariant(part[0]) + part.Substring(1);
+            }
+            return string.Join(" ", parts);
MCPForUnity/Editor/ActionTrace/Semantics/DefaultEventScorer.cs-64-67 (1)

64-67: BuildCompleted score conflicts with its comment.

The comment says success is less important than failure, but BuildCompleted is scored 1.0f (critical). Consider lowering the score or updating the comment.

💡 Possible fix (align to comment)
-                EventTypes.BuildCompleted => 1.0f,
+                EventTypes.BuildCompleted => 0.9f,
MCPForUnity/Editor/Tools/ManageAsset.cs-31-38 (1)

31-38: Documented change map doesn’t match emitted payload.
The XML doc says the changes dictionary contains {old,new}, but ModifyAsset forwards only the incoming properties (see Line 499-503). Either compute diffs or update the contract to avoid misleading consumers.

✏️ Minimal doc fix (if raw properties are intended)
-        /// - changesDictionary: property path -> {old, new} values
+        /// - changesDictionary: property path -> new values (or update implementation to capture old/new)
MCPForUnity/Editor/Tools/ManageAsset.cs-291-294 (1)

291-294: Folder creations don’t trigger OnAssetCreated.
CreateAsset returns CreateFolder early and create_folder also calls CreateFolder, so the callback never fires for folders. Consider emitting the event on successful folder creation.

🔧 Suggested hook in CreateFolder (success path)
                 // AssetDatabase.Refresh(); // CreateFolder usually handles refresh
+                OnAssetCreated?.Invoke(fullPath, "Folder");
                 return new SuccessResponse(
MCPForUnity/Editor/Tools/GetActionTraceTool.cs-34-44 (1)

34-44: Remove unused EventTypes and IncludePayload parameters or implement their functionality.
These parameters are defined in GetActionTraceTool.Parameters (lines 34-44) but are never parsed or used in ActionTraceViewResource.HandleCommand. The resource documentation does not list them as supported parameters. Either implement filtering by event types and payload inclusion logic, or remove these parameters to avoid misleading clients.

MCPForUnity/Editor/ActionTrace/Core/EventStore.Persistence.cs-156-173 (1)

156-173: Missing lock in SaveToStorage could cause concurrent modification issues.

SaveToStorage reads from _events and _contextMappings without holding _queryLock, while other methods like TrimToMaxEventsLimit and Query use the lock. If a save occurs concurrently with event recording or trimming, .ToList() could throw InvalidOperationException.

Suggested fix
         private static void SaveToStorage()
         {
             try
             {
+                EventStoreState state;
+                lock (_queryLock)
+                {
-                var state = new EventStoreState
+                    state = new EventStoreState
-                {
+                    {
-                    SchemaVersion = CurrentSchemaVersion,
+                        SchemaVersion = CurrentSchemaVersion,
-                    SequenceCounter = _sequenceCounter,
+                        SequenceCounter = _sequenceCounter,
-                    Events = _events.ToList(),
+                        Events = _events.ToList(),
-                    ContextMappings = _contextMappings.ToList()
+                        ContextMappings = _contextMappings.ToList()
-                };
+                    };
+                }
                 McpJobStateStore.SaveState(StateKey, state);
             }
MCPForUnity/Editor/Tools/UndoToSequenceTool.cs-133-142 (1)

133-142: Undo operations may silently fail without feedback.

Undo.PerformUndo() doesn't return a success indicator. If the undo history was cleared (e.g., by domain reload), the loop runs but accomplishes nothing. The user receives a "Successfully reverted X steps" message that may be misleading.

Additionally, MarkSceneDirty is called unconditionally, which will mark the scene dirty even if no actual changes were reverted.

Suggested improvement

Consider tracking the undo group before and after to detect if operations actually occurred:

+                int groupBefore = Undo.GetCurrentGroup();
                 for (int i = 0; i < stepsToUndo; i++)
                 {
                     Undo.PerformUndo();
                 }
+                int groupAfter = Undo.GetCurrentGroup();
+                int actualSteps = groupBefore - groupAfter;

                 // Force GUI refresh to update the scene
-                EditorApplication.delayCall += () =>
+                if (actualSteps > 0)
                 {
-                    UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene());
-                };
+                    EditorApplication.delayCall += () =>
+                    {
+                        UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(
+                            UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene());
+                    };
+                }
MCPForUnity/Editor/ActionTrace/Capture/UndoGroupManager.cs-50-69 (1)

50-69: Missing re-entrancy guard could cause lost undo group context.

If BeginToolCall is called twice without an intervening EndToolCall, the first call's _currentUndoGroupStart is silently overwritten. This would prevent proper undo collapsing for the first tool call.

Suggested fix
         public static void BeginToolCall(string toolName, string toolCallId)
         {
+            if (_isInToolCall)
+            {
+                UnityEngine.Debug.LogWarning(
+                    $"[UndoGroupManager] BeginToolCall({toolName}) called while already in tool call ({_currentToolName}). " +
+                    "Ending previous tool call first.");
+                EndToolCall();
+            }
+
             if (string.IsNullOrEmpty(toolName))
             {
MCPForUnity/Editor/ActionTrace/Capture/EventFilter.cs-48-75 (1)

48-75: Non-extension entries in JunkExtensions will not filter correctly.

Several entries in JunkExtensions are not file extensions:

  • "~$" (Line 61) is a filename prefix pattern for Office temp files, not a suffix.
  • ".DS_Store", "Thumbs.db", "desktop.ini" (Lines 64-66) are complete filenames.

The EndsWith check on Line 127 won't match paths like folder/~$document.docx or folder/.DS_Store correctly—only paths literally ending with these strings (e.g., something.DS_Store) would match.

Suggested fix

Move these to separate collections with appropriate matching logic:

+        /// <summary>
+        /// Filename patterns to filter (matched at end of path after last separator).
+        /// </summary>
+        private static readonly HashSet<string> JunkFilenames = new(StringComparer.OrdinalIgnoreCase)
+        {
+            ".DS_Store",
+            "Thumbs.db",
+            "desktop.ini"
+        };
+
+        /// <summary>
+        /// Filename prefix patterns (e.g., Office temp files starting with ~$).
+        /// </summary>
+        private static readonly HashSet<string> JunkFilenamePrefixes = new(StringComparer.OrdinalIgnoreCase)
+        {
+            "~$"
+        };

Then add matching logic in IsJunkPath:

// Extract filename and check against junk filenames/prefixes
string filename = Path.GetFileName(path);
if (JunkFilenames.Contains(filename))
    return true;
foreach (var prefix in JunkFilenamePrefixes)
{
    if (filename.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
        return true;
}
MCPForUnity/Editor/ActionTrace/Core/ActionTraceSettings.cs-183-198 (1)

183-198: "All Events (Debug)" and "Low+" presets are functionally identical.

Both buttons call SetImportance(0f), making "Low+" redundant. Either differentiate the values or remove the duplicate.

🐛 Proposed fix - differentiate or remove
             if (GUILayout.Button("All Events (Debug)"))
             {
                 SetImportance(0f);
             }
-            if (GUILayout.Button("Low+"))
-            {
-                SetImportance(0f);
-            }
+            if (GUILayout.Button("Low+"))
+            {
+                SetImportance(0.2f);  // Or remove this button entirely
+            }
MCPForUnity/Editor/ActionTrace/Core/EventStore.Merging.cs-92-97 (1)

92-97: Unsafe cast of merge_count value could throw InvalidCastException.

If existingCount is stored as a long (e.g., from JSON deserialization), the direct cast (int)existingCount will fail. Use safe conversion instead.

🐛 Proposed fix using Convert.ToInt32
-                if (_lastRecordedEvent.Payload.TryGetValue("merge_count", out var existingCount))
-                {
-                    mergeCount = (int)existingCount + 1;
-                }
+                if (_lastRecordedEvent.Payload.TryGetValue("merge_count", out var existingCount) 
+                    && existingCount != null)
+                {
+                    mergeCount = Convert.ToInt32(existingCount) + 1;
+                }
MCPForUnity/Editor/ActionTrace/Core/EventStore.Context.cs-27-40 (1)

27-40: Optimization comment claims mappings are ordered, but order isn't enforced.

Line 37 states "mappings are ordered by EventSequence" as an optimization, but AddContextMapping simply appends to the list without maintaining sort order. If events are recorded out of order (e.g., from async sources), this early-exit optimization could skip newer duplicates.

🐛 Option 1: Remove the optimization and always scan fully
                 for (int i = _contextMappings.Count - 1; i >= 0; i--)
                 {
                     var existing = _contextMappings[i];
                     if (existing.EventSequence == mapping.EventSequence &&
                         existing.ContextId == mapping.ContextId)
                     {
                         isDuplicate = true;
                         break;
                     }
-                    // Optimization: mappings are ordered by EventSequence
-                    if (existing.EventSequence < mapping.EventSequence)
-                        break;
                 }
MCPForUnity/Editor/ActionTrace/Core/EventStore.Context.cs-144-147 (1)

144-147: QueryWithContext loses multiple contexts per event.

The GroupBy(...).ToDictionary(..., g.FirstOrDefault()) pattern keeps only the first context mapping per event sequence. Per the docstring at line 18-19, "Multiple mappings allowed for same eventSequence (different contexts)". Consider returning all mappings or documenting that only one is returned.

🐛 Option: Return ILookup to preserve all mappings
-            var mappingBySequence = mappingsSnapshot
-                .GroupBy(m => m.EventSequence)
-                .ToDictionary(g => g.Key, g => g.FirstOrDefault());
+            var mappingsBySequence = mappingsSnapshot.ToLookup(m => m.EventSequence);

Then update the select to return all contexts:

.Select(e => (Event: e, Contexts: mappingsBySequence[e.Sequence].ToList()))

This would require changing the return type signature.

MCPForUnity/Editor/ActionTrace/Core/ActionTraceSettings.cs-14-14 (1)

14-14: Move settings file to a dedicated subfolder for better organization.

The settings file is created at Assets/ActionTraceSettings.asset, which places it at the root of the Assets folder. Consider moving it to a dedicated subfolder such as Assets/Editor/ActionTraceSettings.asset or Assets/Resources/ActionTraceSettings.asset (if runtime-accessible) to keep editor configuration organized and separate from user-created assets.

MCPForUnity/Editor/ActionTrace/Descriptors/ComponentEventDescriptor.cs-46-53 (1)

46-53: Timestamp generated at extraction time may be inconsistent.

ExtractPayload generates a new timestamp on every invocation rather than extracting it from the raw payload or the event's actual timestamp. If this method is called multiple times or at a later point, the timestamp won't reflect when the hierarchy actually changed.

Consider extracting from rawPayload if available, or relying on the event's TimestampUnixMs:

Suggested fix
 public override Dictionary<string, object> ExtractPayload(Dictionary<string, object> rawPayload)
 {
     return new Dictionary<string, object>
     {
-        ["timestamp"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
+        ["timestamp"] = rawPayload?.GetValueOrDefault("timestamp", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()) 
+                        ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
     };
 }
MCPForUnity/Editor/ActionTrace/Core/EditorEvent.cs-24-24 (1)

24-24: Potential thread-safety issue with lazy PrecomputedSummary mutation.

The class is documented as immutable, but GetSummary() mutates PrecomputedSummary via the private setter. If multiple threads call GetSummary() concurrently on the same instance, there's a data race. While Unity editor code is typically single-threaded, this could cause issues if events are accessed from background threads (e.g., during persistence or MCP server calls).

Consider using Interlocked.CompareExchange or accepting the benign race (since string assignment is atomic and all threads would compute the same value).

🔒 Thread-safe lazy initialization option
         public string GetSummary()
         {
-            if (PrecomputedSummary != null)
-                return PrecomputedSummary;
-
-            // Compute and cache (this mutates the object, but it's just a string field)
-            PrecomputedSummary = ComputeSummary();
-            return PrecomputedSummary;
+            var summary = PrecomputedSummary;
+            if (summary != null)
+                return summary;
+
+            // Compute and cache - benign race if multiple threads compute simultaneously
+            summary = ComputeSummary();
+            PrecomputedSummary = summary;
+            return summary;
         }

Also applies to: 76-76, 162-170

whatevertogo and others added 3 commits January 18, 2026 00:51
…uery improvements

This commit introduces significant improvements to the ActionTrace system:

**Layered Settings Architecture:**
- Refactor ActionTraceSettings into layered structure (Filtering, Merging, Storage, Sampling)
- Add custom Editor with foldout sections for better UX
- Move GlobalIdHelper from Core/ to Helpers/ for better organization

**Preset System:**
- Add ActionTracePreset with Standard/Verbose/Minimal/Silent presets
- Enable quick configuration switching via ApplyPreset()
- Include preset descriptions and memory estimates

**Configurable Filtering:**
- Transform EventFilter from static blacklist to rule-based system
- Support Prefix, Extension, Regex, and GameObject rule types
- Add EventFilterSettings for persistence and customization

**Stable Cross-Session IDs:**
- Use GlobalIdHelper for all GameObject/Component event TargetIds
- Use Asset:{path} format for asset events
- Ensure TargetIds remain stable across domain reloads

**Query & Analysis Enhancements:**
- Add EventQueryBuilder for fluent query API
- Add ContextCompressor for event data optimization
- Add EventStatistics for comprehensive analytics
- Enhance EventSummarizer with better grouping

**Capture Layer Improvements:**
- Add AssetChangePostprocessor for asset change tracking
- Add SamplingMiddleware for high-frequency event throttling
- Add ToolCallScope for MCP tool call tracking
- Enhance UnityEventHooks with comprehensive event coverage

**UI/UX Improvements:**
- Add sort modes (ByTimeDesc, AIFiltered) to ActionTraceEditorWindow
- Improve filter menu with "All" option
- Add tool descriptions for better AI discoverability

**Helper Classes:**
- Add GameObjectTrackingHelper for GameObject lifecycle tracking
- Add UndoReflectionHelper for Undo system introspection
- Add BuildTargetUtility for build target detection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…capacity

- Add Range attributes to settings fields with Inspector sliders:
  - MinImportanceForRecording: [Range(0f, 1f)]
  - MergeWindowMs: [Range(0, 5000)]
  - TransactionWindowMs: [Range(100, 10000)]
  - HotEventCount: [Range(10, 1000)]
  - MaxEvents: [Range(100, 5000)] (already existed)
- Change ContextMappings from fixed 2000 to dynamic MaxEvents × 2
- Remove redundant validation code (now enforced by Range attributes)
- Fix UndoReflectionHelper.GetPreviousValue to try nested value first
- Clean up StdioBridgeHost: remove ineffective CleanupZombieListeners
- Add task/conversation filtering to ActionTraceViewResource
- Preserve modified-object identity in SelectionPropertyTracker

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
whatevertogo and others added 2 commits January 18, 2026 07:53
…leaks

Replace infinite recursive delayCall patterns with EditorApplication.update
in PropertyChangeTracker and SamplingMiddleware. This ensures proper
cleanup on domain reload and prevents memory leaks.

Changes:
- PropertyChangeTracker: Replace delayCall with update + FlushCheck()
- SamplingMiddleware: Replace delayCall with update directly
- Remove redundant helper methods (use UndoReflectionHelper/PropertyFormatter)
- Clean up unused using statements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@whatevertogo whatevertogo marked this pull request as ready for review January 18, 2026 00:46
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.

Sorry @whatevertogo, your pull request is larger than the review limit of 300000 diff characters

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: 10

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🤖 Fix all issues with AI agents
In `@MCPForUnity/Editor/ActionTrace/Capture/AssetChangePostprocessor.cs`:
- Around line 103-128: The code emits duplicate events for the same imported
assets because importedAssets are processed earlier to emit AssetImported (via
RecordEvent) and this later loop emits AssetModified for the same entries;
either remove this AssetModified loop or make it mutually exclusive by using
IsNewlyCreatedAsset correctly so an asset only triggers
AssetImported/AssetCreated OR AssetModified; update the logic around
importedAssets, IsNewlyCreatedAsset, ShouldTrackModification and RecordEvent (or
adjust ShouldTrackModification) to ensure only one event type is recorded per
asset.

In `@MCPForUnity/Editor/ActionTrace/Capture/PropertyChangeTracker.cs`:
- Around line 127-133: The pending-change cap check in PropertyChangeTracker is
insufficient because _pendingChanges.Count >= MaxPendingEntries only calls
FlushPendingChanges(), which currently only removes expired entries; update the
logic to enforce the cap by performing a real eviction or immediate flush when
the limit is reached: inside the block where MaxPendingEntries is checked
(referencing _pendingChanges, MaxPendingEntries and FlushPendingChanges()), call
a new or existing force flush method (e.g., ForceFlushPendingChanges()) that
writes out or clears all pending entries, or implement explicit eviction (remove
oldest entries from _pendingChanges until Count < MaxPendingEntries) before
adding the new entry so the cache cannot grow beyond the cap.

In `@MCPForUnity/Editor/ActionTrace/Capture/UnityEventHooks.cs`:
- Around line 533-536: There is a stray preprocessor directive "#endregion"
placed after the closing brace of the UnityEventHooks class; remove that
trailing "#endregion" (or move it so it matches the corresponding "#region"
inside the class) to ensure the "#endregion" is inside the class scope and
eliminate the syntax error in UnityEventHooks.cs.

In `@MCPForUnity/Editor/ActionTrace/Context/ToolCallScope.cs`:
- Around line 471-494: ExecuteAsync<T> creates a ToolCallScope but never
disposes it (scope leaks on both asynchronous and synchronous-failure paths);
fix by ensuring scope.Dispose() is always called: wrap the func() invocation so
synchronous exceptions are caught (try/catch around func()), and change the
continuation flow to guarantee disposal (either by converting ExecuteAsync<T> to
an async method that awaits func() and uses try { var result = await func();
scope.Complete(...); return result; } catch (Exception ex) { scope.Fail(...);
throw; } finally { scope.Dispose(); } or, if keeping ContinueWith, invoke
scope.Dispose() after calling scope.Complete/Fail inside every continuation
branch and also catch synchronous exceptions from func() to call scope.Fail(...)
and scope.Dispose() before rethrowing). Ensure you reference ExecuteAsync<T>,
ToolCallScope, scope, func(), ContinueWith when applying the fix.

In `@MCPForUnity/Editor/ActionTrace/Core/EventStore.Context.cs`:
- Around line 155-158: The code uses mappingsSnapshot.GroupBy(m =>
m.EventSequence).ToDictionary(g => g.Key, g => g.FirstOrDefault()), which drops
additional mappings for the same EventSequence; update the logic to preserve all
mappings by changing the dictionary value to a collection (e.g., use g =>
g.ToList()) and update any dependent return types/usages of mappingBySequence to
accept IEnumerable/List of mappings, or if you intentionally want only one
mapping, explicitly document that behavior and keep FirstOrDefault; reference
mappingBySequence, mappingsSnapshot, EventSequence, and FirstOrDefault when
making the change.

In `@MCPForUnity/Editor/ActionTrace/Helpers/GameObjectTrackingHelper.cs`:
- Around line 70-113: DetectChanges is currently calling
FindObjectsOfType<GameObject>(true) every editor frame (via the
EditorApplication.update hook), which is expensive; update DetectChanges (and
the registration in EditorApplication.update) to short-circuit and avoid calling
FindObjectsOfType each frame by adding simple throttling or event-driven checks:
add a static/frame counter or lastRunTime check and return cached results until
N frames or T milliseconds have passed, or only run the full traversal when
specific editor events occur, then keep the existing logic that fills results
and updates _previousInstanceIds when the check actually runs; ensure the method
still asserts main thread via AssertMainThread and preserves behavior of marking
isNew by comparing against _previousInstanceIds.

In `@MCPForUnity/Editor/ActionTrace/Query/EventQueryBuilder.cs`:
- Around line 94-323: WithAllSearchTerms currently adds terms into the same
_searchTerms set (making it behave like OR) and accepts null/empty entries;
change it to record required-all semantics and ignore null/empty terms.
Specifically, introduce or use a distinct collection/name (e.g., _allSearchTerms
or a boolean flag paired with _allSearchTerms) in the EventQueryBuilder and have
WithAllSearchTerms(params string[] terms) skip null/empty strings
(String.IsNullOrWhiteSpace) when adding; update the query evaluation logic to
check _allSearchTerms for an AND (all terms must match) requirement instead of
treating them as OR. Ensure WithSearchTerm retains case-insensitive HashSet
behavior and does not accept null/empty either.

In `@MCPForUnity/Editor/ActionTrace/Query/EventStatistics.cs`:
- Around line 414-447: FindTopTargets currently uses targetEventsList[^1] which
assumes the per-target events are chronological; instead sort each targetEvents
list by TimestampUnixMs (ascending) before calling CalculateRecencyFactor and
before reading the last event for LastActivityMs and DisplayName. Update
FindTopTargets to replace/derive targetEventsList with a timestamp-ordered
sequence (or in-place sort) and then compute recencyFactor =
CalculateRecencyFactor(orderedList, nowMs), ActivityScore, EventCount,
EventTypes, LastActivityMs and DisplayName from the orderedList.Last() (or
orderedList[^1]) so all derived values use the most recent event. Use the same
ordered list when building TargetStats to ensure consistency.

In `@MCPForUnity/Editor/ActionTrace/Query/EventSummarizer.cs`:
- Around line 102-112: The while-loop that replaces placeholders in sb can
infinite-loop if a formatted value contains the same placeholder; replace that
manual index loop with StringBuilder.Replace to perform a safe global
replacement: for each kvp in evt.Payload (or empty dict) compute placeholder =
"{" + kvp.Key + "}" and value = FormatValue(kvp.Value) and call
sb.Replace(placeholder, value). Ensure you keep the surrounding foreach over
evt.Payload (and the null-coalescing) and remove the manual index-based
removal/Insert logic to avoid the self-referential replacement bug.

In `@MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs`:
- Around line 119-121: The window currently forces
ActionTraceSettings.Instance.Filtering.BypassImportanceFilter = true in
ActionTraceEditorWindow (causing a global side-effect); change this so the
previous value is preserved and restored: in ActionTraceEditorWindow add a
private bool (e.g. _previousBypassValue), capture
ActionTraceSettings.Instance.Filtering.BypassImportanceFilter in OnEnable before
setting it to true, and restore that saved value in OnDisable (also ensure
EditorApplication.update handlers are balanced in OnEnable/OnDisable);
alternatively implement a query-level bypass inside the window’s data-fetching
path instead of touching
ActionTraceSettings.Instance.Filtering.BypassImportanceFilter.
♻️ Duplicate comments (1)
MCPForUnity/Editor/Tools/AddTimelineNoteTool.cs (1)

29-30: LGTM!

The tool now includes a Description attribute for MCP compatibility, addressing the previous review feedback.

🟡 Minor comments (21)
MCPForUnity/Editor/Helpers/GlobalIdHelper.cs-152-161 (1)

152-161: Instance ID resolution may fail for non-GameObject objects.

GameObjectLookup.FindById (line 159) returns only GameObject, but GetFallbackId can store Instance IDs for any UnityEngine.Object including Components. Consider using EditorUtility.InstanceIDToObject directly for broader compatibility:

🐛 Suggested fix
 if (idStr.StartsWith(InstancePrefix))
 {
     string instanceStr = idStr.Substring(InstancePrefix.Length);
     if (int.TryParse(instanceStr, out int instanceId))
     {
-        return GameObjectLookup.FindById(instanceId);
+        return EditorUtility.InstanceIDToObject(instanceId);
     }
 }
MCPForUnity/Editor/ActionTrace/VCS/VcsContextProvider.cs-264-269 (1)

264-269: Missing timeout in PATH verification could block editor.

The git --version check when verifying PATH availability has no timeout. If Git exists but hangs (e.g., waiting for credentials), this will block indefinitely.

🐛 Suggested fix: Add timeout to PATH check
 using (var process = Process.Start(startInfo))
 {
-    process.WaitForExit();
-    if (process.ExitCode == 0)
-        return "git"; // Found in PATH
+    if (process.WaitForExit(2000) && process.ExitCode == 0)
+        return "git"; // Found in PATH
+    try { process.Kill(); } catch { }
 }
MCPForUnity/Editor/ActionTrace/Context/ToolCallScope.cs-291-297 (1)

291-297: UnityEngine.Random is not thread-safe.

UnityEngine.Random should only be called from Unity's main thread. Since ToolCallScope uses ThreadLocal storage (implying multi-thread usage), this could cause issues when scopes are created on background threads.

🔧 Proposed fix using thread-safe random
+private static readonly System.Random _random = new();
+private static readonly object _randomLock = new();

 private string GenerateCallId()
 {
     // Compact ID: tool name + timestamp + random suffix
     long timestamp = _startTimestampMs % 1000000; // Last 6 digits of timestamp
-    int random = UnityEngine.Random.Range(1000, 9999);
+    int random;
+    lock (_randomLock)
+    {
+        random = _random.Next(1000, 9999);
+    }
     return $"{_toolId}_{timestamp}_{random}";
 }
MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs-249-255 (1)

249-255: Menu separator syntax may not work correctly.

AppendAction("/", ...) likely won't render as a separator in Unity's ToolbarMenu. Use AppendSeparator() instead.

🔧 Suggested fix
             _filterMenu?.menu.AppendAction("All", a => SetImportance(0f));
-            _filterMenu?.menu.AppendAction("/", a => { });
+            _filterMenu?.menu.AppendSeparator();
             _filterMenu?.menu.AppendAction("AI Can See", a => SetImportanceFromSettings());
MCPForUnity/Editor/ActionTrace/Helpers/GameObjectTrackingHelper.cs-28-29 (1)

28-29: MainThreadId captured at static initialization may be incorrect.

If this class is first accessed from a background thread (e.g., during async asset import), MainThreadId would capture the wrong thread ID. Consider lazy initialization on first use from a known main-thread context.

🔧 Suggested fix
-        // Cache for the main thread ID to validate thread safety
-        private static readonly int MainThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
+        // Cache for the main thread ID to validate thread safety
+        private static int _mainThreadId = -1;
+
+        private static int MainThreadId
+        {
+            get
+            {
+                if (_mainThreadId < 0)
+                {
+                    // Initialize on first access - caller must be on main thread
+                    _mainThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
+                }
+                return _mainThreadId;
+            }
+        }

Alternatively, initialize it in InitializeTracking() which is documented to be called from the main thread.

MCPForUnity/Editor/ActionTrace/Core/EventStore.Context.cs-64-68 (1)

64-68: Potential data race: _isDirty modified outside lock.

_isDirty is written outside the lock (_queryLock) block. If another thread reads or writes _isDirty concurrently, this could lead to a data race. While typically benign on x86/x64, it's inconsistent with the thread-safety guarantees stated in the doc.

🔒 Suggested fix
             lock (_queryLock)
             {
                 // ... existing code ...
+
+                // Mark dirty inside lock for thread safety
+                _isDirty = true;
             }

-            // Mark dirty and schedule deferred save
-            _isDirty = true;
             ScheduleSave();
MCPForUnity/Editor/ActionTrace/Core/EventStore.Persistence.cs-78-87 (1)

78-87: Inconsistent locking for _saveScheduled.

ClearPendingOperations sets _saveScheduled = false (line 85) outside any lock, but ScheduleSave accesses _saveScheduled under _queryLock. This inconsistency could lead to race conditions.

🐛 Suggested fix
         public static void ClearPendingOperations()
         {
             lock (_pendingNotifications)
             {
                 _pendingNotifications.Clear();
                 _notifyScheduled = false;
             }
-            _saveScheduled = false;
+            lock (_queryLock)
+            {
+                _saveScheduled = false;
+            }
             _lastDehydratedCount = -1;  // Reset dehydration optimization marker
         }
MCPForUnity/Editor/ActionTrace/Core/EventStore.Merging.cs-100-105 (1)

100-105: Potential InvalidCastException when casting merge_count.

When merge_count is deserialized from JSON (e.g., after domain reload), Newtonsoft.Json may deserialize it as long rather than int. The direct cast (int)existingCount will throw an InvalidCastException in that case.

🐛 Suggested fix
                 int mergeCount = 1;
                 if (_lastRecordedEvent.Payload.TryGetValue("merge_count", out var existingCount))
                 {
-                    mergeCount = (int)existingCount + 1;
+                    mergeCount = Convert.ToInt32(existingCount) + 1;
                 }
MCPForUnity/Editor/ActionTrace/Capture/AssetChangePostprocessor.cs-135-148 (1)

135-148: IsNewlyCreatedAsset implementation is non-functional.

The method attempts to load .meta files via AssetDatabase.LoadMainAssetAtPath, but .meta files are not Unity assets and cannot be loaded this way—this will always return null. Additionally, even if it worked, the result isn't used before returning false.

The current implementation always returns false, which may be acceptable as a placeholder but should be documented more clearly or implemented properly.

🔧 Suggested approach if implementation is needed

To detect newly created assets, consider using file creation timestamps:

 private static bool IsNewlyCreatedAsset(string assetPath)
 {
     try
     {
-        string metaPath = assetPath + ".meta";
-        var meta = AssetDatabase.LoadMainAssetAtPath(metaPath);
-        // This is a simplified check - in production you'd check file creation time
-        return false; // Default to treating as imported for now
+        string fullPath = System.IO.Path.GetFullPath(assetPath);
+        if (!System.IO.File.Exists(fullPath))
+            return false;
+        
+        var creationTime = System.IO.File.GetCreationTimeUtc(fullPath);
+        var timeSinceCreation = DateTime.UtcNow - creationTime;
+        
+        // Consider "newly created" if created within the last 2 seconds
+        return timeSinceCreation.TotalSeconds < 2;
     }
     catch
     {
         return false;
     }
 }
MCPForUnity/Editor/ActionTrace/Core/EventStore.Persistence.cs-186-203 (1)

186-203: Missing lock when reading shared state in SaveToStorage.

SaveToStorage reads _sequenceCounter, _events, and _contextMappings without holding _queryLock. While the ToList() calls create copies, there's a race window where another thread could modify these collections during iteration.

The comment in ScheduleSave (line 66) says "Perform save outside lock to avoid holding lock during I/O", but the state snapshot should be taken inside the lock.

🐛 Suggested fix
         private static void SaveToStorage()
         {
             try
             {
-                var state = new EventStoreState
+                EventStoreState state;
+                lock (_queryLock)
                 {
-                    SchemaVersion = CurrentSchemaVersion,
-                    SequenceCounter = _sequenceCounter,
-                    Events = _events.ToList(),
-                    ContextMappings = _contextMappings.ToList()
-                };
+                    state = new EventStoreState
+                    {
+                        SchemaVersion = CurrentSchemaVersion,
+                        SequenceCounter = _sequenceCounter,
+                        Events = _events.ToList(),
+                        ContextMappings = _contextMappings.ToList()
+                    };
+                }
+                // I/O happens outside the lock
                 McpJobStateStore.SaveState(StateKey, state);
             }
             catch (Exception ex)
             {
                 McpLog.Error($"[EventStore] Failed to save to storage: {ex.Message}\n{ex.StackTrace}");
             }
         }
MCPForUnity/Editor/Tools/AddTimelineNoteTool.cs-146-153 (1)

146-153: Add AINote constant to EventTypes class for consistency with other event types.

All event types in Core/EventTypes.cs are defined as public const string constants (e.g., ComponentAdded, PropertyModified, GameObjectCreated), but AINote is missing from this pattern and only appears in the metadata dictionary. Add the constant public const string AINote = "AINote"; to EventTypes in MCPForUnity/Editor/ActionTrace/Core/EventTypes.cs, then use EventTypes.AINote instead of the hardcoded string here.

MCPForUnity/Editor/ActionTrace/Capture/UnityEventHooks.cs-393-417 (1)

393-417: Consider adding debug logging to troubleshoot missing compilation error type.

GetCompilationErrorCount relies on reflection to access the internal UnityEditor.Scripting.ScriptCompilationErrorCount type, which may not exist in all Unity versions. The fallback of returning 0 is safe, but adding debug logging when the type is not found would help diagnose issues in environments where the reflection fails. For example, log once when type == null using the existing McpLog infrastructure.

MCPForUnity/Editor/Resources/ActionTrace/ActionTraceViewResource.cs-55-61 (1)

55-61: Guard against null @params to preserve default behavior.

If HandleCommand is called with null, the helpers throw and return an error instead of applying defaults.

🐛 Proposed fix
         public static object HandleCommand(JObject `@params`)
         {
             try
             {
+                `@params` ??= new JObject();
                 int limit = GetLimit(`@params`);
                 long? sinceSequence = GetSinceSequence(`@params`);
MCPForUnity/Editor/ActionTrace/Core/ActionTraceSettings.cs-118-149 (1)

118-149: Preset name won’t survive domain reloads.

_currentPresetName is [NonSerialized], so CurrentPresetName resets to "Standard" even if values match another preset. Consider serializing it or recomputing on load.

MCPForUnity/Editor/ActionTrace/Query/EventStatistics.cs-488-494 (1)

488-494: Build error details are dropped.

Emitters use error_message, but FindErrors only checks error, so the detailed message is lost.

♻️ Proposed fix
                 if (evt.Type == EventTypes.BuildFailed)
                 {
-                    if (evt.Payload != null && evt.Payload.TryGetValue("error", out var error))
-                        errors.Add($"Build: {error}");
-                    else
-                        errors.Add("Build failed");
+                    if (evt.Payload != null &&
+                        (evt.Payload.TryGetValue("error", out var error) ||
+                         evt.Payload.TryGetValue("error_message", out error)))
+                        errors.Add($"Build: {error}");
+                    else
+                        errors.Add("Build failed");
                 }
MCPForUnity/Editor/ActionTrace/Core/ActionTraceSettings.cs-266-272 (1)

266-272: Clamp negative cold-event counts.

If HotEventCount exceeds MaxEvents (e.g., via manual asset edits), coldEvents becomes negative and the estimate underflows.

♻️ Proposed fix
             int hotEvents = Storage.HotEventCount;
             int coldEvents = Storage.MaxEvents - Storage.HotEventCount;
+            if (coldEvents < 0) coldEvents = 0;
             return (long)(hotEvents * 300 + coldEvents * 100);
MCPForUnity/Editor/ActionTrace/Query/EventStatistics.cs-258-276 (1)

258-276: GetSummary can null‑ref on empty stats.

TopTargets/ErrorMessages are null when Analyze returns early; guard before .Count.

♻️ Proposed fix
-            if (stats.TopTargets.Count > 0)
+            if (stats.TopTargets != null && stats.TopTargets.Count > 0)
             {
                 summary.AppendLine("Top Targets:");
                 foreach (var target in stats.TopTargets.Take(5))
                 {
                     summary.AppendLine($"  - {target.DisplayName}: {target.EventCount} events");
                 }
                 summary.AppendLine();
             }
 
-            if (stats.ErrorCount > 0)
+            if (stats.ErrorMessages != null && stats.ErrorCount > 0)
             {
                 summary.AppendLine($"Errors: {stats.ErrorCount}");
                 foreach (var error in stats.ErrorMessages.Take(3))
                 {
                     summary.AppendLine($"  - {error}");
                 }
                 summary.AppendLine();
             }
MCPForUnity/Editor/ActionTrace/Query/EventQueryBuilder.cs-445-449 (1)

445-449: Null event types can throw under category filtering.

EventTypes.Metadata.Get(evt.Type) will throw if evt.Type is null; normalize before lookup.

♻️ Proposed fix
-                var meta = EventTypes.Metadata.Get(evt.Type);
+                var meta = EventTypes.Metadata.Get(evt.Type ?? string.Empty);
MCPForUnity/Editor/ActionTrace/Query/ContextCompressor.cs-419-435 (1)

419-435: Guard against windowMs <= 0 to avoid divide‑by‑zero.

evt.TimestampUnixMs / windowMs will throw for zero or negative values.

♻️ Proposed fix
         public static List<EditorEvent> Deduplicate(this IReadOnlyList<EditorEvent> events, int windowMs = 100)
         {
             if (events == null || events.Count == 0)
                 return new List<EditorEvent>();
 
+            if (windowMs <= 0)
+                windowMs = 1;
+
             var seen = new HashSet<string>();
             var result = new List<EditorEvent>();
MCPForUnity/Editor/ActionTrace/Capture/ActionTraceEventEmitter.cs-509-515 (1)

509-515: Use ToLowerInvariant() for file extension normalization.

ToLower() is culture-sensitive; file extensions should use invariant casing to avoid locale-specific bugs. This is the preferred pattern throughout the codebase.

♻️ Proposed fix
-            var extension = System.IO.Path.GetExtension(assetPath).ToLower();
+            var extension = System.IO.Path.GetExtension(assetPath).ToLowerInvariant();
MCPForUnity/Editor/ActionTrace/Core/ActionTraceSettings.cs-206-214 (1)

206-214: Reorder asset creation before preset application.

CreateSettings() calls ApplyPreset() which invokes Save() before AssetDatabase.CreateAsset() is executed. Since Save() calls EditorUtility.SetDirty() and AssetDatabase.SaveAssets() on an asset not yet registered in the AssetDatabase, the preset values may not persist properly on first creation. Create the asset first.

♻️ Proposed fix
         private static ActionTraceSettings CreateSettings()
         {
             var settings = CreateInstance<ActionTraceSettings>();
+            AssetDatabase.CreateAsset(settings, SettingsPath);
             // Apply Standard preset by default
             settings.ApplyPreset(ActionTracePreset.Standard);
-            AssetDatabase.CreateAsset(settings, SettingsPath);
             AssetDatabase.SaveAssets();
             AssetDatabase.Refresh();
             McpLog.Info($"[ActionTraceSettings] Created new settings at {SettingsPath}");
             return settings;
         }
🧹 Nitpick comments (19)
MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs (2)

172-192: Registration failure is silently swallowed.

If the reflection-based event registration fails (e.g., API change in a future Unity version), the zombie listener prevention won't work and there's no indication of the failure. Consider adding a debug-level warning in the catch block to aid troubleshooting.

💡 Suggested enhancement
             }
-            catch { }
+            catch (Exception ex)
+            {
+                if (IsDebugEnabled())
+                    McpLog.Warn($"[StdioBridgeHost] Failed to register beforeAssemblyReload handler: {ex.Message}");
+            }

521-544: LGTM - Status file preserved during domain reload.

This correctly addresses the past review concern about the status file being deleted during reload. The guard ensures the Python server can detect the reloading state.

Minor note: The status file path computation logic (lines 528-533) duplicates code in WriteHeartbeat. Consider extracting to a helper method for DRY, though this is optional.

MCPForUnity/Editor/ActionTrace/VCS/VcsContextProvider.cs (1)

68-76: Thread-safety claim may be overstated but is practically safe.

The comment claims thread-safety, but GetCurrentContext has a check-then-act pattern (lines 70-73) that isn't atomic. In practice this is benign—worst case is a redundant GetInitialContext() call. Consider using Interlocked.CompareExchange or ??= pattern if strict thread-safety is required, or soften the comment.

MCPForUnity/Editor/Helpers/GlobalIdHelper.cs (1)

224-230: Method name IsValidId may be misleading.

IsValidId only checks for non-empty strings, not actual format validity. Consider renaming to IsNonEmpty or enhancing to verify the format matches one of the known patterns (GlobalObjectId, Scene:, Asset:, Instance:).

MCPForUnity/Editor/ActionTrace/Capture/UndoGroupManager.cs (1)

51-70: Nested BeginToolCall overwrites previous state without warning.

If BeginToolCall is called while already in a tool call, the previous state is silently overwritten. This could lead to orphaned Undo operations from the first call not being collapsed.

♻️ Suggested fix: Warn or auto-end previous tool call
 public static void BeginToolCall(string toolName, string toolCallId)
 {
+    if (_isInToolCall)
+    {
+        McpLog.Warn($"[UndoGroupManager] BeginToolCall called while already in tool call '{_currentToolName}'. Ending previous call.");
+        EndToolCall();
+    }
+
     if (string.IsNullOrEmpty(toolName))
     {
MCPForUnity/Editor/ActionTrace/Context/ToolCallScope.cs (1)

272-289: Consider logging when Dispose finds scope not at stack top.

The Dispose method silently skips popping if this isn't at the top of the stack (line 283). While proper using() patterns should prevent this, silent failure could hide bugs. Consider adding a warning log similar to ContextStack.Pop's mismatch handling.

♻️ Suggested diagnostic logging
 // Pop from stack
 if (_scopeStack.Value.Count > 0 && _scopeStack.Value.Peek() == this)
 {
     _scopeStack.Value.Pop();
 }
+else if (_scopeStack.Value.Count > 0)
+{
+    UnityEngine.Debug.LogWarning(
+        $"[ToolCallScope] Dispose called but scope {CallId} is not at top of stack. " +
+        $"Top is {_scopeStack.Value.Peek().CallId}. Possible out-of-order disposal.");
+}
MCPForUnity/Editor/ActionTrace/Semantics/DefaultEventScorer.cs (1)

79-105: Consider case-insensitive extension matching.

The extension checks (e.g., ext.ToString() == ".cs") are case-sensitive. While Unity typically uses lowercase extensions, imported assets from external sources might have uppercase extensions (e.g., .CS, .Unity).

♻️ Optional: Case-insensitive comparison
 private static bool IsScript(EditorEvent e)
 {
     if (e.Payload.TryGetValue("extension", out var ext))
-        return ext.ToString() == ".cs";
+        return string.Equals(ext.ToString(), ".cs", StringComparison.OrdinalIgnoreCase);
     if (e.Payload.TryGetValue("asset_type", out var type))
         return type.ToString()?.Contains("Script") == true ||
                type.ToString()?.Contains("MonoScript") == true;
     return false;
 }

Apply similar changes to IsScene and IsPrefab.

MCPForUnity/Editor/ActionTrace/Capture/SelectionPropertyTracker.cs (1)

32-44: Event subscriptions are never unregistered.

The static constructor subscribes to Selection.selectionChanged and Undo.postprocessModifications but never unsubscribes. While Unity typically handles this during domain reloads, explicit cleanup in a domain-reload handler (AssemblyReloadEvents) would be more robust and prevent potential duplicate subscriptions.

♻️ Suggested cleanup handling
 static SelectionPropertyTracker()
 {
     // Initialize with current selection
     UpdateSelectionState();

     // Monitor selection changes
     Selection.selectionChanged += OnSelectionChanged;

     // Monitor property modifications
     Undo.postprocessModifications += OnPropertyModified;

+    // Cleanup on domain reload
+    AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload;
+
     McpLog.Debug("[SelectionPropertyTracker] Initialized");
 }

+private static void OnBeforeAssemblyReload()
+{
+    Selection.selectionChanged -= OnSelectionChanged;
+    Undo.postprocessModifications -= OnPropertyModified;
+    AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeAssemblyReload;
+}
MCPForUnity/Editor/ActionTrace/Core/ActionTraceSettings.Editor.cs (1)

74-78: Redundant save when applying presets.

ApplyPreset already calls MarkDirty() and Save() internally (see ActionTraceSettings.cs lines 159-175). Setting GUI.changed = true will cause ApplyModifiedProperties() to return true, triggering another MarkDirty() + Save() cycle at lines 58-59.

Consider removing the redundant call:

♻️ Suggested fix
                 if (GUILayout.Button(preset.Name))
                 {
                     settings.ApplyPreset(preset);
-                    GUI.changed = true;
                 }
MCPForUnity/Editor/ActionTrace/Core/Presets/ActionTracePreset.cs (1)

95-98: Consider making AllPresets immutable to prevent accidental modification.

AllPresets is a mutable List<ActionTracePreset> that external code could accidentally or maliciously modify (e.g., AllPresets.Clear()). Since this is a public API, consider using an immutable collection.

♻️ Suggested fix
-        public static readonly List<ActionTracePreset> AllPresets = new()
+        public static IReadOnlyList<ActionTracePreset> AllPresets { get; } = new List<ActionTracePreset>
         {
             DebugAll, Standard, Lean, AIFocused, Realtime, Performance
-        };
+        }.AsReadOnly();

Note: This also requires updating ActionTraceSettings.FindPreset to use LINQ's FirstOrDefault instead of List.Find:

return ActionTracePreset.AllPresets.FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
MCPForUnity/Editor/ActionTrace/Helpers/GameObjectTrackingHelper.cs (1)

100-105: Misleading comment: "swap hash sets" doesn't match implementation.

The comment says "swap hash sets to avoid allocation" but the code clears _previousInstanceIds and re-adds items from currentIds. This is not a swap operation and does allocate (the foreach iteration). Consider updating the comment or implementing an actual swap.

♻️ Suggested fix - update comment
-                // Update tracking: swap hash sets to avoid allocation
+                // Update tracking: replace previous IDs with current
                 _previousInstanceIds.Clear();
                 foreach (int id in currentIds)
                 {
                     _previousInstanceIds.Add(id);
                 }
MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs (1)

16-23: Consider using English for code comments.

The enum and its members have Chinese comments. For consistency and broader team accessibility, consider using English.

♻️ Suggested change
     /// <summary>
-    /// 排序模式:控制事件列表的排序方式
+    /// Sort mode: controls event list ordering
     /// </summary>
     public enum SortMode
     {
-        /// <summary>纯时间排序(最新优先)- 给用户查看记录</summary>
+        /// <summary>Sort by time (newest first) - for user viewing</summary>
         ByTimeDesc,
-        /// <summary>AI 视角排序 - 先按时间再按重要性分组</summary>
+        /// <summary>AI perspective sorting - by time then importance grouping</summary>
         AIFiltered
     }
MCPForUnity/Editor/ActionTrace/Core/EventStore.Merging.cs (1)

69-71: Unused parameter evtWithSequence.

The parameter evtWithSequence is declared but never used in the method body. Either remove it or use it for its intended purpose (the doc comment mentions "for updating _lastRecordedEvent").

♻️ Suggested fix

If the parameter is not needed:

-        private static void MergeWithLastEventLocked(EditorEvent evt, EditorEvent evtWithSequence)
+        private static void MergeWithLastEventLocked(EditorEvent evt)

Update callers accordingly.

MCPForUnity/Editor/Tools/AddTimelineNoteTool.cs (1)

35-78: Parameters class is defined but not used for deserialization.

The Parameters class with ToolParameter attributes is defined but HandleCommand manually parses the JObject instead of deserializing into a Parameters instance. This creates duplication and potential drift between the two.

Consider either:

  1. Using the Parameters class for deserialization
  2. Removing the Parameters class if it's only for documentation/schema generation

Also applies to: 80-144

MCPForUnity/Editor/ActionTrace/Capture/SamplingMiddleware.cs (1)

360-401: Thread safety concern: Strategies dictionary is not thread-safe.

SamplingConfig.Strategies is a regular Dictionary<string, SamplingStrategy> that's publicly accessible and mutated via SetStrategy/RemoveStrategy. However, it's read from ShouldRecord and FlushExpiredDebounceSamples, which run on EditorApplication.update.

While Unity Editor is mostly single-threaded, the use of ConcurrentDictionary for _pendingSamples suggests thread safety is a design concern. Consider using ConcurrentDictionary for Strategies as well, or documenting that modifications must only occur from the main thread.

♻️ Suggested fix
-        public static readonly Dictionary<string, SamplingStrategy> Strategies = new()
+        public static readonly ConcurrentDictionary<string, SamplingStrategy> Strategies = new(
+            new Dictionary<string, SamplingStrategy>
         {
             // Hierarchy changes: Throttle to 1 event per second
             {
                 EventTypes.HierarchyChanged,
                 new SamplingStrategy(SamplingMode.Throttle, 1000)
             },
-        };
+        });

And update methods:

         public static void SetStrategy(string eventType, SamplingMode mode, long windowMs = 1000)
         {
             Strategies[eventType] = new SamplingStrategy(mode, windowMs);
         }

         public static void RemoveStrategy(string eventType)
         {
-            Strategies.Remove(eventType);
+            Strategies.TryRemove(eventType, out _);
         }
MCPForUnity/Editor/ActionTrace/Capture/UnityEventHooks.cs (1)

198-212: Use DateTime.UtcNow instead of DateTime.Now for debouncing.

Line 200 uses DateTime.Now while other timestamp operations in the codebase use DateTime.UtcNow (e.g., lines 343, 351, 428, 444). Using local time can cause issues during DST transitions.

♻️ Suggested fix
         private static void OnHierarchyChanged()
         {
-            var now = DateTime.Now;
+            var now = DateTime.UtcNow;
             lock (_lock)
             {
MCPForUnity/Editor/ActionTrace/Capture/EventFilter.cs (1)

249-327: Consider caching active rules to avoid per-call allocations.

GetActiveRules() builds and sorts a list on every filter check. If these paths are hot, cache the computed list and invalidate it when settings or rules change.

MCPForUnity/Editor/ActionTrace/Query/ContextCompressor.cs (1)

48-53: Unused config knob.

TargetCompressionRatio isn’t referenced; consider implementing or removing to avoid a misleading config surface.

MCPForUnity/Editor/ActionTrace/Core/EventTypes.cs (1)

98-103: Avoid string literal duplication for AINote.

"AINote" is duplicated here and in MCPForUnity/Editor/Resources/ActionTrace/ActionTraceViewResource.cs. Add an EventTypes.AINote constant and use it here (and update the resource class) to prevent drift.

♻️ Proposed fix (this file)
         // Build events
         public const string BuildStarted = "BuildStarted";
         public const string BuildCompleted = "BuildCompleted";
         public const string BuildFailed = "BuildFailed";
+        // AI/system events
+        public const string AINote = "AINote";
@@
-                ["AINote"] = new EventMetadata
+                [AINote] = new EventMetadata
                 {
                     Category = EventCategory.System,
                     DefaultImportance = 1.0f,
                     SummaryTemplate = "AI Note{if:agent_id, ({agent_id})}: {note}",
                 },

Comment on lines +103 to +128
// ========== Modified Assets ==========
// Track asset modifications separately (e.g., texture imports, prefab changes)
foreach (var assetPath in importedAssets)
{
if (string.IsNullOrEmpty(assetPath)) continue;

// Only track modifications for existing assets
if (!IsNewlyCreatedAsset(assetPath) && EventFilter.ShouldTrackAsset(assetPath))
{
string targetId = $"Asset:{assetPath}";
string assetType = GetAssetType(assetPath);

// Only record modifications for certain asset types
if (ShouldTrackModification(assetPath))
{
var payload = new Dictionary<string, object>
{
["path"] = assetPath,
["extension"] = System.IO.Path.GetExtension(assetPath),
["asset_type"] = assetType
};

RecordEvent(EventTypes.AssetModified, targetId, payload);
}
}
}
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

Duplicate event emission for imported assets.

This section iterates importedAssets again and emits AssetModified events for non-newly-created assets. However, lines 56-58 already emit AssetImported for these same assets, resulting in duplicate events for the same import operation.

Since IsNewlyCreatedAsset always returns false, every imported asset that passes ShouldTrackModification will generate both an AssetImported event (line 58) and an AssetModified event (line 125).

Consider removing this section or making it mutually exclusive with the imported assets handling above.

🐛 Suggested fix
-            // ========== Modified Assets ==========
-            // Track asset modifications separately (e.g., texture imports, prefab changes)
-            foreach (var assetPath in importedAssets)
-            {
-                if (string.IsNullOrEmpty(assetPath)) continue;
-
-                // Only track modifications for existing assets
-                if (!IsNewlyCreatedAsset(assetPath) && EventFilter.ShouldTrackAsset(assetPath))
-                {
-                    string targetId = $"Asset:{assetPath}";
-                    string assetType = GetAssetType(assetPath);
-
-                    // Only record modifications for certain asset types
-                    if (ShouldTrackModification(assetPath))
-                    {
-                        var payload = new Dictionary<string, object>
-                        {
-                            ["path"] = assetPath,
-                            ["extension"] = System.IO.Path.GetExtension(assetPath),
-                            ["asset_type"] = assetType
-                        };
-
-                        RecordEvent(EventTypes.AssetModified, targetId, payload);
-                    }
-                }
-            }

If you need to distinguish modifications from imports, implement IsNewlyCreatedAsset properly first, then use it to emit either AssetCreated/AssetImported OR AssetModified, but not both.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// ========== Modified Assets ==========
// Track asset modifications separately (e.g., texture imports, prefab changes)
foreach (var assetPath in importedAssets)
{
if (string.IsNullOrEmpty(assetPath)) continue;
// Only track modifications for existing assets
if (!IsNewlyCreatedAsset(assetPath) && EventFilter.ShouldTrackAsset(assetPath))
{
string targetId = $"Asset:{assetPath}";
string assetType = GetAssetType(assetPath);
// Only record modifications for certain asset types
if (ShouldTrackModification(assetPath))
{
var payload = new Dictionary<string, object>
{
["path"] = assetPath,
["extension"] = System.IO.Path.GetExtension(assetPath),
["asset_type"] = assetType
};
RecordEvent(EventTypes.AssetModified, targetId, payload);
}
}
}
🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/ActionTrace/Capture/AssetChangePostprocessor.cs` around
lines 103 - 128, The code emits duplicate events for the same imported assets
because importedAssets are processed earlier to emit AssetImported (via
RecordEvent) and this later loop emits AssetModified for the same entries;
either remove this AssetModified loop or make it mutually exclusive by using
IsNewlyCreatedAsset correctly so an asset only triggers
AssetImported/AssetCreated OR AssetModified; update the logic around
importedAssets, IsNewlyCreatedAsset, ShouldTrackModification and RecordEvent (or
adjust ShouldTrackModification) to ensure only one event type is recorded per
asset.

Comment on lines +127 to +133
// Enforce cache limit to prevent unbounded growth
if (_pendingChanges.Count >= MaxPendingEntries)
{
// Force flush before adding new entry
FlushPendingChanges();
}

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

Enforce MaxPendingEntries even when debounce hasn’t expired.

When the cache hits the limit, FlushPendingChanges() only removes expired entries, so under continuous edits the cache can still grow past the cap. Consider a force-flush or explicit eviction path when the limit is reached.

🐛 Proposed fix
-                if (_pendingChanges.Count >= MaxPendingEntries)
-                {
-                    // Force flush before adding new entry
-                    FlushPendingChanges();
-                }
+                if (_pendingChanges.Count >= MaxPendingEntries)
+                {
+                    // Force flush before adding new entry
+                    FlushPendingChanges(force: true);
+                }
-        private static void FlushPendingChanges()
+        private static void FlushPendingChanges(bool force = false)
         {
             if (_pendingChanges.Count == 0)
                 return;

             long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

             foreach (var kvp in _pendingChanges)
             {
                 // Check if entry has expired (no updates for DebounceWindowMs)
-                if (nowMs - kvp.Value.LastUpdateMs >= DebounceWindowMs)
+                if (force || nowMs - kvp.Value.LastUpdateMs >= DebounceWindowMs)
                 {
                     // Record the PropertyModified event
                     RecordPropertyModifiedEvent(kvp.Value);

Also applies to: 159-170

🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/ActionTrace/Capture/PropertyChangeTracker.cs` around lines
127 - 133, The pending-change cap check in PropertyChangeTracker is insufficient
because _pendingChanges.Count >= MaxPendingEntries only calls
FlushPendingChanges(), which currently only removes expired entries; update the
logic to enforce the cap by performing a real eviction or immediate flush when
the limit is reached: inside the block where MaxPendingEntries is checked
(referencing _pendingChanges, MaxPendingEntries and FlushPendingChanges()), call
a new or existing force flush method (e.g., ForceFlushPendingChanges()) that
writes out or clears all pending entries, or implement explicit eviction (remove
oldest entries from _pendingChanges until Count < MaxPendingEntries) before
adding the new entry so the cache cannot grow beyond the cap.

Comment on lines +533 to +536
}

#endregion
}
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 | 🔴 Critical

Syntax error: stray #endregion outside class.

Line 535 has an #endregion directive after the class closing brace, which will cause a compilation error.

🐛 Suggested fix
         }
-    }
 
     `#endregion`
+    }
 }

Or remove the stray #endregion if it's not needed:

         }
     }
-
-    `#endregion`
 }
🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/ActionTrace/Capture/UnityEventHooks.cs` around lines 533 -
536, There is a stray preprocessor directive "#endregion" placed after the
closing brace of the UnityEventHooks class; remove that trailing "#endregion"
(or move it so it matches the corresponding "#region" inside the class) to
ensure the "#endregion" is inside the class scope and eliminate the syntax error
in UnityEventHooks.cs.

Comment on lines +471 to +494
public static System.Threading.Tasks.Task<T> ExecuteAsync<T>(
string toolName,
Func<System.Threading.Tasks.Task<T>> func,
string toolId = null,
Dictionary<string, object> parameters = null)
{
var scope = new ToolCallScope(toolName, toolId, parameters);

var task = func();

return task.ContinueWith(t =>
{
if (t.IsFaulted)
{
scope.Fail(t.Exception?.Message ?? "Async faulted");
throw t.Exception ?? new Exception("Async task faulted");
}
else
{
scope.Complete(t.Result?.ToString() ?? "");
return t.Result;
}
}, System.Threading.Tasks.TaskScheduler.Default);
}
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

ExecuteAsync leaks the scope on all paths.

The ToolCallScope is created but never disposed. The ContinueWith callback completes or fails the scope but doesn't call Dispose(), leaving it on the stack. Additionally, if func() throws synchronously before returning a Task, the scope leaks entirely.

🐛 Proposed fix for async scope management
 public static System.Threading.Tasks.Task<T> ExecuteAsync<T>(
     string toolName,
     Func<System.Threading.Tasks.Task<T>> func,
     string toolId = null,
     Dictionary<string, object> parameters = null)
 {
     var scope = new ToolCallScope(toolName, toolId, parameters);

-    var task = func();
-
-    return task.ContinueWith(t =>
+    System.Threading.Tasks.Task<T> task;
+    try
+    {
+        task = func();
+    }
+    catch (Exception ex)
     {
-        if (t.IsFaulted)
-        {
-            scope.Fail(t.Exception?.Message ?? "Async faulted");
-            throw t.Exception ?? new Exception("Async task faulted");
-        }
-        else
+        scope.Fail(ex.Message);
+        scope.Dispose();
+        throw;
+    }
+
+    return task.ContinueWith(t =>
+    {
+        try
         {
-            scope.Complete(t.Result?.ToString() ?? "");
-            return t.Result;
+            if (t.IsFaulted)
+            {
+                scope.Fail(t.Exception?.InnerException?.Message ?? "Async faulted");
+                throw t.Exception?.InnerException ?? new Exception("Async task faulted");
+            }
+            else
+            {
+                scope.Complete(t.Result?.ToString() ?? "");
+                return t.Result;
+            }
+        }
+        finally
+        {
+            scope.Dispose();
         }
     }, System.Threading.Tasks.TaskScheduler.Default);
 }
🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/ActionTrace/Context/ToolCallScope.cs` around lines 471 -
494, ExecuteAsync<T> creates a ToolCallScope but never disposes it (scope leaks
on both asynchronous and synchronous-failure paths); fix by ensuring
scope.Dispose() is always called: wrap the func() invocation so synchronous
exceptions are caught (try/catch around func()), and change the continuation
flow to guarantee disposal (either by converting ExecuteAsync<T> to an async
method that awaits func() and uses try { var result = await func();
scope.Complete(...); return result; } catch (Exception ex) { scope.Fail(...);
throw; } finally { scope.Dispose(); } or, if keeping ContinueWith, invoke
scope.Dispose() after calling scope.Complete/Fail inside every continuation
branch and also catch synchronous exceptions from func() to call scope.Fail(...)
and scope.Dispose() before rethrowing). Ensure you reference ExecuteAsync<T>,
ToolCallScope, scope, func(), ContinueWith when applying the fix.

Comment on lines +155 to +158
// Build lookup dictionary outside lock
var mappingBySequence = mappingsSnapshot
.GroupBy(m => m.EventSequence)
.ToDictionary(g => g.Key, g => g.FirstOrDefault());
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

FirstOrDefault discards multiple context mappings per event.

The class doc at line 28-29 states "Multiple mappings allowed for same eventSequence (different contexts)", but FirstOrDefault() only returns one mapping per event sequence. This loses context information when an event is associated with multiple contexts.

🐛 Suggested fix: Return all mappings or change the return type

Option 1 - Keep first mapping (document behavior):

-            // Build lookup dictionary outside lock
-            var mappingBySequence = mappingsSnapshot
-                .GroupBy(m => m.EventSequence)
-                .ToDictionary(g => g.Key, g => g.FirstOrDefault());
+            // Build lookup dictionary outside lock (returns first mapping per event)
+            var mappingBySequence = mappingsSnapshot
+                .GroupBy(m => m.EventSequence)
+                .ToDictionary(g => g.Key, g => g.First());

Option 2 - Return all mappings (requires signature change):

// Change return type to include all mappings
var mappingBySequence = mappingsSnapshot
    .GroupBy(m => m.EventSequence)
    .ToDictionary(g => g.Key, g => g.ToList());
🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/ActionTrace/Core/EventStore.Context.cs` around lines 155 -
158, The code uses mappingsSnapshot.GroupBy(m => m.EventSequence).ToDictionary(g
=> g.Key, g => g.FirstOrDefault()), which drops additional mappings for the same
EventSequence; update the logic to preserve all mappings by changing the
dictionary value to a collection (e.g., use g => g.ToList()) and update any
dependent return types/usages of mappingBySequence to accept IEnumerable/List of
mappings, or if you intentionally want only one mapping, explicitly document
that behavior and keep FirstOrDefault; reference mappingBySequence,
mappingsSnapshot, EventSequence, and FirstOrDefault when making the change.

Comment on lines +70 to +113
public static List<(GameObject obj, bool isNew)> DetectChanges()
{
AssertMainThread();

if (!_hasInitialized)
{
InitializeTracking();
return new List<(GameObject, bool)>(0);
}

var results = new List<(GameObject, bool)>(64);
var currentIds = new HashSet<int>(256);

try
{
GameObject[] currentObjects = GameObject.FindObjectsOfType<GameObject>(true);

// First pass: collect current IDs and detect new objects
foreach (var go in currentObjects)
{
if (go == null) continue;

int id = go.GetInstanceID();
currentIds.Add(id);

// HashSet.Contains() is O(1) vs List.Contains() O(n)
bool isNew = !_previousInstanceIds.Contains(id);
results.Add((go, isNew));
}

// Update tracking: swap hash sets to avoid allocation
_previousInstanceIds.Clear();
foreach (int id in currentIds)
{
_previousInstanceIds.Add(id);
}
}
catch (Exception ex)
{
Debug.LogError($"[ActionTrace] Failed to detect GameObject changes: {ex.Message}");
}

return results;
}
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how DetectChanges is called to understand frequency
rg -n "DetectChanges\(\)" --type=cs -C3

Repository: CoplayDev/unity-mcp

Length of output: 1458


🏁 Script executed:

#!/bin/bash
# Check how TrackGameObjectChanges is called and what invokes it
rg -n "TrackGameObjectChanges" --type=cs -B5 -A5

Repository: CoplayDev/unity-mcp

Length of output: 1607


🏁 Script executed:

#!/bin/bash
# Read UnityEventHooks.cs to understand the context and frequency of calls
rg -n "class UnityEventHooks|void.*\(\)|private static void\|public static void" MCPForUnity/Editor/ActionTrace/Capture/UnityEventHooks.cs -B2 -A10 | head -100

Repository: CoplayDev/unity-mcp

Length of output: 3211


🏁 Script executed:

#!/bin/bash
# Search for registration of OnUpdate event
rg -n "OnUpdate|EditorApplication.update" MCPForUnity/Editor/ActionTrace/Capture/UnityEventHooks.cs -B2 -A2

Repository: CoplayDev/unity-mcp

Length of output: 507


FindObjectsOfType is called every editor frame and will impact performance.

FindObjectsOfType<GameObject>(true) is registered to EditorApplication.update and runs unconditionally every frame (see line 70 in UnityEventHooks.cs). This method traverses the entire scene hierarchy and allocates an array on each invocation. In large scenes with many GameObjects, this will noticeably impact editor responsiveness. Consider adding throttling (e.g., checking changes only every N frames or on time-based intervals) or only running detection when specific events occur rather than every frame.

🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/ActionTrace/Helpers/GameObjectTrackingHelper.cs` around
lines 70 - 113, DetectChanges is currently calling
FindObjectsOfType<GameObject>(true) every editor frame (via the
EditorApplication.update hook), which is expensive; update DetectChanges (and
the registration in EditorApplication.update) to short-circuit and avoid calling
FindObjectsOfType each frame by adding simple throttling or event-driven checks:
add a static/frame counter or lastRunTime check and return cached results until
N frames or T milliseconds have passed, or only run the full traversal when
specific editor events occur, then keep the existing logic that fills results
and updates _previousInstanceIds when the check actually runs; ensure the method
still asserts main thread via AssertMainThread and preserves behavior of marking
isNew by comparing against _previousInstanceIds.

Comment on lines +94 to +323
private HashSet<string> _searchTerms;
private float? _minImportance;
private float? _maxImportance;
private QueryTimeRange? _timeRange;
private QuerySortOrder _sortOrder = QuerySortOrder.NewestFirst;
private int? _limit;
private int? _offset;

public EventQueryBuilder(IEventScorer scorer = null, IEventCategorizer categorizer = null)
{
_scorer = scorer ?? new DefaultEventScorer();
_categorizer = categorizer ?? new DefaultCategorizer();
}

// ========== Type Filters ==========

/// <summary>
/// Filter to events of the specified type.
/// </summary>
public EventQueryBuilder OfType(string eventType)
{
_includedTypes ??= new HashSet<string>();
_includedTypes.Add(eventType);
return this;
}

/// <summary>
/// Filter to events of any of the specified types.
/// </summary>
public EventQueryBuilder OfTypes(params string[] eventTypes)
{
_includedTypes ??= new HashSet<string>();
foreach (string type in eventTypes)
_includedTypes.Add(type);
return this;
}

/// <summary>
/// Exclude events of the specified type.
/// </summary>
public EventQueryBuilder NotOfType(string eventType)
{
_excludedTypes ??= new HashSet<string>();
_excludedTypes.Add(eventType);
return this;
}

/// <summary>
/// Exclude events of any of the specified types.
/// </summary>
public EventQueryBuilder NotOfTypes(params string[] eventTypes)
{
_excludedTypes ??= new HashSet<string>();
foreach (string type in eventTypes)
_excludedTypes.Add(type);
return this;
}

// ========== Category Filters ==========

/// <summary>
/// Filter to events in the specified category.
/// </summary>
public EventQueryBuilder InCategory(EventCategory category)
{
_includedCategories ??= new HashSet<EventCategory>();
_includedCategories.Add(category);
return this;
}

/// <summary>
/// Filter to events in any of the specified categories.
/// </summary>
public EventQueryBuilder InCategories(params EventCategory[] categories)
{
_includedCategories ??= new HashSet<EventCategory>();
foreach (var cat in categories)
_includedCategories.Add(cat);
return this;
}

/// <summary>
/// Exclude events in the specified category.
/// </summary>
public EventQueryBuilder NotInCategory(EventCategory category)
{
_excludedCategories ??= new HashSet<EventCategory>();
_excludedCategories.Add(category);
return this;
}

// ========== Target Filters ==========

/// <summary>
/// Filter to events for the specified target ID.
/// </summary>
public EventQueryBuilder ForTarget(string targetId)
{
_includedTargets ??= new HashSet<string>();
_includedTargets.Add(targetId);
return this;
}

/// <summary>
/// Filter to events for any of the specified targets.
/// </summary>
public EventQueryBuilder ForTargets(params string[] targetIds)
{
_includedTargets ??= new HashSet<string>();
foreach (string id in targetIds)
_includedTargets.Add(id);
return this;
}

// ========== Importance Filters ==========

/// <summary>
/// Filter to events with minimum importance score.
/// </summary>
public EventQueryBuilder WithMinImportance(float minScore)
{
_minImportance = minScore;
return this;
}

/// <summary>
/// Filter to events with maximum importance score.
/// </summary>
public EventQueryBuilder WithMaxImportance(float maxScore)
{
_maxImportance = maxScore;
return this;
}

/// <summary>
/// Filter to events within an importance range.
/// </summary>
public EventQueryBuilder WithImportanceBetween(float minScore, float maxScore)
{
_minImportance = minScore;
_maxImportance = maxScore;
return this;
}

/// <summary>
/// Filter to critical events only.
/// </summary>
public EventQueryBuilder CriticalOnly()
{
return WithMinImportance(0.9f);
}

/// <summary>
/// Filter to important events (high and critical).
/// </summary>
public EventQueryBuilder ImportantOnly()
{
return WithMinImportance(0.7f);
}

// ========== Time Filters ==========

/// <summary>
/// Filter to events within the specified time range.
/// </summary>
public EventQueryBuilder InTimeRange(QueryTimeRange range)
{
_timeRange = range;
return this;
}

/// <summary>
/// Filter to events in the last N minutes.
/// </summary>
public EventQueryBuilder InLastMinutes(int minutes)
{
_timeRange = QueryTimeRange.LastMinutes(minutes);
return this;
}

/// <summary>
/// Filter to events in the last N hours.
/// </summary>
public EventQueryBuilder InLastHours(int hours)
{
_timeRange = QueryTimeRange.LastHours(hours);
return this;
}

/// <summary>
/// Filter to events since a specific timestamp.
/// </summary>
public EventQueryBuilder Since(long timestampMs)
{
_timeRange = QueryTimeRange.Since(timestampMs);
return this;
}

/// <summary>
/// Filter to events between two timestamps.
/// </summary>
public EventQueryBuilder Between(long startMs, long endMs)
{
_timeRange = QueryTimeRange.Between(startMs, endMs);
return this;
}

// ========== Search Filters ==========

/// <summary>
/// Filter to events containing any of the search terms (case-insensitive).
/// Searches in summary text and target ID.
/// </summary>
public EventQueryBuilder WithSearchTerm(string term)
{
_searchTerms ??= new HashSet<string>(StringComparer.OrdinalIgnoreCase);
_searchTerms.Add(term);
return this;
}

/// <summary>
/// Filter to events containing all of the search terms.
/// </summary>
public EventQueryBuilder WithAllSearchTerms(params string[] terms)
{
_searchTerms ??= new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (string term in terms)
_searchTerms.Add(term);
return this;
}
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

WithAllSearchTerms behaves like OR and accepts null terms.

This contradicts the API contract and can throw on null entries; track “all terms” semantics and ignore empty terms.

🐛 Proposed fix
         private HashSet<string> _includedTargets;
         private HashSet<string> _searchTerms;
+        private bool _requireAllSearchTerms;
         private float? _minImportance;
         private float? _maxImportance;
@@
         public EventQueryBuilder WithSearchTerm(string term)
         {
-            _searchTerms ??= new HashSet<string>(StringComparer.OrdinalIgnoreCase);
-            _searchTerms.Add(term);
+            if (string.IsNullOrWhiteSpace(term)) return this;
+            _searchTerms ??= new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+            _requireAllSearchTerms = false;
+            _searchTerms.Add(term);
             return this;
         }
@@
         public EventQueryBuilder WithAllSearchTerms(params string[] terms)
         {
             _searchTerms ??= new HashSet<string>(StringComparer.OrdinalIgnoreCase);
-            foreach (string term in terms)
-                _searchTerms.Add(term);
+            _requireAllSearchTerms = true;
+            foreach (string term in terms)
+                if (!string.IsNullOrWhiteSpace(term))
+                    _searchTerms.Add(term);
             return this;
         }
@@
             if (_searchTerms != null && _searchTerms.Count > 0)
             {
                 string summary = (evt.GetSummary() ?? "").ToLowerInvariant();
                 string target = (evt.TargetId ?? "").ToLowerInvariant();
 
-                bool matchesAny = false;
+                bool matchesAny = false;
+                bool matchesAll = true;
                 foreach (string term in _searchTerms)
                 {
                     string lowerTerm = term.ToLowerInvariant();
-                    if (summary.Contains(lowerTerm) || target.Contains(lowerTerm))
-                    {
-                        matchesAny = true;
-                        break;
-                    }
+                    bool matched = summary.Contains(lowerTerm) || target.Contains(lowerTerm);
+                    matchesAny |= matched;
+                    matchesAll &= matched;
+                    if (!_requireAllSearchTerms && matchesAny) break;
+                    if (_requireAllSearchTerms && !matchesAll) break;
                 }
 
-                if (!matchesAny)
+                if (_requireAllSearchTerms ? !matchesAll : !matchesAny)
                     return false;
             }

Also applies to: 478-495

🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/ActionTrace/Query/EventQueryBuilder.cs` around lines 94 -
323, WithAllSearchTerms currently adds terms into the same _searchTerms set
(making it behave like OR) and accepts null/empty entries; change it to record
required-all semantics and ignore null/empty terms. Specifically, introduce or
use a distinct collection/name (e.g., _allSearchTerms or a boolean flag paired
with _allSearchTerms) in the EventQueryBuilder and have
WithAllSearchTerms(params string[] terms) skip null/empty strings
(String.IsNullOrWhiteSpace) when adding; update the query evaluation logic to
check _allSearchTerms for an AND (all terms must match) requirement instead of
treating them as OR. Ensure WithSearchTerm retains case-insensitive HashSet
behavior and does not accept null/empty either.

Comment on lines +414 to +447
private List<TargetStats> FindTopTargets(IReadOnlyList<EditorEvent> events)
{
var targetEvents = new Dictionary<string, List<EditorEvent>>();

foreach (var evt in events)
{
if (string.IsNullOrEmpty(evt.TargetId)) continue;

if (!targetEvents.TryGetValue(evt.TargetId, out var list))
{
list = new List<EditorEvent>();
targetEvents[evt.TargetId] = list;
}
list.Add(evt);
}

var nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var stats = new List<TargetStats>();

foreach (var kvp in targetEvents)
{
var targetEventsList = kvp.Value;
double recencyFactor = CalculateRecencyFactor(targetEventsList, nowMs);
double activityScore = targetEventsList.Count * recencyFactor;

stats.Add(new TargetStats
{
TargetId = kvp.Key,
DisplayName = GetDisplayName(targetEventsList[^1]),
EventCount = targetEventsList.Count,
EventTypes = targetEventsList.Select(e => e.Type).Distinct().ToList(),
LastActivityMs = targetEventsList[^1].TimestampUnixMs,
ActivityScore = activityScore
});
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

Top‑target recency uses list order, not timestamp order.

targetEventsList[^1] depends on input order, so LastActivityMs, DisplayName, and ActivityScore can be wrong if the input isn’t chronological. Sort per‑target lists by timestamp before using the last item.

🐛 Proposed fix
             foreach (var kvp in targetEvents)
             {
                 var targetEventsList = kvp.Value;
-                double recencyFactor = CalculateRecencyFactor(targetEventsList, nowMs);
+                var orderedTargetEvents = targetEventsList.OrderBy(e => e.TimestampUnixMs).ToList();
+                double recencyFactor = CalculateRecencyFactor(orderedTargetEvents, nowMs);
                 double activityScore = targetEventsList.Count * recencyFactor;
 
                 stats.Add(new TargetStats
                 {
                     TargetId = kvp.Key,
-                    DisplayName = GetDisplayName(targetEventsList[^1]),
+                    DisplayName = GetDisplayName(orderedTargetEvents[^1]),
                     EventCount = targetEventsList.Count,
-                    EventTypes = targetEventsList.Select(e => e.Type).Distinct().ToList(),
-                    LastActivityMs = targetEventsList[^1].TimestampUnixMs,
+                    EventTypes = orderedTargetEvents.Select(e => e.Type).Distinct().ToList(),
+                    LastActivityMs = orderedTargetEvents[^1].TimestampUnixMs,
                     ActivityScore = activityScore
                 });
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private List<TargetStats> FindTopTargets(IReadOnlyList<EditorEvent> events)
{
var targetEvents = new Dictionary<string, List<EditorEvent>>();
foreach (var evt in events)
{
if (string.IsNullOrEmpty(evt.TargetId)) continue;
if (!targetEvents.TryGetValue(evt.TargetId, out var list))
{
list = new List<EditorEvent>();
targetEvents[evt.TargetId] = list;
}
list.Add(evt);
}
var nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var stats = new List<TargetStats>();
foreach (var kvp in targetEvents)
{
var targetEventsList = kvp.Value;
double recencyFactor = CalculateRecencyFactor(targetEventsList, nowMs);
double activityScore = targetEventsList.Count * recencyFactor;
stats.Add(new TargetStats
{
TargetId = kvp.Key,
DisplayName = GetDisplayName(targetEventsList[^1]),
EventCount = targetEventsList.Count,
EventTypes = targetEventsList.Select(e => e.Type).Distinct().ToList(),
LastActivityMs = targetEventsList[^1].TimestampUnixMs,
ActivityScore = activityScore
});
private List<TargetStats> FindTopTargets(IReadOnlyList<EditorEvent> events)
{
var targetEvents = new Dictionary<string, List<EditorEvent>>();
foreach (var evt in events)
{
if (string.IsNullOrEmpty(evt.TargetId)) continue;
if (!targetEvents.TryGetValue(evt.TargetId, out var list))
{
list = new List<EditorEvent>();
targetEvents[evt.TargetId] = list;
}
list.Add(evt);
}
var nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var stats = new List<TargetStats>();
foreach (var kvp in targetEvents)
{
var targetEventsList = kvp.Value;
var orderedTargetEvents = targetEventsList.OrderBy(e => e.TimestampUnixMs).ToList();
double recencyFactor = CalculateRecencyFactor(orderedTargetEvents, nowMs);
double activityScore = targetEventsList.Count * recencyFactor;
stats.Add(new TargetStats
{
TargetId = kvp.Key,
DisplayName = GetDisplayName(orderedTargetEvents[^1]),
EventCount = targetEventsList.Count,
EventTypes = orderedTargetEvents.Select(e => e.Type).Distinct().ToList(),
LastActivityMs = orderedTargetEvents[^1].TimestampUnixMs,
ActivityScore = activityScore
});
🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/ActionTrace/Query/EventStatistics.cs` around lines 414 -
447, FindTopTargets currently uses targetEventsList[^1] which assumes the
per-target events are chronological; instead sort each targetEvents list by
TimestampUnixMs (ascending) before calling CalculateRecencyFactor and before
reading the last event for LastActivityMs and DisplayName. Update FindTopTargets
to replace/derive targetEventsList with a timestamp-ordered sequence (or
in-place sort) and then compute recencyFactor =
CalculateRecencyFactor(orderedList, nowMs), ActivityScore, EventCount,
EventTypes, LastActivityMs and DisplayName from the orderedList.Last() (or
orderedList[^1]) so all derived values use the most recent event. Use the same
ordered list when building TargetStats to ensure consistency.

Comment on lines +102 to +112
// Handle regular placeholders
foreach (var kvp in evt.Payload ?? new Dictionary<string, object>())
{
string placeholder = "{" + kvp.Key + "}";
int index = 0;
while ((index = sb.ToString().IndexOf(placeholder, index, StringComparison.Ordinal)) >= 0)
{
string value = FormatValue(kvp.Value);
sb.Remove(index, placeholder.Length);
sb.Insert(index, value);
}
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

Avoid potential infinite loop when placeholder value contains itself.

The current replacement loop can spin indefinitely if a payload value contains its own placeholder (e.g., value = "{foo}"). Using StringBuilder.Replace avoids this hazard and is simpler.

🐛 Proposed fix
-            foreach (var kvp in evt.Payload ?? new Dictionary<string, object>())
-            {
-                string placeholder = "{" + kvp.Key + "}";
-                int index = 0;
-                while ((index = sb.ToString().IndexOf(placeholder, index, StringComparison.Ordinal)) >= 0)
-                {
-                    string value = FormatValue(kvp.Value);
-                    sb.Remove(index, placeholder.Length);
-                    sb.Insert(index, value);
-                }
-            }
+            foreach (var kvp in evt.Payload ?? new Dictionary<string, object>())
+            {
+                string placeholder = "{" + kvp.Key + "}";
+                string value = FormatValue(kvp.Value);
+                sb.Replace(placeholder, value);
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Handle regular placeholders
foreach (var kvp in evt.Payload ?? new Dictionary<string, object>())
{
string placeholder = "{" + kvp.Key + "}";
int index = 0;
while ((index = sb.ToString().IndexOf(placeholder, index, StringComparison.Ordinal)) >= 0)
{
string value = FormatValue(kvp.Value);
sb.Remove(index, placeholder.Length);
sb.Insert(index, value);
}
// Handle regular placeholders
foreach (var kvp in evt.Payload ?? new Dictionary<string, object>())
{
string placeholder = "{" + kvp.Key + "}";
string value = FormatValue(kvp.Value);
sb.Replace(placeholder, value);
}
🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/ActionTrace/Query/EventSummarizer.cs` around lines 102 -
112, The while-loop that replaces placeholders in sb can infinite-loop if a
formatted value contains the same placeholder; replace that manual index loop
with StringBuilder.Replace to perform a safe global replacement: for each kvp in
evt.Payload (or empty dict) compute placeholder = "{" + kvp.Key + "}" and value
= FormatValue(kvp.Value) and call sb.Replace(placeholder, value). Ensure you
keep the surrounding foreach over evt.Payload (and the null-coalescing) and
remove the manual index-based removal/Insert logic to avoid the self-referential
replacement bug.

Comment on lines +119 to +121
// Always record all events, filter at query time based on mode
if (ActionTraceSettings.Instance != null)
ActionTraceSettings.Instance.Filtering.BypassImportanceFilter = true;
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

Side effect: Window modifies global BypassImportanceFilter setting.

Opening this window permanently changes ActionTraceSettings.Instance.Filtering.BypassImportanceFilter to true, affecting all consumers of ActionTraceSettings even after the window is closed. This is unexpected behavior for a viewer window.

🐛 Suggested approaches

Option 1 - Restore on disable:

private bool _previousBypassValue;

private void OnEnable()
{
    EditorApplication.update += OnEditorUpdate;
    if (ActionTraceSettings.Instance != null)
    {
        _previousBypassValue = ActionTraceSettings.Instance.Filtering.BypassImportanceFilter;
        ActionTraceSettings.Instance.Filtering.BypassImportanceFilter = true;
    }
}

private void OnDisable()
{
    EditorApplication.update -= OnEditorUpdate;
    if (ActionTraceSettings.Instance != null)
    {
        ActionTraceSettings.Instance.Filtering.BypassImportanceFilter = _previousBypassValue;
    }
}

Option 2 - Query all events without modifying settings:
Use a separate query path that bypasses filtering at the query level rather than modifying global settings.

🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs` around lines 119 -
121, The window currently forces
ActionTraceSettings.Instance.Filtering.BypassImportanceFilter = true in
ActionTraceEditorWindow (causing a global side-effect); change this so the
previous value is preserved and restored: in ActionTraceEditorWindow add a
private bool (e.g. _previousBypassValue), capture
ActionTraceSettings.Instance.Filtering.BypassImportanceFilter in OnEnable before
setting it to true, and restore that saved value in OnDisable (also ensure
EditorApplication.update handlers are balanced in OnEnable/OnDisable);
alternatively implement a query-level bypass inside the window’s data-fetching
path instead of touching
ActionTraceSettings.Instance.Filtering.BypassImportanceFilter.

@whatevertogo
Copy link
Contributor Author

Overview

ActionTrace is an event tracing and recording system for the Unity Editor, designed to capture, store, and query editor operation events. This system is built for AI-assisted development scenarios, providing traceability of operations and semantic understanding capabilities.

Design Principles

  1. Immutable Event Layer: EditorEvent instances are never modified once written.

  2. Separation of Concerns: Clear separation between the event layer and the context layer.

  3. Query-Time Semantic Calculation: Importance, Category, and Intent are calculated at query time and are not stored.

  4. Memory Optimization: Memory usage is controlled through a Dehydration mechanism.

Module Architecture

text
ActionTrace/
├── Core/               # Core data models and storage
├── Capture/            # Unity event capture and filtering
├── Context/            # Operation context management
├── Semantics/          # Semantic analysis (scoring, categorization, intent inference)
├── Query/              # Query and projection engine
├── VCS/                # Version control integration
├── Helpers/            # Utility classes
└── Descriptors/        # Event descriptors

File Inventory (40 CS Files)

Core Module (10 files)

File | Description -- | -- EventTypes.cs | Event type constant definitions EventMetadata.cs | Event metadata (labels, templates) EditorEvent.cs | Immutable event class supporting dehydration EventStore.cs | Core API for event storage EventStore.Merging.cs | Event merging/deduplication logic EventStore.Persistence.cs | Persistence and domain reload survival EventStore.Context.cs | Context mapping management EventStore.Diagnostics.cs | Memory diagnostics and dehydration ActionTraceSettings.cs | Persistent settings configuration (layered properties) SamplingMode.cs | Sampling mode enumeration

Performance Characteristics

  1. Memory Management

    • Hot events retain full payloads.

    • Cold events are automatically dehydrated (Payload = null).

    • Hard limit is 1.5 times MaxEvents.

  2. Thread Safety

    • Writes: Main thread only.

    • Reads: Any thread, using lock-snapshot mode.

    • Sequence generation: Interlocked.Increment for atomic operations.

  3. Persistence

    • Uses McpJobStateStore to survive domain reloads.

    • Delayed save strategy with merging via delayCall.

Key Design Decisions

  1. Event Immutability: Events are never modified after creation, ensuring traceability accuracy.

  2. Edge Table Context: Context is associated through the ContextMapping edge table, keeping event data pure.

  3. Query-Time Calculation: Semantic information is calculated at query time to avoid storage redundancy.

  4. Layered Filtering: L1 Blacklist → L2 Sampling → L3 Importance Scoring.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant