From cffdf0d288c8aea2521a0936ffaef555513d3b2e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 19:03:31 +0000 Subject: [PATCH 1/2] Initial plan From 35a305c72c3a58b071a5059e3c2045a2b9636569 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 19:18:56 +0000 Subject: [PATCH 2/2] fix: qualify auto sub-orch IDs for continue-as-new generations --- docs/replay-engine-test-spec.md | 3 ++- docs/sub-orchestrations.md | 5 ++--- src/lib.rs | 4 +++- src/runtime/replay_engine.rs | 10 +++++++--- tests/replay_engine/sub_orchestration.rs | 25 ++++++++++++++++++++++++ 5 files changed, 39 insertions(+), 8 deletions(-) diff --git a/docs/replay-engine-test-spec.md b/docs/replay-engine-test-spec.md index 3417f4b..53ffeba 100644 --- a/docs/replay-engine-test-spec.md +++ b/docs/replay-engine-test-spec.md @@ -253,7 +253,8 @@ Tests for deterministic child instance ID. | Test | Baseline | Handler | Expected | |------|----------|---------|----------| -| `auto_instance_id` | `[Started]` | `schedule_sub_orchestration("Child", input)` | Instance = `sub::{event_id}` | +| `auto_instance_id` | `[Started]` | `schedule_sub_orchestration("Child", input)` | Instance = `sub::{event_id}` (execution 1) | +| `auto_instance_id_includes_execution_id_for_later_generations` | `[Started(execution=2)]` | `schedule_sub_orchestration("Child", input)` | Instance = `sub::2.{event_id}` | | `explicit_instance_preserved` | `[Started]` | `schedule_sub_orchestration_with_instance("my-id", ...)` | Instance = "my-id" | | `replay_uses_history_instance` | `[Started, SubOrchScheduled{instance:"sub::2"}]` | Same schedule | Binds to same instance | diff --git a/docs/sub-orchestrations.md b/docs/sub-orchestrations.md index f71b73e..608cdf0 100644 --- a/docs/sub-orchestrations.md +++ b/docs/sub-orchestrations.md @@ -57,8 +57,8 @@ There are two modes for child instance ID assignment: **Auto-generated (default):** - Use `schedule_sub_orchestration()` - no instance ID parameter -- Runtime generates `instance = "sub::{event_id}"` and prefixes with parent: `"{parent_instance}::sub::{id}"` -- This guarantees the same child id on replays and prevents collisions across parents +- Runtime generates `instance = "sub::{event_id}"` for execution 1, and `instance = "sub::{execution_id}.{event_id}"` for later continue-as-new executions; then prefixes with parent. +- This guarantees the same child id on replays and prevents collisions across parents and continue-as-new generations. **Explicit:** - Use `schedule_sub_orchestration_with_id("Child", "my-instance-id", input)` @@ -162,4 +162,3 @@ let root = |ctx: OrchestrationContext, input: String| async move { - Provider-backed durable completion routing (enqueue `SubOrchCompleted/Failed` items), leasing, DLQ. - Visualization: render sub-orchestration edges in Mermaid diagrams under each parent. - diff --git a/src/lib.rs b/src/lib.rs index e465352..46e7f80 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -862,7 +862,9 @@ pub const INITIAL_EVENT_ID: u64 = 1; pub const SUB_ORCH_AUTO_PREFIX: &str = "sub::"; /// Prefix for placeholder instance IDs before event ID assignment. -/// These are replaced with `sub::{event_id}` during action processing. +/// These are replaced during action processing: +/// - execution 1: `sub::{event_id}` +/// - execution N>1: `sub::{execution_id}.{event_id}` pub(crate) const SUB_ORCH_PENDING_PREFIX: &str = "sub::pending_"; /// Determine if a sub-orchestration instance ID is auto-generated (needs parent prefix). diff --git a/src/runtime/replay_engine.rs b/src/runtime/replay_engine.rs index f644195..a125a4d 100644 --- a/src/runtime/replay_engine.rs +++ b/src/runtime/replay_engine.rs @@ -1082,7 +1082,7 @@ impl ReplayEngine { ctx.bind_token(token, event_id); - let updated_action = update_action_event_id(action, event_id); + let updated_action = update_action_event_id(action, event_id, self.execution_id); if let crate::Action::StartSubOrchestration { instance, .. } = &updated_action { ctx.bind_sub_orchestration_instance(token, instance.clone()); @@ -1750,7 +1750,7 @@ fn action_to_event(action: &Action, instance: &str, execution_id: u64, event_id: /// Update an action's scheduling_event_id to the correct event_id. /// Also generates the actual sub-orchestration instance ID from the event_id /// (unless an explicit instance ID was provided, indicated by not starting with SUB_ORCH_PENDING_PREFIX). -fn update_action_event_id(action: Action, event_id: u64) -> Action { +fn update_action_event_id(action: Action, event_id: u64, execution_id: u64) -> Action { match action { Action::CallActivity { name, @@ -1793,7 +1793,11 @@ fn update_action_event_id(action: Action, event_id: u64) -> Action { // If instance starts with the pending prefix, it's a placeholder that needs to be replaced. // Otherwise, it's an explicit instance ID provided by the user. let final_instance = if instance.starts_with(crate::SUB_ORCH_PENDING_PREFIX) { - format!("{}{event_id}", crate::SUB_ORCH_AUTO_PREFIX) + if execution_id == crate::INITIAL_EXECUTION_ID { + format!("{}{event_id}", crate::SUB_ORCH_AUTO_PREFIX) + } else { + format!("{}{execution_id}.{event_id}", crate::SUB_ORCH_AUTO_PREFIX) + } } else { instance }; diff --git a/tests/replay_engine/sub_orchestration.rs b/tests/replay_engine/sub_orchestration.rs index f48ef00..447aa32 100644 --- a/tests/replay_engine/sub_orchestration.rs +++ b/tests/replay_engine/sub_orchestration.rs @@ -8,6 +8,7 @@ use super::helpers::*; use async_trait::async_trait; use duroxide::{OrchestrationContext, OrchestrationHandler}; +use duroxide::runtime::replay_engine::ReplayEngine; use std::sync::Arc; /// Auto-generated instance ID should be sub::{event_id}. @@ -43,6 +44,30 @@ fn auto_instance_id() { } } +/// Auto-generated instance ID includes execution_id after continue-as-new generations. +#[test] +fn auto_instance_id_includes_execution_id_for_later_generations() { + let mut started = started_event(1); // OrchestrationStarted + started.execution_id = 2; + let history = vec![started]; + let mut engine = ReplayEngine::new(TEST_INSTANCE.to_string(), 2, history); + let result = execute(&mut engine, SubOrchHandler::new("Child", "child-input")); + + assert_continue(&result); + + assert_eq!(engine.pending_actions().len(), 1); + match &engine.pending_actions()[0] { + duroxide::Action::StartSubOrchestration { + instance, + scheduling_event_id, + .. + } => { + assert_eq!(instance, &format!("sub::2.{scheduling_event_id}")); + } + _ => panic!("Expected StartSubOrchestration action"), + } +} + /// Replay uses history's instance ID. /// /// Orchestration code: