diff --git a/.agents/skills/uloop-pause-point/SKILL.md b/.agents/skills/uloop-pause-point/SKILL.md
index adc303b84..71bd2aa23 100644
--- a/.agents/skills/uloop-pause-point/SKILL.md
+++ b/.agents/skills/uloop-pause-point/SKILL.md
@@ -30,6 +30,25 @@ uloop await-pause-point --id "Assets/Scripts/Enemy.cs:42" --timeout-seconds 30
5. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot.
6. Clear the marker with `uloop clear-pause-point --id "Assets/Scripts/Enemy.cs:42"` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. Clearing also removes the underlying code patch, so the method runs untouched afterwards.
+## Capture Modes and History
+
+Choose the capture mode when enabling a pause point:
+
+- `single-shot` is the default. The first hit pauses Unity and disarms the marker.
+- `continuous` pauses Unity on every hit and remains armed. Each hit adds a frame to `CapturedVariableHistory`, while `CapturedVariables` remains the latest-hit compatibility view.
+- `trace` remains armed and records each hit without pausing Unity.
+
+`--max-history` defaults to 20 and accepts values from 1 through 100. When the limit is exceeded, the oldest frames are dropped and `HistoryDroppedCount` reports how many were removed. `pause-point-status` returns the current `Mode`, `MaxHistory`, history frames, and dropped count.
+
+To inspect value changes one Editor Step at a time, enable a `continuous` pause point on a line inside `Update` or `FixedUpdate`, trigger the first hit, then run:
+
+```bash
+uloop control-play-mode --action Step
+uloop pause-point-status --id "Assets/Scripts/Enemy.cs:42"
+```
+
+Repeat the Step/status pair to inspect the history tail. A new frame is captured only when the patched line executes during that frame; event handlers such as `OnCollisionEnter` update only when the event occurs again. Use a longer `--timeout-seconds` for a Step session because the enable-time timeout does not extend after hits.
+
## Reading CapturedVariables
Every hit response embeds `CapturedVariables`: the method's in-scope locals, its parameters, and the `this` instance fields, captured at the exact moment execution reached the patched line. Values are point-in-time strings, not live references, so they stay valid as evidence even after Unity resumes.
diff --git a/.claude/skills/uloop-pause-point/SKILL.md b/.claude/skills/uloop-pause-point/SKILL.md
index adc303b84..71bd2aa23 100644
--- a/.claude/skills/uloop-pause-point/SKILL.md
+++ b/.claude/skills/uloop-pause-point/SKILL.md
@@ -30,6 +30,25 @@ uloop await-pause-point --id "Assets/Scripts/Enemy.cs:42" --timeout-seconds 30
5. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot.
6. Clear the marker with `uloop clear-pause-point --id "Assets/Scripts/Enemy.cs:42"` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. Clearing also removes the underlying code patch, so the method runs untouched afterwards.
+## Capture Modes and History
+
+Choose the capture mode when enabling a pause point:
+
+- `single-shot` is the default. The first hit pauses Unity and disarms the marker.
+- `continuous` pauses Unity on every hit and remains armed. Each hit adds a frame to `CapturedVariableHistory`, while `CapturedVariables` remains the latest-hit compatibility view.
+- `trace` remains armed and records each hit without pausing Unity.
+
+`--max-history` defaults to 20 and accepts values from 1 through 100. When the limit is exceeded, the oldest frames are dropped and `HistoryDroppedCount` reports how many were removed. `pause-point-status` returns the current `Mode`, `MaxHistory`, history frames, and dropped count.
+
+To inspect value changes one Editor Step at a time, enable a `continuous` pause point on a line inside `Update` or `FixedUpdate`, trigger the first hit, then run:
+
+```bash
+uloop control-play-mode --action Step
+uloop pause-point-status --id "Assets/Scripts/Enemy.cs:42"
+```
+
+Repeat the Step/status pair to inspect the history tail. A new frame is captured only when the patched line executes during that frame; event handlers such as `OnCollisionEnter` update only when the event occurs again. Use a longer `--timeout-seconds` for a Step session because the enable-time timeout does not extend after hits.
+
## Reading CapturedVariables
Every hit response embeds `CapturedVariables`: the method's in-scope locals, its parameters, and the `this` instance fields, captured at the exact moment execution reached the patched line. Values are point-in-time strings, not live references, so they stay valid as evidence even after Unity resumes.
diff --git a/Assets/Tests/Editor/PausePointCaptureModeTests.cs b/Assets/Tests/Editor/PausePointCaptureModeTests.cs
new file mode 100644
index 000000000..37e310207
--- /dev/null
+++ b/Assets/Tests/Editor/PausePointCaptureModeTests.cs
@@ -0,0 +1,185 @@
+using System;
+using System.Linq;
+
+using NUnit.Framework;
+
+using io.github.hatayama.UnityCliLoop.Runtime;
+
+namespace io.github.hatayama.UnityCliLoop.Tests.Editor
+{
+ ///
+ /// Verifies pause point capture modes and bounded hit history behavior.
+ ///
+ [TestFixture]
+ public sealed class PausePointCaptureModeTests
+ {
+ private DateTime _nowUtc;
+ private FakePausePointPauseController _pauseController;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _nowUtc = new DateTime(2026, 6, 3, 0, 0, 0, DateTimeKind.Utc);
+ _pauseController = new FakePausePointPauseController();
+ UloopPausePointRegistry.ConfigureForTests(_pauseController, () => _nowUtc);
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ UloopPausePointRegistry.ResetForTests();
+ }
+
+ ///
+ /// Verifies continuous capture keeps the marker armed while exposing ordered history and the latest variables.
+ ///
+ [Test]
+ public void Continuous_WhenHitMultipleTimes_PreservesHistoryAndLatestVariables()
+ {
+ UloopCapturedVariable[] firstVariables = { CreateVariable("speed", "1") };
+ UloopCapturedVariable[] secondVariables = { CreateVariable("speed", "2") };
+ UloopPausePointRegistry.Enable("jump", 30, UloopPausePointCaptureMode.Continuous, 20);
+
+ UloopPausePointRegistry.HitWithCapturedVariables("jump", firstVariables, false);
+ UloopPausePointRegistry.HitWithCapturedVariables("jump", secondVariables, false);
+
+ UloopPausePointSnapshot snapshot = UloopPausePointRegistry.GetStatus("jump");
+
+ Assert.That(snapshot.IsEnabled, Is.True);
+ Assert.That(snapshot.HitCount, Is.EqualTo(2));
+ Assert.That(snapshot.CapturedVariables.Single().Value, Is.EqualTo("2"));
+ Assert.That(snapshot.CapturedVariableHistory.Select(frame => frame.HitSequence), Is.EqualTo(new[] { 1, 2 }));
+ Assert.That(snapshot.CapturedVariableHistory.Last().CapturedVariables.Single().Value, Is.EqualTo("2"));
+ Assert.That(_pauseController.PauseCount, Is.EqualTo(2));
+ }
+
+ ///
+ /// Verifies trace capture records a hit without requesting an Editor pause.
+ ///
+ [Test]
+ public void Trace_WhenHit_DoesNotPauseAndKeepsMarkerArmed()
+ {
+ UloopPausePointRegistry.Enable("trace", 30, UloopPausePointCaptureMode.Trace, 20);
+
+ UloopPausePointSnapshot snapshot = UloopPausePointRegistry.Hit("trace");
+
+ Assert.That(snapshot.IsEnabled, Is.True);
+ Assert.That(snapshot.HitCount, Is.EqualTo(1));
+ Assert.That(snapshot.Mode, Is.EqualTo(UloopPausePointCaptureMode.Trace));
+ Assert.That(snapshot.CapturedVariableHistory, Has.Count.EqualTo(1));
+ Assert.That(_pauseController.PauseCount, Is.EqualTo(0));
+ }
+
+ ///
+ /// Verifies the bounded history drops the oldest frame and reports the dropped count.
+ ///
+ [Test]
+ public void History_WhenMaxHistoryIsExceeded_DropsOldestFrames()
+ {
+ UloopPausePointRegistry.Enable("jump", 30, UloopPausePointCaptureMode.Trace, 2);
+
+ UloopPausePointRegistry.Hit("jump");
+ UloopPausePointRegistry.Hit("jump");
+ UloopPausePointRegistry.Hit("jump");
+
+ UloopPausePointSnapshot snapshot = UloopPausePointRegistry.GetStatus("jump");
+
+ Assert.That(snapshot.CapturedVariableHistory.Select(frame => frame.HitSequence), Is.EqualTo(new[] { 2, 3 }));
+ Assert.That(snapshot.HistoryDroppedCount, Is.EqualTo(1));
+ }
+
+ ///
+ /// Verifies the default single-shot mode retains current disarm behavior and records its hit.
+ ///
+ [Test]
+ public void SingleShot_WhenHit_DisarmsAndKeepsOneHistoryFrame()
+ {
+ UloopPausePointRegistry.Enable("jump", 30);
+
+ UloopPausePointRegistry.Hit("jump");
+ UloopPausePointRegistry.Hit("jump");
+
+ UloopPausePointSnapshot snapshot = UloopPausePointRegistry.GetStatus("jump");
+
+ Assert.That(snapshot.IsEnabled, Is.False);
+ Assert.That(snapshot.HitCount, Is.EqualTo(1));
+ Assert.That(snapshot.CapturedVariableHistory, Has.Count.EqualTo(1));
+ Assert.That(_pauseController.PauseCount, Is.EqualTo(1));
+ }
+
+ ///
+ /// Verifies clearing an armed continuous marker disarms it without erasing captured history.
+ ///
+ [Test]
+ public void Clear_WhenContinuousMarkerHasHits_DisarmsAndPreservesHistory()
+ {
+ UloopPausePointRegistry.Enable("jump", 30, UloopPausePointCaptureMode.Continuous, 20);
+ UloopPausePointRegistry.Hit("jump");
+
+ UloopPausePointSnapshot snapshot = UloopPausePointRegistry.Clear("jump");
+
+ Assert.That(snapshot.IsEnabled, Is.False);
+ Assert.That(snapshot.CapturedVariableHistory, Has.Count.EqualTo(1));
+ Assert.That(snapshot.ClearedReason, Is.EqualTo(UloopPausePointClearedReason.ExplicitClear));
+ Assert.That(snapshot.Message, Is.EqualTo("Pause point cleared after 1 hit(s); capture history is preserved."));
+ }
+
+ ///
+ /// Verifies expiry after a continuous hit reports a capture-window message and retains history.
+ ///
+ [Test]
+ public void Expire_WhenContinuousMarkerHasHits_DisarmsAndPreservesHistory()
+ {
+ UloopPausePointRegistry.Enable("jump", 1, UloopPausePointCaptureMode.Continuous, 20);
+ UloopPausePointRegistry.Hit("jump");
+ _nowUtc = _nowUtc.AddSeconds(2);
+
+ UloopPausePointSnapshot snapshot = UloopPausePointRegistry.GetStatus("jump");
+
+ Assert.That(snapshot.Status, Is.EqualTo(UloopPausePointStatus.Expired));
+ Assert.That(snapshot.CapturedVariableHistory, Has.Count.EqualTo(1));
+ Assert.That(snapshot.Message, Is.EqualTo("Pause point capture window expired after 1 hit(s); capture history is preserved."));
+ }
+
+ ///
+ /// Verifies re-enabling a marker starts a fresh generation with empty history.
+ ///
+ [Test]
+ public void Enable_WhenMarkerIsReenabled_ResetsHistory()
+ {
+ UloopPausePointRegistry.Enable("jump", 30, UloopPausePointCaptureMode.Trace, 2);
+ UloopPausePointRegistry.Hit("jump");
+
+ UloopPausePointSnapshot snapshot = UloopPausePointRegistry.Enable(
+ "jump", 30, UloopPausePointCaptureMode.Trace, 2);
+
+ Assert.That(snapshot.HitCount, Is.EqualTo(0));
+ Assert.That(snapshot.CapturedVariableHistory, Is.Empty);
+ Assert.That(snapshot.HistoryDroppedCount, Is.EqualTo(0));
+ }
+
+ private static UloopCapturedVariable CreateVariable(string name, string value)
+ {
+ return new UloopCapturedVariable(
+ name,
+ UloopCapturedVariableScope.Local,
+ "System.String",
+ value,
+ string.Empty,
+ string.Empty,
+ 0);
+ }
+
+ private sealed class FakePausePointPauseController : IUloopPausePointPauseController
+ {
+ public int PauseCount { get; private set; }
+ public bool IsPlaying => true;
+ public bool IsPaused => PauseCount > 0;
+
+ public void Pause()
+ {
+ PauseCount++;
+ }
+ }
+ }
+}
diff --git a/Assets/Tests/Editor/PausePointCaptureModeTests.cs.meta b/Assets/Tests/Editor/PausePointCaptureModeTests.cs.meta
new file mode 100644
index 000000000..02f102676
--- /dev/null
+++ b/Assets/Tests/Editor/PausePointCaptureModeTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: cf4f284db0df5429a8d287aa1833ae45
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Tests/Editor/PausePointStatusResponseContractTests.cs b/Assets/Tests/Editor/PausePointStatusResponseContractTests.cs
index 3e56fb8de..d65130ff6 100644
--- a/Assets/Tests/Editor/PausePointStatusResponseContractTests.cs
+++ b/Assets/Tests/Editor/PausePointStatusResponseContractTests.cs
@@ -31,6 +31,20 @@ public void PausePointStatusResponse_WhenSerialized_MatchesSharedContractFieldSh
IsHit = true,
HitCount = 1,
TimeoutSeconds = 30,
+ Mode = "continuous",
+ MaxHistory = 20,
+ CapturedVariableHistory = new List
+ {
+ new()
+ {
+ HitSequence = 1,
+ FrameCount = 42,
+ HitAtUtc = "2026-06-03T00:00:01.0000000Z",
+ CapturedVariables = new List(),
+ Truncated = false
+ }
+ },
+ HistoryDroppedCount = 0,
Expired = false,
EnabledAtUtc = "2026-06-03T00:00:00.0000000Z",
ElapsedSinceEnabledMilliseconds = 1200,
diff --git a/Assets/Tests/Editor/PausePointToolModeTests.cs b/Assets/Tests/Editor/PausePointToolModeTests.cs
new file mode 100644
index 000000000..a02984b61
--- /dev/null
+++ b/Assets/Tests/Editor/PausePointToolModeTests.cs
@@ -0,0 +1,147 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+
+using io.github.hatayama.UnityCliLoop.FirstPartyTools;
+using io.github.hatayama.UnityCliLoop.Infrastructure;
+using io.github.hatayama.UnityCliLoop.Runtime;
+
+namespace io.github.hatayama.UnityCliLoop.Tests.Editor
+{
+ ///
+ /// Verifies pause point tool mode parameters and response history mapping.
+ ///
+ [TestFixture]
+ public sealed class PausePointToolModeTests
+ {
+ private FakePausePointPauseController _pauseController;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _pauseController = new FakePausePointPauseController();
+ UloopPausePointRegistry.ConfigureForTests(_pauseController, () => DateTime.UtcNow);
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ UloopPausePointRegistry.ResetForTests();
+ }
+
+ ///
+ /// Verifies mode and max-history JSON parameters reach the enable response.
+ ///
+ [Test]
+ public async Task Enable_WhenModeAndMaxHistoryAreProvided_MapsParameters()
+ {
+ EnablePausePointTool tool = new();
+ JObject parameters = new()
+ {
+ ["id"] = "jump",
+ ["timeoutSeconds"] = 30,
+ ["mode"] = UloopPausePointCaptureMode.Continuous,
+ ["maxHistory"] = 2
+ };
+
+ PausePointResponse response = (PausePointResponse)await tool.ExecuteAsync(parameters, CancellationToken.None);
+
+ Assert.That(response.Success, Is.True);
+ Assert.That(response.Mode, Is.EqualTo(UloopPausePointCaptureMode.Continuous));
+ Assert.That(response.MaxHistory, Is.EqualTo(2));
+ }
+
+ ///
+ /// Verifies an unknown mode returns a user-facing validation failure with supported values.
+ ///
+ [Test]
+ public async Task Enable_WhenModeIsUnknown_ReturnsSupportedModeValidationFailure()
+ {
+ EnablePausePointTool tool = new();
+ JObject parameters = new()
+ {
+ ["id"] = "jump",
+ ["mode"] = "unknown",
+ ["maxHistory"] = 20
+ };
+
+ PausePointResponse response = (PausePointResponse)await tool.ExecuteAsync(parameters, CancellationToken.None);
+
+ Assert.That(response.Success, Is.False);
+ Assert.That(response.Message, Is.EqualTo("Mode must be one of: single-shot, continuous, trace."));
+ }
+
+ ///
+ /// Verifies an out-of-range max-history value returns a user-facing validation failure.
+ ///
+ [Test]
+ public async Task Enable_WhenMaxHistoryIsOutOfRange_ReturnsValidationFailure()
+ {
+ EnablePausePointTool tool = new();
+ JObject parameters = new()
+ {
+ ["id"] = "jump",
+ ["mode"] = UloopPausePointCaptureMode.SingleShot,
+ ["maxHistory"] = UloopPausePointRegistry.MaxHistoryLimit + 1
+ };
+
+ PausePointResponse response = (PausePointResponse)await tool.ExecuteAsync(parameters, CancellationToken.None);
+
+ Assert.That(response.Success, Is.False);
+ Assert.That(response.Message, Is.EqualTo("MaxHistory must be between 1 and 100."));
+ }
+
+ ///
+ /// Verifies a non-positive max-history value returns a validation failure.
+ ///
+ [Test]
+ public async Task Enable_WhenMaxHistoryIsZero_ReturnsValidationFailure()
+ {
+ EnablePausePointTool tool = new();
+ JObject parameters = new()
+ {
+ ["id"] = "jump",
+ ["mode"] = UloopPausePointCaptureMode.SingleShot,
+ ["maxHistory"] = 0
+ };
+
+ PausePointResponse response = (PausePointResponse)await tool.ExecuteAsync(parameters, CancellationToken.None);
+
+ Assert.That(response.Success, Is.False);
+ Assert.That(response.Message, Is.EqualTo("MaxHistory must be between 1 and 100."));
+ }
+
+ ///
+ /// Verifies the CLI-only status bridge exposes mode and captured history fields.
+ ///
+ [Test]
+ public void StatusBridge_WhenContinuousMarkerHasHit_ReturnsHistoryFields()
+ {
+ UloopPausePointRegistry.Enable("jump", 30, UloopPausePointCaptureMode.Continuous, 20);
+ UloopPausePointRegistry.Hit("jump");
+
+ PausePointStatusResponse response = PausePointStatusBridgeCommand.Execute(
+ new JObject { ["id"] = "jump" });
+
+ Assert.That(response.Mode, Is.EqualTo(UloopPausePointCaptureMode.Continuous));
+ Assert.That(response.MaxHistory, Is.EqualTo(20));
+ Assert.That(response.CapturedVariableHistory, Has.Count.EqualTo(1));
+ Assert.That(response.HistoryDroppedCount, Is.EqualTo(0));
+ }
+
+ private sealed class FakePausePointPauseController : IUloopPausePointPauseController
+ {
+ public int PauseCount { get; private set; }
+ public bool IsPlaying => true;
+ public bool IsPaused => PauseCount > 0;
+
+ public void Pause()
+ {
+ PauseCount++;
+ }
+ }
+ }
+}
diff --git a/Assets/Tests/Editor/PausePointToolModeTests.cs.meta b/Assets/Tests/Editor/PausePointToolModeTests.cs.meta
new file mode 100644
index 000000000..fcd2817bf
--- /dev/null
+++ b/Assets/Tests/Editor/PausePointToolModeTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e934cb81e3d6542e788dd37faaa6026b
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Tests/Editor/SourcePausePointCapture/SourcePausePointCaptureTests.cs b/Assets/Tests/Editor/SourcePausePointCapture/SourcePausePointCaptureTests.cs
index 403d2528f..06329f899 100644
--- a/Assets/Tests/Editor/SourcePausePointCapture/SourcePausePointCaptureTests.cs
+++ b/Assets/Tests/Editor/SourcePausePointCapture/SourcePausePointCaptureTests.cs
@@ -75,6 +75,26 @@ public void Capture_WhenPausePointWasAlreadyHit_IgnoresSecondCall()
Assert.That(_pauseController.PauseCount, Is.EqualTo(1));
}
+ ///
+ /// Verifies the source capture path records every hit for an armed continuous marker.
+ ///
+ [Test]
+ public void Capture_WhenContinuousPausePointIsEnabled_RecordsEveryFormattedHit()
+ {
+ UloopPausePointRegistry.Enable("jump", 30, UloopPausePointCaptureMode.Continuous, 20);
+
+ SourcePausePointCapture.Capture("jump", null, Array.Empty