Skip to content

fix: round-trip serdes result on first run across ops - #550

Draft
ayushiahjolia wants to merge 1 commit into
mainfrom
serdes-roundtrip-first-run
Draft

fix: round-trip serdes result on first run across ops#550
ayushiahjolia wants to merge 1 commit into
mainfrom
serdes-roundtrip-first-run

Conversation

@ayushiahjolia

@ayushiahjolia ayushiahjolia commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Issue #, if available:
#406
#544

Description of changes:
When a step, child context, or wait_for_condition finishes, we now serialize its result and deserialize it back before saving the SUCCEED checkpoint. The value the function returns on the first run is the deserialized one - exactly what it would return on replay.

Before this, the first run returned the raw in-memory result, but replay returned the value rebuilt from the checkpoint. With a custom serdes that changes the value in transit, those two could differ. Running the round-trip up front makes the first run and replay always agree, and guarantees a SUCCEEDED result is always reconstructable.

Serdes failures are now clearly split:

  • Permanent failures raise SerDesError. The operation fails (no step retry) and user code can catch it.
  • Transient failures raise RetryableSerDesError, which fails the invocation so the backend retries the whole thing (no step retry).

Behavior changes to note

  • For wait_for_condition, the wait strategy now sees the round-tripped value, matching the Java and JS SDKs.
  • A result that wasn't serializable before (for virtual or large-payload child contexts) will now raise SerDesError instead of silently passing through.
  • Custom serdes authors should expect deserialize to be called right after serialize on the first run. For large payloads this means one extra round-trip.
  • Python matches Java's round-trip-before-checkpoint; JS defers deserialize until after SUCCEED for step/child.
  • Serdes failures are now a catchable SerDesError (permanent, no retry) or RetryableSerDesError (transient, backend retry)

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@ayushiahjolia
ayushiahjolia force-pushed the serdes-roundtrip-first-run branch from 6a28b7a to e1293c7 Compare July 17, 2026 23:00
@ayushiahjolia ayushiahjolia changed the title fix: round-trip serdes result on first run across operations fix: round-trip serdes result on first run across ops Jul 17, 2026
@ayushiahjolia
ayushiahjolia marked this pull request as ready for review July 17, 2026 23:09
@ayushiahjolia
ayushiahjolia force-pushed the serdes-roundtrip-first-run branch from 315a90c to cd7a3c4 Compare July 21, 2026 17:37
@ayushiahjolia
ayushiahjolia marked this pull request as draft July 24, 2026 18:57
@ayushiahjolia
ayushiahjolia force-pushed the serdes-roundtrip-first-run branch 5 times, most recently from b4f4ddf to 3d1eef0 Compare July 27, 2026 21:08
@ayushiahjolia
ayushiahjolia marked this pull request as ready for review July 27, 2026 21:17
@ayushiahjolia
ayushiahjolia requested a review from yaythomas July 27, 2026 23:05
@zhongkechen
zhongkechen had a problem deploying to ai-pr-review-runtime July 27, 2026 23:06 — with GitHub Actions Failure
@zhongkechen
zhongkechen had a problem deploying to ai-pr-review-runtime July 27, 2026 23:06 — with GitHub Actions Failure
@ayushiahjolia
ayushiahjolia force-pushed the serdes-roundtrip-first-run branch from 249c4e7 to 6a57868 Compare July 28, 2026 16:56
@ayushiahjolia
ayushiahjolia had a problem deploying to ai-pr-review-runtime July 28, 2026 16:56 — with GitHub Actions Failure
@ayushiahjolia
ayushiahjolia temporarily deployed to ai-pr-review-runtime July 28, 2026 16:56 — with GitHub Actions Inactive
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@ayushiahjolia
ayushiahjolia force-pushed the serdes-roundtrip-first-run branch from 7db4a57 to 782f6db Compare July 28, 2026 21:53
@ayushiahjolia
ayushiahjolia temporarily deployed to ai-pr-review-runtime July 28, 2026 21:53 — with GitHub Actions Inactive
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

# For errors that carry wire fields (DurableOperationError, SerDesError)
# also preserve their data and stack_trace so the inner info survives
# across a single operation boundary.
wire_type: str = _qualified_error_type(exception)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex AI review

[P1] Preserve the existing ErrorType wire contract. This changes every non-builtin error from names such as StepError or MyError to a module-qualified name. Besides breaking consumers of invocation results, existing checkpoints no longer match the new fully-qualified-only reconstruction registry. For example, a previously checkpointed wait-for-callback submitter StepError reconstructs as plain DurableOperationError, so it is no longer translated to CallbackSubmitterError. Keep the existing class-name encoding for ordinary errors, or version the new encoding while accepting both legacy and qualified registry keys, with an upgrade replay test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ships in v2 which is a breaking release. There are no v1 checkpoints to maintain backward compatibility with. The qualified wire format is the v2 contract going forward.

Comment on lines +273 to +279
except RetryableSerDesError:
# Transient serdes failure: fail the invocation for backend retry,
# bypassing the step retry strategy. This narrow catch relies on the
# serdes wrappers raising only RetryableSerDesError or SerDesError;
# any other retryable InvocationError would fall through to the step
# retry strategy below.
raise

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex AI review

[P1] Handle retryable serdes failures for AT_MOST_ONCE_PER_RETRY. Such a step has already persisted START before this rethrow. On the backend retry, check_result_status() treats that checkpoint as an interrupted step and invokes the step retry strategy, potentially exhausting it and permanently failing instead of retrying the serdes at invocation level. Persist a resumable serialized payload before propagating retryable deserialization failures, and explicitly handle retryable serialization failures according to at-most-once semantics. Add coverage for both failure phases with AT_MOST_ONCE_PER_RETRY.

)
# A serdes failure surfaces as SerDesError regardless of the operation
# kind, so it is catchable as itself on both first run and replay.
if self.type == f"{SerDesError.__module__}.{SerDesError.__qualname__}":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex AI review

[P2] Recognize subclasses of SerDesError. The serdes wrappers deliberately propagate subclasses via isinstance, but their checkpointed type is the subclass's qualified name, so this exact string comparison misses it. A custom MySerDesError(SerDesError) is consequently surfaced as StepError/ChildContextError and cannot be caught as SerDesError. Normalize all SerDesError instances to a stable serdes discriminator when creating the ErrorObject, or persist a separate serdes category, and test subclass behavior on first run and replay.

)
new_state = wrapped_user_func(current_state, check_context)

serialized_state = self._serialize(new_state)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex AI review

[P2] Checkpoint the state after the wait strategy runs. Previously serialization followed wait_strategy, so deterministic mutations it made to mutable state were persisted and returned. Capturing serialized_state here discards those mutations for both retry and success; a strategy that advances polling metadata can therefore see the same state forever. Run the strategy on round_tripped_state, then serialize and validate that post-strategy state for the checkpoint and return value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wait strategy doesn't produce or modify state - it only returns a continue/stop decision.

@github-actions

Copy link
Copy Markdown
Contributor

Codex AI review

Four correctness and compatibility issues found. Static review only; repository code was not executed as requested.

Reviewed commit 50f8fcb2f59b79e33ff60a28a0686d9d9ce6765d. Workflow run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants