Skip to content

[Bug]: Agents have no tool to stop a top-level Claude thread's background command after its turn ends #26189

Closed

Description

@AnobleSCM

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. Start a top-level thread on the Claude provider. Ask it to run sleep 900 with the Bash tool and run_in_background: true, call t3_background_wait on that background task, then end its turn. (t3_background_wait does not keep the turn open.)
  2. When t3_thread_read shows the thread completed with activeRunId: null, call t3_thread_interrupt on it from another thread: once with no runId, then once with the finished run's runId.
  3. Check the process list. The sleep 900 is still running.

Expected behavior

An agent can end a top-level thread's background work after its turn has ended. For example, t3_thread_interrupt on a top-level thread with no active run could send thread.stop, the way task_cancel does for a delegated child.

Actual behavior

  • t3_thread_interrupt with no runId returns {"runId":null,"status":"no_active_run"}.
  • t3_thread_interrupt with the finished run's runId returns {"status":"completed"}.
  • In both cases the background command keeps running. Ending its process by hand stopped it. I did not try the web or desktop Stop button for background work ([Bug]: Mobile has no way to stop background work after the turn settles #14655). From reading the installed server code, no other orchestration MCP tool and no t3 CLI subcommand sends thread.stop to a top-level thread, so an agent cannot reach that stop.

This matches the tool description ("No effect when idle"), but it leaves a top-level thread with no agent-callable stop for background work that outlives its turn.

For a delegated child, task_cancel does end the command when the child called t3_background_wait on it (a Monitor works too), so task_status reads waiting_for_children. It sends thread.stop, which also stops the thread's background work, and it accepts a task in waiting_for_children after its run has finished. In one run where the child skipped the wait, task_cancel returned cancel_requested after the run had ended, the task then read completed, and the command kept running. A top-level thread has no equivalent.

Related, not duplicates: #16739 (delegated-task cancel; a comment there sees no_active_run on a turn-ended child), #14655 (web/desktop Stop for background work after a turn), #19926 (closed; asked for background work on t3_thread_read; on this build t3_thread_read still shows a delegated child completed with activeRunId: null while task_status reads waiting_for_children).

Impact

Minor bug or occasional failure

Version or commit

t3 v0.1.3-nightly.20261025.3669

Environment

macOS 26.5 (top-level case) and macOS 27.0 (delegated-child cases). Headless t3 serve runs as a launchd service on each. Threads use the Claude provider, Claude Sonnet 5.5, Claude Code 2.1.311. One agent thread manages the others through the orchestration MCP tools; in the top-level case it reached the thread's server from another machine.

Workaround

Before stopping or archiving a top-level Claude thread, have the thread report each background command's pid, start time and task output file. End only a pid that lsof -t <output file> lists and whose start time matches, then check that it is gone.

Activity

  1. juliusmarminge commented on Oct 27, 2026

    @juliusmarminge
    Member

    This is fixed on main by cb98850 (https://git.cafe/pingdotgg/t3code/pulls/16684, merged 2026-10-27), which came after the 2026-10-25 nightly you tested. t3_thread_interrupt now takes stop: true. That sends thread.stop to the thread, the same thing its Stop button does, and it also works when the thread has no active run but still has background work.

    What I ran on main at b88da91, with a dev server on a fresh database, the Claude provider and Claude Sonnet 5.5:

    1. A top-level Claude thread ran sleep 917 through Bash with run_in_background: true, called t3_background_wait, and ended its turn. t3 thread show reported completed with activeRunId: null, and sleep 917 was still running.
    2. From a second thread, t3_thread_interrupt with only threadId returned {"runId":null,"status":"no_active_run"}. With the finished runId it returned {"status":"completed"}. The sleep was still running after both calls, as you reported.
    3. From a third thread, t3_thread_interrupt with threadId and stop: true returned {"runId":"…:ordinal:1","status":"interrupt_requested"}, and sleep 917 was gone.
    4. Variant: the holder thread started sleep 918 in the background and did not call t3_background_wait. t3_thread_interrupt with stop: true returned interrupt_requested, and sleep 918 was gone.

    A plain interrupt still does nothing on an idle thread, as the tool description says. Pass stop: true to end a top-level thread's background work after its turn. It ships in the next nightly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions