anthropics / anthropics/claude-code

Bridge/queue-delivered user turns are parented to a stale leaf, forking the transcript and silently stranding hours of work (3/3 forks in one session, distinct from #89820)

Abierto
#91,632 0 comentarios 0 reacciones 0 asignados Ver en GitHub
area:core area:desktop bug data-loss has repro platform:windows
Lenguaje dominante
Python
Estrellas
145k
Forks
23.1k
Métricas de merge de PR
Métricas de PR pendientes

Descripción

### Preflight

- Searched. Closest: **#89820** (auto-injected continuation parented to a stale `last-prompt` `leafUuid`), **#89640** (stale extension process flushes late tool_results onto an abandoned branch), **#88597** (loader keeps one branch on resume). This is the same *class* — resume/tip selection on a `parentUuid` tree — but a **different trigger and a different stale source**: see "Why this is not #89820" below.
- Single bug, one causal chain, evidenced from one transcript.
- Claude Desktop (Windows/MSIX), engine `2.1.247`.

### What's wrong

A user turn that arrives through the **session bridge** (an external client posting into a running session, delivered via `queue-operation enqueue`/`dequeue`) is written with `parentUuid` pointing at a **stale leaf** rather than the conversation's live tip. The tree forks. Everything written on the resulting branch — real user turns and the work they produced — is off the branch the window renders and off the model's context. No error, no banner, nothing in `main.log`.

In the measured session this stranded **190 records containing 11 human turns, spanning 9.5 hours.**

### Evidence

Transcript `~/.claude/projects//b57a8c60-….jsonl` — 3223 records, 10 leaves, 43 human turns, covering `2026-08-31T17:33:25Z` → `2026-09-02T22:02:35Z`.

The fork:

```
fork parent line 3666 uuid 20f04735 assistant 2026-09-01T23:13:34Z
├ branch A line 3667 assistant (tool_use) 2026-09-01T23:13:53Z
│ 424 records, 6 human turns, grows to 2026-09-02T22:02:35Z <- live
└ branch B line 3674 user "…" 2026-09-02T01:23:01Z
190 records, 11 human turns, ends at 2026-09-02T10:53:08Z <- stranded
```

Branch B was attached **2 hours 9 minutes after** branch A had already continued from the same parent.

The five records immediately preceding the graft identify the delivery path:

```
line 3670 mode
line 3671 bridge-session <-- external client attaches
line 3672 queue-operation {"operation":"enqueue","content":"…"}
line 3673 queue-operation {"operation":"dequeue"}
line 3674 user parentUuid=20f04735 userType=external <-- stale parent, 2h09m old
isMeta=null isSidechain=false <-- a real human turn
```

### The signature is 3 out of 3

Every fork in this session is preceded by the identical record sequence:

| fork parent (UTC) | late child | preceding records |
|---|---|---|
| 2026-08-31T22:49:09 | line 3551 | `agent-name > mode > bridge-session > queue-operation > queue-operation` |
| 2026-09-01T23:13:34 | line 3674 | `agent-name > mode > bridge-session > queue-operation > queue-operation` |
| 2026-09-02T17:37:21 | line 4146 | `agent-name > mode > bridge-session > queue-operation > queue-operation` |

Turns typed directly in the desktop window produced **zero** forks in this session. The file holds 261 `bridge-session` and 82 `queue-operation` records, so the bridge path is heavily used and only some deliveries misparent — consistent with the stale value being a cache that is sometimes current.

### Why this is not #89820

#89820's mechanism is the auto-injected `isMeta` continuation inheriting a stale `last-prompt` `leafUuid`. Both are absent here:

- The grafted record is `isMeta: null`, `userType: "external"`, with real user text — **not** a `"Continue from where you left off."` meta turn.
- The file contains **246 `last-prompt` records**, and **none** of them has a `leafUuid` equal to the fork parent `20f04735`. The stale parent did not come from `last-prompt`.

It is also not #89640: the late records here are a user turn and its attachment chain, not error `tool_result`s answering abandoned `tool_use` ids.

### Likely proximate trigger

`main.log` shows `WarmLifecycle` disconnecting idle sessions on a 900-second timer throughout, e.g.:

```
2026-09-02 10:22:44 [WarmLifecycle:session] Idle timeout reached, disconnecting local_…
2026-09-02 10:22:48 [WarmLifecycle:session] Idle timeout reached, disconnecting local_…
2026-09-02 10:23:01 <- graft written (KST; = 01:23:01Z)
```

13–17 seconds separate a disconnect sweep from the misparented write. A bridge client posting into a session that has just been idle-disconnected is the shape that matters here, and it is far more frequent than an app restart: **it can recur every 15 minutes of inactivity**, which is exactly the cadence of a human conversing through an external client.

I am flagging this as a correlation, not a proven cause — the disconnect lines name other session ids, and I could not tie one to this session's `local_…` id from the log alone.

### What should happen

1. On delivering a queued/bridged turn, resolve the parent **at dequeue time from the live tip**, not from a value captured when the client attached.
2. If the captured parent already has descendants, either adopt the newest leaf or refuse and surface it. Silently grafting onto a node that has moved on is the data-loss step.
3. Failing both, persist an explicit tip pointer and prefer it over any client-supplied parent (the more robust fix #89640 also asks for).

### Repro

1. Open a session in the desktop app.
2. Attach an external client to that same session over the bridge and post turns from it.
3. Let the session sit idle past the 900s `WarmLifecycle` timeout between posts.
4. Post again from the external client.
5. Inspect the transcript: the new turn's `parentUuid` can point at a leaf that already has descendants; from then on the window and the model see only the other branch.

### Notes on measurement

Timestamps in the transcript are UTC (`Z`); the desktop `main.log` is local time. Leaf and fork counts alone are misleading — an assistant node with a `tool_use` child and a `tool_result` child is a normal two-child node, not a conversation fork. The discriminator used above is **two or more child branches that each contain at least one non-`tool_result`, non-`isMeta`, non-`isSidechain` user turn**; a scanner using that rule found 56 such forks across 187 transcripts (1.45 GB) on this machine, and was validated against hand-built positive and negative fixtures before the numbers were trusted.

Guía de contribución

No hay ninguna guía de contribución indexada para este repositorio

Línea de trabajo

Start by reproducing with Claude Desktop plus an external bridge client: open a session, post through the bridge, wait past the 900s WarmLifecycle idle timeout, then post again. Inspect the transcript JSONL under ~/.claude/projects// for bridge-session and queue-operation records followed by a user record whose parentUuid already has descendants. Done means bridged turns attach to the live tip or surface an error instead of silently forking.

Escrito por el modelo de indexación a partir del texto del issue.

Evaluación

Stack tecnológico
python
Área
backend, data
Tipo de issue
Error
Dificultad
4/5
Tiempo estimado
3-5 días
Estado de actividad
Activo
Claridad
Bastante claro
Aptitud para principiantes
45/100

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.