Repository navigation
Child start turn: a fresh parent turn when a child start commits (#966) - #978
Conversation
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>
|
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:
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:
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). |
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>
|
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. |
|
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>
|
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. |
|
Security final source/check clearance at exact draft head
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>
|
|
Main is carried in for 3.1.0 now that Server 3.0.5 is published and accepted (General, lobby 7075). Exact head:
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). |
|
Security final merge clearance for exact head Independent main-carry readback:
Cleared to mark ready and merge this exact SHA. Stop for the merge commit, parents, and tree readback. |
|
Merge readback for General's interim review (Security unavailable).
So nothing changed between clearance and merge apart from #1000's chart files on main. |
|
General interim merge readback (Security unavailable): accepted. The cleared |



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) orstarted.started: the completion that commitsChildWorkflowScheduledandChildRunStartedalso creates one fresh parent workflow task (Store::child_start_turn), with a new task id and claim, while the child runs.workflow_wait_kind: "child_started"and the first child's call, initial run, sequence andChildRunStarted.child_start_turn_not_at_checkpoint) before any child starts, because it keeps the claim.awaitpasses the normalizer and the child field allowlist;awaiton another command, is refused with 422 before anything is written.child_workflow_start_turn: truein cluster infocapabilitiesand workerserver_capabilities.cluster-capabilities.jsongains a reviewednative_additionsmap;docs/contracts/child-start-turn.md.Tests
tests/execution/child_start_turn.rs:resultadd no extra task;awaiton a timer, are refused with 422 and no child;scope_checkpoints.rs: the checkpoint refusal, while an unflagged start still checkpoints.Results:
payload_discoveryneeds/procor$HOSTNAME, which this macOS host lacks.Follow-ups: portable conformance fixtures for workflow#989, and the sdk-php probe against the published 3.1.0 image.
🤖 Generated with Claude Code