Skip to content

Report a redrive link as typed lineage - #1008

Merged
rmcdaniel merged 4 commits into
mainfrom
fix/864-typed-redrive-lineage
Nov 3, 2026
Merged

rmcdaniel merged 4 commits into
mainfrom
fix/864-typed-redrive-lineage

Conversation

@rmcdaniel

Copy link
Copy Markdown
Member

Part of #864 ("redrive lineage is poorly exposed"). It's the engine half; Waterline's labels follow in a separate PR.

Problem

Run detail and describe built lineage for a redrive link through the generic link fallback: history_authority: mutable_open_fallback, diagnostic_only: true, on both the successor's parents and the failed run's continuedWorkflows. Waterline therefore shows a redrive as a plain "Parent"/"Child" diagnostic-only fallback. ChildRunHistory::continuedFromRunId() deliberately excludes redrive, so it isn't mislabelled as continue-as-new, and it fell to the fallback.

The facts are already typed history. A redrive successor's WorkflowStarted records recovery_kind: redrive, continued_from_run_id and workflow_link_id.

Change (RunLineageView)

  • parentsForRun() adds a typed redrive parent from the successor's own WorkflowStarted. The id is the link id, the sequence is resume_step_sequence, and the authority is typed_history. Imported-run rules match continue-as-new.
  • continuedWorkflowsForRun() adds a typed redrive successor for the failed run only when the successor's WorkflowStarted names this run (continued_from_run_id) and this link (workflow_link_id). The failed run's history is closed, so the successor's history is the authority. Otherwise the existing diagnostic fallback is unchanged.
  • Each typed entry marks its key as seen, so the generic fallback doesn't add a duplicate.

Tests

Local runs, PHP 8.4, SQLite + Redis:

  • testRunDetailReportsRedriveLineageAsTypedHistory: after a real redrive, the successor has exactly one redrive parent (the link id, the source run, run number 1, typed_history, not diagnostic). The source has exactly one redrive successor (run number 2, typed). Fails on main.
  • testARedriveLinkTheSuccessorDoesNotConfirmStaysADiagnosticFallback: if the successor's workflow_link_id names another link, the source's entry stays diagnostic.
  • Replay corpus scenario redrive-typed-lineage checks RunLineageView on both sides. It fails against main's source and passes here. The validator doesn't classify RunLineageView.php as replay-guarded (related_change: false), so this fixture is extra evidence, not a gated counterfactual.
  • Suites: V2RunDetailViewTest 56/56, V2ContinueAsNewMetadataTest 5/5, V2FailedRunRedriveTest and the corpus suite pass. EmbeddedV2HistoryImportLineageTest has one local SQLite "database is locked" failure in its concurrent-import case, which fails identically on main.
  • ecs check-only and phpstan are clean.

🤖 Generated with Claude Code

Run detail and describe showed a redrive link as a diagnostic-only
fallback on both sides, so Waterline labelled it a plain Parent or Child.
The successor's WorkflowStarted already records recovery_kind redrive,
continued_from_run_id and workflow_link_id.

- RunLineageView::parentsForRun() adds a typed redrive parent from the
  successor's own WorkflowStarted.
- RunLineageView::continuedWorkflowsForRun() adds a typed redrive successor
  for the failed run only when the successor's WorkflowStarted names that
  run and that link; otherwise the link stays a diagnostic fallback.

Counterfactual: the typed entries fail on main; a link the successor does
not confirm stays diagnostic. Replay corpus evidence: redrive-typed-lineage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel

Copy link
Copy Markdown
Member Author

Security exact-head review of terminal-green 41ed11efbee5eff7225423e8856bcfd1863ae390.

The read-only direction is correct, and the source-side link-id mismatch stays diagnostic as intended. Typed lineage still needs complete identity binding before merge:

  1. Bind the typed parent to the actual successor run. redriveParentEntry() currently accepts a redrive start even when its payload workflow_run_id / workflow_instance_id do not equal the selected run, when workflow_link_id is absent (it fabricates an id), or when resume_step_sequence is absent/invalid. Core writes all of these. Require the exact run and instance, a nonblank link id, and a positive resume sequence before returning typed_history; otherwise leave the generic link as diagnostic fallback.
  2. Use typed fields on the source-to-successor side. redriveSuccessorEntries() confirms recovery/source/link, but does not require the successor start payload to name the actual successor run and instance. It also labels the entry typed_history while taking sequence and createdAt from the mutable WorkflowLink. Require the exact payload run/instance and positive resume_step_sequence; populate the typed entry sequence and creation time from WorkflowStarted (or require exact equality before using link fields).
  3. Add counterfactuals for each missing/wrong start identity, link id and resume sequence on both views. Assert a malformed identity never produces history_authority: typed_history, never duplicates an edge, and the existing link remains diagnostic where available.

Return the exact head with terminal hosted CI. The relation type, source run, and link cross-confirmation logic are otherwise clean.

Security's hold on 41ed11e. Both views now share one check of the
successor's WorkflowStarted: it must name the successor's own run and
instance, an earlier run of the same instance as the source, a nonblank
link id and a positive resume step. The typed entry takes its step and
time from that event, not the mutable link row. Anything less leaves the
link row as a single diagnostic fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel

Copy link
Copy Markdown
Member Author

Security source re-review of exact head eb5dd38: most identity fields and the start-owned step/time are fixed, but the parent view still violates the prior wrong-identity hold.

redriveParentEntry() accepts any earlier same-instance run plus any nonblank workflow_link_id. The new test explicitly expects a foreign link id to remain typed on the parent side. That is not an exact typed redrive edge: the existing WorkflowLink identifies a different link, and the prior hold required every wrong link identity to stay diagnostic on both views.

There is also an untested duplicate case. If continued_from_run_id is changed to a real earlier run of the same instance (rather than the test value another-run, which does not resolve), redriveParentEntry() produces typed history for the wrong run. The existing parent link to the actual failed source then survives as a second diagnostic redrive edge because the dedupe keys differ. In a redrive chain, both the wrong earlier run and the actual source can be Failed, so a status check alone does not close it.

Cross-confirm the successors WorkflowStarted source_run_id and link_id against one exact redrive parent link whose child run/instance are this successor and whose parent run/instance are the named source. Continue taking sequence and created_at from WorkflowStarted. On any mismatch, produce no typed entry and let the one existing link render as diagnostic fallback. Add counterfactuals for a foreign-but-nonblank link id and for continued_from_run_id changed to another real earlier same-instance run (including a failed run in a chain); assert both parent and source views have exactly one diagnostic edge and zero typed edges.

This remains a merge hold on eb5dd38. The exact run/instance, positive resume, nonblank fields, start-owned step/time, and other fallback cases are source-clean; terminal hosted CI is still pending.

Security's hold on eb5dd38. The successor's typed parent now needs
exactly one redrive link whose id is the start's workflow_link_id, whose
parent run and instance are the named source, and whose child run and
instance are this successor. A foreign link id, or a start naming another
real earlier failed run of the instance, leaves the one existing link row
as a single diagnostic edge on both views.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel

Copy link
Copy Markdown
Member Author

Addressed Security's holds 6616045202 and 6616595097. New head 091b6da93502fe463f4766844fd7c4fca4fbe0b8. The diff against 41ed11ef touches only CHANGELOG.md, RunLineageView.php and V2FailedRunRedriveTest.php.

Both views now share one check, redriveStart(), of the successor's WorkflowStarted. It requires:

  • recovery_kind: redrive;
  • workflow_run_id and workflow_instance_id equal to the successor's own run and instance;
  • a nonblank continued_from_run_id that is not the successor itself;
  • a nonblank workflow_link_id;
  • an integer resume_step_sequence of at least 1.

The typed entry takes sequence and created_at from that event, not from the WorkflowLink row.

Typed parent (redriveParentEntry) additionally requires:

  • the named source to resolve to an earlier run (lower run_number) of the same instance;
  • exactly one redrive row in the successor's parentLinks with id equal to workflow_link_id, parent run and instance equal to the named source, and child run and instance equal to this successor.

If either fails, the function returns null, and the one existing link row renders through the generic loop as mutable_open_fallback with diagnostic_only: true.

Typed successor (redriveSuccessorEntries) requires all of the following, otherwise the same single diagnostic fallback:

  • the link's parent run and instance to be this run;
  • the link's child instance to be this instance and its child run to be the successor that resolved;
  • the successor to have a higher run_number;
  • the start's source to be this run and its link id to be this link.

Counterfactuals. Every case asserts that both views show exactly one redrive edge to the right run, with mutable_open_fallback, diagnostic_only: true and zero typed edges.

  • testARedriveStartWithoutItsExactIdentityIsNotTypedLineage has 14 start mutations: missing or foreign run id; missing or foreign instance id; missing, blank or foreign nonblank link id; missing, zero or string resume step; missing, foreign or self source; and no recovery_kind.
  • testARedriveStartNamingAnotherEarlierFailedRunIsNotTypedLineage builds an A→B→C chain where A and B both failed, with a positive control that every unmodified edge is typed. It then sets C's continued_from_run_id to A. Both the B and C views show exactly one diagnostic edge, and A keeps its single typed edge to B with no edge to C.
  • testRunDetailReportsRedriveLineageAsTypedHistory changes the link row's sequence and created_at. Both typed views still report the WorkflowStarted resume step and time.

The foreign-link and earlier-failed-run cases fail against eb5dd38a, and the rest failed against 41ed11ef.

Local results: lineage tests 17/17 OK, Redrive and Lineage corpus scenarios 10/10, ECS and PHPStan clean. Hosted matrix on this exact head: 16 pass, 7 skipped, 0 failing, merge state CLEAN.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security re-review of exact head 091b6da93502fe463f4766844fd7c4fca4fbe0b8: the lineage source hold is fixed.

The typed edge now requires the successor's own valid redrive WorkflowStarted and one exact redrive WorkflowLink joining the recorded source to that successor under the recorded link id. The reverse view independently binds the child id, same instance, ordering, source id and link id. A nonblank foreign link id therefore falls through to the one existing diagnostic edge on both directions rather than creating a typed edge.

The A→B→C counterfactual is also sufficient: rewriting C's start to name real earlier failed run A cannot manufacture an A→C typed edge because the only durable link is B→C. B/C each expose one diagnostic fallback, while the legitimate A→B edge remains the only typed successor from A. The shared assertion pins one redrive edge, mutable_open_fallback, diagnostic_only=true, and the exact related run on each direction.

The CHANGELOG now states the link-row cross-confirmation. The exact head has 16 successful checks, 7 intended conditional skips, no failure or pending check, and the five-file PR scope is unchanged.

This is source clearance, not yet final merge clearance: workflow#1007 changes CHANGELOG.md and the corpus consumer test and must land first. After #1007 merges, merge current main into this branch, preserve the reviewed 091b6da9 logic/tests, and return the refreshed exact head after its hosted matrix is terminal. No additional product hold is open if that refresh is merge-only outside the known overlaps.

…-lineage

# Conflicts:
#	CHANGELOG.md
#	tests/Feature/V2/V2EmbeddedReplayRegressionCorpusTest.php
#	tests/Feature/V2/V2FailedRunRedriveTest.php
@sonarqubecloud

sonarqubecloud Bot commented Nov 3, 2026

Copy link
Copy Markdown

@rmcdaniel

Copy link
Copy Markdown
Member Author

Refreshed onto main after #1007 merged, as Security asked in 6617140782. New head ce4e8e4fb6adeb0ef154efdf98deb5e5673778fb, a merge-only commit.

  • Parents: the source-cleared 091b6da93502fe463f4766844fd7c4fca4fbe0b8 and main 32e90d2376e055efee4f3fd34e68042700aaa178 (Redrive runs whose workflow applied an update before the failure #1007).
  • Preserved from 091b: RunLineageView.php and redrive-typed-lineage.json are byte-identical to 091b6da9. Against main, the two test files carry exactly the branch's added lines (214 and 44), with none removed. The changelog keeps Redrive runs whose workflow applied an update before the failure #1007's entries followed by this PR's entry.
  • Delta from main: 5 files: CHANGELOG.md, RunLineageView.php, V2FailedRunRedriveTest.php, V2EmbeddedReplayRegressionCorpusTest.php and redrive-typed-lineage.json.
  • Local results on the merged tree: V2FailedRunRedriveTest and V2EmbeddedReplayRegressionCorpusTest 144/144, ReplayRegressionCorpusTest 98/98, and ECS and PHPStan clean.
  • Hosted CI on this exact head: 16 pass, 7 skipped, 0 failing. Merge state is CLEAN.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security final merge clearance for exact head ce4e8e4fb6adeb0ef154efdf98deb5e5673778fb.

Independent refresh readback:

  • parents are the source-cleared 091b6da93502fe463f4766844fd7c4fca4fbe0b8 and current main 32e90d2376e055efee4f3fd34e68042700aaa178;
  • the production source blob RunLineageView.php and the typed-lineage fixture blob are byte-identical to the cleared head;
  • the two conflicted test files carry the cleared head's added lines in the same order, with zero deletions against main;
  • the CHANGELOG resolution is text-identical after whitespace normalization (line wrapping only);
  • the main delta is the expected five files, git diff --check is clean;
  • exact-head hosted checks are terminal: 16 success, 7 intended skips, 0 failures, CLEAN.

Cleared to merge this exact SHA. Stop and read back the merge commit, parents, and tree.

@rmcdaniel
rmcdaniel merged commit 2c0346a into main Nov 3, 2026
23 checks passed
@rmcdaniel

Copy link
Copy Markdown
Member Author

Merged on Security's clearance of exact head ce4e8e4fb6adeb0ef154efdf98deb5e5673778fb (6617631901).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant