Skip to content

fix(telemetry): align records with the JS exporter - #803

Closed
ParidelPooya wants to merge 11 commits into
fix/checkpoint-wrapped-errorfrom
fix/telemetry-align-with-js
Closed

ParidelPooya wants to merge 11 commits into
fix/checkpoint-wrapped-errorfrom
fix/telemetry-align-with-js

Conversation

@ParidelPooya

@ParidelPooya ParidelPooya commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The JS Durable Execution Telemetry exporter is the reference record shape: the JS telemetry readers (-telemetry-core and -telemetry-mcp) read every SDK's records. This PR aligns four Python fields with it, and implements the error.name rule that the JS (aws/aws-durable-execution-sdk-js#1052) and Java (aws/aws-durable-execution-sdk-java#814) exporters implement too. Everything else is unchanged: timestamp precision, sampling, ordering, the custom exporter interface, and names.

Stacked on #807 (fix: checkpoint the wrapped error for failed contexts). This PR's base is fix/checkpoint-wrapped-error, and that branch is merged in, so the diff shows only the exporter changes. Merge #807 first; this PR then retargets to main.

All changes are in packages/aws-durable-execution-sdk-python-telemetry-exporter: plugin.py, exporters/lambda_log_exporter.py, the README and the tests.

⚠️ Breaking changes

The package is at 0.0.1. These go under "Breaking Changes" in the next telemetry-exporter release notes. lintcommit.py rejects a ! after the scope, so the commits carry a BREAKING CHANGE: footer instead.

  • error.name values change for every Python record that has an error (execution error, operations[].error, operationsByName.<name>.error). Before: the module-qualified type, or the SDK wrapper (aws_durable_execution_sdk_python.exceptions.StepError). After: the shared rule below, for example PaymentError. A blank or missing type is now Error, not ""/null. error.message is unchanged.
  • Opted-in results are plain JSON values. Results that were {"t", "v"} envelopes are decoded, and override result transforms receive the plain value. A Decimal that a float cannot hold exactly is now a string, and NaN/Infinity are now null.
  • RUNNING records from a suspend or retry carry endTime/durationMs (on-change mode). Do not use the presence of endTime as a completion signal; use status.
  • The default LambdaLogExporter stdout line is the Lambda JSON log envelope (timestamp, level, message). Consumers that json.loads the line and read record fields directly must parse message first.

1. error.name

This implements version 3 of the shared rule (vectors file still version: 2, unchanged). In version 3 the core SDK checkpoints the wrapped error, not the wrapper, which is what #807 does for Python. Every failed operation and the execution therefore carry the user's type, and the exporter only reduces a type string.

The rule, in the same words in all three READMEs:

  1. A record names the error that caused the failure. A wrapper error that the SDK added is never the name, except in the bounded cases of rules 3 and 9.
  2. The SDK unwraps an error only when the error's type is one of the SDK's own wrapper types. A user error is never unwrapped, even if it has a cause.
  3. Unwrapping follows the chain of SDK wrappers. The depth counts unwrap steps, one per wrapper passed, not levels in the operation tree. At most 16 unwraps happen. If the error reached after 16 unwraps is still a wrapper, its name is reported. If the chain reaches an error it has already visited, the walk stops and the last error visited before the repeat is reported.
  4. If the chain ends at a user error, the name is that error's type name.
  5. If the chain ends at an SDK error that is not a wrapper, the name is that SDK error's simple class name. A callback timeout is an example.
  6. A type name that is a qualified string is reduced to its last segment after any . or $. If that segment is empty, the string is kept as it is. A missing type, or a type made only of blank characters, is Error. The blank characters are the six ASCII whitespace characters: space, \t, \n, \r, \f and \v. No other character counts as blank, and names are never trimmed.
  7. A failure gets the same name whether the record is built live or from a replayed checkpoint.
  8. When the execution fails because an operation's error propagated, the execution error.name equals that operation's error.name.
  9. If a checkpoint holds only a wrapper type, which an older SDK can write, the name is that wrapper's name. The exporter never guesses a cause from other operations.

A callback that is failed externally with an explicit ErrorType is named after that type, after the reduction of rule 6. This includes the type "Error", which reports Error. Only a missing or blank type reports the SDK callback-failure class.

How Python applies it:

  • Core (fix: checkpoint the wrapped error for failed contexts #807) does rules 2 to 5 when it checkpoints: ErrorObject.from_exception unwraps StepError, ChildContextError, InvokeError, WaitForConditionError, CallbackSubmitterError, CallbackExternalError and DurableOperationError (exact class, at most 16 steps, cycle-safe). CallbackTimeoutError, SerDesError, ValidationError, BatchCompletionError and user subclasses are not unwrapped. A callback failed externally keeps the sender's type, so a missing type and "Error" stay different.
  • Operation name = _simple_type_name(operation.error.type), with two callback cases: a TIMED_OUT callback operation is CallbackTimeoutError (the service type Callback.Timeout would reduce to Timeout), and a callback failed externally with a missing or blank type is CallbackExternalError.
  • Execution name = _simple_type_name(execution error type). With fix: checkpoint the wrapped error for failed contexts #807 that is the user's type. A wrapper type from an older core reduces to the wrapper's name (StepError, ChildContextError, ...), per rule 9.
  • No matching by message, data, stack trace, timestamp, operation kind or tree position.

Smaller design

Removed from plugin.py: _ErrorNameIndex (children-by-parent and error-key indexes, subtree links, BFS with the 16-unwrap budget, cycle handling and caching, outermost-candidate search), the wrapper-to-operation-kind filter table, the default-message equivalences (Callback failed, Callback timed out, Callback submitter failed) and the ambiguity unions. What is left is about 60 lines: the reducer, the callback special cases and two one-line name functions. Tests that only covered the removed heuristics (subtree scoping, kind filters, ambiguity, nested-map unwrap counting, tree cycles, default-message matching, the linear-cost and 2000-item timing checks) are gone.

git diff --stat fix/checkpoint-wrapped-error...fix/telemetry-align-with-js:

files insertions deletions
before (head 45582afb) 9 2515 39
after 9 2031 38

plugin.py goes from +452 to +262 lines against main; tests/test_error_name.py from 728 to 400.

Table A (shared vectors)

tests/fixtures/error-name-vectors.json (version 2, unchanged, byte-identical in the three repositories). tests/test_error_name.py runs every vector through the reducer and through a full record (execution, operations[], operationsByName). The vectors changed by this PR:

# Input Before After
A7 trailing. "" trailing.
A8 Outer$ "" Outer$
A9 . / $ "" unchanged
A11 " PaymentError " unchanged unchanged (no trim)
A12 "", " ", None "" / "" / None Error
A13 the six ASCII whitespace characters Error Error
A14 U+00A0 Error (str.strip()) unchanged
A15 U+FEFF unchanged unchanged
A16 U+001C Error (str.strip()) unchanged
A17 U+2003 Error (str.strip()) unchanged

Table B (local runner, live and replayed, on this stacked branch)

tests/e2e/error_name_int_test.py runs every row through DurableFunctionTestRunner twice. The first run is live. In the second, the handler catches the error, waits so that a new invocation starts, and rethrows it. Each row asserts the names below, that error.message is the original message, that the live and replayed names are equal, and that operationsByName matches operations[]. All rows pass in both modes (expected = actual).

# Scenario Operation error.name Execution Live Replayed
B1 step throws PaymentError charge: PaymentError PaymentError pass pass
B2 step fails with TypeError, then on retry PaymentError charge: PaymentError PaymentError pass pass
B3 child context throws checkout: PaymentError PaymentError pass pass
B4 child context > step throws checkout, charge: PaymentError PaymentError pass pass
B5 map item throws map-item-0: PaymentError PaymentError pass pass
B6 map item's step throws map-item-0, charge: PaymentError PaymentError pass pass
B7 parallel branch throws parallel-branch-1: PaymentError PaymentError pass pass
B8 wait_for_condition check throws poll: PaymentError PaymentError pass pass
B9 invoked durable function throws pay: PaymentError PaymentError pass pass
B10 wait_for_callback submitter throws approval, approval submitter: PaymentError PaymentError pass pass
B11 callback times out approval, approval create callback id: CallbackTimeoutError CallbackTimeoutError pass pass
B12 callback failed externally, type com.acme.PaymentError approval, approval create callback id: PaymentError PaymentError pass pass
B13 callback failed externally, no type approval, approval create callback id: CallbackExternalError CallbackExternalError pass pass
B13b callback failed externally, type Error approval, approval create callback id: Error Error pass pass
B14 handler throws PaymentError with cause TypeError none PaymentError pass pass
B15 (Python form) handler throws a user class that subclasses StepError none WrappedPaymentError pass pass
B16 handled ValueError("x") in a step, then a child fails with ValidationError("x") lookup: ValueError; work: ValidationError ValidationError pass pass
B17 step result cannot be serialized charge: SerDesError SerDesError pass pass
B18 class named Payment$Error thrown from a step charge: Error Error pass pass
B19 N/A: JS only (promise.all) n/a n/a
B20 2 map items, same message: item 0 PaymentError, item 1 FraudError map-item-0: PaymentError; map-item-1: FraudError PaymentError (raised by throw_if_error) pass pass
B21 2 parallel branches, same message: PaymentError, FraudError parallel-branch-0: PaymentError; parallel-branch-1: FraudError PaymentError (raised by throw_if_error) pass pass

B20 and B21 run with max_concurrency=1 and CompletionConfig.all_completed(), so both items fail and item 0's error is the one throw_if_error() raises.

Extra rows in the same test:

Row Scenario Operation error.name Execution
B10, empty message submitter throws PaymentError() approval, approval submitter: PaymentError (message "") PaymentError (message Callback submitter failed)
B12, empty message callback failed externally, type com.acme.PaymentError, message "" approval: PaymentError (message Callback failed); approval create callback id: PaymentError (message "") PaymentError
handled in subtree in child outer, child inner catches a step's ValueError; then step charge throws PaymentError outer, charge: PaymentError; lookup: ValueError PaymentError
depth 18 a step throws PaymentError inside 18 nested child contexts every context and the step: PaymentError PaymentError
map in a map in a child child and both items: PaymentError PaymentError

The tree depth no longer limits the name: each context records the user's type, so the 17- and 18-level rows that reported StepError / ChildContextError in version 2 now give PaymentError everywhere. The 16-unwrap bound and cycle handling live in the core (#807 unit tests). The slim exporter depends on #807: run against the main core, 21 of the 26 rows fail (the contexts and the execution report the wrapper, as rule 9 allows for old checkpoints). Only B11, B13, B14, B15 and B17 pass there.

2. Opted-in operation result

before after
operations[charge].result {"t":"m","v":{"amount":{"t":"f","v":99.99}}} {"amount":99.99}

This applies to operations[].result, operationsByName.<n>.result, and the value passed to an override's result transform. An ExtendedTypeSerDes envelope (root is exactly {"t", "v"}, with t a string) is decoded with EXTENDED_TYPES_SERDES and converted to JSON values:

  • Decimal keeps its numeric value, not its scale or trailing zeros (1.10 becomes 1.1, 1E+2 becomes 100). An integral value of at most 300 digits (adjusted() < 300) is an integer; a larger one takes the path below, so 1E+5000 and 1E+1000000 become the strings "1E+5000" and "1E+1000000" in milliseconds. Converting those with int() took tens of seconds, and the record was then dropped because json refuses integers over 4300 digits. Any other value is a float only when Decimal(repr(float(v))) == v, and otherwise str(v). So 12345678901234567.89 becomes "12345678901234567.89", and 1E-400 stays "1E-400" instead of 0.0.
  • Non-finite Decimal or float values become null, in decoded and plain payloads alike, so the line is strict JSON.
  • datetime/date become isoformat() strings, UUID a string, bytes base64, tuple a list, and BatchResult its to_dict() form.

A payload that is not an envelope stays parsed JSON, or the raw string when it is not JSON. An envelope that fails to decode stays parsed JSON. Decoding never raises. The conversion uses an explicit stack, not recursion, so a deeply nested result (for example 600 nested arrays on Python 3.11) is converted. If converting one result fails anyway, that result falls back to its raw string and the rest of the record is still emitted.

Known collision (documented in the README). The hooks do not say which serializer wrote a result, so an envelope is recognised by shape alone. A JsonSerDes, custom-SerDes or chained-invoke result that is exactly {"t": <default tag>, "v": ...} is decoded as if the default serializer wrote it. For example {"t":"s","v":"x"} becomes "x". An unknown tag passes through unchanged. Fixing this needs the serializer on OperationInfo in the core SDK.

3. endTime / durationMs on non-terminal records

JS onInvocationEnd passes endTime: new Date() for every status (index.ts). Python now does the same: every record built at invocation end carries endTime and durationMs, including the RUNNING record of a suspend or retry. Records built at invocation start or on an operation change have neither. Output and error are only set on terminal records.

4. LambdaLogExporter line

{"timestamp":"2026-01-01T00:00:00.000Z","level":"INFO","message":"{\"recordType\":\"DurableExecutionTelemetry\",…}"}

In the JSON log format, the Node.js runtime wraps console.log output in this envelope. The Python runtime captures print as plain text (Python logging) and does not double-encode JSON (log format), so the exporter writes the envelope itself:

  • timestamp is UTC with milliseconds and Z.
  • level is INFO.
  • message is the compact record JSON.
  • requestId is omitted, because exporters do not receive it.

The size limit is still measured on the record (message). The printed line is larger: about 20% for a typical record, and up to about 2x in the worst case, because escaping doubles each " and \. CloudWatch Logs accepts events of up to 1 MB (PutLogEvents), so a line for a record at the default 256 KB limit fits. The README and the docstring say so.

Testing

Mirrors .github/workflows/ci.yml locally (Python 3.14, macOS; CI runs 3.11 to 3.14), on the stacked branch:

Command Result
hatch fmt --check (repository root, 750 files) pass
hatch run types:check pass
hatch run test:cov (full) 5538 passed, 2 skipped. All new and changed lines in plugin.py are covered; the uncovered plugin.py lines predate this PR
hatch build in the core and telemetry-exporter packages pass
python .github/scripts/lintcommit.py --range origin/main..HEAD PASS

New or changed tests:

  • tests/fixtures/error-name-vectors.json: the shared Table A vectors, version 2.
  • tests/test_error_name.py: Table A against the reducer and full records; the core records the wrapped type for every wrapper class; each operation named by its own type (B16); B20/B21 snapshots; a user subclass of a wrapper (B15); old checkpoints holding a wrapper type report the wrapper and never a child's type (rule 9); callback timeout, external failure with a type, no type, "Error", blank types and a non-ASCII space; include_errors=False.
  • tests/e2e/error_name_int_test.py: Table B (B19 not applicable) and the extra rows, live and replayed, with the local runner.
  • tests/test_plugin.py: exact Decimal values, NaN/Infinity to null, 600 nested arrays, a 20000-level conversion without recursion, dict order and duplicate keys, and a normalization failure isolated to one result.

The JS exporter is the reference shape that the record readers use for
every SDK. Align four Python record fields with it:

- error.name is the simple type name of the user's error (for example
  PaymentError), not the module-qualified type or the SDK wrapper. An
  SDK wrapper resolves to the failed operation's user error; with no
  user error the SDK error's simple class name is used.
- An opted-in operation result is the plain JSON value. Results that
  the default serializer wrote as a {"t", "v"} envelope are decoded
  with it and converted to JSON values. Other payloads are unchanged,
  and a decode failure falls back to the previous value.
- Every record built at invocation end carries endTime and durationMs,
  including RUNNING records for a suspend or retry, as in JS.
- LambdaLogExporter writes the Lambda JSON log envelope (timestamp,
  level, message) itself, because Lambda does not wrap Python print
  output. The size limit still applies to the record.
@ParidelPooya
ParidelPooya deployed to ai-pr-review-runtime October 21, 2026 23:09 — with GitHub Actions Active
@ParidelPooya
ParidelPooya deployed to ai-pr-review-runtime October 21, 2026 23:09 — with GitHub Actions Active
Comment on lines +209 to +213
if not _is_extended_type_envelope(parsed):
return parsed
try:
decoded = EXTENDED_TYPES_SERDES.deserialize(raw_result, SerDesContext())
return _to_json_compatible(decoded)

This comment was marked as outdated.

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.

/ai verdict out-of-scope
The hooks do not say which serializer wrote a result, so telling a JsonSerDes payload from an ExtendedTypeSerDes envelope needs serializer provenance from the core; the shape-based decoding stays as documented in _decode_checkpoint_result.

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.

Recorded out-of-scope for AI finding arf_v1_xhn5b5nmzbwy2pb5r35uf3l3zq.

Comment on lines +292 to +296
for op in operations.values()
if (op_error := op.error) is not None
and op_error.type
and not op_error.type.startswith(_SDK_TYPE_PREFIX)
and (op_error.message, op_error.data, op_error.stack_trace) == key

This comment was marked as outdated.

@github-actions

This comment has been minimized.

Implement the error.name rule shared with the JS and Java exporters,
and fix two problems in the previous cause lookup.

- A qualified type is cut to its last segment after any "." or "$".
  An empty segment keeps the string ("trailing.", "Outer$"), and a
  blank or missing type is "Error". Whitespace is not trimmed.
- An SDK wrapper is resolved only from the failing operation's own
  subtree. For the execution error, that is the failed operation of
  the wrapper's kind with the same error fields. Before, any failed
  operation in the execution with the same message could be the
  cause, including a failure that user code had already handled.
- Non-wrapper SDK errors (ValidationError, SerDesError, ...) are
  candidates. More than one distinct candidate type means no guess.
- The chain walk is limited to 16 levels and stops on a cycle.
- CallbackExternalError is a wrapper: an external failure with a
  type is named by that type, and one without a type is
  CallbackExternalError. A timed-out callback operation is
  CallbackTimeoutError, as on its context and the execution.
- The lookup index is built once per record, so a record build is
  linear in the number of operations. Before, each failed operation
  scanned all operations: 2000 failed map items took 800 ms, and now
  take 15 ms.

Tests: the shared Table A vectors are copied unchanged to
tests/fixtures/error-name-vectors.json and drive a parametrized test.
Every Table B row runs through the local runner, live and replayed.

BREAKING CHANGE: error.name values change for records with errors, and
a blank or missing type is now reported as "Error".
Opted-in results decoded from the default serializer lost Decimal
precision and sign, and non-finite floats made the record invalid
JSON.

- An integral Decimal is a JSON integer. Any other finite Decimal is
  a JSON number only when that float converts back to the same
  Decimal, and otherwise its exact string, str(value).
- NaN and Infinity, as Decimal or float, become null in decoded and
  in plain JSON payloads, so the emitted record is strict JSON.
- The README and docstring describe the envelope collision: the hooks
  do not carry the serializer, so a {"t", "v"} object written by
  another SerDes with a default type tag is decoded as an envelope.
  The README no longer says that custom SerDes payloads always pass
  through.

BREAKING CHANGE: a Decimal result that a float cannot hold exactly is
now a string, and NaN or Infinity results are now null.
The LambdaLogExporter size limit is measured on the record, and the
envelope and JSON escaping of message add bytes on top of it. State
how much: about 20% for a typical record and up to about twice the
record in the worst case, since escaping doubles each quote and
backslash. CloudWatch Logs accepts events of up to 1 MB
(PutLogEvents), so a line for a record at the default 256 KB limit
fits.
Comment on lines +228 to +229
decoded = EXTENDED_TYPES_SERDES.deserialize(raw_result, SerDesContext())
return _to_json_compatible(decoded)

This comment was marked as outdated.

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.

/ai verdict out-of-scope
The hooks do not say which serializer wrote a result, so telling a JsonSerDes payload from an ExtendedTypeSerDes envelope needs serializer provenance from the core; the shape-based decoding stays as documented in _decode_checkpoint_result.

Comment on lines +255 to +258
if value == value.to_integral_value() and not (
value.is_zero() and value.is_signed()
):
return int(value)

This comment was marked as outdated.

Comment on lines +469 to +473
for op in self._by_key.get(_error_key(error), ()):
if allowed is None or op.operation_type in allowed:
causes |= self._subtree_causes(op)
if len(causes) == 1:
return causes.pop()

This comment was marked as outdated.

Comment on lines +381 to +385
if op.operation_type == OperationType.CALLBACK and not message:
# The SDK fills in a default message when the callback error has none.
is_timeout = op.status == OperationStatus.TIMED_OUT
message = "Callback timed out" if is_timeout else "Callback failed"
return (message, data, stack)

This comment was marked as outdated.

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.

Removed in 459216c. The core now checkpoints the wrapped error (stacked base #807), so the exporter names each operation and the execution by its own ErrorType and no longer matches by message, kind or position in the tree.

@github-actions

This comment has been minimized.

An integral Decimal with a large exponent, such as Decimal("1E+1000000"),
was converted with int(). That took tens of seconds on the handler thread,
and json then refused to write the integer, so the whole record was lost.

An integral Decimal now becomes a JSON integer only when it has at most 300
digits (adjusted() < 300). A larger one takes the float-or-string path and is
reported as its exact string, for example "1E+5000". The README now says that
a Decimal keeps its numeric value but not its scale.
Rule 3: the depth now counts wrapper hand-offs, not tree levels. One
unwrap is a failed direct child with the same error fields, or a failed
item or branch under a direct map or parallel child. At most 16 unwraps
happen; the error reached then is reported. A cycle reports the last
error visited before the repeat. The execution takes the name of the
outermost operation it propagated from, so rule 8 holds at the bound.

Children that do not carry the wrapper's fields are no longer searched,
so a failure that user code handled inside the failing subtree no
longer makes the name vague.

Rule 6: blank means only the six ASCII whitespace characters. U+00A0,
U+FEFF, U+001C and U+2003 are kept (vectors A13-A17). The vector file
is replaced with version 2.

A wait_for_callback submitter that fails with an empty message is
re-raised by the core as "Callback submitter failed"; the execution now
matches it to the submitter's context. A callback failed externally
with the type "Error" is named Error (B13b), now covered by tests.
Adds B13b, B20 and B21, the 16/17/18 nested child context bound, a map
in a map in a child context, and a failure handled inside the failing
subtree. A callback failed externally with an empty message and a
submitter that fails with an empty message exercise the core's real
default messages, which the plugin has to match.

The harness now waits for a terminal-status record and asserts on the
last one, instead of reading the last record after the first arrives.
A handled failure that is a direct child of the failing operation, with
the same message and data, cannot be told apart from the cause. The
README now says the name is then the wrapper's, and a test pins it.
@ParidelPooya
ParidelPooya deployed to ai-pr-review-runtime October 22, 2026 05:49 — with GitHub Actions Active
Comment on lines +224 to +227
if not _is_extended_type_envelope(parsed):
# Plain JSON already; only non-finite numbers (json.loads accepts NaN
# and Infinity) need mapping to null.
return _to_json_compatible(parsed)

This comment was marked as outdated.

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.

Fixed in 459216c: _to_json_compatible now converts containers with an explicit stack (no recursion), and a normalization failure falls back to that result's raw string, so the rest of the record is still emitted. Tests: test_result_deeply_nested_plain_json_is_converted, test_json_conversion_does_not_recurse, test_result_normalization_failure_is_isolated_to_that_result.

Comment on lines +228 to +230
try:
return transform(parsed)
except Exception: # noqa: BLE001 - untrusted transform must never break emission
decoded = EXTENDED_TYPES_SERDES.deserialize(raw_result, SerDesContext())
return _to_json_compatible(decoded)

This comment was marked as outdated.

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.

/ai verdict out-of-scope
The hooks do not say which serializer wrote a result, so telling a JsonSerDes payload from an ExtendedTypeSerDes envelope needs serializer provenance from the core; the shape-based decoding stays as documented in _decode_checkpoint_result.

Comment on lines +553 to +559
if (
error_type == _CALLBACK_SUBMITTER_TYPE
and message == _CALLBACK_SUBMITTER_MESSAGE
):
# The submitter step failed with no message; the core re-raised it
# with its default message, and the context keeps the empty one.
keys += [("", data, stack), (None, data, stack)]

This comment was marked as outdated.

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.

Removed in 459216c. The core now checkpoints the wrapped error (stacked base #807), so the exporter names each operation and the execution by its own ErrorType and no longer matches by message, kind or position in the tree.

Comment on lines +473 to +475
for child in self._children.get(op.operation_id, ()):
if self._keys[child.operation_id] == key:
links.append(child)

This comment was marked as outdated.

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.

Removed in 459216c. The core now checkpoints the wrapped error (stacked base #807), so the exporter names each operation and the execution by its own ErrorType and no longer matches by message, kind or position in the tree.

@github-actions

This comment has been minimized.

The core now checkpoints the error a wrapper wraps, so every failed
operation and the execution carry the user's type. Name an operation
by its own ErrorType, reduced (Table A), with the callback cases:
TIMED_OUT is CallbackTimeoutError and an external failure with a
missing or blank type is CallbackExternalError. Name the execution by
its own ErrorType, reduced; a wrapper type from an older core reports
the wrapper's name (rule 9).

Remove the cause matching: the error-name index, the subtree walk,
the per-kind link filters, the default-message equivalences and the
ambiguity unions. B20 and B21 now name the execution after the error
that throw_if_error raised.

Convert decoded results to plain JSON with an explicit stack, so a
deeply nested result does not hit the recursion limit, and fall back
to the raw string when one result fails to normalize, so the rest of
the record is still emitted.
@ParidelPooya
ParidelPooya changed the base branch from main to fix/checkpoint-wrapped-error October 22, 2026 16:41
@ParidelPooya
ParidelPooya deployed to ai-pr-review-runtime October 22, 2026 16:43 — with GitHub Actions Active
Comment on lines +296 to +300
value.adjusted() < _MAX_INT_DIGITS
and value == value.to_integral_value()
and not (value.is_zero() and value.is_signed())
):
return int(value)

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 · Finding arf_v1_hovewbithq67gm6ebbmi5dp6ix

[P2] Preserve DynamoDB compatibility when converting Decimals

With an identity result override, a step returning Decimal("9" * 39) now produces a 39-digit integer. DynamoDBExporter._marshal() passes that integer to boto3's TypeSerializer, which rejects numbers exceeding its 38-digit precision; the scheduler consequently drops the entire record. Previously, the decimal was a string inside its serializer envelope and exported successfully. Convert numbers outside DynamoDB's supported precision/range to exact strings during marshalling, or preserve them as strings here, and add a plugin-to-DynamoDB regression test.

To suppress repeats, reply /ai verdict deferred, /ai verdict false-positive, or /ai verdict out-of-scope on the first nonblank line. Add rationale on later lines. A new /ai verdict accepted allows future repeats again.

@github-actions

Copy link
Copy Markdown
Contributor

Codex AI review

Two P2 findings in packages/aws-durable-execution-sdk-python-telemetry-exporter/src/aws_durable_execution_sdk_python_telemetry_exporter/plugin.py:296 and packages/aws-durable-execution-sdk-python-telemetry-exporter/src/aws_durable_execution_sdk_python_telemetry_exporter/plugin.py:223: DynamoDB record loss and the previously reported serializer collision. Tests were inspected but not executed, per review constraints.

Suppressed repeats

  • arf_v1_xhn5b5nmzbwy2pb5r35uf3l3zq — out-of-scope (maintainer verdict)

Reviewed commit aba6d76cb7962a4c708f1678c0a6cd3aa1179ff8. Workflow run

@ParidelPooya

Copy link
Copy Markdown
Contributor Author

Closing in favour of a simpler approach. We no longer try to make error.name identical across SDKs, and the core SDKs stay unchanged. Each exporter will instead write error and its cause in one shared shape, with each language keeping its own type names; the READMEs will document the differences. A new, smaller exporter-only PR replaces this one. The branch is kept, so this can be reopened if needed.

This branch was successfully deployed

1 active deployment
ai-pr-review-runtime — aba6d76c Deployed Oct 22, 2026 by ParidelPooya via ai-pr-review / Codex review / Generate Codex review #2033
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.

1 participant