Skip to content

Conversation

@whatevertogo
Copy link
Contributor

@whatevertogo whatevertogo commented Jan 21, 2026

This PR introduces two foundational systems designed to provide AI agents with "eyes and memory" within the Unity Editor:

ActionTrace: A sophisticated event tracking system that records, summarizes, and analyzes editor operations. It features a unique Dehydration mechanism (reducing memory from ~10KB to ~100 bytes per event) to ensure long-term stability.

HookSystem: A decoupled event dispatching backbone that abstracts Unity's native callbacks into a clean, thread-safe subscription interface.

These systems collectively allow the AI assistant to reason about what happened in the scene, why it happened (intent), and how it relates to the current task.

🏗 Architecture & Event Flow
The following diagram illustrates how a raw Unity event is transformed into high-level AI context:

Data Pipeline: Unity Native Events → UnityEventHooks → HookRegistry → ActionTraceRecorder → EventStore → Semantic Analysis (Scorer/Categorizer) → MCP Tools

🛠 Changes Made

  1. ActionTrace System (The "Memory")
    Capture Layer: Implemented Recorder.cs and SamplingMiddleware.cs for noise-reduced event logging.

Core Layer: Developed an immutable EditorEvent model with automatic dehydration and a thread-safe EventStore.

Analysis Layer: Added EventSummarizer and TransactionAggregator to turn raw logs into human-readable/AI-friendly narratives.

Semantics: Introduced scoring, categorization, and intent inference engines.

  1. HookSystem (The "Sensors")
    Unified API: HookRegistry.cs provides a single point of entry for all editor events.

Coverage: Support for Components, GameObjects, Scenes, Selection, Play Mode, Scripts, and Build events.

Robustness: Built-in exception isolation to prevent third-party subscriber crashes from affecting the Editor.

  1. MCP Tools (The "Interface")
    GetActionTrace: Core retrieval tool with query_mode support.

GetActionTraceSummary: Optimized for token efficiency.

AddActionTraceNote: Allows AI to inject its own reasoning into the timeline.

⚡ Key Highlights
Memory Efficiency: Events are automatically dehydrated to minimize footprint during long sessions.

Resilience: Survives domain reloads; ensures high availability of history.

Agent-Centric: Specifically tuned for AI context windows via intelligent sampling and transaction aggregation.

🧪 Testing & Validation
[x] Verified event capture across Scene saves/loads.

[x] Confirmed Dehydration logic triggers after the memory threshold is met.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added comprehensive Action Trace system for recording and analyzing editor events
    • Event filtering and importance-based categorization for focused analysis
    • New Action Trace inspector window to view and explore recorded events
    • Event query APIs with semantic analysis and context mapping
    • Configurable settings for event capture, storage, and sampling
    • Integration with AI tools for action trace queries and analysis
    • Support for recording AI-generated notes within event history
  • Documentation

    • Added comprehensive design documentation for Action Trace and Hooks systems
    • Added contributing guides for extending Action Trace and Hooks functionality

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

whatevertogo and others added 30 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>
…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>
…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 and others added 12 commits January 20, 2026 12:02
Major ActionTrace system architecture refactoring:
- Add HookRegistry system for unified event hook registration
- Add EventCaptureRegistry for capture point management
- Add new Recorder architecture replacing legacy implementation
- Restructure directories: Core/{Models,Store,Settings}, Analysis/, Sources/
- Add Unity event hooks with EventArgs pattern
- Add built-in capture points system

Co-Authored-By: Claude <noreply@anthropic.com>
- Refactor ContextCompressor for better context handling
- Enhance AssetCapture with improved tracking
- Update SamplingMiddleware for better event sampling
- Extend ToolCallScope with enhanced functionality
- Remove ActionTraceSettings.Editor.cs (consolidated)
- Update ActionTraceSettings core implementation
- Improve EventStore.Diagnostics logging
- Refine UndoToSequenceTool behavior
- Major restructure of ActionTraceEditorWindow UI

Co-Authored-By: Claude <noreply@anthropic.com>
…tion

This architectural refactoring separates the Hooks system (general-purpose
event infrastructure) from ActionTrace (one of its consumers) by introducing
the IGameObjectCacheProvider interface for dependency injection.

Changes:
- Move HookEventArgs to Editor/Hooks/EventArgs/ namespace
- Move UnityEventHooks to Editor/Hooks/UnityEventHooks/ namespace
- Add IGameObjectCacheProvider interface for cache injection
- Implement GameObjectTrackingCacheProvider in ActionTrace
- Relocate GameObjectTrackingHelper to ActionTrace.Sources.Helpers
- Simplify StdioBridgeHost socket binding logic
- Add comprehensive documentation for Hooks system

This allows UnityEventHooks to remain a pure detector without ActionTrace
dependencies, while enabling other systems to provide their own cache
provider implementations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Deleted IEventCapturePoint interface, ContextStack class, ContextTimeline class, OperationContext class, ToolCallScope class, and associated event descriptors.
- This cleanup reduces code complexity and removes unused features related to event capturing and context management in the MCPForUnity ActionTrace system.
- Add GetActionTraceSummaryTool: AI-friendly aggregated event summaries
- Enhance GlobalIdHelper: add GetInstanceId and GetInstanceInfo methods
- Refactor HookRegistry: simplify with generic InvokeSafely method
- Update ActionTraceQuery: convert GlobalID to InstanceID/Name for display
- Update Python models: add target_instance_id and target_name fields
- Add query_mode parameter to get_action_trace tool
- Fix undo_to_sequence example code

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 21, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Introduces a comprehensive ActionTrace system for Unity editor that captures, stores, analyzes, and queries editor events. Includes event capture infrastructure, semantic analysis (scoring, categorization, intent inference), filtering and sampling, persistence, context mapping, query projection, editor UI window, and Python MCP tools for accessing trace data. Spans capture hooks, event store, semantics pipeline, UX components, and server integration.

Changes

Cohort / File(s) Summary
Core Data Models
MCPForUnity/Editor/ActionTrace/Core/Models/EditorEvent.cs, MCPForUnity/Editor/ActionTrace/Core/EventCategory.cs, MCPForUnity/Editor/ActionTrace/Core/EventMetadata.cs, MCPForUnity/Editor/ActionTrace/Core/EventTypes.cs, MCPForUnity/Editor/ActionTrace/Core/SamplingMode.cs, MCPForUnity/Editor/ActionTrace/Context/ContextMapping.cs
Immutable EditorEvent with payload sanitization and dehydration; EventCategory enum; EventMetadata with sampling config; EventTypes central registry with metadata; SamplingMode enum; ContextMapping for linking events to context GUIDs.
Event Store
MCPForUnity/Editor/ActionTrace/Core/Store/EventStore.cs, EventStore.Context.cs, EventStore.Diagnostics.cs, EventStore.Merging.cs, EventStore.Persistence.cs
Thread-safe central event storage with sequence generation, filtering, optional merging, dehydration, persistence, context mapping, diagnostics, and batch notification. Supports trimming to configurable limits and coalesced saves.
Event Capture Infrastructure
MCPForUnity/Editor/ActionTrace/Capture/Recorder.cs, MCPForUnity/Editor/ActionTrace/Capture/Emitter.cs
Centralized event emission with strongly-typed helpers (EmitComponentAdded, EmitAssetImported, etc.); Recorder subscribes to HookRegistry and enriches events with VCS context and Undo group metadata.
Property & Selection Capture
MCPForUnity/Editor/ActionTrace/Capture/Capturers/PropertyCapture.cs, MCPForUnity/Editor/ActionTrace/Capture/Capturers/SelectionCapture.cs
Debounced property-change tracker with object pooling and 500ms window; SelectionPropertyTracker records changes on currently selected objects with rich context.
Asset & Undo Capture
MCPForUnity/Editor/ActionTrace/Capture/Capturers/AssetCapture.cs, MCPForUnity/Editor/ActionTrace/Capture/Capturers/UndoCapture.cs
AssetChangePostprocessor with session-based deduplication and disk cache; UndoGroupManager manages Undo groups for AI tool calls with collapsing and state tracking.
Sampling & Filtering
MCPForUnity/Editor/ActionTrace/Capture/Sampling/SamplingMode.cs, SamplingStrategy.cs, SamplingConfig.cs, SamplingMiddleware.cs, PendingSample.cs, MCPForUnity/Editor/ActionTrace/Capture/Filters/EventFilter.cs
Configurable sampling strategies (None, Throttle, Debounce, DebounceByKey) with SamplingMiddleware; comprehensive EventFilter with serializable rules (Prefix, Extension, Regex, GameObject) and default junk patterns.
Analysis & Aggregation
MCPForUnity/Editor/ActionTrace/Analysis/Query/ActionTraceQuery.cs, MCPForUnity/Editor/ActionTrace/Analysis/Summarization/EventSummarizer.cs, MCPForUnity/Editor/ActionTrace/Analysis/Summarization/TransactionAggregator.cs
ActionTraceQuery projects events with computed semantics and precomputed display fields; EventSummarizer generates human-readable summaries from templates and special-case handlers; TransactionAggregator groups events into atomic operations by ToolCallId/TriggeredByTool/time-window.
Semantics Pipeline
MCPForUnity/Editor/ActionTrace/Semantics/IEventScorer.cs, IEventCategorizer.cs, IIntentInferrer.cs, DefaultEventScorer.cs, DefaultCategorizer.cs, DefaultIntentInferrer.cs
Interfaces for computing importance scores, event categories, and inferred intents; defaults provide base metadata scoring, importance-threshold categorization, and heuristic-based intent inference.
Configuration & Settings
MCPForUnity/Editor/ActionTrace/Core/Settings/ActionTraceSettings.cs, MCPForUnity/Editor/ActionTrace/Core/Presets/ActionTracePreset.cs
ScriptableObject-based settings with Filtering/Merging/Storage/Sampling layers; six predefined presets (DebugAll, Standard, Lean, AIFocused, Realtime, Performance) with validation and memory estimation.
Helper Utilities
MCPForUnity/Editor/ActionTrace/Helpers/ActionTraceHelper.cs, PropertyEventPayloadBuilder.cs, PropertyFormatter.cs, UndoReflectionHelper.cs
Extension methods for event payload access; standardized property modification payload builders; property value serialization and type naming; safe Undo reflection extraction.
Global Helpers
MCPForUnity/Editor/Helpers/GlobalIdHelper.cs, BuildTargetUtility.cs
Cross-session stable object identification with GlobalObjectId fallbacks and resolution; build target name mapping utilities.
Unity Editor Hooks
MCPForUnity/Editor/Hooks/HookRegistry.cs, HookEventArgs.cs, UnityEventHooks.cs, UnityEventHooks.Advanced.cs, IGameObjectCacheProvider.cs, GameObjectTrackingHelper.cs, GameObjectTrackingCacheProvider.cs
Decoupled event dispatch registry; rich event arguments for compilation, build, scene, GameObject, component events; GameObject lifecycle tracking with caching; advanced tracking for script compilation and build timing.
Hook Integration Test
MCPForUnity/Editor/Hooks/HookTest.cs
Non-functional scaffold file with commented event handlers for testing hook infrastructure.
Asset & Context Integration
MCPForUnity/Editor/ActionTrace/Integration/AssetBridge.cs, MCPForUnity/Editor/ActionTrace/Integration/VCS/VcsContextProvider.cs
AssetBridge forwards ManageAsset events to ActionTrace; VcsContextProvider polls Git status (5s interval) for branch, commit, dirty state and supplies context to events.
Editor UI
MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs, ActionTraceEditorWindow.uxml, ActionTraceEditorWindow.uss
Inspector window for ActionTrace with search, filtering, sorting (time/AI), detail panel, export (JSON/CSV), settings access, and refresh controls. Rich styling with event-type color coding.
Editor Tools
MCPForUnity/Editor/Tools/AddActionTraceNoteTool.cs, GetActionTraceTool.cs, GetActionTraceSummaryTool.cs, UndoToSequenceTool.cs, ActionTraceSettingsTool.cs
Tools for recording AI notes, querying events (with preset modes), generating summaries, reverting to past sequences, and inspecting settings.
Asset Tool Integration
MCPForUnity/Editor/Tools/ManageAsset.cs
Added OnAssetModified, OnAssetCreated, OnAssetDeleted events to enable low-coupling ActionTrace notifications.
MCP Resource
MCPForUnity/Editor/Resources/ActionTrace/ActionTraceViewResource.cs
Query resource handling basic/semantic/aggregated event queries with filtering and task context.
Menu Integration
MCPForUnity/Editor/MenuItems/MCPForUnityMenu.cs
Added ShowActionTraceWindow menu item to open ActionTrace editor window.
Server/Python Models & Tools
Server/src/models/action_trace.py, Server/src/services/resources/action_trace.py, Server/src/services/tools/add_action_trace_note.py, get_action_trace.py, get_action_trace_settings.py, get_action_trace_summary.py, undo_to_sequence.py
Python data models for EditorEvent, query results, settings, statistics; MCP resources/tools for querying, filtering, summarizing, and managing ActionTrace from server side.
Documentation
docs/ActionTrace/CONTRIBUTING.md, DESIGN.md, docs/Hooks/CONTRIBUTING.md, DESIGN.md
Comprehensive guides for ActionTrace and Hooks architecture, contribution patterns, coding standards, and design rationale.
Meta Files
ActionTraceEditorWindow.cs.meta, ActionTraceEditorWindow.uxml.meta, ActionTraceEditorWindow.uss.meta, UndoToSequenceTool.cs.meta, MCPSetupWindow.uxml.meta
Unity asset metadata; GUID change for MCPSetupWindow.uxml.

Sequence Diagrams

sequenceDiagram
    participant Unity as Unity Editor
    participant Hook as HookRegistry
    participant Cap as Capture<br/>(Recorder)
    participant Store as EventStore
    participant Query as ActionTraceQuery
    participant Sem as Semantics<br/>(Scorer/Categorizer)
    participant UI as Editor Window

    Unity->>Hook: Property changed / Asset imported
    Hook->>Cap: Event notification
    Cap->>Store: Record(EditorEvent)
    Note over Store: Apply SamplingMiddleware<br/>Apply Merging<br/>Notify subscribers
    
    UI->>Store: Query(limit=50)
    Store-->>UI: List<EditorEvent>
    
    UI->>Query: Project(events)
    Query->>Sem: Score(event)
    Sem-->>Query: importance_score
    Query->>Sem: Categorize(score)
    Sem-->>Query: category
    Query->>Sem: Infer(event, surrounding)
    Sem-->>Query: inferred_intent
    Query-->>UI: ActionTraceViewItem[]
    
    UI->>UI: Render with colors & filters
Loading
sequenceDiagram
    participant Server as Python Server
    participant Unity as Unity Editor
    participant Store as EventStore
    participant Agg as TransactionAggregator
    participant Client as AI Client

    Client->>Server: get_action_trace(limit=50, include_semantics=true)
    Server->>Unity: send command (ActionTraceViewResource)
    Unity->>Store: QueryAll()
    Store-->>Unity: events[]
    
    alt summary_only=true
        Unity->>Agg: Aggregate(events)
        Agg-->>Unity: AtomicOperation[]
        Unity-->>Server: aggregated_results
    else include_semantics=true
        Unity->>Query: Project(events)
        Query-->>Unity: ActionTraceViewItem[] (with scores, categories, intents)
        Unity-->>Server: semantic_results
    else
        Unity-->>Server: basic_results
    end
    
    Server-->>Client: EventQueryResult
Loading

Estimated Code Review Effort

🎯 5 (Critical) | ⏱️ ~120 minutes

This PR introduces a substantial, multi-layered system spanning event capture, storage, analysis, UI, and server integration. The changes are heterogeneous—mixing capture infrastructure, data structures, analytics, persistence, filtering, UI rendering, and tool/resource definitions—requiring separate reasoning for each cohort. Dense logic in sampling, merging, dehydration, and query projection; extensive infrastructure scaffolding across hooks and integrations; and novel semantic computation (scoring, categorization, intent inference) all contribute to high complexity. While some file changes are repetitive (e.g., many capture helpers), the breadth and interconnectedness of the system demand thorough cross-file reasoning and interaction validation.

Possibly Related Issues

Possibly Related PRs

Suggested Reviewers

  • dsarno
  • justinpbarnett
  • Scriptwonder

🐰 A trace of events so grand and vast,
Captures every action, future, present, past,
With semantics bright and filters fine,
The bunny's masterpiece—a system divine!

🚥 Pre-merge checks | ❌ 3
❌ Failed checks (1 warning, 2 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.55% 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 'feat:action trace feat:HookSystem' is vague and uses a repetitive format with two feature names concatenated, making it unclear what the primary change is. It lacks a descriptive summary of the overall impact. Use a single, clear title that captures the main objective: e.g., 'feat: Add ActionTrace system and HookRegistry for editor event tracking and semantic analysis' or 'feat: Implement editor event tracking and hook system for AI reasoning'.
Description check ❓ Inconclusive The PR description comprehensively covers the ActionTrace and HookSystem additions, architecture/flow, changes made, and highlights. However, it does not strictly follow the provided template structure with explicit 'Type of Change', 'Changes Made', 'Testing/Screenshots/Recordings', 'Related Issues', and 'Additional Notes' sections. Reorganize the description to explicitly follow the template sections: include a 'Type of Change' section (this is a New feature), clearly delineate 'Changes Made' with bullet points, add a 'Testing/Screenshots/Recordings' section with specific validation details, and specify any 'Related Issues' and 'Additional Notes'.

✏️ 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.

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

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/Core/Store/EventStore.Persistence.cs`:
- Around line 201-213: SaveToStorage currently copies _events and
_contextMappings without synchronization, risking concurrent mutation; take a
snapshot of the mutable state under the existing _queryLock and then perform
serialization and McpJobStateStore.SaveState outside the lock. Specifically,
inside SaveToStorage acquire _queryLock, copy _events and _contextMappings into
local lists and capture SequenceCounter/CurrentSchemaVersion into a new
EventStoreState local variable, release the lock, then call
McpJobStateStore.SaveState(StateKey, state) with that snapshot so SaveToStorage
no longer iterates shared _events/_contextMappings while unlocked.
- Around line 45-87: ScheduleSave currently returns immediately when throttled
(now - _lastSaveTime < MinSaveIntervalMs) which can leave _isDirty unsaved;
modify ScheduleSave so that when it skips due to throttling it schedules a retry
via EditorApplication.delayCall to re-call ScheduleSave after the remaining
throttle interval (or a small capped retry delay) instead of just returning,
while still respecting the _queryLock/_saveScheduled guards and avoiding
duplicate retries; reference ScheduleSave, _lastSaveTime, MinSaveIntervalMs,
_saveScheduled, _queryLock, _isDirty, EditorApplication.delayCall and
SaveToStorage to locate where to add the retry scheduling logic.

In `@MCPForUnity/Editor/Resources/ActionTrace/ActionTraceViewResource.cs`:
- Around line 349-440: The conversation_id filter currently allows AINote events
without a conversation_id to pass; update both ApplyTaskFilters and
ApplyTaskFiltersToProjected so that when conversationId is non-empty you require
the payload to contain "conversation_id" and that its ToString() equals
conversationId—i.e., if TryGetValue("conversation_id", out var convVal) returns
false, return false (just like the task_id branch), and otherwise compare
convVal?.ToString() != conversationId to reject non-matching values.

In `@MCPForUnity/Editor/Tools/GetActionTraceSummaryTool.cs`:
- Around line 73-88: The code ignores the requested time_range because
CalculateSinceSequence always returns null; update the flow so timeRange from
GetTimeRange(`@params`) is passed into CalculateSinceSequence and that function
computes a proper sinceSequence (e.g., convert timeRange to a timestamp/sequence
number or parse relative ranges) instead of returning null, then pass the
resulting sinceSequence into EventStore.Query(limit, sinceSequence) so Query
filters by the time window; apply the same fix to the other occurrence where
GetTimeRange/CalculateSinceSequence/EventStore.Query are used (the block around
the later occurrence referenced in the review).
- Around line 405-442: In ApplyTaskFilters, the conversation_id branch currently
only excludes events when a conversation_id exists but doesn't match; change it
to require a present matching value when conversationId is provided: in the
Where predicate for e.Type == "AINote" check
e.Payload.TryGetValue("conversation_id", out var convVal) and return false if
TryGetValue is false or convVal?.ToString() != conversationId (mirroring the
task_id logic), ensuring events missing conversation_id are filtered out when
conversationId is non-empty.
- Around line 185-196: The logic treats any event with importanceCategory ==
"critical" (or containing "error"/"exception" in evtTypeLower) as an error, but
AINote events are scored critical and should not be counted; update the
conditional in the error counting block (the if that currently uses
importanceCategory and evtTypeLower) to exclude events whose evt.Type equals
"AINote" (case-insensitive) — e.g., only increment errorCount and add to
errorEvents when the existing conditions are true AND evt.Type does not equal
"AINote" (use StringComparison.OrdinalIgnoreCase for the comparison).

In `@MCPForUnity/Editor/Tools/ManageAsset.cs`:
- Around line 290-294: The three callback invocations (OnAssetCreated,
OnAssetModified, OnAssetDeleted) should be wrapped in their own try-catch blocks
so subscriber exceptions cannot propagate and cause the outer method to return
ErrorResponse even after the asset operation and
AssetDatabase.SaveAssets()/DeleteAsset succeeded; modify the invocation sites
(the lines calling OnAssetCreated?.Invoke(fullPath, assetType),
OnAssetModified?.Invoke(fullPath, assetType), and
OnAssetDeleted?.Invoke(fullPath)) to catch and log any exception locally (e.g.,
processLogger or UnityEngine.Debug.LogException) and continue normal return
flow, preserving the successful response from the core create/modify/delete
methods.

In `@MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs`:
- Around line 682-808: SanitizeJson currently reuses and clears the shared field
_stringBuilder (used by BuildJsonExport), which corrupts the in-progress export;
modify SanitizeJson to allocate and use a local StringBuilder (e.g., new
StringBuilder with capacity based on input.Length) or otherwise avoid mutating
the shared _stringBuilder field, perform the escape loop on the local builder,
and return its ToString() so BuildJsonExport's _stringBuilder is not cleared or
modified.
- Around line 843-851: The Clear handler currently calls
_detailScrollView.Clear(), which removes
_detailPlaceholder/_detailContent/_detailActions from the visual tree and breaks
OnSelectionChanged; change OnClearClicked to preserve the placeholder element
instead of clearing the whole scroll view: do not call
_detailScrollView.Clear(), instead clear the contents of _detailContent and
_detailActions (e.g., remove their child elements or reset text) and ensure
_detailPlaceholder is made visible (or re-added) so the detail UI remains in the
visual tree for future selections; update references in OnSelectionChanged to
assume the placeholder exists.
- Around line 1203-1228: OnEditorUpdate currently calls RefreshEvents()
regardless of whether the UI was initialized in CreateGUI, which can lead to
null dereferences on _actionTraceQuery and _eventListView; add a guard at the
start of OnEditorUpdate to return early if those UI fields are null (e.g., if
_actionTraceQuery == null || _eventListView == null) or alternatively move the
call to RefreshEvents behind a boolean flag set by CreateGUI when initialization
succeeds; ensure the guard references OnEditorUpdate, RefreshEvents,
_actionTraceQuery, _eventListView and preserves the existing scheduled refresh
logic.

In `@Server/src/services/tools/add_action_trace_note.py`:
- Around line 113-122: The generated IDs for effective_task_id and
effective_conv_id are not persisted back to session context, causing new IDs to
be created on subsequent calls; after auto-generating (in the block that sets
effective_task_id = f"task-{uuid.uuid4().hex[:8]}" and effective_conv_id =
f"conv-{uuid.uuid4().hex[:8]}"), assign those values into the context stores
(_current_task_id.set(effective_task_id) and
_current_conversation_id.set(effective_conv_id)) so future calls that read
_current_task_id.get() and _current_conversation_id.get() see the same IDs and
preserve task-level grouping.
🟡 Minor comments (25)
docs/Hooks/CONTRIBUTING.md-337-337 (1)

337-337: Add language specifier to fenced code block.

Markdownlint flagged this fenced code block for missing a language specifier.

📝 Proposed fix
-```
+```text
 feat(Hooks): add undo/redo event hooks
docs/ActionTrace/CONTRIBUTING.md-208-217 (1)

208-217: Clarify test command format and context.

Line 210 shows call_tool("run_tests", {"mode": "EditMode"}) which appears to be MCP client syntax, not a bash command. This is confusing within a bash code block that also contains actual shell commands. Consider:

  1. Separating MCP tool invocations from bash commands into different code blocks
  2. Adding context about when to use MCP vs. Unity Editor directly
  3. Using a different language identifier (e.g., python or text) for the MCP tool syntax
📝 Suggested improvement
 ### Run Tests
 
+**Via MCP Tool:**
+```python
+# Unity tests via MCP client
+call_tool("run_tests", {"mode": "EditMode"})
+```
+
+**Via Command Line:**
 ```bash
-# Unity tests via MCP
-call_tool("run_tests", {"mode": "EditMode"})
-
 # Python tests
 cd Server && uv run pytest tests/ -v
 
 # With coverage
 uv run pytest tests/ --cov --cov-report=html
</details>

</blockquote></details>
<details>
<summary>docs/ActionTrace/CONTRIBUTING.md-35-39 (1)</summary><blockquote>

`35-39`: **Update version requirements to match actual project minimums.**

The CONTRIBUTING.md table has overstated requirements. According to project documentation and configuration:
- **Python** should be **3.10+** (not 3.11+) — confirmed in `Server/pyproject.toml` (`requires-python = ">=3.10"`), main `README.md`, and `Server/README.md`
- **Unity** should be **2021.3 LTS+** (not 2022.3+) — confirmed in main `README.md`, `MCPForUnity/package.json`, and `Server/README.md`; ActionTrace code contains no version-specific preprocessor directives requiring 2022.3+
- **Git** should specify a concrete minimum (e.g., **2.30+** instead of "Latest") for reproducibility

<details>
<summary>Updated table</summary>

| Tool | Version | Purpose |
|------|---------|---------|
| Unity | 2021.3 LTS+ | Run and test |
| Python | 3.10+ | MCP Server |
| Git | 2.30+ | Version control |

</details>

</blockquote></details>
<details>
<summary>Server/src/services/tools/add_action_trace_note.py-32-67 (1)</summary><blockquote>

`32-67`: **Handle leading whitespace in JSON-string arrays.**

Line 52 checks `startswith("[")` without trimming, so valid inputs like `"  [1,2]"` skip JSON parsing and the CSV fallback fails. Normalize the string once and use it for both JSON and CSV paths.


<details>
<summary>🔧 Suggested fix</summary>

```diff
-    if isinstance(value, str):
-        if not value.strip():
-            return None
-        # Try parsing JSON array first
-        if value.startswith("["):
-            try:
-                parsed = json.loads(value)
+    if isinstance(value, str):
+        stripped = value.strip()
+        if not stripped:
+            return None
+        # Try parsing JSON array first
+        if stripped.startswith("["):
+            try:
+                parsed = json.loads(stripped)
                 if isinstance(parsed, list):
                     return _coerce_int_list(parsed)  # Recursively handle parsed list
             except json.JSONDecodeError:
                 pass
         # Try comma-delimited integers
         try:
-            return [int(v.strip()) for v in value.split(",") if v.strip()]
+            return [int(v.strip()) for v in stripped.split(",") if v.strip()]
         except ValueError:
             pass
docs/ActionTrace/DESIGN.md-43-46 (1)

43-46: Add language identifiers to fenced code blocks (MD040).
Markdownlint flags several code blocks missing a language specifier.

✅ Example fix (apply similarly to other blocks)
-```
+```text
 Query (Query) → Semantics (Semantics) → Context (Context)
     → Capture (Capture) → Sources (Event Sources) → Core (Data)

```diff
-```csharp
+```csharp
 public readonly struct EditorEvent
 {
     public readonly long Sequence;
     public readonly long TimestampUnixMs;
     public readonly string Type;
     public readonly string Payload;  // JSON
 }
</details>



Also applies to: 71-90, 114-117, 132-135, 202-205, 209-212, 216-219, 282-290, 294-298

</blockquote></details>
<details>
<summary>MCPForUnity/Editor/Hooks/EventArgs/HookEventArgs.cs-121-131 (1)</summary><blockquote>

`121-131`: **Consider caching Owner data instead of holding a live GameObject reference.**

The `Owner` property holds a live GameObject reference that can become invalid if the owning GameObject is destroyed before or during event consumption. Unlike `GameObjectDestroyedArgs`, which caches data (Name, InstanceId, GlobalId) to avoid this issue, `ComponentRemovedArgs` exposes a reference that may be destroyed in the same operation (e.g., parent destruction cascades to children and their components).

While the current consumer (Recorder) implements defensive null checks, this pattern is inconsistent with the safer design of `GameObjectDestroyedArgs` and creates a trap for future consumers. Consider caching `Owner.name` and `Owner.GetInstanceID()` instead, matching the proven pattern used for GameObject destruction events.

</blockquote></details>
<details>
<summary>docs/Hooks/DESIGN.md-36-63 (1)</summary><blockquote>

`36-63`: **Add languages to fenced code blocks (MD040).**

Both ASCII diagram and file-structure blocks should declare a language to satisfy markdownlint.

<details>
<summary>📝 Suggested doc lint fix</summary>

```diff
-```
+```text
@@
-```
+```text
@@
-```
+```text
@@
-```
+```text

Also applies to: 398-408

MCPForUnity/Editor/ActionTrace/Capture/Capturers/SelectionCapture.cs-56-191 (1)

56-191: Multi-selection edits to non-active objects are dropped.

The code only matches modifications against the active selection (_currentSelectionGlobalId). When multiple objects are selected and properties are edited, the Inspector modifies all selected objects, but modifications on non-active selections are filtered out by IsTargetMatchSelection(). Consider tracking all selected object IDs if multi-selection property modifications should be recorded, while keeping the active object for selection_context.

💡 Possible adjustment to capture multi-selection
         private static string _currentSelectionPath;
+        private static readonly HashSet<string> _currentSelectionIds = new();
 
         private static void UpdateSelectionState()
         {
             var activeObject = Selection.activeObject;
             if (activeObject == null)
             {
                 _currentSelectionGlobalId = null;
                 _currentSelectionName = null;
                 _currentSelectionType = null;
                 _currentSelectionPath = null;
+                _currentSelectionIds.Clear();
                 return;
             }
 
+            _currentSelectionIds.Clear();
+            foreach (var obj in Selection.objects)
+            {
+                var id = GlobalIdHelper.ToGlobalIdString(obj);
+                if (!string.IsNullOrEmpty(id))
+                    _currentSelectionIds.Add(id);
+            }
+
             _currentSelectionGlobalId = GlobalIdHelper.ToGlobalIdString(activeObject);
             _currentSelectionName = activeObject.name;
             _currentSelectionType = activeObject.GetType().Name;
         private static bool IsTargetMatchSelection(UnityEngine.Object target, string targetGlobalId)
         {
-            if (targetGlobalId == _currentSelectionGlobalId)
+            if (_currentSelectionIds.Contains(targetGlobalId))
                 return true;
 
             if (target is Component comp)
             {
                 string gameObjectId = GlobalIdHelper.ToGlobalIdString(comp.gameObject);
-                if (gameObjectId == _currentSelectionGlobalId)
+                if (_currentSelectionIds.Contains(gameObjectId))
                     return true;
             }
             return false;
MCPForUnity/Editor/Tools/UndoToSequenceTool.cs-105-111 (1)

105-111: Handle “already at target” as a no‑op success.
If the target sequence is the most recent recorded event, eventsAfterTarget will be empty and the tool currently returns an error. This should be treated as a successful no‑op.

✅ Proposed fix
-                if (eventsAfterTarget.Count == 0)
-                {
-                    return new ErrorResponse($"No events found after sequence {targetSequence}.");
-                }
+                if (eventsAfterTarget.Count == 0)
+                {
+                    return new SuccessResponse(
+                        $"Already at sequence {targetSequence}; no Undo operations needed."
+                    );
+                }
MCPForUnity/Editor/ActionTrace/Capture/Recorder.cs-221-301 (1)

221-301: Add null guards to detailed scene and build handlers for consistency.

Five detailed handlers (OnSceneOpenedDetailed, OnNewSceneCreatedDetailed, OnScriptCompiledDetailed, OnScriptCompilationFailedDetailed, OnBuildCompletedDetailed) directly access args properties without null checks, while similar handlers (OnComponentRemovedDetailed, OnGameObjectDestroyedDetailed) do include them. While current event sources always provide non-null args, the inconsistent pattern creates a defensive gap. Add null guards to all detailed handlers to maintain a uniform contract.

MCPForUnity/Editor/ActionTrace/Helpers/ActionTraceHelper.cs-43-61 (1)

43-61: Handle underscores before uppercase segments in tool names.
The regex pattern (^|_)([a-z]) only matches lowercase letters after underscores, so inputs like "add_ActionTrace_note" keep the underscore before "ActionTrace" untouched. The proposed change to (^|_)([A-Za-z]) correctly handles both cases.

🐛 Proposed fix
-                "(^|_)([a-z])",
+                "(^|_)([A-Za-z])",
MCPForUnity/Editor/ActionTrace/Integration/VCS/VcsContextProvider.cs-269-274 (1)

269-274: Missing timeout on PATH verification process.

FindGitExecutable calls process.WaitForExit() without a timeout when verifying git is in PATH. If git is slow to start or hangs, this could freeze the editor.

🔧 Suggested fix
                 using (var process = Process.Start(startInfo))
                 {
-                    process.WaitForExit();
-                    if (process.ExitCode == 0)
+                    if (process.WaitForExit(2000) && process.ExitCode == 0)
                         return "git"; // Found in PATH
                 }
MCPForUnity/Editor/ActionTrace/Core/Store/EventStore.Diagnostics.cs-14-19 (1)

14-19: Documentation claims caller holds lock, but method acquires lock.

The doc comment says "Caller must hold _queryLock before calling" but the method itself acquires the lock on line 25. This is redundant (reentrant lock) or the documentation is incorrect.

📝 Fix documentation or remove redundant lock

If _queryLock is a reentrant lock (like object with lock), the nested lock works but is unnecessary overhead. If documentation is correct, remove the inner lock:

-        /// Thread safety: Caller must hold _queryLock before calling.
-        /// All _events access happens within the same lock to prevent concurrent modification.
+        /// Thread safety: Acquires _queryLock internally.
         /// </summary>
         private static void DehydrateOldEvents(int hotEventCount)
         {
             // Clamp to non-negative to prevent negative iteration
             hotEventCount = Math.Max(0, hotEventCount);
 
-            lock (_queryLock)
-            {
                 int count = _events.Count;
                 // ...
-            }
         }
MCPForUnity/Editor/ActionTrace/Core/Store/EventStore.Diagnostics.cs-63-67 (1)

63-67: Remove unnecessary null check.

The null check at lines 65-67 is defensive coding for a scenario that cannot occur. Dehydrate() always returns an EditorEvent instance (never null), and no other code path introduces nulls into _events. The comment attributing nulls to DehydrateOldEvents is inaccurate—that method only replaces events with dehydrated instances.

MCPForUnity/Editor/Hooks/UnityEventHooks/UnityEventHooks.Advanced.cs-239-257 (1)

239-257: Replace reflection on internal API with public alternatives or document the version dependency.

GetCompilationErrorCount uses reflection on UnityEditor.Scripting.ScriptCompilationErrorCount, which is not a documented or publicly supported Unity API. While the try-catch fallback is good defensive coding, consider:

  1. Replace with public alternatives like CompilationPipeline, EditorUtility.scriptCompilationFailed, or EditorApplication.logMessageReceived (the codebase already uses CompilationPipeline elsewhere)
  2. If you must use reflection, add an explanatory comment noting the internal API dependency and version fragility
  3. Test against Unity 6+ before release, as the codebase already has version-specific handling for this
MCPForUnity/Editor/Windows/ActionTraceEditorWindow.uss-446-451 (1)

446-451: Detail panel max-width is smaller than min-width, creating contradictory layout constraints.

min-width: 320px with max-width: 300px causes the max-width to be ignored by the layout system. Set max-widthmin-width or remove it.

🔧 Possible adjustment
-    min-width: 320px;
-    max-width: 300px;
+    min-width: 320px;
+    max-width: 420px;
MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs-451-456 (1)

451-456: Use AppendSeparator() for menu separators instead of empty AppendAction entries.

AppendAction with an empty label creates a clickable empty entry. UI Toolkit provides AppendSeparator() specifically for adding visual separators to menus.

🔧 Proposed fix
-            _filterMenu?.menu.AppendAction("", a => { });  // Separator
+            _filterMenu?.menu.AppendSeparator();
...
-            _filterMenu?.menu.AppendAction("", a => { });  // Separator
+            _filterMenu?.menu.AppendSeparator();
MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs-1043-1051 (1)

1043-1051: Swap sort order to match UI label: "By Importance (AI First)"

The UI menu and button label explicitly state "By Importance (AI First)" and "Sort: Importance," but the current AIFiltered implementation sorts by timestamp first, then importance. This contradicts the expected behavior. Importance should be the primary sort key.

🔧 Required adjustment
-                SortMode.AIFiltered => source
-                    .OrderByDescending(e => e.Event.TimestampUnixMs)
-                    .ThenByDescending(e => e.ImportanceScore),
+                SortMode.AIFiltered => source
+                    .OrderByDescending(e => e.ImportanceScore)
+                    .ThenByDescending(e => e.Event.TimestampUnixMs),
MCPForUnity/Editor/Hooks/UnityEventHooks/UnityEventHooks.cs-177-183 (1)

177-183: SelectionChanged should use Selection.activeGameObject.
Casting Selection.activeObject to GameObject drops selections when a Component is active.

🛠️ Proposed fix
-            GameObject selectedGo = Selection.activeObject as GameObject;
+            GameObject selectedGo = Selection.activeGameObject;
MCPForUnity/Editor/Tools/GetActionTraceSummaryTool.cs-146-150 (1)

146-150: Duration calculation shouldn’t assume event order.
If events are newest-first, durationMs will go negative. Using min/max timestamps is safer.

🛠️ Proposed fix
-            long startTimeMs = events[0].TimestampUnixMs;
-            long endTimeMs = events[events.Count - 1].TimestampUnixMs;
+            long startTimeMs = events.Min(e => e.TimestampUnixMs);
+            long endTimeMs = events.Max(e => e.TimestampUnixMs);
MCPForUnity/Editor/Tools/AddActionTraceNoteTool.cs-147-163 (1)

147-163: Handle scalar related_sequences inputs.
Right now only arrays parse successfully; a single numeric value is silently ignored. Consider supporting scalar values to avoid dropping valid links.

🛠️ Proposed fix to accept scalar values
-                    var relatedSeqs = relatedSeqToken.ToObject<long[]>();
-                    if (relatedSeqs != null && relatedSeqs.Length > 0)
-                    {
-                        payload["related_sequences"] = relatedSeqs;
-                    }
+                    long[] relatedSeqs = null;
+                    if (relatedSeqToken.Type == JTokenType.Array)
+                    {
+                        relatedSeqs = relatedSeqToken.ToObject<long[]>();
+                    }
+                    else if (relatedSeqToken.Type == JTokenType.Integer)
+                    {
+                        relatedSeqs = new[] { relatedSeqToken.Value<long>() };
+                    }
+                    else if (long.TryParse(relatedSeqToken.ToString(), out var seqVal))
+                    {
+                        relatedSeqs = new[] { seqVal };
+                    }
+
+                    if (relatedSeqs != null && relatedSeqs.Length > 0)
+                    {
+                        payload["related_sequences"] = relatedSeqs;
+                    }
MCPForUnity/Editor/ActionTrace/Core/Models/EditorEvent.cs-163-171 (1)

163-171: GetSummary() mutates state, contradicting the "immutable class" documentation.

The class documentation (line 12-13) states this is an immutable class ("once written, never modified"), but GetSummary() mutates PrecomputedSummary. While this is a benign lazy-initialization pattern, it technically violates immutability and could cause issues in multi-threaded scenarios (non-atomic write).

Consider either:

  1. Computing the summary in the constructor (truly immutable)
  2. Using Lazy<string> for thread-safe lazy initialization
  3. Updating the documentation to clarify this is "effectively immutable"
MCPForUnity/Editor/ActionTrace/Analysis/Query/ActionTraceQuery.cs-200-201 (1)

200-201: ProjectWithContext always passes null for surrounding context, limiting intent inference.

Unlike Project() which computes a context window for intent inference, ProjectWithContext() always passes surrounding: null. This means intent inference will be less accurate for events queried with context. The comment mentions "avoid List allocation" but this fundamentally changes the inference behavior.

Consider aligning the behavior or documenting this as an intentional trade-off.

MCPForUnity/Editor/ActionTrace/Sources/Helpers/GameObjectTrackingHelper.cs-60-65 (1)

60-65: Redundant dictionary re-creation after Clear().

Lines 63 and 65 create new dictionaries immediately after calling Clear() on the existing ones. Either Clear() alone or new allocation is needed, not both.

Proposed fix
             _previousInstanceIds.Clear();
             _previousInstanceIds.EnsureCapacity(256);
-            _nameCache.Clear();
-            _nameCache = new Dictionary<int, string>(256);
-            _globalIdCache.Clear();
-            _globalIdCache = new Dictionary<int, string>(256);
+            _nameCache.Clear();
+            _globalIdCache.Clear();
MCPForUnity/Editor/Helpers/GlobalIdHelper.cs-139-141 (1)

139-141: GameObject.Find may fail to resolve objects in non-active scenes or with duplicate paths.

GameObject.Find only searches active GameObjects in loaded scenes and returns the first match. If the scene is loaded but the object is inactive, or if multiple objects share the same hierarchy path, this lookup will fail or return the wrong object.

Consider using scene-scoped search instead:

Proposed fix
                     // Find GameObject by hierarchy path
-                    var found = GameObject.Find(hierarchyPath);
-                    return found;
+                    // Search within the specific scene for better accuracy
+                    foreach (var rootGo in scene.GetRootGameObjects())
+                    {
+                        if (rootGo.name == hierarchyPath || hierarchyPath.StartsWith(rootGo.name + "/"))
+                        {
+                            var found = rootGo.transform.Find(hierarchyPath.Substring(rootGo.name.Length).TrimStart('/'));
+                            if (found != null)
+                                return found.gameObject;
+                            if (rootGo.name == hierarchyPath)
+                                return rootGo;
+                        }
+                    }
+                    return null;
🧹 Nitpick comments (43)
Server/src/models/action_trace.py (1)

71-84: Consider adding a computed property for hydration ratio.

The EventStatistics model tracks both dehydrated_count and hydrated_count. A computed property for the hydration ratio could be useful for monitoring memory optimization effectiveness.

💡 Optional enhancement
`@property`
def hydration_ratio(self) -> float | None:
    """Ratio of hydrated events (0.0-1.0), or None if no events."""
    total = self.dehydrated_count + self.hydrated_count
    return self.hydrated_count / total if total > 0 else None

Note: This would require model_config = ConfigDict(computed=True) or exposing via a method instead.

Server/src/services/tools/get_action_trace_settings.py (2)

12-12: Unused import.

Annotated is imported but not used in this file since get_action_trace_settings takes no annotated parameters.

🧹 Remove unused import
-from typing import Annotated, Any
+from typing import Any

53-54: Broad exception handling is acceptable here, but consider using explicit conversion.

The broad Exception catch at the tool boundary is reasonable for providing user-friendly error messages. Per static analysis (RUF010), consider using an explicit conversion flag.

💡 Use explicit conversion flag
     except Exception as e:
-        return {"success": False, "message": f"Python error getting action trace settings: {str(e)}"}
+        return {"success": False, "message": f"Python error getting action trace settings: {e!s}"}
Server/src/services/tools/undo_to_sequence.py (1)

91-99: Missing exception handling for consistency with other tools.

Unlike get_action_trace_settings and get_action_trace, this tool lacks a try/except wrapper. If send_with_unity_instance raises an unexpected exception, it will propagate as an unhandled error instead of returning a structured failure response.

🔧 Add exception handling for consistency
+    try:
         # Send command to Unity
         response = await send_with_unity_instance(
             async_send_command_with_retry,
             unity_instance,
             "undo_to_sequence",
             params_dict,
         )

         return response if isinstance(response, dict) else {"success": False, "message": str(response)}
+
+    except Exception as e:
+        return {"success": False, "message": f"Python error in undo_to_sequence: {e!s}"}
Server/src/services/tools/get_action_trace_summary.py (1)

107-122: Missing exception handling for consistency.

Similar to undo_to_sequence.py, this tool lacks a try/except wrapper around the Unity transport call. Consider adding exception handling for consistent error reporting.

🔧 Add exception handling
+    try:
         # Send command to Unity
         response = await send_with_unity_instance(
             async_send_command_with_retry,
             unity_instance,
             "get_action_trace_summary",
             params_dict,
         )

         # Preserve structured failure data; unwrap success into a friendlier shape
         if isinstance(response, dict) and response.get("success"):
             return {
                 "success": True,
                 "message": response.get("message", "Generated ActionTrace summary."),
                 "data": response.get("data")
             }
         return response if isinstance(response, dict) else {"success": False, "message": str(response)}
+
+    except Exception as e:
+        return {"success": False, "message": f"Python error getting action trace summary: {e!s}"}
Server/src/services/tools/get_action_trace.py (1)

102-103: Use explicit conversion flag per static analysis.

💡 Use explicit conversion flag (RUF010)
     except Exception as e:
-        return {"success": False, "message": f"Python error getting action trace: {str(e)}"}
+        return {"success": False, "message": f"Python error getting action trace: {e!s}"}
Server/src/services/resources/action_trace.py (3)

17-17: Unused import.

ToolAnnotations is imported but not used. This file uses mcp_for_unity_resource which doesn't require ToolAnnotations.

🧹 Remove unused import
 from fastmcp import Context
-from mcp.types import ToolAnnotations

171-176: Silent exception swallowing reduces observability.

Unlike get_action_trace (line 114), this function silently continues on parse errors without logging. Per static analysis (S112), consider logging for debugging purposes.

🔧 Add logging for parse failures
     for event_dict in events_data:
         try:
             event = EditorEvent(**event_dict)
             events.append(event)
-        except Exception:
+        except Exception as e:
+            ctx.error(f"Failed to parse event for statistics: {e}")  # type: ignore[reportUnusedCoroutine]
             continue

155-161: Hardcoded limit of 1000 events for statistics.

The statistics query uses a hardcoded limit: 1000. For large event stores, this may not represent the full dataset. Consider making this configurable or documenting this limitation in the docstring.

docs/ActionTrace/CONTRIBUTING.md (1)

3-3: Minor grammar improvement.

The phrase "Welcome to contribute" is slightly awkward. Consider: "Welcome! Thanks for contributing to the Unity-mcp ActionTrace system!" or "Welcome to the Unity-mcp ActionTrace contributor guide!"

MCPForUnity/Editor/ActionTrace/Core/Store/EventStore.Merging.cs (1)

115-123: Consider handling potential OverflowException from checked() arithmetic.

The checked() casts on lines 118-119 can throw OverflowException if l or d exceeds int.MaxValue. While unlikely in practice (merge counts shouldn't reach billions), a defensive approach would be to cap the value or use unchecked() with saturation logic.

♻️ Optional: Saturate instead of throwing
                     mergeCount = existingCount switch
                     {
                         int i => i + 1,
-                        long l => checked((int)l + 1),
-                        double d => checked((int)d + 1),
+                        long l => l >= int.MaxValue ? int.MaxValue : (int)l + 1,
+                        double d => d >= int.MaxValue ? int.MaxValue : (int)d + 1,
                         string s when int.TryParse(s, out int parsed) => parsed + 1,
                         _ => 2 // Fallback for unknown types
                     };
MCPForUnity/Editor/ActionTrace/Integration/AssetBridge.cs (1)

35-74: Preserve stack traces in warning logs.
Using ex.Message drops stack context; logging ex.ToString() keeps it for diagnostics.

♻️ Suggested tweak
-                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/Tools/ActionTraceSettingsTool.cs (1)

15-16: Consider adding description to the tool attribute.

Based on learnings, the description parameter in the McpForUnityTool attribute should always be included as a best practice. Without it, there's a higher chance that MCP clients will not parse the tool correctly.

Suggested change
-    [McpForUnityTool("get_action_trace_settings")]
+    [McpForUnityTool("get_action_trace_settings", description: "Retrieves the current ActionTrace system configuration settings.")]
     public static class ActionTraceSettingsTool
MCPForUnity/Editor/ActionTrace/Semantics/DefaultIntentInferrer.cs (1)

71-105: Code duplication with DefaultEventScorer.

The helper methods IsScript, IsScene, and IsPrefab duplicate similar logic found in DefaultEventScorer.cs. Consider extracting these into a shared utility class (e.g., AssetTypeDetector) to centralize asset type detection and reduce maintenance burden.

MCPForUnity/Editor/ActionTrace/Semantics/DefaultEventScorer.cs (2)

60-61: Redundant null check for Payload.

The Score method already returns early at line 38-39 when evt.Payload == null, so this null check in GetPayloadAdjustment is unreachable under current call patterns. However, keeping it provides defensive safety if the method is ever called directly in the future.


80-106: Code duplication with DefaultIntentInferrer.

The IsScript, IsScene, and IsPrefab helpers are similar to those in DefaultIntentInferrer.cs but with subtle differences (checking "asset_type" vs "component_type"). Consider unifying these into a shared utility with configurable payload keys, or document why the differences are intentional.

MCPForUnity/Editor/ActionTrace/Core/Presets/ActionTracePreset.cs (1)

95-98: AllPresets list is mutable and can be modified at runtime.

The List<ActionTracePreset> is exposed as public static readonly, but readonly only prevents reassignment—not mutation. External code could call AllPresets.Add(), Clear(), or modify elements. Consider using IReadOnlyList<ActionTracePreset> for the public API or returning a copy in accessor methods.

Suggested change
-        public static readonly List<ActionTracePreset> AllPresets = new()
+        private static readonly List<ActionTracePreset> _allPresets = new()
         {
             DebugAll, Standard, Lean, AIFocused, Realtime, Performance
         };
+
+        public static IReadOnlyList<ActionTracePreset> AllPresets => _allPresets;
MCPForUnity/Editor/ActionTrace/Helpers/ActionTraceHelper.cs (1)

20-29: Deduplicate payload lookup usage.
MCPForUnity/Editor/ActionTrace/Analysis/Summarization/EventSummarizer.cs (Line 402-409) duplicates this helper; consider switching to ActionTraceHelper.GetPayloadString to centralize behavior.

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

217-237: Unused variable oldestSample.

oldestSample is assigned on line 226 but never used. The code correctly uses removedSample from TryRemove instead.

♻️ Remove unused variable
                     // Manual loop to find oldest entry (avoid LINQ allocation in hot path)
                     string oldestKey = null;
                     long oldestTimestamp = long.MaxValue;
-                    PendingSample oldestSample = default;
                     foreach (var kvp in _pendingSamples)
                     {
                         if (kvp.Value.TimestampMs < oldestTimestamp)
                         {
                             oldestTimestamp = kvp.Value.TimestampMs;
                             oldestKey = kvp.Key;
-                            oldestSample = kvp.Value;
                         }
                     }
MCPForUnity/Editor/Hooks/UnityEventHooks/UnityEventHooks.Advanced.cs (1)

259-278: Consider direct call instead of reflection for same-assembly type.

GetBuildTargetName uses reflection to call BuildTargetUtility.GetBuildTargetName in the same assembly. Unless there's a specific reason (e.g., optional dependency, avoiding circular references), a direct call would be simpler and more maintainable.

♻️ Suggested simplification
         private static string GetBuildTargetName(BuildTarget target)
         {
-            try
-            {
-                var assembly = typeof(HookRegistry).Assembly;
-                var type = assembly.GetType("MCPForUnity.Editor.Helpers.BuildTargetUtility");
-                if (type != null)
-                {
-                    var method = type.GetMethod("GetBuildTargetName", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
-                    if (method != null)
-                    {
-                        var result = method.Invoke(null, new object[] { target });
-                        if (result is string name) return name;
-                    }
-                }
-            }
-            catch { }
-
-            return target.ToString();
+            // Direct call if BuildTargetUtility exists and is accessible
+            // return MCPForUnity.Editor.Helpers.BuildTargetUtility.GetBuildTargetName(target);
+            return target.ToString();  // Fallback
         }
MCPForUnity/Editor/ActionTrace/Integration/VCS/VcsContextProvider.cs (2)

51-67: Missing unsubscribe from EditorApplication.update on domain reload.

The static constructor subscribes to EditorApplication.update but doesn't unsubscribe before re-subscribing. Unlike SamplingMiddleware.cs which does -= then +=, this could accumulate duplicate subscriptions across domain reloads (though Unity typically clears static state on domain reload).

🔧 Suggested fix for safe re-registration
         static VcsContextProvider()
         {
             _currentContext = GetInitialContext();
+            EditorApplication.update -= OnUpdate;
             EditorApplication.update += OnUpdate;
         }

172-174: Unused gitPath variable.

gitPath is declared but never used in RunGitCommand. It appears to be leftover from a different implementation.

♻️ Remove unused variable
             try
             {
                 var projectPath = System.IO.Path.GetDirectoryName(UnityEngine.Application.dataPath);
-                var gitPath = System.IO.Path.Combine(projectPath, ".git");
 
                 // Find git executable
MCPForUnity/Editor/ActionTrace/Core/Store/EventStore.Context.cs (1)

172-183: Events may appear multiple times in results (by design).

The SelectMany correctly handles the multi-agent scenario where one event has multiple contexts. However, callers might not expect the same Event to appear multiple times in the result. Consider adding documentation to the method signature about this behavior.

📝 Suggested documentation enhancement
         /// <summary>
         /// Query events with their context associations.
-        /// Returns a tuple of (Event, Context) where Context may be null.
+        /// Returns a tuple of (Event, Context) where Context may be null.
+        /// Note: An event with multiple contexts will appear multiple times in the result,
+        /// once for each context association.
         /// </summary>
MCPForUnity/Editor/ActionTrace/Core/EventTypes.cs (1)

30-103: Expose AINote as a constant to keep the type list centralized.

Metadata uses the "AINote" literal, which undercuts the “single source of truth” goal and diverges from the other EventTypes class in ActionTraceViewResource.cs. Add a constant here and reference it in the metadata map.

♻️ Proposed fix
         // Build events
         public const string BuildStarted = "BuildStarted";
         public const string BuildCompleted = "BuildCompleted";
         public const string BuildFailed = "BuildFailed";
+        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}",
                 },
MCPForUnity/Editor/Hooks/HookTest.cs (1)

1-84: Consider removing or gating the commented-out HookTest scaffold.

The entire file is commented out, so it adds maintenance noise without providing a runnable test. Either delete it or wrap an uncommented version in a compile symbol (e.g., MCP_HOOK_TEST) so it can be enabled intentionally.

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

262-325: GetInstanceInfo duplicates display name extraction logic from GetDisplayName.

The display name extraction logic in GetInstanceInfo (lines 275-322) largely duplicates what's in GetDisplayName (lines 196-256). Consider extracting a shared helper to reduce maintenance burden.

Refactor suggestion
+        /// <summary>
+        /// Extract display name from ID string when object is not resolvable.
+        /// </summary>
+        private static string ExtractDisplayNameFromId(string globalIdStr)
+        {
+#if UNITY_2020_3_OR_NEWER
+            if (GlobalObjectId.TryParse(globalIdStr, out var globalId))
+            {
+                var guidStr = globalId.assetGUID.ToString();
+                return guidStr.Length >= 8
+                    ? $"[{globalId.identifierType} {guidStr.Substring(0, 8)}...]"
+                    : $"[{globalId.identifierType} {guidStr}]";
+            }
+#endif
+            // ... rest of extraction logic
+        }
MCPForUnity/Editor/Hooks/HookRegistry.cs (1)

107-127: Consider avoiding dynamic dispatch for better performance and type safety.

Using dynamic incurs DLR (Dynamic Language Runtime) overhead on every invocation, which could be problematic for high-frequency events like OnEditorUpdate. Since delegate signatures are known at compile time, consider using strongly-typed overloads or Delegate.DynamicInvoke (which is still slow but avoids DLR).

A more efficient pattern:

Suggested refactor
-        private static void InvokeSafely<TDelegate>(TDelegate handler, string eventName, Action<dynamic> invoke)
-            where TDelegate : class
+        private static void InvokeSafely(Delegate handler, string eventName, object[] args)
         {
             if (handler == null) return;

-            // Cast to Delegate base type to access GetInvocationList()
-            var multicastDelegate = handler as Delegate;
-            if (multicastDelegate == null) return;
-
-            foreach (Delegate subscriber in multicastDelegate.GetInvocationList())
+            foreach (Delegate subscriber in handler.GetInvocationList())
             {
                 try
                 {
-                    invoke(subscriber);
+                    subscriber.DynamicInvoke(args);
                 }
                 catch (Exception ex)
                 {
                     McpLog.Warn($"[HookRegistry] {eventName} subscriber threw exception: {ex.Message}");
                 }
             }
         }
+
+        // Or better: use type-specific helpers
+        private static void InvokeSafely(Action handler, string eventName)
+        {
+            if (handler == null) return;
+            foreach (Action subscriber in handler.GetInvocationList())
+            {
+                try { subscriber(); }
+                catch (Exception ex) { McpLog.Warn($"[HookRegistry] {eventName} subscriber threw: {ex.Message}"); }
+            }
+        }
MCPForUnity/Editor/ActionTrace/Sources/Helpers/GameObjectTrackingHelper.cs (2)

125-150: changes list includes all current objects, not just changed ones.

The variable name changes and the struct field Changes suggest only changed objects, but line 149 adds every current GameObject regardless of whether it's new. Only the isNew flag distinguishes them. Consider renaming to currentObjects or filtering to only include new objects if that's the intent.


186-191: Consider swapping references instead of copying elements.

The current code clears _previousInstanceIds and re-adds all elements from currentIds. A more efficient approach:

Proposed fix
-                // Update tracking for next call
-                _previousInstanceIds.Clear();
-                foreach (int id in currentIds)
-                {
-                    _previousInstanceIds.Add(id);
-                }
+                // Update tracking for next call (swap references)
+                (_previousInstanceIds, currentIds) = (currentIds, _previousInstanceIds);
+                currentIds.Clear(); // Will be GC'd or reused
MCPForUnity/Editor/ActionTrace/Analysis/Query/ActionTraceQuery.cs (2)

103-104: Edge case: order detection may be unreliable with single-element lists.

Line 104 compares events[0].Sequence > events[events.Count - 1].Sequence, which works for lists with 2+ elements. For a single-element list, both indices refer to the same element, making isDescending = false. This is correct but subtle—a brief comment would help.


288-372: Consider using init accessors for ActionTraceViewItem properties.

ActionTraceViewItem is described as a computed projection, yet all properties have public setters allowing mutation after creation. Using C# 9+ init accessors would enforce immutability after construction.

Example
-            public EditorEvent Event { get; set; }
+            public EditorEvent Event { get; init; }
             // ... same for other properties
MCPForUnity/Editor/ActionTrace/Capture/Capturers/AssetCapture.cs (5)

191-213: Inconsistent deduplication cleanup for deleted assets.

For imported assets (lines 156-158), junk assets are removed from _processedAssetsInSession after filtering. However, for deleted assets, the path is added to the set but not removed when it's a junk asset. This causes unnecessary cache growth.

♻️ Suggested fix
             // L1 Blacklist: Skip junk assets
             if (!EventFilter.ShouldTrackAsset(assetPath))
+            {
+                _processedAssetsInSession.Remove(assetPath);
                 continue;
+            }

216-241: Same inconsistency for moved assets.

Similar to deleted assets, junk moved assets are added to _processedAssetsInSession but not removed when filtered. Apply the same fix pattern for consistency.

♻️ Suggested fix
             // L1 Blacklist: Skip junk assets
             if (!EventFilter.ShouldTrackAsset(movedAssets[i]))
+            {
+                _processedAssetsInSession.Remove(movedAssets[i]);
                 continue;
+            }

248-268: Comment/implementation mismatch.

The doc comment states "time-based expiration (30 minutes)" but the implementation uses count-based cleanup (MaxCacheSize = 1000). Either update the comment to reflect the actual behavior or implement the time-based approach if that was the intent.

♻️ Suggested comment fix
         /// <summary>
         /// Cleanup old entries from the cache to prevent unbounded growth.
-        /// Uses time-based expiration (30 minutes) instead of count-based.
+        /// Uses count-based cleanup (MaxCacheSize = 1000) to prevent unbounded growth.
         /// This is called at the start of each OnPostprocessAllAssets batch.
         /// </summary>

323-348: Consider consolidating asset type detection.

This GetAssetType method duplicates DetectAssetType in Emitter.cs (lines 517-539) with different extension coverage. For example, this version includes .tga, .bmp, .blend while the emitter includes audio types (.wav, .mp3) and data files (.xml, .json).

Consider extracting a shared helper to ensure consistent asset type classification across the codebase.


353-384: Missing sampling middleware check.

The RecordEvent method in Recorder.cs applies SamplingMiddleware.ShouldRecord(evt) before recording, but this implementation bypasses it and relies solely on EventStore.Record. While EventStore has its own filters, the sampling middleware is designed to reduce noise at the capture layer.

Consider applying the sampling middleware for consistency with the Recorder pattern:

♻️ Suggested enhancement
+using MCPForUnity.Editor.ActionTrace.Capture.Sampling;
 ...
                 var evt = new EditorEvent(
                     sequence: 0,
                     timestampUnixMs: DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
                     type: type,
                     targetId: targetId,
                     payload: payload
                 );

+                // Apply sampling middleware to maintain consistency with ActionTraceRecorder
+                if (!SamplingMiddleware.ShouldRecord(evt))
+                {
+                    return;
+                }
+
                 // AssetPostprocessor callbacks run on main thread but outside update loop.
                 // Use delayCall to defer recording to main thread update, avoiding thread warnings.
                 UnityEditor.EditorApplication.delayCall += () => EventStore.Record(evt);
MCPForUnity/Editor/ActionTrace/Capture/Emitter.cs (1)

460-482: Clarify distinction between EmitAssetDeleted overloads.

There are two EmitAssetDeleted methods:

  1. EmitAssetDeleted(string assetPath) (line 291) - for Unity-detected deletions
  2. EmitAssetDeleted(string assetPath, string assetType) (line 464) - for MCP tool deletions with source: "mcp_tool" tag

The distinction is important for understanding event provenance, but the doc comments could be more explicit about when to use each.

📝 Suggested doc enhancement
         /// <summary>
         /// Emit an asset deleted event via MCP tool (manage_asset).
         /// Uses Asset:{path} format for cross-session stable target IDs.
+        /// 
+        /// Use this overload for deletions initiated by MCP tools.
+        /// For Unity-detected deletions, use EmitAssetDeleted(string assetPath) instead.
         /// </summary>
MCPForUnity/Editor/ActionTrace/Core/Store/EventStore.cs (1)

288-308: Minor thread safety consideration for merge tracking reset.

Lines 298-300 reset _lastRecordedEvent and _lastRecordedTime outside _queryLock, but these fields are read/written inside the lock in Record. Since the threading model specifies main-thread-only writes, this is safe in practice, but for consistency with the documented threading model, consider resetting these inside the lock.

♻️ Suggested adjustment
             lock (_queryLock)
             {
                 _events.Clear();
                 _contextMappings.Clear();
                 _sequenceCounter = 0;
+                _lastRecordedEvent = null;
+                _lastRecordedTime = 0;
+                _lastDehydratedCount = -1;
             }

-            // Reset merge tracking and pending notifications
-            _lastRecordedEvent = null;
-            _lastRecordedTime = 0;
-            _lastDehydratedCount = -1;
             lock (_pendingNotifications)
MCPForUnity/Editor/ActionTrace/Capture/Filters/EventFilter.cs (2)

35-50: Silent failure on invalid regex patterns.

The empty catch block (lines 44-47) silently swallows regex parsing errors. Users won't know their regex pattern is invalid. Consider logging a warning on first failure.

♻️ Suggested enhancement
+[NonSerialized]
+private bool _regexErrorLogged;

 private Regex GetRegex()
 {
     if (_cachedRegex != null) return _cachedRegex;
     if (!string.IsNullOrEmpty(Pattern))
     {
         try
         {
             _cachedRegex = new Regex(Pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
         }
-        catch
+        catch (ArgumentException ex)
         {
-            // Invalid regex, return null
+            if (!_regexErrorLogged)
+            {
+                UnityEngine.Debug.LogWarning($"[EventFilter] Invalid regex pattern in rule '{Name}': {ex.Message}");
+                _regexErrorLogged = true;
+            }
         }
     }
     return _cachedRegex;
 }

139-140: Incorrect RuleType for full filenames.

.DS_Store and Thumbs.db are filenames, not extensions. Using RuleType.Extension with EndsWith technically works, but it's semantically incorrect and could match unintended files like something.DS_Store.

Consider using RuleType.Regex with end-of-string anchors:

♻️ Suggested fix
-            new() { Name = ".DS_Store", Type = RuleType.Extension, Pattern = ".DS_Store", Action = FilterAction.Block, Priority = 90 },
-            new() { Name = "Thumbs.db", Type = RuleType.Extension, Pattern = "Thumbs.db", Action = FilterAction.Block, Priority = 90 },
+            new() { Name = ".DS_Store", Type = RuleType.Regex, Pattern = @"(^|[/\\])\.DS_Store$", Action = FilterAction.Block, Priority = 90 },
+            new() { Name = "Thumbs.db", Type = RuleType.Regex, Pattern = @"(^|[/\\])Thumbs\.db$", Action = FilterAction.Block, Priority = 90 },
MCPForUnity/Editor/ActionTrace/Core/Settings/ActionTraceSettings.cs (3)

79-96: Consider adding Range constraints to sampling intervals.

The sampling interval fields (HierarchySamplingMs, SelectionSamplingMs, PropertySamplingMs) lack Range attributes. Users could set negative or excessively large values via the Inspector. Consider adding constraints similar to other settings.

♻️ Suggested enhancement
+        [Range(100, 10000)]
         [Tooltip("HierarchyChanged event sampling interval (milliseconds).")]
         public int HierarchySamplingMs = 1000;

+        [Range(50, 5000)]
         [Tooltip("SelectionChanged event sampling interval (milliseconds).")]
         public int SelectionSamplingMs = 500;

+        [Range(50, 2000)]
         [Tooltip("PropertyModified event sampling interval (milliseconds).")]
         public int PropertySamplingMs = 200;

102-154: Consider a more specific settings asset path.

The hardcoded SettingsPath = "Assets/ActionTraceSettings.asset" places the settings file in the project root. This could conflict with user files and clutters the Assets folder.

Consider using a more specific location like Assets/Editor Default Resources/ActionTraceSettings.asset or a subdirectory.


217-227: Redundant save operations during creation.

CreateSettings calls ApplyPreset (line 221) which internally calls Save() (line 184), then immediately calls AssetDatabase.SaveAssets() again (line 223). This results in redundant save operations.

♻️ Suggested optimization
         private static ActionTraceSettings CreateSettings()
         {
             var settings = CreateInstance<ActionTraceSettings>();
-            // Apply Standard preset by default
-            settings.ApplyPreset(ActionTracePreset.Standard);
+            // Apply Standard preset values without triggering save
+            var preset = ActionTracePreset.Standard;
+            settings.Filtering.MinImportanceForRecording = preset.MinImportance;
+            settings.Storage.MaxEvents = preset.MaxEvents;
+            settings.Storage.HotEventCount = preset.HotEventCount;
+            settings.Merging.EnableEventMerging = preset.EnableEventMerging;
+            settings.Merging.MergeWindowMs = preset.MergeWindowMs;
+            settings.Merging.TransactionWindowMs = preset.TransactionWindowMs;
+            settings._currentPresetName = preset.Name;
+
             AssetDatabase.CreateAsset(settings, SettingsPath);
             AssetDatabase.SaveAssets();
             AssetDatabase.Refresh();
             McpLog.Info($"[ActionTraceSettings] Created new settings at {SettingsPath}");
             return settings;
         }

Comment on lines +45 to +87
private static void ScheduleSave()
{
// P1 Fix: Check throttling - skip if too soon since last save
long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
if (now - _lastSaveTime < MinSaveIntervalMs)
{
// Too soon, just mark dirty and skip save
return;
}

// Use lock to prevent race conditions with _saveScheduled
lock (_queryLock)
{
// Only schedule if not already scheduled (prevents callback queue bloat)
if (_saveScheduled)
return;

_saveScheduled = true;
}

// Use delayCall to coalesce multiple saves into one
EditorApplication.delayCall += () =>
{
bool wasDirty;
lock (_queryLock)
{
_saveScheduled = false;
wasDirty = _isDirty;
if (_isDirty)
{
_isDirty = false;
// P1 Fix: Update last save time when actually saving
_lastSaveTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
}
}

// Perform save outside lock to avoid holding lock during I/O
if (wasDirty)
{
SaveToStorage();
}
};
}
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:

find . -name "EventStore.Persistence.cs" -type f

Repository: CoplayDev/unity-mcp

Length of output: 182


🏁 Script executed:

find . -name "EventStore*.cs" -type f | head -20

Repository: CoplayDev/unity-mcp

Length of output: 392


🏁 Script executed:

fd EventStore --extension cs

Repository: CoplayDev/unity-mcp

Length of output: 382


🏁 Script executed:

wc -l MCPForUnity/Editor/ActionTrace/Core/Store/EventStore.Persistence.cs

Repository: CoplayDev/unity-mcp

Length of output: 134


🏁 Script executed:

cat -n MCPForUnity/Editor/ActionTrace/Core/Store/EventStore.Persistence.cs

Repository: CoplayDev/unity-mcp

Length of output: 10014


🏁 Script executed:

wc -l MCPForUnity/Editor/ActionTrace/Core/Store/EventStore.cs

Repository: CoplayDev/unity-mcp

Length of output: 122


🏁 Script executed:

cat -n MCPForUnity/Editor/ActionTrace/Core/Store/EventStore.cs

Repository: CoplayDev/unity-mcp

Length of output: 15857


🏁 Script executed:

rg -n "EditorApplication\.(update|wantsToQuit|quitting|playModeStateChanged)" MCPForUnity/Editor/ActionTrace/

Repository: CoplayDev/unity-mcp

Length of output: 2254


🏁 Script executed:

rg -n "domain.*reload|shutdown|quitting|SaveToStorage" MCPForUnity/Editor/ActionTrace/Core/Store/ -A2

Repository: CoplayDev/unity-mcp

Length of output: 1930


🏁 Script executed:

rg -n "InitializeOnLoad|wantsToQuit|domainReload|OnDisable" MCPForUnity/Editor/ActionTrace/Core/Store/EventStore*.cs

Repository: CoplayDev/unity-mcp

Length of output: 45


🏁 Script executed:

grep -rn "EventStore\." MCPForUnity/Editor/ActionTrace/ | grep -i "save\|quitting\|reload" | head -20

Repository: CoplayDev/unity-mcp

Length of output: 192


🏁 Script executed:

rg -n "InitializeOnLoad|[OnDisable|OnDestroy" MCPForUnity/ --max-count=10

Repository: CoplayDev/unity-mcp

Length of output: 178


🏁 Script executed:

rg -n "McpJobStateStore" MCPForUnity/Editor/ActionTrace/Core/Store/ -B2 -A2

Repository: CoplayDev/unity-mcp

Length of output: 1683


🏁 Script executed:

rg -n "InitializeOnLoad|wantsToQuit" MCPForUnity/

Repository: CoplayDev/unity-mcp

Length of output: 1927


🏁 Script executed:

find MCPForUnity -name "*.cs" -exec grep -l "EventStore\|McpJobStateStore" {} \; | head -5

Repository: CoplayDev/unity-mcp

Length of output: 1017


🏁 Script executed:

cat -n MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs

Repository: CoplayDev/unity-mcp

Length of output: 3728


🏁 Script executed:

wc -l MCPForUnity/Editor/Helpers/McpJobStateStore.cs

Repository: CoplayDev/unity-mcp

Length of output: 112


🏁 Script executed:

sed -n '1,50p' MCPForUnity/Editor/Helpers/McpJobStateStore.cs

Repository: CoplayDev/unity-mcp

Length of output: 1690


Schedule a follow-up save when throttled to avoid losing dirty state.

When ScheduleSave() is throttled, it returns without scheduling any follow-up. If no new events arrive after the throttle hit, the dirty flag remains unsaved indefinitely. This can lead to data loss if the editor closes before the throttle window expires and triggers a new save.

The proposed fix—scheduling a retry delayCall when throttled that repeatedly checks the interval until it passes—correctly addresses this by ensuring eventual persistence of the dirty state.

🔧 Proposed fix (retry until the interval passes)
-            if (now - _lastSaveTime < MinSaveIntervalMs)
-            {
-                // Too soon, just mark dirty and skip save
-                return;
-            }
+            if (now - _lastSaveTime < MinSaveIntervalMs)
+            {
+                lock (_queryLock)
+                {
+                    if (_saveScheduled) return;
+                    _saveScheduled = true;
+                }
+
+                // Retry on the next editor tick until interval passes
+                EditorApplication.delayCall += () =>
+                {
+                    lock (_queryLock)
+                    {
+                        _saveScheduled = false;
+                    }
+                    ScheduleSave();
+                };
+                return;
+            }
📝 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 static void ScheduleSave()
{
// P1 Fix: Check throttling - skip if too soon since last save
long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
if (now - _lastSaveTime < MinSaveIntervalMs)
{
// Too soon, just mark dirty and skip save
return;
}
// Use lock to prevent race conditions with _saveScheduled
lock (_queryLock)
{
// Only schedule if not already scheduled (prevents callback queue bloat)
if (_saveScheduled)
return;
_saveScheduled = true;
}
// Use delayCall to coalesce multiple saves into one
EditorApplication.delayCall += () =>
{
bool wasDirty;
lock (_queryLock)
{
_saveScheduled = false;
wasDirty = _isDirty;
if (_isDirty)
{
_isDirty = false;
// P1 Fix: Update last save time when actually saving
_lastSaveTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
}
}
// Perform save outside lock to avoid holding lock during I/O
if (wasDirty)
{
SaveToStorage();
}
};
}
private static void ScheduleSave()
{
// P1 Fix: Check throttling - skip if too soon since last save
long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
if (now - _lastSaveTime < MinSaveIntervalMs)
{
lock (_queryLock)
{
if (_saveScheduled) return;
_saveScheduled = true;
}
// Retry on the next editor tick until interval passes
EditorApplication.delayCall += () =>
{
lock (_queryLock)
{
_saveScheduled = false;
}
ScheduleSave();
};
return;
}
// Use lock to prevent race conditions with _saveScheduled
lock (_queryLock)
{
// Only schedule if not already scheduled (prevents callback queue bloat)
if (_saveScheduled)
return;
_saveScheduled = true;
}
// Use delayCall to coalesce multiple saves into one
EditorApplication.delayCall += () =>
{
bool wasDirty;
lock (_queryLock)
{
_saveScheduled = false;
wasDirty = _isDirty;
if (_isDirty)
{
_isDirty = false;
// P1 Fix: Update last save time when actually saving
_lastSaveTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
}
}
// Perform save outside lock to avoid holding lock during I/O
if (wasDirty)
{
SaveToStorage();
}
};
}
🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/ActionTrace/Core/Store/EventStore.Persistence.cs` around
lines 45 - 87, ScheduleSave currently returns immediately when throttled (now -
_lastSaveTime < MinSaveIntervalMs) which can leave _isDirty unsaved; modify
ScheduleSave so that when it skips due to throttling it schedules a retry via
EditorApplication.delayCall to re-call ScheduleSave after the remaining throttle
interval (or a small capped retry delay) instead of just returning, while still
respecting the _queryLock/_saveScheduled guards and avoiding duplicate retries;
reference ScheduleSave, _lastSaveTime, MinSaveIntervalMs, _saveScheduled,
_queryLock, _isDirty, EditorApplication.delayCall and SaveToStorage to locate
where to add the retry scheduling logic.

Comment on lines +201 to +213
private static void SaveToStorage()
{
try
{
var state = new EventStoreState
{
SchemaVersion = CurrentSchemaVersion,
SequenceCounter = _sequenceCounter,
Events = _events.ToList(),
ContextMappings = _contextMappings.ToList()
};
McpJobStateStore.SaveState(StateKey, state);
}
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

Serialize a snapshot under lock to avoid concurrent mutation errors.

SaveToStorage copies _events/_contextMappings without locking, so concurrent writers can throw or create inconsistent snapshots. Capture under _queryLock, then serialize outside the lock.

🔧 Proposed fix (snapshot under lock)
-                var state = new EventStoreState
-                {
-                    SchemaVersion = CurrentSchemaVersion,
-                    SequenceCounter = _sequenceCounter,
-                    Events = _events.ToList(),
-                    ContextMappings = _contextMappings.ToList()
-                };
+                List<EditorEvent> eventsSnapshot;
+                List<ContextMapping> contextSnapshot;
+                long sequenceCounter;
+                lock (_queryLock)
+                {
+                    sequenceCounter = _sequenceCounter;
+                    eventsSnapshot = _events.ToList();
+                    contextSnapshot = _contextMappings.ToList();
+                }
+
+                var state = new EventStoreState
+                {
+                    SchemaVersion = CurrentSchemaVersion,
+                    SequenceCounter = sequenceCounter,
+                    Events = eventsSnapshot,
+                    ContextMappings = contextSnapshot
+                };
📝 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 static void SaveToStorage()
{
try
{
var state = new EventStoreState
{
SchemaVersion = CurrentSchemaVersion,
SequenceCounter = _sequenceCounter,
Events = _events.ToList(),
ContextMappings = _contextMappings.ToList()
};
McpJobStateStore.SaveState(StateKey, state);
}
private static void SaveToStorage()
{
try
{
List<EditorEvent> eventsSnapshot;
List<ContextMapping> contextSnapshot;
long sequenceCounter;
lock (_queryLock)
{
sequenceCounter = _sequenceCounter;
eventsSnapshot = _events.ToList();
contextSnapshot = _contextMappings.ToList();
}
var state = new EventStoreState
{
SchemaVersion = CurrentSchemaVersion,
SequenceCounter = sequenceCounter,
Events = eventsSnapshot,
ContextMappings = contextSnapshot
};
McpJobStateStore.SaveState(StateKey, state);
}
🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/ActionTrace/Core/Store/EventStore.Persistence.cs` around
lines 201 - 213, SaveToStorage currently copies _events and _contextMappings
without synchronization, risking concurrent mutation; take a snapshot of the
mutable state under the existing _queryLock and then perform serialization and
McpJobStateStore.SaveState outside the lock. Specifically, inside SaveToStorage
acquire _queryLock, copy _events and _contextMappings into local lists and
capture SequenceCounter/CurrentSchemaVersion into a new EventStoreState local
variable, release the lock, then call McpJobStateStore.SaveState(StateKey,
state) with that snapshot so SaveToStorage no longer iterates shared
_events/_contextMappings while unlocked.

Comment on lines +349 to +440
/// <summary>
/// P1.2: Apply task_id and conversation_id filters to raw event list.
/// Filters AINote events by matching task_id and conversation_id in payload.
/// </summary>
private static List<EditorEvent> ApplyTaskFilters(List<EditorEvent> events, string taskId, string conversationId)
{
// If no filters specified, return original list
if (string.IsNullOrEmpty(taskId) && string.IsNullOrEmpty(conversationId))
return events;

return events.Where(e =>
{
// Only AINote events have task_id and conversation_id
if (e.Type != EventTypes.AINote)
return true;

// Guard against dehydrated events (null payload)
if (e.Payload == null)
return false; // Can't match filters without payload

// Check task_id filter
if (!string.IsNullOrEmpty(taskId))
{
if (e.Payload.TryGetValue("task_id", out var taskVal))
{
string eventTaskId = taskVal?.ToString();
if (eventTaskId != taskId)
return false;
}
else
{
return false;
}
}

// Check conversation_id filter
if (!string.IsNullOrEmpty(conversationId))
{
if (e.Payload.TryGetValue("conversation_id", out var convVal))
{
string eventConvId = convVal?.ToString();
if (eventConvId != conversationId)
return false;
}
}

return true;
}).ToList();
}

/// <summary>
/// P1.2: Apply task filters to projected events (with semantics).
/// </summary>
private static List<ActionTraceViewItem> ApplyTaskFiltersToProjected(List<ActionTraceViewItem> projected, string taskId, string conversationId)
{
if (string.IsNullOrEmpty(taskId) && string.IsNullOrEmpty(conversationId))
return projected;

return projected.Where(p =>
{
if (p.Event.Type != EventTypes.AINote)
return true;

// Guard against dehydrated events (null payload)
if (p.Event.Payload == null)
return false; // Can't match filters without payload

if (!string.IsNullOrEmpty(taskId))
{
if (p.Event.Payload.TryGetValue("task_id", out var taskVal))
{
if (taskVal?.ToString() != taskId)
return false;
}
else
{
return false;
}
}

if (!string.IsNullOrEmpty(conversationId))
{
if (p.Event.Payload.TryGetValue("conversation_id", out var convVal))
{
if (convVal?.ToString() != conversationId)
return false;
}
}

return true;
}).ToList();
}
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

conversation_id filtering should require a match.
With a conversation filter set, AINote events lacking conversation_id currently pass through, which defeats the filter’s intent.

🛠️ Proposed fix
                 if (!string.IsNullOrEmpty(conversationId))
                 {
                     if (e.Payload.TryGetValue("conversation_id", out var convVal))
                     {
                         string eventConvId = convVal?.ToString();
                         if (eventConvId != conversationId)
                             return false;
                     }
+                    else
+                    {
+                        return false;
+                    }
                 }
                 if (!string.IsNullOrEmpty(conversationId))
                 {
                     if (p.Event.Payload.TryGetValue("conversation_id", out var convVal))
                     {
                         if (convVal?.ToString() != conversationId)
                             return false;
                     }
+                    else
+                    {
+                        return false;
+                    }
                 }
📝 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
/// <summary>
/// P1.2: Apply task_id and conversation_id filters to raw event list.
/// Filters AINote events by matching task_id and conversation_id in payload.
/// </summary>
private static List<EditorEvent> ApplyTaskFilters(List<EditorEvent> events, string taskId, string conversationId)
{
// If no filters specified, return original list
if (string.IsNullOrEmpty(taskId) && string.IsNullOrEmpty(conversationId))
return events;
return events.Where(e =>
{
// Only AINote events have task_id and conversation_id
if (e.Type != EventTypes.AINote)
return true;
// Guard against dehydrated events (null payload)
if (e.Payload == null)
return false; // Can't match filters without payload
// Check task_id filter
if (!string.IsNullOrEmpty(taskId))
{
if (e.Payload.TryGetValue("task_id", out var taskVal))
{
string eventTaskId = taskVal?.ToString();
if (eventTaskId != taskId)
return false;
}
else
{
return false;
}
}
// Check conversation_id filter
if (!string.IsNullOrEmpty(conversationId))
{
if (e.Payload.TryGetValue("conversation_id", out var convVal))
{
string eventConvId = convVal?.ToString();
if (eventConvId != conversationId)
return false;
}
}
return true;
}).ToList();
}
/// <summary>
/// P1.2: Apply task filters to projected events (with semantics).
/// </summary>
private static List<ActionTraceViewItem> ApplyTaskFiltersToProjected(List<ActionTraceViewItem> projected, string taskId, string conversationId)
{
if (string.IsNullOrEmpty(taskId) && string.IsNullOrEmpty(conversationId))
return projected;
return projected.Where(p =>
{
if (p.Event.Type != EventTypes.AINote)
return true;
// Guard against dehydrated events (null payload)
if (p.Event.Payload == null)
return false; // Can't match filters without payload
if (!string.IsNullOrEmpty(taskId))
{
if (p.Event.Payload.TryGetValue("task_id", out var taskVal))
{
if (taskVal?.ToString() != taskId)
return false;
}
else
{
return false;
}
}
if (!string.IsNullOrEmpty(conversationId))
{
if (p.Event.Payload.TryGetValue("conversation_id", out var convVal))
{
if (convVal?.ToString() != conversationId)
return false;
}
}
return true;
}).ToList();
}
/// <summary>
/// P1.2: Apply task_id and conversation_id filters to raw event list.
/// Filters AINote events by matching task_id and conversation_id in payload.
/// </summary>
private static List<EditorEvent> ApplyTaskFilters(List<EditorEvent> events, string taskId, string conversationId)
{
// If no filters specified, return original list
if (string.IsNullOrEmpty(taskId) && string.IsNullOrEmpty(conversationId))
return events;
return events.Where(e =>
{
// Only AINote events have task_id and conversation_id
if (e.Type != EventTypes.AINote)
return true;
// Guard against dehydrated events (null payload)
if (e.Payload == null)
return false; // Can't match filters without payload
// Check task_id filter
if (!string.IsNullOrEmpty(taskId))
{
if (e.Payload.TryGetValue("task_id", out var taskVal))
{
string eventTaskId = taskVal?.ToString();
if (eventTaskId != taskId)
return false;
}
else
{
return false;
}
}
// Check conversation_id filter
if (!string.IsNullOrEmpty(conversationId))
{
if (e.Payload.TryGetValue("conversation_id", out var convVal))
{
string eventConvId = convVal?.ToString();
if (eventConvId != conversationId)
return false;
}
else
{
return false;
}
}
return true;
}).ToList();
}
/// <summary>
/// P1.2: Apply task filters to projected events (with semantics).
/// </summary>
private static List<ActionTraceViewItem> ApplyTaskFiltersToProjected(List<ActionTraceViewItem> projected, string taskId, string conversationId)
{
if (string.IsNullOrEmpty(taskId) && string.IsNullOrEmpty(conversationId))
return projected;
return projected.Where(p =>
{
if (p.Event.Type != EventTypes.AINote)
return true;
// Guard against dehydrated events (null payload)
if (p.Event.Payload == null)
return false; // Can't match filters without payload
if (!string.IsNullOrEmpty(taskId))
{
if (p.Event.Payload.TryGetValue("task_id", out var taskVal))
{
if (taskVal?.ToString() != taskId)
return false;
}
else
{
return false;
}
}
if (!string.IsNullOrEmpty(conversationId))
{
if (p.Event.Payload.TryGetValue("conversation_id", out var convVal))
{
if (convVal?.ToString() != conversationId)
return false;
}
else
{
return false;
}
}
return true;
}).ToList();
}
🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/Resources/ActionTrace/ActionTraceViewResource.cs` around
lines 349 - 440, The conversation_id filter currently allows AINote events
without a conversation_id to pass; update both ApplyTaskFilters and
ApplyTaskFiltersToProjected so that when conversationId is non-empty you require
the payload to contain "conversation_id" and that its ToString() equals
conversationId—i.e., if TryGetValue("conversation_id", out var convVal) returns
false, return false (just like the task_id branch), and otherwise compare
convVal?.ToString() != conversationId to reject non-matching values.

Comment on lines +185 to +196
// Check for errors
if (importanceCategory == "critical" || evtTypeLower.Contains("error") || evtTypeLower.Contains("exception"))
{
errorCount++;
errorEvents.Add(new
{
sequence = evt.Sequence,
type = evt.Type,
target = targetName,
summary = evt.GetSummary()
});
}
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 classifying AINote as an error.
AINote is scored as “critical”, so the current logic flags notes as errors. That inflates error counts and warnings.

🛠️ Proposed fix
-                if (importanceCategory == "critical" || evtTypeLower.Contains("error") || evtTypeLower.Contains("exception"))
-                {
+                bool isError = evtTypeLower.Contains("error") || evtTypeLower.Contains("exception");
+                if (importanceCategory == "critical" && !string.Equals(evt.Type, "AINote", StringComparison.Ordinal))
+                    isError = true;
+                if (isError)
+                {
📝 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
// Check for errors
if (importanceCategory == "critical" || evtTypeLower.Contains("error") || evtTypeLower.Contains("exception"))
{
errorCount++;
errorEvents.Add(new
{
sequence = evt.Sequence,
type = evt.Type,
target = targetName,
summary = evt.GetSummary()
});
}
// Check for errors
bool isError = evtTypeLower.Contains("error") || evtTypeLower.Contains("exception");
if (importanceCategory == "critical" && !string.Equals(evt.Type, "AINote", StringComparison.Ordinal))
isError = true;
if (isError)
{
errorCount++;
errorEvents.Add(new
{
sequence = evt.Sequence,
type = evt.Type,
target = targetName,
summary = evt.GetSummary()
});
}
🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/Tools/GetActionTraceSummaryTool.cs` around lines 185 -
196, The logic treats any event with importanceCategory == "critical" (or
containing "error"/"exception" in evtTypeLower) as an error, but AINote events
are scored critical and should not be counted; update the conditional in the
error counting block (the if that currently uses importanceCategory and
evtTypeLower) to exclude events whose evt.Type equals "AINote"
(case-insensitive) — e.g., only increment errorCount and add to errorEvents when
the existing conditions are true AND evt.Type does not equal "AINote" (use
StringComparison.OrdinalIgnoreCase for the comparison).

Comment on lines 290 to +294
AssetDatabase.SaveAssets();

// === ActionTrace Integration: Notify subscribers (low-coupling) ===
OnAssetCreated?.Invoke(fullPath, assetType);

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:

find . -name "ManageAsset.cs" -type f

Repository: CoplayDev/unity-mcp

Length of output: 104


🏁 Script executed:

wc -l ./MCPForUnity/Editor/Tools/ManageAsset.cs

Repository: CoplayDev/unity-mcp

Length of output: 109


🏁 Script executed:

cat -n ./MCPForUnity/Editor/Tools/ManageAsset.cs | sed -n '285,300p'

Repository: CoplayDev/unity-mcp

Length of output: 824


🏁 Script executed:

cat -n ./MCPForUnity/Editor/Tools/ManageAsset.cs | sed -n '493,510p'

Repository: CoplayDev/unity-mcp

Length of output: 1101


🏁 Script executed:

cat -n ./MCPForUnity/Editor/Tools/ManageAsset.cs | sed -n '542,555p'

Repository: CoplayDev/unity-mcp

Length of output: 786


🏁 Script executed:

cat -n ./MCPForUnity/Editor/Tools/ManageAsset.cs | sed -n '245,300p'

Repository: CoplayDev/unity-mcp

Length of output: 3491


🏁 Script executed:

cat -n ./MCPForUnity/Editor/Tools/ManageAsset.cs | sed -n '200,310p'

Repository: CoplayDev/unity-mcp

Length of output: 6407


🏁 Script executed:

cat -n ./MCPForUnity/Editor/Tools/ManageAsset.cs | sed -n '430,515p'

Repository: CoplayDev/unity-mcp

Length of output: 5736


🏁 Script executed:

cat -n ./MCPForUnity/Editor/Tools/ManageAsset.cs | sed -n '350,380p'

Repository: CoplayDev/unity-mcp

Length of output: 1560


🏁 Script executed:

cat -n ./MCPForUnity/Editor/Tools/ManageAsset.cs | sed -n '530,570p'

Repository: CoplayDev/unity-mcp

Length of output: 2117


🏁 Script executed:

cat -n ./MCPForUnity/Editor/Tools/ManageAsset.cs | sed -n '515,530p'

Repository: CoplayDev/unity-mcp

Length of output: 1003


Isolate subscriber exceptions from core asset operations.

All three callback invocations (OnAssetCreated, OnAssetModified, OnAssetDeleted) lack exception isolation. If a subscriber throws, the exception is caught by the outer method try-catch and the methods return ErrorResponse even though the asset operation (create, modify, delete) and SaveAssets/DeleteAsset already succeeded. This causes callers to incorrectly believe the asset operation failed.

Wrap each callback invocation in its own try-catch block to prevent external handlers from breaking the return contract:

🛡️ Suggested pattern (apply to each callback)
-                OnAssetCreated?.Invoke(fullPath, assetType);
+                try
+                {
+                    OnAssetCreated?.Invoke(fullPath, assetType);
+                }
+                catch (Exception ex)
+                {
+                    McpLog.Warn($"[ManageAsset] OnAssetCreated subscriber failed: {ex.Message}");
+                }

Also applies to: 499-503 (OnAssetModified), 548 (OnAssetDeleted)

🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/Tools/ManageAsset.cs` around lines 290 - 294, The three
callback invocations (OnAssetCreated, OnAssetModified, OnAssetDeleted) should be
wrapped in their own try-catch blocks so subscriber exceptions cannot propagate
and cause the outer method to return ErrorResponse even after the asset
operation and AssetDatabase.SaveAssets()/DeleteAsset succeeded; modify the
invocation sites (the lines calling OnAssetCreated?.Invoke(fullPath, assetType),
OnAssetModified?.Invoke(fullPath, assetType), and
OnAssetDeleted?.Invoke(fullPath)) to catch and log any exception locally (e.g.,
processLogger or UnityEngine.Debug.LogException) and continue normal return
flow, preserving the successful response from the core create/modify/delete
methods.

Comment on lines 682 to 808
private string BuildJsonExport()
{
_stringBuilder.Clear();
_stringBuilder.AppendLine("{");
_stringBuilder.AppendLine($" \"exportTime\": \"{DateTime.Now:O}\",");
_stringBuilder.AppendLine($" \"totalEvents\": {_currentEvents.Count},");
_stringBuilder.AppendLine(" \"events\": [");

for (int i = 0; i < _currentEvents.Count; i++)
{
var e = _currentEvents[i];
_stringBuilder.AppendLine(" {");
_stringBuilder.AppendLine($" \"sequence\": {e.Event.Sequence},");
_stringBuilder.AppendLine($" \"type\": \"{SanitizeJson(e.Event.Type)}\",");
_stringBuilder.AppendLine($" \"timestamp\": {e.Event.TimestampUnixMs},");
_stringBuilder.AppendLine($" \"displayTime\": \"{SanitizeJson(e.DisplayTime)}\",");
_stringBuilder.AppendLine($" \"summary\": \"{SanitizeJson(e.DisplaySummary)}\",");

if (!string.IsNullOrEmpty(e.Event.TargetId))
{
_stringBuilder.AppendLine($" \"targetId\": \"{SanitizeJson(e.Event.TargetId)}\",");
_stringBuilder.AppendLine($" \"targetName\": \"{SanitizeJson(e.TargetName)}\",");
_stringBuilder.AppendLine($" \"targetInstanceId\": {(e.TargetInstanceId.HasValue ? e.TargetInstanceId.Value.ToString() : "null")},");
}
else
{
_stringBuilder.AppendLine($" \"targetId\": null,");
_stringBuilder.AppendLine($" \"targetName\": null,");
_stringBuilder.AppendLine($" \"targetInstanceId\": null,");
}

_stringBuilder.AppendLine($" \"importanceScore\": {e.ImportanceScore:F2},");
_stringBuilder.AppendLine($" \"importanceCategory\": \"{SanitizeJson(e.ImportanceCategory)}\"");

if (!string.IsNullOrEmpty(e.InferredIntent))
_stringBuilder.AppendLine($" ,\"inferredIntent\": \"{SanitizeJson(e.InferredIntent)}\"");

if (e.Event.Payload != null && e.Event.Payload.Count > 0)
{
_stringBuilder.AppendLine(" ,\"payload\": {");
var payloadKeys = e.Event.Payload.Keys.ToList();
for (int j = 0; j < payloadKeys.Count; j++)
{
var key = payloadKeys[j];
var value = e.Event.Payload[key];
var valueStr = value?.ToString() ?? "null";
_stringBuilder.AppendLine($" \"{SanitizeJson(key)}\": \"{SanitizeJson(valueStr)}\"{(j < payloadKeys.Count - 1 ? "," : "")}");
}
_stringBuilder.AppendLine(" }");
}

_stringBuilder.Append(i < _currentEvents.Count - 1 ? " }," : " }");
if (i < _currentEvents.Count - 1)
_stringBuilder.AppendLine();
}

_stringBuilder.AppendLine();
_stringBuilder.AppendLine(" ]");
_stringBuilder.AppendLine("}");

return _stringBuilder.ToString();
}

private string BuildCsvExport()
{
_stringBuilder.Clear();
_stringBuilder.AppendLine("Sequence,Time,Type,Summary,Target Name,InstanceID,Importance,Category,Intent");

foreach (var e in _currentEvents)
{
_stringBuilder.Append(e.Event.Sequence);
_stringBuilder.Append(",\"");
_stringBuilder.Append(SanitizeCsv(e.DisplayTime));
_stringBuilder.Append("\",\"");
_stringBuilder.Append(SanitizeCsv(e.Event.Type));
_stringBuilder.Append("\",\"");
_stringBuilder.Append(SanitizeCsv(e.DisplaySummary));
_stringBuilder.Append("\",\"");

// Get instance info for export
if (!string.IsNullOrEmpty(e.TargetName))
{
_stringBuilder.Append(SanitizeCsv(e.TargetName));
_stringBuilder.Append("\",\"");
_stringBuilder.Append(SanitizeCsv(e.TargetInstanceId?.ToString() ?? ""));
}
else
{
_stringBuilder.Append("\",\"");
}

_stringBuilder.Append("\",");
_stringBuilder.Append(e.ImportanceScore.ToString("F2"));
_stringBuilder.Append(",\"");
_stringBuilder.Append(SanitizeCsv(e.ImportanceCategory));
_stringBuilder.Append("\",\"");
_stringBuilder.Append(SanitizeCsv(e.InferredIntent ?? ""));
_stringBuilder.AppendLine("\"");
}

return _stringBuilder.ToString();
}

private string SanitizeJson(string input)
{
if (string.IsNullOrEmpty(input)) return "";

// Performance optimization: use StringBuilder to avoid intermediate strings from multiple Replace calls
var sb = _stringBuilder;
sb.Clear();
sb.EnsureCapacity(input.Length * 2);

foreach (char c in input)
{
switch (c)
{
case '\\': sb.Append("\\\\"); break;
case '\"': sb.Append("\\\""); break;
case '\n': sb.Append("\\n"); break;
case '\r': sb.Append("\\r"); break;
case '\t': sb.Append("\\t"); break;
default: sb.Append(c); break;
}
}

return sb.ToString();
}
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

SanitizeJson clears the export builder, corrupting JSON output.

BuildJsonExport builds JSON with _stringBuilder, but SanitizeJson clears that same builder, wiping the partially built payload.

🔧 Proposed fix (use a local builder in SanitizeJson)
-            var sb = _stringBuilder;
-            sb.Clear();
-            sb.EnsureCapacity(input.Length * 2);
+            var sb = new StringBuilder(input.Length * 2);
📝 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 string BuildJsonExport()
{
_stringBuilder.Clear();
_stringBuilder.AppendLine("{");
_stringBuilder.AppendLine($" \"exportTime\": \"{DateTime.Now:O}\",");
_stringBuilder.AppendLine($" \"totalEvents\": {_currentEvents.Count},");
_stringBuilder.AppendLine(" \"events\": [");
for (int i = 0; i < _currentEvents.Count; i++)
{
var e = _currentEvents[i];
_stringBuilder.AppendLine(" {");
_stringBuilder.AppendLine($" \"sequence\": {e.Event.Sequence},");
_stringBuilder.AppendLine($" \"type\": \"{SanitizeJson(e.Event.Type)}\",");
_stringBuilder.AppendLine($" \"timestamp\": {e.Event.TimestampUnixMs},");
_stringBuilder.AppendLine($" \"displayTime\": \"{SanitizeJson(e.DisplayTime)}\",");
_stringBuilder.AppendLine($" \"summary\": \"{SanitizeJson(e.DisplaySummary)}\",");
if (!string.IsNullOrEmpty(e.Event.TargetId))
{
_stringBuilder.AppendLine($" \"targetId\": \"{SanitizeJson(e.Event.TargetId)}\",");
_stringBuilder.AppendLine($" \"targetName\": \"{SanitizeJson(e.TargetName)}\",");
_stringBuilder.AppendLine($" \"targetInstanceId\": {(e.TargetInstanceId.HasValue ? e.TargetInstanceId.Value.ToString() : "null")},");
}
else
{
_stringBuilder.AppendLine($" \"targetId\": null,");
_stringBuilder.AppendLine($" \"targetName\": null,");
_stringBuilder.AppendLine($" \"targetInstanceId\": null,");
}
_stringBuilder.AppendLine($" \"importanceScore\": {e.ImportanceScore:F2},");
_stringBuilder.AppendLine($" \"importanceCategory\": \"{SanitizeJson(e.ImportanceCategory)}\"");
if (!string.IsNullOrEmpty(e.InferredIntent))
_stringBuilder.AppendLine($" ,\"inferredIntent\": \"{SanitizeJson(e.InferredIntent)}\"");
if (e.Event.Payload != null && e.Event.Payload.Count > 0)
{
_stringBuilder.AppendLine(" ,\"payload\": {");
var payloadKeys = e.Event.Payload.Keys.ToList();
for (int j = 0; j < payloadKeys.Count; j++)
{
var key = payloadKeys[j];
var value = e.Event.Payload[key];
var valueStr = value?.ToString() ?? "null";
_stringBuilder.AppendLine($" \"{SanitizeJson(key)}\": \"{SanitizeJson(valueStr)}\"{(j < payloadKeys.Count - 1 ? "," : "")}");
}
_stringBuilder.AppendLine(" }");
}
_stringBuilder.Append(i < _currentEvents.Count - 1 ? " }," : " }");
if (i < _currentEvents.Count - 1)
_stringBuilder.AppendLine();
}
_stringBuilder.AppendLine();
_stringBuilder.AppendLine(" ]");
_stringBuilder.AppendLine("}");
return _stringBuilder.ToString();
}
private string BuildCsvExport()
{
_stringBuilder.Clear();
_stringBuilder.AppendLine("Sequence,Time,Type,Summary,Target Name,InstanceID,Importance,Category,Intent");
foreach (var e in _currentEvents)
{
_stringBuilder.Append(e.Event.Sequence);
_stringBuilder.Append(",\"");
_stringBuilder.Append(SanitizeCsv(e.DisplayTime));
_stringBuilder.Append("\",\"");
_stringBuilder.Append(SanitizeCsv(e.Event.Type));
_stringBuilder.Append("\",\"");
_stringBuilder.Append(SanitizeCsv(e.DisplaySummary));
_stringBuilder.Append("\",\"");
// Get instance info for export
if (!string.IsNullOrEmpty(e.TargetName))
{
_stringBuilder.Append(SanitizeCsv(e.TargetName));
_stringBuilder.Append("\",\"");
_stringBuilder.Append(SanitizeCsv(e.TargetInstanceId?.ToString() ?? ""));
}
else
{
_stringBuilder.Append("\",\"");
}
_stringBuilder.Append("\",");
_stringBuilder.Append(e.ImportanceScore.ToString("F2"));
_stringBuilder.Append(",\"");
_stringBuilder.Append(SanitizeCsv(e.ImportanceCategory));
_stringBuilder.Append("\",\"");
_stringBuilder.Append(SanitizeCsv(e.InferredIntent ?? ""));
_stringBuilder.AppendLine("\"");
}
return _stringBuilder.ToString();
}
private string SanitizeJson(string input)
{
if (string.IsNullOrEmpty(input)) return "";
// Performance optimization: use StringBuilder to avoid intermediate strings from multiple Replace calls
var sb = _stringBuilder;
sb.Clear();
sb.EnsureCapacity(input.Length * 2);
foreach (char c in input)
{
switch (c)
{
case '\\': sb.Append("\\\\"); break;
case '\"': sb.Append("\\\""); break;
case '\n': sb.Append("\\n"); break;
case '\r': sb.Append("\\r"); break;
case '\t': sb.Append("\\t"); break;
default: sb.Append(c); break;
}
}
return sb.ToString();
}
private string SanitizeJson(string input)
{
if (string.IsNullOrEmpty(input)) return "";
// Performance optimization: use StringBuilder to avoid intermediate strings from multiple Replace calls
var sb = new StringBuilder(input.Length * 2);
foreach (char c in input)
{
switch (c)
{
case '\\': sb.Append("\\\\"); break;
case '\"': sb.Append("\\\""); break;
case '\n': sb.Append("\\n"); break;
case '\r': sb.Append("\\r"); break;
case '\t': sb.Append("\\t"); break;
default: sb.Append(c); break;
}
}
return sb.ToString();
}
🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs` around lines 682 -
808, SanitizeJson currently reuses and clears the shared field _stringBuilder
(used by BuildJsonExport), which corrupts the in-progress export; modify
SanitizeJson to allocate and use a local StringBuilder (e.g., new StringBuilder
with capacity based on input.Length) or otherwise avoid mutating the shared
_stringBuilder field, perform the escape loop on the local builder, and return
its ToString() so BuildJsonExport's _stringBuilder is not cleared or modified.

Comment on lines +843 to +851
private void OnClearClicked()
{
EventStore.Clear();
_currentEvents.Clear();
_lastEventStoreCount = 0;
_eventListView.RefreshItems();
_detailScrollView.Clear();
UpdateStatus();
McpLog.Debug("[ActionTraceEditorWindow] Clear clicked");
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:

cd MCPForUnity/Editor/Windows && cat -n ActionTraceEditorWindow.cs | head -100

Repository: CoplayDev/unity-mcp

Length of output: 182


🏁 Script executed:

cat MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs | sed -n '1,150p'

Repository: CoplayDev/unity-mcp

Length of output: 7270


🏁 Script executed:

# Look at the OnClearClicked method and surrounding methods
cat -n MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs | sed -n '770,870p'

Repository: CoplayDev/unity-mcp

Length of output: 4076


🏁 Script executed:

# Search for OnSelectionChanged or similar methods that populate details
rg -n "OnSelectionChanged|SelectionChanged|DisplayEventDetails|RefreshDetail" MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs

Repository: CoplayDev/unity-mcp

Length of output: 204


🏁 Script executed:

# Check how the visual tree is structured
rg -n "visual tree|RemoveFromHierarchy|Add.*ScrollView|hierarchy" MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs

Repository: CoplayDev/unity-mcp

Length of output: 45


🏁 Script executed:

cat MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs

Repository: CoplayDev/unity-mcp

Length of output: 50011


Clearing the ScrollView removes UI elements from the visual tree permanently.

Calling _detailScrollView.Clear() removes all children (_detailPlaceholder, _detailContent, _detailActions) from the visual tree. When an event is subsequently selected, OnSelectionChanged tries to operate on orphaned elements, preventing the detail UI from rendering. The placeholder should remain visible until an item is selected.

🔧 Proposed fix
-            _detailScrollView.Clear();
+            _detailContent.Clear();
+            _detailPlaceholder.style.display = DisplayStyle.Flex;
+            _detailContent.style.display = DisplayStyle.None;
+            _detailActions.style.display = DisplayStyle.None;
📝 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 void OnClearClicked()
{
EventStore.Clear();
_currentEvents.Clear();
_lastEventStoreCount = 0;
_eventListView.RefreshItems();
_detailScrollView.Clear();
UpdateStatus();
McpLog.Debug("[ActionTraceEditorWindow] Clear clicked");
private void OnClearClicked()
{
EventStore.Clear();
_currentEvents.Clear();
_lastEventStoreCount = 0;
_eventListView.RefreshItems();
_detailContent.Clear();
_detailPlaceholder.style.display = DisplayStyle.Flex;
_detailContent.style.display = DisplayStyle.None;
_detailActions.style.display = DisplayStyle.None;
UpdateStatus();
McpLog.Debug("[ActionTraceEditorWindow] Clear clicked");
}
🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs` around lines 843 -
851, The Clear handler currently calls _detailScrollView.Clear(), which removes
_detailPlaceholder/_detailContent/_detailActions from the visual tree and breaks
OnSelectionChanged; change OnClearClicked to preserve the placeholder element
instead of clearing the whole scroll view: do not call
_detailScrollView.Clear(), instead clear the contents of _detailContent and
_detailActions (e.g., remove their child elements or reset text) and ensure
_detailPlaceholder is made visible (or re-added) so the detail UI remains in the
visual tree for future selections; update references in OnSelectionChanged to
assume the placeholder exists.

Comment on lines +1203 to +1228
private void OnEnable()
{
EditorApplication.update += OnEditorUpdate;
_isScheduledRefreshActive = true;
}

private void OnDisable()
{
EditorApplication.update -= OnEditorUpdate;
_isScheduledRefreshActive = false;

if (ActionTraceSettings.Instance != null && _previousBypassImportanceFilter.HasValue)
{
ActionTraceSettings.Instance.Filtering.BypassImportanceFilter = _previousBypassImportanceFilter.Value;
}
}

private void OnEditorUpdate()
{
// Performance optimization: use time interval to control refresh rate
if (_isScheduledRefreshActive &&
EditorApplication.timeSinceStartup - _lastRefreshTime > RefreshInterval)
{
_lastRefreshTime = EditorApplication.timeSinceStartup;
RefreshEvents();
}
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
# First, let's locate and examine the ActionTraceEditorWindow.cs file
find . -name "ActionTraceEditorWindow.cs" -type f

Repository: CoplayDev/unity-mcp

Length of output: 182


🏁 Script executed:

#!/bin/bash
# Get the file size to determine if we can read it completely
wc -l ./MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs

Repository: CoplayDev/unity-mcp

Length of output: 123


🏁 Script executed:

#!/bin/bash
# Search for CreateGUI method and OnEditorUpdate method to understand the lifecycle
rg -n "CreateGUI|OnEnable|OnDisable|OnEditorUpdate" ./MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs | head -20

Repository: CoplayDev/unity-mcp

Length of output: 339


🏁 Script executed:

#!/bin/bash
# Look for _actionTraceQuery and _eventListView field declarations and usage
rg -n "_actionTraceQuery|_eventListView" ./MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs | head -30

Repository: CoplayDev/unity-mcp

Length of output: 1369


🏁 Script executed:

#!/bin/bash
# Get the full OnEditorUpdate method implementation to see what it does
sed -n '1220,1250p' ./MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs

Repository: CoplayDev/unity-mcp

Length of output: 498


🏁 Script executed:

#!/bin/bash
# Find RefreshEvents method and see what it does
rg -n "RefreshEvents" ./MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs

Repository: CoplayDev/unity-mcp

Length of output: 416


🏁 Script executed:

#!/bin/bash
# Get the RefreshEvents method implementation
sed -n '920,960p' ./MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs

Repository: CoplayDev/unity-mcp

Length of output: 1607


🏁 Script executed:

#!/bin/bash
# Check the full context of CreateGUI and where fields are initialized
sed -n '160,280p' ./MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs

Repository: CoplayDev/unity-mcp

Length of output: 6121


🏁 Script executed:

#!/bin/bash
# Check the beginning of RefreshEvents to see if there are existing null checks
sed -n '890,920p' ./MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs

Repository: CoplayDev/unity-mcp

Length of output: 1767


Add null guards in OnEditorUpdate to prevent accessing uninitialized UI components.

RefreshEvents() directly accesses _actionTraceQuery and _eventListView without null checks. If CreateGUI returns early (when UXML fails to load), these fields remain uninitialized. Meanwhile, OnEnable immediately subscribes OnEditorUpdate to the editor update loop, causing it to invoke RefreshEvents() with null references on the first tick.

Proposed fix
         private void OnEditorUpdate()
         {
+            if (_actionTraceQuery == null || _eventListView == null)
+                return;
             // Performance optimization: use time interval to control refresh rate
             if (_isScheduledRefreshActive && 
                 EditorApplication.timeSinceStartup - _lastRefreshTime > RefreshInterval)
🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs` around lines 1203 -
1228, OnEditorUpdate currently calls RefreshEvents() regardless of whether the
UI was initialized in CreateGUI, which can lead to null dereferences on
_actionTraceQuery and _eventListView; add a guard at the start of OnEditorUpdate
to return early if those UI fields are null (e.g., if _actionTraceQuery == null
|| _eventListView == null) or alternatively move the call to RefreshEvents
behind a boolean flag set by CreateGUI when initialization succeeds; ensure the
guard references OnEditorUpdate, RefreshEvents, _actionTraceQuery,
_eventListView and preserves the existing scheduled refresh logic.

Comment on lines +113 to +122
# Retrieve from context if not explicitly provided
effective_task_id = task_id or _current_task_id.get()
effective_conv_id = conversation_id or _current_conversation_id.get()

# Auto-generate if still empty
if not effective_task_id:
effective_task_id = f"task-{uuid.uuid4().hex[:8]}"
if not effective_conv_id:
effective_conv_id = f"conv-{uuid.uuid4().hex[:8]}"

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

Persist auto-generated task/conversation IDs to session context.

Line 118-121 generates IDs when context is empty, but they aren’t written back to _current_task_id / _current_conversation_id. Subsequent calls without explicit IDs will generate new ones, breaking task-level grouping.

✅ Suggested fix
-    if not effective_task_id:
-        effective_task_id = f"task-{uuid.uuid4().hex[:8]}"
-    if not effective_conv_id:
-        effective_conv_id = f"conv-{uuid.uuid4().hex[:8]}"
+    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)
🤖 Prompt for AI Agents
In `@Server/src/services/tools/add_action_trace_note.py` around lines 113 - 122,
The generated IDs for effective_task_id and effective_conv_id are not persisted
back to session context, causing new IDs to be created on subsequent calls;
after auto-generating (in the block that sets effective_task_id =
f"task-{uuid.uuid4().hex[:8]}" and effective_conv_id =
f"conv-{uuid.uuid4().hex[:8]}"), assign those values into the context stores
(_current_task_id.set(effective_task_id) and
_current_conversation_id.set(effective_conv_id)) so future calls that read
_current_task_id.get() and _current_conversation_id.get() see the same IDs and
preserve task-level grouping.

@dsarno
Copy link
Collaborator

dsarno commented Jan 21, 2026

@whatevertogo thank you for sharing these cool features, you are clearly spending quite a bit of time building and improving them, and they seem interesting and creative.

However for now, they are not the right fit for the Unity MCP. For one thing, large PRs where you're adding 16,000 lines of code are difficult and time consuming for us to review -- we ourselves almost never make additions of that size. It's just too much to evaluate and maintain.

Also, it's a bit tricky to decide if the hook and trace features make sense in the mcp project. They seem like a slightly different type of AI development feature, perhaps beyond the scope of MCP (or something to use alongside it).

For this reason we're going to pass on these PRs for now. But we encourage you to continue working on them and share them with our community Discord for others to try. Or perhaps share some video demos of how you use them. If people start to use them and feel they should be included in the mcp project, we may be able to return to the discussion.

Thanks again!

@whatevertogo
Copy link
Contributor Author

whatevertogo commented Jan 21, 2026

@whatevertogo thank you for sharing these cool features, you are clearly spending quite a bit of time building and improving them, and they seem interesting and creative.

However for now, they are not the right fit for the Unity MCP. For one thing, large PRs where you're adding 16,000 lines of code are difficult and time consuming for us to review -- we ourselves almost never make additions of that size. It's just too much to evaluate and maintain.

Also, it's a bit tricky to decide if the hook and trace features make sense in the mcp project. They seem like a slightly different type of AI development feature, perhaps beyond the scope of MCP (or something to use alongside it).

For this reason we're going to pass on these PRs for now. But we encourage you to continue working on them and share them with our community Discord for others to try. Or perhaps share some video demos of how you use them. If people start to use them and feel they should be included in the mcp project, we may be able to return to the discussion.

Thanks again!

Thank you for your feedback; I fully understand and share your concerns regarding the PR size. Regarding the design, I firmly believe that the Hook system will be essential to this project in the long run, as it enables the AI agent to automatically and precisely guide its next steps based on various Unity runtime states.

The introduction of Action Trace stems from the reality that AI cannot yet achieve full autonomy and still requires human intervention. During this collaborative process, the agent often lacks complete situational awareness; therefore, tracing the context is vital for the agent to understand the current state and provide meaningful assistance. These are just my personal reflections on the matter. Thank you again for your professional insights—your perspective is truly invaluable.

Actually,This is only about 7,200 lines of code, but I wrote a lot of comments, plus about 800 lines of documentation.

If it helps, I would be happy to break this down into smaller, more manageable PRs to make the review process easier for you.

@whatevertogo
Copy link
Contributor Author

whatevertogo commented Jan 21, 2026

I’ll show my Action Trace system this weekend.

@msanatan
Copy link
Member

@whatevertogo Sorry for the late reply on my end - aside from personal things to attend to I needed to think about this one a bit more.

I think the video will help a lot. It's not immediately clear to me how this resource actually helps AI tools when building something. It looks awesome, but what's the real world use case? For e.g. recently @dsarno added a tool specifically for interacting with scriptable objects. The capability existed, but it was inefficient and prone to error. So by adding the tool, the AI tool got over a hurdle with Unity MCP that it couldn't do before.

In this case, what was it that your AI tool COULD NOT DO, and how does this actually make it accomplish more.

As a side note, you've been contributing a lot and I really really appreciate it. I think all of the core maintainers do. Even with disagreements (which we have amongst ourselves too), we hope that you continue being a part of our community and contributing. You're awesome ⭐

Copilot AI review requested due to automatic review settings January 22, 2026 07:42
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a comprehensive event tracking and monitoring infrastructure for AI-assisted Unity development, consisting of two foundational systems:

Changes:

  • Implements HookSystem: A general-purpose event dispatch infrastructure that abstracts Unity's native callbacks into a clean, thread-safe subscription interface with exception isolation
  • Implements ActionTrace: A sophisticated event tracking system featuring event recording, semantic analysis, dehydration for memory efficiency, and AI-friendly query interfaces
  • Adds MCP tools for AI interaction: get_action_trace, get_action_trace_summary, add_action_trace_note, undo_to_sequence, and get_action_trace_settings

Reviewed changes

Copilot reviewed 76 out of 76 changed files in this pull request and generated 57 comments.

Show a summary per file
File Description
docs/Hooks/DESIGN.md Comprehensive design documentation for Hook system architecture
docs/Hooks/CONTRIBUTING.md Contributing guide for extending the Hook system
docs/ActionTrace/DESIGN.md Design philosophy and architectural decisions for ActionTrace
docs/ActionTrace/CONTRIBUTING.md Contributing guide for ActionTrace extensions
Server/src/services/tools/*.py Python MCP tool implementations for ActionTrace queries
Server/src/services/resources/action_trace.py Python resource endpoints for ActionTrace
Server/src/models/action_trace.py Pydantic models for ActionTrace data validation
MCPForUnity/Editor/Windows/ActionTraceEditorWindow.* Unity Editor window for visualizing action traces
MCPForUnity/Editor/Tools/*Tool.cs C# MCP tool handlers for ActionTrace operations
MCPForUnity/Editor/Resources/ActionTrace/*.cs C# resource handlers and query infrastructure
MCPForUnity/Editor/Hooks/*.cs Core Hook system implementation with event registry
MCPForUnity/Editor/Helpers/*.cs Helper utilities for GlobalID resolution and build targets
MCPForUnity/Editor/ActionTrace/**/*.cs ActionTrace subsystems (Semantics, Integration, Sources)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +141 to +188
foreach (var assetPath in importedAssets)
{
if (string.IsNullOrEmpty(assetPath)) continue;

// L0 Deduplication: Skip if already processed in this session
// This prevents duplicate events when Unity fires OnPostprocessAllAssets
// multiple times for the same asset (creation, compilation, re-import)
if (!_processedAssetsInSession.Add(assetPath))
continue; // Already processed, skip to prevent duplicate events

hasChanges = true; // Mark that we added a new entry

// L1 Blacklist: Skip junk assets before creating events
if (!EventFilter.ShouldTrackAsset(assetPath))
{
// Remove from tracking if it's a junk asset (we don't want to track it)
_processedAssetsInSession.Remove(assetPath);
continue;
}

string targetId = $"Asset:{assetPath}";
string assetType = GetAssetType(assetPath);

var payload = new Dictionary<string, object>
{
["path"] = assetPath,
["extension"] = System.IO.Path.GetExtension(assetPath),
["asset_type"] = assetType
};

// Mutually exclusive event classification (prevents duplicate events)
if (IsNewlyCreatedAsset(assetPath))
{
// Priority 1: Newly created assets (first-time existence)
RecordEvent(EventTypes.AssetCreated, targetId, payload);
}
else if (ShouldTrackModification(assetPath))
{
// Priority 2: Existing assets with trackable modification types
// Covers: re-imports, content changes, settings updates
RecordEvent(EventTypes.AssetModified, targetId, payload);
}
else
{
// Priority 3: Generic imports (fallback for untracked types)
RecordEvent(EventTypes.AssetImported, targetId, payload);
}
}
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +191 to +213
foreach (var assetPath in deletedAssets)
{
if (string.IsNullOrEmpty(assetPath)) continue;

// L0 Deduplication: Skip if already processed in this session
if (!_processedAssetsInSession.Add(assetPath))
continue;

hasChanges = true; // Mark that we added a new entry

// L1 Blacklist: Skip junk assets
if (!EventFilter.ShouldTrackAsset(assetPath))
continue;

string targetId = $"Asset:{assetPath}";

var payload = new Dictionary<string, object>
{
["path"] = assetPath
};

RecordEvent(EventTypes.AssetDeleted, targetId, payload);
}
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +153 to +157
foreach (string key in keyList)
{
if (HasMeaningfulValue(evt, key.Trim()))
return thenText;
}
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +173 to +177
foreach (string key in keyList)
{
if (!HasMeaningfulValue(evt, key.Trim()))
return "";
}
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +217 to +231
foreach (var kvp in _pendingChanges)
{
// When forced, flush all entries. Otherwise, only flush expired entries.
if (force || nowMs - kvp.Value.LastUpdateMs >= DebounceWindowMs)
{
// Record the PropertyModified event
RecordPropertyModifiedEvent(kvp.Value);

// Return to object pool
ReturnPendingChange(kvp.Value);

// Mark for removal
_removedKeys.Add(kvp.Key);
}
}
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
from typing import Annotated, Any, Literal, Optional

from fastmcp import Context
from mcp.types import ToolAnnotations
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

Import of 'ToolAnnotations' is not used.

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +25
from models.action_trace import (
EditorEvent,
EventQueryResult,
EventStatistics,
ActionTraceSettings,
)
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

Import of 'ActionTraceSettings' is not used.

Copilot uses AI. Check for mistakes.

from services.registry import mcp_for_unity_tool
from services.tools import get_unity_instance_from_context
from services.tools.utils import coerce_bool
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

Import of 'coerce_bool' is not used.

Copilot uses AI. Check for mistakes.
Unity implementation: MCPForUnity/Editor/Tools/ActionTraceSettingsTool.cs
"""
from typing import Annotated, Any
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

Import of 'Annotated' is not used.

Copilot uses AI. Check for mistakes.
parsed = json.loads(value)
if isinstance(parsed, list):
return _coerce_int_list(parsed) # Recursively handle parsed list
except json.JSONDecodeError:
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

'except' clause does nothing but pass and there is no explanatory comment.

Copilot uses AI. Check for mistakes.
@whatevertogo whatevertogo marked this pull request as draft January 22, 2026 07:51
… and improve property modification handling. Removed unused UndoCapture and UndoToSequenceTool files. Updated ActionTrace settings and enhanced Python tool descriptions for clarity.
@msanatan
Copy link
Member

Just re-iterataing @whatevertogo, please explain the exact use case where this feature improved what the model could do

@whatevertogo Sorry for the late reply on my end - aside from personal things to attend to I needed to think about this one a bit more.

I think the video will help a lot. It's not immediately clear to me how this resource actually helps AI tools when building something. It looks awesome, but what's the real world use case? For e.g. recently @dsarno added a tool specifically for interacting with scriptable objects. The capability existed, but it was inefficient and prone to error. So by adding the tool, the AI tool got over a hurdle with Unity MCP that it couldn't do before.

In this case, what was it that your AI tool COULD NOT DO, and how does this actually make it accomplish more.

As a side note, you've been contributing a lot and I really really appreciate it. I think all of the core maintainers do. Even with disagreements (which we have amongst ourselves too), we hope that you continue being a part of our community and contributing. You're awesome ⭐

@whatevertogo
Copy link
Contributor Author

whatevertogo commented Jan 22, 2026

Just re-iterataing @whatevertogo, please explain the exact use case where this feature improved what the model could do

@whatevertogo Sorry for the late reply on my end - aside from personal things to attend to I needed to think about this one a bit more.
I think the video will help a lot. It's not immediately clear to me how this resource actually helps AI tools when building something. It looks awesome, but what's the real world use case? For e.g. recently @dsarno added a tool specifically for interacting with scriptable objects. The capability existed, but it was inefficient and prone to error. So by adding the tool, the AI tool got over a hurdle with Unity MCP that it couldn't do before.
In this case, what was it that your AI tool COULD NOT DO, and how does this actually make it accomplish more.
As a side note, you've been contributing a lot and I really really appreciate it. I think all of the core maintainers do. Even with disagreements (which we have amongst ourselves too), we hope that you continue being a part of our community and contributing. You're awesome ⭐

https://www.youtube.com/watch?v=ykhhOa-g7PI
sorry i put it in discord.

By the way,I’m working on improving the manage_prefab implementation.

@msanatan msanatan changed the base branch from main to beta January 22, 2026 19:18
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.

3 participants