Repository navigation
tui: interrupted tool can remain stuck in preparing state #35454
Description
Summary
After a stream interruption, a tool that has already failed durably can remain rendered as active, for example Preparing write..., even after later assistant work completes.
The daemon restart loop in #35448 makes this much easier to hit because it repeatedly disconnects the event stream. It is a trigger, not the root cause of this TUI bug: any disconnect in the same window can expose it.
Exact Failure Chain
- The TUI receives
session.tool.input.startedand renders a pending tool part. - Its event stream disconnects before it receives the terminal tool event.
- The server durably commits
session.tool.failedandsession.step.failedand projects the tool asstatus: error. - On reconnect, the TUI refetches the Session messages.
- The fetched snapshot is correct, but the refresh merge keeps the existing same-ID local non-user message.
- The stale local
pendingtool part therefore wins over the authoritative fetchederrorpart and remains visible indefinitely.
Evidence
Observed in Session ses_0cce8f001ffeJMBMk6hIBAIH3e on V2/next:
- The durable event log contained
session.tool.failedfollowed bysession.step.failed. - The message API returned the write tool with
status: error. - The TUI retained a local copy with
status: pendingand renderedPreparing write....
This establishes the fault boundary: persistence and server projection were correct; client reconciliation failed to converge to them.
Reproduction Shape
- Start a tool call and let the TUI observe its pending/input-start state.
- Disconnect the event stream before the terminal tool event reaches the TUI.
- Let the server interrupt or fail the tool and commit the terminal state.
- Reconnect and refetch messages.
- Observe that the tool remains pending locally despite the fetched server projection being terminal.
A deterministic regression test can inject the local pending message, return a same-ID terminal snapshot from message.list, run reconnect hydration, and assert that the terminal snapshot wins.
Required Contract
Reconnect hydration must converge loaded transcripts to the durable server projection while preserving genuinely newer local streaming state.
At minimum:
- A fetched terminal tool state must replace an older local pending state for the same durable message/part.
- Replayed or duplicate start events must not downgrade a terminal tool back to pending.
- A stale fetched snapshot must not overwrite local events known to be newer than that snapshot.
- The repair must run for already-loaded Sessions after event-stream reconnect, not only when a Session is opened again.
Acceptance Criteria
- The failure chain above is covered by a deterministic TUI test.
- After reconnect,
pending -> errorandpending -> completedserver transitions repair the rendered tool. - Duplicate/replayed start events are idempotent and cannot regress terminal state.
- Newer in-flight local state is not overwritten by an older fetched snapshot.
- The fix does not depend on solving durable global event sync.
Distinctions
- V2: reconnecting clients can restart-loop the shared daemon #35448 causes repeated disconnects but is not required to reproduce this reconciliation bug.
- V2: first Location can expose an empty plugin generation #35556 is unrelated: it concerns SDK-contributed tools never entering a Location's plugin generation. This issue concerns a tool call that did start but whose terminal state was missed by the TUI.
Activity
thdxr commentedon Jul 9, 2026
Closing this tracker after the recent V2 server lifecycle, restart continuity, reconnect hydration, and synchronization work. This area has changed substantially, and we want to track any remaining or newly reproducible behavior as a fresh, focused issue against the current implementation.