Skip to content

Commit 684ce98

Browse files
committed
fix(execution): keep preserved base64 inline and stop a failed buffer write failing a resume
The buffered-event threshold was applied even when a caller explicitly asked to preserve UserFile base64 inline, so anything past that threshold came back as a ref the caller cannot read — defeating the request. Preserve the shared cap for that path; the existing strip-and-recompact fallback still bounds the event. The resume path awaited a non-terminal buffer write bare, so a failed write propagated into the executor callback and failed work that had already run. The buffer only backs reconnect replay, so degrade to live-only delivery the same way the execute route does.
1 parent 3219b47 commit 684ce98

3 files changed

Lines changed: 49 additions & 1 deletion

File tree

apps/sim/lib/execution/event-buffer.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { redisConfigMockFns, resetRedisConfigMock } from '@sim/testing'
55
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
66
import type { ExecutionEventEntry } from '@/lib/execution/event-buffer'
7+
import { LARGE_VALUE_REF_MARKER } from '@/lib/execution/payloads/large-value-ref'
78
import type { ExecutionEvent } from '@/lib/workflows/executor/execution-events'
89

910
const { mockRedis, persistedEntries } = vi.hoisted(() => {
@@ -390,6 +391,38 @@ describe('execution event buffer', () => {
390391
expect(persistedBytes).toBeLessThan(256 * 1024)
391392
})
392393

394+
/**
395+
* `preserveUserFileBase64` is an explicit request for inline delivery. The
396+
* tighter buffered-event threshold must not quietly convert that base64 into
397+
* a ref the caller cannot read back.
398+
*/
399+
it('keeps preserved UserFile base64 inline above the buffered-event threshold', async () => {
400+
mockRedis.incrby.mockResolvedValue(100)
401+
const base64 = Buffer.from('y'.repeat(400 * 1024)).toString('base64')
402+
403+
const writer = createExecutionEventWriter('exec-1', {
404+
workspaceId: 'ws-1',
405+
workflowId: 'wf-1',
406+
preserveUserFileBase64: true,
407+
})
408+
await writer.writeTerminal(
409+
{
410+
type: 'execution:completed',
411+
timestamp: new Date().toISOString(),
412+
executionId: 'exec-1',
413+
workflowId: 'wf-1',
414+
data: {
415+
output: { file: { type: 'file', name: 'a.txt', size: 1, key: 'k', base64 } },
416+
},
417+
} as unknown as ExecutionEvent,
418+
'complete'
419+
)
420+
421+
const persisted = JSON.stringify(persistedEntries[0])
422+
expect(persisted).toContain(base64)
423+
expect(persisted).not.toContain(LARGE_VALUE_REF_MARKER)
424+
})
425+
393426
it('preserves requested UserFile base64 when buffering terminal events', async () => {
394427
mockRedis.incrby.mockResolvedValue(100)
395428
const base64 = Buffer.from('hello').toString('base64')

apps/sim/lib/execution/event-buffer.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,11 @@ async function compactEventForBuffer(
312312
let compactedData = await compactExecutionPayload(event.data, {
313313
...baseOptions,
314314
preserveUserFileBase64: context.preserveUserFileBase64,
315+
// Preserving base64 is an explicit request for inline delivery, so the
316+
// tighter buffer threshold must not turn it straight back into a ref the
317+
// caller cannot read. Oversize is still bounded — the strip-and-recompact
318+
// fallback below runs once the whole event exceeds the shared cap.
319+
...(context.preserveUserFileBase64 ? { thresholdBytes: LARGE_VALUE_THRESHOLD_BYTES } : {}),
315320
})
316321
let eventData = trimFinalBlockLogsForEventData(compactedData)
317322
let eventDataSize = getJsonSize(eventData)

apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1323,7 +1323,17 @@ export class PauseResumeManager {
13231323
await degradeTerminalPublish(terminalStatus, error)
13241324
return { eventId: 0, executionId: resumeExecutionId, event }
13251325
})
1326-
: await eventWriter.write(event)
1326+
: await eventWriter.write(event).catch((error) => {
1327+
// The buffer only backs reconnect replay; the live stream is the
1328+
// primary delivery path. A failed buffer write must not fail work
1329+
// that already ran, which a bare await here would do.
1330+
logger.warn('Resume event buffer write failed; delivering live only', {
1331+
resumeExecutionId,
1332+
eventType: event.type,
1333+
error: toError(error).message,
1334+
})
1335+
return { eventId: 0, executionId: resumeExecutionId, event }
1336+
})
13271337
event.eventId = entry.eventId
13281338
terminalEventPublished ||= Boolean(terminalStatus)
13291339
}

0 commit comments

Comments
 (0)