6.1 KiB
Fix Step-Back Duplicating Steps in steps list
Status: Ready for Implementation
Date: January 2026
Related: dap-step-backwards.md
Problem Summary
When stepping backwards during DAP debugging, the step that should be re-run appears twice in steps list:
- Once as a completed step (from
StepManipulator._completedSteps) - Once as a pending step (from the re-queued
JobSteps)
Reproduction Steps
- Run a workflow with DAP debugging enabled
- Let steps 1-4 execute (checkpoints created)
- Pause at step 5
- Step backward
- Run
steps list
Expected:
✓ 1. hello
✓ 2. Run actions/checkout@v6
✓ 3. Run echo "foo=bar" >> "$GITHUB_OUTPUT"
▶ 4. Run cat doesnotexist
5. Run a one-line script
6. Run a multi-line script
Actual (bug):
✓ 1. hello
✓ 2. Run actions/checkout@v6
✓ 3. Run echo "foo=bar" >> "$GITHUB_OUTPUT"
✓ 4. Run cat doesnotexist ← Still in _completedSteps!
▶ 5. Run cat doesnotexist ← Re-queued as current
6. Run a one-line script
7. Run a multi-line script
Root Cause
In DapDebugSession.RestoreCheckpoint() (line 1713), the session's _completedSteps and _completedStepsTracker lists are trimmed to match the checkpoint index:
// Clear completed steps list for frames after this checkpoint
while (_completedSteps.Count > checkpointIndex)
{
_completedSteps.RemoveAt(_completedSteps.Count - 1);
}
// Also clear the step tracker for manipulator sync
while (_completedStepsTracker.Count > checkpointIndex)
{
_completedStepsTracker.RemoveAt(_completedStepsTracker.Count - 1);
}
However, StepManipulator has its own separate _completedSteps list that is not being synced. When ClearChanges() is called (line 1774), it only clears change tracking data (_changes, _modifiedStepIds, _addedStepIds, _originalSteps), not the _completedSteps list.
Data Flow During Step-Back
- Steps 1-4 complete →
StepManipulator._completedSteps= [step1, step2, step3, step4] - Paused at step 5
- Step back →
RestoreCheckpoint(3, ...)is called (checkpoint index 3 = before step 4) DapDebugSession._completedStepstrimmed to 3 items ✓DapDebugSession._completedStepsTrackertrimmed to 3 items ✓StepManipulator._completedStepsNOT trimmed ✗ (still has 4 items)- Step 4 and remaining steps re-queued to
JobSteps steps listcalled →GetAllSteps()combines:_completedSteps(4 items) + queue (step4, step5, step6, step7)- Result: step4 appears twice
Solution
Add a method to IStepManipulator interface to trim completed steps to a specific count, then call it from RestoreCheckpoint.
Files to Modify
| File | Changes |
|---|---|
src/Runner.Worker/Dap/StepCommands/StepManipulator.cs |
Add TrimCompletedSteps(int count) method to interface and implementation |
src/Runner.Worker/Dap/DapDebugSession.cs |
Call TrimCompletedSteps in RestoreCheckpoint() |
Detailed Changes
1. StepManipulator.cs - Add Interface Method and Implementation
Add to IStepManipulator interface (around line 115, after ClearChanges):
/// <summary>
/// Trims the completed steps list to the specified count.
/// Used when restoring a checkpoint to sync state with the debug session.
/// </summary>
/// <param name="count">The number of completed steps to keep.</param>
void TrimCompletedSteps(int count);
Add implementation to StepManipulator class (after ClearChanges() method, around line 577):
/// <inheritdoc/>
public void TrimCompletedSteps(int count)
{
var originalCount = _completedSteps.Count;
while (_completedSteps.Count > count)
{
_completedSteps.RemoveAt(_completedSteps.Count - 1);
}
Trace.Info($"Trimmed completed steps from {originalCount} to {_completedSteps.Count}");
}
2. DapDebugSession.cs - Call TrimCompletedSteps in RestoreCheckpoint
Modify RestoreCheckpoint() (around line 1774), change:
// Reset the step manipulator to match the restored state
// It will be re-initialized when the restored step starts
_stepManipulator?.ClearChanges();
To:
// Reset the step manipulator to match the restored state
_stepManipulator?.ClearChanges();
_stepManipulator?.TrimCompletedSteps(checkpointIndex);
Why This Approach
| Approach | Pros | Cons |
|---|---|---|
Add TrimCompletedSteps() (chosen) |
Minimal change, explicit, mirrors existing DapDebugSession pattern | Requires new interface method |
| Re-initialize StepManipulator completely | Clean slate | Would lose all state, more disruptive, harder to reason about |
| Share completed steps list between DapDebugSession and StepManipulator | Single source of truth | Major refactoring, tight coupling between components |
The chosen approach is the least invasive and follows the existing pattern used by DapDebugSession for its own _completedSteps list.
Test Scenarios
After implementing this fix, verify:
-
Basic step-back:
- Run steps 1, 2, 3, 4
- Pause at step 5
- Step back
steps listshould show: ✓ 1-3 completed, ▶ 4 current, 5-6 pending (no duplicates)
-
Multiple step-backs:
- Run steps 1, 2, 3
- Step back twice (back to step 1)
steps listshould show: ▶ 1 current, 2-N pending (no completed steps)
-
Step back then forward:
- Run steps 1, 2
- Step back (to step 2)
- Step forward, let step 2 re-run and complete
- Step forward again
steps listshould show correct state without duplicates at any point
-
Reverse continue:
- Run steps 1, 2, 3, 4
- Reverse continue (back to step 1)
steps listshould show: ▶ 1 current, 2-N pending (no completed steps)
Implementation Checklist
- Add
TrimCompletedSteps(int count)toIStepManipulatorinterface - Implement
TrimCompletedStepsinStepManipulatorclass - Call
TrimCompletedSteps(checkpointIndex)inDapDebugSession.RestoreCheckpoint() - Test basic step-back scenario
- Test multiple step-backs
- Test step back then forward
- Test reverse continue