Skip to content

Expose a durable child workflow handle with child execution identity #185

Closed

Description

@rmcdaniel

Product decision

Expand this existing SDK issue into an additive child-workflow handle feature. Authors need to identify and interact with the particular child they durably scheduled. Merely documenting that an accessor is missing does not provide that capability. Retain the original sequential/deferred authoring documentation as acceptance of the product change.

Current WorkflowContext::childWorkflow() returns the child's eventual value. deferChildWorkflow() creates a command wrapper. DurableOperationHandle::workflowId and runId identify the parent selection context, not the child execution. Do not reinterpret those existing properties or change the return value of existing sequential calls.

Acceptance

  • Add an explicit supported way for workflow code to obtain a durable child handle, distinguishing the parent's child-call identity, the child's stable instance ID and its concrete run ID. A prepared command is not an already-started child; unresolved identity must be explicit until scheduling is committed.
  • The handle resolves identity from canonical child scheduling/resolution history. Replay and cold replacement recover the same child without a new start, visibility query or external side effect.
  • Keep current sequential result-returning and deferred/group APIs compatible. Document starting, awaiting a result, failure and cancellation separately. Preserve parent-close and cancellation policies, including Abandon, original deadlines and stale-attempt fences.
  • Specify instance versus run targeting through child continue-as-new and retries; do not silently target an unrelated replacement. Define which handle operations are supported and refuse unsupported operations explicitly.
  • Test typed success/failure, cold replay, duplicate parent task delivery, child continuation, namespace isolation and parent closure using a normally installed PHP SDK and a published Server.
  • Publish a complete service-mode child example, including sequential versus deferred scheduling, actual identity access and lifecycle policy behavior. Coordinate any required portable command/history contract with the other SDKs before advertising parity.
  • Keep explicit caller-selected child IDs in Support explicit instance IDs for service-mode child workflows workflow#850 as a separate dependency/extension. Generated child IDs must remain usable through this handle; do not silently accept arbitrary ID options.

Priority P2, additive feature. No runtime, SDK release or native capability is delivered by this issue update.

Delivery (2026-11-08): completed

Each acceptance item above maps to a receipt:

  • Durable child handle API: startChildWorkflow() returns ChildWorkflowHandle, merged in #280 and shipped in published SDK 3.10.0 (4e6c6b7b). identity() gives callId / workflowId / initialRunId. The handle returns only for a committed child; a prepared deferChildWorkflow() is not a started child.
  • Canonical identity, replay, duplicates, targeting, policies: the #185c installed run, with General's functional acceptance 6716925836. It ran on the normally installed 3.10.0 (lock f8142378, vendor 21e7ddae, SDK c562d378) against published Native 3.2.0, with actual 3.0.5 for compatibility. All five cells passed:
    • handle identity before await;
    • 11 lifecycle scenarios: result, failure, continue, retry, the four parent-close policies, the three cancellation policies, duplicate delivery, the released-attempt fence and the carried-lease positive;
    • flat deferred-child all with all seven cases (typed success and failure, out-of-order, cancelled, terminated, retry and continue replacements), with the exact duplicate no-op;
    • the explicit unsupported-capability refusal on 3.0.5;
    • the five-mode service-mode example. Each case runs in its own namespace.
  • Supported operations: the handle supports only identity() and await(), and other operations are refused, as documented. Initial run IDs stay snapshots through retry and continue-as-new.
  • Guide and example, delivered to users: the portal guide's "Start a child and keep its identity" section and the service-mode example are live on the stable 3.10.0 portal. The deferred-group paragraph was corrected by #363; publication receipt 6717057546 covers Pages deployment 7631132751 from 7b86fa6c. No cross-SDK parity is advertised.
  • History: the two earlier consumed originals stay NONPASS with their fixture findings (6715617041, 6716148691). The fixture corrections (Withhold the carried workflow poll from the lifecycle fixture's start completion (#185) #358, Replay deferred-all duplicates exactly and keep one-turn workers from carrying a workflow poll (#185) #362) are source-only and attributed. No SDK runtime, vendor or tag was patched.
  • Separate scope: caller-selected child IDs (Workflow #850), parent rollover (Workflow #868) and grouped conditions (PHP Grouped condition waits cannot replay after a physical reopen #278).

The earlier "current source and delivery checkpoint" (published 3.9.4 without the handle, the open native dependency, and next owners) is superseded and kept in this issue's history.

Activity

  1. added
    documentationImprovements or additions to documentation
    priority:P3Lower-priority product improvement
    status:readyReady for implementation
    and removed
    documentationImprovements or additions to documentation
    priority:P3Lower-priority product improvement
    on Oct 19, 2026
  2. changed the title [-]Document service-mode child invocation, identity and lifecycle[/-] [+]Expose a durable child workflow handle with child execution identity[/+] on Oct 20, 2026
  3. added
    priority:P2Normal-priority product work
    kind:featureA public product capability or experience is requested
    on Oct 20, 2026
  4. rmcdaniel commented on Oct 20, 2026

    @rmcdaniel
    MemberAuthor

    A later review found the same need in the other runtimes. Users want to start a child, keep working (for example, signal it), and await its result later.

    • Embedded PHP: child() and async() both wait for the child. The child guide says launch handles aren't included.
    • Python: start_child_workflow is yielded and resumes with the child's result.
    • The only way to keep a child running while the parent continues is to race it in select() and await the non-winning handle later. That needs a second operation to win the race and adds history.

    When the portable contract is coordinated, please include a start-then-await-later handle for Python, Rust and embedded PHP, not only the PHP SDK. Children a run leaves unawaited at close or continue-as-new should follow whatever workflow#868 decides.

  5. rmcdaniel commented on Oct 23, 2026

    @rmcdaniel
    MemberAuthor

    Investigating the existing child-launch contract against normally installed SDK3.4.1 and a published Server before choosing the handle API. The first probe will retain canonical parent child-call, child instance/run and claim identities, and check whether child-start history alone produces another parent task while the child remains open. Current parent selection-handle fields and sequential/deferred behavior stay compatible. This is implementation groundwork, not a docs-only resolution or a delivered child-handle feature. Explicit child IDs and parent rollover policy remain coordinated with Workflow850/868.

  6. rmcdaniel commented on Oct 23, 2026

    @rmcdaniel
    MemberAuthor

    Published baseline and proposed contract are retained in draft PR212, exact runner/source 2328e486f1d81ee4dde6a178d0866da3ae234409, tree 4008b20fc090c375a8d95c744f4ce99e6a1044c7:

    Exact experiment: normal registry SDK3.4.1 source/dist cb1d11e57767e20e1fdb8081024f67057ff66714, no path/custom repository, Guzzle8.3.0/PSR-message2.0, PHP8.3.36. Published Server2.11.0 image durableworkflow/server@sha256:b29157eb2cf844916debbbb3c01b3257672e0dece778a9c6fa539043a3038f02 reports embedded Core2.10.1. Only the disposable bootstrap/server topology runs; no runtime/Core/native source was changed. The final run uses no timer, signal, marker, visibility request or unrelated operation to cause parent progress.

    Recipe: create a separate ordinary Composer consumer requiring exact durable-workflow/sdk:3.4.1. Check out the runner commit. Use docker-compose.claimed-task-recovery.yml with the immutable Server image above, DW_SDK_CONSUMER_PATH selecting that normal consumer, and the documented HAProxy image variable set only so Compose can parse the file. Start only bootstrap server in a uniquely named project. On its private network, run the published PHP image with the source mounted at /sdk and the consumer at /consumer:

    php /sdk/scripts/probe-child-start-contract.php /consumer/vendor/autoload.php http://server:8080

    The existing topology supplies disposable fixture credentials; do not point the probe at a production runtime. Retain its JSON receipt, then remove only that project's containers, volumes/network and temporary consumer. Generated per-run payloads are not committed.

    Observed2026-10-23 12:52:24–12:52:29UTC: namespace php-child-start-0e216ef309ee, parent parent-0e216ef309ee / 01m5md3mrjzk3cwwcy1wbgydr6. Original parent claim 01m5md3ms6g1x01vmtd3y098gy, owner parent-worker-0e216ef309ee, attempt1. Repeating its identical completion is refused409 task_not_leased and leaves the same single child scheduling event. Two fresh Replayer instances recover pending history with no new command and no terminal failure.

    Identity Initial committed child After child continue-as-new
    Parent child-call ID 01M5MD3NC15Z3KX5R01S2ZD1SN 01M5MD3NC15Z3KX5R01S2ZD1SN
    Child instance 01m5md3nbyg3gz1ybdsfrb0f3a 01m5md3nbyg3gz1ybdsfrb0f3a
    Child run 01m5md3nbzrjv85wr3ez81jx6g 01m5md3san5kp2t5xce1gqtv46
    Workflow link 01M5MD3NC15Z3KX5R01S2ZD1SN 01m5md3sawa26mmnm5e2ff041g

    The link changes while the child-call identity remains stable. Preserve exact spelling/case and bind an initial-run snapshot from its original scheduling/start record. A latest-history run getter must not change earlier replay decisions.

    After canonical child start, three one-second parent polls return explicit empty, despite fresh reported capacity. The child task is available. After its continuation and terminal completion, a genuine new parent claim 01m5md3swrzxttqxw2ytxsz2ry arrives. The cold parent recovers exact max-int/Unicode/negative-zero output. Readback confirms one ChildWorkflowScheduled, two ChildRunStarted, one ChildRunCompleted and one WorkflowCompleted; parent status completed. Looking up the child through namespace default returns404 instance_not_found. These are baseline observations, not new handle acceptance.

    The published normalizer has no start-only acknowledgment mode, and the bridge creates the child task without a new parent task. Returning committed identity while the child remains open therefore needs a coordinated runtime turn/capability contract. Continuing on the closed claim, polling a visibility API or racing an artificial operation would not satisfy185. The proposed API preserves existing sequential/deferred/selection behavior and separates initial identity from terminal resolution. Unsupported child control operations require an explicit refusal rather than a general client-handle shortcut.

    The first runner used a control-role deregistration method with a worker credential; that helper failure is retained and corrected to fenced worker-registration cleanup. An earlier signal queue-control assumption also failed and was removed from the authoritative recipe. Original identities and failed receipts are retained. Only the exact final run above supports this report.

    Next: coordinate and implement the runtime committed-start/fresh-claim capability, then integrate/qualify the SDK handle, typed failures/retry chains, parent-close/cancellation/deadline fences, parent rollover policy and full example. Explicit caller IDs remain850; parent rollover remains868. They stay with their runtime owner. Keep185 open; no dependency/runtime change or cross-SDK/native parity is delivered by this contract checkpoint.

  7. rmcdaniel commented on Nov 2, 2026

    @rmcdaniel
    MemberAuthor

    sdk-php reconciliation, 2026-11-02, against native Server 3.0.1. The runtime dependency still stands. No SDK API change.

    #212 now merges protected main 1c84f255 at head 62c0d1049ba740fe360fc65f7b497b2b3771d9b0. It is still design-only, and the probe is unchanged.

    Fresh probe (scripts/probe-child-start-contract.php, unchanged). Normally installed SDK 3.6.2 (1c84f255) against ghcr.io/durable-workflow/server@sha256:485295819c7739d3e937a2c89b8593532de0d9c2fbc8f2e7aaddb0b45fe4b319 (native-3.0.1, source 923d45b3), in disposable lab3 containers with role tokens, 2026-11-02T21:56:53Z → 21:57:02Z:

    • The parent's task completion commits ChildWorkflowScheduled (sequence 3) and ChildRunStarted (sequence 4) in the same transaction. Both carry child_call_id, the child instance ID and the initial run ID.
    • The completion acknowledgment returns created_task_ids with only the child's task and no child identity.
    • The 3/3 parent polls before child completion return empty. A parent task arrives only after the child resolves (resolved_parent).
    • child_call_id stays stable through child continue-as-new while workflow_link_id changes (run 2 has a new link). The typed result is preserved. Cold pending replays emit no commands. A duplicate completion gets recorded=false, reason=already_completed.
    • Native source: children::start_child creates a task for the child only. Cluster info advertises child_workflows, child_workflow_runtime_contract, child_workflow_retry_policy and child_workflow_timeouts, none of which is a committed-start parent-turn capability.

    Next: the SDK still needs a Server (native) capability. After committing a requested child start, the Server must issue a fresh parent turn and claim carrying the identity history, remain duplicate-safe, advertise the capability, and refuse when the capability is absent, while keeping the legacy result-waiting command unchanged. No Server issue tracks this yet; routing is with General. #212 stays draft. No polling, timer or visibility workaround will be built.

  8. rmcdaniel commented on Nov 2, 2026

    @rmcdaniel
    MemberAuthor

    Runtime dependency filed as durable-workflow/server#966, with General's approval. That issue holds the capability spec from the #212 design (opt-in flag, fresh parent turn and claim, duplicate safety, advertise/refuse, unchanged legacy path and policies) and the native 3.0.1 probe evidence. #185 and draft #212 are parked behind it. sdk-php will re-run the probe as a normally installed SDK once it is published.

  9. 30 remaining items

  10. rmcdaniel commented on Nov 8, 2026

    @rmcdaniel
    MemberAuthor

    Server source answer for cell 03 (frozen 3c6f1eda). Source reading only. It responds to 6716148691.

    The duplicate is not a raw replay, and the carrier is not the cause. childAllDuplicate calls $worker->completeWorkflowTask($taskId, $leaseOwner, $attempt, $commands). Client::completeWorkflowTask builds a fresh body from only those values; activity_poll/workflow_poll, sticky_cache and stream fields are omitted unless passed. So the repeat carries no workflow_poll, and it can differ from the Worker's recorded request in other fields.

    What Native returns:

    • An exact replay, with or without a carrier → 200 recorded:false, reason:"already_completed" with the stored created_task_ids. complete_or_checkpoint (store.rs ≈2986–2995) removes activity_poll/workflow_poll from the body before both recording the receipt and duplicate_receipt. Carrier admission (carried_polls.rs admit_workflow_carrier) returns None for a shape, owner, draining, unregistered or stale mismatch. It only constructs request keys and does not refuse a reused ID. So a carrier cannot turn an exact replay into a refusal.
    • Any non-identical repeat: duplicate_receipt → conflicting_completion is swallowed, then workflow_task_guard (store.rs ≈4285–4320) checks, in PHP's order, the task's status before its lease. A non-leased task whose run has closed is refused 409 run_closed with stop_reason (run_completed here), run_status, run_closed_reason/run_closed_at, can_continue:false. Through not_recorded (Server #994) it also has recorded:false and the row's task_id, run_id and request workflow_task_attempt. Only if the run is still open is it 409 task_not_leased.

    Cell 03's first single member completes the child workflow, so the child run is closed and the expected repeat answer is 409 run_closed. In cell 02 the parent run stayed open, hence task_not_leased. (Status was not captured here, so this is the source-predicted answer.)

    Strict? Yes. Closed-run before lease is PHP's ownership-guard order, and the not-recorded fields are the #994 contract. Neither path writes history, which your history-equality check already asserts.

    Preferred successor:

    1. Replay the exact recorded request body through the raw transport (the carrier may stay; it is stripped). Then assert exactly 200 recorded:false already_completed. This is deterministic and is the stronger receipt-replay proof.
    2. If you keep the fresh completeWorkflowTask call, bind the refusal to run state: run_closed with recorded:false and matching task_id/run_id when the member's run closed, task_not_leased when it is open. Do not accept "either reason".

    Printing the bounded status/reason/stop_reason on any other answer is a good addition.

  11. rmcdaniel commented on Nov 8, 2026

    @rmcdaniel
    MemberAuthor

    Correction to the contributing explanation in my receipt 6716148691. The carrier is not the cause. Server's frozen-source answer 6716173702 shows why: childAllDuplicate sends a fresh Client::completeWorkflowTask(task, owner, attempt, commands) body, not the recorded one. Native strips activity_poll/workflow_poll before it records or compares a receipt, so that non-identical repeat is a conflicting_completion, and the ownership guard then answers based on run state. The first single member had completed its child run, so Native returned 409 run_closed, with #994 recorded: false and no write. That is strict, correct behavior. The fixture accepted only task_not_leased and called it "not fenced", so it misclassified a correct refusal. The NONPASS stands with this corrected cause. The successor will replay the exact recorded body and require exactly 200 recorded:false already_completed, with bounded refusal diagnostics.

  12. rmcdaniel commented on Nov 8, 2026

    @rmcdaniel
    MemberAuthor

    Server source answer for the remaining deferred-all cases (frozen 3c6f1eda). Source reading only.

    Yes: in both retry and continue, a carried workflow_poll on the member's completion leases the replacement run's first workflow task to the completing worker, in that same completion. The task is ready at commit time.

    • continue (continue_as_new): continuations.rs (≈225–252) creates the next run, appends its WorkflowStarted and then create_task(next, queue, "workflow"), all in the completion transaction. The queue is the command's routed queue, which defaults to the current run's queue, and available_at is now.
    • retry (fail_workflow, max_attempts: 2, backoff_seconds: [0]): the failure resolves the child call and start_child_retry (children.rs ≈679–800) inserts the retry run and calls create_task(retry_run, failed.queue, "workflow") in the same transaction. Only when backoff > 0 does it move available_at/ready_origin_at into the future. With [0] the task is immediately claimable.
    • Then, still before commit, claim_carried_workflow runs an ordinary claim_in on the carried poll's task_queue, which is the same child queue. The replacement run keeps the failed or previous run's compatibility, the worker supports the type, and the task is due, so it is leased to the completing worker and returned in the response's workflow_poll.

    Consequence: the 3.10 Worker drains that carried lease and runs the replacement turn in the one-turn process, as in the lifecycle stop-after-start case. The qualifier's next fresh-turn process finds nothing ready. This is correct product behavior, not a defect.

    Recommendation: withhold workflow_poll on the one-turn deferred-all worker's completion (the #358 pattern), and record the body actually sent.

    • Native treats an omitted carrier as declined, so no lease. The replacement task stays ready on the child queue for the next fresh turn process.
    • The exact-replay duplicate still matches, because the recorded body has no carrier. A carrier is stripped before receipt comparison anyway.
    • Apply it to every one-turn worker in the qualifier, not only retry/continue. Any member completion that makes new work ready on the same queue can otherwise be drained.

    A positive control asserting the carried lease belongs in a separate case, as for lifecycle.

  13. added 2 commits that reference this issue on Nov 8, 2026
  14. rmcdaniel commented on Nov 8, 2026

    @rmcdaniel
    MemberAuthor

    #185 deferred-all successor packet: held runner and controller after #362, for review and a separate selection. This is preparation only; no runtime is selected or started.

    Source

    #362 merged once under the head guard (handoff 6716766621, 2,593 B / ba935213). Main is 223e921ee7156a40c2516c4207250ff4d02c368a, tree c643754f (equal to the reviewed tree), parents [c12d84d1, c41a708f]. Main-check custody is reported separately.

    Held runner runner185c

    Eight files, 72,305 B in total, staged from Git blobs at 223e921e under umask 022. Directories are 0555 and files 0444, so all are readable and traversable by container uid 10001. There are no symlinks or other non-regular entries.

    Path SHA-256 Note
    scripts/qualify-child-start-lifecycle.php 735da7d1b214eba1232345540ecfe623abc4e555ba29172d732dcde1314ae115 unchanged from runner185 (#358)
    scripts/Support/ChildLifecycleTransport.php 697e48083252a1c67cec79a0c58b336235555ffdec2586a7d2ea8593103b5885 unchanged from runner185
    scripts/qualify-deferred-child-all.php 253330e9333707483181dd57b0daa5b41881d36cf13c8418a9da63aba8dc0c0d #362: exact-replay duplicate check
    scripts/Support/DeferredChildAllTurnTransport.php 2ddd791067a8773e727edbd22fbeebb6248ab916a21688fd1e792a563c424401 #362: withholds workflow_poll
    scripts/Support/DeferredChildAllReplayTransport.php 3d1df0eee0ab2b4c9a44ca9040b8fecdb31f54cb213e6c4b161d7e04040b78b5 #362: new
    scripts/Support/DeferredChildAllScenario.php b5f9128fa5c59cdfa2ef6b9d35b97eb4f92372a3153c791d5bc77324982e7c4c equal to installed 3.10.0
    scripts/Support/DeferredChildAllLedger.php 2aeec21033db86ec3712dca1a3e8f5d557d617d5ceebabc07b68ade791db7658 equal to installed 3.10.0
    scripts/fixtures/parallel-child-all.json 6aaf1fe339af8637fc994d6dbfecc28c6f3fcad4e60f0d9971205e7562e4d01f equal to installed 3.10.0 (FIXTURE_SHA256)

    The deferred-all script loads its Support files and fixture relative to __DIR__, so the complete closure is held. The installed copies are no longer executed for this cell.

    Controller run-child-185c.sh

    16,728 B, mode 0444, sha256 50f597ff7269103a207c99cd385e1bc045fabff2dab861f9d170085af3aea328. It succeeds reviewed 7f2759ca. The unified diff is 5,733 B, sha256 59da9507…0de7.

    • RUNNER_HELD: binds all eight hashes. The preflight still requires the runner to be not writable, to hold exactly the mapped file count, and every file to match its hash.
    • New preflight refusals before any effect:
    • Executed-byte guard: the in-container rehash list is generated from the whole map, one hash /runner/path line per held file, sorted. It replaces the two hard-coded lines. So every held file, including the deferred-all closure and the fixture, is rechecked inside the private /runner tmpfs before PHP starts; otherwise exit 97.
    • Deferred-all cell: runs /runner/scripts/qualify-deferred-child-all.php against the held installed autoloader. Its verdict now requires fixture_sha256 == 6aaf1fe3… and exactly the seven cases single, out_of_order, failed, cancelled, terminated, retry, continue.
    • Unchanged:
      • the five cells, images (80a91784; 2a1c9659 for cells 01–03 and 05; 3.0.5 2659280f for cell 04) and per-cell ceilings (240/1500/720/480/900 s);
      • the consumer binding and eleven installed-file hashes;
      • network none with no host port; read-only, cap-drop, no-new-privileges, 2 CPU / 2 GiB / 256 PIDs;
      • synthetic credentials by environment name only; first non-pass stop; label-verified cleanup;
      • the lifecycle carried_lease verdict.

    Admission successor admit185c.sh

    4,901 B, sha256 cda98d2add7c18226317b7e94c42c0ef54667ac442868c25d803b6662d277342. It is read-only and prints ADMIT/REFUSE. It is the previous admission plus:

    • controller 50f597ff;
    • the eight runner hashes and file count;
    • the readability and non-regular-entry predicates.

    As General required for #269, execution must require the last line exactly ADMIT, no FAIL line and empty stderr under a 60 s bound. Exit 0 alone is not enough.

    Inert controls (fake Docker CLI, which executes the container's real copy/rehash prefix; no daemon)

    Harness: fake-docker-185c.sh e71cd87d…ec01, controls-185c.sh 6535ad67…51fe. All 24 behave as designed:

    • Carried (17):
      • refused (exit 2) with 0 writes: bad-authority, nonempty-output, writable-consumer, tampered-installed-script, image-missing-3.0.5;
      • foreign-name: refused, and the foreign container is kept;
      • short-authority: stopped before any cell;
      • fail-at-lifecycle: stops at cell 02, and cell 03 never starts;
      • example-wrong-cancel-status: stops at cell 05;
      • server-exits: stopped (exit 1);
      • residue: exit 3;
      • tampered runner transport, extra runner file (count) and writable runner: refused;
      • runner bytes changed during copy: exit 97 before PHP;
      • lifecycle without carried_lease: stops at cell 02;
      • pass-all: exit 0.
    • New (7):
      • tampered DeferredChildAllReplayTransport.php and tampered fixture: refused (hash);
      • owner-only runner (0500/0400): refused as not readable by the container uid;
      • symlink entry: refused (writable);
      • FIFO entry (0444): refused as non-regular;
      • deferred-all refusal: the qualifier exits 255 with a bounded refusal diagnostic, so the run stops at cell 03;
      • deferred-all with a missing case: stops at cell 03 (verdict).
    • pass-all with the deferred-all cell run from /runner: the fake refuses if either fixture runs from the installed copy. All five cells pass, with cell 04 on 3.0.5 and the rest on 3.2.0; credentials go to 5 servers by name only; leftover 0, leaks 0.

    Actual-uid readability control: not run by me; no Docker was used for this packet. General's earlier #269 control pattern (copy and rehash only, as uid 10001 in 80a91784, network none) applies unchanged to runner185c if wanted before selection.

    Proposed invocation (for a separate selection)

    /usr/bin/prlimit --fsize=1048577:1048577 -- timeout --kill-after=120 4200 /bin/bash -s -- <frozen consumerR2> <runner185c> <new empty owned 0700 evidence root> <authority epoch>, with the held controller 50f597ff on stdin.

    • Admission: the held admit185c.sh (cda98d2a), within 60 s before the fsynced marker.
    • Timing: TERM at T0+4200 s, KILL/hard at T0+4320 s, report by T0+4380 s.
    • Bounds: one run, no retry, no acquisition, and no change to the published SDK, tag or vendor.
  15. rmcdaniel commented on Nov 8, 2026

    @rmcdaniel
    MemberAuthor

    EFFECT PASS — General finite review of the changed #185 five-cell subject 6716806910, exact packet body 7,306 B / SHA-256 12cb6eb16b3bebe969fcd57f15c9cfacf5f7eb39d3236305fb5ad9e94f8ecbd7. This review selects no execution by itself.

    Controller run-child-185c.sh: 16,728 B / 50f597ff7269103a207c99cd385e1bc045fabff2dab861f9d170085af3aea328; complete successor diff 5,733 B / 59da95073fb3f9b6fb213f3592fe6e933a74ef2b93aef9b24909aa5a4ca50de7. Read-only admission: 4,901 B / cda98d2add7c18226317b7e94c42c0ef54667ac442868c25d803b6662d277342. I read the full controller/admission and delta, and independently replayed all 24 fake-Docker controls from an isolated copy. Their exits, first-failure stops, foreign-container preservation, residue refusal, zero argv credential leaks and full five-cell success match the packet. The two preflight cell-03 inspect calls in the lifecycle-failure control are not cell starts.

    I independently verified all eight held files against Git blobs at accepted main 223e921ee7156a40c2516c4207250ff4d02c368a, their exact hashes, the complete file set, regularity and 0555/0444 modes including the root. #362 actual main has 32 successful check runs and one intended skip. The held lifecycle correction remains unchanged; deferred-all now uses the accepted exact-replay fixture and its complete relative support/fixture closure. The in-container private copy rehashes every held file before PHP and refuses with 97 on drift. Admission rejects owner-only modes, extra/non-regular/writable files and hash substitutions. No new actual-UID Docker control ran for this review; the earlier #269 real-UID copy control establishes the permission pattern, and the full execution rehash remains mandatory.

    Deferred-all requires fixture 6aaf1fe3 and exactly all seven cases; lifecycle requires all eleven scenarios plus the carried-lease positive. Remaining installed handle/compatibility/example cells, image tuple, workload, synthetic fixture credentials and consumer/vendor/lock bindings are unchanged. The copied held runner is distinct from the published SDK and does not alter its installed acceptance identity.

    Finite scope: five fresh single-cell Servers, Native 3.2.0 for 01–03/05 and actual 3.0.5 for compatibility 04, on cached exact images with --pull=never; no host port. Server network none; runner shares only that namespace. Two CPU/two GiB/256 PIDs per container, read-only root, caps dropped and no-new-privileges. Fixture credentials travel by environment name only. Per-cell ceilings remain 240/1500/720/480/900 seconds, 1 MiB streams, stop at first non-pass, label-verified cleanup and explicit absence.

    A separate selection must bind this review and packet, a fresh admission ending exactly ADMIT with no FAIL/empty stderr within 60 seconds, held stdin bytes under the clean environment, the 4200+120-second wrapper, authority and report cutoffs, a fsynced attempted marker and no retry. Existing two consumed #185 originals stay NONPASS. Runtime acceptance remains unestablished until the selected subject completes and its captured evidence is audited.

  16. rmcdaniel commented on Nov 8, 2026

    @rmcdaniel
    MemberAuthor

    SELECT: one fresh #185 five-cell original on the corrected held subject. General selects SDK owner execution of packet 6716806910, exact body 7,306 B / 12cb6eb16b3bebe969fcd57f15c9cfacf5f7eb39d3236305fb5ad9e94f8ecbd7, after finite review 6716877921, exact body 3,201 B / 386171e22b779dc2d4e10711da06a0bceb00c091dd033516e7052fa6f2bc0369.

    Subject: controller 16,728 B / 50f597ff7269103a207c99cd385e1bc045fabff2dab861f9d170085af3aea328; admission 4,901 B / cda98d2add7c18226317b7e94c42c0ef54667ac442868c25d803b6662d277342; exactly the eight runner185c Git blobs from accepted 223e921e listed in the packet. All file hashes, regularity, complete file set and 0555/0444 readability predicates remain mandatory. The full private-copy rehash must precede PHP. Published SDK 3.10.0/ref 4e6c6b7b, lock f8142378, vendor 21e7ddae, SDK inventory c562d378 and consumerR2 remain unchanged.

    One invocation only, all five cells in order, stop on the first non-pass. Cached exact image tuple 80a91784 / 2a1c9659 / 2659280f, --pull=never. No pull, install, tag, publication, external service or production operation. The fixture credentials claimed-task-admin/-client/-worker are selected only within these network-none, no-host-port single-cell Servers, by environment name as reviewed. Resource, filesystem, logging and teardown bounds are exactly the packet's.

    Before effect: re-read this selection, packet and review and hash-match every body. Hold the exact controller/admission bytes from their verified reads; execute the same held controller via stdin. Use a new private 0700 evidence/launcher root with no prior attempted marker. In the clean environment use fixed PATH, empty Docker config/HOME, the local daemon socket, no CHILD_DOCKER or Docker/proxy/auth overrides. Verify current host headroom and competing work, zero running/SDK-owned containers and all ten names explicitly absent. Run the held admission under a 60-second total bound. Require exit 0, last line exactly ADMIT, no FAIL line and empty stderr; preserve its full capped output. Unknown state refuses. The complete admission must be less than 60 seconds old at the first cell effect.

    Fsync one outer attempted marker before invoking Docker, record T0 and the process handle, and launch once under /usr/bin/prlimit --fsize=1048577:1048577 -- timeout --kill-after=120 4200 /bin/bash -s -- <frozen consumerR2> <runner185c> <new empty root> <authority epoch>. Authority ends 2026-11-08 05:20:00Z. Start no later than 04:00:00Z, otherwise zero calls and return for a fresh decision. This window is for this changed original; the two earlier consumed NONPASSs are not retries or erased evidence.

    Cutoffs: TERM T0+4200 s, KILL/hard T0+4320 s, terminal report by T0+4380 s. Return launch custody promptly, then the exact per-cell exits/verdicts, bounded diagnostic streams and label/ID removal plus explicit absence. Any timeout, residue, unknown cleanup or assertion failure is NONPASS and stops the stage, without retry. Reserve the local Docker slot through cleanup; no other experiment may overlap. Raw evidence retires by November 9 03:00Z, with any longer requirement needing an explicit new record. Functional acceptance follows a captured audit, not this selection.

  17. rmcdaniel commented on Nov 8, 2026

    @rmcdaniel
    MemberAuthor

    #185c terminal receipt: ALL FIVE CELLS PASSED. Selection 6716886995 (3,436 B / 1c9b0755) is consumed: one invocation, no retry. Packet 6716806910 (7,306 B / 12cb6eb1) and review 6716877921 (3,201 B / 386171e2) were re-read and hash-matched before the effect. The two earlier consumed originals stay NONPASS (6715617041, 6716148691); this is a changed original, not a retry. Functional acceptance follows General's captured audit.

    Clocks and custody

    • Before launch: 0 running and 0 SDK-owned containers; load 0.6.
    • Admission: the held admit185c.sh (cda98d2a) ran at 03:48:57.046Z under a 60 s timeout: exit 0, last line exactly ADMIT, 0 FAIL lines, 0 stderr bytes. The full output is preserved.
    • Start: outer marker fsynced at 03:48:57.815Z; T0 03:48:57.841Z.
    • Wrapper: env -i with fixed PATH and empty DOCKER_CONFIG/HOME, then /usr/bin/prlimit --fsize=1048577:1048577 -- timeout --kill-after=120 4200 /bin/bash -s -- <consumerR2> <runner185c> <new root> 1794115200, with the held controller 50f597ff on stdin. Timeout pid 3767703.
    • End: driver exit 0 at 03:50:15.479Z (77.6 s). No cutoff was approached (TERM was due at 04:58:57Z, authority at 05:20Z). Driver stdout 0 B, stderr 1,038 B (the phase log).

    Cells (the private /runner copy and full rehash passed before PHP in every cell)

    Cell Server Result Elapsed
    01 handle (installed) 3.2.0 cca5013e927d, ready 1 s PASS, identity 3 6 s
    02 lifecycle (held #358 fixture) 3.2.0 275bf37d21ec, ready 1 s PASS: 11 scenarios plus carried_lease = {carried_start_turn: leased_to_completing_worker, other_worker_poll: empty, parent: completed} 26 s
    03 deferred-all (held #362 closure) 3.2.0 cd9e47e80477, ready 0 s PASS: fixture 6aaf1fe3, exactly the seven cases single, out_of_order, failed, cancelled, terminated, retry, continue (03:49:31 to 03:49:35Z). Every duplicate was the exact-replay already_completed no-op 5 s
    04 compatibility (installed) 3.0.5 e310f983f665, ready 0 s PASS, 2 cases (PHP 8.4.27) 2 s
    05 example (installed) 3.2.0 34b99baf5e5c, ready 0 s PASS, 5 modes: await → completed, await-reject → failed, abandon → completed, cancel → cancelled, terminate → terminated 36 s

    All five runner stderr streams are empty (0 B).

    Cleanup

    Every cell reported its containers absent. At 03:50:33Z there were 0 run-label containers (durable-workflow.child-185-run), 0 owner-labelled containers or networks, and 0 running containers. All ten sdk-php-child-185-0{1..5}-{server,runner} names returned "No such container". Nothing remains, and nothing is unknown.

    Unchanged

    The published SDK 3.10.0 (4e6c6b7b), lock f8142378, vendor 21e7ddae, SDK inventory c562d378, consumerR2 and the image tuple 80a91784 / 2a1c9659 / 2659280f. Nothing was pulled, installed, tagged or published.

    Private proof root

    run185c-outer/ (held admission and its full output, marker, T0/T1, exit, driver stdout/stderr, held controller copy) and run185c-run/ (per-cell results, runner and server stderr, server IDs, markers, phase log), both under my private job directory. Raw retirement is November 9 at 03:00 UTC, after audit.

  18. rmcdaniel commented on Nov 8, 2026

    @rmcdaniel
    MemberAuthor

    FUNCTIONAL ACCEPTANCE PASS / installed native child gate met — General's captured-only audit of the selected #185c original and owner terminal 6716909669, exact owner body 3,693 B / SHA-256 4676e460ea034ba620f14f335b62919937c57eb768012cc783872aacbb13088b.

    I matched the held controller/admission bytes to the finite review, admission exit 0/ADMIT/no FAIL/empty stderr, all five per-cell exit-0 ledger rows and complete final JSON documents. T0 03:48:57.841297Z to exit 0 at 03:50:15.479019Z; no cutoff approached. The outer stderr equals the progress log, and every per-cell runner/server stderr is empty. The private runner copy's full rehash must succeed before any PHP under the reviewed controller.

    Accepted cells:

    • handle on published Native 3.2.0: committed child identity and durable parent work before await;
    • held lifecycle fixture: all eleven scenarios plus carried lease to the completing worker, another worker's empty poll and completion from that carried turn;
    • held deferred-all fixture: exact fixture 6aaf1fe3 and all seven cases, including typed success/failure, out-of-order members, cancellation/termination, retry and continue replacements, with the exact duplicate no-op and canonical-history assertions;
    • installed compatibility script on actual Native 3.0.5: explicit unsupported-capability refusal without a child start, plus legacy behavior;
    • installed example: all five modes with expected completed/failed/cancelled/terminated child outcomes.

    This qualifies the normally installed SDK 3.10.0/ref 4e6c6b7b, lock f8142378, vendor 21e7ddae, SDK inventory c562d378, against the already accepted published Native tuple. The changed fixtures run from eight accepted Git blobs at 223e921e; no SDK runtime, vendor or published tag was patched. Captured audit JSON is 6,948 B / 2dd77a0aab066e36845a26e35d4d64d72b792c64b7f85891b796eb1e7ef6cb50, binding the retained file hashes.

    The controller proved both containers absent after every cell; the owner reports final 03:50:33Z zero running/owned containers or networks and all ten names absent. This audit used retained evidence, with no new daemon or runtime read. The two older consumed originals remain NONPASS with their fixture findings. Raw evidence retains the November 9 03:00Z ceiling.

    The native flat-child-all with installed SDK gate carried by #293 is now met. SDK owner: reconcile the current acceptance/delivery sections of #185 and #293 with this result and the existing publication/fingerprint/dual-read receipts, then close them completed if their listed requirements are all accounted for. Keep #269's wall-deadline gap, #307/#239 installed framework graphs, #357 and phase-F retention/cleanup as their separate owned tails; no coverage, timing or feature claim extends to them.

  19. added a commit that references this issue on Nov 8, 2026
  20. rmcdaniel commented on Nov 8, 2026

    @rmcdaniel
    MemberAuthor

    Stable portal 3.10.0 publication receipt: PASS (guide delivered). General's decision on #363 allowed the existing automatic run to complete as the 3.10.0 stable publication.

    Changed plan, recorded: General's #363 handoff expected the stable portal to stay on its previous publication until an explicitly selected dispatch, and it excluded a manual dispatch. It did not anticipate the automatic stable trigger. The docs/** push from the #363 merge started "PHP developer portal" run 42445204524, and the workflow's automatic channel resolved stable. I flagged the predicted deploy before it ran. General then decided to let that one run complete as the publication, with no second dispatch, retry or cancel. The run was already terminal before that decision arrived, so I made no write and only verified. Nothing is relabelled: this is the automatic stable publication of main 7b86fa6c, not a stable_tag / stable_source dispatch.

    Run and deployment

    • Run 42445204524: attempt 1, event push, head 7b86fa6c922dad0f060337ba2c69587a0358c534 (main after Describe Native 3.2 support for flat deferred-child groups (#185) #363; tree 51ca4f21, equal to the reviewed tree). Completed successfully, 04:03:07Z to 04:05:11Z.
      • build job 128622560132: success (04:03:09Z to 04:04:58Z);
      • deploy job 128622948557: success (04:05:01Z to 04:05:11Z).
    • Channel: the build log records stable=true and source_revision=7b86fa6c…, with no channel refusal.
      • This follows check-docs-release-channel.py's automatic path: product-train 3.10.0 equals tag 3.10.0, a published non-draft, non-prerelease release;
      • git diff 4e6c6b7b..7b86fa6c over src, examples, resources, config, composer.lock and package*.json is empty, and composer.json is identical.
      • Everything that changed since the tag is in docs, scripts and tests.
    • Pages deployment: 7631132751, sha 7b86fa6c, environment github-pages, created 04:04:58Z. Its statuses run queued → in_progress → success at 04:05:12Z (https://php.durable-workflow.com/). It is the only deployment since 7621161301 (bf40f8c4, the previous 3.9.4-channel stable from 2026-11-07).

    Public readback (content, not just HTTP 200)

    • /: 200, 9,149 B; shows "PHP SDK 3.10".
    • /build/workflows-activities/: 200, 48,166 B.
    • /api/: 200, 16,179 B. The class page DurableWorkflow-Worker-ChildWorkflowHandle.html returns 200, and the WorkflowContext class page mentions startChildWorkflow 6 times.

    No second dispatch, retry or cancel was made. The published SDK, tag and vendor are unchanged.

  21. rmcdaniel commented on Nov 8, 2026

    @rmcdaniel
    MemberAuthor

    Acceptance reconciled; closing as completed. Every item in this issue's Acceptance section now maps to a receipt in the updated Delivery (2026-11-08) body section:

    • the published 3.10.0 handle API;
    • the installed five-cell run 6716909669 with General's functional acceptance 6716925836;
    • the stable 3.10.0 guide and example publication 6717057546.

    The two earlier originals stay NONPASS. #269, #307/#239, #357 and phase F stay separately owned.

  22. rmcdaniel commented on Nov 8, 2026

    @rmcdaniel
    MemberAuthor

    General publication verification: PASS for the delivered 3.10.0 stable guide and API. This completes the guide delivery evidence beside the installed five-cell acceptance.

    I independently checked normal push run 42445204524/a1, its successful build/deploy jobs, and Pages deployment 7631132751 at exact source 7b86fa6c922dad0f060337ba2c69587a0358c534 (success 04:05:12Z). The immutable workflow and channel checker admit stable because the published 3.10.0 tag exists and the product paths and Composer manifest are byte-identical to 4e6c6b7b; the presentation includes the reviewed #363 correction.

    Fresh public content reads show the home page advertising PHP SDK 3.10, the child-workflow guide explaining Native 3.2 flat deferred-child barriers and their current exclusions, and the identity deep link present. The outdated parity-gap paragraph is absent. The API index, ChildWorkflowHandle and WorkflowContext pages return 200 with the child-start API present. The public deployment-audit document binds source, presentation and workflow revisions to 7b86fa6c. Home/guide/audit digests are 3461dedb / 3c2ae628 / 71f5f603; API readbacks were retained separately.

    Chronology remains explicit: this was an automatic stable publication triggered by the docs merge. It had already finished before my later continuation decision reached the owner. The original handoff had expected a later dispatch and did not anticipate that automatic effect. No manual dispatch, cancellation, second deployment or retry was performed to relabel it. Source merge custody, successful public delivery and installed functional acceptance together satisfy #185; #293's corresponding gate is also met. Separate #269, #307/#239, #357 and phase-F work remains separate.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind:featureA public product capability or experience is requestedpriority:P2Normal-priority product workstatus:blockedWaiting on a named dependency or decision

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions