deleting a conversation forgets what the platform stored about it

Found by a sanity check after two commanders were deleted through the UI: the Command HQ tables cascaded
perfectly and four OTHER tables kept a row each, for ever. 24 orphaned rows in chat_session_models, 26 in
chat_session_events, against 14 live transcripts.

Nothing cascades because there is nothing to cascade FROM. All four are keyed on a transcript id, and a
transcript is a file the harness writes — there is no row for a conversation, so the database cannot help and
the delete path has to be explicit. It was not.

Three of the four are inert: a model, a bell preference, a durable copy of turn output nothing will ask for
again. `nudges` is not — it is a SCHEDULE, read by the tick in nudger.ts. A nudge belonging to a deleted
conversation is a timer that keeps firing at something that no longer exists. That is what turned this from
untidy into worth doing now.

deleteClaudeSession returns the IDS it removed rather than a boolean, because a `/clear` chain is several
transcripts presenting as one conversation and forgetting the head while orphaning its parts is the same bug
at a smaller scale.

THE TRAP THAT CHANGE INTRODUCED, caught before shipping: every call site said `if (deleteClaudeSession(...))`
and an empty array is TRUTHY in JavaScript, so a delete that found nothing would have reported success. The
typecheck was clean — `string[]` is a perfectly good truthy expression. All three sites test `.length` now.
Fifth silently-wrong thing today that a typecheck could not see.

Cleared the existing orphans with the same function the delete path now uses, after checking the one thing
that would have made it wrong: whether any of them belonged to a member, whose transcripts live in a home
this scan does not read. There is one account on this machine, so they were all the owner's.

Two chat_session_events rows appeared again immediately after, and are not a leak: the durable log is written
independently of the transcript — that is what makes turn output survive officer being down — so a session
that has just begun legitimately has events before a transcript exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 20:54:23 +00:00
co-authored by Claude Opus 5
parent a6e7f64ef2
commit cb51f35cad
6 changed files with 124 additions and 17 deletions
+6 -3
View File
@@ -642,9 +642,12 @@ const findTranscript = (who: ChatIdentity, cwd: string, sessionId: string): stri
* routinely (see `findTranscript`), and the wrong group would find no chain and silently delete one
* part of several.
*/
export function deleteClaudeSession(who: ChatIdentity, cwd: string, sessionId: string): boolean {
export function deleteClaudeSession(who: ChatIdentity, cwd: string, sessionId: string): string[] {
const filePath = findTranscript(who, cwd, sessionId);
if (!filePath) return false;
// The IDS it removed, not a boolean — empty means nothing was there. The caller needs them to forget the
// rows keyed on each one (`forgetChatSessions`), and a `/clear` chain is several transcripts presenting as
// one conversation: returning true would have told a caller to forget the head and orphan its parts.
if (!filePath) return [];
const ownCwd = firstCwd(who.osUser, filePath);
const ids = ownCwd ? chainFileIds(who, ownCwd, sessionId) : [sessionId];
@@ -653,7 +656,7 @@ export function deleteClaudeSession(who: ChatIdentity, cwd: string, sessionId: s
// success — the conversation reappeared on the next refresh.
const dir = dirname(filePath);
for (const id of ids) removeAs(who.osUser, join(dir, `${id}.jsonl`));
return true;
return ids;
}
/**