@@ -18,6 +18,7 @@ import {
1818 cancelWorkflowGroupExecution ,
1919 type PublishableWorkflowGroupCancellation ,
2020 publishWorkflowGroupCancellationEvent ,
21+ type WorkflowGroupExecutionCancellationResult ,
2122} from '@/lib/table/workflow-group-cancellation'
2223import { WORKFLOW_EXECUTION_JOB_ID_PREFIX } from '@/lib/workflows/executor/execution-job-ids'
2324import { resolveWorkflowExecutionOwnership } from '@/lib/workflows/executor/execution-queries'
@@ -79,24 +80,53 @@ function toTerminalExecutionStatus(
7980}
8081
8182/**
82- * What the direct terminal log claim did: moved the run to `cancelled` here and
83- * now, matched no row, or never ran — the workflow-group and paused paths write
84- * their terminal row elsewhere, and a failed statement knows nothing either way.
83+ * What this request's own terminal claim did: moved the run to `cancelled` here
84+ * and now, provably matched no row, or ran on a path that cannot tell. Every
85+ * path that can terminalize the run — the direct log claim and the
86+ * workflow-group transition — answers in this one vocabulary, so the report can
87+ * ask a single question: did this request durably write?
8588 */
86- type DirectTerminalWriteOutcome = 'applied' | 'no_row' | 'unknown'
89+ type TerminalWriteOutcome = 'applied' | 'no_row' | 'unknown'
90+
91+ /**
92+ * What a returned workflow-group transition durably wrote. `cancelled` claims
93+ * the cell sidecar and `cancelled_without_sidecar` terminalizes the workflow log
94+ * itself, so both are writes this request performed — including the reconciling
95+ * cancel of a sidecar left in `error` behind an already-`cancelled` log.
96+ * `already_cancelled_without_sidecar` found the log already `cancelled` and
97+ * touched nothing. `already_cancelled` left the sidecar alone but may still have
98+ * terminalized an active workflow log, which the result does not distinguish, so
99+ * it cannot claim either way. `conflict` and `not_workflow_group` never reach
100+ * the report — both throw above — and are mapped only to keep this map total.
101+ */
102+ const WORKFLOW_GROUP_TERMINAL_WRITES = {
103+ cancelled : 'applied' ,
104+ cancelled_without_sidecar : 'applied' ,
105+ already_cancelled : 'unknown' ,
106+ already_cancelled_without_sidecar : 'no_row' ,
107+ conflict : 'unknown' ,
108+ not_workflow_group : 'unknown' ,
109+ } as const satisfies Record < WorkflowGroupExecutionCancellationResult [ 'kind' ] , TerminalWriteOutcome >
87110
88111/**
89112 * Names the terminal state the cancel could not move, or `null` when it did
90113 * real work or when this path cannot tell — in which case the caller keeps the
91114 * undifferentiated report rather than guessing.
92115 *
93- * The status read at entry is not enough on its own: a run that finishes
94- * between that read and the claim leaves a stale non-terminal snapshot behind a
95- * cancel that wrote nothing. The claim's own row count settles that, and a
96- * plain post-read cannot: after a successful cancel the row reads `cancelled`
97- * too, so the state has to be attributed to whoever wrote it. A claim that
98- * moved no row against a non-terminal snapshot re-reads the row it lost the
99- * race to, through the same ownership query the entry read came from.
116+ * A request that durably wrote is never a no-op, whatever the entry snapshot
117+ * said. A run can be terminal at entry and still owe this request a real write:
118+ * a workflow-group run whose log is already `cancelled` can carry a sidecar left
119+ * in `error`, and reconciling it is a durable cancellation that the entry
120+ * snapshot cannot see.
121+ *
122+ * The status read at entry is not enough on its own in the other direction
123+ * either: a run that finishes between that read and the claim leaves a stale
124+ * non-terminal snapshot behind a cancel that wrote nothing. The claim's own row
125+ * count settles that, and a plain post-read cannot: after a successful cancel
126+ * the row reads `cancelled` too, so the state has to be attributed to whoever
127+ * wrote it. A claim that moved no row against a non-terminal snapshot re-reads
128+ * the row it lost the race to, through the same ownership query the entry read
129+ * came from.
100130 *
101131 * Purely observational — it gates no effect, and a read failure falls back to
102132 * the undifferentiated report rather than failing the cancel.
@@ -105,10 +135,11 @@ async function resolveTerminalNoOpReason(
105135 executionId : string ,
106136 workflowId : string ,
107137 priorTerminalStatus : TerminalExecutionStatus | null ,
108- directTerminalWrite : DirectTerminalWriteOutcome
138+ terminalWrite : TerminalWriteOutcome
109139) : Promise < CancelWorkflowExecutionReason | null > {
140+ if ( terminalWrite === 'applied' ) return null
110141 if ( priorTerminalStatus !== null ) return TERMINAL_NO_OP_REASONS [ priorTerminalStatus ]
111- if ( directTerminalWrite !== 'no_row' ) return null
142+ if ( terminalWrite !== 'no_row' ) return null
112143 try {
113144 const { priorStatus } = await resolveWorkflowExecutionOwnership ( executionId , workflowId )
114145 const terminalStatus = toTerminalExecutionStatus ( priorStatus )
@@ -441,9 +472,10 @@ export async function cancelWorkflowExecution(
441472 * The claim's row count is read back only to report it — `returning` changes
442473 * what the statement returns, never the row it writes or the rows it matches.
443474 */
444- let directTerminalWrite : DirectTerminalWriteOutcome = 'unknown'
445- if (
446- groupCancellation === null &&
475+ let terminalWrite : TerminalWriteOutcome = 'unknown'
476+ if ( groupCancellation !== null ) {
477+ terminalWrite = WORKFLOW_GROUP_TERMINAL_WRITES [ groupCancellation . kind ]
478+ } else if (
447479 ( cancellation . durablyRecorded || queuedJobCancelled || locallyAborted ) &&
448480 ! pausedCancelled
449481 ) {
@@ -459,7 +491,7 @@ export async function cancelWorkflowExecution(
459491 )
460492 )
461493 . returning ( { id : workflowExecutionLogs . id } )
462- directTerminalWrite = claimedRows . length > 0 ? 'applied' : 'no_row'
494+ terminalWrite = claimedRows . length > 0 ? 'applied' : 'no_row'
463495 } catch ( dbError ) {
464496 logger . warn ( 'Failed to update execution log status directly' , {
465497 executionId,
@@ -510,16 +542,12 @@ export async function cancelWorkflowExecution(
510542 * run — cancelled, or force-failed with paused state left behind — can still
511543 * carry real paused-HITL reconciliation work, and a genuine failure there owes
512544 * the caller the step that failed, not a no-op. So only an otherwise-clean
513- * `recorded` is a candidate, whatever the prior status was.
545+ * `recorded` is a candidate, whatever the prior status was — and only when
546+ * this request wrote nothing durable on any path.
514547 */
515548 const terminalNoOpReason =
516549 reason === 'recorded' && ! pausedCancelled
517- ? await resolveTerminalNoOpReason (
518- executionId ,
519- workflowId ,
520- priorTerminalStatus ,
521- directTerminalWrite
522- )
550+ ? await resolveTerminalNoOpReason ( executionId , workflowId , priorTerminalStatus , terminalWrite )
523551 : null
524552
525553 return {
0 commit comments