Skip to main content

durable_workflow/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::{
4    any::{Any, TypeId},
5    collections::{BTreeMap, HashMap},
6    future::Future,
7    pin::Pin,
8    sync::{
9        atomic::{AtomicBool, Ordering},
10        Arc, Mutex, OnceLock,
11    },
12    task::{Context as TaskContext, Poll},
13    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
14};
15
16use apache_avro::{from_avro_datum, from_value, to_avro_datum, to_value, Schema};
17use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
18use futures_util::{future::OptionFuture, task::noop_waker_ref};
19use serde::{de::DeserializeOwned, Deserialize, Serialize};
20pub use serde_json::{json, Value};
21use thiserror::Error;
22pub use uuid::Uuid;
23
24pub const WORKER_PROTOCOL_VERSION: &str = "1.2";
25pub const CONTROL_PLANE_VERSION: &str = "2";
26pub const DEFAULT_CODEC: &str = "avro";
27pub const JSON_CODEC: &str = "json";
28pub const SDK_VERSION: &str = concat!("durable-workflow-rust/", env!("CARGO_PKG_VERSION"));
29/// Worker-registration capability for server-routed read-only queries.
30pub const QUERY_TASKS_CAPABILITY: &str = "query_tasks";
31/// First additive worker protocol that defines query-task transport.
32pub const QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION: &str = "1.8";
33
34const MAX_LONG_POLL_TIMEOUT_SECONDS: u64 = 60;
35const WORKFLOW_TASK_WAITING_FOR_HISTORY_MESSAGE: &str =
36    "Workflow task waiting for scheduled history.";
37const WORKFLOW_TASK_WAITING_FOR_HISTORY_TYPE: &str = "WorkflowTaskWaitingForHistory";
38
39const QUERY_TASK_FINAL_REJECTION_REASONS: &[&str] = &[
40    "lease_expired",
41    "query_task_not_found",
42    "query_task_not_leased",
43    "query_task_timed_out",
44];
45
46const AVRO_PAYLOAD_SCHEMA_JSON: &str = r#"{"type":"record","name":"Payload","namespace":"durable_workflow","fields":[{"name":"json","type":"string"},{"name":"version","type":"int","default":1}]}"#;
47const AVRO_PAYLOAD_VERSION: i32 = 1;
48
49static AVRO_PAYLOAD_SCHEMA: OnceLock<std::result::Result<Schema, String>> = OnceLock::new();
50
51#[derive(Clone, Copy)]
52enum RequestProtocol {
53    ControlPlane,
54    Worker(&'static str),
55}
56
57impl RequestProtocol {
58    fn is_worker(self) -> bool {
59        matches!(self, Self::Worker(_))
60    }
61}
62
63pub type Result<T> = std::result::Result<T, Error>;
64
65#[derive(Debug, Error)]
66pub enum Error {
67    #[error("transport error: {0}")]
68    Transport(#[from] reqwest::Error),
69    #[error("json error: {0}")]
70    Json(#[from] serde_json::Error),
71    #[error("http {status}: {body}")]
72    Http {
73        status: reqwest::StatusCode,
74        body: String,
75    },
76    #[error("codec error: {0}")]
77    Codec(String),
78    #[error(transparent)]
79    QueryFailed(QueryFailure),
80    #[error(transparent)]
81    Protocol(ProtocolFailure),
82    #[error(transparent)]
83    NonDeterministicReplay(ReplayFailure),
84    #[error(transparent)]
85    ChildWorkflowFailed(ChildWorkflowFailure),
86    #[error(transparent)]
87    ActivityFailed(ActivityFailure),
88    #[error(transparent)]
89    WorkflowCommandRejected(WorkflowCommandRejection),
90    #[error(transparent)]
91    WorkflowFailed(WorkflowTerminalOutcome),
92    #[error(transparent)]
93    WorkflowCancelled(WorkflowTerminalOutcome),
94    #[error(transparent)]
95    WorkflowTerminated(WorkflowTerminalOutcome),
96    #[error(transparent)]
97    WorkflowTimedOut(WorkflowTerminalOutcome),
98    #[error(transparent)]
99    ActivityTaskRejected(ActivityTaskRejection),
100    #[error("workflow handler {0:?} is not registered")]
101    WorkflowNotRegistered(String),
102    #[error("activity handler {0:?} is not registered")]
103    ActivityNotRegistered(String),
104    #[error("workflow future yielded without emitting a durable command")]
105    WorkflowYieldedWithoutCommand,
106    #[error("workflow state lock is poisoned")]
107    WorkflowStatePoisoned,
108    #[error("timer duration is too large for the worker protocol")]
109    TimerDurationOverflow,
110    #[error("operation timed out")]
111    Timeout,
112    #[error("worker loop error: {0}")]
113    WorkerLoop(String),
114    #[error("invalid child workflow options: {0}")]
115    InvalidChildWorkflowOptions(String),
116    #[error(transparent)]
117    InvalidActivityOptions(ActivityOptionsError),
118}
119
120/// The lifecycle command sent to a workflow execution.
121#[derive(Clone, Copy, Debug, PartialEq, Eq)]
122pub enum WorkflowCommandKind {
123    Cancel,
124    Terminate,
125}
126
127impl WorkflowCommandKind {
128    fn as_str(self) -> &'static str {
129        match self {
130            Self::Cancel => "cancel",
131            Self::Terminate => "terminate",
132        }
133    }
134}
135
136/// Optional structured fields for a cancellation or termination request.
137#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
138pub struct WorkflowCommandOptions {
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub reason: Option<String>,
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub request_id: Option<String>,
143}
144
145/// Server-enforced timeout policy for a workflow start.
146///
147/// These deadlines are distinct from [`WorkflowResultOptions::timeout`], which
148/// only bounds how long the caller waits. A server deadline produces a terminal
149/// [`Error::WorkflowTimedOut`] outcome whose reason is `execution_timeout` or
150/// `run_timeout`.
151#[derive(Clone, Debug, PartialEq, Eq)]
152pub struct WorkflowStartOptions {
153    pub execution_timeout_seconds: u64,
154    pub run_timeout_seconds: u64,
155}
156
157impl Default for WorkflowStartOptions {
158    fn default() -> Self {
159        Self {
160            execution_timeout_seconds: 3600,
161            run_timeout_seconds: 600,
162        }
163    }
164}
165
166impl WorkflowStartOptions {
167    pub fn new() -> Self {
168        Self::default()
169    }
170
171    pub fn execution_timeout_seconds(mut self, seconds: u64) -> Self {
172        self.execution_timeout_seconds = seconds;
173        self
174    }
175
176    pub fn run_timeout_seconds(mut self, seconds: u64) -> Self {
177        self.run_timeout_seconds = seconds;
178        self
179    }
180
181    fn validate(&self) -> Result<()> {
182        if self.execution_timeout_seconds == 0 {
183            return Err(Error::Codec(
184                "execution_timeout_seconds must be at least 1".to_string(),
185            ));
186        }
187        if self.run_timeout_seconds == 0 {
188            return Err(Error::Codec(
189                "run_timeout_seconds must be at least 1".to_string(),
190            ));
191        }
192        if self.run_timeout_seconds > self.execution_timeout_seconds {
193            return Err(Error::Codec(
194                "run_timeout_seconds cannot exceed execution_timeout_seconds".to_string(),
195            ));
196        }
197
198        Ok(())
199    }
200}
201
202impl WorkflowCommandOptions {
203    pub fn new() -> Self {
204        Self::default()
205    }
206
207    pub fn reason(mut self, reason: impl Into<String>) -> Self {
208        self.reason = Some(reason.into());
209        self
210    }
211
212    pub fn request_id(mut self, request_id: impl Into<String>) -> Self {
213        self.request_id = Some(request_id.into());
214        self
215    }
216}
217
218/// The accepted, machine-readable result of a lifecycle command.
219#[derive(Clone, Debug, PartialEq)]
220pub struct WorkflowCommandResult {
221    pub command: WorkflowCommandKind,
222    pub workflow_id: String,
223    pub run_id: Option<String>,
224    pub outcome: Option<String>,
225    pub reason: Option<String>,
226    pub command_status: Option<String>,
227    pub raw: Value,
228}
229
230/// A stable rejection returned by instance- or selected-run lifecycle commands.
231#[derive(Clone, Debug, Error)]
232#[error("workflow {command:?} rejected ({reason}, HTTP {status}): {message}")]
233pub struct WorkflowCommandRejection {
234    pub command: WorkflowCommandKind,
235    pub status: u16,
236    pub reason: String,
237    pub message: String,
238    pub workflow_id: String,
239    pub run_id: Option<String>,
240    pub target_scope: Option<String>,
241    pub body: Value,
242}
243
244/// Stable terminal categories returned by [`WorkflowHandle::result`].
245#[derive(Clone, Copy, Debug, PartialEq, Eq)]
246pub enum WorkflowTerminalKind {
247    Failed,
248    Cancelled,
249    Terminated,
250    TimedOut,
251}
252
253/// A typed terminal workflow outcome with durable identity and failure metadata.
254///
255/// Match the corresponding [`enum@Error`] variant and inspect these fields instead
256/// of parsing its display representation. Fields remain `None` when an older
257/// server did not publish that metadata.
258#[derive(Clone, Debug, Error)]
259#[error("workflow {workflow_id} run {run_id:?} ended as {kind:?} ({reason})")]
260pub struct WorkflowTerminalOutcome {
261    pub kind: WorkflowTerminalKind,
262    pub workflow_id: String,
263    pub run_id: Option<String>,
264    pub reason: String,
265    pub failure_category: Option<String>,
266    pub failure_id: Option<String>,
267    pub exception_type: Option<String>,
268    pub exception_class: Option<String>,
269    pub non_retryable: Option<bool>,
270    pub message: Option<String>,
271    pub exception: Option<Value>,
272    pub raw: Value,
273}
274
275/// A worker-side activity settlement or heartbeat rejected by durable state.
276#[derive(Clone, Debug, Error)]
277#[error("activity task {operation} rejected ({reason}, HTTP {status})")]
278pub struct ActivityTaskRejection {
279    pub operation: String,
280    pub status: u16,
281    pub reason: String,
282    pub task_id: String,
283    pub activity_attempt_id: String,
284    pub cancel_requested: bool,
285    pub can_continue: Option<bool>,
286    pub run_closed_reason: Option<String>,
287    pub body: Value,
288}
289
290/// Stable validation categories for [`ActivityOptions`].
291#[derive(Clone, Copy, Debug, PartialEq, Eq)]
292pub enum ActivityOptionsErrorKind {
293    EmptyTaskQueue,
294    EmptyRetryPolicy,
295    InvalidMaxAttempts,
296    BackoffWithoutRetryBudget,
297    TooManyBackoffIntervals,
298    InvalidBackoffCoefficient,
299    BackoffGenerationTooLarge,
300    BackoffOverflow,
301    EmptyNonRetryableErrorType,
302    TimeoutNotPositive,
303    TimeoutOverflow,
304    TimeoutOrder,
305}
306
307/// A machine-readable activity-options validation failure.
308#[derive(Clone, Debug, Error, PartialEq, Eq)]
309#[error("invalid activity options ({kind:?}, {field:?}): {message}")]
310pub struct ActivityOptionsError {
311    pub kind: ActivityOptionsErrorKind,
312    pub field: Option<&'static str>,
313    pub message: String,
314}
315
316impl ActivityOptionsError {
317    fn new(
318        kind: ActivityOptionsErrorKind,
319        field: Option<&'static str>,
320        message: impl Into<String>,
321    ) -> Self {
322        Self {
323            kind,
324            field,
325            message: message.into(),
326        }
327    }
328}
329
330/// Stable terminal categories returned when an awaited activity does not succeed.
331#[derive(Clone, Copy, Debug, PartialEq, Eq)]
332pub enum ActivityFailureKind {
333    Failed,
334    Cancelled,
335    TimedOut,
336}
337
338/// A stable, machine-readable terminal activity failure.
339///
340/// Match [`Error::ActivityFailed`] and inspect `kind`, `reason`,
341/// `failure_category`, or `timeout_kind`; display text is only diagnostic.
342#[derive(Clone, Debug, Error)]
343#[error("activity failed ({reason}): {message}")]
344pub struct ActivityFailure {
345    pub kind: ActivityFailureKind,
346    pub reason: String,
347    pub message: String,
348    pub activity_execution_id: Option<String>,
349    pub activity_attempt_id: Option<String>,
350    pub activity_type: Option<String>,
351    pub activity_class: Option<String>,
352    pub attempt_number: Option<u64>,
353    pub failure_id: Option<String>,
354    pub failure_category: Option<String>,
355    pub timeout_kind: Option<String>,
356    pub non_retryable: bool,
357    pub exception_type: Option<String>,
358    pub exception_class: Option<String>,
359    pub code: Option<Value>,
360    pub exception: Option<Value>,
361}
362
363/// Stable terminal categories returned when an awaited child does not succeed.
364#[derive(Clone, Copy, Debug, PartialEq, Eq)]
365pub enum ChildWorkflowFailureKind {
366    Failed,
367    Cancelled,
368    Terminated,
369}
370
371/// A stable, machine-readable child workflow failure delivered to its parent.
372///
373/// Match [`Error::ChildWorkflowFailed`] and inspect `reason` or `kind` instead
374/// of parsing the display message. Child and parent identifiers retain the
375/// relationship recorded in durable history across worker restarts.
376#[derive(Clone, Debug, Error)]
377#[error("child workflow failed ({reason}): {message}")]
378pub struct ChildWorkflowFailure {
379    pub kind: ChildWorkflowFailureKind,
380    pub reason: String,
381    pub message: String,
382    pub parent_workflow_id: Option<String>,
383    pub parent_workflow_run_id: Option<String>,
384    pub child_workflow_id: Option<String>,
385    pub child_workflow_run_id: Option<String>,
386    pub child_workflow_type: Option<String>,
387    pub failure_id: Option<String>,
388    pub failure_category: Option<String>,
389    pub exception_type: Option<String>,
390    pub exception_class: Option<String>,
391    pub non_retryable: bool,
392    pub code: Option<Value>,
393    pub exception: Option<Value>,
394}
395
396/// The identity of one durable workflow execution.
397#[derive(Clone, Debug, PartialEq, Eq)]
398pub struct WorkflowIdentity {
399    pub workflow_id: Option<String>,
400    pub run_id: Option<String>,
401}
402
403/// A successful child result together with its durable parent-child identity.
404#[derive(Clone, Debug, PartialEq)]
405pub struct ChildWorkflowResult {
406    pub parent: WorkflowIdentity,
407    pub child: WorkflowIdentity,
408    pub child_workflow_type: Option<String>,
409    pub result: Value,
410}
411
412/// Server behavior when a parent closes while its child is still open.
413#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
414pub enum ParentClosePolicy {
415    #[default]
416    Abandon,
417    RequestCancel,
418    Terminate,
419}
420
421impl ParentClosePolicy {
422    fn as_str(self) -> &'static str {
423        match self {
424            Self::Abandon => "abandon",
425            Self::RequestCancel => "request_cancel",
426            Self::Terminate => "terminate",
427        }
428    }
429}
430
431/// Durable retry policy for one child workflow invocation.
432#[derive(Clone, Debug, Default, PartialEq, Eq)]
433pub struct ChildWorkflowRetryPolicy {
434    pub max_attempts: Option<u32>,
435    pub backoff_seconds: Vec<u64>,
436    pub non_retryable_error_types: Vec<String>,
437}
438
439/// Options recorded with a child-workflow command.
440///
441/// The task queue is mandatory so routing is explicit and replay-stable.
442#[derive(Clone, Debug, PartialEq, Eq)]
443pub struct ChildWorkflowOptions {
444    pub task_queue: String,
445    pub parent_close_policy: ParentClosePolicy,
446    pub retry_policy: Option<ChildWorkflowRetryPolicy>,
447    pub execution_timeout_seconds: Option<u64>,
448    pub run_timeout_seconds: Option<u64>,
449}
450
451impl ChildWorkflowOptions {
452    pub fn new(task_queue: impl Into<String>) -> Self {
453        Self {
454            task_queue: task_queue.into(),
455            parent_close_policy: ParentClosePolicy::Abandon,
456            retry_policy: None,
457            execution_timeout_seconds: None,
458            run_timeout_seconds: None,
459        }
460    }
461
462    pub fn parent_close_policy(mut self, policy: ParentClosePolicy) -> Self {
463        self.parent_close_policy = policy;
464        self
465    }
466
467    pub fn retry_policy(mut self, policy: ChildWorkflowRetryPolicy) -> Self {
468        self.retry_policy = Some(policy);
469        self
470    }
471
472    pub fn execution_timeout_seconds(mut self, seconds: u64) -> Self {
473        self.execution_timeout_seconds = Some(seconds);
474        self
475    }
476
477    pub fn run_timeout_seconds(mut self, seconds: u64) -> Self {
478        self.run_timeout_seconds = Some(seconds);
479        self
480    }
481}
482
483/// Backoff intervals for one durable activity retry policy.
484#[derive(Clone, Debug, PartialEq, Eq)]
485pub enum ActivityBackoff {
486    /// Use these intervals between attempts. The server repeats the final
487    /// interval if the retry budget contains more attempts than entries.
488    Explicit(Vec<Duration>),
489    /// Generate one interval for every retry using integer exponential growth.
490    Exponential {
491        initial_interval: Duration,
492        coefficient: u32,
493        maximum_interval: Option<Duration>,
494    },
495}
496
497/// Durable server-side retry policy for one activity execution.
498#[derive(Clone, Debug, Default, PartialEq, Eq)]
499pub struct ActivityRetryPolicy {
500    pub max_attempts: Option<u32>,
501    pub backoff: Option<ActivityBackoff>,
502    pub non_retryable_error_types: Vec<String>,
503}
504
505impl ActivityRetryPolicy {
506    /// Start a policy with a finite attempt budget, including the first attempt.
507    pub fn new(max_attempts: u32) -> Self {
508        Self {
509            max_attempts: Some(max_attempts),
510            ..Self::default()
511        }
512    }
513
514    pub fn backoff_intervals(mut self, intervals: impl IntoIterator<Item = Duration>) -> Self {
515        self.backoff = Some(ActivityBackoff::Explicit(intervals.into_iter().collect()));
516        self
517    }
518
519    pub fn exponential_backoff(
520        mut self,
521        initial_interval: Duration,
522        coefficient: u32,
523        maximum_interval: Option<Duration>,
524    ) -> Self {
525        self.backoff = Some(ActivityBackoff::Exponential {
526            initial_interval,
527            coefficient,
528            maximum_interval,
529        });
530        self
531    }
532
533    pub fn non_retryable_error_type(mut self, error_type: impl Into<String>) -> Self {
534        self.non_retryable_error_types.push(error_type.into());
535        self
536    }
537
538    pub fn non_retryable_error_types(
539        mut self,
540        error_types: impl IntoIterator<Item = impl Into<String>>,
541    ) -> Self {
542        self.non_retryable_error_types
543            .extend(error_types.into_iter().map(Into::into));
544        self
545    }
546}
547
548/// Options recorded atomically on one deterministic `schedule_activity` command.
549///
550/// Durations are rounded up to whole seconds when encoded, so the server never
551/// receives a shorter timeout or backoff than the caller requested.
552#[derive(Clone, Debug, Default, PartialEq, Eq)]
553pub struct ActivityOptions {
554    pub task_queue: Option<String>,
555    pub retry_policy: Option<ActivityRetryPolicy>,
556    pub start_to_close_timeout: Option<Duration>,
557    pub schedule_to_start_timeout: Option<Duration>,
558    pub schedule_to_close_timeout: Option<Duration>,
559    pub heartbeat_timeout: Option<Duration>,
560}
561
562impl ActivityOptions {
563    pub fn new() -> Self {
564        Self::default()
565    }
566
567    pub fn task_queue(mut self, task_queue: impl Into<String>) -> Self {
568        self.task_queue = Some(task_queue.into());
569        self
570    }
571
572    pub fn retry_policy(mut self, policy: ActivityRetryPolicy) -> Self {
573        self.retry_policy = Some(policy);
574        self
575    }
576
577    pub fn start_to_close_timeout(mut self, timeout: Duration) -> Self {
578        self.start_to_close_timeout = Some(timeout);
579        self
580    }
581
582    pub fn schedule_to_start_timeout(mut self, timeout: Duration) -> Self {
583        self.schedule_to_start_timeout = Some(timeout);
584        self
585    }
586
587    pub fn schedule_to_close_timeout(mut self, timeout: Duration) -> Self {
588        self.schedule_to_close_timeout = Some(timeout);
589        self
590    }
591
592    pub fn heartbeat_timeout(mut self, timeout: Duration) -> Self {
593        self.heartbeat_timeout = Some(timeout);
594        self
595    }
596
597    fn validate(&self) -> std::result::Result<ValidatedActivityOptions, ActivityOptionsError> {
598        if self
599            .task_queue
600            .as_deref()
601            .is_some_and(|queue| queue.trim().is_empty())
602        {
603            return Err(ActivityOptionsError::new(
604                ActivityOptionsErrorKind::EmptyTaskQueue,
605                Some("task_queue"),
606                "task_queue must not be empty",
607            ));
608        }
609
610        for (field, value) in [
611            ("start_to_close_timeout", self.start_to_close_timeout),
612            ("schedule_to_start_timeout", self.schedule_to_start_timeout),
613            ("schedule_to_close_timeout", self.schedule_to_close_timeout),
614            ("heartbeat_timeout", self.heartbeat_timeout),
615        ] {
616            if value.is_some_and(|value| value.is_zero()) {
617                return Err(ActivityOptionsError::new(
618                    ActivityOptionsErrorKind::TimeoutNotPositive,
619                    Some(field),
620                    format!("{field} must be positive"),
621                ));
622            }
623        }
624
625        validate_timeout_order(
626            "heartbeat_timeout",
627            self.heartbeat_timeout,
628            "start_to_close_timeout",
629            self.start_to_close_timeout,
630        )?;
631        validate_timeout_order(
632            "start_to_close_timeout",
633            self.start_to_close_timeout,
634            "schedule_to_close_timeout",
635            self.schedule_to_close_timeout,
636        )?;
637        validate_timeout_order(
638            "schedule_to_start_timeout",
639            self.schedule_to_start_timeout,
640            "schedule_to_close_timeout",
641            self.schedule_to_close_timeout,
642        )?;
643
644        Ok(ValidatedActivityOptions {
645            task_queue: self.task_queue.clone(),
646            retry_policy: self
647                .retry_policy
648                .as_ref()
649                .map(validate_activity_retry_policy)
650                .transpose()?,
651            start_to_close_timeout: timeout_seconds(
652                "start_to_close_timeout",
653                self.start_to_close_timeout,
654            )?,
655            schedule_to_start_timeout: timeout_seconds(
656                "schedule_to_start_timeout",
657                self.schedule_to_start_timeout,
658            )?,
659            schedule_to_close_timeout: timeout_seconds(
660                "schedule_to_close_timeout",
661                self.schedule_to_close_timeout,
662            )?,
663            heartbeat_timeout: timeout_seconds("heartbeat_timeout", self.heartbeat_timeout)?,
664        })
665    }
666}
667
668#[derive(Clone, Debug)]
669struct ValidatedActivityOptions {
670    task_queue: Option<String>,
671    retry_policy: Option<Value>,
672    start_to_close_timeout: Option<u64>,
673    schedule_to_start_timeout: Option<u64>,
674    schedule_to_close_timeout: Option<u64>,
675    heartbeat_timeout: Option<u64>,
676}
677
678fn validate_timeout_order(
679    smaller_name: &'static str,
680    smaller: Option<Duration>,
681    larger_name: &'static str,
682    larger: Option<Duration>,
683) -> std::result::Result<(), ActivityOptionsError> {
684    if matches!((smaller, larger), (Some(smaller), Some(larger)) if smaller > larger) {
685        return Err(ActivityOptionsError::new(
686            ActivityOptionsErrorKind::TimeoutOrder,
687            Some(smaller_name),
688            format!("{smaller_name} must be <= {larger_name}"),
689        ));
690    }
691    Ok(())
692}
693
694fn timeout_seconds(
695    field: &'static str,
696    value: Option<Duration>,
697) -> std::result::Result<Option<u64>, ActivityOptionsError> {
698    value
699        .map(|value| {
700            activity_protocol_seconds(value).ok_or_else(|| {
701                ActivityOptionsError::new(
702                    ActivityOptionsErrorKind::TimeoutOverflow,
703                    Some(field),
704                    format!("{field} is too large for the worker protocol"),
705                )
706            })
707        })
708        .transpose()
709}
710
711fn duration_seconds_ceil(value: Duration) -> Option<u64> {
712    value
713        .as_secs()
714        .checked_add(u64::from(value.subsec_nanos() > 0))
715}
716
717fn activity_protocol_seconds(value: Duration) -> Option<u64> {
718    duration_seconds_ceil(value).filter(|seconds| *seconds <= i64::MAX as u64)
719}
720
721fn validate_activity_retry_policy(
722    policy: &ActivityRetryPolicy,
723) -> std::result::Result<Value, ActivityOptionsError> {
724    if policy.max_attempts.is_none()
725        && policy.backoff.is_none()
726        && policy.non_retryable_error_types.is_empty()
727    {
728        return Err(ActivityOptionsError::new(
729            ActivityOptionsErrorKind::EmptyRetryPolicy,
730            Some("retry_policy"),
731            "retry_policy must configure at least one field",
732        ));
733    }
734    if policy.max_attempts == Some(0) {
735        return Err(ActivityOptionsError::new(
736            ActivityOptionsErrorKind::InvalidMaxAttempts,
737            Some("retry_policy.max_attempts"),
738            "max_attempts must be >= 1",
739        ));
740    }
741    if policy
742        .non_retryable_error_types
743        .iter()
744        .any(|error_type| error_type.trim().is_empty())
745    {
746        return Err(ActivityOptionsError::new(
747            ActivityOptionsErrorKind::EmptyNonRetryableErrorType,
748            Some("retry_policy.non_retryable_error_types"),
749            "non_retryable_error_types must not contain empty values",
750        ));
751    }
752
753    let backoff_seconds = match &policy.backoff {
754        None => None,
755        Some(backoff) => {
756            let max_attempts = policy.max_attempts.ok_or_else(|| {
757                ActivityOptionsError::new(
758                    ActivityOptionsErrorKind::BackoffWithoutRetryBudget,
759                    Some("retry_policy.backoff"),
760                    "backoff requires max_attempts",
761                )
762            })?;
763            let retry_count = max_attempts.saturating_sub(1) as usize;
764            let intervals = match backoff {
765                ActivityBackoff::Explicit(intervals) => {
766                    if intervals.len() > retry_count {
767                        return Err(ActivityOptionsError::new(
768                            ActivityOptionsErrorKind::TooManyBackoffIntervals,
769                            Some("retry_policy.backoff"),
770                            "backoff interval count must not exceed max_attempts - 1",
771                        ));
772                    }
773                    intervals.clone()
774                }
775                ActivityBackoff::Exponential {
776                    initial_interval,
777                    coefficient,
778                    maximum_interval,
779                } => {
780                    if *coefficient < 1 {
781                        return Err(ActivityOptionsError::new(
782                            ActivityOptionsErrorKind::InvalidBackoffCoefficient,
783                            Some("retry_policy.backoff.coefficient"),
784                            "backoff coefficient must be >= 1",
785                        ));
786                    }
787                    if retry_count > 10_000 {
788                        return Err(ActivityOptionsError::new(
789                            ActivityOptionsErrorKind::BackoffGenerationTooLarge,
790                            Some("retry_policy.max_attempts"),
791                            "generated backoff supports at most 10000 retry intervals",
792                        ));
793                    }
794                    let mut current = *initial_interval;
795                    let mut intervals = Vec::with_capacity(retry_count);
796                    for _ in 0..retry_count {
797                        let interval = maximum_interval
798                            .map(|maximum| current.min(maximum))
799                            .unwrap_or(current);
800                        intervals.push(interval);
801                        if maximum_interval.is_some_and(|maximum| interval == maximum) {
802                            break;
803                        }
804                        current = current.checked_mul(*coefficient).ok_or_else(|| {
805                            ActivityOptionsError::new(
806                                ActivityOptionsErrorKind::BackoffOverflow,
807                                Some("retry_policy.backoff"),
808                                "generated backoff interval overflowed",
809                            )
810                        })?;
811                    }
812                    intervals
813                }
814            };
815            Some(
816                intervals
817                    .into_iter()
818                    .map(|interval| {
819                        activity_protocol_seconds(interval).ok_or_else(|| {
820                            ActivityOptionsError::new(
821                                ActivityOptionsErrorKind::BackoffOverflow,
822                                Some("retry_policy.backoff"),
823                                "backoff interval is too large for the worker protocol",
824                            )
825                        })
826                    })
827                    .collect::<std::result::Result<Vec<_>, _>>()?,
828            )
829        }
830    };
831
832    let mut encoded = serde_json::Map::new();
833    if let Some(max_attempts) = policy.max_attempts {
834        encoded.insert("max_attempts".to_string(), json!(max_attempts));
835    }
836    if let Some(backoff_seconds) = backoff_seconds {
837        encoded.insert("backoff_seconds".to_string(), json!(backoff_seconds));
838    }
839    if !policy.non_retryable_error_types.is_empty() {
840        let mut canonical_error_types = Vec::new();
841        for error_type in policy
842            .non_retryable_error_types
843            .iter()
844            .map(|error_type| error_type.trim())
845        {
846            if !canonical_error_types.contains(&error_type) {
847                canonical_error_types.push(error_type);
848            }
849        }
850        encoded.insert(
851            "non_retryable_error_types".to_string(),
852            json!(canonical_error_types),
853        );
854    }
855    Ok(Value::Object(encoded))
856}
857
858/// A stable, machine-readable failure raised when workflow code no longer
859/// reconstructs the durable command stream recorded in history.
860#[derive(Clone, Debug, Error)]
861#[error("non-deterministic workflow replay ({reason}) at sequence {sequence:?}: {message}")]
862pub struct ReplayFailure {
863    pub reason: String,
864    pub sequence: Option<u64>,
865    pub expected: Option<String>,
866    pub actual: Option<String>,
867    pub message: String,
868}
869
870impl ReplayFailure {
871    fn new(
872        reason: impl Into<String>,
873        sequence: Option<u64>,
874        expected: Option<String>,
875        actual: Option<String>,
876        message: impl Into<String>,
877    ) -> Self {
878        Self {
879            reason: reason.into(),
880            sequence,
881            expected,
882            actual,
883            message: message.into(),
884        }
885    }
886}
887
888/// A stable, machine-readable workflow query or query-task settlement failure.
889#[derive(Clone, Debug, Error)]
890#[error("query failed ({reason}, HTTP {status}): {message}")]
891pub struct QueryFailure {
892    pub status: u16,
893    pub reason: String,
894    pub message: String,
895    pub body: Value,
896}
897
898/// A stable failure returned when a server rejects an SDK protocol version.
899#[derive(Clone, Debug, Error)]
900#[error("protocol rejected ({reason}, HTTP {status}): {message}")]
901pub struct ProtocolFailure {
902    pub status: u16,
903    pub reason: String,
904    pub message: String,
905    pub supported_version: Option<String>,
906    pub requested_version: Option<String>,
907    pub body: Value,
908}
909
910#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
911pub struct PayloadEnvelope {
912    pub codec: String,
913    pub blob: String,
914}
915
916impl PayloadEnvelope {
917    pub fn avro<T: Serialize>(value: &T) -> Result<Self> {
918        encode_payload(value, DEFAULT_CODEC)
919    }
920
921    pub fn json<T: Serialize>(value: &T) -> Result<Self> {
922        encode_payload(value, JSON_CODEC)
923    }
924}
925
926pub fn encode_payload<T: Serialize>(value: &T, codec: &str) -> Result<PayloadEnvelope> {
927    let value = serde_json::to_value(value)?;
928    let blob = encode_value_blob(&value, codec)?;
929
930    Ok(PayloadEnvelope {
931        codec: codec.to_string(),
932        blob,
933    })
934}
935
936pub fn decode_payload<T: DeserializeOwned>(envelope: &PayloadEnvelope) -> Result<T> {
937    let value = decode_blob(&envelope.blob, &envelope.codec)?;
938    Ok(serde_json::from_value(value)?)
939}
940
941fn encode_value_envelope(value: &Value, codec: &str) -> Result<Value> {
942    Ok(serde_json::to_value(encode_payload(value, codec)?)?)
943}
944
945fn encode_value_blob(value: &Value, codec: &str) -> Result<String> {
946    match codec {
947        JSON_CODEC => Ok(serde_json::to_string(value)?),
948        DEFAULT_CODEC => encode_avro_generic(value),
949        other => Err(Error::Codec(format!("unsupported payload codec {other:?}"))),
950    }
951}
952
953fn decode_wire_value(value: &Value, fallback_codec: &str) -> Result<Value> {
954    if value.is_null() {
955        return Ok(Value::Null);
956    }
957
958    if let Some(object) = value.as_object() {
959        if let (Some(codec), Some(blob)) = (
960            object.get("codec").and_then(Value::as_str),
961            object.get("blob").and_then(Value::as_str),
962        ) {
963            return decode_blob(blob, codec);
964        }
965    }
966
967    if let Some(blob) = value.as_str() {
968        return decode_blob(blob, fallback_codec);
969    }
970
971    Ok(value.clone())
972}
973
974fn decode_blob(blob: &str, codec: &str) -> Result<Value> {
975    match codec {
976        JSON_CODEC => Ok(serde_json::from_str(blob)?),
977        DEFAULT_CODEC => decode_avro_generic(blob),
978        other => Err(Error::Codec(format!("unsupported payload codec {other:?}"))),
979    }
980}
981
982fn encode_avro_generic(value: &Value) -> Result<String> {
983    let json = serde_json::to_string(value)?;
984    let datum = to_value(AvroPayload {
985        json,
986        version: AVRO_PAYLOAD_VERSION,
987    })
988    .map_err(|err| Error::Codec(format!("could not convert avro generic wrapper: {err}")))?;
989    let datum = to_avro_datum(avro_payload_schema()?, datum)
990        .map_err(|err| Error::Codec(format!("could not encode avro generic wrapper: {err}")))?;
991
992    let mut bytes = Vec::with_capacity(datum.len() + 1);
993    bytes.push(0x00);
994    bytes.extend_from_slice(&datum);
995    Ok(BASE64.encode(bytes))
996}
997
998fn decode_avro_generic(blob: &str) -> Result<Value> {
999    let bytes = BASE64
1000        .decode(blob)
1001        .map_err(|err| Error::Codec(format!("invalid avro base64 payload: {err}")))?;
1002
1003    if bytes.is_empty() {
1004        return Err(Error::Codec("avro payload is empty".to_string()));
1005    }
1006
1007    match bytes[0] {
1008        0x00 => {}
1009        0x01 => {
1010            return Err(Error::Codec(
1011                "typed avro payloads require a schema context; v1 supports the generic wrapper"
1012                    .to_string(),
1013            ));
1014        }
1015        other => {
1016            return Err(Error::Codec(format!(
1017                "unknown avro payload prefix 0x{other:02x}"
1018            )));
1019        }
1020    }
1021
1022    let mut datum = &bytes[1..];
1023    let datum = from_avro_datum(avro_payload_schema()?, &mut datum, None)
1024        .map_err(|err| Error::Codec(format!("could not decode avro generic wrapper: {err}")))?;
1025    let payload: AvroPayload = from_value(&datum)
1026        .map_err(|err| Error::Codec(format!("invalid avro generic wrapper record: {err}")))?;
1027
1028    if payload.version != AVRO_PAYLOAD_VERSION {
1029        return Err(Error::Codec(format!(
1030            "unsupported avro generic wrapper version {}",
1031            payload.version
1032        )));
1033    }
1034
1035    Ok(serde_json::from_str(&payload.json)?)
1036}
1037
1038#[derive(Debug, Serialize, Deserialize)]
1039struct AvroPayload {
1040    json: String,
1041    version: i32,
1042}
1043
1044fn avro_payload_schema() -> Result<&'static Schema> {
1045    match AVRO_PAYLOAD_SCHEMA.get_or_init(|| {
1046        Schema::parse_str(AVRO_PAYLOAD_SCHEMA_JSON)
1047            .map_err(|err| format!("could not parse avro payload schema: {err}"))
1048    }) {
1049        Ok(schema) => Ok(schema),
1050        Err(message) => Err(Error::Codec(message.clone())),
1051    }
1052}
1053
1054#[derive(Clone, Debug)]
1055pub struct Client {
1056    http: reqwest::Client,
1057    base_url: String,
1058    token: Option<String>,
1059    control_token: Option<String>,
1060    worker_token: Option<String>,
1061    namespace: String,
1062}
1063
1064impl Client {
1065    pub fn new(base_url: impl Into<String>) -> Result<Self> {
1066        Self::builder(base_url).build()
1067    }
1068
1069    pub fn builder(base_url: impl Into<String>) -> ClientBuilder {
1070        ClientBuilder {
1071            base_url: base_url.into(),
1072            token: None,
1073            control_token: None,
1074            worker_token: None,
1075            namespace: "default".to_string(),
1076            timeout: Duration::from_secs(60),
1077        }
1078    }
1079
1080    pub async fn health(&self) -> Result<Value> {
1081        self.request_json(
1082            reqwest::Method::GET,
1083            "/health",
1084            RequestProtocol::ControlPlane,
1085            Option::<&Value>::None,
1086        )
1087        .await
1088    }
1089
1090    pub async fn cluster_info(&self) -> Result<Value> {
1091        self.request_json(
1092            reqwest::Method::GET,
1093            "/cluster/info",
1094            RequestProtocol::ControlPlane,
1095            Option::<&Value>::None,
1096        )
1097        .await
1098    }
1099
1100    pub async fn start_workflow<T: Serialize>(
1101        &self,
1102        workflow_type: &str,
1103        task_queue: &str,
1104        workflow_id: &str,
1105        input: T,
1106    ) -> Result<WorkflowHandle> {
1107        self.start_workflow_with_options(
1108            workflow_type,
1109            task_queue,
1110            workflow_id,
1111            WorkflowStartOptions::default(),
1112            input,
1113        )
1114        .await
1115    }
1116
1117    /// Start a workflow with explicit server-enforced execution and run
1118    /// deadlines.
1119    pub async fn start_workflow_with_options<T: Serialize>(
1120        &self,
1121        workflow_type: &str,
1122        task_queue: &str,
1123        workflow_id: &str,
1124        options: WorkflowStartOptions,
1125        input: T,
1126    ) -> Result<WorkflowHandle> {
1127        options.validate()?;
1128        let input = serde_json::to_value(input)?;
1129        let input_envelope = encode_value_envelope(&normalize_arguments(input), DEFAULT_CODEC)?;
1130        let body = json!({
1131            "workflow_id": workflow_id,
1132            "workflow_type": workflow_type,
1133            "task_queue": task_queue,
1134            "input": input_envelope,
1135            "execution_timeout_seconds": options.execution_timeout_seconds,
1136            "run_timeout_seconds": options.run_timeout_seconds
1137        });
1138
1139        let data: Value = self
1140            .request_json(
1141                reqwest::Method::POST,
1142                "/workflows",
1143                RequestProtocol::ControlPlane,
1144                Some(&body),
1145            )
1146            .await?;
1147
1148        Ok(WorkflowHandle {
1149            client: self.clone(),
1150            workflow_id: data
1151                .get("workflow_id")
1152                .and_then(Value::as_str)
1153                .unwrap_or(workflow_id)
1154                .to_string(),
1155            run_id: data
1156                .get("run_id")
1157                .and_then(Value::as_str)
1158                .map(str::to_string),
1159            workflow_type: data
1160                .get("workflow_type")
1161                .and_then(Value::as_str)
1162                .unwrap_or(workflow_type)
1163                .to_string(),
1164        })
1165    }
1166
1167    pub async fn signal_workflow<T: Serialize>(
1168        &self,
1169        workflow_id: &str,
1170        signal_name: &str,
1171        input: T,
1172    ) -> Result<Value> {
1173        let input = serde_json::to_value(input)?;
1174        let input_envelope = encode_value_envelope(&normalize_arguments(input), DEFAULT_CODEC)?;
1175        let body = json!({
1176            "input": input_envelope
1177        });
1178        let path = format!("/workflows/{workflow_id}/signal/{signal_name}");
1179        self.request_json(
1180            reqwest::Method::POST,
1181            &path,
1182            RequestProtocol::ControlPlane,
1183            Some(&body),
1184        )
1185        .await
1186    }
1187
1188    /// Request cooperative cancellation of the current run for an instance.
1189    pub async fn cancel_workflow(
1190        &self,
1191        workflow_id: &str,
1192        options: WorkflowCommandOptions,
1193    ) -> Result<WorkflowCommandResult> {
1194        self.workflow_command(workflow_id, None, WorkflowCommandKind::Cancel, options)
1195            .await
1196    }
1197
1198    /// Request cooperative cancellation only if `run_id` is still current.
1199    pub async fn cancel_workflow_run(
1200        &self,
1201        workflow_id: &str,
1202        run_id: &str,
1203        options: WorkflowCommandOptions,
1204    ) -> Result<WorkflowCommandResult> {
1205        self.workflow_command(
1206            workflow_id,
1207            Some(run_id),
1208            WorkflowCommandKind::Cancel,
1209            options,
1210        )
1211        .await
1212    }
1213
1214    /// Forcefully terminate the current run for an instance.
1215    pub async fn terminate_workflow(
1216        &self,
1217        workflow_id: &str,
1218        options: WorkflowCommandOptions,
1219    ) -> Result<WorkflowCommandResult> {
1220        self.workflow_command(workflow_id, None, WorkflowCommandKind::Terminate, options)
1221            .await
1222    }
1223
1224    /// Forcefully terminate only if `run_id` is still current.
1225    pub async fn terminate_workflow_run(
1226        &self,
1227        workflow_id: &str,
1228        run_id: &str,
1229        options: WorkflowCommandOptions,
1230    ) -> Result<WorkflowCommandResult> {
1231        self.workflow_command(
1232            workflow_id,
1233            Some(run_id),
1234            WorkflowCommandKind::Terminate,
1235            options,
1236        )
1237        .await
1238    }
1239
1240    async fn workflow_command(
1241        &self,
1242        workflow_id: &str,
1243        run_id: Option<&str>,
1244        command: WorkflowCommandKind,
1245        options: WorkflowCommandOptions,
1246    ) -> Result<WorkflowCommandResult> {
1247        let path = match run_id {
1248            Some(run_id) => format!(
1249                "/workflows/{workflow_id}/runs/{run_id}/{}",
1250                command.as_str()
1251            ),
1252            None => format!("/workflows/{workflow_id}/{}", command.as_str()),
1253        };
1254        let data = match self
1255            .request_json(
1256                reqwest::Method::POST,
1257                &path,
1258                RequestProtocol::ControlPlane,
1259                Some(&options),
1260            )
1261            .await
1262        {
1263            Ok(data) => data,
1264            Err(Error::Http { status, body }) => {
1265                return Err(Error::WorkflowCommandRejected(workflow_command_rejection(
1266                    command,
1267                    status,
1268                    body,
1269                    workflow_id,
1270                    run_id,
1271                )));
1272            }
1273            Err(error) => return Err(error),
1274        };
1275
1276        Ok(workflow_command_result(command, data, workflow_id, run_id))
1277    }
1278
1279    /// Execute a named, read-only query against a running or completed workflow.
1280    ///
1281    /// Arguments and results use the platform payload envelope. Server and
1282    /// worker rejections are returned as [`Error::QueryFailed`] with a stable
1283    /// reason, HTTP status, and original response body.
1284    pub async fn query_workflow<T: Serialize>(
1285        &self,
1286        workflow_id: &str,
1287        query_name: &str,
1288        input: T,
1289    ) -> Result<Value> {
1290        let input = serde_json::to_value(input)?;
1291        let input_envelope = encode_value_envelope(&normalize_arguments(input), DEFAULT_CODEC)?;
1292        let body = json!({
1293            "input": input_envelope
1294        });
1295        let path = format!("/workflows/{workflow_id}/query/{query_name}");
1296        let response: Value = match self
1297            .request_json(
1298                reqwest::Method::POST,
1299                &path,
1300                RequestProtocol::ControlPlane,
1301                Some(&body),
1302            )
1303            .await
1304        {
1305            Ok(response) => response,
1306            Err(Error::Http { status, body }) => {
1307                return Err(Error::QueryFailed(query_failure(status, body)));
1308            }
1309            Err(error) => return Err(error),
1310        };
1311
1312        if let Some(envelope) = response
1313            .get("result_envelope")
1314            .filter(|envelope| !envelope.is_null())
1315        {
1316            return decode_wire_value(envelope, DEFAULT_CODEC);
1317        }
1318
1319        Ok(response.get("result").cloned().unwrap_or(Value::Null))
1320    }
1321
1322    pub async fn describe_workflow(&self, workflow_id: &str) -> Result<WorkflowDescription> {
1323        let path = format!("/workflows/{workflow_id}");
1324        let mut data: WorkflowDescription = self
1325            .request_json(
1326                reqwest::Method::GET,
1327                &path,
1328                RequestProtocol::ControlPlane,
1329                Option::<&Value>::None,
1330            )
1331            .await?;
1332        data.decode_payloads()?;
1333        Ok(data)
1334    }
1335
1336    /// Describe one selected run, including historical terminal runs.
1337    pub async fn describe_workflow_run(
1338        &self,
1339        workflow_id: &str,
1340        run_id: &str,
1341    ) -> Result<WorkflowDescription> {
1342        let path = format!("/workflows/{workflow_id}/runs/{run_id}");
1343        let mut data: WorkflowDescription = self
1344            .request_json(
1345                reqwest::Method::GET,
1346                &path,
1347                RequestProtocol::ControlPlane,
1348                Option::<&Value>::None,
1349            )
1350            .await?;
1351        data.decode_payloads()?;
1352        Ok(data)
1353    }
1354
1355    pub async fn register_worker(
1356        &self,
1357        worker_id: &str,
1358        task_queue: &str,
1359        supported_workflow_types: Vec<String>,
1360        supported_activity_types: Vec<String>,
1361        max_concurrent_workflow_tasks: usize,
1362        max_concurrent_activity_tasks: usize,
1363    ) -> Result<RegisterWorkerResponse> {
1364        self.register_worker_with_capabilities(
1365            worker_id,
1366            task_queue,
1367            supported_workflow_types,
1368            supported_activity_types,
1369            max_concurrent_workflow_tasks,
1370            max_concurrent_activity_tasks,
1371            Vec::new(),
1372        )
1373        .await
1374    }
1375
1376    /// Register a worker and explicitly advertise additive worker capabilities.
1377    pub async fn register_worker_with_capabilities(
1378        &self,
1379        worker_id: &str,
1380        task_queue: &str,
1381        supported_workflow_types: Vec<String>,
1382        supported_activity_types: Vec<String>,
1383        max_concurrent_workflow_tasks: usize,
1384        max_concurrent_activity_tasks: usize,
1385        capabilities: Vec<String>,
1386    ) -> Result<RegisterWorkerResponse> {
1387        let body = json!({
1388            "worker_id": worker_id,
1389            "task_queue": task_queue,
1390            "runtime": "rust",
1391            "sdk_version": SDK_VERSION,
1392            "supported_workflow_types": supported_workflow_types,
1393            "supported_activity_types": supported_activity_types,
1394            "capabilities": capabilities,
1395            "max_concurrent_workflow_tasks": max_concurrent_workflow_tasks,
1396            "max_concurrent_activity_tasks": max_concurrent_activity_tasks
1397        });
1398
1399        self.request_json(
1400            reqwest::Method::POST,
1401            "/worker/register",
1402            RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
1403            Some(&body),
1404        )
1405        .await
1406    }
1407
1408    /// Long-poll for an ephemeral, read-only workflow query task.
1409    pub async fn poll_query_task(
1410        &self,
1411        worker_id: &str,
1412        task_queue: &str,
1413        timeout: Duration,
1414    ) -> Result<Option<QueryTask>> {
1415        Ok(self
1416            .poll_query_task_response(worker_id, task_queue, timeout)
1417            .await?
1418            .task)
1419    }
1420
1421    /// Poll a query task while preserving server stop and drain metadata.
1422    pub async fn poll_query_task_response(
1423        &self,
1424        worker_id: &str,
1425        task_queue: &str,
1426        timeout: Duration,
1427    ) -> Result<PollQueryTaskResponse> {
1428        let timeout_seconds = long_poll_timeout_seconds(timeout);
1429        let body = json!({
1430            "worker_id": worker_id,
1431            "task_queue": task_queue,
1432            "poll_request_id": unique_request_id("rust-query-poll"),
1433            "timeout_seconds": timeout_seconds,
1434        });
1435        worker_poll_response(
1436            self.request_json_with_timeout(
1437                reqwest::Method::POST,
1438                "/worker/query-tasks/poll",
1439                RequestProtocol::Worker(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION),
1440                Some(&body),
1441                timeout + Duration::from_secs(5),
1442            )
1443            .await,
1444        )
1445    }
1446
1447    /// Complete a query task without appending workflow history.
1448    pub async fn complete_query_task(
1449        &self,
1450        query_task_id: &str,
1451        lease_owner: &str,
1452        query_task_attempt: u64,
1453        result: Value,
1454        codec: &str,
1455    ) -> Result<Value> {
1456        let result_envelope = encode_value_envelope(&result, codec)?;
1457        self.complete_query_task_with_envelope(
1458            query_task_id,
1459            lease_owner,
1460            query_task_attempt,
1461            result,
1462            result_envelope,
1463        )
1464        .await
1465    }
1466
1467    async fn complete_query_task_with_envelope(
1468        &self,
1469        query_task_id: &str,
1470        lease_owner: &str,
1471        query_task_attempt: u64,
1472        result: Value,
1473        result_envelope: Value,
1474    ) -> Result<Value> {
1475        let body = json!({
1476            "lease_owner": lease_owner,
1477            "query_task_attempt": query_task_attempt,
1478            "result": result,
1479            "result_envelope": result_envelope,
1480        });
1481        let path = format!("/worker/query-tasks/{query_task_id}/complete");
1482        let response = self
1483            .request_json(
1484                reqwest::Method::POST,
1485                &path,
1486                RequestProtocol::Worker(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION),
1487                Some(&body),
1488            )
1489            .await;
1490        query_task_response(response)
1491    }
1492
1493    /// Report a stable machine-readable query-task failure.
1494    pub async fn fail_query_task(
1495        &self,
1496        query_task_id: &str,
1497        lease_owner: &str,
1498        query_task_attempt: u64,
1499        message: impl Into<String>,
1500        reason: impl Into<String>,
1501        failure_type: impl Into<String>,
1502    ) -> Result<Value> {
1503        let body = json!({
1504            "lease_owner": lease_owner,
1505            "query_task_attempt": query_task_attempt,
1506            "failure": {
1507                "message": message.into(),
1508                "reason": reason.into(),
1509                "type": failure_type.into(),
1510            }
1511        });
1512        let path = format!("/worker/query-tasks/{query_task_id}/fail");
1513        let response = self
1514            .request_json(
1515                reqwest::Method::POST,
1516                &path,
1517                RequestProtocol::Worker(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION),
1518                Some(&body),
1519            )
1520            .await;
1521        query_task_response(response)
1522    }
1523
1524    pub async fn heartbeat_worker(
1525        &self,
1526        worker_id: &str,
1527        workflow_available: usize,
1528        activity_available: usize,
1529    ) -> Result<Value> {
1530        let body = json!({
1531            "worker_id": worker_id,
1532            "task_slots": {
1533                "workflow_available": workflow_available,
1534                "activity_available": activity_available
1535            },
1536            "process_metrics": {
1537                "process_id": std::process::id(),
1538                "process_uptime_seconds": 0
1539            }
1540        });
1541
1542        self.request_json(
1543            reqwest::Method::POST,
1544            "/worker/heartbeat",
1545            RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
1546            Some(&body),
1547        )
1548        .await
1549    }
1550
1551    pub async fn poll_workflow_task(
1552        &self,
1553        worker_id: &str,
1554        task_queue: &str,
1555        timeout: Duration,
1556    ) -> Result<Option<WorkflowTask>> {
1557        Ok(self
1558            .poll_workflow_task_response(worker_id, task_queue, timeout)
1559            .await?
1560            .task)
1561    }
1562
1563    pub async fn poll_workflow_task_response(
1564        &self,
1565        worker_id: &str,
1566        task_queue: &str,
1567        timeout: Duration,
1568    ) -> Result<PollWorkflowTaskResponse> {
1569        let body = json!({
1570            "worker_id": worker_id,
1571            "task_queue": task_queue,
1572            "timeout_seconds": long_poll_timeout_seconds(timeout),
1573        });
1574        let mut data: PollWorkflowTaskResponse = worker_poll_response(
1575            self.request_json_with_timeout(
1576                reqwest::Method::POST,
1577                "/worker/workflow-tasks/poll",
1578                RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
1579                Some(&body),
1580                timeout + Duration::from_secs(5),
1581            )
1582            .await,
1583        )?;
1584
1585        if let Some(task) = data.task.as_mut() {
1586            self.fetch_remaining_workflow_history(worker_id, task)
1587                .await?;
1588        }
1589
1590        Ok(data)
1591    }
1592
1593    async fn fetch_remaining_workflow_history(
1594        &self,
1595        worker_id: &str,
1596        task: &mut WorkflowTask,
1597    ) -> Result<()> {
1598        let mut next_token = task.next_history_page_token.clone();
1599
1600        while let Some(token) = next_token.take().filter(|token| !token.is_empty()) {
1601            let lease_owner = task
1602                .lease_owner
1603                .clone()
1604                .unwrap_or_else(|| worker_id.to_string());
1605            let page = self
1606                .workflow_task_history_page(
1607                    &task.task_id,
1608                    &lease_owner,
1609                    task.workflow_task_attempt,
1610                    &token,
1611                )
1612                .await?;
1613
1614            task.append_history_page(page);
1615
1616            if task.next_history_page_token.as_deref() == Some(token.as_str()) {
1617                return Err(Error::Codec(
1618                    "workflow history pagination returned the same page token".to_string(),
1619                ));
1620            }
1621
1622            next_token = task.next_history_page_token.clone();
1623        }
1624
1625        Ok(())
1626    }
1627
1628    async fn workflow_task_history_page(
1629        &self,
1630        task_id: &str,
1631        lease_owner: &str,
1632        workflow_task_attempt: u64,
1633        next_history_page_token: &str,
1634    ) -> Result<WorkflowTaskHistoryPage> {
1635        let body = json!({
1636            "lease_owner": lease_owner,
1637            "workflow_task_attempt": workflow_task_attempt,
1638            "next_history_page_token": next_history_page_token
1639        });
1640        let path = format!("/worker/workflow-tasks/{task_id}/history");
1641
1642        self.request_json(
1643            reqwest::Method::POST,
1644            &path,
1645            RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
1646            Some(&body),
1647        )
1648        .await
1649    }
1650
1651    pub async fn complete_workflow_task(
1652        &self,
1653        task_id: &str,
1654        lease_owner: &str,
1655        workflow_task_attempt: u64,
1656        commands: Vec<Value>,
1657    ) -> Result<Value> {
1658        let body = json!({
1659            "lease_owner": lease_owner,
1660            "workflow_task_attempt": workflow_task_attempt,
1661            "commands": commands
1662        });
1663        let path = format!("/worker/workflow-tasks/{task_id}/complete");
1664        self.request_json(
1665            reqwest::Method::POST,
1666            &path,
1667            RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
1668            Some(&body),
1669        )
1670        .await
1671    }
1672
1673    pub async fn fail_workflow_task(
1674        &self,
1675        task_id: &str,
1676        lease_owner: &str,
1677        workflow_task_attempt: u64,
1678        message: impl Into<String>,
1679    ) -> Result<Value> {
1680        self.fail_workflow_task_with_type(
1681            task_id,
1682            lease_owner,
1683            workflow_task_attempt,
1684            message,
1685            "RustWorkflowTaskFailure",
1686        )
1687        .await
1688    }
1689
1690    async fn fail_workflow_task_with_type(
1691        &self,
1692        task_id: &str,
1693        lease_owner: &str,
1694        workflow_task_attempt: u64,
1695        message: impl Into<String>,
1696        failure_type: &str,
1697    ) -> Result<Value> {
1698        let body = json!({
1699            "lease_owner": lease_owner,
1700            "workflow_task_attempt": workflow_task_attempt,
1701            "failure": {
1702                "message": message.into(),
1703                "type": failure_type
1704            }
1705        });
1706        let path = format!("/worker/workflow-tasks/{task_id}/fail");
1707        self.request_json(
1708            reqwest::Method::POST,
1709            &path,
1710            RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
1711            Some(&body),
1712        )
1713        .await
1714    }
1715
1716    pub async fn poll_activity_task(
1717        &self,
1718        worker_id: &str,
1719        task_queue: &str,
1720        timeout: Duration,
1721    ) -> Result<Option<ActivityTask>> {
1722        Ok(self
1723            .poll_activity_task_response(worker_id, task_queue, timeout)
1724            .await?
1725            .task)
1726    }
1727
1728    /// Poll an activity task while preserving server stop and drain metadata.
1729    pub async fn poll_activity_task_response(
1730        &self,
1731        worker_id: &str,
1732        task_queue: &str,
1733        timeout: Duration,
1734    ) -> Result<PollActivityTaskResponse> {
1735        let body = json!({
1736            "worker_id": worker_id,
1737            "task_queue": task_queue,
1738            "timeout_seconds": long_poll_timeout_seconds(timeout),
1739        });
1740        let data: PollActivityTaskResponse = worker_poll_response(
1741            self.request_json_with_timeout(
1742                reqwest::Method::POST,
1743                "/worker/activity-tasks/poll",
1744                RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
1745                Some(&body),
1746                timeout + Duration::from_secs(5),
1747            )
1748            .await,
1749        )?;
1750        Ok(data)
1751    }
1752
1753    pub async fn complete_activity_task(
1754        &self,
1755        task_id: &str,
1756        activity_attempt_id: &str,
1757        lease_owner: &str,
1758        result: Value,
1759        codec: &str,
1760    ) -> Result<Value> {
1761        let result = encode_value_envelope(&result, codec)?;
1762        let body = json!({
1763            "activity_attempt_id": activity_attempt_id,
1764            "lease_owner": lease_owner,
1765            "result": result
1766        });
1767        let path = format!("/worker/activity-tasks/{task_id}/complete");
1768        activity_task_response(
1769            self.request_json(
1770                reqwest::Method::POST,
1771                &path,
1772                RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
1773                Some(&body),
1774            )
1775            .await,
1776            "complete",
1777            task_id,
1778            activity_attempt_id,
1779        )
1780    }
1781
1782    pub async fn fail_activity_task(
1783        &self,
1784        task_id: &str,
1785        activity_attempt_id: &str,
1786        lease_owner: &str,
1787        message: impl Into<String>,
1788        non_retryable: bool,
1789    ) -> Result<Value> {
1790        let body = json!({
1791            "activity_attempt_id": activity_attempt_id,
1792            "lease_owner": lease_owner,
1793            "failure": {
1794                "message": message.into(),
1795                "type": "RustActivityFailure",
1796                "non_retryable": non_retryable
1797            }
1798        });
1799        let path = format!("/worker/activity-tasks/{task_id}/fail");
1800        activity_task_response(
1801            self.request_json(
1802                reqwest::Method::POST,
1803                &path,
1804                RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
1805                Some(&body),
1806            )
1807            .await,
1808            "fail",
1809            task_id,
1810            activity_attempt_id,
1811        )
1812    }
1813
1814    pub async fn heartbeat_activity_task(
1815        &self,
1816        task_id: &str,
1817        activity_attempt_id: &str,
1818        lease_owner: &str,
1819        details: Value,
1820    ) -> Result<ActivityHeartbeatResponse> {
1821        let body = json!({
1822            "activity_attempt_id": activity_attempt_id,
1823            "lease_owner": lease_owner,
1824            "details": details
1825        });
1826        let path = format!("/worker/activity-tasks/{task_id}/heartbeat");
1827        activity_task_response(
1828            self.request_json(
1829                reqwest::Method::POST,
1830                &path,
1831                RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
1832                Some(&body),
1833            )
1834            .await,
1835            "heartbeat",
1836            task_id,
1837            activity_attempt_id,
1838        )
1839    }
1840
1841    async fn request_json<T: DeserializeOwned, B: Serialize + ?Sized>(
1842        &self,
1843        method: reqwest::Method,
1844        path: &str,
1845        protocol: RequestProtocol,
1846        body: Option<&B>,
1847    ) -> Result<T> {
1848        self.request_json_with_timeout(method, path, protocol, body, Duration::from_secs(60))
1849            .await
1850    }
1851
1852    async fn request_json_with_timeout<T: DeserializeOwned, B: Serialize + ?Sized>(
1853        &self,
1854        method: reqwest::Method,
1855        path: &str,
1856        protocol: RequestProtocol,
1857        body: Option<&B>,
1858        timeout: Duration,
1859    ) -> Result<T> {
1860        let mut request = self
1861            .http
1862            .request(method, format!("{}/api{}", self.base_url, path))
1863            .timeout(timeout)
1864            .header(reqwest::header::ACCEPT, "application/json")
1865            .header(reqwest::header::CONTENT_TYPE, "application/json")
1866            .header("X-Namespace", &self.namespace);
1867
1868        match protocol {
1869            RequestProtocol::Worker(version) => {
1870                request = request.header("X-Durable-Workflow-Protocol-Version", version);
1871            }
1872            RequestProtocol::ControlPlane => {
1873                request = request.header(
1874                    "X-Durable-Workflow-Control-Plane-Version",
1875                    CONTROL_PLANE_VERSION,
1876                );
1877            }
1878        }
1879
1880        if let Some(token) = self.auth_token(protocol.is_worker()) {
1881            request = request.bearer_auth(token);
1882        }
1883
1884        if let Some(body) = body {
1885            request = request.json(body);
1886        }
1887
1888        let response = request.send().await?;
1889        let status = response.status();
1890        let bytes = response.bytes().await?;
1891
1892        if !status.is_success() {
1893            let body = String::from_utf8_lossy(&bytes).to_string();
1894            if let Some(protocol) = protocol_failure(status, &body) {
1895                return Err(Error::Protocol(protocol));
1896            }
1897            return Err(Error::Http { status, body });
1898        }
1899
1900        if bytes.is_empty() {
1901            return Ok(serde_json::from_value(Value::Null)?);
1902        }
1903
1904        Ok(serde_json::from_slice(&bytes)?)
1905    }
1906
1907    fn auth_token(&self, worker: bool) -> Option<&str> {
1908        if worker {
1909            self.worker_token
1910                .as_deref()
1911                .or(self.token.as_deref())
1912                .or(self.control_token.as_deref())
1913        } else {
1914            self.control_token
1915                .as_deref()
1916                .or(self.token.as_deref())
1917                .or(self.worker_token.as_deref())
1918        }
1919    }
1920}
1921
1922fn query_failure(status: reqwest::StatusCode, raw_body: String) -> QueryFailure {
1923    let body = serde_json::from_str(&raw_body).unwrap_or_else(|_| json!({"message": raw_body}));
1924    let reason = body
1925        .get("reason")
1926        .and_then(Value::as_str)
1927        .unwrap_or("query_rejected")
1928        .to_string();
1929    let message = body
1930        .get("message")
1931        .or_else(|| body.get("error"))
1932        .and_then(Value::as_str)
1933        .unwrap_or("workflow query was rejected")
1934        .to_string();
1935
1936    QueryFailure {
1937        status: status.as_u16(),
1938        reason,
1939        message,
1940        body,
1941    }
1942}
1943
1944fn workflow_command_result(
1945    command: WorkflowCommandKind,
1946    data: Value,
1947    workflow_id: &str,
1948    run_id: Option<&str>,
1949) -> WorkflowCommandResult {
1950    WorkflowCommandResult {
1951        command,
1952        workflow_id: data
1953            .get("workflow_id")
1954            .and_then(Value::as_str)
1955            .unwrap_or(workflow_id)
1956            .to_string(),
1957        run_id: data
1958            .get("run_id")
1959            .and_then(Value::as_str)
1960            .or(run_id)
1961            .map(str::to_string),
1962        outcome: data
1963            .get("outcome")
1964            .and_then(Value::as_str)
1965            .map(str::to_string),
1966        reason: data
1967            .get("reason")
1968            .and_then(Value::as_str)
1969            .map(str::to_string),
1970        command_status: data
1971            .get("command_status")
1972            .and_then(Value::as_str)
1973            .map(str::to_string),
1974        raw: data,
1975    }
1976}
1977
1978fn workflow_command_rejection(
1979    command: WorkflowCommandKind,
1980    status: reqwest::StatusCode,
1981    raw_body: String,
1982    workflow_id: &str,
1983    run_id: Option<&str>,
1984) -> WorkflowCommandRejection {
1985    let body = serde_json::from_str(&raw_body).unwrap_or_else(|_| json!({"message": raw_body}));
1986    WorkflowCommandRejection {
1987        command,
1988        status: status.as_u16(),
1989        reason: body
1990            .get("reason")
1991            .and_then(Value::as_str)
1992            .unwrap_or("workflow_command_rejected")
1993            .to_string(),
1994        message: body
1995            .get("message")
1996            .or_else(|| body.get("error"))
1997            .and_then(Value::as_str)
1998            .unwrap_or("workflow lifecycle command was rejected")
1999            .to_string(),
2000        workflow_id: body
2001            .get("workflow_id")
2002            .and_then(Value::as_str)
2003            .unwrap_or(workflow_id)
2004            .to_string(),
2005        run_id: body
2006            .get("run_id")
2007            .and_then(Value::as_str)
2008            .or(run_id)
2009            .map(str::to_string),
2010        target_scope: body
2011            .get("target_scope")
2012            .and_then(Value::as_str)
2013            .map(str::to_string),
2014        body,
2015    }
2016}
2017
2018fn query_task_response(response: Result<Value>) -> Result<Value> {
2019    match response {
2020        Err(Error::Http { status, body }) => Err(Error::QueryFailed(query_failure(status, body))),
2021        response => response,
2022    }
2023}
2024
2025fn worker_poll_response<T: DeserializeOwned>(response: Result<T>) -> Result<T> {
2026    match response {
2027        Err(Error::Http { status, body })
2028            if status == reqwest::StatusCode::CONFLICT && worker_poll_body_is_stop(&body) =>
2029        {
2030            Ok(serde_json::from_str(&body)?)
2031        }
2032        response => response,
2033    }
2034}
2035
2036fn worker_poll_body_is_stop(body: &str) -> bool {
2037    serde_json::from_str::<Value>(body)
2038        .ok()
2039        .is_some_and(|body| {
2040            worker_poll_is_stop(
2041                body.get("poll_status").and_then(Value::as_str),
2042                body.get("reason").and_then(Value::as_str),
2043            )
2044        })
2045}
2046
2047fn worker_poll_is_stop(poll_status: Option<&str>, reason: Option<&str>) -> bool {
2048    matches!(poll_status, Some("draining" | "stopped"))
2049        || matches!(reason, Some("worker_draining" | "worker_stopped"))
2050}
2051
2052fn query_task_rejection_is_final(error: &Error) -> bool {
2053    matches!(
2054        error,
2055        Error::QueryFailed(failure)
2056            if QUERY_TASK_FINAL_REJECTION_REASONS.contains(&failure.reason.as_str())
2057    )
2058}
2059
2060fn activity_task_response<T>(
2061    response: Result<T>,
2062    operation: &str,
2063    task_id: &str,
2064    activity_attempt_id: &str,
2065) -> Result<T> {
2066    match response {
2067        Err(Error::Http { status, body }) => {
2068            let body = serde_json::from_str(&body).unwrap_or_else(|_| json!({"message": body}));
2069            Err(Error::ActivityTaskRejected(ActivityTaskRejection {
2070                operation: operation.to_string(),
2071                status: status.as_u16(),
2072                reason: body
2073                    .get("reason")
2074                    .and_then(Value::as_str)
2075                    .unwrap_or("activity_task_rejected")
2076                    .to_string(),
2077                task_id: body
2078                    .get("task_id")
2079                    .and_then(Value::as_str)
2080                    .unwrap_or(task_id)
2081                    .to_string(),
2082                activity_attempt_id: body
2083                    .get("activity_attempt_id")
2084                    .and_then(Value::as_str)
2085                    .unwrap_or(activity_attempt_id)
2086                    .to_string(),
2087                cancel_requested: body
2088                    .get("cancel_requested")
2089                    .and_then(Value::as_bool)
2090                    .unwrap_or(false),
2091                can_continue: body.get("can_continue").and_then(Value::as_bool),
2092                run_closed_reason: body
2093                    .get("run_closed_reason")
2094                    .and_then(Value::as_str)
2095                    .map(str::to_string),
2096                body,
2097            }))
2098        }
2099        response => response,
2100    }
2101}
2102
2103fn activity_task_rejection_is_final(error: &Error) -> bool {
2104    matches!(
2105        error,
2106        Error::ActivityTaskRejected(rejection)
2107            if matches!(
2108                rejection.reason.as_str(),
2109                "run_cancelled"
2110                    | "run_terminated"
2111                    | "attempt_closed"
2112                    | "stale_attempt"
2113                    | "activity_cancelled"
2114                    | "task_cancelled"
2115                    | "run_closed"
2116                    | "activity_not_running"
2117                    | "attempt_not_found"
2118            )
2119    )
2120}
2121
2122fn workflow_task_completion_is_terminal_timeout(
2123    error: &Error,
2124    task_id: &str,
2125    workflow_task_attempt: u64,
2126    run_id: Option<&str>,
2127) -> bool {
2128    let Error::Http { status, body } = error else {
2129        return false;
2130    };
2131    if *status != reqwest::StatusCode::CONFLICT {
2132        return false;
2133    }
2134
2135    let Some(run_id) = run_id else {
2136        return false;
2137    };
2138    let Ok(body) = serde_json::from_str::<Value>(body) else {
2139        return false;
2140    };
2141
2142    body.get("recorded").and_then(Value::as_bool) == Some(false)
2143        && body.get("reason").and_then(Value::as_str) == Some("run_timed_out")
2144        && body.get("run_status").and_then(Value::as_str) == Some("failed")
2145        && body.get("run_id").and_then(Value::as_str) == Some(run_id)
2146        && body.get("task_id").and_then(Value::as_str) == Some(task_id)
2147        && body.get("workflow_task_attempt").and_then(Value::as_u64) == Some(workflow_task_attempt)
2148}
2149
2150fn protocol_failure(status: reqwest::StatusCode, raw_body: &str) -> Option<ProtocolFailure> {
2151    let body: Value = serde_json::from_str(raw_body).ok()?;
2152    let reason = body.get("reason")?.as_str()?;
2153    if !matches!(
2154        reason,
2155        "missing_protocol_version"
2156            | "unsupported_protocol_version"
2157            | "missing_control_plane_version"
2158            | "unsupported_control_plane_version"
2159    ) {
2160        return None;
2161    }
2162
2163    Some(ProtocolFailure {
2164        status: status.as_u16(),
2165        reason: reason.to_string(),
2166        message: body
2167            .get("message")
2168            .or_else(|| body.get("error"))
2169            .and_then(Value::as_str)
2170            .unwrap_or("protocol version rejected")
2171            .to_string(),
2172        supported_version: body
2173            .get("supported_version")
2174            .and_then(Value::as_str)
2175            .map(str::to_string),
2176        requested_version: body
2177            .get("requested_version")
2178            .and_then(Value::as_str)
2179            .map(str::to_string),
2180        body,
2181    })
2182}
2183
2184fn long_poll_timeout_seconds(timeout: Duration) -> u64 {
2185    timeout
2186        .as_secs()
2187        .saturating_add(u64::from(timeout.subsec_nanos() > 0))
2188        .min(MAX_LONG_POLL_TIMEOUT_SECONDS)
2189}
2190
2191fn worker_operation_is_retryable(error: &Error) -> bool {
2192    match error {
2193        Error::Transport(error) => {
2194            error.is_timeout() || error.is_connect() || error.is_request() || error.is_body()
2195        }
2196        Error::Http { status, .. } => {
2197            matches!(
2198                *status,
2199                reqwest::StatusCode::REQUEST_TIMEOUT | reqwest::StatusCode::TOO_MANY_REQUESTS
2200            ) || status.is_server_error()
2201        }
2202        _ => false,
2203    }
2204}
2205
2206fn worker_retry_delay(policy: WorkerRetryPolicy, retry: usize) -> Duration {
2207    let exponent = retry.saturating_sub(1).min(31) as u32;
2208    policy
2209        .initial_backoff
2210        .saturating_mul(1_u32 << exponent)
2211        .min(policy.max_backoff)
2212}
2213
2214#[derive(Debug)]
2215pub struct ClientBuilder {
2216    base_url: String,
2217    token: Option<String>,
2218    control_token: Option<String>,
2219    worker_token: Option<String>,
2220    namespace: String,
2221    timeout: Duration,
2222}
2223
2224impl ClientBuilder {
2225    pub fn token(mut self, token: Option<String>) -> Self {
2226        self.token = token;
2227        self
2228    }
2229
2230    pub fn control_token(mut self, token: Option<String>) -> Self {
2231        self.control_token = token;
2232        self
2233    }
2234
2235    pub fn worker_token(mut self, token: Option<String>) -> Self {
2236        self.worker_token = token;
2237        self
2238    }
2239
2240    pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
2241        self.namespace = namespace.into();
2242        self
2243    }
2244
2245    pub fn timeout(mut self, timeout: Duration) -> Self {
2246        self.timeout = timeout;
2247        self
2248    }
2249
2250    pub fn build(self) -> Result<Client> {
2251        Ok(Client {
2252            http: reqwest::Client::builder().timeout(self.timeout).build()?,
2253            base_url: self.base_url.trim_end_matches('/').to_string(),
2254            token: self.token,
2255            control_token: self.control_token,
2256            worker_token: self.worker_token,
2257            namespace: self.namespace,
2258        })
2259    }
2260}
2261
2262#[derive(Clone, Debug)]
2263pub struct WorkflowHandle {
2264    client: Client,
2265    pub workflow_id: String,
2266    pub run_id: Option<String>,
2267    pub workflow_type: String,
2268}
2269
2270impl WorkflowHandle {
2271    pub async fn describe(&self) -> Result<WorkflowDescription> {
2272        self.client.describe_workflow(&self.workflow_id).await
2273    }
2274
2275    pub async fn signal<T: Serialize>(&self, signal_name: &str, input: T) -> Result<Value> {
2276        self.client
2277            .signal_workflow(&self.workflow_id, signal_name, input)
2278            .await
2279    }
2280
2281    /// Request cooperative cancellation of whichever run is current.
2282    pub async fn cancel(&self, options: WorkflowCommandOptions) -> Result<WorkflowCommandResult> {
2283        self.client
2284            .cancel_workflow(&self.workflow_id, options)
2285            .await
2286    }
2287
2288    /// Request cancellation only if this handle's selected run is still current.
2289    pub async fn cancel_selected_run(
2290        &self,
2291        options: WorkflowCommandOptions,
2292    ) -> Result<WorkflowCommandResult> {
2293        let run_id = self.run_id.as_deref().ok_or_else(|| {
2294            Error::Codec("run_id is required for selected-run cancellation".to_string())
2295        })?;
2296        self.client
2297            .cancel_workflow_run(&self.workflow_id, run_id, options)
2298            .await
2299    }
2300
2301    /// Forcefully terminate whichever run is current.
2302    pub async fn terminate(
2303        &self,
2304        options: WorkflowCommandOptions,
2305    ) -> Result<WorkflowCommandResult> {
2306        self.client
2307            .terminate_workflow(&self.workflow_id, options)
2308            .await
2309    }
2310
2311    /// Terminate only if this handle's selected run is still current.
2312    pub async fn terminate_selected_run(
2313        &self,
2314        options: WorkflowCommandOptions,
2315    ) -> Result<WorkflowCommandResult> {
2316        let run_id = self.run_id.as_deref().ok_or_else(|| {
2317            Error::Codec("run_id is required for selected-run termination".to_string())
2318        })?;
2319        self.client
2320            .terminate_workflow_run(&self.workflow_id, run_id, options)
2321            .await
2322    }
2323
2324    /// Execute a named, read-only query against this workflow.
2325    pub async fn query<T: Serialize>(&self, query_name: &str, input: T) -> Result<Value> {
2326        self.client
2327            .query_workflow(&self.workflow_id, query_name, input)
2328            .await
2329    }
2330
2331    pub async fn result(&self, options: WorkflowResultOptions) -> Result<Value> {
2332        let started = Instant::now();
2333
2334        loop {
2335            let description = match self.run_id.as_deref() {
2336                Some(run_id) => {
2337                    self.client
2338                        .describe_workflow_run(&self.workflow_id, run_id)
2339                        .await?
2340                }
2341                None => self.describe().await?,
2342            };
2343            if description.is_completed() {
2344                return Ok(description.output.unwrap_or(Value::Null));
2345            }
2346
2347            if description.is_terminal() {
2348                let outcome = workflow_terminal_outcome(
2349                    &description,
2350                    &self.workflow_id,
2351                    self.run_id.as_deref(),
2352                );
2353                return Err(match outcome.kind {
2354                    WorkflowTerminalKind::Failed => Error::WorkflowFailed(outcome),
2355                    WorkflowTerminalKind::Cancelled => Error::WorkflowCancelled(outcome),
2356                    WorkflowTerminalKind::Terminated => Error::WorkflowTerminated(outcome),
2357                    WorkflowTerminalKind::TimedOut => Error::WorkflowTimedOut(outcome),
2358                });
2359            }
2360
2361            if started.elapsed() >= options.timeout {
2362                return Err(Error::WorkflowTimedOut(WorkflowTerminalOutcome {
2363                    kind: WorkflowTerminalKind::TimedOut,
2364                    workflow_id: description
2365                        .workflow_id
2366                        .clone()
2367                        .unwrap_or_else(|| self.workflow_id.clone()),
2368                    run_id: description.run_id.clone().or_else(|| self.run_id.clone()),
2369                    reason: "result_wait_timeout".to_string(),
2370                    failure_category: Some("client_timeout".to_string()),
2371                    failure_id: None,
2372                    exception_type: None,
2373                    exception_class: None,
2374                    non_retryable: None,
2375                    message: Some(format!(
2376                        "workflow result was not terminal within {:?}",
2377                        options.timeout
2378                    )),
2379                    exception: None,
2380                    raw: description.raw_value(),
2381                }));
2382            }
2383
2384            tokio::time::sleep(options.poll_interval).await;
2385        }
2386    }
2387}
2388
2389#[derive(Clone, Copy, Debug)]
2390pub struct WorkflowResultOptions {
2391    pub poll_interval: Duration,
2392    pub timeout: Duration,
2393}
2394
2395impl Default for WorkflowResultOptions {
2396    fn default() -> Self {
2397        Self {
2398            poll_interval: Duration::from_millis(500),
2399            timeout: Duration::from_secs(30),
2400        }
2401    }
2402}
2403
2404#[derive(Clone, Debug, Deserialize)]
2405pub struct WorkflowDescription {
2406    pub workflow_id: Option<String>,
2407    pub run_id: Option<String>,
2408    pub workflow_type: Option<String>,
2409    pub status: Option<String>,
2410    #[serde(default)]
2411    pub closed_reason: Option<String>,
2412    #[serde(default)]
2413    pub error: Option<String>,
2414    #[serde(default)]
2415    pub failure: Option<Value>,
2416    #[serde(default)]
2417    pub exception: Option<Value>,
2418    #[serde(default)]
2419    pub failures: Vec<Value>,
2420    #[serde(default)]
2421    pub output: Option<Value>,
2422    #[serde(default)]
2423    pub output_envelope: Option<Value>,
2424    #[serde(flatten)]
2425    pub raw: HashMap<String, Value>,
2426}
2427
2428impl WorkflowDescription {
2429    pub fn is_completed(&self) -> bool {
2430        matches!(self.status.as_deref(), Some("completed" | "Completed"))
2431    }
2432
2433    pub fn is_terminal(&self) -> bool {
2434        matches!(
2435            self.status.as_deref(),
2436            Some(
2437                "completed"
2438                    | "Completed"
2439                    | "failed"
2440                    | "Failed"
2441                    | "cancelled"
2442                    | "Cancelled"
2443                    | "terminated"
2444                    | "Terminated"
2445                    | "timed_out"
2446                    | "TimedOut",
2447            )
2448        )
2449    }
2450
2451    fn decode_payloads(&mut self) -> Result<()> {
2452        if let Some(envelope) = &self.output_envelope {
2453            self.output = Some(decode_wire_value(envelope, DEFAULT_CODEC)?);
2454        }
2455
2456        Ok(())
2457    }
2458
2459    fn raw_value(&self) -> Value {
2460        let mut data = self.raw.clone();
2461        data.insert(
2462            "workflow_id".to_string(),
2463            self.workflow_id
2464                .clone()
2465                .map(Value::String)
2466                .unwrap_or(Value::Null),
2467        );
2468        data.insert(
2469            "run_id".to_string(),
2470            self.run_id
2471                .clone()
2472                .map(Value::String)
2473                .unwrap_or(Value::Null),
2474        );
2475        data.insert(
2476            "workflow_type".to_string(),
2477            self.workflow_type
2478                .clone()
2479                .map(Value::String)
2480                .unwrap_or(Value::Null),
2481        );
2482        data.insert(
2483            "status".to_string(),
2484            self.status
2485                .clone()
2486                .map(Value::String)
2487                .unwrap_or(Value::Null),
2488        );
2489        data.insert(
2490            "closed_reason".to_string(),
2491            self.closed_reason
2492                .clone()
2493                .map(Value::String)
2494                .unwrap_or(Value::Null),
2495        );
2496        if let Some(failure) = &self.failure {
2497            data.insert("failure".to_string(), failure.clone());
2498        }
2499        if let Some(exception) = &self.exception {
2500            data.insert("exception".to_string(), exception.clone());
2501        }
2502        Value::Object(data.into_iter().collect())
2503    }
2504}
2505
2506fn workflow_terminal_outcome(
2507    description: &WorkflowDescription,
2508    workflow_id: &str,
2509    run_id: Option<&str>,
2510) -> WorkflowTerminalOutcome {
2511    let terminal_kind = description
2512        .closed_reason
2513        .as_deref()
2514        .or(description.status.as_deref())
2515        .unwrap_or("failed")
2516        .to_ascii_lowercase();
2517    let kind = match terminal_kind.as_str() {
2518        "cancelled" | "canceled" => WorkflowTerminalKind::Cancelled,
2519        "terminated" => WorkflowTerminalKind::Terminated,
2520        "timed_out" | "timedout" => WorkflowTerminalKind::TimedOut,
2521        _ => WorkflowTerminalKind::Failed,
2522    };
2523    let default_reason = match kind {
2524        WorkflowTerminalKind::Failed => "workflow_failed",
2525        WorkflowTerminalKind::Cancelled => "cancelled",
2526        WorkflowTerminalKind::Terminated => "terminated",
2527        WorkflowTerminalKind::TimedOut => "timed_out",
2528    };
2529    let failure = description
2530        .failure
2531        .as_ref()
2532        .filter(|value| value.is_object());
2533    let nested_failure = failure
2534        .and_then(|value| value.get("failures"))
2535        .and_then(Value::as_array)
2536        .and_then(|failures| failures.last())
2537        .or_else(|| description.failures.last());
2538    let exception = description
2539        .exception
2540        .clone()
2541        .or_else(|| failure.and_then(|value| value.get("exception")).cloned())
2542        .or_else(|| {
2543            nested_failure
2544                .and_then(|value| value.get("exception_payload"))
2545                .cloned()
2546        });
2547    let string_field = |name: &str| {
2548        failure
2549            .and_then(|value| value.get(name))
2550            .and_then(Value::as_str)
2551            .or_else(|| {
2552                nested_failure
2553                    .and_then(|value| value.get(name))
2554                    .and_then(Value::as_str)
2555            })
2556            .map(str::to_string)
2557    };
2558    let exception_field = |name: &str| {
2559        exception
2560            .as_ref()
2561            .and_then(|value| value.get(name))
2562            .and_then(Value::as_str)
2563            .map(str::to_string)
2564    };
2565    let message = description
2566        .error
2567        .clone()
2568        .or_else(|| string_field("message"))
2569        .or_else(|| exception_field("message"));
2570    let reason = description
2571        .raw
2572        .get("reason")
2573        .and_then(Value::as_str)
2574        .map(str::to_string)
2575        .or_else(|| {
2576            failure
2577                .and_then(|value| value.get("reason"))
2578                .and_then(Value::as_str)
2579                .map(str::to_string)
2580        })
2581        .or_else(|| description.closed_reason.clone())
2582        .unwrap_or_else(|| default_reason.to_string());
2583    let failure_id = string_field("failure_id").or_else(|| {
2584        nested_failure
2585            .and_then(|value| value.get("id"))
2586            .and_then(Value::as_str)
2587            .map(str::to_string)
2588    });
2589
2590    WorkflowTerminalOutcome {
2591        kind,
2592        workflow_id: description
2593            .workflow_id
2594            .clone()
2595            .unwrap_or_else(|| workflow_id.to_string()),
2596        run_id: description
2597            .run_id
2598            .clone()
2599            .or_else(|| run_id.map(str::to_string)),
2600        reason,
2601        failure_category: string_field("failure_category")
2602            .or_else(|| Some(default_reason.to_string())),
2603        failure_id,
2604        exception_type: string_field("exception_type").or_else(|| exception_field("type")),
2605        exception_class: string_field("exception_class").or_else(|| exception_field("class")),
2606        non_retryable: failure
2607            .and_then(|value| value.get("non_retryable"))
2608            .and_then(Value::as_bool)
2609            .or_else(|| {
2610                nested_failure
2611                    .and_then(|value| value.get("non_retryable"))
2612                    .and_then(Value::as_bool)
2613            }),
2614        message,
2615        exception,
2616        raw: description.raw_value(),
2617    }
2618}
2619
2620#[derive(Clone, Debug, Deserialize)]
2621pub struct RegisterWorkerResponse {
2622    pub worker_id: String,
2623    pub registered: bool,
2624    #[serde(default)]
2625    pub heartbeat_interval_seconds: Option<u64>,
2626    #[serde(default)]
2627    pub protocol_version: Option<String>,
2628    #[serde(default)]
2629    pub server_capabilities: Option<Value>,
2630}
2631
2632#[derive(Clone, Debug, Deserialize)]
2633pub struct PollWorkflowTaskResponse {
2634    #[serde(default)]
2635    pub task: Option<WorkflowTask>,
2636    #[serde(default)]
2637    pub poll_status: Option<String>,
2638    #[serde(default)]
2639    pub reason: Option<String>,
2640    #[serde(default)]
2641    pub protocol_version: Option<String>,
2642    #[serde(default)]
2643    pub server_capabilities: Option<Value>,
2644}
2645
2646impl PollWorkflowTaskResponse {
2647    /// Classify this response without parsing server display text.
2648    pub fn outcome(&self) -> WorkerPollOutcome {
2649        worker_poll_outcome(
2650            self.task.is_some(),
2651            self.poll_status.as_deref(),
2652            self.reason.as_deref(),
2653        )
2654    }
2655}
2656
2657#[derive(Clone, Debug, Deserialize)]
2658pub struct PollActivityTaskResponse {
2659    #[serde(default)]
2660    pub task: Option<ActivityTask>,
2661    #[serde(default)]
2662    pub poll_status: Option<String>,
2663    #[serde(default)]
2664    pub reason: Option<String>,
2665}
2666
2667impl PollActivityTaskResponse {
2668    /// Classify this response without parsing server display text.
2669    pub fn outcome(&self) -> WorkerPollOutcome {
2670        worker_poll_outcome(
2671            self.task.is_some(),
2672            self.poll_status.as_deref(),
2673            self.reason.as_deref(),
2674        )
2675    }
2676}
2677
2678#[derive(Clone, Debug, Deserialize)]
2679pub struct PollQueryTaskResponse {
2680    #[serde(default)]
2681    pub task: Option<QueryTask>,
2682    #[serde(default)]
2683    pub poll_status: Option<String>,
2684    #[serde(default)]
2685    pub reason: Option<String>,
2686}
2687
2688impl PollQueryTaskResponse {
2689    /// Classify this response without parsing server display text.
2690    pub fn outcome(&self) -> WorkerPollOutcome {
2691        worker_poll_outcome(
2692            self.task.is_some(),
2693            self.poll_status.as_deref(),
2694            self.reason.as_deref(),
2695        )
2696    }
2697}
2698
2699/// Stable classification for worker poll responses.
2700#[derive(Clone, Debug, PartialEq, Eq)]
2701pub enum WorkerPollOutcome {
2702    /// A task was leased and is available on the response.
2703    Task,
2704    /// No task was leased, but the worker should continue polling.
2705    Idle {
2706        poll_status: Option<String>,
2707        reason: Option<String>,
2708    },
2709    /// The server asked this worker to stop claiming new work.
2710    Stop {
2711        poll_status: Option<String>,
2712        reason: Option<String>,
2713    },
2714}
2715
2716impl WorkerPollOutcome {
2717    pub fn should_stop(&self) -> bool {
2718        matches!(self, Self::Stop { .. })
2719    }
2720}
2721
2722fn worker_poll_outcome(
2723    has_task: bool,
2724    poll_status: Option<&str>,
2725    reason: Option<&str>,
2726) -> WorkerPollOutcome {
2727    if worker_poll_is_stop(poll_status, reason) {
2728        return WorkerPollOutcome::Stop {
2729            poll_status: poll_status.map(str::to_string),
2730            reason: reason.map(str::to_string),
2731        };
2732    }
2733
2734    if has_task {
2735        WorkerPollOutcome::Task
2736    } else {
2737        WorkerPollOutcome::Idle {
2738            poll_status: poll_status.map(str::to_string),
2739            reason: reason.map(str::to_string),
2740        }
2741    }
2742}
2743
2744/// An ephemeral server-routed query task.
2745#[derive(Clone, Debug, Deserialize)]
2746pub struct QueryTask {
2747    pub query_task_id: String,
2748    #[serde(default = "default_workflow_task_attempt")]
2749    pub query_task_attempt: u64,
2750    #[serde(default)]
2751    pub lease_owner: Option<String>,
2752    #[serde(default)]
2753    pub workflow_id: Option<String>,
2754    #[serde(default)]
2755    pub run_id: Option<String>,
2756    pub workflow_type: String,
2757    pub query_name: String,
2758    #[serde(default = "default_payload_codec")]
2759    pub payload_codec: String,
2760    #[serde(default)]
2761    pub workflow_arguments: Option<Value>,
2762    #[serde(default)]
2763    pub query_arguments: Option<Value>,
2764    #[serde(default)]
2765    pub history_events: Vec<HistoryEvent>,
2766    #[serde(default)]
2767    pub history_export: Option<Value>,
2768    #[serde(default)]
2769    pub run_status: Option<String>,
2770}
2771
2772#[derive(Clone, Debug, Deserialize)]
2773pub struct WorkflowTask {
2774    pub task_id: String,
2775    #[serde(default)]
2776    pub workflow_id: Option<String>,
2777    #[serde(default)]
2778    pub run_id: Option<String>,
2779    pub workflow_type: String,
2780    #[serde(default = "default_payload_codec")]
2781    pub payload_codec: String,
2782    #[serde(default)]
2783    pub arguments: Option<Value>,
2784    #[serde(default)]
2785    pub history_events: Vec<HistoryEvent>,
2786    #[serde(default)]
2787    pub total_history_events: Option<u64>,
2788    #[serde(default)]
2789    pub next_history_page_token: Option<String>,
2790    #[serde(default = "default_workflow_task_attempt")]
2791    pub workflow_task_attempt: u64,
2792    #[serde(default)]
2793    pub workflow_signal_id: Option<String>,
2794    #[serde(default)]
2795    pub signal_name: Option<String>,
2796    #[serde(default)]
2797    pub signal_arguments: Option<Value>,
2798    #[serde(default)]
2799    pub lease_owner: Option<String>,
2800}
2801
2802impl WorkflowTask {
2803    fn append_history_page(&mut self, page: WorkflowTaskHistoryPage) {
2804        self.history_events.extend(page.history_events);
2805
2806        if page.total_history_events.is_some() {
2807            self.total_history_events = page.total_history_events;
2808        }
2809
2810        self.next_history_page_token = page
2811            .next_history_page_token
2812            .filter(|token| !token.is_empty());
2813    }
2814}
2815
2816#[derive(Clone, Debug, Deserialize)]
2817struct WorkflowTaskHistoryPage {
2818    #[serde(default)]
2819    history_events: Vec<HistoryEvent>,
2820    #[serde(default)]
2821    total_history_events: Option<u64>,
2822    #[serde(default)]
2823    next_history_page_token: Option<String>,
2824}
2825
2826#[derive(Clone, Debug, Deserialize)]
2827pub struct ActivityTask {
2828    pub task_id: String,
2829    #[serde(default)]
2830    pub activity_attempt_id: Option<String>,
2831    #[serde(default)]
2832    pub attempt_id: Option<String>,
2833    pub activity_type: String,
2834    #[serde(default = "default_payload_codec")]
2835    pub payload_codec: String,
2836    #[serde(default)]
2837    pub arguments: Option<Value>,
2838    #[serde(default = "default_attempt_number")]
2839    pub attempt_number: u64,
2840    #[serde(default)]
2841    pub lease_owner: Option<String>,
2842}
2843
2844#[derive(Clone, Debug, Deserialize)]
2845pub struct HistoryEvent {
2846    #[serde(alias = "type")]
2847    pub event_type: String,
2848    #[serde(default)]
2849    pub payload: Value,
2850    #[serde(flatten)]
2851    pub raw: HashMap<String, Value>,
2852}
2853
2854/// One decoded signal in the committed workflow-history snapshot.
2855#[derive(Clone, Debug, PartialEq)]
2856pub struct QuerySignal {
2857    pub id: Option<String>,
2858    pub name: String,
2859    pub arguments: Vec<Value>,
2860    pub workflow_sequence: Option<u64>,
2861}
2862
2863/// Immutable state supplied to a registered query handler.
2864///
2865/// This context intentionally exposes no activity, signal-wait, or command
2866/// APIs. Query handlers inspect committed history and return a value; query
2867/// completion does not append an event or advance deterministic execution.
2868#[derive(Clone, Debug)]
2869pub struct QueryContext {
2870    pub workflow_id: Option<String>,
2871    pub run_id: Option<String>,
2872    pub workflow_type: String,
2873    pub run_status: Option<String>,
2874    workflow_input: Value,
2875    history_events: Arc<Vec<HistoryEvent>>,
2876    signal_events: Arc<Vec<QuerySignal>>,
2877}
2878
2879impl QueryContext {
2880    /// The normalized argument list used to start the workflow.
2881    pub fn workflow_input(&self) -> &Value {
2882        &self.workflow_input
2883    }
2884
2885    /// The immutable committed history used for this query snapshot.
2886    pub fn history_events(&self) -> &[HistoryEvent] {
2887        self.history_events.as_slice()
2888    }
2889
2890    /// All decoded signals in committed workflow order.
2891    pub fn signal_events(&self) -> &[QuerySignal] {
2892        self.signal_events.as_slice()
2893    }
2894
2895    /// Decoded argument lists for each committed signal with `signal_name`.
2896    pub fn signals(&self, signal_name: &str) -> Vec<Vec<Value>> {
2897        self.signal_events
2898            .iter()
2899            .filter(|signal| signal.name == signal_name)
2900            .map(|signal| signal.arguments.clone())
2901            .collect()
2902    }
2903}
2904
2905#[derive(Clone, Debug, Deserialize)]
2906pub struct ActivityHeartbeatResponse {
2907    #[serde(default)]
2908    pub cancel_requested: bool,
2909    #[serde(default)]
2910    pub heartbeat_recorded: bool,
2911    #[serde(default)]
2912    pub can_continue: Option<bool>,
2913    #[serde(default)]
2914    pub reason: Option<String>,
2915    #[serde(default)]
2916    pub run_closed_reason: Option<String>,
2917    #[serde(default)]
2918    pub run_closed_at: Option<String>,
2919    #[serde(default)]
2920    pub lease_expires_at: Option<String>,
2921    #[serde(default)]
2922    pub last_heartbeat_at: Option<String>,
2923}
2924
2925impl ActivityHeartbeatResponse {
2926    /// Whether the activity should stop instead of attempting completion.
2927    pub fn should_stop(&self) -> bool {
2928        self.cancel_requested || self.can_continue == Some(false)
2929    }
2930}
2931
2932fn default_payload_codec() -> String {
2933    DEFAULT_CODEC.to_string()
2934}
2935
2936fn default_workflow_task_attempt() -> u64 {
2937    1
2938}
2939
2940fn default_attempt_number() -> u64 {
2941    1
2942}
2943
2944type WorkflowFuture = Pin<Box<dyn Future<Output = Result<Value>> + Send + 'static>>;
2945type WorkflowHandler = Arc<dyn Fn(WorkflowContext, Value) -> WorkflowFuture + Send + Sync>;
2946type ErasedWorkflowState = Arc<dyn Any + Send + Sync>;
2947type WorkflowStateSnapshot = Arc<dyn Fn() -> Result<ErasedWorkflowState> + Send + Sync>;
2948type ReplayedWorkflowHandler =
2949    Arc<dyn Fn(WorkflowContext, Value) -> ReplayedWorkflowInvocation + Send + Sync>;
2950type ActivityFuture = Pin<Box<dyn Future<Output = Result<Value>> + Send + 'static>>;
2951type ActivityHandler = Arc<dyn Fn(ActivityContext, Value) -> ActivityFuture + Send + Sync>;
2952type QueryFuture = Pin<Box<dyn Future<Output = Result<Value>> + Send + 'static>>;
2953type QueryHandler = Arc<dyn Fn(QueryContext, Value) -> QueryFuture + Send + Sync>;
2954type ReplayedQueryHandler = Arc<
2955    dyn Fn(QueryContext, ErasedWorkflowState, Value) -> std::result::Result<QueryFuture, String>
2956        + Send
2957        + Sync,
2958>;
2959type WorkerHeartbeatObserver = Arc<dyn Fn(&WorkerHeartbeatObservation) + Send + Sync>;
2960
2961struct ReplayedWorkflowInvocation {
2962    future: WorkflowFuture,
2963    snapshot: WorkflowStateSnapshot,
2964}
2965
2966#[derive(Clone)]
2967struct RegisteredWorkflow {
2968    execute: WorkflowHandler,
2969    replay: Option<ReplayedWorkflowHandler>,
2970    state_type: Option<TypeId>,
2971}
2972
2973#[derive(Clone)]
2974enum RegisteredQuery {
2975    Snapshot(QueryHandler),
2976    Replayed {
2977        state_type: TypeId,
2978        handler: ReplayedQueryHandler,
2979    },
2980}
2981
2982#[derive(Clone, Debug)]
2983pub struct WorkerHeartbeatObservation {
2984    pub worker_id: String,
2985    pub task_queue: String,
2986    pub acknowledged_at_unix_millis: u64,
2987    pub acknowledgement: Value,
2988}
2989
2990/// Bounded retry policy for worker poll acquisition and worker heartbeats.
2991///
2992/// Expected empty long polls are normal successful responses. Transport
2993/// failures, HTTP 408/429 responses, and server errors are retried with capped
2994/// exponential backoff. Authentication, protocol, codec, and handler failures
2995/// are never retried by the worker.
2996#[derive(Clone, Copy, Debug)]
2997pub struct WorkerRetryPolicy {
2998    /// Number of retries after the initial request fails.
2999    pub max_retries: usize,
3000    /// Delay before the first retry.
3001    pub initial_backoff: Duration,
3002    /// Maximum delay between retries.
3003    pub max_backoff: Duration,
3004}
3005
3006impl Default for WorkerRetryPolicy {
3007    fn default() -> Self {
3008        Self {
3009            max_retries: 5,
3010            initial_backoff: Duration::from_millis(100),
3011            max_backoff: Duration::from_secs(5),
3012        }
3013    }
3014}
3015
3016#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3017enum ManagedPollOutcome {
3018    Idle,
3019    Handled,
3020    Stop,
3021}
3022
3023#[derive(Clone)]
3024pub struct Worker {
3025    client: Client,
3026    worker_id: String,
3027    task_queue: String,
3028    workflows: HashMap<String, RegisteredWorkflow>,
3029    activities: HashMap<String, ActivityHandler>,
3030    queries: HashMap<String, HashMap<String, RegisteredQuery>>,
3031    max_concurrent_workflow_tasks: usize,
3032    max_concurrent_activity_tasks: usize,
3033    poll_timeout: Duration,
3034    heartbeat_interval: Duration,
3035    retry_policy: WorkerRetryPolicy,
3036    heartbeat_observer: Option<WorkerHeartbeatObserver>,
3037}
3038
3039impl Worker {
3040    pub fn new(client: Client, task_queue: impl Into<String>) -> Self {
3041        Self {
3042            client,
3043            worker_id: default_worker_id(),
3044            task_queue: task_queue.into(),
3045            workflows: HashMap::new(),
3046            activities: HashMap::new(),
3047            queries: HashMap::new(),
3048            max_concurrent_workflow_tasks: 10,
3049            max_concurrent_activity_tasks: 10,
3050            poll_timeout: Duration::from_secs(30),
3051            heartbeat_interval: Duration::from_secs(60),
3052            retry_policy: WorkerRetryPolicy::default(),
3053            heartbeat_observer: None,
3054        }
3055    }
3056
3057    pub fn worker_id(mut self, worker_id: impl Into<String>) -> Self {
3058        self.worker_id = worker_id.into();
3059        self
3060    }
3061
3062    pub fn poll_timeout(mut self, timeout: Duration) -> Self {
3063        self.poll_timeout = timeout;
3064        self
3065    }
3066
3067    pub fn heartbeat_interval(mut self, interval: Duration) -> Self {
3068        self.heartbeat_interval = interval;
3069        self
3070    }
3071
3072    /// Configure bounded retries for task-poll acquisition and worker heartbeats.
3073    pub fn retry_policy(mut self, policy: WorkerRetryPolicy) -> Self {
3074        self.retry_policy = policy;
3075        self
3076    }
3077
3078    pub fn on_worker_heartbeat<F>(mut self, observer: F) -> Self
3079    where
3080        F: Fn(&WorkerHeartbeatObservation) + Send + Sync + 'static,
3081    {
3082        self.heartbeat_observer = Some(Arc::new(observer));
3083        self
3084    }
3085
3086    pub fn max_concurrent_workflow_tasks(mut self, count: usize) -> Self {
3087        self.max_concurrent_workflow_tasks = count.max(1);
3088        self
3089    }
3090
3091    pub fn max_concurrent_activity_tasks(mut self, count: usize) -> Self {
3092        self.max_concurrent_activity_tasks = count.max(1);
3093        self
3094    }
3095
3096    /// Register a workflow handler.
3097    ///
3098    /// An uncaught [`enum@Error`] returned by the handler fails the workflow run and
3099    /// is reported to clients as [`Error::WorkflowFailed`]. Errors that occur
3100    /// while acquiring or decoding a worker task remain worker-operation
3101    /// failures and do not get converted into workflow outcomes.
3102    pub fn register_workflow<F, Fut>(&mut self, workflow_type: impl Into<String>, handler: F)
3103    where
3104        F: Fn(WorkflowContext, Value) -> Fut + Send + Sync + 'static,
3105        Fut: Future<Output = Result<Value>> + Send + 'static,
3106    {
3107        self.workflows.insert(
3108            workflow_type.into(),
3109            RegisteredWorkflow {
3110                execute: Arc::new(move |ctx, input| Box::pin(handler(ctx, input))),
3111                replay: None,
3112                state_type: None,
3113            },
3114        );
3115    }
3116
3117    /// Register a workflow whose typed instance state can be reconstructed for queries.
3118    ///
3119    /// `state_factory` creates a fresh instance for every normal workflow task and
3120    /// query replay. The workflow handler is the single source of truth for state
3121    /// transitions: it updates [`WorkflowInstance`] after activities and signals
3122    /// resolve. Query replay runs this same handler over committed history and
3123    /// discards any commands it would emit.
3124    pub fn register_replayed_workflow<S, Factory, F, Fut>(
3125        &mut self,
3126        workflow_type: impl Into<String>,
3127        state_factory: Factory,
3128        handler: F,
3129    ) where
3130        S: Clone + Send + Sync + 'static,
3131        Factory: Fn() -> S + Send + Sync + 'static,
3132        F: Fn(WorkflowContext, Value, WorkflowInstance<S>) -> Fut + Send + Sync + 'static,
3133        Fut: Future<Output = Result<Value>> + Send + 'static,
3134    {
3135        let state_factory = Arc::new(state_factory);
3136        let handler = Arc::new(handler);
3137
3138        let execute_factory = Arc::clone(&state_factory);
3139        let execute_handler = Arc::clone(&handler);
3140        let execute = Arc::new(move |ctx: WorkflowContext, input: Value| {
3141            let state = WorkflowInstance::new(execute_factory());
3142            let future = execute_handler(ctx, input, state);
3143            Box::pin(future) as WorkflowFuture
3144        });
3145
3146        let replay = Arc::new(move |ctx: WorkflowContext, input: Value| {
3147            let state = WorkflowInstance::new(state_factory());
3148            let snapshot_state = state.clone();
3149            let snapshot: WorkflowStateSnapshot =
3150                Arc::new(move || Ok(Arc::new(snapshot_state.snapshot()?) as ErasedWorkflowState));
3151            let future = handler(ctx, input, state);
3152            ReplayedWorkflowInvocation {
3153                future: Box::pin(future),
3154                snapshot,
3155            }
3156        });
3157
3158        self.workflows.insert(
3159            workflow_type.into(),
3160            RegisteredWorkflow {
3161                execute,
3162                replay: Some(replay),
3163                state_type: Some(TypeId::of::<S>()),
3164            },
3165        );
3166    }
3167
3168    pub fn register_activity<F, Fut>(&mut self, activity_type: impl Into<String>, handler: F)
3169    where
3170        F: Fn(ActivityContext, Value) -> Fut + Send + Sync + 'static,
3171        Fut: Future<Output = Result<Value>> + Send + 'static,
3172    {
3173        self.activities.insert(
3174            activity_type.into(),
3175            Arc::new(move |ctx, args| Box::pin(handler(ctx, args))),
3176        );
3177    }
3178
3179    /// Register a named, read-only query handler for a workflow type.
3180    ///
3181    /// The workflow type must also be registered with [`Worker::register_workflow`]
3182    /// before the worker runs. The handler receives only an immutable committed
3183    /// state snapshot and normalized query arguments.
3184    pub fn register_query<F, Fut>(
3185        &mut self,
3186        workflow_type: impl Into<String>,
3187        query_name: impl Into<String>,
3188        handler: F,
3189    ) where
3190        F: Fn(QueryContext, Value) -> Fut + Send + Sync + 'static,
3191        Fut: Future<Output = Result<Value>> + Send + 'static,
3192    {
3193        self.queries
3194            .entry(workflow_type.into())
3195            .or_default()
3196            .insert(
3197                query_name.into(),
3198                RegisteredQuery::Snapshot(Arc::new(move |ctx, args| Box::pin(handler(ctx, args)))),
3199            );
3200    }
3201
3202    /// Register a named query against deterministically replayed instance state.
3203    ///
3204    /// The workflow type must use [`Worker::register_replayed_workflow`] with the
3205    /// same state type `S`. The handler receives an immutable, detached state
3206    /// clone, so successful and failed queries cannot affect workflow execution
3207    /// or the state reconstructed by a later query.
3208    pub fn register_replayed_query<S, F, Fut>(
3209        &mut self,
3210        workflow_type: impl Into<String>,
3211        query_name: impl Into<String>,
3212        handler: F,
3213    ) where
3214        S: Clone + Send + Sync + 'static,
3215        F: Fn(QueryContext, Arc<S>, Value) -> Fut + Send + Sync + 'static,
3216        Fut: Future<Output = Result<Value>> + Send + 'static,
3217    {
3218        let handler = Arc::new(handler);
3219        let erased_handler: ReplayedQueryHandler = Arc::new(move |ctx, state, args| {
3220            let state = state.downcast::<S>().map_err(|_| {
3221                "registered query state type does not match the replayed workflow state".to_string()
3222            })?;
3223            Ok(Box::pin(handler(ctx, state, args)))
3224        });
3225
3226        self.queries
3227            .entry(workflow_type.into())
3228            .or_default()
3229            .insert(
3230                query_name.into(),
3231                RegisteredQuery::Replayed {
3232                    state_type: TypeId::of::<S>(),
3233                    handler: erased_handler,
3234                },
3235            );
3236    }
3237
3238    pub async fn register(&self) -> Result<RegisterWorkerResponse> {
3239        self.client
3240            .register_worker_with_capabilities(
3241                &self.worker_id,
3242                &self.task_queue,
3243                self.workflows.keys().cloned().collect(),
3244                self.activities.keys().cloned().collect(),
3245                self.max_concurrent_workflow_tasks,
3246                self.max_concurrent_activity_tasks,
3247                (!self.queries.is_empty())
3248                    .then(|| QUERY_TASKS_CAPABILITY.to_string())
3249                    .into_iter()
3250                    .collect(),
3251            )
3252            .await
3253    }
3254
3255    /// Run until shutdown or a terminal worker error occurs.
3256    ///
3257    /// Empty long-poll expirations do not stop the worker. Retryable poll and
3258    /// heartbeat failures use [`WorkerRetryPolicy`] independently, while
3259    /// authentication, protocol, and other non-retryable failures are returned.
3260    pub async fn run(&self) -> Result<()> {
3261        self.run_until(std::future::pending::<()>()).await
3262    }
3263
3264    /// Run until `shutdown` resolves or a terminal worker error occurs.
3265    ///
3266    /// This has the same liveness and terminal-error contract as [`Worker::run`].
3267    pub async fn run_until<F>(&self, shutdown: F) -> Result<()>
3268    where
3269        F: Future<Output = ()>,
3270    {
3271        let registration = self.register().await?;
3272        let heartbeat_interval = Duration::from_secs(
3273            registration
3274                .heartbeat_interval_seconds
3275                .unwrap_or(self.heartbeat_interval.as_secs().max(1)),
3276        );
3277        // The first heartbeat is immediate. Subsequent heartbeats are scheduled
3278        // from the completion of the preceding attempt, including its bounded
3279        // retries. A fixed-epoch interval can leave an already-due tick queued
3280        // while an acknowledgement is slow, producing a catch-up heartbeat as
3281        // soon as that request completes.
3282        let heartbeat = tokio::time::sleep(Duration::ZERO);
3283        tokio::pin!(heartbeat);
3284        tokio::pin!(shutdown);
3285        let stop = Arc::new(AtomicBool::new(false));
3286        // Poll responses may already have leased server-side work by the time
3287        // they become ready, so each poller owns its responses through
3288        // completion or failure instead of racing raw polls in this select.
3289        let mut workflow_poller = (!self.workflows.is_empty()).then(|| {
3290            let worker = self.clone();
3291            let stop = Arc::clone(&stop);
3292            tokio::spawn(async move { worker.poll_workflows_until_stopped(stop).await })
3293        });
3294        let mut activity_poller = (!self.activities.is_empty()).then(|| {
3295            let worker = self.clone();
3296            let stop = Arc::clone(&stop);
3297            tokio::spawn(async move { worker.poll_activities_until_stopped(stop).await })
3298        });
3299        let mut query_poller = (!self.queries.is_empty()).then(|| {
3300            let worker = self.clone();
3301            let stop = Arc::clone(&stop);
3302            tokio::spawn(async move { worker.poll_queries_until_stopped(stop).await })
3303        });
3304
3305        loop {
3306            tokio::select! {
3307                _ = &mut shutdown => {
3308                    stop.store(true, Ordering::SeqCst);
3309                    break;
3310                }
3311                _ = &mut heartbeat => {
3312                    let result = self.retry_worker_operation(|| {
3313                        self.client.heartbeat_worker(
3314                            &self.worker_id,
3315                            self.max_concurrent_workflow_tasks,
3316                            self.max_concurrent_activity_tasks,
3317                        )
3318                    }).await;
3319                    heartbeat
3320                        .as_mut()
3321                        .reset(tokio::time::Instant::now() + heartbeat_interval);
3322                    match result {
3323                        Ok(acknowledgement) => {
3324                            if let Some(observer) = &self.heartbeat_observer {
3325                                observer(&WorkerHeartbeatObservation {
3326                                    worker_id: self.worker_id.clone(),
3327                                    task_queue: self.task_queue.clone(),
3328                                    acknowledged_at_unix_millis: SystemTime::now()
3329                                        .duration_since(UNIX_EPOCH)
3330                                        .unwrap_or_default()
3331                                        .as_millis()
3332                                        .min(u64::MAX as u128)
3333                                        as u64,
3334                                    acknowledgement,
3335                                });
3336                            }
3337                        }
3338                        Err(error) => {
3339                            stop.store(true, Ordering::SeqCst);
3340                            join_pollers(workflow_poller.take(), activity_poller.take(), query_poller.take()).await?;
3341                            return Err(error);
3342                        }
3343                    }
3344                }
3345                result = OptionFuture::from(workflow_poller.as_mut()), if workflow_poller.is_some() => {
3346                    workflow_poller = None;
3347                    let stopped_by_server = stop.load(Ordering::SeqCst);
3348                    stop.store(true, Ordering::SeqCst);
3349                    let poller_result = optional_poller_result("workflow", result);
3350                    let join_result =
3351                        join_pollers(workflow_poller.take(), activity_poller.take(), query_poller.take()).await;
3352                    poller_result?;
3353                    join_result?;
3354                    if stopped_by_server {
3355                        return Ok(());
3356                    }
3357                    return Err(Error::WorkerLoop(
3358                        "workflow poller stopped unexpectedly".to_string(),
3359                    ));
3360                }
3361                result = OptionFuture::from(activity_poller.as_mut()), if activity_poller.is_some() => {
3362                    activity_poller = None;
3363                    let stopped_by_server = stop.load(Ordering::SeqCst);
3364                    stop.store(true, Ordering::SeqCst);
3365                    let poller_result = optional_poller_result("activity", result);
3366                    let join_result =
3367                        join_pollers(workflow_poller.take(), activity_poller.take(), query_poller.take()).await;
3368                    poller_result?;
3369                    join_result?;
3370                    if stopped_by_server {
3371                        return Ok(());
3372                    }
3373                    return Err(Error::WorkerLoop(
3374                        "activity poller stopped unexpectedly".to_string(),
3375                    ));
3376                }
3377                result = OptionFuture::from(query_poller.as_mut()), if query_poller.is_some() => {
3378                    query_poller = None;
3379                    let stopped_by_server = stop.load(Ordering::SeqCst);
3380                    stop.store(true, Ordering::SeqCst);
3381                    let poller_result = optional_poller_result("query", result);
3382                    let join_result =
3383                        join_pollers(workflow_poller.take(), activity_poller.take(), query_poller.take()).await;
3384                    poller_result?;
3385                    join_result?;
3386                    if stopped_by_server {
3387                        return Ok(());
3388                    }
3389                    return Err(Error::WorkerLoop(
3390                        "query poller stopped unexpectedly".to_string(),
3391                    ));
3392                }
3393            }
3394        }
3395
3396        join_pollers(
3397            workflow_poller.take(),
3398            activity_poller.take(),
3399            query_poller.take(),
3400        )
3401        .await
3402    }
3403
3404    /// Poll and settle at most one task from each enabled task family.
3405    ///
3406    /// A workflow may reach its server-enforced run deadline while this worker
3407    /// holds a task. When the completion endpoint authoritatively rejects that
3408    /// selected task and run with `recorded=false`, `reason=run_timed_out`, and
3409    /// terminal `run_status=failed`, the workflow tick is considered settled:
3410    /// the late command was not recorded and cannot replace the terminal run.
3411    /// Every other completion rejection remains an error. This worker-level
3412    /// race handling is distinct from [`WorkflowResultOptions::timeout`], which
3413    /// only bounds how long a client waits for a result.
3414    ///
3415    /// Direct callers of [`Client::complete_workflow_task`] continue to receive
3416    /// the original [`Error::Http`] status and response body.
3417    pub async fn run_once(&self) -> Result<usize> {
3418        let mut handled = 0;
3419        match self.poll_workflow_once().await? {
3420            ManagedPollOutcome::Handled => handled += 1,
3421            ManagedPollOutcome::Stop => return Ok(handled),
3422            ManagedPollOutcome::Idle => {}
3423        }
3424        match self.poll_activity_once().await? {
3425            ManagedPollOutcome::Handled => handled += 1,
3426            ManagedPollOutcome::Stop => return Ok(handled),
3427            ManagedPollOutcome::Idle => {}
3428        }
3429        if !self.queries.is_empty() {
3430            match self.poll_query_once().await? {
3431                ManagedPollOutcome::Handled => handled += 1,
3432                ManagedPollOutcome::Stop => return Ok(handled),
3433                ManagedPollOutcome::Idle => {}
3434            }
3435        }
3436        Ok(handled)
3437    }
3438
3439    async fn poll_workflow_once(&self) -> Result<ManagedPollOutcome> {
3440        let response = self
3441            .retry_worker_operation(|| {
3442                self.client.poll_workflow_task_response(
3443                    &self.worker_id,
3444                    &self.task_queue,
3445                    self.poll_timeout,
3446                )
3447            })
3448            .await?;
3449        if response.outcome().should_stop() {
3450            return Ok(ManagedPollOutcome::Stop);
3451        }
3452        let Some(task) = response.task else {
3453            return Ok(ManagedPollOutcome::Idle);
3454        };
3455
3456        let task_id = task.task_id.clone();
3457        let attempt = task.workflow_task_attempt;
3458        let run_id = task.run_id.clone();
3459        let lease_owner = task
3460            .lease_owner
3461            .clone()
3462            .unwrap_or_else(|| self.worker_id.clone());
3463
3464        match self.execute_workflow_task(task) {
3465            Ok(commands) if commands.is_empty() => {
3466                // A replay can consume a recorded pending durable command
3467                // without producing a new command. The standalone protocol
3468                // acknowledges that state through the typed waiting outcome;
3469                // an empty completion is rejected by servers that require at
3470                // least one executable command.
3471                self.client
3472                    .fail_workflow_task_with_type(
3473                        &task_id,
3474                        &lease_owner,
3475                        attempt,
3476                        WORKFLOW_TASK_WAITING_FOR_HISTORY_MESSAGE,
3477                        WORKFLOW_TASK_WAITING_FOR_HISTORY_TYPE,
3478                    )
3479                    .await?;
3480            }
3481            Ok(commands) => {
3482                let completion = self
3483                    .client
3484                    .complete_workflow_task(&task_id, &lease_owner, attempt, commands)
3485                    .await;
3486                if let Err(error) = completion {
3487                    if !workflow_task_completion_is_terminal_timeout(
3488                        &error,
3489                        &task_id,
3490                        attempt,
3491                        run_id.as_deref(),
3492                    ) {
3493                        return Err(error);
3494                    }
3495                }
3496            }
3497            Err(error) => {
3498                self.client
3499                    .fail_workflow_task(&task_id, &lease_owner, attempt, error.to_string())
3500                    .await?;
3501            }
3502        }
3503
3504        Ok(ManagedPollOutcome::Handled)
3505    }
3506
3507    async fn poll_workflows_until_stopped(self, stop: Arc<AtomicBool>) -> Result<()> {
3508        while !stop.load(Ordering::SeqCst) {
3509            if self.poll_workflow_once().await? == ManagedPollOutcome::Stop {
3510                stop.store(true, Ordering::SeqCst);
3511                break;
3512            }
3513        }
3514
3515        Ok(())
3516    }
3517
3518    async fn poll_activity_once(&self) -> Result<ManagedPollOutcome> {
3519        let response = self
3520            .retry_worker_operation(|| {
3521                self.client.poll_activity_task_response(
3522                    &self.worker_id,
3523                    &self.task_queue,
3524                    self.poll_timeout,
3525                )
3526            })
3527            .await?;
3528        if response.outcome().should_stop() {
3529            return Ok(ManagedPollOutcome::Stop);
3530        }
3531        let Some(task) = response.task else {
3532            return Ok(ManagedPollOutcome::Idle);
3533        };
3534
3535        let task_id = task.task_id.clone();
3536        let attempt_id = task
3537            .activity_attempt_id
3538            .clone()
3539            .or(task.attempt_id.clone())
3540            .unwrap_or_default();
3541        let lease_owner = task
3542            .lease_owner
3543            .clone()
3544            .unwrap_or_else(|| self.worker_id.clone());
3545        let codec = task.payload_codec.clone();
3546        let result = self.execute_activity_task(task).await;
3547        match result {
3548            Ok(value) => {
3549                let completion = self
3550                    .client
3551                    .complete_activity_task(&task_id, &attempt_id, &lease_owner, value, &codec)
3552                    .await;
3553                if let Err(error) = completion {
3554                    if !activity_task_rejection_is_final(&error) {
3555                        return Err(error);
3556                    }
3557                }
3558            }
3559            Err(error) => {
3560                let failure = self
3561                    .client
3562                    .fail_activity_task(
3563                        &task_id,
3564                        &attempt_id,
3565                        &lease_owner,
3566                        error.to_string(),
3567                        false,
3568                    )
3569                    .await;
3570                if let Err(error) = failure {
3571                    if !activity_task_rejection_is_final(&error) {
3572                        return Err(error);
3573                    }
3574                }
3575            }
3576        }
3577
3578        Ok(ManagedPollOutcome::Handled)
3579    }
3580
3581    async fn poll_activities_until_stopped(self, stop: Arc<AtomicBool>) -> Result<()> {
3582        while !stop.load(Ordering::SeqCst) {
3583            if self.poll_activity_once().await? == ManagedPollOutcome::Stop {
3584                stop.store(true, Ordering::SeqCst);
3585                break;
3586            }
3587        }
3588
3589        Ok(())
3590    }
3591
3592    async fn poll_query_once(&self) -> Result<ManagedPollOutcome> {
3593        let response = self
3594            .retry_worker_operation(|| {
3595                self.client.poll_query_task_response(
3596                    &self.worker_id,
3597                    &self.task_queue,
3598                    self.poll_timeout,
3599                )
3600            })
3601            .await?;
3602        if response.outcome().should_stop() {
3603            return Ok(ManagedPollOutcome::Stop);
3604        }
3605        let Some(task) = response.task else {
3606            return Ok(ManagedPollOutcome::Idle);
3607        };
3608
3609        let query_task_id = task.query_task_id.clone();
3610        let attempt = task.query_task_attempt;
3611        let lease_owner = task
3612            .lease_owner
3613            .clone()
3614            .unwrap_or_else(|| self.worker_id.clone());
3615        let codec = task.payload_codec.clone();
3616
3617        match self.execute_query_task(task).await {
3618            Ok(value) => {
3619                let result_envelope = match encode_value_envelope(&value, &codec) {
3620                    Ok(result_envelope) => result_envelope,
3621                    Err(error) => {
3622                        let failure = self
3623                            .client
3624                            .fail_query_task(
3625                                &query_task_id,
3626                                &lease_owner,
3627                                attempt,
3628                                error.to_string(),
3629                                "query_result_encode_failed",
3630                                "QueryResultEncodeFailed",
3631                            )
3632                            .await;
3633                        if let Err(error) = failure {
3634                            if !query_task_rejection_is_final(&error) {
3635                                return Err(error);
3636                            }
3637                        }
3638                        return Ok(ManagedPollOutcome::Handled);
3639                    }
3640                };
3641
3642                if let Err(error) = self
3643                    .client
3644                    .complete_query_task_with_envelope(
3645                        &query_task_id,
3646                        &lease_owner,
3647                        attempt,
3648                        value,
3649                        result_envelope,
3650                    )
3651                    .await
3652                {
3653                    if !query_task_rejection_is_final(&error) {
3654                        return Err(error);
3655                    }
3656                }
3657            }
3658            Err(failure) => {
3659                let result = self
3660                    .client
3661                    .fail_query_task(
3662                        &query_task_id,
3663                        &lease_owner,
3664                        attempt,
3665                        failure.message,
3666                        failure.reason,
3667                        failure.failure_type,
3668                    )
3669                    .await;
3670                if let Err(error) = result {
3671                    if !query_task_rejection_is_final(&error) {
3672                        return Err(error);
3673                    }
3674                }
3675            }
3676        }
3677
3678        Ok(ManagedPollOutcome::Handled)
3679    }
3680
3681    async fn poll_queries_until_stopped(self, stop: Arc<AtomicBool>) -> Result<()> {
3682        while !stop.load(Ordering::SeqCst) {
3683            if self.poll_query_once().await? == ManagedPollOutcome::Stop {
3684                stop.store(true, Ordering::SeqCst);
3685                break;
3686            }
3687        }
3688
3689        Ok(())
3690    }
3691
3692    async fn retry_worker_operation<T, F, Fut>(&self, mut operation: F) -> Result<T>
3693    where
3694        F: FnMut() -> Fut,
3695        Fut: Future<Output = Result<T>>,
3696    {
3697        let mut retries = 0;
3698
3699        loop {
3700            match operation().await {
3701                Err(error)
3702                    if worker_operation_is_retryable(&error)
3703                        && retries < self.retry_policy.max_retries =>
3704                {
3705                    retries += 1;
3706                    tokio::time::sleep(worker_retry_delay(self.retry_policy, retries)).await;
3707                }
3708                result => return result,
3709            }
3710        }
3711    }
3712
3713    async fn execute_query_task(
3714        &self,
3715        mut task: QueryTask,
3716    ) -> std::result::Result<Value, QueryTaskExecutionFailure> {
3717        if !matches!(task.payload_codec.as_str(), DEFAULT_CODEC | JSON_CODEC) {
3718            return Err(QueryTaskExecutionFailure::new(
3719                "query_payload_decode_failed",
3720                format!(
3721                    "cannot decode query payload with unsupported codec {:?}",
3722                    task.payload_codec
3723                ),
3724                "QueryPayloadDecodeFailed",
3725            ));
3726        }
3727
3728        if !self.workflows.contains_key(&task.workflow_type) {
3729            return Err(QueryTaskExecutionFailure::new(
3730                "query_workflow_type_not_registered",
3731                format!("no workflow registered for type {:?}", task.workflow_type),
3732                "WorkflowTypeNotRegistered",
3733            ));
3734        }
3735
3736        let Some(handlers) = self.queries.get(&task.workflow_type) else {
3737            return Err(QueryTaskExecutionFailure::new(
3738                "query_handler_unavailable",
3739                format!(
3740                    "query handlers are unavailable for workflow type {:?}",
3741                    task.workflow_type
3742                ),
3743                "QueryHandlerUnavailable",
3744            ));
3745        };
3746        let Some(query) = handlers.get(&task.query_name) else {
3747            return Err(QueryTaskExecutionFailure::new(
3748                "rejected_unknown_query",
3749                format!("unknown query {:?}", task.query_name),
3750                "QueryFailed",
3751            ));
3752        };
3753
3754        let args = decode_task_arguments(task.query_arguments.as_ref(), &task.payload_codec)
3755            .map_err(|error| {
3756                QueryTaskExecutionFailure::new(
3757                    "query_payload_decode_failed",
3758                    format!("cannot decode query arguments: {error}"),
3759                    "QueryPayloadDecodeFailed",
3760                )
3761            })?;
3762        let workflow_input =
3763            decode_task_arguments(task.workflow_arguments.as_ref(), &task.payload_codec).map_err(
3764                |error| {
3765                    QueryTaskExecutionFailure::new(
3766                        "query_workflow_state_unavailable",
3767                        format!("cannot decode workflow start input: {error}"),
3768                        "QueryWorkflowStateUnavailable",
3769                    )
3770                },
3771            )?;
3772        hydrate_query_history_from_export(&mut task).map_err(|error| {
3773            QueryTaskExecutionFailure::new(
3774                "query_workflow_state_unavailable",
3775                format!("cannot restore query history snapshot: {error}"),
3776                "QueryWorkflowStateUnavailable",
3777            )
3778        })?;
3779        enrich_query_history_from_export(&mut task).map_err(|error| {
3780            QueryTaskExecutionFailure::new(
3781                "query_workflow_state_unavailable",
3782                format!("cannot restore compact query history payloads: {error}"),
3783                "QueryWorkflowStateUnavailable",
3784            )
3785        })?;
3786        let signal_events = query_signal_events(&task).map_err(|error| {
3787            QueryTaskExecutionFailure::new(
3788                "query_workflow_state_unavailable",
3789                format!("cannot decode committed workflow signals: {error}"),
3790                "QueryWorkflowStateUnavailable",
3791            )
3792        })?;
3793        let history_events = Arc::new(std::mem::take(&mut task.history_events));
3794        let context = QueryContext {
3795            workflow_id: task.workflow_id,
3796            run_id: task.run_id,
3797            workflow_type: task.workflow_type.clone(),
3798            run_status: task.run_status,
3799            workflow_input,
3800            history_events: Arc::clone(&history_events),
3801            signal_events: Arc::new(signal_events),
3802        };
3803
3804        let future = match query {
3805            RegisteredQuery::Snapshot(handler) => handler(context, args),
3806            RegisteredQuery::Replayed {
3807                state_type,
3808                handler,
3809            } => {
3810                let workflow = self
3811                    .workflows
3812                    .get(&task.workflow_type)
3813                    .expect("workflow registration was checked above");
3814                if workflow.state_type != Some(*state_type) {
3815                    return Err(QueryTaskExecutionFailure::new(
3816                        "query_workflow_state_unavailable",
3817                        "replayed query state type does not match its workflow registration",
3818                        "QueryWorkflowStateUnavailable",
3819                    ));
3820                }
3821                let replay = workflow.replay.as_ref().ok_or_else(|| {
3822                    QueryTaskExecutionFailure::new(
3823                        "query_workflow_state_unavailable",
3824                        format!(
3825                            "workflow type {:?} is not registered for instance-state replay",
3826                            task.workflow_type
3827                        ),
3828                        "QueryWorkflowStateUnavailable",
3829                    )
3830                })?;
3831                let workflow_state = Arc::new(Mutex::new(
3832                    WorkflowState::new_with_identity(
3833                        history_events.as_ref().clone(),
3834                        context.workflow_id.clone(),
3835                        context.run_id.clone(),
3836                        self.task_queue.clone(),
3837                        task.payload_codec,
3838                        None,
3839                    )
3840                    .map_err(|error| {
3841                        QueryTaskExecutionFailure::new(
3842                            "query_workflow_state_unavailable",
3843                            format!("workflow replay failed before query: {error}"),
3844                            "QueryWorkflowStateUnavailable",
3845                        )
3846                    })?,
3847                ));
3848                let workflow_context = WorkflowContext {
3849                    state: workflow_state,
3850                };
3851                let mut invocation =
3852                    replay(workflow_context.clone(), context.workflow_input.clone());
3853                let mut cx = TaskContext::from_waker(noop_waker_ref());
3854                match invocation.future.as_mut().poll(&mut cx) {
3855                    Poll::Ready(Ok(_)) => {
3856                        workflow_context
3857                            .ensure_history_consumed()
3858                            .map_err(|error| {
3859                                QueryTaskExecutionFailure::new(
3860                                    "query_workflow_state_unavailable",
3861                                    format!("workflow replay failed before query: {error}"),
3862                                    "QueryWorkflowStateUnavailable",
3863                                )
3864                            })?;
3865                    }
3866                    Poll::Ready(Err(error)) => {
3867                        return Err(QueryTaskExecutionFailure::new(
3868                            "query_workflow_state_unavailable",
3869                            format!("workflow replay failed before query: {error}"),
3870                            "QueryWorkflowStateUnavailable",
3871                        ));
3872                    }
3873                    Poll::Pending => {
3874                        let commands = workflow_context.take_commands().map_err(|error| {
3875                            QueryTaskExecutionFailure::new(
3876                                "query_workflow_state_unavailable",
3877                                format!("workflow replay failed before query: {error}"),
3878                                "QueryWorkflowStateUnavailable",
3879                            )
3880                        })?;
3881                        if commands.is_empty()
3882                            && !workflow_context
3883                                .matched_recorded_pending()
3884                                .map_err(|error| {
3885                                    QueryTaskExecutionFailure::new(
3886                                        "query_workflow_state_unavailable",
3887                                        format!("workflow replay failed before query: {error}"),
3888                                        "QueryWorkflowStateUnavailable",
3889                                    )
3890                                })?
3891                        {
3892                            return Err(QueryTaskExecutionFailure::new(
3893                                "query_workflow_state_unavailable",
3894                                "workflow replay yielded without a durable command",
3895                                "QueryWorkflowStateUnavailable",
3896                            ));
3897                        }
3898                    }
3899                }
3900                let state = (invocation.snapshot)().map_err(|error| {
3901                    QueryTaskExecutionFailure::new(
3902                        "query_workflow_state_unavailable",
3903                        format!("cannot snapshot replayed workflow state: {error}"),
3904                        "QueryWorkflowStateUnavailable",
3905                    )
3906                })?;
3907                handler(context, state, args).map_err(|message| {
3908                    QueryTaskExecutionFailure::new(
3909                        "query_workflow_state_unavailable",
3910                        message,
3911                        "QueryWorkflowStateUnavailable",
3912                    )
3913                })?
3914            }
3915        };
3916
3917        future.await.map_err(|error| {
3918            QueryTaskExecutionFailure::new("query_rejected", error.to_string(), "QueryFailed")
3919        })
3920    }
3921
3922    fn execute_workflow_task(&self, task: WorkflowTask) -> Result<Vec<Value>> {
3923        let workflow = self
3924            .workflows
3925            .get(&task.workflow_type)
3926            .ok_or_else(|| Error::WorkflowNotRegistered(task.workflow_type.clone()))?;
3927        let input = decode_task_arguments(task.arguments.as_ref(), &task.payload_codec)?;
3928        let resume_signal = decode_resume_signal(&task)?;
3929        let state = Arc::new(Mutex::new(WorkflowState::new_with_identity(
3930            task.history_events,
3931            task.workflow_id,
3932            task.run_id,
3933            self.task_queue.clone(),
3934            task.payload_codec.clone(),
3935            resume_signal,
3936        )?));
3937        let ctx = WorkflowContext { state };
3938        let mut future = (workflow.execute)(ctx.clone(), input);
3939        let mut cx = TaskContext::from_waker(noop_waker_ref());
3940
3941        match future.as_mut().poll(&mut cx) {
3942            Poll::Ready(Ok(result)) => {
3943                ctx.ensure_history_consumed()?;
3944                let result = encode_value_envelope(&result, &task.payload_codec)?;
3945                let mut commands = ctx.take_commands()?;
3946                commands.push(json!({
3947                    "type": "complete_workflow",
3948                    "result": result
3949                }));
3950                Ok(commands)
3951            }
3952            Poll::Ready(Err(error)) => {
3953                // A handler error must not hide a committed durable command that
3954                // upgraded workflow code no longer consumes.
3955                ctx.ensure_history_consumed()?;
3956                if workflow_task_integrity_error(&error) {
3957                    // Replay and protocol failures describe the workflow-task
3958                    // decision itself. Do not let commands queued earlier in
3959                    // this uncommitted decision escape alongside a terminal
3960                    // workflow failure.
3961                    return Err(error);
3962                }
3963                let mut commands = ctx.take_commands()?;
3964                commands.push(workflow_failure_command(&error));
3965                Ok(commands)
3966            }
3967            Poll::Pending => {
3968                let commands = ctx.take_commands()?;
3969                if commands.is_empty() && !ctx.matched_recorded_pending()? {
3970                    Err(Error::WorkflowYieldedWithoutCommand)
3971                } else {
3972                    Ok(commands)
3973                }
3974            }
3975        }
3976    }
3977
3978    async fn execute_activity_task(&self, task: ActivityTask) -> Result<Value> {
3979        let handler = self
3980            .activities
3981            .get(&task.activity_type)
3982            .ok_or_else(|| Error::ActivityNotRegistered(task.activity_type.clone()))?;
3983        let args = decode_task_arguments(task.arguments.as_ref(), &task.payload_codec)?;
3984        let attempt_id = task
3985            .activity_attempt_id
3986            .clone()
3987            .or(task.attempt_id.clone())
3988            .unwrap_or_default();
3989        let lease_owner = task
3990            .lease_owner
3991            .clone()
3992            .unwrap_or_else(|| self.worker_id.clone());
3993        let ctx = ActivityContext {
3994            client: self.client.clone(),
3995            task_id: task.task_id,
3996            activity_attempt_id: attempt_id,
3997            lease_owner,
3998            activity_type: task.activity_type,
3999            attempt_number: task.attempt_number,
4000            task_queue: self.task_queue.clone(),
4001            worker_id: self.worker_id.clone(),
4002        };
4003
4004        handler(ctx, args).await
4005    }
4006}
4007
4008fn poller_result(
4009    kind: &str,
4010    result: std::result::Result<Result<()>, tokio::task::JoinError>,
4011) -> Result<()> {
4012    match result {
4013        Ok(result) => result,
4014        Err(error) => Err(Error::WorkerLoop(format!(
4015            "{kind} poller join error: {error}"
4016        ))),
4017    }
4018}
4019
4020fn optional_poller_result(
4021    kind: &str,
4022    result: Option<std::result::Result<Result<()>, tokio::task::JoinError>>,
4023) -> Result<()> {
4024    match result {
4025        Some(result) => poller_result(kind, result),
4026        None => Ok(()),
4027    }
4028}
4029
4030async fn join_pollers(
4031    workflow_poller: Option<tokio::task::JoinHandle<Result<()>>>,
4032    activity_poller: Option<tokio::task::JoinHandle<Result<()>>>,
4033    query_poller: Option<tokio::task::JoinHandle<Result<()>>>,
4034) -> Result<()> {
4035    let mut first_error = None;
4036
4037    if let Some(handle) = workflow_poller {
4038        if let Err(error) = poller_result("workflow", handle.await) {
4039            first_error.get_or_insert(error);
4040        }
4041    }
4042
4043    if let Some(handle) = activity_poller {
4044        if let Err(error) = poller_result("activity", handle.await) {
4045            first_error.get_or_insert(error);
4046        }
4047    }
4048
4049    if let Some(handle) = query_poller {
4050        if let Err(error) = poller_result("query", handle.await) {
4051            first_error.get_or_insert(error);
4052        }
4053    }
4054
4055    if let Some(error) = first_error {
4056        Err(error)
4057    } else {
4058        Ok(())
4059    }
4060}
4061
4062fn default_worker_id() -> String {
4063    let millis = SystemTime::now()
4064        .duration_since(UNIX_EPOCH)
4065        .unwrap_or_default()
4066        .as_millis();
4067    format!("rust-worker-{}-{millis}", std::process::id())
4068}
4069
4070fn unique_request_id(prefix: &str) -> String {
4071    let nanos = SystemTime::now()
4072        .duration_since(UNIX_EPOCH)
4073        .unwrap_or_default()
4074        .as_nanos();
4075    format!("{prefix}-{}-{nanos}", std::process::id())
4076}
4077
4078#[derive(Debug)]
4079struct QueryTaskExecutionFailure {
4080    reason: String,
4081    message: String,
4082    failure_type: String,
4083}
4084
4085impl QueryTaskExecutionFailure {
4086    fn new(
4087        reason: impl Into<String>,
4088        message: impl Into<String>,
4089        failure_type: impl Into<String>,
4090    ) -> Self {
4091        Self {
4092            reason: reason.into(),
4093            message: message.into(),
4094            failure_type: failure_type.into(),
4095        }
4096    }
4097}
4098
4099/// Typed local state owned by one deterministic workflow invocation.
4100///
4101/// Use [`WorkflowInstance::update`] for the same state transitions during
4102/// ordinary execution and replay. A replayed query receives a detached
4103/// immutable `Arc<S>` rather than this mutation-capable handle.
4104#[derive(Clone, Debug)]
4105pub struct WorkflowInstance<S> {
4106    state: Arc<Mutex<S>>,
4107}
4108
4109impl<S> WorkflowInstance<S> {
4110    fn new(state: S) -> Self {
4111        Self {
4112            state: Arc::new(Mutex::new(state)),
4113        }
4114    }
4115
4116    /// Read the current workflow-instance state without changing it.
4117    pub fn read<R>(&self, reader: impl FnOnce(&S) -> R) -> Result<R> {
4118        let state = self
4119            .state
4120            .lock()
4121            .map_err(|_| Error::WorkflowStatePoisoned)?;
4122        Ok(reader(&state))
4123    }
4124
4125    /// Apply one deterministic workflow-instance state transition.
4126    pub fn update<R>(&self, transition: impl FnOnce(&mut S) -> R) -> Result<R> {
4127        let mut state = self
4128            .state
4129            .lock()
4130            .map_err(|_| Error::WorkflowStatePoisoned)?;
4131        Ok(transition(&mut state))
4132    }
4133}
4134
4135impl<S: Clone> WorkflowInstance<S> {
4136    fn snapshot(&self) -> Result<S> {
4137        self.read(Clone::clone)
4138    }
4139}
4140
4141#[derive(Clone, Debug)]
4142pub struct WorkflowContext {
4143    state: Arc<Mutex<WorkflowState>>,
4144}
4145
4146impl WorkflowContext {
4147    /// Identity of the parent workflow currently being replayed.
4148    pub fn workflow_identity(&self) -> Result<WorkflowIdentity> {
4149        let state = self
4150            .state
4151            .lock()
4152            .map_err(|_| Error::WorkflowStatePoisoned)?;
4153        Ok(WorkflowIdentity {
4154            workflow_id: state.workflow_id.clone(),
4155            run_id: state.run_id.clone(),
4156        })
4157    }
4158
4159    pub fn activity<T: Serialize>(
4160        &self,
4161        activity_type: impl Into<String>,
4162        args: T,
4163    ) -> ActivityCall {
4164        self.activity_with_options(activity_type, ActivityOptions::new(), args)
4165    }
4166
4167    pub fn activity_on_queue<T, Q>(
4168        &self,
4169        activity_type: impl Into<String>,
4170        task_queue: Option<Q>,
4171        args: T,
4172    ) -> ActivityCall
4173    where
4174        T: Serialize,
4175        Q: Into<String>,
4176    {
4177        let mut options = ActivityOptions::new();
4178        options.task_queue = task_queue.map(Into::into);
4179        self.activity_with_options(activity_type, options, args)
4180    }
4181
4182    /// Schedule one durable activity with retry, routing, and timeout options.
4183    ///
4184    /// Options are validated before the command is emitted. Once the command is
4185    /// recorded, replay consumes the same activity lifecycle at this command
4186    /// position and never emits a duplicate schedule.
4187    ///
4188    /// ```no_run
4189    /// # use durable_workflow::{json, ActivityOptions, ActivityRetryPolicy, Error, Result, WorkflowContext};
4190    /// # use std::time::Duration;
4191    /// # async fn run(ctx: WorkflowContext) -> Result<durable_workflow::Value> {
4192    /// let result = ctx
4193    ///     .activity_with_options(
4194    ///         "charge-card",
4195    ///         ActivityOptions::new()
4196    ///             .task_queue("payments")
4197    ///             .retry_policy(
4198    ///                 ActivityRetryPolicy::new(4).exponential_backoff(
4199    ///                     Duration::from_secs(1),
4200    ///                     2,
4201    ///                     Some(Duration::from_secs(30)),
4202    ///                 ),
4203    ///             )
4204    ///             .start_to_close_timeout(Duration::from_secs(60))
4205    ///             .schedule_to_close_timeout(Duration::from_secs(180))
4206    ///             .heartbeat_timeout(Duration::from_secs(15)),
4207    ///         json!([{"order_id": "order-42"}]),
4208    ///     )
4209    ///     .await;
4210    /// match result {
4211    ///     Err(Error::ActivityFailed(failure)) => Ok(json!({
4212    ///         "reason": failure.reason,
4213    ///         "timeout_kind": failure.timeout_kind,
4214    ///     })),
4215    ///     other => other,
4216    /// }
4217    /// # }
4218    /// ```
4219    pub fn activity_with_options<T: Serialize>(
4220        &self,
4221        activity_type: impl Into<String>,
4222        options: ActivityOptions,
4223        args: T,
4224    ) -> ActivityCall {
4225        ActivityCall {
4226            ctx: self.clone(),
4227            activity_type: activity_type.into(),
4228            options,
4229            args: Some(serde_json::to_value(args).map_err(Error::from)),
4230            scheduled: false,
4231        }
4232    }
4233
4234    pub fn wait_signal(&self, signal_name: impl Into<String>) -> SignalCall {
4235        SignalCall {
4236            ctx: self.clone(),
4237            signal_name: signal_name.into(),
4238            opened_wait: false,
4239            matched_pending: false,
4240        }
4241    }
4242
4243    /// Wait for server-backed durable time without blocking the worker executor.
4244    ///
4245    /// Polling this future emits one `start_timer` command and yields. The
4246    /// server records the deadline, so neither worker nor server restarts reset
4247    /// the wait. Replay resolves the future only from a `TimerScheduled` and
4248    /// `TimerFired` pair at the same position in the shared durable-command
4249    /// stream, with matching sequence, timer identity, and delay. Sub-second
4250    /// durations round up because protocol deadlines use whole seconds.
4251    ///
4252    /// ```no_run
4253    /// # use durable_workflow::{json, Client, Worker};
4254    /// # use std::time::Duration;
4255    /// # fn configure(client: Client) {
4256    /// let mut worker = Worker::new(client, "rust-workers");
4257    /// worker.register_workflow("delayed-greeting", |ctx, _input| async move {
4258    ///     ctx.sleep(Duration::from_secs(5)).await?;
4259    ///     Ok(json!({"status": "timer fired"}))
4260    /// });
4261    /// # }
4262    /// ```
4263    pub fn sleep(&self, duration: Duration) -> TimerCall {
4264        let delay_seconds = duration
4265            .as_secs()
4266            .checked_add(u64::from(duration.subsec_nanos() > 0));
4267        TimerCall {
4268            ctx: self.clone(),
4269            delay_seconds,
4270            scheduled: false,
4271            matched_pending: false,
4272        }
4273    }
4274
4275    /// Alias for [`WorkflowContext::sleep`] for timer-oriented workflow code.
4276    pub fn start_timer(&self, duration: Duration) -> TimerCall {
4277        self.sleep(duration)
4278    }
4279
4280    /// Evaluate a non-deterministic callback once and durably record its typed value.
4281    ///
4282    /// On replay the callback is not invoked: the value is decoded from the
4283    /// sequence-matched `SideEffectRecorded` event using the workflow's payload
4284    /// codec. Use this for UUIDs, wall-clock snapshots, random values, and other
4285    /// small values that must remain fixed for the lifetime of a workflow run.
4286    pub fn side_effect<T, F>(&self, callback: F) -> Result<T>
4287    where
4288        T: Serialize + DeserializeOwned,
4289        F: FnOnce() -> T,
4290    {
4291        {
4292            let mut state = self
4293                .state
4294                .lock()
4295                .map_err(|_| Error::WorkflowStatePoisoned)?;
4296            if let Some(recorded) = state.recorded_commands.get(state.command_cursor).cloned() {
4297                return match recorded {
4298                    RecordedCommand::SideEffect { sequence, value } => {
4299                        state.command_cursor += 1;
4300                        serde_json::from_value(value).map_err(|error| {
4301                            Error::NonDeterministicReplay(ReplayFailure::new(
4302                                "side_effect_type_mismatch",
4303                                Some(sequence),
4304                                Some(std::any::type_name::<T>().to_string()),
4305                                Some(error.to_string()),
4306                                "recorded side-effect value is incompatible with the requested Rust type",
4307                            ))
4308                        })
4309                    }
4310                    other => Err(command_mismatch(&other, "side effect")),
4311                };
4312            }
4313        }
4314
4315        let value = callback();
4316        let json_value = serde_json::to_value(&value)?;
4317        let mut state = self
4318            .state
4319            .lock()
4320            .map_err(|_| Error::WorkflowStatePoisoned)?;
4321        let result = encode_value_envelope(&json_value, &state.payload_codec)?;
4322        state.commands.push(json!({
4323            "type": "record_side_effect",
4324            "result": result,
4325        }));
4326        Ok(value)
4327    }
4328
4329    /// Record a UUIDv4 once and return the same UUID on every replay.
4330    pub fn uuid_v4(&self) -> Result<Uuid> {
4331        self.side_effect(Uuid::new_v4)
4332    }
4333
4334    /// Select the newest supported version for a change, or replay the version
4335    /// already committed for that stable change ID.
4336    pub fn get_version(
4337        &self,
4338        change_id: impl Into<String>,
4339        min_supported: i32,
4340        max_supported: i32,
4341    ) -> Result<i32> {
4342        let change_id = change_id.into();
4343        if change_id.trim().is_empty() {
4344            return Err(Error::NonDeterministicReplay(ReplayFailure::new(
4345                "version_change_id_invalid",
4346                None,
4347                Some("non-empty change ID".to_string()),
4348                Some(change_id),
4349                "version markers require a stable non-empty change ID",
4350            )));
4351        }
4352        if min_supported > max_supported {
4353            return Err(Error::NonDeterministicReplay(ReplayFailure::new(
4354                "version_range_invalid",
4355                None,
4356                Some("min_supported <= max_supported".to_string()),
4357                Some(format!("{min_supported}..={max_supported}")),
4358                "version marker supported range is invalid",
4359            )));
4360        }
4361
4362        let mut state = self
4363            .state
4364            .lock()
4365            .map_err(|_| Error::WorkflowStatePoisoned)?;
4366        if let Some((version, sequence)) = state.version_markers.get(&change_id).copied() {
4367            ensure_version_supported(&change_id, version, min_supported, max_supported, sequence)?;
4368            return Ok(version);
4369        }
4370
4371        if let Some(recorded) = state.recorded_commands.get(state.command_cursor).cloned() {
4372            return match recorded {
4373                RecordedCommand::VersionMarker {
4374                    sequence,
4375                    change_id: recorded_change_id,
4376                    version,
4377                    ..
4378                } => {
4379                    if recorded_change_id != change_id {
4380                        return Err(Error::NonDeterministicReplay(ReplayFailure::new(
4381                            "version_change_id_mismatch",
4382                            Some(sequence),
4383                            Some(recorded_change_id),
4384                            Some(change_id),
4385                            "recorded version marker change ID differs from current workflow code",
4386                        )));
4387                    }
4388                    ensure_version_supported(
4389                        &change_id,
4390                        version,
4391                        min_supported,
4392                        max_supported,
4393                        sequence,
4394                    )?;
4395                    state.command_cursor += 1;
4396                    state.version_markers.insert(change_id, (version, sequence));
4397                    Ok(version)
4398                }
4399                other => Err(command_mismatch(
4400                    &other,
4401                    format!("version marker:{change_id}"),
4402                )),
4403            };
4404        }
4405
4406        let version = max_supported;
4407        state.commands.push(json!({
4408            "type": "record_version_marker",
4409            "change_id": change_id,
4410            "version": version,
4411            "min_supported": min_supported,
4412            "max_supported": max_supported,
4413        }));
4414        // Sequence numbers are assigned by the server. Zero identifies a marker
4415        // selected in this uncommitted decision batch for duplicate-call checks.
4416        state.version_markers.insert(change_id, (version, 0));
4417        Ok(version)
4418    }
4419
4420    /// Record or replay the standard `-1` (legacy) / `1` (patched) marker.
4421    pub fn patched(&self, change_id: impl Into<String>) -> Result<bool> {
4422        Ok(self.get_version(change_id, -1, 1)? == 1)
4423    }
4424
4425    /// Keep a patch marker in history after the legacy branch has been removed.
4426    pub fn deprecate_patch(&self, change_id: impl Into<String>) -> Result<()> {
4427        self.get_version(change_id, -1, 1).map(|_| ())
4428    }
4429
4430    /// Start a named durable child on an explicit queue and await its result.
4431    ///
4432    /// The command is recorded in the parent's sequence-ordered durable command
4433    /// stream. Replay keeps a scheduled child pending without emitting another
4434    /// start, or consumes its matching terminal `ChildRun*` outcome. Successful
4435    /// values preserve the history payload codec and include both sides of the
4436    /// durable relationship; failures are returned as
4437    /// [`Error::ChildWorkflowFailed`].
4438    ///
4439    /// ```no_run
4440    /// # use durable_workflow::{json, ChildWorkflowOptions, Client, ParentClosePolicy, Worker};
4441    /// # fn configure(client: Client) {
4442    /// let mut worker = Worker::new(client, "parent-workers");
4443    /// worker.register_workflow("order-parent", |ctx, _input| async move {
4444    ///     let child = ctx
4445    ///         .start_child_workflow(
4446    ///             "fulfil-order",
4447    ///             ChildWorkflowOptions::new("fulfilment-workers")
4448    ///                 .parent_close_policy(ParentClosePolicy::RequestCancel),
4449    ///             json!([{"order_id": "order-42"}]),
4450    ///         )
4451    ///         .await?;
4452    ///     Ok(child.result)
4453    /// });
4454    /// # }
4455    /// ```
4456    pub fn start_child_workflow<T: Serialize>(
4457        &self,
4458        workflow_type: impl Into<String>,
4459        options: ChildWorkflowOptions,
4460        args: T,
4461    ) -> ChildWorkflowCall {
4462        ChildWorkflowCall {
4463            ctx: self.clone(),
4464            workflow_type: workflow_type.into(),
4465            options,
4466            args: Some(serde_json::to_value(args).map_err(Error::from)),
4467            scheduled: false,
4468            matched_pending: false,
4469        }
4470    }
4471
4472    fn take_commands(&self) -> Result<Vec<Value>> {
4473        let mut state = self
4474            .state
4475            .lock()
4476            .map_err(|_| Error::WorkflowStatePoisoned)?;
4477        Ok(std::mem::take(&mut state.commands))
4478    }
4479
4480    fn matched_recorded_pending(&self) -> Result<bool> {
4481        let state = self
4482            .state
4483            .lock()
4484            .map_err(|_| Error::WorkflowStatePoisoned)?;
4485        Ok(state.matched_recorded_pending)
4486    }
4487
4488    fn ensure_history_consumed(&self) -> Result<()> {
4489        let state = self
4490            .state
4491            .lock()
4492            .map_err(|_| Error::WorkflowStatePoisoned)?;
4493        if let Some(command) = state.recorded_commands.get(state.command_cursor) {
4494            return Err(Error::NonDeterministicReplay(ReplayFailure::new(
4495                "recorded_commands_unconsumed",
4496                Some(command.sequence()),
4497                Some(command.shape().to_string()),
4498                Some("workflow completion".to_string()),
4499                "workflow completed before consuming all recorded durable commands",
4500            )));
4501        }
4502        Ok(())
4503    }
4504}
4505
4506#[derive(Debug)]
4507struct WorkflowState {
4508    history: Vec<HistoryEvent>,
4509    workflow_id: Option<String>,
4510    run_id: Option<String>,
4511    task_queue: String,
4512    payload_codec: String,
4513    resume_signal: Option<ResumeSignal>,
4514    recorded_commands: Vec<RecordedCommand>,
4515    command_cursor: usize,
4516    matched_recorded_pending: bool,
4517    signal_cursors: HashMap<String, usize>,
4518    version_markers: HashMap<String, (i32, u64)>,
4519    commands: Vec<Value>,
4520}
4521
4522impl WorkflowState {
4523    #[cfg(test)]
4524    fn new(
4525        history: Vec<HistoryEvent>,
4526        task_queue: String,
4527        payload_codec: String,
4528        resume_signal: Option<ResumeSignal>,
4529    ) -> Result<Self> {
4530        Self::new_with_identity(
4531            history,
4532            None,
4533            None,
4534            task_queue,
4535            payload_codec,
4536            resume_signal,
4537        )
4538    }
4539
4540    fn new_with_identity(
4541        history: Vec<HistoryEvent>,
4542        workflow_id: Option<String>,
4543        run_id: Option<String>,
4544        task_queue: String,
4545        payload_codec: String,
4546        resume_signal: Option<ResumeSignal>,
4547    ) -> Result<Self> {
4548        let recorded_commands = recorded_commands(
4549            &history,
4550            &payload_codec,
4551            WorkflowIdentity {
4552                workflow_id: workflow_id.clone(),
4553                run_id: run_id.clone(),
4554            },
4555        )?;
4556        Ok(Self {
4557            history,
4558            workflow_id,
4559            run_id,
4560            task_queue,
4561            payload_codec,
4562            resume_signal,
4563            recorded_commands,
4564            command_cursor: 0,
4565            matched_recorded_pending: false,
4566            signal_cursors: HashMap::new(),
4567            version_markers: HashMap::new(),
4568            commands: Vec::new(),
4569        })
4570    }
4571}
4572
4573#[derive(Clone, Debug)]
4574enum RecordedCommand {
4575    Activity {
4576        sequence: u64,
4577        activity_type: Option<String>,
4578        options: Option<RecordedActivityOptions>,
4579        outcome: Option<ActivityOutcome>,
4580    },
4581    Timer {
4582        sequence: u64,
4583        delay_seconds: u64,
4584        fired: bool,
4585    },
4586    ChildWorkflow {
4587        sequence: u64,
4588        workflow_type: Option<String>,
4589        outcome: Option<ChildWorkflowOutcome>,
4590    },
4591    SignalWait {
4592        sequence: u64,
4593        signal_name: Option<String>,
4594    },
4595    SideEffect {
4596        sequence: u64,
4597        value: Value,
4598    },
4599    VersionMarker {
4600        sequence: u64,
4601        change_id: String,
4602        version: i32,
4603    },
4604}
4605
4606#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
4607struct RecordedActivityOptions {
4608    task_queue: RecordedSnapshotValue<Option<String>>,
4609    execution_mode: RecordedSnapshotValue<Option<String>>,
4610    retry_policy: ActivityRetrySnapshot,
4611}
4612
4613#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
4614enum RecordedSnapshotValue<T> {
4615    /// Older history did not persist this field, so it cannot constrain replay.
4616    Unknown,
4617    Known(T),
4618}
4619
4620impl<T: PartialEq> RecordedSnapshotValue<T> {
4621    fn matches_current(&self, current: &Self) -> bool {
4622        match self {
4623            Self::Unknown => true,
4624            Self::Known(recorded) => matches!(current, Self::Known(value) if value == recorded),
4625        }
4626    }
4627}
4628
4629#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
4630struct ActivityRetrySnapshot {
4631    snapshot_version: RecordedSnapshotValue<Option<u64>>,
4632    max_attempts: RecordedSnapshotValue<Option<u64>>,
4633    backoff_seconds: RecordedSnapshotValue<Vec<u64>>,
4634    start_to_close_timeout: RecordedSnapshotValue<Option<u64>>,
4635    schedule_to_start_timeout: RecordedSnapshotValue<Option<u64>>,
4636    schedule_to_close_timeout: RecordedSnapshotValue<Option<u64>>,
4637    heartbeat_timeout: RecordedSnapshotValue<Option<u64>>,
4638    non_retryable_error_types: RecordedSnapshotValue<Vec<String>>,
4639}
4640
4641impl ActivityRetrySnapshot {
4642    fn matches_current(&self, current: &Self) -> bool {
4643        self.snapshot_version
4644            .matches_current(&current.snapshot_version)
4645            && self.max_attempts.matches_current(&current.max_attempts)
4646            && self
4647                .backoff_seconds
4648                .matches_current(&current.backoff_seconds)
4649            && self
4650                .start_to_close_timeout
4651                .matches_current(&current.start_to_close_timeout)
4652            && self
4653                .schedule_to_start_timeout
4654                .matches_current(&current.schedule_to_start_timeout)
4655            && self
4656                .schedule_to_close_timeout
4657                .matches_current(&current.schedule_to_close_timeout)
4658            && self
4659                .heartbeat_timeout
4660                .matches_current(&current.heartbeat_timeout)
4661            && self
4662                .non_retryable_error_types
4663                .matches_current(&current.non_retryable_error_types)
4664    }
4665}
4666
4667fn recorded_optional_u64(
4668    object: Option<&serde_json::Map<String, Value>>,
4669    field: &str,
4670) -> RecordedSnapshotValue<Option<u64>> {
4671    match object.and_then(|object| object.get(field)) {
4672        None => RecordedSnapshotValue::Unknown,
4673        Some(Value::Null) => RecordedSnapshotValue::Known(None),
4674        Some(value) => RecordedSnapshotValue::Known(value_as_u64(value)),
4675    }
4676}
4677
4678fn recorded_optional_string(
4679    object: &serde_json::Map<String, Value>,
4680    field: &str,
4681) -> RecordedSnapshotValue<Option<String>> {
4682    match object.get(field) {
4683        None => RecordedSnapshotValue::Unknown,
4684        Some(Value::Null) => RecordedSnapshotValue::Known(None),
4685        Some(value) => RecordedSnapshotValue::Known(value.as_str().map(str::to_string)),
4686    }
4687}
4688
4689fn recorded_activity_retry_snapshot(policy: Option<&Value>) -> ActivityRetrySnapshot {
4690    let policy = policy.and_then(Value::as_object);
4691    let backoff_seconds = policy
4692        .and_then(|policy| policy.get("backoff_seconds"))
4693        .and_then(Value::as_array)
4694        .map(|intervals| intervals.iter().filter_map(value_as_u64).collect())
4695        .map_or(RecordedSnapshotValue::Unknown, RecordedSnapshotValue::Known);
4696    let mut non_retryable_error_types = Vec::new();
4697    for error_type in policy
4698        .and_then(|policy| policy.get("non_retryable_error_types"))
4699        .and_then(Value::as_array)
4700        .into_iter()
4701        .flatten()
4702        .filter_map(Value::as_str)
4703        .map(str::trim)
4704        .filter(|error_type| !error_type.is_empty())
4705    {
4706        if !non_retryable_error_types
4707            .iter()
4708            .any(|recorded| recorded == error_type)
4709        {
4710            non_retryable_error_types.push(error_type.to_string());
4711        }
4712    }
4713
4714    ActivityRetrySnapshot {
4715        snapshot_version: recorded_optional_u64(policy, "snapshot_version"),
4716        max_attempts: recorded_optional_u64(policy, "max_attempts"),
4717        backoff_seconds,
4718        start_to_close_timeout: recorded_optional_u64(policy, "start_to_close_timeout"),
4719        schedule_to_start_timeout: recorded_optional_u64(policy, "schedule_to_start_timeout"),
4720        schedule_to_close_timeout: recorded_optional_u64(policy, "schedule_to_close_timeout"),
4721        heartbeat_timeout: recorded_optional_u64(policy, "heartbeat_timeout"),
4722        non_retryable_error_types: if policy
4723            .is_some_and(|policy| policy.contains_key("non_retryable_error_types"))
4724        {
4725            RecordedSnapshotValue::Known(non_retryable_error_types)
4726        } else {
4727            RecordedSnapshotValue::Unknown
4728        },
4729    }
4730}
4731
4732fn current_activity_retry_snapshot(options: &ValidatedActivityOptions) -> ActivityRetrySnapshot {
4733    let policy = options.retry_policy.as_ref();
4734    let max_attempts = match policy.and_then(|policy| policy.get("max_attempts")) {
4735        Some(Value::Null) => None,
4736        Some(value) => value_as_u64(value),
4737        None => Some(1),
4738    };
4739    let backoff_seconds = policy
4740        .and_then(|policy| policy.get("backoff_seconds"))
4741        .and_then(Value::as_array)
4742        .map(|intervals| intervals.iter().filter_map(value_as_u64).collect())
4743        .unwrap_or_default();
4744    let non_retryable_error_types = policy
4745        .and_then(|policy| policy.get("non_retryable_error_types"))
4746        .and_then(Value::as_array)
4747        .into_iter()
4748        .flatten()
4749        .filter_map(Value::as_str)
4750        .map(str::to_string)
4751        .collect();
4752
4753    ActivityRetrySnapshot {
4754        snapshot_version: RecordedSnapshotValue::Known(Some(1)),
4755        max_attempts: RecordedSnapshotValue::Known(max_attempts),
4756        backoff_seconds: RecordedSnapshotValue::Known(backoff_seconds),
4757        start_to_close_timeout: RecordedSnapshotValue::Known(options.start_to_close_timeout),
4758        schedule_to_start_timeout: RecordedSnapshotValue::Known(options.schedule_to_start_timeout),
4759        schedule_to_close_timeout: RecordedSnapshotValue::Known(options.schedule_to_close_timeout),
4760        heartbeat_timeout: RecordedSnapshotValue::Known(options.heartbeat_timeout),
4761        non_retryable_error_types: RecordedSnapshotValue::Known(non_retryable_error_types),
4762    }
4763}
4764
4765fn activity_options_description(options: &RecordedActivityOptions) -> String {
4766    serde_json::to_string(options).unwrap_or_else(|_| format!("{options:?}"))
4767}
4768
4769impl RecordedCommand {
4770    fn sequence(&self) -> u64 {
4771        match self {
4772            Self::Activity { sequence, .. }
4773            | Self::Timer { sequence, .. }
4774            | Self::ChildWorkflow { sequence, .. }
4775            | Self::SignalWait { sequence, .. }
4776            | Self::SideEffect { sequence, .. }
4777            | Self::VersionMarker { sequence, .. } => *sequence,
4778        }
4779    }
4780
4781    fn shape(&self) -> &'static str {
4782        match self {
4783            Self::Activity { .. } => "activity",
4784            Self::Timer { .. } => "timer",
4785            Self::ChildWorkflow { .. } => "child workflow",
4786            Self::SignalWait { .. } => "signal wait",
4787            Self::SideEffect { .. } => "side effect",
4788            Self::VersionMarker { .. } => "version marker",
4789        }
4790    }
4791}
4792
4793fn ensure_version_supported(
4794    change_id: &str,
4795    version: i32,
4796    min_supported: i32,
4797    max_supported: i32,
4798    sequence: u64,
4799) -> Result<()> {
4800    if (min_supported..=max_supported).contains(&version) {
4801        return Ok(());
4802    }
4803    Err(Error::NonDeterministicReplay(ReplayFailure::new(
4804        "version_marker_incompatible_range",
4805        (sequence != 0).then_some(sequence),
4806        Some(format!("{min_supported}..={max_supported}")),
4807        Some(format!("{change_id}:{version}")),
4808        "recorded workflow version is outside the range supported by current code",
4809    )))
4810}
4811
4812#[derive(Clone, Debug)]
4813struct ResumeSignal {
4814    signal_id: Option<String>,
4815    signal_name: String,
4816    arguments: Vec<Value>,
4817}
4818
4819pub struct ActivityCall {
4820    ctx: WorkflowContext,
4821    activity_type: String,
4822    options: ActivityOptions,
4823    args: Option<Result<Value>>,
4824    scheduled: bool,
4825}
4826
4827impl Future for ActivityCall {
4828    type Output = Result<Value>;
4829
4830    fn poll(mut self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
4831        let ctx = self.ctx.clone();
4832        let mut state = match ctx.state.lock() {
4833            Ok(state) => state,
4834            Err(_) => return Poll::Ready(Err(Error::WorkflowStatePoisoned)),
4835        };
4836
4837        if self.scheduled {
4838            return Poll::Pending;
4839        }
4840
4841        let options = match self.options.validate() {
4842            Ok(options) => options,
4843            Err(error) => {
4844                return Poll::Ready(Err(Error::InvalidActivityOptions(error)));
4845            }
4846        };
4847        let task_queue = options
4848            .task_queue
4849            .clone()
4850            .unwrap_or_else(|| state.task_queue.clone());
4851        let current_recorded_options = RecordedActivityOptions {
4852            task_queue: RecordedSnapshotValue::Known(Some(task_queue.clone())),
4853            // Rust schedules ordinary durable activities. The server records a
4854            // non-null mode only for a specialized execution primitive.
4855            execution_mode: RecordedSnapshotValue::Known(None),
4856            retry_policy: current_activity_retry_snapshot(&options),
4857        };
4858
4859        if let Some(recorded) = state.recorded_commands.get(state.command_cursor).cloned() {
4860            let sequence = recorded.sequence();
4861            match recorded {
4862                RecordedCommand::Activity {
4863                    activity_type,
4864                    options: recorded_options,
4865                    outcome,
4866                    ..
4867                } => {
4868                    if let Some(recorded_type) = activity_type {
4869                        if recorded_type != self.activity_type {
4870                            return Poll::Ready(Err(Error::NonDeterministicReplay(
4871                                ReplayFailure::new(
4872                                    "recorded_command_detail_mismatch",
4873                                    Some(sequence),
4874                                    Some(format!("activity:{recorded_type}")),
4875                                    Some(format!("activity:{}", self.activity_type)),
4876                                    "recorded activity type differs from the current workflow command",
4877                                ),
4878                            )));
4879                        }
4880                    }
4881                    if let Some(recorded_options) = recorded_options {
4882                        if !recorded_options
4883                            .task_queue
4884                            .matches_current(&current_recorded_options.task_queue)
4885                        {
4886                            return Poll::Ready(Err(Error::NonDeterministicReplay(
4887                                ReplayFailure::new(
4888                                    "activity_task_queue_mismatch",
4889                                    Some(sequence),
4890                                    Some(activity_options_description(&recorded_options)),
4891                                    Some(activity_options_description(&current_recorded_options)),
4892                                    "recorded activity task queue differs from the current workflow command",
4893                                ),
4894                            )));
4895                        }
4896                        if !recorded_options
4897                            .execution_mode
4898                            .matches_current(&current_recorded_options.execution_mode)
4899                        {
4900                            return Poll::Ready(Err(Error::NonDeterministicReplay(
4901                                ReplayFailure::new(
4902                                    "activity_execution_mode_mismatch",
4903                                    Some(sequence),
4904                                    Some(activity_options_description(&recorded_options)),
4905                                    Some(activity_options_description(&current_recorded_options)),
4906                                    "recorded activity execution mode differs from the current workflow command",
4907                                ),
4908                            )));
4909                        }
4910                        if !recorded_options
4911                            .retry_policy
4912                            .matches_current(&current_recorded_options.retry_policy)
4913                        {
4914                            return Poll::Ready(Err(Error::NonDeterministicReplay(
4915                                ReplayFailure::new(
4916                                    "activity_retry_policy_mismatch",
4917                                    Some(sequence),
4918                                    Some(activity_options_description(&recorded_options)),
4919                                    Some(activity_options_description(&current_recorded_options)),
4920                                    "recorded activity retry policy differs from the current workflow command",
4921                                ),
4922                            )));
4923                        }
4924                    }
4925                    state.command_cursor += 1;
4926                    if let Some(outcome) = outcome {
4927                        return Poll::Ready(outcome.map_err(Error::ActivityFailed));
4928                    }
4929                    state.matched_recorded_pending = true;
4930                    self.scheduled = true;
4931                    return Poll::Pending;
4932                }
4933                other => {
4934                    return Poll::Ready(Err(command_mismatch(
4935                        &other,
4936                        format!("activity:{}", self.activity_type),
4937                    )));
4938                }
4939            }
4940        }
4941
4942        if !self.scheduled {
4943            let args = match self.args.take().unwrap_or(Ok(Value::Null)) {
4944                Ok(args) => args,
4945                Err(error) => return Poll::Ready(Err(error)),
4946            };
4947            let arguments = normalize_arguments(args);
4948            let envelope = match encode_value_envelope(&arguments, &state.payload_codec) {
4949                Ok(envelope) => envelope,
4950                Err(error) => return Poll::Ready(Err(error)),
4951            };
4952
4953            let mut command = serde_json::Map::from_iter([
4954                ("type".to_string(), json!("schedule_activity")),
4955                (
4956                    "activity_type".to_string(),
4957                    json!(self.activity_type.clone()),
4958                ),
4959                ("queue".to_string(), json!(task_queue)),
4960                ("arguments".to_string(), envelope),
4961            ]);
4962            for (field, value) in [
4963                ("start_to_close_timeout", options.start_to_close_timeout),
4964                (
4965                    "schedule_to_start_timeout",
4966                    options.schedule_to_start_timeout,
4967                ),
4968                (
4969                    "schedule_to_close_timeout",
4970                    options.schedule_to_close_timeout,
4971                ),
4972                ("heartbeat_timeout", options.heartbeat_timeout),
4973            ] {
4974                if let Some(value) = value {
4975                    command.insert(field.to_string(), json!(value));
4976                }
4977            }
4978            if let Some(retry_policy) = options.retry_policy {
4979                command.insert("retry_policy".to_string(), retry_policy);
4980            }
4981            state.commands.push(Value::Object(command));
4982            self.scheduled = true;
4983        }
4984
4985        Poll::Pending
4986    }
4987}
4988
4989/// Future returned by [`WorkflowContext::sleep`].
4990pub struct TimerCall {
4991    ctx: WorkflowContext,
4992    delay_seconds: Option<u64>,
4993    scheduled: bool,
4994    matched_pending: bool,
4995}
4996
4997impl Future for TimerCall {
4998    type Output = Result<()>;
4999
5000    fn poll(mut self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
5001        if self.matched_pending {
5002            return Poll::Pending;
5003        }
5004
5005        let ctx = self.ctx.clone();
5006        let Some(requested_delay) = self.delay_seconds else {
5007            return Poll::Ready(Err(Error::TimerDurationOverflow));
5008        };
5009        let mut state = match ctx.state.lock() {
5010            Ok(state) => state,
5011            Err(_) => return Poll::Ready(Err(Error::WorkflowStatePoisoned)),
5012        };
5013
5014        if let Some(recorded) = state.recorded_commands.get(state.command_cursor).cloned() {
5015            match recorded {
5016                RecordedCommand::Timer {
5017                    sequence,
5018                    delay_seconds,
5019                    fired,
5020                    ..
5021                } => {
5022                    if delay_seconds != requested_delay {
5023                        return Poll::Ready(Err(Error::NonDeterministicReplay(
5024                            ReplayFailure::new(
5025                                "timer_delay_mismatch",
5026                                Some(sequence),
5027                                Some(format!("timer:{delay_seconds}s")),
5028                                Some(format!("timer:{requested_delay}s")),
5029                                "recorded timer delay differs from the current workflow command",
5030                            ),
5031                        )));
5032                    }
5033                    state.command_cursor += 1;
5034                    if fired {
5035                        return Poll::Ready(Ok(()));
5036                    }
5037                    state.matched_recorded_pending = true;
5038                    self.scheduled = true;
5039                    self.matched_pending = true;
5040                    return Poll::Pending;
5041                }
5042                other => return Poll::Ready(Err(command_mismatch(&other, "timer"))),
5043            }
5044        }
5045
5046        if !self.scheduled {
5047            state.commands.push(json!({
5048                "type": "start_timer",
5049                "delay_seconds": requested_delay,
5050            }));
5051            self.scheduled = true;
5052        }
5053
5054        Poll::Pending
5055    }
5056}
5057
5058/// Future returned by [`WorkflowContext::start_child_workflow`].
5059pub struct ChildWorkflowCall {
5060    ctx: WorkflowContext,
5061    workflow_type: String,
5062    options: ChildWorkflowOptions,
5063    args: Option<Result<Value>>,
5064    scheduled: bool,
5065    matched_pending: bool,
5066}
5067
5068impl Future for ChildWorkflowCall {
5069    type Output = Result<ChildWorkflowResult>;
5070
5071    fn poll(mut self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
5072        if self.matched_pending {
5073            return Poll::Pending;
5074        }
5075
5076        let ctx = self.ctx.clone();
5077        let mut state = match ctx.state.lock() {
5078            Ok(state) => state,
5079            Err(_) => return Poll::Ready(Err(Error::WorkflowStatePoisoned)),
5080        };
5081
5082        if let Some(recorded) = state.recorded_commands.get(state.command_cursor).cloned() {
5083            let sequence = recorded.sequence();
5084            match recorded {
5085                RecordedCommand::ChildWorkflow {
5086                    workflow_type,
5087                    outcome,
5088                    ..
5089                } => {
5090                    if let Some(recorded_type) = workflow_type {
5091                        if recorded_type != self.workflow_type {
5092                            return Poll::Ready(Err(Error::NonDeterministicReplay(
5093                                ReplayFailure::new(
5094                                    "recorded_command_detail_mismatch",
5095                                    Some(sequence),
5096                                    Some(format!("child workflow:{recorded_type}")),
5097                                    Some(format!("child workflow:{}", self.workflow_type)),
5098                                    "recorded child workflow type differs from the current workflow command",
5099                                ),
5100                            )));
5101                        }
5102                    }
5103                    state.command_cursor += 1;
5104                    if let Some(outcome) = outcome {
5105                        return Poll::Ready(outcome.map_err(Error::ChildWorkflowFailed));
5106                    }
5107                    state.matched_recorded_pending = true;
5108                    self.scheduled = true;
5109                    self.matched_pending = true;
5110                    return Poll::Pending;
5111                }
5112                other => {
5113                    return Poll::Ready(Err(command_mismatch(
5114                        &other,
5115                        format!("child workflow:{}", self.workflow_type),
5116                    )));
5117                }
5118            }
5119        }
5120
5121        if !self.scheduled {
5122            if self.options.task_queue.trim().is_empty() {
5123                return Poll::Ready(Err(Error::InvalidChildWorkflowOptions(
5124                    "task_queue must not be empty".to_string(),
5125                )));
5126            }
5127            for (name, value) in [
5128                (
5129                    "execution_timeout_seconds",
5130                    self.options.execution_timeout_seconds,
5131                ),
5132                ("run_timeout_seconds", self.options.run_timeout_seconds),
5133            ] {
5134                if value == Some(0) {
5135                    return Poll::Ready(Err(Error::InvalidChildWorkflowOptions(format!(
5136                        "{name} must be at least 1"
5137                    ))));
5138                }
5139            }
5140
5141            let args = match self.args.take().unwrap_or(Ok(Value::Null)) {
5142                Ok(args) => args,
5143                Err(error) => return Poll::Ready(Err(error)),
5144            };
5145            let arguments =
5146                match encode_value_envelope(&normalize_arguments(args), &state.payload_codec) {
5147                    Ok(arguments) => arguments,
5148                    Err(error) => return Poll::Ready(Err(error)),
5149                };
5150            let mut command = json!({
5151                "type": "start_child_workflow",
5152                "workflow_type": self.workflow_type,
5153                "queue": self.options.task_queue,
5154                "parent_close_policy": self.options.parent_close_policy.as_str(),
5155                "arguments": arguments,
5156            });
5157            let object = command
5158                .as_object_mut()
5159                .expect("child workflow command is always an object");
5160            if let Some(policy) = &self.options.retry_policy {
5161                let mut retry_policy = serde_json::Map::new();
5162                if let Some(max_attempts) = policy.max_attempts {
5163                    if max_attempts == 0 {
5164                        return Poll::Ready(Err(Error::InvalidChildWorkflowOptions(
5165                            "retry_policy.max_attempts must be at least 1".to_string(),
5166                        )));
5167                    }
5168                    retry_policy.insert("max_attempts".to_string(), json!(max_attempts));
5169                }
5170                if !policy.backoff_seconds.is_empty() {
5171                    retry_policy
5172                        .insert("backoff_seconds".to_string(), json!(policy.backoff_seconds));
5173                }
5174                if !policy.non_retryable_error_types.is_empty() {
5175                    retry_policy.insert(
5176                        "non_retryable_error_types".to_string(),
5177                        json!(policy.non_retryable_error_types),
5178                    );
5179                }
5180                if retry_policy.is_empty() {
5181                    return Poll::Ready(Err(Error::InvalidChildWorkflowOptions(
5182                        "retry_policy must configure at least one field".to_string(),
5183                    )));
5184                }
5185                object.insert("retry_policy".to_string(), Value::Object(retry_policy));
5186            }
5187            if let Some(seconds) = self.options.execution_timeout_seconds {
5188                object.insert("execution_timeout_seconds".to_string(), json!(seconds));
5189            }
5190            if let Some(seconds) = self.options.run_timeout_seconds {
5191                object.insert("run_timeout_seconds".to_string(), json!(seconds));
5192            }
5193            state.commands.push(command);
5194            self.scheduled = true;
5195        }
5196
5197        Poll::Pending
5198    }
5199}
5200
5201fn command_mismatch(recorded: &RecordedCommand, actual: impl Into<String>) -> Error {
5202    Error::NonDeterministicReplay(ReplayFailure::new(
5203        "recorded_command_mismatch",
5204        Some(recorded.sequence()),
5205        Some(recorded.shape().to_string()),
5206        Some(actual.into()),
5207        "current workflow command does not match the recorded durable command sequence",
5208    ))
5209}
5210
5211pub struct SignalCall {
5212    ctx: WorkflowContext,
5213    signal_name: String,
5214    opened_wait: bool,
5215    matched_pending: bool,
5216}
5217
5218impl Future for SignalCall {
5219    type Output = Result<Vec<Value>>;
5220
5221    fn poll(mut self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
5222        if self.matched_pending {
5223            return Poll::Pending;
5224        }
5225
5226        let ctx = self.ctx.clone();
5227        let mut state = match ctx.state.lock() {
5228            Ok(state) => state,
5229            Err(_) => return Poll::Ready(Err(Error::WorkflowStatePoisoned)),
5230        };
5231
5232        let signals = match signal_values(
5233            &state.history,
5234            &self.signal_name,
5235            &state.payload_codec,
5236            state.resume_signal.as_ref(),
5237        ) {
5238            Ok(signals) => signals,
5239            Err(error) => return Poll::Ready(Err(error)),
5240        };
5241        let cursor = *state.signal_cursors.get(&self.signal_name).unwrap_or(&0);
5242
5243        if let Some(recorded) = state.recorded_commands.get(state.command_cursor).cloned() {
5244            match recorded {
5245                RecordedCommand::SignalWait {
5246                    sequence,
5247                    signal_name,
5248                } => {
5249                    if let Some(recorded_name) = signal_name {
5250                        if recorded_name != self.signal_name {
5251                            return Poll::Ready(Err(Error::NonDeterministicReplay(
5252                                ReplayFailure::new(
5253                                    "recorded_command_detail_mismatch",
5254                                    Some(sequence),
5255                                    Some(format!("signal wait:{recorded_name}")),
5256                                    Some(format!("signal wait:{}", self.signal_name)),
5257                                    "recorded signal name differs from the current workflow command",
5258                                ),
5259                            )));
5260                        }
5261                    }
5262
5263                    state.command_cursor += 1;
5264                    if cursor < signals.len() {
5265                        state
5266                            .signal_cursors
5267                            .insert(self.signal_name.clone(), cursor + 1);
5268                        return Poll::Ready(Ok(signals[cursor].clone()));
5269                    }
5270
5271                    state.matched_recorded_pending = true;
5272                    self.opened_wait = true;
5273                    self.matched_pending = true;
5274                    return Poll::Pending;
5275                }
5276                other => {
5277                    return Poll::Ready(Err(command_mismatch(
5278                        &other,
5279                        format!("signal wait:{}", self.signal_name),
5280                    )));
5281                }
5282            }
5283        }
5284
5285        if cursor < signals.len() {
5286            state
5287                .signal_cursors
5288                .insert(self.signal_name.clone(), cursor + 1);
5289            return Poll::Ready(Ok(signals[cursor].clone()));
5290        }
5291
5292        if !self.opened_wait {
5293            state.commands.push(json!({
5294                "type": "open_condition_wait",
5295                "condition_key": format!("signal:{}", self.signal_name)
5296            }));
5297            self.opened_wait = true;
5298        }
5299
5300        Poll::Pending
5301    }
5302}
5303
5304#[derive(Clone, Debug)]
5305pub struct ActivityContext {
5306    client: Client,
5307    pub task_id: String,
5308    pub activity_attempt_id: String,
5309    pub lease_owner: String,
5310    pub activity_type: String,
5311    pub attempt_number: u64,
5312    pub task_queue: String,
5313    pub worker_id: String,
5314}
5315
5316impl ActivityContext {
5317    pub async fn heartbeat<T: Serialize>(&self, details: T) -> Result<ActivityHeartbeatResponse> {
5318        self.client
5319            .heartbeat_activity_task(
5320                &self.task_id,
5321                &self.activity_attempt_id,
5322                &self.lease_owner,
5323                serde_json::to_value(details)?,
5324            )
5325            .await
5326    }
5327}
5328
5329fn decode_task_arguments(value: Option<&Value>, codec: &str) -> Result<Value> {
5330    match value {
5331        Some(value) => Ok(normalize_arguments(decode_wire_value(value, codec)?)),
5332        None => Ok(Value::Array(Vec::new())),
5333    }
5334}
5335
5336fn decode_resume_signal(task: &WorkflowTask) -> Result<Option<ResumeSignal>> {
5337    let Some(signal_name) = task
5338        .signal_name
5339        .as_deref()
5340        .filter(|value| !value.is_empty())
5341    else {
5342        return Ok(None);
5343    };
5344    let Some(arguments) = task.signal_arguments.as_ref() else {
5345        return Ok(None);
5346    };
5347
5348    let decoded = normalize_arguments(decode_wire_value(arguments, &task.payload_codec)?);
5349    let Value::Array(arguments) = decoded else {
5350        unreachable!("normalize_arguments always returns an array");
5351    };
5352
5353    Ok(Some(ResumeSignal {
5354        signal_id: task.workflow_signal_id.clone(),
5355        signal_name: signal_name.to_string(),
5356        arguments,
5357    }))
5358}
5359
5360fn normalize_arguments(value: Value) -> Value {
5361    match value {
5362        Value::Null => Value::Array(Vec::new()),
5363        Value::Array(_) => value,
5364        other => Value::Array(vec![other]),
5365    }
5366}
5367
5368fn recorded_commands(
5369    events: &[HistoryEvent],
5370    fallback_codec: &str,
5371    parent: WorkflowIdentity,
5372) -> Result<Vec<RecordedCommand>> {
5373    let mut events_by_sequence: BTreeMap<u64, Vec<&HistoryEvent>> = BTreeMap::new();
5374
5375    for event in events {
5376        let is_activity = matches!(
5377            event.event_type.as_str(),
5378            "ActivityScheduled"
5379                | "ActivityStarted"
5380                | "ActivityHeartbeatRecorded"
5381                | "ActivityRetryScheduled"
5382                | "ActivityCompleted"
5383                | "ActivityFailed"
5384                | "ActivityCancelled"
5385                | "ActivityTimedOut"
5386        );
5387        let is_workflow_timer = matches!(
5388            event.event_type.as_str(),
5389            "TimerScheduled" | "TimerCancelled" | "TimerFired"
5390        ) && !is_internal_timer_event(event);
5391        let is_child_workflow = matches!(
5392            event.event_type.as_str(),
5393            "ChildWorkflowScheduled"
5394                | "ChildRunCompleted"
5395                | "ChildRunFailed"
5396                | "ChildRunCancelled"
5397                | "ChildRunTerminated"
5398        );
5399        let is_signal_wait = is_recorded_signal_wait_event(event);
5400        let is_side_effect = event.event_type == "SideEffectRecorded";
5401        let is_version_marker = event.event_type == "VersionMarkerRecorded";
5402        if !is_activity
5403            && !is_workflow_timer
5404            && !is_child_workflow
5405            && !is_signal_wait
5406            && !is_side_effect
5407            && !is_version_marker
5408        {
5409            continue;
5410        }
5411
5412        let sequence = durable_event_sequence(event).ok_or_else(|| {
5413            Error::NonDeterministicReplay(ReplayFailure::new(
5414                "durable_command_sequence_missing",
5415                None,
5416                Some("positive workflow sequence".to_string()),
5417                Some(event.event_type.clone()),
5418                "durable command history event has no workflow sequence",
5419            ))
5420        })?;
5421        if sequence == 0 {
5422            return Err(Error::NonDeterministicReplay(ReplayFailure::new(
5423                "durable_command_sequence_invalid",
5424                Some(sequence),
5425                Some("positive workflow sequence".to_string()),
5426                Some(sequence.to_string()),
5427                "durable command history uses an invalid workflow sequence",
5428            )));
5429        }
5430        events_by_sequence.entry(sequence).or_default().push(event);
5431    }
5432
5433    let commands: Vec<RecordedCommand> = events_by_sequence
5434        .into_iter()
5435        .map(|(sequence, sequence_events)| {
5436            let activity_events: Vec<_> = sequence_events
5437                .iter()
5438                .copied()
5439                .filter(|event| event.event_type.starts_with("Activity"))
5440                .collect();
5441            let timer_events: Vec<_> = sequence_events
5442                .iter()
5443                .copied()
5444                .filter(|event| event.event_type.starts_with("Timer"))
5445                .collect();
5446            let child_events: Vec<_> = sequence_events
5447                .iter()
5448                .copied()
5449                .filter(|event| {
5450                    event.event_type == "ChildWorkflowScheduled"
5451                        || event.event_type.starts_with("ChildRun")
5452                })
5453                .collect();
5454            let signal_wait_events: Vec<_> = sequence_events
5455                .iter()
5456                .copied()
5457                .filter(|event| is_recorded_signal_wait_event(event))
5458                .collect();
5459            let side_effect_events: Vec<_> = sequence_events
5460                .iter()
5461                .copied()
5462                .filter(|event| event.event_type == "SideEffectRecorded")
5463                .collect();
5464            let version_marker_events: Vec<_> = sequence_events
5465                .iter()
5466                .copied()
5467                .filter(|event| event.event_type == "VersionMarkerRecorded")
5468                .collect();
5469
5470            let command_kind_count = usize::from(!activity_events.is_empty())
5471                + usize::from(!timer_events.is_empty())
5472                + usize::from(!child_events.is_empty())
5473                + usize::from(!signal_wait_events.is_empty())
5474                + usize::from(!side_effect_events.is_empty())
5475                + usize::from(!version_marker_events.is_empty());
5476            if command_kind_count > 1 {
5477                let actual = [
5478                    (!activity_events.is_empty()).then_some("activity"),
5479                    (!timer_events.is_empty()).then_some("timer"),
5480                    (!child_events.is_empty()).then_some("child workflow"),
5481                    (!signal_wait_events.is_empty()).then_some("signal wait"),
5482                    (!side_effect_events.is_empty()).then_some("side effect"),
5483                    (!version_marker_events.is_empty()).then_some("version marker"),
5484                ]
5485                .into_iter()
5486                .flatten()
5487                .collect::<Vec<_>>()
5488                .join(" and ");
5489                return Err(invalid_recorded_history(
5490                    "durable_command_sequence_collision",
5491                    sequence,
5492                    "one durable command kind",
5493                    &actual,
5494                    "one workflow sequence records more than one durable command kind",
5495                ));
5496            }
5497
5498            if !activity_events.is_empty() {
5499                let scheduled_count = activity_events
5500                    .iter()
5501                    .filter(|event| event.event_type == "ActivityScheduled")
5502                    .count();
5503                if scheduled_count > 1 {
5504                    return Err(invalid_recorded_history(
5505                        "duplicate_activity_schedule",
5506                        sequence,
5507                        "at most one ActivityScheduled event",
5508                        "multiple ActivityScheduled events",
5509                        "activity history schedules more than one command at one workflow sequence",
5510                    ));
5511                }
5512                let activity_type = activity_events.iter().find_map(|event| {
5513                    event
5514                        .payload
5515                        .get("activity_type")
5516                        .or_else(|| event.payload.get("activity_name"))
5517                        .and_then(Value::as_str)
5518                        .map(str::to_string)
5519                });
5520                if activity_events.iter().filter_map(|event| {
5521                    event
5522                        .payload
5523                        .get("activity_type")
5524                        .or_else(|| event.payload.get("activity_name"))
5525                        .and_then(Value::as_str)
5526                }).any(|candidate| Some(candidate) != activity_type.as_deref()) {
5527                    return Err(invalid_recorded_history(
5528                        "activity_identity_mismatch",
5529                        sequence,
5530                        activity_type.as_deref().unwrap_or("one activity identity"),
5531                        "conflicting activity identities",
5532                        "activity lifecycle events at one workflow sequence disagree on identity",
5533                    ));
5534                }
5535                let terminal: Vec<_> = activity_events
5536                    .iter()
5537                    .copied()
5538                    .filter(|event| {
5539                        matches!(
5540                            event.event_type.as_str(),
5541                            "ActivityCompleted"
5542                                | "ActivityFailed"
5543                                | "ActivityCancelled"
5544                                | "ActivityTimedOut"
5545                        )
5546                    })
5547                    .collect();
5548                if terminal.len() > 1 {
5549                    return Err(invalid_recorded_history(
5550                        "duplicate_activity_terminal_event",
5551                        sequence,
5552                        "at most one terminal activity event",
5553                        "multiple terminal activity events",
5554                        "activity history settles one command more than once",
5555                    ));
5556                }
5557                let outcome = terminal
5558                    .first()
5559                    .map(|event| activity_outcome(event, fallback_codec, activity_type.clone()))
5560                    .transpose()?;
5561                let options = activity_events
5562                    .iter()
5563                    .find(|event| event.event_type == "ActivityScheduled")
5564                    .and_then(|event| event.payload.get("activity"))
5565                    .and_then(Value::as_object)
5566                    .map(|activity| RecordedActivityOptions {
5567                        task_queue: recorded_optional_string(activity, "queue"),
5568                        execution_mode: recorded_optional_string(activity, "execution_mode"),
5569                        retry_policy: recorded_activity_retry_snapshot(
5570                            activity.get("retry_policy"),
5571                        ),
5572                    });
5573                return Ok(RecordedCommand::Activity {
5574                    sequence,
5575                    activity_type,
5576                    options,
5577                    outcome,
5578                });
5579            }
5580
5581            if !child_events.is_empty() {
5582                let scheduled: Vec<_> = child_events
5583                    .iter()
5584                    .copied()
5585                    .filter(|event| event.event_type == "ChildWorkflowScheduled")
5586                    .collect();
5587                if scheduled.len() != 1 {
5588                    return Err(invalid_recorded_history(
5589                        "child_workflow_schedule_missing_or_duplicate",
5590                        sequence,
5591                        "one ChildWorkflowScheduled event",
5592                        &format!("{} ChildWorkflowScheduled events", scheduled.len()),
5593                        "child workflow replay requires exactly one recorded schedule event",
5594                    ));
5595                }
5596                let workflow_type = child_events.iter().find_map(|event| {
5597                    event
5598                        .payload
5599                        .get("child_workflow_type")
5600                        .or_else(|| event.payload.get("workflow_type"))
5601                        .and_then(Value::as_str)
5602                        .filter(|value| !value.is_empty())
5603                        .map(str::to_string)
5604                });
5605                if child_events
5606                    .iter()
5607                    .filter_map(|event| {
5608                        event
5609                            .payload
5610                            .get("child_workflow_type")
5611                            .or_else(|| event.payload.get("workflow_type"))
5612                            .and_then(Value::as_str)
5613                    })
5614                    .any(|candidate| Some(candidate) != workflow_type.as_deref())
5615                {
5616                    return Err(invalid_recorded_history(
5617                        "child_workflow_identity_mismatch",
5618                        sequence,
5619                        workflow_type
5620                            .as_deref()
5621                            .unwrap_or("one child workflow type"),
5622                        "conflicting child workflow types",
5623                        "child workflow lifecycle events at one sequence disagree on type",
5624                    ));
5625                }
5626                let mut outcomes = child_workflow_outcomes(
5627                    &child_events.iter().map(|event| (*event).clone()).collect::<Vec<_>>(),
5628                    fallback_codec,
5629                    parent.clone(),
5630                )?;
5631                if outcomes.len() > 1 {
5632                    return Err(invalid_recorded_history(
5633                        "duplicate_child_workflow_terminal_event",
5634                        sequence,
5635                        "at most one terminal child event",
5636                        "multiple terminal child events",
5637                        "child workflow history settles one command more than once",
5638                    ));
5639                }
5640                return Ok(RecordedCommand::ChildWorkflow {
5641                    sequence,
5642                    workflow_type,
5643                    outcome: outcomes.pop(),
5644                });
5645            }
5646
5647            if !signal_wait_events.is_empty() {
5648                let opened_count = signal_wait_events
5649                    .iter()
5650                    .filter(|event| {
5651                        matches!(
5652                            event.event_type.as_str(),
5653                            "SignalWaitOpened" | "ConditionWaitOpened"
5654                        )
5655                    })
5656                    .count();
5657                if opened_count > 1 {
5658                    return Err(invalid_recorded_history(
5659                        "duplicate_signal_wait_open",
5660                        sequence,
5661                        "at most one signal wait open event",
5662                        "multiple signal wait open events",
5663                        "signal history opens more than one durable wait at one workflow sequence",
5664                    ));
5665                }
5666
5667                let signal_name = signal_wait_events
5668                    .iter()
5669                    .find_map(|event| recorded_signal_wait_name(event));
5670                if signal_wait_events
5671                    .iter()
5672                    .filter_map(|event| recorded_signal_wait_name(event))
5673                    .any(|candidate| Some(candidate.as_str()) != signal_name.as_deref())
5674                {
5675                    return Err(invalid_recorded_history(
5676                        "signal_wait_identity_mismatch",
5677                        sequence,
5678                        signal_name.as_deref().unwrap_or("one signal name"),
5679                        "conflicting signal names",
5680                        "signal wait lifecycle events at one workflow sequence disagree on identity",
5681                    ));
5682                }
5683                return Ok(RecordedCommand::SignalWait {
5684                    sequence,
5685                    signal_name,
5686                });
5687            }
5688
5689            if !side_effect_events.is_empty() {
5690                if side_effect_events.len() != 1 {
5691                    return Err(invalid_recorded_history(
5692                        "duplicate_side_effect_record",
5693                        sequence,
5694                        "one SideEffectRecorded event",
5695                        &format!("{} SideEffectRecorded events", side_effect_events.len()),
5696                        "side-effect history records one workflow command more than once",
5697                    ));
5698                }
5699                let event = side_effect_events[0];
5700                let result = event.payload.get("result").ok_or_else(|| {
5701                    invalid_recorded_history(
5702                        "side_effect_result_missing",
5703                        sequence,
5704                        "recorded result payload",
5705                        "missing result",
5706                        "side-effect history is missing its recorded value",
5707                    )
5708                })?;
5709                let has_published_envelope = result.as_str().is_some()
5710                    || result.as_object().is_some_and(|envelope| {
5711                        envelope.get("codec").and_then(Value::as_str).is_some()
5712                            && envelope.get("blob").and_then(Value::as_str).is_some()
5713                    });
5714                if !has_published_envelope {
5715                    return Err(invalid_recorded_history(
5716                        "side_effect_payload_malformed",
5717                        sequence,
5718                        "payload blob or {codec, blob} envelope",
5719                        &result.to_string(),
5720                        "side-effect history result does not use a published payload envelope",
5721                    ));
5722                }
5723                let codec = event
5724                    .payload
5725                    .get("payload_codec")
5726                    .and_then(Value::as_str)
5727                    .unwrap_or(fallback_codec);
5728                let value = decode_wire_value(result, codec).map_err(|error| {
5729                    invalid_recorded_history(
5730                        "side_effect_payload_incompatible",
5731                        sequence,
5732                        &format!("valid {codec} payload envelope"),
5733                        &error.to_string(),
5734                        "side-effect history payload cannot be decoded with its recorded codec",
5735                    )
5736                })?;
5737                return Ok(RecordedCommand::SideEffect { sequence, value });
5738            }
5739
5740            if !version_marker_events.is_empty() {
5741                if version_marker_events.len() != 1 {
5742                    return Err(invalid_recorded_history(
5743                        "duplicate_version_marker_record",
5744                        sequence,
5745                        "one VersionMarkerRecorded event",
5746                        &format!("{} VersionMarkerRecorded events", version_marker_events.len()),
5747                        "version-marker history records one workflow command more than once",
5748                    ));
5749                }
5750                let payload = &version_marker_events[0].payload;
5751                let change_id = payload
5752                    .get("change_id")
5753                    .and_then(Value::as_str)
5754                    .filter(|value| !value.is_empty())
5755                    .map(str::to_string)
5756                    .ok_or_else(|| {
5757                        invalid_recorded_history(
5758                            "version_marker_field_missing",
5759                            sequence,
5760                            "non-empty change_id",
5761                            "missing or invalid change_id",
5762                            "version-marker history is missing its stable change ID",
5763                        )
5764                    })?;
5765                let version = required_version_i32(payload, "version", sequence)?;
5766                let min_supported = required_version_i32(payload, "min_supported", sequence)?;
5767                let max_supported = required_version_i32(payload, "max_supported", sequence)?;
5768                if min_supported > max_supported || version < min_supported || version > max_supported {
5769                    return Err(invalid_recorded_history(
5770                        "version_marker_history_range_invalid",
5771                        sequence,
5772                        "min_supported <= version <= max_supported",
5773                        &format!("{min_supported} <= {version} <= {max_supported}"),
5774                        "recorded version marker contains an internally incompatible range",
5775                    ));
5776                }
5777                return Ok(RecordedCommand::VersionMarker {
5778                    sequence,
5779                    change_id,
5780                    version,
5781                });
5782            }
5783
5784            let scheduled: Vec<_> = timer_events
5785                .iter()
5786                .copied()
5787                .filter(|event| event.event_type == "TimerScheduled")
5788                .collect();
5789            let fired: Vec<_> = timer_events
5790                .iter()
5791                .copied()
5792                .filter(|event| event.event_type == "TimerFired")
5793                .collect();
5794            if scheduled.len() != 1 {
5795                return Err(invalid_recorded_history(
5796                    "timer_schedule_missing_or_duplicate",
5797                    sequence,
5798                    "one TimerScheduled event",
5799                    &format!("{} TimerScheduled events", scheduled.len()),
5800                    "timer replay requires exactly one recorded schedule event",
5801                ));
5802            }
5803            if fired.len() > 1 {
5804                return Err(invalid_recorded_history(
5805                    "duplicate_timer_fire",
5806                    sequence,
5807                    "at most one TimerFired event",
5808                    "multiple TimerFired events",
5809                    "timer history contains more than one fire event for a workflow sequence",
5810                ));
5811            }
5812
5813            let scheduled = scheduled[0];
5814            let timer_id = required_history_string(scheduled, "timer_id", sequence)?;
5815            let delay_seconds = required_history_u64(scheduled, "delay_seconds", sequence)?;
5816            if let Some(fired) = fired.first() {
5817                let fired_timer_id = required_history_string(fired, "timer_id", sequence)?;
5818                if fired_timer_id != timer_id {
5819                    return Err(invalid_recorded_history(
5820                        "timer_identity_mismatch",
5821                        sequence,
5822                        &timer_id,
5823                        &fired_timer_id,
5824                        "TimerFired does not correspond to the recorded TimerScheduled event",
5825                    ));
5826                }
5827                let fired_delay = required_history_u64(fired, "delay_seconds", sequence)?;
5828                if fired_delay != delay_seconds {
5829                    return Err(invalid_recorded_history(
5830                        "timer_history_delay_mismatch",
5831                        sequence,
5832                        &delay_seconds.to_string(),
5833                        &fired_delay.to_string(),
5834                        "TimerScheduled and TimerFired record different delays",
5835                    ));
5836                }
5837            }
5838
5839            Ok(RecordedCommand::Timer {
5840                sequence,
5841                delay_seconds,
5842                fired: !fired.is_empty(),
5843            })
5844        })
5845        .collect::<Result<_>>()?;
5846
5847    for (index, command) in commands.iter().enumerate() {
5848        let expected = u64::try_from(index)
5849            .ok()
5850            .and_then(|index| index.checked_add(1))
5851            .ok_or_else(|| {
5852                invalid_recorded_history(
5853                    "durable_command_sequence_invalid",
5854                    command.sequence(),
5855                    "representable positive workflow sequence",
5856                    &command.sequence().to_string(),
5857                    "durable command history exceeds the supported workflow sequence range",
5858                )
5859            })?;
5860        if command.sequence() != expected {
5861            return Err(invalid_recorded_history(
5862                "durable_command_sequence_mismatch",
5863                command.sequence(),
5864                &expected.to_string(),
5865                &command.sequence().to_string(),
5866                "durable command history does not follow the recorded workflow command sequence",
5867            ));
5868        }
5869    }
5870
5871    let mut marker_sequences = HashMap::new();
5872    for command in &commands {
5873        if let RecordedCommand::VersionMarker {
5874            sequence,
5875            change_id,
5876            ..
5877        } = command
5878        {
5879            if let Some(first_sequence) = marker_sequences.insert(change_id.clone(), *sequence) {
5880                return Err(invalid_recorded_history(
5881                    "duplicate_version_marker",
5882                    *sequence,
5883                    &format!("one marker for change ID {change_id:?}"),
5884                    &format!("markers at sequences {first_sequence} and {sequence}"),
5885                    "workflow history contains duplicate markers for one stable change ID",
5886                ));
5887            }
5888        }
5889    }
5890
5891    Ok(commands)
5892}
5893
5894fn required_version_i32(payload: &Value, field: &str, sequence: u64) -> Result<i32> {
5895    payload
5896        .get(field)
5897        .and_then(Value::as_i64)
5898        .and_then(|value| i32::try_from(value).ok())
5899        .ok_or_else(|| {
5900            invalid_recorded_history(
5901                "version_marker_field_missing",
5902                sequence,
5903                &format!("integer {field}"),
5904                "missing or out-of-range integer",
5905                "version-marker history is missing a required integer field",
5906            )
5907        })
5908}
5909
5910fn durable_event_sequence(event: &HistoryEvent) -> Option<u64> {
5911    event
5912        .payload
5913        .get("sequence")
5914        .or_else(|| event.payload.get("workflow_sequence"))
5915        .or_else(|| event.raw.get("sequence"))
5916        .or_else(|| event.raw.get("workflow_sequence"))
5917        .and_then(value_as_u64)
5918}
5919
5920fn is_internal_timer_event(event: &HistoryEvent) -> bool {
5921    matches!(
5922        event
5923            .payload
5924            .get("timer_kind")
5925            .or_else(|| event.raw.get("timer_kind"))
5926            .and_then(Value::as_str),
5927        Some("condition_timeout" | "signal_timeout")
5928    )
5929}
5930
5931fn recorded_signal_wait_name(event: &HistoryEvent) -> Option<String> {
5932    match event.event_type.as_str() {
5933        "SignalWaitOpened" | "SignalApplied" => event
5934            .payload
5935            .get("signal_name")
5936            .or_else(|| event.raw.get("signal_name"))
5937            .and_then(Value::as_str)
5938            .filter(|value| !value.is_empty())
5939            .map(str::to_string),
5940        "ConditionWaitOpened" | "ConditionWaitSatisfied" | "ConditionWaitTimedOut" => event
5941            .payload
5942            .get("condition_key")
5943            .or_else(|| event.raw.get("condition_key"))
5944            .and_then(Value::as_str)
5945            .and_then(|key| key.strip_prefix("signal:"))
5946            .filter(|value| !value.is_empty())
5947            .map(str::to_string),
5948        _ => None,
5949    }
5950}
5951
5952fn is_recorded_signal_wait_event(event: &HistoryEvent) -> bool {
5953    match event.event_type.as_str() {
5954        "SignalWaitOpened" | "SignalApplied" => true,
5955        "ConditionWaitOpened" | "ConditionWaitSatisfied" | "ConditionWaitTimedOut" => event
5956            .payload
5957            .get("condition_key")
5958            .or_else(|| event.raw.get("condition_key"))
5959            .and_then(Value::as_str)
5960            .is_some_and(|key| key.starts_with("signal:")),
5961        _ => false,
5962    }
5963}
5964
5965fn required_history_string(event: &HistoryEvent, field: &str, sequence: u64) -> Result<String> {
5966    event
5967        .payload
5968        .get(field)
5969        .and_then(Value::as_str)
5970        .filter(|value| !value.is_empty())
5971        .map(str::to_string)
5972        .ok_or_else(|| {
5973            invalid_recorded_history(
5974                "timer_history_field_missing",
5975                sequence,
5976                field,
5977                &event.event_type,
5978                "timer history is missing a required identity field",
5979            )
5980        })
5981}
5982
5983fn required_history_u64(event: &HistoryEvent, field: &str, sequence: u64) -> Result<u64> {
5984    event
5985        .payload
5986        .get(field)
5987        .and_then(value_as_u64)
5988        .ok_or_else(|| {
5989            invalid_recorded_history(
5990                "timer_history_field_missing",
5991                sequence,
5992                field,
5993                &event.event_type,
5994                "timer history is missing a required numeric field",
5995            )
5996        })
5997}
5998
5999fn invalid_recorded_history(
6000    reason: &str,
6001    sequence: u64,
6002    expected: &str,
6003    actual: &str,
6004    message: &str,
6005) -> Error {
6006    Error::NonDeterministicReplay(ReplayFailure::new(
6007        reason,
6008        Some(sequence),
6009        Some(expected.to_string()),
6010        Some(actual.to_string()),
6011        message,
6012    ))
6013}
6014
6015type ActivityOutcome = std::result::Result<Value, ActivityFailure>;
6016
6017fn activity_outcome(
6018    event: &HistoryEvent,
6019    fallback_codec: &str,
6020    recorded_activity_type: Option<String>,
6021) -> Result<ActivityOutcome> {
6022    if event.event_type == "ActivityCompleted" {
6023        let codec = event
6024            .payload
6025            .get("payload_codec")
6026            .and_then(Value::as_str)
6027            .unwrap_or(fallback_codec);
6028        return Ok(Ok(decode_wire_value(
6029            event.payload.get("result").unwrap_or(&Value::Null),
6030            codec,
6031        )?));
6032    }
6033
6034    let payload = &event.payload;
6035    let (kind, fallback_reason, fallback_message) = match event.event_type.as_str() {
6036        "ActivityFailed" => (ActivityFailureKind::Failed, "activity", "activity failed"),
6037        "ActivityCancelled" => (
6038            ActivityFailureKind::Cancelled,
6039            "cancelled",
6040            "activity was cancelled",
6041        ),
6042        "ActivityTimedOut" => (
6043            ActivityFailureKind::TimedOut,
6044            "timeout",
6045            "activity timed out",
6046        ),
6047        _ => unreachable!("activity_outcome is called only for terminal activity events"),
6048    };
6049    let exception = payload
6050        .get("exception")
6051        .filter(|value| !value.is_null())
6052        .cloned();
6053    let failure_category = payload_string(payload, "failure_category");
6054    let timeout_kind = payload_string(payload, "timeout_kind");
6055    let reason = payload_string(payload, "reason").unwrap_or_else(|| match kind {
6056        ActivityFailureKind::Failed => failure_category
6057            .clone()
6058            .unwrap_or_else(|| fallback_reason.to_string()),
6059        ActivityFailureKind::Cancelled => fallback_reason.to_string(),
6060        ActivityFailureKind::TimedOut => timeout_kind
6061            .clone()
6062            .unwrap_or_else(|| fallback_reason.to_string()),
6063    });
6064    let message = payload_string(payload, "message")
6065        .or_else(|| {
6066            exception
6067                .as_ref()
6068                .and_then(|value| payload_string(value, "message"))
6069        })
6070        .unwrap_or_else(|| fallback_message.to_string());
6071
6072    Ok(Err(ActivityFailure {
6073        kind,
6074        reason,
6075        message,
6076        activity_execution_id: payload_string(payload, "activity_execution_id"),
6077        activity_attempt_id: payload_string(payload, "activity_attempt_id"),
6078        activity_type: payload_string(payload, "activity_type")
6079            .or_else(|| payload_string(payload, "activity_name"))
6080            .or(recorded_activity_type),
6081        activity_class: payload_string(payload, "activity_class"),
6082        attempt_number: payload.get("attempt_number").and_then(value_as_u64),
6083        failure_id: payload_string(payload, "failure_id"),
6084        failure_category,
6085        timeout_kind,
6086        non_retryable: payload
6087            .get("non_retryable")
6088            .and_then(Value::as_bool)
6089            .unwrap_or(false),
6090        exception_type: payload_string(payload, "exception_type").or_else(|| {
6091            exception
6092                .as_ref()
6093                .and_then(|value| payload_string(value, "type"))
6094        }),
6095        exception_class: payload_string(payload, "exception_class").or_else(|| {
6096            exception
6097                .as_ref()
6098                .and_then(|value| payload_string(value, "class"))
6099        }),
6100        code: payload
6101            .get("code")
6102            .filter(|value| !value.is_null())
6103            .cloned(),
6104        exception,
6105    }))
6106}
6107
6108type ChildWorkflowOutcome = std::result::Result<ChildWorkflowResult, ChildWorkflowFailure>;
6109
6110fn child_workflow_outcomes(
6111    events: &[HistoryEvent],
6112    fallback_codec: &str,
6113    parent: WorkflowIdentity,
6114) -> Result<Vec<ChildWorkflowOutcome>> {
6115    let mut outcomes = Vec::new();
6116
6117    for event in events {
6118        let kind = match event.event_type.as_str() {
6119            "ChildRunCompleted" => None,
6120            "ChildRunFailed" => Some((
6121                ChildWorkflowFailureKind::Failed,
6122                "child_workflow",
6123                "child workflow failed",
6124            )),
6125            "ChildRunCancelled" => Some((
6126                ChildWorkflowFailureKind::Cancelled,
6127                "cancelled",
6128                "child workflow was cancelled",
6129            )),
6130            "ChildRunTerminated" => Some((
6131                ChildWorkflowFailureKind::Terminated,
6132                "terminated",
6133                "child workflow was terminated",
6134            )),
6135            _ => continue,
6136        };
6137        let payload = &event.payload;
6138        let child_workflow_id = payload_string(payload, "child_workflow_instance_id");
6139        let child_workflow_run_id = payload_string(payload, "child_workflow_run_id");
6140        let child_workflow_type = payload_string(payload, "child_workflow_type");
6141
6142        if let Some((kind, reason, fallback_message)) = kind {
6143            let exception = payload
6144                .get("exception")
6145                .filter(|value| !value.is_null())
6146                .cloned();
6147            let message = payload_string(payload, "message")
6148                .or_else(|| {
6149                    exception
6150                        .as_ref()
6151                        .and_then(|value| payload_string(value, "message"))
6152                })
6153                .unwrap_or_else(|| fallback_message.to_string());
6154            let exception_type = payload_string(payload, "exception_type").or_else(|| {
6155                exception
6156                    .as_ref()
6157                    .and_then(|value| payload_string(value, "type"))
6158            });
6159            let exception_class = payload_string(payload, "exception_class").or_else(|| {
6160                exception
6161                    .as_ref()
6162                    .and_then(|value| payload_string(value, "class"))
6163            });
6164            outcomes.push(Err(ChildWorkflowFailure {
6165                kind,
6166                reason: reason.to_string(),
6167                message,
6168                parent_workflow_id: parent.workflow_id.clone(),
6169                parent_workflow_run_id: parent.run_id.clone(),
6170                child_workflow_id,
6171                child_workflow_run_id,
6172                child_workflow_type,
6173                failure_id: payload_string(payload, "failure_id"),
6174                failure_category: payload_string(payload, "failure_category"),
6175                exception_type,
6176                exception_class,
6177                non_retryable: payload
6178                    .get("non_retryable")
6179                    .and_then(Value::as_bool)
6180                    .unwrap_or(false),
6181                code: payload
6182                    .get("code")
6183                    .filter(|value| !value.is_null())
6184                    .cloned(),
6185                exception,
6186            }));
6187            continue;
6188        }
6189
6190        let codec = payload
6191            .get("payload_codec")
6192            .and_then(Value::as_str)
6193            .unwrap_or(fallback_codec);
6194        let result = payload
6195            .get("result")
6196            .or_else(|| payload.get("output"))
6197            .unwrap_or(&Value::Null);
6198        outcomes.push(Ok(ChildWorkflowResult {
6199            parent: parent.clone(),
6200            child: WorkflowIdentity {
6201                workflow_id: child_workflow_id,
6202                run_id: child_workflow_run_id,
6203            },
6204            child_workflow_type,
6205            result: decode_wire_value(result, codec)?,
6206        }));
6207    }
6208
6209    Ok(outcomes)
6210}
6211
6212fn payload_string(payload: &Value, key: &str) -> Option<String> {
6213    payload
6214        .get(key)
6215        .and_then(Value::as_str)
6216        .filter(|value| !value.is_empty())
6217        .map(str::to_string)
6218}
6219
6220fn workflow_failure_command(error: &Error) -> Value {
6221    let (exception_type, exception_class, properties) = match error {
6222        Error::ActivityFailed(failure) => (
6223            match failure.kind {
6224                ActivityFailureKind::Failed => "ActivityFailed",
6225                ActivityFailureKind::Cancelled => "ActivityCancelled",
6226                ActivityFailureKind::TimedOut => "ActivityTimedOut",
6227            },
6228            "durable_workflow::ActivityFailure",
6229            json!({
6230                "reason": failure.reason,
6231                "activity_execution_id": failure.activity_execution_id,
6232                "activity_attempt_id": failure.activity_attempt_id,
6233                "activity_type": failure.activity_type,
6234                "activity_class": failure.activity_class,
6235                "attempt_number": failure.attempt_number,
6236                "failure_id": failure.failure_id,
6237                "failure_category": failure.failure_category,
6238                "timeout_kind": failure.timeout_kind,
6239                "activity_non_retryable": failure.non_retryable,
6240                "activity_exception_type": failure.exception_type,
6241                "activity_exception_class": failure.exception_class,
6242                "activity_code": failure.code,
6243                "activity_exception": failure.exception,
6244            }),
6245        ),
6246        Error::ChildWorkflowFailed(failure) => (
6247            match failure.kind {
6248                ChildWorkflowFailureKind::Failed => "ChildWorkflowFailed",
6249                ChildWorkflowFailureKind::Cancelled => "ChildWorkflowCancelled",
6250                ChildWorkflowFailureKind::Terminated => "ChildWorkflowTerminated",
6251            },
6252            "durable_workflow::ChildWorkflowFailure",
6253            json!({
6254                "reason": failure.reason,
6255                "parent_workflow_id": failure.parent_workflow_id,
6256                "parent_workflow_run_id": failure.parent_workflow_run_id,
6257                "child_workflow_id": failure.child_workflow_id,
6258                "child_workflow_run_id": failure.child_workflow_run_id,
6259                "child_workflow_type": failure.child_workflow_type,
6260                "failure_id": failure.failure_id,
6261                "failure_category": failure.failure_category,
6262                "child_exception_type": failure.exception_type,
6263                "child_exception_class": failure.exception_class,
6264                "child_non_retryable": failure.non_retryable,
6265                "child_code": failure.code,
6266                "child_exception": failure.exception,
6267            }),
6268        ),
6269        Error::NonDeterministicReplay(_) => (
6270            "NonDeterministicReplay",
6271            "durable_workflow::Error",
6272            Value::Null,
6273        ),
6274        _ => ("RustWorkflowError", "durable_workflow::Error", Value::Null),
6275    };
6276    let non_retryable = match error {
6277        Error::ActivityFailed(failure) => failure.non_retryable,
6278        Error::ChildWorkflowFailed(failure) => failure.non_retryable,
6279        Error::NonDeterministicReplay(_) => true,
6280        _ => false,
6281    };
6282
6283    json!({
6284        "type": "fail_workflow",
6285        "message": error.to_string(),
6286        "exception_type": exception_type,
6287        "exception_class": exception_class,
6288        "non_retryable": non_retryable,
6289        "exception": {
6290            "type": exception_type,
6291            "class": exception_class,
6292            "message": error.to_string(),
6293            "properties": properties,
6294        }
6295    })
6296}
6297
6298fn workflow_task_integrity_error(error: &Error) -> bool {
6299    matches!(
6300        error,
6301        Error::NonDeterministicReplay(_) | Error::Protocol(_) | Error::WorkflowStatePoisoned
6302    )
6303}
6304
6305fn signal_values(
6306    events: &[HistoryEvent],
6307    signal_name: &str,
6308    fallback_codec: &str,
6309    resume_signal: Option<&ResumeSignal>,
6310) -> Result<Vec<Vec<Value>>> {
6311    let mut signals = Vec::new();
6312
6313    for event in events {
6314        if event.event_type != "SignalApplied" && event.event_type != "SignalReceived" {
6315            continue;
6316        }
6317
6318        if event.payload.get("signal_name").and_then(Value::as_str) != Some(signal_name) {
6319            continue;
6320        }
6321
6322        let codec = event
6323            .payload
6324            .get("payload_codec")
6325            .and_then(Value::as_str)
6326            .unwrap_or(fallback_codec);
6327        let raw = event
6328            .payload
6329            .get("value")
6330            .or_else(|| event.payload.get("input"))
6331            .or_else(|| event.payload.get("arguments"));
6332        let decoded = match raw.filter(|value| !value.is_null()) {
6333            Some(value) => decode_wire_value(value, codec)?,
6334            None => resume_signal
6335                .filter(|signal| resume_signal_matches_event(signal, event, signal_name))
6336                .map(|signal| Value::Array(signal.arguments.clone()))
6337                .unwrap_or_else(|| Value::Array(Vec::new())),
6338        };
6339        let args = match normalize_arguments(decoded) {
6340            Value::Array(values) => values,
6341            _ => unreachable!("normalize_arguments always returns an array"),
6342        };
6343        signals.push(args);
6344    }
6345
6346    Ok(signals)
6347}
6348
6349fn hydrate_query_history_from_export(task: &mut QueryTask) -> Result<()> {
6350    let Some(export_events) = task
6351        .history_export
6352        .as_ref()
6353        .and_then(|export| export.get("history_events"))
6354        .and_then(Value::as_array)
6355    else {
6356        return Ok(());
6357    };
6358
6359    if export_events.len() > task.history_events.len() {
6360        task.history_events = serde_json::from_value(Value::Array(export_events.clone()))?;
6361    }
6362
6363    Ok(())
6364}
6365
6366fn enrich_query_history_from_export(task: &mut QueryTask) -> Result<()> {
6367    let Some(export) = task.history_export.as_ref() else {
6368        return Ok(());
6369    };
6370    let signals = export
6371        .get("signals")
6372        .and_then(Value::as_array)
6373        .cloned()
6374        .unwrap_or_default();
6375    let activities = export
6376        .get("activities")
6377        .and_then(Value::as_array)
6378        .cloned()
6379        .unwrap_or_default();
6380    let export_codec = export
6381        .get("payloads")
6382        .and_then(|payloads| payloads.get("codec"))
6383        .and_then(Value::as_str)
6384        .unwrap_or(&task.payload_codec)
6385        .to_string();
6386    let mut signal_name_offsets: HashMap<String, usize> = HashMap::new();
6387
6388    for event in &mut task.history_events {
6389        if event.event_type == "ActivityCompleted" {
6390            let sequence = event
6391                .payload
6392                .get("sequence")
6393                .or_else(|| event.payload.get("workflow_sequence"))
6394                .and_then(value_as_u64);
6395            let Some(activity) = sequence.and_then(|sequence| {
6396                activities.iter().find(|activity| {
6397                    activity.get("sequence").and_then(value_as_u64) == Some(sequence)
6398                })
6399            }) else {
6400                continue;
6401            };
6402            let Some(payload) = event.payload.as_object_mut() else {
6403                continue;
6404            };
6405            if missing_payload(payload.get("result")) {
6406                if let Some(result) = activity
6407                    .get("result")
6408                    .filter(|value| !missing_payload(Some(value)))
6409                {
6410                    payload.insert("result".to_string(), result.clone());
6411                }
6412            }
6413            for field in ["payload_codec", "activity_type"] {
6414                if payload
6415                    .get(field)
6416                    .and_then(Value::as_str)
6417                    .unwrap_or_default()
6418                    .is_empty()
6419                {
6420                    if let Some(value) = activity.get(field) {
6421                        payload.insert(field.to_string(), value.clone());
6422                    }
6423                }
6424            }
6425            continue;
6426        }
6427
6428        if event.event_type != "SignalReceived" && event.event_type != "SignalApplied" {
6429            continue;
6430        }
6431        let signal_id = event.payload.get("signal_id").and_then(Value::as_str);
6432        let command_id = event
6433            .payload
6434            .get("workflow_command_id")
6435            .or_else(|| event.raw.get("workflow_command_id"))
6436            .and_then(Value::as_str);
6437        let signal_name = event
6438            .payload
6439            .get("signal_name")
6440            .and_then(Value::as_str)
6441            .unwrap_or_default()
6442            .to_string();
6443        let matched = signals
6444            .iter()
6445            .find(|signal| {
6446                signal_id.is_some() && signal.get("id").and_then(Value::as_str) == signal_id
6447            })
6448            .or_else(|| {
6449                signals.iter().find(|signal| {
6450                    command_id.is_some()
6451                        && signal.get("command_id").and_then(Value::as_str) == command_id
6452                })
6453            })
6454            .or_else(|| {
6455                let offset = signal_name_offsets.entry(signal_name.clone()).or_default();
6456                let signal = signals
6457                    .iter()
6458                    .filter(|signal| {
6459                        signal.get("name").and_then(Value::as_str) == Some(signal_name.as_str())
6460                    })
6461                    .nth(*offset);
6462                if signal.is_some() {
6463                    *offset += 1;
6464                }
6465                signal
6466            });
6467        let Some(signal) = matched else {
6468            continue;
6469        };
6470        let signal_codec = signal
6471            .get("payload_codec")
6472            .and_then(Value::as_str)
6473            .unwrap_or(&export_codec);
6474        let Some(payload) = event.payload.as_object_mut() else {
6475            continue;
6476        };
6477        if missing_payload(payload.get("arguments")) {
6478            if let Some(arguments) = signal
6479                .get("arguments")
6480                .filter(|value| !missing_payload(Some(value)))
6481            {
6482                let envelope = match arguments {
6483                    Value::String(blob) => json!({"codec": signal_codec, "blob": blob}),
6484                    other => other.clone(),
6485                };
6486                payload.insert("arguments".to_string(), envelope);
6487            }
6488        }
6489        if payload
6490            .get("payload_codec")
6491            .and_then(Value::as_str)
6492            .unwrap_or_default()
6493            .is_empty()
6494        {
6495            payload.insert("payload_codec".to_string(), json!(signal_codec));
6496        }
6497    }
6498
6499    Ok(())
6500}
6501
6502fn missing_payload(value: Option<&Value>) -> bool {
6503    match value {
6504        None | Some(Value::Null) => true,
6505        Some(Value::String(value)) => value.is_empty(),
6506        Some(_) => false,
6507    }
6508}
6509
6510fn query_signal_events(task: &QueryTask) -> Result<Vec<QuerySignal>> {
6511    let export_signals = task
6512        .history_export
6513        .as_ref()
6514        .and_then(|export| export.get("signals"))
6515        .and_then(Value::as_array)
6516        .cloned()
6517        .unwrap_or_default();
6518    let export_codec = task
6519        .history_export
6520        .as_ref()
6521        .and_then(|export| export.get("payloads"))
6522        .and_then(|payloads| payloads.get("codec"))
6523        .and_then(Value::as_str)
6524        .unwrap_or(&task.payload_codec);
6525    let mut name_offsets: HashMap<String, usize> = HashMap::new();
6526    let mut signals = Vec::new();
6527
6528    for event in &task.history_events {
6529        if event.event_type != "SignalApplied" && event.event_type != "SignalReceived" {
6530            continue;
6531        }
6532
6533        let name = event
6534            .payload
6535            .get("signal_name")
6536            .and_then(Value::as_str)
6537            .unwrap_or_default();
6538        if name.is_empty() {
6539            continue;
6540        }
6541        let signal_id = event.payload.get("signal_id").and_then(Value::as_str);
6542        let command_id = event
6543            .payload
6544            .get("workflow_command_id")
6545            .or_else(|| event.raw.get("workflow_command_id"))
6546            .and_then(Value::as_str);
6547        let matched_export = export_signals
6548            .iter()
6549            .find(|candidate| {
6550                signal_id.is_some() && candidate.get("id").and_then(Value::as_str) == signal_id
6551            })
6552            .or_else(|| {
6553                export_signals.iter().find(|candidate| {
6554                    command_id.is_some()
6555                        && candidate.get("command_id").and_then(Value::as_str) == command_id
6556                })
6557            })
6558            .or_else(|| {
6559                let offset = name_offsets.entry(name.to_string()).or_default();
6560                let candidate = export_signals
6561                    .iter()
6562                    .filter(|candidate| candidate.get("name").and_then(Value::as_str) == Some(name))
6563                    .nth(*offset);
6564                if candidate.is_some() {
6565                    *offset += 1;
6566                }
6567                candidate
6568            });
6569        let codec = event
6570            .payload
6571            .get("payload_codec")
6572            .and_then(Value::as_str)
6573            .or_else(|| {
6574                matched_export
6575                    .and_then(|signal| signal.get("payload_codec"))
6576                    .and_then(Value::as_str)
6577            })
6578            .unwrap_or(export_codec);
6579        let raw_arguments = event
6580            .payload
6581            .get("value")
6582            .or_else(|| event.payload.get("input"))
6583            .or_else(|| event.payload.get("arguments"))
6584            .filter(|value| !value.is_null())
6585            .or_else(|| matched_export.and_then(|signal| signal.get("arguments")));
6586        let arguments = decode_query_signal_arguments(raw_arguments, codec)?;
6587        let workflow_sequence = event
6588            .payload
6589            .get("workflow_sequence")
6590            .and_then(value_as_u64)
6591            .or_else(|| {
6592                matched_export
6593                    .and_then(|signal| signal.get("workflow_sequence"))
6594                    .and_then(value_as_u64)
6595            });
6596
6597        signals.push(QuerySignal {
6598            id: signal_id.map(str::to_string).or_else(|| {
6599                matched_export
6600                    .and_then(|signal| signal.get("id"))
6601                    .and_then(Value::as_str)
6602                    .map(str::to_string)
6603            }),
6604            name: name.to_string(),
6605            arguments,
6606            workflow_sequence,
6607        });
6608    }
6609
6610    if signals.is_empty() {
6611        for signal in export_signals {
6612            if signal.get("status").and_then(Value::as_str) == Some("rejected") {
6613                continue;
6614            }
6615            let Some(name) = signal.get("name").and_then(Value::as_str) else {
6616                continue;
6617            };
6618            let codec = signal
6619                .get("payload_codec")
6620                .and_then(Value::as_str)
6621                .unwrap_or(export_codec);
6622            let arguments = decode_query_signal_arguments(signal.get("arguments"), codec)?;
6623            signals.push(QuerySignal {
6624                id: signal.get("id").and_then(Value::as_str).map(str::to_string),
6625                name: name.to_string(),
6626                arguments,
6627                workflow_sequence: signal.get("workflow_sequence").and_then(value_as_u64),
6628            });
6629        }
6630        signals.sort_by_key(|signal| signal.workflow_sequence.unwrap_or(u64::MAX));
6631    }
6632
6633    Ok(signals)
6634}
6635
6636fn decode_query_signal_arguments(raw: Option<&Value>, codec: &str) -> Result<Vec<Value>> {
6637    let decoded = match raw.filter(|value| !value.is_null()) {
6638        Some(value) => decode_wire_value(value, codec)?,
6639        None => Value::Array(Vec::new()),
6640    };
6641    let Value::Array(arguments) = normalize_arguments(decoded) else {
6642        unreachable!("normalize_arguments always returns an array");
6643    };
6644    Ok(arguments)
6645}
6646
6647fn value_as_u64(value: &Value) -> Option<u64> {
6648    value
6649        .as_u64()
6650        .or_else(|| value.as_str().and_then(|value| value.parse().ok()))
6651}
6652
6653fn resume_signal_matches_event(
6654    resume_signal: &ResumeSignal,
6655    event: &HistoryEvent,
6656    signal_name: &str,
6657) -> bool {
6658    if resume_signal.signal_name != signal_name {
6659        return false;
6660    }
6661
6662    match (
6663        resume_signal.signal_id.as_deref(),
6664        event.payload.get("signal_id").and_then(Value::as_str),
6665    ) {
6666        (Some(resume_id), Some(event_id)) => resume_id == event_id,
6667        _ => true,
6668    }
6669}
6670
6671#[cfg(test)]
6672mod tests {
6673    use super::*;
6674    use std::{
6675        io::{Read, Write},
6676        net::{SocketAddr, TcpListener, TcpStream},
6677        sync::atomic::AtomicUsize,
6678        thread,
6679    };
6680
6681    #[derive(Clone, Debug, Default, PartialEq)]
6682    struct ReplayCounterState {
6683        loaded: Option<String>,
6684        count: i64,
6685        finished: bool,
6686    }
6687
6688    fn replay_counter_worker() -> Worker {
6689        let client = Client::new("http://127.0.0.1:8080").expect("client");
6690        let mut worker = Worker::new(client, "rust-workers");
6691        worker.register_replayed_workflow(
6692            "replay-counter",
6693            ReplayCounterState::default,
6694            |ctx, _input, state| async move {
6695                let loaded = ctx.activity("load-counter", json!([])).await?;
6696                state.update(|current| {
6697                    current.loaded = loaded.as_str().map(str::to_string);
6698                })?;
6699                for _ in 0..2 {
6700                    let signal = ctx.wait_signal("increment").await?;
6701                    let amount = signal.first().and_then(Value::as_i64).unwrap_or_default();
6702                    state.update(|current| current.count += amount)?;
6703                }
6704                state.update(|current| current.finished = true)?;
6705                state.read(|current| Ok(json!(current.count)))?
6706            },
6707        );
6708        worker.register_replayed_query::<ReplayCounterState, _, _>(
6709            "replay-counter",
6710            "current",
6711            |_ctx, state, _args| async move {
6712                Ok(json!({
6713                    "loaded": state.loaded,
6714                    "count": state.count,
6715                    "finished": state.finished,
6716                }))
6717            },
6718        );
6719        worker.register_replayed_query::<ReplayCounterState, _, _>(
6720            "replay-counter",
6721            "detached-mutation",
6722            |_ctx, state, _args| async move {
6723                let mut detached = (*state).clone();
6724                detached.count = 999;
6725                Ok(json!(detached.count))
6726            },
6727        );
6728        worker.register_replayed_query::<ReplayCounterState, _, _>(
6729            "replay-counter",
6730            "failed-mutation",
6731            |_ctx, state, _args| async move {
6732                let mut detached = (*state).clone();
6733                detached.count = 999;
6734                Err(Error::WorkerLoop("query refused".to_string()))
6735            },
6736        );
6737        worker
6738    }
6739
6740    fn replay_counter_query(
6741        query_name: &str,
6742        history_events: Value,
6743        run_status: &str,
6744    ) -> QueryTask {
6745        serde_json::from_value(json!({
6746            "query_task_id": format!("query-{query_name}"),
6747            "workflow_type": "replay-counter",
6748            "query_name": query_name,
6749            "payload_codec": "json",
6750            "workflow_arguments": {"codec": "json", "blob": "[]"},
6751            "query_arguments": {"codec": "json", "blob": "[]"},
6752            "history_events": history_events,
6753            "run_status": run_status,
6754        }))
6755        .expect("query task")
6756    }
6757
6758    fn workflow_context(history: Vec<HistoryEvent>) -> WorkflowContext {
6759        workflow_context_with_codec(history, JSON_CODEC)
6760    }
6761
6762    fn workflow_context_with_codec(
6763        history: Vec<HistoryEvent>,
6764        payload_codec: &str,
6765    ) -> WorkflowContext {
6766        WorkflowContext {
6767            state: Arc::new(Mutex::new(
6768                WorkflowState::new_with_identity(
6769                    history,
6770                    None,
6771                    None,
6772                    "rust-workers".to_string(),
6773                    payload_codec.to_string(),
6774                    None,
6775                )
6776                .expect("valid workflow history"),
6777            )),
6778        }
6779    }
6780
6781    fn history_event(event_type: &str, payload: Value) -> HistoryEvent {
6782        HistoryEvent {
6783            event_type: event_type.to_string(),
6784            payload,
6785            raw: HashMap::new(),
6786        }
6787    }
6788
6789    fn workflow_task(
6790        workflow_type: &str,
6791        history_events: Vec<HistoryEvent>,
6792        payload_codec: &str,
6793    ) -> WorkflowTask {
6794        WorkflowTask {
6795            task_id: format!("wft-{workflow_type}"),
6796            workflow_id: Some(format!("wf-{workflow_type}")),
6797            run_id: Some(format!("run-{workflow_type}")),
6798            workflow_type: workflow_type.to_string(),
6799            payload_codec: payload_codec.to_string(),
6800            arguments: Some(
6801                encode_value_envelope(&json!([]), payload_codec).expect("workflow arguments"),
6802            ),
6803            total_history_events: Some(history_events.len() as u64),
6804            history_events,
6805            next_history_page_token: None,
6806            workflow_task_attempt: 1,
6807            workflow_signal_id: None,
6808            signal_name: None,
6809            signal_arguments: None,
6810            lease_owner: Some("rust-worker".to_string()),
6811        }
6812    }
6813
6814    #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
6815    struct SideEffectProbe {
6816        request_id: String,
6817        attempt: u32,
6818    }
6819
6820    #[test]
6821    fn typed_side_effect_runs_callback_once_and_replay_skips_it() {
6822        let calls = AtomicUsize::new(0);
6823        let ctx = workflow_context(Vec::new());
6824        let value = ctx
6825            .side_effect(|| {
6826                calls.fetch_add(1, Ordering::SeqCst);
6827                SideEffectProbe {
6828                    request_id: "request-42".to_string(),
6829                    attempt: 3,
6830                }
6831            })
6832            .expect("first side effect");
6833        assert_eq!(value.attempt, 3);
6834        assert_eq!(calls.load(Ordering::SeqCst), 1);
6835        let commands = ctx.take_commands().expect("commands");
6836        assert_eq!(commands.len(), 1);
6837        assert_eq!(commands[0]["type"], "record_side_effect");
6838        assert_eq!(
6839            decode_wire_value(&commands[0]["result"], JSON_CODEC).expect("JSON result"),
6840            serde_json::to_value(&value).expect("value")
6841        );
6842
6843        let replay = workflow_context(vec![history_event(
6844            "SideEffectRecorded",
6845            json!({"sequence": 1, "result": commands[0]["result"].clone()}),
6846        )]);
6847        let replayed: SideEffectProbe = replay
6848            .side_effect(|| {
6849                calls.fetch_add(1, Ordering::SeqCst);
6850                panic!("committed side-effect callbacks must not run during replay")
6851            })
6852            .expect("replayed side effect");
6853        assert_eq!(replayed, value);
6854        assert_eq!(calls.load(Ordering::SeqCst), 1);
6855        assert!(replay.take_commands().expect("commands").is_empty());
6856        replay.ensure_history_consumed().expect("history consumed");
6857    }
6858
6859    #[test]
6860    fn side_effect_uses_avro_envelope_and_uuid_is_replay_stable() {
6861        let ctx = workflow_context_with_codec(Vec::new(), DEFAULT_CODEC);
6862        let value = ctx
6863            .side_effect(|| SideEffectProbe {
6864                request_id: "avro-request".to_string(),
6865                attempt: 1,
6866            })
6867            .expect("Avro side effect");
6868        let uuid = ctx.uuid_v4().expect("deterministic UUID");
6869        let commands = ctx.take_commands().expect("commands");
6870        assert_eq!(commands.len(), 2);
6871        assert_eq!(commands[0]["result"]["codec"], DEFAULT_CODEC);
6872        assert_eq!(commands[1]["result"]["codec"], DEFAULT_CODEC);
6873        assert_eq!(
6874            decode_wire_value(&commands[0]["result"], DEFAULT_CODEC).expect("Avro result"),
6875            serde_json::to_value(&value).expect("value")
6876        );
6877
6878        let replay = workflow_context_with_codec(
6879            vec![
6880                history_event(
6881                    "SideEffectRecorded",
6882                    json!({"sequence": 1, "result": commands[0]["result"].clone()}),
6883                ),
6884                history_event(
6885                    "SideEffectRecorded",
6886                    json!({"sequence": 2, "result": commands[1]["result"].clone()}),
6887                ),
6888            ],
6889            DEFAULT_CODEC,
6890        );
6891        let replayed: SideEffectProbe = replay
6892            .side_effect(|| panic!("Avro callback must not run"))
6893            .expect("replayed Avro value");
6894        let replayed_uuid = replay.uuid_v4().expect("replayed UUID");
6895        assert_eq!(replayed, value);
6896        assert_eq!(replayed_uuid, uuid);
6897        assert!(replay.take_commands().expect("commands").is_empty());
6898    }
6899
6900    #[test]
6901    fn ordered_side_effects_share_the_durable_command_stream() {
6902        let first = encode_value_envelope(&json!("first"), JSON_CODEC).expect("first");
6903        let second = encode_value_envelope(&json!(29), JSON_CODEC).expect("second");
6904        let ctx = workflow_context(vec![
6905            history_event(
6906                "SideEffectRecorded",
6907                json!({"sequence": 1, "result": first}),
6908            ),
6909            history_event(
6910                "SideEffectRecorded",
6911                json!({"sequence": 2, "result": second}),
6912            ),
6913        ]);
6914        let first: String = ctx
6915            .side_effect(|| panic!("first callback must not run"))
6916            .expect("first replay");
6917        let second: i32 = ctx
6918            .side_effect(|| panic!("second callback must not run"))
6919            .expect("second replay");
6920        assert_eq!(first, "first");
6921        assert_eq!(second, 29);
6922        ctx.ensure_history_consumed().expect("ordered history");
6923
6924        let reordered = workflow_context(vec![history_event(
6925            "VersionMarkerRecorded",
6926            json!({
6927                "sequence": 1,
6928                "change_id": "before-side-effect",
6929                "version": 1,
6930                "min_supported": 1,
6931                "max_supported": 1,
6932            }),
6933        )]);
6934        let error = reordered
6935            .side_effect(|| "new".to_string())
6936            .expect_err("command reordering must fail");
6937        assert!(matches!(
6938            error,
6939            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
6940                if reason == "recorded_command_mismatch"
6941        ));
6942    }
6943
6944    #[test]
6945    fn version_markers_replay_across_upgrades_and_do_not_duplicate() {
6946        let ctx = workflow_context(Vec::new());
6947        assert_eq!(ctx.get_version("checkout-v2", 1, 2).expect("version"), 2);
6948        assert_eq!(ctx.get_version("checkout-v2", 1, 3).expect("cached"), 2);
6949        assert!(ctx.patched("new-search").expect("patch"));
6950        ctx.deprecate_patch("new-search").expect("deprecate patch");
6951        let commands = ctx.take_commands().expect("commands");
6952        assert_eq!(commands.len(), 2);
6953        assert_eq!(commands[0]["type"], "record_version_marker");
6954        assert_eq!(commands[0]["version"], 2);
6955        assert_eq!(commands[1]["change_id"], "new-search");
6956
6957        let replay = workflow_context(vec![history_event(
6958            "VersionMarkerRecorded",
6959            json!({
6960                "sequence": 1,
6961                "change_id": "checkout-v2",
6962                "version": 2,
6963                "min_supported": 1,
6964                "max_supported": 2,
6965            }),
6966        )]);
6967        assert_eq!(replay.get_version("checkout-v2", 1, 4).expect("upgrade"), 2);
6968        assert_eq!(replay.get_version("checkout-v2", 2, 5).expect("repeat"), 2);
6969        assert!(replay.take_commands().expect("commands").is_empty());
6970        replay.ensure_history_consumed().expect("history consumed");
6971    }
6972
6973    #[test]
6974    fn version_markers_reject_incompatible_or_malformed_history() {
6975        let incompatible = workflow_context(vec![history_event(
6976            "VersionMarkerRecorded",
6977            json!({
6978                "sequence": 1,
6979                "change_id": "checkout-v2",
6980                "version": 1,
6981                "min_supported": 1,
6982                "max_supported": 2,
6983            }),
6984        )]);
6985        let error = incompatible
6986            .get_version("checkout-v2", 2, 3)
6987            .expect_err("old version is unsupported");
6988        assert!(matches!(
6989            error,
6990            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
6991                if reason == "version_marker_incompatible_range"
6992        ));
6993
6994        for (history, reason) in [
6995            (
6996                vec![history_event("SideEffectRecorded", json!({"sequence": 1}))],
6997                "side_effect_result_missing",
6998            ),
6999            (
7000                vec![history_event(
7001                    "SideEffectRecorded",
7002                    json!({
7003                        "sequence": 1,
7004                        "result": {"codec": "avro", "blob": "not-base64"},
7005                    }),
7006                )],
7007                "side_effect_payload_incompatible",
7008            ),
7009            (
7010                vec![history_event(
7011                    "SideEffectRecorded",
7012                    json!({"sequence": 1, "result": {"unwrapped": true}}),
7013                )],
7014                "side_effect_payload_malformed",
7015            ),
7016            (
7017                vec![history_event(
7018                    "VersionMarkerRecorded",
7019                    json!({
7020                        "sequence": 1,
7021                        "change_id": "change",
7022                        "version": 1,
7023                        "min_supported": 2,
7024                        "max_supported": 1,
7025                    }),
7026                )],
7027                "version_marker_history_range_invalid",
7028            ),
7029        ] {
7030            let error = WorkflowState::new(
7031                history,
7032                "rust-workers".to_string(),
7033                JSON_CODEC.to_string(),
7034                None,
7035            )
7036            .expect_err("malformed history must fail");
7037            assert!(matches!(
7038                error,
7039                Error::NonDeterministicReplay(ReplayFailure { reason: actual, .. })
7040                    if actual == reason
7041            ));
7042        }
7043    }
7044
7045    #[test]
7046    fn duplicate_side_effects_and_version_markers_are_rejected() {
7047        let duplicate_side_effect = WorkflowState::new(
7048            vec![
7049                history_event(
7050                    "SideEffectRecorded",
7051                    json!({"sequence": 1, "result": {"codec": "json", "blob": "1"}}),
7052                ),
7053                history_event(
7054                    "SideEffectRecorded",
7055                    json!({"sequence": 1, "result": {"codec": "json", "blob": "2"}}),
7056                ),
7057            ],
7058            "rust-workers".to_string(),
7059            JSON_CODEC.to_string(),
7060            None,
7061        )
7062        .expect_err("duplicate side effect");
7063        assert!(matches!(
7064            duplicate_side_effect,
7065            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
7066                if reason == "duplicate_side_effect_record"
7067        ));
7068
7069        let marker = |sequence| {
7070            history_event(
7071                "VersionMarkerRecorded",
7072                json!({
7073                    "sequence": sequence,
7074                    "change_id": "same-change",
7075                    "version": 1,
7076                    "min_supported": 1,
7077                    "max_supported": 1,
7078                }),
7079            )
7080        };
7081        let duplicate_marker = WorkflowState::new(
7082            vec![marker(1), marker(2)],
7083            "rust-workers".to_string(),
7084            JSON_CODEC.to_string(),
7085            None,
7086        )
7087        .expect_err("duplicate marker");
7088        assert!(matches!(
7089            duplicate_marker,
7090            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
7091                if reason == "duplicate_version_marker"
7092        ));
7093    }
7094
7095    #[test]
7096    fn cold_worker_replay_does_not_repeat_committed_side_effects_or_markers() {
7097        fn worker(calls: Arc<AtomicUsize>) -> Worker {
7098            let client = Client::new("http://127.0.0.1:8080").expect("client");
7099            let mut worker = Worker::new(client, "rust-workers");
7100            worker.register_workflow("rust.side-effect-version", move |ctx, _input| {
7101                let calls = Arc::clone(&calls);
7102                async move {
7103                    let captured = ctx.side_effect(|| {
7104                        calls.fetch_add(1, Ordering::SeqCst);
7105                        "captured-once".to_string()
7106                    })?;
7107                    let version = ctx.get_version("cold-restart", 1, 2)?;
7108                    Ok(json!({"captured": captured, "version": version}))
7109                }
7110            });
7111            worker
7112        }
7113
7114        fn task(history_events: Vec<HistoryEvent>) -> WorkflowTask {
7115            WorkflowTask {
7116                task_id: "wft-side-effect-version".to_string(),
7117                workflow_id: Some("wf-side-effect-version".to_string()),
7118                run_id: Some("run-side-effect-version".to_string()),
7119                workflow_type: "rust.side-effect-version".to_string(),
7120                payload_codec: JSON_CODEC.to_string(),
7121                arguments: Some(encode_value_envelope(&json!([]), JSON_CODEC).expect("arguments")),
7122                history_events,
7123                total_history_events: None,
7124                next_history_page_token: None,
7125                workflow_task_attempt: 1,
7126                workflow_signal_id: None,
7127                signal_name: None,
7128                signal_arguments: None,
7129                lease_owner: Some("rust-worker".to_string()),
7130            }
7131        }
7132
7133        let calls = Arc::new(AtomicUsize::new(0));
7134        let initial = worker(Arc::clone(&calls))
7135            .execute_workflow_task(task(Vec::new()))
7136            .expect("initial execution");
7137        assert_eq!(
7138            initial
7139                .iter()
7140                .map(|command| &command["type"])
7141                .collect::<Vec<_>>(),
7142            vec![
7143                "record_side_effect",
7144                "record_version_marker",
7145                "complete_workflow"
7146            ]
7147        );
7148        assert_eq!(calls.load(Ordering::SeqCst), 1);
7149
7150        let restarted = worker(Arc::clone(&calls));
7151        let replayed = restarted
7152            .execute_workflow_task(task(vec![
7153                history_event(
7154                    "SideEffectRecorded",
7155                    json!({"sequence": 1, "result": initial[0]["result"].clone()}),
7156                ),
7157                history_event(
7158                    "VersionMarkerRecorded",
7159                    json!({
7160                        "sequence": 2,
7161                        "change_id": "cold-restart",
7162                        "version": 2,
7163                        "min_supported": 1,
7164                        "max_supported": 2,
7165                    }),
7166                ),
7167            ]))
7168            .expect("cold replay");
7169        assert_eq!(replayed.len(), 1);
7170        assert_eq!(replayed[0]["type"], "complete_workflow");
7171        assert_eq!(calls.load(Ordering::SeqCst), 1);
7172    }
7173
7174    #[test]
7175    fn side_effect_replay_rejects_changed_rust_value_type() {
7176        let result = encode_value_envelope(&json!({"value": 42}), JSON_CODEC).expect("result");
7177        let ctx = workflow_context(vec![history_event(
7178            "SideEffectRecorded",
7179            json!({"sequence": 1, "result": result}),
7180        )]);
7181        let error = ctx
7182            .side_effect::<Vec<String>, _>(|| panic!("callback must not run"))
7183            .expect_err("changed type must fail replay");
7184        assert!(matches!(
7185            error,
7186            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
7187                if reason == "side_effect_type_mismatch"
7188        ));
7189    }
7190
7191    fn completed_retry_activity_history() -> Vec<HistoryEvent> {
7192        vec![
7193            history_event(
7194                "ActivityScheduled",
7195                json!({
7196                    "sequence": 1,
7197                    "activity_type": "flaky",
7198                    "activity_execution_id": "act-1",
7199                    "activity": {
7200                        "id": "act-1",
7201                        "sequence": 1,
7202                        "type": "flaky",
7203                        "queue": "critical-activities",
7204                        "execution_mode": null,
7205                        "retry_policy": {
7206                            "snapshot_version": 1,
7207                            "max_attempts": 3,
7208                            "backoff_seconds": [2, 4],
7209                            "start_to_close_timeout": 30,
7210                            "schedule_to_start_timeout": 5,
7211                            "schedule_to_close_timeout": 90,
7212                            "heartbeat_timeout": 10,
7213                            "non_retryable_error_types": ["PermanentError"]
7214                        }
7215                    }
7216                }),
7217            ),
7218            history_event(
7219                "ActivityStarted",
7220                json!({
7221                    "sequence": 1,
7222                    "activity_type": "flaky",
7223                    "activity_execution_id": "act-1",
7224                    "activity_attempt_id": "attempt-1",
7225                    "attempt_number": 1
7226                }),
7227            ),
7228            history_event(
7229                "ActivityRetryScheduled",
7230                json!({
7231                    "sequence": 1,
7232                    "activity_type": "flaky",
7233                    "activity_execution_id": "act-1",
7234                    "activity_attempt_id": "attempt-1",
7235                    "attempt_number": 1,
7236                    "retry_after_attempt": 1,
7237                    "retry_backoff_seconds": 2,
7238                    "failure_category": "activity",
7239                    "exception_type": "TransientError"
7240                }),
7241            ),
7242            history_event(
7243                "ActivityStarted",
7244                json!({
7245                    "sequence": 1,
7246                    "activity_type": "flaky",
7247                    "activity_execution_id": "act-1",
7248                    "activity_attempt_id": "attempt-2",
7249                    "attempt_number": 2
7250                }),
7251            ),
7252            history_event(
7253                "ActivityCompleted",
7254                json!({
7255                    "sequence": 1,
7256                    "activity_type": "flaky",
7257                    "activity_execution_id": "act-1",
7258                    "activity_attempt_id": "attempt-2",
7259                    "attempt_number": 2,
7260                    "payload_codec": "json",
7261                    "result": {"codec": "json", "blob": "{\"status\":\"recovered\"}"}
7262                }),
7263            ),
7264        ]
7265    }
7266
7267    fn retry_activity_options() -> ActivityOptions {
7268        ActivityOptions::new()
7269            .task_queue("critical-activities")
7270            .retry_policy(
7271                ActivityRetryPolicy::new(3)
7272                    .backoff_intervals([Duration::from_secs(2), Duration::from_secs(4)])
7273                    .non_retryable_error_type("PermanentError"),
7274            )
7275            .start_to_close_timeout(Duration::from_secs(30))
7276            .schedule_to_start_timeout(Duration::from_secs(5))
7277            .schedule_to_close_timeout(Duration::from_secs(90))
7278            .heartbeat_timeout(Duration::from_secs(10))
7279    }
7280
7281    #[test]
7282    fn avro_generic_wrapper_round_trips_json_values() {
7283        let value = json!({"greeting": "hello", "count": 3, "ok": true});
7284        let envelope = PayloadEnvelope::avro(&value).expect("encode");
7285        assert_eq!(envelope.codec, DEFAULT_CODEC);
7286        assert_eq!(decode_payload::<Value>(&envelope).expect("decode"), value);
7287    }
7288
7289    #[test]
7290    fn json_codec_remains_plain_json() {
7291        let value = json!({"greeting": "hello", "count": 3, "ok": true});
7292        let envelope = PayloadEnvelope::json(&value).expect("encode");
7293
7294        assert_eq!(envelope.codec, JSON_CODEC);
7295        assert_eq!(envelope.blob, serde_json::to_string(&value).expect("json"));
7296        assert_eq!(decode_payload::<Value>(&envelope).expect("decode"), value);
7297    }
7298
7299    #[test]
7300    fn typed_avro_payload_without_schema_context_keeps_diagnostic() {
7301        let envelope = PayloadEnvelope {
7302            codec: DEFAULT_CODEC.to_string(),
7303            blob: BASE64.encode([0x01]),
7304        };
7305
7306        let error = decode_payload::<Value>(&envelope).expect_err("typed payload must fail");
7307        assert_eq!(
7308            error.to_string(),
7309            "codec error: typed avro payloads require a schema context; v1 supports the generic wrapper"
7310        );
7311    }
7312
7313    #[test]
7314    fn workflow_context_schedules_activity_until_completion_is_in_history() {
7315        let ctx = WorkflowContext {
7316            state: Arc::new(Mutex::new(
7317                WorkflowState::new_with_identity(
7318                    Vec::new(),
7319                    Some("wf-parent".to_string()),
7320                    Some("run-parent".to_string()),
7321                    "rust-workers".to_string(),
7322                    DEFAULT_CODEC.to_string(),
7323                    None,
7324                )
7325                .expect("workflow state"),
7326            )),
7327        };
7328
7329        let mut call = Box::pin(ctx.activity("hello.activity", json!(["Ada"])));
7330        let mut task_context = TaskContext::from_waker(noop_waker_ref());
7331        assert!(matches!(
7332            call.as_mut().poll(&mut task_context),
7333            Poll::Pending
7334        ));
7335
7336        let commands = ctx.take_commands().expect("commands");
7337        assert_eq!(commands[0]["type"], "schedule_activity");
7338        assert_eq!(commands[0]["activity_type"], "hello.activity");
7339    }
7340
7341    #[test]
7342    fn activity_options_encode_retry_policy_queue_and_every_timeout() {
7343        let ctx = workflow_context(Vec::new());
7344        let options = ActivityOptions::new()
7345            .task_queue("payments")
7346            .retry_policy(
7347                ActivityRetryPolicy::new(4)
7348                    .exponential_backoff(Duration::from_secs(1), 3, Some(Duration::from_secs(10)))
7349                    .non_retryable_error_type("ValidationError"),
7350            )
7351            .start_to_close_timeout(Duration::from_secs(120))
7352            .schedule_to_start_timeout(Duration::from_secs(10))
7353            .schedule_to_close_timeout(Duration::from_secs(300))
7354            .heartbeat_timeout(Duration::from_secs(15));
7355        let mut call = Box::pin(ctx.activity_with_options(
7356            "charge-card",
7357            options,
7358            json!([{"order_id": "o-1"}]),
7359        ));
7360        let mut task_context = TaskContext::from_waker(noop_waker_ref());
7361
7362        assert!(matches!(
7363            call.as_mut().poll(&mut task_context),
7364            Poll::Pending
7365        ));
7366        assert!(matches!(
7367            call.as_mut().poll(&mut task_context),
7368            Poll::Pending
7369        ));
7370
7371        let commands = ctx.take_commands().expect("activity command");
7372        assert_eq!(commands.len(), 1, "one future emits one logical schedule");
7373        assert_eq!(commands[0]["queue"], "payments");
7374        assert_eq!(
7375            commands[0]["retry_policy"],
7376            json!({
7377                "max_attempts": 4,
7378                "backoff_seconds": [1, 3, 9],
7379                "non_retryable_error_types": ["ValidationError"],
7380            })
7381        );
7382        assert_eq!(commands[0]["start_to_close_timeout"], 120);
7383        assert_eq!(commands[0]["schedule_to_start_timeout"], 10);
7384        assert_eq!(commands[0]["schedule_to_close_timeout"], 300);
7385        assert_eq!(commands[0]["heartbeat_timeout"], 15);
7386    }
7387
7388    #[test]
7389    fn activity_options_encode_explicit_and_rounded_backoff_intervals() {
7390        let ctx = workflow_context(Vec::new());
7391        let options = ActivityOptions::new().retry_policy(
7392            ActivityRetryPolicy::new(3)
7393                .backoff_intervals([Duration::from_millis(1), Duration::from_millis(1_001)]),
7394        );
7395        let mut call = Box::pin(ctx.activity_with_options("work", options, json!([])));
7396        let mut task_context = TaskContext::from_waker(noop_waker_ref());
7397
7398        assert!(matches!(
7399            call.as_mut().poll(&mut task_context),
7400            Poll::Pending
7401        ));
7402        assert_eq!(
7403            ctx.take_commands().expect("command")[0]["retry_policy"]["backoff_seconds"],
7404            json!([1, 2])
7405        );
7406    }
7407
7408    #[test]
7409    fn invalid_activity_options_return_typed_errors_before_emitting_commands() {
7410        let cases = [
7411            (
7412                ActivityOptions::new().task_queue("  "),
7413                ActivityOptionsErrorKind::EmptyTaskQueue,
7414            ),
7415            (
7416                ActivityOptions::new().retry_policy(ActivityRetryPolicy::default()),
7417                ActivityOptionsErrorKind::EmptyRetryPolicy,
7418            ),
7419            (
7420                ActivityOptions::new().retry_policy(ActivityRetryPolicy::new(0)),
7421                ActivityOptionsErrorKind::InvalidMaxAttempts,
7422            ),
7423            (
7424                ActivityOptions::new().retry_policy(ActivityRetryPolicy {
7425                    max_attempts: None,
7426                    backoff: Some(ActivityBackoff::Explicit(vec![Duration::from_secs(1)])),
7427                    non_retryable_error_types: Vec::new(),
7428                }),
7429                ActivityOptionsErrorKind::BackoffWithoutRetryBudget,
7430            ),
7431            (
7432                ActivityOptions::new().retry_policy(
7433                    ActivityRetryPolicy::new(2)
7434                        .backoff_intervals([Duration::from_secs(1), Duration::from_secs(2)]),
7435                ),
7436                ActivityOptionsErrorKind::TooManyBackoffIntervals,
7437            ),
7438            (
7439                ActivityOptions::new().retry_policy(
7440                    ActivityRetryPolicy::new(2).exponential_backoff(
7441                        Duration::from_secs(1),
7442                        0,
7443                        None,
7444                    ),
7445                ),
7446                ActivityOptionsErrorKind::InvalidBackoffCoefficient,
7447            ),
7448            (
7449                ActivityOptions::new()
7450                    .retry_policy(ActivityRetryPolicy::new(2).non_retryable_error_type("  ")),
7451                ActivityOptionsErrorKind::EmptyNonRetryableErrorType,
7452            ),
7453            (
7454                ActivityOptions::new().retry_policy(
7455                    ActivityRetryPolicy::new(10_002).exponential_backoff(
7456                        Duration::from_secs(1),
7457                        1,
7458                        None,
7459                    ),
7460                ),
7461                ActivityOptionsErrorKind::BackoffGenerationTooLarge,
7462            ),
7463            (
7464                ActivityOptions::new().retry_policy(
7465                    ActivityRetryPolicy::new(2)
7466                        .backoff_intervals([Duration::from_secs(i64::MAX as u64 + 1)]),
7467                ),
7468                ActivityOptionsErrorKind::BackoffOverflow,
7469            ),
7470        ];
7471
7472        for (options, expected_kind) in cases {
7473            let ctx = workflow_context(Vec::new());
7474            let mut call = Box::pin(ctx.activity_with_options("work", options, json!([])));
7475            let mut task_context = TaskContext::from_waker(noop_waker_ref());
7476            let Poll::Ready(Err(Error::InvalidActivityOptions(error))) =
7477                call.as_mut().poll(&mut task_context)
7478            else {
7479                panic!("expected typed activity validation error");
7480            };
7481            assert_eq!(error.kind, expected_kind);
7482            assert!(ctx.take_commands().expect("commands").is_empty());
7483        }
7484    }
7485
7486    #[test]
7487    fn activity_options_validate_positive_and_ordered_timeouts() {
7488        let zero_timeout_cases = [
7489            ActivityOptions::new().start_to_close_timeout(Duration::ZERO),
7490            ActivityOptions::new().schedule_to_start_timeout(Duration::ZERO),
7491            ActivityOptions::new().schedule_to_close_timeout(Duration::ZERO),
7492            ActivityOptions::new().heartbeat_timeout(Duration::ZERO),
7493        ];
7494        for options in zero_timeout_cases {
7495            assert_eq!(
7496                options.validate().expect_err("zero timeout").kind,
7497                ActivityOptionsErrorKind::TimeoutNotPositive
7498            );
7499        }
7500
7501        let ordering_cases = [
7502            ActivityOptions::new()
7503                .heartbeat_timeout(Duration::from_secs(11))
7504                .start_to_close_timeout(Duration::from_secs(10)),
7505            ActivityOptions::new()
7506                .start_to_close_timeout(Duration::from_secs(31))
7507                .schedule_to_close_timeout(Duration::from_secs(30)),
7508            ActivityOptions::new()
7509                .schedule_to_start_timeout(Duration::from_secs(31))
7510                .schedule_to_close_timeout(Duration::from_secs(30)),
7511        ];
7512        for options in ordering_cases {
7513            assert_eq!(
7514                options.validate().expect_err("timeout order").kind,
7515                ActivityOptionsErrorKind::TimeoutOrder
7516            );
7517        }
7518
7519        assert_eq!(
7520            ActivityOptions::new()
7521                .start_to_close_timeout(Duration::from_secs(i64::MAX as u64 + 1))
7522                .validate()
7523                .expect_err("protocol integer overflow")
7524                .kind,
7525            ActivityOptionsErrorKind::TimeoutOverflow
7526        );
7527    }
7528
7529    #[test]
7530    fn replayed_activity_retry_history_completes_without_duplicate_schedule() {
7531        let ctx = workflow_context(completed_retry_activity_history());
7532        let mut call =
7533            Box::pin(ctx.activity_with_options("flaky", retry_activity_options(), json!([])));
7534        let mut task_context = TaskContext::from_waker(noop_waker_ref());
7535
7536        assert!(matches!(
7537            call.as_mut().poll(&mut task_context),
7538            Poll::Ready(Ok(result)) if result == json!({"status": "recovered"})
7539        ));
7540        assert!(ctx.take_commands().expect("commands").is_empty());
7541        ctx.ensure_history_consumed().expect("history consumed");
7542    }
7543
7544    #[test]
7545    fn duplicate_non_retryable_types_use_one_command_and_replay_representation() {
7546        let mut options = retry_activity_options();
7547        options
7548            .retry_policy
7549            .as_mut()
7550            .expect("retry policy")
7551            .non_retryable_error_types
7552            .extend([" PermanentError ".to_string(), "PermanentError".to_string()]);
7553
7554        let new_ctx = workflow_context(Vec::new());
7555        let mut new_call =
7556            Box::pin(new_ctx.activity_with_options("flaky", options.clone(), json!([])));
7557        let mut task_context = TaskContext::from_waker(noop_waker_ref());
7558        assert!(matches!(
7559            new_call.as_mut().poll(&mut task_context),
7560            Poll::Pending
7561        ));
7562        let commands = new_ctx.take_commands().expect("commands");
7563        assert_eq!(commands.len(), 1);
7564        assert_eq!(
7565            commands[0]["retry_policy"]["non_retryable_error_types"],
7566            json!(["PermanentError"])
7567        );
7568
7569        let replay_ctx = workflow_context(completed_retry_activity_history());
7570        let mut replay_call =
7571            Box::pin(replay_ctx.activity_with_options("flaky", options, json!([])));
7572        assert!(matches!(
7573            replay_call.as_mut().poll(&mut task_context),
7574            Poll::Ready(Ok(result)) if result == json!({"status": "recovered"})
7575        ));
7576        assert!(replay_ctx.take_commands().expect("commands").is_empty());
7577        replay_ctx
7578            .ensure_history_consumed()
7579            .expect("history consumed");
7580    }
7581
7582    #[test]
7583    fn replayed_intermediate_retry_remains_pending_across_restarts() {
7584        let history = completed_retry_activity_history()
7585            .into_iter()
7586            .take(3)
7587            .collect::<Vec<_>>();
7588
7589        for _restart in 0..2 {
7590            let ctx = workflow_context(history.clone());
7591            let mut call =
7592                Box::pin(ctx.activity_with_options("flaky", retry_activity_options(), json!([])));
7593            let mut task_context = TaskContext::from_waker(noop_waker_ref());
7594            assert!(matches!(
7595                call.as_mut().poll(&mut task_context),
7596                Poll::Pending
7597            ));
7598            assert!(ctx.take_commands().expect("commands").is_empty());
7599        }
7600    }
7601
7602    #[test]
7603    fn replayed_activity_rejects_changed_queue_retry_and_every_timeout_field() {
7604        let mut changed_queue = retry_activity_options();
7605        changed_queue.task_queue = Some("different-queue".to_string());
7606
7607        let mut changed_max_attempts = retry_activity_options();
7608        let retry_policy = changed_max_attempts
7609            .retry_policy
7610            .as_mut()
7611            .expect("retry policy");
7612        retry_policy.max_attempts = Some(4);
7613
7614        let mut changed_backoff = retry_activity_options();
7615        let retry_policy = changed_backoff.retry_policy.as_mut().expect("retry policy");
7616        retry_policy.backoff = Some(ActivityBackoff::Explicit(vec![
7617            Duration::from_secs(3),
7618            Duration::from_secs(4),
7619        ]));
7620
7621        let mut changed_non_retryable_types = retry_activity_options();
7622        let retry_policy = changed_non_retryable_types
7623            .retry_policy
7624            .as_mut()
7625            .expect("retry policy");
7626        retry_policy.non_retryable_error_types = vec!["AnotherPermanentError".to_string()];
7627
7628        let mut changed_start_to_close = retry_activity_options();
7629        changed_start_to_close.start_to_close_timeout = Some(Duration::from_secs(31));
7630        let mut changed_schedule_to_start = retry_activity_options();
7631        changed_schedule_to_start.schedule_to_start_timeout = Some(Duration::from_secs(6));
7632        let mut changed_schedule_to_close = retry_activity_options();
7633        changed_schedule_to_close.schedule_to_close_timeout = Some(Duration::from_secs(91));
7634        let mut changed_heartbeat = retry_activity_options();
7635        changed_heartbeat.heartbeat_timeout = Some(Duration::from_secs(11));
7636
7637        let cases = [
7638            (changed_queue, "activity_task_queue_mismatch"),
7639            (changed_max_attempts, "activity_retry_policy_mismatch"),
7640            (changed_backoff, "activity_retry_policy_mismatch"),
7641            (
7642                changed_non_retryable_types,
7643                "activity_retry_policy_mismatch",
7644            ),
7645            (changed_start_to_close, "activity_retry_policy_mismatch"),
7646            (changed_schedule_to_start, "activity_retry_policy_mismatch"),
7647            (changed_schedule_to_close, "activity_retry_policy_mismatch"),
7648            (changed_heartbeat, "activity_retry_policy_mismatch"),
7649        ];
7650
7651        for (options, expected_reason) in cases {
7652            let ctx = workflow_context(completed_retry_activity_history());
7653            let mut call = Box::pin(ctx.activity_with_options("flaky", options, json!([])));
7654            let mut task_context = TaskContext::from_waker(noop_waker_ref());
7655            let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
7656                call.as_mut().poll(&mut task_context)
7657            else {
7658                panic!("changed activity options must fail replay");
7659            };
7660            assert_eq!(failure.reason, expected_reason);
7661            assert_eq!(failure.sequence, Some(1));
7662            assert!(ctx.take_commands().expect("commands").is_empty());
7663        }
7664    }
7665
7666    #[test]
7667    fn replayed_activity_rejects_changed_execution_mode_and_snapshot_version() {
7668        let cases = [
7669            (
7670                "execution_mode",
7671                json!("local"),
7672                "activity_execution_mode_mismatch",
7673            ),
7674            (
7675                "snapshot_version",
7676                json!(2),
7677                "activity_retry_policy_mismatch",
7678            ),
7679        ];
7680
7681        for (field, value, expected_reason) in cases {
7682            let mut history = completed_retry_activity_history();
7683            let activity = history[0].payload["activity"]
7684                .as_object_mut()
7685                .expect("activity snapshot");
7686            if field == "execution_mode" {
7687                activity.insert(field.to_string(), value);
7688            } else {
7689                activity["retry_policy"]
7690                    .as_object_mut()
7691                    .expect("retry snapshot")
7692                    .insert(field.to_string(), value);
7693            }
7694
7695            let ctx = workflow_context(history);
7696            let mut call =
7697                Box::pin(ctx.activity_with_options("flaky", retry_activity_options(), json!([])));
7698            let mut task_context = TaskContext::from_waker(noop_waker_ref());
7699            let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
7700                call.as_mut().poll(&mut task_context)
7701            else {
7702                panic!("changed {field} must fail replay");
7703            };
7704            assert_eq!(failure.reason, expected_reason);
7705            assert_eq!(failure.sequence, Some(1));
7706            assert!(ctx.take_commands().expect("commands").is_empty());
7707        }
7708    }
7709
7710    #[test]
7711    fn replayed_legacy_activity_treats_missing_option_snapshot_as_unknown() {
7712        let mut history = completed_retry_activity_history();
7713        let activity = history[0].payload["activity"]
7714            .as_object_mut()
7715            .expect("activity snapshot");
7716        activity.remove("execution_mode");
7717        activity.remove("retry_policy");
7718
7719        let mut current = retry_activity_options();
7720        current.start_to_close_timeout = Some(Duration::from_secs(45));
7721        current.schedule_to_start_timeout = Some(Duration::from_secs(8));
7722        current.schedule_to_close_timeout = Some(Duration::from_secs(120));
7723        current.heartbeat_timeout = Some(Duration::from_secs(12));
7724
7725        let ctx = workflow_context(history);
7726        let mut call = Box::pin(ctx.activity_with_options("flaky", current, json!([])));
7727        let mut task_context = TaskContext::from_waker(noop_waker_ref());
7728        assert!(matches!(
7729            call.as_mut().poll(&mut task_context),
7730            Poll::Ready(Ok(result)) if result == json!({"status": "recovered"})
7731        ));
7732        assert!(ctx.take_commands().expect("commands").is_empty());
7733        ctx.ensure_history_consumed().expect("history consumed");
7734    }
7735
7736    #[test]
7737    fn terminal_activity_failed_after_start_returns_typed_failure() {
7738        let history = vec![
7739            history_event(
7740                "ActivityScheduled",
7741                json!({
7742                    "sequence": 1,
7743                    "activity_type": "flaky",
7744                    "activity_execution_id": "act-terminal",
7745                    "activity": {
7746                        "id": "act-terminal",
7747                        "sequence": 1,
7748                        "type": "flaky",
7749                        "queue": "critical-activities",
7750                        "retry_policy": {
7751                            "snapshot_version": 1,
7752                            "max_attempts": 3,
7753                            "backoff_seconds": [2, 4],
7754                            "non_retryable_error_types": ["PermanentError"]
7755                        }
7756                    }
7757                }),
7758            ),
7759            history_event(
7760                "ActivityStarted",
7761                json!({
7762                    "sequence": 1,
7763                    "activity_type": "flaky",
7764                    "activity_execution_id": "act-terminal",
7765                    "activity_attempt_id": "attempt-1",
7766                    "attempt_number": 1
7767                }),
7768            ),
7769            history_event(
7770                "ActivityFailed",
7771                json!({
7772                    "sequence": 1,
7773                    "activity_type": "flaky",
7774                    "activity_execution_id": "act-terminal",
7775                    "activity_attempt_id": "attempt-1",
7776                    "attempt_number": 1,
7777                    "failure_id": "failure-terminal",
7778                    "failure_category": "activity",
7779                    "exception_type": "PermanentError",
7780                    "message": "cannot retry",
7781                    "non_retryable": true
7782                }),
7783            ),
7784        ];
7785        let ctx = workflow_context(history);
7786        let mut call =
7787            Box::pin(ctx.activity_with_options("flaky", retry_activity_options(), json!([])));
7788        let mut task_context = TaskContext::from_waker(noop_waker_ref());
7789
7790        let Poll::Ready(Err(Error::ActivityFailed(failure))) =
7791            call.as_mut().poll(&mut task_context)
7792        else {
7793            panic!("terminal ActivityFailed must settle the activity future");
7794        };
7795        assert_eq!(failure.kind, ActivityFailureKind::Failed);
7796        assert_eq!(
7797            failure.activity_execution_id.as_deref(),
7798            Some("act-terminal")
7799        );
7800        assert_eq!(failure.exception_type.as_deref(), Some("PermanentError"));
7801        assert!(failure.non_retryable);
7802        assert!(ctx.take_commands().expect("commands").is_empty());
7803        ctx.ensure_history_consumed().expect("history consumed");
7804    }
7805
7806    #[test]
7807    fn activity_terminal_events_return_machine_readable_failures() {
7808        let cases = [
7809            (
7810                "ActivityFailed",
7811                json!({
7812                    "sequence": 1,
7813                    "activity_type": "charge-card",
7814                    "activity_execution_id": "act-1",
7815                    "activity_attempt_id": "attempt-2",
7816                    "attempt_number": 2,
7817                    "failure_id": "failure-1",
7818                    "failure_category": "activity",
7819                    "exception_type": "PaymentDeclined",
7820                    "exception_class": "payments.PaymentDeclined",
7821                    "message": "card declined",
7822                    "non_retryable": true
7823                }),
7824                ActivityFailureKind::Failed,
7825                "activity",
7826            ),
7827            (
7828                "ActivityCancelled",
7829                json!({
7830                    "sequence": 1,
7831                    "activity_type": "charge-card",
7832                    "activity_execution_id": "act-1",
7833                    "activity_attempt_id": "attempt-1"
7834                }),
7835                ActivityFailureKind::Cancelled,
7836                "cancelled",
7837            ),
7838        ];
7839
7840        for (event_type, payload, expected_kind, expected_reason) in cases {
7841            let ctx = workflow_context(vec![history_event(event_type, payload)]);
7842            let mut call = Box::pin(ctx.activity("charge-card", json!([])));
7843            let mut task_context = TaskContext::from_waker(noop_waker_ref());
7844            let Poll::Ready(Err(Error::ActivityFailed(failure))) =
7845                call.as_mut().poll(&mut task_context)
7846            else {
7847                panic!("expected terminal activity failure");
7848            };
7849            assert_eq!(failure.kind, expected_kind);
7850            assert_eq!(failure.reason, expected_reason);
7851            assert_eq!(failure.activity_execution_id.as_deref(), Some("act-1"));
7852            assert_eq!(failure.activity_type.as_deref(), Some("charge-card"));
7853        }
7854    }
7855
7856    #[test]
7857    fn every_activity_timeout_class_is_typed() {
7858        for timeout_kind in [
7859            "start_to_close",
7860            "schedule_to_start",
7861            "schedule_to_close",
7862            "heartbeat",
7863        ] {
7864            let ctx = workflow_context(vec![history_event(
7865                "ActivityTimedOut",
7866                json!({
7867                    "sequence": 1,
7868                    "activity_type": "slow",
7869                    "activity_execution_id": "act-timeout",
7870                    "activity_attempt_id": "attempt-timeout",
7871                    "failure_category": "timeout",
7872                    "timeout_kind": timeout_kind,
7873                    "message": "deadline expired"
7874                }),
7875            )]);
7876            let mut call = Box::pin(ctx.activity("slow", json!([])));
7877            let mut task_context = TaskContext::from_waker(noop_waker_ref());
7878            let Poll::Ready(Err(Error::ActivityFailed(failure))) =
7879                call.as_mut().poll(&mut task_context)
7880            else {
7881                panic!("expected timeout failure");
7882            };
7883            assert_eq!(failure.kind, ActivityFailureKind::TimedOut);
7884            assert_eq!(failure.reason, timeout_kind);
7885            assert_eq!(failure.timeout_kind.as_deref(), Some(timeout_kind));
7886            assert_eq!(failure.failure_category.as_deref(), Some("timeout"));
7887        }
7888    }
7889
7890    #[test]
7891    fn workflow_sleep_emits_one_durable_timer_and_rounds_up() {
7892        let ctx = workflow_context(Vec::new());
7893        let mut sleep = Box::pin(ctx.sleep(Duration::from_millis(1_001)));
7894        let mut task_context = TaskContext::from_waker(noop_waker_ref());
7895
7896        assert!(matches!(
7897            sleep.as_mut().poll(&mut task_context),
7898            Poll::Pending
7899        ));
7900        assert!(matches!(
7901            sleep.as_mut().poll(&mut task_context),
7902            Poll::Pending
7903        ));
7904
7905        let commands = ctx.take_commands().expect("timer command");
7906        assert_eq!(
7907            commands,
7908            vec![json!({
7909                "type": "start_timer",
7910                "delay_seconds": 2,
7911            })]
7912        );
7913    }
7914
7915    #[test]
7916    fn workflow_sleep_replays_matching_schedule_and_fire_without_a_command() {
7917        let history = vec![
7918            history_event(
7919                "TimerScheduled",
7920                json!({
7921                    "sequence": 1,
7922                    "timer_id": "timer-1",
7923                    "delay_seconds": 5,
7924                    "fire_at": "2026-07-11T12:00:05Z",
7925                }),
7926            ),
7927            history_event(
7928                "TimerFired",
7929                json!({
7930                    "sequence": 1,
7931                    "timer_id": "timer-1",
7932                    "delay_seconds": 5,
7933                    "fire_at": "2026-07-11T12:00:05Z",
7934                    "fired_at": "2026-07-11T12:00:05Z",
7935                }),
7936            ),
7937        ];
7938
7939        for _restart in 0..2 {
7940            let ctx = workflow_context(history.clone());
7941            let mut sleep = Box::pin(ctx.sleep(Duration::from_secs(5)));
7942            let mut task_context = TaskContext::from_waker(noop_waker_ref());
7943            assert!(matches!(
7944                sleep.as_mut().poll(&mut task_context),
7945                Poll::Ready(Ok(()))
7946            ));
7947            assert!(ctx.take_commands().expect("commands").is_empty());
7948            ctx.ensure_history_consumed().expect("history consumed");
7949        }
7950    }
7951
7952    #[test]
7953    fn workflow_sleep_rejects_changed_delay_during_replay() {
7954        let ctx = workflow_context(vec![
7955            history_event(
7956                "TimerScheduled",
7957                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
7958            ),
7959            history_event(
7960                "TimerFired",
7961                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
7962            ),
7963        ]);
7964        let mut sleep = Box::pin(ctx.sleep(Duration::from_secs(500)));
7965        let mut task_context = TaskContext::from_waker(noop_waker_ref());
7966
7967        let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
7968            sleep.as_mut().poll(&mut task_context)
7969        else {
7970            panic!("changed timer delay must be rejected");
7971        };
7972        assert_eq!(failure.reason, "timer_delay_mismatch");
7973        assert_eq!(failure.sequence, Some(1));
7974    }
7975
7976    #[test]
7977    fn workflow_history_rejects_unpaired_or_mismatched_timer_events() {
7978        let lone_fire = WorkflowState::new(
7979            vec![history_event(
7980                "TimerFired",
7981                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
7982            )],
7983            "rust-workers".to_string(),
7984            JSON_CODEC.to_string(),
7985            None,
7986        )
7987        .expect_err("TimerFired requires TimerScheduled");
7988        assert!(matches!(
7989            lone_fire,
7990            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
7991                if reason == "timer_schedule_missing_or_duplicate"
7992        ));
7993
7994        let wrong_identity = WorkflowState::new(
7995            vec![
7996                history_event(
7997                    "TimerScheduled",
7998                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
7999                ),
8000                history_event(
8001                    "TimerFired",
8002                    json!({"sequence": 1, "timer_id": "timer-2", "delay_seconds": 5}),
8003                ),
8004            ],
8005            "rust-workers".to_string(),
8006            JSON_CODEC.to_string(),
8007            None,
8008        )
8009        .expect_err("fire must match scheduled timer identity");
8010        assert!(matches!(
8011            wrong_identity,
8012            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
8013                if reason == "timer_identity_mismatch"
8014        ));
8015
8016        let duplicate_fire = WorkflowState::new(
8017            vec![
8018                history_event(
8019                    "TimerScheduled",
8020                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8021                ),
8022                history_event(
8023                    "TimerFired",
8024                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8025                ),
8026                history_event(
8027                    "TimerFired",
8028                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8029                ),
8030            ],
8031            "rust-workers".to_string(),
8032            JSON_CODEC.to_string(),
8033            None,
8034        )
8035        .expect_err("a durable timer cannot fire twice");
8036        assert!(matches!(
8037            duplicate_fire,
8038            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
8039                if reason == "duplicate_timer_fire"
8040        ));
8041
8042        let wrong_fired_delay = WorkflowState::new(
8043            vec![
8044                history_event(
8045                    "TimerScheduled",
8046                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8047                ),
8048                history_event(
8049                    "TimerFired",
8050                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 6}),
8051                ),
8052            ],
8053            "rust-workers".to_string(),
8054            JSON_CODEC.to_string(),
8055            None,
8056        )
8057        .expect_err("timer schedule and fire delays must agree");
8058        assert!(matches!(
8059            wrong_fired_delay,
8060            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
8061                if reason == "timer_history_delay_mismatch"
8062        ));
8063    }
8064
8065    #[test]
8066    fn replay_rejects_activity_moved_before_recorded_timer() {
8067        let ctx = workflow_context(vec![
8068            history_event(
8069                "TimerScheduled",
8070                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8071            ),
8072            history_event(
8073                "TimerFired",
8074                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8075            ),
8076            history_event(
8077                "ActivityCompleted",
8078                json!({
8079                    "sequence": 2,
8080                    "activity_type": "after-timer",
8081                    "payload_codec": "json",
8082                    "result": {"codec": "json", "blob": "\"done\""},
8083                }),
8084            ),
8085        ]);
8086        let mut activity = Box::pin(ctx.activity("after-timer", json!([])));
8087        let mut task_context = TaskContext::from_waker(noop_waker_ref());
8088
8089        let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
8090            activity.as_mut().poll(&mut task_context)
8091        else {
8092            panic!("reordered durable command must be rejected");
8093        };
8094        assert_eq!(failure.reason, "recorded_command_mismatch");
8095        assert_eq!(failure.sequence, Some(1));
8096        assert_eq!(failure.expected.as_deref(), Some("timer"));
8097        assert_eq!(failure.actual.as_deref(), Some("activity:after-timer"));
8098    }
8099
8100    #[test]
8101    fn replay_orders_signal_waits_and_timers_in_one_command_stream() {
8102        let signal_then_timer = vec![
8103            history_event(
8104                "ConditionWaitOpened",
8105                json!({"sequence": 1, "condition_key": "signal:go"}),
8106            ),
8107            history_event(
8108                "SignalReceived",
8109                json!({
8110                    "signal_name": "go",
8111                    "arguments": ["now"],
8112                }),
8113            ),
8114            history_event(
8115                "TimerScheduled",
8116                json!({"sequence": 2, "timer_id": "timer-2", "delay_seconds": 5}),
8117            ),
8118            history_event(
8119                "TimerFired",
8120                json!({"sequence": 2, "timer_id": "timer-2", "delay_seconds": 5}),
8121            ),
8122        ];
8123
8124        let ctx = workflow_context(signal_then_timer.clone());
8125        let mut signal = Box::pin(ctx.wait_signal("go"));
8126        let mut task_context = TaskContext::from_waker(noop_waker_ref());
8127        assert!(matches!(
8128            signal.as_mut().poll(&mut task_context),
8129            Poll::Ready(Ok(arguments)) if arguments == vec![json!("now")]
8130        ));
8131        let mut timer = Box::pin(ctx.sleep(Duration::from_secs(5)));
8132        assert!(matches!(
8133            timer.as_mut().poll(&mut task_context),
8134            Poll::Ready(Ok(()))
8135        ));
8136        ctx.ensure_history_consumed()
8137            .expect("signal and timer history consumed in order");
8138
8139        let reordered = workflow_context(signal_then_timer);
8140        let mut timer_first = Box::pin(reordered.sleep(Duration::from_secs(5)));
8141        let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
8142            timer_first.as_mut().poll(&mut task_context)
8143        else {
8144            panic!("timer cannot consume signal-wait-first history");
8145        };
8146        assert_eq!(failure.reason, "recorded_command_mismatch");
8147        assert_eq!(failure.sequence, Some(1));
8148        assert_eq!(failure.expected.as_deref(), Some("signal wait"));
8149
8150        let timer_then_signal = vec![
8151            history_event(
8152                "TimerScheduled",
8153                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8154            ),
8155            history_event(
8156                "TimerFired",
8157                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8158            ),
8159            history_event(
8160                "ConditionWaitOpened",
8161                json!({"sequence": 2, "condition_key": "signal:go"}),
8162            ),
8163            history_event(
8164                "SignalReceived",
8165                json!({"signal_name": "go", "arguments": []}),
8166            ),
8167        ];
8168        let reordered = workflow_context(timer_then_signal);
8169        let mut signal_first = Box::pin(reordered.wait_signal("go"));
8170        let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
8171            signal_first.as_mut().poll(&mut task_context)
8172        else {
8173            panic!("signal wait cannot consume timer-first history");
8174        };
8175        assert_eq!(failure.reason, "recorded_command_mismatch");
8176        assert_eq!(failure.sequence, Some(1));
8177        assert_eq!(failure.expected.as_deref(), Some("timer"));
8178    }
8179
8180    #[test]
8181    fn workflow_history_rejects_duplicate_or_colliding_command_sequences() {
8182        let duplicate_timer = WorkflowState::new(
8183            vec![
8184                history_event(
8185                    "TimerScheduled",
8186                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8187                ),
8188                history_event(
8189                    "TimerScheduled",
8190                    json!({"sequence": 1, "timer_id": "timer-2", "delay_seconds": 5}),
8191                ),
8192            ],
8193            "rust-workers".to_string(),
8194            JSON_CODEC.to_string(),
8195            None,
8196        )
8197        .expect_err("one workflow sequence cannot schedule two timers");
8198        assert!(matches!(
8199            duplicate_timer,
8200            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
8201                if reason == "timer_schedule_missing_or_duplicate"
8202        ));
8203
8204        let colliding_kinds = WorkflowState::new(
8205            vec![
8206                history_event(
8207                    "TimerScheduled",
8208                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8209                ),
8210                history_event(
8211                    "ActivityCompleted",
8212                    json!({"sequence": 1, "activity_type": "same-sequence"}),
8213                ),
8214            ],
8215            "rust-workers".to_string(),
8216            JSON_CODEC.to_string(),
8217            None,
8218        )
8219        .expect_err("one workflow sequence cannot identify two command kinds");
8220        assert!(matches!(
8221            colliding_kinds,
8222            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
8223                if reason == "durable_command_sequence_collision"
8224        ));
8225
8226        let duplicate_signal_wait = WorkflowState::new(
8227            vec![
8228                history_event(
8229                    "SignalWaitOpened",
8230                    json!({"sequence": 1, "signal_name": "go"}),
8231                ),
8232                history_event(
8233                    "SignalWaitOpened",
8234                    json!({"sequence": 1, "signal_name": "go"}),
8235                ),
8236            ],
8237            "rust-workers".to_string(),
8238            JSON_CODEC.to_string(),
8239            None,
8240        )
8241        .expect_err("one workflow sequence cannot open two signal waits");
8242        assert!(matches!(
8243            duplicate_signal_wait,
8244            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
8245                if reason == "duplicate_signal_wait_open"
8246        ));
8247    }
8248
8249    #[test]
8250    fn workflow_history_rejects_changed_numeric_command_sequence() {
8251        let result = encode_value_envelope(&json!({"captured": true}), JSON_CODEC)
8252            .expect("side-effect result");
8253        let error = WorkflowState::new(
8254            vec![history_event(
8255                "SideEffectRecorded",
8256                json!({"sequence": 99, "result": result}),
8257            )],
8258            "rust-workers".to_string(),
8259            JSON_CODEC.to_string(),
8260            None,
8261        )
8262        .expect_err("the first durable command must have sequence one");
8263
8264        let Error::NonDeterministicReplay(failure) = error else {
8265            panic!("expected typed replay failure");
8266        };
8267        assert_eq!(failure.reason, "durable_command_sequence_mismatch");
8268        assert_eq!(failure.sequence, Some(99));
8269        assert_eq!(failure.expected.as_deref(), Some("1"));
8270        assert_eq!(failure.actual.as_deref(), Some("99"));
8271    }
8272
8273    #[test]
8274    fn workflow_sleep_rejects_unrepresentable_rounded_duration() {
8275        let ctx = workflow_context(Vec::new());
8276        let mut sleep = Box::pin(ctx.start_timer(Duration::new(u64::MAX, 1)));
8277        let mut task_context = TaskContext::from_waker(noop_waker_ref());
8278        assert!(matches!(
8279            sleep.as_mut().poll(&mut task_context),
8280            Poll::Ready(Err(Error::TimerDurationOverflow))
8281        ));
8282        assert!(ctx.take_commands().expect("commands").is_empty());
8283    }
8284
8285    #[test]
8286    fn workflow_task_replay_completes_without_rescheduling_recorded_commands() {
8287        let client = Client::new("http://127.0.0.1:8080").expect("client");
8288        let mut worker = Worker::new(client, "rust-workers");
8289        worker.register_workflow("rust.timer", |ctx, _input| async move {
8290            ctx.sleep(Duration::from_secs(5)).await?;
8291            ctx.activity("after-timer", json!([])).await
8292        });
8293
8294        let task = |history_events| WorkflowTask {
8295            task_id: "wft-rust-timer-1".to_string(),
8296            workflow_id: Some("wf-rust-timer".to_string()),
8297            run_id: Some("run-rust-timer".to_string()),
8298            workflow_type: "rust.timer".to_string(),
8299            payload_codec: JSON_CODEC.to_string(),
8300            arguments: Some(json!({"codec": "json", "blob": "[]"})),
8301            history_events,
8302            total_history_events: None,
8303            next_history_page_token: None,
8304            workflow_task_attempt: 1,
8305            workflow_signal_id: None,
8306            signal_name: None,
8307            signal_arguments: None,
8308            lease_owner: Some("rust-worker".to_string()),
8309        };
8310
8311        let initial = worker
8312            .execute_workflow_task(task(Vec::new()))
8313            .expect("initial timer task");
8314        assert_eq!(
8315            initial,
8316            vec![json!({"type": "start_timer", "delay_seconds": 5})]
8317        );
8318
8319        let replayed = worker
8320            .execute_workflow_task(task(vec![
8321                history_event(
8322                    "TimerScheduled",
8323                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8324                ),
8325                history_event(
8326                    "TimerFired",
8327                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8328                ),
8329                history_event(
8330                    "ActivityCompleted",
8331                    json!({
8332                        "sequence": 2,
8333                        "activity_type": "after-timer",
8334                        "payload_codec": "json",
8335                        "result": {"codec": "json", "blob": "\"done\""},
8336                    }),
8337                ),
8338            ]))
8339            .expect("replayed workflow task");
8340        assert_eq!(replayed.len(), 1);
8341        assert_eq!(replayed[0]["type"], "complete_workflow");
8342        assert_eq!(
8343            decode_wire_value(&replayed[0]["result"], JSON_CODEC).expect("result"),
8344            json!("done")
8345        );
8346    }
8347
8348    #[test]
8349    fn uncaught_workflow_handler_error_emits_terminal_failure_command() {
8350        let client = Client::new("http://127.0.0.1:8080").expect("client");
8351        let mut worker = Worker::new(client, "rust-workers");
8352        worker.register_workflow("rust.failing", |_ctx, _input| async move {
8353            Err(Error::Codec("rust_conformance_failure".to_string()))
8354        });
8355        let task = WorkflowTask {
8356            task_id: "wft-rust-failing-1".to_string(),
8357            workflow_id: Some("wf-rust-failing".to_string()),
8358            run_id: Some("run-rust-failing".to_string()),
8359            workflow_type: "rust.failing".to_string(),
8360            payload_codec: JSON_CODEC.to_string(),
8361            arguments: Some(encode_value_envelope(&json!([]), JSON_CODEC).expect("input")),
8362            history_events: Vec::new(),
8363            total_history_events: Some(0),
8364            next_history_page_token: None,
8365            workflow_task_attempt: 1,
8366            workflow_signal_id: None,
8367            signal_name: None,
8368            signal_arguments: None,
8369            lease_owner: Some("rust-worker".to_string()),
8370        };
8371
8372        let commands = worker
8373            .execute_workflow_task(task)
8374            .expect("handler failure becomes a workflow command");
8375
8376        assert_eq!(commands.len(), 1);
8377        assert_eq!(commands[0]["type"], "fail_workflow");
8378        assert_eq!(commands[0]["exception_type"], "RustWorkflowError");
8379        assert_eq!(commands[0]["exception_class"], "durable_workflow::Error");
8380        assert_eq!(commands[0]["non_retryable"], false);
8381        assert_eq!(
8382            commands[0]["message"],
8383            "codec error: rust_conformance_failure"
8384        );
8385        assert_eq!(
8386            commands[0]["exception"]["message"],
8387            "codec error: rust_conformance_failure"
8388        );
8389    }
8390
8391    #[test]
8392    fn ordinary_handler_error_preserves_commands_queued_in_the_same_decision() {
8393        let client = Client::new("http://127.0.0.1:8080").expect("client");
8394        let mut worker = Worker::new(client, "rust-workers");
8395        worker.register_workflow("rust.failing-after-side-effect", |ctx, _input| async move {
8396            let _: String = ctx.side_effect(|| "captured".to_string())?;
8397            Err(Error::WorkerLoop("application failure".to_string()))
8398        });
8399
8400        let commands = worker
8401            .execute_workflow_task(workflow_task(
8402                "rust.failing-after-side-effect",
8403                Vec::new(),
8404                JSON_CODEC,
8405            ))
8406            .expect("ordinary failure remains a workflow decision");
8407
8408        assert_eq!(commands.len(), 2);
8409        assert_eq!(commands[0]["type"], "record_side_effect");
8410        assert_eq!(commands[1]["type"], "fail_workflow");
8411    }
8412
8413    #[test]
8414    fn handler_error_cannot_hide_an_unconsumed_committed_side_effect() {
8415        let client = Client::new("http://127.0.0.1:8080").expect("client");
8416        let mut worker = Worker::new(client, "rust-workers");
8417        worker.register_workflow("rust.removed-side-effect", |_ctx, _input| async move {
8418            Err(Error::WorkerLoop("application failure".to_string()))
8419        });
8420        let result =
8421            encode_value_envelope(&json!("committed"), JSON_CODEC).expect("side-effect result");
8422
8423        let error = worker
8424            .execute_workflow_task(workflow_task(
8425                "rust.removed-side-effect",
8426                vec![history_event(
8427                    "SideEffectRecorded",
8428                    json!({"sequence": 1, "result": result}),
8429                )],
8430                JSON_CODEC,
8431            ))
8432            .expect_err("removed committed history must not become fail_workflow");
8433
8434        let Error::NonDeterministicReplay(failure) = error else {
8435            panic!("expected typed replay failure");
8436        };
8437        assert_eq!(failure.reason, "recorded_commands_unconsumed");
8438        assert_eq!(failure.sequence, Some(1));
8439        assert_eq!(failure.expected.as_deref(), Some("side effect"));
8440    }
8441
8442    #[test]
8443    fn replay_error_discards_side_effect_queued_before_incompatible_marker_check() {
8444        let client = Client::new("http://127.0.0.1:8080").expect("client");
8445        let mut worker = Worker::new(client, "rust-workers");
8446        worker.register_workflow(
8447            "rust.side-effect-before-marker-error",
8448            |ctx, _input| async move {
8449                assert_eq!(ctx.get_version("restart-safe", 1, 1)?, 1);
8450                let _: String = ctx.side_effect(|| "must-not-commit".to_string())?;
8451                ctx.get_version("restart-safe", 2, 2)?;
8452                Ok(Value::Null)
8453            },
8454        );
8455
8456        let error = worker
8457            .execute_workflow_task(workflow_task(
8458                "rust.side-effect-before-marker-error",
8459                vec![history_event(
8460                    "VersionMarkerRecorded",
8461                    json!({
8462                        "sequence": 1,
8463                        "change_id": "restart-safe",
8464                        "version": 1,
8465                        "min_supported": 1,
8466                        "max_supported": 1,
8467                    }),
8468                )],
8469                JSON_CODEC,
8470            ))
8471            .expect_err("replay error must return no queued workflow commands");
8472
8473        let Error::NonDeterministicReplay(failure) = error else {
8474            panic!("expected typed replay failure");
8475        };
8476        assert_eq!(failure.reason, "version_marker_incompatible_range");
8477        assert_eq!(failure.sequence, Some(1));
8478    }
8479
8480    #[test]
8481    fn workflow_task_replay_keeps_recorded_unfired_timer_pending_without_rescheduling() {
8482        let client = Client::new("http://127.0.0.1:8080").expect("client");
8483        let mut worker = Worker::new(client, "rust-workers");
8484        worker.register_workflow("rust.timer.pending", |ctx, _input| async move {
8485            ctx.sleep(Duration::from_secs(5)).await?;
8486            Ok(json!({"status": "timer fired"}))
8487        });
8488
8489        let task = WorkflowTask {
8490            task_id: "wft-rust-timer-pending".to_string(),
8491            workflow_id: Some("wf-rust-timer".to_string()),
8492            run_id: Some("run-rust-timer".to_string()),
8493            workflow_type: "rust.timer.pending".to_string(),
8494            payload_codec: JSON_CODEC.to_string(),
8495            arguments: Some(json!({"codec": "json", "blob": "[]"})),
8496            history_events: vec![history_event(
8497                "TimerScheduled",
8498                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8499            )],
8500            total_history_events: Some(1),
8501            next_history_page_token: None,
8502            workflow_task_attempt: 1,
8503            workflow_signal_id: None,
8504            signal_name: None,
8505            signal_arguments: None,
8506            lease_owner: Some("rust-worker".to_string()),
8507        };
8508
8509        for _redelivery_or_restart in 0..2 {
8510            let commands = worker
8511                .execute_workflow_task(task.clone())
8512                .expect("recorded timer remains pending");
8513            assert!(
8514                commands.is_empty(),
8515                "recorded timer must not be rescheduled"
8516            );
8517        }
8518    }
8519
8520    #[test]
8521    fn workflow_task_rejects_recorded_command_removed_from_workflow_code() {
8522        let client = Client::new("http://127.0.0.1:8080").expect("client");
8523        let mut worker = Worker::new(client, "rust-workers");
8524        worker.register_workflow("rust.timer.removed", |_ctx, _input| async move {
8525            Ok(json!({"status": "completed"}))
8526        });
8527        let task = WorkflowTask {
8528            task_id: "wft-rust-timer-removed".to_string(),
8529            workflow_id: Some("wf-rust-timer".to_string()),
8530            run_id: Some("run-rust-timer".to_string()),
8531            workflow_type: "rust.timer.removed".to_string(),
8532            payload_codec: JSON_CODEC.to_string(),
8533            arguments: Some(json!({"codec": "json", "blob": "[]"})),
8534            history_events: vec![
8535                history_event(
8536                    "TimerScheduled",
8537                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8538                ),
8539                history_event(
8540                    "TimerFired",
8541                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8542                ),
8543            ],
8544            total_history_events: Some(2),
8545            next_history_page_token: None,
8546            workflow_task_attempt: 1,
8547            workflow_signal_id: None,
8548            signal_name: None,
8549            signal_arguments: None,
8550            lease_owner: Some("rust-worker".to_string()),
8551        };
8552
8553        let Error::NonDeterministicReplay(failure) = worker
8554            .execute_workflow_task(task)
8555            .expect_err("removed timer must fail replay")
8556        else {
8557            panic!("expected typed replay failure");
8558        };
8559        assert_eq!(failure.reason, "recorded_commands_unconsumed");
8560        assert_eq!(failure.sequence, Some(1));
8561    }
8562
8563    #[test]
8564    fn workflow_context_emits_explicit_child_workflow_contract() {
8565        let ctx = WorkflowContext {
8566            state: Arc::new(Mutex::new(
8567                WorkflowState::new_with_identity(
8568                    Vec::new(),
8569                    Some("wf-parent".to_string()),
8570                    Some("run-parent".to_string()),
8571                    "parent-workers".to_string(),
8572                    JSON_CODEC.to_string(),
8573                    None,
8574                )
8575                .expect("workflow state"),
8576            )),
8577        };
8578        let options = ChildWorkflowOptions::new("python-workers")
8579            .parent_close_policy(ParentClosePolicy::RequestCancel)
8580            .retry_policy(ChildWorkflowRetryPolicy {
8581                max_attempts: Some(3),
8582                backoff_seconds: vec![1, 5],
8583                non_retryable_error_types: vec!["ValidationError".to_string()],
8584            })
8585            .execution_timeout_seconds(600)
8586            .run_timeout_seconds(120);
8587        let mut call = Box::pin(ctx.start_child_workflow(
8588            "python.fulfil-order",
8589            options,
8590            json!([{"order_id": "order-42"}]),
8591        ));
8592        let mut task_context = TaskContext::from_waker(noop_waker_ref());
8593
8594        assert!(matches!(
8595            call.as_mut().poll(&mut task_context),
8596            Poll::Pending
8597        ));
8598        let commands = ctx.take_commands().expect("commands");
8599        assert_eq!(commands.len(), 1);
8600        let command = &commands[0];
8601        assert_eq!(command["type"], "start_child_workflow");
8602        assert_eq!(command["workflow_type"], "python.fulfil-order");
8603        assert_eq!(command["queue"], "python-workers");
8604        assert_eq!(command["parent_close_policy"], "request_cancel");
8605        assert_eq!(command["retry_policy"]["max_attempts"], 3);
8606        assert_eq!(command["execution_timeout_seconds"], 600);
8607        assert_eq!(command["run_timeout_seconds"], 120);
8608        assert_eq!(
8609            decode_wire_value(&command["arguments"], JSON_CODEC).expect("child args"),
8610            json!([{"order_id": "order-42"}])
8611        );
8612    }
8613
8614    fn child_parent_worker() -> Worker {
8615        let client = Client::new("http://127.0.0.1:8080").expect("client");
8616        let mut worker = Worker::new(client, "rust-parent-workers");
8617        worker.register_workflow("rust.parent", |ctx, _input| async move {
8618            let child = ctx
8619                .start_child_workflow(
8620                    "python.child",
8621                    ChildWorkflowOptions::new("python-child-workers")
8622                        .parent_close_policy(ParentClosePolicy::Terminate),
8623                    json!([{"codec_probe": [1, true, "rust"]}]),
8624                )
8625                .await?;
8626            Ok(json!({
8627                "parent_workflow_id": child.parent.workflow_id,
8628                "parent_run_id": child.parent.run_id,
8629                "child_workflow_id": child.child.workflow_id,
8630                "child_run_id": child.child.run_id,
8631                "child_workflow_type": child.child_workflow_type,
8632                "result": child.result,
8633            }))
8634        });
8635        worker
8636    }
8637
8638    fn child_parent_task(event_type: &str, payload: Value) -> WorkflowTask {
8639        WorkflowTask {
8640            task_id: "wft-child-parent".to_string(),
8641            workflow_id: Some("wf-parent".to_string()),
8642            run_id: Some("run-parent".to_string()),
8643            workflow_type: "rust.parent".to_string(),
8644            payload_codec: JSON_CODEC.to_string(),
8645            arguments: Some(encode_value_envelope(&json!([]), JSON_CODEC).expect("input")),
8646            history_events: vec![
8647                HistoryEvent {
8648                    event_type: "ChildWorkflowScheduled".to_string(),
8649                    payload: json!({
8650                        "sequence": 1,
8651                        "child_call_id": "call-child",
8652                        "child_workflow_instance_id": "wf-child",
8653                        "child_workflow_run_id": "run-child",
8654                        "child_workflow_type": "python.child",
8655                    }),
8656                    raw: HashMap::new(),
8657                },
8658                HistoryEvent {
8659                    event_type: event_type.to_string(),
8660                    payload,
8661                    raw: HashMap::new(),
8662                },
8663            ],
8664            total_history_events: Some(2),
8665            next_history_page_token: None,
8666            workflow_task_attempt: 1,
8667            workflow_signal_id: None,
8668            signal_name: None,
8669            signal_arguments: None,
8670            lease_owner: Some("rust-worker".to_string()),
8671        }
8672    }
8673
8674    #[test]
8675    fn committed_child_result_replays_without_starting_a_duplicate() {
8676        let worker = child_parent_worker();
8677        let task = child_parent_task(
8678            "ChildRunCompleted",
8679            json!({
8680                "sequence": 1,
8681                "child_call_id": "call-child",
8682                "child_workflow_instance_id": "wf-child",
8683                "child_workflow_run_id": "run-child",
8684                "child_workflow_type": "python.child",
8685                "payload_codec": "json",
8686                "result": {"codec": "json", "blob": "{\"from\":\"python\",\"ok\":true}"},
8687            }),
8688        );
8689
8690        for _restart in 0..2 {
8691            let commands = worker
8692                .execute_workflow_task(task.clone())
8693                .expect("replayed parent task");
8694            assert_eq!(commands.len(), 1);
8695            assert_eq!(commands[0]["type"], "complete_workflow");
8696            assert!(!commands
8697                .iter()
8698                .any(|command| command["type"] == "start_child_workflow"));
8699            let output =
8700                decode_wire_value(&commands[0]["result"], JSON_CODEC).expect("parent output");
8701            assert_eq!(output["parent_workflow_id"], "wf-parent");
8702            assert_eq!(output["parent_run_id"], "run-parent");
8703            assert_eq!(output["child_workflow_id"], "wf-child");
8704            assert_eq!(output["child_run_id"], "run-child");
8705            assert_eq!(output["result"], json!({"from": "python", "ok": true}));
8706        }
8707    }
8708
8709    #[test]
8710    fn pending_child_replays_after_restart_without_starting_a_duplicate() {
8711        let worker = child_parent_worker();
8712        let mut task = child_parent_task("unused", Value::Null);
8713        task.history_events.truncate(1);
8714        task.total_history_events = Some(1);
8715
8716        for _redelivery_or_restart in 0..2 {
8717            let commands = worker
8718                .execute_workflow_task(task.clone())
8719                .expect("recorded child remains pending");
8720            assert!(
8721                commands.is_empty(),
8722                "recorded pending child must not be started again"
8723            );
8724        }
8725    }
8726
8727    #[test]
8728    fn child_cancellation_becomes_stable_parent_failure_command() {
8729        let worker = child_parent_worker();
8730        let task = child_parent_task(
8731            "ChildRunCancelled",
8732            json!({
8733                "sequence": 1,
8734                "child_workflow_instance_id": "wf-child",
8735                "child_workflow_run_id": "run-child",
8736                "child_workflow_type": "python.child",
8737                "failure_id": "failure-child",
8738                "failure_category": "cancelled",
8739                "message": "cancelled by parent-close policy",
8740            }),
8741        );
8742
8743        let commands = worker
8744            .execute_workflow_task(task)
8745            .expect("parent settlement");
8746        assert_eq!(commands.len(), 1);
8747        assert_eq!(commands[0]["type"], "fail_workflow");
8748        assert_eq!(commands[0]["exception_type"], "ChildWorkflowCancelled");
8749        assert_eq!(
8750            commands[0]["exception"]["properties"]["reason"],
8751            "cancelled"
8752        );
8753        assert_eq!(
8754            commands[0]["exception"]["properties"]["child_workflow_run_id"],
8755            "run-child"
8756        );
8757    }
8758
8759    #[test]
8760    fn workflow_can_handle_typed_child_failure() {
8761        let client = Client::new("http://127.0.0.1:8080").expect("client");
8762        let mut worker = Worker::new(client, "rust-parent-workers");
8763        worker.register_workflow("rust.handled-parent", |ctx, _input| async move {
8764            match ctx
8765                .start_child_workflow(
8766                    "python.child",
8767                    ChildWorkflowOptions::new("python-child-workers"),
8768                    json!([]),
8769                )
8770                .await
8771            {
8772                Err(Error::ChildWorkflowFailed(failure)) => Ok(json!({
8773                    "reason": failure.reason,
8774                    "failure_id": failure.failure_id,
8775                    "exception_class": failure.exception_class,
8776                    "child_run_id": failure.child_workflow_run_id,
8777                })),
8778                Err(error) => Err(error),
8779                Ok(_) => Err(Error::WorkerLoop(
8780                    "child unexpectedly succeeded".to_string(),
8781                )),
8782            }
8783        });
8784        let mut task = child_parent_task(
8785            "ChildRunFailed",
8786            json!({
8787                "sequence": 1,
8788                "child_workflow_instance_id": "wf-child",
8789                "child_workflow_run_id": "run-child",
8790                "child_workflow_type": "python.child",
8791                "failure_id": "failure-child",
8792                "failure_category": "child_workflow",
8793                "message": "payment rejected",
8794                "exception": {
8795                    "type": "PaymentRejected",
8796                    "class": "payments.PaymentRejected",
8797                    "message": "payment rejected"
8798                }
8799            }),
8800        );
8801        task.workflow_type = "rust.handled-parent".to_string();
8802
8803        let commands = worker.execute_workflow_task(task).expect("handled failure");
8804        assert_eq!(commands[0]["type"], "complete_workflow");
8805        let output = decode_wire_value(&commands[0]["result"], JSON_CODEC).expect("parent output");
8806        assert_eq!(output["reason"], "child_workflow");
8807        assert_eq!(output["failure_id"], "failure-child");
8808        assert_eq!(output["exception_class"], "payments.PaymentRejected");
8809        assert_eq!(output["child_run_id"], "run-child");
8810    }
8811
8812    #[test]
8813    fn rust_hello_world_uses_signal_arguments_from_resume_payload() {
8814        let client = Client::new("http://127.0.0.1:8080").expect("client");
8815        let mut worker = Worker::new(client, "rust-workers");
8816
8817        worker.register_workflow("rust.hello_workflow", |ctx, _input| async move {
8818            let signal = ctx.wait_signal("start").await?;
8819            let name = signal
8820                .first()
8821                .and_then(|value| value.as_str())
8822                .unwrap_or("world");
8823            let greeting = ctx.activity("rust.hello_activity", json!([name])).await?;
8824            Ok(json!({
8825                "greeting": greeting,
8826                "language": "rust"
8827            }))
8828        });
8829
8830        let signal_arguments =
8831            encode_value_envelope(&json!(["Rust"]), DEFAULT_CODEC).expect("signal arguments");
8832        let task = WorkflowTask {
8833            task_id: "wft-rust-signal-1".to_string(),
8834            workflow_id: Some("wf-rust-hello".to_string()),
8835            run_id: Some("run-rust-hello".to_string()),
8836            workflow_type: "rust.hello_workflow".to_string(),
8837            payload_codec: DEFAULT_CODEC.to_string(),
8838            arguments: Some(encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("input")),
8839            history_events: vec![HistoryEvent {
8840                event_type: "SignalReceived".to_string(),
8841                payload: json!({
8842                    "signal_id": "sig-rust-1",
8843                    "signal_name": "start"
8844                }),
8845                raw: HashMap::new(),
8846            }],
8847            total_history_events: Some(1),
8848            next_history_page_token: None,
8849            workflow_task_attempt: 1,
8850            workflow_signal_id: Some("sig-rust-1".to_string()),
8851            signal_name: Some("start".to_string()),
8852            signal_arguments: Some(signal_arguments),
8853            lease_owner: Some("rust-worker".to_string()),
8854        };
8855
8856        let commands = worker.execute_workflow_task(task).expect("workflow task");
8857
8858        assert_eq!(commands.len(), 1);
8859        assert_eq!(commands[0]["type"], "schedule_activity");
8860        assert_eq!(commands[0]["activity_type"], "rust.hello_activity");
8861        assert_eq!(
8862            decode_wire_value(&commands[0]["arguments"], DEFAULT_CODEC).expect("activity args"),
8863            json!(["Rust"])
8864        );
8865    }
8866
8867    #[test]
8868    fn workflow_task_appends_paginated_history_events() {
8869        let mut task = WorkflowTask {
8870            task_id: "wft-rust-pages-1".to_string(),
8871            workflow_id: Some("wf-rust-pages".to_string()),
8872            run_id: Some("run-rust-pages".to_string()),
8873            workflow_type: "rust.hello_workflow".to_string(),
8874            payload_codec: DEFAULT_CODEC.to_string(),
8875            arguments: Some(encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("input")),
8876            history_events: vec![HistoryEvent {
8877                event_type: "WorkflowStarted".to_string(),
8878                payload: json!({}),
8879                raw: HashMap::new(),
8880            }],
8881            total_history_events: Some(3),
8882            next_history_page_token: Some("MQ==".to_string()),
8883            workflow_task_attempt: 1,
8884            workflow_signal_id: None,
8885            signal_name: None,
8886            signal_arguments: None,
8887            lease_owner: Some("rust-worker".to_string()),
8888        };
8889
8890        task.append_history_page(WorkflowTaskHistoryPage {
8891            history_events: vec![
8892                HistoryEvent {
8893                    event_type: "SignalReceived".to_string(),
8894                    payload: json!({
8895                        "signal_id": "sig-rust-1",
8896                        "signal_name": "start",
8897                        "arguments": encode_value_envelope(&json!(["Rust"]), DEFAULT_CODEC)
8898                            .expect("signal arguments")
8899                    }),
8900                    raw: HashMap::new(),
8901                },
8902                HistoryEvent {
8903                    event_type: "MarkerRecorded".to_string(),
8904                    payload: json!({"sequence": 3}),
8905                    raw: HashMap::new(),
8906                },
8907            ],
8908            total_history_events: Some(3),
8909            next_history_page_token: None,
8910        });
8911
8912        assert_eq!(task.history_events.len(), 3);
8913        assert_eq!(task.total_history_events, Some(3));
8914        assert_eq!(task.next_history_page_token, None);
8915
8916        let signals =
8917            signal_values(&task.history_events, "start", DEFAULT_CODEC, None).expect("signals");
8918        assert_eq!(signals, vec![vec![json!("Rust")]]);
8919    }
8920
8921    #[tokio::test]
8922    async fn query_handler_reads_ordered_cross_codec_signals_without_commands() {
8923        let client = Client::new("http://127.0.0.1:8080").expect("client");
8924        let mut worker = Worker::new(client, "rust-workers");
8925        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
8926        worker.register_query("counter", "current", |ctx, _args| async move {
8927            let mut count = 0_i64;
8928            for signal in ctx.signal_events() {
8929                let value = signal
8930                    .arguments
8931                    .first()
8932                    .and_then(Value::as_i64)
8933                    .unwrap_or_default();
8934                match signal.name.as_str() {
8935                    "increment" => count += value,
8936                    "set" => count = value,
8937                    _ => {}
8938                }
8939            }
8940            Ok(json!(count))
8941        });
8942
8943        let task = QueryTask {
8944            query_task_id: "query-rust-counter".to_string(),
8945            query_task_attempt: 1,
8946            lease_owner: Some("rust-worker".to_string()),
8947            workflow_id: Some("counter-1".to_string()),
8948            run_id: Some("run-counter-1".to_string()),
8949            workflow_type: "counter".to_string(),
8950            query_name: "current".to_string(),
8951            payload_codec: DEFAULT_CODEC.to_string(),
8952            workflow_arguments: Some(
8953                encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("workflow input"),
8954            ),
8955            query_arguments: Some(
8956                encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("query arguments"),
8957            ),
8958            history_events: vec![
8959                HistoryEvent {
8960                    event_type: "SignalReceived".to_string(),
8961                    payload: json!({
8962                        "signal_id": "php-signal-1",
8963                        "signal_name": "increment",
8964                        "workflow_sequence": 1,
8965                        "payload_codec": DEFAULT_CODEC,
8966                        "arguments": encode_value_envelope(&json!([3]), DEFAULT_CODEC).expect("php avro signal")
8967                    }),
8968                    raw: HashMap::new(),
8969                },
8970                HistoryEvent {
8971                    event_type: "SignalReceived".to_string(),
8972                    payload: json!({
8973                        "signal_id": "python-signal-2",
8974                        "signal_name": "increment",
8975                        "workflow_sequence": 2,
8976                        "payload_codec": JSON_CODEC,
8977                        "arguments": encode_value_envelope(&json!([5]), JSON_CODEC).expect("python json signal")
8978                    }),
8979                    raw: HashMap::new(),
8980                },
8981                HistoryEvent {
8982                    event_type: "SignalReceived".to_string(),
8983                    payload: json!({
8984                        "signal_id": "rust-signal-3",
8985                        "signal_name": "set",
8986                        "workflow_sequence": 3,
8987                        "payload_codec": DEFAULT_CODEC,
8988                        "arguments": encode_value_envelope(&json!([0]), DEFAULT_CODEC).expect("rust avro signal")
8989                    }),
8990                    raw: HashMap::new(),
8991                },
8992            ],
8993            history_export: None,
8994            run_status: Some("completed".to_string()),
8995        };
8996
8997        let result = worker.execute_query_task(task).await.expect("query result");
8998        assert_eq!(result, json!(0));
8999    }
9000
9001    #[tokio::test]
9002    async fn replayed_queries_read_running_completed_and_cold_restarted_instance_state() {
9003        let worker = replay_counter_worker();
9004        let running_history = json!([
9005            {
9006                "type": "ActivityCompleted",
9007                "payload": {
9008                    "sequence": 1,
9009                    "activity_type": "load-counter",
9010                    "payload_codec": "json",
9011                    "result": {"codec": "json", "blob": "\"loaded\""}
9012                }
9013            },
9014            {
9015                "type": "SignalReceived",
9016                "payload": {
9017                    "signal_id": "signal-3",
9018                    "signal_name": "increment",
9019                    "payload_codec": "json",
9020                    "arguments": {"codec": "json", "blob": "[3]"}
9021                }
9022            }
9023        ]);
9024
9025        let running = worker
9026            .execute_query_task(replay_counter_query(
9027                "current",
9028                running_history.clone(),
9029                "running",
9030            ))
9031            .await
9032            .expect("running replay query");
9033        assert_eq!(
9034            running,
9035            json!({"loaded": "loaded", "count": 3, "finished": false})
9036        );
9037
9038        let detached = worker
9039            .execute_query_task(replay_counter_query(
9040                "detached-mutation",
9041                running_history.clone(),
9042                "running",
9043            ))
9044            .await
9045            .expect("query mutates only its detached state clone");
9046        assert_eq!(detached, json!(999));
9047        let failed = worker
9048            .execute_query_task(replay_counter_query(
9049                "failed-mutation",
9050                running_history.clone(),
9051                "running",
9052            ))
9053            .await
9054            .expect_err("failed query");
9055        assert_eq!(failed.reason, "query_rejected");
9056        let unchanged = worker
9057            .execute_query_task(replay_counter_query("current", running_history, "running"))
9058            .await
9059            .expect("later query reconstructs unchanged state");
9060        assert_eq!(unchanged, running);
9061
9062        let restarted_worker = replay_counter_worker();
9063        let restarted_task: QueryTask = serde_json::from_value(json!({
9064            "query_task_id": "query-after-restart",
9065            "workflow_id": "counter-1",
9066            "run_id": "run-counter-1",
9067            "workflow_type": "replay-counter",
9068            "query_name": "current",
9069            "payload_codec": "json",
9070            "workflow_arguments": {"codec": "json", "blob": "[]"},
9071            "query_arguments": {"codec": "json", "blob": "[]"},
9072            "history_events": [],
9073            "history_export": {
9074                "payloads": {"codec": "json"},
9075                "history_events": [
9076                    {
9077                        "type": "ActivityCompleted",
9078                        "payload": {
9079                            "sequence": 1,
9080                            "activity_type": "load-counter",
9081                            "payload_codec": "json",
9082                            "result": null
9083                        }
9084                    },
9085                    {
9086                        "type": "SignalReceived",
9087                        "payload": {"signal_id": "signal-3", "signal_name": "increment"}
9088                    },
9089                    {
9090                        "type": "SignalReceived",
9091                        "payload": {"signal_id": "signal-5", "signal_name": "increment"}
9092                    }
9093                ],
9094                "activities": [{
9095                    "sequence": 1,
9096                    "activity_type": "load-counter",
9097                    "payload_codec": "json",
9098                    "result": {"codec": "json", "blob": "\"loaded\""}
9099                }],
9100                "signals": [
9101                    {
9102                        "id": "signal-3",
9103                        "name": "increment",
9104                        "payload_codec": "json",
9105                        "arguments": "[3]"
9106                    },
9107                    {
9108                        "id": "signal-5",
9109                        "name": "increment",
9110                        "payload_codec": "json",
9111                        "arguments": "[5]"
9112                    }
9113                ]
9114            },
9115            "run_status": "completed"
9116        }))
9117        .expect("cold replay query task");
9118        let completed = restarted_worker
9119            .execute_query_task(restarted_task)
9120            .await
9121            .expect("completed cold replay query");
9122        assert_eq!(
9123            completed,
9124            json!({"loaded": "loaded", "count": 8, "finished": true})
9125        );
9126    }
9127
9128    #[tokio::test]
9129    async fn replayed_query_replay_failures_are_machine_readable() {
9130        let worker = replay_counter_worker();
9131        let task = replay_counter_query(
9132            "current",
9133            json!([{
9134                "type": "ActivityCompleted",
9135                "payload": {
9136                    "sequence": 1,
9137                    "payload_codec": "json",
9138                    "result": {"codec": "json", "blob": "{"}
9139                }
9140            }]),
9141            "running",
9142        );
9143        let failure = worker
9144            .execute_query_task(task)
9145            .await
9146            .expect_err("invalid replay history payload");
9147        assert_eq!(failure.reason, "query_workflow_state_unavailable");
9148        assert_eq!(failure.failure_type, "QueryWorkflowStateUnavailable");
9149    }
9150
9151    #[tokio::test]
9152    async fn query_task_restores_compact_history_from_export() {
9153        let client = Client::new("http://127.0.0.1:8080").expect("client");
9154        let mut worker = Worker::new(client, "rust-workers");
9155        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
9156        worker.register_query("counter", "current", |ctx, _args| async move {
9157            Ok(json!(ctx.signals("increment")[0][0]))
9158        });
9159        let task: QueryTask = serde_json::from_value(json!({
9160            "query_task_id": "query-export",
9161            "workflow_type": "counter",
9162            "query_name": "current",
9163            "payload_codec": "json",
9164            "workflow_arguments": {"codec": "json", "blob": "[]"},
9165            "query_arguments": {"codec": "json", "blob": "[]"},
9166            "history_events": [],
9167            "history_export": {
9168                "payloads": {"codec": "json"},
9169                "history_events": [{
9170                    "type": "SignalReceived",
9171                    "payload": {"signal_id": "signal-export", "signal_name": "increment"}
9172                }],
9173                "signals": [{
9174                    "id": "signal-export",
9175                    "name": "increment",
9176                    "status": "applied",
9177                    "workflow_sequence": 1,
9178                    "payload_codec": "json",
9179                    "arguments": "[9]"
9180                }]
9181            }
9182        }))
9183        .expect("query task");
9184
9185        let result = worker.execute_query_task(task).await.expect("query result");
9186        assert_eq!(result, json!(9));
9187    }
9188
9189    #[tokio::test]
9190    async fn query_task_failures_have_stable_reasons() {
9191        let client = Client::new("http://127.0.0.1:8080").expect("client");
9192        let mut worker = Worker::new(client, "rust-workers");
9193        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
9194        worker.register_query(
9195            "counter",
9196            "current",
9197            |_ctx, _args| async move { Ok(json!(0)) },
9198        );
9199
9200        let base_task = QueryTask {
9201            query_task_id: "query-errors".to_string(),
9202            query_task_attempt: 1,
9203            lease_owner: None,
9204            workflow_id: Some("counter-errors".to_string()),
9205            run_id: Some("run-errors".to_string()),
9206            workflow_type: "counter".to_string(),
9207            query_name: "missing".to_string(),
9208            payload_codec: JSON_CODEC.to_string(),
9209            workflow_arguments: Some(json!({"codec": "json", "blob": "[]"})),
9210            query_arguments: Some(json!({"codec": "json", "blob": "[]"})),
9211            history_events: Vec::new(),
9212            history_export: None,
9213            run_status: Some("running".to_string()),
9214        };
9215
9216        let unknown = worker
9217            .execute_query_task(base_task.clone())
9218            .await
9219            .expect_err("unknown query");
9220        assert_eq!(unknown.reason, "rejected_unknown_query");
9221
9222        let mut malformed = base_task;
9223        malformed.query_name = "current".to_string();
9224        malformed.query_arguments = Some(json!({"codec": "json", "blob": "{"}));
9225        let malformed = worker
9226            .execute_query_task(malformed)
9227            .await
9228            .expect_err("malformed payload");
9229        assert_eq!(malformed.reason, "query_payload_decode_failed");
9230
9231        let client = Client::new("http://127.0.0.1:8080").expect("client");
9232        let mut unavailable_worker = Worker::new(client, "rust-workers");
9233        unavailable_worker
9234            .register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
9235        let unavailable_task: QueryTask = serde_json::from_value(json!({
9236            "query_task_id": "query-unavailable",
9237            "workflow_type": "counter",
9238            "query_name": "current",
9239            "payload_codec": "json",
9240            "workflow_arguments": {"codec": "json", "blob": "[]"},
9241            "query_arguments": {"codec": "json", "blob": "[]"}
9242        }))
9243        .expect("query task");
9244        let unavailable = unavailable_worker
9245            .execute_query_task(unavailable_task)
9246            .await
9247            .expect_err("query handler unavailable");
9248        assert_eq!(unavailable.reason, "query_handler_unavailable");
9249    }
9250
9251    #[tokio::test]
9252    async fn client_query_decodes_result_and_typed_failure() {
9253        let server = MockWorkerServer::start();
9254        let client = Client::builder(server.base_url())
9255            .timeout(Duration::from_secs(2))
9256            .build()
9257            .expect("client");
9258
9259        let result = client
9260            .query_workflow("counter-1", "current", json!([]))
9261            .await
9262            .expect("query result");
9263        assert_eq!(result, json!({"count": 8}));
9264
9265        let error = client
9266            .query_workflow("counter-1", "missing", json!([]))
9267            .await
9268            .expect_err("unknown query");
9269        let Error::QueryFailed(failure) = error else {
9270            panic!("expected typed query failure");
9271        };
9272        assert_eq!(failure.status, 404);
9273        assert_eq!(failure.reason, "rejected_unknown_query");
9274    }
9275
9276    #[tokio::test]
9277    async fn lifecycle_commands_support_instance_and_selected_run_targets() {
9278        let server = MockWorkerServer::start();
9279        let client = Client::builder(server.base_url())
9280            .timeout(Duration::from_secs(2))
9281            .build()
9282            .expect("client");
9283
9284        let options = WorkflowCommandOptions::new()
9285            .reason("cleanup requested")
9286            .request_id("cancel-17");
9287        let cancelled = client
9288            .cancel_workflow("wf-lifecycle", options)
9289            .await
9290            .expect("instance cancellation");
9291        assert_eq!(cancelled.command, WorkflowCommandKind::Cancel);
9292        assert_eq!(cancelled.run_id.as_deref(), Some("run-current"));
9293        assert_eq!(cancelled.outcome.as_deref(), Some("cancelled"));
9294        assert_eq!(
9295            server.request_body("/api/workflows/wf-lifecycle/cancel"),
9296            json!({"reason":"cleanup requested","request_id":"cancel-17"})
9297        );
9298
9299        let terminated = client
9300            .terminate_workflow(
9301                "wf-lifecycle",
9302                WorkflowCommandOptions::new().reason("forced stop"),
9303            )
9304            .await
9305            .expect("instance termination");
9306        assert_eq!(terminated.command, WorkflowCommandKind::Terminate);
9307        assert_eq!(terminated.outcome.as_deref(), Some("terminated"));
9308
9309        client
9310            .cancel_workflow_run(
9311                "wf-lifecycle",
9312                "run-current",
9313                WorkflowCommandOptions::default(),
9314            )
9315            .await
9316            .expect("selected run cancellation");
9317        client
9318            .terminate_workflow_run(
9319                "wf-lifecycle",
9320                "run-current",
9321                WorkflowCommandOptions::default(),
9322            )
9323            .await
9324            .expect("selected run termination");
9325
9326        for (command, error) in [
9327            (
9328                WorkflowCommandKind::Cancel,
9329                client
9330                    .cancel_workflow_run(
9331                        "wf-lifecycle",
9332                        "run-stale",
9333                        WorkflowCommandOptions::default(),
9334                    )
9335                    .await
9336                    .expect_err("stale cancellation must be rejected"),
9337            ),
9338            (
9339                WorkflowCommandKind::Terminate,
9340                client
9341                    .terminate_workflow_run(
9342                        "wf-lifecycle",
9343                        "run-stale",
9344                        WorkflowCommandOptions::default(),
9345                    )
9346                    .await
9347                    .expect_err("stale termination must be rejected"),
9348            ),
9349        ] {
9350            let Error::WorkflowCommandRejected(rejection) = error else {
9351                panic!("expected typed command rejection");
9352            };
9353            assert_eq!(rejection.command, command);
9354            assert_eq!(rejection.status, 409);
9355            assert_eq!(rejection.reason, "historical_run_command_rejected");
9356            assert_eq!(rejection.run_id.as_deref(), Some("run-stale"));
9357            assert_eq!(rejection.target_scope.as_deref(), Some("run"));
9358        }
9359    }
9360
9361    #[tokio::test]
9362    async fn workflow_start_options_send_server_enforced_deadlines() {
9363        let server = MockWorkerServer::start();
9364        let client = Client::builder(server.base_url())
9365            .timeout(Duration::from_secs(2))
9366            .build()
9367            .expect("client");
9368
9369        let handle = client
9370            .start_workflow_with_options(
9371                "rust.timeout",
9372                "rust-timeouts",
9373                "wf-start-options",
9374                WorkflowStartOptions::new()
9375                    .execution_timeout_seconds(30)
9376                    .run_timeout_seconds(1),
9377                json!([]),
9378            )
9379            .await
9380            .expect("workflow start");
9381
9382        assert_eq!(handle.run_id.as_deref(), Some("run-start-options"));
9383        let body = server.request_body("/api/workflows");
9384        assert_eq!(body["execution_timeout_seconds"], 30);
9385        assert_eq!(body["run_timeout_seconds"], 1);
9386
9387        let invalid = client
9388            .start_workflow_with_options(
9389                "rust.timeout",
9390                "rust-timeouts",
9391                "wf-invalid-options",
9392                WorkflowStartOptions::new()
9393                    .execution_timeout_seconds(1)
9394                    .run_timeout_seconds(2),
9395                json!([]),
9396            )
9397            .await
9398            .expect_err("invalid deadline ordering");
9399        assert!(invalid
9400            .to_string()
9401            .contains("run_timeout_seconds cannot exceed execution_timeout_seconds"));
9402    }
9403
9404    #[tokio::test]
9405    async fn workflow_result_returns_each_typed_terminal_outcome() {
9406        let server = MockWorkerServer::start();
9407        let client = Client::builder(server.base_url())
9408            .timeout(Duration::from_secs(2))
9409            .build()
9410            .expect("client");
9411        let options = WorkflowResultOptions {
9412            poll_interval: Duration::ZERO,
9413            timeout: Duration::from_secs(1),
9414        };
9415
9416        let failed = WorkflowHandle {
9417            client: client.clone(),
9418            workflow_id: "wf-failed".to_string(),
9419            run_id: Some("run-failed".to_string()),
9420            workflow_type: "failure".to_string(),
9421        }
9422        .result(options)
9423        .await
9424        .expect_err("failed outcome");
9425        let Error::WorkflowFailed(failure) = failed else {
9426            panic!("expected WorkflowFailed");
9427        };
9428        assert_eq!(failure.workflow_id, "wf-failed");
9429        assert_eq!(failure.run_id.as_deref(), Some("run-failed"));
9430        assert_eq!(failure.failure_id.as_deref(), Some("failure-17"));
9431        assert_eq!(failure.failure_category.as_deref(), Some("application"));
9432        assert_eq!(failure.exception_type.as_deref(), Some("PaymentError"));
9433        assert_eq!(
9434            failure.exception_class.as_deref(),
9435            Some("billing::PaymentError")
9436        );
9437        assert_eq!(failure.non_retryable, Some(true));
9438
9439        for (workflow_id, expected_kind, expected_reason) in [
9440            (
9441                "wf-cancelled",
9442                WorkflowTerminalKind::Cancelled,
9443                "cleanup requested",
9444            ),
9445            (
9446                "wf-terminated",
9447                WorkflowTerminalKind::Terminated,
9448                "forced stop",
9449            ),
9450            (
9451                "wf-timed-out",
9452                WorkflowTerminalKind::TimedOut,
9453                "run_timeout",
9454            ),
9455        ] {
9456            let error = WorkflowHandle {
9457                client: client.clone(),
9458                workflow_id: workflow_id.to_string(),
9459                run_id: None,
9460                workflow_type: "terminal".to_string(),
9461            }
9462            .result(options)
9463            .await
9464            .expect_err("typed terminal outcome");
9465            let outcome = match error {
9466                Error::WorkflowCancelled(outcome) => outcome,
9467                Error::WorkflowTerminated(outcome) => outcome,
9468                Error::WorkflowTimedOut(outcome) => outcome,
9469                other => panic!("unexpected terminal error: {other}"),
9470            };
9471            assert_eq!(outcome.kind, expected_kind);
9472            assert_eq!(outcome.workflow_id, workflow_id);
9473            assert_eq!(outcome.reason, expected_reason);
9474        }
9475
9476        let wait_timeout = WorkflowHandle {
9477            client,
9478            workflow_id: "wf-waiting".to_string(),
9479            run_id: Some("run-waiting".to_string()),
9480            workflow_type: "waiting".to_string(),
9481        }
9482        .result(WorkflowResultOptions {
9483            poll_interval: Duration::ZERO,
9484            timeout: Duration::ZERO,
9485        })
9486        .await
9487        .expect_err("client wait timeout");
9488        let Error::WorkflowTimedOut(timeout) = wait_timeout else {
9489            panic!("expected typed client timeout");
9490        };
9491        assert_eq!(timeout.reason, "result_wait_timeout");
9492        assert_eq!(timeout.failure_category.as_deref(), Some("client_timeout"));
9493        assert_eq!(timeout.run_id.as_deref(), Some("run-waiting"));
9494    }
9495
9496    #[tokio::test]
9497    async fn workflow_result_awaits_the_handle_selected_run() {
9498        let server = MockWorkerServer::start();
9499        let client = Client::builder(server.base_url())
9500            .timeout(Duration::from_secs(2))
9501            .build()
9502            .expect("client");
9503
9504        let error = WorkflowHandle {
9505            client,
9506            workflow_id: "wf-selected".to_string(),
9507            run_id: Some("run-selected".to_string()),
9508            workflow_type: "selected".to_string(),
9509        }
9510        .result(WorkflowResultOptions {
9511            poll_interval: Duration::ZERO,
9512            timeout: Duration::from_secs(1),
9513        })
9514        .await
9515        .expect_err("the selected run is cancelled even though the current run completed");
9516
9517        let Error::WorkflowCancelled(outcome) = error else {
9518            panic!("expected selected run cancellation");
9519        };
9520        assert_eq!(outcome.run_id.as_deref(), Some("run-selected"));
9521        assert_eq!(outcome.reason, "selected run cancelled");
9522        assert_eq!(
9523            server.request_count("/api/workflows/wf-selected/runs/run-selected"),
9524            1
9525        );
9526        assert_eq!(server.request_count("/api/workflows/wf-selected"), 0);
9527    }
9528
9529    #[tokio::test]
9530    async fn poll_responses_decode_http_conflict_drain_as_a_stable_stop() {
9531        let server = MockWorkerServer::draining_polls();
9532        let client = Client::builder(server.base_url())
9533            .timeout(Duration::from_secs(2))
9534            .build()
9535            .expect("client");
9536
9537        let workflow = client
9538            .poll_workflow_task_response("draining-worker", "rust-workers", Duration::ZERO)
9539            .await
9540            .expect("workflow drain response");
9541        let activity = client
9542            .poll_activity_task_response("draining-worker", "rust-workers", Duration::ZERO)
9543            .await
9544            .expect("activity drain response");
9545        let query = client
9546            .poll_query_task_response("draining-worker", "rust-workers", Duration::ZERO)
9547            .await
9548            .expect("query drain response");
9549
9550        for outcome in [workflow.outcome(), activity.outcome(), query.outcome()] {
9551            assert_eq!(
9552                outcome,
9553                WorkerPollOutcome::Stop {
9554                    poll_status: Some("draining".to_string()),
9555                    reason: Some("worker_draining".to_string()),
9556                }
9557            );
9558        }
9559
9560        assert!(client
9561            .poll_workflow_task("draining-worker", "rust-workers", Duration::ZERO)
9562            .await
9563            .expect("compatibility poll")
9564            .is_none());
9565    }
9566
9567    #[tokio::test]
9568    async fn managed_worker_honors_drain_stop_for_every_task_family() {
9569        let server = MockWorkerServer::draining_polls();
9570        let client = Client::builder(server.base_url())
9571            .timeout(Duration::from_secs(2))
9572            .build()
9573            .expect("client");
9574
9575        let mut workflow_worker = Worker::new(client.clone(), "rust-workers")
9576            .worker_id("draining-workflow-worker")
9577            .poll_timeout(Duration::ZERO);
9578        workflow_worker.register_workflow("counter", |_ctx, _args| async { Ok(Value::Null) });
9579        workflow_worker
9580            .run()
9581            .await
9582            .expect("workflow drain is a clean stop");
9583
9584        let mut activity_worker = Worker::new(client.clone(), "rust-workers")
9585            .worker_id("draining-activity-worker")
9586            .poll_timeout(Duration::ZERO);
9587        activity_worker.register_activity("write", |_ctx, _args| async { Ok(Value::Null) });
9588        activity_worker
9589            .run()
9590            .await
9591            .expect("activity drain is a clean stop");
9592
9593        let mut query_worker = Worker::new(client, "rust-workers")
9594            .worker_id("draining-query-worker")
9595            .poll_timeout(Duration::ZERO);
9596        query_worker.register_query("counter", "current", |_ctx, _args| async {
9597            Ok(Value::Null)
9598        });
9599        query_worker
9600            .run()
9601            .await
9602            .expect("query drain is a clean stop");
9603    }
9604
9605    #[tokio::test]
9606    async fn activity_cancellation_and_late_completion_remain_machine_readable() {
9607        let server = MockWorkerServer::start();
9608        let client = Client::builder(server.base_url())
9609            .timeout(Duration::from_secs(2))
9610            .build()
9611            .expect("client");
9612
9613        let heartbeat = client
9614            .heartbeat_activity_task(
9615                "activity-cancel",
9616                "attempt-cancel",
9617                "rust-worker",
9618                json!({"stage":"cleanup"}),
9619            )
9620            .await
9621            .expect("cancellation heartbeat");
9622        assert!(heartbeat.cancel_requested);
9623        assert!(heartbeat.should_stop());
9624        assert_eq!(heartbeat.reason.as_deref(), Some("run_cancelled"));
9625        assert_eq!(heartbeat.run_closed_reason.as_deref(), Some("cancelled"));
9626
9627        let error = client
9628            .complete_activity_task(
9629                "activity-cancel",
9630                "attempt-cancel",
9631                "rust-worker",
9632                json!({"late":true}),
9633                JSON_CODEC,
9634            )
9635            .await
9636            .expect_err("late completion must be refused");
9637        assert!(activity_task_rejection_is_final(&error));
9638        let Error::ActivityTaskRejected(rejection) = error else {
9639            panic!("expected typed activity rejection");
9640        };
9641        assert_eq!(rejection.status, 409);
9642        assert_eq!(rejection.reason, "run_cancelled");
9643        assert!(rejection.cancel_requested);
9644        assert_eq!(rejection.can_continue, Some(false));
9645    }
9646
9647    #[tokio::test]
9648    async fn managed_worker_survives_late_completion_and_restart_during_cancellation() {
9649        let server = MockWorkerServer::cancelled_activity();
9650        let client = Client::builder(server.base_url())
9651            .timeout(Duration::from_secs(2))
9652            .build()
9653            .expect("client");
9654        let cancellation_observed = Arc::new(AtomicBool::new(false));
9655        let observed = Arc::clone(&cancellation_observed);
9656        let mut worker = Worker::new(client.clone(), "rust-workers")
9657            .worker_id("rust-cancel-worker")
9658            .poll_timeout(Duration::from_millis(10));
9659        worker.register_activity("cancel-aware", move |ctx, _args| {
9660            let observed = Arc::clone(&observed);
9661            async move {
9662                let heartbeat = ctx.heartbeat(json!({"stage":"running"})).await?;
9663                observed.store(heartbeat.should_stop(), Ordering::SeqCst);
9664                Ok(json!({"late":"completion"}))
9665            }
9666        });
9667
9668        assert_eq!(
9669            worker.run_once().await.expect("cancelled attempt handled"),
9670            1
9671        );
9672        assert!(cancellation_observed.load(Ordering::SeqCst));
9673        assert_eq!(
9674            server.request_count("/api/worker/activity-tasks/activity-cancel/complete"),
9675            1
9676        );
9677
9678        let mut restarted = Worker::new(client, "rust-workers")
9679            .worker_id("rust-cancel-worker-restarted")
9680            .poll_timeout(Duration::from_millis(10));
9681        restarted.register_activity("cancel-aware", |_ctx, _args| async move { Ok(Value::Null) });
9682        assert_eq!(
9683            restarted
9684                .run_once()
9685                .await
9686                .expect("replacement worker continues polling"),
9687            0
9688        );
9689    }
9690
9691    #[tokio::test]
9692    async fn managed_worker_absorbs_selected_run_terminal_timeout_completion_race() {
9693        let response = r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"outcome":"completed","recorded":false,"run_id":"run-selected-timeout","run_status":"failed","created_task_ids":[],"reason":"run_timed_out"}"#;
9694        let server = MockWorkerServer::workflow_completion("409 Conflict", response);
9695        let client = Client::builder(server.base_url())
9696            .timeout(Duration::from_secs(2))
9697            .build()
9698            .expect("client");
9699
9700        let direct_error = client
9701            .complete_workflow_task(
9702                "workflow-timeout-task",
9703                "timeout-worker",
9704                3,
9705                vec![json!({"type": "complete_workflow", "result": null})],
9706            )
9707            .await
9708            .expect_err("the low-level client preserves the completion rejection");
9709        let Error::Http { status, body } = direct_error else {
9710            panic!("expected the original HTTP completion rejection");
9711        };
9712        assert_eq!(status, reqwest::StatusCode::CONFLICT);
9713        assert_eq!(
9714            serde_json::from_str::<Value>(&body).expect("response body")["reason"],
9715            "run_timed_out"
9716        );
9717
9718        let mut worker = Worker::new(client, "rust-workers")
9719            .worker_id("timeout-worker")
9720            .poll_timeout(Duration::from_millis(10));
9721        worker.register_workflow("timeout.workflow", |_ctx, _input| async move {
9722            Ok(json!({"late": "result"}))
9723        });
9724
9725        assert_eq!(
9726            worker
9727                .run_once()
9728                .await
9729                .expect("authoritative selected-run timeout settles the tick"),
9730            1
9731        );
9732        assert_eq!(
9733            server.request_count("/api/worker/workflow-tasks/workflow-timeout-task/complete"),
9734            2,
9735            "both the direct client proof and managed worker must see the rejection"
9736        );
9737    }
9738
9739    #[tokio::test]
9740    async fn managed_worker_does_not_swallow_nearby_completion_errors() {
9741        for (name, status, response) in [
9742            ("bare conflict", "409 Conflict", r#"{"message":"conflict"}"#),
9743            (
9744                "command was recorded",
9745                "409 Conflict",
9746                r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"recorded":true,"run_id":"run-selected-timeout","run_status":"failed","reason":"run_timed_out"}"#,
9747            ),
9748            (
9749                "lease conflict",
9750                "409 Conflict",
9751                r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"recorded":false,"run_id":"run-selected-timeout","run_status":"failed","reason":"lease_expired"}"#,
9752            ),
9753            (
9754                "nonterminal run",
9755                "409 Conflict",
9756                r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"recorded":false,"run_id":"run-selected-timeout","run_status":"waiting","reason":"run_timed_out"}"#,
9757            ),
9758            (
9759                "different selected run",
9760                "409 Conflict",
9761                r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"recorded":false,"run_id":"run-reused-workflow-current","run_status":"failed","reason":"run_timed_out"}"#,
9762            ),
9763            (
9764                "different task attempt",
9765                "409 Conflict",
9766                r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":4,"recorded":false,"run_id":"run-selected-timeout","run_status":"failed","reason":"run_timed_out"}"#,
9767            ),
9768            (
9769                "authentication failure",
9770                "401 Unauthorized",
9771                r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"recorded":false,"run_id":"run-selected-timeout","run_status":"failed","reason":"run_timed_out"}"#,
9772            ),
9773            (
9774                "authorization failure",
9775                "403 Forbidden",
9776                r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"recorded":false,"run_id":"run-selected-timeout","run_status":"failed","reason":"run_timed_out"}"#,
9777            ),
9778            (
9779                "protocol failure",
9780                "400 Bad Request",
9781                r#"{"reason":"unsupported_protocol_version","message":"unsupported worker protocol","supported_version":"1.2","requested_version":"1.3"}"#,
9782            ),
9783            (
9784                "malformed command",
9785                "422 Unprocessable Entity",
9786                r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"recorded":false,"run_id":"run-selected-timeout","run_status":"failed","reason":"run_timed_out"}"#,
9787            ),
9788            (
9789                "transient server failure",
9790                "503 Service Unavailable",
9791                r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"recorded":false,"run_id":"run-selected-timeout","run_status":"failed","reason":"run_timed_out"}"#,
9792            ),
9793        ] {
9794            let server = MockWorkerServer::workflow_completion(status, response);
9795            let client = Client::builder(server.base_url())
9796                .timeout(Duration::from_secs(2))
9797                .build()
9798                .expect("client");
9799            let mut worker = Worker::new(client, "rust-workers")
9800                .worker_id("timeout-worker")
9801                .poll_timeout(Duration::from_millis(10));
9802            worker.register_workflow("timeout.workflow", |_ctx, _input| async move {
9803                Ok(json!({"late": "result"}))
9804            });
9805
9806            let error = worker
9807                .run_once()
9808                .await
9809                .expect_err(&format!("{name} must remain an error"));
9810            assert!(
9811                matches!(error, Error::Http { .. } | Error::Protocol(_)),
9812                "{name} returned an unexpected error variant: {error}"
9813            );
9814        }
9815    }
9816
9817    #[tokio::test]
9818    async fn baseline_worker_endpoints_send_the_baseline_protocol() {
9819        let server = MockWorkerServer::start();
9820        let client = Client::builder(server.base_url())
9821            .timeout(Duration::from_secs(2))
9822            .build()
9823            .expect("client");
9824
9825        client
9826            .register_worker("capture-worker", "capture", vec![], vec![], 1, 1)
9827            .await
9828            .expect("register");
9829        client
9830            .heartbeat_worker("capture-worker", 1, 1)
9831            .await
9832            .expect("heartbeat");
9833        client
9834            .poll_workflow_task("capture-worker", "capture", Duration::from_millis(10))
9835            .await
9836            .expect("workflow poll");
9837        client
9838            .poll_activity_task("capture-worker", "capture", Duration::from_millis(10))
9839            .await
9840            .expect("activity poll");
9841
9842        for path in [
9843            "/api/worker/register",
9844            "/api/worker/heartbeat",
9845            "/api/worker/workflow-tasks/poll",
9846            "/api/worker/activity-tasks/poll",
9847        ] {
9848            assert_eq!(
9849                server.worker_protocol_for(path).as_deref(),
9850                Some(WORKER_PROTOCOL_VERSION),
9851                "unexpected protocol for {path}"
9852            );
9853        }
9854
9855        assert_eq!(
9856            server.request_body("/api/worker/workflow-tasks/poll")["timeout_seconds"],
9857            1
9858        );
9859        assert_eq!(
9860            server.request_body("/api/worker/activity-tasks/poll")["timeout_seconds"],
9861            1
9862        );
9863    }
9864
9865    #[tokio::test]
9866    async fn query_task_endpoints_send_the_query_feature_protocol() {
9867        let server = MockWorkerServer::start();
9868        let client = Client::builder(server.base_url())
9869            .timeout(Duration::from_secs(2))
9870            .build()
9871            .expect("client");
9872
9873        client
9874            .poll_query_task("capture-worker", "capture", Duration::from_millis(10))
9875            .await
9876            .expect("query poll");
9877        client
9878            .complete_query_task("query-capture", "capture-worker", 1, json!(8), JSON_CODEC)
9879            .await
9880            .expect("query complete");
9881        client
9882            .fail_query_task(
9883                "query-capture",
9884                "capture-worker",
9885                1,
9886                "failed",
9887                "query_rejected",
9888                "QueryFailed",
9889            )
9890            .await
9891            .expect("query fail");
9892
9893        for path in [
9894            "/api/worker/query-tasks/poll",
9895            "/api/worker/query-tasks/query-capture/complete",
9896            "/api/worker/query-tasks/query-capture/fail",
9897        ] {
9898            assert_eq!(
9899                server.worker_protocol_for(path).as_deref(),
9900                Some(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION),
9901                "unexpected protocol for {path}"
9902            );
9903        }
9904
9905        assert_eq!(
9906            server.request_body("/api/worker/query-tasks/poll")["timeout_seconds"],
9907            1
9908        );
9909    }
9910
9911    #[tokio::test]
9912    async fn query_protocol_rejection_from_older_server_is_typed() {
9913        let server = MockWorkerServer::reject_query_protocol();
9914        let client = Client::builder(server.base_url())
9915            .timeout(Duration::from_secs(2))
9916            .build()
9917            .expect("client");
9918
9919        let error = client
9920            .poll_query_task("capture-worker", "capture", Duration::from_millis(10))
9921            .await
9922            .expect_err("server below query protocol floor must reject");
9923        let Error::Protocol(failure) = error else {
9924            panic!("expected typed protocol failure");
9925        };
9926
9927        assert_eq!(failure.status, 400);
9928        assert_eq!(failure.reason, "unsupported_protocol_version");
9929        assert_eq!(failure.supported_version.as_deref(), Some("1.7"));
9930        assert_eq!(
9931            failure.requested_version.as_deref(),
9932            Some(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION)
9933        );
9934        assert_eq!(
9935            server
9936                .worker_protocol_for("/api/worker/query-tasks/poll")
9937                .as_deref(),
9938            Some(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION)
9939        );
9940    }
9941
9942    #[tokio::test]
9943    async fn run_once_without_query_handlers_keeps_pre_query_server_compatibility() {
9944        let server = MockWorkerServer::reject_query_protocol();
9945        let client = Client::builder(server.base_url())
9946            .timeout(Duration::from_secs(2))
9947            .build()
9948            .expect("client");
9949        let mut worker = Worker::new(client, "rust-workers")
9950            .worker_id("baseline-worker")
9951            .poll_timeout(Duration::from_millis(10));
9952
9953        worker.register_workflow("baseline.workflow", |_ctx, _input| async move {
9954            Ok(Value::Null)
9955        });
9956
9957        assert_eq!(worker.run_once().await.expect("baseline run once"), 0);
9958        assert_eq!(
9959            server
9960                .worker_protocol_for("/api/worker/workflow-tasks/poll")
9961                .as_deref(),
9962            Some(WORKER_PROTOCOL_VERSION)
9963        );
9964        assert_eq!(
9965            server.worker_protocol_for("/api/worker/query-tasks/poll"),
9966            None,
9967            "a worker without query handlers must not use the query-task endpoint"
9968        );
9969    }
9970
9971    #[tokio::test]
9972    async fn completion_time_query_rejection_is_typed_without_stopping_worker() {
9973        let server = MockWorkerServer::reject_query_completion();
9974        let client = Client::builder(server.base_url())
9975            .timeout(Duration::from_secs(2))
9976            .build()
9977            .expect("client");
9978
9979        let error = client
9980            .complete_query_task("query-late", "late-worker", 1, json!(8), JSON_CODEC)
9981            .await
9982            .expect_err("expired completion must be rejected");
9983        let Error::QueryFailed(failure) = error else {
9984            panic!("expected typed query failure");
9985        };
9986        assert_eq!(failure.status, 409);
9987        assert_eq!(failure.reason, "query_task_timed_out");
9988
9989        let mut worker = Worker::new(client, "rust-workers")
9990            .worker_id("late-worker")
9991            .poll_timeout(Duration::from_millis(10));
9992        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
9993        worker.register_query(
9994            "counter",
9995            "current",
9996            |_ctx, _args| async move { Ok(json!(8)) },
9997        );
9998
9999        assert_eq!(worker.run_once().await.expect("late task is handled"), 1);
10000        assert_eq!(
10001            worker
10002                .run_once()
10003                .await
10004                .expect("worker continues after late completion"),
10005            0
10006        );
10007        assert_eq!(
10008            server.request_count("/api/worker/query-tasks/query-late/complete"),
10009            2
10010        );
10011        assert_eq!(
10012            server.request_count("/api/worker/query-tasks/query-late/fail"),
10013            0,
10014            "a server completion rejection must not be reported as an encoding failure"
10015        );
10016    }
10017
10018    #[tokio::test]
10019    async fn activity_only_worker_can_shutdown_without_workflow_poller() {
10020        let server = MockWorkerServer::start();
10021        let client = Client::builder(server.base_url())
10022            .timeout(Duration::from_secs(2))
10023            .build()
10024            .expect("client");
10025        let mut worker = Worker::new(client, "rust-workers")
10026            .worker_id("activity-only-worker")
10027            .poll_timeout(Duration::from_millis(10));
10028
10029        worker.register_activity(
10030            "activity.only",
10031            |_ctx, _args| async move { Ok(Value::Null) },
10032        );
10033
10034        worker.run_until(async {}).await.expect("run worker");
10035    }
10036
10037    #[tokio::test]
10038    async fn workflow_only_worker_can_shutdown_without_activity_poller() {
10039        let server = MockWorkerServer::start();
10040        let client = Client::builder(server.base_url())
10041            .timeout(Duration::from_secs(2))
10042            .build()
10043            .expect("client");
10044        let mut worker = Worker::new(client, "rust-workers")
10045            .worker_id("workflow-only-worker")
10046            .poll_timeout(Duration::from_millis(10));
10047
10048        worker.register_workflow(
10049            "workflow.only",
10050            |_ctx, _input| async move { Ok(Value::Null) },
10051        );
10052
10053        worker.run_until(async {}).await.expect("run worker");
10054    }
10055
10056    #[tokio::test]
10057    async fn worker_heartbeat_observer_receives_server_acknowledgements() {
10058        let server = MockWorkerServer::start();
10059        let client = Client::builder(server.base_url())
10060            .timeout(Duration::from_secs(2))
10061            .build()
10062            .expect("client");
10063        let observations = Arc::new(Mutex::new(Vec::new()));
10064        let observed = Arc::clone(&observations);
10065        let mut worker = Worker::new(client, "rust-workers")
10066            .worker_id("observed-heartbeat-worker")
10067            .poll_timeout(Duration::from_millis(10))
10068            .on_worker_heartbeat(move |observation| {
10069                observed
10070                    .lock()
10071                    .expect("heartbeat observations")
10072                    .push(observation.clone());
10073            });
10074
10075        worker.register_workflow("workflow.observed", |_ctx, _input| async move {
10076            Ok(Value::Null)
10077        });
10078        worker
10079            .run_until(tokio::time::sleep(Duration::from_millis(20)))
10080            .await
10081            .expect("run worker");
10082
10083        let observations = observations.lock().expect("heartbeat observations");
10084        let first = observations.first().expect("heartbeat acknowledgement");
10085        assert_eq!(first.worker_id, "observed-heartbeat-worker");
10086        assert_eq!(first.task_queue, "rust-workers");
10087        assert!(first.acknowledged_at_unix_millis > 0);
10088        assert_eq!(first.acknowledgement, json!({}));
10089    }
10090
10091    #[tokio::test]
10092    async fn delayed_worker_heartbeat_keeps_cadence_and_pollers_live() {
10093        let server = MockWorkerServer::delayed_heartbeat_worker();
10094        let client = Client::builder(server.base_url())
10095            .timeout(Duration::from_secs(3))
10096            .build()
10097            .expect("client");
10098        let observations = Arc::new(Mutex::new(Vec::new()));
10099        let observed = Arc::clone(&observations);
10100        let mut worker = Worker::new(client, "rust-snapshot-workers")
10101            .worker_id("rust-snapshot-worker")
10102            .poll_timeout(Duration::from_millis(10))
10103            .on_worker_heartbeat(move |observation| {
10104                observed
10105                    .lock()
10106                    .expect("heartbeat observations")
10107                    .push(observation.clone());
10108            });
10109
10110        worker.register_workflow("snapshot", |ctx, _input| async move {
10111            ctx.wait_signal("finish").await?;
10112            Ok(json!({"status": "finished"}))
10113        });
10114        worker.register_query("snapshot", "current", |ctx, _args| async move {
10115            Ok(json!(ctx
10116                .signals("increment")
10117                .iter()
10118                .filter_map(|arguments| arguments.first().and_then(Value::as_i64))
10119                .sum::<i64>()))
10120        });
10121        worker.register_activity("cancel-aware", |_ctx, _args| async move {
10122            Ok(json!({"late": "completion"}))
10123        });
10124
10125        worker
10126            .run_until(tokio::time::sleep(Duration::from_millis(3_800)))
10127            .await
10128            .expect("delayed heartbeat must allow a clean worker shutdown");
10129
10130        let observations = observations.lock().expect("heartbeat observations");
10131        assert!(
10132            observations.len() >= 3,
10133            "the immediate heartbeat, delayed acknowledgement, and next cadence heartbeat must complete"
10134        );
10135        assert!(
10136            observations.windows(2).all(|pair| {
10137                pair[1].acknowledged_at_unix_millis
10138                    .saturating_sub(pair[0].acknowledged_at_unix_millis)
10139                    >= 850
10140            }),
10141            "successful acknowledgements must not catch up faster than the advertised one-second cadence: {observations:?}"
10142        );
10143        drop(observations);
10144
10145        let heartbeat_times = server.request_times("/api/worker/heartbeat");
10146        let delayed_request_at = *heartbeat_times
10147            .get(1)
10148            .expect("intentionally delayed heartbeat request");
10149        let delay_window_start = delayed_request_at + Duration::from_millis(100);
10150        let delay_window_end = delayed_request_at + Duration::from_millis(1_400);
10151        for path in [
10152            "/api/worker/workflow-tasks/poll",
10153            "/api/worker/activity-tasks/poll",
10154            "/api/worker/query-tasks/poll",
10155        ] {
10156            assert!(
10157                server
10158                    .request_times(path)
10159                    .iter()
10160                    .any(|received_at| *received_at >= delay_window_start
10161                        && *received_at <= delay_window_end),
10162                "{path} must keep polling while a heartbeat acknowledgement is delayed"
10163            );
10164        }
10165        assert!(
10166            server.request_count("/api/worker/workflow-tasks/snapshot-wait-3/fail") >= 1,
10167            "workflow work must be settled"
10168        );
10169        assert!(
10170            server.request_count("/api/worker/activity-tasks/activity-cancel/complete") >= 1,
10171            "activity work must be settled"
10172        );
10173        assert!(
10174            server.request_count("/api/worker/query-tasks/snapshot-current/complete") >= 1,
10175            "query work must be settled"
10176        );
10177    }
10178
10179    #[tokio::test]
10180    async fn retried_worker_heartbeat_restarts_the_advertised_cadence() {
10181        let server = MockWorkerServer::heartbeat_retry_worker();
10182        let client = Client::builder(server.base_url())
10183            .timeout(Duration::from_secs(2))
10184            .build()
10185            .expect("client");
10186        let observations = Arc::new(Mutex::new(Vec::new()));
10187        let observed = Arc::clone(&observations);
10188        let worker = Worker::new(client, "rust-workers")
10189            .worker_id("heartbeat-retry-worker")
10190            .retry_policy(WorkerRetryPolicy {
10191                max_retries: 1,
10192                initial_backoff: Duration::from_millis(300),
10193                max_backoff: Duration::from_millis(300),
10194            })
10195            .on_worker_heartbeat(move |observation| {
10196                observed
10197                    .lock()
10198                    .expect("heartbeat observations")
10199                    .push(observation.clone());
10200            });
10201
10202        worker
10203            .run_until(tokio::time::sleep(Duration::from_millis(2_700)))
10204            .await
10205            .expect("retryable heartbeat failure must remain bounded and recover");
10206
10207        let observations = observations.lock().expect("heartbeat observations");
10208        assert!(observations.len() >= 3, "heartbeat retry must recover");
10209        assert!(
10210            observations.windows(2).all(|pair| {
10211                pair[1]
10212                    .acknowledged_at_unix_millis
10213                    .saturating_sub(pair[0].acknowledged_at_unix_millis)
10214                    >= 850
10215            }),
10216            "a successful retry must start a fresh advertised cadence: {observations:?}"
10217        );
10218        assert_eq!(
10219            server.request_count("/api/worker/heartbeat"),
10220            observations.len() + 1,
10221            "one retryable failure must add exactly one bounded request"
10222        );
10223    }
10224
10225    #[tokio::test]
10226    async fn query_enabled_worker_stays_live_when_signal_replay_emits_no_commands() {
10227        let server = MockWorkerServer::waiting_query_worker();
10228        let client = Client::builder(server.base_url())
10229            .timeout(Duration::from_secs(2))
10230            .build()
10231            .expect("client");
10232        let observations = Arc::new(Mutex::new(Vec::new()));
10233        let observed = Arc::clone(&observations);
10234        let mut worker = Worker::new(client, "rust-snapshot-workers")
10235            .worker_id("rust-snapshot-worker")
10236            .poll_timeout(Duration::from_millis(10))
10237            .on_worker_heartbeat(move |observation| {
10238                observed
10239                    .lock()
10240                    .expect("heartbeat observations")
10241                    .push(observation.clone());
10242            });
10243
10244        worker.register_workflow("snapshot", |ctx, _input| async move {
10245            ctx.wait_signal("finish").await?;
10246            Ok(json!({"status": "finished"}))
10247        });
10248        worker.register_query("snapshot", "current", |ctx, _args| async move {
10249            let current = ctx
10250                .signals("increment")
10251                .iter()
10252                .filter_map(|arguments| arguments.first().and_then(Value::as_i64))
10253                .sum::<i64>();
10254            Ok(json!(current))
10255        });
10256
10257        worker
10258            .run_until(tokio::time::sleep(Duration::from_millis(3_200)))
10259            .await
10260            .expect("pending workflow and query poller must remain live until shutdown");
10261
10262        assert!(
10263            observations.lock().expect("heartbeat observations").len() >= 4,
10264            "the immediate heartbeat and at least three advertised one-second intervals must be acknowledged"
10265        );
10266        assert!(
10267            server.request_count("/api/worker/workflow-tasks/poll") >= 3,
10268            "workflow polling must continue after empty replay acknowledgements"
10269        );
10270        assert!(
10271            server.request_count("/api/worker/query-tasks/poll") >= 2,
10272            "query polling must continue after serving the current query"
10273        );
10274        assert_eq!(
10275            server.request_body("/api/worker/register")["capabilities"],
10276            json!([QUERY_TASKS_CAPABILITY])
10277        );
10278
10279        for task_id in ["snapshot-wait-3", "snapshot-wait-5"] {
10280            let fail_path = format!("/api/worker/workflow-tasks/{task_id}/fail");
10281            let completion_path = format!("/api/worker/workflow-tasks/{task_id}/complete");
10282            let failure = server.request_body(&fail_path);
10283            assert_eq!(
10284                failure["failure"]["type"],
10285                WORKFLOW_TASK_WAITING_FOR_HISTORY_TYPE
10286            );
10287            assert_eq!(server.request_count(&completion_path), 0);
10288        }
10289
10290        let query_completion =
10291            server.request_body("/api/worker/query-tasks/snapshot-current/complete");
10292        assert_eq!(query_completion["result"], json!(8));
10293    }
10294
10295    #[tokio::test]
10296    async fn worker_retries_poll_and_heartbeat_transport_failures_independently() {
10297        let server = MockWorkerServer::transient_worker_failures();
10298        let client = Client::builder(server.base_url())
10299            .timeout(Duration::from_secs(2))
10300            .build()
10301            .expect("client");
10302        let mut worker = Worker::new(client, "rust-workers")
10303            .worker_id("retry-worker")
10304            .poll_timeout(Duration::from_millis(10))
10305            .retry_policy(WorkerRetryPolicy {
10306                max_retries: 2,
10307                initial_backoff: Duration::from_millis(1),
10308                max_backoff: Duration::from_millis(1),
10309            });
10310        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
10311        worker.register_activity(
10312            "counter.activity",
10313            |_ctx, _input| async move { Ok(Value::Null) },
10314        );
10315        worker.register_query(
10316            "counter",
10317            "current",
10318            |_ctx, _args| async move { Ok(json!(8)) },
10319        );
10320
10321        worker
10322            .run_until(tokio::time::sleep(Duration::from_millis(75)))
10323            .await
10324            .expect("transient failures must not stop the worker");
10325
10326        for path in [
10327            "/api/worker/heartbeat",
10328            "/api/worker/workflow-tasks/poll",
10329            "/api/worker/activity-tasks/poll",
10330            "/api/worker/query-tasks/poll",
10331        ] {
10332            assert!(
10333                server.request_count(path) >= 2,
10334                "{path} must continue after its transient failure"
10335            );
10336        }
10337    }
10338
10339    #[tokio::test]
10340    async fn worker_bounds_transport_retries() {
10341        let server = MockWorkerServer::unavailable_polls();
10342        let client = Client::builder(server.base_url())
10343            .timeout(Duration::from_secs(2))
10344            .build()
10345            .expect("client");
10346        let mut worker = Worker::new(client, "rust-workers")
10347            .worker_id("bounded-retry-worker")
10348            .poll_timeout(Duration::from_millis(10))
10349            .retry_policy(WorkerRetryPolicy {
10350                max_retries: 2,
10351                initial_backoff: Duration::from_millis(1),
10352                max_backoff: Duration::from_millis(1),
10353            });
10354        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
10355
10356        let error = worker.run().await.expect_err("retry bound must terminate");
10357        assert!(matches!(error, Error::Transport(_)));
10358        assert_eq!(
10359            server.request_count("/api/worker/workflow-tasks/poll"),
10360            3,
10361            "one initial request plus two retries"
10362        );
10363    }
10364
10365    #[tokio::test]
10366    async fn worker_does_not_retry_authentication_failures() {
10367        let server = MockWorkerServer::unauthorized_polls();
10368        let client = Client::builder(server.base_url())
10369            .timeout(Duration::from_secs(2))
10370            .build()
10371            .expect("client");
10372        let mut worker = Worker::new(client, "rust-workers")
10373            .worker_id("unauthorized-worker")
10374            .poll_timeout(Duration::from_millis(10));
10375        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
10376
10377        let error = worker
10378            .run()
10379            .await
10380            .expect_err("authentication must terminate");
10381        let Error::Http { status, body } = error else {
10382            panic!("expected stable HTTP authentication error");
10383        };
10384        assert_eq!(status, reqwest::StatusCode::UNAUTHORIZED);
10385        assert!(body.contains("authentication_failed"));
10386        assert_eq!(
10387            server.request_count("/api/worker/workflow-tasks/poll"),
10388            1,
10389            "authentication failures must not be retried"
10390        );
10391    }
10392
10393    #[derive(Clone, Debug)]
10394    struct CapturedRequest {
10395        path: String,
10396        worker_protocol: Option<String>,
10397        body: String,
10398        received_at: Instant,
10399    }
10400
10401    struct MockWorkerServer {
10402        addr: SocketAddr,
10403        stop: Arc<AtomicBool>,
10404        requests: Arc<Mutex<Vec<CapturedRequest>>>,
10405        thread: Option<thread::JoinHandle<()>>,
10406    }
10407
10408    #[derive(Clone, Copy, Default)]
10409    struct MockWorkerBehavior {
10410        reject_query_protocol: bool,
10411        reject_query_completion: bool,
10412        waiting_query_worker: bool,
10413        poll_failures_per_path: usize,
10414        heartbeat_failures: usize,
10415        heartbeat_failure_request: Option<usize>,
10416        delayed_heartbeat_request: Option<usize>,
10417        heartbeat_response_delay: Duration,
10418        concurrent_requests: bool,
10419        unauthorized_polls: bool,
10420        cancelled_activity: bool,
10421        draining_polls: bool,
10422        workflow_completion_status: Option<&'static str>,
10423        workflow_completion_body: Option<&'static str>,
10424    }
10425
10426    impl MockWorkerServer {
10427        fn start() -> Self {
10428            Self::start_with_behavior(MockWorkerBehavior::default())
10429        }
10430
10431        fn reject_query_protocol() -> Self {
10432            Self::start_with_behavior(MockWorkerBehavior {
10433                reject_query_protocol: true,
10434                ..MockWorkerBehavior::default()
10435            })
10436        }
10437
10438        fn reject_query_completion() -> Self {
10439            Self::start_with_behavior(MockWorkerBehavior {
10440                reject_query_completion: true,
10441                ..MockWorkerBehavior::default()
10442            })
10443        }
10444
10445        fn waiting_query_worker() -> Self {
10446            Self::start_with_behavior(MockWorkerBehavior {
10447                waiting_query_worker: true,
10448                ..MockWorkerBehavior::default()
10449            })
10450        }
10451
10452        fn transient_worker_failures() -> Self {
10453            Self::start_with_behavior(MockWorkerBehavior {
10454                poll_failures_per_path: 1,
10455                heartbeat_failures: 1,
10456                ..MockWorkerBehavior::default()
10457            })
10458        }
10459
10460        fn delayed_heartbeat_worker() -> Self {
10461            Self::start_with_behavior(MockWorkerBehavior {
10462                waiting_query_worker: true,
10463                delayed_heartbeat_request: Some(2),
10464                heartbeat_response_delay: Duration::from_millis(1_500),
10465                concurrent_requests: true,
10466                cancelled_activity: true,
10467                ..MockWorkerBehavior::default()
10468            })
10469        }
10470
10471        fn heartbeat_retry_worker() -> Self {
10472            Self::start_with_behavior(MockWorkerBehavior {
10473                waiting_query_worker: true,
10474                heartbeat_failure_request: Some(2),
10475                concurrent_requests: true,
10476                ..MockWorkerBehavior::default()
10477            })
10478        }
10479
10480        fn unavailable_polls() -> Self {
10481            Self::start_with_behavior(MockWorkerBehavior {
10482                poll_failures_per_path: usize::MAX,
10483                ..MockWorkerBehavior::default()
10484            })
10485        }
10486
10487        fn unauthorized_polls() -> Self {
10488            Self::start_with_behavior(MockWorkerBehavior {
10489                unauthorized_polls: true,
10490                ..MockWorkerBehavior::default()
10491            })
10492        }
10493
10494        fn cancelled_activity() -> Self {
10495            Self::start_with_behavior(MockWorkerBehavior {
10496                cancelled_activity: true,
10497                ..MockWorkerBehavior::default()
10498            })
10499        }
10500
10501        fn draining_polls() -> Self {
10502            Self::start_with_behavior(MockWorkerBehavior {
10503                draining_polls: true,
10504                ..MockWorkerBehavior::default()
10505            })
10506        }
10507
10508        fn workflow_completion(status: &'static str, body: &'static str) -> Self {
10509            Self::start_with_behavior(MockWorkerBehavior {
10510                workflow_completion_status: Some(status),
10511                workflow_completion_body: Some(body),
10512                ..MockWorkerBehavior::default()
10513            })
10514        }
10515
10516        fn start_with_behavior(behavior: MockWorkerBehavior) -> Self {
10517            let listener = TcpListener::bind("127.0.0.1:0").expect("bind mock server");
10518            listener
10519                .set_nonblocking(true)
10520                .expect("configure mock listener");
10521            let addr = listener.local_addr().expect("mock server address");
10522            let stop = Arc::new(AtomicBool::new(false));
10523            let server_stop = Arc::clone(&stop);
10524            let requests = Arc::new(Mutex::new(Vec::new()));
10525            let server_requests = Arc::clone(&requests);
10526            let thread = thread::spawn(move || {
10527                let mut request_threads = Vec::new();
10528                while !server_stop.load(Ordering::SeqCst) {
10529                    match listener.accept() {
10530                        Ok((mut stream, _)) => {
10531                            if behavior.concurrent_requests {
10532                                let requests = Arc::clone(&server_requests);
10533                                request_threads.push(thread::spawn(move || {
10534                                    handle_mock_worker_request(&mut stream, &requests, behavior)
10535                                }));
10536                            } else {
10537                                handle_mock_worker_request(&mut stream, &server_requests, behavior);
10538                            }
10539                        }
10540                        Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
10541                            let mut index = 0;
10542                            while index < request_threads.len() {
10543                                if request_threads[index].is_finished() {
10544                                    request_threads
10545                                        .swap_remove(index)
10546                                        .join()
10547                                        .expect("join mock request");
10548                                } else {
10549                                    index += 1;
10550                                }
10551                            }
10552                            thread::sleep(Duration::from_millis(5));
10553                        }
10554                        Err(_) => break,
10555                    }
10556                }
10557                for request_thread in request_threads {
10558                    request_thread.join().expect("join mock request");
10559                }
10560            });
10561
10562            Self {
10563                addr,
10564                stop,
10565                requests,
10566                thread: Some(thread),
10567            }
10568        }
10569
10570        fn base_url(&self) -> String {
10571            format!("http://{}", self.addr)
10572        }
10573
10574        fn worker_protocol_for(&self, path: &str) -> Option<String> {
10575            self.requests
10576                .lock()
10577                .expect("captured requests")
10578                .iter()
10579                .find(|request| request.path == path)
10580                .and_then(|request| request.worker_protocol.clone())
10581        }
10582
10583        fn request_count(&self, path: &str) -> usize {
10584            self.requests
10585                .lock()
10586                .expect("captured requests")
10587                .iter()
10588                .filter(|request| request.path == path)
10589                .count()
10590        }
10591
10592        fn request_times(&self, path: &str) -> Vec<Instant> {
10593            self.requests
10594                .lock()
10595                .expect("captured requests")
10596                .iter()
10597                .filter(|request| request.path == path)
10598                .map(|request| request.received_at)
10599                .collect()
10600        }
10601
10602        fn request_body(&self, path: &str) -> Value {
10603            let requests = self.requests.lock().expect("captured requests");
10604            let body = &requests
10605                .iter()
10606                .find(|request| request.path == path)
10607                .unwrap_or_else(|| panic!("missing request for {path}"))
10608                .body;
10609            serde_json::from_str(body).unwrap_or_else(|error| {
10610                panic!("invalid JSON request body for {path}: {error}: {body:?}")
10611            })
10612        }
10613    }
10614
10615    impl Drop for MockWorkerServer {
10616        fn drop(&mut self) {
10617            self.stop.store(true, Ordering::SeqCst);
10618            let _ = TcpStream::connect(self.addr);
10619
10620            if let Some(thread) = self.thread.take() {
10621                thread.join().expect("join mock server");
10622            }
10623        }
10624    }
10625
10626    fn handle_mock_worker_request(
10627        stream: &mut TcpStream,
10628        requests: &Arc<Mutex<Vec<CapturedRequest>>>,
10629        behavior: MockWorkerBehavior,
10630    ) {
10631        let _ = stream.set_read_timeout(Some(Duration::from_millis(200)));
10632        let mut buffer = [0_u8; 8192];
10633        let mut request = Vec::new();
10634
10635        loop {
10636            match stream.read(&mut buffer) {
10637                Ok(0) => break,
10638                Ok(read) => {
10639                    request.extend_from_slice(&buffer[..read]);
10640                    if mock_request_is_complete(&request) {
10641                        break;
10642                    }
10643                }
10644                Err(error)
10645                    if matches!(
10646                        error.kind(),
10647                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
10648                    ) =>
10649                {
10650                    break;
10651                }
10652                Err(_) => return,
10653            }
10654        }
10655
10656        let request = String::from_utf8_lossy(&request);
10657        let body = request
10658            .split_once("\r\n\r\n")
10659            .map(|(_, body)| body)
10660            .unwrap_or_default();
10661        let path = request
10662            .lines()
10663            .next()
10664            .and_then(|line| line.split_whitespace().nth(1))
10665            .unwrap_or_default();
10666        let worker_protocol = request.lines().find_map(|line| {
10667            let (name, value) = line.split_once(':')?;
10668            name.eq_ignore_ascii_case("X-Durable-Workflow-Protocol-Version")
10669                .then(|| value.trim().to_string())
10670        });
10671        let request_number = {
10672            let mut requests = requests.lock().expect("captured requests");
10673            requests.push(CapturedRequest {
10674                path: path.to_string(),
10675                worker_protocol: worker_protocol.clone(),
10676                body: body.to_string(),
10677                received_at: Instant::now(),
10678            });
10679            requests
10680                .iter()
10681                .filter(|request| request.path == path)
10682                .count()
10683        };
10684
10685        let is_poll = matches!(
10686            path,
10687            "/api/worker/workflow-tasks/poll"
10688                | "/api/worker/activity-tasks/poll"
10689                | "/api/worker/query-tasks/poll"
10690        );
10691        if is_poll && request_number <= behavior.poll_failures_per_path {
10692            return;
10693        }
10694        if path == "/api/worker/heartbeat" && request_number <= behavior.heartbeat_failures {
10695            return;
10696        }
10697        if path == "/api/worker/heartbeat"
10698            && behavior.heartbeat_failure_request == Some(request_number)
10699        {
10700            return;
10701        }
10702        if path == "/api/worker/heartbeat"
10703            && behavior.delayed_heartbeat_request == Some(request_number)
10704        {
10705            thread::sleep(behavior.heartbeat_response_delay);
10706        }
10707        if behavior.unauthorized_polls && is_poll {
10708            write_mock_response(
10709                stream,
10710                "401 Unauthorized",
10711                r#"{"reason":"authentication_failed","message":"invalid worker token"}"#,
10712            );
10713            return;
10714        }
10715        if behavior.draining_polls && is_poll {
10716            write_mock_response(
10717                stream,
10718                "409 Conflict",
10719                r#"{"task":null,"poll_status":"draining","reason":"worker_draining","worker_status":"draining","drain_intent":"draining"}"#,
10720            );
10721            return;
10722        }
10723
10724        if behavior.reject_query_protocol && path.starts_with("/api/worker/query-tasks/") {
10725            let requested_version = worker_protocol.as_deref().unwrap_or("missing");
10726            let body = format!(
10727                r#"{{"reason":"unsupported_protocol_version","message":"Query tasks require worker protocol 1.8 or newer.","supported_version":"1.7","requested_version":"{requested_version}"}}"#
10728            );
10729            write_mock_response(stream, "400 Bad Request", &body);
10730            return;
10731        }
10732
10733        if behavior.reject_query_completion && path == "/api/worker/query-tasks/query-late/complete"
10734        {
10735            write_mock_response(
10736                stream,
10737                "409 Conflict",
10738                r#"{"reason":"query_task_timed_out","message":"query task timed out before completion"}"#,
10739            );
10740            return;
10741        }
10742
10743        if behavior.workflow_completion_status.is_some()
10744            && path == "/api/worker/workflow-tasks/poll"
10745            && request_number == 1
10746        {
10747            write_mock_response(
10748                stream,
10749                "200 OK",
10750                r#"{"task":{"task_id":"workflow-timeout-task","workflow_id":"reused-workflow-id","run_id":"run-selected-timeout","workflow_type":"timeout.workflow","payload_codec":"json","arguments":{"codec":"json","blob":"[]"},"history_events":[],"workflow_task_attempt":3,"lease_owner":"timeout-worker"}}"#,
10751            );
10752            return;
10753        }
10754
10755        if path == "/api/worker/workflow-tasks/workflow-timeout-task/complete" {
10756            if let (Some(status), Some(body)) = (
10757                behavior.workflow_completion_status,
10758                behavior.workflow_completion_body,
10759            ) {
10760                write_mock_response(stream, status, body);
10761                return;
10762            }
10763        }
10764
10765        if behavior.waiting_query_worker {
10766            if path == "/api/worker/workflow-tasks/poll" && request_number <= 2 {
10767                let amounts = if request_number == 1 {
10768                    vec![3]
10769                } else {
10770                    vec![3, 5]
10771                };
10772                let task_id = if request_number == 1 {
10773                    "snapshot-wait-3"
10774                } else {
10775                    "snapshot-wait-5"
10776                };
10777                let history_events = std::iter::once(json!({
10778                    "event_type": "SignalWaitOpened",
10779                    "payload": {"sequence": 1, "signal_name": "finish"}
10780                }))
10781                .chain(amounts.iter().enumerate().map(|(index, amount)| {
10782                    json!({
10783                        "event_type": "SignalReceived",
10784                        "payload": {
10785                            "signal_id": format!("increment-{amount}"),
10786                            "signal_name": "increment",
10787                            "workflow_sequence": index + 2,
10788                            "payload_codec": DEFAULT_CODEC,
10789                            "arguments": encode_value_envelope(&json!([amount]), DEFAULT_CODEC)
10790                                .expect("Avro signal envelope")
10791                        }
10792                    })
10793                }))
10794                .collect::<Vec<_>>();
10795                let body = json!({
10796                    "task": {
10797                        "task_id": task_id,
10798                        "workflow_id": "snapshot-1",
10799                        "run_id": "snapshot-run-1",
10800                        "workflow_type": "snapshot",
10801                        "payload_codec": DEFAULT_CODEC,
10802                        "arguments": encode_value_envelope(&json!([]), DEFAULT_CODEC)
10803                            .expect("Avro workflow arguments"),
10804                        "history_events": history_events,
10805                        "workflow_task_attempt": 1,
10806                        "workflow_signal_id": format!("increment-{}", amounts.last().expect("amount")),
10807                        "signal_name": "increment",
10808                        "signal_arguments": encode_value_envelope(
10809                            &json!([amounts.last().expect("amount")]),
10810                            DEFAULT_CODEC,
10811                        )
10812                        .expect("Avro resume signal"),
10813                        "lease_owner": "rust-snapshot-worker"
10814                    }
10815                })
10816                .to_string();
10817                write_mock_response(stream, "200 OK", &body);
10818                return;
10819            }
10820
10821            if path == "/api/worker/query-tasks/poll" && request_number == 1 {
10822                let history_events = [3, 5]
10823                    .into_iter()
10824                    .enumerate()
10825                    .map(|(index, amount)| {
10826                        json!({
10827                            "event_type": "SignalReceived",
10828                            "payload": {
10829                                "signal_id": format!("increment-{amount}"),
10830                                "signal_name": "increment",
10831                                "workflow_sequence": index + 2,
10832                                "payload_codec": DEFAULT_CODEC,
10833                                "arguments": encode_value_envelope(&json!([amount]), DEFAULT_CODEC)
10834                                    .expect("Avro query signal envelope")
10835                            }
10836                        })
10837                    })
10838                    .collect::<Vec<_>>();
10839                let body = json!({
10840                    "task": {
10841                        "query_task_id": "snapshot-current",
10842                        "query_task_attempt": 1,
10843                        "lease_owner": "rust-snapshot-worker",
10844                        "workflow_id": "snapshot-1",
10845                        "run_id": "snapshot-run-1",
10846                        "workflow_type": "snapshot",
10847                        "query_name": "current",
10848                        "payload_codec": DEFAULT_CODEC,
10849                        "workflow_arguments": encode_value_envelope(&json!([]), DEFAULT_CODEC)
10850                            .expect("Avro workflow arguments"),
10851                        "query_arguments": encode_value_envelope(&json!([]), DEFAULT_CODEC)
10852                            .expect("Avro query arguments"),
10853                        "history_events": history_events,
10854                        "run_status": "waiting"
10855                    }
10856                })
10857                .to_string();
10858                write_mock_response(stream, "200 OK", &body);
10859                return;
10860            }
10861
10862            if path == "/api/worker/workflow-tasks/snapshot-wait-3/fail"
10863                || path == "/api/worker/workflow-tasks/snapshot-wait-5/fail"
10864            {
10865                write_mock_response(
10866                    stream,
10867                    "200 OK",
10868                    r#"{"outcome":"waiting_for_history","recorded":true}"#,
10869                );
10870                return;
10871            }
10872
10873            if path == "/api/worker/query-tasks/snapshot-current/complete" {
10874                write_mock_response(stream, "200 OK", r#"{"outcome":"completed"}"#);
10875                return;
10876            }
10877        }
10878
10879        let (status, body) = match path {
10880            "/api/workflows" => (
10881                "201 Created",
10882                r#"{"workflow_id":"wf-start-options","run_id":"run-start-options","workflow_type":"rust.timeout"}"#,
10883            ),
10884            "/api/worker/register" if behavior.waiting_query_worker => (
10885                "200 OK",
10886                r#"{"worker_id":"rust-snapshot-worker","registered":true,"heartbeat_interval_seconds":1}"#,
10887            ),
10888            "/api/worker/register" => (
10889                "200 OK",
10890                r#"{"worker_id":"mock-worker","registered":true,"heartbeat_interval_seconds":3600}"#,
10891            ),
10892            "/api/worker/heartbeat" => ("200 OK", "{}"),
10893            "/api/worker/activity-tasks/poll"
10894                if behavior.cancelled_activity && request_number == 1 =>
10895            {
10896                (
10897                    "200 OK",
10898                    r#"{"task":{"task_id":"activity-cancel","activity_attempt_id":"attempt-cancel","activity_type":"cancel-aware","payload_codec":"json","arguments":{"codec":"json","blob":"[]"},"attempt_number":1,"lease_owner":"rust-cancel-worker"}}"#,
10899                )
10900            }
10901            "/api/worker/activity-tasks/poll" | "/api/worker/workflow-tasks/poll" => {
10902                ("200 OK", r#"{"task":null}"#)
10903            }
10904            "/api/worker/query-tasks/poll"
10905                if behavior.reject_query_completion && request_number == 1 =>
10906            {
10907                (
10908                    "200 OK",
10909                    r#"{"task":{"query_task_id":"query-late","query_task_attempt":1,"lease_owner":"late-worker","workflow_id":"counter-late","run_id":"run-late","workflow_type":"counter","query_name":"current","payload_codec":"json","workflow_arguments":{"codec":"json","blob":"[]"},"query_arguments":{"codec":"json","blob":"[]"},"history_events":[],"run_status":"running"}}"#,
10910                )
10911            }
10912            "/api/worker/query-tasks/poll" => ("200 OK", r#"{"task":null}"#),
10913            "/api/worker/query-tasks/query-capture/complete"
10914            | "/api/worker/query-tasks/query-capture/fail" => ("200 OK", "{}"),
10915            "/api/worker/activity-tasks/activity-cancel/heartbeat" => (
10916                "200 OK",
10917                r#"{"activity_attempt_id":"attempt-cancel","cancel_requested":true,"can_continue":false,"reason":"run_cancelled","run_closed_reason":"cancelled","heartbeat_recorded":false}"#,
10918            ),
10919            "/api/worker/activity-tasks/activity-cancel/complete" => (
10920                "409 Conflict",
10921                r#"{"task_id":"activity-cancel","activity_attempt_id":"attempt-cancel","reason":"run_cancelled","cancel_requested":true,"can_continue":false,"run_closed_reason":"cancelled"}"#,
10922            ),
10923            "/api/workflows/counter-1/query/current" => (
10924                "200 OK",
10925                r#"{"workflow_id":"counter-1","query_name":"current","result":{"count":8},"result_envelope":{"codec":"json","blob":"{\"count\":8}"}}"#,
10926            ),
10927            "/api/workflows/counter-1/query/missing" => (
10928                "404 Not Found",
10929                r#"{"workflow_id":"counter-1","query_name":"missing","reason":"rejected_unknown_query","message":"unknown query"}"#,
10930            ),
10931            "/api/workflows/wf-lifecycle/cancel" => (
10932                "200 OK",
10933                r#"{"workflow_id":"wf-lifecycle","run_id":"run-current","outcome":"cancelled","reason":"cleanup requested","command_status":"accepted"}"#,
10934            ),
10935            "/api/workflows/wf-lifecycle/terminate" => (
10936                "200 OK",
10937                r#"{"workflow_id":"wf-lifecycle","run_id":"run-current","outcome":"terminated","reason":"forced stop","command_status":"accepted"}"#,
10938            ),
10939            "/api/workflows/wf-lifecycle/runs/run-current/cancel" => (
10940                "200 OK",
10941                r#"{"workflow_id":"wf-lifecycle","run_id":"run-current","outcome":"cancelled","command_status":"accepted"}"#,
10942            ),
10943            "/api/workflows/wf-lifecycle/runs/run-current/terminate" => (
10944                "200 OK",
10945                r#"{"workflow_id":"wf-lifecycle","run_id":"run-current","outcome":"terminated","command_status":"accepted"}"#,
10946            ),
10947            "/api/workflows/wf-lifecycle/runs/run-stale/cancel"
10948            | "/api/workflows/wf-lifecycle/runs/run-stale/terminate" => (
10949                "409 Conflict",
10950                r#"{"workflow_id":"wf-lifecycle","run_id":"run-stale","reason":"historical_run_command_rejected","target_scope":"run","message":"Commands cannot target historical runs."}"#,
10951            ),
10952            "/api/workflows/wf-failed" | "/api/workflows/wf-failed/runs/run-failed" => (
10953                "200 OK",
10954                r#"{"workflow_id":"wf-failed","run_id":"run-failed","status":"failed","closed_reason":"failed","error":"payment failed","failure":{"message":"payment failed","failure_category":"application","exception_type":"PaymentError","exception_class":"billing::PaymentError","non_retryable":true,"exception":{"type":"PaymentError","class":"billing::PaymentError","message":"payment failed"},"failures":[{"id":"failure-17","failure_category":"application"}]}}"#,
10955            ),
10956            "/api/workflows/wf-cancelled" => (
10957                "200 OK",
10958                r#"{"workflow_id":"wf-cancelled","run_id":"run-cancelled","status":"cancelled","closed_reason":"cancelled","reason":"cleanup requested"}"#,
10959            ),
10960            "/api/workflows/wf-terminated" => (
10961                "200 OK",
10962                r#"{"workflow_id":"wf-terminated","run_id":"run-terminated","status":"terminated","closed_reason":"terminated","reason":"forced stop"}"#,
10963            ),
10964            "/api/workflows/wf-timed-out" => (
10965                "200 OK",
10966                r#"{"workflow_id":"wf-timed-out","run_id":"run-timed-out","status":"failed","closed_reason":"timed_out","reason":"run_timeout"}"#,
10967            ),
10968            "/api/workflows/wf-waiting" | "/api/workflows/wf-waiting/runs/run-waiting" => (
10969                "200 OK",
10970                r#"{"workflow_id":"wf-waiting","run_id":"run-waiting","status":"waiting"}"#,
10971            ),
10972            "/api/workflows/wf-selected" => (
10973                "200 OK",
10974                r#"{"workflow_id":"wf-selected","run_id":"run-current","status":"completed","output":"current run output"}"#,
10975            ),
10976            "/api/workflows/wf-selected/runs/run-selected" => (
10977                "200 OK",
10978                r#"{"workflow_id":"wf-selected","run_id":"run-selected","status":"cancelled","closed_reason":"cancelled","reason":"selected run cancelled"}"#,
10979            ),
10980            _ => ("404 Not Found", r#"{"message":"not found"}"#),
10981        };
10982        write_mock_response(stream, status, body);
10983    }
10984
10985    fn mock_request_is_complete(request: &[u8]) -> bool {
10986        let Some(header_end) = request
10987            .windows(4)
10988            .position(|window| window == b"\r\n\r\n")
10989            .map(|position| position + 4)
10990        else {
10991            return false;
10992        };
10993        let headers = String::from_utf8_lossy(&request[..header_end]);
10994        let content_length = headers.lines().find_map(|line| {
10995            let (name, value) = line.split_once(':')?;
10996            name.eq_ignore_ascii_case("content-length")
10997                .then(|| value.trim().parse::<usize>().ok())
10998                .flatten()
10999        });
11000
11001        request.len() >= header_end + content_length.unwrap_or(0)
11002    }
11003
11004    fn write_mock_response(stream: &mut TcpStream, status: &str, body: &str) {
11005        let response = format!(
11006            "HTTP/1.1 {status}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
11007            body.len()
11008        );
11009
11010        let _ = stream.write_all(response.as_bytes());
11011        let _ = stream.flush();
11012    }
11013}