Repository navigation
Gate durable service scheduling by worker protocol, not HTTP transactions #942
Description
Problem
Workflow 2.14.x activates the protocol 1.21 durable service-operation scheduler whenever a service call has caller_workflow_run_id and the model connection has any open transaction.
That predicate does not identify workflow-task completion. DefaultServiceControlPlane::execute() also opens a transaction for any request carrying service_call_id or idempotency_key, so an ordinary HTTP Nexus execute request with caller workflow context now returns admission-only (retry_scheduled=true, wait_for=accepted) instead of honoring wait_for=completed with an inline terminal result or typed failure.
This breaks current protocol 1.20 clients. In particular, Python SDK Client.execute_nexus_operation() sends caller workflow instance/run identity for workflow-originated HTTP execution, and its contract tests require a completed response or typed HTTP 409. PHP Client::executeServiceOperation() defaults to wait_for=completed and accepts caller identity in its options.
The protocol boundary is also not enforceable in the package API: DefaultWorkflowTaskBridge::complete(string $taskId, array $commands) does not receive the completing worker's protocol version, while every start_service_operation completed through it can now record ServiceOperationScheduled. The published contract says the command remains available from 1.13 and only the scheduled semantics require 1.21.
At diagnosis, PHP, Python, and Rust SDK main branches all declared worker protocol 1.20.
Required behavior
- Direct/control-plane HTTP execute requests must not enter the durable workflow-task scheduler merely because caller workflow identity and an idempotency transaction are present.
wait_for=completedmust retain its terminal result / typed failure contract. - Workflow-task completion must activate
ServiceOperationScheduledonly when the completing worker protocol is at leastDURABLE_SERVICE_OPERATION_SCHEDULER_MINIMUM_PROTOCOL_VERSION(1.21). - Workers below 1.21 must retain the pre-1.21 inline
start_service_operationpath; they must not be rejected solely because the package supports the new scheduler. - Extend the stable completion surface compatibly so hosts can provide the authenticated worker protocol version. Existing embedded/default calls should retain the current package protocol default.
- Add regression coverage for caller-context HTTP execution, protocol 1.20 inline completion/failure, protocol 1.21 scheduled admission, and no-write refusal of invalid protocol input.
- Publish a patch release for Server adoption; Server must remain at protocol 1.20 until it consumes the corrected package and explicitly passes worker protocol context.
Evidence
- Introduced by
2a7511501(Schedule workflow service calls outside completion locks). - Reported release at diagnosis:
af9dbadb41bf96f51ad78047022c392887e3c7a6(2.14.1). - Server adoption tests correctly observe three regressions: sync completed and typed-failure HTTP calls with caller workflow identity now return 200 admission-only.
Current state and next action
General owns delivery. Workflow 2.14.2 is published at d60ce5f60e70b834c5d4ef142c3d5dc415cd39fe, tree 675082c4bc86810467f9abaf852868b8144c95fb. The release and Packagist source/dist references identify that exact commit.
PRs #943, #944 and the forward qualification correction #945 are merged. The correction preserves the nested non-waiting workflow-update guard inside idempotency transactions. Exact-source Security acceptance and full corrected-main build 40361556659 are successful: 5,013 unit cases / 71,655 assertions, all applicable database, quality, corpus and upgrade jobs, and combined line coverage 93.63%. The prior failed builds remain historical evidence and were not treated as release qualification.
Published release verification 40365636740 passed all 16 Laravel/PHP upgrade cells. The installed embedded Nexus receipt passed 12/12 scenarios with real worker processes, cold readers, typed failures, cancellation, interruption and external payloads, using the normally installed 2.14.2 package. The separate #899 installed acceptance passed on SQLite, MySQL and PostgreSQL, 6 tests / 7,152 assertions.
Remaining: published Server consumer acceptance. Server #791, exact head 9d7cd8fcba3dda8bcec984e7048d0ffa27566d00, implements #784 alongside the accepted PHP stream/payload repairs. It pins published2.14.2, preserves server protocol1.20, passes the authenticated version through the optional protocol-aware bridge, retains request/replay authority and the60-second bound. Product source has429 focused cases/5,123 assertions with four-engine races; final test corrections are qualified locally and hosted CI is running.
General owns the protected merge, independent main qualification, prepared PHPServer2.11.3 publication and actual installed PHP/Python Nexus callers, registry-installed Rust Nexus profiles and nested non-waiting workflow-update acceptance. The existing published Nexus workflow's current-artifact pins are prepared on test/php791-installed-acceptance; it has not been dispatched against the unpublished Server tag and will use the exact image digest after registry verification. No version-only SDK release is needed. Native2.14 schema takeover recognition remains with the Rust owner. Keep this issue open until the published Server acceptance is complete; no operator action is required.
Activity
Fix service scheduler protocol gating (#942)
Preserve completion ABI and qualify protocol scheduling (#942)
rmcdaniel commentedon Oct 26, 2026
General's consumer inventory for the coordinated 2.14.2 patch, checked against GitHub main on October 26.
Publication, installed Laravel qualification, Server adoption, and #899's consumer qualification remain pending. This inventory is not an artifact acceptance receipt.
rmcdaniel commentedon Oct 26, 2026
Workflow 2.14.2 is published and the package-level installed acceptance passes.
d60ce5f60e70b834c5d4ef142c3d5dc415cd39fe, tree675082c4bc86810467f9abaf852868b8144c95fb. Release, registry metadata.The normal installed embedded run executed October 26, 17:15:49.319670–17:16:47.688743 UTC, on PHP 8.5.12 with real SQLite database queues. The runtime image was
composer@sha256:da624c2c705d54e2f5a9bb77a5dc6c7d091711433ebefd7035672e84c2010fee, under UID/GID 1000, 2 CPUs and 2 GiB, with networking disabled during execution. The installed package's exact version, reference and vendor path are asserted by the probe.All 12 originals pass: completion, typed failure, target cancellation, waiting-caller cancellation, backend interruption, and external payloads, each in the same namespace and across namespaces. There are 24 original runs, 12 calls, 93 distinct real queue-worker processes, a separate cold reader for each original, zero remaining runnable tasks and zero failed Laravel jobs. Each original retains its 30-second observer budget; call/execution/cleanup deadlines and identities are checked unchanged. The largest value is 114,688 bytes, above the real 1,024-byte external-storage threshold. int64, null, Unicode, external references and typed results survive cold reads without changing database bytes.
The existing embedded fixture was copied into a disposable consumer. Its historical version/reference pins were advanced, the CLI dependency/provider preparation followed its README, and the observer now checks the current two-phase durable scheduling contract: scheduled admission with no inline target, a separate dispatch worker, one
ServiceOperationScheduled, no inlineServiceCallStarted, and one terminal service event. The fixture host authorizer permits only its caller class, the two fixture namespaces, and its fixed target service/operation. All package authorization checks remain unchanged.The first attempt deliberately remains separate: the historical fixture lacked the now-required host binding and the installed default policy refused with
service_operation_authority_invalid, producing one failed run and zero service calls. After adding the documented fixture-scoped host binding, all 12 cases ran on a fresh database. The observer also now exits nonzero on a failed assertion. Neither the original incomplete attempt nor its exit code is counted as acceptance.Identities:
1fa968bd9c27a97722c74665dff0bc955a86c27542b65b3ee3037d33983b3997.3e3451cd2013f5fb52450d4355ea38e6f82922848349309325bee433f5997242.eddc35bcd504e9bbc778deb0130d9786e430379ae4fe5af7a4e548d5d921dce1.The patch below records the fixture adaptations against the runner revision linked above. The original checked-in historical profile remains unchanged. Use the profile's CLI preparation, the published package and framework versions above, and a fresh disposable database.
Server #784 retains the published Server artifact and actual PHP/Python/Rust SDK runtime qualification. Workflow #899's owner retains its normally installed Workflow + published Waterline acceptance on SQLite/MySQL/PostgreSQL. These are separate pending receipts; #942 remains open.
rmcdaniel commentedon Oct 26, 2026
The first exact-published-tuple Nexus acceptance run 40418210934 finished failed. Server 2.11.3 / Core 2.14.2 / PHP SDK 3.5.1 / Python SDK 2.11.2 / CLI 2.3.0 / Waterline 2.13.0 were used; the image was pinned to published index
sha256:923a8924ecad13485562d7af30dc87d7fd6d9d7d4758ec8113bac0766fa8603e.The hosted check annotations establish:
The acceptance assertions and original budgets remain intact. Diagnostic run 40420956902 repeats the same published tuple at fixture head
1e12cdbc33d4d1ec0a32362088e5b9c8a53b3c8f, adding only bounded last-observed event types and worker diagnostics. The existing 13 evidence-reader checks and PHP/Python syntax checks pass. This distinguishes an obsolete fixture event expectation from an actual delivery defect before changing either behavior or the qualification contract.Installed consumer acceptance remains open. The package/source lifecycle fixes in Server #794 / Workflow #947 remain unreleased and are not claimed by this published-image result. Release/image/chart publication itself passed; this result does not establish completion of the remaining acceptance work.