Skip to content

Commit 20d6f49

Browse files
committed
fix(resume): stop a failed run-buffer publish from stranding a resumed execution
The run buffer is a replay convenience for stream readers; the durable execution record is authoritative. A failed terminal event publish was deciding the outcome of work that had already run: it threw, the resume was marked failed, and the paused execution was left paused forever. That path is also not retryable — the workflow had already executed — so the next resume attempt re-ran its side effects. Degrade instead. Record the terminal status on the stream meta so readers are not left polling an 'active' stream, log the failure, and let the resume settle on its real result. The same treatment applies when the terminal event is never published at all, which previously synthesized an error for the same stranding effect. Removes the now-unreachable TERMINAL_PUBLISH_ERROR constant and its branch. Pre-execution buffer failures stay fatal and retryable, since no work has happened yet.
1 parent 1377256 commit 20d6f49

1 file changed

Lines changed: 48 additions & 12 deletions

File tree

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

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
flushExecutionStreamReplayBuffer,
1515
initializeExecutionStreamMeta,
1616
resetExecutionStreamBuffer,
17+
setExecutionMeta,
1718
type TerminalExecutionStreamStatus,
1819
} from '@/lib/execution/event-buffer'
1920
import {
@@ -71,7 +72,6 @@ const execDb = dbFor('exec')
7172

7273
const logger = createLogger('HumanInTheLoopManager')
7374
const RUN_BUFFER_UNAVAILABLE_ERROR = 'Run buffer temporarily unavailable'
74-
const TERMINAL_PUBLISH_ERROR = 'Run buffer terminal event publish failed'
7575
const RESUMABLE_PAUSED_STATUSES = ['paused', 'partially_resumed'] as const
7676
const CANCELLABLE_PAUSED_STATUSES = ['paused', 'partially_resumed'] as const
7777
const AUTOMATIC_RESUME_INTERVENTION_PREFIX = 'Automatic resume requires manual intervention: '
@@ -771,7 +771,7 @@ export class PauseResumeManager {
771771
preserveForRetry: true,
772772
retryable: error.retryable,
773773
})
774-
} else if (message === RUN_BUFFER_UNAVAILABLE_ERROR || message === TERMINAL_PUBLISH_ERROR) {
774+
} else if (message === RUN_BUFFER_UNAVAILABLE_ERROR) {
775775
await PauseResumeManager.markResumeAttemptFailed({
776776
resumeEntryId,
777777
pausedExecutionId: pausedExecution.id,
@@ -1280,20 +1280,48 @@ export class PauseResumeManager {
12801280
}
12811281

12821282
let terminalEventPublished = false
1283+
let terminalPublishDegraded = false
1284+
1285+
/**
1286+
* The run buffer is a replay convenience for stream readers; the durable
1287+
* execution record is authoritative. A failed terminal publish must not
1288+
* decide the outcome of work that already ran — the resume is not
1289+
* retryable at this point, so throwing here would strand the execution as
1290+
* paused and re-run its side effects on the next attempt. Degrade instead:
1291+
* record the terminal status on the stream meta so readers are not left
1292+
* polling an 'active' stream forever, and let the resume settle normally.
1293+
*/
1294+
const degradeTerminalPublish = async (
1295+
terminalStatus: TerminalExecutionStreamStatus,
1296+
error: unknown
1297+
) => {
1298+
terminalPublishDegraded = true
1299+
logger.warn('Failed to publish resume terminal event', {
1300+
resumeExecutionId,
1301+
status: terminalStatus,
1302+
error: toError(error).message,
1303+
})
1304+
const metaPersisted = await setExecutionMeta(resumeExecutionId, {
1305+
status: terminalStatus,
1306+
}).catch(() => false)
1307+
if (!metaPersisted) {
1308+
logger.warn('Failed to record degraded terminal status on resume stream meta', {
1309+
resumeExecutionId,
1310+
status: terminalStatus,
1311+
})
1312+
}
1313+
}
1314+
12831315
const writeBufferedEvent = async (
12841316
event: ExecutionEvent,
12851317
terminalStatus?: TerminalExecutionStreamStatus
12861318
) => {
12871319
const isBuffered = !LIVE_ONLY_EXECUTION_EVENT_TYPES.has(event.type)
12881320
if (isBuffered) {
12891321
const entry = terminalStatus
1290-
? await eventWriter.writeTerminal(event, terminalStatus).catch((error) => {
1291-
logger.warn('Failed to publish resume terminal event', {
1292-
resumeExecutionId,
1293-
status: terminalStatus,
1294-
error: toError(error).message,
1295-
})
1296-
throw new Error(TERMINAL_PUBLISH_ERROR)
1322+
? await eventWriter.writeTerminal(event, terminalStatus).catch(async (error) => {
1323+
await degradeTerminalPublish(terminalStatus, error)
1324+
return { eventId: 0, executionId: resumeExecutionId, event }
12971325
})
12981326
: await eventWriter.write(event)
12991327
event.eventId = entry.eventId
@@ -1686,9 +1714,10 @@ export class PauseResumeManager {
16861714
status: finalMetaStatus,
16871715
replayBufferFlushed,
16881716
})
1689-
if (!executionError) {
1690-
executionError = new Error(TERMINAL_PUBLISH_ERROR)
1691-
}
1717+
await degradeTerminalPublish(
1718+
finalMetaStatus,
1719+
new Error('Terminal event was never published')
1720+
)
16921721
} else {
16931722
await eventWriter.close().catch((error) => {
16941723
logger.warn('Failed to close resume event writer after terminal publish', {
@@ -1707,6 +1736,13 @@ export class PauseResumeManager {
17071736
*/
17081737
await loggingSession.waitForPostExecution()
17091738

1739+
if (terminalPublishDegraded) {
1740+
logger.warn('Resume settled with a degraded run buffer', {
1741+
resumeExecutionId,
1742+
status: finalMetaStatus,
1743+
})
1744+
}
1745+
17101746
if (executionError || !result) {
17111747
throw executionError ?? new Error('Resume execution did not produce a result')
17121748
}

0 commit comments

Comments
 (0)