# fix\(server\): Stop stops a thread's subagents again · PR \#23886 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/23886)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: merged

Source repository: Unavailable fork

Source branch: fix/stop-stops-subagents

Target branch: main

Head commit: 4cae03c1816552936fb38c665c9de44aaa2414e5

[Discussion and reviews](https://git.cafe/pingdotgg/t3code/pull/23886)

[Commits](https://git.cafe/pingdotgg/t3code/pull/23886/commits?format=markdown)

[Files changed](https://git.cafe/pingdotgg/t3code/pull/23886/diffs?format=markdown)

## Description

---

Closes #23871

## Problem

Pressing Stop on a thread is supposed to stop the subagents it delegated to. Since #22052, the Stop button sends `thread.stop`, and that path never stops the children. It still turns off their completion notifications. So a child delegated in an earlier turn keeps running after Stop, finishes later, and the parent never hears about it. That's the stall in #23871.

#16002 had put the child-stop cascade on the old Stop path (`run.interrupt` with `holdQueue`). `thread.stop` was internal back then, and the caller walking the tree stopped each child itself. #22052 moved the Stop button to `thread.stop` but didn't move the cascade with it.

## Fix

- A `thread.stop` with delegated children now queues the same `delegated-tasks.stop` effect the old Stop path queues. It goes after the interrupt, since a thread's effects run in order.
- The server's own tree walks (`stopDelegatedTasks` and the MCP `cancel_task` tool) already stop each child themselves under their caller's permission-mode limit. They pass the new `stopDelegatedTasks: false`, so a child doesn't queue a second walk below itself.
- A Stop that changes nothing still queues the cascade. Before, a command that wrote no events kept only its receipt and dropped its effects. That's the case of pressing Stop again on a thread an older server already stopped without stopping its children. `EventSink.commitCommand` now commits a command that has effects but no events.
- Older clients need no change: leaving the field out means Stop also stops the children.

The issue also proposes a way to selectively resume disabled notifications after Stop. This PR doesn't add that. Stop now stops the children as the docs say ("Stop on a thread also stops the subagents it delegated to"), so nothing is left running with no way to wake the parent.

## Evidence

Same steps on main and on this branch, in the web UI on Linux with a Codex thread and empty test state:

1. Ask the agent to delegate one async child that runs `sleep 600`, then end its turn.
2. Send an unrelated turn that runs `sleep 120`, and press Stop while it runs.

**Before (main):** after Stop the child is still running ("Lineage · 1 running"). `pgrep` on the host still showed its `sleep 600` process 20 seconds later.

![before](https://github.com/user-attachments/assets/829b9b54-dcba-47aa-8b01-756eec25426f)

**After (this branch):** Stop also stops the child. It moves to "Previous agents", and `pgrep` on the host no longer finds its `sleep 600` process.

![after](https://github.com/user-attachments/assets/7f0578be-83fd-4aaf-984f-5165e7dd22a2)

Test: `ThreadStop.test.ts` now runs the "Stop stops the delegated tasks under the thread" case for both `run.interrupt` and `thread.stop`. The `thread.stop` case fails on main (`expected [] to include 'delegated-tasks.stop'`) and passes here. It also checks that the walk queues only the one root effect. A new test, "a repeated Stop that changes nothing still stops the delegated tasks", fails without the `EventSink` change and passes with it. `vp run check:changed` passes.

Written by Claude Opus 5.5 in Claude Code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)



