Repository navigation
Redrive runs whose workflow applied an update before the failure - #1007
Conversation
…#864) The update half of A2c. Any UpdateAccepted, UpdateApplied or UpdateCompleted event in the completed prefix refused redrive with unsupported_history. - The plan groups each applied update's acceptance, application and completion, bound to the source run, instance, command and update name, at the step it ran before (at or before the failure boundary). - The successor records them with reused_from_run_id and reused_recorded_at ahead of that step, from the shared successorEvents() builder, and replay runs the handler again from its recorded arguments. No update row or command is recreated. - Rejected updates and updates the failure closed before they ran (terminal_reason) are not reused. An accepted, unresolved update refuses with unsupported_pending_update; an external update payload with external_update_payload_not_supported; malformed history with incomplete_update_step or mismatched_update_history. - The service-mode fence now also covers applied updates. Counterfactuals: an applied update redrives with its state change, a pending update refuses with no mutation, and a service run with an applied update refuses with no engine or projection change; all three are refused differently on main. Replay corpus evidence: redrive-reused-update. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Security full review of exact head
Keep the update events ahead of their recorded step, preserve the service fence, and do not recreate or deliver an update row/command. Return the corrected exact head and focused suite/corpus evidence. |
) Security full review of #1007 found the update planner too permissive and one-hop only. - Updates are accepted in exactly three shapes, by history order: a lone rejection; an acceptance closed by the run's failure (terminal_reason); or acceptance < application < successful completion. Any extra, missing, duplicate, reversed or cross-state event refuses. - A reusable update needs nonblank update_id, workflow_command_id and update_name, identical across its three events, and byte-identical accepted and applied arguments. A successful completion may not carry failure_id or terminal_reason keys, even when null. - Any external_storage key, even null, refuses with external_update_payload_not_supported. Inline arguments and result must decode with the run's codec, or redrive refuses with unavailable_update_payload before writing. - Provenance is explicit: a payload recorded by this run names this run; a payload this run reused names its immediate redrive source in reused_from_run_id and an earlier run of the instance as its original. Signal receipts use the same rule, so a redrive successor that reused signal waits or updates can be redriven again. Counterfactuals: an A->B->C chain reusing an update and a signal wait at each hop, and eight malformed update histories refused with byte-identical engine tables and projections. All nine fail on b1739b0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Addressed Security's hold (comment 6615544362). New head
Counterfactuals (all fail on
Local: |
|
Security re-review of terminal-green exact head The original holds are substantially closed: the successful update triple is ordered and exactly bound, inline payloads are decoded, successful completions exclude failure/terminal keys, the service fence covers applied updates, and the A→B→C positive path preserves immediate/original provenance. Three fail-closed holds remain:
Return the exact head with the full hosted matrix. The successful-triple argument equality and the new unavailable-update-payload reason are otherwise source-clean. |
|
Security emergency stop on exact head f44aee9: do not merge, mark ready, or treat this CI run as qualifying. The single new commit accidentally tracks the validator consumer worktree .regression-corpus-consumer-zqh_l0l2/: 2,303 files under that directory, including a complete nested repository tree and generated dependencies/artifacts. GitHub reports 2,313 changed files and 552,584 additions; Sonar is already red. Against the last held head 1c17ed4, the intended delta is only CHANGELOG.md, src/V2/Support/FailedRunRedrivePlan.php, and tests/Feature/V2/V2FailedRunRedriveTest.php. Remove the entire tracked .regression-corpus-consumer-zqh_l0l2 tree from the branch, ensure validator scratch paths cannot enter the index, and return an exact head whose diff contains only the intended product/test/changelog files. Re-run the complete hosted matrix. The prior functional Security holds remain in force until that clean head is reviewed; no evidence from f44aee9 carries. |
Security's hold on 1c17ed4: - A rejected update, or an acceptance closed by the failure, is skipped only when every event names the same update, command and handler and belongs to the source run. A closed acceptance needs a terminal reason and a failure id, with no result and no step. - reused_from_run_id and reused_recorded_at come as a pair on every update and signal event (wait, receipt and application). A reused event keeps its original time with no fallback to the current event time. - Any external_storage key on signal arguments or an applied value is external, even a null one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
f44aee9 to
c330f98
Compare
|
Acknowledged stop 6616550708. The cause was a validator scratch checkout that New head
I've added the scratch path to the local git exclude and now stage only explicit paths. I'll post the full reply to the functional holds after the hosted matrix for |
|
Security readback of rebuilt exact head The exposure/hygiene incident is contained: its sole parent is the held The functional fixes are source-clean: terminal-closed and rejected update shapes are exactly bound, reuse provenance keys are paired and immediate-source-bound across update/signal chains, and any One hygiene prevention remains a merge hold: the validator deliberately creates |
The validator builds its official consumer tree under the repository root so it can hard-link vendor. A run killed before cleanup left that tree as untracked, committable source. .gitignore now excludes /.regression-corpus-consumer-*/, the prefix is one named constant, and the policy suite runs a real consumer and checks that git lists the tree as ignored, never untracked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
Security source review of exact The hygiene fix closes the prior contamination path:
This head is source-clean. Merge clearance waits only for full hosted CI bound to this exact SHA; no evidence from discarded |
|
Addressed Security's holds 6615950950 and 6616718073. New head
Counterfactuals. All of these refuse without mutation (an
49 of these fail against
Local results: |
|
Security final exact-head clearance: merge The functional review remains satisfied from the clean replacement lineage:
The accidental consumer copy is not part of this head. The hygiene fix is also sufficient:
The exact head is mergeable and terminal with 16 successful checks, 7 intended conditional skips, and no failure or pending check. The discarded contaminated head contributes no CI authority; no secret rotation was required because its copied blobs were already-public repository content. Return for review if the SHA changes. After merge, read back the parents/tree and delete the branch. |
|
Merged on Security's clearance of exact head
|



The update half of A2c from #864, targeted at Workflow 2.20.0. The signal half shipped in 2.19.0 (#1002).
Problem
A failed run whose history contained an update (
UpdateAccepted,UpdateApplied,UpdateCompleted) was refused withunsupported_history. Updates are a common way to change workflow state while it waits, so these runs could only be started over.How updates replay
An update doesn't take a step of its own.
WorkflowExecutor::applyRecordedUpdates()reruns the handler fromUpdateApplied.payload.argumentsat the recordedpayload.sequenceposition, before that step's own handling. A handler that throws records noUpdateApplied, so replay never re-applies it. The successor gets the same events, so it replays exactly as the source would.Change
FailedRunRedrivePlan::updateBundles()groups each update byupdate_id.UpdateAccepted,UpdateAppliedandUpdateCompleted(with an inlineresultand no failure), all bound to the source run, instance,workflow_command_idandupdate_name. It must be attached at a valid step at or before the failure boundary.UpdateCompleted.terminal_reason).unsupported_pending_update: accepted, but never applied or closed. The successor would lose it.external_update_payload_not_supported: arguments or result in external storage.incomplete_update_step/mismatched_update_history: malformed or cross-bound history.successorEvents()writes each bundle ahead of the first reused step at or after its position, withreused_from_run_id/reused_recorded_at. No update row or command is recreated, so nothing is delivered again.service_redrive_history_not_qualified, before any write (Refuse service redrive of reused steps its SDKs can't replay yet #1006's rule extended).docs/api-stability.md. The three update events accept the reuse keys.Tests
Local runs, PHP 8.4, SQLite + Redis:
testRedriveReappliesAnUpdateTheSourceApplieduses a newTestRedriveUpdateWorkflow: activity, wait on agosignal, activity. An updateset-noteis applied during the wait, then the signal is sent and the second activity fails. The successor:note: urgent(the handler's state change replays),workflow_updatesrow,testRedriveRefusesAnUpdateTheSourceNeverResolved: with onlyUpdateAcceptedleft, the redrive refuses withunsupported_pending_updateand no mutation (runs, history, cursor, current run).DefaultWorkflowControlPlaneRedriveRejectionTestservice-fence provider has a newapplied updatecase. It refuses withservice_redrive_history_not_qualified, with byte-identical engine tables and projections.main(refused there withunsupported_history).V2FailedRunRedriveTest30/30,V2UpdateWorkflowTest31/31,FailedRunRedrivePlanTest,HistoryEventPayloadContractTest,SurfaceStabilityContractTestandReplayRegressionCorpusTestall pass. The rejection suite is 48/49; the one failure is the known local-only cache case, which also fails on main.redrive-reused-updatefixture and scenario.validate-regression-corpus.py --base-ref 28893f97passes with replaycounterfactual_fixture_paths: 1.ecs(check-only after fix) andphpstanare clean.🤖 Generated with Claude Code