end a turn whose agent has gone
restarting officer-agent takes every persistent session with it and nothing downstream notices: the browser's socket is healthy, officer's subscription is a bus filter, and there is simply never another event. the spinner ran forever and a refresh didn't help, because the transcript has no ending to read. keyed off the agent *registering*, not disconnecting — a disconnect fires on every `pm2 restart officer`, when the turn is fine. a registration socket dies with its process, so an agent appearing on it is a new one. covers the sitting tab; the reconnect path covers the rest, with the client now sending its belief that a turn is in flight and officer checking it against the agent over a new claude:is-generating. the check fails toward alive. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -522,6 +522,48 @@ ends.
|
||||
|
||||
---
|
||||
|
||||
## 19. The turn that never ends
|
||||
|
||||
**The symptom you described:** the last thing on screen is a tool call, the spinner runs forever, and
|
||||
refreshing puts you back on the same dead conversation. It looked like a cancel you didn't press.
|
||||
|
||||
**What actually happens.** It is the agent sidecar restarting. `officer-agent` holds the persistent
|
||||
`query()` for each session, so when that process goes, so does every turn it was running — and nothing
|
||||
downstream finds out. The browser's socket is to `officer`, which is fine; officer's subscription is an
|
||||
event-bus filter, which is also fine; there is simply never another event. The `pm2` logs make the two
|
||||
cases plain: `[sidecar] disconnected from API server` with no `SIGINT` beside it is officer restarting
|
||||
underneath a healthy agent (turn survives — that's the documented design), while `[agent] SIGINT
|
||||
received` is the case that kills turns.
|
||||
|
||||
So the fix could not key off the sidecar _disconnecting_ — that fires on every `pm2 restart officer`,
|
||||
when nothing is wrong. It keys off a **registration**: a registration socket lives and dies with its
|
||||
process, so an agent appearing on it is an agent that has just started, and anything it was mid-turn on
|
||||
is gone.
|
||||
|
||||
Two paths, because the tab can be in two states:
|
||||
|
||||
- **The tab is sitting there with a live socket.** The new agent registers, and every session officer
|
||||
still believes is generating gets checked and ended. On a freshly-restarted _officer_ this loop is
|
||||
empty — no sessions yet — which is right, because that case belongs to the other path.
|
||||
- **The tab reconnects** (socket blip, or a refresh, or officer itself restarted). The client now sends
|
||||
`generating` in its resume handshake — its belief that a turn is in flight. Officer can't confirm that
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
## Things noticed and deliberately left alone
|
||||
|
||||
- **`useChatWebSocket` silently ignores unparseable frames.** That one is intentional and the comment
|
||||
|
||||
Reference in New Issue
Block a user