From 586c929d36d440e2300c38e2e5333be621708911 Mon Sep 17 00:00:00 2001 From: Vellum Assistant Date: Wed, 25 Feb 2026 09:16:50 -0500 Subject: [PATCH] fix: resolve handleRecordingStop globally for cross-conversation stop Co-Authored-By: Claude --- .../src/__tests__/recording-handler.test.ts | 22 ++++++++++++ assistant/src/daemon/handlers/recording.ts | 36 +++++++++++++------ 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/assistant/src/__tests__/recording-handler.test.ts b/assistant/src/__tests__/recording-handler.test.ts index 305a29a04ef..b9ac31f7884 100644 --- a/assistant/src/__tests__/recording-handler.test.ts +++ b/assistant/src/__tests__/recording-handler.test.ts @@ -238,6 +238,28 @@ describe('handleRecordingStop', () => { expect(result).toBeUndefined(); }); + test('resolves to globally active recording from a different conversation', () => { + const { ctx, sent, fakeSocket } = createCtx(); + const convA = 'conv-owner'; + const convB = 'conv-stopper'; + + // Bind socket to conv-A (the owning conversation) + ctx.socketToSession.set(fakeSocket, convA); + + // Start a recording on conv-A + const recordingId = handleRecordingStart(convA, undefined, fakeSocket, ctx); + expect(recordingId).not.toBeNull(); + sent.length = 0; + + // Stop from conv-B — should resolve to the globally active recording on conv-A + const result = handleRecordingStop(convB, ctx); + + expect(result).toBe(recordingId!); + expect(sent).toHaveLength(1); + expect(sent[0].type).toBe('recording_stop'); + expect(sent[0].recordingId).toBe(recordingId!); + }); + test('returns undefined when no socket bound to conversation', () => { const { ctx, fakeSocket } = createCtx(); const conversationId = 'conv-no-socket'; diff --git a/assistant/src/daemon/handlers/recording.ts b/assistant/src/daemon/handlers/recording.ts index cc11dd48e63..0e1f6e6a1f9 100644 --- a/assistant/src/daemon/handlers/recording.ts +++ b/assistant/src/daemon/handlers/recording.ts @@ -63,30 +63,44 @@ export function handleRecordingStart( // ─── Stop ──────────────────────────────────────────────────────────────────── /** - * Stop the active standalone recording for a conversation. - * Looks up the recording ID from `recordingOwnerByConversation` and sends - * a `recording_stop` message to the client. + * Stop the active standalone recording. + * First checks if the given conversation owns a recording; if not, falls back + * to the globally active recording (since only one can be active at a time). + * This allows users to stop a recording from a different conversation than + * the one that started it. * * Returns the recording ID if a stop was sent, or `undefined` if no active - * recording was found for the conversation. + * recording exists. */ export function handleRecordingStop( conversationId: string, ctx: HandlerContext, ): string | undefined { - const recordingId = recordingOwnerByConversation.get(conversationId); + let recordingId = recordingOwnerByConversation.get(conversationId); + let ownerConversationId = conversationId; + + // Global fallback: since only one recording can be active at a time, + // resolve globally if the current conversation doesn't own a recording. + if (!recordingId && recordingOwnerByConversation.size > 0) { + const [activeConv, activeRec] = [...recordingOwnerByConversation.entries()][0]; + recordingId = activeRec; + ownerConversationId = activeConv; + log.info({ conversationId, ownerConversationId, resolvedRecordingId: recordingId }, 'Resolved stop to globally active recording'); + } + if (!recordingId) { - log.debug({ conversationId }, 'No active standalone recording to stop for conversation'); + log.debug({ conversationId }, 'No active standalone recording to stop'); return undefined; } - // Look up the socket currently bound to the conversation so we can send - // the stop command to the correct client connection. - const socket = findSocketForSession(conversationId, ctx); + // Look up the socket currently bound to the owning conversation so we can + // send the stop command to the correct client connection. + const socket = findSocketForSession(ownerConversationId, ctx) + ?? findSocketForSession(conversationId, ctx); if (!socket) { - log.warn({ conversationId, recordingId }, 'Cannot send recording_stop: no socket bound to conversation'); + log.warn({ conversationId, ownerConversationId, recordingId }, 'Cannot send recording_stop: no socket bound to conversation'); standaloneRecordingConversationId.delete(recordingId); - recordingOwnerByConversation.delete(conversationId); + recordingOwnerByConversation.delete(ownerConversationId); return undefined; }