feat: debugger execution lifecycle#9970
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
3 issues found across 48 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="frontend/src/core/codemirror/cells/debugger-decorations.ts">
<violation number="1" location="frontend/src/core/codemirror/cells/debugger-decorations.ts:141">
P2: Initial breakpoint sync captures a stale snapshot and can race with newer updates. This can briefly restore removed/old breakpoint markers after mount.</violation>
</file>
<file name="frontend/src/components/editor/output/MarimoTracebackOutput.tsx">
<violation number="1" location="frontend/src/components/editor/output/MarimoTracebackOutput.tsx:231">
P1: Debugger breakpoint toggle can throw in static notebooks. Guard the new toggle path with `!isStaticNotebook()` before calling `toggleBreakpoint`.</violation>
</file>
<file name="marimo/_runtime/runtime.py">
<violation number="1" location="marimo/_runtime/runtime.py:691">
P1: Out-of-band dispatch can delay breakpoint updates behind code completion. During a running cell, this can miss intended stops because breakpoints are applied too late.</violation>
</file>
Architecture diagram
sequenceDiagram
participant UI as Frontend (Browser)
participant Editor as CodeMirror Editor
participant WS as WebSocket / Kernel Connection
participant OOB as Out‑of‑Band Worker
participant Kernel as Kernel (Runtime)
participant FW as FrameWatcher (sys.settrace)
participant Pdb as MarimoPdb
Note over UI,Pdb: NEW: Live Debugger Lifecycle
UI->>Editor: User toggles gutter breakpoint
Editor->>Editor: toggleBreakpoint(cellId, line)
Editor->>UI: update breakpointsAtom → sendSetBreakpoints
UI->>WS: POST /api/kernel/pdb/breakpoints
WS->>OOB: enqueue SetBreakpointsCommand
OOB->>Kernel: dispatch_out_of_band(SetBreakpointsCommand)
Kernel->>Pdb: update self.breakpoints[cellId]
Note over UI,Pdb: Cell Execution with Debugger
UI->>WS: User runs cell
WS->>Kernel: control queue → ExecuteCellCommand
Kernel->>Kernel: Runner creates DebuggerLifecycle (if flag enabled)
Kernel->>FW: setup() → install()
FW->>FW: sys.settrace(self._trace)
FW->>FW: start heartbeat thread (75ms interval)
Kernel->>Kernel: execute cell body (Python code)
Note over FW: trace fires on every line event
loop Every line
FW->>FW: record (cellId, line) as current
alt Line matches a breakpoint
FW->>Pdb: interaction(frame, traceback)
Pdb-->>UI: stdin message (Pdb prompt)
UI->>Pdb: user sends pdb command (e.g. c, n, s)
alt User quits (q)
Pdb->>FW: quitting flag set
FW->>Kernel: raise MarimoStopError
Kernel->>FW: teardown() → uninstall()
end
else No breakpoint
FW->>FW: continue tracing
end
end
loop Heartbeat (75ms)
FW-->>WS: DebuggerLineNotification (cellId, line)
WS->>UI: handleMessage("debugger-line")
UI->>UI: update debuggerCurrentLineAtom
UI->>Editor: highlight current line (cm-debugger-current-line)
end
Note over UI,Pdb: Cell finishes or is interrupted
Kernel->>FW: teardown() → uninstall()
FW->>FW: sys.settrace(prev), stop heartbeat
alt Cell became idle
FW->>WS: DebuggerLineNotification (cellId, line=None)
WS->>UI: clear highlight
end
Kernel->>UI: status="idle"
UI->>UI: resolve dangling stdin prompt, debuggerActive=false
Note over UI,Pdb: Clearing cell (trashcan)
UI->>Editor: onClear()
Editor->>Editor: clearCellBreakpoints(cellId)
Editor->>WS: POST /api/kernel/pdb/breakpoints (empty map)
WS->>OOB: SetBreakpointsCommand (no breakpoints)
OOB->>Kernel: dispatch_out_of_band
Kernel->>Pdb: clear breakpoints[cellId]
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| if (debuggerEnabled) { | ||
| toggleBreakpoint(info.cellId, info.lineNumber); | ||
| return; |
There was a problem hiding this comment.
P1: Debugger breakpoint toggle can throw in static notebooks. Guard the new toggle path with !isStaticNotebook() before calling toggleBreakpoint.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/components/editor/output/MarimoTracebackOutput.tsx, line 231:
<comment>Debugger breakpoint toggle can throw in static notebooks. Guard the new toggle path with `!isStaticNotebook()` before calling `toggleBreakpoint`.</comment>
<file context>
@@ -219,6 +228,10 @@ export const replaceTracebackFilenames = (domNode: DOMNode) => {
>
<BugPlayIcon
onClick={() => {
+ if (debuggerEnabled) {
+ toggleBreakpoint(info.cellId, info.lineNumber);
+ return;
</file context>
| if (debuggerEnabled) { | |
| toggleBreakpoint(info.cellId, info.lineNumber); | |
| return; | |
| if (debuggerEnabled && !isStaticNotebook()) { | |
| toggleBreakpoint(info.cellId, info.lineNumber); | |
| return; | |
| } |
| self.code_completion(request, docstrings_limit=80) | ||
| # Block for the next command, then drain and dispatch whatever | ||
| # else is queued in one pass (latest of each type wins). | ||
| for command in collapse_out_of_band( |
There was a problem hiding this comment.
P1: Out-of-band dispatch can delay breakpoint updates behind code completion. During a running cell, this can miss intended stops because breakpoints are applied too late.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At marimo/_runtime/runtime.py, line 691:
<comment>Out-of-band dispatch can delay breakpoint updates behind code completion. During a running cell, this can miss intended stops because breakpoints are applied too late.</comment>
<file context>
@@ -663,30 +665,62 @@ def globals(self) -> dict[Any, Any]:
- self.code_completion(request, docstrings_limit=80)
+ # Block for the next command, then drain and dispatch whatever
+ # else is queued in one pass (latest of each type wins).
+ for command in collapse_out_of_band(
+ out_of_band_queue, first=out_of_band_queue.get()
+ ):
</file context>
| // sync (e.g. for editors that mount after breakpoints already exist). | ||
| const initial = observable.get(); | ||
| if (initial.size > 0) { | ||
| queueMicrotask(() => apply(initial)); |
There was a problem hiding this comment.
P2: Initial breakpoint sync captures a stale snapshot and can race with newer updates. This can briefly restore removed/old breakpoint markers after mount.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/core/codemirror/cells/debugger-decorations.ts, line 141:
<comment>Initial breakpoint sync captures a stale snapshot and can race with newer updates. This can briefly restore removed/old breakpoint markers after mount.</comment>
<file context>
@@ -0,0 +1,187 @@
+ // sync (e.g. for editors that mount after breakpoints already exist).
+ const initial = observable.get();
+ if (initial.size > 0) {
+ queueMicrotask(() => apply(initial));
+ }
+ this.unsubscribe = observable.sub(apply);
</file context>
| - cellId | ||
| title: DebugCellRequest | ||
| type: object | ||
| DebuggerLineNotification: |
There was a problem hiding this comment.
maybe ActiveLineNotification
There was a problem hiding this comment.
Pull request overview
Adds an experimental “debugger execution lifecycle” to marimo, enabling live per-line execution indication and session-scoped breakpoints that can apply while a cell is running (Colab-like current-line tracking + gutter breakpoints + pdb integration).
Changes:
- Introduces
DebuggerLifecycle/FrameWatcherto trace executing cell frames and emitdebugger-linenotifications + enter pdb on breakpoints. - Adds a new out-of-band command path (
OutOfBandCommand) and worker to process completions + breakpoint updates off the main control loop. - Adds API + frontend plumbing for session breakpoints (
/api/kernel/pdb/breakpoints) and UI elements (breakpoint gutter + live line highlight + updated debugger controls).
Reviewed changes
Copilot reviewed 48 out of 48 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/_session/test_queue.py | Verifies breakpoint commands are routed to the off-main-loop queue. |
| tests/_runtime/test_request_router.py | Updates NOT_ROUTED surface to include breakpoint updates. |
| tests/_runtime/test_kernel_lifecycle.py | Adds tests for out-of-band command collapsing behavior. |
| tests/_runtime/test_executor_debugger.py | New tests for FrameWatcher tracing, breakpoints, stepping, quitting, and notifications. |
| tests/_runtime/runner/test_cell_runner.py | Tests that debugger lifecycle is gated by experimental flag and debugger presence. |
| packages/openapi/src/api.ts | Updates generated OpenAPI TS types for new endpoint + schemas. |
| packages/openapi/api.yaml | Adds endpoint + schemas for breakpoints and debugger-line notification. |
| marimo/_smoke_tests/debugger_test.py | Adds a manual smoke-test notebook for the debugger lifecycle UX. |
| marimo/_session/types.py | Widens completion queue type to OutOfBandCommand. |
| marimo/_session/queue.py | Routes out-of-band commands to the separate queue. |
| marimo/_session/managers/queue.py | Stores out-of-band queue instead of completion-only queue. |
| marimo/_session/managers/ipc.py | Updates IPC manager typing for out-of-band queue. |
| marimo/_session/managers/app_host.py | Updates app-host push queue typing for out-of-band queue. |
| marimo/_server/models/models.py | Adds request model for set-breakpoints mapped to command. |
| marimo/_server/api/endpoints/execution.py | Adds /pdb/breakpoints endpoint to dispatch breakpoint updates. |
| marimo/_runtime/runtime.py | Adds out-of-band worker + dispatch + kernel-side breakpoint update handler. |
| marimo/_runtime/runner/cell_runner.py | Installs DebuggerLifecycle when experimental flag is enabled. |
| marimo/_runtime/marimo_pdb.py | Adds session-scoped breakpoints store + SIGINT disable helper. |
| marimo/_runtime/kernel_lifecycle.py | Adds collapse_out_of_band queue-draining helper. |
| marimo/_runtime/executor/lifecycles/debugger.py | Implements FrameWatcher + DebuggerLifecycle. |
| marimo/_runtime/executor/init.py | Exports DebuggerLifecycle. |
| marimo/_runtime/commands.py | Adds SetBreakpointsCommand + OutOfBandCommand union. |
| marimo/_pyodide/pyodide_session.py | Mirrors out-of-band queue handling in pyodide runtime. |
| marimo/_messaging/notification.py | Adds DebuggerLineNotification. |
| marimo/_ipc/queue_manager.py | Updates IPC queue types/docs for out-of-band queue. |
| marimo/_ipc/connection.py | Updates IPC completion channel to decode OutOfBandCommand. |
| marimo/_config/config.py | Adds experimental.debugger config flag. |
| marimo/_cli/development/commands.py | Ensures new schemas/notifications are included in API schema generation. |
| frontend/src/core/websocket/useMarimoKernelConnection.tsx | Handles debugger-line notifications to update UI state. |
| frontend/src/core/wasm/bridge.ts | Adds stub for sendSetBreakpoints in wasm bridge. |
| frontend/src/core/network/types.ts | Adds SetBreakpointsRequest and sendSetBreakpoints to request interface. |
| frontend/src/core/network/requests-toasting.tsx | Adds no-toast handling for sendSetBreakpoints. |
| frontend/src/core/network/requests-static.ts | Adds sendSetBreakpoints to static-mode request client. |
| frontend/src/core/network/requests-network.ts | Implements network request for /api/kernel/pdb/breakpoints. |
| frontend/src/core/network/requests-lazy.ts | Registers lazy action for sendSetBreakpoints. |
| frontend/src/core/islands/bridge.ts | Adds stub for sendSetBreakpoints in islands bridge. |
| frontend/src/core/islands/bootstrap.ts | Allows debugger-line messages through islands message handler. |
| frontend/src/core/config/feature-flag.tsx | Adds experimental.debugger feature flag. |
| frontend/src/core/codemirror/cells/extensions.ts | Adds debugger gutter + live-line highlighter extensions behind flag. |
| frontend/src/core/codemirror/cells/debugger-state.ts | Adds breakpoint/current-line atoms and breakpoint sync to kernel. |
| frontend/src/core/codemirror/cells/debugger-decorations.ts | Implements CodeMirror gutter markers + current-line decoration. |
| frontend/src/core/cells/cell.ts | Clears stdin/pdb prompt state when cell finishes (idle) as well as interrupt. |
| frontend/src/core/cells/tests/cell.test.ts | Tests stdin/pdb prompt resolution on idle transition. |
| frontend/src/components/editor/output/MarimoTracebackOutput.tsx | Bug icon toggles real gutter breakpoint when debugger flag enabled. |
| frontend/src/components/editor/notebook-cell.tsx | “Clear” also clears cell breakpoints to keep UI/kernel in sync. |
| frontend/src/components/debugger/debugger-code.tsx | “Clear” now quits pdb first, then clears console output. |
| frontend/src/components/app-config/user-config-form.tsx | Adds UI checkbox for experimental.debugger with description. |
| frontend/src/mocks/requests.ts | Adds mock implementation for sendSetBreakpoints. |
| if isinstance(command, SetBreakpointsCommand): | ||
| self.set_breakpoints(command) | ||
| elif isinstance(command, CodeCompletionCommand): | ||
| self.code_completion(command, docstrings_limit=docstrings_limit) | ||
|
|
| function sendBreakpoints(map: ReadonlyMap<CellId, ReadonlySet<number>>): void { | ||
| const breakpoints: Record<string, number[]> = {}; | ||
| for (const [cellId, lines] of map) { | ||
| if (lines.size > 0) { | ||
| breakpoints[cellId] = [...lines].toSorted((a, b) => a - b); | ||
| } | ||
| } | ||
| void getRequestClient().sendSetBreakpoints({ breakpoints }); | ||
| } |
| @app.cell | ||
| def _(): | ||
| # large line in gutter | ||
| # large line in gutter | ||
| # large line in gutter | ||
| # large line in gutter | ||
| # large line in gutter | ||
| # large line in gutter | ||
| # large line in gutter | ||
| # large line in gutter |
📝 Summary
Adds an experimental debugger execution life cycle that adds frame watching to execution. This in turn is able to highlight executing lines, and allow debug points.
Closes #8335