Skip to content

[Bug]: wait_for_callback errors lose callback_id during reconstruction #564

Description

@zhongkechen

Expected Behavior

Callback errors surfaced by DurableContext.wait_for_callback should retain the callback ID generated for the operation, consistent with the CallbackError.callback_id field and the callback error hierarchy introduced by #563. The value should be available both when the error first surfaces and on replay.

Actual Behavior

CallbackExternalError and CallbackTimeoutError are reconstructed at the failed child-context boundary with callback_id=None. The original nested __cause__ is also lost. CallbackSubmitterError is constructed without a callback ID as well.

The direct Callback.result() error is initially created with the ID, but ChildOperationExecutor serializes it through ErrorObject. DurableOperationError.from_error_fields() then reconstructs the registered callback type using only message, error_type, data, and stack_trace; callback_id defaults to None. wait_for_callback subsequently re-raises that reconstructed exception.

Steps to Reproduce

  1. Define a durable handler that calls context.wait_for_callback(...) and catches CallbackError.
  2. Start the execution and obtain the callback ID from the callback operation.
  3. Complete the callback with SendDurableExecutionCallbackFailure or the local runner equivalent.
  4. Read error.callback_id in the handler.

Expected: the ID sent to the external caller.

Actual: None.

A local durable-runner regression test against PR #563 reproduced:

CallbackExternalError.callback_id == None
CallbackExternalError.__cause__ == None

SDK Version

Unreleased PR #563 (2ac05c8d712bcdac492eee07ddc930f7f9ef2088)

Python Version

3.14

Is this a regression?

No. Before #563, wait_for_callback surfaced a ChildContextError and the callback ID was already unavailable. This issue tracks completing the new callback-error metadata contract rather than restoring previously working behavior.

Additional Context

Relevant paths:

  • ErrorObject.raise_as_operation_error() reconstructs the inner exception.
  • DurableOperationError.from_error_fields() does not pass callback_id to callback subclasses.
  • DurableContext.wait_for_callback() re-raises the reconstructed callback error and constructs CallbackSubmitterError without an ID.

Add a durable-runner test that catches each callback error type across the actual child-context checkpoint/replay boundary; the existing mocked pass-through test does not exercise serialization.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

Status
Done

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions