forgetting a conversation needs both of its ids, not one twice

`chat_session_events` is keyed on officer's SESSION KEY. The other three tables
are keyed on the harness's TRANSCRIPT ID. Both are uuids for the same
conversation, and `forgetChatSessions` took one list and passed it to all four —
so the events delete matched nothing. Not sometimes: by construction, every time.
A deleted conversation left its entire durable output behind.

It survived two verifications because counting events BY TRANSCRIPT ID also
returns zero by construction, and zero read as "cleaned". What settled it was
reading an orphaned event instead of counting it — the payload carries both ids
side by side and says outright which key the row is filed under.

So the parameter is now named for its id space. That is the whole defence and it
is the only one available: nothing else distinguishes the two, since both are
uuids of the same shape, and a positional `string[]` cannot be got wrong loudly.

Empty `sessionKeys` is legitimate and common — a session spawned by `claude -p`
outside officer's websocket never had a key and never wrote an event, which is
exactly a commander's children.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 22:50:29 +00:00
co-authored by Claude Opus 5
parent 03e7ca39c3
commit 8ac3ca7fe5
3 changed files with 61 additions and 17 deletions
+12 -6
View File
@@ -1,5 +1,5 @@
import type { Context } from 'hono';
import { emitToSession } from './websocket';
import { emitToSession, sessionKeyForTranscript } from './websocket';
import { pauseBackgroundTask, restartBackgroundTask } from './task-restart';
import { createRouter } from '../../create-router';
import * as errors from '@@/custom-errors';
@@ -422,11 +422,17 @@ chatRouter.delete('/sessions/:id', async (ctx) => {
const removed = deleteClaudeSession(who, cwd, id);
if (!removed.length) return ctx.text('Not found', 404);
// The transcript is gone; forget what the platform stored ABOUT it. Four tables are keyed on a transcript
// id with no foreign key — there is no row for a conversation to reference — so nothing cascades and this
// has to be explicit. One of them, `nudges`, is a live schedule: left behind it keeps firing at a
// conversation that no longer exists.
await forgetChatSessions(removed);
/*
The transcript is gone; forget what the platform stored ABOUT it. Four tables describe a conversation with
no foreign key — there is no row for one to reference — so nothing cascades and this has to be explicit.
One of them, `nudges`, is a live schedule: left behind it keeps firing at a conversation that is gone.
Two id spaces, and they are not interchangeable: three tables are keyed on the transcript id, while
`chat_session_events` is keyed on officer's session key. Undefined here is the normal case for an old
conversation — no live session holds it, so nothing is keyed on a key to remove.
*/
const liveKey = sessionKeyForTranscript(id);
await forgetChatSessions({ transcriptIds: removed, sessionKeys: liveKey ? [liveKey] : [] });
return ctx.json({ ok: true });
});