Session branches while the agent is still running, and the abandoned branch keeps executing tools

Status Open
Reported on v2.1.222
Maintainer reply None cached
Activity 0 comments · opened Aug 11, 2026

Summary

Twice in one evening, the desktop client created a new session branch while the agent was still
mid turn. The original session was not cancelled. It kept running for 30 seconds and 2 minutes
respectively, executing further tool calls against live production hardware, while the UI presented
the new branch as the conversation.

The two branches then acted on the same devices within seconds of each other. Neither could observe
the other's writes. One branch changed a configuration value on four network devices; the other read
those same devices immediately afterward, saw the new value, and concluded that its own earlier and
correct diagnosis had been mistaken. That incorrect retraction was committed to a git repository
before the collision was found.

The damage here was recoverable. The same mechanism on a destructive operation would not be.

Environment

Claude Code           2.1.222
entrypoint            claude-desktop
OS                    Windows 10 Home 10.0.19045
transcripts           ~/.claude/projects/<project-slug>/*.jsonl

What happens

A branch appears as a new session file whose head is a copy of the parent transcript, followed by an
injected meta turn:

{"type":"user","isMeta":true,"promptId":"...","message":{"content":"Continue from where you left off."}}
{"type":"assistant","message":{"content":"No response requested."}}
{"type":"user","origin":{"kind":"human"},"promptSource":"sdk","message":{"content":"<the real message>"}}

That marker is the normal resume path. Across 41 session files in this project it appears 9 times,
and 7 of those are unremarkable: gaps of 27 minutes to 3 days, where the session had genuinely ended
and the user returned to it. The client titles the new file <name> (fork), then (fork 2).

The two pathological cases are the ones where the same resume fired while the agent was still
executing
.

| Branch created (UTC) | Cut point was behind the agent by | Parent's state at the cut |
|---|---|---|
| 2026-08-10T23:23:05 | 16 s | Bash in flight, issued 23:23:03 |
| 2026-08-11T00:40:56 | 208 s | 186 s Bash had returned 10 s earlier, still generating |

In the first case the parent had already emitted an assistant message at 23:22:53 and issued a Bash
at 23:23:03. The branch was cut at 23:22:49, so the new branch never contained either record. The
client branched from its own view of the transcript, which was behind the agent's actual state.

The part that causes harm

The parent is not stopped. After each branch was taken it ran to completion:

branch 1   parent continued 7 records, 1 further tool call, ending 23:23:38Z
branch 2   parent continued 16 records, 4 further tool calls at 00:41:12, 00:41:26,
           00:42:04 and 00:42:15, ending 00:42:41Z

Those were not idle records. The parent of branch 2 wrote a configuration change to four networked
cameras at 00:42:21 through 00:42:26. The child branch performed a read of the same four devices at
00:42:26 and reported them as already carrying the new value.

The result is worse than a duplicated action, because the second branch treats the first branch's
write as evidence about the world:

00:41:30   branch A   reads device: setting = X
00:42:21   branch A   writes setting = Y on all four devices
00:42:26   branch B   reads all four: "already Y"
00:43:39   branch B   concludes the original reading of X was an error, and retracts it

Branch B's reasoning is locally sound. It measured the devices and they disagreed with its earlier
finding, so it corrected itself. There is no signal available to it that another branch of the same
session made the change one minute earlier. A correct diagnosis was withdrawn and the withdrawal was
written to a repository.

Two further collisions from the same two events:

  1. Duplicate outbound side effects. Both branches independently sent a test push notification, so

two arrived. In a workflow that posts, pays, deploys or deletes, this is the whole problem.

  1. Shared scratchpad. The scratchpad path is keyed to the originating session id, and branches

inherit it. Both branches wrote the same file path, clobbering each other.

Reproduction

Not established, and I want to be straight about that. Both branches followed a message sent while
the agent was working. But a message sent mid turn is normally injected into the running turn
instead, which is the overwhelmingly common behaviour and which I confirmed twice while writing this
report, with no new session file created. So mid turn input is not by itself sufficient.

The one property both branches share, and which inline delivery does not, is that the cut point was
behind the agent's real position
, by 16 and 208 seconds. My hypothesis is that the client had
silently lost the event stream, concluded the session was idle, and re-attached using the ordinary
resume path, branching from its stale view while the agent carried on. The second branch followed a
186 second tool call, which was the longest of 1,566 calls measured in these transcripts against a
p99 of 129 s, so a client side timeout on a long running tool is a plausible way to lose the stream.
The first followed a 2 second call, so that alone does not explain both.

This is a hypothesis. It would be falsified by a branch occurring when the client's view is provably
current.

How to verify from transcripts

The relationships are recoverable from the JSONL alone, without reproducing the fault:

  • Branch descent is provable by shared record uuids. The last shared uuid is the exact cut.
  • Branch children preserve their ancestors' sessionId on copied records. Compaction children

rewrite copied records to their own sessionId and insert a
{"type":"system","subtype":"compact_boundary"} record. That distinguishes the two cleanly.

  • Whether the agent was live at the cut must be measured in the parent file. Measuring it in the

child gives a false negative every time, because the child's copied history stops at the cut, so no
tool call can appear outstanding. I made exactly this mistake while investigating and it inverted
my conclusion.

Suggested fixes, in order of value

  1. Cancel the parent turn when a branch is taken, or at minimum refuse to start further tool

calls on it. An abandoned branch executing side effecting tools with live credentials is the
defect, independent of whatever triggers the branch.

  1. Do not treat a session as idle without confirming it. If the client has lost the stream, probe

the agent's state before offering resume. Resuming a running session is the actual fault here; the
branch is a symptom.

  1. Cut the branch at the agent's real position, not the client's last received record. The 16 and

208 second lags mean the new branch is missing work that already happened, so it starts from a
false picture of the world.

  1. Surface the collision. If a session has a live sibling, say so. Neither branch had any way to

know, and that is what turned a duplicated action into a wrong conclusion.

  1. Give each branch its own scratchpad directory, rather than inheriting the originating

session's.

  1. Disambiguate the recents list. Compaction inherits the parent's title, so a compacted branch

and its parent appear as two entries with the identical name (fork 2), one live and one dormant
holding an unanswered message.

Severity

Medium to high, depending on the tools in play. It is silent in both directions: nothing warns the
user that a second branch is running, and nothing warns either branch that the other exists. It
produced duplicate outbound messages, concurrent writes to production hardware, and a documented,
committed, incorrect conclusion. On any workflow with irreversible tools the same mechanism executes
the irreversible action twice.

View original on GitHub ↗