durable-workflow 2.5.6__tar.gz → 2.5.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. {durable_workflow-2.5.6/src/durable_workflow.egg-info → durable_workflow-2.5.8}/PKG-INFO +11 -9
  2. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/README.md +10 -8
  3. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/pyproject.toml +4 -4
  4. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/client.py +2 -1
  5. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/worker.py +21 -3
  6. {durable_workflow-2.5.6 → durable_workflow-2.5.8/src/durable_workflow.egg-info}/PKG-INFO +11 -9
  7. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_cancellation_scope_opening.py +2 -2
  8. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_client.py +6 -1
  9. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_cooperative_cancellation_client.py +4 -7
  10. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_cooperative_cancellation_worker.py +1 -1
  11. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_cooperative_remote_worker.py +3 -3
  12. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_prepared_local_activity_client.py +2 -2
  13. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_release_metadata.py +2 -2
  14. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_worker.py +64 -1
  15. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/LICENSE +0 -0
  16. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/setup.cfg +0 -0
  17. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/__init__.py +0 -0
  18. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/_activity_process.py +0 -0
  19. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/_avro.py +0 -0
  20. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/_cancellation_scope.py +0 -0
  21. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/_cancellation_scope_history.py +0 -0
  22. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/_cooperative_cancellation.py +0 -0
  23. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/_prepared_local_activity.py +0 -0
  24. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
  25. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/activity.py +0 -0
  26. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/auth_composition.py +0 -0
  27. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/cancellation.py +0 -0
  28. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/errors.py +0 -0
  29. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/external_storage.py +0 -0
  30. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/external_task_input.py +0 -0
  31. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/external_task_result.py +0 -0
  32. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/history_bundle_verify.py +0 -0
  33. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/interceptors.py +0 -0
  34. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/invocable.py +0 -0
  35. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/metrics.py +0 -0
  36. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/nexus.py +0 -0
  37. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/py.typed +0 -0
  38. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/python_conformance.py +0 -0
  39. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/replay_conformance.py +0 -0
  40. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/replay_verify.py +0 -0
  41. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/retry_policy.py +0 -0
  42. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/serializer.py +0 -0
  43. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/sync.py +0 -0
  44. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/testing.py +0 -0
  45. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/worker_session.py +0 -0
  46. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/workflow.py +0 -0
  47. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow/workflow_updates_conformance.py +0 -0
  48. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow.egg-info/SOURCES.txt +0 -0
  49. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  50. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  51. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow.egg-info/requires.txt +0 -0
  52. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/src/durable_workflow.egg-info/top_level.txt +0 -0
  53. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_activity_cancellation_policies.py +0 -0
  54. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_activity_context.py +0 -0
  55. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_activity_process.py +0 -0
  56. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_auth_composition.py +0 -0
  57. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_avro_value_benchmark.py +0 -0
  58. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_avro_value_protocol.py +0 -0
  59. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_cancellation_context.py +0 -0
  60. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_cancellation_remaining_time.py +0 -0
  61. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_cancellation_scope_admission.py +0 -0
  62. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_cancellation_scope_authoring.py +0 -0
  63. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_cancellation_scope_boundary.py +0 -0
  64. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_child_workflow_policies.py +0 -0
  65. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_ci_checkout.py +0 -0
  66. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_ci_integration_endpoint.py +0 -0
  67. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_client_timeouts.py +0 -0
  68. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_codec_regression_corpus.py +0 -0
  69. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_committed_cancellation_scope_descendants.py +0 -0
  70. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_committed_cancellation_scope_groups.py +0 -0
  71. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_committed_cancellation_scope_history.py +0 -0
  72. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_committed_cancellation_scope_replay.py +0 -0
  73. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_condition_replay_drift.py +0 -0
  74. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_control_plane_parity_fixtures.py +0 -0
  75. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_cooperative_cancellation.py +0 -0
  76. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_docstring_cross_references.py +0 -0
  77. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_durable_selection.py +0 -0
  78. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_errors.py +0 -0
  79. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_external_storage.py +0 -0
  80. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_external_task_input.py +0 -0
  81. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_external_task_result.py +0 -0
  82. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_golden_history_replay.py +0 -0
  83. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_history_bundle_verify.py +0 -0
  84. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_history_event_contract.py +0 -0
  85. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_invocable.py +0 -0
  86. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_message_streams.py +0 -0
  87. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_metrics.py +0 -0
  88. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_nexus_namespace.py +0 -0
  89. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_nexus_wire.py +0 -0
  90. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_order_processing_example.py +0 -0
  91. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_parallel_completion_order_replay.py +0 -0
  92. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_parallel_saga_authoring.py +0 -0
  93. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_patch_history.py +0 -0
  94. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_pending_operation_replay.py +0 -0
  95. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_prepared_local_activity_groups.py +0 -0
  96. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_prepared_local_activity_policies.py +0 -0
  97. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_prepared_local_activity_worker.py +0 -0
  98. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_public_boundary_scanner.py +0 -0
  99. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_pypi_project_surface.py +0 -0
  100. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_python_conformance.py +0 -0
  101. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_queries.py +0 -0
  102. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_readme_quickstart.py +0 -0
  103. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_redrive_replay.py +0 -0
  104. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_registration_ambiguity.py +0 -0
  105. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_replay.py +0 -0
  106. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_replay_conformance.py +0 -0
  107. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_replay_regression_corpus.py +0 -0
  108. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_replay_verify.py +0 -0
  109. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_retry_policy.py +0 -0
  110. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_run_cancellation_scope_replay.py +0 -0
  111. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_runtime_external_payload_transport.py +0 -0
  112. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_schedules.py +0 -0
  113. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_scoped_run_cancellation_context.py +0 -0
  114. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_serializer.py +0 -0
  115. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_signals.py +0 -0
  116. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_sleep.py +0 -0
  117. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_standalone_activity_client.py +0 -0
  118. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_sticky_execution.py +0 -0
  119. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_storage_admission.py +0 -0
  120. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_sync.py +0 -0
  121. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_testing_harness.py +0 -0
  122. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_update_signal_condition_replay.py +0 -0
  123. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_updates.py +0 -0
  124. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_wait_condition.py +0 -0
  125. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_worker_session.py +0 -0
  126. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_workflow_entrypoint_validation.py +0 -0
  127. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_workflow_result_timeout.py +0 -0
  128. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_workflow_streams.py +0 -0
  129. {durable_workflow-2.5.6 → durable_workflow-2.5.8}/tests/test_workflow_updates_conformance.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 2.5.6
3
+ Version: 2.5.8
4
4
  Summary: Python client and worker SDK for Durable Workflow Cloud and self-hosted Server
5
5
  Author: Durable Workflow Contributors
6
6
  License-Expression: MIT
@@ -155,12 +155,15 @@ capabilities with Server at startup. Use stable `2.x` SDK and Server channels
155
155
  for new applications. The [compatibility guide](https://durable-workflow.com/docs/2.0/compatibility/)
156
156
  documents protocol and upgrade guarantees.
157
157
 
158
- ## Cooperative cancellation release candidate
158
+ ## Worker protocol and cooperative cancellation
159
+
160
+ Workers use protocol 1.20 by default. For a Server that supports only 1.19, set
161
+ `DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION=1.19`. This changes the request protocol
162
+ without enabling optional worker features.
159
163
 
160
164
  Use `request_cancellation()` for bounded, replayable workflow cleanup. Opt in
161
165
  against a Server that advertises protocol 1.20 and the required capabilities:
162
- set `DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION=1.20` and include
163
- `cooperative_cancellation` in the Worker's `capabilities`. Durable local callback
166
+ include `cooperative_cancellation` in the Worker's `capabilities`. Durable local callback
164
167
  admission also needs `prepared_local_activities`, with
165
168
  `prepared_local_activity_cancellation_policies` for explicit local policies.
166
169
  Independently cancellable scopes remain disabled.
@@ -196,11 +199,10 @@ to true. CI verifies that checkout, builds the candidate Server, enables protoco
196
199
  authority and exact source provenance. An optional exact `native_commit` mounts
197
200
  that public Native checkout read-only into the test stack. The image's published
198
201
  Composer authority stays intact and the evidence identifies the source overlay.
199
- These are source qualification runs. For a local
200
- candidate, set `DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION=1.20` before starting
201
- Compose and `DURABLE_WORKFLOW_COOPERATIVE_QUALIFICATION=1` for pytest. These cases
202
- fail if the runtime does not discover the required capability. Ordinary CI
203
- keeps protocol 1.19 and skips this unpublished feature's connected cases.
202
+ These are source qualification runs. For a local candidate, set
203
+ `DURABLE_WORKFLOW_COOPERATIVE_QUALIFICATION=1` for pytest. These cases fail if the
204
+ runtime does not discover the required capability. Ordinary CI uses the SDK's
205
+ default protocol and skips the optional connected cooperative cases.
204
206
 
205
207
  ## License
206
208
 
@@ -115,12 +115,15 @@ capabilities with Server at startup. Use stable `2.x` SDK and Server channels
115
115
  for new applications. The [compatibility guide](https://durable-workflow.com/docs/2.0/compatibility/)
116
116
  documents protocol and upgrade guarantees.
117
117
 
118
- ## Cooperative cancellation release candidate
118
+ ## Worker protocol and cooperative cancellation
119
+
120
+ Workers use protocol 1.20 by default. For a Server that supports only 1.19, set
121
+ `DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION=1.19`. This changes the request protocol
122
+ without enabling optional worker features.
119
123
 
120
124
  Use `request_cancellation()` for bounded, replayable workflow cleanup. Opt in
121
125
  against a Server that advertises protocol 1.20 and the required capabilities:
122
- set `DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION=1.20` and include
123
- `cooperative_cancellation` in the Worker's `capabilities`. Durable local callback
126
+ include `cooperative_cancellation` in the Worker's `capabilities`. Durable local callback
124
127
  admission also needs `prepared_local_activities`, with
125
128
  `prepared_local_activity_cancellation_policies` for explicit local policies.
126
129
  Independently cancellable scopes remain disabled.
@@ -156,11 +159,10 @@ to true. CI verifies that checkout, builds the candidate Server, enables protoco
156
159
  authority and exact source provenance. An optional exact `native_commit` mounts
157
160
  that public Native checkout read-only into the test stack. The image's published
158
161
  Composer authority stays intact and the evidence identifies the source overlay.
159
- These are source qualification runs. For a local
160
- candidate, set `DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION=1.20` before starting
161
- Compose and `DURABLE_WORKFLOW_COOPERATIVE_QUALIFICATION=1` for pytest. These cases
162
- fail if the runtime does not discover the required capability. Ordinary CI
163
- keeps protocol 1.19 and skips this unpublished feature's connected cases.
162
+ These are source qualification runs. For a local candidate, set
163
+ `DURABLE_WORKFLOW_COOPERATIVE_QUALIFICATION=1` for pytest. These cases fail if the
164
+ runtime does not discover the required capability. Ordinary CI uses the SDK's
165
+ default protocol and skips the optional connected cooperative cases.
164
166
 
165
167
  ## License
166
168
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "durable-workflow"
7
- version = "2.5.6"
7
+ version = "2.5.8"
8
8
  description = "Python client and worker SDK for Durable Workflow Cloud and self-hosted Server"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -71,10 +71,10 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main"
71
71
  durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main"
72
72
 
73
73
  [tool.durable-workflow]
74
- product-train = "2.5.6"
75
- registry-version = "2.5.6"
74
+ product-train = "2.5.8"
75
+ registry-version = "2.5.8"
76
76
  supported-server-versions = "2.5.16"
77
- worker-protocol-version = "1.19"
77
+ worker-protocol-version = "1.20"
78
78
  control-plane-version = "2"
79
79
  durable-selection = true
80
80
  durable-selection-minimum-worker-protocol-version = "1.19"
@@ -72,7 +72,8 @@ from .nexus import NexusOperationResult, nexus_request_payload
72
72
  from .retry_policy import TransportRetryPolicy
73
73
  from .worker_session import WorkerSessionOptions
74
74
 
75
- PROTOCOL_VERSION = "1.19"
75
+ PROTOCOL_VERSION = "1.20"
76
+ MINIMUM_WORKER_PROTOCOL_VERSION = "1.19"
76
77
  CONTROL_PLANE_VERSION = "2"
77
78
  PORTABLE_WORKER_AFFINITY_CAPABILITY_MANIFEST: dict[str, dict[str, str | bool]] = {
78
79
  "local_activities": {
@@ -55,6 +55,7 @@ from .client import (
55
55
  CONTROL_PLANE_REQUEST_CONTRACT_VERSION,
56
56
  CONTROL_PLANE_VERSION,
57
57
  DEFAULT_SDK_VERSION,
58
+ MINIMUM_WORKER_PROTOCOL_VERSION,
58
59
  PORTABLE_WORKER_AFFINITY_CAPABILITY_MANIFEST,
59
60
  PROTOCOL_VERSION,
60
61
  Client,
@@ -877,6 +878,16 @@ def _server_protocol_compatible(server_version: str, sdk_version: str) -> bool:
877
878
 
878
879
 
879
880
  def _validate_server_compatibility(info: dict[str, Any]) -> None:
881
+ requested_protocol = _protocol_version_from_env(
882
+ "DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", PROTOCOL_VERSION,
883
+ )
884
+ if requested_protocol not in (MINIMUM_WORKER_PROTOCOL_VERSION, PROTOCOL_VERSION):
885
+ raise RuntimeError(
886
+ "Server compatibility error: sdk-python supports worker protocol "
887
+ f"{MINIMUM_WORKER_PROTOCOL_VERSION} or protocol {PROTOCOL_VERSION}; "
888
+ f"requested {requested_protocol!r}."
889
+ )
890
+
880
891
  control_plane = info.get("control_plane")
881
892
  if not isinstance(control_plane, dict):
882
893
  raise RuntimeError(
@@ -914,15 +925,15 @@ def _validate_server_compatibility(info: dict[str, Any]) -> None:
914
925
  if not isinstance(worker_protocol, dict):
915
926
  raise RuntimeError(
916
927
  "Server compatibility error: missing worker_protocol manifest; "
917
- f"sdk-python requires worker_protocol.version {PROTOCOL_VERSION}."
928
+ f"sdk-python requires worker_protocol.version {requested_protocol}."
918
929
  )
919
930
 
920
931
  worker_protocol_version = _manifest_version(worker_protocol)
921
- if not _server_protocol_compatible(worker_protocol_version, PROTOCOL_VERSION):
932
+ if not _server_protocol_compatible(worker_protocol_version, requested_protocol):
922
933
  raise RuntimeError(
923
934
  "Server compatibility error: incompatible worker_protocol.version "
924
935
  f"{worker_protocol_version!r}; sdk-python requires major-equal and "
925
- f"minor>={PROTOCOL_VERSION!r}."
936
+ f"minor>={requested_protocol!r}."
926
937
  )
927
938
 
928
939
  auth_composition = info.get("auth_composition_contract")
@@ -4233,6 +4244,7 @@ class Worker:
4233
4244
  """
4234
4245
  background_tasks: list[asyncio.Task[Any]] = []
4235
4246
  deadline = asyncio.get_running_loop().time() + timeout
4247
+ startup_completed = False
4236
4248
 
4237
4249
  self._begin_run()
4238
4250
  try:
@@ -4241,6 +4253,7 @@ class Worker:
4241
4253
  finally:
4242
4254
  self._registration_done.set()
4243
4255
 
4256
+ startup_completed = True
4244
4257
  if self._stop.is_set():
4245
4258
  raise asyncio.CancelledError
4246
4259
  background_tasks.append(asyncio.create_task(self._heartbeat_loop()))
@@ -4262,6 +4275,11 @@ class Worker:
4262
4275
  run_until_loop, timeout=max(0.0, deadline - asyncio.get_running_loop().time()),
4263
4276
  )
4264
4277
  except asyncio.TimeoutError as error:
4278
+ if not startup_completed:
4279
+ raise TimeoutError(
4280
+ f"worker {self.worker_id} startup did not finish within the "
4281
+ f"{timeout}s workflow wait budget for {workflow_id}"
4282
+ ) from error
4265
4283
  raise TimeoutError(f"workflow {workflow_id} not terminal after {timeout}s") from error
4266
4284
  finally:
4267
4285
  primary_error = sys.exc_info()[1]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 2.5.6
3
+ Version: 2.5.8
4
4
  Summary: Python client and worker SDK for Durable Workflow Cloud and self-hosted Server
5
5
  Author: Durable Workflow Contributors
6
6
  License-Expression: MIT
@@ -155,12 +155,15 @@ capabilities with Server at startup. Use stable `2.x` SDK and Server channels
155
155
  for new applications. The [compatibility guide](https://durable-workflow.com/docs/2.0/compatibility/)
156
156
  documents protocol and upgrade guarantees.
157
157
 
158
- ## Cooperative cancellation release candidate
158
+ ## Worker protocol and cooperative cancellation
159
+
160
+ Workers use protocol 1.20 by default. For a Server that supports only 1.19, set
161
+ `DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION=1.19`. This changes the request protocol
162
+ without enabling optional worker features.
159
163
 
160
164
  Use `request_cancellation()` for bounded, replayable workflow cleanup. Opt in
161
165
  against a Server that advertises protocol 1.20 and the required capabilities:
162
- set `DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION=1.20` and include
163
- `cooperative_cancellation` in the Worker's `capabilities`. Durable local callback
166
+ include `cooperative_cancellation` in the Worker's `capabilities`. Durable local callback
164
167
  admission also needs `prepared_local_activities`, with
165
168
  `prepared_local_activity_cancellation_policies` for explicit local policies.
166
169
  Independently cancellable scopes remain disabled.
@@ -196,11 +199,10 @@ to true. CI verifies that checkout, builds the candidate Server, enables protoco
196
199
  authority and exact source provenance. An optional exact `native_commit` mounts
197
200
  that public Native checkout read-only into the test stack. The image's published
198
201
  Composer authority stays intact and the evidence identifies the source overlay.
199
- These are source qualification runs. For a local
200
- candidate, set `DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION=1.20` before starting
201
- Compose and `DURABLE_WORKFLOW_COOPERATIVE_QUALIFICATION=1` for pytest. These cases
202
- fail if the runtime does not discover the required capability. Ordinary CI
203
- keeps protocol 1.19 and skips this unpublished feature's connected cases.
202
+ These are source qualification runs. For a local candidate, set
203
+ `DURABLE_WORKFLOW_COOPERATIVE_QUALIFICATION=1` for pytest. These cases fail if the
204
+ runtime does not discover the required capability. Ordinary CI uses the SDK's
205
+ default protocol and skips the optional connected cooperative cases.
204
206
 
205
207
  ## License
206
208
 
@@ -152,8 +152,8 @@ async def test_invalid_authority_fails_before_io(change: dict[str, Any]) -> None
152
152
  send.assert_not_awaited()
153
153
 
154
154
 
155
- async def test_published_protocol_refuses_opening_before_io(monkeypatch: pytest.MonkeyPatch) -> None:
156
- monkeypatch.delenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION")
155
+ async def test_previous_protocol_refuses_opening_before_io(monkeypatch: pytest.MonkeyPatch) -> None:
156
+ monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", "1.19")
157
157
  async with Client("http://server", namespace="tenant") as client:
158
158
  with patch.object(client, "_request", new_callable=AsyncMock) as send:
159
159
  with pytest.raises(ValueError, match="1.20"):
@@ -164,10 +164,15 @@ class TestHeaders:
164
164
 
165
165
  def test_worker_headers(self, client: Client) -> None:
166
166
  h = client._headers(worker=True)
167
- assert PROTOCOL_VERSION == "1.19"
167
+ assert PROTOCOL_VERSION == "1.20"
168
168
  assert h["X-Durable-Workflow-Protocol-Version"] == PROTOCOL_VERSION
169
169
  assert "X-Durable-Workflow-Control-Plane-Version" not in h
170
170
 
171
+ def test_explicit_previous_worker_protocol(self, client: Client, monkeypatch: pytest.MonkeyPatch) -> None:
172
+ monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", "1.19")
173
+ assert client._headers(worker=True)["X-Durable-Workflow-Protocol-Version"] == "1.19"
174
+ assert client._headers(worker=False)["X-Durable-Workflow-Control-Plane-Version"] == "2"
175
+
171
176
  def test_conformance_protocol_header_overrides(self, client: Client, monkeypatch: pytest.MonkeyPatch) -> None:
172
177
  monkeypatch.setenv("DURABLE_WORKFLOW_CONTROL_PLANE_VERSION", "3")
173
178
  monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", "2.0")
@@ -406,14 +406,11 @@ async def test_activity_stop_receipt_rejects_invalid_identity_before_io(
406
406
 
407
407
 
408
408
  @pytest.mark.asyncio
409
- @pytest.mark.parametrize("version", [None, "1.19", "2.20", "invalid"])
410
- async def test_activity_stop_receipt_requires_explicit_worker_opt_in(
411
- client: Client, monkeypatch: pytest.MonkeyPatch, version: str | None,
409
+ @pytest.mark.parametrize("version", ["1.19", "2.20", "invalid"])
410
+ async def test_activity_stop_receipt_requires_compatible_protocol(
411
+ client: Client, monkeypatch: pytest.MonkeyPatch, version: str,
412
412
  ) -> None:
413
- if version is None:
414
- monkeypatch.delenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", raising=False)
415
- else:
416
- monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", version)
413
+ monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", version)
417
414
  with patch.object(client._http, "request", new_callable=AsyncMock) as send, pytest.raises(ValueError):
418
415
  await client.acknowledge_activity_cancellation(
419
416
  task_id="task/1", activity_attempt_id="attempt-1", lease_owner="worker-1", request_id="original-request",
@@ -134,7 +134,7 @@ class ClaimServer:
134
134
  return {"outcome": "completed"}
135
135
 
136
136
  async def worker(self, monkeypatch: pytest.MonkeyPatch, **kwargs: Any) -> Worker:
137
- monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", "1.20")
137
+ monkeypatch.delenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", raising=False)
138
138
  # These transport/replay fixtures use local callbacks that share mock
139
139
  # state with this process. They never poll or advertise remote handlers.
140
140
  local_callbacks = kwargs.pop("activities", [])
@@ -94,7 +94,7 @@ async def application_failure(kind: str) -> None:
94
94
 
95
95
  @pytest_asyncio.fixture
96
96
  async def owner(monkeypatch: pytest.MonkeyPatch): # type: ignore[no-untyped-def]
97
- monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", "1.20")
97
+ monkeypatch.delenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", raising=False)
98
98
  client = AsyncMock(spec=Client)
99
99
  client.get_cluster_info.return_value = compatible_cluster_info(worker_protocol={
100
100
  "version": "1.20", "server_capabilities": {"cooperative_cancellation": True},
@@ -317,8 +317,8 @@ async def test_client_observation_keeps_worker_credentials_namespace_and_fence(m
317
317
  assert send.await_args.kwargs["json"] == {"activity_attempt_id": "attempt", "lease_owner": "owner"}
318
318
 
319
319
 
320
- async def test_client_observation_requires_explicit_protocol_before_io(monkeypatch: pytest.MonkeyPatch) -> None:
321
- monkeypatch.delenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", raising=False)
320
+ async def test_client_observation_refuses_previous_protocol_before_io(monkeypatch: pytest.MonkeyPatch) -> None:
321
+ monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", "1.19")
322
322
  async with Client("http://runtime.test") as client:
323
323
  with (patch.object(client._http, "request", new_callable=AsyncMock) as send,
324
324
  pytest.raises(ValueError, match="1.20")):
@@ -66,8 +66,8 @@ async def test_invalid_claim_or_authority_never_starts_transport(change: dict[st
66
66
  send.assert_not_awaited()
67
67
 
68
68
 
69
- async def test_default_protocol_refuses_before_io(monkeypatch: pytest.MonkeyPatch) -> None:
70
- monkeypatch.delenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION")
69
+ async def test_previous_protocol_refuses_before_io(monkeypatch: pytest.MonkeyPatch) -> None:
70
+ monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", "1.19")
71
71
  async with Client("http://server") as client:
72
72
  with patch.object(client, "_request", new_callable=AsyncMock) as send:
73
73
  with pytest.raises(ValueError, match="1.20"):
@@ -45,13 +45,13 @@ def test_worker_release_identity_matches_supported_server_and_protocol() -> None
45
45
  assert release["product-train"] == project["version"]
46
46
  assert release["registry-version"] == project["version"].replace("-rc.", "rc")
47
47
  assert check_release_metadata.VERSION_PATTERN.fullmatch(release["supported-server-versions"]) is not None
48
- assert release["worker-protocol-version"] == PROTOCOL_VERSION == "1.19"
48
+ assert release["worker-protocol-version"] == PROTOCOL_VERSION == "1.20"
49
49
  assert release["durable-selection"] is True
50
50
  assert release["durable-selection-minimum-worker-protocol-version"] == "1.19"
51
51
 
52
52
  assert declared_runtime_protocol_version(
53
53
  (REPO_ROOT / "src" / "durable_workflow" / "client.py").read_text(encoding="utf-8")
54
- ) == "1.19"
54
+ ) == "1.20"
55
55
 
56
56
 
57
57
  def pypi_json(source: SourceMetadata, **overrides: object) -> bytes:
@@ -1114,6 +1114,39 @@ class TestWorkerRegistration:
1114
1114
  await worker._register()
1115
1115
  mock_client.register_worker.assert_not_called()
1116
1116
 
1117
+ @pytest.mark.asyncio
1118
+ async def test_register_accepts_explicit_previous_protocol(
1119
+ self, mock_client: AsyncMock, monkeypatch: pytest.MonkeyPatch,
1120
+ ) -> None:
1121
+ monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", "1.19")
1122
+ mock_client.get_cluster_info = AsyncMock(
1123
+ return_value=compatible_cluster_info(worker_protocol={"version": "1.19"})
1124
+ )
1125
+ worker = Worker(mock_client, task_queue="q1", workflows=[TestWorkflow], activities=[])
1126
+ await worker._register()
1127
+ mock_client.register_worker.assert_called_once()
1128
+
1129
+ @pytest.mark.asyncio
1130
+ async def test_default_protocol_refuses_previous_server_before_registration(self, mock_client: AsyncMock) -> None:
1131
+ mock_client.get_cluster_info = AsyncMock(
1132
+ return_value=compatible_cluster_info(worker_protocol={"version": "1.19"})
1133
+ )
1134
+ worker = Worker(mock_client, task_queue="q1", workflows=[TestWorkflow], activities=[])
1135
+ with pytest.raises(RuntimeError, match="minor>='1.20'"):
1136
+ await worker._register()
1137
+ mock_client.register_worker.assert_not_called()
1138
+
1139
+ @pytest.mark.asyncio
1140
+ @pytest.mark.parametrize("version", ["1.18", "1.21", "2.0"])
1141
+ async def test_register_rejects_unsupported_protocol_override(
1142
+ self, mock_client: AsyncMock, monkeypatch: pytest.MonkeyPatch, version: str,
1143
+ ) -> None:
1144
+ monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", version)
1145
+ worker = Worker(mock_client, task_queue="q1", workflows=[TestWorkflow], activities=[])
1146
+ with pytest.raises(RuntimeError, match="supports worker protocol 1.19 or protocol 1.20"):
1147
+ await worker._register()
1148
+ mock_client.register_worker.assert_not_called()
1149
+
1117
1150
  @pytest.mark.asyncio
1118
1151
  async def test_register_accepts_higher_compatible_minor_protocol(self, mock_client: AsyncMock) -> None:
1119
1152
  # Server is one minor ahead of the SDK. MINOR bumps in workflow:v2's
@@ -5068,6 +5101,34 @@ class TestRunUntil:
5068
5101
  assert mock_client.describe_workflow.await_count == 2
5069
5102
  assert mock_client.poll_workflow_task.call_args.kwargs["history_page_size"] == 500
5070
5103
 
5104
+ @pytest.mark.asyncio
5105
+ @pytest.mark.parametrize("blocked_method", ["get_cluster_info", "register_worker"])
5106
+ async def test_run_until_identifies_startup_timeout_before_polling(
5107
+ self, mock_client: AsyncMock, blocked_method: str
5108
+ ) -> None:
5109
+ started = asyncio.Event()
5110
+
5111
+ async def blocked(*args: object, **kwargs: object) -> None:
5112
+ started.set()
5113
+ await asyncio.Event().wait()
5114
+
5115
+ setattr(mock_client, blocked_method, AsyncMock(side_effect=blocked))
5116
+ worker = Worker(mock_client, task_queue="q1", workflows=[TestWorkflow])
5117
+
5118
+ with pytest.raises(TimeoutError) as failure:
5119
+ await worker.run_until(workflow_id="wf-startup-timeout", timeout=0.05)
5120
+
5121
+ assert started.is_set()
5122
+ assert "startup" in str(failure.value)
5123
+ assert worker.worker_id in str(failure.value)
5124
+ assert "wf-startup-timeout" in str(failure.value)
5125
+ assert isinstance(failure.value.__cause__, asyncio.TimeoutError)
5126
+ assert worker._stop.is_set()
5127
+ assert worker._registration_done.is_set()
5128
+ mock_client.poll_workflow_task.assert_not_awaited()
5129
+ mock_client.poll_activity_task.assert_not_awaited()
5130
+ mock_client.deregister_worker_registration.assert_not_awaited()
5131
+
5071
5132
  @pytest.mark.asyncio
5072
5133
  async def test_run_until_times_out_and_stops_worker(self, mock_client: AsyncMock) -> None:
5073
5134
  mock_client.describe_workflow = AsyncMock(
@@ -5086,9 +5147,11 @@ class TestRunUntil:
5086
5147
  poll_timeout=0.01,
5087
5148
  )
5088
5149
 
5089
- with pytest.raises(TimeoutError, match="wf-timeout"):
5150
+ with pytest.raises(TimeoutError, match="wf-timeout") as failure:
5090
5151
  await worker.run_until(workflow_id="wf-timeout", timeout=0.02, poll_interval=0.01)
5091
5152
 
5153
+ assert "not terminal" in str(failure.value)
5154
+ assert "startup" not in str(failure.value)
5092
5155
  assert worker._stop.is_set()
5093
5156
 
5094
5157
  @pytest.mark.asyncio