Skip to content

Commit c9f0dca

Browse files
committed
Fix start.files
1 parent 4880614 commit c9f0dca

12 files changed

Lines changed: 241 additions & 38 deletions

File tree

apps/sim/app/api/workflows/[id]/execute/route.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ async function handleExecutePost(
359359
includeFileBase64,
360360
base64MaxBytes,
361361
workflowStateOverride,
362+
executionId: requestedExecutionId,
362363
triggerBlockId: parsedTriggerBlockId,
363364
startBlockId,
364365
stopAfterBlockId,
@@ -508,7 +509,8 @@ async function handleExecutePost(
508509
)
509510
}
510511

511-
const executionId = generateId()
512+
const executionId =
513+
isClientSession && requestedExecutionId ? requestedExecutionId : generateId()
512514
reqLogger = reqLogger.withMetadata({ userId, executionId })
513515

514516
reqLogger.info('Starting server-side execution', {

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,7 @@ export function useWorkflowExecution() {
524524
size: fileData.file.size,
525525
type: fileData.file.type,
526526
key: result.key,
527+
context: 'execution',
527528
})
528529
} catch (uploadError) {
529530
if (
@@ -565,6 +566,7 @@ export function useWorkflowExecution() {
565566
size: r.size,
566567
type: r.type,
567568
key: r.key,
569+
context: r.context || 'execution',
568570
uploadedAt: r.uploadedAt,
569571
expiresAt: r.expiresAt,
570572
})
@@ -1126,6 +1128,7 @@ export function useWorkflowExecution() {
11261128
await executionStream.execute({
11271129
workflowId: activeWorkflowId,
11281130
input: finalWorkflowInput,
1131+
executionId,
11291132
startBlockId,
11301133
selectedOutputs,
11311134
triggerType: overrideTriggerType || 'manual',

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,7 @@ export async function executeWorkflowWithFullLogging(
903903
triggerType: options.overrideTriggerType || 'manual',
904904
useDraftState: options.useDraftState ?? true,
905905
isClientSession: true,
906+
...(options.executionId ? { executionId: options.executionId } : {}),
906907
...(options.triggerBlockId ? { triggerBlockId: options.triggerBlockId } : {}),
907908
...(options.stopAfterBlockId ? { stopAfterBlockId: options.stopAfterBlockId } : {}),
908909
...(options.runFromBlock

apps/sim/executor/handlers/agent/agent-handler.ts

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import { buildAPIUrl, buildAuthHeaders } from '@/executor/utils/http'
3939
import { stringifyJSON } from '@/executor/utils/json'
4040
import { resolveVertexCredential } from '@/executor/utils/vertex-credential'
4141
import { executeProviderRequest } from '@/providers'
42-
import { getAttachmentProvider, getProviderAttachmentMaxBytes } from '@/providers/attachments'
42+
import { getProviderAttachmentMaxBytes, supportsFileAttachments } from '@/providers/attachments'
4343
import { getProviderFromModel, transformBlockTool } from '@/providers/utils'
4444
import type { SerializedBlock } from '@/serializer/types'
4545
import { filterSchemaForLLM, type ToolSchema } from '@/tools/params'
@@ -91,10 +91,14 @@ export class AgentBlockHandler implements BlockHandler {
9191

9292
const streamingConfig = this.getStreamingConfig(ctx, block)
9393
const messages = await this.buildMessages(ctx, filteredInputs, skillMetadata)
94-
const messagesWithFiles = await this.attachFilesToLastUserMessage(
94+
const messagesWithInputFiles = this.attachFilesToLastUserMessage(
9595
ctx,
9696
messages,
97-
filteredInputs.files,
97+
filteredInputs.files
98+
)
99+
const messagesWithFiles = await this.hydrateMessageFilesForProvider(
100+
ctx,
101+
messagesWithInputFiles,
98102
providerId
99103
)
100104

@@ -682,12 +686,11 @@ export class AgentBlockHandler implements BlockHandler {
682686
return messages.length > 0 ? messages : undefined
683687
}
684688

685-
private async attachFilesToLastUserMessage(
689+
private attachFilesToLastUserMessage(
686690
ctx: ExecutionContext,
687691
messages: Message[] | undefined,
688-
filesInput: unknown,
689-
providerId: string
690-
): Promise<Message[] | undefined> {
692+
filesInput: unknown
693+
): Message[] | undefined {
691694
const normalizedFiles = normalizeFileInput(filesInput)
692695
if (!normalizedFiles || normalizedFiles.length === 0) {
693696
return messages
@@ -697,10 +700,6 @@ export class AgentBlockHandler implements BlockHandler {
697700
throw new Error('Files require at least one user message in the agent prompt')
698701
}
699702

700-
if (!getAttachmentProvider(providerId)) {
701-
throw new Error(`File attachments are not supported for provider "${providerId}"`)
702-
}
703-
704703
let lastUserMessageIndex = -1
705704
for (let index = messages.length - 1; index >= 0; index--) {
706705
if (messages[index].role === 'user') {
@@ -718,21 +717,71 @@ export class AgentBlockHandler implements BlockHandler {
718717
throw new Error('Files must include at least one valid file object')
719718
}
720719

721-
const hydratedFiles = await hydrateUserFilesWithBase64(userFiles, {
722-
requestId,
723-
workspaceId: ctx.workspaceId,
724-
workflowId: ctx.workflowId,
725-
executionId: ctx.executionId,
726-
userId: ctx.userId,
727-
logger,
728-
maxBytes: getProviderAttachmentMaxBytes(providerId),
729-
})
730-
731720
const lastUserMessage = messages[lastUserMessageIndex]
732721
const nextMessages = [...messages]
733722
nextMessages[lastUserMessageIndex] = {
734723
...lastUserMessage,
735-
files: [...(lastUserMessage.files ?? []), ...hydratedFiles],
724+
files: [...(lastUserMessage.files ?? []), ...userFiles],
725+
}
726+
727+
return nextMessages
728+
}
729+
730+
private async hydrateMessageFilesForProvider(
731+
ctx: ExecutionContext,
732+
messages: Message[] | undefined,
733+
providerId: string
734+
): Promise<Message[] | undefined> {
735+
if (!messages?.some((message) => message.files?.length)) {
736+
return messages
737+
}
738+
739+
if (!supportsFileAttachments(providerId)) {
740+
throw new Error(`File attachments are not supported for provider "${providerId}"`)
741+
}
742+
743+
const requestId = ctx.executionId || ctx.workflowId || 'agent-files'
744+
const nextMessages = [...messages]
745+
746+
for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {
747+
const message = messages[messageIndex]
748+
const normalizedFiles = normalizeFileInput(message.files)
749+
if (!normalizedFiles || normalizedFiles.length === 0) {
750+
continue
751+
}
752+
753+
const userFiles = processFilesToUserFiles(
754+
normalizedFiles as RawFileInput[],
755+
requestId,
756+
logger
757+
)
758+
if (userFiles.length === 0) {
759+
throw new Error('Files must include at least one valid file object')
760+
}
761+
762+
const hydratedFiles = await hydrateUserFilesWithBase64(userFiles, {
763+
requestId,
764+
workspaceId: ctx.workspaceId,
765+
workflowId: ctx.workflowId,
766+
executionId: ctx.executionId,
767+
largeValueExecutionIds: ctx.largeValueExecutionIds,
768+
allowLargeValueWorkflowScope: ctx.allowLargeValueWorkflowScope,
769+
userId: ctx.userId,
770+
logger,
771+
maxBytes: getProviderAttachmentMaxBytes(providerId),
772+
})
773+
774+
const missingFile = hydratedFiles.find((file) => !file.base64)
775+
if (missingFile) {
776+
throw new Error(
777+
`File "${missingFile.name}" could not be read for provider "${providerId}". Make sure the file is still accessible and under the provider attachment size limit.`
778+
)
779+
}
780+
781+
nextMessages[messageIndex] = {
782+
...message,
783+
files: hydratedFiles,
784+
}
736785
}
737786

738787
return nextMessages

apps/sim/executor/handlers/agent/memory.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,34 @@ describe('Memory', () => {
178178
})
179179
})
180180

181+
describe('sanitizeMessageForStorage', () => {
182+
it('should strip file payloads and provider-only fields before memory persistence', () => {
183+
const message: Message = {
184+
role: 'user',
185+
content: 'Analyze this file',
186+
executionId: 'exec-1',
187+
files: [
188+
{
189+
id: 'file-1',
190+
key: 'workspace/ws-1/example.png',
191+
name: 'example.png',
192+
url: '/api/files/serve/workspace%2Fws-1%2Fexample.png?context=workspace',
193+
size: 128,
194+
type: 'image/png',
195+
base64: 'iVBORw0KGgo=',
196+
},
197+
],
198+
tool_calls: [{ id: 'call-1' }],
199+
}
200+
201+
expect((memoryService as any).sanitizeMessageForStorage(message)).toEqual({
202+
role: 'user',
203+
content: 'Analyze this file',
204+
executionId: 'exec-1',
205+
})
206+
})
207+
})
208+
181209
describe('Token-based vs Message-based comparison', () => {
182210
it('should produce different results for same limit concept', () => {
183211
const messages: Message[] = [

apps/sim/executor/handlers/agent/memory.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,14 @@ export class Memory {
122122
return messages.slice(-limit)
123123
}
124124

125+
private sanitizeMessageForStorage(message: Message): Message {
126+
return {
127+
role: message.role,
128+
content: message.content,
129+
...(message.executionId && { executionId: message.executionId }),
130+
}
131+
}
132+
125133
private applyTokenWindow(messages: Message[], maxTokens: number, model?: string): Message[] {
126134
const result: Message[] = []
127135
let tokenCount = 0
@@ -177,9 +185,17 @@ export class Memory {
177185
const data = result[0].data
178186
if (!Array.isArray(data)) return []
179187

180-
return data.filter(
181-
(msg): msg is Message => msg && typeof msg === 'object' && 'role' in msg && 'content' in msg
182-
)
188+
return data
189+
.filter(
190+
(msg): msg is Message =>
191+
msg &&
192+
typeof msg === 'object' &&
193+
'role' in msg &&
194+
'content' in msg &&
195+
['system', 'user', 'assistant'].includes(msg.role) &&
196+
typeof msg.content === 'string'
197+
)
198+
.map((msg) => this.sanitizeMessageForStorage(msg))
183199
}
184200

185201
private async seedMemoryRecord(
@@ -189,13 +205,15 @@ export class Memory {
189205
): Promise<void> {
190206
const now = new Date()
191207

208+
const sanitizedMessages = messages.map((message) => this.sanitizeMessageForStorage(message))
209+
192210
await db
193211
.insert(memory)
194212
.values({
195213
id: generateId(),
196214
workspaceId,
197215
key,
198-
data: messages,
216+
data: sanitizedMessages,
199217
createdAt: now,
200218
updatedAt: now,
201219
})
@@ -205,20 +223,22 @@ export class Memory {
205223
private async appendMessage(workspaceId: string, key: string, message: Message): Promise<void> {
206224
const now = new Date()
207225

226+
const sanitizedMessage = this.sanitizeMessageForStorage(message)
227+
208228
await db
209229
.insert(memory)
210230
.values({
211231
id: generateId(),
212232
workspaceId,
213233
key,
214-
data: [message],
234+
data: [sanitizedMessage],
215235
createdAt: now,
216236
updatedAt: now,
217237
})
218238
.onConflictDoUpdate({
219239
target: [memory.workspaceId, memory.key],
220240
set: {
221-
data: sql`${memory.data} || ${JSON.stringify([message])}::jsonb`,
241+
data: sql`${memory.data} || ${JSON.stringify([sanitizedMessage])}::jsonb`,
222242
updatedAt: now,
223243
},
224244
})

apps/sim/executor/utils/start-block.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,42 @@ describe('start-block utilities', () => {
119119
expect(output.files).toEqual(files)
120120
})
121121

122+
it.concurrent('buildStartBlockOutput normalizes Start files from internal serve URLs', () => {
123+
const block = createBlock('start_trigger', 'start')
124+
const resolution = {
125+
blockId: 'start',
126+
block,
127+
path: StartBlockPath.UNIFIED,
128+
} as const
129+
130+
const output = buildStartBlockOutput({
131+
resolution,
132+
workflowInput: {
133+
files: [
134+
{
135+
id: 'file_1',
136+
name: 'screenshot.png',
137+
url: '/api/files/serve/s3/execution%2Fworkspace-id%2Fworkflow-id%2Fexecution-id%2Fscreenshot.png?context=execution',
138+
size: 243289,
139+
type: 'image/png',
140+
},
141+
],
142+
},
143+
})
144+
145+
expect(output.files).toEqual([
146+
{
147+
id: 'file_1',
148+
name: 'screenshot.png',
149+
url: '/api/files/serve/s3/execution%2Fworkspace-id%2Fworkflow-id%2Fexecution-id%2Fscreenshot.png?context=execution',
150+
size: 243289,
151+
type: 'image/png',
152+
key: 'execution/workspace-id/workflow-id/execution-id/screenshot.png',
153+
context: 'execution',
154+
},
155+
])
156+
})
157+
122158
it.concurrent('rejects inputFormat fields that collide with executor routing keys', () => {
123159
const block = createBlock('start_trigger', 'start', {
124160
subBlocks: {

0 commit comments

Comments
 (0)