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    workflow_id: Option<String>,
4788    run_id: Option<String>,
4789    task_queue: String,
4790    payload_codec: String,
4791    history_budget: WorkflowHistoryBudget,
4792    resume_signal: Option<ResumeSignal>,
4793    recorded_commands: Vec<RecordedCommand>,
4794    recorded_continue_as_new_sequence: Option<u64>,
4795    continue_as_new_consumed: bool,
4796    command_cursor: usize,
4797    matched_recorded_pending: bool,
4798    version_markers: HashMap<String, (i32, u64)>,
4799    commands: Vec<Value>,
4800}
4801
4802impl WorkflowState {
4803    #[cfg(test)]
4804    fn new(
4805        history: Vec<HistoryEvent>,
4806        task_queue: String,
4807        payload_codec: String,
4808        resume_signal: Option<ResumeSignal>,
4809    ) -> Result<Self> {
4810        Self::new_with_identity(
4811            history,
4812            None,
4813            None,
4814            task_queue,
4815            payload_codec,
4816            resume_signal,
4817        )
4818    }
4819
4820    fn new_with_identity(
4821        history: Vec<HistoryEvent>,
4822        workflow_id: Option<String>,
4823        run_id: Option<String>,
4824        task_queue: String,
4825        payload_codec: String,
4826        resume_signal: Option<ResumeSignal>,
4827    ) -> Result<Self> {
4828        let recorded_commands = recorded_commands(
4829            &history,
4830            &payload_codec,
4831            WorkflowIdentity {
4832                workflow_id: workflow_id.clone(),
4833                run_id: run_id.clone(),
4834            },
4835        )?;
4836        let recorded_continue_as_new = history
4837            .iter()
4838            .filter(|event| event.event_type == "WorkflowContinuedAsNew")
4839            .collect::<Vec<_>>();
4840        if recorded_continue_as_new.len() > 1 {
4841            return Err(invalid_recorded_history(
4842                "duplicate_continue_as_new_transition",
4843                recorded_continue_as_new
4844                    .last()
4845                    .and_then(|event| durable_event_sequence(event))
4846                    .unwrap_or(0),
4847                "one WorkflowContinuedAsNew event",
4848                &format!(
4849                    "{} WorkflowContinuedAsNew events",
4850                    recorded_continue_as_new.len()
4851                ),
4852                "workflow history records one continue-as-new transition more than once",
4853            ));
4854        }
4855        let recorded_continue_as_new_sequence = recorded_continue_as_new
4856            .first()
4857            .map(|event| {
4858                durable_event_sequence(event).ok_or_else(|| {
4859                    Error::NonDeterministicReplay(ReplayFailure::new(
4860                        "continue_as_new_sequence_missing",
4861                        None,
4862                        Some("recorded transition sequence".to_string()),
4863                        Some("missing sequence".to_string()),
4864                        "WorkflowContinuedAsNew history is missing its recorded sequence",
4865                    ))
4866                })
4867            })
4868            .transpose()?;
4869        let event_count = u64::try_from(history.len()).unwrap_or(u64::MAX);
4870        Ok(Self {
4871            workflow_id,
4872            run_id,
4873            task_queue,
4874            payload_codec,
4875            history_budget: WorkflowHistoryBudget {
4876                event_count,
4877                ..WorkflowHistoryBudget::default()
4878            },
4879            resume_signal,
4880            recorded_commands,
4881            recorded_continue_as_new_sequence,
4882            continue_as_new_consumed: false,
4883            command_cursor: 0,
4884            matched_recorded_pending: false,
4885            version_markers: HashMap::new(),
4886            commands: Vec::new(),
4887        })
4888    }
4889}
4890
4891#[derive(Clone, Debug)]
4892enum RecordedCommand {
4893    Activity {
4894        sequence: u64,
4895        activity_type: Option<String>,
4896        options: Option<RecordedActivityOptions>,
4897        outcome: Option<ActivityOutcome>,
4898    },
4899    Timer {
4900        sequence: u64,
4901        delay_seconds: u64,
4902        fired: bool,
4903    },
4904    ChildWorkflow {
4905        sequence: u64,
4906        workflow_type: Option<String>,
4907        outcome: Option<ChildWorkflowOutcome>,
4908    },
4909    SignalWait {
4910        sequence: u64,
4911        signal_name: String,
4912        value: Option<Vec<Value>>,
4913    },
4914    SideEffect {
4915        sequence: u64,
4916        value: Value,
4917    },
4918    VersionMarker {
4919        sequence: u64,
4920        change_id: String,
4921        version: i32,
4922    },
4923}
4924
4925#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
4926struct RecordedActivityOptions {
4927    task_queue: RecordedSnapshotValue<Option<String>>,
4928    execution_mode: RecordedSnapshotValue<Option<String>>,
4929    retry_policy: ActivityRetrySnapshot,
4930}
4931
4932#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
4933enum RecordedSnapshotValue<T> {
4934    /// Older history did not persist this field, so it cannot constrain replay.
4935    Unknown,
4936    Known(T),
4937}
4938
4939impl<T: PartialEq> RecordedSnapshotValue<T> {
4940    fn matches_current(&self, current: &Self) -> bool {
4941        match self {
4942            Self::Unknown => true,
4943            Self::Known(recorded) => matches!(current, Self::Known(value) if value == recorded),
4944        }
4945    }
4946}
4947
4948#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
4949struct ActivityRetrySnapshot {
4950    snapshot_version: RecordedSnapshotValue<Option<u64>>,
4951    max_attempts: RecordedSnapshotValue<Option<u64>>,
4952    backoff_seconds: RecordedSnapshotValue<Vec<u64>>,
4953    start_to_close_timeout: RecordedSnapshotValue<Option<u64>>,
4954    schedule_to_start_timeout: RecordedSnapshotValue<Option<u64>>,
4955    schedule_to_close_timeout: RecordedSnapshotValue<Option<u64>>,
4956    heartbeat_timeout: RecordedSnapshotValue<Option<u64>>,
4957    non_retryable_error_types: RecordedSnapshotValue<Vec<String>>,
4958}
4959
4960impl ActivityRetrySnapshot {
4961    fn matches_current(&self, current: &Self) -> bool {
4962        self.snapshot_version
4963            .matches_current(&current.snapshot_version)
4964            && self.max_attempts.matches_current(&current.max_attempts)
4965            && self
4966                .backoff_seconds
4967                .matches_current(&current.backoff_seconds)
4968            && self
4969                .start_to_close_timeout
4970                .matches_current(&current.start_to_close_timeout)
4971            && self
4972                .schedule_to_start_timeout
4973                .matches_current(&current.schedule_to_start_timeout)
4974            && self
4975                .schedule_to_close_timeout
4976                .matches_current(&current.schedule_to_close_timeout)
4977            && self
4978                .heartbeat_timeout
4979                .matches_current(&current.heartbeat_timeout)
4980            && self
4981                .non_retryable_error_types
4982                .matches_current(&current.non_retryable_error_types)
4983    }
4984}
4985
4986fn recorded_optional_u64(
4987    object: Option<&serde_json::Map<String, Value>>,
4988    field: &str,
4989) -> RecordedSnapshotValue<Option<u64>> {
4990    match object.and_then(|object| object.get(field)) {
4991        None => RecordedSnapshotValue::Unknown,
4992        Some(Value::Null) => RecordedSnapshotValue::Known(None),
4993        Some(value) => RecordedSnapshotValue::Known(value_as_u64(value)),
4994    }
4995}
4996
4997fn recorded_optional_string(
4998    object: &serde_json::Map<String, Value>,
4999    field: &str,
5000) -> RecordedSnapshotValue<Option<String>> {
5001    match object.get(field) {
5002        None => RecordedSnapshotValue::Unknown,
5003        Some(Value::Null) => RecordedSnapshotValue::Known(None),
5004        Some(value) => RecordedSnapshotValue::Known(value.as_str().map(str::to_string)),
5005    }
5006}
5007
5008fn recorded_activity_retry_snapshot(policy: Option<&Value>) -> ActivityRetrySnapshot {
5009    let policy = policy.and_then(Value::as_object);
5010    let backoff_seconds = policy
5011        .and_then(|policy| policy.get("backoff_seconds"))
5012        .and_then(Value::as_array)
5013        .map(|intervals| intervals.iter().filter_map(value_as_u64).collect())
5014        .map_or(RecordedSnapshotValue::Unknown, RecordedSnapshotValue::Known);
5015    let mut non_retryable_error_types = Vec::new();
5016    for error_type in policy
5017        .and_then(|policy| policy.get("non_retryable_error_types"))
5018        .and_then(Value::as_array)
5019        .into_iter()
5020        .flatten()
5021        .filter_map(Value::as_str)
5022        .map(str::trim)
5023        .filter(|error_type| !error_type.is_empty())
5024    {
5025        if !non_retryable_error_types
5026            .iter()
5027            .any(|recorded| recorded == error_type)
5028        {
5029            non_retryable_error_types.push(error_type.to_string());
5030        }
5031    }
5032
5033    ActivityRetrySnapshot {
5034        snapshot_version: recorded_optional_u64(policy, "snapshot_version"),
5035        max_attempts: recorded_optional_u64(policy, "max_attempts"),
5036        backoff_seconds,
5037        start_to_close_timeout: recorded_optional_u64(policy, "start_to_close_timeout"),
5038        schedule_to_start_timeout: recorded_optional_u64(policy, "schedule_to_start_timeout"),
5039        schedule_to_close_timeout: recorded_optional_u64(policy, "schedule_to_close_timeout"),
5040        heartbeat_timeout: recorded_optional_u64(policy, "heartbeat_timeout"),
5041        non_retryable_error_types: if policy
5042            .is_some_and(|policy| policy.contains_key("non_retryable_error_types"))
5043        {
5044            RecordedSnapshotValue::Known(non_retryable_error_types)
5045        } else {
5046            RecordedSnapshotValue::Unknown
5047        },
5048    }
5049}
5050
5051fn current_activity_retry_snapshot(options: &ValidatedActivityOptions) -> ActivityRetrySnapshot {
5052    let policy = options.retry_policy.as_ref();
5053    let max_attempts = match policy.and_then(|policy| policy.get("max_attempts")) {
5054        Some(Value::Null) => None,
5055        Some(value) => value_as_u64(value),
5056        None => Some(1),
5057    };
5058    let backoff_seconds = policy
5059        .and_then(|policy| policy.get("backoff_seconds"))
5060        .and_then(Value::as_array)
5061        .map(|intervals| intervals.iter().filter_map(value_as_u64).collect())
5062        .unwrap_or_default();
5063    let non_retryable_error_types = policy
5064        .and_then(|policy| policy.get("non_retryable_error_types"))
5065        .and_then(Value::as_array)
5066        .into_iter()
5067        .flatten()
5068        .filter_map(Value::as_str)
5069        .map(str::to_string)
5070        .collect();
5071
5072    ActivityRetrySnapshot {
5073        snapshot_version: RecordedSnapshotValue::Known(Some(1)),
5074        max_attempts: RecordedSnapshotValue::Known(max_attempts),
5075        backoff_seconds: RecordedSnapshotValue::Known(backoff_seconds),
5076        start_to_close_timeout: RecordedSnapshotValue::Known(options.start_to_close_timeout),
5077        schedule_to_start_timeout: RecordedSnapshotValue::Known(options.schedule_to_start_timeout),
5078        schedule_to_close_timeout: RecordedSnapshotValue::Known(options.schedule_to_close_timeout),
5079        heartbeat_timeout: RecordedSnapshotValue::Known(options.heartbeat_timeout),
5080        non_retryable_error_types: RecordedSnapshotValue::Known(non_retryable_error_types),
5081    }
5082}
5083
5084fn activity_options_description(options: &RecordedActivityOptions) -> String {
5085    serde_json::to_string(options).unwrap_or_else(|_| format!("{options:?}"))
5086}
5087
5088impl RecordedCommand {
5089    fn sequence(&self) -> u64 {
5090        match self {
5091            Self::Activity { sequence, .. }
5092            | Self::Timer { sequence, .. }
5093            | Self::ChildWorkflow { sequence, .. }
5094            | Self::SignalWait { sequence, .. }
5095            | Self::SideEffect { sequence, .. }
5096            | Self::VersionMarker { sequence, .. } => *sequence,
5097        }
5098    }
5099
5100    fn shape(&self) -> &'static str {
5101        match self {
5102            Self::Activity { .. } => "activity",
5103            Self::Timer { .. } => "timer",
5104            Self::ChildWorkflow { .. } => "child workflow",
5105            Self::SignalWait { .. } => "signal wait",
5106            Self::SideEffect { .. } => "side effect",
5107            Self::VersionMarker { .. } => "version marker",
5108        }
5109    }
5110}
5111
5112fn ensure_version_supported(
5113    change_id: &str,
5114    version: i32,
5115    min_supported: i32,
5116    max_supported: i32,
5117    sequence: u64,
5118) -> Result<()> {
5119    if (min_supported..=max_supported).contains(&version) {
5120        return Ok(());
5121    }
5122    Err(Error::NonDeterministicReplay(ReplayFailure::new(
5123        "version_marker_incompatible_range",
5124        (sequence != 0).then_some(sequence),
5125        Some(format!("{min_supported}..={max_supported}")),
5126        Some(format!("{change_id}:{version}")),
5127        "recorded workflow version is outside the range supported by current code",
5128    )))
5129}
5130
5131#[derive(Clone, Debug)]
5132struct ResumeSignal {
5133    signal_name: String,
5134    arguments: Vec<Value>,
5135}
5136
5137pub struct ActivityCall {
5138    ctx: WorkflowContext,
5139    activity_type: String,
5140    options: ActivityOptions,
5141    args: Option<Result<Value>>,
5142    scheduled: bool,
5143}
5144
5145impl Future for ActivityCall {
5146    type Output = Result<Value>;
5147
5148    fn poll(mut self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
5149        let ctx = self.ctx.clone();
5150        let mut state = match ctx.state.lock() {
5151            Ok(state) => state,
5152            Err(_) => return Poll::Ready(Err(Error::WorkflowStatePoisoned)),
5153        };
5154
5155        if self.scheduled {
5156            return Poll::Pending;
5157        }
5158
5159        let options = match self.options.validate() {
5160            Ok(options) => options,
5161            Err(error) => {
5162                return Poll::Ready(Err(Error::InvalidActivityOptions(error)));
5163            }
5164        };
5165        let task_queue = options
5166            .task_queue
5167            .clone()
5168            .unwrap_or_else(|| state.task_queue.clone());
5169        let current_recorded_options = RecordedActivityOptions {
5170            task_queue: RecordedSnapshotValue::Known(Some(task_queue.clone())),
5171            // Rust schedules ordinary durable activities. The server records a
5172            // non-null mode only for a specialized execution primitive.
5173            execution_mode: RecordedSnapshotValue::Known(None),
5174            retry_policy: current_activity_retry_snapshot(&options),
5175        };
5176
5177        if let Some(recorded) = state.recorded_commands.get(state.command_cursor).cloned() {
5178            let sequence = recorded.sequence();
5179            match recorded {
5180                RecordedCommand::Activity {
5181                    activity_type,
5182                    options: recorded_options,
5183                    outcome,
5184                    ..
5185                } => {
5186                    if let Some(recorded_type) = activity_type {
5187                        if recorded_type != self.activity_type {
5188                            return Poll::Ready(Err(Error::NonDeterministicReplay(
5189                                ReplayFailure::new(
5190                                    "recorded_command_detail_mismatch",
5191                                    Some(sequence),
5192                                    Some(format!("activity:{recorded_type}")),
5193                                    Some(format!("activity:{}", self.activity_type)),
5194                                    "recorded activity type differs from the current workflow command",
5195                                ),
5196                            )));
5197                        }
5198                    }
5199                    if let Some(recorded_options) = recorded_options {
5200                        if !recorded_options
5201                            .task_queue
5202                            .matches_current(&current_recorded_options.task_queue)
5203                        {
5204                            return Poll::Ready(Err(Error::NonDeterministicReplay(
5205                                ReplayFailure::new(
5206                                    "activity_task_queue_mismatch",
5207                                    Some(sequence),
5208                                    Some(activity_options_description(&recorded_options)),
5209                                    Some(activity_options_description(&current_recorded_options)),
5210                                    "recorded activity task queue differs from the current workflow command",
5211                                ),
5212                            )));
5213                        }
5214                        if !recorded_options
5215                            .execution_mode
5216                            .matches_current(&current_recorded_options.execution_mode)
5217                        {
5218                            return Poll::Ready(Err(Error::NonDeterministicReplay(
5219                                ReplayFailure::new(
5220                                    "activity_execution_mode_mismatch",
5221                                    Some(sequence),
5222                                    Some(activity_options_description(&recorded_options)),
5223                                    Some(activity_options_description(&current_recorded_options)),
5224                                    "recorded activity execution mode differs from the current workflow command",
5225                                ),
5226                            )));
5227                        }
5228                        if !recorded_options
5229                            .retry_policy
5230                            .matches_current(&current_recorded_options.retry_policy)
5231                        {
5232                            return Poll::Ready(Err(Error::NonDeterministicReplay(
5233                                ReplayFailure::new(
5234                                    "activity_retry_policy_mismatch",
5235                                    Some(sequence),
5236                                    Some(activity_options_description(&recorded_options)),
5237                                    Some(activity_options_description(&current_recorded_options)),
5238                                    "recorded activity retry policy differs from the current workflow command",
5239                                ),
5240                            )));
5241                        }
5242                    }
5243                    state.command_cursor += 1;
5244                    if let Some(outcome) = outcome {
5245                        return Poll::Ready(outcome.map_err(Error::ActivityFailed));
5246                    }
5247                    state.matched_recorded_pending = true;
5248                    self.scheduled = true;
5249                    return Poll::Pending;
5250                }
5251                other => {
5252                    return Poll::Ready(Err(command_mismatch(
5253                        &other,
5254                        format!("activity:{}", self.activity_type),
5255                    )));
5256                }
5257            }
5258        }
5259
5260        if !self.scheduled {
5261            let args = match self.args.take().unwrap_or(Ok(Value::Null)) {
5262                Ok(args) => args,
5263                Err(error) => return Poll::Ready(Err(error)),
5264            };
5265            let arguments = normalize_arguments(args);
5266            let envelope = match encode_value_envelope(&arguments, &state.payload_codec) {
5267                Ok(envelope) => envelope,
5268                Err(error) => return Poll::Ready(Err(error)),
5269            };
5270
5271            let mut command = serde_json::Map::from_iter([
5272                ("type".to_string(), json!("schedule_activity")),
5273                (
5274                    "activity_type".to_string(),
5275                    json!(self.activity_type.clone()),
5276                ),
5277                ("queue".to_string(), json!(task_queue)),
5278                ("arguments".to_string(), envelope),
5279            ]);
5280            for (field, value) in [
5281                ("start_to_close_timeout", options.start_to_close_timeout),
5282                (
5283                    "schedule_to_start_timeout",
5284                    options.schedule_to_start_timeout,
5285                ),
5286                (
5287                    "schedule_to_close_timeout",
5288                    options.schedule_to_close_timeout,
5289                ),
5290                ("heartbeat_timeout", options.heartbeat_timeout),
5291            ] {
5292                if let Some(value) = value {
5293                    command.insert(field.to_string(), json!(value));
5294                }
5295            }
5296            if let Some(retry_policy) = options.retry_policy {
5297                command.insert("retry_policy".to_string(), retry_policy);
5298            }
5299            state.commands.push(Value::Object(command));
5300            self.scheduled = true;
5301        }
5302
5303        Poll::Pending
5304    }
5305}
5306
5307/// Future returned by [`WorkflowContext::sleep`].
5308pub struct TimerCall {
5309    ctx: WorkflowContext,
5310    delay_seconds: Option<u64>,
5311    scheduled: bool,
5312    matched_pending: bool,
5313}
5314
5315impl Future for TimerCall {
5316    type Output = Result<()>;
5317
5318    fn poll(mut self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
5319        if self.matched_pending {
5320            return Poll::Pending;
5321        }
5322
5323        let ctx = self.ctx.clone();
5324        let Some(requested_delay) = self.delay_seconds else {
5325            return Poll::Ready(Err(Error::TimerDurationOverflow));
5326        };
5327        let mut state = match ctx.state.lock() {
5328            Ok(state) => state,
5329            Err(_) => return Poll::Ready(Err(Error::WorkflowStatePoisoned)),
5330        };
5331
5332        if let Some(recorded) = state.recorded_commands.get(state.command_cursor).cloned() {
5333            match recorded {
5334                RecordedCommand::Timer {
5335                    sequence,
5336                    delay_seconds,
5337                    fired,
5338                    ..
5339                } => {
5340                    if delay_seconds != requested_delay {
5341                        return Poll::Ready(Err(Error::NonDeterministicReplay(
5342                            ReplayFailure::new(
5343                                "timer_delay_mismatch",
5344                                Some(sequence),
5345                                Some(format!("timer:{delay_seconds}s")),
5346                                Some(format!("timer:{requested_delay}s")),
5347                                "recorded timer delay differs from the current workflow command",
5348                            ),
5349                        )));
5350                    }
5351                    state.command_cursor += 1;
5352                    if fired {
5353                        return Poll::Ready(Ok(()));
5354                    }
5355                    state.matched_recorded_pending = true;
5356                    self.scheduled = true;
5357                    self.matched_pending = true;
5358                    return Poll::Pending;
5359                }
5360                other => return Poll::Ready(Err(command_mismatch(&other, "timer"))),
5361            }
5362        }
5363
5364        if !self.scheduled {
5365            state.commands.push(json!({
5366                "type": "start_timer",
5367                "delay_seconds": requested_delay,
5368            }));
5369            self.scheduled = true;
5370        }
5371
5372        Poll::Pending
5373    }
5374}
5375
5376/// Future returned by [`WorkflowContext::start_child_workflow`].
5377pub struct ChildWorkflowCall {
5378    ctx: WorkflowContext,
5379    workflow_type: String,
5380    options: ChildWorkflowOptions,
5381    args: Option<Result<Value>>,
5382    scheduled: bool,
5383    matched_pending: bool,
5384}
5385
5386impl Future for ChildWorkflowCall {
5387    type Output = Result<ChildWorkflowResult>;
5388
5389    fn poll(mut self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
5390        if self.matched_pending {
5391            return Poll::Pending;
5392        }
5393
5394        let ctx = self.ctx.clone();
5395        let mut state = match ctx.state.lock() {
5396            Ok(state) => state,
5397            Err(_) => return Poll::Ready(Err(Error::WorkflowStatePoisoned)),
5398        };
5399
5400        if let Some(recorded) = state.recorded_commands.get(state.command_cursor).cloned() {
5401            let sequence = recorded.sequence();
5402            match recorded {
5403                RecordedCommand::ChildWorkflow {
5404                    workflow_type,
5405                    outcome,
5406                    ..
5407                } => {
5408                    if let Some(recorded_type) = workflow_type {
5409                        if recorded_type != self.workflow_type {
5410                            return Poll::Ready(Err(Error::NonDeterministicReplay(
5411                                ReplayFailure::new(
5412                                    "recorded_command_detail_mismatch",
5413                                    Some(sequence),
5414                                    Some(format!("child workflow:{recorded_type}")),
5415                                    Some(format!("child workflow:{}", self.workflow_type)),
5416                                    "recorded child workflow type differs from the current workflow command",
5417                                ),
5418                            )));
5419                        }
5420                    }
5421                    state.command_cursor += 1;
5422                    if let Some(outcome) = outcome {
5423                        return Poll::Ready(outcome.map_err(Error::ChildWorkflowFailed));
5424                    }
5425                    state.matched_recorded_pending = true;
5426                    self.scheduled = true;
5427                    self.matched_pending = true;
5428                    return Poll::Pending;
5429                }
5430                other => {
5431                    return Poll::Ready(Err(command_mismatch(
5432                        &other,
5433                        format!("child workflow:{}", self.workflow_type),
5434                    )));
5435                }
5436            }
5437        }
5438
5439        if !self.scheduled {
5440            if self.options.task_queue.trim().is_empty() {
5441                return Poll::Ready(Err(Error::InvalidChildWorkflowOptions(
5442                    "task_queue must not be empty".to_string(),
5443                )));
5444            }
5445            for (name, value) in [
5446                (
5447                    "execution_timeout_seconds",
5448                    self.options.execution_timeout_seconds,
5449                ),
5450                ("run_timeout_seconds", self.options.run_timeout_seconds),
5451            ] {
5452                if value == Some(0) {
5453                    return Poll::Ready(Err(Error::InvalidChildWorkflowOptions(format!(
5454                        "{name} must be at least 1"
5455                    ))));
5456                }
5457            }
5458
5459            let args = match self.args.take().unwrap_or(Ok(Value::Null)) {
5460                Ok(args) => args,
5461                Err(error) => return Poll::Ready(Err(error)),
5462            };
5463            let arguments =
5464                match encode_value_envelope(&normalize_arguments(args), &state.payload_codec) {
5465                    Ok(arguments) => arguments,
5466                    Err(error) => return Poll::Ready(Err(error)),
5467                };
5468            let mut command = json!({
5469                "type": "start_child_workflow",
5470                "workflow_type": self.workflow_type,
5471                "queue": self.options.task_queue,
5472                "parent_close_policy": self.options.parent_close_policy.as_str(),
5473                "arguments": arguments,
5474            });
5475            let object = command
5476                .as_object_mut()
5477                .expect("child workflow command is always an object");
5478            if let Some(policy) = &self.options.retry_policy {
5479                let mut retry_policy = serde_json::Map::new();
5480                if let Some(max_attempts) = policy.max_attempts {
5481                    if max_attempts == 0 {
5482                        return Poll::Ready(Err(Error::InvalidChildWorkflowOptions(
5483                            "retry_policy.max_attempts must be at least 1".to_string(),
5484                        )));
5485                    }
5486                    retry_policy.insert("max_attempts".to_string(), json!(max_attempts));
5487                }
5488                if !policy.backoff_seconds.is_empty() {
5489                    retry_policy
5490                        .insert("backoff_seconds".to_string(), json!(policy.backoff_seconds));
5491                }
5492                if !policy.non_retryable_error_types.is_empty() {
5493                    retry_policy.insert(
5494                        "non_retryable_error_types".to_string(),
5495                        json!(policy.non_retryable_error_types),
5496                    );
5497                }
5498                if retry_policy.is_empty() {
5499                    return Poll::Ready(Err(Error::InvalidChildWorkflowOptions(
5500                        "retry_policy must configure at least one field".to_string(),
5501                    )));
5502                }
5503                object.insert("retry_policy".to_string(), Value::Object(retry_policy));
5504            }
5505            if let Some(seconds) = self.options.execution_timeout_seconds {
5506                object.insert("execution_timeout_seconds".to_string(), json!(seconds));
5507            }
5508            if let Some(seconds) = self.options.run_timeout_seconds {
5509                object.insert("run_timeout_seconds".to_string(), json!(seconds));
5510            }
5511            state.commands.push(command);
5512            self.scheduled = true;
5513        }
5514
5515        Poll::Pending
5516    }
5517}
5518
5519fn command_mismatch(recorded: &RecordedCommand, actual: impl Into<String>) -> Error {
5520    Error::NonDeterministicReplay(ReplayFailure::new(
5521        "recorded_command_mismatch",
5522        Some(recorded.sequence()),
5523        Some(recorded.shape().to_string()),
5524        Some(actual.into()),
5525        "current workflow command does not match the recorded durable command sequence",
5526    ))
5527}
5528
5529pub struct SignalCall {
5530    ctx: WorkflowContext,
5531    signal_name: String,
5532    opened_wait: bool,
5533    matched_pending: bool,
5534}
5535
5536impl Future for SignalCall {
5537    type Output = Result<Vec<Value>>;
5538
5539    fn poll(mut self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
5540        if self.matched_pending {
5541            return Poll::Pending;
5542        }
5543
5544        let ctx = self.ctx.clone();
5545        let mut state = match ctx.state.lock() {
5546            Ok(state) => state,
5547            Err(_) => return Poll::Ready(Err(Error::WorkflowStatePoisoned)),
5548        };
5549
5550        if let Some(recorded) = state.recorded_commands.get(state.command_cursor).cloned() {
5551            match recorded {
5552                RecordedCommand::SignalWait {
5553                    sequence,
5554                    signal_name,
5555                    value,
5556                } => {
5557                    if signal_name != self.signal_name {
5558                        return Poll::Ready(Err(Error::NonDeterministicReplay(
5559                            ReplayFailure::new(
5560                                "recorded_command_detail_mismatch",
5561                                Some(sequence),
5562                                Some(format!("signal wait:{signal_name}")),
5563                                Some(format!("signal wait:{}", self.signal_name)),
5564                                "recorded signal name differs from the current workflow command",
5565                            ),
5566                        )));
5567                    }
5568
5569                    state.command_cursor += 1;
5570                    if let Some(value) = value {
5571                        return Poll::Ready(Ok(value));
5572                    }
5573                    if state
5574                        .resume_signal
5575                        .as_ref()
5576                        .is_some_and(|signal| signal.signal_name == self.signal_name)
5577                    {
5578                        let signal = state
5579                            .resume_signal
5580                            .take()
5581                            .expect("matching resume signal is present");
5582                        return Poll::Ready(Ok(signal.arguments));
5583                    }
5584
5585                    state.matched_recorded_pending = true;
5586                    self.opened_wait = true;
5587                    self.matched_pending = true;
5588                    return Poll::Pending;
5589                }
5590                other => {
5591                    return Poll::Ready(Err(command_mismatch(
5592                        &other,
5593                        format!("signal wait:{}", self.signal_name),
5594                    )));
5595                }
5596            }
5597        }
5598
5599        if state
5600            .resume_signal
5601            .as_ref()
5602            .is_some_and(|signal| signal.signal_name == self.signal_name)
5603        {
5604            let signal = state
5605                .resume_signal
5606                .take()
5607                .expect("matching resume signal is present");
5608            return Poll::Ready(Ok(signal.arguments));
5609        }
5610
5611        if !self.opened_wait {
5612            state.commands.push(json!({
5613                "type": "open_signal_wait",
5614                "signal_name": self.signal_name
5615            }));
5616            self.opened_wait = true;
5617        }
5618
5619        Poll::Pending
5620    }
5621}
5622
5623#[derive(Clone, Debug)]
5624pub struct ActivityContext {
5625    client: Client,
5626    pub task_id: String,
5627    pub activity_attempt_id: String,
5628    pub lease_owner: String,
5629    pub activity_type: String,
5630    pub attempt_number: u64,
5631    pub task_queue: String,
5632    pub worker_id: String,
5633}
5634
5635impl ActivityContext {
5636    pub async fn heartbeat<T: Serialize>(&self, details: T) -> Result<ActivityHeartbeatResponse> {
5637        self.client
5638            .heartbeat_activity_task(
5639                &self.task_id,
5640                &self.activity_attempt_id,
5641                &self.lease_owner,
5642                serde_json::to_value(details)?,
5643            )
5644            .await
5645    }
5646}
5647
5648fn decode_task_arguments(value: Option<&Value>, codec: &str) -> Result<Value> {
5649    match value {
5650        Some(value) => Ok(normalize_arguments(decode_wire_value(value, codec)?)),
5651        None => Ok(Value::Array(Vec::new())),
5652    }
5653}
5654
5655fn decode_resume_signal(task: &WorkflowTask) -> Result<Option<ResumeSignal>> {
5656    let Some(signal_name) = task
5657        .signal_name
5658        .as_deref()
5659        .filter(|value| !value.is_empty())
5660    else {
5661        return Ok(None);
5662    };
5663    let Some(arguments) = task.signal_arguments.as_ref() else {
5664        return Ok(None);
5665    };
5666
5667    let decoded = normalize_arguments(decode_wire_value(arguments, &task.payload_codec)?);
5668    let Value::Array(arguments) = decoded else {
5669        unreachable!("normalize_arguments always returns an array");
5670    };
5671
5672    Ok(Some(ResumeSignal {
5673        signal_name: signal_name.to_string(),
5674        arguments,
5675    }))
5676}
5677
5678fn normalize_arguments(value: Value) -> Value {
5679    match value {
5680        Value::Null => Value::Array(Vec::new()),
5681        Value::Array(_) => value,
5682        other => Value::Array(vec![other]),
5683    }
5684}
5685
5686fn recorded_commands(
5687    events: &[HistoryEvent],
5688    fallback_codec: &str,
5689    parent: WorkflowIdentity,
5690) -> Result<Vec<RecordedCommand>> {
5691    let mut events_by_sequence: BTreeMap<u64, Vec<&HistoryEvent>> = BTreeMap::new();
5692    let mut last_new_sequence = None;
5693
5694    for event in events {
5695        let is_activity = matches!(
5696            event.event_type.as_str(),
5697            "ActivityScheduled"
5698                | "ActivityStarted"
5699                | "ActivityHeartbeatRecorded"
5700                | "ActivityRetryScheduled"
5701                | "ActivityCompleted"
5702                | "ActivityFailed"
5703                | "ActivityCancelled"
5704                | "ActivityTimedOut"
5705        );
5706        let is_workflow_timer = matches!(
5707            event.event_type.as_str(),
5708            "TimerScheduled" | "TimerCancelled" | "TimerFired"
5709        ) && !is_internal_timer_event(event);
5710        let is_child_workflow = matches!(
5711            event.event_type.as_str(),
5712            "ChildWorkflowScheduled"
5713                | "ChildRunCompleted"
5714                | "ChildRunFailed"
5715                | "ChildRunCancelled"
5716                | "ChildRunTerminated"
5717        );
5718        let is_signal_wait = is_recorded_signal_wait_event(event);
5719        let is_side_effect = event.event_type == "SideEffectRecorded";
5720        let is_version_marker = event.event_type == "VersionMarkerRecorded";
5721        if !is_activity
5722            && !is_workflow_timer
5723            && !is_child_workflow
5724            && !is_signal_wait
5725            && !is_side_effect
5726            && !is_version_marker
5727        {
5728            continue;
5729        }
5730
5731        let sequence = durable_event_sequence(event).ok_or_else(|| {
5732            Error::NonDeterministicReplay(ReplayFailure::new(
5733                "durable_command_sequence_missing",
5734                None,
5735                Some("positive workflow sequence".to_string()),
5736                Some(event.event_type.clone()),
5737                "durable command history event has no workflow sequence",
5738            ))
5739        })?;
5740        if sequence == 0 {
5741            return Err(Error::NonDeterministicReplay(ReplayFailure::new(
5742                "durable_command_sequence_invalid",
5743                Some(sequence),
5744                Some("positive workflow sequence".to_string()),
5745                Some(sequence.to_string()),
5746                "durable command history uses an invalid workflow sequence",
5747            )));
5748        }
5749        if !events_by_sequence.contains_key(&sequence) {
5750            if let Some(previous) = last_new_sequence {
5751                if sequence < previous {
5752                    return Err(invalid_recorded_history(
5753                        "durable_command_sequence_mismatch",
5754                        sequence,
5755                        &format!("workflow sequence greater than {previous}"),
5756                        &sequence.to_string(),
5757                        "durable commands are not strictly ordered by their recorded workflow sequence",
5758                    ));
5759                }
5760            }
5761            last_new_sequence = Some(sequence);
5762        }
5763        events_by_sequence.entry(sequence).or_default().push(event);
5764    }
5765
5766    let commands: Vec<RecordedCommand> = events_by_sequence
5767        .into_iter()
5768        .map(|(sequence, sequence_events)| {
5769            let activity_events: Vec<_> = sequence_events
5770                .iter()
5771                .copied()
5772                .filter(|event| event.event_type.starts_with("Activity"))
5773                .collect();
5774            let timer_events: Vec<_> = sequence_events
5775                .iter()
5776                .copied()
5777                .filter(|event| event.event_type.starts_with("Timer"))
5778                .collect();
5779            let child_events: Vec<_> = sequence_events
5780                .iter()
5781                .copied()
5782                .filter(|event| {
5783                    event.event_type == "ChildWorkflowScheduled"
5784                        || event.event_type.starts_with("ChildRun")
5785                })
5786                .collect();
5787            let signal_wait_events: Vec<_> = sequence_events
5788                .iter()
5789                .copied()
5790                .filter(|event| is_recorded_signal_wait_event(event))
5791                .collect();
5792            let side_effect_events: Vec<_> = sequence_events
5793                .iter()
5794                .copied()
5795                .filter(|event| event.event_type == "SideEffectRecorded")
5796                .collect();
5797            let version_marker_events: Vec<_> = sequence_events
5798                .iter()
5799                .copied()
5800                .filter(|event| event.event_type == "VersionMarkerRecorded")
5801                .collect();
5802
5803            let command_kind_count = usize::from(!activity_events.is_empty())
5804                + usize::from(!timer_events.is_empty())
5805                + usize::from(!child_events.is_empty())
5806                + usize::from(!signal_wait_events.is_empty())
5807                + usize::from(!side_effect_events.is_empty())
5808                + usize::from(!version_marker_events.is_empty());
5809            if command_kind_count > 1 {
5810                let actual = [
5811                    (!activity_events.is_empty()).then_some("activity"),
5812                    (!timer_events.is_empty()).then_some("timer"),
5813                    (!child_events.is_empty()).then_some("child workflow"),
5814                    (!signal_wait_events.is_empty()).then_some("signal wait"),
5815                    (!side_effect_events.is_empty()).then_some("side effect"),
5816                    (!version_marker_events.is_empty()).then_some("version marker"),
5817                ]
5818                .into_iter()
5819                .flatten()
5820                .collect::<Vec<_>>()
5821                .join(" and ");
5822                return Err(invalid_recorded_history(
5823                    "durable_command_sequence_collision",
5824                    sequence,
5825                    "one durable command kind",
5826                    &actual,
5827                    "one workflow sequence records more than one durable command kind",
5828                ));
5829            }
5830
5831            if !activity_events.is_empty() {
5832                let scheduled_count = activity_events
5833                    .iter()
5834                    .filter(|event| event.event_type == "ActivityScheduled")
5835                    .count();
5836                if scheduled_count > 1 {
5837                    return Err(invalid_recorded_history(
5838                        "duplicate_activity_schedule",
5839                        sequence,
5840                        "at most one ActivityScheduled event",
5841                        "multiple ActivityScheduled events",
5842                        "activity history schedules more than one command at one workflow sequence",
5843                    ));
5844                }
5845                let activity_type = activity_events.iter().find_map(|event| {
5846                    event
5847                        .payload
5848                        .get("activity_type")
5849                        .or_else(|| event.payload.get("activity_name"))
5850                        .and_then(Value::as_str)
5851                        .map(str::to_string)
5852                });
5853                if activity_events.iter().filter_map(|event| {
5854                    event
5855                        .payload
5856                        .get("activity_type")
5857                        .or_else(|| event.payload.get("activity_name"))
5858                        .and_then(Value::as_str)
5859                }).any(|candidate| Some(candidate) != activity_type.as_deref()) {
5860                    return Err(invalid_recorded_history(
5861                        "activity_identity_mismatch",
5862                        sequence,
5863                        activity_type.as_deref().unwrap_or("one activity identity"),
5864                        "conflicting activity identities",
5865                        "activity lifecycle events at one workflow sequence disagree on identity",
5866                    ));
5867                }
5868                let terminal: Vec<_> = activity_events
5869                    .iter()
5870                    .copied()
5871                    .filter(|event| {
5872                        matches!(
5873                            event.event_type.as_str(),
5874                            "ActivityCompleted"
5875                                | "ActivityFailed"
5876                                | "ActivityCancelled"
5877                                | "ActivityTimedOut"
5878                        )
5879                    })
5880                    .collect();
5881                if terminal.len() > 1 {
5882                    return Err(invalid_recorded_history(
5883                        "duplicate_activity_terminal_event",
5884                        sequence,
5885                        "at most one terminal activity event",
5886                        "multiple terminal activity events",
5887                        "activity history settles one command more than once",
5888                    ));
5889                }
5890                let outcome = terminal
5891                    .first()
5892                    .map(|event| activity_outcome(event, fallback_codec, activity_type.clone()))
5893                    .transpose()?;
5894                let options = activity_events
5895                    .iter()
5896                    .find(|event| event.event_type == "ActivityScheduled")
5897                    .and_then(|event| event.payload.get("activity"))
5898                    .and_then(Value::as_object)
5899                    .map(|activity| RecordedActivityOptions {
5900                        task_queue: recorded_optional_string(activity, "queue"),
5901                        execution_mode: recorded_optional_string(activity, "execution_mode"),
5902                        retry_policy: recorded_activity_retry_snapshot(
5903                            activity.get("retry_policy"),
5904                        ),
5905                    });
5906                return Ok(RecordedCommand::Activity {
5907                    sequence,
5908                    activity_type,
5909                    options,
5910                    outcome,
5911                });
5912            }
5913
5914            if !child_events.is_empty() {
5915                let scheduled: Vec<_> = child_events
5916                    .iter()
5917                    .copied()
5918                    .filter(|event| event.event_type == "ChildWorkflowScheduled")
5919                    .collect();
5920                if scheduled.len() != 1 {
5921                    return Err(invalid_recorded_history(
5922                        "child_workflow_schedule_missing_or_duplicate",
5923                        sequence,
5924                        "one ChildWorkflowScheduled event",
5925                        &format!("{} ChildWorkflowScheduled events", scheduled.len()),
5926                        "child workflow replay requires exactly one recorded schedule event",
5927                    ));
5928                }
5929                let workflow_type = child_events.iter().find_map(|event| {
5930                    event
5931                        .payload
5932                        .get("child_workflow_type")
5933                        .or_else(|| event.payload.get("workflow_type"))
5934                        .and_then(Value::as_str)
5935                        .filter(|value| !value.is_empty())
5936                        .map(str::to_string)
5937                });
5938                if child_events
5939                    .iter()
5940                    .filter_map(|event| {
5941                        event
5942                            .payload
5943                            .get("child_workflow_type")
5944                            .or_else(|| event.payload.get("workflow_type"))
5945                            .and_then(Value::as_str)
5946                    })
5947                    .any(|candidate| Some(candidate) != workflow_type.as_deref())
5948                {
5949                    return Err(invalid_recorded_history(
5950                        "child_workflow_identity_mismatch",
5951                        sequence,
5952                        workflow_type
5953                            .as_deref()
5954                            .unwrap_or("one child workflow type"),
5955                        "conflicting child workflow types",
5956                        "child workflow lifecycle events at one sequence disagree on type",
5957                    ));
5958                }
5959                let mut outcomes = child_workflow_outcomes(
5960                    &child_events.iter().map(|event| (*event).clone()).collect::<Vec<_>>(),
5961                    fallback_codec,
5962                    parent.clone(),
5963                )?;
5964                if outcomes.len() > 1 {
5965                    return Err(invalid_recorded_history(
5966                        "duplicate_child_workflow_terminal_event",
5967                        sequence,
5968                        "at most one terminal child event",
5969                        "multiple terminal child events",
5970                        "child workflow history settles one command more than once",
5971                    ));
5972                }
5973                return Ok(RecordedCommand::ChildWorkflow {
5974                    sequence,
5975                    workflow_type,
5976                    outcome: outcomes.pop(),
5977                });
5978            }
5979
5980            if !signal_wait_events.is_empty() {
5981                let opened: Vec<_> = signal_wait_events
5982                    .iter()
5983                    .copied()
5984                    .filter(|event| event.event_type == "SignalWaitOpened")
5985                    .collect();
5986                if opened.len() != 1 {
5987                    return Err(invalid_recorded_history(
5988                        "signal_wait_open_missing_or_duplicate",
5989                        sequence,
5990                        "one SignalWaitOpened event",
5991                        &format!("{} SignalWaitOpened events", opened.len()),
5992                        "signal replay requires exactly one canonical wait-open event",
5993                    ));
5994                }
5995
5996                let applied: Vec<_> = signal_wait_events
5997                    .iter()
5998                    .copied()
5999                    .filter(|event| event.event_type == "SignalApplied")
6000                    .collect();
6001                if applied.len() > 1 {
6002                    return Err(invalid_recorded_history(
6003                        "duplicate_signal_wait_apply",
6004                        sequence,
6005                        "at most one SignalApplied event",
6006                        "multiple SignalApplied events",
6007                        "signal history applies one durable wait more than once",
6008                    ));
6009                }
6010
6011                let signal_names = signal_wait_events
6012                    .iter()
6013                    .map(|event| required_signal_wait_name(event, sequence))
6014                    .collect::<Result<Vec<_>>>()?;
6015                let signal_name = signal_names
6016                    .first()
6017                    .expect("signal wait events are not empty")
6018                    .clone();
6019                if signal_names.iter().any(|candidate| candidate != &signal_name) {
6020                    return Err(invalid_recorded_history(
6021                        "signal_wait_identity_mismatch",
6022                        sequence,
6023                        &signal_name,
6024                        "conflicting signal names",
6025                        "signal wait lifecycle events at one workflow sequence disagree on identity",
6026                    ));
6027                }
6028                let value = applied
6029                    .first()
6030                    .map(|event| decode_signal_event_arguments(event, fallback_codec))
6031                    .transpose()?;
6032                return Ok(RecordedCommand::SignalWait {
6033                    sequence,
6034                    signal_name,
6035                    value,
6036                });
6037            }
6038
6039            if !side_effect_events.is_empty() {
6040                if side_effect_events.len() != 1 {
6041                    return Err(invalid_recorded_history(
6042                        "duplicate_side_effect_record",
6043                        sequence,
6044                        "one SideEffectRecorded event",
6045                        &format!("{} SideEffectRecorded events", side_effect_events.len()),
6046                        "side-effect history records one workflow command more than once",
6047                    ));
6048                }
6049                let event = side_effect_events[0];
6050                let result = event.payload.get("result").ok_or_else(|| {
6051                    invalid_recorded_history(
6052                        "side_effect_result_missing",
6053                        sequence,
6054                        "recorded result payload",
6055                        "missing result",
6056                        "side-effect history is missing its recorded value",
6057                    )
6058                })?;
6059                let has_published_envelope = result.as_str().is_some()
6060                    || result.as_object().is_some_and(|envelope| {
6061                        envelope.get("codec").and_then(Value::as_str).is_some()
6062                            && envelope.get("blob").and_then(Value::as_str).is_some()
6063                    });
6064                if !has_published_envelope {
6065                    return Err(invalid_recorded_history(
6066                        "side_effect_payload_malformed",
6067                        sequence,
6068                        "payload blob or {codec, blob} envelope",
6069                        &result.to_string(),
6070                        "side-effect history result does not use a published payload envelope",
6071                    ));
6072                }
6073                let codec = event
6074                    .payload
6075                    .get("payload_codec")
6076                    .and_then(Value::as_str)
6077                    .unwrap_or(fallback_codec);
6078                let value = decode_wire_value(result, codec).map_err(|error| {
6079                    invalid_recorded_history(
6080                        "side_effect_payload_incompatible",
6081                        sequence,
6082                        &format!("valid {codec} payload envelope"),
6083                        &error.to_string(),
6084                        "side-effect history payload cannot be decoded with its recorded codec",
6085                    )
6086                })?;
6087                return Ok(RecordedCommand::SideEffect { sequence, value });
6088            }
6089
6090            if !version_marker_events.is_empty() {
6091                if version_marker_events.len() != 1 {
6092                    return Err(invalid_recorded_history(
6093                        "duplicate_version_marker_record",
6094                        sequence,
6095                        "one VersionMarkerRecorded event",
6096                        &format!("{} VersionMarkerRecorded events", version_marker_events.len()),
6097                        "version-marker history records one workflow command more than once",
6098                    ));
6099                }
6100                let payload = &version_marker_events[0].payload;
6101                let change_id = payload
6102                    .get("change_id")
6103                    .and_then(Value::as_str)
6104                    .filter(|value| !value.is_empty())
6105                    .map(str::to_string)
6106                    .ok_or_else(|| {
6107                        invalid_recorded_history(
6108                            "version_marker_field_missing",
6109                            sequence,
6110                            "non-empty change_id",
6111                            "missing or invalid change_id",
6112                            "version-marker history is missing its stable change ID",
6113                        )
6114                    })?;
6115                let version = required_version_i32(payload, "version", sequence)?;
6116                let min_supported = required_version_i32(payload, "min_supported", sequence)?;
6117                let max_supported = required_version_i32(payload, "max_supported", sequence)?;
6118                if min_supported > max_supported || version < min_supported || version > max_supported {
6119                    return Err(invalid_recorded_history(
6120                        "version_marker_history_range_invalid",
6121                        sequence,
6122                        "min_supported <= version <= max_supported",
6123                        &format!("{min_supported} <= {version} <= {max_supported}"),
6124                        "recorded version marker contains an internally incompatible range",
6125                    ));
6126                }
6127                return Ok(RecordedCommand::VersionMarker {
6128                    sequence,
6129                    change_id,
6130                    version,
6131                });
6132            }
6133
6134            let scheduled: Vec<_> = timer_events
6135                .iter()
6136                .copied()
6137                .filter(|event| event.event_type == "TimerScheduled")
6138                .collect();
6139            let fired: Vec<_> = timer_events
6140                .iter()
6141                .copied()
6142                .filter(|event| event.event_type == "TimerFired")
6143                .collect();
6144            if scheduled.len() != 1 {
6145                return Err(invalid_recorded_history(
6146                    "timer_schedule_missing_or_duplicate",
6147                    sequence,
6148                    "one TimerScheduled event",
6149                    &format!("{} TimerScheduled events", scheduled.len()),
6150                    "timer replay requires exactly one recorded schedule event",
6151                ));
6152            }
6153            if fired.len() > 1 {
6154                return Err(invalid_recorded_history(
6155                    "duplicate_timer_fire",
6156                    sequence,
6157                    "at most one TimerFired event",
6158                    "multiple TimerFired events",
6159                    "timer history contains more than one fire event for a workflow sequence",
6160                ));
6161            }
6162
6163            let scheduled = scheduled[0];
6164            let timer_id = required_history_string(scheduled, "timer_id", sequence)?;
6165            let delay_seconds = required_history_u64(scheduled, "delay_seconds", sequence)?;
6166            if let Some(fired) = fired.first() {
6167                let fired_timer_id = required_history_string(fired, "timer_id", sequence)?;
6168                if fired_timer_id != timer_id {
6169                    return Err(invalid_recorded_history(
6170                        "timer_identity_mismatch",
6171                        sequence,
6172                        &timer_id,
6173                        &fired_timer_id,
6174                        "TimerFired does not correspond to the recorded TimerScheduled event",
6175                    ));
6176                }
6177                let fired_delay = required_history_u64(fired, "delay_seconds", sequence)?;
6178                if fired_delay != delay_seconds {
6179                    return Err(invalid_recorded_history(
6180                        "timer_history_delay_mismatch",
6181                        sequence,
6182                        &delay_seconds.to_string(),
6183                        &fired_delay.to_string(),
6184                        "TimerScheduled and TimerFired record different delays",
6185                    ));
6186                }
6187            }
6188
6189            Ok(RecordedCommand::Timer {
6190                sequence,
6191                delay_seconds,
6192                fired: !fired.is_empty(),
6193            })
6194        })
6195        .collect::<Result<_>>()?;
6196
6197    let mut marker_sequences = HashMap::new();
6198    for command in &commands {
6199        if let RecordedCommand::VersionMarker {
6200            sequence,
6201            change_id,
6202            ..
6203        } = command
6204        {
6205            if let Some(first_sequence) = marker_sequences.insert(change_id.clone(), *sequence) {
6206                return Err(invalid_recorded_history(
6207                    "duplicate_version_marker",
6208                    *sequence,
6209                    &format!("one marker for change ID {change_id:?}"),
6210                    &format!("markers at sequences {first_sequence} and {sequence}"),
6211                    "workflow history contains duplicate markers for one stable change ID",
6212                ));
6213            }
6214        }
6215    }
6216
6217    Ok(commands)
6218}
6219
6220fn required_version_i32(payload: &Value, field: &str, sequence: u64) -> Result<i32> {
6221    payload
6222        .get(field)
6223        .and_then(Value::as_i64)
6224        .and_then(|value| i32::try_from(value).ok())
6225        .ok_or_else(|| {
6226            invalid_recorded_history(
6227                "version_marker_field_missing",
6228                sequence,
6229                &format!("integer {field}"),
6230                "missing or out-of-range integer",
6231                "version-marker history is missing a required integer field",
6232            )
6233        })
6234}
6235
6236fn durable_event_sequence(event: &HistoryEvent) -> Option<u64> {
6237    event
6238        .payload
6239        .get("sequence")
6240        .or_else(|| event.payload.get("workflow_sequence"))
6241        .or_else(|| event.raw.get("sequence"))
6242        .or_else(|| event.raw.get("workflow_sequence"))
6243        .and_then(value_as_u64)
6244}
6245
6246fn is_internal_timer_event(event: &HistoryEvent) -> bool {
6247    matches!(
6248        event
6249            .payload
6250            .get("timer_kind")
6251            .or_else(|| event.raw.get("timer_kind"))
6252            .and_then(Value::as_str),
6253        Some("condition_timeout" | "signal_timeout")
6254    )
6255}
6256
6257fn required_signal_wait_name(event: &HistoryEvent, sequence: u64) -> Result<String> {
6258    event
6259        .payload
6260        .get("signal_name")
6261        .or_else(|| event.raw.get("signal_name"))
6262        .and_then(Value::as_str)
6263        .filter(|value| !value.is_empty())
6264        .map(str::to_string)
6265        .ok_or_else(|| {
6266            invalid_recorded_history(
6267                "signal_wait_name_missing",
6268                sequence,
6269                "non-empty signal_name",
6270                &event.event_type,
6271                "canonical signal-wait history is missing its signal identity",
6272            )
6273        })
6274}
6275
6276fn is_recorded_signal_wait_event(event: &HistoryEvent) -> bool {
6277    matches!(
6278        event.event_type.as_str(),
6279        "SignalWaitOpened" | "SignalApplied"
6280    )
6281}
6282
6283fn required_history_string(event: &HistoryEvent, field: &str, sequence: u64) -> Result<String> {
6284    event
6285        .payload
6286        .get(field)
6287        .and_then(Value::as_str)
6288        .filter(|value| !value.is_empty())
6289        .map(str::to_string)
6290        .ok_or_else(|| {
6291            invalid_recorded_history(
6292                "timer_history_field_missing",
6293                sequence,
6294                field,
6295                &event.event_type,
6296                "timer history is missing a required identity field",
6297            )
6298        })
6299}
6300
6301fn required_history_u64(event: &HistoryEvent, field: &str, sequence: u64) -> Result<u64> {
6302    event
6303        .payload
6304        .get(field)
6305        .and_then(value_as_u64)
6306        .ok_or_else(|| {
6307            invalid_recorded_history(
6308                "timer_history_field_missing",
6309                sequence,
6310                field,
6311                &event.event_type,
6312                "timer history is missing a required numeric field",
6313            )
6314        })
6315}
6316
6317fn invalid_recorded_history(
6318    reason: &str,
6319    sequence: u64,
6320    expected: &str,
6321    actual: &str,
6322    message: &str,
6323) -> Error {
6324    Error::NonDeterministicReplay(ReplayFailure::new(
6325        reason,
6326        Some(sequence),
6327        Some(expected.to_string()),
6328        Some(actual.to_string()),
6329        message,
6330    ))
6331}
6332
6333type ActivityOutcome = std::result::Result<Value, ActivityFailure>;
6334
6335fn activity_outcome(
6336    event: &HistoryEvent,
6337    fallback_codec: &str,
6338    recorded_activity_type: Option<String>,
6339) -> Result<ActivityOutcome> {
6340    if event.event_type == "ActivityCompleted" {
6341        let codec = event
6342            .payload
6343            .get("payload_codec")
6344            .and_then(Value::as_str)
6345            .unwrap_or(fallback_codec);
6346        return Ok(Ok(decode_wire_value(
6347            event.payload.get("result").unwrap_or(&Value::Null),
6348            codec,
6349        )?));
6350    }
6351
6352    let payload = &event.payload;
6353    let (kind, fallback_reason, fallback_message) = match event.event_type.as_str() {
6354        "ActivityFailed" => (ActivityFailureKind::Failed, "activity", "activity failed"),
6355        "ActivityCancelled" => (
6356            ActivityFailureKind::Cancelled,
6357            "cancelled",
6358            "activity was cancelled",
6359        ),
6360        "ActivityTimedOut" => (
6361            ActivityFailureKind::TimedOut,
6362            "timeout",
6363            "activity timed out",
6364        ),
6365        _ => unreachable!("activity_outcome is called only for terminal activity events"),
6366    };
6367    let exception = payload
6368        .get("exception")
6369        .filter(|value| !value.is_null())
6370        .cloned();
6371    let failure_category = payload_string(payload, "failure_category");
6372    let timeout_kind = payload_string(payload, "timeout_kind");
6373    let reason = payload_string(payload, "reason").unwrap_or_else(|| match kind {
6374        ActivityFailureKind::Failed => failure_category
6375            .clone()
6376            .unwrap_or_else(|| fallback_reason.to_string()),
6377        ActivityFailureKind::Cancelled => fallback_reason.to_string(),
6378        ActivityFailureKind::TimedOut => timeout_kind
6379            .clone()
6380            .unwrap_or_else(|| fallback_reason.to_string()),
6381    });
6382    let message = payload_string(payload, "message")
6383        .or_else(|| {
6384            exception
6385                .as_ref()
6386                .and_then(|value| payload_string(value, "message"))
6387        })
6388        .unwrap_or_else(|| fallback_message.to_string());
6389
6390    Ok(Err(ActivityFailure {
6391        kind,
6392        reason,
6393        message,
6394        activity_execution_id: payload_string(payload, "activity_execution_id"),
6395        activity_attempt_id: payload_string(payload, "activity_attempt_id"),
6396        activity_type: payload_string(payload, "activity_type")
6397            .or_else(|| payload_string(payload, "activity_name"))
6398            .or(recorded_activity_type),
6399        activity_class: payload_string(payload, "activity_class"),
6400        attempt_number: payload.get("attempt_number").and_then(value_as_u64),
6401        failure_id: payload_string(payload, "failure_id"),
6402        failure_category,
6403        timeout_kind,
6404        non_retryable: payload
6405            .get("non_retryable")
6406            .and_then(Value::as_bool)
6407            .unwrap_or(false),
6408        exception_type: payload_string(payload, "exception_type").or_else(|| {
6409            exception
6410                .as_ref()
6411                .and_then(|value| payload_string(value, "type"))
6412        }),
6413        exception_class: payload_string(payload, "exception_class").or_else(|| {
6414            exception
6415                .as_ref()
6416                .and_then(|value| payload_string(value, "class"))
6417        }),
6418        code: payload
6419            .get("code")
6420            .filter(|value| !value.is_null())
6421            .cloned(),
6422        exception,
6423    }))
6424}
6425
6426type ChildWorkflowOutcome = std::result::Result<ChildWorkflowResult, ChildWorkflowFailure>;
6427
6428fn child_workflow_outcomes(
6429    events: &[HistoryEvent],
6430    fallback_codec: &str,
6431    parent: WorkflowIdentity,
6432) -> Result<Vec<ChildWorkflowOutcome>> {
6433    let mut outcomes = Vec::new();
6434
6435    for event in events {
6436        let kind = match event.event_type.as_str() {
6437            "ChildRunCompleted" => None,
6438            "ChildRunFailed" => Some((
6439                ChildWorkflowFailureKind::Failed,
6440                "child_workflow",
6441                "child workflow failed",
6442            )),
6443            "ChildRunCancelled" => Some((
6444                ChildWorkflowFailureKind::Cancelled,
6445                "cancelled",
6446                "child workflow was cancelled",
6447            )),
6448            "ChildRunTerminated" => Some((
6449                ChildWorkflowFailureKind::Terminated,
6450                "terminated",
6451                "child workflow was terminated",
6452            )),
6453            _ => continue,
6454        };
6455        let payload = &event.payload;
6456        let child_workflow_id = payload_string(payload, "child_workflow_instance_id");
6457        let child_workflow_run_id = payload_string(payload, "child_workflow_run_id");
6458        let child_workflow_type = payload_string(payload, "child_workflow_type");
6459
6460        if let Some((kind, reason, fallback_message)) = kind {
6461            let exception = payload
6462                .get("exception")
6463                .filter(|value| !value.is_null())
6464                .cloned();
6465            let message = payload_string(payload, "message")
6466                .or_else(|| {
6467                    exception
6468                        .as_ref()
6469                        .and_then(|value| payload_string(value, "message"))
6470                })
6471                .unwrap_or_else(|| fallback_message.to_string());
6472            let exception_type = payload_string(payload, "exception_type").or_else(|| {
6473                exception
6474                    .as_ref()
6475                    .and_then(|value| payload_string(value, "type"))
6476            });
6477            let exception_class = payload_string(payload, "exception_class").or_else(|| {
6478                exception
6479                    .as_ref()
6480                    .and_then(|value| payload_string(value, "class"))
6481            });
6482            outcomes.push(Err(ChildWorkflowFailure {
6483                kind,
6484                reason: reason.to_string(),
6485                message,
6486                parent_workflow_id: parent.workflow_id.clone(),
6487                parent_workflow_run_id: parent.run_id.clone(),
6488                child_workflow_id,
6489                child_workflow_run_id,
6490                child_workflow_type,
6491                failure_id: payload_string(payload, "failure_id"),
6492                failure_category: payload_string(payload, "failure_category"),
6493                exception_type,
6494                exception_class,
6495                non_retryable: payload
6496                    .get("non_retryable")
6497                    .and_then(Value::as_bool)
6498                    .unwrap_or(false),
6499                code: payload
6500                    .get("code")
6501                    .filter(|value| !value.is_null())
6502                    .cloned(),
6503                exception,
6504            }));
6505            continue;
6506        }
6507
6508        let codec = payload
6509            .get("payload_codec")
6510            .and_then(Value::as_str)
6511            .unwrap_or(fallback_codec);
6512        let result = payload
6513            .get("result")
6514            .or_else(|| payload.get("output"))
6515            .unwrap_or(&Value::Null);
6516        outcomes.push(Ok(ChildWorkflowResult {
6517            parent: parent.clone(),
6518            child: WorkflowIdentity {
6519                workflow_id: child_workflow_id,
6520                run_id: child_workflow_run_id,
6521            },
6522            child_workflow_type,
6523            result: decode_wire_value(result, codec)?,
6524        }));
6525    }
6526
6527    Ok(outcomes)
6528}
6529
6530fn payload_string(payload: &Value, key: &str) -> Option<String> {
6531    payload
6532        .get(key)
6533        .and_then(Value::as_str)
6534        .filter(|value| !value.is_empty())
6535        .map(str::to_string)
6536}
6537
6538fn workflow_failure_command(error: &Error) -> Value {
6539    let (exception_type, exception_class, properties) = match error {
6540        Error::ActivityFailed(failure) => (
6541            match failure.kind {
6542                ActivityFailureKind::Failed => "ActivityFailed",
6543                ActivityFailureKind::Cancelled => "ActivityCancelled",
6544                ActivityFailureKind::TimedOut => "ActivityTimedOut",
6545            },
6546            "durable_workflow::ActivityFailure",
6547            json!({
6548                "reason": failure.reason,
6549                "activity_execution_id": failure.activity_execution_id,
6550                "activity_attempt_id": failure.activity_attempt_id,
6551                "activity_type": failure.activity_type,
6552                "activity_class": failure.activity_class,
6553                "attempt_number": failure.attempt_number,
6554                "failure_id": failure.failure_id,
6555                "failure_category": failure.failure_category,
6556                "timeout_kind": failure.timeout_kind,
6557                "activity_non_retryable": failure.non_retryable,
6558                "activity_exception_type": failure.exception_type,
6559                "activity_exception_class": failure.exception_class,
6560                "activity_code": failure.code,
6561                "activity_exception": failure.exception,
6562            }),
6563        ),
6564        Error::ChildWorkflowFailed(failure) => (
6565            match failure.kind {
6566                ChildWorkflowFailureKind::Failed => "ChildWorkflowFailed",
6567                ChildWorkflowFailureKind::Cancelled => "ChildWorkflowCancelled",
6568                ChildWorkflowFailureKind::Terminated => "ChildWorkflowTerminated",
6569            },
6570            "durable_workflow::ChildWorkflowFailure",
6571            json!({
6572                "reason": failure.reason,
6573                "parent_workflow_id": failure.parent_workflow_id,
6574                "parent_workflow_run_id": failure.parent_workflow_run_id,
6575                "child_workflow_id": failure.child_workflow_id,
6576                "child_workflow_run_id": failure.child_workflow_run_id,
6577                "child_workflow_type": failure.child_workflow_type,
6578                "failure_id": failure.failure_id,
6579                "failure_category": failure.failure_category,
6580                "child_exception_type": failure.exception_type,
6581                "child_exception_class": failure.exception_class,
6582                "child_non_retryable": failure.non_retryable,
6583                "child_code": failure.code,
6584                "child_exception": failure.exception,
6585            }),
6586        ),
6587        Error::NonDeterministicReplay(_) => (
6588            "NonDeterministicReplay",
6589            "durable_workflow::Error",
6590            Value::Null,
6591        ),
6592        _ => ("RustWorkflowError", "durable_workflow::Error", Value::Null),
6593    };
6594    let non_retryable = match error {
6595        Error::ActivityFailed(failure) => failure.non_retryable,
6596        Error::ChildWorkflowFailed(failure) => failure.non_retryable,
6597        Error::NonDeterministicReplay(_) => true,
6598        _ => false,
6599    };
6600
6601    json!({
6602        "type": "fail_workflow",
6603        "message": error.to_string(),
6604        "exception_type": exception_type,
6605        "exception_class": exception_class,
6606        "non_retryable": non_retryable,
6607        "exception": {
6608            "type": exception_type,
6609            "class": exception_class,
6610            "message": error.to_string(),
6611            "properties": properties,
6612        }
6613    })
6614}
6615
6616fn workflow_task_integrity_error(error: &Error) -> bool {
6617    matches!(
6618        error,
6619        Error::NonDeterministicReplay(_) | Error::Protocol(_) | Error::WorkflowStatePoisoned
6620    )
6621}
6622
6623fn decode_signal_event_arguments(event: &HistoryEvent, fallback_codec: &str) -> Result<Vec<Value>> {
6624    let codec = event
6625        .payload
6626        .get("payload_codec")
6627        .and_then(Value::as_str)
6628        .unwrap_or(fallback_codec);
6629    let raw = event
6630        .payload
6631        .get("value")
6632        .or_else(|| event.payload.get("input"))
6633        .or_else(|| event.payload.get("arguments"));
6634    let decoded = match raw.filter(|value| !value.is_null()) {
6635        Some(value) => decode_wire_value(value, codec)?,
6636        None => Value::Array(Vec::new()),
6637    };
6638    let Value::Array(arguments) = normalize_arguments(decoded) else {
6639        unreachable!("normalize_arguments always returns an array");
6640    };
6641    Ok(arguments)
6642}
6643
6644fn hydrate_query_history_from_export(task: &mut QueryTask) -> Result<()> {
6645    let Some(export_events) = task
6646        .history_export
6647        .as_ref()
6648        .and_then(|export| export.get("history_events"))
6649        .and_then(Value::as_array)
6650    else {
6651        return Ok(());
6652    };
6653
6654    if export_events.len() > task.history_events.len() {
6655        task.history_events = serde_json::from_value(Value::Array(export_events.clone()))?;
6656    }
6657
6658    Ok(())
6659}
6660
6661fn enrich_query_history_from_export(task: &mut QueryTask) -> Result<()> {
6662    let Some(export) = task.history_export.as_ref() else {
6663        return Ok(());
6664    };
6665    let signals = export
6666        .get("signals")
6667        .and_then(Value::as_array)
6668        .cloned()
6669        .unwrap_or_default();
6670    let activities = export
6671        .get("activities")
6672        .and_then(Value::as_array)
6673        .cloned()
6674        .unwrap_or_default();
6675    let export_codec = export
6676        .get("payloads")
6677        .and_then(|payloads| payloads.get("codec"))
6678        .and_then(Value::as_str)
6679        .unwrap_or(&task.payload_codec)
6680        .to_string();
6681    let mut signal_name_offsets: HashMap<String, usize> = HashMap::new();
6682
6683    for event in &mut task.history_events {
6684        if event.event_type == "ActivityCompleted" {
6685            let sequence = event
6686                .payload
6687                .get("sequence")
6688                .or_else(|| event.payload.get("workflow_sequence"))
6689                .and_then(value_as_u64);
6690            let Some(activity) = sequence.and_then(|sequence| {
6691                activities.iter().find(|activity| {
6692                    activity.get("sequence").and_then(value_as_u64) == Some(sequence)
6693                })
6694            }) else {
6695                continue;
6696            };
6697            let Some(payload) = event.payload.as_object_mut() else {
6698                continue;
6699            };
6700            if missing_payload(payload.get("result")) {
6701                if let Some(result) = activity
6702                    .get("result")
6703                    .filter(|value| !missing_payload(Some(value)))
6704                {
6705                    payload.insert("result".to_string(), result.clone());
6706                }
6707            }
6708            for field in ["payload_codec", "activity_type"] {
6709                if payload
6710                    .get(field)
6711                    .and_then(Value::as_str)
6712                    .unwrap_or_default()
6713                    .is_empty()
6714                {
6715                    if let Some(value) = activity.get(field) {
6716                        payload.insert(field.to_string(), value.clone());
6717                    }
6718                }
6719            }
6720            continue;
6721        }
6722
6723        if event.event_type != "SignalReceived" && event.event_type != "SignalApplied" {
6724            continue;
6725        }
6726        let signal_id = event.payload.get("signal_id").and_then(Value::as_str);
6727        let command_id = event
6728            .payload
6729            .get("workflow_command_id")
6730            .or_else(|| event.raw.get("workflow_command_id"))
6731            .and_then(Value::as_str);
6732        let signal_name = event
6733            .payload
6734            .get("signal_name")
6735            .and_then(Value::as_str)
6736            .unwrap_or_default()
6737            .to_string();
6738        let matched = signals
6739            .iter()
6740            .find(|signal| {
6741                signal_id.is_some() && signal.get("id").and_then(Value::as_str) == signal_id
6742            })
6743            .or_else(|| {
6744                signals.iter().find(|signal| {
6745                    command_id.is_some()
6746                        && signal.get("command_id").and_then(Value::as_str) == command_id
6747                })
6748            })
6749            .or_else(|| {
6750                let offset = signal_name_offsets.entry(signal_name.clone()).or_default();
6751                let signal = signals
6752                    .iter()
6753                    .filter(|signal| {
6754                        signal.get("name").and_then(Value::as_str) == Some(signal_name.as_str())
6755                    })
6756                    .nth(*offset);
6757                if signal.is_some() {
6758                    *offset += 1;
6759                }
6760                signal
6761            });
6762        let Some(signal) = matched else {
6763            continue;
6764        };
6765        let signal_codec = signal
6766            .get("payload_codec")
6767            .and_then(Value::as_str)
6768            .unwrap_or(&export_codec);
6769        let Some(payload) = event.payload.as_object_mut() else {
6770            continue;
6771        };
6772        if missing_payload(payload.get("arguments")) {
6773            if let Some(arguments) = signal
6774                .get("arguments")
6775                .filter(|value| !missing_payload(Some(value)))
6776            {
6777                let envelope = match arguments {
6778                    Value::String(blob) => json!({"codec": signal_codec, "blob": blob}),
6779                    other => other.clone(),
6780                };
6781                payload.insert("arguments".to_string(), envelope);
6782            }
6783        }
6784        if payload
6785            .get("payload_codec")
6786            .and_then(Value::as_str)
6787            .unwrap_or_default()
6788            .is_empty()
6789        {
6790            payload.insert("payload_codec".to_string(), json!(signal_codec));
6791        }
6792    }
6793
6794    Ok(())
6795}
6796
6797fn missing_payload(value: Option<&Value>) -> bool {
6798    match value {
6799        None | Some(Value::Null) => true,
6800        Some(Value::String(value)) => value.is_empty(),
6801        Some(_) => false,
6802    }
6803}
6804
6805fn query_signal_events(task: &QueryTask) -> Result<Vec<QuerySignal>> {
6806    let export_signals = task
6807        .history_export
6808        .as_ref()
6809        .and_then(|export| export.get("signals"))
6810        .and_then(Value::as_array)
6811        .cloned()
6812        .unwrap_or_default();
6813    let export_codec = task
6814        .history_export
6815        .as_ref()
6816        .and_then(|export| export.get("payloads"))
6817        .and_then(|payloads| payloads.get("codec"))
6818        .and_then(Value::as_str)
6819        .unwrap_or(&task.payload_codec);
6820    let mut name_offsets: HashMap<String, usize> = HashMap::new();
6821    let mut signals = Vec::new();
6822
6823    for event in &task.history_events {
6824        if event.event_type != "SignalApplied" && event.event_type != "SignalReceived" {
6825            continue;
6826        }
6827
6828        let name = event
6829            .payload
6830            .get("signal_name")
6831            .and_then(Value::as_str)
6832            .unwrap_or_default();
6833        if name.is_empty() {
6834            continue;
6835        }
6836        let signal_id = event.payload.get("signal_id").and_then(Value::as_str);
6837        let command_id = event
6838            .payload
6839            .get("workflow_command_id")
6840            .or_else(|| event.raw.get("workflow_command_id"))
6841            .and_then(Value::as_str);
6842        let matched_export = export_signals
6843            .iter()
6844            .find(|candidate| {
6845                signal_id.is_some() && candidate.get("id").and_then(Value::as_str) == signal_id
6846            })
6847            .or_else(|| {
6848                export_signals.iter().find(|candidate| {
6849                    command_id.is_some()
6850                        && candidate.get("command_id").and_then(Value::as_str) == command_id
6851                })
6852            })
6853            .or_else(|| {
6854                let offset = name_offsets.entry(name.to_string()).or_default();
6855                let candidate = export_signals
6856                    .iter()
6857                    .filter(|candidate| candidate.get("name").and_then(Value::as_str) == Some(name))
6858                    .nth(*offset);
6859                if candidate.is_some() {
6860                    *offset += 1;
6861                }
6862                candidate
6863            });
6864        let codec = event
6865            .payload
6866            .get("payload_codec")
6867            .and_then(Value::as_str)
6868            .or_else(|| {
6869                matched_export
6870                    .and_then(|signal| signal.get("payload_codec"))
6871                    .and_then(Value::as_str)
6872            })
6873            .unwrap_or(export_codec);
6874        let raw_arguments = event
6875            .payload
6876            .get("value")
6877            .or_else(|| event.payload.get("input"))
6878            .or_else(|| event.payload.get("arguments"))
6879            .filter(|value| !value.is_null())
6880            .or_else(|| matched_export.and_then(|signal| signal.get("arguments")));
6881        let arguments = decode_query_signal_arguments(raw_arguments, codec)?;
6882        let workflow_sequence = event
6883            .payload
6884            .get("workflow_sequence")
6885            .and_then(value_as_u64)
6886            .or_else(|| {
6887                matched_export
6888                    .and_then(|signal| signal.get("workflow_sequence"))
6889                    .and_then(value_as_u64)
6890            });
6891
6892        signals.push(QuerySignal {
6893            id: signal_id.map(str::to_string).or_else(|| {
6894                matched_export
6895                    .and_then(|signal| signal.get("id"))
6896                    .and_then(Value::as_str)
6897                    .map(str::to_string)
6898            }),
6899            name: name.to_string(),
6900            arguments,
6901            workflow_sequence,
6902        });
6903    }
6904
6905    if signals.is_empty() {
6906        for signal in export_signals {
6907            if signal.get("status").and_then(Value::as_str) == Some("rejected") {
6908                continue;
6909            }
6910            let Some(name) = signal.get("name").and_then(Value::as_str) else {
6911                continue;
6912            };
6913            let codec = signal
6914                .get("payload_codec")
6915                .and_then(Value::as_str)
6916                .unwrap_or(export_codec);
6917            let arguments = decode_query_signal_arguments(signal.get("arguments"), codec)?;
6918            signals.push(QuerySignal {
6919                id: signal.get("id").and_then(Value::as_str).map(str::to_string),
6920                name: name.to_string(),
6921                arguments,
6922                workflow_sequence: signal.get("workflow_sequence").and_then(value_as_u64),
6923            });
6924        }
6925        signals.sort_by_key(|signal| signal.workflow_sequence.unwrap_or(u64::MAX));
6926    }
6927
6928    Ok(signals)
6929}
6930
6931fn decode_query_signal_arguments(raw: Option<&Value>, codec: &str) -> Result<Vec<Value>> {
6932    let decoded = match raw.filter(|value| !value.is_null()) {
6933        Some(value) => decode_wire_value(value, codec)?,
6934        None => Value::Array(Vec::new()),
6935    };
6936    let Value::Array(arguments) = normalize_arguments(decoded) else {
6937        unreachable!("normalize_arguments always returns an array");
6938    };
6939    Ok(arguments)
6940}
6941
6942fn value_as_u64(value: &Value) -> Option<u64> {
6943    value
6944        .as_u64()
6945        .or_else(|| value.as_str().and_then(|value| value.parse().ok()))
6946}
6947
6948#[cfg(test)]
6949mod tests {
6950    use super::*;
6951    use std::{
6952        io::{Read, Write},
6953        net::{SocketAddr, TcpListener, TcpStream},
6954        sync::atomic::AtomicUsize,
6955        thread,
6956    };
6957
6958    #[derive(Clone, Debug, Default, PartialEq)]
6959    struct ReplayCounterState {
6960        loaded: Option<String>,
6961        count: i64,
6962        finished: bool,
6963    }
6964
6965    fn replay_counter_worker() -> Worker {
6966        let client = Client::new("http://127.0.0.1:8080").expect("client");
6967        let mut worker = Worker::new(client, "rust-workers");
6968        worker.register_replayed_workflow(
6969            "replay-counter",
6970            ReplayCounterState::default,
6971            |ctx, _input, state| async move {
6972                let loaded = ctx.activity("load-counter", json!([])).await?;
6973                state.update(|current| {
6974                    current.loaded = loaded.as_str().map(str::to_string);
6975                })?;
6976                for _ in 0..2 {
6977                    let signal = ctx.wait_signal("increment").await?;
6978                    let amount = signal.first().and_then(Value::as_i64).unwrap_or_default();
6979                    state.update(|current| current.count += amount)?;
6980                }
6981                state.update(|current| current.finished = true)?;
6982                state.read(|current| Ok(json!(current.count)))?
6983            },
6984        );
6985        worker.register_replayed_query::<ReplayCounterState, _, _>(
6986            "replay-counter",
6987            "current",
6988            |_ctx, state, _args| async move {
6989                Ok(json!({
6990                    "loaded": state.loaded,
6991                    "count": state.count,
6992                    "finished": state.finished,
6993                }))
6994            },
6995        );
6996        worker.register_replayed_query::<ReplayCounterState, _, _>(
6997            "replay-counter",
6998            "detached-mutation",
6999            |_ctx, state, _args| async move {
7000                let mut detached = (*state).clone();
7001                detached.count = 999;
7002                Ok(json!(detached.count))
7003            },
7004        );
7005        worker.register_replayed_query::<ReplayCounterState, _, _>(
7006            "replay-counter",
7007            "failed-mutation",
7008            |_ctx, state, _args| async move {
7009                let mut detached = (*state).clone();
7010                detached.count = 999;
7011                Err(Error::WorkerLoop("query refused".to_string()))
7012            },
7013        );
7014        worker
7015    }
7016
7017    fn replay_counter_query(
7018        query_name: &str,
7019        history_events: Value,
7020        run_status: &str,
7021    ) -> QueryTask {
7022        serde_json::from_value(json!({
7023            "query_task_id": format!("query-{query_name}"),
7024            "workflow_type": "replay-counter",
7025            "query_name": query_name,
7026            "payload_codec": "json",
7027            "workflow_arguments": {"codec": "json", "blob": "[]"},
7028            "query_arguments": {"codec": "json", "blob": "[]"},
7029            "history_events": history_events,
7030            "run_status": run_status,
7031        }))
7032        .expect("query task")
7033    }
7034
7035    fn workflow_context(history: Vec<HistoryEvent>) -> WorkflowContext {
7036        workflow_context_with_codec(history, JSON_CODEC)
7037    }
7038
7039    fn workflow_context_with_codec(
7040        history: Vec<HistoryEvent>,
7041        payload_codec: &str,
7042    ) -> WorkflowContext {
7043        WorkflowContext {
7044            state: Arc::new(Mutex::new(
7045                WorkflowState::new_with_identity(
7046                    history,
7047                    None,
7048                    None,
7049                    "rust-workers".to_string(),
7050                    payload_codec.to_string(),
7051                    None,
7052                )
7053                .expect("valid workflow history"),
7054            )),
7055        }
7056    }
7057
7058    fn history_event(event_type: &str, payload: Value) -> HistoryEvent {
7059        HistoryEvent {
7060            event_type: event_type.to_string(),
7061            payload,
7062            raw: HashMap::new(),
7063        }
7064    }
7065
7066    fn workflow_task(
7067        workflow_type: &str,
7068        history_events: Vec<HistoryEvent>,
7069        payload_codec: &str,
7070    ) -> WorkflowTask {
7071        WorkflowTask {
7072            task_id: format!("wft-{workflow_type}"),
7073            workflow_id: Some(format!("wf-{workflow_type}")),
7074            run_id: Some(format!("run-{workflow_type}")),
7075            workflow_type: workflow_type.to_string(),
7076            payload_codec: payload_codec.to_string(),
7077            arguments: Some(
7078                encode_value_envelope(&json!([]), payload_codec).expect("workflow arguments"),
7079            ),
7080            total_history_events: Some(history_events.len() as u64),
7081            history_size_bytes: None,
7082            continue_as_new_recommended: None,
7083            history_budget_pressure: None,
7084            history_events,
7085            next_history_page_token: None,
7086            workflow_task_attempt: 1,
7087            workflow_signal_id: None,
7088            signal_name: None,
7089            signal_arguments: None,
7090            lease_owner: Some("rust-worker".to_string()),
7091        }
7092    }
7093
7094    #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
7095    struct SideEffectProbe {
7096        request_id: String,
7097        attempt: u32,
7098    }
7099
7100    #[test]
7101    fn typed_side_effect_runs_callback_once_and_replay_skips_it() {
7102        let calls = AtomicUsize::new(0);
7103        let ctx = workflow_context(Vec::new());
7104        let value = ctx
7105            .side_effect(|| {
7106                calls.fetch_add(1, Ordering::SeqCst);
7107                SideEffectProbe {
7108                    request_id: "request-42".to_string(),
7109                    attempt: 3,
7110                }
7111            })
7112            .expect("first side effect");
7113        assert_eq!(value.attempt, 3);
7114        assert_eq!(calls.load(Ordering::SeqCst), 1);
7115        let commands = ctx.take_commands().expect("commands");
7116        assert_eq!(commands.len(), 1);
7117        assert_eq!(commands[0]["type"], "record_side_effect");
7118        assert_eq!(
7119            decode_wire_value(&commands[0]["result"], JSON_CODEC).expect("JSON result"),
7120            serde_json::to_value(&value).expect("value")
7121        );
7122
7123        let replay = workflow_context(vec![history_event(
7124            "SideEffectRecorded",
7125            json!({"sequence": 1, "result": commands[0]["result"].clone()}),
7126        )]);
7127        let replayed: SideEffectProbe = replay
7128            .side_effect(|| {
7129                calls.fetch_add(1, Ordering::SeqCst);
7130                panic!("committed side-effect callbacks must not run during replay")
7131            })
7132            .expect("replayed side effect");
7133        assert_eq!(replayed, value);
7134        assert_eq!(calls.load(Ordering::SeqCst), 1);
7135        assert!(replay.take_commands().expect("commands").is_empty());
7136        replay.ensure_history_consumed().expect("history consumed");
7137    }
7138
7139    #[test]
7140    fn side_effect_uses_avro_envelope_and_uuid_is_replay_stable() {
7141        let ctx = workflow_context_with_codec(Vec::new(), DEFAULT_CODEC);
7142        let value = ctx
7143            .side_effect(|| SideEffectProbe {
7144                request_id: "avro-request".to_string(),
7145                attempt: 1,
7146            })
7147            .expect("Avro side effect");
7148        let uuid = ctx.uuid_v4().expect("deterministic UUID");
7149        let commands = ctx.take_commands().expect("commands");
7150        assert_eq!(commands.len(), 2);
7151        assert_eq!(commands[0]["result"]["codec"], DEFAULT_CODEC);
7152        assert_eq!(commands[1]["result"]["codec"], DEFAULT_CODEC);
7153        assert_eq!(
7154            decode_wire_value(&commands[0]["result"], DEFAULT_CODEC).expect("Avro result"),
7155            serde_json::to_value(&value).expect("value")
7156        );
7157
7158        let replay = workflow_context_with_codec(
7159            vec![
7160                history_event(
7161                    "SideEffectRecorded",
7162                    json!({"sequence": 1, "result": commands[0]["result"].clone()}),
7163                ),
7164                history_event(
7165                    "SideEffectRecorded",
7166                    json!({"sequence": 2, "result": commands[1]["result"].clone()}),
7167                ),
7168            ],
7169            DEFAULT_CODEC,
7170        );
7171        let replayed: SideEffectProbe = replay
7172            .side_effect(|| panic!("Avro callback must not run"))
7173            .expect("replayed Avro value");
7174        let replayed_uuid = replay.uuid_v4().expect("replayed UUID");
7175        assert_eq!(replayed, value);
7176        assert_eq!(replayed_uuid, uuid);
7177        assert!(replay.take_commands().expect("commands").is_empty());
7178    }
7179
7180    #[test]
7181    fn ordered_side_effects_share_the_durable_command_stream() {
7182        let first = encode_value_envelope(&json!("first"), JSON_CODEC).expect("first");
7183        let second = encode_value_envelope(&json!(29), JSON_CODEC).expect("second");
7184        let ctx = workflow_context(vec![
7185            history_event(
7186                "SideEffectRecorded",
7187                json!({"sequence": 1, "result": first}),
7188            ),
7189            history_event(
7190                "SideEffectRecorded",
7191                json!({"sequence": 2, "result": second}),
7192            ),
7193        ]);
7194        let first: String = ctx
7195            .side_effect(|| panic!("first callback must not run"))
7196            .expect("first replay");
7197        let second: i32 = ctx
7198            .side_effect(|| panic!("second callback must not run"))
7199            .expect("second replay");
7200        assert_eq!(first, "first");
7201        assert_eq!(second, 29);
7202        ctx.ensure_history_consumed().expect("ordered history");
7203
7204        let reordered = workflow_context(vec![history_event(
7205            "VersionMarkerRecorded",
7206            json!({
7207                "sequence": 1,
7208                "change_id": "before-side-effect",
7209                "version": 1,
7210                "min_supported": 1,
7211                "max_supported": 1,
7212            }),
7213        )]);
7214        let error = reordered
7215            .side_effect(|| "new".to_string())
7216            .expect_err("command reordering must fail");
7217        assert!(matches!(
7218            error,
7219            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
7220                if reason == "recorded_command_mismatch"
7221        ));
7222    }
7223
7224    #[test]
7225    fn version_markers_replay_across_upgrades_and_do_not_duplicate() {
7226        let ctx = workflow_context(Vec::new());
7227        assert_eq!(ctx.get_version("checkout-v2", 1, 2).expect("version"), 2);
7228        assert_eq!(ctx.get_version("checkout-v2", 1, 3).expect("cached"), 2);
7229        assert!(ctx.patched("new-search").expect("patch"));
7230        ctx.deprecate_patch("new-search").expect("deprecate patch");
7231        let commands = ctx.take_commands().expect("commands");
7232        assert_eq!(commands.len(), 2);
7233        assert_eq!(commands[0]["type"], "record_version_marker");
7234        assert_eq!(commands[0]["version"], 2);
7235        assert_eq!(commands[1]["change_id"], "new-search");
7236
7237        let replay = workflow_context(vec![history_event(
7238            "VersionMarkerRecorded",
7239            json!({
7240                "sequence": 1,
7241                "change_id": "checkout-v2",
7242                "version": 2,
7243                "min_supported": 1,
7244                "max_supported": 2,
7245            }),
7246        )]);
7247        assert_eq!(replay.get_version("checkout-v2", 1, 4).expect("upgrade"), 2);
7248        assert_eq!(replay.get_version("checkout-v2", 2, 5).expect("repeat"), 2);
7249        assert!(replay.take_commands().expect("commands").is_empty());
7250        replay.ensure_history_consumed().expect("history consumed");
7251    }
7252
7253    #[test]
7254    fn version_markers_reject_incompatible_or_malformed_history() {
7255        let incompatible = workflow_context(vec![history_event(
7256            "VersionMarkerRecorded",
7257            json!({
7258                "sequence": 1,
7259                "change_id": "checkout-v2",
7260                "version": 1,
7261                "min_supported": 1,
7262                "max_supported": 2,
7263            }),
7264        )]);
7265        let error = incompatible
7266            .get_version("checkout-v2", 2, 3)
7267            .expect_err("old version is unsupported");
7268        assert!(matches!(
7269            error,
7270            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
7271                if reason == "version_marker_incompatible_range"
7272        ));
7273
7274        for (history, reason) in [
7275            (
7276                vec![history_event("SideEffectRecorded", json!({"sequence": 1}))],
7277                "side_effect_result_missing",
7278            ),
7279            (
7280                vec![history_event(
7281                    "SideEffectRecorded",
7282                    json!({
7283                        "sequence": 1,
7284                        "result": {"codec": "avro", "blob": "not-base64"},
7285                    }),
7286                )],
7287                "side_effect_payload_incompatible",
7288            ),
7289            (
7290                vec![history_event(
7291                    "SideEffectRecorded",
7292                    json!({"sequence": 1, "result": {"unwrapped": true}}),
7293                )],
7294                "side_effect_payload_malformed",
7295            ),
7296            (
7297                vec![history_event(
7298                    "VersionMarkerRecorded",
7299                    json!({
7300                        "sequence": 1,
7301                        "change_id": "change",
7302                        "version": 1,
7303                        "min_supported": 2,
7304                        "max_supported": 1,
7305                    }),
7306                )],
7307                "version_marker_history_range_invalid",
7308            ),
7309        ] {
7310            let error = WorkflowState::new(
7311                history,
7312                "rust-workers".to_string(),
7313                JSON_CODEC.to_string(),
7314                None,
7315            )
7316            .expect_err("malformed history must fail");
7317            assert!(matches!(
7318                error,
7319                Error::NonDeterministicReplay(ReplayFailure { reason: actual, .. })
7320                    if actual == reason
7321            ));
7322        }
7323    }
7324
7325    #[test]
7326    fn duplicate_side_effects_and_version_markers_are_rejected() {
7327        let duplicate_side_effect = WorkflowState::new(
7328            vec![
7329                history_event(
7330                    "SideEffectRecorded",
7331                    json!({"sequence": 1, "result": {"codec": "json", "blob": "1"}}),
7332                ),
7333                history_event(
7334                    "SideEffectRecorded",
7335                    json!({"sequence": 1, "result": {"codec": "json", "blob": "2"}}),
7336                ),
7337            ],
7338            "rust-workers".to_string(),
7339            JSON_CODEC.to_string(),
7340            None,
7341        )
7342        .expect_err("duplicate side effect");
7343        assert!(matches!(
7344            duplicate_side_effect,
7345            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
7346                if reason == "duplicate_side_effect_record"
7347        ));
7348
7349        let marker = |sequence| {
7350            history_event(
7351                "VersionMarkerRecorded",
7352                json!({
7353                    "sequence": sequence,
7354                    "change_id": "same-change",
7355                    "version": 1,
7356                    "min_supported": 1,
7357                    "max_supported": 1,
7358                }),
7359            )
7360        };
7361        let duplicate_marker = WorkflowState::new(
7362            vec![marker(1), marker(3)],
7363            "rust-workers".to_string(),
7364            JSON_CODEC.to_string(),
7365            None,
7366        )
7367        .expect_err("duplicate marker");
7368        assert!(matches!(
7369            duplicate_marker,
7370            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
7371                if reason == "duplicate_version_marker"
7372        ));
7373    }
7374
7375    #[test]
7376    fn cold_worker_replay_does_not_repeat_committed_side_effects_or_markers() {
7377        fn worker(calls: Arc<AtomicUsize>) -> Worker {
7378            let client = Client::new("http://127.0.0.1:8080").expect("client");
7379            let mut worker = Worker::new(client, "rust-workers");
7380            worker.register_workflow("rust.side-effect-version", move |ctx, _input| {
7381                let calls = Arc::clone(&calls);
7382                async move {
7383                    let captured = ctx.side_effect(|| {
7384                        calls.fetch_add(1, Ordering::SeqCst);
7385                        "captured-once".to_string()
7386                    })?;
7387                    let version = ctx.get_version("cold-restart", 1, 2)?;
7388                    Ok(json!({"captured": captured, "version": version}))
7389                }
7390            });
7391            worker
7392        }
7393
7394        fn task(history_events: Vec<HistoryEvent>) -> WorkflowTask {
7395            WorkflowTask {
7396                task_id: "wft-side-effect-version".to_string(),
7397                workflow_id: Some("wf-side-effect-version".to_string()),
7398                run_id: Some("run-side-effect-version".to_string()),
7399                workflow_type: "rust.side-effect-version".to_string(),
7400                payload_codec: JSON_CODEC.to_string(),
7401                arguments: Some(encode_value_envelope(&json!([]), JSON_CODEC).expect("arguments")),
7402                history_events,
7403                total_history_events: None,
7404                history_size_bytes: None,
7405                continue_as_new_recommended: None,
7406                history_budget_pressure: None,
7407                next_history_page_token: None,
7408                workflow_task_attempt: 1,
7409                workflow_signal_id: None,
7410                signal_name: None,
7411                signal_arguments: None,
7412                lease_owner: Some("rust-worker".to_string()),
7413            }
7414        }
7415
7416        let calls = Arc::new(AtomicUsize::new(0));
7417        let initial = worker(Arc::clone(&calls))
7418            .execute_workflow_task(task(Vec::new()))
7419            .expect("initial execution");
7420        assert_eq!(
7421            initial
7422                .iter()
7423                .map(|command| &command["type"])
7424                .collect::<Vec<_>>(),
7425            vec![
7426                "record_side_effect",
7427                "record_version_marker",
7428                "complete_workflow"
7429            ]
7430        );
7431        assert_eq!(calls.load(Ordering::SeqCst), 1);
7432
7433        let restarted = worker(Arc::clone(&calls));
7434        let replayed = restarted
7435            .execute_workflow_task(task(vec![
7436                history_event(
7437                    "SideEffectRecorded",
7438                    json!({"sequence": 1, "result": initial[0]["result"].clone()}),
7439                ),
7440                history_event(
7441                    "VersionMarkerRecorded",
7442                    json!({
7443                        "sequence": 2,
7444                        "change_id": "cold-restart",
7445                        "version": 2,
7446                        "min_supported": 1,
7447                        "max_supported": 2,
7448                    }),
7449                ),
7450            ]))
7451            .expect("cold replay");
7452        assert_eq!(replayed.len(), 1);
7453        assert_eq!(replayed[0]["type"], "complete_workflow");
7454        assert_eq!(calls.load(Ordering::SeqCst), 1);
7455    }
7456
7457    #[test]
7458    fn side_effect_replay_rejects_changed_rust_value_type() {
7459        let result = encode_value_envelope(&json!({"value": 42}), JSON_CODEC).expect("result");
7460        let ctx = workflow_context(vec![history_event(
7461            "SideEffectRecorded",
7462            json!({"sequence": 1, "result": result}),
7463        )]);
7464        let error = ctx
7465            .side_effect::<Vec<String>, _>(|| panic!("callback must not run"))
7466            .expect_err("changed type must fail replay");
7467        assert!(matches!(
7468            error,
7469            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
7470                if reason == "side_effect_type_mismatch"
7471        ));
7472    }
7473
7474    fn completed_retry_activity_history() -> Vec<HistoryEvent> {
7475        vec![
7476            history_event(
7477                "ActivityScheduled",
7478                json!({
7479                    "sequence": 1,
7480                    "activity_type": "flaky",
7481                    "activity_execution_id": "act-1",
7482                    "activity": {
7483                        "id": "act-1",
7484                        "sequence": 1,
7485                        "type": "flaky",
7486                        "queue": "critical-activities",
7487                        "execution_mode": null,
7488                        "retry_policy": {
7489                            "snapshot_version": 1,
7490                            "max_attempts": 3,
7491                            "backoff_seconds": [2, 4],
7492                            "start_to_close_timeout": 30,
7493                            "schedule_to_start_timeout": 5,
7494                            "schedule_to_close_timeout": 90,
7495                            "heartbeat_timeout": 10,
7496                            "non_retryable_error_types": ["PermanentError"]
7497                        }
7498                    }
7499                }),
7500            ),
7501            history_event(
7502                "ActivityStarted",
7503                json!({
7504                    "sequence": 1,
7505                    "activity_type": "flaky",
7506                    "activity_execution_id": "act-1",
7507                    "activity_attempt_id": "attempt-1",
7508                    "attempt_number": 1
7509                }),
7510            ),
7511            history_event(
7512                "ActivityRetryScheduled",
7513                json!({
7514                    "sequence": 1,
7515                    "activity_type": "flaky",
7516                    "activity_execution_id": "act-1",
7517                    "activity_attempt_id": "attempt-1",
7518                    "attempt_number": 1,
7519                    "retry_after_attempt": 1,
7520                    "retry_backoff_seconds": 2,
7521                    "failure_category": "activity",
7522                    "exception_type": "TransientError"
7523                }),
7524            ),
7525            history_event(
7526                "ActivityStarted",
7527                json!({
7528                    "sequence": 1,
7529                    "activity_type": "flaky",
7530                    "activity_execution_id": "act-1",
7531                    "activity_attempt_id": "attempt-2",
7532                    "attempt_number": 2
7533                }),
7534            ),
7535            history_event(
7536                "ActivityCompleted",
7537                json!({
7538                    "sequence": 1,
7539                    "activity_type": "flaky",
7540                    "activity_execution_id": "act-1",
7541                    "activity_attempt_id": "attempt-2",
7542                    "attempt_number": 2,
7543                    "payload_codec": "json",
7544                    "result": {"codec": "json", "blob": "{\"status\":\"recovered\"}"}
7545                }),
7546            ),
7547        ]
7548    }
7549
7550    fn retry_activity_options() -> ActivityOptions {
7551        ActivityOptions::new()
7552            .task_queue("critical-activities")
7553            .retry_policy(
7554                ActivityRetryPolicy::new(3)
7555                    .backoff_intervals([Duration::from_secs(2), Duration::from_secs(4)])
7556                    .non_retryable_error_type("PermanentError"),
7557            )
7558            .start_to_close_timeout(Duration::from_secs(30))
7559            .schedule_to_start_timeout(Duration::from_secs(5))
7560            .schedule_to_close_timeout(Duration::from_secs(90))
7561            .heartbeat_timeout(Duration::from_secs(10))
7562    }
7563
7564    #[test]
7565    fn avro_generic_wrapper_round_trips_json_values() {
7566        let value = json!({"greeting": "hello", "count": 3, "ok": true});
7567        let envelope = PayloadEnvelope::avro(&value).expect("encode");
7568        assert_eq!(envelope.codec, DEFAULT_CODEC);
7569        assert_eq!(decode_payload::<Value>(&envelope).expect("decode"), value);
7570    }
7571
7572    #[test]
7573    fn json_codec_remains_plain_json() {
7574        let value = json!({"greeting": "hello", "count": 3, "ok": true});
7575        let envelope = PayloadEnvelope::json(&value).expect("encode");
7576
7577        assert_eq!(envelope.codec, JSON_CODEC);
7578        assert_eq!(envelope.blob, serde_json::to_string(&value).expect("json"));
7579        assert_eq!(decode_payload::<Value>(&envelope).expect("decode"), value);
7580    }
7581
7582    #[test]
7583    fn typed_avro_payload_without_schema_context_keeps_diagnostic() {
7584        let envelope = PayloadEnvelope {
7585            codec: DEFAULT_CODEC.to_string(),
7586            blob: BASE64.encode([0x01]),
7587        };
7588
7589        let error = decode_payload::<Value>(&envelope).expect_err("typed payload must fail");
7590        assert_eq!(
7591            error.to_string(),
7592            "codec error: typed avro payloads require a schema context; v1 supports the generic wrapper"
7593        );
7594    }
7595
7596    #[test]
7597    fn workflow_context_schedules_activity_until_completion_is_in_history() {
7598        let ctx = WorkflowContext {
7599            state: Arc::new(Mutex::new(
7600                WorkflowState::new_with_identity(
7601                    Vec::new(),
7602                    Some("wf-parent".to_string()),
7603                    Some("run-parent".to_string()),
7604                    "rust-workers".to_string(),
7605                    DEFAULT_CODEC.to_string(),
7606                    None,
7607                )
7608                .expect("workflow state"),
7609            )),
7610        };
7611
7612        let mut call = Box::pin(ctx.activity("hello.activity", json!(["Ada"])));
7613        let mut task_context = TaskContext::from_waker(noop_waker_ref());
7614        assert!(matches!(
7615            call.as_mut().poll(&mut task_context),
7616            Poll::Pending
7617        ));
7618
7619        let commands = ctx.take_commands().expect("commands");
7620        assert_eq!(commands[0]["type"], "schedule_activity");
7621        assert_eq!(commands[0]["activity_type"], "hello.activity");
7622    }
7623
7624    #[test]
7625    fn activity_options_encode_retry_policy_queue_and_every_timeout() {
7626        let ctx = workflow_context(Vec::new());
7627        let options = ActivityOptions::new()
7628            .task_queue("payments")
7629            .retry_policy(
7630                ActivityRetryPolicy::new(4)
7631                    .exponential_backoff(Duration::from_secs(1), 3, Some(Duration::from_secs(10)))
7632                    .non_retryable_error_type("ValidationError"),
7633            )
7634            .start_to_close_timeout(Duration::from_secs(120))
7635            .schedule_to_start_timeout(Duration::from_secs(10))
7636            .schedule_to_close_timeout(Duration::from_secs(300))
7637            .heartbeat_timeout(Duration::from_secs(15));
7638        let mut call = Box::pin(ctx.activity_with_options(
7639            "charge-card",
7640            options,
7641            json!([{"order_id": "o-1"}]),
7642        ));
7643        let mut task_context = TaskContext::from_waker(noop_waker_ref());
7644
7645        assert!(matches!(
7646            call.as_mut().poll(&mut task_context),
7647            Poll::Pending
7648        ));
7649        assert!(matches!(
7650            call.as_mut().poll(&mut task_context),
7651            Poll::Pending
7652        ));
7653
7654        let commands = ctx.take_commands().expect("activity command");
7655        assert_eq!(commands.len(), 1, "one future emits one logical schedule");
7656        assert_eq!(commands[0]["queue"], "payments");
7657        assert_eq!(
7658            commands[0]["retry_policy"],
7659            json!({
7660                "max_attempts": 4,
7661                "backoff_seconds": [1, 3, 9],
7662                "non_retryable_error_types": ["ValidationError"],
7663            })
7664        );
7665        assert_eq!(commands[0]["start_to_close_timeout"], 120);
7666        assert_eq!(commands[0]["schedule_to_start_timeout"], 10);
7667        assert_eq!(commands[0]["schedule_to_close_timeout"], 300);
7668        assert_eq!(commands[0]["heartbeat_timeout"], 15);
7669    }
7670
7671    #[test]
7672    fn activity_options_encode_explicit_and_rounded_backoff_intervals() {
7673        let ctx = workflow_context(Vec::new());
7674        let options = ActivityOptions::new().retry_policy(
7675            ActivityRetryPolicy::new(3)
7676                .backoff_intervals([Duration::from_millis(1), Duration::from_millis(1_001)]),
7677        );
7678        let mut call = Box::pin(ctx.activity_with_options("work", options, json!([])));
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_eq!(
7686            ctx.take_commands().expect("command")[0]["retry_policy"]["backoff_seconds"],
7687            json!([1, 2])
7688        );
7689    }
7690
7691    #[test]
7692    fn invalid_activity_options_return_typed_errors_before_emitting_commands() {
7693        let cases = [
7694            (
7695                ActivityOptions::new().task_queue("  "),
7696                ActivityOptionsErrorKind::EmptyTaskQueue,
7697            ),
7698            (
7699                ActivityOptions::new().retry_policy(ActivityRetryPolicy::default()),
7700                ActivityOptionsErrorKind::EmptyRetryPolicy,
7701            ),
7702            (
7703                ActivityOptions::new().retry_policy(ActivityRetryPolicy::new(0)),
7704                ActivityOptionsErrorKind::InvalidMaxAttempts,
7705            ),
7706            (
7707                ActivityOptions::new().retry_policy(ActivityRetryPolicy {
7708                    max_attempts: None,
7709                    backoff: Some(ActivityBackoff::Explicit(vec![Duration::from_secs(1)])),
7710                    non_retryable_error_types: Vec::new(),
7711                }),
7712                ActivityOptionsErrorKind::BackoffWithoutRetryBudget,
7713            ),
7714            (
7715                ActivityOptions::new().retry_policy(
7716                    ActivityRetryPolicy::new(2)
7717                        .backoff_intervals([Duration::from_secs(1), Duration::from_secs(2)]),
7718                ),
7719                ActivityOptionsErrorKind::TooManyBackoffIntervals,
7720            ),
7721            (
7722                ActivityOptions::new().retry_policy(
7723                    ActivityRetryPolicy::new(2).exponential_backoff(
7724                        Duration::from_secs(1),
7725                        0,
7726                        None,
7727                    ),
7728                ),
7729                ActivityOptionsErrorKind::InvalidBackoffCoefficient,
7730            ),
7731            (
7732                ActivityOptions::new()
7733                    .retry_policy(ActivityRetryPolicy::new(2).non_retryable_error_type("  ")),
7734                ActivityOptionsErrorKind::EmptyNonRetryableErrorType,
7735            ),
7736            (
7737                ActivityOptions::new().retry_policy(
7738                    ActivityRetryPolicy::new(10_002).exponential_backoff(
7739                        Duration::from_secs(1),
7740                        1,
7741                        None,
7742                    ),
7743                ),
7744                ActivityOptionsErrorKind::BackoffGenerationTooLarge,
7745            ),
7746            (
7747                ActivityOptions::new().retry_policy(
7748                    ActivityRetryPolicy::new(2)
7749                        .backoff_intervals([Duration::from_secs(i64::MAX as u64 + 1)]),
7750                ),
7751                ActivityOptionsErrorKind::BackoffOverflow,
7752            ),
7753        ];
7754
7755        for (options, expected_kind) in cases {
7756            let ctx = workflow_context(Vec::new());
7757            let mut call = Box::pin(ctx.activity_with_options("work", options, json!([])));
7758            let mut task_context = TaskContext::from_waker(noop_waker_ref());
7759            let Poll::Ready(Err(Error::InvalidActivityOptions(error))) =
7760                call.as_mut().poll(&mut task_context)
7761            else {
7762                panic!("expected typed activity validation error");
7763            };
7764            assert_eq!(error.kind, expected_kind);
7765            assert!(ctx.take_commands().expect("commands").is_empty());
7766        }
7767    }
7768
7769    #[test]
7770    fn activity_options_validate_positive_and_ordered_timeouts() {
7771        let zero_timeout_cases = [
7772            ActivityOptions::new().start_to_close_timeout(Duration::ZERO),
7773            ActivityOptions::new().schedule_to_start_timeout(Duration::ZERO),
7774            ActivityOptions::new().schedule_to_close_timeout(Duration::ZERO),
7775            ActivityOptions::new().heartbeat_timeout(Duration::ZERO),
7776        ];
7777        for options in zero_timeout_cases {
7778            assert_eq!(
7779                options.validate().expect_err("zero timeout").kind,
7780                ActivityOptionsErrorKind::TimeoutNotPositive
7781            );
7782        }
7783
7784        let ordering_cases = [
7785            ActivityOptions::new()
7786                .heartbeat_timeout(Duration::from_secs(11))
7787                .start_to_close_timeout(Duration::from_secs(10)),
7788            ActivityOptions::new()
7789                .start_to_close_timeout(Duration::from_secs(31))
7790                .schedule_to_close_timeout(Duration::from_secs(30)),
7791            ActivityOptions::new()
7792                .schedule_to_start_timeout(Duration::from_secs(31))
7793                .schedule_to_close_timeout(Duration::from_secs(30)),
7794        ];
7795        for options in ordering_cases {
7796            assert_eq!(
7797                options.validate().expect_err("timeout order").kind,
7798                ActivityOptionsErrorKind::TimeoutOrder
7799            );
7800        }
7801
7802        assert_eq!(
7803            ActivityOptions::new()
7804                .start_to_close_timeout(Duration::from_secs(i64::MAX as u64 + 1))
7805                .validate()
7806                .expect_err("protocol integer overflow")
7807                .kind,
7808            ActivityOptionsErrorKind::TimeoutOverflow
7809        );
7810    }
7811
7812    #[test]
7813    fn replayed_activity_retry_history_completes_without_duplicate_schedule() {
7814        let ctx = workflow_context(completed_retry_activity_history());
7815        let mut call =
7816            Box::pin(ctx.activity_with_options("flaky", retry_activity_options(), json!([])));
7817        let mut task_context = TaskContext::from_waker(noop_waker_ref());
7818
7819        assert!(matches!(
7820            call.as_mut().poll(&mut task_context),
7821            Poll::Ready(Ok(result)) if result == json!({"status": "recovered"})
7822        ));
7823        assert!(ctx.take_commands().expect("commands").is_empty());
7824        ctx.ensure_history_consumed().expect("history consumed");
7825    }
7826
7827    #[test]
7828    fn duplicate_non_retryable_types_use_one_command_and_replay_representation() {
7829        let mut options = retry_activity_options();
7830        options
7831            .retry_policy
7832            .as_mut()
7833            .expect("retry policy")
7834            .non_retryable_error_types
7835            .extend([" PermanentError ".to_string(), "PermanentError".to_string()]);
7836
7837        let new_ctx = workflow_context(Vec::new());
7838        let mut new_call =
7839            Box::pin(new_ctx.activity_with_options("flaky", options.clone(), json!([])));
7840        let mut task_context = TaskContext::from_waker(noop_waker_ref());
7841        assert!(matches!(
7842            new_call.as_mut().poll(&mut task_context),
7843            Poll::Pending
7844        ));
7845        let commands = new_ctx.take_commands().expect("commands");
7846        assert_eq!(commands.len(), 1);
7847        assert_eq!(
7848            commands[0]["retry_policy"]["non_retryable_error_types"],
7849            json!(["PermanentError"])
7850        );
7851
7852        let replay_ctx = workflow_context(completed_retry_activity_history());
7853        let mut replay_call =
7854            Box::pin(replay_ctx.activity_with_options("flaky", options, json!([])));
7855        assert!(matches!(
7856            replay_call.as_mut().poll(&mut task_context),
7857            Poll::Ready(Ok(result)) if result == json!({"status": "recovered"})
7858        ));
7859        assert!(replay_ctx.take_commands().expect("commands").is_empty());
7860        replay_ctx
7861            .ensure_history_consumed()
7862            .expect("history consumed");
7863    }
7864
7865    #[test]
7866    fn replayed_intermediate_retry_remains_pending_across_restarts() {
7867        let history = completed_retry_activity_history()
7868            .into_iter()
7869            .take(3)
7870            .collect::<Vec<_>>();
7871
7872        for _restart in 0..2 {
7873            let ctx = workflow_context(history.clone());
7874            let mut call =
7875                Box::pin(ctx.activity_with_options("flaky", retry_activity_options(), json!([])));
7876            let mut task_context = TaskContext::from_waker(noop_waker_ref());
7877            assert!(matches!(
7878                call.as_mut().poll(&mut task_context),
7879                Poll::Pending
7880            ));
7881            assert!(ctx.take_commands().expect("commands").is_empty());
7882        }
7883    }
7884
7885    #[test]
7886    fn replayed_activity_rejects_changed_queue_retry_and_every_timeout_field() {
7887        let mut changed_queue = retry_activity_options();
7888        changed_queue.task_queue = Some("different-queue".to_string());
7889
7890        let mut changed_max_attempts = retry_activity_options();
7891        let retry_policy = changed_max_attempts
7892            .retry_policy
7893            .as_mut()
7894            .expect("retry policy");
7895        retry_policy.max_attempts = Some(4);
7896
7897        let mut changed_backoff = retry_activity_options();
7898        let retry_policy = changed_backoff.retry_policy.as_mut().expect("retry policy");
7899        retry_policy.backoff = Some(ActivityBackoff::Explicit(vec![
7900            Duration::from_secs(3),
7901            Duration::from_secs(4),
7902        ]));
7903
7904        let mut changed_non_retryable_types = retry_activity_options();
7905        let retry_policy = changed_non_retryable_types
7906            .retry_policy
7907            .as_mut()
7908            .expect("retry policy");
7909        retry_policy.non_retryable_error_types = vec!["AnotherPermanentError".to_string()];
7910
7911        let mut changed_start_to_close = retry_activity_options();
7912        changed_start_to_close.start_to_close_timeout = Some(Duration::from_secs(31));
7913        let mut changed_schedule_to_start = retry_activity_options();
7914        changed_schedule_to_start.schedule_to_start_timeout = Some(Duration::from_secs(6));
7915        let mut changed_schedule_to_close = retry_activity_options();
7916        changed_schedule_to_close.schedule_to_close_timeout = Some(Duration::from_secs(91));
7917        let mut changed_heartbeat = retry_activity_options();
7918        changed_heartbeat.heartbeat_timeout = Some(Duration::from_secs(11));
7919
7920        let cases = [
7921            (changed_queue, "activity_task_queue_mismatch"),
7922            (changed_max_attempts, "activity_retry_policy_mismatch"),
7923            (changed_backoff, "activity_retry_policy_mismatch"),
7924            (
7925                changed_non_retryable_types,
7926                "activity_retry_policy_mismatch",
7927            ),
7928            (changed_start_to_close, "activity_retry_policy_mismatch"),
7929            (changed_schedule_to_start, "activity_retry_policy_mismatch"),
7930            (changed_schedule_to_close, "activity_retry_policy_mismatch"),
7931            (changed_heartbeat, "activity_retry_policy_mismatch"),
7932        ];
7933
7934        for (options, expected_reason) in cases {
7935            let ctx = workflow_context(completed_retry_activity_history());
7936            let mut call = Box::pin(ctx.activity_with_options("flaky", options, json!([])));
7937            let mut task_context = TaskContext::from_waker(noop_waker_ref());
7938            let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
7939                call.as_mut().poll(&mut task_context)
7940            else {
7941                panic!("changed activity options must fail replay");
7942            };
7943            assert_eq!(failure.reason, expected_reason);
7944            assert_eq!(failure.sequence, Some(1));
7945            assert!(ctx.take_commands().expect("commands").is_empty());
7946        }
7947    }
7948
7949    #[test]
7950    fn replayed_activity_rejects_changed_execution_mode_and_snapshot_version() {
7951        let cases = [
7952            (
7953                "execution_mode",
7954                json!("local"),
7955                "activity_execution_mode_mismatch",
7956            ),
7957            (
7958                "snapshot_version",
7959                json!(2),
7960                "activity_retry_policy_mismatch",
7961            ),
7962        ];
7963
7964        for (field, value, expected_reason) in cases {
7965            let mut history = completed_retry_activity_history();
7966            let activity = history[0].payload["activity"]
7967                .as_object_mut()
7968                .expect("activity snapshot");
7969            if field == "execution_mode" {
7970                activity.insert(field.to_string(), value);
7971            } else {
7972                activity["retry_policy"]
7973                    .as_object_mut()
7974                    .expect("retry snapshot")
7975                    .insert(field.to_string(), value);
7976            }
7977
7978            let ctx = workflow_context(history);
7979            let mut call =
7980                Box::pin(ctx.activity_with_options("flaky", retry_activity_options(), json!([])));
7981            let mut task_context = TaskContext::from_waker(noop_waker_ref());
7982            let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
7983                call.as_mut().poll(&mut task_context)
7984            else {
7985                panic!("changed {field} must fail replay");
7986            };
7987            assert_eq!(failure.reason, expected_reason);
7988            assert_eq!(failure.sequence, Some(1));
7989            assert!(ctx.take_commands().expect("commands").is_empty());
7990        }
7991    }
7992
7993    #[test]
7994    fn replayed_legacy_activity_treats_missing_option_snapshot_as_unknown() {
7995        let mut history = completed_retry_activity_history();
7996        let activity = history[0].payload["activity"]
7997            .as_object_mut()
7998            .expect("activity snapshot");
7999        activity.remove("execution_mode");
8000        activity.remove("retry_policy");
8001
8002        let mut current = retry_activity_options();
8003        current.start_to_close_timeout = Some(Duration::from_secs(45));
8004        current.schedule_to_start_timeout = Some(Duration::from_secs(8));
8005        current.schedule_to_close_timeout = Some(Duration::from_secs(120));
8006        current.heartbeat_timeout = Some(Duration::from_secs(12));
8007
8008        let ctx = workflow_context(history);
8009        let mut call = Box::pin(ctx.activity_with_options("flaky", current, json!([])));
8010        let mut task_context = TaskContext::from_waker(noop_waker_ref());
8011        assert!(matches!(
8012            call.as_mut().poll(&mut task_context),
8013            Poll::Ready(Ok(result)) if result == json!({"status": "recovered"})
8014        ));
8015        assert!(ctx.take_commands().expect("commands").is_empty());
8016        ctx.ensure_history_consumed().expect("history consumed");
8017    }
8018
8019    #[test]
8020    fn terminal_activity_failed_after_start_returns_typed_failure() {
8021        let history = vec![
8022            history_event(
8023                "ActivityScheduled",
8024                json!({
8025                    "sequence": 1,
8026                    "activity_type": "flaky",
8027                    "activity_execution_id": "act-terminal",
8028                    "activity": {
8029                        "id": "act-terminal",
8030                        "sequence": 1,
8031                        "type": "flaky",
8032                        "queue": "critical-activities",
8033                        "retry_policy": {
8034                            "snapshot_version": 1,
8035                            "max_attempts": 3,
8036                            "backoff_seconds": [2, 4],
8037                            "non_retryable_error_types": ["PermanentError"]
8038                        }
8039                    }
8040                }),
8041            ),
8042            history_event(
8043                "ActivityStarted",
8044                json!({
8045                    "sequence": 1,
8046                    "activity_type": "flaky",
8047                    "activity_execution_id": "act-terminal",
8048                    "activity_attempt_id": "attempt-1",
8049                    "attempt_number": 1
8050                }),
8051            ),
8052            history_event(
8053                "ActivityFailed",
8054                json!({
8055                    "sequence": 1,
8056                    "activity_type": "flaky",
8057                    "activity_execution_id": "act-terminal",
8058                    "activity_attempt_id": "attempt-1",
8059                    "attempt_number": 1,
8060                    "failure_id": "failure-terminal",
8061                    "failure_category": "activity",
8062                    "exception_type": "PermanentError",
8063                    "message": "cannot retry",
8064                    "non_retryable": true
8065                }),
8066            ),
8067        ];
8068        let ctx = workflow_context(history);
8069        let mut call =
8070            Box::pin(ctx.activity_with_options("flaky", retry_activity_options(), json!([])));
8071        let mut task_context = TaskContext::from_waker(noop_waker_ref());
8072
8073        let Poll::Ready(Err(Error::ActivityFailed(failure))) =
8074            call.as_mut().poll(&mut task_context)
8075        else {
8076            panic!("terminal ActivityFailed must settle the activity future");
8077        };
8078        assert_eq!(failure.kind, ActivityFailureKind::Failed);
8079        assert_eq!(
8080            failure.activity_execution_id.as_deref(),
8081            Some("act-terminal")
8082        );
8083        assert_eq!(failure.exception_type.as_deref(), Some("PermanentError"));
8084        assert!(failure.non_retryable);
8085        assert!(ctx.take_commands().expect("commands").is_empty());
8086        ctx.ensure_history_consumed().expect("history consumed");
8087    }
8088
8089    #[test]
8090    fn activity_terminal_events_return_machine_readable_failures() {
8091        let cases = [
8092            (
8093                "ActivityFailed",
8094                json!({
8095                    "sequence": 1,
8096                    "activity_type": "charge-card",
8097                    "activity_execution_id": "act-1",
8098                    "activity_attempt_id": "attempt-2",
8099                    "attempt_number": 2,
8100                    "failure_id": "failure-1",
8101                    "failure_category": "activity",
8102                    "exception_type": "PaymentDeclined",
8103                    "exception_class": "payments.PaymentDeclined",
8104                    "message": "card declined",
8105                    "non_retryable": true
8106                }),
8107                ActivityFailureKind::Failed,
8108                "activity",
8109            ),
8110            (
8111                "ActivityCancelled",
8112                json!({
8113                    "sequence": 1,
8114                    "activity_type": "charge-card",
8115                    "activity_execution_id": "act-1",
8116                    "activity_attempt_id": "attempt-1"
8117                }),
8118                ActivityFailureKind::Cancelled,
8119                "cancelled",
8120            ),
8121        ];
8122
8123        for (event_type, payload, expected_kind, expected_reason) in cases {
8124            let ctx = workflow_context(vec![history_event(event_type, payload)]);
8125            let mut call = Box::pin(ctx.activity("charge-card", json!([])));
8126            let mut task_context = TaskContext::from_waker(noop_waker_ref());
8127            let Poll::Ready(Err(Error::ActivityFailed(failure))) =
8128                call.as_mut().poll(&mut task_context)
8129            else {
8130                panic!("expected terminal activity failure");
8131            };
8132            assert_eq!(failure.kind, expected_kind);
8133            assert_eq!(failure.reason, expected_reason);
8134            assert_eq!(failure.activity_execution_id.as_deref(), Some("act-1"));
8135            assert_eq!(failure.activity_type.as_deref(), Some("charge-card"));
8136        }
8137    }
8138
8139    #[test]
8140    fn every_activity_timeout_class_is_typed() {
8141        for timeout_kind in [
8142            "start_to_close",
8143            "schedule_to_start",
8144            "schedule_to_close",
8145            "heartbeat",
8146        ] {
8147            let ctx = workflow_context(vec![history_event(
8148                "ActivityTimedOut",
8149                json!({
8150                    "sequence": 1,
8151                    "activity_type": "slow",
8152                    "activity_execution_id": "act-timeout",
8153                    "activity_attempt_id": "attempt-timeout",
8154                    "failure_category": "timeout",
8155                    "timeout_kind": timeout_kind,
8156                    "message": "deadline expired"
8157                }),
8158            )]);
8159            let mut call = Box::pin(ctx.activity("slow", json!([])));
8160            let mut task_context = TaskContext::from_waker(noop_waker_ref());
8161            let Poll::Ready(Err(Error::ActivityFailed(failure))) =
8162                call.as_mut().poll(&mut task_context)
8163            else {
8164                panic!("expected timeout failure");
8165            };
8166            assert_eq!(failure.kind, ActivityFailureKind::TimedOut);
8167            assert_eq!(failure.reason, timeout_kind);
8168            assert_eq!(failure.timeout_kind.as_deref(), Some(timeout_kind));
8169            assert_eq!(failure.failure_category.as_deref(), Some("timeout"));
8170        }
8171    }
8172
8173    #[test]
8174    fn workflow_sleep_emits_one_durable_timer_and_rounds_up() {
8175        let ctx = workflow_context(Vec::new());
8176        let mut sleep = Box::pin(ctx.sleep(Duration::from_millis(1_001)));
8177        let mut task_context = TaskContext::from_waker(noop_waker_ref());
8178
8179        assert!(matches!(
8180            sleep.as_mut().poll(&mut task_context),
8181            Poll::Pending
8182        ));
8183        assert!(matches!(
8184            sleep.as_mut().poll(&mut task_context),
8185            Poll::Pending
8186        ));
8187
8188        let commands = ctx.take_commands().expect("timer command");
8189        assert_eq!(
8190            commands,
8191            vec![json!({
8192                "type": "start_timer",
8193                "delay_seconds": 2,
8194            })]
8195        );
8196    }
8197
8198    #[test]
8199    fn workflow_sleep_replays_matching_schedule_and_fire_without_a_command() {
8200        let history = vec![
8201            history_event(
8202                "TimerScheduled",
8203                json!({
8204                    "sequence": 1,
8205                    "timer_id": "timer-1",
8206                    "delay_seconds": 5,
8207                    "fire_at": "2026-07-11T12:00:05Z",
8208                }),
8209            ),
8210            history_event(
8211                "TimerFired",
8212                json!({
8213                    "sequence": 1,
8214                    "timer_id": "timer-1",
8215                    "delay_seconds": 5,
8216                    "fire_at": "2026-07-11T12:00:05Z",
8217                    "fired_at": "2026-07-11T12:00:05Z",
8218                }),
8219            ),
8220        ];
8221
8222        for _restart in 0..2 {
8223            let ctx = workflow_context(history.clone());
8224            let mut sleep = Box::pin(ctx.sleep(Duration::from_secs(5)));
8225            let mut task_context = TaskContext::from_waker(noop_waker_ref());
8226            assert!(matches!(
8227                sleep.as_mut().poll(&mut task_context),
8228                Poll::Ready(Ok(()))
8229            ));
8230            assert!(ctx.take_commands().expect("commands").is_empty());
8231            ctx.ensure_history_consumed().expect("history consumed");
8232        }
8233    }
8234
8235    #[test]
8236    fn workflow_sleep_rejects_changed_delay_during_replay() {
8237        let ctx = workflow_context(vec![
8238            history_event(
8239                "TimerScheduled",
8240                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8241            ),
8242            history_event(
8243                "TimerFired",
8244                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8245            ),
8246        ]);
8247        let mut sleep = Box::pin(ctx.sleep(Duration::from_secs(500)));
8248        let mut task_context = TaskContext::from_waker(noop_waker_ref());
8249
8250        let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
8251            sleep.as_mut().poll(&mut task_context)
8252        else {
8253            panic!("changed timer delay must be rejected");
8254        };
8255        assert_eq!(failure.reason, "timer_delay_mismatch");
8256        assert_eq!(failure.sequence, Some(1));
8257    }
8258
8259    #[test]
8260    fn workflow_history_rejects_unpaired_or_mismatched_timer_events() {
8261        let lone_fire = WorkflowState::new(
8262            vec![history_event(
8263                "TimerFired",
8264                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8265            )],
8266            "rust-workers".to_string(),
8267            JSON_CODEC.to_string(),
8268            None,
8269        )
8270        .expect_err("TimerFired requires TimerScheduled");
8271        assert!(matches!(
8272            lone_fire,
8273            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
8274                if reason == "timer_schedule_missing_or_duplicate"
8275        ));
8276
8277        let wrong_identity = WorkflowState::new(
8278            vec![
8279                history_event(
8280                    "TimerScheduled",
8281                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8282                ),
8283                history_event(
8284                    "TimerFired",
8285                    json!({"sequence": 1, "timer_id": "timer-2", "delay_seconds": 5}),
8286                ),
8287            ],
8288            "rust-workers".to_string(),
8289            JSON_CODEC.to_string(),
8290            None,
8291        )
8292        .expect_err("fire must match scheduled timer identity");
8293        assert!(matches!(
8294            wrong_identity,
8295            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
8296                if reason == "timer_identity_mismatch"
8297        ));
8298
8299        let duplicate_fire = WorkflowState::new(
8300            vec![
8301                history_event(
8302                    "TimerScheduled",
8303                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8304                ),
8305                history_event(
8306                    "TimerFired",
8307                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8308                ),
8309                history_event(
8310                    "TimerFired",
8311                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8312                ),
8313            ],
8314            "rust-workers".to_string(),
8315            JSON_CODEC.to_string(),
8316            None,
8317        )
8318        .expect_err("a durable timer cannot fire twice");
8319        assert!(matches!(
8320            duplicate_fire,
8321            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
8322                if reason == "duplicate_timer_fire"
8323        ));
8324
8325        let wrong_fired_delay = WorkflowState::new(
8326            vec![
8327                history_event(
8328                    "TimerScheduled",
8329                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8330                ),
8331                history_event(
8332                    "TimerFired",
8333                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 6}),
8334                ),
8335            ],
8336            "rust-workers".to_string(),
8337            JSON_CODEC.to_string(),
8338            None,
8339        )
8340        .expect_err("timer schedule and fire delays must agree");
8341        assert!(matches!(
8342            wrong_fired_delay,
8343            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
8344                if reason == "timer_history_delay_mismatch"
8345        ));
8346    }
8347
8348    #[test]
8349    fn replay_rejects_activity_moved_before_recorded_timer() {
8350        let ctx = workflow_context(vec![
8351            history_event(
8352                "TimerScheduled",
8353                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8354            ),
8355            history_event(
8356                "TimerFired",
8357                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8358            ),
8359            history_event(
8360                "ActivityCompleted",
8361                json!({
8362                    "sequence": 2,
8363                    "activity_type": "after-timer",
8364                    "payload_codec": "json",
8365                    "result": {"codec": "json", "blob": "\"done\""},
8366                }),
8367            ),
8368        ]);
8369        let mut activity = Box::pin(ctx.activity("after-timer", json!([])));
8370        let mut task_context = TaskContext::from_waker(noop_waker_ref());
8371
8372        let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
8373            activity.as_mut().poll(&mut task_context)
8374        else {
8375            panic!("reordered durable command must be rejected");
8376        };
8377        assert_eq!(failure.reason, "recorded_command_mismatch");
8378        assert_eq!(failure.sequence, Some(1));
8379        assert_eq!(failure.expected.as_deref(), Some("timer"));
8380        assert_eq!(failure.actual.as_deref(), Some("activity:after-timer"));
8381    }
8382
8383    #[test]
8384    fn workflow_context_emits_a_typed_named_signal_wait() {
8385        let ctx = workflow_context(Vec::new());
8386        let mut signal = Box::pin(ctx.wait_signal("finish"));
8387        let mut task_context = TaskContext::from_waker(noop_waker_ref());
8388
8389        assert!(matches!(
8390            signal.as_mut().poll(&mut task_context),
8391            Poll::Pending
8392        ));
8393        assert_eq!(
8394            ctx.take_commands().expect("signal-wait command"),
8395            vec![json!({
8396                "type": "open_signal_wait",
8397                "signal_name": "finish",
8398            })]
8399        );
8400    }
8401
8402    #[test]
8403    fn condition_wait_history_does_not_resolve_a_typed_signal_wait() {
8404        let ctx = workflow_context(vec![
8405            history_event(
8406                "ConditionWaitOpened",
8407                json!({"sequence": 1, "condition_key": "signal:finish"}),
8408            ),
8409            history_event(
8410                "ConditionWaitSatisfied",
8411                json!({"sequence": 1, "condition_key": "signal:finish"}),
8412            ),
8413            history_event(
8414                "SignalReceived",
8415                json!({"signal_name": "finish", "arguments": []}),
8416            ),
8417        ]);
8418        let mut signal = Box::pin(ctx.wait_signal("finish"));
8419        let mut task_context = TaskContext::from_waker(noop_waker_ref());
8420
8421        assert!(matches!(
8422            signal.as_mut().poll(&mut task_context),
8423            Poll::Pending
8424        ));
8425        assert_eq!(
8426            ctx.take_commands().expect("typed signal-wait command"),
8427            vec![json!({
8428                "type": "open_signal_wait",
8429                "signal_name": "finish",
8430            })]
8431        );
8432    }
8433
8434    #[test]
8435    fn replay_orders_signal_waits_and_timers_in_one_command_stream() {
8436        let signal_then_timer = vec![
8437            history_event(
8438                "SignalWaitOpened",
8439                json!({"sequence": 1, "signal_name": "go"}),
8440            ),
8441            history_event(
8442                "SignalApplied",
8443                json!({
8444                    "sequence": 1,
8445                    "signal_name": "go",
8446                    "value": {"codec": "json", "blob": "[\"now\"]"},
8447                }),
8448            ),
8449            history_event(
8450                "TimerScheduled",
8451                json!({"sequence": 2, "timer_id": "timer-2", "delay_seconds": 5}),
8452            ),
8453            history_event(
8454                "TimerFired",
8455                json!({"sequence": 2, "timer_id": "timer-2", "delay_seconds": 5}),
8456            ),
8457        ];
8458
8459        let ctx = workflow_context(signal_then_timer.clone());
8460        let mut signal = Box::pin(ctx.wait_signal("go"));
8461        let mut task_context = TaskContext::from_waker(noop_waker_ref());
8462        assert!(matches!(
8463            signal.as_mut().poll(&mut task_context),
8464            Poll::Ready(Ok(arguments)) if arguments == vec![json!("now")]
8465        ));
8466        let mut timer = Box::pin(ctx.sleep(Duration::from_secs(5)));
8467        assert!(matches!(
8468            timer.as_mut().poll(&mut task_context),
8469            Poll::Ready(Ok(()))
8470        ));
8471        ctx.ensure_history_consumed()
8472            .expect("signal and timer history consumed in order");
8473
8474        let reordered = workflow_context(signal_then_timer);
8475        let mut timer_first = Box::pin(reordered.sleep(Duration::from_secs(5)));
8476        let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
8477            timer_first.as_mut().poll(&mut task_context)
8478        else {
8479            panic!("timer cannot consume signal-wait-first history");
8480        };
8481        assert_eq!(failure.reason, "recorded_command_mismatch");
8482        assert_eq!(failure.sequence, Some(1));
8483        assert_eq!(failure.expected.as_deref(), Some("signal wait"));
8484
8485        let timer_then_signal = vec![
8486            history_event(
8487                "TimerScheduled",
8488                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8489            ),
8490            history_event(
8491                "TimerFired",
8492                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8493            ),
8494            history_event(
8495                "SignalWaitOpened",
8496                json!({"sequence": 2, "signal_name": "go"}),
8497            ),
8498            history_event(
8499                "SignalApplied",
8500                json!({
8501                    "sequence": 2,
8502                    "signal_name": "go",
8503                    "value": {"codec": "json", "blob": "[]"},
8504                }),
8505            ),
8506        ];
8507        let reordered = workflow_context(timer_then_signal);
8508        let mut signal_first = Box::pin(reordered.wait_signal("go"));
8509        let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
8510            signal_first.as_mut().poll(&mut task_context)
8511        else {
8512            panic!("signal wait cannot consume timer-first history");
8513        };
8514        assert_eq!(failure.reason, "recorded_command_mismatch");
8515        assert_eq!(failure.sequence, Some(1));
8516        assert_eq!(failure.expected.as_deref(), Some("timer"));
8517    }
8518
8519    #[test]
8520    fn workflow_history_rejects_duplicate_or_colliding_command_sequences() {
8521        let duplicate_timer = WorkflowState::new(
8522            vec![
8523                history_event(
8524                    "TimerScheduled",
8525                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8526                ),
8527                history_event(
8528                    "TimerScheduled",
8529                    json!({"sequence": 1, "timer_id": "timer-2", "delay_seconds": 5}),
8530                ),
8531            ],
8532            "rust-workers".to_string(),
8533            JSON_CODEC.to_string(),
8534            None,
8535        )
8536        .expect_err("one workflow sequence cannot schedule two timers");
8537        assert!(matches!(
8538            duplicate_timer,
8539            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
8540                if reason == "timer_schedule_missing_or_duplicate"
8541        ));
8542
8543        let colliding_kinds = WorkflowState::new(
8544            vec![
8545                history_event(
8546                    "TimerScheduled",
8547                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8548                ),
8549                history_event(
8550                    "ActivityCompleted",
8551                    json!({"sequence": 1, "activity_type": "same-sequence"}),
8552                ),
8553            ],
8554            "rust-workers".to_string(),
8555            JSON_CODEC.to_string(),
8556            None,
8557        )
8558        .expect_err("one workflow sequence cannot identify two command kinds");
8559        assert!(matches!(
8560            colliding_kinds,
8561            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
8562                if reason == "durable_command_sequence_collision"
8563        ));
8564
8565        let duplicate_signal_wait = WorkflowState::new(
8566            vec![
8567                history_event(
8568                    "SignalWaitOpened",
8569                    json!({"sequence": 1, "signal_name": "go"}),
8570                ),
8571                history_event(
8572                    "SignalWaitOpened",
8573                    json!({"sequence": 1, "signal_name": "go"}),
8574                ),
8575            ],
8576            "rust-workers".to_string(),
8577            JSON_CODEC.to_string(),
8578            None,
8579        )
8580        .expect_err("one workflow sequence cannot open two signal waits");
8581        assert!(matches!(
8582            duplicate_signal_wait,
8583            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
8584                if reason == "signal_wait_open_missing_or_duplicate"
8585        ));
8586    }
8587
8588    #[test]
8589    fn workflow_history_accepts_a_first_command_after_global_sequence_gaps() {
8590        let result = encode_value_envelope(&json!({"captured": true}), JSON_CODEC)
8591            .expect("side-effect result");
8592        let ctx = workflow_context(vec![history_event(
8593            "SideEffectRecorded",
8594            json!({"sequence": 99, "result": result}),
8595        )]);
8596
8597        let replayed: Value = ctx
8598            .side_effect(|| panic!("recorded side effect must not run"))
8599            .expect("positive global workflow sequence is valid");
8600        assert_eq!(replayed, json!({"captured": true}));
8601        ctx.ensure_history_consumed().expect("history consumed");
8602    }
8603
8604    #[test]
8605    fn workflow_history_rejects_zero_and_descending_command_sequences() {
8606        let result =
8607            encode_value_envelope(&json!("captured"), JSON_CODEC).expect("side-effect result");
8608        let zero = WorkflowState::new(
8609            vec![history_event(
8610                "SideEffectRecorded",
8611                json!({"sequence": 0, "result": result.clone()}),
8612            )],
8613            "rust-workers".to_string(),
8614            JSON_CODEC.to_string(),
8615            None,
8616        )
8617        .expect_err("durable command sequences must be positive");
8618        assert!(matches!(
8619            zero,
8620            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
8621                if reason == "durable_command_sequence_invalid"
8622        ));
8623
8624        let descending = WorkflowState::new(
8625            vec![
8626                history_event(
8627                    "SideEffectRecorded",
8628                    json!({"sequence": 3, "result": result}),
8629                ),
8630                history_event(
8631                    "VersionMarkerRecorded",
8632                    json!({
8633                        "sequence": 2,
8634                        "change_id": "descending-marker",
8635                        "version": 1,
8636                        "min_supported": 1,
8637                        "max_supported": 1,
8638                    }),
8639                ),
8640            ],
8641            "rust-workers".to_string(),
8642            JSON_CODEC.to_string(),
8643            None,
8644        )
8645        .expect_err("new durable commands must remain strictly ordered");
8646        let Error::NonDeterministicReplay(failure) = descending else {
8647            panic!("expected typed replay failure");
8648        };
8649        assert_eq!(failure.reason, "durable_command_sequence_mismatch");
8650        assert_eq!(failure.sequence, Some(2));
8651        assert_eq!(
8652            failure.expected.as_deref(),
8653            Some("workflow sequence greater than 3")
8654        );
8655        assert_eq!(failure.actual.as_deref(), Some("2"));
8656    }
8657
8658    #[test]
8659    fn workflow_task_replay_completes_after_signals_create_sequence_gaps() {
8660        fn worker() -> Worker {
8661            let client = Client::new("http://127.0.0.1:8080").expect("client");
8662            let mut worker = Worker::new(client, "rust-workers");
8663            worker.register_workflow("rust.finish-after-gaps", |ctx, _input| async move {
8664                ctx.wait_signal("finish").await?;
8665                let marker: String =
8666                    ctx.side_effect(|| panic!("recorded side effect must not run"))?;
8667                assert_eq!(marker, "after-finish");
8668                Ok(json!("finished"))
8669            });
8670            worker
8671        }
8672
8673        let marker =
8674            encode_value_envelope(&json!("after-finish"), JSON_CODEC).expect("side-effect result");
8675        let task = workflow_task(
8676            "rust.finish-after-gaps",
8677            vec![
8678                history_event(
8679                    "SignalWaitOpened",
8680                    json!({"sequence": 1, "signal_name": "finish"}),
8681                ),
8682                history_event(
8683                    "SignalReceived",
8684                    json!({
8685                        "signal_id": "increment-3",
8686                        "signal_name": "increment",
8687                        "workflow_sequence": 2,
8688                        "payload_codec": "json",
8689                        "arguments": {"codec": "json", "blob": "[3]"},
8690                    }),
8691                ),
8692                history_event(
8693                    "SignalReceived",
8694                    json!({
8695                        "signal_id": "increment-5",
8696                        "signal_name": "increment",
8697                        "workflow_sequence": 3,
8698                        "payload_codec": "json",
8699                        "arguments": {"codec": "json", "blob": "[5]"},
8700                    }),
8701                ),
8702                history_event(
8703                    "SignalReceived",
8704                    json!({
8705                        "signal_id": "finish",
8706                        "signal_name": "finish",
8707                        "workflow_sequence": 4,
8708                        "payload_codec": "json",
8709                        "arguments": {"codec": "json", "blob": "[]"},
8710                    }),
8711                ),
8712                history_event(
8713                    "SignalApplied",
8714                    json!({
8715                        "sequence": 1,
8716                        "signal_id": "finish",
8717                        "signal_name": "finish",
8718                        "payload_codec": "json",
8719                        "value": {"codec": "json", "blob": "[]"},
8720                    }),
8721                ),
8722                history_event(
8723                    "SideEffectRecorded",
8724                    json!({"sequence": 5, "result": marker}),
8725                ),
8726            ],
8727            JSON_CODEC,
8728        );
8729
8730        for _original_or_cold_worker in 0..2 {
8731            let commands = worker()
8732                .execute_workflow_task(task.clone())
8733                .expect("signal gaps preserve deterministic replay");
8734            assert_eq!(commands.len(), 1, "replay emits only terminal completion");
8735            assert_eq!(commands[0]["type"], "complete_workflow");
8736            assert_eq!(
8737                decode_wire_value(&commands[0]["result"], JSON_CODEC).expect("workflow output"),
8738                json!("finished")
8739            );
8740        }
8741    }
8742
8743    #[test]
8744    fn workflow_sleep_rejects_unrepresentable_rounded_duration() {
8745        let ctx = workflow_context(Vec::new());
8746        let mut sleep = Box::pin(ctx.start_timer(Duration::new(u64::MAX, 1)));
8747        let mut task_context = TaskContext::from_waker(noop_waker_ref());
8748        assert!(matches!(
8749            sleep.as_mut().poll(&mut task_context),
8750            Poll::Ready(Err(Error::TimerDurationOverflow))
8751        ));
8752        assert!(ctx.take_commands().expect("commands").is_empty());
8753    }
8754
8755    #[test]
8756    fn workflow_task_replay_completes_without_rescheduling_recorded_commands() {
8757        let client = Client::new("http://127.0.0.1:8080").expect("client");
8758        let mut worker = Worker::new(client, "rust-workers");
8759        worker.register_workflow("rust.timer", |ctx, _input| async move {
8760            ctx.sleep(Duration::from_secs(5)).await?;
8761            ctx.activity("after-timer", json!([])).await
8762        });
8763
8764        let task = |history_events| WorkflowTask {
8765            task_id: "wft-rust-timer-1".to_string(),
8766            workflow_id: Some("wf-rust-timer".to_string()),
8767            run_id: Some("run-rust-timer".to_string()),
8768            workflow_type: "rust.timer".to_string(),
8769            payload_codec: JSON_CODEC.to_string(),
8770            arguments: Some(json!({"codec": "json", "blob": "[]"})),
8771            history_events,
8772            total_history_events: None,
8773            history_size_bytes: None,
8774            continue_as_new_recommended: None,
8775            history_budget_pressure: None,
8776            next_history_page_token: None,
8777            workflow_task_attempt: 1,
8778            workflow_signal_id: None,
8779            signal_name: None,
8780            signal_arguments: None,
8781            lease_owner: Some("rust-worker".to_string()),
8782        };
8783
8784        let initial = worker
8785            .execute_workflow_task(task(Vec::new()))
8786            .expect("initial timer task");
8787        assert_eq!(
8788            initial,
8789            vec![json!({"type": "start_timer", "delay_seconds": 5})]
8790        );
8791
8792        let replayed = worker
8793            .execute_workflow_task(task(vec![
8794                history_event(
8795                    "TimerScheduled",
8796                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8797                ),
8798                history_event(
8799                    "TimerFired",
8800                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8801                ),
8802                history_event(
8803                    "ActivityCompleted",
8804                    json!({
8805                        "sequence": 2,
8806                        "activity_type": "after-timer",
8807                        "payload_codec": "json",
8808                        "result": {"codec": "json", "blob": "\"done\""},
8809                    }),
8810                ),
8811            ]))
8812            .expect("replayed workflow task");
8813        assert_eq!(replayed.len(), 1);
8814        assert_eq!(replayed[0]["type"], "complete_workflow");
8815        assert_eq!(
8816            decode_wire_value(&replayed[0]["result"], JSON_CODEC).expect("result"),
8817            json!("done")
8818        );
8819    }
8820
8821    #[test]
8822    fn workflow_continue_as_new_emits_arguments_type_and_queue_once() {
8823        let client = Client::new("http://127.0.0.1:8080").expect("client");
8824        let mut worker = Worker::new(client, "rust-workers");
8825        worker.register_workflow("rust.continue", |ctx, _input| async move {
8826            ctx.continue_as_new_with_options(
8827                ContinueAsNewOptions::new()
8828                    .workflow_type("rust.next")
8829                    .task_queue("next-workers"),
8830                json!([2, {"cursor": "next"}]),
8831            )
8832        });
8833
8834        let commands = worker
8835            .execute_workflow_task(workflow_task("rust.continue", Vec::new(), DEFAULT_CODEC))
8836            .expect("continue-as-new command");
8837
8838        assert_eq!(commands.len(), 1);
8839        assert_eq!(commands[0]["type"], "continue_as_new");
8840        assert_eq!(commands[0]["workflow_type"], "rust.next");
8841        assert_eq!(commands[0]["queue"], "next-workers");
8842        assert_eq!(
8843            decode_wire_value(&commands[0]["arguments"], DEFAULT_CODEC)
8844                .expect("continue-as-new arguments"),
8845            json!([2, {"cursor": "next"}])
8846        );
8847    }
8848
8849    #[test]
8850    fn recorded_continue_as_new_is_consumed_without_duplicate_successor_command() {
8851        let client = Client::new("http://127.0.0.1:8080").expect("client");
8852        let mut worker = Worker::new(client, "rust-workers");
8853        worker.register_workflow("rust.continue", |ctx, _input| async move {
8854            ctx.continue_as_new(json!([2]))
8855        });
8856        let task = workflow_task(
8857            "rust.continue",
8858            vec![history_event(
8859                "WorkflowContinuedAsNew",
8860                json!({"sequence": 1, "continued_to_run_id": "run-next"}),
8861            )],
8862            JSON_CODEC,
8863        );
8864
8865        for _worker_restart_or_redelivery in 0..2 {
8866            let commands = worker
8867                .execute_workflow_task(task.clone())
8868                .expect("recorded transition replays");
8869            assert!(
8870                commands.is_empty(),
8871                "replay must not emit another successor"
8872            );
8873        }
8874    }
8875
8876    #[test]
8877    fn continue_as_new_rejects_invalid_overrides_before_emitting_a_command() {
8878        let ctx = workflow_context(Vec::new());
8879        let error = ctx
8880            .continue_as_new_with_options(ContinueAsNewOptions::new().task_queue("  "), json!([1]))
8881            .expect_err("blank queue must be rejected");
8882
8883        let Error::InvalidContinueAsNewOptions(error) = error else {
8884            panic!("expected typed continue-as-new validation error");
8885        };
8886        assert_eq!(error.field, "task_queue");
8887        assert!(ctx.take_commands().expect("commands").is_empty());
8888    }
8889
8890    #[test]
8891    fn workflow_context_exposes_server_history_budget() {
8892        let client = Client::new("http://127.0.0.1:8080").expect("client");
8893        let mut worker = Worker::new(client, "rust-workers");
8894        worker.register_workflow("rust.history-budget", |ctx, _input| async move {
8895            let budget = ctx.history_budget()?;
8896            Ok(json!({
8897                "events": budget.event_count,
8898                "bytes": budget.size_bytes,
8899                "recommended": budget.continue_as_new_recommended,
8900                "pressure": budget.pressure,
8901            }))
8902        });
8903        let task: WorkflowTask = serde_json::from_value(json!({
8904            "task_id": "task-history-budget",
8905            "workflow_type": "rust.history-budget",
8906            "payload_codec": JSON_CODEC,
8907            "history_events": [],
8908            "total_history_events": 480,
8909            "history_size_bytes": 1_048_576,
8910            "continue_as_new_recommended": true,
8911            "history_budget_pressure": "continue_as_new_recommended",
8912        }))
8913        .expect("published workflow task");
8914
8915        let commands = worker
8916            .execute_workflow_task(task)
8917            .expect("history-budget workflow");
8918        let result = decode_wire_value(&commands[0]["result"], JSON_CODEC).expect("result");
8919        assert_eq!(result["events"], 480);
8920        assert_eq!(result["bytes"], 1_048_576);
8921        assert_eq!(result["recommended"], true);
8922        assert_eq!(result["pressure"], "continue_as_new_recommended");
8923    }
8924
8925    #[test]
8926    fn uncaught_workflow_handler_error_emits_terminal_failure_command() {
8927        let client = Client::new("http://127.0.0.1:8080").expect("client");
8928        let mut worker = Worker::new(client, "rust-workers");
8929        worker.register_workflow("rust.failing", |_ctx, _input| async move {
8930            Err(Error::Codec("rust_conformance_failure".to_string()))
8931        });
8932        let task = WorkflowTask {
8933            task_id: "wft-rust-failing-1".to_string(),
8934            workflow_id: Some("wf-rust-failing".to_string()),
8935            run_id: Some("run-rust-failing".to_string()),
8936            workflow_type: "rust.failing".to_string(),
8937            payload_codec: JSON_CODEC.to_string(),
8938            arguments: Some(encode_value_envelope(&json!([]), JSON_CODEC).expect("input")),
8939            history_events: Vec::new(),
8940            total_history_events: Some(0),
8941            history_size_bytes: None,
8942            continue_as_new_recommended: None,
8943            history_budget_pressure: None,
8944            next_history_page_token: None,
8945            workflow_task_attempt: 1,
8946            workflow_signal_id: None,
8947            signal_name: None,
8948            signal_arguments: None,
8949            lease_owner: Some("rust-worker".to_string()),
8950        };
8951
8952        let commands = worker
8953            .execute_workflow_task(task)
8954            .expect("handler failure becomes a workflow command");
8955
8956        assert_eq!(commands.len(), 1);
8957        assert_eq!(commands[0]["type"], "fail_workflow");
8958        assert_eq!(commands[0]["exception_type"], "RustWorkflowError");
8959        assert_eq!(commands[0]["exception_class"], "durable_workflow::Error");
8960        assert_eq!(commands[0]["non_retryable"], false);
8961        assert_eq!(
8962            commands[0]["message"],
8963            "codec error: rust_conformance_failure"
8964        );
8965        assert_eq!(
8966            commands[0]["exception"]["message"],
8967            "codec error: rust_conformance_failure"
8968        );
8969    }
8970
8971    #[test]
8972    fn ordinary_handler_error_preserves_commands_queued_in_the_same_decision() {
8973        let client = Client::new("http://127.0.0.1:8080").expect("client");
8974        let mut worker = Worker::new(client, "rust-workers");
8975        worker.register_workflow("rust.failing-after-side-effect", |ctx, _input| async move {
8976            let _: String = ctx.side_effect(|| "captured".to_string())?;
8977            Err(Error::WorkerLoop("application failure".to_string()))
8978        });
8979
8980        let commands = worker
8981            .execute_workflow_task(workflow_task(
8982                "rust.failing-after-side-effect",
8983                Vec::new(),
8984                JSON_CODEC,
8985            ))
8986            .expect("ordinary failure remains a workflow decision");
8987
8988        assert_eq!(commands.len(), 2);
8989        assert_eq!(commands[0]["type"], "record_side_effect");
8990        assert_eq!(commands[1]["type"], "fail_workflow");
8991    }
8992
8993    #[test]
8994    fn handler_error_cannot_hide_an_unconsumed_committed_side_effect() {
8995        let client = Client::new("http://127.0.0.1:8080").expect("client");
8996        let mut worker = Worker::new(client, "rust-workers");
8997        worker.register_workflow("rust.removed-side-effect", |_ctx, _input| async move {
8998            Err(Error::WorkerLoop("application failure".to_string()))
8999        });
9000        let result =
9001            encode_value_envelope(&json!("committed"), JSON_CODEC).expect("side-effect result");
9002
9003        let error = worker
9004            .execute_workflow_task(workflow_task(
9005                "rust.removed-side-effect",
9006                vec![history_event(
9007                    "SideEffectRecorded",
9008                    json!({"sequence": 1, "result": result}),
9009                )],
9010                JSON_CODEC,
9011            ))
9012            .expect_err("removed committed history must not become fail_workflow");
9013
9014        let Error::NonDeterministicReplay(failure) = error else {
9015            panic!("expected typed replay failure");
9016        };
9017        assert_eq!(failure.reason, "recorded_commands_unconsumed");
9018        assert_eq!(failure.sequence, Some(1));
9019        assert_eq!(failure.expected.as_deref(), Some("side effect"));
9020    }
9021
9022    #[test]
9023    fn replay_error_discards_side_effect_queued_before_incompatible_marker_check() {
9024        let client = Client::new("http://127.0.0.1:8080").expect("client");
9025        let mut worker = Worker::new(client, "rust-workers");
9026        worker.register_workflow(
9027            "rust.side-effect-before-marker-error",
9028            |ctx, _input| async move {
9029                assert_eq!(ctx.get_version("restart-safe", 1, 1)?, 1);
9030                let _: String = ctx.side_effect(|| "must-not-commit".to_string())?;
9031                ctx.get_version("restart-safe", 2, 2)?;
9032                Ok(Value::Null)
9033            },
9034        );
9035
9036        let error = worker
9037            .execute_workflow_task(workflow_task(
9038                "rust.side-effect-before-marker-error",
9039                vec![history_event(
9040                    "VersionMarkerRecorded",
9041                    json!({
9042                        "sequence": 1,
9043                        "change_id": "restart-safe",
9044                        "version": 1,
9045                        "min_supported": 1,
9046                        "max_supported": 1,
9047                    }),
9048                )],
9049                JSON_CODEC,
9050            ))
9051            .expect_err("replay error must return no queued workflow commands");
9052
9053        let Error::NonDeterministicReplay(failure) = error else {
9054            panic!("expected typed replay failure");
9055        };
9056        assert_eq!(failure.reason, "version_marker_incompatible_range");
9057        assert_eq!(failure.sequence, Some(1));
9058    }
9059
9060    #[test]
9061    fn workflow_task_replay_keeps_recorded_unfired_timer_pending_without_rescheduling() {
9062        let client = Client::new("http://127.0.0.1:8080").expect("client");
9063        let mut worker = Worker::new(client, "rust-workers");
9064        worker.register_workflow("rust.timer.pending", |ctx, _input| async move {
9065            ctx.sleep(Duration::from_secs(5)).await?;
9066            Ok(json!({"status": "timer fired"}))
9067        });
9068
9069        let task = WorkflowTask {
9070            task_id: "wft-rust-timer-pending".to_string(),
9071            workflow_id: Some("wf-rust-timer".to_string()),
9072            run_id: Some("run-rust-timer".to_string()),
9073            workflow_type: "rust.timer.pending".to_string(),
9074            payload_codec: JSON_CODEC.to_string(),
9075            arguments: Some(json!({"codec": "json", "blob": "[]"})),
9076            history_events: vec![history_event(
9077                "TimerScheduled",
9078                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
9079            )],
9080            total_history_events: Some(1),
9081            history_size_bytes: None,
9082            continue_as_new_recommended: None,
9083            history_budget_pressure: None,
9084            next_history_page_token: None,
9085            workflow_task_attempt: 1,
9086            workflow_signal_id: None,
9087            signal_name: None,
9088            signal_arguments: None,
9089            lease_owner: Some("rust-worker".to_string()),
9090        };
9091
9092        for _redelivery_or_restart in 0..2 {
9093            let commands = worker
9094                .execute_workflow_task(task.clone())
9095                .expect("recorded timer remains pending");
9096            assert!(
9097                commands.is_empty(),
9098                "recorded timer must not be rescheduled"
9099            );
9100        }
9101    }
9102
9103    #[test]
9104    fn workflow_task_rejects_recorded_command_removed_from_workflow_code() {
9105        let client = Client::new("http://127.0.0.1:8080").expect("client");
9106        let mut worker = Worker::new(client, "rust-workers");
9107        worker.register_workflow("rust.timer.removed", |_ctx, _input| async move {
9108            Ok(json!({"status": "completed"}))
9109        });
9110        let task = WorkflowTask {
9111            task_id: "wft-rust-timer-removed".to_string(),
9112            workflow_id: Some("wf-rust-timer".to_string()),
9113            run_id: Some("run-rust-timer".to_string()),
9114            workflow_type: "rust.timer.removed".to_string(),
9115            payload_codec: JSON_CODEC.to_string(),
9116            arguments: Some(json!({"codec": "json", "blob": "[]"})),
9117            history_events: vec![
9118                history_event(
9119                    "TimerScheduled",
9120                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
9121                ),
9122                history_event(
9123                    "TimerFired",
9124                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
9125                ),
9126            ],
9127            total_history_events: Some(2),
9128            history_size_bytes: None,
9129            continue_as_new_recommended: None,
9130            history_budget_pressure: None,
9131            next_history_page_token: None,
9132            workflow_task_attempt: 1,
9133            workflow_signal_id: None,
9134            signal_name: None,
9135            signal_arguments: None,
9136            lease_owner: Some("rust-worker".to_string()),
9137        };
9138
9139        let Error::NonDeterministicReplay(failure) = worker
9140            .execute_workflow_task(task)
9141            .expect_err("removed timer must fail replay")
9142        else {
9143            panic!("expected typed replay failure");
9144        };
9145        assert_eq!(failure.reason, "recorded_commands_unconsumed");
9146        assert_eq!(failure.sequence, Some(1));
9147    }
9148
9149    #[test]
9150    fn workflow_context_emits_explicit_child_workflow_contract() {
9151        let ctx = WorkflowContext {
9152            state: Arc::new(Mutex::new(
9153                WorkflowState::new_with_identity(
9154                    Vec::new(),
9155                    Some("wf-parent".to_string()),
9156                    Some("run-parent".to_string()),
9157                    "parent-workers".to_string(),
9158                    JSON_CODEC.to_string(),
9159                    None,
9160                )
9161                .expect("workflow state"),
9162            )),
9163        };
9164        let options = ChildWorkflowOptions::new("python-workers")
9165            .parent_close_policy(ParentClosePolicy::RequestCancel)
9166            .retry_policy(ChildWorkflowRetryPolicy {
9167                max_attempts: Some(3),
9168                backoff_seconds: vec![1, 5],
9169                non_retryable_error_types: vec!["ValidationError".to_string()],
9170            })
9171            .execution_timeout_seconds(600)
9172            .run_timeout_seconds(120);
9173        let mut call = Box::pin(ctx.start_child_workflow(
9174            "python.fulfil-order",
9175            options,
9176            json!([{"order_id": "order-42"}]),
9177        ));
9178        let mut task_context = TaskContext::from_waker(noop_waker_ref());
9179
9180        assert!(matches!(
9181            call.as_mut().poll(&mut task_context),
9182            Poll::Pending
9183        ));
9184        let commands = ctx.take_commands().expect("commands");
9185        assert_eq!(commands.len(), 1);
9186        let command = &commands[0];
9187        assert_eq!(command["type"], "start_child_workflow");
9188        assert_eq!(command["workflow_type"], "python.fulfil-order");
9189        assert_eq!(command["queue"], "python-workers");
9190        assert_eq!(command["parent_close_policy"], "request_cancel");
9191        assert_eq!(command["retry_policy"]["max_attempts"], 3);
9192        assert_eq!(command["execution_timeout_seconds"], 600);
9193        assert_eq!(command["run_timeout_seconds"], 120);
9194        assert_eq!(
9195            decode_wire_value(&command["arguments"], JSON_CODEC).expect("child args"),
9196            json!([{"order_id": "order-42"}])
9197        );
9198    }
9199
9200    fn child_parent_worker() -> Worker {
9201        let client = Client::new("http://127.0.0.1:8080").expect("client");
9202        let mut worker = Worker::new(client, "rust-parent-workers");
9203        worker.register_workflow("rust.parent", |ctx, _input| async move {
9204            let child = ctx
9205                .start_child_workflow(
9206                    "python.child",
9207                    ChildWorkflowOptions::new("python-child-workers")
9208                        .parent_close_policy(ParentClosePolicy::Terminate),
9209                    json!([{"codec_probe": [1, true, "rust"]}]),
9210                )
9211                .await?;
9212            Ok(json!({
9213                "parent_workflow_id": child.parent.workflow_id,
9214                "parent_run_id": child.parent.run_id,
9215                "child_workflow_id": child.child.workflow_id,
9216                "child_run_id": child.child.run_id,
9217                "child_workflow_type": child.child_workflow_type,
9218                "result": child.result,
9219            }))
9220        });
9221        worker
9222    }
9223
9224    fn child_parent_task(event_type: &str, payload: Value) -> WorkflowTask {
9225        WorkflowTask {
9226            task_id: "wft-child-parent".to_string(),
9227            workflow_id: Some("wf-parent".to_string()),
9228            run_id: Some("run-parent".to_string()),
9229            workflow_type: "rust.parent".to_string(),
9230            payload_codec: JSON_CODEC.to_string(),
9231            arguments: Some(encode_value_envelope(&json!([]), JSON_CODEC).expect("input")),
9232            history_events: vec![
9233                HistoryEvent {
9234                    event_type: "ChildWorkflowScheduled".to_string(),
9235                    payload: json!({
9236                        "sequence": 1,
9237                        "child_call_id": "call-child",
9238                        "child_workflow_instance_id": "wf-child",
9239                        "child_workflow_run_id": "run-child",
9240                        "child_workflow_type": "python.child",
9241                    }),
9242                    raw: HashMap::new(),
9243                },
9244                HistoryEvent {
9245                    event_type: event_type.to_string(),
9246                    payload,
9247                    raw: HashMap::new(),
9248                },
9249            ],
9250            total_history_events: Some(2),
9251            history_size_bytes: None,
9252            continue_as_new_recommended: None,
9253            history_budget_pressure: None,
9254            next_history_page_token: None,
9255            workflow_task_attempt: 1,
9256            workflow_signal_id: None,
9257            signal_name: None,
9258            signal_arguments: None,
9259            lease_owner: Some("rust-worker".to_string()),
9260        }
9261    }
9262
9263    #[test]
9264    fn committed_child_result_replays_without_starting_a_duplicate() {
9265        let worker = child_parent_worker();
9266        let task = child_parent_task(
9267            "ChildRunCompleted",
9268            json!({
9269                "sequence": 1,
9270                "child_call_id": "call-child",
9271                "child_workflow_instance_id": "wf-child",
9272                "child_workflow_run_id": "run-child",
9273                "child_workflow_type": "python.child",
9274                "payload_codec": "json",
9275                "result": {"codec": "json", "blob": "{\"from\":\"python\",\"ok\":true}"},
9276            }),
9277        );
9278
9279        for _restart in 0..2 {
9280            let commands = worker
9281                .execute_workflow_task(task.clone())
9282                .expect("replayed parent task");
9283            assert_eq!(commands.len(), 1);
9284            assert_eq!(commands[0]["type"], "complete_workflow");
9285            assert!(!commands
9286                .iter()
9287                .any(|command| command["type"] == "start_child_workflow"));
9288            let output =
9289                decode_wire_value(&commands[0]["result"], JSON_CODEC).expect("parent output");
9290            assert_eq!(output["parent_workflow_id"], "wf-parent");
9291            assert_eq!(output["parent_run_id"], "run-parent");
9292            assert_eq!(output["child_workflow_id"], "wf-child");
9293            assert_eq!(output["child_run_id"], "run-child");
9294            assert_eq!(output["result"], json!({"from": "python", "ok": true}));
9295        }
9296    }
9297
9298    #[test]
9299    fn pending_child_replays_after_restart_without_starting_a_duplicate() {
9300        let worker = child_parent_worker();
9301        let mut task = child_parent_task("unused", Value::Null);
9302        task.history_events.truncate(1);
9303        task.total_history_events = Some(1);
9304
9305        for _redelivery_or_restart in 0..2 {
9306            let commands = worker
9307                .execute_workflow_task(task.clone())
9308                .expect("recorded child remains pending");
9309            assert!(
9310                commands.is_empty(),
9311                "recorded pending child must not be started again"
9312            );
9313        }
9314    }
9315
9316    #[test]
9317    fn child_cancellation_becomes_stable_parent_failure_command() {
9318        let worker = child_parent_worker();
9319        let task = child_parent_task(
9320            "ChildRunCancelled",
9321            json!({
9322                "sequence": 1,
9323                "child_workflow_instance_id": "wf-child",
9324                "child_workflow_run_id": "run-child",
9325                "child_workflow_type": "python.child",
9326                "failure_id": "failure-child",
9327                "failure_category": "cancelled",
9328                "message": "cancelled by parent-close policy",
9329            }),
9330        );
9331
9332        let commands = worker
9333            .execute_workflow_task(task)
9334            .expect("parent settlement");
9335        assert_eq!(commands.len(), 1);
9336        assert_eq!(commands[0]["type"], "fail_workflow");
9337        assert_eq!(commands[0]["exception_type"], "ChildWorkflowCancelled");
9338        assert_eq!(
9339            commands[0]["exception"]["properties"]["reason"],
9340            "cancelled"
9341        );
9342        assert_eq!(
9343            commands[0]["exception"]["properties"]["child_workflow_run_id"],
9344            "run-child"
9345        );
9346    }
9347
9348    #[test]
9349    fn workflow_can_handle_typed_child_failure() {
9350        let client = Client::new("http://127.0.0.1:8080").expect("client");
9351        let mut worker = Worker::new(client, "rust-parent-workers");
9352        worker.register_workflow("rust.handled-parent", |ctx, _input| async move {
9353            match ctx
9354                .start_child_workflow(
9355                    "python.child",
9356                    ChildWorkflowOptions::new("python-child-workers"),
9357                    json!([]),
9358                )
9359                .await
9360            {
9361                Err(Error::ChildWorkflowFailed(failure)) => Ok(json!({
9362                    "reason": failure.reason,
9363                    "failure_id": failure.failure_id,
9364                    "exception_class": failure.exception_class,
9365                    "child_run_id": failure.child_workflow_run_id,
9366                })),
9367                Err(error) => Err(error),
9368                Ok(_) => Err(Error::WorkerLoop(
9369                    "child unexpectedly succeeded".to_string(),
9370                )),
9371            }
9372        });
9373        let mut task = child_parent_task(
9374            "ChildRunFailed",
9375            json!({
9376                "sequence": 1,
9377                "child_workflow_instance_id": "wf-child",
9378                "child_workflow_run_id": "run-child",
9379                "child_workflow_type": "python.child",
9380                "failure_id": "failure-child",
9381                "failure_category": "child_workflow",
9382                "message": "payment rejected",
9383                "exception": {
9384                    "type": "PaymentRejected",
9385                    "class": "payments.PaymentRejected",
9386                    "message": "payment rejected"
9387                }
9388            }),
9389        );
9390        task.workflow_type = "rust.handled-parent".to_string();
9391
9392        let commands = worker.execute_workflow_task(task).expect("handled failure");
9393        assert_eq!(commands[0]["type"], "complete_workflow");
9394        let output = decode_wire_value(&commands[0]["result"], JSON_CODEC).expect("parent output");
9395        assert_eq!(output["reason"], "child_workflow");
9396        assert_eq!(output["failure_id"], "failure-child");
9397        assert_eq!(output["exception_class"], "payments.PaymentRejected");
9398        assert_eq!(output["child_run_id"], "run-child");
9399    }
9400
9401    #[test]
9402    fn rust_hello_world_uses_signal_arguments_from_resume_payload() {
9403        let client = Client::new("http://127.0.0.1:8080").expect("client");
9404        let mut worker = Worker::new(client, "rust-workers");
9405
9406        worker.register_workflow("rust.hello_workflow", |ctx, _input| async move {
9407            let signal = ctx.wait_signal("start").await?;
9408            let name = signal
9409                .first()
9410                .and_then(|value| value.as_str())
9411                .unwrap_or("world");
9412            let greeting = ctx.activity("rust.hello_activity", json!([name])).await?;
9413            Ok(json!({
9414                "greeting": greeting,
9415                "language": "rust"
9416            }))
9417        });
9418
9419        let signal_arguments =
9420            encode_value_envelope(&json!(["Rust"]), DEFAULT_CODEC).expect("signal arguments");
9421        let task = WorkflowTask {
9422            task_id: "wft-rust-signal-1".to_string(),
9423            workflow_id: Some("wf-rust-hello".to_string()),
9424            run_id: Some("run-rust-hello".to_string()),
9425            workflow_type: "rust.hello_workflow".to_string(),
9426            payload_codec: DEFAULT_CODEC.to_string(),
9427            arguments: Some(encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("input")),
9428            history_events: vec![HistoryEvent {
9429                event_type: "SignalReceived".to_string(),
9430                payload: json!({
9431                    "signal_id": "sig-rust-1",
9432                    "signal_name": "start"
9433                }),
9434                raw: HashMap::new(),
9435            }],
9436            total_history_events: Some(1),
9437            history_size_bytes: None,
9438            continue_as_new_recommended: None,
9439            history_budget_pressure: None,
9440            next_history_page_token: None,
9441            workflow_task_attempt: 1,
9442            workflow_signal_id: Some("sig-rust-1".to_string()),
9443            signal_name: Some("start".to_string()),
9444            signal_arguments: Some(signal_arguments),
9445            lease_owner: Some("rust-worker".to_string()),
9446        };
9447
9448        let commands = worker.execute_workflow_task(task).expect("workflow task");
9449
9450        assert_eq!(commands.len(), 1);
9451        assert_eq!(commands[0]["type"], "schedule_activity");
9452        assert_eq!(commands[0]["activity_type"], "rust.hello_activity");
9453        assert_eq!(
9454            decode_wire_value(&commands[0]["arguments"], DEFAULT_CODEC).expect("activity args"),
9455            json!(["Rust"])
9456        );
9457    }
9458
9459    #[test]
9460    fn workflow_task_appends_paginated_history_events() {
9461        let mut task = WorkflowTask {
9462            task_id: "wft-rust-pages-1".to_string(),
9463            workflow_id: Some("wf-rust-pages".to_string()),
9464            run_id: Some("run-rust-pages".to_string()),
9465            workflow_type: "rust.hello_workflow".to_string(),
9466            payload_codec: DEFAULT_CODEC.to_string(),
9467            arguments: Some(encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("input")),
9468            history_events: vec![HistoryEvent {
9469                event_type: "WorkflowStarted".to_string(),
9470                payload: json!({}),
9471                raw: HashMap::new(),
9472            }],
9473            total_history_events: Some(3),
9474            history_size_bytes: None,
9475            continue_as_new_recommended: None,
9476            history_budget_pressure: None,
9477            next_history_page_token: Some("MQ==".to_string()),
9478            workflow_task_attempt: 1,
9479            workflow_signal_id: None,
9480            signal_name: None,
9481            signal_arguments: None,
9482            lease_owner: Some("rust-worker".to_string()),
9483        };
9484
9485        task.append_history_page(WorkflowTaskHistoryPage {
9486            history_events: vec![
9487                HistoryEvent {
9488                    event_type: "SignalReceived".to_string(),
9489                    payload: json!({
9490                        "signal_id": "sig-rust-1",
9491                        "signal_name": "start",
9492                        "arguments": encode_value_envelope(&json!(["Rust"]), DEFAULT_CODEC)
9493                            .expect("signal arguments")
9494                    }),
9495                    raw: HashMap::new(),
9496                },
9497                HistoryEvent {
9498                    event_type: "MarkerRecorded".to_string(),
9499                    payload: json!({"sequence": 3}),
9500                    raw: HashMap::new(),
9501                },
9502            ],
9503            total_history_events: Some(3),
9504            next_history_page_token: None,
9505        });
9506
9507        assert_eq!(task.history_events.len(), 3);
9508        assert_eq!(task.total_history_events, Some(3));
9509        assert_eq!(task.next_history_page_token, None);
9510
9511        let signal = task
9512            .history_events
9513            .iter()
9514            .find(|event| event.event_type == "SignalReceived")
9515            .expect("signal event");
9516        assert_eq!(
9517            decode_signal_event_arguments(signal, DEFAULT_CODEC).expect("signal arguments"),
9518            vec![json!("Rust")]
9519        );
9520    }
9521
9522    #[tokio::test]
9523    async fn query_handler_reads_ordered_cross_codec_signals_without_commands() {
9524        let client = Client::new("http://127.0.0.1:8080").expect("client");
9525        let mut worker = Worker::new(client, "rust-workers");
9526        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
9527        worker.register_query("counter", "current", |ctx, _args| async move {
9528            let mut count = 0_i64;
9529            for signal in ctx.signal_events() {
9530                let value = signal
9531                    .arguments
9532                    .first()
9533                    .and_then(Value::as_i64)
9534                    .unwrap_or_default();
9535                match signal.name.as_str() {
9536                    "increment" => count += value,
9537                    "set" => count = value,
9538                    _ => {}
9539                }
9540            }
9541            Ok(json!(count))
9542        });
9543
9544        let task = QueryTask {
9545            query_task_id: "query-rust-counter".to_string(),
9546            query_task_attempt: 1,
9547            lease_owner: Some("rust-worker".to_string()),
9548            workflow_id: Some("counter-1".to_string()),
9549            run_id: Some("run-counter-1".to_string()),
9550            workflow_type: "counter".to_string(),
9551            query_name: "current".to_string(),
9552            payload_codec: DEFAULT_CODEC.to_string(),
9553            workflow_arguments: Some(
9554                encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("workflow input"),
9555            ),
9556            query_arguments: Some(
9557                encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("query arguments"),
9558            ),
9559            history_events: vec![
9560                HistoryEvent {
9561                    event_type: "SignalReceived".to_string(),
9562                    payload: json!({
9563                        "signal_id": "php-signal-1",
9564                        "signal_name": "increment",
9565                        "workflow_sequence": 1,
9566                        "payload_codec": DEFAULT_CODEC,
9567                        "arguments": encode_value_envelope(&json!([3]), DEFAULT_CODEC).expect("php avro signal")
9568                    }),
9569                    raw: HashMap::new(),
9570                },
9571                HistoryEvent {
9572                    event_type: "SignalReceived".to_string(),
9573                    payload: json!({
9574                        "signal_id": "python-signal-2",
9575                        "signal_name": "increment",
9576                        "workflow_sequence": 2,
9577                        "payload_codec": JSON_CODEC,
9578                        "arguments": encode_value_envelope(&json!([5]), JSON_CODEC).expect("python json signal")
9579                    }),
9580                    raw: HashMap::new(),
9581                },
9582                HistoryEvent {
9583                    event_type: "SignalReceived".to_string(),
9584                    payload: json!({
9585                        "signal_id": "rust-signal-3",
9586                        "signal_name": "set",
9587                        "workflow_sequence": 3,
9588                        "payload_codec": DEFAULT_CODEC,
9589                        "arguments": encode_value_envelope(&json!([0]), DEFAULT_CODEC).expect("rust avro signal")
9590                    }),
9591                    raw: HashMap::new(),
9592                },
9593            ],
9594            history_export: None,
9595            run_status: Some("completed".to_string()),
9596        };
9597
9598        let result = worker.execute_query_task(task).await.expect("query result");
9599        assert_eq!(result, json!(0));
9600    }
9601
9602    #[tokio::test]
9603    async fn replayed_queries_read_running_completed_and_cold_restarted_instance_state() {
9604        let worker = replay_counter_worker();
9605        let running_history = json!([
9606            {
9607                "type": "ActivityCompleted",
9608                "payload": {
9609                    "sequence": 1,
9610                    "activity_type": "load-counter",
9611                    "payload_codec": "json",
9612                    "result": {"codec": "json", "blob": "\"loaded\""}
9613                }
9614            },
9615            {
9616                "type": "SignalWaitOpened",
9617                "payload": {
9618                    "sequence": 3,
9619                    "signal_name": "increment"
9620                }
9621            },
9622            {
9623                "type": "SignalReceived",
9624                "payload": {
9625                    "signal_id": "signal-3",
9626                    "signal_name": "increment",
9627                    "workflow_sequence": 2,
9628                    "payload_codec": "json",
9629                    "arguments": {"codec": "json", "blob": "[3]"}
9630                }
9631            },
9632            {
9633                "type": "SignalApplied",
9634                "payload": {
9635                    "sequence": 3,
9636                    "signal_id": "signal-3",
9637                    "signal_name": "increment",
9638                    "payload_codec": "json",
9639                    "value": {"codec": "json", "blob": "[3]"}
9640                }
9641            }
9642        ]);
9643
9644        let running = worker
9645            .execute_query_task(replay_counter_query(
9646                "current",
9647                running_history.clone(),
9648                "running",
9649            ))
9650            .await
9651            .expect("running replay query");
9652        assert_eq!(
9653            running,
9654            json!({"loaded": "loaded", "count": 3, "finished": false})
9655        );
9656
9657        let detached = worker
9658            .execute_query_task(replay_counter_query(
9659                "detached-mutation",
9660                running_history.clone(),
9661                "running",
9662            ))
9663            .await
9664            .expect("query mutates only its detached state clone");
9665        assert_eq!(detached, json!(999));
9666        let failed = worker
9667            .execute_query_task(replay_counter_query(
9668                "failed-mutation",
9669                running_history.clone(),
9670                "running",
9671            ))
9672            .await
9673            .expect_err("failed query");
9674        assert_eq!(failed.reason, "query_rejected");
9675        let unchanged = worker
9676            .execute_query_task(replay_counter_query("current", running_history, "running"))
9677            .await
9678            .expect("later query reconstructs unchanged state");
9679        assert_eq!(unchanged, running);
9680
9681        let restarted_worker = replay_counter_worker();
9682        let restarted_task: QueryTask = serde_json::from_value(json!({
9683            "query_task_id": "query-after-restart",
9684            "workflow_id": "counter-1",
9685            "run_id": "run-counter-1",
9686            "workflow_type": "replay-counter",
9687            "query_name": "current",
9688            "payload_codec": "json",
9689            "workflow_arguments": {"codec": "json", "blob": "[]"},
9690            "query_arguments": {"codec": "json", "blob": "[]"},
9691            "history_events": [],
9692            "history_export": {
9693                "payloads": {"codec": "json"},
9694                "history_events": [
9695                    {
9696                        "type": "ActivityCompleted",
9697                        "payload": {
9698                            "sequence": 1,
9699                            "activity_type": "load-counter",
9700                            "payload_codec": "json",
9701                            "result": null
9702                        }
9703                    },
9704                    {
9705                        "type": "SignalWaitOpened",
9706                        "payload": {
9707                            "sequence": 3,
9708                            "signal_name": "increment"
9709                        }
9710                    },
9711                    {
9712                        "type": "SignalReceived",
9713                        "payload": {
9714                            "signal_id": "signal-3",
9715                            "signal_name": "increment",
9716                            "workflow_sequence": 2
9717                        }
9718                    },
9719                    {
9720                        "type": "SignalApplied",
9721                        "payload": {
9722                            "sequence": 3,
9723                            "signal_id": "signal-3",
9724                            "signal_name": "increment"
9725                        }
9726                    },
9727                    {
9728                        "type": "SignalWaitOpened",
9729                        "payload": {
9730                            "sequence": 5,
9731                            "signal_name": "increment"
9732                        }
9733                    },
9734                    {
9735                        "type": "SignalReceived",
9736                        "payload": {
9737                            "signal_id": "signal-5",
9738                            "signal_name": "increment",
9739                            "workflow_sequence": 4
9740                        }
9741                    },
9742                    {
9743                        "type": "SignalApplied",
9744                        "payload": {
9745                            "sequence": 5,
9746                            "signal_id": "signal-5",
9747                            "signal_name": "increment"
9748                        }
9749                    }
9750                ],
9751                "activities": [{
9752                    "sequence": 1,
9753                    "activity_type": "load-counter",
9754                    "payload_codec": "json",
9755                    "result": {"codec": "json", "blob": "\"loaded\""}
9756                }],
9757                "signals": [
9758                    {
9759                        "id": "signal-3",
9760                        "name": "increment",
9761                        "workflow_sequence": 2,
9762                        "payload_codec": "json",
9763                        "arguments": "[3]"
9764                    },
9765                    {
9766                        "id": "signal-5",
9767                        "name": "increment",
9768                        "workflow_sequence": 4,
9769                        "payload_codec": "json",
9770                        "arguments": "[5]"
9771                    }
9772                ]
9773            },
9774            "run_status": "completed"
9775        }))
9776        .expect("cold replay query task");
9777        let completed = restarted_worker
9778            .execute_query_task(restarted_task)
9779            .await
9780            .expect("completed cold replay query");
9781        assert_eq!(
9782            completed,
9783            json!({"loaded": "loaded", "count": 8, "finished": true})
9784        );
9785    }
9786
9787    #[tokio::test]
9788    async fn replayed_query_replay_failures_are_machine_readable() {
9789        let worker = replay_counter_worker();
9790        let task = replay_counter_query(
9791            "current",
9792            json!([{
9793                "type": "ActivityCompleted",
9794                "payload": {
9795                    "sequence": 1,
9796                    "payload_codec": "json",
9797                    "result": {"codec": "json", "blob": "{"}
9798                }
9799            }]),
9800            "running",
9801        );
9802        let failure = worker
9803            .execute_query_task(task)
9804            .await
9805            .expect_err("invalid replay history payload");
9806        assert_eq!(failure.reason, "query_workflow_state_unavailable");
9807        assert_eq!(failure.failure_type, "QueryWorkflowStateUnavailable");
9808    }
9809
9810    #[tokio::test]
9811    async fn query_task_restores_compact_history_from_export() {
9812        let client = Client::new("http://127.0.0.1:8080").expect("client");
9813        let mut worker = Worker::new(client, "rust-workers");
9814        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
9815        worker.register_query("counter", "current", |ctx, _args| async move {
9816            Ok(json!(ctx.signals("increment")[0][0]))
9817        });
9818        let task: QueryTask = serde_json::from_value(json!({
9819            "query_task_id": "query-export",
9820            "workflow_type": "counter",
9821            "query_name": "current",
9822            "payload_codec": "json",
9823            "workflow_arguments": {"codec": "json", "blob": "[]"},
9824            "query_arguments": {"codec": "json", "blob": "[]"},
9825            "history_events": [],
9826            "history_export": {
9827                "payloads": {"codec": "json"},
9828                "history_events": [{
9829                    "type": "SignalReceived",
9830                    "payload": {"signal_id": "signal-export", "signal_name": "increment"}
9831                }],
9832                "signals": [{
9833                    "id": "signal-export",
9834                    "name": "increment",
9835                    "status": "applied",
9836                    "workflow_sequence": 1,
9837                    "payload_codec": "json",
9838                    "arguments": "[9]"
9839                }]
9840            }
9841        }))
9842        .expect("query task");
9843
9844        let result = worker.execute_query_task(task).await.expect("query result");
9845        assert_eq!(result, json!(9));
9846    }
9847
9848    #[tokio::test]
9849    async fn query_task_failures_have_stable_reasons() {
9850        let client = Client::new("http://127.0.0.1:8080").expect("client");
9851        let mut worker = Worker::new(client, "rust-workers");
9852        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
9853        worker.register_query(
9854            "counter",
9855            "current",
9856            |_ctx, _args| async move { Ok(json!(0)) },
9857        );
9858
9859        let base_task = QueryTask {
9860            query_task_id: "query-errors".to_string(),
9861            query_task_attempt: 1,
9862            lease_owner: None,
9863            workflow_id: Some("counter-errors".to_string()),
9864            run_id: Some("run-errors".to_string()),
9865            workflow_type: "counter".to_string(),
9866            query_name: "missing".to_string(),
9867            payload_codec: JSON_CODEC.to_string(),
9868            workflow_arguments: Some(json!({"codec": "json", "blob": "[]"})),
9869            query_arguments: Some(json!({"codec": "json", "blob": "[]"})),
9870            history_events: Vec::new(),
9871            history_export: None,
9872            run_status: Some("running".to_string()),
9873        };
9874
9875        let unknown = worker
9876            .execute_query_task(base_task.clone())
9877            .await
9878            .expect_err("unknown query");
9879        assert_eq!(unknown.reason, "rejected_unknown_query");
9880
9881        let mut malformed = base_task;
9882        malformed.query_name = "current".to_string();
9883        malformed.query_arguments = Some(json!({"codec": "json", "blob": "{"}));
9884        let malformed = worker
9885            .execute_query_task(malformed)
9886            .await
9887            .expect_err("malformed payload");
9888        assert_eq!(malformed.reason, "query_payload_decode_failed");
9889
9890        let client = Client::new("http://127.0.0.1:8080").expect("client");
9891        let mut unavailable_worker = Worker::new(client, "rust-workers");
9892        unavailable_worker
9893            .register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
9894        let unavailable_task: QueryTask = serde_json::from_value(json!({
9895            "query_task_id": "query-unavailable",
9896            "workflow_type": "counter",
9897            "query_name": "current",
9898            "payload_codec": "json",
9899            "workflow_arguments": {"codec": "json", "blob": "[]"},
9900            "query_arguments": {"codec": "json", "blob": "[]"}
9901        }))
9902        .expect("query task");
9903        let unavailable = unavailable_worker
9904            .execute_query_task(unavailable_task)
9905            .await
9906            .expect_err("query handler unavailable");
9907        assert_eq!(unavailable.reason, "query_handler_unavailable");
9908    }
9909
9910    #[tokio::test]
9911    async fn client_query_decodes_result_and_typed_failure() {
9912        let server = MockWorkerServer::start();
9913        let client = Client::builder(server.base_url())
9914            .timeout(Duration::from_secs(2))
9915            .build()
9916            .expect("client");
9917
9918        let result = client
9919            .query_workflow("counter-1", "current", json!([]))
9920            .await
9921            .expect("query result");
9922        assert_eq!(result, json!({"count": 8}));
9923
9924        let error = client
9925            .query_workflow("counter-1", "missing", json!([]))
9926            .await
9927            .expect_err("unknown query");
9928        let Error::QueryFailed(failure) = error else {
9929            panic!("expected typed query failure");
9930        };
9931        assert_eq!(failure.status, 404);
9932        assert_eq!(failure.reason, "rejected_unknown_query");
9933    }
9934
9935    #[tokio::test]
9936    async fn lifecycle_commands_support_instance_and_selected_run_targets() {
9937        let server = MockWorkerServer::start();
9938        let client = Client::builder(server.base_url())
9939            .timeout(Duration::from_secs(2))
9940            .build()
9941            .expect("client");
9942
9943        let options = WorkflowCommandOptions::new()
9944            .reason("cleanup requested")
9945            .request_id("cancel-17");
9946        let cancelled = client
9947            .cancel_workflow("wf-lifecycle", options)
9948            .await
9949            .expect("instance cancellation");
9950        assert_eq!(cancelled.command, WorkflowCommandKind::Cancel);
9951        assert_eq!(cancelled.run_id.as_deref(), Some("run-current"));
9952        assert_eq!(cancelled.outcome.as_deref(), Some("cancelled"));
9953        assert_eq!(
9954            server.request_body("/api/workflows/wf-lifecycle/cancel"),
9955            json!({"reason":"cleanup requested","request_id":"cancel-17"})
9956        );
9957
9958        let terminated = client
9959            .terminate_workflow(
9960                "wf-lifecycle",
9961                WorkflowCommandOptions::new().reason("forced stop"),
9962            )
9963            .await
9964            .expect("instance termination");
9965        assert_eq!(terminated.command, WorkflowCommandKind::Terminate);
9966        assert_eq!(terminated.outcome.as_deref(), Some("terminated"));
9967
9968        client
9969            .cancel_workflow_run(
9970                "wf-lifecycle",
9971                "run-current",
9972                WorkflowCommandOptions::default(),
9973            )
9974            .await
9975            .expect("selected run cancellation");
9976        client
9977            .terminate_workflow_run(
9978                "wf-lifecycle",
9979                "run-current",
9980                WorkflowCommandOptions::default(),
9981            )
9982            .await
9983            .expect("selected run termination");
9984
9985        for (command, error) in [
9986            (
9987                WorkflowCommandKind::Cancel,
9988                client
9989                    .cancel_workflow_run(
9990                        "wf-lifecycle",
9991                        "run-stale",
9992                        WorkflowCommandOptions::default(),
9993                    )
9994                    .await
9995                    .expect_err("stale cancellation must be rejected"),
9996            ),
9997            (
9998                WorkflowCommandKind::Terminate,
9999                client
10000                    .terminate_workflow_run(
10001                        "wf-lifecycle",
10002                        "run-stale",
10003                        WorkflowCommandOptions::default(),
10004                    )
10005                    .await
10006                    .expect_err("stale termination must be rejected"),
10007            ),
10008        ] {
10009            let Error::WorkflowCommandRejected(rejection) = error else {
10010                panic!("expected typed command rejection");
10011            };
10012            assert_eq!(rejection.command, command);
10013            assert_eq!(rejection.status, 409);
10014            assert_eq!(rejection.reason, "historical_run_command_rejected");
10015            assert_eq!(rejection.run_id.as_deref(), Some("run-stale"));
10016            assert_eq!(rejection.target_scope.as_deref(), Some("run"));
10017        }
10018    }
10019
10020    #[tokio::test]
10021    async fn workflow_start_options_send_server_enforced_deadlines() {
10022        let server = MockWorkerServer::start();
10023        let client = Client::builder(server.base_url())
10024            .timeout(Duration::from_secs(2))
10025            .build()
10026            .expect("client");
10027
10028        let handle = client
10029            .start_workflow_with_options(
10030                "rust.timeout",
10031                "rust-timeouts",
10032                "wf-start-options",
10033                WorkflowStartOptions::new()
10034                    .execution_timeout_seconds(30)
10035                    .run_timeout_seconds(1),
10036                json!([]),
10037            )
10038            .await
10039            .expect("workflow start");
10040
10041        assert_eq!(handle.run_id.as_deref(), Some("run-start-options"));
10042        let body = server.request_body("/api/workflows");
10043        assert_eq!(body["execution_timeout_seconds"], 30);
10044        assert_eq!(body["run_timeout_seconds"], 1);
10045
10046        let invalid = client
10047            .start_workflow_with_options(
10048                "rust.timeout",
10049                "rust-timeouts",
10050                "wf-invalid-options",
10051                WorkflowStartOptions::new()
10052                    .execution_timeout_seconds(1)
10053                    .run_timeout_seconds(2),
10054                json!([]),
10055            )
10056            .await
10057            .expect_err("invalid deadline ordering");
10058        assert!(invalid
10059            .to_string()
10060            .contains("run_timeout_seconds cannot exceed execution_timeout_seconds"));
10061    }
10062
10063    #[tokio::test]
10064    async fn workflow_result_returns_each_typed_terminal_outcome() {
10065        let server = MockWorkerServer::start();
10066        let client = Client::builder(server.base_url())
10067            .timeout(Duration::from_secs(2))
10068            .build()
10069            .expect("client");
10070        let options = WorkflowResultOptions {
10071            poll_interval: Duration::ZERO,
10072            timeout: Duration::from_secs(1),
10073        };
10074
10075        let failed = WorkflowHandle {
10076            client: client.clone(),
10077            workflow_id: "wf-failed".to_string(),
10078            run_id: Some("run-failed".to_string()),
10079            workflow_type: "failure".to_string(),
10080        }
10081        .result(options)
10082        .await
10083        .expect_err("failed outcome");
10084        let Error::WorkflowFailed(failure) = failed else {
10085            panic!("expected WorkflowFailed");
10086        };
10087        assert_eq!(failure.workflow_id, "wf-failed");
10088        assert_eq!(failure.run_id.as_deref(), Some("run-failed"));
10089        assert_eq!(failure.failure_id.as_deref(), Some("failure-17"));
10090        assert_eq!(failure.failure_category.as_deref(), Some("application"));
10091        assert_eq!(failure.exception_type.as_deref(), Some("PaymentError"));
10092        assert_eq!(
10093            failure.exception_class.as_deref(),
10094            Some("billing::PaymentError")
10095        );
10096        assert_eq!(failure.non_retryable, Some(true));
10097
10098        for (workflow_id, expected_kind, expected_reason) in [
10099            (
10100                "wf-cancelled",
10101                WorkflowTerminalKind::Cancelled,
10102                "cleanup requested",
10103            ),
10104            (
10105                "wf-terminated",
10106                WorkflowTerminalKind::Terminated,
10107                "forced stop",
10108            ),
10109            (
10110                "wf-timed-out",
10111                WorkflowTerminalKind::TimedOut,
10112                "run_timeout",
10113            ),
10114        ] {
10115            let error = WorkflowHandle {
10116                client: client.clone(),
10117                workflow_id: workflow_id.to_string(),
10118                run_id: None,
10119                workflow_type: "terminal".to_string(),
10120            }
10121            .result(options)
10122            .await
10123            .expect_err("typed terminal outcome");
10124            let outcome = match error {
10125                Error::WorkflowCancelled(outcome) => outcome,
10126                Error::WorkflowTerminated(outcome) => outcome,
10127                Error::WorkflowTimedOut(outcome) => outcome,
10128                other => panic!("unexpected terminal error: {other}"),
10129            };
10130            assert_eq!(outcome.kind, expected_kind);
10131            assert_eq!(outcome.workflow_id, workflow_id);
10132            assert_eq!(outcome.reason, expected_reason);
10133        }
10134
10135        let wait_timeout = WorkflowHandle {
10136            client,
10137            workflow_id: "wf-waiting".to_string(),
10138            run_id: Some("run-waiting".to_string()),
10139            workflow_type: "waiting".to_string(),
10140        }
10141        .result(WorkflowResultOptions {
10142            poll_interval: Duration::ZERO,
10143            timeout: Duration::ZERO,
10144        })
10145        .await
10146        .expect_err("client wait timeout");
10147        let Error::WorkflowTimedOut(timeout) = wait_timeout else {
10148            panic!("expected typed client timeout");
10149        };
10150        assert_eq!(timeout.reason, "result_wait_timeout");
10151        assert_eq!(timeout.failure_category.as_deref(), Some("client_timeout"));
10152        assert_eq!(timeout.run_id.as_deref(), Some("run-waiting"));
10153    }
10154
10155    #[tokio::test]
10156    async fn workflow_result_follows_chain_and_selected_result_preserves_history() {
10157        let server = MockWorkerServer::start();
10158        let client = Client::builder(server.base_url())
10159            .timeout(Duration::from_secs(2))
10160            .build()
10161            .expect("client");
10162
10163        let handle = WorkflowHandle {
10164            client,
10165            workflow_id: "wf-selected".to_string(),
10166            run_id: Some("run-selected".to_string()),
10167            workflow_type: "selected".to_string(),
10168        };
10169        let options = WorkflowResultOptions {
10170            poll_interval: Duration::ZERO,
10171            timeout: Duration::from_secs(1),
10172        };
10173
10174        let current = handle
10175            .result(options)
10176            .await
10177            .expect("instance result follows the current run");
10178        assert_eq!(current, json!("current run output"));
10179
10180        let error = handle
10181            .result_selected_run(options)
10182            .await
10183            .expect_err("the selected run is cancelled even though the current run completed");
10184
10185        let Error::WorkflowCancelled(outcome) = error else {
10186            panic!("expected selected run cancellation");
10187        };
10188        assert_eq!(outcome.run_id.as_deref(), Some("run-selected"));
10189        assert_eq!(outcome.reason, "selected run cancelled");
10190        assert_eq!(
10191            server.request_count("/api/workflows/wf-selected/runs/run-selected"),
10192            1
10193        );
10194        assert_eq!(server.request_count("/api/workflows/wf-selected"), 1);
10195    }
10196
10197    #[tokio::test]
10198    async fn poll_responses_decode_http_conflict_drain_as_a_stable_stop() {
10199        let server = MockWorkerServer::draining_polls();
10200        let client = Client::builder(server.base_url())
10201            .timeout(Duration::from_secs(2))
10202            .build()
10203            .expect("client");
10204
10205        let workflow = client
10206            .poll_workflow_task_response("draining-worker", "rust-workers", Duration::ZERO)
10207            .await
10208            .expect("workflow drain response");
10209        let activity = client
10210            .poll_activity_task_response("draining-worker", "rust-workers", Duration::ZERO)
10211            .await
10212            .expect("activity drain response");
10213        let query = client
10214            .poll_query_task_response("draining-worker", "rust-workers", Duration::ZERO)
10215            .await
10216            .expect("query drain response");
10217
10218        for outcome in [workflow.outcome(), activity.outcome(), query.outcome()] {
10219            assert_eq!(
10220                outcome,
10221                WorkerPollOutcome::Stop {
10222                    poll_status: Some("draining".to_string()),
10223                    reason: Some("worker_draining".to_string()),
10224                }
10225            );
10226        }
10227
10228        assert!(client
10229            .poll_workflow_task("draining-worker", "rust-workers", Duration::ZERO)
10230            .await
10231            .expect("compatibility poll")
10232            .is_none());
10233    }
10234
10235    #[tokio::test]
10236    async fn managed_worker_honors_drain_stop_for_every_task_family() {
10237        let server = MockWorkerServer::draining_polls();
10238        let client = Client::builder(server.base_url())
10239            .timeout(Duration::from_secs(2))
10240            .build()
10241            .expect("client");
10242
10243        let mut workflow_worker = Worker::new(client.clone(), "rust-workers")
10244            .worker_id("draining-workflow-worker")
10245            .poll_timeout(Duration::ZERO);
10246        workflow_worker.register_workflow("counter", |_ctx, _args| async { Ok(Value::Null) });
10247        workflow_worker
10248            .run()
10249            .await
10250            .expect("workflow drain is a clean stop");
10251
10252        let mut activity_worker = Worker::new(client.clone(), "rust-workers")
10253            .worker_id("draining-activity-worker")
10254            .poll_timeout(Duration::ZERO);
10255        activity_worker.register_activity("write", |_ctx, _args| async { Ok(Value::Null) });
10256        activity_worker
10257            .run()
10258            .await
10259            .expect("activity drain is a clean stop");
10260
10261        let mut query_worker = Worker::new(client, "rust-workers")
10262            .worker_id("draining-query-worker")
10263            .poll_timeout(Duration::ZERO);
10264        query_worker.register_query("counter", "current", |_ctx, _args| async {
10265            Ok(Value::Null)
10266        });
10267        query_worker
10268            .run()
10269            .await
10270            .expect("query drain is a clean stop");
10271    }
10272
10273    #[tokio::test]
10274    async fn activity_cancellation_and_late_completion_remain_machine_readable() {
10275        let server = MockWorkerServer::start();
10276        let client = Client::builder(server.base_url())
10277            .timeout(Duration::from_secs(2))
10278            .build()
10279            .expect("client");
10280
10281        let heartbeat = client
10282            .heartbeat_activity_task(
10283                "activity-cancel",
10284                "attempt-cancel",
10285                "rust-worker",
10286                json!({"stage":"cleanup"}),
10287            )
10288            .await
10289            .expect("cancellation heartbeat");
10290        assert!(heartbeat.cancel_requested);
10291        assert!(heartbeat.should_stop());
10292        assert_eq!(heartbeat.reason.as_deref(), Some("run_cancelled"));
10293        assert_eq!(heartbeat.run_closed_reason.as_deref(), Some("cancelled"));
10294
10295        let error = client
10296            .complete_activity_task(
10297                "activity-cancel",
10298                "attempt-cancel",
10299                "rust-worker",
10300                json!({"late":true}),
10301                JSON_CODEC,
10302            )
10303            .await
10304            .expect_err("late completion must be refused");
10305        assert!(activity_task_rejection_is_final(&error));
10306        let Error::ActivityTaskRejected(rejection) = error else {
10307            panic!("expected typed activity rejection");
10308        };
10309        assert_eq!(rejection.status, 409);
10310        assert_eq!(rejection.reason, "run_cancelled");
10311        assert!(rejection.cancel_requested);
10312        assert_eq!(rejection.can_continue, Some(false));
10313    }
10314
10315    #[tokio::test]
10316    async fn managed_worker_survives_late_completion_and_restart_during_cancellation() {
10317        let server = MockWorkerServer::cancelled_activity();
10318        let client = Client::builder(server.base_url())
10319            .timeout(Duration::from_secs(2))
10320            .build()
10321            .expect("client");
10322        let cancellation_observed = Arc::new(AtomicBool::new(false));
10323        let observed = Arc::clone(&cancellation_observed);
10324        let mut worker = Worker::new(client.clone(), "rust-workers")
10325            .worker_id("rust-cancel-worker")
10326            .poll_timeout(Duration::from_millis(10));
10327        worker.register_activity("cancel-aware", move |ctx, _args| {
10328            let observed = Arc::clone(&observed);
10329            async move {
10330                let heartbeat = ctx.heartbeat(json!({"stage":"running"})).await?;
10331                observed.store(heartbeat.should_stop(), Ordering::SeqCst);
10332                Ok(json!({"late":"completion"}))
10333            }
10334        });
10335
10336        assert_eq!(
10337            worker.run_once().await.expect("cancelled attempt handled"),
10338            1
10339        );
10340        assert!(cancellation_observed.load(Ordering::SeqCst));
10341        assert_eq!(
10342            server.request_count("/api/worker/activity-tasks/activity-cancel/complete"),
10343            1
10344        );
10345
10346        let mut restarted = Worker::new(client, "rust-workers")
10347            .worker_id("rust-cancel-worker-restarted")
10348            .poll_timeout(Duration::from_millis(10));
10349        restarted.register_activity("cancel-aware", |_ctx, _args| async move { Ok(Value::Null) });
10350        assert_eq!(
10351            restarted
10352                .run_once()
10353                .await
10354                .expect("replacement worker continues polling"),
10355            0
10356        );
10357    }
10358
10359    #[tokio::test]
10360    async fn managed_worker_absorbs_selected_run_terminal_timeout_completion_race() {
10361        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"}"#;
10362        let server = MockWorkerServer::workflow_completion("409 Conflict", response);
10363        let client = Client::builder(server.base_url())
10364            .timeout(Duration::from_secs(2))
10365            .build()
10366            .expect("client");
10367
10368        let direct_error = client
10369            .complete_workflow_task(
10370                "workflow-timeout-task",
10371                "timeout-worker",
10372                3,
10373                vec![json!({"type": "complete_workflow", "result": null})],
10374            )
10375            .await
10376            .expect_err("the low-level client preserves the completion rejection");
10377        let Error::Http { status, body } = direct_error else {
10378            panic!("expected the original HTTP completion rejection");
10379        };
10380        assert_eq!(status, reqwest::StatusCode::CONFLICT);
10381        assert_eq!(
10382            serde_json::from_str::<Value>(&body).expect("response body")["reason"],
10383            "run_timed_out"
10384        );
10385
10386        let mut worker = Worker::new(client, "rust-workers")
10387            .worker_id("timeout-worker")
10388            .poll_timeout(Duration::from_millis(10));
10389        worker.register_workflow("timeout.workflow", |_ctx, _input| async move {
10390            Ok(json!({"late": "result"}))
10391        });
10392
10393        assert_eq!(
10394            worker
10395                .run_once()
10396                .await
10397                .expect("authoritative selected-run timeout settles the tick"),
10398            1
10399        );
10400        assert_eq!(
10401            server.request_count("/api/worker/workflow-tasks/workflow-timeout-task/complete"),
10402            2,
10403            "both the direct client proof and managed worker must see the rejection"
10404        );
10405    }
10406
10407    #[tokio::test]
10408    async fn managed_worker_does_not_swallow_nearby_completion_errors() {
10409        for (name, status, response) in [
10410            ("bare conflict", "409 Conflict", r#"{"message":"conflict"}"#),
10411            (
10412                "command was recorded",
10413                "409 Conflict",
10414                r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"recorded":true,"run_id":"run-selected-timeout","run_status":"failed","reason":"run_timed_out"}"#,
10415            ),
10416            (
10417                "lease conflict",
10418                "409 Conflict",
10419                r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"recorded":false,"run_id":"run-selected-timeout","run_status":"failed","reason":"lease_expired"}"#,
10420            ),
10421            (
10422                "nonterminal run",
10423                "409 Conflict",
10424                r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"recorded":false,"run_id":"run-selected-timeout","run_status":"waiting","reason":"run_timed_out"}"#,
10425            ),
10426            (
10427                "different selected run",
10428                "409 Conflict",
10429                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"}"#,
10430            ),
10431            (
10432                "different task attempt",
10433                "409 Conflict",
10434                r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":4,"recorded":false,"run_id":"run-selected-timeout","run_status":"failed","reason":"run_timed_out"}"#,
10435            ),
10436            (
10437                "authentication failure",
10438                "401 Unauthorized",
10439                r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"recorded":false,"run_id":"run-selected-timeout","run_status":"failed","reason":"run_timed_out"}"#,
10440            ),
10441            (
10442                "authorization failure",
10443                "403 Forbidden",
10444                r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"recorded":false,"run_id":"run-selected-timeout","run_status":"failed","reason":"run_timed_out"}"#,
10445            ),
10446            (
10447                "protocol failure",
10448                "400 Bad Request",
10449                r#"{"reason":"unsupported_protocol_version","message":"unsupported worker protocol","supported_version":"1.2","requested_version":"1.3"}"#,
10450            ),
10451            (
10452                "malformed command",
10453                "422 Unprocessable Entity",
10454                r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"recorded":false,"run_id":"run-selected-timeout","run_status":"failed","reason":"run_timed_out"}"#,
10455            ),
10456            (
10457                "transient server failure",
10458                "503 Service Unavailable",
10459                r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"recorded":false,"run_id":"run-selected-timeout","run_status":"failed","reason":"run_timed_out"}"#,
10460            ),
10461        ] {
10462            let server = MockWorkerServer::workflow_completion(status, response);
10463            let client = Client::builder(server.base_url())
10464                .timeout(Duration::from_secs(2))
10465                .build()
10466                .expect("client");
10467            let mut worker = Worker::new(client, "rust-workers")
10468                .worker_id("timeout-worker")
10469                .poll_timeout(Duration::from_millis(10));
10470            worker.register_workflow("timeout.workflow", |_ctx, _input| async move {
10471                Ok(json!({"late": "result"}))
10472            });
10473
10474            let error = worker
10475                .run_once()
10476                .await
10477                .expect_err(&format!("{name} must remain an error"));
10478            assert!(
10479                matches!(error, Error::Http { .. } | Error::Protocol(_)),
10480                "{name} returned an unexpected error variant: {error}"
10481            );
10482        }
10483    }
10484
10485    #[tokio::test]
10486    async fn baseline_worker_endpoints_send_the_baseline_protocol() {
10487        let server = MockWorkerServer::start();
10488        let client = Client::builder(server.base_url())
10489            .timeout(Duration::from_secs(2))
10490            .build()
10491            .expect("client");
10492
10493        client
10494            .register_worker("capture-worker", "capture", vec![], vec![], 1, 1)
10495            .await
10496            .expect("register");
10497        client
10498            .heartbeat_worker("capture-worker", 1, 1)
10499            .await
10500            .expect("heartbeat");
10501        client
10502            .poll_workflow_task("capture-worker", "capture", Duration::from_millis(10))
10503            .await
10504            .expect("workflow poll");
10505        client
10506            .poll_activity_task("capture-worker", "capture", Duration::from_millis(10))
10507            .await
10508            .expect("activity poll");
10509
10510        for path in [
10511            "/api/worker/register",
10512            "/api/worker/heartbeat",
10513            "/api/worker/workflow-tasks/poll",
10514            "/api/worker/activity-tasks/poll",
10515        ] {
10516            assert_eq!(
10517                server.worker_protocol_for(path).as_deref(),
10518                Some(WORKER_PROTOCOL_VERSION),
10519                "unexpected protocol for {path}"
10520            );
10521        }
10522
10523        assert_eq!(
10524            server.request_body("/api/worker/workflow-tasks/poll")["timeout_seconds"],
10525            1
10526        );
10527        assert_eq!(
10528            server.request_body("/api/worker/activity-tasks/poll")["timeout_seconds"],
10529            1
10530        );
10531    }
10532
10533    #[tokio::test]
10534    async fn query_task_endpoints_send_the_query_feature_protocol() {
10535        let server = MockWorkerServer::start();
10536        let client = Client::builder(server.base_url())
10537            .timeout(Duration::from_secs(2))
10538            .build()
10539            .expect("client");
10540
10541        client
10542            .poll_query_task("capture-worker", "capture", Duration::from_millis(10))
10543            .await
10544            .expect("query poll");
10545        client
10546            .complete_query_task("query-capture", "capture-worker", 1, json!(8), JSON_CODEC)
10547            .await
10548            .expect("query complete");
10549        client
10550            .fail_query_task(
10551                "query-capture",
10552                "capture-worker",
10553                1,
10554                "failed",
10555                "query_rejected",
10556                "QueryFailed",
10557            )
10558            .await
10559            .expect("query fail");
10560
10561        for path in [
10562            "/api/worker/query-tasks/poll",
10563            "/api/worker/query-tasks/query-capture/complete",
10564            "/api/worker/query-tasks/query-capture/fail",
10565        ] {
10566            assert_eq!(
10567                server.worker_protocol_for(path).as_deref(),
10568                Some(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION),
10569                "unexpected protocol for {path}"
10570            );
10571        }
10572
10573        assert_eq!(
10574            server.request_body("/api/worker/query-tasks/poll")["timeout_seconds"],
10575            1
10576        );
10577    }
10578
10579    #[tokio::test]
10580    async fn query_protocol_rejection_from_older_server_is_typed() {
10581        let server = MockWorkerServer::reject_query_protocol();
10582        let client = Client::builder(server.base_url())
10583            .timeout(Duration::from_secs(2))
10584            .build()
10585            .expect("client");
10586
10587        let error = client
10588            .poll_query_task("capture-worker", "capture", Duration::from_millis(10))
10589            .await
10590            .expect_err("server below query protocol floor must reject");
10591        let Error::Protocol(failure) = error else {
10592            panic!("expected typed protocol failure");
10593        };
10594
10595        assert_eq!(failure.status, 400);
10596        assert_eq!(failure.reason, "unsupported_protocol_version");
10597        assert_eq!(failure.supported_version.as_deref(), Some("1.7"));
10598        assert_eq!(
10599            failure.requested_version.as_deref(),
10600            Some(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION)
10601        );
10602        assert_eq!(
10603            server
10604                .worker_protocol_for("/api/worker/query-tasks/poll")
10605                .as_deref(),
10606            Some(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION)
10607        );
10608    }
10609
10610    #[tokio::test]
10611    async fn run_once_without_query_handlers_keeps_pre_query_server_compatibility() {
10612        let server = MockWorkerServer::reject_query_protocol();
10613        let client = Client::builder(server.base_url())
10614            .timeout(Duration::from_secs(2))
10615            .build()
10616            .expect("client");
10617        let mut worker = Worker::new(client, "rust-workers")
10618            .worker_id("baseline-worker")
10619            .poll_timeout(Duration::from_millis(10));
10620
10621        worker.register_workflow("baseline.workflow", |_ctx, _input| async move {
10622            Ok(Value::Null)
10623        });
10624
10625        assert_eq!(worker.run_once().await.expect("baseline run once"), 0);
10626        assert_eq!(
10627            server
10628                .worker_protocol_for("/api/worker/workflow-tasks/poll")
10629                .as_deref(),
10630            Some(WORKER_PROTOCOL_VERSION)
10631        );
10632        assert_eq!(
10633            server.worker_protocol_for("/api/worker/query-tasks/poll"),
10634            None,
10635            "a worker without query handlers must not use the query-task endpoint"
10636        );
10637    }
10638
10639    #[tokio::test]
10640    async fn completion_time_query_rejection_is_typed_without_stopping_worker() {
10641        let server = MockWorkerServer::reject_query_completion();
10642        let client = Client::builder(server.base_url())
10643            .timeout(Duration::from_secs(2))
10644            .build()
10645            .expect("client");
10646
10647        let error = client
10648            .complete_query_task("query-late", "late-worker", 1, json!(8), JSON_CODEC)
10649            .await
10650            .expect_err("expired completion must be rejected");
10651        let Error::QueryFailed(failure) = error else {
10652            panic!("expected typed query failure");
10653        };
10654        assert_eq!(failure.status, 409);
10655        assert_eq!(failure.reason, "query_task_timed_out");
10656
10657        let mut worker = Worker::new(client, "rust-workers")
10658            .worker_id("late-worker")
10659            .poll_timeout(Duration::from_millis(10));
10660        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
10661        worker.register_query(
10662            "counter",
10663            "current",
10664            |_ctx, _args| async move { Ok(json!(8)) },
10665        );
10666
10667        assert_eq!(worker.run_once().await.expect("late task is handled"), 1);
10668        assert_eq!(
10669            worker
10670                .run_once()
10671                .await
10672                .expect("worker continues after late completion"),
10673            0
10674        );
10675        assert_eq!(
10676            server.request_count("/api/worker/query-tasks/query-late/complete"),
10677            2
10678        );
10679        assert_eq!(
10680            server.request_count("/api/worker/query-tasks/query-late/fail"),
10681            0,
10682            "a server completion rejection must not be reported as an encoding failure"
10683        );
10684    }
10685
10686    #[tokio::test]
10687    async fn activity_only_worker_can_shutdown_without_workflow_poller() {
10688        let server = MockWorkerServer::start();
10689        let client = Client::builder(server.base_url())
10690            .timeout(Duration::from_secs(2))
10691            .build()
10692            .expect("client");
10693        let mut worker = Worker::new(client, "rust-workers")
10694            .worker_id("activity-only-worker")
10695            .poll_timeout(Duration::from_millis(10));
10696
10697        worker.register_activity(
10698            "activity.only",
10699            |_ctx, _args| async move { Ok(Value::Null) },
10700        );
10701
10702        worker.run_until(async {}).await.expect("run worker");
10703    }
10704
10705    #[tokio::test]
10706    async fn workflow_only_worker_can_shutdown_without_activity_poller() {
10707        let server = MockWorkerServer::start();
10708        let client = Client::builder(server.base_url())
10709            .timeout(Duration::from_secs(2))
10710            .build()
10711            .expect("client");
10712        let mut worker = Worker::new(client, "rust-workers")
10713            .worker_id("workflow-only-worker")
10714            .poll_timeout(Duration::from_millis(10));
10715
10716        worker.register_workflow(
10717            "workflow.only",
10718            |_ctx, _input| async move { Ok(Value::Null) },
10719        );
10720
10721        worker.run_until(async {}).await.expect("run worker");
10722    }
10723
10724    #[tokio::test]
10725    async fn worker_heartbeat_observer_receives_server_acknowledgements() {
10726        let server = MockWorkerServer::start();
10727        let client = Client::builder(server.base_url())
10728            .timeout(Duration::from_secs(2))
10729            .build()
10730            .expect("client");
10731        let observations = Arc::new(Mutex::new(Vec::new()));
10732        let observed = Arc::clone(&observations);
10733        let mut worker = Worker::new(client, "rust-workers")
10734            .worker_id("observed-heartbeat-worker")
10735            .poll_timeout(Duration::from_millis(10))
10736            .on_worker_heartbeat(move |observation| {
10737                observed
10738                    .lock()
10739                    .expect("heartbeat observations")
10740                    .push(observation.clone());
10741            });
10742
10743        worker.register_workflow("workflow.observed", |_ctx, _input| async move {
10744            Ok(Value::Null)
10745        });
10746        worker
10747            .run_until(tokio::time::sleep(Duration::from_millis(20)))
10748            .await
10749            .expect("run worker");
10750
10751        let observations = observations.lock().expect("heartbeat observations");
10752        let first = observations.first().expect("heartbeat acknowledgement");
10753        assert_eq!(first.worker_id, "observed-heartbeat-worker");
10754        assert_eq!(first.task_queue, "rust-workers");
10755        assert!(first.acknowledged_at_unix_millis > 0);
10756        assert_eq!(first.acknowledgement, json!({}));
10757    }
10758
10759    #[tokio::test]
10760    async fn delayed_worker_heartbeat_keeps_cadence_and_pollers_live() {
10761        let server = MockWorkerServer::delayed_heartbeat_worker();
10762        let client = Client::builder(server.base_url())
10763            .timeout(Duration::from_secs(3))
10764            .build()
10765            .expect("client");
10766        let observations = Arc::new(Mutex::new(Vec::new()));
10767        let observed = Arc::clone(&observations);
10768        let mut worker = Worker::new(client, "rust-snapshot-workers")
10769            .worker_id("rust-snapshot-worker")
10770            .poll_timeout(Duration::from_millis(10))
10771            .on_worker_heartbeat(move |observation| {
10772                observed
10773                    .lock()
10774                    .expect("heartbeat observations")
10775                    .push(observation.clone());
10776            });
10777
10778        worker.register_workflow("snapshot", |ctx, _input| async move {
10779            ctx.wait_signal("finish").await?;
10780            Ok(json!({"status": "finished"}))
10781        });
10782        worker.register_query("snapshot", "current", |ctx, _args| async move {
10783            Ok(json!(ctx
10784                .signals("increment")
10785                .iter()
10786                .filter_map(|arguments| arguments.first().and_then(Value::as_i64))
10787                .sum::<i64>()))
10788        });
10789        worker.register_activity("cancel-aware", |_ctx, _args| async move {
10790            Ok(json!({"late": "completion"}))
10791        });
10792
10793        worker
10794            .run_until(tokio::time::sleep(Duration::from_millis(3_800)))
10795            .await
10796            .expect("delayed heartbeat must allow a clean worker shutdown");
10797
10798        let observations = observations.lock().expect("heartbeat observations");
10799        assert!(
10800            observations.len() >= 3,
10801            "the immediate heartbeat, delayed acknowledgement, and next cadence heartbeat must complete"
10802        );
10803        assert!(
10804            observations.windows(2).all(|pair| {
10805                pair[1].acknowledged_at_unix_millis
10806                    .saturating_sub(pair[0].acknowledged_at_unix_millis)
10807                    >= 850
10808            }),
10809            "successful acknowledgements must not catch up faster than the advertised one-second cadence: {observations:?}"
10810        );
10811        drop(observations);
10812
10813        let heartbeat_times = server.request_times("/api/worker/heartbeat");
10814        let delayed_request_at = *heartbeat_times
10815            .get(1)
10816            .expect("intentionally delayed heartbeat request");
10817        let delay_window_start = delayed_request_at + Duration::from_millis(100);
10818        let delay_window_end = delayed_request_at + Duration::from_millis(1_400);
10819        for path in [
10820            "/api/worker/workflow-tasks/poll",
10821            "/api/worker/activity-tasks/poll",
10822            "/api/worker/query-tasks/poll",
10823        ] {
10824            assert!(
10825                server
10826                    .request_times(path)
10827                    .iter()
10828                    .any(|received_at| *received_at >= delay_window_start
10829                        && *received_at <= delay_window_end),
10830                "{path} must keep polling while a heartbeat acknowledgement is delayed"
10831            );
10832        }
10833        assert!(
10834            server.request_count("/api/worker/workflow-tasks/snapshot-wait-3/fail") >= 1,
10835            "workflow work must be settled"
10836        );
10837        assert!(
10838            server.request_count("/api/worker/activity-tasks/activity-cancel/complete") >= 1,
10839            "activity work must be settled"
10840        );
10841        assert!(
10842            server.request_count("/api/worker/query-tasks/snapshot-current/complete") >= 1,
10843            "query work must be settled"
10844        );
10845    }
10846
10847    #[tokio::test]
10848    async fn retried_worker_heartbeat_restarts_the_advertised_cadence() {
10849        let server = MockWorkerServer::heartbeat_retry_worker();
10850        let client = Client::builder(server.base_url())
10851            .timeout(Duration::from_secs(2))
10852            .build()
10853            .expect("client");
10854        let observations = Arc::new(Mutex::new(Vec::new()));
10855        let observed = Arc::clone(&observations);
10856        let worker = Worker::new(client, "rust-workers")
10857            .worker_id("heartbeat-retry-worker")
10858            .retry_policy(WorkerRetryPolicy {
10859                max_retries: 1,
10860                initial_backoff: Duration::from_millis(300),
10861                max_backoff: Duration::from_millis(300),
10862            })
10863            .on_worker_heartbeat(move |observation| {
10864                observed
10865                    .lock()
10866                    .expect("heartbeat observations")
10867                    .push(observation.clone());
10868            });
10869
10870        worker
10871            .run_until(tokio::time::sleep(Duration::from_millis(2_700)))
10872            .await
10873            .expect("retryable heartbeat failure must remain bounded and recover");
10874
10875        let observations = observations.lock().expect("heartbeat observations");
10876        assert!(observations.len() >= 3, "heartbeat retry must recover");
10877        assert!(
10878            observations.windows(2).all(|pair| {
10879                pair[1]
10880                    .acknowledged_at_unix_millis
10881                    .saturating_sub(pair[0].acknowledged_at_unix_millis)
10882                    >= 850
10883            }),
10884            "a successful retry must start a fresh advertised cadence: {observations:?}"
10885        );
10886        assert_eq!(
10887            server.request_count("/api/worker/heartbeat"),
10888            observations.len() + 1,
10889            "one retryable failure must add exactly one bounded request"
10890        );
10891    }
10892
10893    #[tokio::test]
10894    async fn query_enabled_worker_ignores_unmatched_signals_then_completes_once() {
10895        let server = MockWorkerServer::waiting_query_worker();
10896        let client = Client::builder(server.base_url())
10897            .timeout(Duration::from_secs(2))
10898            .build()
10899            .expect("client");
10900        let observations = Arc::new(Mutex::new(Vec::new()));
10901        let observed = Arc::clone(&observations);
10902        let mut worker = Worker::new(client, "rust-snapshot-workers")
10903            .worker_id("rust-snapshot-worker")
10904            .poll_timeout(Duration::from_millis(10))
10905            .on_worker_heartbeat(move |observation| {
10906                observed
10907                    .lock()
10908                    .expect("heartbeat observations")
10909                    .push(observation.clone());
10910            });
10911
10912        worker.register_workflow("snapshot", |ctx, _input| async move {
10913            ctx.wait_signal("finish").await?;
10914            Ok(json!({"status": "finished"}))
10915        });
10916        worker.register_query("snapshot", "current", |ctx, _args| async move {
10917            let current = ctx
10918                .signals("increment")
10919                .iter()
10920                .filter_map(|arguments| arguments.first().and_then(Value::as_i64))
10921                .sum::<i64>();
10922            Ok(json!(current))
10923        });
10924
10925        worker
10926            .run_until(tokio::time::sleep(Duration::from_millis(3_200)))
10927            .await
10928            .expect("pending workflow and query poller must remain live until shutdown");
10929
10930        assert!(
10931            observations.lock().expect("heartbeat observations").len() >= 4,
10932            "the immediate heartbeat and at least three advertised one-second intervals must be acknowledged"
10933        );
10934        assert!(
10935            server.request_count("/api/worker/workflow-tasks/poll") >= 3,
10936            "workflow polling must continue after empty replay acknowledgements"
10937        );
10938        assert!(
10939            server.request_count("/api/worker/query-tasks/poll") >= 2,
10940            "query polling must continue after serving the current query"
10941        );
10942        assert_eq!(
10943            server.request_body("/api/worker/register")["capabilities"],
10944            json!([QUERY_TASKS_CAPABILITY])
10945        );
10946
10947        let opened = server.request_body("/api/worker/workflow-tasks/snapshot-open/complete");
10948        assert_eq!(
10949            opened["commands"],
10950            json!([{
10951                "type": "open_signal_wait",
10952                "signal_name": "finish",
10953            }])
10954        );
10955
10956        for task_id in ["snapshot-wait-3", "snapshot-wait-5"] {
10957            let fail_path = format!("/api/worker/workflow-tasks/{task_id}/fail");
10958            let completion_path = format!("/api/worker/workflow-tasks/{task_id}/complete");
10959            let failure = server.request_body(&fail_path);
10960            assert_eq!(
10961                failure["failure"]["type"],
10962                WORKFLOW_TASK_WAITING_FOR_HISTORY_TYPE
10963            );
10964            assert_eq!(server.request_count(&completion_path), 0);
10965        }
10966
10967        let query_completion =
10968            server.request_body("/api/worker/query-tasks/snapshot-current/complete");
10969        assert_eq!(query_completion["result"], json!(8));
10970
10971        let terminal_path = "/api/worker/workflow-tasks/snapshot-finish/complete";
10972        assert_eq!(
10973            server.request_count(terminal_path),
10974            1,
10975            "the matching signal must settle the workflow exactly once"
10976        );
10977        let terminal = server.request_body(terminal_path);
10978        assert_eq!(terminal["commands"].as_array().map(Vec::len), Some(1));
10979        assert_eq!(terminal["commands"][0]["type"], "complete_workflow");
10980        assert_eq!(
10981            decode_wire_value(&terminal["commands"][0]["result"], DEFAULT_CODEC)
10982                .expect("terminal workflow result"),
10983            json!({"status": "finished"})
10984        );
10985    }
10986
10987    #[tokio::test]
10988    async fn worker_retries_poll_and_heartbeat_transport_failures_independently() {
10989        let server = MockWorkerServer::transient_worker_failures();
10990        let client = Client::builder(server.base_url())
10991            .timeout(Duration::from_secs(2))
10992            .build()
10993            .expect("client");
10994        let mut worker = Worker::new(client, "rust-workers")
10995            .worker_id("retry-worker")
10996            .poll_timeout(Duration::from_millis(10))
10997            .retry_policy(WorkerRetryPolicy {
10998                max_retries: 2,
10999                initial_backoff: Duration::from_millis(1),
11000                max_backoff: Duration::from_millis(1),
11001            });
11002        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
11003        worker.register_activity(
11004            "counter.activity",
11005            |_ctx, _input| async move { Ok(Value::Null) },
11006        );
11007        worker.register_query(
11008            "counter",
11009            "current",
11010            |_ctx, _args| async move { Ok(json!(8)) },
11011        );
11012
11013        worker
11014            .run_until(tokio::time::sleep(Duration::from_millis(75)))
11015            .await
11016            .expect("transient failures must not stop the worker");
11017
11018        for path in [
11019            "/api/worker/heartbeat",
11020            "/api/worker/workflow-tasks/poll",
11021            "/api/worker/activity-tasks/poll",
11022            "/api/worker/query-tasks/poll",
11023        ] {
11024            assert!(
11025                server.request_count(path) >= 2,
11026                "{path} must continue after its transient failure"
11027            );
11028        }
11029    }
11030
11031    #[tokio::test]
11032    async fn worker_bounds_transport_retries() {
11033        let server = MockWorkerServer::unavailable_polls();
11034        let client = Client::builder(server.base_url())
11035            .timeout(Duration::from_secs(2))
11036            .build()
11037            .expect("client");
11038        let mut worker = Worker::new(client, "rust-workers")
11039            .worker_id("bounded-retry-worker")
11040            .poll_timeout(Duration::from_millis(10))
11041            .retry_policy(WorkerRetryPolicy {
11042                max_retries: 2,
11043                initial_backoff: Duration::from_millis(1),
11044                max_backoff: Duration::from_millis(1),
11045            });
11046        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
11047
11048        let error = worker.run().await.expect_err("retry bound must terminate");
11049        assert!(matches!(error, Error::Transport(_)));
11050        assert_eq!(
11051            server.request_count("/api/worker/workflow-tasks/poll"),
11052            3,
11053            "one initial request plus two retries"
11054        );
11055    }
11056
11057    #[tokio::test]
11058    async fn worker_does_not_retry_authentication_failures() {
11059        let server = MockWorkerServer::unauthorized_polls();
11060        let client = Client::builder(server.base_url())
11061            .timeout(Duration::from_secs(2))
11062            .build()
11063            .expect("client");
11064        let mut worker = Worker::new(client, "rust-workers")
11065            .worker_id("unauthorized-worker")
11066            .poll_timeout(Duration::from_millis(10));
11067        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
11068
11069        let error = worker
11070            .run()
11071            .await
11072            .expect_err("authentication must terminate");
11073        let Error::Http { status, body } = error else {
11074            panic!("expected stable HTTP authentication error");
11075        };
11076        assert_eq!(status, reqwest::StatusCode::UNAUTHORIZED);
11077        assert!(body.contains("authentication_failed"));
11078        assert_eq!(
11079            server.request_count("/api/worker/workflow-tasks/poll"),
11080            1,
11081            "authentication failures must not be retried"
11082        );
11083    }
11084
11085    #[derive(Clone, Debug)]
11086    struct CapturedRequest {
11087        path: String,
11088        worker_protocol: Option<String>,
11089        body: String,
11090        received_at: Instant,
11091    }
11092
11093    struct MockWorkerServer {
11094        addr: SocketAddr,
11095        stop: Arc<AtomicBool>,
11096        requests: Arc<Mutex<Vec<CapturedRequest>>>,
11097        thread: Option<thread::JoinHandle<()>>,
11098    }
11099
11100    #[derive(Clone, Copy, Default)]
11101    struct MockWorkerBehavior {
11102        reject_query_protocol: bool,
11103        reject_query_completion: bool,
11104        waiting_query_worker: bool,
11105        complete_named_signal: bool,
11106        poll_failures_per_path: usize,
11107        heartbeat_failures: usize,
11108        heartbeat_failure_request: Option<usize>,
11109        delayed_heartbeat_request: Option<usize>,
11110        heartbeat_response_delay: Duration,
11111        concurrent_requests: bool,
11112        unauthorized_polls: bool,
11113        cancelled_activity: bool,
11114        draining_polls: bool,
11115        workflow_completion_status: Option<&'static str>,
11116        workflow_completion_body: Option<&'static str>,
11117    }
11118
11119    impl MockWorkerServer {
11120        fn start() -> Self {
11121            Self::start_with_behavior(MockWorkerBehavior::default())
11122        }
11123
11124        fn reject_query_protocol() -> Self {
11125            Self::start_with_behavior(MockWorkerBehavior {
11126                reject_query_protocol: true,
11127                ..MockWorkerBehavior::default()
11128            })
11129        }
11130
11131        fn reject_query_completion() -> Self {
11132            Self::start_with_behavior(MockWorkerBehavior {
11133                reject_query_completion: true,
11134                ..MockWorkerBehavior::default()
11135            })
11136        }
11137
11138        fn waiting_query_worker() -> Self {
11139            Self::start_with_behavior(MockWorkerBehavior {
11140                waiting_query_worker: true,
11141                complete_named_signal: true,
11142                ..MockWorkerBehavior::default()
11143            })
11144        }
11145
11146        fn transient_worker_failures() -> Self {
11147            Self::start_with_behavior(MockWorkerBehavior {
11148                poll_failures_per_path: 1,
11149                heartbeat_failures: 1,
11150                ..MockWorkerBehavior::default()
11151            })
11152        }
11153
11154        fn delayed_heartbeat_worker() -> Self {
11155            Self::start_with_behavior(MockWorkerBehavior {
11156                waiting_query_worker: true,
11157                delayed_heartbeat_request: Some(2),
11158                heartbeat_response_delay: Duration::from_millis(1_500),
11159                concurrent_requests: true,
11160                cancelled_activity: true,
11161                ..MockWorkerBehavior::default()
11162            })
11163        }
11164
11165        fn heartbeat_retry_worker() -> Self {
11166            Self::start_with_behavior(MockWorkerBehavior {
11167                waiting_query_worker: true,
11168                heartbeat_failure_request: Some(2),
11169                concurrent_requests: true,
11170                ..MockWorkerBehavior::default()
11171            })
11172        }
11173
11174        fn unavailable_polls() -> Self {
11175            Self::start_with_behavior(MockWorkerBehavior {
11176                poll_failures_per_path: usize::MAX,
11177                ..MockWorkerBehavior::default()
11178            })
11179        }
11180
11181        fn unauthorized_polls() -> Self {
11182            Self::start_with_behavior(MockWorkerBehavior {
11183                unauthorized_polls: true,
11184                ..MockWorkerBehavior::default()
11185            })
11186        }
11187
11188        fn cancelled_activity() -> Self {
11189            Self::start_with_behavior(MockWorkerBehavior {
11190                cancelled_activity: true,
11191                ..MockWorkerBehavior::default()
11192            })
11193        }
11194
11195        fn draining_polls() -> Self {
11196            Self::start_with_behavior(MockWorkerBehavior {
11197                draining_polls: true,
11198                ..MockWorkerBehavior::default()
11199            })
11200        }
11201
11202        fn workflow_completion(status: &'static str, body: &'static str) -> Self {
11203            Self::start_with_behavior(MockWorkerBehavior {
11204                workflow_completion_status: Some(status),
11205                workflow_completion_body: Some(body),
11206                ..MockWorkerBehavior::default()
11207            })
11208        }
11209
11210        fn start_with_behavior(behavior: MockWorkerBehavior) -> Self {
11211            let listener = TcpListener::bind("127.0.0.1:0").expect("bind mock server");
11212            listener
11213                .set_nonblocking(true)
11214                .expect("configure mock listener");
11215            let addr = listener.local_addr().expect("mock server address");
11216            let stop = Arc::new(AtomicBool::new(false));
11217            let server_stop = Arc::clone(&stop);
11218            let requests = Arc::new(Mutex::new(Vec::new()));
11219            let server_requests = Arc::clone(&requests);
11220            let thread = thread::spawn(move || {
11221                let mut request_threads = Vec::new();
11222                while !server_stop.load(Ordering::SeqCst) {
11223                    match listener.accept() {
11224                        Ok((mut stream, _)) => {
11225                            if behavior.concurrent_requests {
11226                                let requests = Arc::clone(&server_requests);
11227                                request_threads.push(thread::spawn(move || {
11228                                    handle_mock_worker_request(&mut stream, &requests, behavior)
11229                                }));
11230                            } else {
11231                                handle_mock_worker_request(&mut stream, &server_requests, behavior);
11232                            }
11233                        }
11234                        Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
11235                            let mut index = 0;
11236                            while index < request_threads.len() {
11237                                if request_threads[index].is_finished() {
11238                                    request_threads
11239                                        .swap_remove(index)
11240                                        .join()
11241                                        .expect("join mock request");
11242                                } else {
11243                                    index += 1;
11244                                }
11245                            }
11246                            thread::sleep(Duration::from_millis(5));
11247                        }
11248                        Err(_) => break,
11249                    }
11250                }
11251                for request_thread in request_threads {
11252                    request_thread.join().expect("join mock request");
11253                }
11254            });
11255
11256            Self {
11257                addr,
11258                stop,
11259                requests,
11260                thread: Some(thread),
11261            }
11262        }
11263
11264        fn base_url(&self) -> String {
11265            format!("http://{}", self.addr)
11266        }
11267
11268        fn worker_protocol_for(&self, path: &str) -> Option<String> {
11269            self.requests
11270                .lock()
11271                .expect("captured requests")
11272                .iter()
11273                .find(|request| request.path == path)
11274                .and_then(|request| request.worker_protocol.clone())
11275        }
11276
11277        fn request_count(&self, path: &str) -> usize {
11278            self.requests
11279                .lock()
11280                .expect("captured requests")
11281                .iter()
11282                .filter(|request| request.path == path)
11283                .count()
11284        }
11285
11286        fn request_times(&self, path: &str) -> Vec<Instant> {
11287            self.requests
11288                .lock()
11289                .expect("captured requests")
11290                .iter()
11291                .filter(|request| request.path == path)
11292                .map(|request| request.received_at)
11293                .collect()
11294        }
11295
11296        fn request_body(&self, path: &str) -> Value {
11297            let requests = self.requests.lock().expect("captured requests");
11298            let body = &requests
11299                .iter()
11300                .find(|request| request.path == path)
11301                .unwrap_or_else(|| panic!("missing request for {path}"))
11302                .body;
11303            serde_json::from_str(body).unwrap_or_else(|error| {
11304                panic!("invalid JSON request body for {path}: {error}: {body:?}")
11305            })
11306        }
11307    }
11308
11309    impl Drop for MockWorkerServer {
11310        fn drop(&mut self) {
11311            self.stop.store(true, Ordering::SeqCst);
11312            let _ = TcpStream::connect(self.addr);
11313
11314            if let Some(thread) = self.thread.take() {
11315                thread.join().expect("join mock server");
11316            }
11317        }
11318    }
11319
11320    fn handle_mock_worker_request(
11321        stream: &mut TcpStream,
11322        requests: &Arc<Mutex<Vec<CapturedRequest>>>,
11323        behavior: MockWorkerBehavior,
11324    ) {
11325        let _ = stream.set_read_timeout(Some(Duration::from_millis(200)));
11326        let mut buffer = [0_u8; 8192];
11327        let mut request = Vec::new();
11328
11329        loop {
11330            match stream.read(&mut buffer) {
11331                Ok(0) => break,
11332                Ok(read) => {
11333                    request.extend_from_slice(&buffer[..read]);
11334                    if mock_request_is_complete(&request) {
11335                        break;
11336                    }
11337                }
11338                Err(error)
11339                    if matches!(
11340                        error.kind(),
11341                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
11342                    ) =>
11343                {
11344                    break;
11345                }
11346                Err(_) => return,
11347            }
11348        }
11349
11350        let request = String::from_utf8_lossy(&request);
11351        let body = request
11352            .split_once("\r\n\r\n")
11353            .map(|(_, body)| body)
11354            .unwrap_or_default();
11355        let path = request
11356            .lines()
11357            .next()
11358            .and_then(|line| line.split_whitespace().nth(1))
11359            .unwrap_or_default();
11360        let worker_protocol = request.lines().find_map(|line| {
11361            let (name, value) = line.split_once(':')?;
11362            name.eq_ignore_ascii_case("X-Durable-Workflow-Protocol-Version")
11363                .then(|| value.trim().to_string())
11364        });
11365        let request_number = {
11366            let mut requests = requests.lock().expect("captured requests");
11367            requests.push(CapturedRequest {
11368                path: path.to_string(),
11369                worker_protocol: worker_protocol.clone(),
11370                body: body.to_string(),
11371                received_at: Instant::now(),
11372            });
11373            requests
11374                .iter()
11375                .filter(|request| request.path == path)
11376                .count()
11377        };
11378
11379        let is_poll = matches!(
11380            path,
11381            "/api/worker/workflow-tasks/poll"
11382                | "/api/worker/activity-tasks/poll"
11383                | "/api/worker/query-tasks/poll"
11384        );
11385        if is_poll && request_number <= behavior.poll_failures_per_path {
11386            return;
11387        }
11388        if path == "/api/worker/heartbeat" && request_number <= behavior.heartbeat_failures {
11389            return;
11390        }
11391        if path == "/api/worker/heartbeat"
11392            && behavior.heartbeat_failure_request == Some(request_number)
11393        {
11394            return;
11395        }
11396        if path == "/api/worker/heartbeat"
11397            && behavior.delayed_heartbeat_request == Some(request_number)
11398        {
11399            thread::sleep(behavior.heartbeat_response_delay);
11400        }
11401        if behavior.unauthorized_polls && is_poll {
11402            write_mock_response(
11403                stream,
11404                "401 Unauthorized",
11405                r#"{"reason":"authentication_failed","message":"invalid worker token"}"#,
11406            );
11407            return;
11408        }
11409        if behavior.draining_polls && is_poll {
11410            write_mock_response(
11411                stream,
11412                "409 Conflict",
11413                r#"{"task":null,"poll_status":"draining","reason":"worker_draining","worker_status":"draining","drain_intent":"draining"}"#,
11414            );
11415            return;
11416        }
11417
11418        if behavior.reject_query_protocol && path.starts_with("/api/worker/query-tasks/") {
11419            let requested_version = worker_protocol.as_deref().unwrap_or("missing");
11420            let body = format!(
11421                r#"{{"reason":"unsupported_protocol_version","message":"Query tasks require worker protocol 1.8 or newer.","supported_version":"1.7","requested_version":"{requested_version}"}}"#
11422            );
11423            write_mock_response(stream, "400 Bad Request", &body);
11424            return;
11425        }
11426
11427        if behavior.reject_query_completion && path == "/api/worker/query-tasks/query-late/complete"
11428        {
11429            write_mock_response(
11430                stream,
11431                "409 Conflict",
11432                r#"{"reason":"query_task_timed_out","message":"query task timed out before completion"}"#,
11433            );
11434            return;
11435        }
11436
11437        if behavior.workflow_completion_status.is_some()
11438            && path == "/api/worker/workflow-tasks/poll"
11439            && request_number == 1
11440        {
11441            write_mock_response(
11442                stream,
11443                "200 OK",
11444                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"}}"#,
11445            );
11446            return;
11447        }
11448
11449        if path == "/api/worker/workflow-tasks/workflow-timeout-task/complete" {
11450            if let (Some(status), Some(body)) = (
11451                behavior.workflow_completion_status,
11452                behavior.workflow_completion_body,
11453            ) {
11454                write_mock_response(stream, status, body);
11455                return;
11456            }
11457        }
11458
11459        if behavior.waiting_query_worker {
11460            if behavior.complete_named_signal
11461                && path == "/api/worker/workflow-tasks/poll"
11462                && request_number == 1
11463            {
11464                let body = json!({
11465                    "task": {
11466                        "task_id": "snapshot-open",
11467                        "workflow_id": "snapshot-1",
11468                        "run_id": "snapshot-run-1",
11469                        "workflow_type": "snapshot",
11470                        "payload_codec": DEFAULT_CODEC,
11471                        "arguments": encode_value_envelope(&json!([]), DEFAULT_CODEC)
11472                            .expect("Avro workflow arguments"),
11473                        "history_events": [],
11474                        "workflow_task_attempt": 1,
11475                        "lease_owner": "rust-snapshot-worker"
11476                    }
11477                })
11478                .to_string();
11479                write_mock_response(stream, "200 OK", &body);
11480                return;
11481            }
11482
11483            let signal_request = request_number - usize::from(behavior.complete_named_signal);
11484            let signal_request_limit = 2 + usize::from(behavior.complete_named_signal);
11485            if path == "/api/worker/workflow-tasks/poll"
11486                && signal_request >= 1
11487                && signal_request <= signal_request_limit
11488            {
11489                let finish = behavior.complete_named_signal && signal_request == 3;
11490                let amounts = if signal_request == 1 {
11491                    vec![3]
11492                } else {
11493                    vec![3, 5]
11494                };
11495                let task_id = if signal_request == 1 {
11496                    "snapshot-wait-3"
11497                } else if finish {
11498                    "snapshot-finish"
11499                } else {
11500                    "snapshot-wait-5"
11501                };
11502                let mut history_events = std::iter::once(json!({
11503                    "event_type": "SignalWaitOpened",
11504                    "payload": {"sequence": 1, "signal_name": "finish"}
11505                }))
11506                .chain(amounts.iter().enumerate().map(|(index, amount)| {
11507                    json!({
11508                        "event_type": "SignalReceived",
11509                        "payload": {
11510                            "signal_id": format!("increment-{amount}"),
11511                            "signal_name": "increment",
11512                            "workflow_sequence": index + 2,
11513                            "payload_codec": DEFAULT_CODEC,
11514                            "arguments": encode_value_envelope(&json!([amount]), DEFAULT_CODEC)
11515                                .expect("Avro signal envelope")
11516                        }
11517                    })
11518                }))
11519                .collect::<Vec<_>>();
11520                let (resume_id, resume_name, resume_arguments) = if finish {
11521                    history_events.push(json!({
11522                        "event_type": "SignalReceived",
11523                        "payload": {
11524                            "signal_id": "finish",
11525                            "signal_name": "finish",
11526                            "workflow_sequence": 4,
11527                            "payload_codec": DEFAULT_CODEC,
11528                            "arguments": encode_value_envelope(&json!([]), DEFAULT_CODEC)
11529                                .expect("Avro finish signal envelope")
11530                        }
11531                    }));
11532                    (
11533                        "finish".to_string(),
11534                        "finish".to_string(),
11535                        encode_value_envelope(&json!([]), DEFAULT_CODEC)
11536                            .expect("Avro finish resume signal"),
11537                    )
11538                } else {
11539                    let amount = amounts.last().expect("amount");
11540                    (
11541                        format!("increment-{amount}"),
11542                        "increment".to_string(),
11543                        encode_value_envelope(&json!([amount]), DEFAULT_CODEC)
11544                            .expect("Avro increment resume signal"),
11545                    )
11546                };
11547                let body = json!({
11548                    "task": {
11549                        "task_id": task_id,
11550                        "workflow_id": "snapshot-1",
11551                        "run_id": "snapshot-run-1",
11552                        "workflow_type": "snapshot",
11553                        "payload_codec": DEFAULT_CODEC,
11554                        "arguments": encode_value_envelope(&json!([]), DEFAULT_CODEC)
11555                            .expect("Avro workflow arguments"),
11556                        "history_events": history_events,
11557                        "workflow_task_attempt": 1,
11558                        "workflow_signal_id": resume_id,
11559                        "signal_name": resume_name,
11560                        "signal_arguments": resume_arguments,
11561                        "lease_owner": "rust-snapshot-worker"
11562                    }
11563                })
11564                .to_string();
11565                write_mock_response(stream, "200 OK", &body);
11566                return;
11567            }
11568
11569            if path == "/api/worker/query-tasks/poll" && request_number == 1 {
11570                let history_events = [3, 5]
11571                    .into_iter()
11572                    .enumerate()
11573                    .map(|(index, amount)| {
11574                        json!({
11575                            "event_type": "SignalReceived",
11576                            "payload": {
11577                                "signal_id": format!("increment-{amount}"),
11578                                "signal_name": "increment",
11579                                "workflow_sequence": index + 2,
11580                                "payload_codec": DEFAULT_CODEC,
11581                                "arguments": encode_value_envelope(&json!([amount]), DEFAULT_CODEC)
11582                                    .expect("Avro query signal envelope")
11583                            }
11584                        })
11585                    })
11586                    .collect::<Vec<_>>();
11587                let body = json!({
11588                    "task": {
11589                        "query_task_id": "snapshot-current",
11590                        "query_task_attempt": 1,
11591                        "lease_owner": "rust-snapshot-worker",
11592                        "workflow_id": "snapshot-1",
11593                        "run_id": "snapshot-run-1",
11594                        "workflow_type": "snapshot",
11595                        "query_name": "current",
11596                        "payload_codec": DEFAULT_CODEC,
11597                        "workflow_arguments": encode_value_envelope(&json!([]), DEFAULT_CODEC)
11598                            .expect("Avro workflow arguments"),
11599                        "query_arguments": encode_value_envelope(&json!([]), DEFAULT_CODEC)
11600                            .expect("Avro query arguments"),
11601                        "history_events": history_events,
11602                        "run_status": "waiting"
11603                    }
11604                })
11605                .to_string();
11606                write_mock_response(stream, "200 OK", &body);
11607                return;
11608            }
11609
11610            if path == "/api/worker/workflow-tasks/snapshot-wait-3/fail"
11611                || path == "/api/worker/workflow-tasks/snapshot-wait-5/fail"
11612            {
11613                write_mock_response(
11614                    stream,
11615                    "200 OK",
11616                    r#"{"outcome":"waiting_for_history","recorded":true}"#,
11617                );
11618                return;
11619            }
11620
11621            if path == "/api/worker/workflow-tasks/snapshot-open/complete" {
11622                write_mock_response(stream, "200 OK", r#"{"outcome":"waiting","recorded":true}"#);
11623                return;
11624            }
11625
11626            if path == "/api/worker/workflow-tasks/snapshot-finish/complete" {
11627                write_mock_response(
11628                    stream,
11629                    "200 OK",
11630                    r#"{"outcome":"completed","run_status":"completed","recorded":true}"#,
11631                );
11632                return;
11633            }
11634
11635            if path == "/api/worker/query-tasks/snapshot-current/complete" {
11636                write_mock_response(stream, "200 OK", r#"{"outcome":"completed"}"#);
11637                return;
11638            }
11639        }
11640
11641        let (status, body) = match path {
11642            "/api/workflows" => (
11643                "201 Created",
11644                r#"{"workflow_id":"wf-start-options","run_id":"run-start-options","workflow_type":"rust.timeout"}"#,
11645            ),
11646            "/api/worker/register" if behavior.waiting_query_worker => (
11647                "200 OK",
11648                r#"{"worker_id":"rust-snapshot-worker","registered":true,"heartbeat_interval_seconds":1}"#,
11649            ),
11650            "/api/worker/register" => (
11651                "200 OK",
11652                r#"{"worker_id":"mock-worker","registered":true,"heartbeat_interval_seconds":3600}"#,
11653            ),
11654            "/api/worker/heartbeat" => ("200 OK", "{}"),
11655            "/api/worker/activity-tasks/poll"
11656                if behavior.cancelled_activity && request_number == 1 =>
11657            {
11658                (
11659                    "200 OK",
11660                    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"}}"#,
11661                )
11662            }
11663            "/api/worker/activity-tasks/poll" | "/api/worker/workflow-tasks/poll" => {
11664                ("200 OK", r#"{"task":null}"#)
11665            }
11666            "/api/worker/query-tasks/poll"
11667                if behavior.reject_query_completion && request_number == 1 =>
11668            {
11669                (
11670                    "200 OK",
11671                    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"}}"#,
11672                )
11673            }
11674            "/api/worker/query-tasks/poll" => ("200 OK", r#"{"task":null}"#),
11675            "/api/worker/query-tasks/query-capture/complete"
11676            | "/api/worker/query-tasks/query-capture/fail" => ("200 OK", "{}"),
11677            "/api/worker/activity-tasks/activity-cancel/heartbeat" => (
11678                "200 OK",
11679                r#"{"activity_attempt_id":"attempt-cancel","cancel_requested":true,"can_continue":false,"reason":"run_cancelled","run_closed_reason":"cancelled","heartbeat_recorded":false}"#,
11680            ),
11681            "/api/worker/activity-tasks/activity-cancel/complete" => (
11682                "409 Conflict",
11683                r#"{"task_id":"activity-cancel","activity_attempt_id":"attempt-cancel","reason":"run_cancelled","cancel_requested":true,"can_continue":false,"run_closed_reason":"cancelled"}"#,
11684            ),
11685            "/api/workflows/counter-1/query/current" => (
11686                "200 OK",
11687                r#"{"workflow_id":"counter-1","query_name":"current","result":{"count":8},"result_envelope":{"codec":"json","blob":"{\"count\":8}"}}"#,
11688            ),
11689            "/api/workflows/counter-1/query/missing" => (
11690                "404 Not Found",
11691                r#"{"workflow_id":"counter-1","query_name":"missing","reason":"rejected_unknown_query","message":"unknown query"}"#,
11692            ),
11693            "/api/workflows/wf-lifecycle/cancel" => (
11694                "200 OK",
11695                r#"{"workflow_id":"wf-lifecycle","run_id":"run-current","outcome":"cancelled","reason":"cleanup requested","command_status":"accepted"}"#,
11696            ),
11697            "/api/workflows/wf-lifecycle/terminate" => (
11698                "200 OK",
11699                r#"{"workflow_id":"wf-lifecycle","run_id":"run-current","outcome":"terminated","reason":"forced stop","command_status":"accepted"}"#,
11700            ),
11701            "/api/workflows/wf-lifecycle/runs/run-current/cancel" => (
11702                "200 OK",
11703                r#"{"workflow_id":"wf-lifecycle","run_id":"run-current","outcome":"cancelled","command_status":"accepted"}"#,
11704            ),
11705            "/api/workflows/wf-lifecycle/runs/run-current/terminate" => (
11706                "200 OK",
11707                r#"{"workflow_id":"wf-lifecycle","run_id":"run-current","outcome":"terminated","command_status":"accepted"}"#,
11708            ),
11709            "/api/workflows/wf-lifecycle/runs/run-stale/cancel"
11710            | "/api/workflows/wf-lifecycle/runs/run-stale/terminate" => (
11711                "409 Conflict",
11712                r#"{"workflow_id":"wf-lifecycle","run_id":"run-stale","reason":"historical_run_command_rejected","target_scope":"run","message":"Commands cannot target historical runs."}"#,
11713            ),
11714            "/api/workflows/wf-failed" | "/api/workflows/wf-failed/runs/run-failed" => (
11715                "200 OK",
11716                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"}]}}"#,
11717            ),
11718            "/api/workflows/wf-cancelled" => (
11719                "200 OK",
11720                r#"{"workflow_id":"wf-cancelled","run_id":"run-cancelled","status":"cancelled","closed_reason":"cancelled","reason":"cleanup requested"}"#,
11721            ),
11722            "/api/workflows/wf-terminated" => (
11723                "200 OK",
11724                r#"{"workflow_id":"wf-terminated","run_id":"run-terminated","status":"terminated","closed_reason":"terminated","reason":"forced stop"}"#,
11725            ),
11726            "/api/workflows/wf-timed-out" => (
11727                "200 OK",
11728                r#"{"workflow_id":"wf-timed-out","run_id":"run-timed-out","status":"failed","closed_reason":"timed_out","reason":"run_timeout"}"#,
11729            ),
11730            "/api/workflows/wf-waiting" | "/api/workflows/wf-waiting/runs/run-waiting" => (
11731                "200 OK",
11732                r#"{"workflow_id":"wf-waiting","run_id":"run-waiting","status":"waiting"}"#,
11733            ),
11734            "/api/workflows/wf-selected" => (
11735                "200 OK",
11736                r#"{"workflow_id":"wf-selected","run_id":"run-current","status":"completed","output":"current run output"}"#,
11737            ),
11738            "/api/workflows/wf-selected/runs/run-selected" => (
11739                "200 OK",
11740                r#"{"workflow_id":"wf-selected","run_id":"run-selected","status":"cancelled","closed_reason":"cancelled","reason":"selected run cancelled"}"#,
11741            ),
11742            _ => ("404 Not Found", r#"{"message":"not found"}"#),
11743        };
11744        write_mock_response(stream, status, body);
11745    }
11746
11747    fn mock_request_is_complete(request: &[u8]) -> bool {
11748        let Some(header_end) = request
11749            .windows(4)
11750            .position(|window| window == b"\r\n\r\n")
11751            .map(|position| position + 4)
11752        else {
11753            return false;
11754        };
11755        let headers = String::from_utf8_lossy(&request[..header_end]);
11756        let content_length = headers.lines().find_map(|line| {
11757            let (name, value) = line.split_once(':')?;
11758            name.eq_ignore_ascii_case("content-length")
11759                .then(|| value.trim().parse::<usize>().ok())
11760                .flatten()
11761        });
11762
11763        request.len() >= header_end + content_length.unwrap_or(0)
11764    }
11765
11766    fn write_mock_response(stream: &mut TcpStream, status: &str, body: &str) {
11767        let response = format!(
11768            "HTTP/1.1 {status}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
11769            body.len()
11770        );
11771
11772        let _ = stream.write_all(response.as_bytes());
11773        let _ = stream.flush();
11774    }
11775}