From 483752e8dfe78c4cf5fd0a3218d23d003808efa6 Mon Sep 17 00:00:00 2001 From: "n8n-cat-bot[bot]" <283985454+n8n-cat-bot[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 16:25:16 +0100 Subject: [PATCH] fix(core): Preserve code-builder thread suffix when parsing session (#30829) Co-authored-by: n8n-cat-bot[bot] Co-authored-by: Claude Opus 4.7 Co-authored-by: Declan Carroll --- .../src/ai-workflow-builder-agent.service.ts | 5 ++- .../ai-workflow-builder-agent.service.test.ts | 29 +++++++----- .../ai/ai-session-retrieval-request.dto.ts | 1 + .../__tests__/ai.controller.test.ts | 45 +++++++++++++++++++ packages/cli/src/controllers/ai.controller.ts | 6 ++- ...orkflow-builder-session.repository.test.ts | 21 +++++++++ .../workflow-builder-session.repository.ts | 7 ++- .../ai-workflow-builder.service.test.ts | 16 ++++++- .../services/ai-workflow-builder.service.ts | 4 +- 9 files changed, 115 insertions(+), 19 deletions(-) diff --git a/packages/@n8n/ai-workflow-builder.ee/src/ai-workflow-builder-agent.service.ts b/packages/@n8n/ai-workflow-builder.ee/src/ai-workflow-builder-agent.service.ts index 0578a8f3690..cde78aa4d07 100644 --- a/packages/@n8n/ai-workflow-builder.ee/src/ai-workflow-builder-agent.service.ts +++ b/packages/@n8n/ai-workflow-builder.ee/src/ai-workflow-builder-agent.service.ts @@ -417,9 +417,10 @@ export class AiWorkflowBuilderService { this.onTelemetryEvent('Builder replied to user message', properties); } - async getSessions(workflowId: string | undefined, user?: IUser) { + async getSessions(workflowId: string | undefined, user?: IUser, isCodeBuilder?: boolean) { const userId = user?.id?.toString(); - return await this.sessionManager.getSessions(workflowId, userId, 'code-builder'); + const agentType = isCodeBuilder ? 'code-builder' : undefined; + return await this.sessionManager.getSessions(workflowId, userId, agentType); } async getBuilderInstanceCredits( diff --git a/packages/@n8n/ai-workflow-builder.ee/src/test/ai-workflow-builder-agent.service.test.ts b/packages/@n8n/ai-workflow-builder.ee/src/test/ai-workflow-builder-agent.service.test.ts index 657aacdeae7..a7fcf58ace6 100644 --- a/packages/@n8n/ai-workflow-builder.ee/src/test/ai-workflow-builder-agent.service.test.ts +++ b/packages/@n8n/ai-workflow-builder.ee/src/test/ai-workflow-builder-agent.service.test.ts @@ -550,7 +550,7 @@ describe('AiWorkflowBuilderService', () => { expect(mockSessionManager.getSessions).toHaveBeenCalledWith( undefined, 'test-user-id', - 'code-builder', + undefined, ); }); @@ -578,6 +578,19 @@ describe('AiWorkflowBuilderService', () => { lastUpdated: '2023-12-01T12:00:00Z', }); expect(result.sessions[0].messages).toHaveLength(2); + expect(mockSessionManager.getSessions).toHaveBeenCalledWith( + workflowId, + 'test-user-id', + undefined, + ); + }); + + it('should request code-builder threads when isCodeBuilder is true', async () => { + const workflowId = 'test-workflow'; + (mockSessionManager.getSessions as jest.Mock).mockResolvedValue({ sessions: [] }); + + await service.getSessions(workflowId, mockUser, true); + expect(mockSessionManager.getSessions).toHaveBeenCalledWith( workflowId, 'test-user-id', @@ -597,7 +610,7 @@ describe('AiWorkflowBuilderService', () => { expect(mockSessionManager.getSessions).toHaveBeenCalledWith( workflowId, 'test-user-id', - 'code-builder', + undefined, ); }); @@ -621,7 +634,7 @@ describe('AiWorkflowBuilderService', () => { expect(mockSessionManager.getSessions).toHaveBeenCalledWith( workflowId, 'test-user-id', - 'code-builder', + undefined, ); }); @@ -645,7 +658,7 @@ describe('AiWorkflowBuilderService', () => { expect(mockSessionManager.getSessions).toHaveBeenCalledWith( workflowId, 'test-user-id', - 'code-builder', + undefined, ); }); @@ -658,11 +671,7 @@ describe('AiWorkflowBuilderService', () => { const result = await service.getSessions(workflowId); expect(result.sessions).toEqual([]); - expect(mockSessionManager.getSessions).toHaveBeenCalledWith( - workflowId, - undefined, - 'code-builder', - ); + expect(mockSessionManager.getSessions).toHaveBeenCalledWith(workflowId, undefined, undefined); }); }); @@ -709,7 +718,7 @@ describe('AiWorkflowBuilderService', () => { expect(mockSessionManager.getSessions).toHaveBeenCalledWith( workflowId, 'test-user-id', - 'code-builder', + undefined, ); }); }); diff --git a/packages/@n8n/api-types/src/dto/ai/ai-session-retrieval-request.dto.ts b/packages/@n8n/api-types/src/dto/ai/ai-session-retrieval-request.dto.ts index 5f886dec273..8c8000defb7 100644 --- a/packages/@n8n/api-types/src/dto/ai/ai-session-retrieval-request.dto.ts +++ b/packages/@n8n/api-types/src/dto/ai/ai-session-retrieval-request.dto.ts @@ -4,4 +4,5 @@ import { Z } from '../../zod-class'; export class AiSessionRetrievalRequestDto extends Z.class({ workflowId: z.string().optional(), + codeBuilder: z.boolean().optional(), }) {} diff --git a/packages/cli/src/controllers/__tests__/ai.controller.test.ts b/packages/cli/src/controllers/__tests__/ai.controller.test.ts index 658286ef8a1..087e0bba274 100644 --- a/packages/cli/src/controllers/__tests__/ai.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/ai.controller.test.ts @@ -530,6 +530,51 @@ describe('AiController', () => { }); }); + describe('getSessions', () => { + it('should call workflowBuilderService.getSessions with correct parameters', async () => { + const mockSessions = { sessions: [] }; + const payload = { workflowId: 'workflow123' }; + + workflowBuilderService.getSessions.mockResolvedValue(mockSessions); + + const result = await controller.getSessions(request, response, payload); + + expect(workflowBuilderService.getSessions).toHaveBeenCalledWith( + payload.workflowId, + request.user, + undefined, + ); + expect(result).toEqual(mockSessions); + }); + + it('should forward the codeBuilder flag to the service', async () => { + const mockSessions = { sessions: [] }; + const payload = { workflowId: 'workflow123', codeBuilder: true as const }; + + workflowBuilderService.getSessions.mockResolvedValue(mockSessions); + + const result = await controller.getSessions(request, response, payload); + + expect(workflowBuilderService.getSessions).toHaveBeenCalledWith( + payload.workflowId, + request.user, + true, + ); + expect(result).toEqual(mockSessions); + }); + + it('should throw InternalServerError when service throws an error', async () => { + const payload = { workflowId: 'workflow123' }; + const mockError = new Error('Database error'); + + workflowBuilderService.getSessions.mockRejectedValue(mockError); + + await expect(controller.getSessions(request, response, payload)).rejects.toThrow( + InternalServerError, + ); + }); + }); + describe('truncateMessages', () => { it('should call workflowBuilderService.truncateMessagesAfter with correct parameters', async () => { const payload = { diff --git a/packages/cli/src/controllers/ai.controller.ts b/packages/cli/src/controllers/ai.controller.ts index 24510311cdf..1c70e7eea4e 100644 --- a/packages/cli/src/controllers/ai.controller.ts +++ b/packages/cli/src/controllers/ai.controller.ts @@ -251,7 +251,11 @@ export class AiController { @Body payload: AiSessionRetrievalRequestDto, ) { try { - const sessions = await this.workflowBuilderService.getSessions(payload.workflowId, req.user); + const sessions = await this.workflowBuilderService.getSessions( + payload.workflowId, + req.user, + payload.codeBuilder, + ); return sessions; } catch (e) { assert(e instanceof Error); diff --git a/packages/cli/src/modules/workflow-builder/__tests__/workflow-builder-session.repository.test.ts b/packages/cli/src/modules/workflow-builder/__tests__/workflow-builder-session.repository.test.ts index d30eb177c60..167dd5efd7c 100644 --- a/packages/cli/src/modules/workflow-builder/__tests__/workflow-builder-session.repository.test.ts +++ b/packages/cli/src/modules/workflow-builder/__tests__/workflow-builder-session.repository.test.ts @@ -69,6 +69,27 @@ describe('WorkflowBuilderSessionRepository', () => { it('should throw error for empty thread ID', async () => { await expect(repository.getSession('')).rejects.toThrow('Invalid thread ID format: '); }); + + it('should strip the code-builder "-code" suffix from the userId', async () => { + entityManager.findOne.mockResolvedValueOnce(null); + + const userUuid = 'af6cc09d-e809-41c6-9e92-ca26dfd11487'; + await repository.getSession(`workflow-wf123-user-${userUuid}-code`); + + expect(entityManager.findOne).toHaveBeenCalledWith(WorkflowBuilderSession, { + where: { workflowId: 'wf123', userId: userUuid }, + }); + }); + + it('should not strip "-code" from a non-suffixed userId', async () => { + entityManager.findOne.mockResolvedValueOnce(null); + + await repository.getSession('workflow-wf123-user-user456'); + + expect(entityManager.findOne).toHaveBeenCalledWith(WorkflowBuilderSession, { + where: { workflowId: 'wf123', userId: 'user456' }, + }); + }); }); describe('getSession', () => { diff --git a/packages/cli/src/modules/workflow-builder/workflow-builder-session.repository.ts b/packages/cli/src/modules/workflow-builder/workflow-builder-session.repository.ts index 8439efc51ef..5f425b0967b 100644 --- a/packages/cli/src/modules/workflow-builder/workflow-builder-session.repository.ts +++ b/packages/cli/src/modules/workflow-builder/workflow-builder-session.repository.ts @@ -76,8 +76,11 @@ export class WorkflowBuilderSessionRepository } private parseThreadId(threadId: string): { workflowId: string; userId: string } { - // Format: "workflow-{workflowId}-user-{userId}" - const match = threadId.match(/^workflow-(.+)-user-(.+)$/); + // Format: "workflow-{workflowId}-user-{userId}" with an optional "-code" suffix + // for the code-builder agent thread variant. Strip the suffix before parsing so + // the greedy userId capture doesn't swallow it (userId is a uuid column in PG). + const normalized = threadId.endsWith('-code') ? threadId.slice(0, -'-code'.length) : threadId; + const match = normalized.match(/^workflow-(.+)-user-(.+)$/); if (!match) { throw new Error(`Invalid thread ID format: ${threadId}`); } diff --git a/packages/cli/src/services/__tests__/ai-workflow-builder.service.test.ts b/packages/cli/src/services/__tests__/ai-workflow-builder.service.test.ts index e96ec40830a..19e63a7804d 100644 --- a/packages/cli/src/services/__tests__/ai-workflow-builder.service.test.ts +++ b/packages/cli/src/services/__tests__/ai-workflow-builder.service.test.ts @@ -273,7 +273,7 @@ describe('WorkflowBuilderService', () => { const result = await service.getSessions('workflow-123', mockUser); expect(MockedAiWorkflowBuilderService).toHaveBeenCalledTimes(1); - expect(mockAiService.getSessions).toHaveBeenCalledWith('workflow-123', mockUser); + expect(mockAiService.getSessions).toHaveBeenCalledWith('workflow-123', mockUser, undefined); expect(result).toEqual(mockSessions); }); @@ -286,9 +286,21 @@ describe('WorkflowBuilderService', () => { const result = await service.getSessions(undefined, mockUser); - expect(mockAiService.getSessions).toHaveBeenCalledWith(undefined, mockUser); + expect(mockAiService.getSessions).toHaveBeenCalledWith(undefined, mockUser, undefined); expect(result).toEqual(mockSessions); }); + + it('should forward the isCodeBuilder flag to the inner service', async () => { + const mockSessions = { sessions: [] }; + + const mockAiService = mock(); + (mockAiService.getSessions as jest.Mock).mockResolvedValue(mockSessions); + MockedAiWorkflowBuilderService.mockImplementation(() => mockAiService); + + await service.getSessions('workflow-123', mockUser, true); + + expect(mockAiService.getSessions).toHaveBeenCalledWith('workflow-123', mockUser, true); + }); }); describe('onCreditsUpdated callback', () => { diff --git a/packages/cli/src/services/ai-workflow-builder.service.ts b/packages/cli/src/services/ai-workflow-builder.service.ts index 13fe28e637a..4ee7f3c8684 100644 --- a/packages/cli/src/services/ai-workflow-builder.service.ts +++ b/packages/cli/src/services/ai-workflow-builder.service.ts @@ -187,9 +187,9 @@ export class WorkflowBuilderService { yield* service.chat(payload, user, abortSignal); } - async getSessions(workflowId: string | undefined, user: IUser) { + async getSessions(workflowId: string | undefined, user: IUser, isCodeBuilder?: boolean) { const service = await this.getService(); - const sessions = await service.getSessions(workflowId, user); + const sessions = await service.getSessions(workflowId, user, isCodeBuilder); return sessions; }