ask the agent what it is still running

Officer's session records are in memory and die with `pm2 restart officer`, while the agent is a PM2
peer and keeps generating. `adoptOrphanedSession` rebuilds a binding — but only when a browser
reconnects to a session *by id*, which you can only do if you already knew the id. So a session that
survived a restart was invisible, and nothing could answer "what is running right now".

`claude:list` returns each live session with `isGenerating` and `pendingTasks` — the same two fields the
agent's own `armIdle` consults before collecting a session, so a caller can tell "busy" from "merely
open" the way it does. Surfaced as `GET /chat/live`, which sits beside `/chat/sessions`: those are
transcripts on disk, these are the ones with a process behind them.

`getActiveSessionKeys` is replaced rather than joined. It returned bare keys, could not distinguish a
session mid-turn from one merely open, and had never been called by anything.

`listLiveClaudeSessions` fails toward EMPTY, where `isClaudeGenerating` beside it fails toward alive.
The asymmetry is deliberate: not knowing there means leaving a spinner up, and not knowing here would
mean inventing sessions.

Step 2 of docs/chat-session-lifetime.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 00:35:11 +01:00
co-authored by Claude Opus 5
parent be266da9e2
commit a75bf7a283
5 changed files with 76 additions and 3 deletions
@@ -215,6 +215,10 @@ async function handleCommand(cmd: SidecarCommand, reply: ReplyFn) {
reply({ type: 'claude:interrupted', id: cmd.id });
break;
case 'claude:list':
reply({ type: 'claude:sessions', id: cmd.id, sessions: claudeManager.listSessions() });
break;
case 'claude:is-generating':
reply({ type: 'claude:generating', id: cmd.id, generating: claudeManager.isSessionGenerating(cmd.sessionKey) });
break;