durable-workflow 2.12.0__tar.gz → 2.12.2__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 (168) hide show
  1. {durable_workflow-2.12.0/src/durable_workflow.egg-info → durable_workflow-2.12.2}/PKG-INFO +13 -1
  2. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/README.md +9 -0
  3. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/pyproject.toml +6 -3
  4. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/workflow.py +222 -38
  5. {durable_workflow-2.12.0 → durable_workflow-2.12.2/src/durable_workflow.egg-info}/PKG-INFO +13 -1
  6. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow.egg-info/SOURCES.txt +1 -0
  7. durable_workflow-2.12.2/tests/test_generator_lifecycle.py +401 -0
  8. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_replay_regression_corpus.py +26 -7
  9. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_wait_condition.py +41 -1
  10. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/LICENSE +0 -0
  11. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/setup.cfg +0 -0
  12. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/__init__.py +0 -0
  13. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_activity_lease.py +0 -0
  14. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_activity_process.py +0 -0
  15. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_avro.py +0 -0
  16. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_cancellation_scope.py +0 -0
  17. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_cancellation_scope_history.py +0 -0
  18. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_cooperative_cancellation.py +0 -0
  19. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_failure_reporting.py +0 -0
  20. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_metric_routes.py +0 -0
  21. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_poll_transport.py +0 -0
  22. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_prepared_local_activity.py +0 -0
  23. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_query_task_authority.py +0 -0
  24. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
  25. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/activity.py +0 -0
  26. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/auth_composition.py +0 -0
  27. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/cancellation.py +0 -0
  28. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/client.py +0 -0
  29. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/errors.py +0 -0
  30. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/external_storage.py +0 -0
  31. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/external_task_input.py +0 -0
  32. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/external_task_result.py +0 -0
  33. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/history_bundle_verify.py +0 -0
  34. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/interceptors.py +0 -0
  35. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/invocable.py +0 -0
  36. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/metrics.py +0 -0
  37. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/nexus.py +0 -0
  38. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/py.typed +0 -0
  39. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/python_conformance.py +0 -0
  40. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/replay_conformance.py +0 -0
  41. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/replay_verify.py +0 -0
  42. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/retry_policy.py +0 -0
  43. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/serializer.py +0 -0
  44. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/sync.py +0 -0
  45. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/telemetry.py +0 -0
  46. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/telemetry_carrier.py +0 -0
  47. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/testing.py +0 -0
  48. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/worker.py +0 -0
  49. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/worker_session.py +0 -0
  50. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/workflow_updates_conformance.py +0 -0
  51. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  52. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  53. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow.egg-info/requires.txt +0 -0
  54. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow.egg-info/top_level.txt +0 -0
  55. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_activity_cancellation_policies.py +0 -0
  56. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_activity_context.py +0 -0
  57. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_activity_heartbeat_authority.py +0 -0
  58. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_activity_lease_authority.py +0 -0
  59. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_activity_lease_renewal.py +0 -0
  60. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_activity_lease_worker.py +0 -0
  61. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_activity_process.py +0 -0
  62. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_activity_retry_info.py +0 -0
  63. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_auth_composition.py +0 -0
  64. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_avro_value_benchmark.py +0 -0
  65. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_avro_value_protocol.py +0 -0
  66. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_cancellation_context.py +0 -0
  67. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_cancellation_remaining_time.py +0 -0
  68. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_cancellation_scope_admission.py +0 -0
  69. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_cancellation_scope_authoring.py +0 -0
  70. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_cancellation_scope_boundary.py +0 -0
  71. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_cancellation_scope_opening.py +0 -0
  72. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_child_workflow_policies.py +0 -0
  73. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_ci_checkout.py +0 -0
  74. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_ci_integration_endpoint.py +0 -0
  75. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_client.py +0 -0
  76. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_client_telemetry.py +0 -0
  77. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_client_timeouts.py +0 -0
  78. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_codec_regression_corpus.py +0 -0
  79. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_committed_cancellation_scope_descendants.py +0 -0
  80. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_committed_cancellation_scope_groups.py +0 -0
  81. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_committed_cancellation_scope_history.py +0 -0
  82. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_committed_cancellation_scope_replay.py +0 -0
  83. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_condition_replay_drift.py +0 -0
  84. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_control_plane_parity_fixtures.py +0 -0
  85. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_cooperative_cancellation.py +0 -0
  86. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_cooperative_cancellation_client.py +0 -0
  87. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_cooperative_cancellation_worker.py +0 -0
  88. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_cooperative_remote_worker.py +0 -0
  89. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_docstring_cross_references.py +0 -0
  90. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_durable_selection.py +0 -0
  91. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_errors.py +0 -0
  92. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_external_storage.py +0 -0
  93. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_external_task_input.py +0 -0
  94. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_external_task_result.py +0 -0
  95. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_failure_reporting.py +0 -0
  96. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_golden_history_replay.py +0 -0
  97. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_history_bundle_schema2.py +0 -0
  98. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_history_bundle_verify.py +0 -0
  99. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_history_event_contract.py +0 -0
  100. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_invocable.py +0 -0
  101. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_local_failure_reporting.py +0 -0
  102. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_message_streams.py +0 -0
  103. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_metric_routes.py +0 -0
  104. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_metrics.py +0 -0
  105. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_nexus_durable_wait.py +0 -0
  106. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_nexus_namespace.py +0 -0
  107. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_nexus_wire.py +0 -0
  108. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_order_processing_example.py +0 -0
  109. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_parallel_completion_order_replay.py +0 -0
  110. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_parallel_saga_authoring.py +0 -0
  111. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_patch_history.py +0 -0
  112. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_pending_operation_replay.py +0 -0
  113. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_poll_failure_backoff.py +0 -0
  114. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_poll_transport.py +0 -0
  115. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_prepared_local_activity_client.py +0 -0
  116. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_prepared_local_activity_groups.py +0 -0
  117. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_prepared_local_activity_policies.py +0 -0
  118. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_prepared_local_activity_worker.py +0 -0
  119. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_public_boundary_scanner.py +0 -0
  120. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_pypi_project_surface.py +0 -0
  121. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_python_conformance.py +0 -0
  122. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_queries.py +0 -0
  123. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_query_task_authority.py +0 -0
  124. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_query_timeout_retry.py +0 -0
  125. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_readme_quickstart.py +0 -0
  126. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_redrive_replay.py +0 -0
  127. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_registration_ambiguity.py +0 -0
  128. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_release_metadata.py +0 -0
  129. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_replay.py +0 -0
  130. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_replay_clock.py +0 -0
  131. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_replay_conformance.py +0 -0
  132. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_replay_verify.py +0 -0
  133. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_retry_policy.py +0 -0
  134. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_run_cancellation_scope_replay.py +0 -0
  135. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_runtime_external_payload_transport.py +0 -0
  136. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_schedules.py +0 -0
  137. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_scoped_run_cancellation_context.py +0 -0
  138. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_sdk_metrics_contract.py +0 -0
  139. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_serializer.py +0 -0
  140. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_signals.py +0 -0
  141. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_sleep.py +0 -0
  142. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_standalone_activity_client.py +0 -0
  143. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_sticky_execution.py +0 -0
  144. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_storage_admission.py +0 -0
  145. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_sync.py +0 -0
  146. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_sync_activities.py +0 -0
  147. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_telemetry.py +0 -0
  148. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_telemetry_carrier.py +0 -0
  149. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_testing_failures.py +0 -0
  150. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_testing_harness.py +0 -0
  151. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_testing_nexus.py +0 -0
  152. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_threaded_heartbeat_example.py +0 -0
  153. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_tls_trust.py +0 -0
  154. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_transport_diagnostics.py +0 -0
  155. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_update_signal_condition_replay.py +0 -0
  156. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_updates.py +0 -0
  157. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_worker.py +0 -0
  158. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_worker_deregistration.py +0 -0
  159. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_worker_observation.py +0 -0
  160. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_worker_session.py +0 -0
  161. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_worker_telemetry_wire.py +0 -0
  162. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_workflow_entry_arguments.py +0 -0
  163. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_workflow_entrypoint_validation.py +0 -0
  164. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_workflow_handler_validation.py +0 -0
  165. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_workflow_identity.py +0 -0
  166. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_workflow_result_timeout.py +0 -0
  167. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_workflow_streams.py +0 -0
  168. {durable_workflow-2.12.0 → durable_workflow-2.12.2}/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.12.0
3
+ Version: 2.12.2
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
@@ -14,6 +14,9 @@ Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Programming Language :: Python :: 3.10
15
15
  Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Programming Language :: Python :: 3.15
17
20
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
21
  Classifier: Typing :: Typed
19
22
  Requires-Python: >=3.10
@@ -186,6 +189,15 @@ independently of application heartbeats. Existing terminal cancellation remains
186
189
  available. See the [cancellation guide](docs/cooperative-cancellation-design.md)
187
190
  for immutable context, operation policies, shielded cleanup and recovery.
188
191
 
192
+ A suspended replay is not a workflow exit. Python may run `finally` blocks and
193
+ context-manager exits when disposing of that replay. The SDK discards commands
194
+ and `ctx.logger` messages produced only by disposal; queries observe the state
195
+ at the pending command. Actual completion, failure and cooperative cancellation
196
+ run cleanup through the normal history, and cold replay uses its recorded
197
+ results. Put external cleanup in an Activity rather than direct I/O in a
198
+ finalizer, and use `ctx.cancellation_shield()` when that Activity must finish
199
+ after cancellation. The disposal guarantees require SDK 2.12.2 or later.
200
+
189
201
  Both `await activity.context().heartbeat()` and synchronous
190
202
  `activity.context().heartbeat_sync()` stop at an explicit Server refusal.
191
203
  They raise `ActivityLeaseAuthorityLost` with its original `.reason` for an
@@ -142,6 +142,15 @@ independently of application heartbeats. Existing terminal cancellation remains
142
142
  available. See the [cancellation guide](docs/cooperative-cancellation-design.md)
143
143
  for immutable context, operation policies, shielded cleanup and recovery.
144
144
 
145
+ A suspended replay is not a workflow exit. Python may run `finally` blocks and
146
+ context-manager exits when disposing of that replay. The SDK discards commands
147
+ and `ctx.logger` messages produced only by disposal; queries observe the state
148
+ at the pending command. Actual completion, failure and cooperative cancellation
149
+ run cleanup through the normal history, and cold replay uses its recorded
150
+ results. Put external cleanup in an Activity rather than direct I/O in a
151
+ finalizer, and use `ctx.cancellation_shield()` when that Activity must finish
152
+ after cancellation. The disposal guarantees require SDK 2.12.2 or later.
153
+
145
154
  Both `await activity.context().heartbeat()` and synchronous
146
155
  `activity.context().heartbeat_sync()` stop at an explicit Server refusal.
147
156
  They raise `ActivityLeaseAuthorityLost` with its original `.reason` for an
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "durable-workflow"
7
- version = "2.12.0"
7
+ version = "2.12.2"
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"
@@ -29,6 +29,9 @@ classifiers = [
29
29
  "Programming Language :: Python :: 3.10",
30
30
  "Programming Language :: Python :: 3.11",
31
31
  "Programming Language :: Python :: 3.12",
32
+ "Programming Language :: Python :: 3.13",
33
+ "Programming Language :: Python :: 3.14",
34
+ "Programming Language :: Python :: 3.15",
32
35
  "Topic :: Software Development :: Libraries :: Python Modules",
33
36
  "Typing :: Typed",
34
37
  ]
@@ -76,8 +79,8 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main"
76
79
  durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main"
77
80
 
78
81
  [tool.durable-workflow]
79
- product-train = "2.12.0"
80
- registry-version = "2.12.0"
82
+ product-train = "2.12.2"
83
+ registry-version = "2.12.2"
81
84
  supported-server-versions = "2.8.0"
82
85
  worker-protocol-version = "1.20"
83
86
  control-plane-version = "2"
@@ -30,8 +30,8 @@ import uuid
30
30
  import weakref
31
31
  from collections.abc import Callable, Generator, Iterable, Mapping, Sequence
32
32
  from contextvars import ContextVar
33
- from copy import copy
34
- from dataclasses import dataclass, field
33
+ from copy import copy, deepcopy
34
+ from dataclasses import dataclass, field, replace
35
35
  from datetime import datetime, timezone
36
36
  from typing import Any, TypeVar, cast
37
37
 
@@ -2682,6 +2682,66 @@ class WorkflowContext:
2682
2682
 
2683
2683
 
2684
2684
  # ── Replay ───────────────────────────────────────────────────────────
2685
+ _ReplayCommandT = TypeVar("_ReplayCommandT", bound=Command)
2686
+
2687
+
2688
+ def _snapshot_command(command: _ReplayCommandT) -> _ReplayCommandT:
2689
+ """Detach payload containers while preserving predicates and group metadata."""
2690
+ snapshot = copy(command)
2691
+ for name, value in vars(command).items():
2692
+ if isinstance(value, dict | list | tuple | set | bytearray):
2693
+ object.__setattr__(snapshot, name, deepcopy(value))
2694
+ return snapshot
2695
+
2696
+
2697
+ def _snapshot_local_call(call: PreparedLocalActivityCall) -> PreparedLocalActivityCall:
2698
+ return replace(call, command=_snapshot_command(call.command), cleanup=deepcopy(call.cleanup))
2699
+
2700
+
2701
+ class _ReplayLifetime:
2702
+ """Own one suspended replay until its result or handler is consumed."""
2703
+
2704
+ def __init__(self, logger: _ReplayLogger) -> None:
2705
+ self.logger = logger
2706
+ self.generator: Any = None
2707
+ self.awaitable: Any = None
2708
+
2709
+ def close(self) -> None:
2710
+ self.logger._set_replaying(True)
2711
+ awaitable, self.awaitable = self.awaitable, None
2712
+ if inspect.iscoroutine(awaitable):
2713
+ awaitable.close()
2714
+ generator, self.generator = self.generator, None
2715
+ if generator is None:
2716
+ return
2717
+
2718
+ # Keep delegated generators alive while closing from the inside out.
2719
+ # A finally block may yield during GeneratorExit; inject exit again
2720
+ # instead of advancing or submitting that discarded durable command.
2721
+ pending = [generator]
2722
+ finished: list[Any] = []
2723
+ while pending:
2724
+ current = pending[-1]
2725
+ child = getattr(current, "gi_yieldfrom", None)
2726
+ if child is not None and callable(getattr(child, "close", None)) and not any(
2727
+ child is closed for closed in finished
2728
+ ):
2729
+ pending.append(child)
2730
+ continue
2731
+ close = getattr(current, "close", None)
2732
+ if close is not None:
2733
+ # Discard errors are not a logical workflow failure. Actual
2734
+ # success/failure/cancellation still runs through the driver.
2735
+ with contextlib.suppress(BaseException):
2736
+ close()
2737
+ if not inspect.isgenerator(current) or current.gi_frame is None:
2738
+ finished.append(pending.pop())
2739
+
2740
+ def __del__(self) -> None:
2741
+ with contextlib.suppress(BaseException):
2742
+ self.close()
2743
+
2744
+
2685
2745
  @dataclass
2686
2746
  class ReplayOutcome:
2687
2747
  commands: list[Command]
@@ -2693,6 +2753,19 @@ class ReplayOutcome:
2693
2753
  cancellation_scope_opening: CancellationScopeOpening | None = None
2694
2754
  cancellation_scope_delivery: CancellationScopeDeliveryIntent | None = None
2695
2755
 
2756
+ def __post_init__(self) -> None:
2757
+ # This ownership is not a dataclass/wire field.
2758
+ self._lifetime: _ReplayLifetime | None = None
2759
+
2760
+ def close(self) -> None:
2761
+ lifetime, self._lifetime = self._lifetime, None
2762
+ if lifetime is not None:
2763
+ lifetime.close()
2764
+
2765
+ def __del__(self) -> None:
2766
+ with contextlib.suppress(BaseException):
2767
+ self.close()
2768
+
2696
2769
 
2697
2770
  @dataclass(frozen=True)
2698
2771
  class _ServiceCallDelivery:
@@ -2815,6 +2888,25 @@ class _ReplayState:
2815
2888
  instance: Any
2816
2889
 
2817
2890
 
2891
+ def _consume_replay_result(state: _ReplayState, result: Any) -> Any:
2892
+ """Detach a handler value before discarded replay cleanup can mutate it."""
2893
+ if inspect.isawaitable(result):
2894
+ if state.outcome._lifetime is not None:
2895
+ state.outcome._lifetime.awaitable = result
2896
+
2897
+ async def consume() -> Any:
2898
+ try:
2899
+ return deepcopy(await result)
2900
+ finally:
2901
+ state.outcome.close()
2902
+
2903
+ return consume()
2904
+ try:
2905
+ return deepcopy(result)
2906
+ finally:
2907
+ state.outcome.close()
2908
+
2909
+
2818
2910
  @dataclass
2819
2911
  class _PendingReceiver:
2820
2912
  history_index: int
@@ -3160,6 +3252,7 @@ def _prepare_query_state(
3160
3252
  workflow_cls,
3161
3253
  history_events,
3162
3254
  start_input,
3255
+ reconstruction_only=True,
3163
3256
  workflow_id=workflow_id,
3164
3257
  run_id=run_id,
3165
3258
  payload_codec=payload_codec,
@@ -3174,26 +3267,35 @@ def _prepare_query_state(
3174
3267
  raise QueryFailed(f"workflow replay failed before query: {exc}") from exc
3175
3268
  if state.outcome.commands and isinstance(state.outcome.commands[0], FailWorkflow):
3176
3269
  failure = state.outcome.commands[0]
3270
+ state.outcome.close()
3177
3271
  raise QueryFailed(f"workflow replay failed before query: {failure.message}") from None
3178
3272
 
3179
3273
  query_registry: dict[str, str] = getattr(workflow_cls, "__workflow_queries__", {}) or {}
3180
3274
  method_name = query_registry.get(query_name)
3181
3275
  if method_name is None:
3276
+ state.outcome.close()
3182
3277
  raise QueryFailed(f"unknown query {query_name!r}")
3183
3278
 
3184
3279
  handler = getattr(state.instance, method_name, None)
3185
3280
  if handler is None:
3281
+ state.outcome.close()
3186
3282
  raise QueryFailed(f"query handler {query_name!r} is not available")
3187
3283
 
3188
3284
  def invoke() -> Any:
3189
3285
  try:
3190
- return _invoke_workflow_handler(
3286
+ result = _invoke_workflow_handler(
3191
3287
  handler, list(args) if args is not None else [], workflow_cls, method_name, allow_async=True,
3192
3288
  )
3289
+ return _consume_replay_result(state, result)
3193
3290
  except QueryFailed:
3291
+ state.outcome.close()
3194
3292
  raise
3195
3293
  except Exception as exc:
3294
+ state.outcome.close()
3196
3295
  raise QueryFailed(str(exc) or f"query {query_name!r} failed") from exc
3296
+ except BaseException:
3297
+ state.outcome.close()
3298
+ raise
3197
3299
 
3198
3300
  return invoke
3199
3301
 
@@ -3263,6 +3365,7 @@ def apply_update(
3263
3365
  workflow_cls,
3264
3366
  events,
3265
3367
  start_input,
3368
+ reconstruction_only=True,
3266
3369
  workflow_id=workflow_id,
3267
3370
  run_id=run_id,
3268
3371
  payload_codec=payload_codec,
@@ -3337,10 +3440,14 @@ def apply_update(
3337
3440
  )
3338
3441
 
3339
3442
  try:
3443
+ if state.outcome._lifetime is not None:
3444
+ state.outcome._lifetime.logger._set_replaying(False)
3340
3445
  result = _invoke_workflow_handler(handler, args, workflow_cls, method_name)
3341
- return CompleteUpdate(update_id=update_id, result=result)
3446
+ return CompleteUpdate(update_id=update_id, result=deepcopy(result))
3342
3447
  except Exception as exc:
3343
3448
  return _fail_update_from_exception(update_id, "update handler failed", exc)
3449
+ finally:
3450
+ state.outcome.close()
3344
3451
 
3345
3452
 
3346
3453
  @dataclass(frozen=True)
@@ -3414,6 +3521,7 @@ def _prepare_update_validation(
3414
3521
  workflow_cls,
3415
3522
  events,
3416
3523
  start_input,
3524
+ reconstruction_only=True,
3417
3525
  workflow_id=replay_options.workflow_id,
3418
3526
  run_id=replay_options.run_id,
3419
3527
  payload_codec=replay_options.payload_codec,
@@ -3430,10 +3538,12 @@ def _prepare_update_validation(
3430
3538
 
3431
3539
  if state.outcome.commands and isinstance(state.outcome.commands[0], FailWorkflow):
3432
3540
  failure = state.outcome.commands[0]
3541
+ state.outcome.close()
3433
3542
  raise UpdateValidationFailed(f"workflow replay failed before update validation: {failure.message}")
3434
3543
 
3435
3544
  update_registry: dict[str, str] = getattr(workflow_cls, "__workflow_updates__", {}) or {}
3436
3545
  if update_name not in update_registry:
3546
+ state.outcome.close()
3437
3547
  raise UpdateValidationFailed(f"unknown update {update_name!r}")
3438
3548
 
3439
3549
  validator_registry: dict[str, str] = (
@@ -3446,13 +3556,23 @@ def _prepare_update_validation(
3446
3556
  )
3447
3557
  validator_name = validator_registry.get(update_name)
3448
3558
  if validator_name is None:
3559
+ state.outcome.close()
3449
3560
  raise UpdateValidationFailed(f"update validator {update_name!r} is not declared")
3450
3561
 
3451
3562
  validator = getattr(state.instance, validator_name, None)
3452
3563
  if validator is None:
3564
+ state.outcome.close()
3453
3565
  raise UpdateValidationFailed(f"update validator {update_name!r} is not available")
3454
3566
 
3455
- return lambda: _invoke_update_validator(validator, args, workflow_cls, validator_name, update_name)
3567
+ def invoke() -> Any:
3568
+ try:
3569
+ result = _invoke_update_validator(validator, args, workflow_cls, validator_name, update_name)
3570
+ return _consume_replay_result(state, result)
3571
+ except BaseException:
3572
+ state.outcome.close()
3573
+ raise
3574
+
3575
+ return invoke
3456
3576
 
3457
3577
 
3458
3578
  def _invoke_update_validator(
@@ -4351,6 +4471,7 @@ def _replay_state(
4351
4471
  prepare_local_activity_groups: bool = False,
4352
4472
  local_activity_cancellation_policies: tuple[str, ...] = (),
4353
4473
  stop_at_uncommitted_cancellation: bool = False,
4474
+ reconstruction_only: bool = False,
4354
4475
  allow_cancellation_scope_authoring: bool = False,
4355
4476
  allow_cancellation_scope_delivery: bool = False,
4356
4477
  ) -> _ReplayState:
@@ -4582,6 +4703,11 @@ def _replay_state(
4582
4703
  _allow_wall_clock=not events,
4583
4704
  )
4584
4705
  ctx._replay_clock_error = replay_clock_error if workflow_start_time is None else None
4706
+ lifetime = _ReplayLifetime(ctx.logger)
4707
+
4708
+ def _set_replaying(replaying: bool) -> None:
4709
+ ctx.logger._set_replaying(replaying or reconstruction_only)
4710
+
4585
4711
  # Continue-as-new cursor checkpoints describe instance state that predates
4586
4712
  # this run. Seed them before the workflow opens its first stream wait so
4587
4713
  # both the durable wait key and worker acknowledgement use the global
@@ -4614,25 +4740,32 @@ def _replay_state(
4614
4740
  prepared_local_activity_group: PreparedLocalActivityGroup | None = None,
4615
4741
  cancellation_scope_opening: CancellationScopeOpening | None = None,
4616
4742
  ) -> _ReplayState:
4617
- return _ReplayState(
4618
- outcome=ReplayOutcome(
4619
- commands=commands,
4620
- cancellation_delivery=cancellation_intent,
4621
- prepared_local_activity=prepared_local_activity,
4622
- prepared_local_activity_group=prepared_local_activity_group,
4623
- cancellation_scope_opening=cancellation_scope_opening,
4624
- cancellation_scope_delivery=scope_intent,
4625
- message_stream_cursors=[
4626
- {"stream_name": name, "through_position": position}
4627
- for name, position in sorted(ctx._message_stream_cursors.items())
4628
- ],
4629
- message_stream_waits=[
4630
- {"stream_name": name, "after_position": position}
4631
- for name, position in sorted(ctx._message_stream_waits.items())
4632
- ],
4633
- ),
4634
- instance=instance,
4743
+ if prepared_local_activity is not None:
4744
+ prepared_local_activity = _snapshot_local_call(prepared_local_activity)
4745
+ if prepared_local_activity_group is not None:
4746
+ prepared_local_activity_group = replace(
4747
+ prepared_local_activity_group,
4748
+ commands=tuple(_snapshot_command(command) for command in prepared_local_activity_group.commands),
4749
+ calls=tuple(_snapshot_local_call(call) for call in prepared_local_activity_group.calls),
4750
+ )
4751
+ outcome = ReplayOutcome(
4752
+ commands=[_snapshot_command(command) for command in commands],
4753
+ cancellation_delivery=cancellation_intent,
4754
+ prepared_local_activity=prepared_local_activity,
4755
+ prepared_local_activity_group=prepared_local_activity_group,
4756
+ cancellation_scope_opening=cancellation_scope_opening,
4757
+ cancellation_scope_delivery=scope_intent,
4758
+ message_stream_cursors=[
4759
+ {"stream_name": name, "through_position": position}
4760
+ for name, position in sorted(ctx._message_stream_cursors.items())
4761
+ ],
4762
+ message_stream_waits=[
4763
+ {"stream_name": name, "after_position": position}
4764
+ for name, position in sorted(ctx._message_stream_waits.items())
4765
+ ],
4635
4766
  )
4767
+ outcome._lifetime = lifetime
4768
+ return _ReplayState(outcome=outcome, instance=instance)
4636
4769
 
4637
4770
  resolved_results: list[Any] = []
4638
4771
  recorded_steps: list[_RecordedStep] = []
@@ -4661,6 +4794,7 @@ def _replay_state(
4661
4794
  # Receiver replay uses this exact boundary so interleaved input is applied
4662
4795
  # in durable order without leaking input recorded after the terminal row.
4663
4796
  condition_wait_terminal_indexes: dict[str, int] = {}
4797
+ condition_wait_opened_indexes: dict[str, int] = {}
4664
4798
  # Selection members are bound by their durable sequence instead of being
4665
4799
  # inserted into the ordinary completion-order cursor. This is what lets a
4666
4800
  # loser complete before or after unrelated successor work without changing
@@ -5006,6 +5140,7 @@ def _replay_state(
5006
5140
  wait_id = payload.get("condition_wait_id")
5007
5141
  if not isinstance(wait_id, str) or not wait_id:
5008
5142
  continue
5143
+ condition_wait_opened_indexes.setdefault(wait_id, index)
5009
5144
 
5010
5145
  if current_wait_id is None:
5011
5146
  for receiver_index in prefix_receivers:
@@ -5070,7 +5205,14 @@ def _replay_state(
5070
5205
 
5071
5206
  for receiver_index in receivers_since_wait:
5072
5207
  if receiver_index is not None and receiver_index not in bindings:
5073
- bindings[receiver_index] = current_wait_id
5208
+ if boundary_kind == "condition":
5209
+ # A terminal row can precede the next physical reopen.
5210
+ # Later inputs admitted during the first receiver's
5211
+ # leased replay belong to that reopen, not the wait
5212
+ # already resolved by the first receiver.
5213
+ prefix_receivers.append(receiver_index)
5214
+ else:
5215
+ bindings[receiver_index] = current_wait_id
5074
5216
  current_wait_id = None
5075
5217
  receivers_since_wait = []
5076
5218
  prefix_can_bind_to_first_wait = boundary_kind == "condition"
@@ -5400,7 +5542,7 @@ def _replay_state(
5400
5542
  if receiver.kind == "signal":
5401
5543
  return
5402
5544
  raise TypeError(f"update handler {receiver.name!r} is not available")
5403
- ctx.logger._set_replaying(True)
5545
+ _set_replaying(True)
5404
5546
  _invoke_workflow_handler(handler, receiver.args, workflow_cls, method_name)
5405
5547
 
5406
5548
  def _receiver_due(receiver: _PendingReceiver, *, before_consuming_result: bool) -> bool:
@@ -5431,6 +5573,11 @@ def _replay_state(
5431
5573
  terminal_index = condition_wait_terminal_indexes.get(condition_wait_id)
5432
5574
  if terminal_index is not None:
5433
5575
  while pending_receivers and pending_receivers[0].history_index < terminal_index:
5576
+ receiver_wait_id = pending_receivers[0].condition_wait_id
5577
+ receiver_wait_index = (condition_wait_opened_indexes.get(receiver_wait_id)
5578
+ if receiver_wait_id is not None else None)
5579
+ if receiver_wait_index is not None and receiver_wait_index > terminal_index:
5580
+ break
5434
5581
  _apply_receiver(pending_receivers.pop(0))
5435
5582
  return
5436
5583
  target_index = next(
@@ -5503,6 +5650,25 @@ def _replay_state(
5503
5650
  result_cursor = 0
5504
5651
  pending_step_cursor = 0
5505
5652
  wait_yield_count = 0
5653
+ has_terminal_history = any(_history_event_type(event) in {
5654
+ "WorkflowCompleted", "WorkflowFailed", "WorkflowCancelled", "WorkflowTerminated",
5655
+ "WorkflowContinuedAsNew", "WorkflowTimedOut",
5656
+ } for event in events)
5657
+
5658
+ def _refresh_replay_logging() -> None:
5659
+ # The resumed Python code is live before it yields its next command.
5660
+ # Read-only reconstruction and discarded-generator disposal stay quiet.
5661
+ _set_replaying(
5662
+ has_terminal_history
5663
+ or result_cursor < len(recorded_steps)
5664
+ or wait_yield_count < len(recorded_wait_steps)
5665
+ or pending_step_cursor < len(recorded_pending_steps)
5666
+ or selection_marker_cursor < len(selection_markers)
5667
+ or bool(pending_receivers)
5668
+ or (cancellation.delivery is not None and not cancellation_consumed)
5669
+ or len(consumed_scope_deliveries) < len(committed_scopes.deliveries)
5670
+ )
5671
+
5506
5672
  entry = instance.run
5507
5673
  workflow_type = str(getattr(workflow_cls, "__workflow_name__", workflow_cls.__name__))
5508
5674
  try:
@@ -5517,6 +5683,7 @@ def _replay_state(
5517
5683
  # Binding happens outside the application-error path. A TypeError raised
5518
5684
  # by a successfully bound body is still an actual workflow failure.
5519
5685
  try:
5686
+ _refresh_replay_logging()
5520
5687
  gen = entry(ctx, *start_input)
5521
5688
  except NonDeterministicReplayError:
5522
5689
  raise
@@ -5534,7 +5701,9 @@ def _replay_state(
5534
5701
  _assert_no_unconsumed_history("complete workflow")
5535
5702
  return _state([CompleteWorkflow(result=gen)])
5536
5703
 
5537
- ctx.logger._set_replaying(True)
5704
+ lifetime.generator = gen
5705
+
5706
+ _set_replaying(True)
5538
5707
 
5539
5708
  next_value: Any = None
5540
5709
  first = True
@@ -5617,7 +5786,7 @@ def _replay_state(
5617
5786
  version = command.version
5618
5787
  if type(version) is not int:
5619
5788
  raise _version_error(f"Invalid version for change ID {change_id!r}.")
5620
- ctx.logger._set_replaying(False)
5789
+ _set_replaying(False)
5621
5790
  pending.append(command)
5622
5791
  authored_sequence += 1
5623
5792
  if not command.min_supported <= version <= command.max_supported:
@@ -6440,6 +6609,7 @@ def _replay_state(
6440
6609
  if isinstance(command, DurableOperationHandle):
6441
6610
  authored_sequence += 1
6442
6611
  try:
6612
+ _refresh_replay_logging()
6443
6613
  advanced_cmd = gen.throw(WorkflowCancelled(
6444
6614
  "workflow cancellation was requested", request_id=boundary.request_id,
6445
6615
  context=ctx._cancellation_context,
@@ -6496,6 +6666,7 @@ def _replay_state(
6496
6666
  ctx._cancellation_context = active_context
6497
6667
  _apply_due_receivers()
6498
6668
  try:
6669
+ _refresh_replay_logging()
6499
6670
  advanced_cmd = gen.throw(WorkflowCancelled(
6500
6671
  "scope cancellation was requested", request_id=active_context.request_id, context=active_context,
6501
6672
  ))
@@ -6531,6 +6702,7 @@ def _replay_state(
6531
6702
  advanced_cmd = None
6532
6703
  else:
6533
6704
  try:
6705
+ _refresh_replay_logging()
6534
6706
  cmd = gen.send(None) if first else gen.send(next_value)
6535
6707
  except StopIteration as stop:
6536
6708
  return _terminal_state(stop.value, include_pending=True)
@@ -6574,7 +6746,7 @@ def _replay_state(
6574
6746
  current_call_sequence, "cancellation scope", next_step.event_types if next_step else [],
6575
6747
  detail="cancellation_scope_opening_changed: authored scope has no original canonical opening",
6576
6748
  )
6577
- ctx.logger._set_replaying(False)
6749
+ _set_replaying(False)
6578
6750
  return _state(pending, cancellation_scope_opening=CancellationScopeOpening(
6579
6751
  current_call_sequence, cmd.parent_scope_id, cmd.shield_parent,
6580
6752
  ))
@@ -6696,7 +6868,7 @@ def _replay_state(
6696
6868
  if marker is None:
6697
6869
  if has_recorded_member_history:
6698
6870
  return _state(pending)
6699
- ctx.logger._set_replaying(False)
6871
+ _set_replaying(False)
6700
6872
  pending.extend(annotated)
6701
6873
  return _state(pending)
6702
6874
 
@@ -6785,7 +6957,7 @@ def _replay_state(
6785
6957
  if prepared_group:
6786
6958
  group = _prepared_group(cmd)
6787
6959
  if group is not None:
6788
- ctx.logger._set_replaying(False)
6960
+ _set_replaying(False)
6789
6961
  return _state(pending, prepared_local_activity_group=group)
6790
6962
  if any(current_call_sequence + offset not in resolved_sequences for offset in range(needed)):
6791
6963
  # Local results are durable. Remote members own their
@@ -6815,13 +6987,14 @@ def _replay_state(
6815
6987
  _advance_cancellation_clock(consumed_indexes)
6816
6988
  if failed is not None:
6817
6989
  try:
6990
+ _refresh_replay_logging()
6818
6991
  advanced_cmd = gen.throw(failed)
6819
6992
  continue
6820
6993
  except StopIteration as stop:
6821
6994
  return _terminal_state(stop.value, include_pending=False)
6822
6995
  next_value = _parallel_result_shape(result_shape, vals)
6823
6996
  continue
6824
- ctx.logger._set_replaying(False)
6997
+ _set_replaying(False)
6825
6998
  for offset, child_command in enumerate(cmd):
6826
6999
  _assert_pending_step_matches(child_command, offset)
6827
7000
  pending.extend(cmd)
@@ -6833,6 +7006,7 @@ def _replay_state(
6833
7006
  cancelled_selection_indexes[(cmd.selection_group_id, cmd.base_sequence)],
6834
7007
  ])
6835
7008
  try:
7009
+ _refresh_replay_logging()
6836
7010
  advanced_cmd = gen.throw(
6837
7011
  DurableOperationCancelled(
6838
7012
  cmd.selection_group_id,
@@ -6860,11 +7034,12 @@ def _replay_state(
6860
7034
  selection_shape,
6861
7035
  )
6862
7036
  if not resolved:
6863
- ctx.logger._set_replaying(False)
7037
+ _set_replaying(False)
6864
7038
  return _state(pending)
6865
7039
  _advance_selection_clock(cmd.base_sequence, cmd.size, failure)
6866
7040
  if failure is not None:
6867
7041
  try:
7042
+ _refresh_replay_logging()
6868
7043
  advanced_cmd = gen.throw(failure)
6869
7044
  continue
6870
7045
  except StopIteration as stop:
@@ -6929,12 +7104,13 @@ def _replay_state(
6929
7104
  )
6930
7105
  except NexusOperationFailed as exc:
6931
7106
  try:
7107
+ _refresh_replay_logging()
6932
7108
  advanced_cmd = gen.throw(exc)
6933
7109
  continue
6934
7110
  except StopIteration as stop:
6935
7111
  return _terminal_state(stop.value, include_pending=False)
6936
7112
  continue
6937
- ctx.logger._set_replaying(False)
7113
+ _set_replaying(False)
6938
7114
  _assert_pending_step_matches(cmd)
6939
7115
  if _next_unconsumed_recorded_step() is not None:
6940
7116
  # Admission already has its durable sequence. A later task
@@ -6949,13 +7125,19 @@ def _replay_state(
6949
7125
  next_value = resolved_results[result_cursor]
6950
7126
  result_cursor += 1
6951
7127
  continue
6952
- ctx.logger._set_replaying(False)
7128
+ _set_replaying(False)
6953
7129
  _assert_pending_step_matches(cmd)
7130
+ if reconstruction_only:
7131
+ # A query/update rebuild cannot author a missing marker or
7132
+ # execute its callback. Only a workflow task may do that.
7133
+ pending.append(cmd)
7134
+ return _state(pending)
6954
7135
  if cmd._effect is not None:
6955
7136
  try:
6956
7137
  cmd.result = cmd._effect()
6957
7138
  except Exception as exc:
6958
7139
  try:
7140
+ _refresh_replay_logging()
6959
7141
  advanced_cmd = gen.throw(exc)
6960
7142
  continue
6961
7143
  except StopIteration as stop:
@@ -6970,7 +7152,7 @@ def _replay_state(
6970
7152
  next_value = resolved_results[result_cursor]
6971
7153
  result_cursor += 1
6972
7154
  continue
6973
- ctx.logger._set_replaying(False)
7155
+ _set_replaying(False)
6974
7156
  _assert_pending_step_matches(cmd)
6975
7157
  pending.append(cmd)
6976
7158
  next_value = None
@@ -7012,7 +7194,7 @@ def _replay_state(
7012
7194
  step = _next_unconsumed_recorded_step()
7013
7195
  if step is not None:
7014
7196
  _assert_step_matches(cmd, step)
7015
- ctx.logger._set_replaying(False)
7197
+ _set_replaying(False)
7016
7198
  pending.append(cmd)
7017
7199
  return _state(pending)
7018
7200
  # Terminal cleanup may only skip a later open after this
@@ -7119,7 +7301,7 @@ def _replay_state(
7119
7301
  wait_yield_count = next_wait_index
7120
7302
  continue
7121
7303
 
7122
- ctx.logger._set_replaying(False)
7304
+ _set_replaying(False)
7123
7305
  pending.append(cmd)
7124
7306
  wait_yield_count = next_wait_index
7125
7307
  return _state(pending)
@@ -7133,13 +7315,14 @@ def _replay_state(
7133
7315
  result_cursor += 1
7134
7316
  if isinstance(val, ActivityFailed | ChildWorkflowFailed):
7135
7317
  try:
7318
+ _refresh_replay_logging()
7136
7319
  advanced_cmd = gen.throw(val)
7137
7320
  continue
7138
7321
  except StopIteration as stop:
7139
7322
  return _terminal_state(stop.value, include_pending=False)
7140
7323
  next_value = val
7141
7324
  continue
7142
- ctx.logger._set_replaying(False)
7325
+ _set_replaying(False)
7143
7326
  _assert_pending_step_matches(cmd)
7144
7327
  if not isinstance(cmd, RecordLocalActivity) and _next_unconsumed_recorded_step() is not None:
7145
7328
  # A signal may wake a workflow while its existing operation is
@@ -7155,6 +7338,7 @@ def _replay_state(
7155
7338
  pending.append(cmd)
7156
7339
  if isinstance(local_result, ActivityFailed):
7157
7340
  try:
7341
+ _refresh_replay_logging()
7158
7342
  advanced_cmd = gen.throw(local_result)
7159
7343
  continue
7160
7344
  except StopIteration as stop: