A Space task transition that routes to the workflow runtime — park_stopped, recover_transition, stop_for_status — is not written atomically against the status the pipeline decided on.
decide returns the executor result directly for those routes, so writeStatus (and its expectedStatus / expectedWorkflowRunId compare-and-set) never runs. The only protection is snapshotStillCurrent() in transition-task.ts, a plain re-read of status and workflowRunId immediately before dispatch. The executors behind SpaceRuntimeService.parkStoppedWorkflowTask, stopWorkflowBackedTaskForStatus and recoverTaskExecution then read and update the task again with no expectation carried in.
So there is a window between that re-read and the executor's write. If another writer moves an in_progress task to blocked inside it, a cancellation aimed at in_progress still cancels — from blocked — and performs the workflow and session side effects, rather than rejecting.
This predates #4524 and is not caused by it. #4524 added an optional caller-supplied expectedStatus, which is a true compare-and-set on the direct-write path and a pre-dispatch check on these routes; its description says exactly that, so no caller is promised atomicity it does not get. Closing the window properly means threading the expected snapshot into each runtime executor's own transition, which touches three SpaceRuntimeService entry points and their call sites — a separate slice.
Worth deciding whether the guarantee should be uniform before more callers adopt expectedStatus. spaceTask.publish will be the first, and publish routes to a direct write (draft tasks have no active run), so it is unaffected.
A Space task transition that routes to the workflow runtime —
park_stopped,recover_transition,stop_for_status— is not written atomically against the status the pipeline decided on.decidereturns the executor result directly for those routes, sowriteStatus(and itsexpectedStatus/expectedWorkflowRunIdcompare-and-set) never runs. The only protection issnapshotStillCurrent()intransition-task.ts, a plain re-read of status andworkflowRunIdimmediately before dispatch. The executors behindSpaceRuntimeService.parkStoppedWorkflowTask,stopWorkflowBackedTaskForStatusandrecoverTaskExecutionthen read and update the task again with no expectation carried in.So there is a window between that re-read and the executor's write. If another writer moves an
in_progresstask toblockedinside it, a cancellation aimed atin_progressstill cancels — fromblocked— and performs the workflow and session side effects, rather than rejecting.This predates #4524 and is not caused by it. #4524 added an optional caller-supplied
expectedStatus, which is a true compare-and-set on the direct-write path and a pre-dispatch check on these routes; its description says exactly that, so no caller is promised atomicity it does not get. Closing the window properly means threading the expected snapshot into each runtime executor's own transition, which touches threeSpaceRuntimeServiceentry points and their call sites — a separate slice.Worth deciding whether the guarantee should be uniform before more callers adopt
expectedStatus.spaceTask.publishwill be the first, and publish routes to a direct write (drafttasks have no active run), so it is unaffected.