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)
- 主要言語
- Python
- スター
- 145k
- フォーク
- 23.1k
- PR マージ指標
- PR 指標を取得中
説明
### 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.
コントリビューションガイド
このリポジトリのコントリビューションガイドは索引されていません
調査の方向性
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.
索引モデルが issue の本文から書いたものです。
評価
- 技術スタック
- python
- 領域
- backend, data
- issue の種類
- バグ
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 活発さ
- 活発
- 明瞭さ
- おおむね明確
- 初心者へのやさしさ
- 45/100