diff --git a/src/Runner.Worker/Dap/DapDebugSession.cs b/src/Runner.Worker/Dap/DapDebugSession.cs
new file mode 100644
index 000000000..edac98102
--- /dev/null
+++ b/src/Runner.Worker/Dap/DapDebugSession.cs
@@ -0,0 +1,644 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using GitHub.DistributedTask.WebApi;
+using GitHub.Runner.Common;
+using Newtonsoft.Json;
+
+namespace GitHub.Runner.Worker.Dap
+{
+ ///
+ /// Stores information about a completed step for stack trace display.
+ ///
+ internal sealed class CompletedStepInfo
+ {
+ public string DisplayName { get; set; }
+ public TaskResult? Result { get; set; }
+ public int FrameId { get; set; }
+ }
+
+ ///
+ /// Minimal production DAP debug session.
+ /// Handles step-level breakpoints with next/continue flow control,
+ /// client reconnection, and cancellation signal propagation.
+ ///
+ /// Scope inspection, REPL, step manipulation, and time-travel debugging
+ /// are intentionally deferred to future iterations.
+ ///
+ public sealed class DapDebugSession : RunnerService, IDapDebugSession
+ {
+ // Thread ID for the single job execution thread
+ private const int JobThreadId = 1;
+
+ // Frame ID for the current step (always 1)
+ private const int CurrentFrameId = 1;
+
+ // Frame IDs for completed steps start at 1000
+ private const int CompletedFrameIdBase = 1000;
+
+ private IDapServer _server;
+ private DapSessionState _state = DapSessionState.WaitingForConnection;
+
+ // Synchronization for step execution
+ private TaskCompletionSource _commandTcs;
+ private readonly object _stateLock = new object();
+
+ // Handshake completion — signaled when configurationDone is received
+ private readonly TaskCompletionSource _handshakeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ // Whether to pause before the next step (set by 'next' command)
+ private bool _pauseOnNextStep = true;
+
+ // Current execution context
+ private IStep _currentStep;
+ private IExecutionContext _jobContext;
+ private int _currentStepIndex;
+
+ // Track completed steps for stack trace
+ private readonly List _completedSteps = new List();
+ private int _nextCompletedFrameId = CompletedFrameIdBase;
+
+ // Client connection tracking for reconnection support
+ private volatile bool _isClientConnected;
+
+ public bool IsActive =>
+ _state == DapSessionState.Ready ||
+ _state == DapSessionState.Paused ||
+ _state == DapSessionState.Running;
+
+ public DapSessionState State => _state;
+
+ public override void Initialize(IHostContext hostContext)
+ {
+ base.Initialize(hostContext);
+ Trace.Info("DapDebugSession initialized");
+ }
+
+ public void SetDapServer(IDapServer server)
+ {
+ _server = server;
+ Trace.Info("DAP server reference set");
+ }
+
+ public async Task WaitForHandshakeAsync(CancellationToken cancellationToken)
+ {
+ Trace.Info("Waiting for DAP handshake (configurationDone)...");
+
+ using (cancellationToken.Register(() => _handshakeTcs.TrySetCanceled()))
+ {
+ await _handshakeTcs.Task;
+ }
+
+ Trace.Info("DAP handshake complete, session is ready");
+ }
+
+ #region Message Dispatch
+
+ public async Task HandleMessageAsync(string messageJson, CancellationToken cancellationToken)
+ {
+ Request request = null;
+ try
+ {
+ request = JsonConvert.DeserializeObject(messageJson);
+ if (request == null)
+ {
+ Trace.Warning("Failed to deserialize DAP request");
+ return;
+ }
+
+ Trace.Info($"Handling DAP request: {request.Command}");
+
+ var response = request.Command switch
+ {
+ "initialize" => HandleInitialize(request),
+ "attach" => HandleAttach(request),
+ "configurationDone" => HandleConfigurationDone(request),
+ "disconnect" => HandleDisconnect(request),
+ "threads" => HandleThreads(request),
+ "stackTrace" => HandleStackTrace(request),
+ "scopes" => HandleScopes(request),
+ "variables" => HandleVariables(request),
+ "continue" => HandleContinue(request),
+ "next" => HandleNext(request),
+ "setBreakpoints" => HandleSetBreakpoints(request),
+ "setExceptionBreakpoints" => HandleSetExceptionBreakpoints(request),
+ _ => CreateResponse(request, false, $"Unsupported command: {request.Command}", body: null)
+ };
+
+ response.RequestSeq = request.Seq;
+ response.Command = request.Command;
+
+ _server?.SendResponse(response);
+ }
+ catch (Exception ex)
+ {
+ Trace.Error($"Error handling request '{request?.Command}': {ex}");
+ if (request != null)
+ {
+ var errorResponse = CreateResponse(request, false, ex.Message, body: null);
+ errorResponse.RequestSeq = request.Seq;
+ errorResponse.Command = request.Command;
+ _server?.SendResponse(errorResponse);
+ }
+ }
+
+ await Task.CompletedTask;
+ }
+
+ #endregion
+
+ #region DAP Request Handlers
+
+ private Response HandleInitialize(Request request)
+ {
+ if (request.Arguments != null)
+ {
+ try
+ {
+ var clientCaps = request.Arguments.ToObject();
+ Trace.Info($"Client: {clientCaps?.ClientName ?? clientCaps?.ClientId ?? "unknown"}");
+ }
+ catch (Exception ex)
+ {
+ Trace.Warning($"Failed to parse initialize arguments: {ex.Message}");
+ }
+ }
+
+ _state = DapSessionState.Initializing;
+
+ // Build capabilities — MVP only supports configurationDone
+ var capabilities = new Capabilities
+ {
+ SupportsConfigurationDoneRequest = true,
+ // All other capabilities are false for MVP
+ SupportsFunctionBreakpoints = false,
+ SupportsConditionalBreakpoints = false,
+ SupportsEvaluateForHovers = false,
+ SupportsStepBack = false,
+ SupportsSetVariable = false,
+ SupportsRestartFrame = false,
+ SupportsGotoTargetsRequest = false,
+ SupportsStepInTargetsRequest = false,
+ SupportsCompletionsRequest = false,
+ SupportsModulesRequest = false,
+ SupportsTerminateRequest = false,
+ SupportTerminateDebuggee = false,
+ SupportsDelayedStackTraceLoading = false,
+ SupportsLoadedSourcesRequest = false,
+ SupportsProgressReporting = false,
+ SupportsRunInTerminalRequest = false,
+ SupportsCancelRequest = false,
+ SupportsExceptionOptions = false,
+ SupportsValueFormattingOptions = false,
+ SupportsExceptionInfoRequest = false,
+ };
+
+ // Send initialized event after a brief delay to ensure the
+ // response is delivered first (DAP spec requirement)
+ _ = Task.Run(async () =>
+ {
+ await Task.Delay(50);
+ _server?.SendEvent(new Event
+ {
+ EventType = "initialized"
+ });
+ Trace.Info("Sent initialized event");
+ });
+
+ Trace.Info("Initialize request handled, capabilities sent");
+ return CreateResponse(request, true, body: capabilities);
+ }
+
+ private Response HandleAttach(Request request)
+ {
+ Trace.Info("Attach request handled");
+ return CreateResponse(request, true, body: null);
+ }
+
+ private Response HandleConfigurationDone(Request request)
+ {
+ lock (_stateLock)
+ {
+ _state = DapSessionState.Ready;
+ }
+
+ _handshakeTcs.TrySetResult(true);
+
+ Trace.Info("Configuration done, debug session is ready");
+ return CreateResponse(request, true, body: null);
+ }
+
+ private Response HandleDisconnect(Request request)
+ {
+ Trace.Info("Disconnect request received");
+
+ lock (_stateLock)
+ {
+ _state = DapSessionState.Terminated;
+
+ // Release any blocked step execution
+ _commandTcs?.TrySetResult(DapCommand.Disconnect);
+ }
+
+ return CreateResponse(request, true, body: null);
+ }
+
+ private Response HandleThreads(Request request)
+ {
+ var body = new ThreadsResponseBody
+ {
+ Threads = new List
+ {
+ new Thread
+ {
+ Id = JobThreadId,
+ Name = _jobContext != null
+ ? $"Job: {_jobContext.GetGitHubContext("job") ?? "workflow job"}"
+ : "Job Thread"
+ }
+ }
+ };
+
+ return CreateResponse(request, true, body: body);
+ }
+
+ private Response HandleStackTrace(Request request)
+ {
+ var frames = new List();
+
+ // Add current step as the top frame
+ if (_currentStep != null)
+ {
+ var resultIndicator = _currentStep.ExecutionContext?.Result != null
+ ? $" [{_currentStep.ExecutionContext.Result}]"
+ : " [running]";
+
+ frames.Add(new StackFrame
+ {
+ Id = CurrentFrameId,
+ Name = $"{_currentStep.DisplayName ?? "Current Step"}{resultIndicator}",
+ Line = _currentStepIndex + 1,
+ Column = 1,
+ PresentationHint = "normal"
+ });
+ }
+ else
+ {
+ frames.Add(new StackFrame
+ {
+ Id = CurrentFrameId,
+ Name = "(no step executing)",
+ Line = 0,
+ Column = 1,
+ PresentationHint = "subtle"
+ });
+ }
+
+ // Add completed steps as additional frames (most recent first)
+ for (int i = _completedSteps.Count - 1; i >= 0; i--)
+ {
+ var completedStep = _completedSteps[i];
+ var resultStr = completedStep.Result.HasValue ? $" [{completedStep.Result}]" : "";
+ frames.Add(new StackFrame
+ {
+ Id = completedStep.FrameId,
+ Name = $"{completedStep.DisplayName}{resultStr}",
+ Line = 1,
+ Column = 1,
+ PresentationHint = "subtle"
+ });
+ }
+
+ var body = new StackTraceResponseBody
+ {
+ StackFrames = frames,
+ TotalFrames = frames.Count
+ };
+
+ return CreateResponse(request, true, body: body);
+ }
+
+ private Response HandleScopes(Request request)
+ {
+ // MVP: return empty scopes — scope inspection deferred
+ return CreateResponse(request, true, body: new ScopesResponseBody
+ {
+ Scopes = new List()
+ });
+ }
+
+ private Response HandleVariables(Request request)
+ {
+ // MVP: return empty variables — variable inspection deferred
+ return CreateResponse(request, true, body: new VariablesResponseBody
+ {
+ Variables = new List()
+ });
+ }
+
+ private Response HandleContinue(Request request)
+ {
+ Trace.Info("Continue command received");
+
+ lock (_stateLock)
+ {
+ if (_state == DapSessionState.Paused)
+ {
+ _state = DapSessionState.Running;
+ _pauseOnNextStep = false;
+ _commandTcs?.TrySetResult(DapCommand.Continue);
+ }
+ }
+
+ return CreateResponse(request, true, body: new ContinueResponseBody
+ {
+ AllThreadsContinued = true
+ });
+ }
+
+ private Response HandleNext(Request request)
+ {
+ Trace.Info("Next (step over) command received");
+
+ lock (_stateLock)
+ {
+ if (_state == DapSessionState.Paused)
+ {
+ _state = DapSessionState.Running;
+ _pauseOnNextStep = true;
+ _commandTcs?.TrySetResult(DapCommand.Next);
+ }
+ }
+
+ return CreateResponse(request, true, body: null);
+ }
+
+ private Response HandleSetBreakpoints(Request request)
+ {
+ // MVP: acknowledge but don't process breakpoints
+ // All steps pause automatically via _pauseOnNextStep
+ return CreateResponse(request, true, body: new { breakpoints = Array.Empty