Skip to content

feat: Capture and format pause-point variables (locals, parameters, instance fields, UnityEngine.Object)#1682

Merged
hatayama merged 3 commits into
v3-betafrom
feat/pause-point-capture-formatter
Jul 10, 2026
Merged

feat: Capture and format pause-point variables (locals, parameters, instance fields, UnityEngine.Object)#1682
hatayama merged 3 commits into
v3-betafrom
feat/pause-point-capture-formatter

Conversation

@hatayama

@hatayama hatayama commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds the pipeline that turns a source pause-point hit's raw locals/parameters/instance into the structured variable list surfaced by wait-for-pause-point.
  • Classifies live UnityEngine.Object references into scene/prefab/asset/runtime/destroyed categories so an AI agent can act on what it's looking at.

User Impact

  • Once wired up in a later PR, hitting a source pause point will report each captured variable's name, scope (local/parameter/instance field), type, value, and — for Unity object references — enough context (hierarchy path, asset path, or instance ID) to look the object up next.
  • Handles edge cases gracefully: destroyed objects, off-main-thread captures, values whose ToString() throws, and async/iterator methods whose locals get hoisted into compiler-generated state machine fields.

Changes

  • UloopCapturedVariable, UloopCapturedVariableScope, UloopCapturedVariableUnityObjectKind (Runtime DTOs) and UloopPausePointEntry/UloopPausePointSnapshot/UloopPausePointRegistry extensions to carry captured variables through to a pause-point snapshot (HitWithCapturedVariables, IsArmed).
  • SourcePausePointVariableFormatter (Editor): formats locals/parameters (in order) and instance fields, demangles Roslyn/mcs hoisted async-local fields (<name>5__N → local) and follows the hoisted <>4__this one level deep, de-dupes names already reported as Local/Parameter, degrades UnityEngine.Object values to a placeholder off the main thread, and caps value length / variable count.
  • SourcePausePointUnityObjectClassifier (Editor): classifies a live UnityEngine.Object into SceneObject / PrefabAsset / Asset / RuntimeInstance.
  • SourcePausePointCapture (Editor): the landing point a later Harmony patch will call — checks UloopPausePointRegistry.IsArmed first so an inactive marker costs a single dictionary lookup.

Verification

  • dist/darwin-arm64/uloop compile — 0 errors, 0 warnings.
  • uloop run-tests (assembly filters): UnityCLILoop.Tests.Editor.SourcePausePointCapture 15/15 passed, UnityCLILoop.Tests.Editor.SourcePausePointResolver 15/15 passed, PausePointTests (regex filter) 30/30 passed.

Review in cubic

hatayama added 2 commits July 11, 2026 01:06
Source pause points need to report captured local/parameter/instance
field values alongside a hit, in addition to the existing bare
Hit(id) used by the handwritten UloopPausePoint.Pause("id") marker.
Adds UloopCapturedVariable (string/int only, no Editor API
dependency) plus scope/UnityObject-kind name constants, threads a
captured variable list and truncation flag through
UloopPausePointEntry/UloopPausePointSnapshot, and adds
UloopPausePointRegistry.HitWithCapturedVariables (kept separate from
Hit(id) per this project's no-overload rule) sharing a private
HitCore with the existing Hit(id) path. Also adds IsArmed(id) so the
upcoming injected Capture landing point can no-op on a single
dictionary lookup without paying reflection/formatting cost when the
marker isn't armed.

Refs: Phase 3 (ToDo 3-1) of the source pause point execution plan.
…d capture entry point

Implements ToDo 3-2/3-3: formats captured locals/parameters/instance
fields (with async/iterator hoisted-local demangling and one-level
<>4__this following), classifies live UnityEngine.Object references
into the 6-category kind set, degrades gracefully off the main thread,
and caps both value length and variable count. SourcePausePointCapture
is the landing point Harmony will call in PR4, gated on the registry's
armed check.
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds source pause-point variable capture, including formatting, Unity object classification, truncation, and safe string conversion. Captured data now flows through the pause-point registry into snapshots, with tests covering capture, arming, reflection, classification, and threading.

Changes

Source pause-point variable capture

Layer / File(s) Summary
Captured-variable contracts and snapshot propagation
Packages/src/Runtime/PausePoints/*, Packages/src/Editor/ToolContracts/AssemblyInfo.cs
Captured-variable DTOs, scopes, Unity object kinds, truncation state, thread-safe registry storage, and snapshot propagation are added.
Capture entry point and variable formatting
Packages/src/Editor/FirstPartyTools/PausePoint/*, Packages/src/Runtime/PausePoints/AssemblyInfo.cs
Source captures check armed state, format locals, parameters, and instance fields, classify Unity objects, enforce capture limits, and forward results to the registry.
Capture and formatter validation
Assets/Tests/Editor/PausePointTests.cs, Assets/Tests/Editor/SourcePausePointCapture/*
Tests cover snapshot propagation, armed-state behavior, repeated captures, formatting order, reflection, truncation, Unity objects, exception handling, and off-main-thread behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SourcePausePointCapture
  participant SourcePausePointVariableFormatter
  participant SourcePausePointUnityObjectClassifier
  participant UloopPausePointRegistry
  participant UloopPausePointSnapshot
  SourcePausePointCapture->>UloopPausePointRegistry: Check armed state
  SourcePausePointCapture->>SourcePausePointVariableFormatter: Format captured values
  SourcePausePointVariableFormatter->>SourcePausePointUnityObjectClassifier: Classify Unity objects
  SourcePausePointCapture->>UloopPausePointRegistry: Record formatted variables
  UloopPausePointRegistry->>UloopPausePointSnapshot: Include captured variables and truncation
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.34% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: capturing and formatting pause-point variables and UnityEngine.Object values.
Description check ✅ Passed The description is directly related to the changeset and accurately describes the new capture, formatting, and classification pipeline.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pause-point-capture-formatter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointVariableFormatter.cs (1)

166-206: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider value equality for UloopCapturedVariable to make tests robust.

The DTO doesn't override Equals/GetHashCode, so PausePointTests line 394 (Is.EqualTo(capturedVariables)) passes only because the registry stores the same array reference. If the implementation ever copies the array or reconstructs DTOs, the test would silently break. Making UloopCapturedVariable a record (or adding IEquatable<UloopCapturedVariable>) would make value comparisons explicit and future-proof.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointVariableFormatter.cs`
around lines 166 - 206, Make UloopCapturedVariable use value-based equality by
converting it to a record or implementing IEquatable<UloopCapturedVariable> with
matching Equals and GetHashCode behavior across all fields. Update its
definition and preserve existing construction semantics so PausePointTests
comparisons remain correct even when arrays or DTO instances are copied or
reconstructed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointVariableFormatter.cs`:
- Around line 166-206: Make UloopCapturedVariable use value-based equality by
converting it to a record or implementing IEquatable<UloopCapturedVariable> with
matching Equals and GetHashCode behavior across all fields. Update its
definition and preserve existing construction semantics so PausePointTests
comparisons remain correct even when arrays or DTO instances are copied or
reconstructed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a8f4f284-6da5-469a-8266-eb45edb8337d

📥 Commits

Reviewing files that changed from the base of the PR and between 2cf5fe2 and d8bee87.

⛔ Files ignored due to path filters (11)
  • Assets/Tests/Editor/SourcePausePointCapture.meta is excluded by none and included by none
  • Assets/Tests/Editor/SourcePausePointCapture/SourcePausePointCaptureTests.cs.meta is excluded by none and included by none
  • Assets/Tests/Editor/SourcePausePointCapture/SourcePausePointVariableFormatterTests.cs.meta is excluded by none and included by none
  • Assets/Tests/Editor/SourcePausePointCapture/UnityCLILoop.Tests.Editor.SourcePausePointCapture.asmdef is excluded by none and included by none
  • Assets/Tests/Editor/SourcePausePointCapture/UnityCLILoop.Tests.Editor.SourcePausePointCapture.asmdef.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointCapture.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointUnityObjectClassifier.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointVariableFormatter.cs.meta is excluded by none and included by none
  • Packages/src/Runtime/PausePoints/UloopCapturedVariable.cs.meta is excluded by none and included by none
  • Packages/src/Runtime/PausePoints/UloopCapturedVariableScope.cs.meta is excluded by none and included by none
  • Packages/src/Runtime/PausePoints/UloopCapturedVariableUnityObjectKind.cs.meta is excluded by none and included by none
📒 Files selected for processing (15)
  • Assets/Tests/Editor/PausePointTests.cs
  • Assets/Tests/Editor/SourcePausePointCapture/SourcePausePointCaptureTests.cs
  • Assets/Tests/Editor/SourcePausePointCapture/SourcePausePointVariableFormatterTests.cs
  • Packages/src/Editor/FirstPartyTools/PausePoint/AssemblyInfo.cs
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointCapture.cs
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointConstants.cs
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointUnityObjectClassifier.cs
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointVariableFormatter.cs
  • Packages/src/Runtime/PausePoints/AssemblyInfo.cs
  • Packages/src/Runtime/PausePoints/UloopCapturedVariable.cs
  • Packages/src/Runtime/PausePoints/UloopCapturedVariableScope.cs
  • Packages/src/Runtime/PausePoints/UloopCapturedVariableUnityObjectKind.cs
  • Packages/src/Runtime/PausePoints/UloopPausePointEntry.cs
  • Packages/src/Runtime/PausePoints/UloopPausePointRegistry.cs
  • Packages/src/Runtime/PausePoints/UloopPausePointSnapshot.cs

…fication coverage

Addresses Fable 5 review of PR #1682:
- Separate "count cap reached" (stops enumeration) from "a value was
  truncated" (continues); a single over-long value no longer drops
  every subsequent local/parameter/instance field.
- Marshal off-main-thread Capture hits onto the main thread via
  MainThreadSwitcher, since EditorApplication.isPaused and the
  registry's bookkeeping are main-thread-only; make the registry's
  Entries dictionary concurrent so IsArmed stays safe to read
  cross-thread while inactive markers remain allocation-free.
- Report a hoisted async parameter as Scope=Parameter instead of
  InstanceField, since it belongs to the compiler-generated state
  machine rather than the calling object's own class.
- Add PrefabAsset/Asset/Component classification tests and a
  regression test proving over-long values don't truncate capture.
- Translate a stray Japanese comment to English per repo convention.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
Assets/Tests/Editor/SourcePausePointCapture/SourcePausePointVariableFormatterTests.cs (2)

282-299: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hardcoded asmdef path is fragile.

The test loads "Assets/Tests/Editor/SourcePausePointCapture/UnityCLILoop.Tests.Editor.SourcePausePointCapture.asmdef" by a hardcoded path. If the file or directory is renamed, the test fails with a non-obvious null-reference rather than a clear path-mismatch message. The Assert.That(asset, Is.Not.Null, ...) guard mitigates this, so the risk is low.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Assets/Tests/Editor/SourcePausePointCapture/SourcePausePointVariableFormatterTests.cs`
around lines 282 - 299, The test relies on a fragile hardcoded asmdef path in
Format_WithPersistedAssetValue_ClassifiesAsAsset. Replace the literal with a
path resolved from the test fixture or project asset metadata, and retain the
explicit non-null assertion so missing or renamed assets produce a clear
failure.

330-357: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Same fire-and-forget exception-swallowing concern as the capture test.

If Format throws on the background thread, completed stays false, the test times out, and the actual exception is lost. Consider capturing the task and asserting IsFaulted on timeout for a clearer failure message.

♻️ Suggested refactor
-            Task.Run(() =>
+            Task formatTask = Task.Run(() =>
             {
                 (capturedVariables, _) = SourcePausePointVariableFormatter.Format(null, Array.Empty<object>(), locals);
                 completed = true;
             });

             float timeoutTime = Time.realtimeSinceStartup + 5f;
             while (!completed && Time.realtimeSinceStartup < timeoutTime)
             {
                 yield return null;
             }

+            if (formatTask.IsFaulted)
+            {
+                Assert.Fail($"Background Format threw: {formatTask.Exception?.Flatten().InnerException?.Message}");
+            }
             Assert.That(completed, Is.True, "background formatting should complete within timeout");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Assets/Tests/Editor/SourcePausePointCapture/SourcePausePointVariableFormatterTests.cs`
around lines 330 - 357, Update
Format_WhenCalledOffMainThread_DegradesUnityObjectValueWithoutEngineApiAccess to
retain the Task returned by Task.Run and capture its exception state. On timeout
or completion, assert the task is not faulted and surface its Exception in the
failure, ensuring background Format failures are reported directly instead of
appearing only as a timeout.
Assets/Tests/Editor/SourcePausePointCapture/SourcePausePointCaptureTests.cs (1)

78-101: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Fire-and-forget Task.Run may swallow background exceptions.

If SourcePausePointCapture.Capture throws on the background thread (e.g., a future regression), the unobserved task exception won't fail the test — it'll be silently swallowed and the test will simply time out with an unhelpful "hit should be recorded" message. Consider capturing the task and asserting no fault on timeout.

♻️ Suggested refactor to surface background exceptions
             UloopPausePointRegistry.Enable("jump", 30);
             object[] locals = { "speed", 5 };

-            Task.Run(() => SourcePausePointCapture.Capture("jump", null, Array.Empty<object>(), locals));
+            Task captureTask = Task.Run(() => SourcePausePointCapture.Capture("jump", null, Array.Empty<object>(), locals));

             float timeoutTime = Time.realtimeSinceStartup + 5f;
             UloopPausePointSnapshot snapshot = UloopPausePointRegistry.GetStatus("jump");
             while (!snapshot.IsHit && Time.realtimeSinceStartup < timeoutTime)
             {
                 yield return null;
                 snapshot = UloopPausePointRegistry.GetStatus("jump");
             }

+            if (captureTask.IsFaulted)
+            {
+                Assert.Fail($"Background Capture threw: {captureTask.Exception?.Flatten().InnerException?.Message}");
+            }
             Assert.That(snapshot.IsHit, Is.True, "hit should be recorded on the main thread within timeout");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Assets/Tests/Editor/SourcePausePointCapture/SourcePausePointCaptureTests.cs`
around lines 78 - 101, Capture the Task returned by Task.Run in
Capture_WhenCalledOffMainThread_RecordsHitOnNextMainThreadTick instead of
discarding it, and observe/assert its completion and exception state during the
wait or before the final assertions. Ensure any background exception from
SourcePausePointCapture.Capture is surfaced directly, particularly when the
timeout expires, rather than producing only the generic hit assertion failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@Assets/Tests/Editor/SourcePausePointCapture/SourcePausePointCaptureTests.cs`:
- Around line 78-101: Capture the Task returned by Task.Run in
Capture_WhenCalledOffMainThread_RecordsHitOnNextMainThreadTick instead of
discarding it, and observe/assert its completion and exception state during the
wait or before the final assertions. Ensure any background exception from
SourcePausePointCapture.Capture is surfaced directly, particularly when the
timeout expires, rather than producing only the generic hit assertion failure.

In
`@Assets/Tests/Editor/SourcePausePointCapture/SourcePausePointVariableFormatterTests.cs`:
- Around line 282-299: The test relies on a fragile hardcoded asmdef path in
Format_WithPersistedAssetValue_ClassifiesAsAsset. Replace the literal with a
path resolved from the test fixture or project asset metadata, and retain the
explicit non-null assertion so missing or renamed assets produce a clear
failure.
- Around line 330-357: Update
Format_WhenCalledOffMainThread_DegradesUnityObjectValueWithoutEngineApiAccess to
retain the Task returned by Task.Run and capture its exception state. On timeout
or completion, assert the task is not faulted and surface its Exception in the
failure, ensuring background Format failures are reported directly instead of
appearing only as a timeout.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d0b860f8-9e4e-40be-b856-764ddbcd2c19

📥 Commits

Reviewing files that changed from the base of the PR and between d8bee87 and 50c0e61.

📒 Files selected for processing (6)
  • Assets/Tests/Editor/SourcePausePointCapture/SourcePausePointCaptureTests.cs
  • Assets/Tests/Editor/SourcePausePointCapture/SourcePausePointVariableFormatterTests.cs
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointCapture.cs
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointVariableFormatter.cs
  • Packages/src/Editor/ToolContracts/AssemblyInfo.cs
  • Packages/src/Runtime/PausePoints/UloopPausePointRegistry.cs
🚧 Files skipped from review as they are similar to previous changes (2)
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointCapture.cs
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointVariableFormatter.cs

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant