From c04fa15eb5e89bd826296fa484e6ac7427f17bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sat, 22 Aug 2026 23:16:42 +0000 Subject: [PATCH] ask the events table which key a transcript belongs to, instead of a live map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The id-space fix worked only for conversations younger than the process. The caller resolves a session key from an in-memory map filled as sessions announce themselves, so anything created before the last restart resolves to null and the key list arrives empty — which is exactly what the first live test did. Everything else cascaded; the events stayed, silently, for the one reason the code could not report. The answer was already in the rows. Every event carries `claudeSessionId` in its payload: durable, restart-proof, and the one place the two id spaces are written down beside each other. One event carrying that id resolves the key for all of that session's rows — including the majority that carry no id of their own. `sessionKeys` stays as a fast path for a live session and is now a hint rather than the answer. A cleanup that only works for conversations younger than the process is not a cleanup. Co-Authored-By: Claude Opus 5 --- .../officer_db/src/chat-session-cleanup.ts | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/databases/officer_db/src/chat-session-cleanup.ts b/src/databases/officer_db/src/chat-session-cleanup.ts index b287a4b1..a181c2aa 100644 --- a/src/databases/officer_db/src/chat-session-cleanup.ts +++ b/src/databases/officer_db/src/chat-session-cleanup.ts @@ -1,4 +1,4 @@ -import { inArray } from 'drizzle-orm'; +import { inArray, sql } from 'drizzle-orm'; import { db } from './db'; import { chatSessionEvents } from './chat-events/schema'; import { chatNotificationPrefs } from './chat-notification-prefs/schema'; @@ -66,7 +66,27 @@ export async function forgetChatSessions({ transcriptIds, sessionKeys = [] }: Fo await db.delete(chatSessionModels).where(inArray(chatSessionModels.sessionId, transcriptIds)); await db.delete(chatNotificationPrefs).where(inArray(chatNotificationPrefs.sessionId, transcriptIds)); } - if (sessionKeys.length) { + /* + Events, resolved from the table's OWN data rather than from the caller's key. + + `sessionKeys` is a fast path and not a reliable one: the caller gets it from an in-memory map that is + empty for any session created before the last restart, so the first real test of this deleted everything + except the events — the key came back null and the list was empty. A cleanup that only works for + conversations younger than the process is not a cleanup. + + So ask the rows. Every event carries `claudeSessionId` in its payload, which is durable, survives any + restart, and is the one place the two id spaces are written down together. One id inside the JSON is + enough to resolve the key for all of that session's rows, including the events that carry no id of their + own — which is most of them. + */ + if (transcriptIds.length) { + const keysFromEvents = await db + .selectDistinct({ sessionId: chatSessionEvents.sessionId }) + .from(chatSessionEvents) + .where(inArray(sql`${chatSessionEvents.event}->>'claudeSessionId'`, transcriptIds)); + const keys = [...new Set([...sessionKeys, ...keysFromEvents.map((r) => r.sessionId)])]; + if (keys.length) await db.delete(chatSessionEvents).where(inArray(chatSessionEvents.sessionId, keys)); + } else if (sessionKeys.length) { await db.delete(chatSessionEvents).where(inArray(chatSessionEvents.sessionId, sessionKeys)); } }