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
+9 -1
View File
@@ -608,7 +608,15 @@ commandHqRouter.delete('/commanders/:sessionId', async (ctx) => {
output — except `nudges`, which is a live schedule that would keep firing at a conversation that no
longer exists.
*/
await forgetChatSessions(forgotten);
/*
`sessionKeys` carries the commander's own key and nothing else, on purpose. Children are spawned by
`claude -p` outside officer's websocket, so they never had a key and never wrote a durable event — an
empty entry for them would be noise, not caution.
`sessionKey` is resolved above for the kill and reused here rather than looked up twice; when it is null
the session was already gone, and so were its events.
*/
await forgetChatSessions({ transcriptIds: forgotten, sessionKeys: sessionKey ? [sessionKey] : [] });
// Said back rather than a bare ok: this is the one irreversible action here, and the counts are what
// make it auditable after the fact.