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