Skip to content

Do not silently discard recorded activity input during dependency injection #910

Open

Description

@rmcdaniel

Problem

After an activity signature changes, Laravel dependency resolution can discard
its recorded business argument and execute the activity with a newly constructed
DTO's defaults. The run then records successful completion with the wrong data.
This was reproduced while qualifying website#363, rather than inferred from
its documentation report.

Published reproduction

Normally installed Workflow 2.11.2, source
a730773a3dcdf4c6d4ea0297f6c9010ccc0603f9; PHP 8.5.12, Laravel
13.38.0, SQLite. Real package migrations, workflow/activity jobs and durable
history are used. Queue transport is faked so those public jobs can be driven
explicitly. Execution crosses two fresh PHP processes; no service SDK or native
Server behavior is inferred.

  1. The first process starts a workflow with 7. Its workflow task records an
    activity with positional arguments [7], then stops before that activity.
    The original activity is:

    final class QuoteActivity extends \Workflow\V2\Activity
    {
        public function handle(int $amount): array
        {
            return ['amount' => $amount];
        }
    }
  2. A fresh process loads the same original database and the same activity class
    identity with this changed definition:

    final class QuoteRequest
    {
        public function __construct(public int $amount = 0) {}
    }
    
    final class QuoteActivity extends \Workflow\V2\Activity
    {
        public function handle(QuoteRequest $request): array
        {
            return ['amount' => $request->amount];
        }
    }
  3. Execute its original ready activity task, then the original parent task.
    The recorded arguments still decode to [7], with SHA-256
    73ab292f3f07ffff5c58d9a88b09cf78a3a2b7490f9011dd10a0d33ed84d4536.
    The activity and workflow nevertheless complete with amount: 0.

Actual original run 01m5kr1j590a3vcc3j0hh7meat: one activity attempt,
three first-attempt tasks, zero repairs, and exactly
StartAccepted → WorkflowStarted → ActivityScheduled → ActivityStarted → ActivityCompleted → WorkflowCompleted.
There is no failure or input-mismatch diagnostic. An assertion requiring the
original amount 7 fails against actual 0.

Independent controls on the same published package pass: adding a trailing
optional activity argument uses its default; a new workflow reader can consume
an old result with a field fallback. Adding a required argument records
ArgumentCountError/ActivityFailed, and reading a missing old result field
records an application failure. Those outcomes are separate from this silent
DTO substitution.

Cause and acceptance

RunActivityTask
resolves method dependencies before invoking the activity. The container inserts
a default DTO ahead of the recorded scalar; the scalar becomes an extra PHP
argument and is silently unused. This is the activity counterpart of the entry
safety concern fixed for workflow methods in #866.

  • Refuse binding that would silently replace or discard recorded activity input
    before application activity code or its side effects run.
  • Keep original input, run/activity/task identities and committed history intact.
    Define a typed, inspectable mismatch outcome and a supported recovery path
    after compatible code is restored; do not invent a successful result.
  • Preserve legitimate dependency-injected services, scalar input, optional
    trailing defaults, variadics and supported input adapters.
  • Add a failing-before regression using the real activity job, with cold
    persisted input plus valid DI/default controls. Cover supported SQL backends.
  • Verify the affected published embedded consumer and document the resulting
    compatibility/recovery contract in website#363 and all offered locales.
    Check PHP service-mode binding independently before claiming it affected or
    fixed. Native/shared parity stays coordinated with its owner.

Current delivery

Published 2.11.3 at 71213ae1 after exact candidate and independent main checks passed. Normally installed cold-process refusal and same-original authored-retry recovery pass, with the old 2.11.2 originals retained as failures. Full receipt. Published Laravel/PHP profile 39933343761 passes all seventeen jobs. Website #363 / PR #379 is delivered in all eight locales (receipt6362864678). Sample #275 is delivered at 40be451c after all candidate/main checks and anonymous both-registry amd64/arm64 image verification. The affected PHP Server package/image pin remains a separate qualified-release follow-through, with no native or Cloud adoption inferred.

Activity

  1. added
    kind:defectA public product behavior is incorrect
    priority:P1High-priority product or release risk
    completion:evidence-requiredClose only after all explicit acceptance and operational evidence is public
    status:in-progressApproved work is actively being implemented or validated
    on Oct 23, 2026
  2. rmcdaniel commented on Oct 23, 2026

    @rmcdaniel
    MemberAuthor

    General accepted this as a P1 product defect. #911 now guards both queued and local activities before their entry methods run. Real failing-before controls separately establish silent local input loss and dependency-resolution errors escaping the activity outcome path.

    The failure policy preserves the activity's authored attempts, backoff and deadlines. An input-binding mismatch is a typed failed attempt, not an invented successful result. With retries configured, restoring compatible code completes the same original run/activity/input on a later attempt. A one-attempt activity can still fail its workflow normally; this change does not refund attempts or reopen terminal runs. Existing valid injected services, defaults and variadics remain supported.

    Eleven focused source tests pass. Full exact-head checks and SQL qualification are in progress. Published 2.11.2 remains affected; publication, installed acceptance and the all-locale website#363 handoff are still required.

    Source qualification checkpoint: #911 merged normally at 71213ae / whole b48b19574b13c369274d986158bf4511041566ca after all seventeen candidate checks passed, with six explicit PR skips. The remote head is independently absent. Independent full main qualification is run 39925010321 and remains queued/running. Patch 2.11.3 has not been tagged or published; fixed-package cold-process acceptance and downstream follow-through are still required.

  3. rmcdaniel commented on Oct 23, 2026

    @rmcdaniel
    MemberAuthor

    Workflow 2.11.3 is published; cold-process acceptance passes.

    The immutable annotated tag 55945e03cc98e5441a6f129d2b389bdd8c00664e points to source 71213ae1e0338cda5967e0840782a981530ed854, whole tree b48b19574b13c369274d986158bf4511041566ca. All five independent main workflows pass, including the complete SQL/framework/coverage build. The release and Packagist source/dist references match.

    A normal Composer consumer changed only Workflow 2.11.2 to 2.11.3 in its 119-package lock. PHP 8.5.12, Laravel 13.38.0, SQLite and real public workflow/activity jobs are used, with queue transport faked for explicit job execution. There is no source overlay or service-worker/native claim.

    Five fresh PHP processes, five tests / 52 assertions pass:

    • Default one-attempt policy: run 01m5m2669r7ffmm7sgz9knkq92, activity 01m5m266jb3p27r757n4kxd74y. The cold incompatible DTO is refused before its body runs. Typed ActivityEntryArgumentsMismatchException produces ActivityFailed, then normal WorkflowFailed, with null output, one activity attempt, three tasks and zero repairs.
    • Authored two-attempt policy, zero backoff: run 01m5m26qbp25snv1jaqjzfbkcf, activity 01m5m26qmy0vfxtzr0w2a9xnd0. The incompatible process records ActivityRetryScheduled without entering the body. A third fresh process restores the compatible scalar method and completes the same original run/activity with output 7, two activity attempts, one ActivityCompleted, no ActivityFailed and zero repairs. Four tasks remain; the retry task correctly records attempt two.

    Both preserve recorded input [7] and SHA-256 73ab292f3f07ffff5c58d9a88b09cf78a3a2b7490f9011dd10a0d33ed84d4536. The same fixture/assertions against installed 2.11.2 retain two incompatible-body failures after successful seed phases: original runs 01m5m1ydkwx91qpat5dw1m8xe3 and 01m5m1zcjkhnzdb97rgvaavgmh. Fixture SHA-256 is 70477c16aa0355a8d90a6b25e568ae728df36f81e0464333f04c66232ab3c332. Public source regressions in #911 also cover valid DI/default/variadic controls and local activities.

    The published verification profile now passes all seventeen jobs: package/platform identity and all sixteen supported Laravel/PHP upgrade combinations. Website #363's all-locale fixed-version/recovery guidance and affected consumer pins/rebuilds remain in progress. This library tag does not update existing Server images, reopen terminal runs or grant additional attempts.

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

    completion:evidence-requiredClose only after all explicit acceptance and operational evidence is publickind:defectA public product behavior is incorrectpriority:P1High-priority product or release riskstatus:in-progressApproved work is actively being implemented or validated

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions