@aws/durable-execution-sdk-js-otel 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +338 -12
  2. package/dist/execution-plugin.mjs +227 -50
  3. package/dist/execution-plugin.mjs.map +1 -1
  4. package/dist/index.mjs +1 -1
  5. package/dist/invocation-plugin.mjs +209 -105
  6. package/dist/invocation-plugin.mjs.map +1 -1
  7. package/dist/otel-execution.mjs +1 -1
  8. package/dist/otel-execution.mjs.map +1 -1
  9. package/dist/otel-invocation.mjs +2 -2
  10. package/dist/otel-invocation.mjs.map +1 -1
  11. package/dist/{global-id-generator.mjs → plugin-registration.mjs} +147 -4
  12. package/dist/plugin-registration.mjs.map +1 -0
  13. package/dist-cjs/execution-plugin.js +240 -63
  14. package/dist-cjs/execution-plugin.js.map +1 -1
  15. package/dist-cjs/index.js +10 -10
  16. package/dist-cjs/invocation-plugin.js +221 -117
  17. package/dist-cjs/invocation-plugin.js.map +1 -1
  18. package/dist-cjs/otel-execution.js +1 -1
  19. package/dist-cjs/otel-execution.js.map +1 -1
  20. package/dist-cjs/otel-invocation.js +2 -2
  21. package/dist-cjs/otel-invocation.js.map +1 -1
  22. package/dist-cjs/{global-id-generator.js → plugin-registration.js} +152 -2
  23. package/dist-cjs/plugin-registration.js.map +1 -0
  24. package/dist-types/execution-plugin.d.ts +25 -4
  25. package/dist-types/execution-plugin.d.ts.map +1 -1
  26. package/dist-types/execution-trace-context.d.ts +4 -2
  27. package/dist-types/execution-trace-context.d.ts.map +1 -1
  28. package/dist-types/external-completions.d.ts +11 -0
  29. package/dist-types/external-completions.d.ts.map +1 -0
  30. package/dist-types/global-sampler.d.ts +2 -0
  31. package/dist-types/global-sampler.d.ts.map +1 -1
  32. package/dist-types/invocation-clock.d.ts +9 -0
  33. package/dist-types/invocation-clock.d.ts.map +1 -0
  34. package/dist-types/invocation-plugin.d.ts +18 -2
  35. package/dist-types/invocation-plugin.d.ts.map +1 -1
  36. package/dist-types/operation-span-status.d.ts +5 -0
  37. package/dist-types/operation-span-status.d.ts.map +1 -0
  38. package/dist-types/otel-plugin-config.d.ts +8 -0
  39. package/dist-types/otel-plugin-config.d.ts.map +1 -1
  40. package/dist-types/otel-plugin-provider.d.ts +7 -0
  41. package/dist-types/otel-plugin-provider.d.ts.map +1 -1
  42. package/dist-types/plugin-registration.d.ts +2 -0
  43. package/dist-types/plugin-registration.d.ts.map +1 -0
  44. package/dist-types/propagation-metadata.d.ts +12 -0
  45. package/dist-types/propagation-metadata.d.ts.map +1 -0
  46. package/package.json +13 -9
  47. package/dist/global-id-generator.mjs.map +0 -1
  48. package/dist-cjs/global-id-generator.js.map +0 -1
package/README.md CHANGED
@@ -153,7 +153,25 @@ tracer from it. Register it globally when the application also needs its
153
153
  context manager, propagator, or library instrumentation.
154
154
 
155
155
  At the end of each enabled invocation, both plugins call `forceFlush()` when
156
- the provider exposes it. They never call `shutdown()`.
156
+ the provider exposes it. For the default global API provider, they also check
157
+ its public delegate, so an SDK `BatchSpanProcessor` is flushed when the API proxy
158
+ does not expose `forceFlush()` itself. Explicit factory providers are used as
159
+ returned and are not unwrapped. The plugins never call `shutdown()`.
160
+
161
+ Both views use one `flushTimeoutMillis` budget across the invocation-end
162
+ drain/flush rounds (default **1000 ms**, positive and at most **2147483647 ms**,
163
+ Node.js's maximum timer delay). This bounds
164
+ asynchronous provider waiting on both the global and explicit-provider paths.
165
+ Choose a value that leaves room in the Lambda invocation timeout and configure
166
+ the exporter's own transport timeouts too. The plugin has no remaining-runtime
167
+ deadline in its invocation metadata and cannot guarantee that this budget fits
168
+ the time left after the handler.
169
+
170
+ On expiry, the plugin drains one bounded snapshot of terminal notifications
171
+ already received, then returns without starting another provider flush. Export
172
+ delivery is best effort: spans may remain queued. Timing out does not cancel an
173
+ export, shut down the provider, or preempt synchronously blocking provider or
174
+ processor code.
157
175
 
158
176
  ## Dynamic Loading from a Lambda Layer
159
177
 
@@ -183,6 +201,13 @@ needed.
183
201
 
184
202
  ## Choosing a Plugin
185
203
 
204
+ Configure exactly one durable OTel view: `ExecutionOtelPlugin` for a workflow
205
+ view, or `InvocationOtelPlugin` for an invocation view. Registering both (or
206
+ registering one view twice) raises `PluginLoadError` before invocation hooks
207
+ run. This applies to explicit `plugins`, `DURABLE_EXECUTION_PLUGINS`, and any
208
+ combination of the two. Other instrumentation plugins can run alongside the
209
+ selected view.
210
+
186
211
  ### `ExecutionOtelPlugin`
187
212
 
188
213
  Use this plugin for a workflow-centered view. Operations and attempts are
@@ -238,6 +263,33 @@ Open operation spans are ended at the invocation boundary and retain
238
263
  `durable.operation.status=STARTED`. When an operation completes in a later
239
264
  invocation, the plugin emits a continuation span in that invocation.
240
265
 
266
+ Live invocation-view span boundaries and exception events use a wall-clock
267
+ origin captured with `Date.now()` at invocation start, advanced by monotonic
268
+ elapsed time. This matches the clock model of ordinary `tracer.startSpan()`
269
+ spans instead of using the process-wide `performance.timeOrigin`. Absolute
270
+ `HrTime` tuples keep epoch milliseconds distinct from relative performance time.
271
+ Each resumed invocation captures its own anchor. Supplied execution-start
272
+ Dates used to backdate Workflow and synthetic roots are preserved; execution-view
273
+ provided timestamps are retained without adding cross-clock boundary compensation.
274
+
275
+ The wall and monotonic reads are not atomic. Sampling delay, millisecond wall
276
+ precision, later wall-clock adjustments, and clocks on different machines can
277
+ still disagree. The plugin does not rewrite backend Dates or user spans, retain
278
+ a clock high-water mark across invocations, or promise simultaneous strict
279
+ ordering and complete containment across arbitrary clock skew.
280
+
281
+ Independently of the invocation clock origin, execution-view operation and attempt
282
+ exception events use the supplied backend end timestamp when one is available,
283
+ just as their span end does. Recording the event at local receipt time can place
284
+ it after the backend-ended span. Missing end timestamps retain the normal tracer
285
+ default; this does not add rounding or cross-clock ordering policies.
286
+
287
+ When an execution-view completion has no recorded start timestamp, an available
288
+ backend end timestamp supplies a zero-duration fallback. This keeps its exception
289
+ at that known completion time instead of before a later local default start.
290
+ Recorded starts and missing-end defaults retain their existing behavior; this
291
+ does not reconcile a captured local start with an earlier backend end.
292
+
241
293
  Because the original span context is not checkpointed, replayed `STEP` and
242
294
  `CONTEXT` spans and cross-invocation continuation spans use new provider IDs.
243
295
  They correlate through two links: the real exported `Workflow` span, and the
@@ -298,6 +350,66 @@ The `Invocation` span may still nest under a same-trace ambient handler span —
298
350
  see [Invocation parent](#invocation-parent) — without changing the execution
299
351
  ancestor the `Workflow` span joins.
300
352
 
353
+ ### Synthetic root export lifecycle
354
+
355
+ When fallback tracing is sampled, the backend supplies `executionStartTimestamp`,
356
+ and the configured SDK tracer allows the durable sampler wrapper to be installed,
357
+ both views create and end `DurableExecutionRoot` during each invocation's start
358
+ hook. Its start and end are both that backend timestamp. The normal
359
+ invocation-boundary flush can export it before a `PENDING` or `RETRYING` invocation
360
+ returns. A retry with checkpoint history or an intermediate resume can therefore
361
+ recover an earlier missing export, including when tracing became available only
362
+ after the first invocation. Recovery requires a later sampled invocation to
363
+ actually deliver its copy; stopping or timing out before any successful delivery
364
+ can still leave the execution without an exported root. This policy does not rely
365
+ on `isFirstInvocation`: a retry after checkpointed work runs in replay mode, while
366
+ an attempt without that history can still be identified as the first invocation.
367
+
368
+ Each sampled invocation emits at most one anchor: a terminal invocation does not
369
+ emit a second copy if its start hook already created one. Additional copies on
370
+ resumes are intentional, allowing consumers to deduplicate by trace ID and span ID.
371
+ The SDK-owned span fields are identical when the execution's trace identity,
372
+ backend start timestamp and instrumentation configuration remain stable. The
373
+ anchor carries `durable.execution.arn`, but no terminal status, request ID or
374
+ execution duration; `Workflow` continues to carry the duration and outcome.
375
+ `DurableExecutionRoot` is a zero-duration identity span: its descendants end after
376
+ it, which some trace viewers or containment validators may flag.
377
+
378
+ A forwarding tracer that hides the configured sampler retains the existing
379
+ terminal-only root creation path. Creating an early root through that tracer would
380
+ query the hidden sampler independently and could sample a root after dropping its
381
+ Invocation child. The plugin does not add that early sampler call; the underlying
382
+ provider still controls sampling, and these tracers do not receive early-anchor
383
+ recovery. A terminal backup can recover a missing first export only if a sampled
384
+ terminal invocation actually runs and delivers it.
385
+
386
+ Resources belong to the configured provider. Resource attributes such as
387
+ `faas.instance` can differ across execution environments even for identical
388
+ anchor span content; backends may retain whichever resource accompanies their
389
+ chosen copy. Span processors that enrich these spans must likewise avoid
390
+ invocation-varying span fields when relying on identity-based deduplication.
391
+ Export delivery and flushing remain the provider/layer's responsibility. The
392
+ plugin awaits an available provider or global-delegate `forceFlush()`, but a killed invocation
393
+ or a failed export cannot guarantee delivery; this is not exactly-once export.
394
+
395
+ If a caller omits `executionStartTimestamp`, the plugin retains terminal-only
396
+ synthetic-root materialization with its existing timestamp fallback. It does not
397
+ invent a wall-clock-based early anchor. These callers do not get the early-anchor
398
+ or identical-timestamp guarantee; the backend timestamp must be consistently
399
+ supplied to enable that contract. Complete propagated remote parents are never
400
+ materialized by the SDK. With the durable sampler wrapper installed, explicit
401
+ `NOT_SAMPLED` executions emit no root.
402
+
403
+ With the durable sampler wrapper installed, sampling is resolved once per
404
+ invocation and the anchor reuses that decision without a second sampler query.
405
+ Explicit upstream decisions or a deterministic trace-based policy (for example a
406
+ ratio sampler) keep anchor and terminal `Workflow` decisions consistent across
407
+ resumes. A non-deterministic custom sampler can select different invocations,
408
+ leaving an anchor without a sampled terminal `Workflow`. Decisions are not
409
+ persisted. These once-per-invocation guarantees do not apply to an opaque tracer:
410
+ its provider can query its sampler independently for each span, including the
411
+ existing terminal root; the early-anchor path is skipped to preserve that baseline.
412
+
301
413
  ### Workflow identity and lifecycle
302
414
 
303
415
  `Workflow` is an `INTERNAL` span that **joins the execution trace** by parenting
@@ -317,7 +429,7 @@ The plugins carry the same `Workflow` span identity on each invocation as a
317
429
  non-recording span context, and create the single recording `Workflow` span only
318
430
  when the execution reaches `SUCCEEDED` or `FAILED`, backdated to the execution
319
431
  start. `PENDING` and `RETRYING` invocations create no recording `Workflow` span,
320
- so none is ever left unended, and exactly one `Workflow` root is exported for the
432
+ so none is ever left unended, and one terminal `Workflow` span is exported for the
321
433
  durable execution while intermediate invocation spans export normally. The
322
434
  non-recording context carries the execution's sampling decision, so operations
323
435
  under it are sampled consistently with the eventual root. In-flight attempt spans
@@ -415,6 +527,9 @@ interface OtelPluginConfig {
415
527
 
416
528
  /** Add active OTel IDs to durable log records. Defaults to true. */
417
529
  enrichLogger?: boolean;
530
+
531
+ /** Total invocation-end provider wait budget; defaults to 1000 ms. */
532
+ flushTimeoutMillis?: number;
418
533
  }
419
534
  ```
420
535
 
@@ -455,16 +570,28 @@ environment values are available.
455
570
 
456
571
  OpenTelemetry status mapping is:
457
572
 
458
- | Span | Durable result | OTel status |
459
- | ---------- | ------------------------------------------------------------------ | ------------------------ |
460
- | Workflow | `SUCCEEDED` / `FAILED` | `OK` / `ERROR` |
461
- | Invocation | `SUCCEEDED` or `PENDING` / `FAILED` / `RETRYING` | `OK` / `ERROR` / `UNSET` |
462
- | Operation | `SUCCEEDED` / error object / other failure without an error object | `OK` / `ERROR` / `UNSET` |
463
- | Attempt | `FAILED` / any other outcome | `ERROR` / `OK` |
464
-
465
- Operation and attempt errors are recorded as exception events when an error
466
- object is available. Workflow and Invocation failures set an `ERROR` status and
467
- status message without recording an exception event.
573
+ | Span | Durable result | OTel status |
574
+ | ---------- | -------------- | ----------- |
575
+ | Workflow | `SUCCEEDED` / `FAILED` | `OK` / `ERROR` |
576
+ | Invocation | `SUCCEEDED` or `PENDING` / `FAILED` / `RETRYING` | `OK` / `ERROR` / `UNSET` |
577
+ | Operation | `SUCCEEDED` / `FAILED`, `TIMED_OUT`, `STOPPED` or an error object / otherwise | `OK` / `ERROR` / `UNSET` |
578
+ | Attempt | `FAILED` / any other outcome | `ERROR` / `OK` |
579
+
580
+ Operation failure status and exception details are independent. `FAILED`,
581
+ `TIMED_OUT`, and `STOPPED` remain `ERROR` when error details are absent, including when
582
+ the handler catches its failure and returns successfully. The status message uses
583
+ the supplied error message, or the failed status name when no error exists.
584
+
585
+ `CANCELLED` without error details remains `UNSET`. A supplied error still makes
586
+ a cancelled operation `ERROR`; this mapping does not introduce a general
587
+ cancellation policy.
588
+
589
+ Operation and attempt exceptions are recorded only when an error object is
590
+ available. Wire errors with no defined values do not synthesize exception events,
591
+ including when an unrecognized property is `undefined`. Explicit partial
592
+ details, including empty strings or an empty stack trace, remain error information.
593
+ Workflow and Invocation failures set `ERROR` without recording an exception event.
594
+ Caller exceptions, checkpoint payloads and replay behavior are unchanged.
468
595
 
469
596
  ## Log Correlation
470
597
 
@@ -591,6 +718,102 @@ The package also exports the `ContextExtractor`, `ContextExtractorResult`,
591
718
  `ExecutionTraceEnvironment`, `IdGeneratorFactory`, `TracerProviderFactory`, and
592
719
  `OtelPluginConfig` types.
593
720
 
721
+ ### External completions and replay
722
+
723
+ Both views retain terminal wait, invoke, and callback notifications received at
724
+ invocation start or in checkpoint responses. If workflow traversal does not
725
+ reach a completed operation before suspending or returning, its completion is
726
+ exported at invocation end. Supplied operation timestamps, parent identity,
727
+ status, and error details are preserved in the execution view; the invocation
728
+ view completes the live segment or emits a linked continuation.
729
+
730
+ A failed invoke still rejects with `InvokeError` when backend error details are
731
+ absent. Its telemetry retains the absent error metadata in live, resumed, and
732
+ deferred delivery, rather than inventing an exception. Provided error details
733
+ continue through the existing metadata converter.
734
+
735
+ For a named `waitForCallback`, a deferred inner callback recovers its derived
736
+ `<name>-callback` display name from a known `WaitForCallback` parent when replay
737
+ skips that branch. This does not change checkpointed names, operation identity,
738
+ parent identity, or the source notification; explicit names remain unchanged.
739
+
740
+ At the final cleanup of a terminal invocation, the execution view parents a
741
+ deferred completion to `Workflow` if its direct parent is still an open local
742
+ placeholder that will be discarded. During the open drain/flush phase, a live
743
+ parent can still finish and its normal end hook exports the queued child with
744
+ the real parent identity. An open parent alone does not trigger repeated flushes.
745
+ The plugin does not invent a completion or span for an abandoned context. Pending
746
+ or retrying invocations retain the parent identity
747
+ because a later invocation can still complete and export that context. An
748
+ exception is an unfinished parent below a known completed context in checkpoint
749
+ history: replay cannot restart that unfinished branch, so its deferred completion
750
+ also uses `Workflow`. `ReplayChildren` can traverse completed children to rebuild
751
+ a result, but `ReplaySucceededContext` still blocks unfinished child operations.
752
+ Missing or inconclusive ancestry is not treated as proof of abandonment.
753
+
754
+ In both views, a completion notification already queued when its live context
755
+ ends is exported before that context closes, preserving its OTel parent. Queued
756
+ child draining and parent finalization run synchronously within the End hook;
757
+ flush completion cannot reset invocation state between them. Invocation-end
758
+ drains likewise finish their current snapshot before yielding. This
759
+ includes an unawaited callback from a prior invocation: the invocation view emits
760
+ its linked continuation while the resumed parent segment is still open.
761
+
762
+ In the execution view, a completed context in checkpoint history remains a valid
763
+ parent for a later completion. A context span actually recorded during the current
764
+ invocation also remains a valid parent when an enclosing context suppressed its
765
+ terminal checkpoint and history still shows it as unfinished. This recording takes
766
+ precedence over abandonment inference within that invocation; knowledge of an
767
+ uncheckpointed recording is not persisted into later invocations.
768
+
769
+ A notification delivered after the parent's end hook may carry a
770
+ completion that occurred earlier in the backend. Delivery order does not establish
771
+ that chronology. Reusing the recorded parent identity does not recreate a parent
772
+ span, compare timestamps, retime supplied dates, or promise temporal containment.
773
+ The unfinished-parent safeguards still apply when no such recorded parent is known.
774
+
775
+ This requires the SDK to receive the operation metadata. Lambda normally removes
776
+ a completed context's children from subsequent execution state unless
777
+ `ReplayChildren` preserves them. For completion visibility after a child context
778
+ has finished, enable the appropriate `pluginsConfig.childOperationsDepth` (for
779
+ example, `1` for direct children of a top-level context). Preserved contexts
780
+ replay their orchestration; completed durable work still uses stored results.
781
+ The plugin cannot reconstruct an operation that was pruned and whose lifecycle
782
+ hooks do not run. The local runner now applies the service's pruning rule to
783
+ invocation input and `GetDurableExecutionState`; its inspection results and
784
+ checkpoint responses still retain their existing operation data.
785
+
786
+ Within the configured flush budget, the invocation-end hook drains notifications
787
+ received while it awaits a provider flush, then flushes the newly exported
788
+ completions. On expiry it drains one snapshot of already-received notifications
789
+ without another provider wait before clearing its state. In the execution view,
790
+ completions held for a still-open parent are finalized after the open drain/flush
791
+ phase. Both views close their invocation state before a final provider flush for
792
+ operation or attempt spans ended during the previous wait or final cleanup,
793
+ using only the original budget remaining. New operation-end hooks after
794
+ finalization do not record additional spans during that final wait. At deadline
795
+ expiry these final exports remain best effort. Change hooks enqueue notifications;
796
+ the end hook owns this loop. In the execution view, these drains use the latest operation ancestry received during
797
+ the end hook to detect an unfinished parent abandoned by a completed ancestor.
798
+ Missing ancestry alone does not prove abandonment. In the
799
+ invocation view, a completion after its individual segment has ended uses a linked
800
+ continuation. A segment first created while a provider flush is waiting remains
801
+ eligible for normal completion. Any such segment still open at final cleanup is
802
+ ended before state is cleared and included in the final bounded flush. The
803
+ execution view likewise closes recording attempt spans that started during the
804
+ wait, without inventing a completed operation span for unfinished work.
805
+ Enclosing spans may already have ended, so this does not guarantee
806
+ late-child timestamp containment. Updates after the final drain/reset, a frozen
807
+ runtime, and concurrent executions sharing a plugin instance remain outside this
808
+ shutdown guarantee; the shared-instance limitation is tracked in [#950](https://github.com/aws/aws-durable-execution-sdk-js/issues/950).
809
+
810
+ Notifications and operation-end hooks are deduplicated within each invocation.
811
+ Plain replay without a fresh update does not re-export stored external
812
+ completions. Deduplication is not persisted: a retry carrying the same updated
813
+ operation IDs can export the completion again, preserving recovery eligibility.
814
+ This is a conditional redelivery behavior, not a claim that Lambda always sends
815
+ those IDs again or that the exporter has a durable delivery acknowledgement.
816
+
594
817
  ## Verification and Troubleshooting
595
818
 
596
819
  After deployment:
@@ -623,3 +846,106 @@ remote parent, or a synthetic execution root).
623
846
  ## License
624
847
 
625
848
  Apache-2.0
849
+
850
+ ## Chained-invoke propagation
851
+
852
+ When preparing a new `context.invoke()` START, the SDK calls the optional
853
+ `providePropagationMetadata(input)` hook after assigning the operation identity
854
+ and before serializing the checkpoint. The readonly input contains
855
+ `executionArn`, `operationId`, optional `parentOperationId`, and
856
+ `targetFunctionName`. The SDK places the collected `xAmznTraceId` in the flat
857
+ `ChainedInvokeOptions.XAmznTraceId` wire member, separately for each operation
858
+ in a batched checkpoint. Function name, tenant, payload and operation identity
859
+ are unchanged; the SDK does not inject tracing fields into user payloads.
860
+
861
+ With the durable sampler bridge still installed on a compatible SDK tracer,
862
+ both OTel views encode the resolved execution trace ID as `Root`, the calling
863
+ CHAINED_INVOKE operation span ID as `Parent`, and the resolved sampling decision
864
+ as `Sampled`, including `Sampled=0`. Producing metadata does not create an extra
865
+ span or use an unrelated active span. For example, existing plugin registration
866
+ also covers both calls here:
867
+
868
+ ```typescript
869
+ const first = context.invoke("first", "first-function:1", { item: 1 });
870
+ const second = context.invoke("second", "second-function:1", { item: 2 });
871
+ return context.promise.all([first, second]);
872
+ ```
873
+
874
+ An opaque tracer or a tracer whose sampler was replaced may make an independent
875
+ decision for the invoke span. The plugins omit their optional propagation metadata
876
+ when they cannot verify that their sampler bridge still controls that decision.
877
+ They do not probe the sampler, create a span to obtain a decision, reinstall a
878
+ customer's sampler, or change ordinary span behavior. The check occurs when the
879
+ START is prepared; later provider reconfiguration cannot retroactively change a
880
+ checkpointed header.
881
+
882
+ The dispatcher calls providers synchronously with a frozen input snapshot.
883
+ The first non-null supported value wins; identical values do not conflict.
884
+ Later different values generate a warning with both plugin identities and a
885
+ conflict count. Invalid results and ordinary plugin failures are isolated.
886
+ Blank or whitespace-only headers are ignored so later providers can contribute;
887
+ nonblank values remain opaque and are not trimmed or parsed by the core SDK.
888
+ With no plugin, an absent hook or no returned value, the wire member is omitted
889
+ and the backend's existing tracing fallback applies.
890
+
891
+ Replaying a persisted START, including a pending or completed invoke, does not
892
+ call the hook or send another START. A failed, uncommitted START can collect
893
+ metadata again when the invocation is retried. Providers must therefore use the
894
+ stable identity, avoid side effects, and not assume exactly-once callbacks.
895
+
896
+ The runtime dependency requires `@aws-sdk/client-lambda` 3.1152.0 or newer.
897
+ That published release declares the optional `XAmznTraceId` field and includes
898
+ it in the generated serializer. The strict wire-model parity guard and actual
899
+ serializer regression pass with that release.
900
+
901
+ **Deployed backend trace-topology qualification remains separate.** Local
902
+ serialization and package compatibility do not establish
903
+ that a deployed chained invocation receives the intended Root/Parent/Sampled
904
+ or exports the expected caller/child topology. Existing sampled cloud traces
905
+ do not qualify unsampled delivery.
906
+
907
+ The design also adds `DistributedMapOptions.XAmznTraceId` to a future service
908
+ model. This SDK currently has no distributed-map options or START API; its
909
+ `map` and `parallel` APIs use CONTEXT operations. They are not distributed-map
910
+ transport paths, and this change does not introduce a new operation API or
911
+ invent the future Source/Processor/Destination structures.
912
+ ## Core compatibility
913
+
914
+ OTel 1.2 preserves valid core 2.4–2.x registrations, including separate
915
+ OTel-only Lambda layers. Its core peer is optional (`>=2.4.0 <3.0.0`), and the
916
+ dynamic provider contract remains version 1. Existing on-demand environment
917
+ carrier behavior remains available on older cores; upgrading the plugin alone
918
+ does not add fields to the invocation metadata those cores supply.
919
+
920
+ The new invocation-local LMI carrier and core registration exclusivity require
921
+ the coordinated core 2.7 / OTel 1.2 pair. Upgrade both for those capabilities.
922
+ Older core/plugin combinations do not gain those guarantees. The two OTel views
923
+ remain an unsupported combination, and the updated core rejects it with updated
924
+ plugin metadata. The later core 3 factory migration is a separate major release.
925
+
926
+ Instrumentation error details are optional even for failed operations. The core
927
+ metadata correction for an empty callback error payload no longer invents an
928
+ error object. API-v1 plugins that previously used only `OperationInfo.error`
929
+ presence as a failure signal should use `OperationInfo.status` instead (and
930
+ `AttemptEndInfo.outcome` for an attempt). Missing error details do not imply a
931
+ successful operation; supplied partial or explicitly empty detail fields are
932
+ retained. This does not change the provider/hook shape, callback rejection,
933
+ persisted error payload, or replay identity.
934
+
935
+ Registration metadata uses the namespaced symbol
936
+ `Symbol.for("aws.lambda.durable.instrumentation.plugin-registration")`.
937
+ The bundled views declare it on their constructors, so the core can validate
938
+ all selected provider types before calling any environment-selected factory.
939
+ Conflicting dynamic views therefore do not install sampler or ID-generator
940
+ wrappers on the global tracer. Subclasses retain all inherited static exclusive
941
+ groups; declaring their own static metadata adds a constraint and cannot replace
942
+ an inherited one. Repeating a group within one constructor chain does not count
943
+ as a second plugin registration. Subclasses are identified by their actual
944
+ constructor names in diagnostics. Explicit instances
945
+ have already been constructed by the application before registration; validation
946
+ cannot undo that construction. Existing instance metadata remains supported and
947
+ returned instances are checked too, including subclasses returned by providers
948
+ that declare a broader base type.
949
+ The base hook interface stays unchanged, and ordinary properties such as a
950
+ custom plugin's `registration` field are not interpreted by the SDK. This keeps
951
+ existing valid plugin classes and generic hook dispatchers source-compatible.