durable-workflow 2.13.0__tar.gz → 2.13.1__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 (173) hide show
  1. {durable_workflow-2.13.0/src/durable_workflow.egg-info → durable_workflow-2.13.1}/PKG-INFO +1 -1
  2. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/pyproject.toml +3 -3
  3. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/workflow.py +13 -3
  4. {durable_workflow-2.13.0 → durable_workflow-2.13.1/src/durable_workflow.egg-info}/PKG-INFO +1 -1
  5. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_cancellation_remaining_time.py +19 -0
  6. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_history_event_contract.py +26 -1
  7. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_replay_clock.py +41 -1
  8. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/LICENSE +0 -0
  9. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/README.md +0 -0
  10. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/setup.cfg +0 -0
  11. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/__init__.py +0 -0
  12. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/_activity_lease.py +0 -0
  13. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/_activity_process.py +0 -0
  14. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/_avro.py +0 -0
  15. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/_cancellation_scope.py +0 -0
  16. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/_cancellation_scope_history.py +0 -0
  17. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/_cooperative_cancellation.py +0 -0
  18. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/_failure_reporting.py +0 -0
  19. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/_metric_routes.py +0 -0
  20. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/_poll_transport.py +0 -0
  21. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/_prepared_local_activity.py +0 -0
  22. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/_query_task_authority.py +0 -0
  23. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
  24. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/activity.py +0 -0
  25. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/auth_composition.py +0 -0
  26. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/cancellation.py +0 -0
  27. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/client.py +0 -0
  28. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/errors.py +0 -0
  29. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/external_storage.py +0 -0
  30. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/external_task_input.py +0 -0
  31. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/external_task_result.py +0 -0
  32. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/history_bundle_verify.py +0 -0
  33. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/interceptors.py +0 -0
  34. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/invocable.py +0 -0
  35. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/metrics.py +0 -0
  36. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/nexus.py +0 -0
  37. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/py.typed +0 -0
  38. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/python_conformance.py +0 -0
  39. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/replay_conformance.py +0 -0
  40. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/replay_verify.py +0 -0
  41. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/retry_policy.py +0 -0
  42. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/serializer.py +0 -0
  43. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/server_compatibility.json +0 -0
  44. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/sync.py +0 -0
  45. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/telemetry.py +0 -0
  46. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/telemetry_carrier.py +0 -0
  47. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/testing.py +0 -0
  48. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/worker.py +0 -0
  49. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/worker_session.py +0 -0
  50. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/worker_status.py +0 -0
  51. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow/workflow_updates_conformance.py +0 -0
  52. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow.egg-info/SOURCES.txt +0 -0
  53. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  54. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  55. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow.egg-info/requires.txt +0 -0
  56. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/src/durable_workflow.egg-info/top_level.txt +0 -0
  57. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_activity_cancellation_policies.py +0 -0
  58. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_activity_context.py +0 -0
  59. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_activity_heartbeat_authority.py +0 -0
  60. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_activity_lease_authority.py +0 -0
  61. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_activity_lease_renewal.py +0 -0
  62. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_activity_lease_worker.py +0 -0
  63. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_activity_process.py +0 -0
  64. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_activity_retry_info.py +0 -0
  65. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_auth_composition.py +0 -0
  66. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_avro_value_benchmark.py +0 -0
  67. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_avro_value_protocol.py +0 -0
  68. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_cancellation_context.py +0 -0
  69. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_cancellation_scope_admission.py +0 -0
  70. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_cancellation_scope_authoring.py +0 -0
  71. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_cancellation_scope_boundary.py +0 -0
  72. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_cancellation_scope_opening.py +0 -0
  73. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_child_workflow_policies.py +0 -0
  74. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_ci_checkout.py +0 -0
  75. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_ci_integration_endpoint.py +0 -0
  76. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_client.py +0 -0
  77. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_client_telemetry.py +0 -0
  78. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_client_timeouts.py +0 -0
  79. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_codec_regression_corpus.py +0 -0
  80. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_committed_cancellation_scope_descendants.py +0 -0
  81. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_committed_cancellation_scope_groups.py +0 -0
  82. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_committed_cancellation_scope_history.py +0 -0
  83. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_committed_cancellation_scope_replay.py +0 -0
  84. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_condition_replay_drift.py +0 -0
  85. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_control_plane_parity_fixtures.py +0 -0
  86. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_cooperative_cancellation.py +0 -0
  87. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_cooperative_cancellation_client.py +0 -0
  88. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_cooperative_cancellation_worker.py +0 -0
  89. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_cooperative_remote_worker.py +0 -0
  90. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_docstring_cross_references.py +0 -0
  91. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_durable_selection.py +0 -0
  92. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_empty_callback_failure.py +0 -0
  93. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_errors.py +0 -0
  94. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_external_storage.py +0 -0
  95. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_external_task_input.py +0 -0
  96. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_external_task_result.py +0 -0
  97. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_failure_reporting.py +0 -0
  98. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_generator_lifecycle.py +0 -0
  99. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_golden_history_replay.py +0 -0
  100. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_history_bundle_pruned.py +0 -0
  101. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_history_bundle_schema2.py +0 -0
  102. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_history_bundle_verify.py +0 -0
  103. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_invocable.py +0 -0
  104. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_local_failure_reporting.py +0 -0
  105. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_message_streams.py +0 -0
  106. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_metric_routes.py +0 -0
  107. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_metrics.py +0 -0
  108. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_nexus_durable_wait.py +0 -0
  109. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_nexus_namespace.py +0 -0
  110. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_nexus_wire.py +0 -0
  111. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_order_processing_example.py +0 -0
  112. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_parallel_completion_order_replay.py +0 -0
  113. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_parallel_saga_authoring.py +0 -0
  114. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_patch_history.py +0 -0
  115. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_pending_operation_replay.py +0 -0
  116. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_poll_failure_backoff.py +0 -0
  117. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_poll_transport.py +0 -0
  118. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_prepared_local_activity_client.py +0 -0
  119. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_prepared_local_activity_groups.py +0 -0
  120. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_prepared_local_activity_policies.py +0 -0
  121. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_prepared_local_activity_worker.py +0 -0
  122. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_public_boundary_scanner.py +0 -0
  123. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_pypi_project_surface.py +0 -0
  124. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_python_conformance.py +0 -0
  125. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_queries.py +0 -0
  126. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_query_task_authority.py +0 -0
  127. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_query_timeout_retry.py +0 -0
  128. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_readme_quickstart.py +0 -0
  129. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_redrive_replay.py +0 -0
  130. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_registration_ambiguity.py +0 -0
  131. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_release_metadata.py +0 -0
  132. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_replay.py +0 -0
  133. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_replay_conformance.py +0 -0
  134. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_replay_regression_corpus.py +0 -0
  135. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_replay_verify.py +0 -0
  136. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_retry_policy.py +0 -0
  137. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_run_cancellation_scope_replay.py +0 -0
  138. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_runtime_external_payload_transport.py +0 -0
  139. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_schedules.py +0 -0
  140. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_scoped_run_cancellation_context.py +0 -0
  141. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_sdk_metrics_contract.py +0 -0
  142. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_serializer.py +0 -0
  143. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_signals.py +0 -0
  144. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_sleep.py +0 -0
  145. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_standalone_activity_client.py +0 -0
  146. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_sticky_execution.py +0 -0
  147. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_storage_admission.py +0 -0
  148. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_sync.py +0 -0
  149. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_sync_activities.py +0 -0
  150. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_telemetry.py +0 -0
  151. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_telemetry_carrier.py +0 -0
  152. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_testing_failures.py +0 -0
  153. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_testing_harness.py +0 -0
  154. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_testing_nexus.py +0 -0
  155. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_threaded_heartbeat_example.py +0 -0
  156. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_tls_trust.py +0 -0
  157. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_transport_diagnostics.py +0 -0
  158. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_update_signal_condition_replay.py +0 -0
  159. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_updates.py +0 -0
  160. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_wait_condition.py +0 -0
  161. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_worker.py +0 -0
  162. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_worker_deregistration.py +0 -0
  163. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_worker_observation.py +0 -0
  164. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_worker_session.py +0 -0
  165. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_worker_status.py +0 -0
  166. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_worker_telemetry_wire.py +0 -0
  167. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_workflow_entry_arguments.py +0 -0
  168. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_workflow_entrypoint_validation.py +0 -0
  169. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_workflow_handler_validation.py +0 -0
  170. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_workflow_identity.py +0 -0
  171. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_workflow_result_timeout.py +0 -0
  172. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/tests/test_workflow_streams.py +0 -0
  173. {durable_workflow-2.13.0 → durable_workflow-2.13.1}/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.13.0
3
+ Version: 2.13.1
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "durable-workflow"
7
- version = "2.13.0"
7
+ version = "2.13.1"
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"
@@ -79,8 +79,8 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main"
79
79
  durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main"
80
80
 
81
81
  [tool.durable-workflow]
82
- product-train = "2.13.0"
83
- registry-version = "2.13.0"
82
+ product-train = "2.13.1"
83
+ registry-version = "2.13.1"
84
84
  supported-server-versions = "2.8.0"
85
85
  supported-server-release-baselines = { "2" = "2.8.0", "3" = "3.0.0" }
86
86
  worker-protocol-version = "1.20"
@@ -2205,9 +2205,15 @@ class WorkflowContext:
2205
2205
  return self._cancellation_context
2206
2206
 
2207
2207
  def _observe_cancellation_replay_time(self, event: Mapping[str, Any] | None) -> None:
2208
- timestamp = event.get("timestamp") if event is not None else None
2209
- if timestamp is None and event is not None:
2210
- timestamp = event.get("recorded_at")
2208
+ payload = event.get("payload") if event is not None else None
2209
+ # A step reused by redrive keeps the time it was first recorded, as in
2210
+ # Core's RedriveReplayClock::recordedAt; it overrides the event's own time.
2211
+ if isinstance(payload, Mapping) and isinstance(payload.get("reused_recorded_at"), str):
2212
+ timestamp: Any = payload["reused_recorded_at"]
2213
+ else:
2214
+ timestamp = event.get("timestamp") if event is not None else None
2215
+ if timestamp is None and event is not None:
2216
+ timestamp = event.get("recorded_at")
2211
2217
  self._cancellation_replay_time_available = False
2212
2218
  if not isinstance(timestamp, str):
2213
2219
  return
@@ -4680,6 +4686,10 @@ def _replay_state(
4680
4686
  explicit_time = "timestamp" in start_payload
4681
4687
  ts = start_payload.get("timestamp") if explicit_time else ev.get("recorded_at")
4682
4688
  clock_source = "payload.timestamp" if explicit_time else "recorded_at"
4689
+ # A redriven run replays from the failed run's start time, as Core does.
4690
+ if start_payload.get("recovery_kind") == "redrive" and "replayed_started_at" in start_payload:
4691
+ ts = start_payload.get("replayed_started_at")
4692
+ clock_source = "payload.replayed_started_at"
4683
4693
  replay_clock_error = f"Workflow replay clock unavailable: invalid or missing WorkflowStarted.{clock_source}"
4684
4694
  if isinstance(ts, str):
4685
4695
  with contextlib.suppress(ValueError):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 2.13.0
3
+ Version: 2.13.1
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
@@ -263,3 +263,22 @@ def test_inline_local_result_persistence_preserves_the_first_execution_budget()
263
263
  ]
264
264
  assert observations == [22.123456, 22.123456]
265
265
  assert calls == ["once"]
266
+
267
+
268
+ def test_a_reused_step_keeps_its_original_time_for_the_remaining_budget() -> None:
269
+ class Cleanup:
270
+ def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
271
+ try:
272
+ yield ctx.start_timer(60)
273
+ except WorkflowCancelled as error:
274
+ assert error.context is not None
275
+ with ctx.cancellation_shield():
276
+ yield ctx.schedule_activity("cleanup", [])
277
+ return error.context.remaining()
278
+
279
+ reused = completed_activity(2, "cleanup", "cleaned")
280
+ reused["payload"] = {**reused["payload"], "reused_from_run_id": "failed-run",
281
+ "reused_recorded_at": "2026-10-01T00:00:12Z"}
282
+ events = history() + [{**reused, "timestamp": "2026-10-01T00:00:29Z"}]
283
+ # Redrive recorded the step again at 00:29; its workflow time stays 00:12.
284
+ assert replay(Cleanup, events, [], run_id="run-1").commands == [CompleteWorkflow(18.123456)]
@@ -674,6 +674,22 @@ DOCUMENTED_HISTORY_EVENT_PAYLOAD_KEYS: dict[str, tuple[str, ...]] = {
674
674
  }
675
675
 
676
676
 
677
+ # Keys Core adds only to runs created by redrive (workflow#864). The SDK reads
678
+ # replayed_started_at for the start clock and reused_recorded_at for step time.
679
+ REDRIVE_HISTORY_EVENT_PAYLOAD_KEYS = {
680
+ "WorkflowStarted": ("resume_step_sequence", "recovery_kind", "replayed_started_at"),
681
+ "ActivityCompleted": ("reused_from_run_id", "reused_activity_execution_id", "reused_recorded_at"),
682
+ "TimerFired": ("reused_from_run_id", "reused_recorded_at"),
683
+ "SideEffectRecorded": ("reused_from_run_id", "reused_recorded_at"),
684
+ "VersionMarkerRecorded": ("reused_from_run_id", "reused_recorded_at"),
685
+ }
686
+
687
+ for _event_type, _keys in REDRIVE_HISTORY_EVENT_PAYLOAD_KEYS.items():
688
+ DOCUMENTED_HISTORY_EVENT_PAYLOAD_KEYS[_event_type] = (
689
+ *DOCUMENTED_HISTORY_EVENT_PAYLOAD_KEYS[_event_type],
690
+ *(key for key in _keys if key not in DOCUMENTED_HISTORY_EVENT_PAYLOAD_KEYS[_event_type]),
691
+ )
692
+
677
693
  SDK_INTERPRETED_HISTORY_EVENTS = frozenset(
678
694
  {
679
695
  "WorkflowStarted",
@@ -732,9 +748,18 @@ def test_schedule_history_events_are_preserved_only() -> None:
732
748
 
733
749
  def test_php_history_fixture_key_expectations_match_documented_contract() -> None:
734
750
  for event_type, fixture_keys in PHP_HISTORY_EVENT_EXPECTED_KEYS.items():
735
- documented_keys = set(DOCUMENTED_HISTORY_EVENT_PAYLOAD_KEYS[event_type])
751
+ # Published PHP fixtures come from ordinary runs, which never carry redrive keys.
752
+ documented_keys = set(DOCUMENTED_HISTORY_EVENT_PAYLOAD_KEYS[event_type]) - set(
753
+ REDRIVE_HISTORY_EVENT_PAYLOAD_KEYS.get(event_type, ())
754
+ )
736
755
  expected_keys = set(fixture_keys)
737
756
  tolerated_extra_keys = set(SDK_TOLERATED_FIXTURE_EXTRA_KEYS.get(event_type, frozenset()))
738
757
 
739
758
  assert expected_keys - documented_keys == tolerated_extra_keys, event_type
740
759
  assert documented_keys - expected_keys == set(), event_type
760
+
761
+
762
+ def test_redrive_payload_keys_are_documented_on_the_interpreted_events() -> None:
763
+ for event_type, keys in REDRIVE_HISTORY_EVENT_PAYLOAD_KEYS.items():
764
+ assert event_type in SDK_INTERPRETED_HISTORY_EVENTS, event_type
765
+ assert set(keys) <= set(DOCUMENTED_HISTORY_EVENT_PAYLOAD_KEYS[event_type]), event_type
@@ -7,7 +7,7 @@ from uuid import UUID
7
7
 
8
8
  import pytest
9
9
 
10
- from durable_workflow import workflow
10
+ from durable_workflow import serializer, workflow
11
11
  from durable_workflow.errors import NonDeterministicReplayError
12
12
  from durable_workflow.workflow import CompleteWorkflow, WorkflowContext, replay
13
13
 
@@ -91,3 +91,43 @@ def test_missing_replay_time_is_explicit(history: list[dict[str, Any]]) -> None:
91
91
  def test_empty_local_authoring_remains_available() -> None:
92
92
  assert datetime.fromisoformat(result(PersistedClockWorkflow, [])).tzinfo is not None
93
93
  assert UUID(result(PersistedUUID7Workflow, [])).version == 7
94
+
95
+
96
+ @workflow.defn(name="tests.replay.redrive-clock")
97
+ class RedriveClockWorkflow:
98
+ def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
99
+ before = ctx.now().isoformat(timespec="microseconds")
100
+ token = str(ctx.uuid7())
101
+ recorded = yield ctx.schedule_activity("tests.first", [before])
102
+ return [before, token, recorded]
103
+
104
+
105
+ def redriven_history(**start_payload: Any) -> list[dict[str, Any]]:
106
+ return [
107
+ {"event_type": "WorkflowStarted", "recorded_at": "2026-06-01T10:05:00.000000Z", "payload": start_payload},
108
+ {"event_type": "ActivityCompleted", "recorded_at": "2026-06-01T10:05:00.500000Z",
109
+ "payload": {"sequence": 1, "activity_type": "tests.first",
110
+ "result": serializer.encode("recorded", codec="avro"), "payload_codec": "avro",
111
+ "reused_from_run_id": "failed-run",
112
+ "reused_recorded_at": "2026-06-01T10:00:01.000000Z"}},
113
+ ]
114
+
115
+
116
+ def test_redriven_run_replays_from_the_failed_run_start_time() -> None:
117
+ history = redriven_history(recovery_kind="redrive", replayed_started_at="2026-06-01T10:00:00.000000Z")
118
+ before, token, recorded = result(RedriveClockWorkflow, history)
119
+ assert before == "2026-06-01T10:00:00.000000+00:00"
120
+ assert UUID(token).int >> 80 == int(datetime.fromisoformat(before).timestamp() * 1000)
121
+ assert recorded == "recorded"
122
+
123
+
124
+ def test_replayed_start_time_requires_a_redrive_start() -> None:
125
+ history = redriven_history(replayed_started_at="2026-06-01T10:00:00.000000Z")
126
+ assert result(RedriveClockWorkflow, history)[0] == "2026-06-01T10:05:00.000000+00:00"
127
+
128
+
129
+ @pytest.mark.parametrize("invalid", [None, 123, "", "not-a-time", "2026-06-01T10:00:00"])
130
+ def test_invalid_replayed_start_time_is_refused_without_fallback(invalid: Any) -> None:
131
+ history = redriven_history(recovery_kind="redrive", replayed_started_at=invalid)
132
+ with pytest.raises(NonDeterministicReplayError, match="replayed_started_at"):
133
+ replay(RedriveClockWorkflow, history, [], run_id="redriven-run", payload_codec="avro")