retry a turn the agent restart cut off

the cut-off notice is its own event now rather than an error: nothing is broken
and nothing is lost but the turn, so the row says what happened and offers the
one action that fixes it. the conversation is already durable — the claude
session id is written through to disk and passed back as resume: — so retry
just resends the prompt on a session the fresh agent picks up with full
context. read back out of the transcript, so a second window on the same
session can offer it too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 02:00:16 +00:00
co-authored by Claude Opus 5
parent 876b39b301
commit 66fca28927
7 changed files with 83 additions and 10 deletions
+25 -3
View File
@@ -550,15 +550,37 @@ Two paths, because the tab can be in two states:
from its own memory, which died with the process, so it asks the agent over a new `claude:is-generating`
command. The agent is the only party that knows.
Either way you get a line saying the agent restarted and the turn was cut off, and it's written to
`chat_session_events` — so a reload afterwards shows the same explanation rather than a conversation that
trails off mid-tool-call.
Either way you get a seam across the transcript — **AGENT RESTARTED — TURN CUT OFF** — with a **Retry**
button beside it that sends the same prompt again.
The retry is the whole reason this is worth having, and it works because most of what looked like the
hard problem is already solved. `sessionKey → claude session_id` is written through to
`data/<email>/sidecar/claude-state.json` on every change — not just at shutdown, so it survives a
`SIGKILL` — and `createSession` passes it back as `resume:`. **A restarted agent costs you the turn, not
the conversation:** the next prompt picks the thread up from the transcript on disk with full context.
Retry just spares you scrolling up to copy what you'd said.
It's deliberately a seam and not a red error bubble. Nothing is broken and nothing is lost but the turn,
so the row's job is to say what happened and offer the one action that fixes it. The prompt is read back
out of the transcript rather than remembered separately, because this can land in a second window on the
same session — one that never sent it.
Making the turn _itself_ survive is the part that stays unsolved, and deliberately so. The Agent SDK
spawns `claude` as a child with piped stdio; re-adopting it after the sidecar dies would mean the CLI
becoming a detached grandchild talking over a socket, i.e. not using the SDK's process management at all.
That is a large, risky rewrite that buys exactly one turn — and Retry buys most of it for thirty lines.
The liveness check **fails toward alive**: a timeout, or no answer, is read as "still running". Telling
you a turn died while it is quietly typing would be a worse lie than a spinner that stays up a bit
longer. Only a registered agent answering "no", or no agent at all, counts as dead. OpenCode sessions are
left alone — that harness runs a turn per invocation and has no equivalent question.
The notice is also appended to `chat_session_events`, but **don't count on it surviving a refresh**. That
table is keyed by officer's own session id, while a session reopened from history is addressed by
Claude's transcript uuid; the two converge once a conversation has been resumed at least once, and don't
before that. The live case is the one that matters here and it is unaffected. Untangling those two ids is
a separate job.
**Not verified:** the browser, and the restart itself. The mechanism is reasoned from the code plus the
`pm2` logs that pinned the cause; typecheck and the full 365-test suite are clean.