Repository navigation
Enforce search-attribute replay identity and qualify redriven memo and search-attribute reuse - #236
Conversation
Workflow 2.19 redrive reuses MemoUpserted and SearchAttributesUpserted (workflow#1001) and an applied signal wait: SignalWaitOpened, SignalReceived and SignalApplied (workflow#1002). Core replays now() after them at payload.reused_recorded_at. This SDK takes no replay time from any of these events. ctx.now() is the persisted start time, which on a redriven run is WorkflowStarted.replayed_started_at (#232). The cancellation budget is fed by activity, timer, child, condition, selection and cancellation events only. No source change is needed. Add two regression fixtures shaped like Core's redrive successor histories, with Python counterparts of the Core test workflows. Tests assert per-event reuse provenance, now() after each reused event, that changing or removing each event's recorded_at or reused_recorded_at does not move the clock, unchanged signal and memo values, one signal application, and no repeated upserts. Document the reuse keys in the history-event contract. Fixes #235 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Security review hold on sdk-python #236 exact head b1019e6. Verified clean:
Two holds remain before this PR can say it qualifies all five events, close #235, or count toward lifting Workflow #1006:
These are source/evidence holds, so a correcting commit will produce a new exact head. Hosted CI can continue, but do not mark ready or close #235 at b1019e6. |
Security held #236 on two gaps. Search-attribute identity: replay recorded SearchAttributesUpserted as a step without its values, so a workflow that changed its upserted attributes replayed against the old history unnoticed. Record the attributes as replay identity and compare them with the current UpsertSearchAttributes command, as memo replay does. A mismatch raises NonDeterministicReplayError with a search_attribute_value_mismatch detail, the reason the Server and the Rust SDK use. Both sides are normalized as the Server stores them (trimmed strings, empty string as null). A recorded attributes value that is not an object is refused. The polyglot test workflow upserted a value its PHP fixture never recorded and now matches it. Cancellation clock: add a valid redrive successor in which a cooperative cancellation is requested and delivered after the reused prefix, and show that retiming each reused upsert or signal event (recorded_at and reused_recorded_at) changes neither remaining() nor the commands. The ordinary-run cleanup control now also receives a signal while the clock is live. The earlier variant that placed reused events after a delivery is removed, since redrive cannot write that. Fixtures fail on origin/main and pass here: search-attribute-value-mismatch and redrive-reused-search-attribute-value-mismatch. Refs #235 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Security re-review of exact head
The valid-history cancellation conclusion is sound: reused prefix events precede delivery and therefore cannot move an active budget. The plain post-delivery signal/upsert mutation test is the relevant leakage counterfactual. Python has no signal-wait primitive, so this PR also must not be treated as qualification to lift the signal-wait fence for Python; record that capability as unsupported/granular rather than global. Hosted CI is still running, but these are source/evidence holds independent of CI. |
Security's second review of #236 held three points. Redrive successors: the fixtures carried Core's metadata resume steps (4 and 3) although the highest reused workflow steps are 2 and 1, and lacked the successor identity. WorkflowStarted now carries the full redrive envelope Core writes (instance, run, command and link ids, continued_from_run_id, replayed_started_at, resume_step_sequence 3 for upserts and 2 for the signal wait). A shared check_redrive_successor test helper requires that envelope, every reused_from_run_id equal to continued_from_run_id, the reused prefix directly after WorkflowStarted at its time, later successor events, and resume_step_sequence equal to the highest reused step plus one (SignalReceived has no step). It runs on every reused fixture, with wrong, off-by-one and null counterfactuals. Fail-closed values: replace the str(key)/json default=repr identity with Core's UpsertSearchAttributesCall domain. Keys must be portable strings; values must be null, bool, 64-bit int, finite float, string or a list of strings. Strings and list items are trimmed and a blank string is null. Values compare with their types. Anything else, on either side, raises the typed NonDeterministicReplayError. Types: when SearchAttributesUpserted records attribute_types, the keys must match the typed values and each type must be one the Server can record for the value of an untyped upsert. The SDK cannot see the namespace registry, so keyword, string and RFC 3339 datetime stay interchangeable for a string, as do int and float for an integer. History without attribute_types keeps value-only identity, as in Core. Refs #235 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner decision on #236: a tuple of strings is sent as exactly the JSON list the Server records, so refusing it would break replay of deployed workflows on upgrade without closing any ambiguity. Normalize a tuple to a list on the authored side only, then apply the same strict rules: string elements and type-strict comparison. Sets, bytes, objects, non-string keys and non-finite numbers are still refused. Refs #235 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Security re-review hold on exact head
The signal-wait fence must remain, as stated. With exact inferred types, Python search-attribute type identity can be qualified for commands this SDK can actually emit; do not preserve an artificial “namespace type” ambiguity that the Server path does not have. Re-request exact-head review with fail-before evidence for these cases and terminal hosted CI. |
|
Correction to item 2 of Security comment 6615192720 after exact native 3.0.4 readback: do not replace the partial type check with Workflow-only exact inference. The type fence remains. The implementations differ:
So the broad category choices are directionally correct for cross-server replay, and Python search-attribute type identity is not qualified. My Workflow-only examples ( One compatibility edge remains inside this partial approach: native accepts registered datetime strings in a broader deterministic set than Items 1, 3 and 4 of 6615192720 still stand unchanged: missing |
Security's review of 13c76c3 and its corrected ruling: - A SearchAttributesUpserted event without attributes is refused with search_attribute_value_mismatch instead of skipping identity. The identity is now recorded by the event parser for every upsert, and a step without it is refused. The test harness records the attributes. - A recorded attribute_types value that is not a string (a list, map, number, bool or null) is refused with search_attribute_type_mismatch rather than raising TypeError on an unhashable value. - Type check: both Servers resolve each key as registered, then declared, then inferred (native search_attribute_upserts.rs:254-289; PHP SearchAttributeValueValidator.php:93 via WorkerController.php:2734), and the SDK cannot see the registry. Keep the type fence for Python and refuse only a type that cannot hold the value: bool as bool, an integer as int or float, a float as float, a list as keyword_list, and any string as keyword, string or datetime. Every non-null key, including a blank string, is typed, as both Servers do. - check_redrive_successor now takes the fixture's workflow and binds the start's workflow_type, instance and run to it, requires every event's instance to match, successor events to carry the successor run and reused events the source run, and both reuse keys on every reused event, with counterfactuals for each. Refs #235 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Security exact-head review of 910f7e9: hold. The search-attribute replay comparison is substantially corrected and hosted CI is fully green, but four exact issues remain:
Positive exact evidence: values remain type-strict; malformed type values become typed replay refusals; registered-type uncertainty is handled conservatively; sequence-less SearchAttributesUpserted is no longer skipped; and 21/21 hosted checks are green. Return the corrected exact head with the counterfactual counts. |
…issing Use the reason sdk-rust and sdk-php report for a recorded SearchAttributesUpserted without attributes. Present but different or invalid values keep search_attribute_value_mismatch. Refs #235 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Security's review of 910f7e9: - CHANGELOG: qualify memo reuse and search-attribute value identity only. Type identity stays fenced, and signal waits are not qualified for Python, which has no signal-wait primitive. - Add search-attribute-update-missing and its redrive counterpart, which require the search_attribute_update_missing reason. - check_redrive_successor now requires every WorkflowStarted envelope key, including execution_deadline_at and run_deadline_at as null or a zoned time; the identity Core writes on each event kind (signal ids and wait ids, the signal and cancellation instance and run, step sequences), with unknown event kinds refused; signal events that agree on their wait, name, command and signal ids; one cancellation command; and reused steps that are whole steps forming the contiguous prefix 1..resume-1 in order, with SignalReceived carrying no step. - Counterfactuals delete or blank every envelope field and every event identity field, and cover missing, offset, duplicate, descending and split steps and an injected SignalReceived sequence. Refs #235 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Security exact-head review of The prior holds are closed in the evidence: the public claim is narrowed correctly, missing attributes have the converged reason, reused steps are contiguous whole steps, the full start envelope/deadlines are required, and the event-specific deletion/layout controls now fail closed. One product-source hold remains before merge:
Return the exact source head with terminal hosted CI. Search-attribute type identity and Python signal waits remain explicitly fenced as the corrected body says. |
Security's review of 36c1e0f held product replay on two points. - A SearchAttributesUpserted without a positive workflow sequence was bound to the next authored step. It now refuses replay with search_attribute_update_missing_sequence; no sequence is synthesized. - Each workflow sequence is classified as its events are read, before any details are recorded. A second SearchAttributesUpserted at a sequence is duplicate_search_attribute_update, and two different durable command kinds at one sequence are durable_command_sequence_collision, so details_by_sequence is never overwritten by a later record. The only shared sequences allowed are a condition or signal wait with its own timeout timer. Events that refer to another step's sequence (signals, updates, cancellation and scope records) are not command records. - The test harness gives each search-attribute upsert its sequence. Fixtures: search-attribute-update-missing-sequence, duplicate-search-attribute-update and durable-command-sequence-collision. Refs #235 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Align with sdk-rust and sdk-php#246: - A SearchAttributesUpserted without a sequence, or with a null one, is durable_command_sequence_missing. Zero, negative, string, float or bool is durable_command_sequence_invalid. The earlier search_attribute_update_missing_sequence name is gone. - durable_command_sequence_collision is refused when it involves an upsert (MemoUpserted or SearchAttributesUpserted): an upsert must be the only command at its sequence. Lifecycle events of one command (activity, timer, condition wait, signal wait, child run, service call) share their sequence, and events that only point at a step, such as SignalReceived or a cancellation delivery, claim nothing. Fixtures durable-command-sequence-missing (renamed) and durable-command-sequence-invalid. Refs #235 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Security correction to my preliminary source read: exact head The explicit Please add these event kinds to the explicit ownership table ( The typed sequence reasons, duplicate-upsert check, write-before-details ordering, and the rest of the ownership table remain source-clean. |
Security's review of c569f63: the claim table omitted rows that carry a command's sequence, so with the ordinary opener deleted one of them could sit at an upsert's sequence and bypass durable_command_sequence_collision. Claim Core's WorkflowStepHistory step events and every other lifecycle row that carries its command's sequence: ActivityHeartbeatRecorded, ActivityRetryScheduled and ActivityCancellationAcknowledged (activity), ChildCancellationRequested and ChildCancellationResolved (child), ServiceOperationScheduled (service call), SignalWaitReceived (signal wait), CancellationScopeOpened (cancellation scope) and WorkflowContinuedAsNew (continue as new). Pointer records stay unclaimed: signal receipts, update records, failure handling, selection records and cooperative or scope cancellation requests, preparations and deliveries. Tests fix the claim table exactly and, for every claimed kind with its opener absent, put it at an upsert's sequence before and after the upsert on ordinary and reused histories. A scope opening uses canonical scope history so replay reaches the classification. Refs #235 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Security re-review of exact head c038abc: the prior activity/child/service/signal/scope/continue-as-new claim-table omissions are closed, but one durable-position fail-open remains. CooperativeCancellationDelivered and CancellationScopeDelivered are classified as pointer-only and absent from _DURABLE_COMMAND_KINDS. WorkflowStepHistory explicitly treats both delivery markers as authoritative durable-command positions: a delivered cancellation consumes the interrupted call even when that call was never scheduled, and sequence_span reserves the full interrupted range. Therefore, with the ordinary opener absent, an upsert can currently share the delivery sequence—or a sequence inside a parallel delivery span—without durable_command_sequence_collision, and its details can occupy a position the cancellation already consumed. Please claim both delivery types for upsert-involved collision detection, including every sequence in their validated sequence_span. Add ordinary and reused counterfactuals with no scheduled opener, both history orders, for: a single-step CooperativeCancellationDelivered; a parallel delivery whose upsert is inside rather than only at the base; and CancellationScopeDelivered under a valid scope envelope. The request/preparation rows may remain pointer-only. If a scope-delivery counterfactual cannot reach this classifier because the scope gate refuses first, pin the range-claim helper directly and include a product history that reaches it with delivery enabled. This is a merge hold on c038abc. The search-attribute value/type and previously held sequence checks otherwise remain source-clean; terminal hosted CI will still be required at the corrected exact head. |
Security's review of c038abc: CooperativeCancellationDelivered and CancellationScopeDelivered are durable command positions in Core's WorkflowStepHistory (nextDurableCommandSequence adds them to the step types, and nextSequenceFromEvents extends each by sequence_span - 1). A delivery consumes the interrupted call even when it was never scheduled, so an upsert at its sequence, or inside its span, must not replay. Claim both as cancellation_delivery. _durable_command_claimed_range mirrors Core: an integer or numeric-string span, 1 when absent, claims sequence through sequence + span - 1 inclusive, and a span below 2 claims only the sequence. An upsert inside any delivery range, recorded before or after it, is durable_command_sequence_collision. Tests, with no scheduled opener, on ordinary and reused histories and in both orders: a single-call cooperative delivery at the upsert's sequence; parallel spans with the upsert interior, at the end and at the base; a span that stops short; a committed scope delivery replayed with scope delivery enabled; and the range helper pinned against Core. Refs #235 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Security source re-review of exact The delivery-span collision coverage closes the position gap, including ordinary/reused histories, both orders, interiors, and scope delivery. One cross-SDK rule must be aligned before clearance:
WorkflowStepHistory::intValue() is a permissive cursor-recovery parser; it is not the producer payload contract. The 2.19 producer and CancellationScopeDelivery validation write/require integers, and silently treating a malformed span as 1 can under-claim an authoritative delivery range. Follow the fail-closed producer contract, matching sdk-php#246. Add direct controls for missing=>1 and each malformed class, plus an interior-collision counterfactual showing malformed input cannot shrink the range. Return the exact head with terminal hosted CI. The claim map, delivery-range checks, and the rest of this source head are clean. |
Security's ruling on e44cf64, matching sdk-php#246: Core's WorkflowStepHistory::intValue is a permissive cursor-recovery parser, not the producer contract. A delivery's sequence_span is 1 when absent; when present it must be a positive JSON integer. Zero, negative, bool, float, numeric string, null and any other value refuse replay with durable_command_sequence_invalid, before the cooperative or scope history parsers read the event, so a malformed span can never shrink the claimed range. _delivery_sequence_span holds the contract; _durable_command_claimed_range uses it. Tests cover absent, 1 and 3 claiming their inclusive range, eleven malformed values on the helper and on cooperative (ordinary and reused, both orders, upsert interior to the intended span) and scope delivery replays. Refs #235 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Security source re-review of exact The canonical span rule and all requested counterfactuals are implemented correctly: absence means 1, present values must be positive JSON integers, malformed inputs refuse before parser-specific handling, and neither ordinary nor reused delivery can shrink an interior collision. One narrow data-minimization hold remains. Return the exact head with terminal hosted CI. No other source hold remains. |
|
Security correction to the span rule while the raw-value hold is being fixed:
This matches Core's The current exact head remains held for the separate raw-value disclosure: no rejection message may interpolate |
Security's data-minimization hold on 2a8f1c5: _delivery_sequence_span put the malformed span's repr into the replay error. It now names only the JSON type ("got string", "got object"). An invalid search-attribute key is reported by type too, never by its text. The other diagnostics added on this branch already name only reasons, keys that passed validation, known type names, sequences and event types. Null span: Core reads sequence_span with `?? null) ?? 1` (WorkflowStepHistory.php:270 at workflow 28893f97), and sdk-php#246 uses `?? 1`, so an explicit null means one call, like an absent span. The cooperative delivery parser and the scope cleanup bound read it the same way. Numeric strings and every other non-positive-integer value are still refused with durable_command_sequence_invalid. Controls: secret-shaped string and nested-map spans on the helper and on cooperative and scope replays, and secret-shaped search-attribute values, keys and types, assert the raw values appear in neither the message, the args nor the repr. Refs #235 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Security source re-review of The disclosure hold is fixed in the reviewed source. Span and invalid-key diagnostics now name only a bounded type, and the new controls cover exception string, repr and args for secret-shaped string/object spans, search values/keys/types/maps, and sequences. The remaining new messages expose only validated keys, known event/type names and numeric sequences. The cooperative parser and replay claim path also now apply the corrected null-as-one rule. One narrow scope-path hold remains before merge:
No further non-disclosure or search-identity finding is open. Hosted CI is still pending in any case. Return the new exact head with the focused counterfactual and terminal checks. |
Security's hold on 4116362: `recorded["sequence_span"] or 1` in _cancellation_scope_history._cleanup_operation also read 0, false and "" as one call. Only an absent or null span now reads as 1; any other value that is not a positive integer refuses the cleanup operation, so the parser keeps no broader contract than replay even when called on its own. Tests: an end-to-end committed scope replay through the cleanup timer path gives identical results with span null and span 1, the cleanup lower bound refuses and accepts the same sequences for both, and 0, false, -1, "1", 1.0 and "" are refused by replay with durable_command_sequence_invalid and by the cleanup bound called directly. Refs #235 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
Security source re-review at exact head 909f353: SOURCE-CLEAN, pending the final hosted integration result. I independently compared it with the previously reviewed 4116362 head. The delta is limited to _cancellation_scope_history.py and the committed-scope replay tests. The parser now treats missing/null sequence_span as one, rejects every other non-positive JSON integer shape without narrowing, and applies that same validated span to the cleanup lower bound. The new end-to-end test exercises the cleanup wait, parent timer, and completion with null versus one; the lower-bound and direct-parser counterfactuals cover 0, false, negative, string, float, and blank forms. diff --check is clean. Current exact-head checks are otherwise green; integration was still running at readback. Return the terminal exact-head result for final merge clearance. |
|
Security final clearance at exact 909f353: ACCEPTED FOR MERGE. This is the source-clean head from 6617362692, now terminal with 21/21 hosted checks successful, including integration, 0 skipped or failed, and CLEAN. Scope remains memo and search-value replay qualification plus durable sequence identity. Search-attribute type identity and signal-wait replay remain fenced under #235; merging this PR does not qualify either. |



Fixes #235.
Head
909f3535addresses Security's scope-path hold on4116362b(comment 6617229056). Earlier heads closed comments 6616894909, 6616717910, 6616445862, 6616210579, 6615888062, 6615716272, 6615192720 and 6615262197.What this qualifies for Python
The fence lifts per runtime and per event, not as one switch (workflow#1006, comment 6614976031).
MemoUpserted): qualified. Replay time, exact value identity, and no re-upsert.SearchAttributesUpserted): values qualified, types not.attributesis missing (search_attribute_update_missing), and no re-upsert.SignalWaitOpened,SignalReceived,SignalApplied): not qualified.Why the type fence stays
The SDK's
UpsertSearchAttributessends noattribute_types. Both Servers still record a key's namespace-registered type ahead of the inferred one:33bb2911,rust/src/runtime/search_attribute_upserts.rs)::821loadssearch_attribute_definitions.validate(:197) takesregistered_type(:254), usesregistered_type.or(declared_type)(:264), and only otherwise infers (:289).:856records that map asattribute_types.:879records an integer asfloatand"a b"asstringfor registered keys.SearchAttributeValueValidator.php:93sets$effectiveType = $registeredType ?? $declaredType, and:113infers only when the key is not registered.WorkerController.php:2734writes that map into the command'sattribute_types.DefaultWorkflowTaskBridge.php:5215-5240(workflow28893f97) then recordscanonicalTypes($call, $attributeTypes), which prefers the given type (SearchAttributeUpsertService.php:177) overinferType(:184).The type check that remains
The check refuses only a recorded type that cannot hold the value at all. It is a superset of what either Server accepts, so it cannot falsely refuse a valid history.
bool.intorfloat.float.keyword_list.keyword,stringordatetime. Every string counts as a possible registered datetime, because native accepts no-offset ISO, date-only and RFC 2822 forms.search_attribute_type_mismatch: unknown type names, a map that isn't an object, and type values that aren't strings (list, map, number, bool, null).attribute_types: value-only identity, as in Core.Durable command sequences (this head)
Product replay no longer guesses or overwrites a search-attribute step. Each workflow sequence is classified as the first pass reads it, before anything is recorded in
details_by_sequence.durable_command_sequence_missing: aSearchAttributesUpsertedwith nosequenceor a null one. Replay no longer binds it to the next authored step.durable_command_sequence_invalid: a sequence that is zero, negative, a string (including"2"), a float or a bool.duplicate_search_attribute_update: a secondSearchAttributesUpsertedat the same sequence.durable_command_sequence_collision: an upsert (MemoUpsertedorSearchAttributesUpserted) that shares its sequence with any other durable command record. This is the same scope as sdk-php#246; an upsert is the only command at its sequence. A later record therefore never overwrites an earlier one.Sequence audit. Every event type that carries a
sequence(Core'sHistoryEventPayloadContract, and every one the SDK reads) either claims its command's sequence or only points at a step. Rows of one command share its sequence. An upsert sharing a sequence with any claimed row from another command is refused. The claim table (_DURABLE_COMMAND_KINDS) is pinned exactly bytest_claim_table_is_exact.ActivityScheduled,ActivityStarted,ActivityCompleted,ActivityFailed,ActivityTimedOut,ActivityCancelledWorkflowStepHistorystep events for the activity's own sequence.ActivityHeartbeatRecorded,ActivityRetryScheduledActivityCancellationAcknowledgedTimerScheduled,TimerFired,TimerCancelledChildWorkflowScheduled,ChildRunStarted,ChildRunCompleted,ChildRunFailed,ChildRunCancelled,ChildRunTerminatedChildCancellationRequested,ChildCancellationResolvedConditionWaitOpened,ConditionWaitSatisfied,ConditionWaitTimedOut,ConditionWaitCancelledSignalWaitOpened,SignalApplied,SignalWaitTimedOut,SignalWaitCancelledSignalWaitReceivedServiceOperationScheduledServiceCallStarted,ServiceCallCompleted,ServiceCallFailed,ServiceCallCancelledCooperativeCancellationDelivered,CancellationScopeDeliveredsequencethroughsequence + sequence_span − 1, inclusive (see below).CancellationScopeOpenedcancellation scoperesult.WorkflowContinuedAsNewContinuedRunMessageHandoffrecords the command's own sequence.SideEffectRecorded,VersionMarkerRecorded,MemoUpserted,SearchAttributesUpsertedSignalReceivedworkflow_sequencenames the wait or condition it feeds. A receipt is not a command.UpdateApplied,UpdateCompletedFailureHandledrecordFailureHandled($run, $task, $id, $sequence, …)).CancellationScopeDeliveryPreparedSelectionResolved,SelectionOperationCancelledCooperativeCancellationRequested,CancellationScopeRequested,CancellationScopeRequestConflictedDelivery spans. These follow Core's
src/V2/Support/WorkflowStepHistory.php(workflow28893f9):nextDurableCommandSequenceaddsCooperativeCancellationDeliveredandCancellationScopeDeliveredto the step types (lines 108–109).nextSequenceFromEventsreads$span = self::intValue($event->payload['sequence_span'] ?? null) ?? 1(line 270). When$span > 1, the delivery reaches$sequence + $span - 1(lines 271–272), so the end is inclusive.intValueaccepts an int or a numeric string (lines 598–607).Span contract (fail closed). Python follows the producer contract, as sdk-php#246 does, not
intValue's permissive cursor recovery:nullsequence_spanmeans 1. Core reads it asself::intValue($event->payload['sequence_span'] ?? null) ?? 1(WorkflowStepHistory.php:270, workflow28893f97), and sdk-php#246 uses?? 1, so refusing null would reject a history Core accepts.durable_command_sequence_invalid. Refusing numeric strings is Security's stricter producer rule, which sdk-php#246 also applies._cancellation_scope_history._cleanup_operation) also read only an absent or null span as 1. Any other value that is not a positive integer refuses the cleanup operation even when the parser is called on its own; it is never narrowed to 1.CancellationScopeDelivered.sequence_span = nulland with1: the wait, the parent timer and completion.0,false,-1,"1",1.0and""are refused by replay withdurable_command_sequence_invalid, and by the cleanup bound called directly.4116362b: the direct bound accepted all six malformed values (or 1and arithmetic on the raw span). On origin/main's parser, a null span also failed the cleanup path._delivery_sequence_spanholds the rule._durable_command_claimed_rangecoverssequence..sequence+span−1, inclusive.Span results:
sequence_spannull13sequence..sequence+2)0,-1durable_command_sequence_invalid"2","3"durable_command_sequence_invalid2.0,3.0durable_command_sequence_invalidtrue,falsedurable_command_sequence_invalid[3],{"span": 3},"sk_live_TESTSECRET123",{"token": "TESTSECRET456"}durable_command_sequence_invalidMalformed-span counterfactuals:
e44cf64e: the helper narrowed these values ("2"→ 2; 0 or null → 1). Replay refused them only through the cooperative or scope parsers' own errors, never withdurable_command_sequence_invalid. 77 cases fail there and all pass here.Diagnostics never echo values. I audited every error message this branch adds. They name only reasons, keys that passed validation, known type names, JSON type names, sequences and event types.
sequence_span must be a positive integer (got string), or(got object)and so on, never by its value.sk_live_TESTSECRET123) and a nested-map span ({"token": "TESTSECRET456"}), on the helper and on cooperative and scope replays;2a8f1c57: the span value leaked (4 cases), secret-shaped invalid keys leaked (4), and null was refused (4). All pass here.Delivery counterfactuals (no scheduled opener):
CancellationScopeDeliveredfromcommitted-scope-delivery.json, whose timer was never scheduled, with an upsert at its sequence. Replayed with scope authoring and delivery enabled, so it reaches the classifier rather than the scope gate. Both orders.c038abc2. All pass here.Mutations for every claimed kind:
durable_command_sequence_collision.c569f636these cases fail: 36 for the nine newly claimed kinds, plus the table pin, the new-claims check and the scope test (39 in total). All pass here.Collisions without an upsert, for example a condition wait and its own timeout timer, are not judged by this rule (same scope as sdk-php#246). No existing fixture or history in the suite is refused, including
retained-duplicate-patch-markersand the condition-timer sequence fixtures.The test harness (
testing.py) now gives each upsert its sequence.Fixtures:
durable-command-sequence-missing,durable-command-sequence-invalid,duplicate-search-attribute-updateanddurable-command-sequence-collision.pytest (plain and reused histories):
Fail-before: all 44 new cases fail on
36c1e0ff, where the upsert was accepted or bound to a guessed step. 22 fail on6f0b57f4, which used the earlier reason name. All pass here.Changes at this head
attributes.attributeson everySearchAttributesUpsertedand records its identity, whatever the event's sequence.NonDeterministicReplayErrorwithsearch_attribute_update_missing, the reason sdk-rust and sdk-php use. A present but invalid or different value raisessearch_attribute_value_mismatch. A step without identity is refused.testing.py) now records the attributes. Two unit histories that omitted them now include them.TypeError. The check isisinstance(kind, str)before set membership.check_redrive_successor(history, workflow), intests/test_replay_clock.py). It requires:WorkflowStartedenvelope key:workflow_type, instance, run, command and link ids,continued_from_run_id,replayed_started_at,recovery_kind: redrive,resume_step_sequence, andexecution_deadline_atandrun_deadline_at(null or a zoned time).SignalWaitOpened: sequence, name, wait id.SignalReceived: command, signal, instance, run, name and wait ids, and no step sequence.SignalApplied: command, signal, name and wait ids, and sequence.CooperativeCancellationRequested: command, instance and run.CooperativeCancellationDelivered: command, run and sequence.reused_from_run_idequal tocontinued_from_run_id.SignalReceived) are whole steps forming the contiguous prefix 1..resume_step_sequence−1, in order. A whole step is one upsert, orSignalWaitOpened+SignalApplied.Tuples (owner decision): an authored tuple of strings is checked as the JSON list it is sent as. Its elements must still be strings.
Evidence (local, Python 3.13)
Missing-reason fixtures:
search-attribute-update-missing(plain) andredrive-reused-search-attribute-update-missing(reused) requiresearch_attribute_update_missing.910f7e97, where the reason issearch_attribute_value_mismatch. They pass here.search_attribute_value_mismatch.Validator counterfactuals. Each case below is accepted by the
910f7e97validator in the stated number of cases, and refused by this one in every case:910f7e97"","not-a-time", naive time, 5, true, [])SignalReceivedsequences, split signal waitAlso refused: event
sequenceorevent_typedeletion on every event (history order or unknown kind), and the earlier resume −1/+1/null/string and identity cases.Fail-before from the previous round: missing
attributeswas accepted on13c76c31. Unhashable type values raisedTypeErrorthere. 109 new tests fail on origin/main'sworkflow.py.Clock: none of the five events is a replay clock source. Mutations taking
now()from the event time (19 fail) or fromreused_recorded_at(7 fail) are caught. Upserts orSignalReceivedfeeding the cancellation clock fail the ordinary-run cleanup control.Checks:
ruff check src/ tests/,mypy src/durable_workflow/and the Avro benchmark gate pass.Full unit suite: 4,546 passed, 3 skipped, 8 failed. The same 8 fail on unmodified main: TLS fixture certificates rejected by the host's OpenSSL, and two boundary-scanner tests tied to the host git identity.
🤖 Generated with Claude Code