stop the harness before deleting its transcript, and wait for it to actually stop
Deleting a commander removed its transcript and the `claude` CLI wrote it straight back. Verified rather than inferred: the file returned with 22 lines, every one timestamped after the delete, and the deleted commander was sitting in /chat as "(untitled)" — no longer in `command_hq_sessions`, so nothing filtered it out. Delete a commander, get an untitled chat back. Two causes, and the first one hid the second. The kill never happened. `killClaudeSession` was imported straight from `claude-manager` into a route that runs in `officer`, while the session map lives in `officer-claude-code`. It read an empty map in the wrong process and returned false every time. `claude:kill` and `killClaude` already existed for this; the route just wasn't using them. And killing is not being dead. `abort()` returns while the CLI is still unwinding, and officer does not own that file — the CLI does. So a session now carries an `exited` promise resolved by the same exit hook that already reaps it, `claude:kill` replies only once that resolves, and both delete routes await the reply before touching the filesystem. Bounded at 5s and never fatal. A child that will not die delays a delete by seconds instead of hanging it, and a sidecar that is down leaves the caller with exactly the race it already had. `markExited` fires before `dropDeadSession`'s identity guard, deliberately: a waiter has to be released even when a newer session has taken its key, or it blocks for the full timeout on a process that died on schedule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -330,6 +330,26 @@ export function killClaude(sessionKey: string, userId: number): void {
|
||||
sendFire('claude', { type: 'claude:kill', id: nextId(), sessionKey, userId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill a session and wait for the agent to confirm its child process is gone.
|
||||
*
|
||||
* For callers that then delete the session's FILES. The transcript belongs to the `claude` CLI, not to
|
||||
* officer, so removing it while the CLI is still unwinding deletes a file that is written straight back —
|
||||
* which resurrects a conversation the user deleted, as an untitled ordinary chat that nothing filters out.
|
||||
*
|
||||
* Never fatal: a sidecar that is down or slow must not block a delete. The caller proceeds and accepts the
|
||||
* race it was already running, which is no worse than before this existed.
|
||||
*/
|
||||
export async function killClaudeAndWait(sessionKey: string, userId: number): Promise<boolean> {
|
||||
try {
|
||||
const sc = await waitForHandler('claude');
|
||||
const res = await sendCommandToSidecar(sc, { type: 'claude:kill', id: nextId(), sessionKey, userId });
|
||||
return res.type === 'claude:killed';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Interrupt the current turn but keep the persistent session warm (the "stop" button).
|
||||
export function interruptClaude(sessionKey: string, userId: number): void {
|
||||
sendFire('claude', { type: 'claude:interrupt', id: nextId(), sessionKey, userId });
|
||||
|
||||
Reference in New Issue
Block a user