diff --git a/src/servers/api/chat/websocket.ts b/src/servers/api/chat/websocket.ts index 908fa75b..7c24224d 100644 --- a/src/servers/api/chat/websocket.ts +++ b/src/servers/api/chat/websocket.ts @@ -34,7 +34,6 @@ import { resolveHomeDir } from '@@/user-home'; import { renameClaudeSession, type ChatIdentity } from './claude-sessions'; import { chatIdentity } from './chat'; import { commanderBlock } from '../command-hq/commander-block'; -import { getClaudeSession } from '../../sidecar/claude/state'; import { claudeLoginState } from '@@/os-user-claude'; import { mkdirSync } from 'node:fs'; import { logger } from './logger'; @@ -561,6 +560,8 @@ function createMessageHandler( // written on its very first turn rather than on its second. if (msg.type === 'session:claude' && msg.claudeSessionId) { transcriptId = msg.claudeSessionId; + // The platform's own copy of the mapping — see `transcriptIdBySessionKey`. + transcriptIdBySessionKey.set(sessionId, msg.claudeSessionId); record(); fileAsCommander(); } @@ -904,6 +905,24 @@ export type ClaudeTurnOutcome = { ok: true; transcriptId: string } | { ok: false * generic timeout: the reasons below (member not signed in, Claude not installed, spawn failure) are the * same reasons a websocket caller already sees via `sendToClient`, just mirrored to a second listener. */ +/** + * Officer's session key → Claude's transcript id, for the platform's own use. + * + * The sidecar keeps this mapping and the platform cannot read it: `state.ts` holds it in the sidecar's + * memory, and re-loading its file from here does not find a live session. So the platform keeps its own, + * filled from the `session:claude` message it already handles. + * + * This is the third distinct place the two ids have caused a bug today. The mapping is not derivable — it is + * announced, once, mid-turn — so anything needing it either records it when it passes or does without. + */ +const transcriptIdBySessionKey = new Map(); + +/** The mapping read backwards, for callers holding a transcript id — the delete route, mainly. */ +export function sessionKeyForTranscript(transcriptId: string): string | undefined { + for (const [key, id] of transcriptIdBySessionKey) if (id === transcriptId) return key; + return undefined; +} + /** * Sessions already told what they are and where to record it. * @@ -960,7 +979,7 @@ export async function startClaudeTurn(params: { if (!briefedSessions.has(sessionId)) { const block = await commanderBlock({ userId, - sessionId: getClaudeSession(sessionId, userId) ?? msg.resumeSessionId ?? null, + sessionId: transcriptIdBySessionKey.get(sessionId) ?? msg.resumeSessionId ?? null, }); if (block) { briefedSessions.add(sessionId); diff --git a/src/servers/api/command-hq/router.ts b/src/servers/api/command-hq/router.ts index 42246ecf..a5943c55 100644 --- a/src/servers/api/command-hq/router.ts +++ b/src/servers/api/command-hq/router.ts @@ -8,6 +8,7 @@ import { getCommandersDir, toShellUsername } from '@@/data-path'; import { forgetChatSessions, getChatEventsSince, ingestSpawnedChildren } from 'officerdb'; import { killClaudeSession } from '@@/sidecar/claude/claude-manager'; import { findSessionKeyByClaudeSession } from '@@/sidecar/claude/state'; +import { sessionKeyForTranscript } from '../chat/websocket'; import { clearSpawnedRecord, listSpawnedSessions, @@ -556,7 +557,9 @@ commandHqRouter.delete('/commanders/:sessionId', async (ctx) => { holds only the uuid. Ending the session takes its children with it, and the pid-based stop below then catches anything that outlived its parent. */ - const sessionKey = findSessionKeyByClaudeSession(sessionId, user.id); + // Same mapping problem as the briefing: the sidecar's map is not visible from here, so the platform's own + // is asked first and the sidecar's is kept as a fallback for a session that predates this process. + const sessionKey = sessionKeyForTranscript(sessionId) ?? findSessionKeyByClaudeSession(sessionId, user.id); const sessionKilled = sessionKey ? killClaudeSession(sessionKey, user.id) : false; const stopped = stopCommanderProcesses(who.email, sessionId);