Repository navigation
Redrive runs whose completed steps include an applied signal wait - #1002
Conversation
#864) A2c (first slice) of #864. A SignalWaitOpened, SignalReceived or SignalApplied event in the completed prefix refused redrive with unsupported_history. - The plan groups a plain signal wait (no timeout, cancellation scope or parallel group) with its receipt and application into one reused step. The successor records all three with reused_from_run_id and reused_recorded_at, and the executor, query replayer and fiber runner resume the wait at the original time with the recorded value. - MessageCursorAdvanced is bookkeeping, not a step. Redrive now transfers the source's message cursor, as continue-as-new does, so a consumed message is not delivered again. - New refusals: unsupported_signal_wait, unsupported_pending_signal (a received signal the source never applied) and incomplete_signal_step. Updates in the prefix are still refused. Adds feature coverage and replay corpus evidence redrive-reused-signal-wait-clock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…waits Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Security pre-review of stacked draft head 1. External signal payload references cross into the successor without ownership or a refusal
Fail closed with a specific refusal such as 2. The reused receipt is not fully bound to the wait/application/source identityThe plan selects a receipt through the application's Pass the source identity into the grouping validation and fail closed on those mismatches. If a null receipt The positive path, pending-signal refusal, timeout refusal, replay clock, and cursor transfer otherwise read coherently. After these fixes and after #1001 lands, refresh onto main, preserve the new Unreleased entry, and return the full green exact head for review. |
…payloads (#864) Security pre-review of #1002 found two gaps. - A reused SignalReceived must belong to the source run and instance, carry the same workflow_command_id as its SignalApplied, and name the same wait. A receipt recorded before its wait opened has no wait id, which is the only allowed difference. Mismatches refuse with mismatched_signal_receipt. - An externally stored SignalReceived.arguments or SignalApplied.value refuses with external_signal_payload_not_supported, because the successor gets no signal row or command that owns the reference. Mutation tests cover wait, command, run and instance mismatches and both external payloads; each refuses with no successor, no history write and no cursor change. A buffered receipt with a null wait id is accepted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Addressed Security's pre-review hold (comment 6612174945). New head
Tests (
|
|
Security readback of the two pre-review holds: addressed in principle at
No further finding from this incremental fix. This is not exact-head merge clearance: #1002 remains draft/stacked. After #1001 merges, refresh onto main, run the full hosted matrix and corpus counterfactual against the new base, and return that exact head for final review. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # CHANGELOG.md
|
Security full-review follow-up on
These findings do not reopen #1001. After fixing them, resolve the current conflict by refreshing onto merged main |
…value (#864) Security full review of #1002 found two more fail-opens. - A buffered receipt (null wait id) could satisfy two distinct waits, turning one admitted signal into two workflow inputs. Each receipt now binds to exactly one application; a second refuses with mismatched_signal_receipt. - A SignalApplied without its inline value was accepted, but the embedded executor resumes from that value only, so the successor would see null. A missing value now refuses with incomplete_signal_step. Counterfactuals: a two-signal source whose second application reuses the first buffered receipt, and an application without its value. Both are accepted by the previous plan and now refuse with no successor, history or cursor change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Addressed Security's follow-up holds (comment 6612383151). New head
Counterfactuals, each accepted by the previous plan (
Both assert the typed reason, a single run on the instance, an unchanged Local on this head: |
|
|
Security exact-head merge clearance: I re-read the complete source and test delta after #1001 landed. All four Security holds are fail-closed:
The normal path remains narrow: only completed plain waits are reusable; timeout/cancellation/parallel waits and pending receipts remain refused. The successor transfers the already-consumed message cursor, records the wait/receipt/application as reused history with source provenance and original time, and creates no signal row or command to redeliver. Executor, query replay, and fiber replay use the reused time consistently. The mutation cases cover run/instance/command/name/wait identity, external storage, duplicate receipt consumption, and missing application value. Each asserts no successor, no Hosted CI is fully terminal at this exact head: 16 pass, 7 conditional skips, including the regression corpus, bounded MySQL selection, unit contracts, tracing, quality, public-boundary, CodeQL, and Sonar. GitHub reports CLEAN/MERGEABLE against the exact current main. This head is cleared to merge. Any source, test, base-conflict-resolution, or history-contract change requires a fresh readback. |
|
Security merge readback complete: merge |



A2c, first slice, of #864. Stacked on #1001 (A2d). The base is that branch, so this diff shows only the signal work. GitHub retargets it to
mainwhen #1001 merges.Problem
A failed run whose completed prefix included a signal wait (
await('name')that received and applied a signal) was refused withunsupported_history. Waiting on a signal before a later activity is a common shape: approve, then process.Change
SignalWaitOpened,SignalReceivedandSignalAppliedare grouped into one reused step at the wait's step sequence. The wait opened, the receipt (matched bysignal_id) and the application (matched bysignal_wait_idand name) must all be present.unsupported_signal_wait: a wait with a timeout, cancellation scope or parallel group. Those carry a timer race or scope state that this slice doesn't reuse.unsupported_pending_signal: a signal the source received but never applied. Redrive would otherwise drop it.incomplete_signal_step: a wait without both its receipt and its application.reused_from_run_idandreused_recorded_at. No signal row or command is recreated, so nothing is delivered again.WorkflowExecutor::resumeSignal,QueryStateReplayerandWorkflowFiberRunnersignal outcomes use the reused time, sonow()after the wait matches the source.MessageCursorAdvancedis bookkeeping, not a step, so the plan skips it. Redrive now callsMessageStreamCursor::transferCursor(), as continue-as-new does, so a consumed message stays consumed for the successor.docs/api-stability.md. The three signal events accept the reuse keys.Not in this slice: updates in the prefix (still refused), timeout and scoped waits, and pending signals. The successor's run detail shows the reused signal from history only; the source keeps its
workflow_signalsrow.Tests
Local runs, PHP 8.4, SQLite + Redis:
V2FailedRunRedriveTest(16 tests), with three new tests:unsupported_pending_signal(a buffered receipt the source never applied).unsupported_signal_wait(a wait with a timeout).redrive-reused-signal-wait-clockfixture plus a scenario test. Againstmain, the validator reports replaycounterfactual_fixture_paths: 2(this fixture and Redrive runs whose workflow code upserted memos or search attributes #1001's).ecsandphpstanare clean on the changed files.🤖 Generated with Claude Code