Skip to main content
Debug Python: pdb REPL + debugpy remote (DAP).

Skill metadata

Reference: full SKILL.md

The following is the complete skill definition that Mibyan loads when this skill is triggered. This is what the agent sees as instructions when the skill is active.

Python Debugger (pdb + debugpy)

Overview

Three tools, picked by situation: Start with breakpoint(). It’s the cheapest thing that works.

When to Use

  • A test fails and the traceback doesn’t reveal why a value is wrong
  • You need to step through a function and watch a collection mutate
  • A long-running process (mibyan gateway, tui_gateway) misbehaves and you can’t restart it
  • Post-mortem: an exception fired in prod-ish code and you want to inspect locals at the crash site
  • A subprocess / child (Python _SlashWorker, PTY bridge worker) is the actual bug site
Don’t use for: things print() / logging.debug solve in under a minute, or things pytest -vv --tb=long --showlocals already reveals.

pdb Quick Reference

Inside any pdb prompt ((Pdb)): The interact command is the most powerful — you can import anything, inspect complex objects, even call methods that mutate state. Locals are read-only by default; use !x = 42 from the (Pdb) prompt to mutate.

Recipe 1: Local breakpoint

Easiest. Edit the file:
Run the code normally. You land at the breakpoint() line with full access to locals. Don’t forget to remove breakpoint() before committing. Use git diff or a pre-commit grep:

Recipe 2: Launch a script under pdb (no source edits)

Recipe 3: Debug a pytest test

Use terminal and the canonical runner for noninteractive diagnostics:
scripts/run_tests.sh captures each file in a separate subprocess, so --pdb or --trace cannot provide an interactive prompt there. For an interactive debugger only, use the independent development/test interpreter prepared in Recipe 5 (never a production generation):
This bypasses the hermetic-env guarantees — fine for debugging, but re-run under the wrapper to confirm before pushing.

Recipe 4: Post-mortem on any exception

Or wrap a whole script:
Or set a global hook in a repl/jupyter:

Recipe 5: Remote debug with debugpy (attach to running process)

For long-lived processes: Mibyan gateway, tui_gateway, a daemon, a process that’s already misbehaving and can’t be restarted clean.

Setup

For Mibyan, use a separate development checkout and data home, not a live production generation. Follow the PM developer workflow and activate that checkout — PowerShell: . .\activate.ps1. The declared dev extra includes debugpy, which PM activation does not sync (all excludes it). Through terminal, build a fresh, caller-owned debug/test environment with the prepared checkout’s Python:
The output must not already exist. Stop its processes and intentionally remove only that disposable environment before rebuilding. Keep the same isolated mibyan_HOME for the debug target. .venv/bin/python is this explicitly built debug environment, not a guessed application venv, and the patterns below run through it. Do not add debugpy to a running production environment; reproduce there only with an already-prepared debug target or arrange a restart in the development environment.

Pattern A: Source-edit — process waits for debugger at launch

Add near the top of the entry point (or inside the function you want to debug):
Start the process; it blocks on wait_for_client().

Pattern B: No source edit — launch with -m debugpy

Equivalent for module entry:

Pattern C: Attach to an already-running process

Needs the PID and debugpy preinstalled in the target’s environment:
Some kernels/security configs block the ptrace-based injection (/proc/sys/kernel/yama/ptrace_scope). Fix with:

Connecting a client from the terminal

The easiest terminal-side DAP client is VS Code CLI or a small script. From inside Mibyan you have two practical options: Option 1: debugpy’s own CLI REPL — not an official feature, but a tiny DAP client script:
This is fine for one-off automation but painful as an interactive UX. Option 2: Attach from VS Code / Cursor / Zed — if the user has one open, they can add a launch.json:
Option 3: Ditch DAP, use remote-pdb — usually what you actually want from a terminal agent: For an independently owned Python project, declare remote-pdb in that project’s development dependencies and prepare its debug environment through the project’s package manager. This is not a Mibyan SDK install recipe. For Mibyan, prefer the declared debugpy dependency; the remote-pdb examples below require a separately declared, freshly built debug environment, never an in-place pip install into the selected application generation. In your code:
Then from the terminal:
remote-pdb is the cleanest agent-friendly choice when debugpy’s DAP protocol is overkill. Use debugpy only when you actually need IDE integration.

Debugging Mibyan-specific Processes

Tests

See Recipe 3. The wrapper captures subprocess output, so run pytest directly for interactive pdb.

run_agent.py / CLI — one-shot

In the prepared debug checkout, add breakpoint() near the suspect line, then run python mibyan. Control returns to your terminal at the pause point.

tui_gateway subprocess (spawned by mibyan --tui)

The gateway runs as a child of the Node TUI. Options: A. Source-edit the gateway:
Start python mibyan --tui from the prepared debug checkout. The TUI will appear frozen (its backend is waiting). Attach a client; execution resumes when you continue. Check the child’s interpreter and imports before assuming it inherited the debug environment. B. Use remote-pdb at a specific handler:
Trigger the matching slash command from the TUI, then nc 127.0.0.1 4444 in another terminal.

_SlashWorker subprocess

Same pattern — remote-pdb with set_trace() inside the worker’s exec path. The worker is persistent across slash commands, so the first trigger blocks until you connect; subsequent slash commands pass through normally unless you re-arm.

Gateway (gateway/run.py)

Long-lived. Use remote-pdb at a handler, or debugpy with --wait-for-client if you’re restarting the gateway anyway.

Common Pitfalls

  1. pdb under a parallel/output-capturing runner silently does nothing. You won’t see the prompt, the test just hangs (true of pytest-xdist and of scripts/run_tests.sh’s captured per-file subprocesses). Run pytest directly on a single file for interactive debugging.
  2. breakpoint() in CI / non-TTY contexts hangs the process. Safe locally; never commit it. Add a pre-commit grep as a safety net.
  3. PYTHONBREAKPOINT=0 disables all breakpoint() calls. Check the env if your breakpoint isn’t hitting:
  4. debugpy.listen blocks only if you also call wait_for_client(). Without it, execution continues and your first breakpoint may fire before the client is attached.
  5. Attach to PID fails on hardened kernels. ptrace_scope=1 (Ubuntu default) allows only same-user ptrace of child processes. Workaround: echo 0 > /proc/sys/kernel/yama/ptrace_scope (needs root) or launch under debugpy from the start.
  6. Threads. pdb only debugs the current thread. For multithreaded code, use debugpy (thread-aware DAP) or set threading.settrace() per thread.
  7. asyncio. pdb works in coroutines but await inside pdb requires Python 3.13+ or await from interact mode on older versions. For 3.11/3.12, use asyncio.run_coroutine_threadsafe tricks or !stmt-based awaits via asyncio.ensure_future.
  8. scripts/run_tests.sh strips credentials and sets HOME=<tmpdir>. If your bug depends on user config or real API keys, it won’t reproduce under the wrapper. Debug with raw pytest first to repro, then re-confirm under the wrapper.
  9. Forking / multiprocessing. pdb does not follow forks. Each child needs its own breakpoint() or set_trace(). For Mibyan subagents, debug one process at a time.

Verification Checklist

  • In the independently built debug environment, confirm: .venv/bin/python -c "import debugpy; print(debugpy.__version__); print(debugpy.__file__)"
  • For remote debug, confirm the port is actually listening: ss -tlnp | grep 5678
  • First breakpoint actually hits (if it doesn’t, you likely have PYTHONBREAKPOINT=0, you’re under a parallel/capturing runner, or execution finished before attach)
  • where / w shows the expected call stack
  • Post-debug cleanup: no stray breakpoint() / set_trace() in committed code

One-Shot Recipes

“Why is this dict missing a key?”
“This test passes in isolation but fails in the suite.”
“My async handler deadlocks.”
Trigger the handler. nc 127.0.0.1 4444, then w to see the suspended frame, !import asyncio; asyncio.all_tasks() to see what else is pending. “Post-mortem on a crash in an Ink child process / subprocess.”