The Debug Adapter Protocol defines several events related to program execution and termination. The key events are:
-
stoppedEvent: Indicates the debuggee is paused due to some condition (breakpoint hit, step completion, pause request, exception, etc.). This event signals a break in execution, not an end-of-program. In a paused state, the program is halted but still loaded in memory (e.g. waiting at a breakpoint). -
continuedEvent: Indicates the debuggee has resumed execution after being in a stopped state. According to the DAP spec, acontinuedevent is typically not sent if the resume was explicitly requested by the client (e.g. after acontinuerequest). It’s mainly used for spontaneous resumes or to broadcast resume state in multi-threaded scenarios. -
exitedEvent: Indicates the debuggee program (the target process) has actually exited, and it carries the exit code of the program. This event corresponds to the termination of the debuggee process. It is often sent when a debugged program ends normally or is terminated, allowing the client/UI to know the process ended and to display the exit code if needed. -
terminatedEvent: Indicates that the debugging session has ended (the adapter is terminating debugging of the debuggee). Importantly, the spec notes this does not necessarily mean the debuggee process itself has exited. For example, an adapter could terminate a session by detaching from a running process (in which case debugging ends but the process continues running). In all situations where the debug session is ending (for any reason), the adapter must fire aterminatedevent to notify the client that no further debugging is occurring. Theterminatedevent may include an optionalrestartfield to signal the client to automatically restart a session (used in some workflows like debugger self-restart).
Difference between “stopped” and termination events: A stopped event means paused in the middle of debugging (e.g. at a breakpoint) – the session is still active and can continue. Termination events (terminated/exited) mean the debuggee has finished execution or the session is ending. After a program completes execution, the debug session should not remain in a stopped/paused state; it should transition to a terminated state (session ended). In other words, once the program is done (or the user explicitly stops debugging), the adapter should signal that debugging is over. A general rule (from the DAP overview) is: if a debug adapter is ending a session, it must send a terminated event; if the debuggee actually exits and the adapter knows the exit code, it should send an exited event as well (the exited event is optional but provides the exit code to the client).
Thus, after the program completes execution (naturally or via user stop), the expected state is that the debug session is terminated (ended). The UI will typically show the session as ended or removed. For example, in one debugger's documentation: "A session terminates automatically when the program completes execution.". The debug adapter signals this via the terminated event (and possibly an exited event if applicable), after which the client will typically perform cleanup (often sending a disconnect request to the adapter to formally end the connection).
Different language debug adapters implement these events in practice, sometimes with slight variations or extra steps due to runtime specifics. Below is how a few real-world debuggers handle program termination:
The debugpy adapter (used in VS Code for Python) follows the DAP closely: when a debugged Python program finishes, debugpy will send an exited event (with the process’s exit code) followed by a terminated event. This ensures the IDE knows the program ended and the debugging session is over. In fact, the predecessor to debugpy (PTVSD) explicitly implemented the sequence of sending both events before shutting down: upon a normal disconnect or program end, it would “send the exited and terminated event, then kill itself” – a design considered “clean and consistent” by its maintainers. This means the Python debug adapter always tries to report the program’s exit code and then indicate the session is done.
One nuance in Python debugging is handling an abnormal exit or sys.exit() calls. For example, if a program calls sys.exit(1) (an exit with error code), older PTVSD/debugpy versions treated this as an exception to break on. There was discussion on whether an exit with a non-zero code should be considered an "uncaught exception" or just normal termination (so behavior could vary based on settings). In normal configurations, however, an exit (even with code 1) will simply terminate the program — debugpy will not leave the session in a stopped state but proceed to send the termination events. The debugpy adapter also has a feature to wait for user input on program exit (for console applications) – when enabled, it will delay sending the termination events until the user acknowledges, to keep the console open. Only after that does it send the terminated event (and exited if applicable). In summary, debugpy emits: exited (with code) → terminated (session ended) in a normal shutdown. This matches the DAP requirement that the session is closed out with a terminated notification to the client.
The js-debug adapter (Microsoft’s current Node.js debug adapter, using the pwa-node launch type) sends the standard termination events, but accounts for Node runtime specifics. Normally, when a Node program being debugged ends, the adapter captures the process exit event and notifies the client. It sends an exited event with the Node process’s exit code, then a terminated event to end the session (VS Code’s Node debug shows the exit code in the debug console and ends the session).
A historical quirk (from the predecessor node-debug2): in certain older versions of Node, if a debugger was attached, Node would not fully exit on its own until the debugger detached. The Node process would pause at the end waiting for the debug connection to close. js-debug handles this gracefully by detecting end-of-script and proactively managing the detach/terminate sequence.
For Node, the event order might differ slightly depending on how it’s implemented. But the recommended sequence (and what js-debug follows) is to send the exited event as soon as the process ends to report the code, then send terminated to signal the end of the debugging session. After terminated, VS Code will respond by terminating the debug session (and will send a disconnect request if needed to detach). In summary, js-debug emits: exited (with code, if known) → terminated (session over). It also takes care to detach so that the Node process can actually exit.
The Delve debugger (which has a DAP mode, often invoked via dlv dap) likewise follows the protocol. According to the Delve DAP documentation, when the debugged program terminates, Delve sends a terminated event, and it expects the client (IDE) to then issue a disconnect request to finish the session (and shut down the adapter). This design implies that Delve treats the terminated event as the primary signal that “the debug session is done.” Delve will also send an exited event if it knows the program’s exit code. In practice, Delve’s adapter will report the process exit with a code (if it was a launched process) via an exited event, and then send terminated. (If you attach to an external process and then detach, Delve would just send terminated since the process is still running and there is no exit code to report.)
One particular difference in Delve’s design is that the terminated event triggers the client to clean up. The VS Code Go extension, upon seeing terminated, will send a disconnect to Delve DAP, which then causes Delve to shut down the debugger backend. In effect, Delve emits: exited (with code, on program end) → terminated → (then awaits disconnect from client). If the debug session is ended by user (stop command), the sequence is similar except the client may initiate a terminate or disconnect request first, and Delve will still emit terminated during the shutdown.
To summarize the behaviors of these adapters, here’s a comparison:
| Debug Adapter | Normal Program End Behavior | Events Emitted (order) | Notes |
|---|---|---|---|
| debugpy (Python) | Detects Python script exit (including exit code). | exited (with exit code) → terminated |
Always sends both events on a normal exit to signal end. Handles special cases like sys.exit() as normal termination (no lingering pause). May delay termination if configured to wait for input on exit. |
| js-debug / pwa-node (Node.js) | Detects end-of-script or process exit. | exited (with code) → terminated (order in practice) |
Sends both events. Handles Node’s historical behavior of waiting for detach – the adapter ensures it sends terminated to close session, allowing Node process to exit. In attach scenarios, if user disconnects, it might only send terminated (since the process continues running). |
| Delve (Go) | Detects program termination. | exited (with code) → terminated |
Sends terminated event to indicate session should end, and expects a client disconnect. In attach mode (no process exit), an exited event isn’t sent – just terminated on detach. |
Note: Nearly all debug adapters follow the rule that the session ends with a terminated event. The exited event is used when applicable to convey the exit code or confirmation that the debuggee process ended. Some adapters might emit these in quick succession such that the order doesn't visibly matter to the user, but the sequence above (exit then terminate) is typical. Importantly, a stopped event is not used for program end – receiving stopped means a pause, whereas terminated/exited mean finish.
When describing debugger state, common terms are used across many tools, even if not formally defined by DAP. Typically, you’ll encounter states such as:
- Running – the debuggee is actively running (no current break/pause). The debugger is simply waiting for events (or for user commands). This corresponds to times between breakpoints or after a continue command.
- Paused (Stopped) – the debuggee is suspended at a breakpoint or due to a pause request or exception. In VS Code UI, this is shown as "Paused on ". Many adapters internally refer to this as the "stopped" state since DAP’s event is named
stopped. (Some documentation uses paused for user-friendly wording, but technically it’s the same as a stopped event state.) - Terminated – the debuggee program has finished execution or the debugging session has ended. No code is running under the debugger’s control anymore. Some may also use the word Ended for the session state here. Once terminated, the session typically cannot be resumed (you would have to restart a new debug session).
There isn’t an official enforced naming convention for these states in all debuggers’ code, but the concepts are consistent. For example, a debugger might implement an internal state machine with enum values like STATE_RUNNING, STATE_PAUSED, STATE_TERMINATED. The DAP events map onto these concepts: a stopped event signals the transition to paused, a continued event or a continue response signals running, and a terminated event signals terminated.
Note on mcp-debugger’s state model: The project uses SessionState as the primary driving state model (CREATED → INITIALIZING → READY → RUNNING ⇄ PAUSED → STOPPED | ERROR), stored directly on each ManagedSession and checked throughout the codebase. The READY state indicates that the session is initialized and ready to start debugging but has not yet begun execution.
A dual-state overlay is derived from SessionState via mapLegacyState() in _updateSessionState():
- SessionLifecycleState:
CREATED→ACTIVE→TERMINATED(coarse lifecycle) - ExecutionState:
INITIALIZING→RUNNING⇄PAUSED→TERMINATED|ERROR(fine-grained execution)
Note: SessionState.READY maps identically to INITIALIZING in the dual-state model (both SessionLifecycleState.ACTIVE and ExecutionState.INITIALIZING).
These derived sessionLifecycle and executionState fields are kept in sync as a secondary representation, but SessionState is the actively used model.
All debuggers distinguish:
- Active vs Paused: Whether the debuggee is currently running or halted at a debug stop.
- Active vs Terminated: Whether the session is ongoing or completely finished.
In usage, the term “stopped” can be confusing – in DAP it means paused, not “stopped debugging”. To avoid confusion, many UIs say “Paused” for a stopped event. Conversely, “terminated” is unambiguous as session finished. As a best practice for a mock debugger implementation, one can use names like “Running”, “Paused”, and “Terminated” for the session states. This aligns with user expectations and most debugger UIs. Some might include an “Initializing” state at the very start (during launch/attach setup), and perhaps a “Disconnected” state after termination (once the adapter is fully shut down). But generally, running/paused/terminated cover the main lifecycle.
Different debug scenarios produce different sequences of events. Below are step-by-step sequences for a common scenario, along with notes if variations occur in different debuggers:
-
Breakpoint Hit: The program hits a breakpoint. The debug adapter sends a
stoppedevent with reason"breakpoint"(and the thread ID, etc.) to notify the client that execution is paused at a breakpoint. At this point, the debugger state is paused. (Client may query stack frames, variables, etc. at this time.) -
User Continues: The user resumes execution (e.g. presses "Continue" in the IDE). The client sends a
continuerequest to the debug adapter. In response, the adapter resumes the debuggee. Since the continue was explicit, the adapter may not send acontinuedevent (the DAP spec notes this is optional because the client knows it issued a continue). Many adapters simply omit thecontinuedevent here. (If the adapter did not receive an explicit request – e.g. program auto-continues after a step – it would send acontinuedevent to update the UI.) For clarity, we'll assume nocontinuedevent is sent in this manual continue case. -
Program Runs to Completion: After resuming, the program executes the rest of its code and eventually exits (for example, reaches the end of
main()or calls an exit function). -
Debuggee Process Exits: The debug adapter detects that the target process has ended. It then sends an
exitedevent to the client, providing the process exit code (e.g., 0 for success). This informs the IDE of the actual program outcome. (If the adapter is in attach mode and the program ended on its own, it would still do this if it can detect the termination. In a detach scenario where the program continues running, no exited event would be sent because the program didn’t end.) -
Debug Session Termination: Immediately after signaling the process exit, the adapter sends the
terminatedevent. This indicates that the debugging session is ending. The adapter will no longer send any further debug events for this session. The client, upon receivingterminated, knows it can clean up the UI and will typically send adisconnectrequest to the adapter to finalize the shutdown (this is often automatic in IDEs). For example, the Go Delve adapter expects the terminated event to trigger the client’s disconnect. The ordering ofexitedandterminatedis usually as described: first exit code, then session end. (If an adapter ever sentterminatedfirst, the client would still get the exit event, but it might be handled slightly differently. The conventional approach is exit then terminate.) -
Session Ends: The client sends a
disconnectrequest (if needed) and the debug adapter shuts down the debugging session entirely. Any UI indicators (like debug toolbar, variables view, etc.) are removed since the session is over. The state is now terminated – no process is being debugged. If the user restarts or arestartwas indicated, a new session would begin afresh.
Sequence summary:
[ Program running ]
→ (Breakpoint hit)
→ Adapter sends 'stopped' (reason: breakpoint)
→ User resumes (continue request)
→ [ Program continues running ]
→ Program ends naturally
→ Adapter sends 'exited' (exitCode: N)
→ Adapter sends 'terminated'
→ Client cleans up (disconnects session)
This sequence (paused → continued → exited → terminated) is the typical flow when execution goes from a break to completion. All three event types (stopped, exited, terminated) appear in this lifecycle with distinct meanings.
-
Program runs to end without any breakpoints: In this case, there would be no
stoppedevent at all. The sequence would simply be: the program runs → exits → adapter sendsexited→ adapter sendsterminated. From the user perspective, the debug session just ends when the program finishes (perhaps the console closes or a message like "Process exited with code 0" is shown, then the session terminates). -
User stops the program manually: If the user presses a "Stop" button, the client might send a
terminaterequest ordisconnectrequest to the adapter. The adapter will then end the program (e.g., kill the process if it launched it, or detach if attached). The adapter still should send aterminatedevent to signal the session is ending (even if the stop was user-initiated). If the program was killed, anexitedevent may or may not be sent depending on if the adapter was able to capture an exit code (sometimes a forced kill might not have a meaningful exit code, but typically it would be treated as exit code 0 or a specific code). The sequence in that scenario: (User stop request) → adapter possibly sendsterminatedimmediately (and kills process) → maybe anexitedif there is an exit code to report. The key is thatterminatedis always emitted once debugging stops.Note on mcp-debugger event handling: In this project there are two layers of event processing. Raw DAP events are forwarded via
handleDapEvent(): most events (terminated,exited,continued) are forwarded with empty args, butstoppedis forwarded with its full args (threadId,reason,body). Separately, proxy status messages (adapter_exited,dap_connection_closed,terminated) are normalized byhandleStatusMessage()into a unified localexitevent with[code ?? 1, signal || undefined]. Note that exit code uses??(nullish coalescing), so exit code0is preserved as0-- onlynullorundefinedcodes fall back to1. Signal uses||, so an empty string signal also falls back toundefined. This means a proxy statusterminatedbecomes anexitevent, while a raw DAPterminatedevent remains aterminatedevent. -
Exception causes program to end: If the debuggee crashes or encounters an unhandled exception that terminates it, the adapter might first send a
stoppedevent with reasonexception(if it breaks on the exception). If the user doesn’t intervene and the program truly crashes/exits, the adapter will then send theexitedandterminated. In some configurations, adapters are set to break on all uncaught exceptions – giving a chance to inspect – which would be astoppedevent, and if the user then continues, the program may immediately exit, leading to the exit/terminated events as usual. -
Attach/detach scenario: In attach mode, if the user disconnects but the program keeps running, the adapter would send
terminated(to end the session) without anexited(since the debuggee didn’t end, it’s simply that the debugger detached). For instance, attaching to a long-running server and then detaching would result in aterminatedevent only. Conversely, if the process exits on its own while attached, the adapter would ideally send bothexitedandterminated(the session ends because the process died).
In all cases, the final event marking the end of a debug session is terminated. The client (IDE) uses that as the cue that no further debug interaction is possible for that session. The stopped event is never used to signal session end – it's strictly for pause states during an active session. By adhering to these sequences and state transitions, a debugger (or a mock adapter) can ensure it behaves in line with user expectations and the DAP spec, regardless of which IDE is controlling it.
Sources:
- Microsoft Debug Adapter Protocol Specification – definitions of
stopped,continued,exited, andterminatedevents - Progress Documentation (OpenEdge Debugger) – note on session terminating when program ends
- Node Debug2 Issue – describes Node waiting for debugger detach on program end
- PTVSD GitHub Issue – describes sending
exitedandterminatedon disconnect (precursor to debugpy)