a deleted bot stays deleted

the bot came back three times after being deleted, untitled each time, and the
mechanism is worse than an orphan process.

its watch had no record, so deleting it stopped nothing. the watch kept polling,
fired, and the hand-back resumed the session by transcript id — at which point
`claude --resume` RECREATED the transcript that had just been deleted. so the
bot reappeared in the list and did it again on the next fire. same shape
22a9d58d fixed for commanders, arriving from a different direction.

the record could never have covered this: it depends on the bot having written
one, and a bot that did not is precisely the one nobody can stop. a mechanism
that only works when the agent remembered is not a mechanism.

so deleting also sweeps: any process running a .sh inside the bots directory
whose pid appears in NO record is killed. claimed pids are never touched,
whatever else is true — a live bot's watch is the one thing this must not
reach. reads /proc rather than shelling out, since it runs inside the delete.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-26 08:14:46 +00:00
co-authored by Claude Opus 5
parent f6bd7564bb
commit d58652bf14
2 changed files with 71 additions and 1 deletions
+7 -1
View File
@@ -1,5 +1,5 @@
import { getPluginChatsDir, getBotsDir } from '@@/data-path';
import { tearDownBot } from '../bots/teardown';
import { tearDownBot, sweepOrphanWatches } from '../bots/teardown';
import type { Context } from 'hono';
import { emitToSession, sessionKeyForTranscript } from './websocket';
import { pauseBackgroundTask, restartBackgroundTask } from './task-restart';
@@ -491,6 +491,12 @@ chatRouter.delete('/sessions/:id', async (ctx) => {
if (stopped > 0) console.log(`[bots] ${transcriptId} deleted — stopped ${stopped} process(es)`);
}
// And anything left running out of the bots directory that no remaining record claims. This is what
// catches a bot that never wrote one — which cannot be stopped by id, and whose watch RESURRECTS it on
// its next fire, because the hand-back resumes a transcript that `claude --resume` then recreates.
const orphaned = sweepOrphanWatches(who.email);
if (orphaned > 0) console.log(`[bots] swept ${orphaned} orphaned watch process(es)`);
return ctx.json({ ok: true });
});