Skip to content

Stop button appears dead when a tool call is in flight — session stays 'running' until the tool's own timeout fires #368

Description

@jiege6-66

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)

  1. Model issues ctx_execute with timeout: 600000 (npm install github:NetrisTV/ws-scrcpy). The sandbox child spawns and hangs on network I/O.
  2. 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.
  3. 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

  1. 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.
  2. 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).
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions