Skip to content

Commit 7bb2ca6

Browse files
committed
fix(stream): end a replay cleanly when terminal metadata has no terminal event
Terminal metadata is the authoritative end-of-run signal — the happy path writes it atomically with the terminal event, and a run whose terminal event could not be buffered records the status on its own. The reader required both, so the degraded case threw and turned a replay that was merely incomplete into a broken one. Metadata without a matching event means the buffer degraded, not that the run is still going. Log it and close the stream: the reader has already received every event that was buffered, and the durable record is unaffected either way.
1 parent 4c7df92 commit 7bb2ca6

2 files changed

Lines changed: 14 additions & 5 deletions

File tree

apps/sim/app/api/workflows/[id]/executions/[executionId]/stream/route.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ describe('execution stream reconnect route', () => {
9090
expect(mockReadExecutionEventsState).toHaveBeenNthCalledWith(2, 'exec-1', 3)
9191
})
9292

93-
it('errors when terminal metadata has no terminal event to replay', async () => {
93+
it('ends the stream cleanly when terminal metadata has no terminal event to replay', async () => {
9494
mockReadExecutionMetaState
9595
.mockResolvedValueOnce({
9696
status: 'found',
@@ -115,9 +115,7 @@ describe('execution stream reconnect route', () => {
115115
})
116116

117117
expect(response.status).toBe(200)
118-
await expect(response.text()).rejects.toThrow(
119-
'Execution reached terminal metadata without a terminal event'
120-
)
118+
await expect(response.text()).resolves.toContain('data: [DONE]')
121119
})
122120

123121
it('allows replay event id gaps from reserved but unused writer ids', async () => {

apps/sim/app/api/workflows/[id]/executions/[executionId]/stream/route.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,20 @@ export const GET = withRouteHandler(
142142
if (!closed) controller.close()
143143
}
144144

145+
/**
146+
* Terminal metadata is the authoritative end-of-run signal. The
147+
* happy path writes it atomically with the terminal event, and a run
148+
* whose terminal event could not be buffered records the status on
149+
* its own — so metadata without a matching event means the buffer
150+
* degraded, not that the run is still going. End the stream cleanly:
151+
* the reader has already received every event that was buffered, and
152+
* failing here would turn a degraded replay into a broken one.
153+
*/
145154
const closeAfterTerminalEvent = (events: ExecutionEventEntry[]) => {
146155
if (!enqueueEvents(events)) {
147-
throw new Error('Execution reached terminal metadata without a terminal event')
156+
logger.warn('Execution reached terminal metadata without a terminal event', {
157+
executionId,
158+
})
148159
}
149160
closeWithDone()
150161
}

0 commit comments

Comments
 (0)