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:
@@ -65,9 +65,31 @@ export function registerSidecar(ws: ServerWebSocket<any>, registration: SidecarR
|
||||
console.log(
|
||||
`[registry] registered sidecar "${registration.name}" (id=${id}, capabilities=[${registration.capabilities.join(', ')}])`,
|
||||
);
|
||||
|
||||
// A registration socket lives and dies with its process, so an agent showing up here is an agent that
|
||||
// has just started — and whatever it was running before is gone. Announced rather than inferred from
|
||||
// the *disconnect*, which is the wrong signal entirely: every `pm2 restart officer` drops these sockets
|
||||
// while the sidecars, and their turns, carry on perfectly well.
|
||||
if (registration.capabilities.includes('claude')) {
|
||||
for (const handler of claudeRestartHandlers) {
|
||||
try {
|
||||
handler();
|
||||
} catch (err) {
|
||||
console.error('[registry] claude-restart handler failed', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
const claudeRestartHandlers = new Set<() => void>();
|
||||
|
||||
/** Notified when the agent sidecar registers — i.e. when a new agent process has come up. */
|
||||
export function onClaudeSidecarStarted(handler: () => void): () => void {
|
||||
claudeRestartHandlers.add(handler);
|
||||
return () => claudeRestartHandlers.delete(handler);
|
||||
}
|
||||
|
||||
export function unregisterSidecar(id: string): void {
|
||||
const sc = sidecars.get(id);
|
||||
if (!sc) return;
|
||||
@@ -273,6 +295,29 @@ export function interruptClaude(sessionKey: string): void {
|
||||
sendFire('claude', { type: 'claude:interrupt', id: nextId(), sessionKey });
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the agent still running a turn for this session?
|
||||
*
|
||||
* Asked of the sidecar because it is the only party that can answer: officer's own memory of a turn dies
|
||||
* with `pm2 restart officer` while the turn itself carries on, so "we don't remember it" is not evidence
|
||||
* of anything. Used on reconnect to tell those two apart — a turn that outlived a restart from one whose
|
||||
* process is gone and is never going to produce another token.
|
||||
*
|
||||
* Fails *toward alive*: no answer means we don't know, and wrongly telling someone their turn died while
|
||||
* it is still typing is worse than leaving a spinner up a little longer. Only a registered agent that
|
||||
* says "no", or no agent at all, counts as dead.
|
||||
*/
|
||||
export async function isClaudeGenerating(sessionKey: string): Promise<boolean> {
|
||||
const sc = findSidecarByCapability('claude');
|
||||
if (!sc) return false;
|
||||
try {
|
||||
const res = await sendCommandToSidecar(sc, { type: 'claude:is-generating', id: nextId(), sessionKey });
|
||||
return res.type === 'claude:generating' ? res.generating : true;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export function clearClaudeSession(sessionKey: string): void {
|
||||
sendFire('claude', { type: 'claude:clear-session', id: nextId(), sessionKey });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user