* fix: stop requeuing Finished+Interrupted workflow instances
InterruptedRecoveryScanner now requires Status=Running, matching the
sibling crash-recovery task. DrainOrchestrator skips already-terminal
instances so a runner-clobber race cannot stamp Interrupted onto a
Finished row. Fixes#8052.
Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
* fix: conditionally mark Interrupted so drain cannot clobber Finished
PersistInterruptedAsync no longer SaveAsync-es the Find snapshot. The
store now applies Interrupted only when Status is still non-terminal
(EF: ExecuteUpdate WHERE Status != Finished; memory: mutate the live
row). A runner that commits Finished between read and write keeps its
terminal state, so startup recovery cannot requeue completed work.
Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
* test: disambiguate NSubstitute Returns for TryMarkInterruptedAsync
Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
* fix: make in-memory Interrupted mark atomic against completion
Lock the memory-store check and mutations together, then abort if Status
became Finished in-place so drain cannot record Interrupted on a
completed instance.
Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
* fix: share memory-store lock between Interrupted mark and Save
TryMarkInterruptedAsync now serializes with Save/Update/SaveMany so a
runner's terminal persist cannot land between the non-terminal check
and the Interrupted mutations. A finishing Save therefore cannot leave
Finished+Interrupted.
Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>