Skip to content

Commit 76be10c

Browse files
committed
fix(executor): dedupe replay attachments on user turns only and type the run-read fixture
1 parent aa652bd commit 76be10c

5 files changed

Lines changed: 34 additions & 8 deletions

File tree

‎.claude/rules/sim-content-delivery.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ the workflow executor see byte-identical results.
3030

3131
A run Chat starts executes as Chat's principal, so `executeWorkflowCore` runs every execution under
3232
`runWorkflowOutsideContentDelivery`, where that principal reads like any other. What the run returns
33-
is gated where Chat started it: run execute/resume withhold unadmitted inline file bytes
34-
(`withholdUnadmittedInlineFiles`); Chat's run tools return no file bytes.
33+
is gated where Chat started it: run execute/resume keep inline file bytes only for admitted files
34+
(`withholdUnadmittedInlineFiles`), and Chat's in-app run tools strip `base64` from their result.
3535

3636
## The pieces
3737

‎.cursor/rules/sim-content-delivery.mdc‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ the workflow executor see byte-identical results.
2121

2222
A run Chat starts executes as Chat's principal, so `executeWorkflowCore` runs every execution under
2323
`runWorkflowOutsideContentDelivery`, where that principal reads like any other. What the run returns
24-
is gated where Chat started it: run execute/resume withhold unadmitted inline file bytes
25-
(`withholdUnadmittedInlineFiles`); Chat's run tools return no file bytes.
24+
is gated where Chat started it: run execute/resume keep inline file bytes only for admitted files
25+
(`withholdUnadmittedInlineFiles`), and Chat's in-app run tools strip `base64` from their result.
2626

2727
## The pieces
2828

‎apps/sim/executor/handlers/agent/handler.test.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,29 @@ describe('AgentBlockHandler', () => {
780780
expect(userTurns.map((message) => message.files?.length)).toEqual([1, 1, undefined])
781781
hydrate.mockRestore()
782782
})
783+
784+
it('keeps a user turn’s file when only an earlier assistant turn carried it', async () => {
785+
mockGetProviderFromModel.mockReturnValue('openai')
786+
const hydrate = hydrateWithBytes()
787+
queueTableRows(schemaMock.memory, [
788+
{
789+
secretProvenanceVersion: null,
790+
data: [
791+
{ role: 'assistant', content: 'Here is a file', files: [file] },
792+
{ role: 'user', content: 'Read it', files: [file] },
793+
],
794+
},
795+
])
796+
await handler.execute({ ...mockContext, executionId: 'exec-3' }, mockBlock, {
797+
model: 'gpt-4o',
798+
memoryType: 'conversation',
799+
conversationId: 'conversation-1',
800+
messages: [{ role: 'user', content: 'And now?' }],
801+
})
802+
const sent = mockExecuteProviderRequest.mock.calls.at(-1)?.[1].messages as Message[]
803+
expect(sent.find((message) => message.content === 'Read it')?.files).toHaveLength(1)
804+
hydrate.mockRestore()
805+
})
783806
})
784807

785808
describe('model fallback', () => {

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,16 @@ function isOptionalMemoryStorageFailure(error: unknown): boolean {
119119
}
120120

121121
/**
122-
* The attachments each message sends to the model. A storage key pins its bytes, so a file attached
123-
* on several turns is sent once, where it first appears, keeping the replayed prefix stable across
124-
* runs; a message with no text keeps its own so it is never sent empty.
122+
* The attachments each message sends to the model. A storage key pins its bytes, so a file a user
123+
* attached on several turns is sent once, where it first appears, keeping the replayed prefix
124+
* stable across runs; a user turn with no text keeps its own so it is never sent empty. Only user
125+
* turns are deduplicated: every provider sends their attachments, while some drop other roles'.
125126
*/
126127
export function selectReplayAttachments(messages: readonly Message[]): UserFile[][] {
127128
const sent = new Set<string>()
128129
return messages.map((message) => {
129130
const files = message.files ?? []
131+
if (message.role !== 'user') return files
130132
const unsent = files.filter((file) => {
131133
const identity = file.key || file.id
132134
if (sent.has(identity)) return false

‎apps/sim/lib/workspace-files/delivery.integration.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { hydrateUserFilesWithBase64 } from '@/lib/uploads/utils/user-file-base64
4141
import { downloadWorkspaceFileStream } from '@/lib/workspace-files/application/download-workspace-file'
4242
import { readWorkspaceInlineFile } from '@/lib/workspace-files/application/read-workspace-inline-file'
4343
import { createFileReadReceipt, recheckFileReadReceipt } from '@/lib/workspace-files/read-receipt'
44+
import type { UserFile } from '@/executor/types'
4445

4546
const uploadRoot = mkdtempSync(join(tmpdir(), 'sim-delivery-test-'))
4647
setUploadDirServer(uploadRoot)
@@ -315,7 +316,7 @@ describe('A workflow run Chat started reads workspace files as any run does', ()
315316
if (provenance === 'unknown') {
316317
await markWorkspaceFileSecretProvenanceUnknown(f.workspaceId, [uploaded.id])
317318
}
318-
const file = {
319+
const file: UserFile = {
319320
id: uploaded.id,
320321
name: uploaded.name,
321322
url: uploaded.url,

0 commit comments

Comments
 (0)