From 0ee2024f4eb4cb3384c8209cadfbe025c08a7fa9 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 20:51:18 +0000 Subject: [PATCH] Fix continueAsNew dropping fire-and-forget actions (sendEvent, signalEntity) When an orchestration called sendEvent() or signalEntity() before continueAsNew(), those fire-and-forget actions were silently dropped because getActions() returned only the continue-as-new completion action, ignoring all other pending actions. This is inconsistent with setComplete() and setFailed(), which both preserve pending fire-and-forget actions alongside the completion action. The fix makes getActions() include all pending actions when continuing-as-new, matching the behavior of the other completion paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../worker/runtime-orchestration-context.ts | 7 ++- .../test/in-memory-backend.spec.ts | 43 +++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/durabletask-js/src/worker/runtime-orchestration-context.ts b/packages/durabletask-js/src/worker/runtime-orchestration-context.ts index c01e19d..8f171bb 100644 --- a/packages/durabletask-js/src/worker/runtime-orchestration-context.ts +++ b/packages/durabletask-js/src/worker/runtime-orchestration-context.ts @@ -242,7 +242,6 @@ export class RuntimeOrchestrationContext extends OrchestrationContext { getActions(): pb.OrchestratorAction[] { if (this._completionStatus === pb.OrchestrationStatus.ORCHESTRATION_STATUS_CONTINUED_AS_NEW) { - // Only return the single completion actions when continuing-as-new let carryoverEvents: pb.HistoryEvent[] | null = null; if (this._saveEvents) { @@ -266,7 +265,11 @@ export class RuntimeOrchestrationContext extends OrchestrationContext { carryoverEvents, ); - return [action]; + // Include fire-and-forget actions (sendEvent, signalEntity, etc.) that were + // scheduled before continueAsNew was called, consistent with setComplete/setFailed + const allActions = Object.values(this._pendingActions); + allActions.push(action); + return allActions; } return Object.values(this._pendingActions); diff --git a/packages/durabletask-js/test/in-memory-backend.spec.ts b/packages/durabletask-js/test/in-memory-backend.spec.ts index 56e5f15..222f539 100644 --- a/packages/durabletask-js/test/in-memory-backend.spec.ts +++ b/packages/durabletask-js/test/in-memory-backend.spec.ts @@ -229,6 +229,49 @@ describe("In-Memory Backend", () => { expect(state?.serializedOutput).toEqual(JSON.stringify(5)); }); + it("should preserve sendEvent actions when continuing-as-new", async () => { + // Receiver orchestration that waits for an event + const receiver: TOrchestrator = async function* (ctx: OrchestrationContext): any { + const value = yield ctx.waitForExternalEvent("ping"); + return value; + }; + + // Sender orchestration that sends an event then continues-as-new + const sender: TOrchestrator = async (ctx: OrchestrationContext, input: { receiverId: string; iteration: number }) => { + if (input.iteration === 1) { + // On first iteration, send event to receiver then continue-as-new + ctx.sendEvent(input.receiverId, "ping", "hello from sender"); + ctx.continueAsNew({ receiverId: input.receiverId, iteration: 2 }, false); + } else { + return "sender done"; + } + }; + + worker.addOrchestrator(receiver); + worker.addOrchestrator(sender); + await worker.start(); + + // Start receiver first, then sender + const receiverId = await client.scheduleNewOrchestration(receiver); + await client.waitForOrchestrationStart(receiverId, false, 5); + + const senderId = await client.scheduleNewOrchestration(sender, { receiverId, iteration: 1 }); + + // Wait for both to complete + const senderState = await client.waitForOrchestrationCompletion(senderId, true, 10); + const receiverState = await client.waitForOrchestrationCompletion(receiverId, true, 10); + + // Sender should complete after continuing-as-new + expect(senderState).toBeDefined(); + expect(senderState?.runtimeStatus).toEqual(OrchestrationStatus.COMPLETED); + expect(senderState?.serializedOutput).toEqual(JSON.stringify("sender done")); + + // Receiver should have received the event sent before continue-as-new + expect(receiverState).toBeDefined(); + expect(receiverState?.runtimeStatus).toEqual(OrchestrationStatus.COMPLETED); + expect(receiverState?.serializedOutput).toEqual(JSON.stringify("hello from sender")); + }); + it("should handle orchestration without activities", async () => { const orchestrator: TOrchestrator = async (_: OrchestrationContext, input: number) => { return input * 2;