From 17a9a7bdc1b468c11ffcad38c89d3b6d3e16e04c Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 12 May 2026 17:20:28 -0700 Subject: [PATCH 1/2] fix(console): match child-workflow inner blocks by instanceId when reconciling dropped SSE events --- ...rkflow-execution-utils.integration.test.ts | 368 ++++++++++++++++ .../utils/workflow-execution-utils.test.ts | 397 +++++++++++++++++- .../utils/workflow-execution-utils.ts | 36 +- 3 files changed, 784 insertions(+), 17 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.integration.test.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.integration.test.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.integration.test.ts new file mode 100644 index 0000000000..f3b3d7cde6 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.integration.test.ts @@ -0,0 +1,368 @@ +/** + * @vitest-environment node + * + * Integration tests that exercise `reconcileFinalBlockLogs` against the real + * `useTerminalConsoleStore` to validate end-to-end matching behavior. The + * sibling unit-test file mocks the store and only verifies call args, which + * cannot catch identity-mismatch regressions of the kind that produced the + * 34.57s wall-clock symptom. + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.unmock('@/stores/terminal') +vi.unmock('@/stores/terminal/console/store') + +import { reconcileFinalBlockLogs } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils' +import { useExecutionStore } from '@/stores/execution' +import { useTerminalConsoleStore } from '@/stores/terminal/console/store' + +describe('reconcileFinalBlockLogs (real store)', () => { + beforeEach(() => { + useTerminalConsoleStore.setState({ + workflowEntries: {}, + entryIdsByBlockExecution: {}, + entryLocationById: {}, + isOpen: false, + _hasHydrated: true, + }) + vi.mocked(useExecutionStore.getState).mockReturnValue({ + getCurrentExecutionId: vi.fn(() => 'exec-1'), + } as any) + }) + + it('actually flips a child-workflow inner block from running to success', () => { + const store = useTerminalConsoleStore.getState() + store.addConsole({ + workflowId: 'wf-1', + blockId: 'workflow-1', + blockName: 'Workflow 1', + blockType: 'workflow', + executionId: 'exec-1', + executionOrder: 1, + isRunning: false, + success: true, + childWorkflowInstanceId: 'child-inst-1', + }) + store.addConsole({ + workflowId: 'wf-1', + blockId: 'set-projects', + blockName: 'setProjects', + blockType: 'variables', + executionId: 'exec-1', + executionOrder: 5, + isRunning: true, + childWorkflowBlockId: 'child-inst-1', + childWorkflowName: 'Workflow 1', + }) + + const startedAt = new Date().toISOString() + const endedAt = new Date(Date.now() + 27).toISOString() + + reconcileFinalBlockLogs(store.updateConsole, 'wf-1', 'exec-1', [ + { + blockId: 'workflow-1', + blockName: 'Workflow 1', + blockType: 'workflow', + startedAt, + endedAt, + durationMs: 100, + success: true, + executionOrder: 1, + childTraceSpans: [ + { + id: 'set-projects-span', + name: 'setProjects', + type: 'variables', + blockId: 'set-projects', + executionOrder: 5, + status: 'success', + duration: 27, + startTime: startedAt, + endTime: endedAt, + output: { value: [{ id: 'p1' }] }, + }, + ], + } as any, + ]) + + const innerEntry = useTerminalConsoleStore + .getState() + .getWorkflowEntries('wf-1') + .find((e) => e.blockId === 'set-projects') + + expect(innerEntry).toBeDefined() + expect(innerEntry?.isRunning).toBe(false) + expect(innerEntry?.success).toBe(true) + expect(innerEntry?.durationMs).toBe(27) + expect(innerEntry?.output).toEqual({ value: [{ id: 'p1' }] }) + }) + + it('targets the correct invocation when the same child nodeId runs twice', () => { + const store = useTerminalConsoleStore.getState() + store.addConsole({ + workflowId: 'wf-1', + blockId: 'workflow-1', + blockName: 'Workflow 1', + blockType: 'workflow', + executionId: 'exec-1', + executionOrder: 1, + isRunning: false, + success: true, + childWorkflowInstanceId: 'inst-A', + }) + store.addConsole({ + workflowId: 'wf-1', + blockId: 'workflow-1', + blockName: 'Workflow 1', + blockType: 'workflow', + executionId: 'exec-1', + executionOrder: 2, + isRunning: false, + success: true, + childWorkflowInstanceId: 'inst-B', + }) + store.addConsole({ + workflowId: 'wf-1', + blockId: 'fn-inner', + blockName: 'Inner', + blockType: 'function', + executionId: 'exec-1', + executionOrder: 3, + isRunning: true, + childWorkflowBlockId: 'inst-A', + }) + store.addConsole({ + workflowId: 'wf-1', + blockId: 'fn-inner', + blockName: 'Inner', + blockType: 'function', + executionId: 'exec-1', + executionOrder: 4, + isRunning: true, + childWorkflowBlockId: 'inst-B', + }) + + const startedAt = new Date().toISOString() + const endedAt = new Date(Date.now() + 5).toISOString() + const baseLog = { + blockName: 'Workflow 1', + blockType: 'workflow', + startedAt, + endedAt, + durationMs: 50, + success: true, + } + + reconcileFinalBlockLogs(store.updateConsole, 'wf-1', 'exec-1', [ + { + ...baseLog, + blockId: 'workflow-1', + executionOrder: 1, + childTraceSpans: [ + { + id: 'a', + name: 'Inner', + type: 'function', + blockId: 'fn-inner', + executionOrder: 3, + status: 'success', + duration: 5, + startTime: startedAt, + endTime: endedAt, + output: { result: 'A' }, + }, + ], + } as any, + { + ...baseLog, + blockId: 'workflow-1', + executionOrder: 2, + childTraceSpans: [ + { + id: 'b', + name: 'Inner', + type: 'function', + blockId: 'fn-inner', + executionOrder: 4, + status: 'success', + duration: 5, + startTime: startedAt, + endTime: endedAt, + output: { result: 'B' }, + }, + ], + } as any, + ]) + + const entries = useTerminalConsoleStore.getState().getWorkflowEntries('wf-1') + const a = entries.find((e) => e.blockId === 'fn-inner' && e.childWorkflowBlockId === 'inst-A') + const b = entries.find((e) => e.blockId === 'fn-inner' && e.childWorkflowBlockId === 'inst-B') + + expect(a?.isRunning).toBe(false) + expect(a?.output).toEqual({ result: 'A' }) + expect(b?.isRunning).toBe(false) + expect(b?.output).toEqual({ result: 'B' }) + }) + + it('propagates error state for spans with error status', () => { + const store = useTerminalConsoleStore.getState() + store.addConsole({ + workflowId: 'wf-1', + blockId: 'workflow-1', + blockName: 'Workflow 1', + blockType: 'workflow', + executionId: 'exec-1', + executionOrder: 1, + isRunning: false, + success: true, + childWorkflowInstanceId: 'inst-1', + }) + store.addConsole({ + workflowId: 'wf-1', + blockId: 'http-1', + blockName: 'API', + blockType: 'api', + executionId: 'exec-1', + executionOrder: 2, + isRunning: true, + childWorkflowBlockId: 'inst-1', + }) + + const startedAt = new Date().toISOString() + const endedAt = new Date(Date.now() + 30).toISOString() + + reconcileFinalBlockLogs(store.updateConsole, 'wf-1', 'exec-1', [ + { + blockId: 'workflow-1', + blockName: 'Workflow 1', + blockType: 'workflow', + startedAt, + endedAt, + durationMs: 100, + success: true, + executionOrder: 1, + childTraceSpans: [ + { + id: 'http-span', + name: 'API', + type: 'api', + blockId: 'http-1', + executionOrder: 2, + status: 'error', + duration: 30, + startTime: startedAt, + endTime: endedAt, + output: { error: 'Connection refused' }, + }, + ], + } as any, + ]) + + const entry = useTerminalConsoleStore + .getState() + .getWorkflowEntries('wf-1') + .find((e) => e.blockId === 'http-1') + + expect(entry?.isRunning).toBe(false) + expect(entry?.success).toBe(false) + expect(entry?.error).toBe('Connection refused') + }) + + it('matches the correct iteration row inside a child workflow loop', () => { + const store = useTerminalConsoleStore.getState() + store.addConsole({ + workflowId: 'wf-1', + blockId: 'workflow-1', + blockName: 'Workflow 1', + blockType: 'workflow', + executionId: 'exec-1', + executionOrder: 1, + isRunning: false, + success: true, + childWorkflowInstanceId: 'inst-1', + }) + store.addConsole({ + workflowId: 'wf-1', + blockId: 'fn-leaf', + blockName: 'Leaf', + blockType: 'function', + executionId: 'exec-1', + executionOrder: 2, + isRunning: false, + success: true, + iterationCurrent: 0, + iterationType: 'loop', + iterationContainerId: 'loop-1', + childWorkflowBlockId: 'inst-1', + output: { i: 0 }, + }) + store.addConsole({ + workflowId: 'wf-1', + blockId: 'fn-leaf', + blockName: 'Leaf', + blockType: 'function', + executionId: 'exec-1', + executionOrder: 3, + isRunning: true, + iterationCurrent: 1, + iterationType: 'loop', + iterationContainerId: 'loop-1', + childWorkflowBlockId: 'inst-1', + }) + + const startedAt = new Date().toISOString() + const endedAt = new Date(Date.now() + 12).toISOString() + + reconcileFinalBlockLogs(store.updateConsole, 'wf-1', 'exec-1', [ + { + blockId: 'workflow-1', + blockName: 'Workflow 1', + blockType: 'workflow', + startedAt, + endedAt, + durationMs: 100, + success: true, + executionOrder: 1, + childTraceSpans: [ + { + id: 'leaf-0', + name: 'Leaf', + type: 'function', + blockId: 'fn-leaf', + executionOrder: 2, + loopId: 'loop-1', + iterationIndex: 0, + status: 'success', + duration: 5, + startTime: startedAt, + endTime: endedAt, + output: { i: 0 }, + }, + { + id: 'leaf-1', + name: 'Leaf', + type: 'function', + blockId: 'fn-leaf', + executionOrder: 3, + loopId: 'loop-1', + iterationIndex: 1, + status: 'success', + duration: 12, + startTime: startedAt, + endTime: endedAt, + output: { i: 1 }, + }, + ], + } as any, + ]) + + const entries = useTerminalConsoleStore.getState().getWorkflowEntries('wf-1') + const iter0 = entries.find((e) => e.blockId === 'fn-leaf' && e.iterationCurrent === 0) + const iter1 = entries.find((e) => e.blockId === 'fn-leaf' && e.iterationCurrent === 1) + + expect(iter0?.isRunning).toBe(false) + expect(iter0?.output).toEqual({ i: 0 }) + expect(iter1?.isRunning).toBe(false) + expect(iter1?.output).toEqual({ i: 1 }) + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.test.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.test.ts index d2c999beef..e7272f901c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.test.ts @@ -427,7 +427,7 @@ describe('workflow-execution-utils', () => { executionId: 'exec-1', executionOrder: 3, isRunning: true, - childWorkflowBlockId: 'workflow-1', + childWorkflowBlockId: 'child-inst-1', childWorkflowName: 'Workflow 1', }) terminalConsoleMockFns.mockAddConsole({ @@ -489,7 +489,7 @@ describe('workflow-execution-utils', () => { success: true, isRunning: false, isCanceled: false, - childWorkflowBlockId: 'workflow-1', + childWorkflowBlockId: 'child-inst-1', }), 'exec-1', ]) @@ -501,7 +501,7 @@ describe('workflow-execution-utils', () => { error: 'Request failed', isRunning: false, isCanceled: false, - childWorkflowBlockId: 'workflow-1', + childWorkflowBlockId: 'child-inst-1', }), 'exec-1', ]) @@ -529,7 +529,7 @@ describe('workflow-execution-utils', () => { iterationCurrent: 0, iterationType: 'loop', iterationContainerId: 'loop-1', - childWorkflowBlockId: 'workflow-1', + childWorkflowBlockId: 'child-inst-1', }) terminalConsoleMockFns.mockAddConsole({ workflowId: 'wf-1', @@ -542,7 +542,7 @@ describe('workflow-execution-utils', () => { iterationCurrent: 1, iterationType: 'loop', iterationContainerId: 'loop-1', - childWorkflowBlockId: 'workflow-1', + childWorkflowBlockId: 'child-inst-1', }) const startedAt = new Date().toISOString() @@ -632,7 +632,7 @@ describe('workflow-execution-utils', () => { executionId: 'exec-1', executionOrder: 3, isRunning: false, - childWorkflowBlockId: 'workflow-1', + childWorkflowBlockId: 'child-inst-1', childWorkflowInstanceId: 'nested-inst-1', }) terminalConsoleMockFns.mockAddConsole({ @@ -643,7 +643,7 @@ describe('workflow-execution-utils', () => { executionId: 'exec-1', executionOrder: 1, isRunning: true, - childWorkflowBlockId: 'nested-workflow', + childWorkflowBlockId: 'nested-inst-1', }) const startedAt = new Date().toISOString() @@ -688,7 +688,7 @@ describe('workflow-execution-utils', () => { expect(updateConsole.mock.calls[1]).toEqual([ 'nested-api', expect.objectContaining({ - childWorkflowBlockId: 'nested-workflow', + childWorkflowBlockId: 'nested-inst-1', success: true, isRunning: false, isCanceled: false, @@ -697,6 +697,387 @@ describe('workflow-execution-utils', () => { ]) }) + it('rescues a child-workflow block whose block:completed SSE event was dropped', () => { + terminalConsoleMockFns.mockAddConsole({ + workflowId: 'wf-1', + blockId: 'workflow-1', + blockName: 'Workflow 1', + blockType: 'workflow', + executionId: 'exec-1', + executionOrder: 1, + success: true, + isRunning: false, + childWorkflowInstanceId: 'child-inst-1', + }) + terminalConsoleMockFns.mockAddConsole({ + workflowId: 'wf-1', + blockId: 'set-projects', + blockName: 'setProjects', + blockType: 'variables', + executionId: 'exec-1', + executionOrder: 5, + isRunning: true, + childWorkflowBlockId: 'child-inst-1', + childWorkflowName: 'Workflow 1', + }) + + const startedAt = new Date().toISOString() + const endedAt = new Date(Date.now() + 27).toISOString() + const updateConsole = vi.fn() + reconcileFinalBlockLogs(updateConsole, 'wf-1', 'exec-1', [ + makeLog({ + blockId: 'workflow-1', + blockType: 'workflow', + executionOrder: 1, + childTraceSpans: [ + { + id: 'set-projects-span', + name: 'setProjects', + type: 'variables', + blockId: 'set-projects', + executionOrder: 5, + status: 'success', + duration: 27, + startTime: startedAt, + endTime: endedAt, + output: { value: [{ id: 'p1' }, { id: 'p2' }] }, + }, + ], + }), + ]) + + expect(updateConsole).toHaveBeenCalledTimes(1) + expect(updateConsole.mock.calls[0]).toEqual([ + 'set-projects', + expect.objectContaining({ + executionOrder: 5, + childWorkflowBlockId: 'child-inst-1', + replaceOutput: { value: [{ id: 'p1' }, { id: 'p2' }] }, + success: true, + isRunning: false, + isCanceled: false, + durationMs: 27, + startedAt, + endedAt, + }), + 'exec-1', + ]) + }) + + it('matches per-invocation when the same child workflow nodeId runs twice', () => { + terminalConsoleMockFns.mockAddConsole({ + workflowId: 'wf-1', + blockId: 'workflow-1', + blockName: 'Workflow 1', + blockType: 'workflow', + executionId: 'exec-1', + executionOrder: 1, + success: true, + childWorkflowInstanceId: 'inst-A', + }) + terminalConsoleMockFns.mockAddConsole({ + workflowId: 'wf-1', + blockId: 'workflow-1', + blockName: 'Workflow 1', + blockType: 'workflow', + executionId: 'exec-1', + executionOrder: 2, + success: true, + childWorkflowInstanceId: 'inst-B', + }) + terminalConsoleMockFns.mockAddConsole({ + workflowId: 'wf-1', + blockId: 'fn-inner', + blockName: 'Inner', + blockType: 'function', + executionId: 'exec-1', + executionOrder: 3, + isRunning: true, + childWorkflowBlockId: 'inst-A', + }) + terminalConsoleMockFns.mockAddConsole({ + workflowId: 'wf-1', + blockId: 'fn-inner', + blockName: 'Inner', + blockType: 'function', + executionId: 'exec-1', + executionOrder: 4, + isRunning: true, + childWorkflowBlockId: 'inst-B', + }) + + const startedAt = new Date().toISOString() + const endedAt = new Date(Date.now() + 10).toISOString() + const updateConsole = vi.fn() + reconcileFinalBlockLogs(updateConsole, 'wf-1', 'exec-1', [ + makeLog({ + blockId: 'workflow-1', + blockType: 'workflow', + executionOrder: 1, + childTraceSpans: [ + { + id: 'a', + name: 'Inner', + type: 'function', + blockId: 'fn-inner', + executionOrder: 3, + status: 'success', + duration: 5, + startTime: startedAt, + endTime: endedAt, + output: { result: 'A' }, + }, + ], + }), + makeLog({ + blockId: 'workflow-1', + blockType: 'workflow', + executionOrder: 2, + childTraceSpans: [ + { + id: 'b', + name: 'Inner', + type: 'function', + blockId: 'fn-inner', + executionOrder: 4, + status: 'success', + duration: 5, + startTime: startedAt, + endTime: endedAt, + output: { result: 'B' }, + }, + ], + }), + ]) + + expect(updateConsole).toHaveBeenCalledTimes(2) + expect(updateConsole.mock.calls[0][1]).toMatchObject({ + executionOrder: 3, + childWorkflowBlockId: 'inst-A', + replaceOutput: { result: 'A' }, + }) + expect(updateConsole.mock.calls[1][1]).toMatchObject({ + executionOrder: 4, + childWorkflowBlockId: 'inst-B', + replaceOutput: { result: 'B' }, + }) + }) + + it('reconciles parallel-iteration spans inside a child workflow', () => { + terminalConsoleMockFns.mockAddConsole({ + workflowId: 'wf-1', + blockId: 'workflow-1', + blockType: 'workflow', + blockName: 'Workflow 1', + executionId: 'exec-1', + executionOrder: 1, + success: true, + childWorkflowInstanceId: 'inst-1', + }) + terminalConsoleMockFns.mockAddConsole({ + workflowId: 'wf-1', + blockId: 'fn-leaf', + blockType: 'function', + blockName: 'Leaf', + executionId: 'exec-1', + executionOrder: 2, + isRunning: true, + iterationCurrent: 0, + iterationType: 'parallel', + iterationContainerId: 'par-1', + childWorkflowBlockId: 'inst-1', + }) + + const startedAt = new Date().toISOString() + const endedAt = new Date(Date.now() + 8).toISOString() + const updateConsole = vi.fn() + reconcileFinalBlockLogs(updateConsole, 'wf-1', 'exec-1', [ + makeLog({ + blockId: 'workflow-1', + blockType: 'workflow', + executionOrder: 1, + childTraceSpans: [ + { + id: 'leaf-span', + name: 'Leaf', + type: 'function', + blockId: 'fn-leaf', + executionOrder: 2, + parallelId: 'par-1', + iterationIndex: 0, + status: 'success', + duration: 8, + startTime: startedAt, + endTime: endedAt, + output: { ok: true }, + }, + ], + }), + ]) + + expect(updateConsole).toHaveBeenCalledTimes(1) + expect(updateConsole.mock.calls[0][1]).toMatchObject({ + executionOrder: 2, + iterationCurrent: 0, + iterationType: 'parallel', + iterationContainerId: 'par-1', + childWorkflowBlockId: 'inst-1', + success: true, + }) + }) + + it('rescues only the iteration whose terminal SSE event was dropped', () => { + terminalConsoleMockFns.mockAddConsole({ + workflowId: 'wf-1', + blockId: 'workflow-1', + blockType: 'workflow', + blockName: 'Workflow 1', + executionId: 'exec-1', + executionOrder: 1, + success: true, + childWorkflowInstanceId: 'inst-1', + }) + terminalConsoleMockFns.mockAddConsole({ + workflowId: 'wf-1', + blockId: 'fn-leaf', + blockType: 'function', + blockName: 'Leaf', + executionId: 'exec-1', + executionOrder: 2, + isRunning: false, + success: true, + iterationCurrent: 0, + iterationType: 'loop', + iterationContainerId: 'loop-1', + childWorkflowBlockId: 'inst-1', + }) + terminalConsoleMockFns.mockAddConsole({ + workflowId: 'wf-1', + blockId: 'fn-leaf', + blockType: 'function', + blockName: 'Leaf', + executionId: 'exec-1', + executionOrder: 3, + isRunning: true, + iterationCurrent: 1, + iterationType: 'loop', + iterationContainerId: 'loop-1', + childWorkflowBlockId: 'inst-1', + }) + + const startedAt = new Date().toISOString() + const endedAt = new Date(Date.now() + 12).toISOString() + const updateConsole = vi.fn() + reconcileFinalBlockLogs(updateConsole, 'wf-1', 'exec-1', [ + makeLog({ + blockId: 'workflow-1', + blockType: 'workflow', + executionOrder: 1, + childTraceSpans: [ + { + id: 'leaf-0', + name: 'Leaf', + type: 'function', + blockId: 'fn-leaf', + executionOrder: 2, + loopId: 'loop-1', + iterationIndex: 0, + status: 'success', + duration: 5, + startTime: startedAt, + endTime: endedAt, + output: { i: 0 }, + }, + { + id: 'leaf-1', + name: 'Leaf', + type: 'function', + blockId: 'fn-leaf', + executionOrder: 3, + loopId: 'loop-1', + iterationIndex: 1, + status: 'success', + duration: 12, + startTime: startedAt, + endTime: endedAt, + output: { i: 1 }, + }, + ], + }), + ]) + + // updateConsole is called for both spans (idempotent re-application), but + // production matchesEntryForUpdate filters by the identity so only the + // still-running iteration is actually mutated. We assert the args carry + // distinct iteration identities so the store can target the right row. + expect(updateConsole.mock.calls[0][1]).toMatchObject({ + executionOrder: 2, + iterationCurrent: 0, + }) + expect(updateConsole.mock.calls[1][1]).toMatchObject({ + executionOrder: 3, + iterationCurrent: 1, + replaceOutput: { i: 1 }, + }) + }) + + it('propagates span error state when the block:error SSE was lost', () => { + terminalConsoleMockFns.mockAddConsole({ + workflowId: 'wf-1', + blockId: 'workflow-1', + blockType: 'workflow', + blockName: 'Workflow 1', + executionId: 'exec-1', + executionOrder: 1, + success: true, + childWorkflowInstanceId: 'inst-1', + }) + terminalConsoleMockFns.mockAddConsole({ + workflowId: 'wf-1', + blockId: 'http-1', + blockType: 'api', + blockName: 'API', + executionId: 'exec-1', + executionOrder: 2, + isRunning: true, + childWorkflowBlockId: 'inst-1', + }) + + const startedAt = new Date().toISOString() + const endedAt = new Date(Date.now() + 30).toISOString() + const updateConsole = vi.fn() + reconcileFinalBlockLogs(updateConsole, 'wf-1', 'exec-1', [ + makeLog({ + blockId: 'workflow-1', + blockType: 'workflow', + executionOrder: 1, + childTraceSpans: [ + { + id: 'http-span', + name: 'API', + type: 'api', + blockId: 'http-1', + executionOrder: 2, + status: 'error', + duration: 30, + startTime: startedAt, + endTime: endedAt, + output: { error: 'Connection refused' }, + }, + ], + }), + ]) + + expect(updateConsole).toHaveBeenCalledTimes(1) + expect(updateConsole.mock.calls[0][1]).toMatchObject({ + success: false, + error: 'Connection refused', + childWorkflowBlockId: 'inst-1', + isRunning: false, + isCanceled: false, + }) + }) + it('is a no-op when finalBlockLogs is empty or executionId is missing', () => { const updateConsole = vi.fn() reconcileFinalBlockLogs(updateConsole, 'wf-1', 'exec-1', []) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts index 008a1567cd..38e1738c97 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts @@ -512,7 +512,6 @@ export function reconcileFinalBlockLogs( reconcileChildTraceSpans( updateConsole, workflowId, - log.blockId, childWorkflowInstanceId, executionId, log.childTraceSpans @@ -521,24 +520,44 @@ export function reconcileFinalBlockLogs( } } +/** + * Reconciles trace spans for blocks inside a child workflow. + * + * Inner-block console entries are created from SSE `block:started` events whose + * `childWorkflowBlockId` field carries the parent's per-invocation instanceId + * (see `execute/route.ts` where the server emits `childWorkflowContext.parentBlockId`). + * The matcher must therefore key on that instanceId — using the parent workflow + * block's static nodeId would never match and the rescue silently no-ops, leaving + * inner blocks stuck `isRunning: true` until `finishRunningEntries` sweeps them + * with a wall-clock duration. + */ function reconcileChildTraceSpans( updateConsole: UpdateConsoleFn, workflowId: string, - childWorkflowBlockId: string, childWorkflowInstanceId: string, executionId: string, spans: TraceSpan[] ): void { for (const span of spans) { const matchingEntry = span.blockId - ? findConsoleEntryForSpan(workflowId, executionId, childWorkflowBlockId, span) + ? findConsoleEntryForSpan(workflowId, executionId, childWorkflowInstanceId, span) : undefined if (span.blockId) { + if (!matchingEntry) { + logger.warn('reconcileChildTraceSpans found no matching console entry for span', { + workflowId, + executionId, + spanBlockId: span.blockId, + childWorkflowInstanceId, + spanExecutionOrder: span.executionOrder, + spanIterationIndex: span.iterationIndex, + }) + } const errorMessage = normalizeSpanError(span.output?.error) updateConsole( span.blockId, { - ...spanConsoleIdentity(span, childWorkflowBlockId), + ...spanConsoleIdentity(span, childWorkflowInstanceId), replaceOutput: (span.output ?? {}) as Record, success: span.status !== 'error', ...(errorMessage !== undefined ? { error: errorMessage } : {}), @@ -555,7 +574,6 @@ function reconcileChildTraceSpans( reconcileChildTraceSpans( updateConsole, workflowId, - matchingEntry?.blockId ?? childWorkflowBlockId, matchingEntry?.childWorkflowInstanceId ?? childWorkflowInstanceId, executionId, span.children @@ -564,7 +582,7 @@ function reconcileChildTraceSpans( } } -function spanConsoleIdentity(span: TraceSpan, childWorkflowBlockId: string): ConsoleUpdate { +function spanConsoleIdentity(span: TraceSpan, childWorkflowInstanceId: string): ConsoleUpdate { const iterationContainerId = span.loopId ?? span.parallelId const iterationType = span.loopId ? 'loop' : span.parallelId ? 'parallel' : undefined return { @@ -573,18 +591,18 @@ function spanConsoleIdentity(span: TraceSpan, childWorkflowBlockId: string): Con ...(iterationType !== undefined && { iterationType }), ...(iterationContainerId !== undefined && { iterationContainerId }), ...(span.parentIterations !== undefined && { parentIterations: span.parentIterations }), - childWorkflowBlockId, + childWorkflowBlockId: childWorkflowInstanceId, } } function findConsoleEntryForSpan( workflowId: string, executionId: string, - childWorkflowBlockId: string, + childWorkflowInstanceId: string, span: TraceSpan ): ConsoleEntry | undefined { if (!span.blockId) return undefined - const identity = spanConsoleIdentity(span, childWorkflowBlockId) + const identity = spanConsoleIdentity(span, childWorkflowInstanceId) return useTerminalConsoleStore .getState() .getWorkflowEntries(workflowId) From 575493959fd10cefecba01ea01feaf81184ec973 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 12 May 2026 17:26:50 -0700 Subject: [PATCH 2/2] fix(console): drop noisy warn when reconcile finds no matching entry --- .../w/[workflowId]/utils/workflow-execution-utils.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts index 38e1738c97..4b9e726d08 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts @@ -543,16 +543,6 @@ function reconcileChildTraceSpans( ? findConsoleEntryForSpan(workflowId, executionId, childWorkflowInstanceId, span) : undefined if (span.blockId) { - if (!matchingEntry) { - logger.warn('reconcileChildTraceSpans found no matching console entry for span', { - workflowId, - executionId, - spanBlockId: span.blockId, - childWorkflowInstanceId, - spanExecutionOrder: span.executionOrder, - spanIterationIndex: span.iterationIndex, - }) - } const errorMessage = normalizeSpanError(span.output?.error) updateConsole( span.blockId,