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:
2026-08-07 01:43:01 +00:00
co-authored by Claude Opus 5
parent d8ee678ec4
commit 876b39b301
8 changed files with 173 additions and 3 deletions
@@ -379,3 +379,15 @@ export function clearSession(sessionKey: string): void {
export function getActiveSessionKeys(): string[] {
return Array.from(sessions.keys());
}
/**
* Is a turn actually in flight for this session, right now, in this process?
*
* This is the ground truth a reconnecting browser has no way to work out for itself. Officer's own view
* dies with `pm2 restart officer` while the turn keeps running here, so "officer doesn't remember" means
* nothing — and if *this* process was the one that restarted, the session is simply absent and the turn
* it was running is gone, however alive the client still believes it to be.
*/
export function isSessionGenerating(sessionKey: string): boolean {
return sessions.get(sessionKey)?.isGenerating ?? false;
}
@@ -190,6 +190,10 @@ async function handleCommand(cmd: SidecarCommand, reply: ReplyFn) {
reply({ type: 'claude:interrupted', id: cmd.id });
break;
case 'claude:is-generating':
reply({ type: 'claude:generating', id: cmd.id, generating: claudeManager.isSessionGenerating(cmd.sessionKey) });
break;
case 'claude:clear-session':
claudeManager.clearSession(cmd.sessionKey);
sessionLog.drop(cmd.sessionKey);