From 0bfad0e59284557d85d372c46e4216e7f4edea89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sat, 22 Aug 2026 21:25:39 +0000 Subject: [PATCH] deleting a commander ends its session, not just its loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Its own test caught this too. Delete removed the row and the transcript, and the transcript was BACK a minute later — because the commander's `claude` process was still alive and rewrote it. That happened twice tonight and both times it read as the delete having failed. Killing the loop was never enough: the loop is a grandchild of the session, so ending it leaves the session running, still holding the conversation open and still able to write. So the session is ended first, with `killClaudeSession` — which already existed and does the whole teardown (abort, close input, settle pending plans and questions). It takes officer's session key while this route holds a transcript id, so the map is read backwards through `findSessionKeyByClaudeSession`, which exists for the same reason a refreshed browser holds only the uuid. Ending the session takes its children with it; the pid-based stop still runs afterwards, catching anything that outlived its parent. The response reports `sessionKilled` beside `processesStopped` so the two are distinguishable in a report rather than one number standing for both. Co-Authored-By: Claude Opus 5 --- src/servers/api/command-hq/router.ts | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/servers/api/command-hq/router.ts b/src/servers/api/command-hq/router.ts index d88c75c0..42246ecf 100644 --- a/src/servers/api/command-hq/router.ts +++ b/src/servers/api/command-hq/router.ts @@ -6,6 +6,8 @@ import * as errors from '@@/custom-errors'; import { resolveHomeDir } from '@@/user-home'; import { getCommandersDir, toShellUsername } from '@@/data-path'; import { forgetChatSessions, getChatEventsSince, ingestSpawnedChildren } from 'officerdb'; +import { killClaudeSession } from '@@/sidecar/claude/claude-manager'; +import { findSessionKeyByClaudeSession } from '@@/sidecar/claude/state'; import { clearSpawnedRecord, listSpawnedSessions, @@ -541,7 +543,21 @@ commandHqRouter.delete('/commanders/:sessionId', async (ctx) => { const row = await getCommandHqSession(user.id, sessionId); if (!row) throw errors.NOT_FOUND('not a commander'); - // 1. Everything this commander is still running. + /* + 1. Everything this commander is still running — the SESSION first, then the loop. + + Killing the loop alone is not enough, and its own test proved it: the loop is a grandchild of the + commander's `claude` process, so ending the loop leaves the session alive — and a live session REWRITES + its transcript after the file is deleted. Twice tonight a deleted commander's transcript reappeared, and + twice it read as the delete having failed. + + `killClaudeSession` takes officer's session key while this route holds a transcript id, so the map is + read backwards — `findSessionKeyByClaudeSession`, which exists for the same reason a refreshed browser + holds only the uuid. Ending the session takes its children with it, and the pid-based stop below then + catches anything that outlived its parent. + */ + const sessionKey = findSessionKeyByClaudeSession(sessionId, user.id); + const sessionKilled = sessionKey ? killClaudeSession(sessionKey, user.id) : false; const stopped = stopCommanderProcesses(who.email, sessionId); // 2. Everything it spawned. Best-effort per child: one transcript that has already been cleaned up must @@ -593,7 +609,13 @@ commandHqRouter.delete('/commanders/:sessionId', async (ctx) => { // Said back rather than a bare ok: this is the one irreversible action here, and the counts are what // make it auditable after the fact. - return ctx.json({ ok: true, processesStopped: stopped, childrenFound: children.length, childrenDeleted }); + return ctx.json({ + ok: true, + sessionKilled, + processesStopped: stopped, + childrenFound: children.length, + childrenDeleted, + }); }); /**