Repository navigation
Description
Python counterpart of sdk-php#283 (P0 in PHP, where it was a 3.9.2 regression). In Python it is not a regression: every release tested fails, including 2.12.4, 2.13.1, 2.13.2 (installed from PyPI) and main d5e31249.
Problem
Server 3.0.x can record a duplicate open_condition_wait (sdk-php#269 part b, server#1015). A second queued workflow task re-sends the open while the first wait is still open, so the Server opens a new sequence and leaves the first wait's timer live. If a wrong signal then satisfies and reopens the newer wait, and the orphan's condition_timeout timer fires later, Python replay fails with NonDeterministicReplayError at the duplicate's sequence.
Cause: the first wait_condition yield binds to the orphan's ConditionWaitOpened. Its later TimerFired resolution is honoured as timed_out before the duplicate open is considered. Workflow code therefore takes the timeout branch, for example scheduling an activity, while history continues with the duplicate → satisfied → reopen chain.
Reproduction
The shape is sample-app's, from sdk-php#283:
wait_condition(key="decision-1", timeout=300)opens w3 (seq 1, 300) and then w4 (seq 2, 300) with no wake between.- A wrong signal at +120 s leads to
ConditionWaitSatisfiedw4 andTimerCancelled, then a reopen w5 (seq 3, 300 or 180). TimerFiredcondition_timeout for w3 at +300 s.
Before step 3, replay reopens correctly. After it, replay throws.
Done when
- A wait counts as reopened when the next same-logical open (same key or fingerprint) was recorded before the wait's terminal event, even if its timer fired later. That is the state when the next wait opened, which is General's rule for sdk-php#283.
- A wait that timed out before the next same-key open is still finished, and the next open is a separate call (the grace vector,
TestWaitAfterATimedOutWait). - sample-app's shape is a corpus fixture. It replays to the reopen (no throw) on the fixed head and throws on
main. - Tests cover both reopen shapes (300 and 180), and the grace and longer-grace controls.
Activity
Merge main (#251) and bound an orphan's duplicate like any reopen
rmcdaniel commentedon Nov 4, 2026
Python SDK 2.14.0 is published, and its installed-package acceptance passes. This release carries #253, #248, #252 and #184 slice 1.
Release chain (General's conditional go on #257):
48562620ase2c607b86496523d34ba2347b961ee0ccb0e76bc, with parents maine319c9b0and48562620. The merge tree72321dfbequals the head tree, so the go held. The merge carries version/product-train/registry-version 2.14.0.2.14.0(object73db6973) peels toe2c607b8.publish.yml, run 41777555563, at exactlye2c607b8. Its build, publish, verify and github-release jobs all succeeded.distartifact, and each sha256 matches the PyPI JSON:durable_workflow-2.14.0-py3-none-any.whl, 339,804 bytes, sha256d98549a2a9a844733b19f1cabc33d28a9c01560b772b4cd28f4215268d4d0328;durable_workflow-2.14.0.tar.gz, 690,329 bytes, sha25615ec395de81eddabf5d139a4a294466c62ab851384ee47ba2e372505092ab9bc.durable-workflow/sdk-python, workflowpublish.yml, environmentpypi. The in-toto subject name and sha256 equal the downloaded file.releases/latestis 2.14.0.Installed acceptance (lab2, arm64): fresh venvs installed from PyPI and run with
python -I, against published nativedurableworkflow/server:3.0.5(sha256:3cb6e5bf8ec766ac2abd933369b40a77af62794b6d7b921ce21d3fb263e88b72) on SQLite. The container and volume have since been removed.NonDeterministicReplayErrorNonDeterministicReplayErroractivity.defn(typed=True)andschedule_activity(result_type=Customer){"tier": "gold", "tags": ["vip", "upgraded"], "type": "Customer"}