Summary
Clicking Stop while a tool call is in flight does not unstick the session. The LLM request is aborted (recorded in the session JSONL), but the session keeps showing as running, the Stop button appears dead, and new prompts stay blocked — until the tool's own timeout argument eventually fires. In our case that was 8 minutes of a frozen session; if the tool call has no timeout, the session is stuck indefinitely.
Environment
- pi-web
0.8.6
@earendil-works/pi-coding-agent 0.83.0
- context-mode
1.0.169 (the ctx_execute tool runs in its sandbox via the Pi MCP bridge)
- Linux container, Node
v22.22.1, provider opencode-go / deepseek-v4-flash
Reproduction timeline (from the session JSONL)
- Model issues
ctx_execute with timeout: 600000 (npm install github:NetrisTV/ws-scrcpy). The sandbox child spawns and hangs on network I/O.
- User clicks Stop at 07:16:08. The JSONL records
"stopReason":"aborted","errorMessage":"Operation aborted" — so the model request was aborted. But the session remains in "running" state; further Stop clicks do nothing visible; the input stays blocked.
- At 07:24:19 — exactly 600 s after dispatch — the tool's own timeout fires, the sandbox is SIGKILLed, the
toolResult arrives, and only then does the run settle and the session become idle.
Net effect: the Stop button was dead for the full remaining tool timeout (8 minutes). If the model had omitted timeout (the schema allows it), the session would have stayed stuck forever.
Root cause analysis
- pi-web's session wrapper reports running as
promptRunning || inner.isStreaming || inner.isCompacting || inner.isBashRunning; inner.isStreaming is _isAgentRunActive, which stays true from prompt start until the run fully settles — including while the turn is awaiting a tool result.
- The Stop button sends the
abort command, which calls session.abort() = abortRetry() + agent.abort() + await waitForIdle(). agent.abort() only aborts the LLM HTTP stream; with a tool in flight there is no stream to abort, and waitForIdle() blocks until the tool promise settles.
- The agent loop (
pi-agent-core executeToolCallsParallel) awaits all in-flight tool executions with Promise.all; the abort signal is only checked between tool calls, so an in-flight call is never settled by abort.
- The
ctx_execute tool itself is not cancellable end-to-end: the Pi MCP bridge ignores the abort signal and tools/call is unbounded, and the context-mode sandbox only kills its child via the timeout param (see context-mode issue #959).
Expected behavior
- Stop should settle the turn promptly: cancel/fail the in-flight tool call (recording an error tool result), so the session returns to idle within seconds.
- The abort path must not block indefinitely on
waitForIdle().
Suggested fixes
- Bound
waitForIdle() in the abort path (e.g. force-settle the run after a short grace period) so the UI never freezes on a hung tool.
- On abort, settle in-flight tool promises and persist an error
toolResult (core agent-loop change — related pi issues: #7336 closed as no-action, #7053 open).
- Propagate the abort signal to MCP tool calls so servers can cancel (context-mode #959, which also needs the server to kill the sandbox process group on cancellation).
Related
Summary
Clicking Stop while a tool call is in flight does not unstick the session. The LLM request is aborted (recorded in the session JSONL), but the session keeps showing as running, the Stop button appears dead, and new prompts stay blocked — until the tool's own
timeoutargument eventually fires. In our case that was 8 minutes of a frozen session; if the tool call has notimeout, the session is stuck indefinitely.Environment
0.8.6@earendil-works/pi-coding-agent0.83.01.0.169(thectx_executetool runs in its sandbox via the Pi MCP bridge)v22.22.1, provideropencode-go/deepseek-v4-flashReproduction timeline (from the session JSONL)
ctx_executewithtimeout: 600000(npm install github:NetrisTV/ws-scrcpy). The sandbox child spawns and hangs on network I/O."stopReason":"aborted","errorMessage":"Operation aborted"— so the model request was aborted. But the session remains in "running" state; further Stop clicks do nothing visible; the input stays blocked.toolResultarrives, and only then does the run settle and the session become idle.Net effect: the Stop button was dead for the full remaining tool timeout (8 minutes). If the model had omitted
timeout(the schema allows it), the session would have stayed stuck forever.Root cause analysis
promptRunning || inner.isStreaming || inner.isCompacting || inner.isBashRunning;inner.isStreamingis_isAgentRunActive, which staystruefrom prompt start until the run fully settles — including while the turn is awaiting a tool result.abortcommand, which callssession.abort()=abortRetry() + agent.abort() + await waitForIdle().agent.abort()only aborts the LLM HTTP stream; with a tool in flight there is no stream to abort, andwaitForIdle()blocks until the tool promise settles.pi-agent-coreexecuteToolCallsParallel) awaits all in-flight tool executions withPromise.all; the abort signal is only checked between tool calls, so an in-flight call is never settled by abort.ctx_executetool itself is not cancellable end-to-end: the Pi MCP bridge ignores the abort signal andtools/callis unbounded, and the context-mode sandbox only kills its child via thetimeoutparam (see context-mode issue #959).Expected behavior
waitForIdle().Suggested fixes
waitForIdle()in the abort path (e.g. force-settle the run after a short grace period) so the UI never freezes on a hung tool.toolResult(core agent-loop change — related pi issues: #7336 closed as no-action, #7053 open).Related
ctx_executecannot be aborted (same incident, server-side analysis in the comments)