-
Notifications
You must be signed in to change notification settings - Fork 693
Feat:actiontrace Feat:HookSystem #588
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
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>
…ides and system paths
…ethods across platform detectors
… WindowsPlatformDetector
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>
…ctionTraceEditorWindow
… clarity and consistency
…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>
…al loops and enhancing GameObject tracking
…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>
…on in ActionTrace
…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>
- Add HookRegistry.cs: Central event hub with dual event system (simple + detailed) - Add UnityEventHooks.cs: Pure detector for Unity editor events - Add ActionTraceRecorder.cs: ActionTrace-specific recorder using HookRegistry - Add HookTest.cs: Test script for hook verification (commented out) Architecture: Unity Events → UnityEventHooks → HookRegistry → Subscribers This keeps UnityEventHooks as a pure detector without ActionTrace dependencies. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this 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
📝 WalkthroughWalkthroughAdds a comprehensive ActionTrace subsystem to the Unity editor: event capture points, sampling, filtering, scoring/categorization, context and tool-call tracking, an append-only EventStore with persistence and merging, query/aggregation/compression layers, an ActionTrace editor window, Python server endpoints/tools, and multiple helpers/integrations. Changes
Sequence Diagram(s)sequenceDiagram
participant Capture as "Capture Point\n(Asset/Property/Selection)"
participant Sampling as "SamplingMiddleware\n(Debounce/Throttle)"
participant Store as "EventStore\n(Record, Merge, Persist)"
participant Context as "ContextStack / ContextTrace"
participant UI as "ActionTraceEditorWindow"
participant API as "Python Resource\nget_action_trace"
rect rgba(200,200,255,0.5)
Capture->>Sampling: Emit EditorEvent
Sampling->>Store: ShouldRecord? -> Record or Cache
Store->>Context: EventRecorded notification
Context->>Store: AddContextMapping (if current context exists)
end
rect rgba(200,255,200,0.5)
UI->>API: query (limit, semantics)
API->>Store: request events (via Unity transport)
Store-->>API: return events (with semantics/context if requested)
API-->>UI: response
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 18
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
MCPForUnity/Editor/Tools/ManageAsset.cs (1)
218-223: Folder creation bypasses OnAssetCreated
assetType == "folder"routes toCreateFolder, and thecreate_folderaction also usesCreateFolder, but that method never raisesOnAssetCreated. If ActionTrace expects folder creation events, emit the callback on success.🛠️ Suggested fix (emit on success)
string guid = AssetDatabase.CreateFolder(parentDir, folderName); if (string.IsNullOrEmpty(guid)) { return new ErrorResponse( $"Failed to create folder '{fullPath}'. Check logs and permissions." ); } + OnAssetCreated?.Invoke(fullPath, "folder"); + // AssetDatabase.Refresh(); // CreateFolder usually handles refresh return new SuccessResponse( $"Folder '{fullPath}' created successfully.", GetAssetData(fullPath)Also applies to: 333-355
🤖 Fix all issues with AI agents
In `@MCPForUnity/Editor/ActionTrace/Capture/AssetChangePostprocessor.cs`:
- Around line 103-128: Duplicate events occur because the same asset can be
emitted as both AssetImported and AssetModified; update the modified-assets loop
in the importedAssets foreach to skip assets already emitted as imports by
checking the same condition used for "import" emission (e.g., if
IsNewlyCreatedAsset(assetPath) or a flag indicating you emitted AssetImported),
so only one event is recorded; modify the foreach that calls
ShouldTrackModification and RecordEvent(EventTypes.AssetModified, ...) to
early-continue when the asset was already handled as imported (use
IsNewlyCreatedAsset or introduce/consult a local HashSet<string> of
alreadyEmitted assets) and keep RecordEvent and ShouldTrackModification logic
unchanged for truly modified assets.
In `@MCPForUnity/Editor/ActionTrace/Capture/PropertyChangeTracker.cs`:
- Around line 155-188: FlushPendingChanges currently only emits entries older
than DebounceWindowMs causing ForceFlush to drop recent changes; change
FlushPendingChanges to accept a bool force (default false) and when force==true
bypass the debounce age check so all _pendingChanges are recorded, returned to
pool, and removed; update the EditorApplication.update caller to call the
default path and modify the ForceFlush method (the one around lines 243-250) to
call FlushPendingChanges(true) so shutdown/force flush emits all pending
entries.
In `@MCPForUnity/Editor/ActionTrace/Capture/SamplingMiddleware.cs`:
- Around line 360-401: SamplingConfig.Strategies is a non-thread-safe Dictionary
accessed concurrently (e.g., ShouldRecord, FlushExpiredDebounceSamples via
EditorApplication.update, and SetStrategy/RemoveStrategy), which can throw
during enumeration; fix by making access thread-safe — replace the Dictionary
with a ConcurrentDictionary<string, SamplingStrategy> or guard all reads/writes
with a lock object, update SetStrategy and RemoveStrategy to use the new
concurrent API or acquire the lock, and ensure ShouldRecord and
FlushExpiredDebounceSamples read from the thread-safe store (or snapshot
keys/values under the same lock) to avoid modification-during-enumeration
exceptions.
In `@MCPForUnity/Editor/ActionTrace/Context/ToolCallScope.cs`:
- Around line 471-494: ExecuteAsync<T> creates a ToolCallScope (scope) but never
disposes it, leaking the scope on the stack; update ExecuteAsync<T> (the method
and its use of the local variable scope) to ensure scope.Dispose() is always
called: either refactor to an async/await implementation that wraps the awaited
func() in a try/finally and calls scope.Complete()/scope.Fail() then
scope.Dispose() in the finally, or keep ContinueWith but invoke scope.Dispose()
in both the faulted and success branches (ensuring the original exception is
rethrown unchanged). Ensure the disposal happens after calling
scope.Complete/Fail and that the thrown exception semantics remain the same.
In `@MCPForUnity/Editor/ActionTrace/Core/EventStore.Context.cs`:
- Around line 155-176: The current grouping creates mappingBySequence with
GroupBy(...).ToDictionary(... FirstOrDefault()) which discards extra context
mappings for the same EventSequence; update the grouping logic so
mappingBySequence stores all mappings per sequence (e.g., ToDictionary(g =>
g.Key, g => g.ToList()) or similar) and then change the selection that builds
results (the Select over eventsSnapshot) to either emit one tuple per mapping
(SelectMany producing (Event, Context) for each mapping) or aggregate the
mappings into a collection on the Context field so all contexts added via
AddContextMapping are preserved; update references to mappingBySequence,
mappingsSnapshot, and the Select/SelectMany that constructs results accordingly.
In `@MCPForUnity/Editor/ActionTrace/Core/EventStore.Merging.cs`:
- Around line 99-105: The merge_count extraction in EventStore.Merging.cs
currently casts existingCount with (int) which will throw for deserialized
longs/doubles; update the logic in the block that reads from
_lastRecordedEvent.Payload and writes to newPayload to safely convert numeric or
string types to int (e.g., check types: if existingCount is int/long/double use
Convert.ToInt32 or cast from long after bounds check, or if string attempt
int.TryParse, and default to 1 on failure) so mergeCount increments robustly
without throwing during deserialization.
In `@MCPForUnity/Editor/ActionTrace/Core/SamplingMode.cs`:
- Around line 1-20: There are duplicate SamplingMode enums; remove the duplicate
enum declaration from the Capture namespace (the one inside
SamplingMiddleware.cs), update SamplingMiddleware to use the single canonical
enum in Core (SamplingMode) by adding using MCPForUnity.Editor.ActionTrace.Core;
and update any types that referenced the removed local enum (e.g., references in
SamplingMiddleware) to reference the Core versions used by EventMetadata and
EventTypes so all code uses the single SamplingMode definition.
In `@MCPForUnity/Editor/ActionTrace/Descriptors/IEventDescriptor.cs`:
- Around line 52-84: GetString and GetTargetName must guard against a null
EditorEvent.Payload and null values inside the payload to avoid
NullReferenceException; update GetString(EditorEvent evt, string key, string
defaultValue = "") to first check evt and evt.Payload for null and return
defaultValue if missing, then use TryGetValue and return value?.ToString() ??
defaultValue; update GetTargetName(EditorEvent evt) to return early if evt or
evt.Payload is null (fallback to evt?.TargetId ?? ""), and for each TryGetValue
check the out value for null before calling ToString(), falling back to the next
key and finally to evt?.TargetId ?? "".
In `@MCPForUnity/Editor/ActionTrace/Helpers/GameObjectTrackingHelper.cs`:
- Around line 28-29: The static MainThreadId is captured during type
initialization which can occur on a background thread; change it to be lazily
captured on first use in the main-thread check to avoid wrong values. Replace
the current readonly field initialization with a mutable backing field in
GameObjectTrackingHelper (e.g., int _mainThreadId = 0) and add a small helper
(e.g., EnsureMainThreadIdCaptured or a MainThreadId getter) that uses
thread-safe initialization (Interlocked.CompareExchange or Lazy<int>) to set the
backing field to Thread.CurrentThread.ManagedThreadId if still zero, and update
all places that reference MainThreadId to call that helper so the main thread id
is recorded when the real main thread first performs checks.
In `@MCPForUnity/Editor/ActionTrace/Query/EventQueryBuilder.cs`:
- Around line 369-387: The Execute() method calls EventStore.Query(int.MaxValue)
which triggers overflow and huge memory allocation; change Execute() to stop
passing int.MaxValue and instead call a new EventStore.QueryAll() (or a
Query(streaming:true) overload) so the store can handle streaming/pre-filtering,
or pass a sensible upper bound computed from _limit/_offset; update EventStore
to add QueryAll() that safely enumerates all events (or streams results) and
ensure Execute() continues to use MatchesFilters, ApplySorting, _offset and
_limit on the returned sequence rather than forcing full in-memory load.
In `@MCPForUnity/Editor/ActionTrace/Query/EventSummarizer.cs`:
- Around line 525-542: GetTargetName should guard against null values in
evt.Payload entries before calling ToString; update the method (GetTargetName in
EventSummarizer) to check that evt.Payload is non-null and that each TryGetValue
returned object is not null (or use a safe conversion) before returning its
string, otherwise continue to the next key and ultimately fall back to
evt.TargetId; ensure checks cover keys "name", "game_object", "scene_name",
"component_type", and "path" to avoid NullReferenceException from ToString().
- Around line 99-113: The placeholder replacement loop in EventSummarizer (the
block using var sb = new StringBuilder(result) and iterating evt.Payload) can
spin forever when a replacement value contains the same placeholder text;
replace the manual while loop with a single StringBuilder.Replace call per
placeholder (or if you prefer to keep the loop, advance the index by the
inserted value length after insertion) so the code uses sb.Replace(placeholder,
value) (or increments index by value.Length) when handling each kvp and calls
FormatValue(kvp.Value) once per replacement to avoid infinite matching.
In `@MCPForUnity/Editor/Hooks/UnityEventHooks.cs`:
- Around line 103-121: TrackGameObjectChanges currently calls
DetectGameObjectChanges which updates _previousInstanceIds before
GetDestroyedInstanceIds runs, preventing destroy detection; change the logic to
capture the current snapshot once, compare it to the existing
_previousInstanceIds to compute both created and destroyed sets in the same
pass, invoke HookRegistry.NotifyGameObjectCreated(change.obj) for newly created
GameObjects (using the created IDs to resolve objects) and Invoke
HookRegistry.NotifyGameObjectDestroyed(null) for each destroyed ID, then update
_previousInstanceIds to the current snapshot; adjust or replace
DetectGameObjectChanges/GetDestroyedInstanceIds accordingly so no intermediate
update to _previousInstanceIds happens before destroy detection.
- Around line 158-162: OnPlayModeStateChanged currently notifies for every
PlayModeStateChange (including transitional states); change it so it only emits
when the state is a final state: call HookRegistry.NotifyPlayModeChanged(true)
only when state == PlayModeStateChange.EnteredPlayMode and call
HookRegistry.NotifyPlayModeChanged(false) only when state ==
PlayModeStateChange.EnteredEditMode, and do nothing (return) for all other
states; locate the logic in the OnPlayModeStateChanged method and update the
conditional accordingly.
In `@MCPForUnity/Editor/Tools/ManageAsset.cs`:
- Around line 31-38: The event OnAssetModified currently emits the raw
properties object (new values only) but the docstring promises a
changesDictionary of {old,new} pairs; update the code that raises
OnAssetModified to compute a true diff: for each property path in the incoming
properties compare against the asset's current value, build a dictionary mapping
propertyPath -> { old = previousValue, new = newValue } (include only entries
where old != new), and pass that dictionary to OnAssetModified; ensure the event
payload matches the documentation (IReadOnlyDictionary with objects containing
old and new) and reuse the existing properties variable names when performing
the comparisons so consumers of OnAssetModified receive the described diff.
In `@MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs`:
- Around line 283-286: SetSortMode currently mutates the global
ActionTraceSettings.Instance.Filtering.BypassImportanceFilter unconditionally
(same as CreateGUI), causing unwanted global side effects; change SetSortMode to
stop modifying the global setting and instead maintain mode-specific behavior
locally (e.g., a private field on ActionTraceEditorWindow like currentSortMode
or bypassImportanceFilter) and use that local flag when applying filters during
queries/rendering, or only set BypassImportanceFilter on the global settings
when absolutely required for a global change; update any calls that read
ActionTraceSettings.Instance.Filtering.BypassImportanceFilter to consult the
local mode flag in SetSortMode/OnQuery instead.
- Around line 119-122: The window currently mutates the global
ActionTraceSettings by setting
ActionTraceSettings.Instance.Filtering.BypassImportanceFilter = true during
initialization; instead preserve and restore the original value or avoid
mutating the global state. In ActionTraceEditorWindow, capture the prior value
into a private field (e.g. _prevBypassImportanceFilter) when you set it, and
restore that value in OnDisable or OnDestroy; alternatively, replace usage with
a local filtering flag inside ActionTraceEditorWindow so you never touch
ActionTraceSettings.Instance.Filtering.BypassImportanceFilter. Ensure the
restore happens unconditionally to avoid leaving the global flag altered if the
window closes.
In `@MCPForUnity/Editor/Windows/ActionTraceEditorWindow.uss`:
- Around line 119-124: The .detail-placeholder USS rule uses the standard CSS
property text-align which Unity USS ignores; update the rule for
.detail-placeholder to remove text-align and use Unity's proprietary
-unity-text-align with the same value (e.g., -unity-text-align: middle-center)
so the text alignment works in Unity UI; keep the other properties (color,
font-size, padding-top) unchanged.
🟡 Minor comments (23)
Server/src/services/tools/add_action_trace_note.py-144-151 (1)
144-151: Handle MCPResponse objects explicitly to preserve error structure.When
async_send_command_with_retryfails, it returns anMCPResponseobject with structured fields (success,error,hint). Converting it to a string loses this information. The same issue exists inmanage_asset.pyandmanage_editor.py.Use the established pattern from integration tests and HTTP transport handlers:
Proposed fix
- return response if isinstance(response, dict) else {"success": False, "message": str(response)} + if isinstance(response, dict): + return response + if hasattr(response, 'model_dump'): + return response.model_dump() + return {"success": False, "message": str(response)}MCPForUnity/Editor/ActionTrace/VCS/VcsContextProvider.cs-163-169 (1)
163-169: Remove unused variablegitPath.The variable
gitPathon line 168 is declared but never used in this method.try { var projectPath = System.IO.Path.GetDirectoryName(UnityEngine.Application.dataPath); - var gitPath = System.IO.Path.Combine(projectPath, ".git"); // Find git executable string gitExe = FindGitExecutable();MCPForUnity/Editor/ActionTrace/Semantics/DefaultEventScorer.cs-34-51 (1)
34-51: Potential logic inconsistency: AINote with null payload returns 0.1 instead of 1.0.The comment states "AINote is always critical" (line 44), but the null-payload check (line 37-38) returns early with 0.1f before reaching the AINote check. If a dehydrated AINote event is possible, it would score 0.1 instead of 1.0.
If AINote should truly always be critical regardless of payload state, move the AINote check before the null check:
💡 Suggested reordering
public float Score(EditorEvent evt) { + // Special case: AINote is always critical + if (evt.Type == "AINote") + return 1.0f; + // Dehydrated events (Payload is null) use low default score if (evt.Payload == null) return 0.1f; // Get base score from metadata var meta = EventTypes.Metadata.Get(evt.Type); float baseScore = meta.DefaultImportance; - // Special case: AINote is always critical - if (evt.Type == "AINote") - return 1.0f; - // Apply payload-based adjustments for asset events float adjustment = GetPayloadAdjustment(evt); return Mathf.Clamp01(baseScore + adjustment); }MCPForUnity/Editor/ActionTrace/VCS/VcsContextProvider.cs-64-76 (1)
64-76: Thread-safety claim in documentation is not accurate.The comment states "Thread-safe (called from any thread during event recording)" but the implementation has no synchronization:
_currentContextcan be read and written concurrently fromGetCurrentContext()andRefreshContext().- The null-check-then-assign pattern (lines 70-73) is a classic race condition.
If this is only called from Unity's main thread (which is typical for Editor code), update the documentation to reflect that:
/// <summary> /// Gets the current VCS context for event injection. -/// Thread-safe (called from any thread during event recording). +/// Must be called from the main thread. /// </summary>Alternatively, if true thread-safety is needed, consider using
Volatile.Read/Volatile.Writeor a lock.MCPForUnity/Editor/ActionTrace/Helpers/ActionTraceHelper.cs-28-47 (1)
28-47: Documentation examples don't match actual behavior.The regex only capitalizes the first letter after each underscore. For example:
"manage_gameobject"→"Manage Gameobject"(not"Manage GameObject"as documented)"add_ActionTrace_note"→"Add ActionTrace Note"(this one works because "ActionTrace" already has capital letters)The current implementation is reasonable for snake_case conversion, but the doc examples are misleading.
📝 Fix documentation
/// Examples: - /// - "manage_gameobject" → "Manage GameObject" + /// - "manage_gameobject" → "Manage Gameobject" /// - "add_ActionTrace_note" → "Add ActionTrace Note" /// - "get_ActionTrace" → "Get ActionTrace"MCPForUnity/Editor/Helpers/GlobalIdHelper.cs-131-142 (1)
131-142:GameObject.Findmay resolve to wrong object in multi-scene setup.
GameObject.Find(hierarchyPath)searches all loaded scenes globally, not just the scene specified inscenePath. If multiple scenes are loaded with identically-named objects at the same hierarchy path, this could return the wrong object.Consider searching within the specific scene's root objects:
Proposed scene-scoped resolution
// Find GameObject by hierarchy path - var found = GameObject.Find(hierarchyPath); - return found; + // Search within the specific scene + foreach (var root in scene.GetRootGameObjects()) + { + if (hierarchyPath.StartsWith(root.name)) + { + // If path is just the root name + if (hierarchyPath == root.name) + return root; + + // Otherwise find child by relative path + string relativePath = hierarchyPath.Substring(root.name.Length + 1); + var found = root.transform.Find(relativePath); + if (found != null) + return found.gameObject; + } + } + return null;MCPForUnity/Editor/Hooks/HookTest.cs-1-84 (1)
1-84: Entirely commented-out file should be removed or converted.This file contains only commented-out code, which creates noise in the codebase. Consider one of these alternatives:
- Remove the file – If it's not needed, don't commit it
- Convert to actual tests – Place in a test project with proper test framework assertions
- Use conditional compilation – Wrap with
#if DEBUG_HOOK_EVENTSif needed for debugging- Create documentation – If serving as an example, a markdown file or XML doc would be clearer
MCPForUnity/Editor/ActionTrace/Semantics/DefaultIntentInferrer.cs-62-63 (1)
62-63: Batch detection should key off the current event type.Line 155 uses the first surrounding event’s type; if it differs from the current event, batch inference may be missed.
🛠️ Suggested update
- EventTypes.HierarchyChanged when IsBatchOperation(surrounding) => "Batch Operation", + EventTypes.HierarchyChanged when IsBatchOperation(evt, surrounding) => "Batch Operation", @@ - private static bool IsBatchOperation(IReadOnlyList<EditorEvent> surrounding) + private static bool IsBatchOperation(EditorEvent evt, IReadOnlyList<EditorEvent> surrounding) { // Many events of the same type suggest a batch operation if (surrounding.Count < 5) return false; - var firstType = surrounding[0].Type; + var targetType = evt.Type; int sameTypeCount = 0; foreach (var e in surrounding) { - if (e.Type == firstType) sameTypeCount++; + if (e.Type == targetType) sameTypeCount++; } return sameTypeCount >= 5; }Also applies to: 150-161
MCPForUnity/Editor/ActionTrace/Core/EventStore.Diagnostics.cs-16-27 (1)
16-27: Guard against negative hotEventCount to avoid out-of-range indexing.If settings are misconfigured,
_events.Count - hotEventCountcan exceed the list bounds.🛠️ Suggested update
private static void DehydrateOldEvents(int hotEventCount) { + hotEventCount = Math.Max(0, hotEventCount); // Find events that need dehydration (not already dehydrated and beyond hot count) for (int i = 0; i < _events.Count - hotEventCount; i++) { var evt = _events[i];MCPForUnity/Editor/ActionTrace/Semantics/DefaultIntentInferrer.cs-70-115 (1)
70-115: Make extension checks case-insensitive to avoid missed intent inference.Line 72+ compares extensions with case-sensitive equality; uppercase extensions (e.g., “.PNG”) won’t match.
🛠️ Suggested update
- private static bool IsScript(EditorEvent e) - { - if (e.Payload.TryGetValue("extension", out var ext)) - return ext.ToString() == ".cs"; - if (e.Payload.TryGetValue("component_type", out var type)) - return type.ToString()?.Contains("MonoBehaviour") == true; - return false; - } + private static bool HasExtension(EditorEvent e, params string[] extensions) + { + if (!e.Payload.TryGetValue("extension", out var ext) || ext == null) return false; + var extStr = ext.ToString(); + foreach (var candidate in extensions) + { + if (string.Equals(extStr, candidate, StringComparison.OrdinalIgnoreCase)) + return true; + } + return false; + } + + private static bool IsScript(EditorEvent e) + { + if (HasExtension(e, ".cs")) + return true; + if (e.Payload.TryGetValue("component_type", out var type)) + return type.ToString()?.Contains("MonoBehaviour") == true; + return false; + } @@ - private static bool IsScene(EditorEvent e) - { - if (e.Payload.TryGetValue("extension", out var ext)) - return ext.ToString() == ".unity"; - return false; - } + private static bool IsScene(EditorEvent e) + { + return HasExtension(e, ".unity"); + } @@ - private static bool IsPrefab(EditorEvent e) - { - if (e.Payload.TryGetValue("extension", out var ext)) - return ext.ToString() == ".prefab"; - return false; - } + private static bool IsPrefab(EditorEvent e) + { + return HasExtension(e, ".prefab"); + } @@ - private static bool IsTexture(EditorEvent e) - { - if (e.Payload.TryGetValue("extension", out var ext)) - { - var extStr = ext.ToString(); - return extStr == ".png" || extStr == ".jpg" || extStr == ".jpeg" || - extStr == ".psd" || extStr == ".tga" || extStr == ".exr"; - } - if (e.Payload.TryGetValue("type", out var type)) - return type.ToString()?.Contains("Texture") == true; - return false; - } + private static bool IsTexture(EditorEvent e) + { + if (HasExtension(e, ".png", ".jpg", ".jpeg", ".psd", ".tga", ".exr")) + return true; + if (e.Payload.TryGetValue("type", out var type)) + return type.ToString()?.Contains("Texture") == true; + return false; + } @@ - private static bool IsAudio(EditorEvent e) - { - if (e.Payload.TryGetValue("extension", out var ext)) - { - var extStr = ext.ToString(); - return extStr == ".wav" || extStr == ".mp3" || extStr == ".ogg" || - extStr == ".aif" || extStr == ".aiff"; - } - return false; - } + private static bool IsAudio(EditorEvent e) + { + if (HasExtension(e, ".wav", ".mp3", ".ogg", ".aif", ".aiff")) + return true; + return false; + }MCPForUnity/Editor/ActionTrace/Capture/ActionTraceRecorder.cs-64-69 (1)
64-69: Skip recording when GameObject is null to prevent collapsing distinct deletions.The current code records all destroyed GameObjects with null references as
Instance:0, which collapses multiple distinct deletions into a single target ID. SinceUnityEventHooksdetects destroyed objects after they're already destroyed (passing null to the handler), adopt the same pattern used inOnGameObjectCreated: skip recording whengois null.Suggested fix
- // GameObject might already be destroyed - int instanceId = go != null ? go.GetInstanceID() : 0; - _actionTrace.RecordGameObjectDestroyed(instanceId); + if (go == null) return; + _actionTrace.RecordGameObjectDestroyed(go.GetInstanceID());MCPForUnity/Editor/Tools/GetActionTraceTool.cs-20-75 (1)
20-75: Remove unused parameters or implement event type filtering.
ParametersexposesEventTypesandIncludePayload, butActionTraceViewResource.HandleCommandhas no getter functions for these fields and does not parse them. These parameters are silently ignored, breaking the tool's contract. Either implement filtering by event types and payload control, or remove these parameters.MCPForUnity/Editor/ActionTrace/Capture/AssetChangePostprocessor.cs-135-148 (1)
135-148: Placeholder implementation with incorrect logic and empty catch.
AssetDatabase.LoadMainAssetAtPath(metaPath)won't work -.metafiles aren't Unity assets- The function always returns
false, soAssetCreatedis never emitted- The empty
catchswallows all exceptions silentlyIf distinguishing created vs imported isn't feasible now, consider removing the
AssetCreatedevent type or using a heuristic (e.g., checking file creation timestamp viaSystem.IO.File.GetCreationTime).MCPForUnity/Editor/ActionTrace/Core/EventStore.cs-240-260 (1)
240-260: Clearing sequence counter may cause sequence reuse.Setting
_sequenceCounter = 0at line 246 means new events afterClear()will reuse sequence numbers from before the clear. If any external system (e.g., an AI agent, logs, or persisted state) cached old sequence numbers forsince_sequencequeries, the results could be confusing or incorrect.Consider:
- Not resetting the counter (sequences continue incrementing)
- Starting from
DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()as an epoch baseMCPForUnity/Editor/ActionTrace/Core/EventStore.Persistence.cs-78-87 (1)
78-87: Potential race condition when clearing_saveScheduledoutside lock.
_saveScheduledis read and written inside_queryLockinScheduleSave(lines 43-50, 56-58), but here at line 85 it's set tofalseoutside the lock. This could cause a race ifScheduleSaveis called concurrently.🔧 Suggested fix
public static void ClearPendingOperations() { lock (_pendingNotifications) { _pendingNotifications.Clear(); _notifyScheduled = false; } - _saveScheduled = false; + lock (_queryLock) + { + _saveScheduled = false; + } _lastDehydratedCount = -1; // Reset dehydration optimization marker }MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs-250-251 (1)
250-251: Menu separator creates a clickable empty item.
AppendAction("/", ...)creates an action labeled "/" rather than a visual separator. Unity'sDropdownMenuusesAppendSeparator()for separators.🔧 Suggested fix
_filterMenu?.menu.AppendAction("All", a => SetImportance(0f)); - _filterMenu?.menu.AppendAction("/", a => { }); + _filterMenu?.menu.AppendSeparator(); _filterMenu?.menu.AppendAction("AI Can See", a => SetImportanceFromSettings());MCPForUnity/Editor/ActionTrace/Core/EventStore.Persistence.cs-174-179 (1)
174-179: Index-based trimming of context mappings should use EventSequence filtering instead.The code at lines 174-179 removes mappings by index position (
RemoveRange(0, removeCount)), which assumes mappings are strictly ordered by EventSequence. While a cascade delete usingRemoveAll(m => removedSequences.Contains(m.EventSequence))already executes at line 168, the subsequent index-based trim is fragile and inconsistent with the safer removal pattern used elsewhere in the codebase (e.g., EventStore.cs line 144).Replace the index-based trim with a sequence-based approach to ensure orphaned mappings are properly removed regardless of insertion order:
if (_contextMappings.Count > maxContextMappings) { var mappingsToKeep = _contextMappings .OrderByDescending(m => m.EventSequence) .Take(maxContextMappings) .Select(m => m.EventSequence) .ToHashSet(); _contextMappings.RemoveAll(m => !mappingsToKeep.Contains(m.EventSequence)); }MCPForUnity/Editor/ActionTrace/Capture/ActionTraceEventEmitter.cs-289-306 (1)
289-306: TwoEmitAssetDeletedoverloads emit same event type with inconsistent payloads.
EmitAssetDeleted(string assetPath)(Line 289): Noasset_type, nosourcefieldEmitAssetDeleted(string assetPath, string assetType)(Line 462): Includesasset_typeandsource = "mcp_tool"Consumers querying
AssetDeletedevents may receive inconsistent payloads. Consider:
- Adding
asset_typeto the first overload (viaDetectAssetType)- Renaming the second overload to distinguish MCP-originated deletions
🔧 Suggested fix for consistency
public static void EmitAssetDeleted(string assetPath) { if (string.IsNullOrEmpty(assetPath)) { McpLog.Warn("[ActionTraceEventEmitter] Attempted to emit AssetDeleted with null or empty path"); return; } string targetId = $"Asset:{assetPath}"; var payload = new Dictionary<string, object> { ["path"] = assetPath, - ["extension"] = System.IO.Path.GetExtension(assetPath) + ["extension"] = System.IO.Path.GetExtension(assetPath), + ["asset_type"] = DetectAssetType(assetPath) }; EmitEvent(EventTypes.AssetDeleted, targetId, payload); }Also applies to: 462-480
MCPForUnity/Editor/ActionTrace/Capture/ActionTraceEventEmitter.cs-514-514 (1)
514-514: UseToLowerInvariant()instead ofToLower()for culture-invariant comparison.
ToLower()is culture-sensitive and could produce unexpected results in certain locales (e.g., Turkish "I" handling). File extensions should use invariant comparison.🐛 Proposed fix
-var extension = System.IO.Path.GetExtension(assetPath).ToLower(); +var extension = System.IO.Path.GetExtension(assetPath).ToLowerInvariant();Server/src/services/resources/action_trace.py-170-177 (1)
170-177: Silent exception swallowing loses debugging information.Unlike the similar block in
get_action_trace(which logs viactx.error), this loop silently discards parsing failures. This makes debugging production issues harder.🐛 Proposed fix to add logging
for event_dict in events_data: try: event = EditorEvent(**event_dict) events.append(event) - except Exception: + except (TypeError, ValueError, KeyError) as e: + # Log but continue - statistics should still work with partial data + import logging + logging.warning(f"Failed to parse event for statistics: {e}") continueMCPForUnity/Editor/ActionTrace/Core/EventTypes.cs-98-103 (1)
98-103: DefineAINoteas a public const like all other event types.All other event types are defined as
public const string(lines 31–71), butAINoteis hardcoded as a string literal at line 98. The file's own checklist (lines 4–16) explicitly states step 1 is "Add event constant."♻️ Suggested fix
Add the constant with the other event types:
// Build events public const string BuildStarted = "BuildStarted"; public const string BuildCompleted = "BuildCompleted"; public const string BuildFailed = "BuildFailed"; + +// AI events +public const string AINote = "AINote";Then update the metadata dictionary:
-["AINote"] = new EventMetadata +[AINote] = new EventMetadataMCPForUnity/Editor/ActionTrace/Core/ActionTraceSettings.cs-266-272 (1)
266-272: Memory estimation can produce incorrect results ifHotEventCount > MaxEvents.If a user configures
HotEventCountgreater thanMaxEvents(whichValidate()warns about but doesn't prevent),coldEventsbecomes negative, resulting in an underestimated memory usage.Clamp coldEvents to non-negative
public long GetEstimatedMemoryUsage() { // Approximate: each event ~300 bytes when hydrated, ~100 bytes when dehydrated int hotEvents = Storage.HotEventCount; - int coldEvents = Storage.MaxEvents - Storage.HotEventCount; + int coldEvents = Math.Max(0, Storage.MaxEvents - Storage.HotEventCount); return (long)(hotEvents * 300 + coldEvents * 100); }MCPForUnity/Editor/ActionTrace/Core/EditorEvent.cs-162-170 (1)
162-170: Race condition in lazy summary computation.
GetSummary()mutatesPrecomputedSummarywithout synchronization. If called concurrently from multiple threads, this could result in redundant computation or inconsistent state reads.While the class is described as "immutable," this lazy initialization pattern breaks that guarantee.
Consider using Lazy<T> or lock for thread-safe lazy evaluation
+ private readonly object _summaryLock = new object(); + public string GetSummary() { if (PrecomputedSummary != null) return PrecomputedSummary; - // Compute and cache (this mutates the object, but it's just a string field) - PrecomputedSummary = ComputeSummary(); - return PrecomputedSummary; + lock (_summaryLock) + { + if (PrecomputedSummary != null) + return PrecomputedSummary; + PrecomputedSummary = ComputeSummary(); + return PrecomputedSummary; + } }Alternatively, use
Interlocked.CompareExchangefor lock-free approach ifComputeSummary()is idempotent.
MCPForUnity/Editor/ActionTrace/Capture/AssetChangePostprocessor.cs
Outdated
Show resolved
Hide resolved
Thread Safety (3 fixes): - Add exception handling to all HookRegistry Notify* methods Prevents subscriber errors from breaking invocation chain - Use foreach with GetInvocationList() for safe delegate iteration - Log warnings without breaking notification pipeline Memory Management (4 fixes): - Add MaxPendingNotifications limit (256) to EventStore Prevents unbounded notification buffer growth - Add save throttling (MinSaveIntervalMs = 1000ms) to EventStore.Persistence Prevents excessive disk writes during rapid event recording - Existing dehydration already optimal (hot/cold event split) Performance (5 fixes): - Add score caching (_scoreCache) to EventQueryBuilder Avoids repeated semantic analysis scoring of same events - Add rule caching (_cachedActiveRules) to EventFilterSettings Avoids repeated sorting of filter rules - Existing sampling strategies already optimal Design (8 fixes): - Improve filter performance with lazy cache invalidation - Ensure consistent exception handling across recording pipeline - Maintain backward compatibility with existing HookRegistry API This commit completes all P1 priority issues from the ActionTrace refactoring. The system now has better resilience, memory efficiency, and query performance for production use. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 13
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/Analysis/Context/ContextCompressor.cs`:
- Around line 420-433: The Deduplicate extension method can divide by zero when
windowMs <= 0; update Deduplicate to validate windowMs (e.g., if windowMs <= 0)
either throw an ArgumentOutOfRangeException or normalize it to a sensible
default (like 100 or 1) before computing string key =
$"{evt.Type}|{evt.TargetId}|{evt.TimestampUnixMs / windowMs}"; ensure the check
happens at the start of Deduplicate so all subsequent uses of windowMs are safe.
- Around line 179-183: The early return in CompressWithDetails produces a
CompressedContext with null list properties causing NREs; change that return to
initialize PreservedEvents, DehydratedEvents, and SummaryEvents as empty
collections and set OriginalCount = 0 (i.e., construct a CompressedContext with
its list properties assigned empty lists) so consumers can safely iterate;
locate this in the CompressWithDetails method and update the returned
CompressedContext instance accordingly.
In `@MCPForUnity/Editor/ActionTrace/Analysis/Query/EventQueryBuilder.cs`:
- Around line 371-449: Execute() uses CalculateEffectiveLimit() heuristics that
may under-fetch events for a given _timeRange; modify Execute() to verify
coverage when _timeRange.HasValue by checking the oldest event returned (e.g.,
examine timestamps on the last item of allEvents after
EventStore.Query(effectiveLimit)) and if that event's timestamp is newer than
_timeRange.Value.StartMs, iteratively increase the queried limit (or immediately
fall back to EventStore.QueryAll()) until the oldest fetched event is
older-or-equal to StartMs or a safe max threshold is reached; update
CalculateEffectiveLimit/Execute to perform this requery loop (using
EventStore.Query and EventStore.QueryAll as fallbacks) so filtering by time is
exact instead of silently dropping earlier events.
In
`@MCPForUnity/Editor/ActionTrace/Capture/CapturePoints/EventCaptureRegistry.cs`:
- Around line 41-49: The Unregister method can leave the registry inconsistent
if point.Shutdown() throws; wrap the Shutdown() call in a try/catch (or
try/finally) so that _capturePoints.Remove(point) always runs even on error, and
log the exception (e.g., via UnityEngine.Debug.LogException or your project's
logger) including the capturePointId and context; ensure the method still
returns true for successful removal and avoid swallowing errors silently
(rethrow only if caller must know), referencing Unregister, _capturePoints,
Shutdown(), and _isInitialized to locate and update the code.
In `@MCPForUnity/Editor/ActionTrace/Capture/Capturers/AssetCapture.cs`:
- Around line 344-347: VcsContextProvider.GetCurrentContext() can return null
but the code calls ToDictionary() unconditionally; update the block around
VcsContextProvider.GetCurrentContext(), payload["vcs_context"], and
ToDictionary() to guard against null by checking the returned context and only
calling ToDictionary() when non-null (or assign a safe default such as null or
an empty dictionary into payload["vcs_context"] if the context is null) so no
NullReferenceException is possible.
In `@MCPForUnity/Editor/ActionTrace/Capture/Capturers/UndoCapture.cs`:
- Around line 51-69: Guard against nested BeginToolCall by checking the
_isInToolCall flag at the start of BeginToolCall; if a tool call is already
active, log a warning (including _currentToolName and _currentToolCallId),
safely close the existing group by invoking EndToolCall (so
_currentUndoGroupStart is collapsed correctly) or otherwise finalize the prior
undo group before proceeding, then create the new undo group via
Undo.SetCurrentGroupName and Undo.GetCurrentGroup and update
_currentToolName/_currentToolCallId/_isInToolCall; ensure no silent overwrites
of _currentUndoGroupStart occur.
In `@MCPForUnity/Editor/ActionTrace/Capture/Recorder.cs`:
- Around line 28-47: The static constructor of ActionTraceRecorder subscribes
many HookRegistry events but never unsubscribes, causing stale delegates across
domain reloads; add a static cleanup handler that unsubscribes all the same
HookRegistry events (the ones added in the static constructor: OnComponentAdded,
OnComponentRemoved, OnComponentRemovedDetailed, OnGameObjectCreated,
OnGameObjectDestroyedDetailed, OnSelectionChanged, OnHierarchyChanged,
OnPlayModeChanged, OnSceneSaved, OnSceneOpenedDetailed,
OnNewSceneCreatedDetailed, OnScriptCompiledDetailed,
OnScriptCompilationFailedDetailed, OnBuildCompletedDetailed) and register that
cleanup to Unity's domain-reload event (e.g.,
AssemblyReloadEvents.beforeAssemblyReload) from the static constructor so
UnsubscribeFromHookRegistry is invoked on reload and prevents stale delegates.
In `@MCPForUnity/Editor/ActionTrace/Capture/Sampling/SamplingMiddleware.cs`:
- Around line 124-133: In SamplingMiddleware (the asset-event handling block),
guard against a null evt.Payload before calling evt.Payload.TryGetValue to avoid
throwing on asset events; update the logic that sets assetPath (currently using
evt.TargetId and evt.Payload.TryGetValue("path", ...)) to first check
evt.Payload != null (or use a null-conditional) and only call TryGetValue when
payload exists, leaving assetPath as TargetId or null otherwise so the sampling
pipeline doesn't hard-crash on asset events.
- Around line 257-266: The CleanupExpiredSamples function currently uses a fixed
CleanupAgeMs to drop pending entries, which can remove samples before a
strategy's WindowMs has elapsed; change the expiration logic in
CleanupExpiredSamples to compute each sample's expiry using its strategy's
configured WindowMs (from the sample/strategy metadata stored in
_pendingSamples) and only remove when nowMs - kvp.Value.TimestampMs >
kvp.Value.Strategy.WindowMs (or the strategy-specific max), and when removing
due to window expiry ensure you record/emit the expired debounce sample (so
debounce handlers see the late event) rather than silently TryRemove; update
references to CleanupAgeMs so it becomes a fallback or max cap if needed.
In `@MCPForUnity/Editor/ActionTrace/Context/ToolCallScope.cs`:
- Around line 494-503: ExecuteAsync is calling func() directly so if func()
throws synchronously the ToolCallScope (variable scope) is never completed or
disposed; wrap the func() invocation in a try/catch that catches synchronous
exceptions, call the appropriate scope cleanup/completion (e.g.,
scope.Complete/fail and scope.Dispose) before rethrowing the exception, and keep
the existing continuation logic that completes/disposes the scope when the
returned Task finishes; refer to ExecuteAsync<T>, the local variable scope and
the task = func() call to locate where to add the try/catch and cleanup.
In `@MCPForUnity/Editor/ActionTrace/Core/Store/EventStore.Diagnostics.cs`:
- Around line 17-29: DehydrateOldEvents mutates the shared _events list without
synchronization; wrap the body (the for-loop that reads and assigns _events[i])
in a lock on _queryLock to prevent races with readers like GetMemoryDiagnostics,
i.e. acquire _queryLock before iterating and releasing after, so every access
that mutates or reads _events is protected.
In `@MCPForUnity/Editor/Tools/UndoToSequenceTool.cs`:
- Around line 80-96: The code currently only compares targetSequence to
EventStore.CurrentSequence and fails to detect if the target is older than the
oldest retained event; call the EventStore API that exposes the oldest retained
sequence (e.g., EventStore.OldestSequence or EventStore.GetOldestSequence())
before querying, and if targetSequence < oldest return an ErrorResponse stating
the target has been trimmed and include the oldest and current sequences; then
proceed to call EventStore.Query (as in the existing code) only when
targetSequence is within the stored window to avoid under‑counting undo steps.
In `@Server/src/models/action_trace.py`:
- Around line 112-124: The TransactionInfo model's triggered_by_tool field is
typed incorrectly as Optional[bool]; change its type to Optional[str] and update
the Field description accordingly so it matches the C#
AtomicOperation.TriggeredByTool (which contains the tool name string, e.g.,
"manage_gameobject"); modify the TransactionInfo class's triggered_by_tool
declaration to accept a string (Optional[str]) and ensure any
serialization/deserialization expectations reflect a string value rather than a
boolean.
♻️ Duplicate comments (1)
MCPForUnity/Editor/ActionTrace/Helpers/GameObjectTrackingHelper.cs (1)
28-55: Main thread ID capture issue has been properly addressed.The lazy initialization pattern with
[InitializeOnLoadMethod]correctly ensures the main thread ID is captured during Unity's editor initialization rather than during static type initialization. This resolves the previous concern about incorrect thread ID capture.
🟡 Minor comments (18)
MCPForUnity/Editor/Tools/ManageAsset.cs-292-294 (1)
292-294: Use actual created asset type forOnAssetCreatedto keep payloads consistent.
OnAssetCreatedcurrently emits the user‑suppliedassetType, while modify/delete emit the runtime type. This can produce inconsistent payloads for consumers.🛠️ Suggested fix
- OnAssetCreated?.Invoke(fullPath, assetType); + var createdType = + newAsset != null + ? newAsset.GetType().FullName + : AssetDatabase.GetMainAssetTypeAtPath(fullPath)?.FullName ?? assetType; + OnAssetCreated?.Invoke(fullPath, createdType);Also applies to: 498-503, 539-548
MCPForUnity/Editor/ActionTrace/Capture/CapturePoints/EventCapturePointAttribute.cs-16-20 (1)
16-20: Validate capture point Id to avoid null/empty registry keys.If a capture point is declared with a null/whitespace
id, discovery/registration can fail later (e.g., dictionary key errors). Guard early and normalize the description.🛠️ Suggested fix
public EventCapturePointAttribute(string id, string description = null, int priority = 0) { + if (string.IsNullOrWhiteSpace(id)) + throw new ArgumentException("Capture point id must be non-empty.", nameof(id)); Id = id; - Description = description ?? id; + Description = string.IsNullOrWhiteSpace(description) ? id : description; Priority = priority; }MCPForUnity/Editor/ActionTrace/Integration/VCS/VcsContextProvider.cs-149-157 (1)
149-157: Detect both.gitdirectory and file for worktree/submodule support.Git worktrees store
.gitas a file pointing to the worktree's metadata, not as a directory. The current check using onlyDirectory.Exists()causes false negatives, disabling VCS context for worktrees.🛠️ Suggested fix
- return System.IO.Directory.Exists(gitPath); + return System.IO.Directory.Exists(gitPath) || System.IO.File.Exists(gitPath);MCPForUnity/Editor/ActionTrace/Core/Settings/ActionTraceSettings.cs-267-273 (1)
267-273: Potential negative value whenHotEventCount > MaxEvents.If
HotEventCountexceedsMaxEvents(an invalid state thatValidate()flags but doesn't prevent),coldEventsbecomes negative, producing an incorrect memory estimate.🔧 Proposed fix: Clamp to zero
public long GetEstimatedMemoryUsage() { // Approximate: each event ~300 bytes when hydrated, ~100 bytes when dehydrated int hotEvents = Storage.HotEventCount; - int coldEvents = Storage.MaxEvents - Storage.HotEventCount; + int coldEvents = Math.Max(0, Storage.MaxEvents - Storage.HotEventCount); return (long)(hotEvents * 300 + coldEvents * 100); }MCPForUnity/Editor/ActionTrace/Capture/Filters/EventFilter.cs-123-148 (1)
123-148:DefaultRulescontains mutableFilterRuleinstances.
DefaultRulesisstatic readonly, but theFilterRuleobjects within are mutable. If any code modifies a rule (e.g.,DefaultRules[0].Enabled = false), that change persists globally. Consider makingFilterRuleimmutable or creating defensive copies inGetActiveRules().🔧 Option: Create copies in GetActiveRules
if (EnableDefaultFilters) { foreach (var rule in DefaultRules) { if (rule.Enabled) - rules.Add(rule); + rules.Add(new FilterRule + { + Name = rule.Name, + Enabled = rule.Enabled, + Type = rule.Type, + Pattern = rule.Pattern, + Action = rule.Action, + Priority = rule.Priority + }); } }Note: This adds allocation overhead. Alternatively, document that
DefaultRulesshould not be modified.MCPForUnity/Editor/ActionTrace/Core/Settings/ActionTraceSettings.cs-207-217 (1)
207-217:ApplyPresetis called before the asset exists on disk, causing prematureSave()calls.
CreateSettingscallsApplyPreset(line 211) beforeCreateAsset(line 212). SinceApplyPresetinvokesMarkDirty()andSave(),EditorUtility.SetDirty(this)andAssetDatabase.SaveAssets()are called on an asset that hasn't been created yet, which may silently fail or cause unexpected behavior.🔧 Proposed fix: Reorder to create asset first
private static ActionTraceSettings CreateSettings() { var settings = CreateInstance<ActionTraceSettings>(); - // Apply Standard preset by default - settings.ApplyPreset(ActionTracePreset.Standard); AssetDatabase.CreateAsset(settings, SettingsPath); + // Apply Standard preset by default (after asset exists) + settings.ApplyPreset(ActionTracePreset.Standard); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); McpLog.Info($"[ActionTraceSettings] Created new settings at {SettingsPath}"); return settings; }MCPForUnity/Editor/ActionTrace/Core/EventTypes.cs-98-103 (1)
98-103: Missing constant for"AINote"event type.The
"AINote"string literal is used directly in the metadata dictionary (line 98), but unlike all other event types, it lacks a correspondingpublic const stringdeclaration. This breaks the type-safe pattern established for the rest of the file.🔧 Proposed fix: Add the constant
Add with the other event type constants (around line 70):
// Build events public const string BuildStarted = "BuildStarted"; public const string BuildCompleted = "BuildCompleted"; public const string BuildFailed = "BuildFailed"; + +// AI events +public const string AINote = "AINote";Then update the metadata dictionary entry:
-["AINote"] = new EventMetadata +[AINote] = new EventMetadataMCPForUnity/Editor/ActionTrace/Analysis/Query/EventQueryBuilder.cs-312-327 (1)
312-327: Guard against null/empty search terms.
Line 314+: a null term will causeterm.ToLowerInvariant()to throw inMatchesFilters. Accepting empty strings also bloats the set without adding value.🐛 Proposed fix
public EventQueryBuilder WithSearchTerm(string term) { + if (string.IsNullOrWhiteSpace(term)) return this; _searchTerms ??= new HashSet<string>(StringComparer.OrdinalIgnoreCase); _searchTerms.Add(term); return this; } public EventQueryBuilder WithAllSearchTerms(params string[] terms) { _searchTerms ??= new HashSet<string>(StringComparer.OrdinalIgnoreCase); - foreach (string term in terms) - _searchTerms.Add(term); + foreach (string term in terms) + { + if (string.IsNullOrWhiteSpace(term)) continue; + _searchTerms.Add(term); + } return this; }MCPForUnity/Editor/ActionTrace/Capture/Capturers/AssetCapture.cs-181-232 (1)
181-232: Cache persistence skipped for deleted and moved assets.The
hasChangesflag is only set when processing imported assets (line 142). When a batch contains only deleted or moved assets, the cache entries are added butSaveProcessedAssets()is never called (line 231-232), so changes won't persist across domain reloads.Suggested fix
// ========== Deleted Assets ========== 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; + // L1 Blacklist: Skip junk assets if (!EventFilter.ShouldTrackAsset(assetPath)) continue;Apply similarly to the moved assets section (after line 210-211).
MCPForUnity/Editor/ActionTrace/Capture/Capturers/SelectionCapture.cs-139-170 (1)
139-170: Missing VCS context and undo_group injection.Other capture points (e.g.,
Recorder.RecordEventandAssetChangePostprocessor.RecordEvent) injectvcs_contextandundo_groupinto the payload for consistency and to support theundo_to_sequencefunctionality. This event is missing both.Suggested fix
private static void RecordSelectionPropertyModified(UndoPropertyModification undoMod, UnityEngine.Object target, string targetGlobalId, string propertyPath) { var currentValue = UndoReflectionHelper.GetCurrentValue(undoMod); var prevValue = UndoReflectionHelper.GetPreviousValue(undoMod); var payload = new Dictionary<string, object> { ["target_name"] = target.name, ["component_type"] = target.GetType().Name, ["property_path"] = propertyPath, ["start_value"] = PropertyFormatter.FormatPropertyValue(prevValue), ["end_value"] = PropertyFormatter.FormatPropertyValue(currentValue), ["value_type"] = PropertyFormatter.GetPropertyTypeName(currentValue), ["selection_context"] = new Dictionary<string, object> { ["selection_id"] = _currentSelectionGlobalId, ["selection_name"] = _currentSelectionName, ["selection_type"] = _currentSelectionType, ["selection_path"] = _currentSelectionPath ?? string.Empty - } + }, + ["undo_group"] = Undo.GetCurrentGroup() }; + // Inject VCS context if available + var vcsContext = MCPForUnity.Editor.ActionTrace.Integration.VCS.VcsContextProvider.GetCurrentContext(); + if (vcsContext != null) + { + payload["vcs_context"] = vcsContext.ToDictionary(); + } + var evt = new EditorEvent(MCPForUnity/Editor/ActionTrace/Core/Store/EventStore.Persistence.cs-93-102 (1)
93-102: Inconsistent locking for_saveScheduled.
_saveScheduledis reset on line 100 outside any lock, but it's set on line 62 inside_queryLock. This inconsistency could lead to race conditions betweenScheduleSave()andClearPendingOperations().🔧 Proposed fix
public static void ClearPendingOperations() { lock (_pendingNotifications) { _pendingNotifications.Clear(); _notifyScheduled = false; } + lock (_queryLock) + { - _saveScheduled = false; + _saveScheduled = false; + } _lastDehydratedCount = -1; // Reset dehydration optimization marker }MCPForUnity/Editor/ActionTrace/Capture/Recorder.cs-163-172 (1)
163-172: Semantic mismatch:ExitingPlayModeused forisPlaying=false.When
isPlayingisfalse, the state is set toExitingPlayMode, but according toUnityEventHooks.OnPlayModeStateChanged,NotifyPlayModeChanged(false)is only called when the state isEnteredEditMode. This creates a semantic inconsistency between what the recorder logs and what actually happened.🐛 Proposed fix
private static void OnPlayModeChanged(bool isPlaying) { - var state = isPlaying ? PlayModeStateChange.EnteredPlayMode : PlayModeStateChange.ExitingPlayMode; + var state = isPlaying ? PlayModeStateChange.EnteredPlayMode : PlayModeStateChange.EnteredEditMode; var payload = new Dictionary<string, object> { ["state"] = state.ToString() }; RecordEvent("PlayModeChanged", "Editor", payload); }MCPForUnity/Editor/ActionTrace/Core/Store/EventStore.Diagnostics.cs-51-60 (1)
51-60: Missing null check onevtbefore accessingIsDehydrated.The loop iterates over
_eventsand accessesevt.IsDehydratedwithout a null check. If_eventscan contain null entries (as suggested by the null check inDehydrateOldEvents), this will throw aNullReferenceException.🐛 Proposed fix
foreach (var evt in _events) { + if (evt == null) continue; + if (evt.IsDehydrated) dehydratedCount++; else if (evt.Payload != null) { hydratedCount++; estimatedPayloadBytes += EstimatePayloadSize(evt.Payload); } }MCPForUnity/Editor/ActionTrace/Core/Store/EventStore.Persistence.cs-45-87 (1)
45-87: Throttle check has potential race condition.The throttle check on lines 48-53 reads
_lastSaveTimewithout holding the lock, but_lastSaveTimeis updated on line 77 inside the lock. This could lead to missed saves if two threads check throttling simultaneously.Consider moving the throttle check inside the lock, or use
Interlockedfor atomic access.🔧 Proposed fix
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) { + // 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; + } + // Only schedule if not already scheduled (prevents callback queue bloat) if (_saveScheduled) return; _saveScheduled = true; }MCPForUnity/Editor/ActionTrace/Capture/CapturePoints/CapturePointStats.cs-20-31 (1)
20-31: Guard EndCapture when StartCapture wasn’t called (and reset timing state).If
EndCapture()is invoked beforeStartCapture()(or afterReset()),_startTimeTicksis 0 and elapsed time becomes enormous, skewing stats. A simple guard + reset avoids that.🐛 Proposed fix
public void EndCapture() { + if (_startTimeTicks == 0) + return; + long elapsedTicks = DateTimeOffset.UtcNow.Ticks - _startTimeTicks; TotalCaptureTimeMs += elapsedTicks / 10000; TotalEventsCaptured++; + _startTimeTicks = 0; UpdateAverage(); } @@ public void Reset() { TotalEventsCaptured = 0; EventsFiltered = 0; EventsSampled = 0; TotalCaptureTimeMs = 0; AverageCaptureTimeMs = 0; ErrorCount = 0; + _startTimeTicks = 0; }Also applies to: 55-63
MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs-258-261 (1)
258-261: Use a real menu separator instead of a "/" action.The
"/"character inAppendAction()is interpreted as a menu path separator for nested submenus, not as a separator indicator. UseAppendSeparator()to properly add a menu separator and avoid unintended UI behavior.Proposed fix
- _filterMenu?.menu.AppendAction("/", a => { }); + _filterMenu?.menu.AppendSeparator();MCPForUnity/Editor/ActionTrace/Core/Store/EventStore.cs-94-101 (1)
94-101: Importance threshold should be inclusive.
Using<=drops events that match the configured minimum. Query‑side filtering is inclusive, so this is inconsistent.🛠️ Proposed fix
- if (importance <= settings.Filtering.MinImportanceForRecording) + if (importance < settings.Filtering.MinImportanceForRecording)MCPForUnity/Editor/ActionTrace/Analysis/Context/ContextCompressor.cs-226-235 (1)
226-235: Guard against non-positivecountin summaries.A negative
countproduces a misleading negative summary and skips the loop silently.🔧 Proposed fix
- if (events == null || startIdx < 0 || startIdx >= events.Count) + if (events == null || startIdx < 0 || startIdx >= events.Count || count <= 0) return string.Empty;
🧹 Nitpick comments (28)
MCPForUnity/Editor/ActionTrace/Capture/Capturers/UndoCapture.cs (1)
164-185: Avoid empty Undo labels when note is blank.
Ifnoteis null/whitespace, the Undo history entry becomes"AI: "which is not very helpful. A small fallback makes the history clearer.♻️ Suggested tweak
public static string HandleActionTraceNote(string note, bool isTransactionEnd) { string groupName; + var safeNote = string.IsNullOrWhiteSpace(note) ? (_currentToolName ?? "AI Operation") : note; if (_isInToolCall) { // Use the AI note as the final Undo group name - groupName = $"AI: {note}"; + groupName = $"AI: {safeNote}"; Undo.SetCurrentGroupName(groupName); if (isTransactionEnd) { EndToolCall(); } return groupName; } // Not in a tool call - just set the Undo name - groupName = $"AI: {note}"; + groupName = $"AI: {safeNote}"; Undo.SetCurrentGroupName(groupName); return groupName; }MCPForUnity/Editor/ActionTrace/Integration/VCS/VcsContextProvider.cs (1)
44-81: Clarify the thread‑safety guarantee for_currentContext.
GetCurrentContextis documented as thread‑safe, but reads/writes have no memory barriers. If background event recording is expected, make the intent explicit withvolatile/Volatile.Reador drop the guarantee.🛠️ Suggested fix
- private static VcsContext _currentContext; + private static volatile VcsContext _currentContext; @@ public static VcsContext GetCurrentContext() { - if (_currentContext == null) + var current = _currentContext; + if (current == null) { - _currentContext = GetInitialContext(); + current = GetInitialContext(); + _currentContext = current; } - return _currentContext; + return current; }MCPForUnity/Editor/ActionTrace/Metadata/Descriptors/IEventDescriptor.cs (1)
62-88: Avoid duplicated target‑name resolution logic.
GetTargetNamemirrors the logic inEventSummarizer.GetTargetName. Consider extracting a shared helper to prevent drift if the key list changes.MCPForUnity/Editor/ActionTrace/Core/Settings/ActionTraceSettings.Editor.cs (1)
65-75: UnusedChangeCheckScopewrapper.The
ChangeCheckScopeon line 65 is created but itschangedproperty is never read. The manualnewIndex != currentIndexcheck makes the scope redundant.♻️ Optional simplification
- using (new EditorGUI.ChangeCheckScope()) - { var presetNames = ActionTracePreset.AllPresets.ConvertAll(p => p.Name); var currentIndex = presetNames.FindIndex(n => n == settings.CurrentPresetName); var newIndex = EditorGUILayout.Popup(currentIndex, presetNames.ToArray()); if (newIndex != currentIndex && newIndex >= 0) { settings.ApplyPreset(ActionTracePreset.AllPresets[newIndex]); } - }MCPForUnity/Editor/ActionTrace/Core/EventTypes.cs (1)
112-117:BuildCompletedimportance (1.0f) doesn't match section comment.
BuildCompletedhasDefaultImportance = 1.0fwhich qualifies as "Critical", but it's placed under the "High (0.7-0.9)" section comment. Consider moving it to the Critical section for consistency.MCPForUnity/Editor/ActionTrace/Capture/Capturers/PropertyCapture.cs (1)
92-158: Inconsistent indentation inProcessModifications.The
foreachloop body (lines 93-155) has inconsistent indentation - it appears to be indented by 8 spaces instead of the expected 12 (3 levels within the lock). Thereturn modifications;on line 157 is correctly inside the lock but visually appears at an unexpected level.♻️ Fix indentation
The
foreachblock content starting at line 93 should be indented one more level to align with being inside bothlock (_lock)andforeach (var undoMod ...).MCPForUnity/Editor/ActionTrace/Capture/Filters/EventFilter.cs (2)
139-140:Thumbs.dbrule won't match correctly withExtensiontype.
RuleType.ExtensionusesEndsWith, so".DS_Store"will match paths ending in.DS_Store, but"Thumbs.db"won't match"path/to/Thumbs.db"since the path doesn't end withThumbs.db(it does, but the pattern lacks the leading dot that extensions typically have).Actually,
"path/to/Thumbs.db".EndsWith("Thumbs.db")is true, so this works. However, semantically these are filenames, not extensions. Consider usingRuleType.Regexwith@"(^|/)Thumbs\.db$"for clarity, or document thatExtensionalso works for filename matching.
44-47: Silent exception swallowing on invalid regex patterns.When regex compilation fails, the exception is caught and ignored. Consider logging a warning so users can diagnose why their custom regex rules aren't matching.
♻️ Proposed enhancement
catch { - // Invalid regex, return null + // Invalid regex - log warning for diagnostics + UnityEngine.Debug.LogWarning($"[EventFilter] Invalid regex pattern in rule '{Name}': {Pattern}"); }MCPForUnity/Editor/ActionTrace/Capture/CapturePoints/EventCaptureRegistry.cs (1)
28-36: Consider guarding duplicate IDs and late registration.
Line 32 onward: duplicateCapturePointIdvalues makeSetEnabled/Unregisterambiguous, and registering afterInitializeAll()leaves the new point uninitialized. A small guard keeps the registry deterministic.♻️ Suggested update (duplicate guard + late-init)
public void Register(IEventCapturePoint capturePoint) { if (capturePoint == null) return; + if (_capturePoints.Exists(p => p.CapturePointId == capturePoint.CapturePointId)) + { + Debug.LogWarning($"[EventCaptureRegistry] Duplicate capture point id: {capturePoint.CapturePointId}"); + return; + } _capturePoints.Add(capturePoint); // Sort by priority _capturePoints.Sort((a, b) => b.InitializationPriority.CompareTo(a.InitializationPriority)); + + if (_isInitialized) + { + try { capturePoint.Initialize(); } + catch (Exception ex) + { + Debug.LogError($"[EventCaptureRegistry] Failed to initialize {capturePoint.CapturePointId}: {ex.Message}"); + } + } }MCPForUnity/Editor/Hooks/HookRegistry.cs (1)
102-450: Optional: factor out the repeated subscriber-invocation pattern.
Line 104+ repeats the same try/catch and logging logic across many methods. A small helper (e.g.,InvokeSafely<T>(Action<T>, T)) would reduce duplication and keep logging consistent.MCPForUnity/Editor/ActionTrace/Capture/Sampling/PendingSample.cs (1)
8-18: Optional: make the struct immutable.
If mutation isn’t required, consider areadonly structwith readonly fields or properties to avoid accidental copies and improve clarity.MCPForUnity/Editor/ActionTrace/Capture/Sampling/SamplingStrategy.cs (1)
3-4: Namespace doesn't match folder structure.The file is located in
Capture/Sampling/but the namespace isMCPForUnity.Editor.ActionTrace.Capture. Consider aligning the namespace with the folder path for consistency.Suggested fix
-namespace MCPForUnity.Editor.ActionTrace.Capture +namespace MCPForUnity.Editor.ActionTrace.Capture.SamplingMCPForUnity/Editor/ActionTrace/Semantics/DefaultEventScorer.cs (2)
45-47: Redundant special case for AINote.The
AINoteevent type is already configured withDefaultImportance = 1.0finEventTypes.Metadata(per the relevant context). This hardcoded check duplicates that configuration and creates a maintenance burden—if the metadata changes, this won't update accordingly.Remove redundant check
// Get base score from metadata var meta = EventTypes.Metadata.Get(evt.Type); float baseScore = meta.DefaultImportance; - // Special case: AINote is always critical - if (evt.Type == "AINote") - return 1.0f; - // Apply payload-based adjustments for asset events float adjustment = GetPayloadAdjustment(evt); return Mathf.Clamp01(baseScore + adjustment);
58-68: Redundant null check.The
Score()method already returns early whenevt.Payload == null(line 38-39), soGetPayloadAdjustment()will never receive an event with a null payload. The null check at line 60-61 is unreachable.Remove redundant check
private static float GetPayloadAdjustment(EditorEvent evt) { - if (evt.Payload == null) - return 0f; - // Asset type adjustments (only for AssetCreated/AssetImported) bool isAssetEvent = evt.Type == EventTypes.AssetCreated || evt.Type == EventTypes.AssetImported;MCPForUnity/Editor/ActionTrace/Capture/Capturers/AssetCapture.cs (2)
296-305: UseToLowerInvariant()for consistent cross-locale behavior.
ToLower()uses the current culture, which can produce unexpected results for certain characters (e.g., Turkish 'I'). UseToLowerInvariant()for file extension comparisons.Suggested fix
private static bool ShouldTrackModification(string assetPath) { - string ext = System.IO.Path.GetExtension(assetPath).ToLower(); + string ext = System.IO.Path.GetExtension(assetPath).ToLowerInvariant(); // Track modifications for these asset typesAlso apply this change to
GetAssetType()at line 312.
284-290: Silent exception swallowing hinders debugging.Catching all exceptions without logging makes it difficult to diagnose issues with the
.metafile heuristic.Suggested fix
catch + catch (Exception ex) { // On any error, default to treating as imported (conservative) + McpLog.Debug($"[AssetChangePostprocessor] IsNewlyCreatedAsset check failed for {assetPath}: {ex.Message}"); return false; }MCPForUnity/Editor/ActionTrace/Sources/Unity/UnityEventHooks.cs (3)
40-42: Consider naming the lock more specifically.
_lockis only used for hierarchy change debouncing. A more descriptive name like_hierarchyDebounceLockwould clarify its purpose.
92-94: Potential for multipleInitializeTrackingcalls.If
SubscribeToUnityEventsis somehow invoked multiple times before the firstdelayCallexecutes, multipleInitializeTrackingcalls could be queued. While the_isInitializedguard should prevent re-subscription, consider adding a guard insideInitializeTrackingas well.
194-205: Missing handling forExitingPlayModeandExitingEditModestates.
PlayModeStateChangehas four states, but onlyEnteredPlayModeandEnteredEditModeare handled. If downstream systems need to react to the exiting phases (e.g., for cleanup or preparation), those cases should be added.♻️ Proposed enhancement to handle all states
private static void OnPlayModeStateChanged(PlayModeStateChange state) { switch (state) { case PlayModeStateChange.EnteredPlayMode: HookRegistry.NotifyPlayModeChanged(true); break; case PlayModeStateChange.EnteredEditMode: HookRegistry.NotifyPlayModeChanged(false); break; + case PlayModeStateChange.ExitingEditMode: + // Optionally notify subscribers about exiting edit mode + break; + case PlayModeStateChange.ExitingPlayMode: + // Optionally notify subscribers about exiting play mode + break; } }MCPForUnity/Editor/ActionTrace/Capture/Recorder.cs (1)
312-326: Consider usingStringBuilderfor deeply nested hierarchies.
GetGameObjectPathbuilds paths via repeated string concatenation in a while loop. For deeply nested hierarchies, this creates many intermediate string allocations. If performance becomes a concern, consider usingStringBuilder.MCPForUnity/Editor/ActionTrace/Capture/Sampling/SamplingConfig.cs (1)
51-54: Consider validatingwindowMsparameter.
SetStrategyaccepts anylongvalue forwindowMs, including negative values. Consider adding validation to ensurewindowMs > 0when mode isThrottleorDebounce.♻️ Proposed validation
public static void SetStrategy(string eventType, SamplingMode mode, long windowMs = 1000) { + if (mode != SamplingMode.None && windowMs <= 0) + throw new ArgumentOutOfRangeException(nameof(windowMs), "Window must be positive when sampling is enabled"); Strategies[eventType] = new SamplingStrategy(mode, windowMs); }MCPForUnity/Editor/ActionTrace/Core/Store/EventStore.Diagnostics.cs (1)
82-112:EstimatePayloadSizedoesn't recurse into nested dictionaries.For nested
IDictionary<string, object>values, the estimation uses a flatdict.Count * 100approximation instead of recursively callingEstimatePayloadSize. This may underestimate memory for deeply nested payloads.If precision matters, consider recursive estimation.
MCPForUnity/Editor/ActionTrace/Core/Store/EventStore.Persistence.cs (1)
184-186: Log message slightly misleading.The log states "now {maxEvents}" but
_events.CountafterRemoveRangeequalsmaxEventsonly if exactlyremoveCountitems were removed. The message is technically correct but could be clearer by using_events.Countdirectly.♻️ Suggested improvement
- McpLog.Info($"[EventStore] Trimmed {removeCount} old events " + - $"(was {_events.Count + removeCount}, now {maxEvents}, hard limit was {hardLimit})"); + McpLog.Info($"[EventStore] Trimmed {removeCount} old events " + + $"(was {_events.Count + removeCount}, now {_events.Count}, hard limit was {hardLimit})");MCPForUnity/Editor/Resources/ActionTrace/ActionTraceViewResource.cs (1)
188-227: Confirm aggregation input ordering.
EventStore.Queryreturns newest‑first. IfTransactionAggregator.Aggregateexpects chronological input, sort bySequencebefore aggregating.♻️ Suggested adjustment (if chronological order is required)
- var operations = TransactionAggregator.Aggregate(filteredEvents); + var orderedEvents = filteredEvents.OrderBy(e => e.Sequence).ToList(); + var operations = TransactionAggregator.Aggregate(orderedEvents);MCPForUnity/Editor/ActionTrace/Core/Store/EventStore.cs (1)
332-349: Shield batch notifications from subscriber exceptions.
One faulty subscriber can interrupt the batch. Consider guarding each invoke, mirroring HookRegistry’s pattern.♻️ Suggested hardening
- foreach (var evt in toNotify) - { - EventRecorded?.Invoke(evt); - } + foreach (var evt in toNotify) + { + try + { + EventRecorded?.Invoke(evt); + } + catch (Exception ex) + { + McpLog.Warn($"[EventStore] EventRecorded subscriber threw exception: {ex.Message}"); + } + }+using MCPForUnity.Editor.Helpers;MCPForUnity/Editor/ActionTrace/Sources/Unity/UnityEventHooks.Advanced.cs (1)
114-136: Ensure build telemetry runs even when BuildPlayer throws.
IfBuildPipeline.BuildPlayerthrows,_currentBuildPlatformisn’t reset and no failure event is emitted. A try/catch/finally keeps telemetry consistent.🛠️ Suggested hardening
- _buildStartTime = DateTime.UtcNow; - _currentBuildPlatform = GetBuildTargetName(options.target); - - BuildReport result = BuildPipeline.BuildPlayer(options); - - var duration = DateTime.UtcNow - _buildStartTime; - bool success = result.summary.result == BuildResult.Succeeded; - - HookRegistry.NotifyBuildCompleted(success); - HookRegistry.NotifyBuildCompletedDetailed(new BuildArgs - { - Platform = _currentBuildPlatform, - Location = options.locationPathName, - DurationMs = (long)duration.TotalMilliseconds, - SizeBytes = success ? result.summary.totalSize : null, - Success = success, - Summary = success ? null : result.summary.ToString() - }); - - _currentBuildPlatform = null; + _buildStartTime = DateTime.UtcNow; + _currentBuildPlatform = GetBuildTargetName(options.target); + var platform = _currentBuildPlatform; + + try + { + BuildReport result = BuildPipeline.BuildPlayer(options); + + var duration = DateTime.UtcNow - _buildStartTime; + bool success = result.summary.result == BuildResult.Succeeded; + + HookRegistry.NotifyBuildCompleted(success); + HookRegistry.NotifyBuildCompletedDetailed(new BuildArgs + { + Platform = platform, + Location = options.locationPathName, + DurationMs = (long)duration.TotalMilliseconds, + SizeBytes = success ? result.summary.totalSize : null, + Success = success, + Summary = success ? null : result.summary.ToString() + }); + } + catch (Exception ex) + { + var duration = DateTime.UtcNow - _buildStartTime; + HookRegistry.NotifyBuildCompleted(false); + HookRegistry.NotifyBuildCompletedDetailed(new BuildArgs + { + Platform = platform, + Location = options.locationPathName, + DurationMs = (long)duration.TotalMilliseconds, + Success = false, + Summary = ex.Message + }); + throw; + } + finally + { + _currentBuildPlatform = null; + }MCPForUnity/Editor/ActionTrace/Capture/CapturePoints/EventCapturePointBase.cs (1)
45-62: Unusedcontextparameter.The
ContextMapping contextparameter is declared but never used in the method body. If this is intentional for future extension, consider documenting it or removing it until needed to avoid confusion.♻️ Option 1: Remove unused parameter
-protected void RecordEvent(EditorEvent evt, ContextMapping context = null) +protected void RecordEvent(EditorEvent evt)♻️ Option 2: Add TODO comment if intentional
protected void RecordEvent(EditorEvent evt, ContextMapping context = null) { if (!IsEnabled) return; _stats.StartCapture(); try { - // Create event and record via EventStore + // TODO: Integrate ContextMapping with EventStore.RecordWithContext when context layer is connected EventStore.Record(evt); _stats.EndCapture(); }MCPForUnity/Editor/ActionTrace/Metadata/Descriptors/ComponentEventDescriptor.cs (1)
83-117: Consider sharing asset-type detection with the emitter.
DetectAssetTypeduplicates the extension mapping already present inMCPForUnity/Editor/ActionTrace/Emitter.cs(lines 516-538). A shared helper would prevent drift and keep classification consistent across emission and metadata.
MCPForUnity/Editor/ActionTrace/Capture/CapturePoints/EventCaptureRegistry.cs
Show resolved
Hide resolved
- Redesigned styles in ActionTraceEditorWindow.uss for improved aesthetics and usability, including new color schemes and layout adjustments. - Updated UXML structure in ActionTraceEditorWindow.uxml to incorporate a new header bar, main toolbar with categorized sections, and enhanced event list display. - Added empty state and no results state visual elements to provide better user feedback. - Introduced responsive design elements for better usability on smaller screens.
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>
There was a problem hiding this 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
There was a problem hiding this 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
🤖 Fix all issues with AI agents
In `@MCPForUnity/Editor/ActionTrace/Analysis/Context/ContextCompressor.cs`:
- Around line 232-241: SummarizeEvents currently allows a negative count which
makes endIdx and actualCount negative and can produce odd summaries; add an
input guard at the start of SummarizeEvents to handle invalid count values
(e.g., if count <= 0) and return string.Empty (or throw) instead of proceeding;
update the logic around endIdx, actualCount, and the check that follows so they
assume count is non-negative (refer to SummarizeEvents, startIdx, count, endIdx,
actualCount).
In `@MCPForUnity/Editor/ActionTrace/Capture/Capturers/AssetCapture.cs`:
- Around line 181-228: The dedup set updates in AssetCapture.cs aren't flagged
for persistence for deletions/moves because hasChanges is only set in the
imported-assets loop; update the deleted-assets foreach and moved-assets for
loop so that whenever _processedAssetsInSession.Add(assetPath) (or
_processedAssetsInSession.Add(movedAssets[i])) returns true you also set
hasChanges = true, ensuring the same persistence/save logic that runs after
imports will persist these dedup entries and prevent duplicates after domain
reloads.
- Around line 85-98: SaveProcessedAssets currently returns early when
_cachedProcessedAssets is null or empty, so CleanupOldEntries can clear the
in-memory list but the on-disk cache remains stale; update SaveProcessedAssets
(and use GetCacheFilePath) to persist the cleared state by either writing an
empty JSON array to the cache file or deleting the cache file when
_cachedProcessedAssets is empty/null, ensuring the on-disk cache reflects the
in-memory _cachedProcessedAssets after CleanupOldEntries.
In `@MCPForUnity/Editor/ActionTrace/Capture/Sampling/SamplingMiddleware.cs`:
- Around line 124-133: evt.TargetId may contain a prefix like "Asset:{path}",
causing EventFilter.ShouldTrackAsset to misclassify assets; in
SamplingMiddleware (the asset event handling block) prefer using
evt.Payload["path"] when present and otherwise normalize evt.TargetId by
stripping any "Asset:" (or similar) prefix before passing the path to
EventFilter.ShouldTrackAsset so blacklist checks run against the real filesystem
path.
In `@MCPForUnity/Editor/ActionTrace/Context/ToolCallScope.cs`:
- Around line 28-110: The scope is popped from a ThreadLocal stack on the wrong
thread when disposed off-thread (e.g., via ContinueWith in ExecuteAsync),
causing leaks; capture the creator's SynchronizationContext in the ToolCallScope
constructor (store in a new _syncContext field) and, in disposal/complete logic
(the methods that pop the scope and run cleanup—used by
ExecuteAsync/ContinueWith), if SynchronizationContext.Current != _syncContext,
marshal the cleanup back by calling _syncContext.Post(...); if _syncContext is
null fall back to EditorApplication.delayCall as before; ensure all places that
currently call EditorApplication.delayCall (and the dispose path used by
ExecuteAsync) use this marshaling so the original thread's ThreadLocal stack is
accessed.
In `@MCPForUnity/Editor/ActionTrace/Core/Store/EventStore.Diagnostics.cs`:
- Around line 17-23: Clamp the incoming hotEventCount to a non‑negative value
before using it in loops to prevent iterating past _events.Count; e.g. at the
start of DehydrateOldEvents(int hotEventCount) and the other similar method
(lines referenced) replace uses of hotEventCount with a defensive value such as
Math.Max(0, hotEventCount) (or assign hotEventCount = Math.Max(0,
hotEventCount)) so subsequent logic that uses _events.Count - hotEventCount
cannot go negative or produce negative diagnostics.
- Around line 54-63: GetMemoryDiagnostics iterates _events without guarding
against null entries (DehydrateOldEvents can leave nulls), causing a
NullReferenceException when accessing evt.IsDehydrated; update the loop in
GetMemoryDiagnostics (in EventStore.Diagnostics.cs) to skip or account for null
events (e.g., if evt == null continue / increment a null counter) before
checking evt.IsDehydrated or evt.Payload and still call EstimatePayloadSize only
when evt.Payload is non-null so the diagnostics loop is null-safe.
In `@MCPForUnity/Editor/Tools/UndoToSequenceTool.cs`:
- Around line 155-158: The delayed lambda should guard against an invalid active
scene: call UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene(),
store it in a local variable, check scene.IsValid() (and optionally
scene.isLoaded) and only call EditorSceneManager.MarkSceneDirty(scene) if the
check passes; otherwise skip the call to avoid throwing on no/invalid scene.
Target the lambda registered with EditorApplication.delayCall and replace the
direct GetActiveScene()/MarkSceneDirty call with this guarded flow.
In `@MCPForUnity/Editor/Windows/ActionTraceEditorWindow.uss`:
- Around line 661-685: Replace the unsupported `@media` block with a class-based
responsive solution: in the window's C# editor (where you build the UI and have
access to rootVisualElement) register a GeometryChangedEvent callback, compute
isCompact = evt.newRect.width < 800 and call
rootVisualElement.EnableInClass("compact-layout", isCompact); then remove the
media query from ActionTraceEditorWindow.uss and add equivalent class-based
selectors (e.g., .compact-layout .event-list-container, .compact-layout
.detail-scroll-view, .compact-layout .toolbar-display, .compact-layout
.event-type, .compact-layout .event-time) that mirror the styles inside the
former media block so the UI toggles styles based on container width.
- Around line 590-594: The CSS selector .importance-bar-label is duplicated;
remove the later duplicate block (the one at the end with font-size: 10px;
color: `#808080`; margin-right: 8px;) so only the original definition (the earlier
.importance-bar-label selector) remains; ensure no other uses depend on the
second block and run a quick style check to confirm no regressions.
- Around line 377-379: The USS attribute selectors
(data-source="Human"/"AI"/"System") are never applied because the C# code in
bindItem does not set the attribute on the contextIndicator Label; update the
bindItem method to call contextIndicator.SetAttribute("data-source", sourceType)
(where sourceType is the event's context/source string you derive) whenever you
set contextIndicator.text or style so the selectors
(context-indicator[data-source="Human"], etc.) will match and the colors will
apply.
♻️ Duplicate comments (1)
MCPForUnity/Editor/ActionTrace/Context/ToolCallScope.cs (1)
495-525: Handle synchronous exceptions fromfunc()to avoid scope leaks.If
func()throws before returning aTask, the scope is never completed or disposed and the failure event is lost.🐛 Proposed fix
- var task = func(); + System.Threading.Tasks.Task<T> task; + try + { + task = func(); + } + catch (Exception ex) + { + scope.Fail(ex.Message); + scope.Dispose(); + throw; + }
🧹 Nitpick comments (9)
Server/src/models/action_trace.py (1)
49-52: Consider using Pydantic v2model_configsyntax.The
populate_by_name = Truesetting enables population by field name when aliases are defined, but no aliases are currently defined on fields. If aliases are needed for C# JSON interop (e.g.,timestamp_unix_ms↔timestampUnixMs), consider defining them explicitly withalias=...orserialization_alias=....Additionally, Pydantic v2 recommends
model_config = ConfigDict(...)over nestedclass Config:from pydantic import ConfigDict class EditorEvent(BaseModel): model_config = ConfigDict(populate_by_name=True) # ...fields...MCPForUnity/Editor/ActionTrace/Capture/Capturers/UndoCapture.cs (1)
66-71: Consider callingUndo.IncrementCurrentGroup()before setting the group name.The nested-call guard (lines 53-58) is a good addition. However, without
Undo.IncrementCurrentGroup()before line 68, any pre-existing operations in the current Undo group will be collapsed together with the tool call's operations whenEndToolCall()runs. This could result in unrelated operations being bundled into the AI tool's undo entry.🔧 Proposed fix
+ // Start a fresh Undo group to isolate this tool call's operations + Undo.IncrementCurrentGroup(); + // Set the current Undo group name // This name will appear in the Undo history (e.g., "Ctrl+Z AI: Create Player") Undo.SetCurrentGroupName($"AI: {ActionTraceHelper.FormatToolName(toolName)}");MCPForUnity/Editor/Windows/ActionTraceEditorWindow.uss (1)
711-725: CSS variables are defined but never used in this stylesheet.The custom properties (e.g.,
--accent-primary,--bg-primary) are defined but all color and styling values throughout the file are hardcoded. Consider using these variables for consistency and easier theming:.root-container { flex-direction: column; flex-grow: 1; overflow: hidden; - background-color: `#252525`; + background-color: var(--bg-primary); }This would apply to many other hardcoded values like
#4a90e2→var(--accent-primary),#e0e0e0→var(--text-primary), etc.MCPForUnity/Editor/ActionTrace/Analysis/Context/ContextCompressor.cs (2)
49-54:TargetCompressionRatiois currently unused.
Callers setting this will see no effect. Consider either wiring it intoApplySizeLimits(e.g., deriveMaxCompressedEventsfrom the ratio) or removing the field until it’s supported to avoid a misleading config surface.
382-388: ValidateminutesinGetRecentto avoid surprising results.
Negative values shift the threshold into the future and silently return empty lists. Consider guarding or clamping.🔧 Suggested fix
public static List<EditorEvent> GetRecent(this IReadOnlyList<EditorEvent> events, int minutes) { if (events == null || events.Count == 0) return new List<EditorEvent>(); + if (minutes <= 0) + return new List<EditorEvent>(); long thresholdMs = DateTimeOffset.UtcNow.AddMinutes(-minutes).ToUnixTimeMilliseconds();MCPForUnity/Editor/ActionTrace/Capture/CapturePoints/EventCaptureRegistry.cs (2)
24-36: Guard duplicate IDs and clarify late registration behavior.
Without a uniqueness check, duplicates makeGetCapturePoint/Unregisterambiguous and can double-init/shutdown. Also, ifRegistercan occur afterInitializeAll, decide whether to auto-initialize or explicitly warn.♻️ Proposed improvement
public void Register(IEventCapturePoint capturePoint) { if (capturePoint == null) return; + if (_capturePoints.Exists(p => p.CapturePointId == capturePoint.CapturePointId)) + { + Debug.LogWarning($"[EventCaptureRegistry] Duplicate capture point id '{capturePoint.CapturePointId}' ignored."); + return; + } + _capturePoints.Add(capturePoint); // Sort by priority _capturePoints.Sort((a, b) => b.InitializationPriority.CompareTo(a.InitializationPriority)); + + if (_isInitialized) + Debug.LogWarning($"[EventCaptureRegistry] Registered '{capturePoint.CapturePointId}' after initialization; it will not be initialized automatically."); }
66-83: Avoid collection mutation hazards during init/shutdown.
If a capture point registers/unregisters insideInitialize()orShutdown(), the current loops can throw or skip items. Consider iterating over a snapshot or enforce a no-mutation contract.♻️ Snapshot iteration option
public void InitializeAll() { if (_isInitialized) return; - foreach (var point in _capturePoints) + var points = _capturePoints.ToArray(); + foreach (var point in points) { try { point.Initialize(); } catch (Exception ex) { Debug.LogError($"[EventCaptureRegistry] Failed to initialize {point.CapturePointId}: {ex.Message}"); } } @@ public void ShutdownAll() { if (!_isInitialized) return; // Shutdown in reverse order - for (int i = _capturePoints.Count - 1; i >= 0; i--) + var points = _capturePoints.ToArray(); + for (int i = points.Length - 1; i >= 0; i--) { try { - _capturePoints[i].Shutdown(); + points[i].Shutdown(); } catch (Exception ex) { - Debug.LogError($"[EventCaptureRegistry] Failed to shutdown {_capturePoints[i].CapturePointId}: {ex.Message}"); + Debug.LogError($"[EventCaptureRegistry] Failed to shutdown {points[i].CapturePointId}: {ex.Message}"); } }Also applies to: 89-104
MCPForUnity/Editor/ActionTrace/Capture/Capturers/AssetCapture.cs (1)
235-249: Align cleanup comments with actual behavior.The XML comment says time-based expiry (30 minutes), but the implementation is size-based (
MaxCacheSize). Please update the comment or implement time-based expiry.MCPForUnity/Editor/ActionTrace/Context/ToolCallScope.cs (1)
314-320: Prefer Guid-based suffix to avoid UnityEngine.Random thread affinity.
UnityEngine.Random.Rangecan be problematic off the main thread and relies on global state. AGuidsuffix is thread-safe and avoids Unity dependencies here.♻️ Proposed change
- int random = UnityEngine.Random.Range(1000, 9999); - return $"{_toolId}_{timestamp}_{random}"; + string suffix = Guid.NewGuid().ToString("N")[..4]; + return $"{_toolId}_{timestamp}_{suffix}";
- 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>
…ActionTraceEditorWindow
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
🤖 Fix all issues with AI agents
In `@MCPForUnity/Editor/ActionTrace/Capture/Capturers/AssetCapture.cs`:
- Around line 1-10: Add the missing using for the EventFilter type so
EventFilter.ShouldTrackAsset(...) resolves: update AssetCapture.cs to include
the directive "using MCPForUnity.Editor.ActionTrace.Filters;" near the other
using statements so references to EventFilter (used in methods in AssetCapture
class at calls to ShouldTrackAsset) compile correctly.
In `@MCPForUnity/Editor/ActionTrace/Capture/Sampling/SamplingMiddleware.cs`:
- Around line 200-224: The eviction code that forcibly removes the oldest entry
from _pendingSamples when _pendingSamples.Count >= MaxSampleCache can drop
debounce samples silently; before calling _pendingSamples.TryRemove(oldestKey,
out _), retrieve the sample (TryGetValue/out var sample), detect if it is a
debounce sample (check the sample's debounce indicator, e.g. sample.IsDebounce
or sample.Type == SampleType.Debounce), and if so invoke the middleware's normal
recording/flushing path for finalized debounce samples (the same code used when
debounce samples are normally emitted) to persist/emit it, then remove the
entry; otherwise proceed with the TryRemove as before — use existing helpers
like CleanupExpiredSamples and the middleware's sample-recording method to avoid
duplicating logic.
In `@MCPForUnity/Editor/ActionTrace/Context/ToolCallScope.cs`:
- Around line 529-538: Add an explicit check for task cancellation before
accessing t.Result: when t.IsCanceled is true, call scope.Fail with a
cancellation message (e.g., "Async canceled") and throw a TaskCanceledException
instead of falling through to the else branch; update the block around the
existing t.IsFaulted/t.Result logic in ToolCallScope.cs (referencing the task
variable t and the scope instance) to handle t.IsCanceled first so t.Result is
never accessed for canceled tasks.
- Around line 334-340: GenerateCallId in ToolCallScope uses
UnityEngine.Random.Range which requires the Unity main thread; replace it with a
thread-safe RNG (e.g.,
System.Security.Cryptography.RandomNumberGenerator.GetInt32 or a
ThreadLocal<System.Random>) so GenerateCallId can be called from background
threads. Update GenerateCallId to compute the random suffix with a thread-safe
call (keep the same numeric range 1000–9999) and leave the rest of the ID format
unchanged so background construction via ExecuteAsync or direct construction no
longer throws UnityException.
In `@MCPForUnity/Editor/ActionTrace/Core/Settings/ActionTraceSettings.cs`:
- Around line 217-227: CreateSettings currently calls ApplyPreset (which calls
Save -> EditorUtility.SetDirty(this) and AssetDatabase.SaveAssets()) before the
ScriptableObject is added to the AssetDatabase, making SetDirty/SaveAssets
ineffective; fix by creating/adding the asset first with
AssetDatabase.CreateAsset(settings, SettingsPath) and
AssetDatabase.SaveAssets()/Refresh(), then call
settings.ApplyPreset(ActionTracePreset.Standard) (so SetDirty/SaveAssets operate
on an asset already registered), followed by final
AssetDatabase.SaveAssets()/Refresh() and the McpLog.Info call referencing
SettingsPath.
In `@MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs`:
- Around line 651-757: BuildJsonExport is corrupting its output because
SanitizeJson clears and reuses the shared field _stringBuilder; change
SanitizeJson to use a new local StringBuilder (or plain string operations)
instead of the shared _stringBuilder so it no longer mutates the in-progress
JSON, keeping BuildJsonExport and any other callers safe; locate the
SanitizeJson method and replace uses of the field _stringBuilder with a local
variable (e.g., var sb = new StringBuilder()) and return sb.ToString() without
touching the class-level _stringBuilder.
- Around line 830-841: The early-return in RefreshEvents only checks
EventStore.Count, _lastEventStoreCount, _currentEvents and _searchText, so
changes to filter/sort/context fields (_minImportance, _showContext, _sortMode)
won’t trigger a refresh; update RefreshEvents to also detect changes in those
values by adding cached last-seen fields (e.g., _lastMinImportance,
_lastShowContext, _lastSortMode) and include comparisons against them in the
short-circuit condition (or alternatively reset _lastEventStoreCount when any of
_minImportance/_showContext/_sortMode/_searchText change), then update those
cached fields
(_lastMinImportance/_lastShowContext/_lastSortMode/_lastSearchText) when
performing the refresh so filter/sort/context changes correctly force a list
refresh in RefreshEvents.
In `@MCPForUnity/Editor/Windows/ActionTraceEditorWindow.uss`:
- Around line 446-452: The .detail-scroll-view CSS has conflicting width
constraints (min-width: 320px vs max-width: 300px); update the rule for
.detail-scroll-view to make min-width <= max-width — either lower min-width to
300px or raise max-width to 320px (or choose a consistent value like min-width:
300px; max-width: 320px) so the width constraints are not contradictory; modify
the declaration in the ActionTraceEditorWindow.uss .detail-scroll-view block
accordingly.
♻️ Duplicate comments (3)
MCPForUnity/Editor/ActionTrace/Core/Store/EventStore.Diagnostics.cs (1)
45-51: ClamphotEventCountin diagnostics to avoid negative counts (Line 46).If settings allow negative values,
hotEventscan go negative andcoldEventscan exceedtotalEvents. Add a non‑negative clamp here too.🔧 Proposed fix
- int hotEventCount = settings != null ? settings.Storage.HotEventCount : 100; - int maxEvents = settings != null ? settings.Storage.MaxEvents : 800; + int hotEventCount = settings != null ? settings.Storage.HotEventCount : 100; + int maxEvents = settings != null ? settings.Storage.MaxEvents : 800; + hotEventCount = Math.Max(0, hotEventCount);MCPForUnity/Editor/ActionTrace/Context/ToolCallScope.cs (1)
521-524: Synchronous exception fromfunc()still leaks the scope.If
func()throws synchronously before returning aTask, the scope is never completed or disposed. TheContinueWithcallback never runs because there's no task to continue from.🐛 Proposed fix wrapping func() invocation
public static System.Threading.Tasks.Task<T> ExecuteAsync<T>( string toolName, Func<System.Threading.Tasks.Task<T>> func, string toolId = null, Dictionary<string, object> parameters = null) { var scope = new ToolCallScope(toolName, toolId, parameters); - var task = func(); + System.Threading.Tasks.Task<T> task; + try + { + task = func(); + } + catch (Exception ex) + { + scope.Fail(ex.Message); + scope.Dispose(); + throw; + } return task.ContinueWith(t =>MCPForUnity/Editor/ActionTrace/Capture/Sampling/SamplingMiddleware.cs (1)
266-306: Previous cleanup concern has been addressed with 2x safety margin.The implementation now uses a 2x window safety margin for debounce modes (line 281), which prevents samples from being cleaned up before
FlushExpiredDebounceSamplescan record them. While this differs from the previously suggested fix, it's a reasonable approach that addresses the core concern.
🧹 Nitpick comments (10)
MCPForUnity/Editor/Tools/UndoToSequenceTool.cs (1)
35-48:Parametersclass is defined but never instantiated.
HandleCommandmanually parses theJObjectinstead of deserializing toParameters. This creates dead code and risks drift if someone updatesParameterswithout updating the parsing logic.Consider either:
- Deserialize to
Parametersand use it, or- Remove the
Parametersclass if manual parsing is preferred.MCPForUnity/Editor/ActionTrace/Core/Settings/ActionTraceSettings.cs (3)
17-28: Potential documentation inconsistency in importance scoring.The
MinImportanceForRecordingfield uses a 0.0-1.0 range (line 17-19), but theEnableEmergencyAITriggertooltip references "Critical events (score 10.0)" (line 27). This inconsistency could confuse future maintainers about whether importance is measured on a 0-1 or 0-10 scale.Consider clarifying the tooltip to use consistent terminology, e.g., "importance 1.0" instead of "score 10.0".
65-72: Consider adding Range attributes for consistency.
MinKeepEventsandAutoSaveIntervalSecondslack[Range]attributes unlike other numeric fields in this class. This allows potentially invalid values (e.g., negative or extremely large) to be set in the Inspector.♻️ Proposed fix
- [Tooltip("Minimum number of events to keep when auto-cleaning.")] - public int MinKeepEvents = 100; + [Range(10, 1000)] + [Tooltip("Minimum number of events to keep when auto-cleaning.")] + public int MinKeepEvents = 100; - [Tooltip("Auto-save interval in seconds. 0 = disable auto-save.")] - public int AutoSaveIntervalSeconds = 30; + [Range(0, 300)] + [Tooltip("Auto-save interval in seconds. 0 = disable auto-save.")] + public int AutoSaveIntervalSeconds = 30;
276-282: Add defensive check for invalid HotEventCount configuration.If
HotEventCount > MaxEvents(which theValidate()method checks but doesn't prevent),coldEventsbecomes negative, producing an incorrect memory estimate.♻️ Proposed fix
public long GetEstimatedMemoryUsage() { // Approximate: each event ~300 bytes when hydrated, ~100 bytes when dehydrated int hotEvents = Storage.HotEventCount; - int coldEvents = Storage.MaxEvents - Storage.HotEventCount; + int coldEvents = Math.Max(0, Storage.MaxEvents - Storage.HotEventCount); return (long)(hotEvents * 300 + coldEvents * 100); }MCPForUnity/Editor/ActionTrace/Capture/Capturers/AssetCapture.cs (3)
248-268: Documentation mismatch and aggressive cache clearing.The method comment (lines 249-250) mentions "time-based expiration (30 minutes)" but the implementation uses count-based clearing (
MaxCacheSize = 1000). Additionally, clearing the entire cache when the threshold is exceeded is aggressive and could potentially cause duplicate events.♻️ Suggested improvements
- Update the comment to accurately describe the count-based approach:
/// <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 with a maximum cache size of 1000 entries. /// This is called at the start of each OnPostprocessAllAssets batch. /// </summary>
- Optionally, consider partial eviction instead of full clear to reduce duplicate event risk:
// Remove oldest 20% of entries instead of clearing all int removeCount = _cachedProcessedAssets.Count / 5; var toRemove = _cachedProcessedAssets.Take(removeCount).ToList(); foreach (var item in toRemove) _cachedProcessedAssets.Remove(item);
280-303: Consider logging suppressed exceptions.The empty catch block at line 298 silently swallows all exceptions. While returning
false(treating as imported) is a safe conservative default, adding debug-level logging would help diagnose unexpected issues.♻️ Suggested improvement
catch (Exception ex) { - // On any error, default to treating as imported (conservative) + // On any error, default to treating as imported (conservative) + McpLog.Debug($"[AssetChangePostprocessor] IsNewlyCreatedAsset check failed for '{assetPath}': {ex.Message}"); return false; }
309-348: Use culture-invariant string comparison for file extensions.Both
ShouldTrackModification(line 311) andGetAssetType(line 325) use.ToLower(), which is culture-sensitive. For file extension comparisons,.ToLowerInvariant()is more appropriate to avoid locale-specific issues (e.g., the Turkish 'İ'/'I' problem).♻️ Proposed fix
private static bool ShouldTrackModification(string assetPath) { - string ext = System.IO.Path.GetExtension(assetPath).ToLower(); + string ext = System.IO.Path.GetExtension(assetPath).ToLowerInvariant(); // Track modifications for these asset types ... } private static string GetAssetType(string assetPath) { - string ext = System.IO.Path.GetExtension(assetPath).ToLower(); + string ext = System.IO.Path.GetExtension(assetPath).ToLowerInvariant(); return ext switch ... }MCPForUnity/Editor/ActionTrace/Context/ToolCallScope.cs (2)
24-27: Thread safety concern with shared lists.
_capturedEventsand_childScopesare regularList<T>instances that can be accessed concurrently:
_childScopesis modified by child scopes via_parentScope?._childScopes.Add(this)(line 120), which may run on a different thread than the parent_capturedEventsis accessed viaRecordEventwhich could be called from any threadConsider using
ConcurrentBag<T>or adding synchronization to prevent race conditions in multi-threaded scenarios.♻️ Suggested fix using concurrent collections
- private readonly List<EditorEvent> _capturedEvents; + private readonly System.Collections.Concurrent.ConcurrentBag<EditorEvent> _capturedEvents; private readonly long _startTimestampMs; - private readonly List<ToolCallScope> _childScopes; + private readonly System.Collections.Concurrent.ConcurrentBag<ToolCallScope> _childScopes;Note: This would require updating the
IReadOnlyListproperty types and iteration patterns accordingly.
326-332: Consider logging when out-of-order disposal is detected.If
Peek() != this, the scope won't be popped and will remain on the stack. This defensive check is good, but the silent failure could mask bugs where scopes are disposed out of order.♻️ Suggested diagnostic logging
private void PopFromStack() { if (_scopeStack.Value.Count > 0 && _scopeStack.Value.Peek() == this) { _scopeStack.Value.Pop(); } + else if (_scopeStack.Value.Count > 0) + { + UnityEngine.Debug.LogWarning($"[ToolCallScope] Out-of-order disposal detected for '{_toolName}'. Expected top: {_scopeStack.Value.Peek()._toolName}"); + } }MCPForUnity/Editor/ActionTrace/Capture/Sampling/SamplingMiddleware.cs (1)
312-322: Minor race window between iteration and Clear().If another thread adds an item between the iteration (lines 316-319) and
Clear()(line 320), that item will be lost. For the documented "shutdown" use case this is likely acceptable, but consider using a draining loop if atomicity is needed.♻️ Optional: Atomic drain approach
public static List<EditorEvent> FlushPending() { - // Manual loop instead of LINQ Select to avoid allocation - var result = new List<EditorEvent>(_pendingSamples.Count); - foreach (var kvp in _pendingSamples) + var result = new List<EditorEvent>(); + // Drain atomically by removing each entry + foreach (var key in _pendingSamples.Keys.ToArray()) { - result.Add(kvp.Value.Event); + if (_pendingSamples.TryRemove(key, out var sample)) + { + result.Add(sample.Event); + } } - _pendingSamples.Clear(); return result; }
| // Enforce cache limit to prevent unbounded growth | ||
| if (_pendingSamples.Count >= MaxSampleCache) | ||
| { | ||
| CleanupExpiredSamples(nowMs); | ||
|
|
||
| // If still over limit after cleanup, force remove oldest entry | ||
| if (_pendingSamples.Count >= MaxSampleCache) | ||
| { | ||
| // Manual loop to find oldest entry (avoid LINQ allocation in hot path) | ||
| string oldestKey = null; | ||
| long oldestTimestamp = long.MaxValue; | ||
| foreach (var kvp in _pendingSamples) | ||
| { | ||
| if (kvp.Value.TimestampMs < oldestTimestamp) | ||
| { | ||
| oldestTimestamp = kvp.Value.TimestampMs; | ||
| oldestKey = kvp.Key; | ||
| } | ||
| } | ||
| if (!string.IsNullOrEmpty(oldestKey)) | ||
| { | ||
| _pendingSamples.TryRemove(oldestKey, out _); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Force-eviction can silently drop pending debounce events.
When the cache hits MaxSampleCache, the oldest entry is removed without checking if it's a debounce sample that should be recorded. This can cause silent data loss under high event load.
🛠️ Proposed fix: Record debounce samples before eviction
if (_pendingSamples.Count >= MaxSampleCache)
{
// 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;
}
}
- if (!string.IsNullOrEmpty(oldestKey))
+ if (!string.IsNullOrEmpty(oldestKey) && _pendingSamples.TryRemove(oldestKey, out _))
{
- _pendingSamples.TryRemove(oldestKey, out _);
+ // Record evicted debounce samples to prevent data loss
+ if (SamplingConfig.Strategies.TryGetValue(oldestSample.Event.Type, out var evictedStrategy) &&
+ (evictedStrategy.Mode == SamplingMode.Debounce || evictedStrategy.Mode == SamplingMode.DebounceByKey))
+ {
+ EventStore.Record(oldestSample.Event);
+ }
}
}🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/ActionTrace/Capture/Sampling/SamplingMiddleware.cs` around
lines 200 - 224, The eviction code that forcibly removes the oldest entry from
_pendingSamples when _pendingSamples.Count >= MaxSampleCache can drop debounce
samples silently; before calling _pendingSamples.TryRemove(oldestKey, out _),
retrieve the sample (TryGetValue/out var sample), detect if it is a debounce
sample (check the sample's debounce indicator, e.g. sample.IsDebounce or
sample.Type == SampleType.Debounce), and if so invoke the middleware's normal
recording/flushing path for finalized debounce samples (the same code used when
debounce samples are normally emitted) to persist/emit it, then remove the
entry; otherwise proceed with the TryRemove as before — use existing helpers
like CleanupExpiredSamples and the middleware's sample-recording method to avoid
duplicating logic.
| private string GenerateCallId() | ||
| { | ||
| // Compact ID: tool name + timestamp + random suffix | ||
| long timestamp = _startTimestampMs % 1000000; // Last 6 digits of timestamp | ||
| int random = UnityEngine.Random.Range(1000, 9999); | ||
| return $"{_toolId}_{timestamp}_{random}"; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UnityEngine.Random is not thread-safe and requires main thread.
UnityEngine.Random.Range() can only be called from Unity's main thread. If a ToolCallScope is created on a background thread (which is possible with ExecuteAsync or direct construction), this will throw a UnityException.
🐛 Proposed fix using thread-safe random
+ private static readonly ThreadLocal<System.Random> _random =
+ new(() => new System.Random(Guid.NewGuid().GetHashCode()));
+
private string GenerateCallId()
{
// Compact ID: tool name + timestamp + random suffix
long timestamp = _startTimestampMs % 1000000; // Last 6 digits of timestamp
- int random = UnityEngine.Random.Range(1000, 9999);
+ int random = _random.Value.Next(1000, 9999);
return $"{_toolId}_{timestamp}_{random}";
}📝 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.
| private string GenerateCallId() | |
| { | |
| // Compact ID: tool name + timestamp + random suffix | |
| long timestamp = _startTimestampMs % 1000000; // Last 6 digits of timestamp | |
| int random = UnityEngine.Random.Range(1000, 9999); | |
| return $"{_toolId}_{timestamp}_{random}"; | |
| } | |
| private static readonly ThreadLocal<System.Random> _random = | |
| new(() => new System.Random(Guid.NewGuid().GetHashCode())); | |
| private string GenerateCallId() | |
| { | |
| // Compact ID: tool name + timestamp + random suffix | |
| long timestamp = _startTimestampMs % 1000000; // Last 6 digits of timestamp | |
| int random = _random.Value.Next(1000, 9999); | |
| return $"{_toolId}_{timestamp}_{random}"; | |
| } |
🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/ActionTrace/Context/ToolCallScope.cs` around lines 334 -
340, GenerateCallId in ToolCallScope uses UnityEngine.Random.Range which
requires the Unity main thread; replace it with a thread-safe RNG (e.g.,
System.Security.Cryptography.RandomNumberGenerator.GetInt32 or a
ThreadLocal<System.Random>) so GenerateCallId can be called from background
threads. Update GenerateCallId to compute the random suffix with a thread-safe
call (keep the same numeric range 1000–9999) and leave the rest of the ID format
unchanged so background construction via ExecuteAsync or direct construction no
longer throws UnityException.
| if (t.IsFaulted) | ||
| { | ||
| scope.Fail(t.Exception?.Message ?? "Async faulted"); | ||
| throw t.Exception ?? new Exception("Async task faulted"); | ||
| } | ||
| else | ||
| { | ||
| scope.Complete(t.Result?.ToString() ?? ""); | ||
| return t.Result; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Task cancellation not handled - will throw wrong exception.
When t.IsCanceled is true, the code falls through to the else branch and accesses t.Result, which throws AggregateException wrapping TaskCanceledException instead of properly handling the cancellation.
🐛 Proposed fix handling cancellation
return task.ContinueWith(t =>
{
try
{
if (t.IsFaulted)
{
scope.Fail(t.Exception?.Message ?? "Async faulted");
throw t.Exception ?? new Exception("Async task faulted");
}
+ else if (t.IsCanceled)
+ {
+ scope.Fail("Task was canceled");
+ throw new System.Threading.Tasks.TaskCanceledException(t);
+ }
else
{
scope.Complete(t.Result?.ToString() ?? "");
return t.Result;
}
}📝 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.
| if (t.IsFaulted) | |
| { | |
| scope.Fail(t.Exception?.Message ?? "Async faulted"); | |
| throw t.Exception ?? new Exception("Async task faulted"); | |
| } | |
| else | |
| { | |
| scope.Complete(t.Result?.ToString() ?? ""); | |
| return t.Result; | |
| } | |
| try | |
| { | |
| if (t.IsFaulted) | |
| { | |
| scope.Fail(t.Exception?.Message ?? "Async faulted"); | |
| throw t.Exception ?? new Exception("Async task faulted"); | |
| } | |
| else if (t.IsCanceled) | |
| { | |
| scope.Fail("Task was canceled"); | |
| throw new System.Threading.Tasks.TaskCanceledException(t); | |
| } | |
| else | |
| { | |
| scope.Complete(t.Result?.ToString() ?? ""); | |
| return t.Result; | |
| } | |
| } | |
| finally | |
| { | |
| scope.Dispose(); | |
| } |
🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/ActionTrace/Context/ToolCallScope.cs` around lines 529 -
538, Add an explicit check for task cancellation before accessing t.Result: when
t.IsCanceled is true, call scope.Fail with a cancellation message (e.g., "Async
canceled") and throw a TaskCanceledException instead of falling through to the
else branch; update the block around the existing t.IsFaulted/t.Result logic in
ToolCallScope.cs (referencing the task variable t and the scope instance) to
handle t.IsCanceled first so t.Result is never accessed for canceled tasks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs`:
- Around line 792-801: The Clear handler is clearing the ScrollView children
instead of resetting the detail panel state; in OnClearClicked replace the
_detailScrollView.Clear() call with logic that resets the detail area by
clearing _detailContent's children (or removing its current detail element),
re-attaching or showing _detailPlaceholder, and ensuring any selected/detail
state is nullified so the panel is consistent (refer to _detailContent,
_detailPlaceholder, and OnClearClicked to locate the change).
In `@MCPForUnity/Editor/Windows/ActionTraceEditorWindow.uss`:
- Around line 380-383: The ContextMapping side-table currently lacks the
OperationSource enum value so the UI can only apply context-source--System; add
an OperationSource (or Source) property of type OperationSource to the
ContextMapping class, update all places that construct/save/deserialize
ContextMapping instances so they persist this value to the event store, ensure
the code that reads ContextMappings for display (used by
ActionTraceEditorWindow) uses the new OperationSource to choose the CSS class
(context-source--Human / --AI / --System), and update any
migration/serialization routines to maintain backwards compatibility for
existing mappings.
♻️ Duplicate comments (3)
MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs (2)
734-757: JSON export is corrupted by shared StringBuilder usage.
SanitizeJsonreuses the class-level_stringBuilder(line 739), butBuildJsonExportis also using_stringBuilderto build the JSON output. Each call toSanitizeJsonfrom withinBuildJsonExportclears the builder at line 740, wiping the partially built JSON.This is a critical bug that will produce corrupt/empty JSON exports.
🔧 Proposed fix - use a local StringBuilder in SanitizeJson
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); + // Use a local StringBuilder to avoid corrupting shared state + 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(); }
830-841: Refresh short-circuit skips filter/sort/context changes.The early return at lines 834-839 only considers
EventStore.Countand_searchText. If_minImportance,_showContext, or_sortModechange without new events, the list won't refresh, leaving stale data.🔧 Proposed fix - track additional state for refresh decisions
Add tracking fields:
private int _lastEventStoreCount = -1; +private string _lastSearchText = string.Empty; +private float _lastMinImportance = -1f; +private bool _lastShowContext; +private SortMode _lastSortMode = SortMode.ByTimeDesc;Update the short-circuit logic:
-if (currentStoreCount == _lastEventStoreCount && - _currentEvents.Count > 0 && - string.IsNullOrEmpty(_searchText)) +bool canSkip = currentStoreCount == _lastEventStoreCount + && _currentEvents.Count > 0 + && _lastSearchText == _searchText + && Mathf.Approximately(_lastMinImportance, _minImportance) + && _lastShowContext == _showContext + && _lastSortMode == _sortMode; +if (canSkip) { // No new events and no search criteria, skip refresh return; } _lastEventStoreCount = currentStoreCount; +_lastSearchText = _searchText; +_lastMinImportance = _minImportance; +_lastShowContext = _showContext; +_lastSortMode = _sortMode;MCPForUnity/Editor/Windows/ActionTraceEditorWindow.uss (1)
446-455: Conflicting width constraints and duplicate min-height.Lines 448-451 have issues:
min-width: 320pxconflicts withmax-width: 300px(min > max)min-heightis declared twice (lines 449-450)The detail pane may render unexpectedly due to the impossible width constraints.
🔧 Proposed fix
.detail-scroll-view { flex: 1; - min-width: 320px; - min-height: 100%; + min-width: 300px; min-height: 100px; - max-width: 300px; + max-width: 450px; background-color: `#252525`; border-left-width: 1px; border-left-color: `#3a3a3a`; }
🧹 Nitpick comments (3)
MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs (1)
997-1006: Redundant global state mutation in SetSortMode.Line 1002 unconditionally sets
BypassImportanceFilter = trueon every sort mode change. SinceCreateGUIalready sets this and stores the original value for restoration inOnDisable, this repeated mutation is redundant and could cause confusion.Consider removing this line since the bypass is already active for the window's lifetime:
💡 Suggested fix
private void SetSortMode(SortMode mode) { _sortMode = mode; - if (ActionTraceSettings.Instance != null) - ActionTraceSettings.Instance.Filtering.BypassImportanceFilter = true; - UpdateSortButtonText(); RefreshEvents(); }MCPForUnity/Editor/Windows/ActionTraceEditorWindow.uss (1)
661-683: Class-based responsive styles added, but C# implementation missing.The USS defines
.compact-layoutrules (lines 663-683), which is the correct approach for responsive behavior in Unity. However, the C# window code does not currently add/remove thecompact-layoutclass based on window size.To enable responsive behavior, add a
GeometryChangedEventhandler inCreateGUI:💡 C# code to add in ActionTraceEditorWindow.cs
rootVisualElement.RegisterCallback<GeometryChangedEvent>(evt => { bool isCompact = evt.newRect.width < 800; rootVisualElement.EnableInClassList("compact-layout", isCompact); });MCPForUnity/Editor/Tools/ActionTraceSettingsTool.cs (1)
27-63: Add null check for ActionTraceSettings.Instance for consistency with codebase patterns.While
ActionTraceSettings.Instanceincludes fallback logic (LoadSettings → CreateSettings), it can theoretically return null in edge cases (e.g., ifCreateInstancefails). Throughout the codebase, other code defensively checks for null (ActionTraceEditorWindow.cs uses?., EventStore.cs uses explicit null checks, ActionTraceViewResource.cs early-returns on null), but this method does not. Add an explicit null check to align with the defensive patterns used elsewhere:💡 Suggested improvement
try { var settings = ActionTraceSettings.Instance; + if (settings == null) + { + return new ErrorResponse("ActionTraceSettings not initialized."); + } return new SuccessResponse("Retrieved ActionTrace settings.", new
| private void OnClearClicked() | ||
| { | ||
| EventStore.Clear(); | ||
| _currentEvents.Clear(); | ||
| _lastEventStoreCount = 0; | ||
| _eventListView.RefreshItems(); | ||
| _detailScrollView.Clear(); | ||
| UpdateStatus(); | ||
| McpLog.Debug("[ActionTraceEditorWindow] Clear clicked"); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OnClearClicked clears wrong element for detail panel.
Line 798 calls _detailScrollView.Clear(), which removes child elements from the ScrollView. However, the detail content is managed separately via _detailContent and _detailPlaceholder. This may leave the detail panel in an inconsistent state.
Consider resetting the detail panel properly:
💡 Suggested fix
private void OnClearClicked()
{
EventStore.Clear();
_currentEvents.Clear();
_lastEventStoreCount = 0;
_eventListView.RefreshItems();
- _detailScrollView.Clear();
+ _selectedItem = null;
+ _detailContent?.Clear();
+ if (_detailPlaceholder != null)
+ _detailPlaceholder.style.display = DisplayStyle.Flex;
+ if (_detailContent != null)
+ _detailContent.style.display = DisplayStyle.None;
+ if (_detailActions != null)
+ _detailActions.style.display = DisplayStyle.None;
UpdateStatus();
McpLog.Debug("[ActionTraceEditorWindow] Clear clicked");
}📝 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.
| 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(); | |
| _selectedItem = null; | |
| _detailContent?.Clear(); | |
| if (_detailPlaceholder != null) | |
| _detailPlaceholder.style.display = DisplayStyle.Flex; | |
| if (_detailContent != null) | |
| _detailContent.style.display = DisplayStyle.None; | |
| if (_detailActions != null) | |
| _detailActions.style.display = DisplayStyle.None; | |
| UpdateStatus(); | |
| McpLog.Debug("[ActionTraceEditorWindow] Clear clicked"); | |
| } |
🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/Windows/ActionTraceEditorWindow.cs` around lines 792 -
801, The Clear handler is clearing the ScrollView children instead of resetting
the detail panel state; in OnClearClicked replace the _detailScrollView.Clear()
call with logic that resets the detail area by clearing _detailContent's
children (or removing its current detail element), re-attaching or showing
_detailPlaceholder, and ensuring any selected/detail state is nullified so the
panel is consistent (refer to _detailContent, _detailPlaceholder, and
OnClearClicked to locate the change).
| /* Use class-based selectors instead of data-* attributes (Unity doesn't support data-*) */ | ||
| .context-indicator.context-source--Human { color: #4caf50; } | ||
| .context-indicator.context-source--AI { color: #2196f3; } | ||
| .context-indicator.context-source--System { color: #9e9e9e; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, check for context-source references in C# code
rg -n "context-source" --type cs -C 3Repository: CoplayDev/unity-mcp
Length of output: 801
🏁 Script executed:
# Also search for ContextSource or related enums/mappings
rg -n "ContextSource|ContextMapping|ContextType" --type cs -A 2Repository: CoplayDev/unity-mcp
Length of output: 12190
🏁 Script executed:
# Read the USS file to see the actual selectors
find . -name "ActionTraceEditorWindow.uss" -exec head -400 {} \; | tail -30Repository: CoplayDev/unity-mcp
Length of output: 755
🏁 Script executed:
# Find and read the C# file's BindListItem method
find . -name "ActionTraceEditorWindow.cs" -type fRepository: CoplayDev/unity-mcp
Length of output: 118
🏁 Script executed:
# Search for OperationContext.Source property and how context source is determined
rg -n "OperationContext" --type cs -A 10 | head -80Repository: CoplayDev/unity-mcp
Length of output: 7386
🏁 Script executed:
# Look for context source enumeration or Source property
rg -n "Source.*Human|Source.*AI|class OperationContext" --type cs -B 2 -A 8Repository: CoplayDev/unity-mcp
Length of output: 8103
🏁 Script executed:
# Check OperationContext.cs file content
find . -name "OperationContext.cs" -type f -exec cat -n {} \;Repository: CoplayDev/unity-mcp
Length of output: 5350
🏁 Script executed:
# Find the BindListItem method signature and implementation
rg -n "void BindListItem|BindListItem" --type cs -B 2 -A 15 MCPForUnity/Editor/Windows/ActionTraceEditorWindow.csRepository: CoplayDev/unity-mcp
Length of output: 1434
🏁 Script executed:
# Search for methods that retrieve OperationContext by contextId
rg -n "GetContext|RetrieveContext|LookupContext|Find.*Context" --type csRepository: CoplayDev/unity-mcp
Length of output: 45
🏁 Script executed:
# Check the type of _currentEvents and how it's populated
rg -n "_currentEvents" --type cs -B 2 -A 2 MCPForUnity/Editor/Windows/ActionTraceEditorWindow.csRepository: CoplayDev/unity-mcp
Length of output: 3951
🏁 Script executed:
# Look for QueryResult or similar class that holds event + context
rg -n "class QueryResult|struct QueryResult" --type cs -A 10Repository: CoplayDev/unity-mcp
Length of output: 45
🏁 Script executed:
# Check the full BindListItem method
sed -n '320,450p' MCPForUnity/Editor/Windows/ActionTraceEditorWindow.csRepository: CoplayDev/unity-mcp
Length of output: 5731
🏁 Script executed:
# Find ActionTraceViewItem definition to understand the Context property
rg -n "class ActionTraceViewItem|struct ActionTraceViewItem" --type cs -A 20Repository: CoplayDev/unity-mcp
Length of output: 2286
🏁 Script executed:
# Check how the query populates the context information
rg -n "new ActionTraceViewItem|ActionTraceViewItem\(" --type cs -B 3 -A 3Repository: CoplayDev/unity-mcp
Length of output: 3134
🏁 Script executed:
# Check the complete ContextMapping class definition
rg -n "public sealed class ContextMapping" --type cs -A 15Repository: CoplayDev/unity-mcp
Length of output: 1546
🏁 Script executed:
# Look for any public API to retrieve OperationContext by contextId
rg -n "public.*GetContext|public.*LookupContext|public.*FindContext|OperationContextCache" --type csRepository: CoplayDev/unity-mcp
Length of output: 45
🏁 Script executed:
# Check how ContextTimeline or EventStore manages the mapping between contextId and OperationContext
sed -n '1,100p' MCPForUnity/Editor/ActionTrace/Context/ContextTimeline.csRepository: CoplayDev/unity-mcp
Length of output: 2828
🏁 Script executed:
# Check if there's a way to retrieve OperationContext by contextId in EventStore or elsewhere
rg -n "ContextId|contextId.*=>|contextStack|_contextMappings" --type cs | grep -i "operationcontext\|retrieve\|lookup\|get" | head -20Repository: CoplayDev/unity-mcp
Length of output: 739
🏁 Script executed:
# Look at the full ContextStack implementation to see if it stores contexts by ID
cat MCPForUnity/Editor/ActionTrace/Context/ContextStack.cs | head -100Repository: CoplayDev/unity-mcp
Length of output: 3189
🏁 Script executed:
# Check the complete ContextMapping definition to see if Source is stored anywhere
cat MCPForUnity/Editor/ActionTrace/Context/ContextMapping.csRepository: CoplayDev/unity-mcp
Length of output: 1896
🏁 Script executed:
# Search for any cache or lookup mechanism for OperationContext
rg -n "static.*Dictionary|static.*Cache|_contexts|_operationContexts" --type cs MCPForUnity/Editor/ActionTrace/Repository: CoplayDev/unity-mcp
Length of output: 4047
🏁 Script executed:
# Check if Source is serialized/persisted anywhere in the ContextMapping storage
rg -n "OperationSource|Source.*serialize|Source.*json" --type cs MCPForUnity/Editor/ActionTrace/Context/ MCPForUnity/Editor/ActionTrace/Core/Store/Repository: CoplayDev/unity-mcp
Length of output: 952
🏁 Script executed:
# Look at EventStore persistence to see what's actually stored
sed -n '1,60p' MCPForUnity/Editor/ActionTrace/Core/Store/EventStore.Persistence.csRepository: CoplayDev/unity-mcp
Length of output: 2434
Context source information not persisted—Human/AI classes cannot be determined at display time.
The USS selectors are correct, but the C# code can only apply context-source--System because the ContextMapping side-table stores only contextId and eventSequence—not the OperationSource value. The OperationContext.Source (Human/AI/System) only exists in the thread-local ContextStack during execution and is not persisted in the store.
To display the correct context source colors, OperationSource must be added to the ContextMapping class and persisted alongside ContextMappings in the event store.
🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/Windows/ActionTraceEditorWindow.uss` around lines 380 -
383, The ContextMapping side-table currently lacks the OperationSource enum
value so the UI can only apply context-source--System; add an OperationSource
(or Source) property of type OperationSource to the ContextMapping class, update
all places that construct/save/deserialize ContextMapping instances so they
persist this value to the event store, ensure the code that reads
ContextMappings for display (used by ActionTraceEditorWindow) uses the new
OperationSource to choose the CSS class (context-source--Human / --AI /
--System), and update any migration/serialization routines to maintain backwards
compatibility for existing mappings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because the first version doesn't have this feature yet
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@whatevertogo, understood — this is a v1 limitation and the feature isn't implemented yet. The CSS classes are in place for when you're ready to add context-source persistence in a future version. Feel free to reach out if you'd like help implementing it later!
Summary by CodeRabbit
New Features
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.
Add HookSystem
Hook UnityEvents