deleting a bot stops it, and a dead bot wakes up
a bot was deleted tonight and its watcher was still polling a website eleven minutes later. nothing in the product could find it: a bot's watch is a detached shell process, killing the harness ends the agent, and the loop it started is not its child. so a bot writes <sessionId>.bot.json beside its script — what it watches, the script, the state file, the pids. that record is the ONLY thing connecting a process to the bot that owns it, and the brief now says so in those words rather than as bookkeeping. the id is in the FILENAME because bots share a directory: deleting knows only an id, so this is a lookup rather than a scan, and a record whose contents are damaged still gets found and cleaned up. DELETE /chat/sessions/:id tears it down after forgetting the tables — children first then the recorded pid, same ordering as stopCommanderProcesses, so the loop cannot spawn a replacement between the two. every transcript in the returned chain, not just the id asked for. unconditional rather than gated on "was this a bot", because the record only exists in the bots directory and a branch on cwd is a second place to get it wrong. script and state go too, and only if they are inside the bots directory — an agent wrote that path. and the reviver sweeps bots on the same tick. a record with pids and none of them alive is a stopped watch; there is no armed flag and there should not be, since the record is written when it arms, so its existence IS the intent. the three-wake bound is shared, keyed bot-<sessionId>. the wake prompt forbids reconstructing a watch from memory when the record is gone: a watch subtly different from the one agreed is worse than none, because it looks like it is working. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { getPluginChatsDir, getBotsDir } from '@@/data-path';
|
||||
import { tearDownBot } from '../bots/teardown';
|
||||
import type { Context } from 'hono';
|
||||
import { emitToSession, sessionKeyForTranscript } from './websocket';
|
||||
import { pauseBackgroundTask, restartBackgroundTask } from './task-restart';
|
||||
@@ -470,6 +471,26 @@ chatRouter.delete('/sessions/:id', async (ctx) => {
|
||||
conversation — no live session holds it, so nothing is keyed on a key to remove.
|
||||
*/
|
||||
await forgetChatSessions({ transcriptIds: removed, sessionKeys: liveKey ? [liveKey] : [] });
|
||||
|
||||
/*
|
||||
And stop anything a BOT armed, which nothing above would have touched.
|
||||
|
||||
A bot's watch is a detached shell process. Killing the harness ends the agent; the loop it started is
|
||||
not its child and carries on. That gap was paid for on 2026-08-26: a bot was deleted and its watcher
|
||||
was still polling a website eleven minutes later, with nothing in the product able to find it.
|
||||
|
||||
Every transcript removed, not just the id asked for — `deleteClaudeSession` returns a chain, and a
|
||||
resumed conversation's earlier transcripts can each carry their own record.
|
||||
|
||||
Unconditional rather than gated on "was this a bot": the record only exists in the bots directory and
|
||||
is keyed by session id, so for anything else this reads a path that is not there and returns 0. A
|
||||
branch on the cwd would be a second place to get the question wrong.
|
||||
*/
|
||||
for (const transcriptId of removed) {
|
||||
const stopped = tearDownBot(who.email, transcriptId);
|
||||
if (stopped > 0) console.log(`[bots] ${transcriptId} deleted — stopped ${stopped} process(es)`);
|
||||
}
|
||||
|
||||
return ctx.json({ ok: true });
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user