← Pi Agents

Debugging Pi

What Pi records when something goes wrong — pi-debug.log, /bug, /session, /hotkeys — and how extension lifecycle rules explain most failures before you read a single log line.

The extension worked yesterday. Today the same command does nothing, the transcript looks fine, and there is no error anywhere. You have three things you can look at, and none of them is a stack trace: /debug, /bug, and /session.

That is not a gap in the tooling — it is a shape. Pi is a long-lived process with an event pipeline, an extension runtime that can be replaced mid-session, and a model that produces whatever it produces. Most failures here are not crashes. They are a lifecycle rule violated, a mode guard missing, or a message that is not where you expected it. This chapter is about reaching for the right artefact for each class, and about the small number of extension rules that explain most of what goes wrong.

Start with /debug

usage.md is precise about what this does:

When troubleshooting terminal rendering or conversation state, run /debug. Pi writes the rendered terminal lines and current session messages to pi-debug.log in your agent directory.

Two things in that sentence matter. It writes the rendered terminal lines, so it captures what the terminal actually displayed — which is how you diagnose a rendering bug rather than a state bug. And it captures current session messages, so you get the transcript as Pi reconstructed it for the model, not as you think you sent it.

# then, from another terminal
type "$HOME/.pi/agent/pi-debug.log"

The documentation adds a warning that is not optional: review this file before sharing it. It can contain prompts, model responses, tool output, file contents, and terminal data. The same warning appears on /export and /share, and on the /bug report. Three separate warnings about the same class of file is Pi telling you that sessions are sensitive by default.

If the problem is the terminal rather than the session, tui.md names a different instrument: set PI_TUI_WRITE_LOG to capture the raw ANSI stream. That is what you want when a theme renders wrong or a resize leaves stale lines, because it shows the bytes rather than Pi’s interpretation of them.

/bug for something you cannot reproduce

/debug captures the current moment. /bug [description] prepares a report. From sessions.md:

The report includes environment and provider configuration without credential values, plus recorded error diagnostics. Upload it through radius.pi.dev or export the same report as a zip to inspect and share yourself.

Read “without credential values” as a property of the report format, not a promise about its contents. It will still contain your conversation if the diagnostics include it — which is why the same section says you can include the session transcript, omit it, or ask the current model to summarize the problem. That last option is the one to reach for when the transcript is large and you know what you were trying to do.

Uploads do not require a login, and with Radius authentication the report is attributed to your account so the developers can follow up. If an upload fails, Pi offers to export the zip instead, so you are never blocked on the network.

Three questions Pi can answer for you

Question Command What it tells you
Which session am I actually in? /session File, ID, message count, token usage, cost
Which shortcuts are live right now? /hotkeys The bindings active in this session
Which resources loaded? The startup header Instructions, extensions, skills, templates

/session is the one people skip and then need. When you cd into a different folder, Pi uses that folder for session grouping — so “the session I resumed is not the one I wanted” is almost always a working-directory question, and /session answers it in one keystroke. usage.md also notes that after leaving Pi you can run pi --continue from the same folder to resume that folder’s most recent session, which makes the folder the first thing to check.

--verbose is the startup-side equivalent: it shows verbose interactive startup information, overriding quietStartup. That is what you want when a resource you expected to load did not appear in the header.

The extension rules that explain most failures

Most “it silently did nothing” reports in extensions come from one of four documented rules.

1. The factory is not a place to start processes

extensions.md states it directly: “Do not start processes, sockets, watchers, or timers in the factory because some invocations load extensions without starting a session.” Start long-lived resources from session_start or from the command or tool that needs them, and close session-scoped resources from an idempotent session_shutdown handler.

A handler registered in the factory but a resource started there is the classic failure: it works in an interactive session, does nothing under --print, and leaves a socket behind on exit.

2. Command-context operations are command-only

ExtensionCommandContext adds operations for waiting until idle, reloading, tree navigation, and session replacement — and extensions.md explains why they are not on ExtensionContext: “These operations are command-only because calling them from lifecycle handlers can deadlock the runtime.”

If a lifecycle handler hangs, this is the first thing to check.

3. Reload replaces the runtime, it does not patch it

Reload replaces the extension runtime, so code after await ctx.reload() must not reuse state from the old runtime.

State captured before the reload and used after it is undefined behaviour that usually looks like stale data rather than a bug. If a command works once and then returns nothing on the second run, this is your hypothesis.

4. A handler failure is not a no-op — but it is not a crash either

extensions.md draws the line precisely: Pi reports handler errors and continues where possible, a tool_call handler failure blocks the tool as a fail-safe, and a tool execution failure becomes an error result for the model.

That third clause is the one to design around. If your gate blocks a tool, the model does not see a crash; it sees a failed result and will usually try something else. So a gate that is “too strict” does not look like a strict gate in the transcript — it looks like a model that keeps retrying.

There is a matching rule for tools themselves: throw from execute() to produce a failed tool result, because “Returning an object does not mark it as an error.” A tool that returns { isError: true } as plain data teaches the model nothing.

Errors that only exist in one mode

Before reading a log, check that the failure is not a mode problem. extensions.md gives the rule: extensions load in interactive, RPC, JSON, and print modes, JSON and print modes have no UI at all, and RPC can forward supported dialogs but not custom terminal components.

So a ctx.ui.confirm() in a --print run is not a bug in the extension — it is a documented absence of UI. The fix is the ctx.hasUI guard from the custom-UI chapter, and if the failure is ctx.ui.custom() specifically, the fix is ctx.mode === "tui". rpc-extension-ui.md gives the long list of methods that are no-ops or degraded in RPC mode, including getTheme() returning undefined and setTheme() returning { success: false }.

When the stream itself is the problem

provider_stream_event fires for each parsed provider stream event before Pi normalises it. extensions.md documents three properties worth remembering while debugging a handler you added:

  • Handlers are awaited in stream order, so slow handlers delay stream consumption. A handler doing network I/O per token will look like a stalled stream.
  • Handler errors are reported without changing the provider response.
  • event.data is the earliest structured value available to Pi, not the original HTTP bytes or SSE frame.

There is a shipped viewer for this: debug-provider.ts, an opt-in extension that groups raw events by assistant message. If you suspect a provider adapter problem rather than an extension problem, that is the first tool to reach for, and it is worth reading as an example in its own right.

Reconstruct the session, not the screen

When the model behaves as if it never saw something, the question is what was in context — and that is a session question, not a transcript question. /session gives the message count; the session file gives the rest.

A quick check:

pi --export ~/.pi/agent/sessions/<path>/<session-id>.jsonl session.html

Or, without leaving the session, /export writes HTML or JSONL and /copy (Ctrl+X) puts the last assistant response on the clipboard.

Remember that the model receives the active branch, not every branch in the session file. If you have been exploring alternatives with /tree or /fork, the branch you are not on is still in the file and still irrelevant to what the model saw. session-manager.ts-level debugging is in the SDK chapter; the terminal-side fact is simply that the branch is the unit of context.

Compaction failures look like model failures

sessions.md is explicit: compaction can fail if the provider is unavailable or cannot accept the summarisation request. The instruction is to correct the provider problem and run /compact again. Disabling automatic compaction does not disable the manual command.

This matters because a failed compaction leaves the context uncompacted. The next request is larger than expected, which can look like a slow model or a truncated conversation. If a long session suddenly degrades, check compaction before blaming the model.

Which artefact to reach for

Most of these are answerable without a log:

    flowchart TD
    Q{"What kind of failure?"}
    Q -->|"wrong session or folder"| S["/session"]
    Q -->|"shortcut did nothing"| K["/hotkeys"]
    Q -->|"a resource did not load"| H["startup header, or start with --verbose"]
    Q -->|"only in one mode"| M["ctx.mode and ctx.hasUI"]
    Q -->|"the model ignored something"| C["the active branch, not the file"]
    Q -->|"a long session degraded"| P["/compact: compaction can fail"]
    Q -->|"still unexplained"| G["/debug, then pi-debug.log"]
    G -->|"the terminal bytes are wrong"| L["PI_TUI_WRITE_LOG"]
    L -->|"still unexplained"| B["/bug, transcript omitted or summarised"]
  

All but one of these cost a single keystroke. The exception is the lifecycle rules, which you can only check by already knowing them.

What this chapter does not settle

It does not settle which failures deserve a fix in your code and which are documented behaviour. A tool that refuses to run in --print mode because it draws a custom screen is not broken; an extension that returns stale state after ctx.reload() is. Telling those two apart requires knowing what Pi promises — which is what the next four chapters are about.

Next: The Terminal Interface — the surface you have been debugging through, described as a system rather than a habit.