diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1
index 474457b97..2a4cff2eb 100644
--- a/PowerShellEditorServices.build.ps1
+++ b/PowerShellEditorServices.build.ps1
@@ -202,6 +202,11 @@ task TestE2E Build, SetupHelpForTests, {
 
     # Run E2E tests in ConstrainedLanguage mode.
     if (!$script:IsNix) {
+        if (-not [Security.Principal.WindowsIdentity]::GetCurrent().Owner.IsWellKnown("BuiltInAdministratorsSid")) {
+            Write-Warning 'Skipping E2E CLM tests as they must be ran in an elevated process.'
+            return
+        }
+
         try {
             [System.Environment]::SetEnvironmentVariable("__PSLockdownPolicy", "0x80000007", [System.EnvironmentVariableTarget]::Machine);
             exec { & dotnet $script:dotnetTestArgs $script:NetRuntime.PS7 }
diff --git a/src/PowerShellEditorServices/Server/PsesDebugServer.cs b/src/PowerShellEditorServices/Server/PsesDebugServer.cs
index c058f9460..789c727b4 100644
--- a/src/PowerShellEditorServices/Server/PsesDebugServer.cs
+++ b/src/PowerShellEditorServices/Server/PsesDebugServer.cs
@@ -121,7 +121,7 @@ public void Dispose()
             // It represents the debugger on the PowerShell process we're in,
             // while a new debug server is spun up for every debugging session
             _psesHost.DebugContext.IsDebugServerActive = false;
-            _debugAdapterServer.Dispose();
+            _debugAdapterServer?.Dispose();
             _inputStream.Dispose();
             _outputStream.Dispose();
             _loggerFactory.Dispose();
diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs
index 3b7e75ec6..4f806ce39 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs
@@ -12,6 +12,7 @@
 using Microsoft.PowerShell.EditorServices.Services.DebugAdapter;
 using Microsoft.PowerShell.EditorServices.Services.PowerShell;
 using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
+using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility;
 
 namespace Microsoft.PowerShell.EditorServices.Services
 {
@@ -43,6 +44,7 @@ public async Task<List<Breakpoint>> GetBreakpointsAsync()
         {
             if (BreakpointApiUtils.SupportsBreakpointApis(_editorServicesHost.CurrentRunspace))
             {
+                _editorServicesHost.Runspace.ThrowCancelledIfUnusable();
                 return BreakpointApiUtils.GetBreakpoints(
                     _editorServicesHost.Runspace.Debugger,
                     _debugStateService.RunspaceId);
diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs
index e0854cb2b..5d32451ed 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs
@@ -16,6 +16,7 @@
 using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging;
 using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution;
 using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
+using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility;
 using Microsoft.PowerShell.EditorServices.Services.TextDocument;
 using Microsoft.PowerShell.EditorServices.Utility;
 
@@ -74,6 +75,15 @@ internal class DebugService
         /// </summary>
         public DebuggerStoppedEventArgs CurrentDebuggerStoppedEventArgs { get; private set; }
 
+        /// <summary>
+        /// Tracks whether we are running <c>Debug-Runspace</c> in an out-of-process runspace.
+        /// </summary>
+        public bool IsDebuggingRemoteRunspace
+        {
+            get => _debugContext.IsDebuggingRemoteRunspace;
+            set => _debugContext.IsDebuggingRemoteRunspace = value;
+        }
+
         #endregion
 
         #region Constructors
@@ -128,6 +138,8 @@ public async Task<BreakpointDetails[]> SetLineBreakpointsAsync(
             DscBreakpointCapability dscBreakpoints = await _debugContext.GetDscBreakpointCapabilityAsync(CancellationToken.None).ConfigureAwait(false);
 
             string scriptPath = scriptFile.FilePath;
+
+            _psesHost.Runspace.ThrowCancelledIfUnusable();
             // Make sure we're using the remote script path
             if (_psesHost.CurrentRunspace.IsOnRemoteMachine && _remoteFileManager is not null)
             {
@@ -771,22 +783,23 @@ private async Task FetchStackFramesAsync(string scriptNameOverride)
             const string callStackVarName = $"$global:{PsesGlobalVariableNamePrefix}CallStack";
             const string getPSCallStack = $"Get-PSCallStack | ForEach-Object {{ [void]{callStackVarName}.Add(@($PSItem, $PSItem.GetFrameVariables())) }}";
 
+            _psesHost.Runspace.ThrowCancelledIfUnusable();
             // If we're attached to a remote runspace, we need to serialize the list prior to
             // transport because the default depth is too shallow. From testing, we determined the
-            // correct depth is 3. The script always calls `Get-PSCallStack`. On a local machine, we
-            // just return its results. On a remote machine we serialize it first and then later
+            // correct depth is 3. The script always calls `Get-PSCallStack`. In a local runspace, we
+            // just return its results. In a remote runspace we serialize it first and then later
             // deserialize it.
-            bool isOnRemoteMachine = _psesHost.CurrentRunspace.IsOnRemoteMachine;
-            string returnSerializedIfOnRemoteMachine = isOnRemoteMachine
+            bool isRemoteRunspace = _psesHost.CurrentRunspace.Runspace.RunspaceIsRemote;
+            string returnSerializedIfInRemoteRunspace = isRemoteRunspace
                 ? $"[Management.Automation.PSSerializer]::Serialize({callStackVarName}, 3)"
                 : callStackVarName;
 
             // PSObject is used here instead of the specific type because we get deserialized
             // objects from remote sessions and want a common interface.
-            PSCommand psCommand = new PSCommand().AddScript($"[Collections.ArrayList]{callStackVarName} = @(); {getPSCallStack}; {returnSerializedIfOnRemoteMachine}");
+            PSCommand psCommand = new PSCommand().AddScript($"[Collections.ArrayList]{callStackVarName} = @(); {getPSCallStack}; {returnSerializedIfInRemoteRunspace}");
             IReadOnlyList<PSObject> results = await _executionService.ExecutePSCommandAsync<PSObject>(psCommand, CancellationToken.None).ConfigureAwait(false);
 
-            IEnumerable callStack = isOnRemoteMachine
+            IEnumerable callStack = isRemoteRunspace
                 ? (PSSerializer.Deserialize(results[0].BaseObject as string) as PSObject)?.BaseObject as IList
                 : results;
 
@@ -797,7 +810,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride)
                 // We have to use reflection to get the variable dictionary.
                 IList callStackFrameComponents = (callStackFrameItem as PSObject)?.BaseObject as IList;
                 PSObject callStackFrame = callStackFrameComponents[0] as PSObject;
-                IDictionary callStackVariables = isOnRemoteMachine
+                IDictionary callStackVariables = isRemoteRunspace
                     ? (callStackFrameComponents[1] as PSObject)?.BaseObject as IDictionary
                     : callStackFrameComponents[1] as IDictionary;
 
@@ -861,7 +874,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride)
                 {
                     stackFrameDetailsEntry.ScriptPath = scriptNameOverride;
                 }
-                else if (isOnRemoteMachine
+                else if (_psesHost.CurrentRunspace.IsOnRemoteMachine
                     && _remoteFileManager is not null
                     && !string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath))
                 {
@@ -905,83 +918,98 @@ private static string TrimScriptListingLine(PSObject scriptLineObj, ref int pref
 
         internal async void OnDebuggerStopAsync(object sender, DebuggerStopEventArgs e)
         {
-            bool noScriptName = false;
-            string localScriptPath = e.InvocationInfo.ScriptName;
-
-            // If there's no ScriptName, get the "list" of the current source
-            if (_remoteFileManager is not null && string.IsNullOrEmpty(localScriptPath))
+            try
             {
-                // Get the current script listing and create the buffer
-                PSCommand command = new PSCommand().AddScript($"list 1 {int.MaxValue}");
+                bool noScriptName = false;
+                string localScriptPath = e.InvocationInfo.ScriptName;
 
-                IReadOnlyList<PSObject> scriptListingLines =
-                    await _executionService.ExecutePSCommandAsync<PSObject>(
-                        command, CancellationToken.None).ConfigureAwait(false);
-
-                if (scriptListingLines is not null)
+                // If there's no ScriptName, get the "list" of the current source
+                if (_remoteFileManager is not null && string.IsNullOrEmpty(localScriptPath))
                 {
-                    int linePrefixLength = 0;
+                    // Get the current script listing and create the buffer
+                    PSCommand command = new PSCommand().AddScript($"list 1 {int.MaxValue}");
 
-                    string scriptListing =
-                        string.Join(
-                            Environment.NewLine,
-                            scriptListingLines
-                                .Select(o => TrimScriptListingLine(o, ref linePrefixLength))
-                                .Where(s => s is not null));
+                    IReadOnlyList<PSObject> scriptListingLines =
+                        await _executionService.ExecutePSCommandAsync<PSObject>(
+                            command, CancellationToken.None).ConfigureAwait(false);
 
-                    temporaryScriptListingPath =
-                        _remoteFileManager.CreateTemporaryFile(
-                            $"[{_psesHost.CurrentRunspace.SessionDetails.ComputerName}] {TemporaryScriptFileName}",
-                            scriptListing,
-                            _psesHost.CurrentRunspace);
+                    if (scriptListingLines is not null)
+                    {
+                        int linePrefixLength = 0;
+
+                        string scriptListing =
+                            string.Join(
+                                Environment.NewLine,
+                                scriptListingLines
+                                    .Select(o => TrimScriptListingLine(o, ref linePrefixLength))
+                                    .Where(s => s is not null));
+
+                        temporaryScriptListingPath =
+                            _remoteFileManager.CreateTemporaryFile(
+                                $"[{_psesHost.CurrentRunspace.SessionDetails.ComputerName}] {TemporaryScriptFileName}",
+                                scriptListing,
+                                _psesHost.CurrentRunspace);
+
+                        localScriptPath =
+                            temporaryScriptListingPath
+                            ?? StackFrameDetails.NoFileScriptPath;
+
+                        noScriptName = localScriptPath is not null;
+                    }
+                    else
+                    {
+                        _logger.LogWarning("Could not load script context");
+                    }
+                }
 
-                    localScriptPath =
-                        temporaryScriptListingPath
-                        ?? StackFrameDetails.NoFileScriptPath;
+                // Get call stack and variables.
+                await FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null).ConfigureAwait(false);
 
-                    noScriptName = localScriptPath is not null;
+                // If this is a remote connection and the debugger stopped at a line
+                // in a script file, get the file contents
+                if (_psesHost.CurrentRunspace.IsOnRemoteMachine
+                    && _remoteFileManager is not null
+                    && !noScriptName)
+                {
+                    localScriptPath =
+                        await _remoteFileManager.FetchRemoteFileAsync(
+                            e.InvocationInfo.ScriptName,
+                            _psesHost.CurrentRunspace).ConfigureAwait(false);
                 }
-                else
+
+                if (stackFrameDetails.Length > 0)
                 {
-                    _logger.LogWarning("Could not load script context");
+                    // Augment the top stack frame with details from the stop event
+                    if (invocationTypeScriptPositionProperty.GetValue(e.InvocationInfo) is IScriptExtent scriptExtent)
+                    {
+                        stackFrameDetails[0].StartLineNumber = scriptExtent.StartLineNumber;
+                        stackFrameDetails[0].EndLineNumber = scriptExtent.EndLineNumber;
+                        stackFrameDetails[0].StartColumnNumber = scriptExtent.StartColumnNumber;
+                        stackFrameDetails[0].EndColumnNumber = scriptExtent.EndColumnNumber;
+                    }
                 }
-            }
 
-            // Get call stack and variables.
-            await FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null).ConfigureAwait(false);
+                CurrentDebuggerStoppedEventArgs =
+                    new DebuggerStoppedEventArgs(
+                        e,
+                        _psesHost.CurrentRunspace,
+                        localScriptPath);
 
-            // If this is a remote connection and the debugger stopped at a line
-            // in a script file, get the file contents
-            if (_psesHost.CurrentRunspace.IsOnRemoteMachine
-                && _remoteFileManager is not null
-                && !noScriptName)
+                // Notify the host that the debugger is stopped.
+                DebuggerStopped?.Invoke(sender, CurrentDebuggerStoppedEventArgs);
+            }
+            catch (OperationCanceledException)
             {
-                localScriptPath =
-                    await _remoteFileManager.FetchRemoteFileAsync(
-                        e.InvocationInfo.ScriptName,
-                        _psesHost.CurrentRunspace).ConfigureAwait(false);
+                // Ignore, likely means that a remote runspace has closed.
             }
-
-            if (stackFrameDetails.Length > 0)
+            catch (Exception exception)
             {
-                // Augment the top stack frame with details from the stop event
-                if (invocationTypeScriptPositionProperty.GetValue(e.InvocationInfo) is IScriptExtent scriptExtent)
-                {
-                    stackFrameDetails[0].StartLineNumber = scriptExtent.StartLineNumber;
-                    stackFrameDetails[0].EndLineNumber = scriptExtent.EndLineNumber;
-                    stackFrameDetails[0].StartColumnNumber = scriptExtent.StartColumnNumber;
-                    stackFrameDetails[0].EndColumnNumber = scriptExtent.EndColumnNumber;
-                }
+                // Log in a catch all so we don't crash the process.
+                _logger.LogError(
+                    exception,
+                    "Error occurred while obtaining debug info. Message: {message}",
+                    exception.Message);
             }
-
-            CurrentDebuggerStoppedEventArgs =
-                new DebuggerStoppedEventArgs(
-                    e,
-                    _psesHost.CurrentRunspace,
-                    localScriptPath);
-
-            // Notify the host that the debugger is stopped.
-            DebuggerStopped?.Invoke(sender, CurrentDebuggerStoppedEventArgs);
         }
 
         private void OnDebuggerResuming(object sender, DebuggerResumingEventArgs debuggerResumingEventArgs) => CurrentDebuggerStoppedEventArgs = null;
diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs
index 6c3db9dab..2849fee4d 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs
@@ -5,6 +5,7 @@
 using System.Collections.Generic;
 using System.IO;
 using System.Management.Automation;
+using System.Management.Automation.Remoting;
 using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.Extensions.Logging;
@@ -18,6 +19,7 @@
 using Microsoft.PowerShell.EditorServices.Services.PowerShell;
 using Microsoft.PowerShell.EditorServices.Services.PowerShell.Context;
 using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace;
+using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution;
 
 namespace Microsoft.PowerShell.EditorServices.Handlers
 {
@@ -249,7 +251,11 @@ public async Task<AttachResponse> Handle(PsesAttachRequestArguments request, Can
 
                 try
                 {
-                    await _executionService.ExecutePSCommandAsync(enterPSSessionCommand, cancellationToken).ConfigureAwait(false);
+                    await _executionService.ExecutePSCommandAsync(
+                        enterPSSessionCommand,
+                        cancellationToken,
+                        PowerShellExecutionOptions.ImmediateInteractive)
+                        .ConfigureAwait(false);
                 }
                 catch (Exception e)
                 {
@@ -261,6 +267,19 @@ public async Task<AttachResponse> Handle(PsesAttachRequestArguments request, Can
                 _debugStateService.IsRemoteAttach = true;
             }
 
+            // Set up a temporary runspace changed event handler so we can ensure
+            // that the context switch is complete before attempting to debug
+            // a runspace in the target.
+            TaskCompletionSource<bool> runspaceChanged = new();
+
+            void RunspaceChangedHandler(object s, RunspaceChangedEventArgs _)
+            {
+                ((IInternalPowerShellExecutionService)s).RunspaceChanged -= RunspaceChangedHandler;
+                runspaceChanged.TrySetResult(true);
+            }
+
+            _executionService.RunspaceChanged += RunspaceChangedHandler;
+
             if (processIdIsSet && int.TryParse(request.ProcessId, out int processId) && (processId > 0))
             {
                 if (runspaceVersion.Version.Major < 5)
@@ -274,11 +293,15 @@ public async Task<AttachResponse> Handle(PsesAttachRequestArguments request, Can
 
                 try
                 {
-                    await _executionService.ExecutePSCommandAsync(enterPSHostProcessCommand, cancellationToken).ConfigureAwait(false);
+                    await _executionService.ExecutePSCommandAsync(
+                        enterPSHostProcessCommand,
+                        cancellationToken,
+                        PowerShellExecutionOptions.ImmediateInteractive)
+                        .ConfigureAwait(false);
                 }
                 catch (Exception e)
                 {
-                    string msg = $"Could not attach to process '{processId}'";
+                    string msg = $"Could not attach to process with Id: '{request.ProcessId}'";
                     _logger.LogError(e, msg);
                     throw new RpcErrorException(0, msg);
                 }
@@ -296,7 +319,11 @@ public async Task<AttachResponse> Handle(PsesAttachRequestArguments request, Can
 
                 try
                 {
-                    await _executionService.ExecutePSCommandAsync(enterPSHostProcessCommand, cancellationToken).ConfigureAwait(false);
+                    await _executionService.ExecutePSCommandAsync(
+                        enterPSHostProcessCommand,
+                        cancellationToken,
+                        PowerShellExecutionOptions.ImmediateInteractive)
+                        .ConfigureAwait(false);
                 }
                 catch (Exception e)
                 {
@@ -313,6 +340,8 @@ public async Task<AttachResponse> Handle(PsesAttachRequestArguments request, Can
                 throw new RpcErrorException(0, "A positive integer must be specified for the processId field.");
             }
 
+            await runspaceChanged.Task.ConfigureAwait(false);
+
             // Execute the Debug-Runspace command but don't await it because it
             // will block the debug adapter initialization process.  The
             // InitializedEvent will be sent as soon as the RunspaceChanged
@@ -327,13 +356,27 @@ public async Task<AttachResponse> Handle(PsesAttachRequestArguments request, Can
                     .AddCommand("Microsoft.PowerShell.Utility\\Select-Object")
                         .AddParameter("ExpandProperty", "Id");
 
-                IEnumerable<int?> ids = await _executionService.ExecutePSCommandAsync<int?>(getRunspaceIdCommand, cancellationToken).ConfigureAwait(false);
-                foreach (int? id in ids)
+                try
                 {
-                    _debugStateService.RunspaceId = id;
-                    break;
+                    IEnumerable<int?> ids = await _executionService.ExecutePSCommandAsync<int?>(
+                        getRunspaceIdCommand,
+                        cancellationToken)
+                        .ConfigureAwait(false);
+
+                    foreach (int? id in ids)
+                    {
+                        _debugStateService.RunspaceId = id;
+                        break;
 
-                    // TODO: If we don't end up setting this, we should throw
+                        // TODO: If we don't end up setting this, we should throw
+                    }
+                }
+                catch (Exception getRunspaceException)
+                {
+                    _logger.LogError(
+                        getRunspaceException,
+                        "Unable to determine runspace to attach to. Message: {message}",
+                        getRunspaceException.Message);
                 }
 
                 // TODO: We have the ID, why not just use that?
@@ -363,10 +406,11 @@ public async Task<AttachResponse> Handle(PsesAttachRequestArguments request, Can
             // Clear any existing breakpoints before proceeding
             await _breakpointService.RemoveAllBreakpointsAsync().ConfigureAwait(continueOnCapturedContext: false);
 
+            _debugService.IsDebuggingRemoteRunspace = true;
             _debugStateService.WaitingForAttach = true;
             Task nonAwaitedTask = _executionService
-                .ExecutePSCommandAsync(debugRunspaceCmd, CancellationToken.None)
-                .ContinueWith(OnExecutionCompletedAsync);
+                .ExecutePSCommandAsync(debugRunspaceCmd, CancellationToken.None, PowerShellExecutionOptions.ImmediateInteractive)
+                .ContinueWith( OnExecutionCompletedAsync, TaskScheduler.Default);
 
             if (runspaceVersion.Version.Major >= 7)
             {
@@ -395,10 +439,15 @@ public async Task OnStarted(IDebugAdapterServer server, CancellationToken cancel
 
         private async Task OnExecutionCompletedAsync(Task executeTask)
         {
+            bool isRunspaceClosed = false;
             try
             {
                 await executeTask.ConfigureAwait(false);
             }
+            catch (PSRemotingTransportException)
+            {
+                isRunspaceClosed = true;
+            }
             catch (Exception e)
             {
                 _logger.LogError(
@@ -411,14 +460,20 @@ private async Task OnExecutionCompletedAsync(Task executeTask)
 
             _debugEventHandlerService.UnregisterEventHandlers();
 
-            if (_debugStateService.IsAttachSession)
+            _debugService.IsDebuggingRemoteRunspace = false;
+
+            if (!isRunspaceClosed && _debugStateService.IsAttachSession)
             {
                 // Pop the sessions
                 if (_runspaceContext.CurrentRunspace.RunspaceOrigin == RunspaceOrigin.EnteredProcess)
                 {
                     try
                     {
-                        await _executionService.ExecutePSCommandAsync(new PSCommand().AddCommand("Exit-PSHostProcess"), CancellationToken.None).ConfigureAwait(false);
+                        await _executionService.ExecutePSCommandAsync(
+                            new PSCommand().AddCommand("Exit-PSHostProcess"),
+                            CancellationToken.None,
+                            PowerShellExecutionOptions.ImmediateInteractive)
+                            .ConfigureAwait(false);
 
                         if (_debugStateService.IsRemoteAttach)
                         {
diff --git a/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellContextFrame.cs b/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellContextFrame.cs
index b3007a0e7..2db852c6e 100644
--- a/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellContextFrame.cs
+++ b/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellContextFrame.cs
@@ -1,13 +1,17 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
 
+using System;
+using System.Diagnostics;
+using System.Text;
 using Microsoft.Extensions.Logging;
 using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace;
-using System;
+using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility;
 using SMA = System.Management.Automation;
 
 namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Context
 {
+    [DebuggerDisplay("{ToDebuggerDisplayString()}")]
     internal class PowerShellContextFrame : IDisposable
     {
         public static PowerShellContextFrame CreateForPowerShellInstance(
@@ -35,13 +39,34 @@ public PowerShellContextFrame(SMA.PowerShell powerShell, RunspaceInfo runspaceIn
 
         public PowerShellFrameType FrameType { get; }
 
+        public bool IsRepl => (FrameType & PowerShellFrameType.Repl) is not 0;
+
+        public bool IsRemote => (FrameType & PowerShellFrameType.Remote) is not 0;
+
+        public bool IsNested => (FrameType & PowerShellFrameType.Nested) is not 0;
+
+        public bool IsDebug => (FrameType & PowerShellFrameType.Debug) is not 0;
+
+        public bool IsAwaitingPop { get; set; }
+
+        public bool SessionExiting { get; set; }
+
         protected virtual void Dispose(bool disposing)
         {
             if (!disposedValue)
             {
                 if (disposing)
                 {
-                    PowerShell.Dispose();
+                    // When runspace is popping from `Exit-PSHostProcess` or similar, attempting
+                    // to dispose directly in the same frame would dead lock.
+                    if (SessionExiting)
+                    {
+                        PowerShell.DisposeWhenCompleted();
+                    }
+                    else
+                    {
+                        PowerShell.Dispose();
+                    }
                 }
 
                 disposedValue = true;
@@ -54,5 +79,40 @@ public void Dispose()
             Dispose(disposing: true);
             GC.SuppressFinalize(this);
         }
+
+#if DEBUG
+        private string ToDebuggerDisplayString()
+        {
+            StringBuilder text = new();
+
+            if ((FrameType & PowerShellFrameType.Nested) is not 0)
+            {
+                text.Append("Ne-");
+            }
+
+            if ((FrameType & PowerShellFrameType.Debug) is not 0)
+            {
+                text.Append("De-");
+            }
+
+            if ((FrameType & PowerShellFrameType.Remote) is not 0)
+            {
+                text.Append("Rem-");
+            }
+
+            if ((FrameType & PowerShellFrameType.NonInteractive) is not 0)
+            {
+                text.Append("NI-");
+            }
+
+            if ((FrameType & PowerShellFrameType.Repl) is not 0)
+            {
+                text.Append("Repl-");
+            }
+
+            text.Append(PowerShellDebugDisplay.ToDebuggerString(PowerShell));
+            return text.ToString();
+        }
+#endif
     }
 }
diff --git a/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellFrameType.cs b/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellFrameType.cs
index cb20ff8ff..9bf2fca02 100644
--- a/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellFrameType.cs
+++ b/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellFrameType.cs
@@ -8,10 +8,11 @@ namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Context
     [Flags]
     internal enum PowerShellFrameType
     {
-        Normal = 0x0,
-        Nested = 0x1,
-        Debug = 0x2,
-        Remote = 0x4,
-        NonInteractive = 0x8,
+        Normal = 0 << 0,
+        Nested = 1 << 0,
+        Debug = 1 << 1,
+        Remote = 1 << 2,
+        NonInteractive = 1 << 3,
+        Repl = 1 << 4,
     }
 }
diff --git a/src/PowerShellEditorServices/Services/PowerShell/Debugging/IPowerShellDebugContext.cs b/src/PowerShellEditorServices/Services/PowerShell/Debugging/IPowerShellDebugContext.cs
index bf78d80b1..173e5992b 100644
--- a/src/PowerShellEditorServices/Services/PowerShell/Debugging/IPowerShellDebugContext.cs
+++ b/src/PowerShellEditorServices/Services/PowerShell/Debugging/IPowerShellDebugContext.cs
@@ -14,6 +14,8 @@ internal interface IPowerShellDebugContext
 
         DebuggerStopEventArgs LastStopEventArgs { get; }
 
+        public bool IsDebuggingRemoteRunspace { get; set; }
+
         public event Action<object, DebuggerStopEventArgs> DebuggerStopped;
 
         public event Action<object, DebuggerResumingEventArgs> DebuggerResuming;
diff --git a/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs b/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs
index 60a858a4c..878caf18d 100644
--- a/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs
+++ b/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs
@@ -7,6 +7,7 @@
 using System.Threading.Tasks;
 using Microsoft.Extensions.Logging;
 using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
+using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility;
 
 namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging
 {
@@ -68,19 +69,32 @@ public PowerShellDebugContext(
         /// </summary>
         public bool IsDebugServerActive { get; set; }
 
+        /// <summary>
+        /// Tracks whether we are running <c>Debug-Runspace</c> in an out-of-process runspace.
+        /// </summary>
+        public bool IsDebuggingRemoteRunspace { get; set; }
+
         public DebuggerStopEventArgs LastStopEventArgs { get; private set; }
 
         public event Action<object, DebuggerStopEventArgs> DebuggerStopped;
         public event Action<object, DebuggerResumingEventArgs> DebuggerResuming;
         public event Action<object, BreakpointUpdatedEventArgs> BreakpointUpdated;
 
-        public Task<DscBreakpointCapability> GetDscBreakpointCapabilityAsync(CancellationToken cancellationToken) => _psesHost.CurrentRunspace.GetDscBreakpointCapabilityAsync(_logger, _psesHost, cancellationToken);
+        public Task<DscBreakpointCapability> GetDscBreakpointCapabilityAsync(CancellationToken cancellationToken)
+        {
+            _psesHost.Runspace.ThrowCancelledIfUnusable();
+            return _psesHost.CurrentRunspace.GetDscBreakpointCapabilityAsync(_logger, _psesHost, cancellationToken);
+        }
 
         // This is required by the PowerShell API so that remote debugging works. Without it, a
         // runspace may not have these options set and attempting to set breakpoints remotely fails.
-        public void EnableDebugMode() => _psesHost.Runspace.Debugger.SetDebugMode(DebugModes.LocalScript | DebugModes.RemoteScript);
+        public void EnableDebugMode()
+        {
+            _psesHost.Runspace.ThrowCancelledIfUnusable();
+            _psesHost.Runspace.Debugger.SetDebugMode(DebugModes.LocalScript | DebugModes.RemoteScript);
+        }
 
-        public void Abort() => SetDebugResuming(DebuggerResumeAction.Stop);
+        public void Abort() => SetDebugResuming(DebuggerResumeAction.Stop, isDisconnect: true);
 
         public void BreakExecution() => _psesHost.Runspace.Debugger.SetDebuggerStepMode(enabled: true);
 
@@ -92,7 +106,7 @@ public PowerShellDebugContext(
 
         public void StepOver() => SetDebugResuming(DebuggerResumeAction.StepOver);
 
-        public void SetDebugResuming(DebuggerResumeAction debuggerResumeAction)
+        public void SetDebugResuming(DebuggerResumeAction debuggerResumeAction, bool isDisconnect = false)
         {
             // NOTE: We exit because the paused/stopped debugger is currently in a prompt REPL, and
             // to resume the debugger we must exit that REPL.
@@ -108,7 +122,26 @@ public void SetDebugResuming(DebuggerResumeAction debuggerResumeAction)
             // then we'd accidentally cancel the debugged task since no prompt is running. We can
             // test this by checking if the UI's type is NullPSHostUI which is used specifically in
             // this scenario. This mostly applies to unit tests.
-            if (_psesHost.UI is not NullPSHostUI)
+            if (_psesHost.UI is NullPSHostUI)
+            {
+                return;
+            }
+
+            if (debuggerResumeAction is DebuggerResumeAction.Stop)
+            {
+                // If we're disconnecting we want to unwind all the way back to the default, local
+                // state. So we use UnwindCallStack here to ensure every context frame is cancelled.
+                if (isDisconnect)
+                {
+                    _psesHost.UnwindCallStack();
+                    return;
+                }
+
+                _psesHost.CancelIdleParentTask();
+                return;
+            }
+
+            if (_psesHost.CurrentFrame.IsRepl)
             {
                 _psesHost.CancelCurrentTask();
             }
@@ -134,7 +167,22 @@ public void ProcessDebuggerResult(DebuggerCommandResults debuggerResult)
             if (debuggerResult?.ResumeAction is not null)
             {
                 SetDebugResuming(debuggerResult.ResumeAction.Value);
+
+                // If a debugging command like `c` is specified in a nested remote
+                // debugging prompt we need to unwind the nested execution loop.
+                if (_psesHost.CurrentFrame.IsRemote)
+                {
+                    _psesHost.ForceSetExit();
+                }
+
                 RaiseDebuggerResumingEvent(new DebuggerResumingEventArgs(debuggerResult.ResumeAction.Value));
+
+                // The Terminate exception is used by the engine for flow control
+                // when it needs to unwind the callstack out of the debugger.
+                if (debuggerResult.ResumeAction is DebuggerResumeAction.Stop)
+                {
+                    throw new TerminateException();
+                }
             }
         }
 
diff --git a/src/PowerShellEditorServices/Services/PowerShell/Execution/ExecutionOptions.cs b/src/PowerShellEditorServices/Services/PowerShell/Execution/ExecutionOptions.cs
index af5d9e7e9..27bb8935a 100644
--- a/src/PowerShellEditorServices/Services/PowerShell/Execution/ExecutionOptions.cs
+++ b/src/PowerShellEditorServices/Services/PowerShell/Execution/ExecutionOptions.cs
@@ -22,6 +22,13 @@ public record ExecutionOptions
 
     public record PowerShellExecutionOptions : ExecutionOptions
     {
+        internal static PowerShellExecutionOptions ImmediateInteractive = new()
+        {
+            Priority = ExecutionPriority.Next,
+            MustRunInForeground = true,
+            InterruptCurrentForeground = true,
+        };
+
         public bool WriteOutputToHost { get; init; }
         public bool WriteInputToHost { get; init; }
         public bool ThrowOnError { get; init; } = true;
diff --git a/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs
index 56239947f..636f18ce6 100644
--- a/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs
+++ b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs
@@ -8,6 +8,7 @@
 using System.Management.Automation.Remoting;
 using System.Threading;
 using Microsoft.Extensions.Logging;
+using Microsoft.PowerShell.EditorServices.Services.PowerShell.Context;
 using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
 using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility;
 using Microsoft.PowerShell.EditorServices.Utility;
@@ -17,6 +18,8 @@ namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution
 {
     internal class SynchronousPowerShellTask<TResult> : SynchronousTask<IReadOnlyList<TResult>>
     {
+        private static readonly PowerShellExecutionOptions s_defaultPowerShellExecutionOptions = new();
+
         private readonly ILogger _logger;
 
         private readonly PsesInternalHost _psesHost;
@@ -25,7 +28,7 @@ internal class SynchronousPowerShellTask<TResult> : SynchronousTask<IReadOnlyLis
 
         private SMA.PowerShell _pwsh;
 
-        private static readonly PowerShellExecutionOptions s_defaultPowerShellExecutionOptions = new();
+        private PowerShellContextFrame _frame;
 
         public SynchronousPowerShellTask(
             ILogger logger,
@@ -51,25 +54,53 @@ public SynchronousPowerShellTask(
 
         public override IReadOnlyList<TResult> Run(CancellationToken cancellationToken)
         {
-            _pwsh = _psesHost.CurrentPowerShell;
+            _psesHost.Runspace.ThrowCancelledIfUnusable();
+            PowerShellContextFrame frame = _psesHost.PushPowerShellForExecution();
+            try
+            {
+                _pwsh = _psesHost.CurrentPowerShell;
+
+                if (PowerShellExecutionOptions.WriteInputToHost)
+                {
+                    _psesHost.WriteWithPrompt(_psCommand, cancellationToken);
+                }
 
-            if (PowerShellExecutionOptions.WriteInputToHost)
+                return _pwsh.Runspace.Debugger.InBreakpoint
+                    && (IsDebuggerCommand(_psCommand) || _pwsh.Runspace.RunspaceIsRemote)
+                    ? ExecuteInDebugger(cancellationToken)
+                    : ExecuteNormally(cancellationToken);
+            }
+            finally
             {
-                _psesHost.WriteWithPrompt(_psCommand, cancellationToken);
+                _psesHost.PopPowerShellForExecution(frame);
             }
-
-            return _pwsh.Runspace.Debugger.InBreakpoint
-                && Array.Exists(
-                    DebuggerCommands,
-                    c => c.Equals(_psCommand.GetInvocationText(), StringComparison.CurrentCultureIgnoreCase))
-                ? ExecuteInDebugger(cancellationToken)
-                : ExecuteNormally(cancellationToken);
         }
 
         public override string ToString() => _psCommand.GetInvocationText();
 
+        private static bool IsDebuggerCommand(PSCommand command)
+        {
+            if (command.Commands.Count is not 1
+                || command.Commands[0] is { IsScript: false } or { Parameters.Count: > 0 })
+            {
+                return false;
+            }
+
+            string commandText = command.Commands[0].CommandText;
+            foreach (string knownCommand in DebuggerCommands)
+            {
+                if (commandText.Equals(knownCommand, StringComparison.OrdinalIgnoreCase))
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
         private IReadOnlyList<TResult> ExecuteNormally(CancellationToken cancellationToken)
         {
+            _frame = _psesHost.CurrentFrame;
             if (PowerShellExecutionOptions.WriteOutputToHost)
             {
                 _psCommand.AddOutputCommand();
@@ -93,6 +124,11 @@ private IReadOnlyList<TResult> ExecuteNormally(CancellationToken cancellationTok
                 result = _pwsh.InvokeCommand<TResult>(_psCommand, invocationSettings);
                 cancellationToken.ThrowIfCancellationRequested();
             }
+            // Allow terminate exceptions to propogate for flow control.
+            catch (TerminateException)
+            {
+                throw;
+            }
             // Test if we've been cancelled. If we're remoting, PSRemotingDataStructureException
             // effectively means the pipeline was stopped.
             catch (Exception e) when (cancellationToken.IsCancellationRequested || e is PipelineStoppedException || e is PSRemotingDataStructureException)
@@ -108,6 +144,17 @@ private IReadOnlyList<TResult> ExecuteNormally(CancellationToken cancellationTok
             // Other errors are bubbled up to the caller
             catch (RuntimeException e)
             {
+                if (e is PSRemotingTransportException)
+                {
+                    _ = System.Threading.Tasks.Task.Run(
+                        () => _psesHost.UnwindCallStack(),
+                        CancellationToken.None)
+                        .HandleErrorsAsync(_logger);
+
+                    _psesHost.WaitForExternalDebuggerStops();
+                    throw new OperationCanceledException("The operation was canceled.", e);
+                }
+
                 Logger.LogWarning($"Runtime exception occurred while executing command:{Environment.NewLine}{Environment.NewLine}{e}");
 
                 if (PowerShellExecutionOptions.ThrowOnError)
@@ -119,7 +166,14 @@ private IReadOnlyList<TResult> ExecuteNormally(CancellationToken cancellationTok
                     .AddOutputCommand()
                     .AddParameter("InputObject", e.ErrorRecord.AsPSObject());
 
-                _pwsh.InvokeCommand(command);
+                if (_pwsh.Runspace.RunspaceStateInfo.IsUsable())
+                {
+                    _pwsh.InvokeCommand(command);
+                }
+                else
+                {
+                    _psesHost.UI.WriteErrorLine(e.ToString());
+                }
             }
             finally
             {
@@ -173,7 +227,11 @@ private IReadOnlyList<TResult> ExecuteInDebugger(CancellationToken cancellationT
                 debuggerResult = _pwsh.Runspace.Debugger.ProcessCommand(_psCommand, outputCollection);
                 cancellationToken.ThrowIfCancellationRequested();
             }
-
+            // Allow terminate exceptions to propogate for flow control.
+            catch (TerminateException)
+            {
+                throw;
+            }
             // Test if we've been cancelled. If we're remoting, PSRemotingDataStructureException
             // effectively means the pipeline was stopped.
             catch (Exception e) when (cancellationToken.IsCancellationRequested || e is PipelineStoppedException || e is PSRemotingDataStructureException)
@@ -185,6 +243,17 @@ private IReadOnlyList<TResult> ExecuteInDebugger(CancellationToken cancellationT
             // Other errors are bubbled up to the caller
             catch (RuntimeException e)
             {
+                if (e is PSRemotingTransportException)
+                {
+                    _ = System.Threading.Tasks.Task.Run(
+                        () => _psesHost.UnwindCallStack(),
+                        CancellationToken.None)
+                        .HandleErrorsAsync(_logger);
+
+                    _psesHost.WaitForExternalDebuggerStops();
+                    throw new OperationCanceledException("The operation was canceled.", e);
+                }
+
                 Logger.LogWarning($"Runtime exception occurred while executing command:{Environment.NewLine}{Environment.NewLine}{e}");
 
                 if (PowerShellExecutionOptions.ThrowOnError)
@@ -192,14 +261,9 @@ private IReadOnlyList<TResult> ExecuteInDebugger(CancellationToken cancellationT
                     throw;
                 }
 
-                PSDataCollection<PSObject> errorOutputCollection = new();
-                errorOutputCollection.DataAdded += (object sender, DataAddedEventArgs args) =>
-                    {
-                        for (int i = args.Index; i < outputCollection.Count; i++)
-                        {
-                            _psesHost.UI.WriteLine(outputCollection[i].ToString());
-                        }
-                    };
+                using PSDataCollection<PSObject> errorOutputCollection = new();
+                errorOutputCollection.DataAdding += (object sender, DataAddingEventArgs args)
+                    => _psesHost.UI.WriteLine(args.ItemAdded?.ToString());
 
                 PSCommand command = new PSCommand()
                     .AddDebugOutputCommand()
@@ -249,6 +313,7 @@ private void StopDebuggerIfRemoteDebugSessionFailed()
             // Instead we have to query the remote directly
             if (_pwsh.Runspace.RunspaceIsRemote)
             {
+                _pwsh.Runspace.ThrowCancelledIfUnusable();
                 PSCommand assessDebuggerCommand = new PSCommand().AddScript("$Host.Runspace.Debugger.InBreakpoint");
 
                 PSDataCollection<PSObject> outputCollection = new();
@@ -266,8 +331,42 @@ private void StopDebuggerIfRemoteDebugSessionFailed()
             }
         }
 
-        private void CancelNormalExecution() => _pwsh.Stop();
+        private void CancelNormalExecution()
+        {
+            if (_pwsh.Runspace.RunspaceStateInfo.IsUsable())
+            {
+                return;
+            }
 
-        private void CancelDebugExecution() => _pwsh.Runspace.Debugger.StopProcessCommand();
+            // If we're signaled to exit a runspace then that'll trigger a stop,
+            // if we block on that stop we'll never exit the runspace (
+            // and essentially deadlock).
+            if (_frame?.SessionExiting is true)
+            {
+                _pwsh.BeginStop(null, null);
+                return;
+            }
+
+            try
+            {
+                _pwsh.Stop();
+            }
+            catch (NullReferenceException nre)
+            {
+                _logger.LogError(
+                    nre,
+                    "Null reference exception from PowerShell.Stop received.");
+            }
+        }
+
+        private void CancelDebugExecution()
+        {
+            if (_pwsh.Runspace.RunspaceStateInfo.IsUsable())
+            {
+                return;
+            }
+
+            _pwsh.Runspace.Debugger.StopProcessCommand();
+        }
     }
 }
diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs
index 3bb797054..1917e3432 100644
--- a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs
+++ b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs
@@ -154,13 +154,15 @@ public PsesInternalHost(
 
         IRunspaceInfo IRunspaceContext.CurrentRunspace => CurrentRunspace;
 
-        private PowerShellContextFrame CurrentFrame => _psFrameStack.Peek();
+        internal PowerShellContextFrame CurrentFrame => _psFrameStack.Peek();
 
         public event Action<object, RunspaceChangedEventArgs> RunspaceChanged;
 
         private bool ShouldExitExecutionLoop => _shouldExit || _shuttingDown != 0;
 
-        public override void EnterNestedPrompt() => PushPowerShellAndRunLoop(CreateNestedPowerShell(CurrentRunspace), PowerShellFrameType.Nested);
+        public override void EnterNestedPrompt() => PushPowerShellAndRunLoop(
+            CreateNestedPowerShell(CurrentRunspace),
+            PowerShellFrameType.Nested | PowerShellFrameType.Repl);
 
         public override void ExitNestedPrompt() => SetExit();
 
@@ -170,18 +172,38 @@ public PsesInternalHost(
 
         public void PopRunspace()
         {
+            if (!Runspace.RunspaceIsRemote)
+            {
+                return;
+            }
+
             IsRunspacePushed = false;
+            CurrentFrame.SessionExiting = true;
+            PopPowerShell();
             SetExit();
         }
 
         public void PushRunspace(Runspace runspace)
         {
             IsRunspacePushed = true;
-            PushPowerShellAndRunLoop(CreatePowerShellForRunspace(runspace), PowerShellFrameType.Remote);
+            PushPowerShellAndMaybeRunLoop(
+                CreatePowerShellForRunspace(runspace),
+                PowerShellFrameType.Remote | PowerShellFrameType.Repl,
+                skipRunLoop: true);
         }
 
         // TODO: Handle exit code if needed
-        public override void SetShouldExit(int exitCode) => SetExit();
+        public override void SetShouldExit(int exitCode)
+        {
+            if (CurrentFrame.IsRemote)
+            {
+                // PopRunspace also calls SetExit.
+                PopRunspace();
+                return;
+            }
+
+            SetExit();
+        }
 
         /// <summary>
         /// Try to start the PowerShell loop in the host.
@@ -241,7 +263,8 @@ public void SetExit()
         {
             // Can't exit from the top level of PSES
             // since if you do, you lose all LSP services
-            if (_psFrameStack.Count <= 1)
+            PowerShellContextFrame frame = CurrentFrame;
+            if (!frame.IsRepl || _psFrameStack.Count <= 1)
             {
                 return;
             }
@@ -249,6 +272,8 @@ public void SetExit()
             _shouldExit = true;
         }
 
+        internal void ForceSetExit() => _shouldExit = true;
+
         public Task<T> InvokeTaskOnPipelineThreadAsync<T>(
             SynchronousTask<T> task)
         {
@@ -287,6 +312,10 @@ public Task<T> InvokeTaskOnPipelineThreadAsync<T>(
 
         public void CancelCurrentTask() => _cancellationContext.CancelCurrentTask();
 
+        public void CancelIdleParentTask() => _cancellationContext.CancelIdleParentTask();
+
+        public void UnwindCallStack() => _cancellationContext.CancelCurrentTaskStack();
+
         public Task<TResult> ExecuteDelegateAsync<TResult>(
             string representation,
             ExecutionOptions executionOptions,
@@ -399,7 +428,7 @@ private void Run()
                 _mainRunspaceEngineIntrinsics = engineIntrinsics;
                 _localComputerName = localRunspaceInfo.SessionDetails.ComputerName;
                 _runspaceStack.Push(new RunspaceFrame(pwsh.Runspace, localRunspaceInfo));
-                PushPowerShellAndRunLoop(pwsh, PowerShellFrameType.Normal, localRunspaceInfo);
+                PushPowerShellAndRunLoop(pwsh, PowerShellFrameType.Normal | PowerShellFrameType.Repl, localRunspaceInfo);
             }
             catch (Exception e)
             {
@@ -415,7 +444,28 @@ private void Run()
             return (pwsh, localRunspaceInfo, engineIntrinsics);
         }
 
+        internal PowerShellContextFrame PushPowerShellForExecution()
+        {
+            PowerShellContextFrame frame = CurrentFrame;
+            PowerShellFrameType currentFrameType = frame.FrameType;
+            currentFrameType &= ~PowerShellFrameType.Repl;
+            PowerShellContextFrame newFrame = new(
+                frame.PowerShell.CloneForNewFrame(),
+                frame.RunspaceInfo,
+                currentFrameType);
+
+            PushPowerShell(newFrame);
+            return newFrame;
+        }
+
         private void PushPowerShellAndRunLoop(PowerShell pwsh, PowerShellFrameType frameType, RunspaceInfo newRunspaceInfo = null)
+            => PushPowerShellAndMaybeRunLoop(pwsh, frameType, newRunspaceInfo, skipRunLoop: false);
+
+        private void PushPowerShellAndMaybeRunLoop(
+            PowerShell pwsh,
+            PowerShellFrameType frameType,
+            RunspaceInfo newRunspaceInfo = null,
+            bool skipRunLoop = false)
         {
             // TODO: Improve runspace origin detection here
             if (newRunspaceInfo is null)
@@ -430,7 +480,7 @@ private void PushPowerShellAndRunLoop(PowerShell pwsh, PowerShellFrameType frame
                 }
             }
 
-            PushPowerShellAndRunLoop(new PowerShellContextFrame(pwsh, newRunspaceInfo, frameType));
+            PushPowerShellAndMaybeRunLoop(new PowerShellContextFrame(pwsh, newRunspaceInfo, frameType), skipRunLoop);
         }
 
         private RunspaceInfo GetRunspaceInfoForPowerShell(PowerShell pwsh, out bool isNewRunspace, out RunspaceFrame oldRunspaceFrame)
@@ -455,9 +505,13 @@ private RunspaceInfo GetRunspaceInfoForPowerShell(PowerShell pwsh, out bool isNe
             return RunspaceInfo.CreateFromPowerShell(_logger, pwsh, _localComputerName);
         }
 
-        private void PushPowerShellAndRunLoop(PowerShellContextFrame frame)
+        private void PushPowerShellAndMaybeRunLoop(PowerShellContextFrame frame, bool skipRunLoop = false)
         {
             PushPowerShell(frame);
+            if (skipRunLoop)
+            {
+                return;
+            }
 
             try
             {
@@ -465,7 +519,7 @@ private void PushPowerShellAndRunLoop(PowerShellContextFrame frame)
                 {
                     RunTopLevelExecutionLoop();
                 }
-                else if ((frame.FrameType & PowerShellFrameType.Debug) != 0)
+                else if (frame.IsDebug)
                 {
                     RunDebugExecutionLoop();
                 }
@@ -476,7 +530,14 @@ private void PushPowerShellAndRunLoop(PowerShellContextFrame frame)
             }
             finally
             {
-                PopPowerShell();
+                if (CurrentFrame != frame)
+                {
+                    frame.IsAwaitingPop = true;
+                }
+                else
+                {
+                    PopPowerShell();
+                }
             }
         }
 
@@ -484,6 +545,12 @@ private void PushPowerShell(PowerShellContextFrame frame)
         {
             if (_psFrameStack.Count > 0)
             {
+                if (frame.PowerShell.Runspace == CurrentFrame.PowerShell.Runspace)
+                {
+                    _psFrameStack.Push(frame);
+                    return;
+                }
+
                 RemoveRunspaceEventHandlers(CurrentFrame.PowerShell.Runspace);
             }
 
@@ -492,15 +559,25 @@ private void PushPowerShell(PowerShellContextFrame frame)
             _psFrameStack.Push(frame);
         }
 
+        internal void PopPowerShellForExecution(PowerShellContextFrame expectedFrame)
+        {
+            if (CurrentFrame != expectedFrame)
+            {
+                expectedFrame.IsAwaitingPop = true;
+                return;
+            }
+
+            PopPowerShellImpl();
+        }
+
         private void PopPowerShell(RunspaceChangeAction runspaceChangeAction = RunspaceChangeAction.Exit)
         {
             _shouldExit = false;
-            PowerShellContextFrame frame = _psFrameStack.Pop();
-            try
+            PopPowerShellImpl(_ =>
             {
                 // If we're changing runspace, make sure we move the handlers over. If we just
                 // popped the last frame, then we're exiting and should pop the runspace too.
-                if (_psFrameStack.Count == 0 || CurrentRunspace.Runspace != CurrentPowerShell.Runspace)
+                if (_psFrameStack.Count == 0 || Runspace != CurrentPowerShell.Runspace)
                 {
                     RunspaceFrame previousRunspaceFrame = _runspaceStack.Pop();
                     RemoveRunspaceEventHandlers(previousRunspaceFrame.Runspace);
@@ -519,11 +596,24 @@ private void PopPowerShell(RunspaceChangeAction runspaceChangeAction = RunspaceC
                                 newRunspaceFrame.RunspaceInfo));
                     }
                 }
-            }
-            finally
+            });
+        }
+
+        private void PopPowerShellImpl(Action<PowerShellContextFrame> action = null)
+        {
+            do
             {
-                frame.Dispose();
+                PowerShellContextFrame frame = _psFrameStack.Pop();
+                try
+                {
+                    action?.Invoke(frame);
+                }
+                finally
+                {
+                    frame.Dispose();
+                }
             }
+            while (_psFrameStack.Count > 0 && CurrentFrame.IsAwaitingPop);
         }
 
         private void RunTopLevelExecutionLoop()
@@ -539,7 +629,21 @@ private void RunTopLevelExecutionLoop()
                 // Signal that we are ready for outside services to use
                 _started.TrySetResult(true);
 
-                RunExecutionLoop();
+                // While loop is purely so we can recover gracefully from a
+                // terminate exception.
+                while (true)
+                {
+                    try
+                    {
+                        RunExecutionLoop();
+                        break;
+                    }
+                    catch (TerminateException)
+                    {
+                        // Do nothing, since we are at the top level of the loop
+                        // the call stack has been unwound successfully.
+                    }
+                }
             }
             catch (Exception e)
             {
@@ -557,7 +661,7 @@ private void RunDebugExecutionLoop()
             try
             {
                 DebugContext.EnterDebugLoop();
-                RunExecutionLoop();
+                RunExecutionLoop(isForDebug: true);
             }
             finally
             {
@@ -565,11 +669,17 @@ private void RunDebugExecutionLoop()
             }
         }
 
-        private void RunExecutionLoop()
+        private void RunExecutionLoop(bool isForDebug = false)
         {
+            Runspace initialRunspace = Runspace;
             while (!ShouldExitExecutionLoop)
             {
-                using CancellationScope cancellationScope = _cancellationContext.EnterScope(isIdleScope: false);
+                if (isForDebug && !initialRunspace.RunspaceStateInfo.IsUsable())
+                {
+                    return;
+                }
+
+                using CancellationScope cancellationScope = _cancellationContext.EnterScope(false);
                 DoOneRepl(cancellationScope.CancellationToken);
 
                 while (!ShouldExitExecutionLoop
@@ -577,6 +687,18 @@ private void RunExecutionLoop()
                     && _taskQueue.TryTake(out ISynchronousTask task))
                 {
                     task.ExecuteSynchronously(cancellationScope.CancellationToken);
+                    while (Runspace is { RunspaceIsRemote: true } remoteRunspace
+                        && !remoteRunspace.RunspaceStateInfo.IsUsable())
+                    {
+                        PopPowerShell(RunspaceChangeAction.Exit);
+                    }
+                }
+
+                if (_shouldExit
+                    && CurrentFrame is { IsRemote: true, IsRepl: true, IsNested: false })
+                {
+                    _shouldExit = false;
+                    PopPowerShell();
                 }
             }
         }
@@ -595,7 +717,9 @@ private void DoOneRepl(CancellationToken cancellationToken)
             // the debugger (instead of using a Code launch configuration) via Wait-Debugger or
             // simply hitting a PSBreakpoint. We need to synchronize the state and stop the debug
             // context (and likely the debug server).
-            if (DebugContext.IsActive && !CurrentRunspace.Runspace.Debugger.InBreakpoint)
+            if (!DebugContext.IsDebuggingRemoteRunspace
+                && DebugContext.IsActive
+                && !CurrentRunspace.Runspace.Debugger.InBreakpoint)
             {
                 StopDebugContext();
             }
@@ -637,6 +761,15 @@ private void DoOneRepl(CancellationToken cancellationToken)
             {
                 // Do nothing, since we were just cancelled
             }
+            // Propagate exceptions thrown from the debugger when quitting.
+            catch (TerminateException)
+            {
+                throw;
+            }
+            catch (FlowControlException)
+            {
+                // Do nothing, a break or continue statement was used outside of a loop.
+            }
             catch (Exception e)
             {
                 UI.WriteErrorLine($"An error occurred while running the REPL loop:{Environment.NewLine}{e}");
@@ -655,13 +788,14 @@ private void DoOneRepl(CancellationToken cancellationToken)
 
         private string GetPrompt(CancellationToken cancellationToken)
         {
+            Runspace.ThrowCancelledIfUnusable();
             string prompt = DefaultPrompt;
             try
             {
                 // TODO: Should we cache PSCommands like this as static members?
                 PSCommand command = new PSCommand().AddCommand("prompt");
                 IReadOnlyList<string> results = InvokePSCommand<string>(command, executionOptions: null, cancellationToken);
-                if (results.Count > 0)
+                if (results?.Count > 0)
                 {
                     prompt = results[0];
                 }
@@ -934,28 +1068,59 @@ private void StopDebugContext()
             }
         }
 
+        private readonly object _replFromAnotherThread = new();
+
+        internal void WaitForExternalDebuggerStops()
+        {
+            lock (_replFromAnotherThread)
+            {
+            }
+        }
+
         private void OnDebuggerStopped(object sender, DebuggerStopEventArgs debuggerStopEventArgs)
         {
             // The debugger has officially started. We use this to later check if we should stop it.
             DebugContext.IsActive = true;
 
-            // If the debug server is NOT active, we need to synchronize state and start it.
-            if (!DebugContext.IsDebugServerActive)
+            // The local debugging architecture works mostly because we control the pipeline thread,
+            // but remote runspaces will trigger debugger stops on a separate thread. We lock here
+            // if we're on a different thread so in then event of a transport error, we can
+            // safely wind down REPL loops in a different thread.
+            bool isExternal = Environment.CurrentManagedThreadId != _pipelineThread.ManagedThreadId;
+            if (!isExternal)
             {
-                _languageServer?.SendNotification("powerShell/startDebugger");
+                OnDebuggerStoppedImpl(sender, debuggerStopEventArgs);
+                return;
             }
 
-            DebugContext.SetDebuggerStopped(debuggerStopEventArgs);
-
-            try
+            lock (_replFromAnotherThread)
             {
-                CurrentPowerShell.WaitForRemoteOutputIfNeeded();
-                PushPowerShellAndRunLoop(CreateNestedPowerShell(CurrentRunspace), PowerShellFrameType.Debug | PowerShellFrameType.Nested);
-                CurrentPowerShell.ResumeRemoteOutputIfNeeded();
+                OnDebuggerStoppedImpl(sender, debuggerStopEventArgs);
             }
-            finally
+
+            void OnDebuggerStoppedImpl(object sender, DebuggerStopEventArgs debuggerStopEventArgs)
             {
-                DebugContext.SetDebuggerResumed();
+                    // If the debug server is NOT active, we need to synchronize state and start it.
+                    if (!DebugContext.IsDebugServerActive)
+                    {
+                        _languageServer?.SendNotification("powerShell/startDebugger");
+                    }
+
+                    DebugContext.SetDebuggerStopped(debuggerStopEventArgs);
+
+                    try
+                    {
+                        CurrentPowerShell.WaitForRemoteOutputIfNeeded();
+                        PowerShellFrameType frameBase = CurrentFrame.FrameType & PowerShellFrameType.Remote;
+                        PushPowerShellAndRunLoop(
+                            CreateNestedPowerShell(CurrentRunspace),
+                            frameBase | PowerShellFrameType.Debug | PowerShellFrameType.Nested | PowerShellFrameType.Repl);
+                        CurrentPowerShell.ResumeRemoteOutputIfNeeded();
+                    }
+                    finally
+                    {
+                        DebugContext.SetDebuggerResumed();
+                    }
             }
         }
 
diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellDebugDisplay.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellDebugDisplay.cs
new file mode 100644
index 000000000..4a1536ff0
--- /dev/null
+++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellDebugDisplay.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#if DEBUG
+using System.Diagnostics;
+using SMA = System.Management.Automation;
+
+[assembly: DebuggerDisplay("{Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility.PowerShellDebugDisplay.ToDebuggerString(this)}", Target = typeof(SMA.PowerShell))]
+[assembly: DebuggerDisplay("{Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility.PSCommandDebugDisplay.ToDebuggerString(this)}", Target = typeof(SMA.PSCommand))]
+
+namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility;
+
+internal static class PowerShellDebugDisplay
+{
+    public static string ToDebuggerString(SMA.PowerShell pwsh)
+    {
+        if (pwsh.Commands.Commands.Count == 0)
+        {
+            return "{}";
+        }
+
+        return $"{{{pwsh.Commands.Commands[0].CommandText}}}";
+    }
+}
+
+internal static class PSCommandDebugDisplay
+{
+    public static string ToDebuggerString(SMA.PSCommand command)
+    {
+        if (command.Commands.Count == 0)
+        {
+            return "{}";
+        }
+
+        return $"{{{command.Commands[0].CommandText}}}";
+    }
+}
+#endif
diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs
index 78748a91f..173d1d6ae 100644
--- a/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs
+++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs
@@ -37,6 +37,38 @@ static PowerShellExtensions()
                 typeof(PowerShell).GetMethod("ResumeIncomingData", BindingFlags.Instance | BindingFlags.NonPublic));
         }
 
+        public static PowerShell CloneForNewFrame(this PowerShell pwsh)
+        {
+            if (pwsh.IsNested)
+            {
+                return PowerShell.Create(RunspaceMode.CurrentRunspace);
+            }
+
+            PowerShell newPwsh = PowerShell.Create();
+            newPwsh.Runspace = pwsh.Runspace;
+            return newPwsh;
+        }
+
+        public static void DisposeWhenCompleted(this PowerShell pwsh)
+        {
+            static void handler(object self, PSInvocationStateChangedEventArgs e)
+            {
+                if (e.InvocationStateInfo.State is
+                    not PSInvocationState.Completed
+                    and not PSInvocationState.Failed
+                    and not PSInvocationState.Stopped)
+                {
+                    return;
+                }
+
+                PowerShell pwsh = (PowerShell)self;
+                pwsh.InvocationStateChanged -= handler;
+                pwsh.Dispose();
+            }
+
+            pwsh.InvocationStateChanged += handler;
+        }
+
         public static Collection<TResult> InvokeAndClear<TResult>(this PowerShell pwsh, PSInvocationSettings invocationSettings = null)
         {
             try
diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/RunspaceExtensions.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/RunspaceExtensions.cs
index 681e16d8f..adb4bf99a 100644
--- a/src/PowerShellEditorServices/Services/PowerShell/Utility/RunspaceExtensions.cs
+++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/RunspaceExtensions.cs
@@ -50,6 +50,17 @@ static RunspaceExtensions()
         /// <returns>A prompt string decorated with remote connection details.</returns>
         public static string GetRemotePrompt(this Runspace runspace, string basePrompt) => s_getRemotePromptFunc(runspace, basePrompt);
 
+        public static void ThrowCancelledIfUnusable(this Runspace runspace)
+            => runspace.RunspaceStateInfo.ThrowCancelledIfUnusable();
+
+        public static void ThrowCancelledIfUnusable(this RunspaceStateInfo runspaceStateInfo)
+        {
+            if (!IsUsable(runspaceStateInfo))
+            {
+                throw new OperationCanceledException();
+            }
+        }
+
         public static bool IsUsable(this RunspaceStateInfo runspaceStateInfo)
         {
             return runspaceStateInfo.State switch
diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs
index 562f3c764..1cafd5f56 100644
--- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs
+++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs
@@ -962,7 +962,7 @@ public async Task CanSendCompletionAndCompletionResolveRequestAsync()
                 });
 
             CompletionItem completionItem = Assert.Single(completionItems,
-                completionItem1 => completionItem1.Label == "Write-Host");
+                completionItem1 => completionItem1.FilterText == "Write-Host");
 
             CompletionItem updatedCompletionItem = await PsesLanguageClient
                 .SendRequest("completionItem/resolve", completionItem)
@@ -1102,9 +1102,8 @@ await PsesLanguageClient
                         })
                     .Returning<GetProjectTemplatesResponse>(CancellationToken.None).ConfigureAwait(true);
 
-            Assert.Collection(getProjectTemplatesResponse.Templates.OrderBy(t => t.Title),
-                template1 => Assert.Equal("AddPSScriptAnalyzerSettings", template1.Title),
-                template2 => Assert.Equal("New PowerShell Manifest Module", template2.Title));
+            Assert.Contains(getProjectTemplatesResponse.Templates, t => t.Title is "AddPSScriptAnalyzerSettings");
+            Assert.Contains(getProjectTemplatesResponse.Templates, t => t.Title is "New PowerShell Manifest Module");
         }
 
         [SkippableFact]