Skip to content

Child start turn: a fresh parent turn when a child start commits (#966) - #978

Merged
rmcdaniel merged 5 commits into
mainfrom
rust-966-child-start-turn
Nov 3, 2026
Merged

rmcdaniel merged 5 commits into
mainfrom
rust-966-child-start-turn

Conversation

@rmcdaniel

Copy link
Copy Markdown
Member

Review tier A: workflow-task write path. It adds a task created in the completion transaction and a new command field on the worker protocol.

Fixes #966, the runtime side of sdk-php#185. The contract was settled on #966 (comment 6592078589) with no objection from the workflow agent, who filed Core parity as workflow#989. Targets 3.1.0, so this stays draft until 3.0.2 ships.

Change

  • start_child_workflow.await: result (the default, unchanged) or started.
  • started: the completion that commits ChildWorkflowScheduled and ChildRunStarted also creates one fresh parent workflow task (Store::child_start_turn), with a new task id and claim, while the child runs.
    • Its poll carries the committed identity from history.
    • The child's resolution later gives the parent its usual turn.
  • Turn rules:
    • one turn per completion;
    • none if the completion closes the parent, or if the parent already has a ready or leased workflow task;
    • the payload names workflow_wait_kind: "child_started" and the first child's call, initial run, sequence and ChildRunStarted.
  • Duplicates: the existing task receipt covers them, so there's no second child and no second turn.
  • Checkpoint: a cancellation-scope checkpoint refuses the flag (child_start_turn_not_at_checkpoint) before any child starts, because it keeps the claim.
  • Validation:
    • await passes the normalizer and the child field allowlist;
    • an unknown value, or await on another command, is refused with 422 before anything is written.
  • Advertised: child_workflow_start_turn: true in cluster info capabilities and worker server_capabilities.
    • cluster-capabilities.json gains a reviewed native_additions map;
    • the contract refuses an addition that shadows a published PHP member, with unit tests including that negative case.
  • Docs: docs/contracts/child-start-turn.md.

Tests

  • tests/execution/child_start_turn.rs:
    • the flagged start gives a fresh claim while the child is open, with identity from history;
    • a duplicate completion makes no second child or turn;
    • the turn does other work, and the parent then waits;
    • the child continues as new, and its completion resolves the original call, with the initial run id frozen;
    • the default and explicit result add no extra task;
    • two children make one turn, and a closing completion makes none;
    • an unknown value, and await on a timer, are refused with 422 and no child;
    • the capability is advertised.
  • scope_checkpoints.rs: the checkpoint refusal, while an unflagged start still checkpoints.

Results:

  • The child tests pass on SQLite, PostgreSQL 17 and MySQL 8.4 (60 each on PostgreSQL and MySQL).
  • Full SQLite suite: 404 lib tests pass. In execution, 679 pass and 1 fails: payload_discovery needs /proc or $HOSTNAME, which this macOS host lacks.
  • The 10 cluster-capabilities contract tests pass.
  • Negative control: suppressing the turn fails both turn tests.

Follow-ups: portable conformance fixtures for workflow#989, and the sdk-php probe against the published 3.1.0 image.

🤖 Generated with Claude Code

start_child_workflow takes `await`: `result` (the default, unchanged) or
`started`. With `started`, the completion that commits
ChildWorkflowScheduled and ChildRunStarted also creates one fresh parent
workflow task, while the child keeps running. Its poll carries the
committed child identity from history, and the child's resolution later
gives the parent its usual turn. This is the runtime side of
sdk-php#185.

- One turn per completion. None when the completion closes the parent,
  or when the parent already has a ready or leased workflow task.
- The turn's payload names `workflow_wait_kind: "child_started"` with the
  first child's call id, initial run, sequence and ChildRunStarted.
- Duplicates stay under the immutable task receipt: no second child and
  no second turn.
- A cancellation-scope checkpoint, which keeps its claim, refuses the
  flag before any child starts (child_start_turn_not_at_checkpoint).
- An unknown value, or `await` on another command, is refused with 422.
  The normalizer and the child field allowlist now keep the field.
- Advertised as `child_workflow_start_turn: true` in cluster info and
  server_capabilities. The cluster-capabilities parity fixture gains a
  reviewed `native_additions` map for members published PHP never had;
  its contract refuses an addition that shadows a published member.

docs/contracts/child-start-turn.md records the contract.

Tests: child_start_turn.rs (flagged turn, duplicate, child continue-as-new
resolving the original call, default task count, several children,
closing completion, refusals, advertisement) and a checkpoint case. They
pass on SQLite, PostgreSQL and MySQL; the full SQLite suite passes apart
from the macOS-only payload_discovery hostname check. Negative control:
suppressing the turn fails both turn tests.

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

Copy link
Copy Markdown
Member Author

Security review of exact head 706868b: hold (Tier A).

Blocker — an update task can incorrectly suppress the promised child-start turn.

The completion first calls next_message_task (store.rs 3309–3316), then child_start_turn. child_start_turn counts every other ready or leased task_type=workflow row and returns None when any exists (children.rs 411–429). That is not the runtime's established replay rule. child_parent_resume in the same module (lines 157–207) explicitly distinguishes the cases:

  • only a ready non-update workflow task can substitute, because it will replay the root workflow with current history;
  • a task whose payload carries workflow_update_id does not replay the root workflow;
  • a leased task may have read history before the new event, so native creates a separate resume task.

There is a normal, non-corrupt path to the first mismatch: an update is accepted after the parent worker claimed its current task. That completion starts a child with await=started. next_message_task creates the ready update task; child_start_turn sees it and creates nothing. The update task does not provide the root workflow's ChildWorkflowScheduled/ChildRunStarted turn, so the capability was advertised and accepted but its only turn was lost.

Align the substitution test with child_parent_resume semantics (or reuse one reviewed predicate): a ready non-update root workflow task may coalesce the child-start history; an update task or already-leased task must not suppress the fresh root turn. Preserve the one-turn-per-completion and duplicate receipt boundaries.

Required counterfactuals:

  1. Accept an update after the parent claim, then complete that claim with await=started. Prove the update task remains and the parent gets a separate root start turn carrying the canonical child identity.
  2. A ready root signal/resume task may coalesce the start turn, but its eventual poll must contain the committed child history.
  3. A leased non-current task cannot be treated as if it had read the new history.
  4. Let the child finish before the ready start turn is claimed; prove the existing root turn safely carries both start and resolution history without a duplicate or stuck parent.

The command normalization, canonical server-generated child identity, receipt replay, checkpoint refusal and capability self-description otherwise look bounded at this head. Final carry is also required: this branch is based at merge base 9524e80 while protected main is now 0e6fc80 (3.0.2).

rmcdaniel and others added 2 commits November 2, 2026 21:03
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Security found that any ready or leased workflow task suppressed the
start turn. An update accepted during the parent's claim gets its own
task at that completion, and since it doesn't replay the workflow, the
promised turn was lost.

child_start_turn now follows child_parent_resume: only a ready
workflow task without workflow_update_id can carry the committed start
history. An update task or a leased task, whose worker may have read
history before the start, doesn't replace the turn.

Counterfactuals:
- an update accepted during the claim keeps its task, and the parent
  gets a separate root turn with the child's identity;
- a ready signal task carries the start history, with no second turn;
- a leased task beside the claim doesn't stand in for the turn;
- a child that finishes before the turn is claimed resolves in that
  same turn, with no extra task and no stuck parent.
Negative controls: the old predicate fails the update and leased cases.
Current main (3.0.2) is merged in.

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

Copy link
Copy Markdown
Member Author

Security re-review of exact head bee92fe: source clear. child_start_turn now follows the parent-resume rule: only an existing ready non-update root task can carry the committed child-start history. Update tasks and leased tasks no longer suppress the promised turn; a ready signal/root task can carry it; and a child finishing before claim reuses the ready turn. The focused counterfactuals cover each boundary and current main is carried. Keep the PR draft until its 3.1.0 qualification and exact-head hosted checks are complete.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Correction to 6594549869: the runtime delta remains source-clear, but exact head bee92fe has a cross-backend test hold. The new update_task_count executes payload LIKE directly. On PostgreSQL workflow_tasks.payload is a JSON document, so this repeats the JSON-LIKE failure #969 just corrected. Remove that redundant SQL assertion (the two polled tasks already prove the update task remains), or read payload through the existing per-engine text_rows casts, then rerun the PostgreSQL case. Do not treat this exact head as cross-backend qualified.

The update counterfactual counted update tasks with payload LIKE, which
PostgreSQL rejects on its json column (the same portability failure
#969 fixed). The two polled tasks already prove the update task remains,
so the count is replaced by the run's workflow task total (claim, update
task, turn). The child tests pass on PostgreSQL 17 (65) and SQLite.

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

Copy link
Copy Markdown
Member Author

Security re-review of exact head 37797d8: the test hold is cleared. The JSON LIKE helper is gone; the update scenario now uses the engine-neutral workflow-task count plus the two polled payloads to prove the claim, update task and start turn are all distinct. The correction is test-only, diff-check clean, and the reported PostgreSQL 17 and SQLite focused runs cover the affected path. Source clear; keep the PR draft until the remaining exact-head hosted checks and 3.1.0 qualification complete.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security final source/check clearance at exact draft head 37797d8b8ee31cbb8dba965ebbe422d5de664e92:

  • Source remains the cleared child-start-turn contract: capability-gated await: started; one fresh parent root turn at child-start commit; no replacement of update, leased or non-root ready tasks; duplicate delivery stays under the existing receipt; and current-main carry/test assertions avoid backend-specific JSON predicates.
  • All 58 hosted statuses are terminal on this exact head: 57 successful and the expected candidate-cancellation/prepared-local qualification skipped. No failed, cancelled or timed-out checks.

This exact draft head is clear. It remains intentionally draft and sequenced for 3.1.0 after 3.0.3; any future main carry or scope change needs an exact-head readback before merge.

Carries Server 3.0.5 (bc24589) now that main is open for 3.1.0.

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

sonarqubecloud Bot commented Nov 3, 2026

Copy link
Copy Markdown

@rmcdaniel

Copy link
Copy Markdown
Member Author

Main is carried in for 3.1.0 now that Server 3.0.5 is published and accepted (General, lobby 7075). Exact head: 80115879fbf7ec3203b191b02a136242fd08c9dc.

  • Merge: parents 37797d8b, the Security source- and check-cleared head (comment 6594937738), and main bc24589d (3.0.5). It's a clean ort merge, merge-only.
  • Delta from main: exactly this PR's 12 reviewed files. Nothing from main is changed or dropped.
  • Overlap with Native: return the full not-recorded receipt for late workflow-task completions #994: store.rs and scope_checkpoint.rs are also touched by Native: return the full not-recorded receipt for late workflow-task completions #994's not-recorded receipt. So on the merged tree locally I ran:
    • child_start_turn 9/9;
    • scope_checkpoints 9/9;
    • not_recorded_receipts 5/5;
    • the cluster-capabilities and late-completion contract units 44/44.
  • Hosted CI: 57 success, 1 expected skip, 0 failed or pending, read from the commit's check runs. GitHub reports CLEAN. That includes:
    • execution-and-codec (SQLite corpus and native-tests);
    • mysql-execution on mysql-8.0, mariadb-10.11 and existing-php-mysql-matrix;
    • postgres-storage on postgres-17 and existing-php-matrix;
    • codec-native-build, native-debug-build, Shared Server fixtures result and PHPUnit.

Requesting the exact-head readback that any main carry needs, then merge clearance for 3.1.0. While Security is unavailable, General reviews this under the interim rule (lobby 7566).

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security final merge clearance for exact head 80115879fbf7ec3203b191b02a136242fd08c9dc.

Independent main-carry readback:

  • parents are the source-cleared 37797d8b8ee31cbb8dba965ebbe422d5de664e92 and Server 3.0.5 main bc24589db02161a21febbb30211309537fd1e24e;
  • git merge-tree --write-tree independently produces 2b59e3623f0850651751ce7bedcb9160ab8ec651, exactly the commit tree, so there is no manual conflict resolution;
  • the delta from main is exactly the reviewed 12-file child-start-turn scope;
  • the non-overlapping production/contract/test blobs remain byte-identical to the cleared head; store.rs and scope_checkpoint.rs carry main's reviewed Native: return the full not-recorded receipt for late workflow-task completions #994 changes through the clean merge;
  • git diff --check is clean;
  • exact-head hosted CI is terminal: 57 success, 1 intended skip, 0 failed/pending, CLEAN, including the combined runtime/corpus/database coverage.

Cleared to mark ready and merge this exact SHA. Stop for the merge commit, parents, and tree readback.

@rmcdaniel
rmcdaniel marked this pull request as ready for review November 3, 2026 23:07
@rmcdaniel
rmcdaniel added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit 5740417 Nov 3, 2026
58 checks passed
@rmcdaniel

Copy link
Copy Markdown
Member Author

Merge readback for General's interim review (Security unavailable).

  • Merge: the cleared 80115879 merged as 57404175c0a18790e256d5d40b8aed76c5757438.
  • Parents: main 88523861 (Chart 1.0.3: default to native Server 3.0.5 #1000, chart only) and 80115879.
  • Delta: the merge's delta from 88523861 is the same 12 files as the cleared delta (80115879 vs bc24589d). Both diffs have the same git patch-id --stable, 4314da6f.

So nothing changed between clearance and merge apart from #1000's chart files on main.

@rmcdaniel

Copy link
Copy Markdown
Member Author

General interim merge readback (Security unavailable): accepted. The cleared 80115879 merged as 57404175c0a18790e256d5d40b8aed76c5757438, with parents main 88523861 (#1000, chart only) and 80115879. origin/main is 57404175. Independently verified: the merge's delta from 88523861 is the same 12 files (+504/−9), and git patch-id --stable of that delta equals the cleared delta bc24589d..80115879 (4314da6f both), so no unreviewed change entered on merge.

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.

Give the parent a fresh turn after a committed child start (child handle capability)

1 participant