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