Skip to content

Commit 4e90041

Browse files
committed
fix(workflows): clean resume base64 cache under the durable run
Resumed blocks now write the base64 cache under the durable execution ID, so cleanup must use that ID too; the per-attempt ID matched no entries and left cache bytes and budget counters in place until TTL.
1 parent 04033f3 commit 4e90041

2 files changed

Lines changed: 86 additions & 24 deletions

File tree

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

Lines changed: 85 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,40 @@ const {
1818
mockReleaseExecutionSlot,
1919
mockReplaceLargeValueReferenceKeysWithClient,
2020
mockPreprocessExecution,
21+
mockExecuteWorkflowCore,
22+
mockCleanupExecutionBase64Cache,
23+
mockResetExecutionStreamBuffer,
24+
mockInitializeExecutionStreamMeta,
25+
mockEventWriter,
2126
} = vi.hoisted(() => ({
2227
mockReleaseExecutionSlot: vi.fn(),
2328
mockPreprocessExecution: vi.fn(),
2429
mockReplaceLargeValueReferenceKeysWithClient: vi.fn(),
30+
mockExecuteWorkflowCore: vi.fn(),
31+
mockCleanupExecutionBase64Cache: vi.fn(),
32+
mockResetExecutionStreamBuffer: vi.fn(),
33+
mockInitializeExecutionStreamMeta: vi.fn(),
34+
mockEventWriter: { write: vi.fn(), writeTerminal: vi.fn(), close: vi.fn() },
2535
}))
2636

2737
vi.mock('@/lib/execution/preprocessing', () => ({ preprocessExecution: mockPreprocessExecution }))
2838

39+
vi.mock('@/lib/workflows/executor/execution-core', () => ({
40+
executeWorkflowCore: mockExecuteWorkflowCore,
41+
}))
42+
43+
vi.mock('@/lib/uploads/utils/user-file-base64.server', () => ({
44+
cleanupExecutionBase64Cache: mockCleanupExecutionBase64Cache,
45+
}))
46+
47+
vi.mock('@/lib/execution/event-buffer', () => ({
48+
createExecutionEventWriter: vi.fn(() => mockEventWriter),
49+
flushExecutionStreamReplayBuffer: vi.fn(),
50+
initializeExecutionStreamMeta: mockInitializeExecutionStreamMeta,
51+
markExecutionStreamTerminal: vi.fn(),
52+
resetExecutionStreamBuffer: mockResetExecutionStreamBuffer,
53+
}))
54+
2955
vi.mock('@/lib/billing/calculations/usage-reservation', () => ({
3056
releaseExecutionSlot: mockReleaseExecutionSlot,
3157
}))
@@ -2008,10 +2034,11 @@ describe('repeated human review pauses', () => {
20082034
resetDbChainMock()
20092035
})
20102036

2011-
it('keeps the next pause snapshot attached to the log that admission claimed', async () => {
2012-
dbChainMockFns.returning.mockResolvedValueOnce([{ id: 'log-1', deploymentVersionId: null }])
2013-
const stopAfterSnapshot = new Error('stop after snapshot construction')
2014-
mockPreprocessExecution.mockRejectedValueOnce(stopAfterSnapshot)
2037+
const runResumeExecution = Reflect.get(PauseResumeManager, 'runResumeExecution') as (
2038+
args: Record<string, unknown>
2039+
) => Promise<unknown>
2040+
2041+
function createRepeatedReviewResumeArgs(): Record<string, unknown> {
20152042
const seed = createSnapshotSeed()
20162043
const snapshot = JSON.parse(seed.snapshot)
20172044
snapshot.metadata = {
@@ -2023,26 +2050,30 @@ describe('repeated human review pauses', () => {
20232050
}
20242051
snapshot.workflow = { blocks: [], connections: [] }
20252052
snapshot.state = { ...createExecutionState(), dagIncomingEdges: {} }
2026-
const runResumeExecution = Reflect.get(PauseResumeManager, 'runResumeExecution') as (
2027-
args: Record<string, unknown>
2028-
) => Promise<unknown>
2053+
return {
2054+
reservationId: 'reservation-1',
2055+
resumeExecutionId: 'attempt-1',
2056+
pausedExecution: {
2057+
id: 'pause-1',
2058+
workflowId: 'workflow-1',
2059+
executionId: 'durable-run',
2060+
executionSnapshot: { ...seed, snapshot: JSON.stringify(snapshot) },
2061+
pausePoints: { hitl_loop0: { blockId: 'hitl', pauseKind: 'human' } },
2062+
},
2063+
contextId: 'hitl_loop0',
2064+
resumeInput: { reply: 'partial answer' },
2065+
userId: 'user-1',
2066+
}
2067+
}
20292068

2030-
await expect(
2031-
runResumeExecution({
2032-
reservationId: 'reservation-1',
2033-
resumeExecutionId: 'attempt-1',
2034-
pausedExecution: {
2035-
id: 'pause-1',
2036-
workflowId: 'workflow-1',
2037-
executionId: 'durable-run',
2038-
executionSnapshot: { ...seed, snapshot: JSON.stringify(snapshot) },
2039-
pausePoints: { hitl_loop0: { blockId: 'hitl', pauseKind: 'human' } },
2040-
},
2041-
contextId: 'hitl_loop0',
2042-
resumeInput: { reply: 'partial answer' },
2043-
userId: 'user-1',
2044-
})
2045-
).rejects.toBe(stopAfterSnapshot)
2069+
it('keeps the next pause snapshot attached to the log that admission claimed', async () => {
2070+
dbChainMockFns.returning.mockResolvedValueOnce([{ id: 'log-1', deploymentVersionId: null }])
2071+
const stopAfterSnapshot = new Error('stop after snapshot construction')
2072+
mockPreprocessExecution.mockRejectedValueOnce(stopAfterSnapshot)
2073+
2074+
await expect(runResumeExecution(createRepeatedReviewResumeArgs())).rejects.toBe(
2075+
stopAfterSnapshot
2076+
)
20462077

20472078
expect(humanInTheLoopLogger.info).toHaveBeenCalledWith(
20482079
'Created resume snapshot',
@@ -2060,6 +2091,37 @@ describe('repeated human review pauses', () => {
20602091
).toBe(true)
20612092
})
20622093

2094+
it('cleans the base64 cache under the durable run that the resumed blocks wrote to', async () => {
2095+
dbChainMockFns.returning.mockResolvedValueOnce([{ id: 'log-1', deploymentVersionId: null }])
2096+
mockPreprocessExecution.mockResolvedValueOnce({
2097+
success: true,
2098+
actorUserId: 'user-1',
2099+
executionTimeout: { async: 60_000 },
2100+
})
2101+
mockResetExecutionStreamBuffer.mockResolvedValueOnce(true)
2102+
mockInitializeExecutionStreamMeta.mockResolvedValueOnce(true)
2103+
mockEventWriter.write.mockResolvedValue({ eventId: 1 })
2104+
mockEventWriter.writeTerminal.mockResolvedValue({ eventId: 2 })
2105+
mockEventWriter.close.mockResolvedValue(undefined)
2106+
mockExecuteWorkflowCore.mockResolvedValueOnce({
2107+
success: true,
2108+
status: 'completed',
2109+
output: {},
2110+
logs: [],
2111+
metadata: { executionId: 'durable-run' },
2112+
})
2113+
2114+
await expect(runResumeExecution(createRepeatedReviewResumeArgs())).resolves.toMatchObject({
2115+
status: 'completed',
2116+
})
2117+
2118+
expect(mockExecuteWorkflowCore).toHaveBeenCalledWith(
2119+
expect.objectContaining({ includeFileBase64: true })
2120+
)
2121+
expect(mockCleanupExecutionBase64Cache).toHaveBeenCalledTimes(1)
2122+
expect(mockCleanupExecutionBase64Cache).toHaveBeenCalledWith('durable-run')
2123+
})
2124+
20632125
it('settles the answered context exactly once when the same run pauses again', async () => {
20642126
const runSpy = vi
20652127
.spyOn(PauseResumeManager as unknown as PauseResumeManagerInternals, 'runResumeExecution')

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2026,7 +2026,7 @@ export class PauseResumeManager {
20262026
)
20272027
})
20282028
}
2029-
void cleanupExecutionBase64Cache(resumeExecutionId)
2029+
void cleanupExecutionBase64Cache(parentExecutionId)
20302030
}
20312031

20322032
/**

0 commit comments

Comments
 (0)