Observed Defect
In the published Workflow 2.0.13 package (PHP 8.4), Workflow\V2\now() returns the mutable Carbon instance held by WorkflowFiberContext. Deriving a deadline with now()->addHour() mutates the clock itself. A subsequent now() is already at that deadline, despite no history event advancing time.
Minimal reproduction against the installed package:
require 'vendor/autoload.php';
$fiber = new Fiber(function () {
\Workflow\V2\Support\WorkflowFiberContext::setTime(
\Carbon\Carbon::parse('2026-01-01T00:00:00Z')
);
$before = \Workflow\V2\now()->getTimestamp();
$deadline = \Workflow\V2\now()->addHour();
echo json_encode([
'clock_advanced_seconds' => \Workflow\V2\now()->getTimestamp() - $before,
'deadline_already_reached' => \Workflow\V2\now()->greaterThanOrEqualTo($deadline),
]);
});
$fiber->start();
Observed: {"clock_advanced_seconds":3600,"deadline_already_reached":true}.
Expected: 0 and false.
Also reproduced in a real database-backed workflow before its first activity. Later history replay can replace the stored clock, which makes the symptom depend on where deadline arithmetic occurs.
Fix And Qualification
- Return an isolated clock value (defensive copy or immutable instance) without exposing mutable executor state.
- Inspect
setTime() ownership too: retaining a caller-owned mutable object can expose the same alias in the opposite direction.
- Preserve deterministic history timestamps; do not replace workflow time with wall time.
- Cover two consecutive clock reads, mutation of an earlier returned value, mutation of the supplied clock object, and separate fibers.
- Prove an ordinary deadline workflow remains pending initially and crosses its deadline only after durable time advancement and replay.
- Review affected consumers and run focused published-artifact conformance after release.
Immediate application-level mitigation: derive deadlines from now()->toImmutable(). This does not replace the engine fix.
No public API or private infrastructure change is requested.
Observed Defect
In the published Workflow 2.0.13 package (PHP 8.4),
Workflow\V2\now()returns the mutable Carbon instance held byWorkflowFiberContext. Deriving a deadline withnow()->addHour()mutates the clock itself. A subsequentnow()is already at that deadline, despite no history event advancing time.Minimal reproduction against the installed package:
Observed:
{"clock_advanced_seconds":3600,"deadline_already_reached":true}.Expected:
0andfalse.Also reproduced in a real database-backed workflow before its first activity. Later history replay can replace the stored clock, which makes the symptom depend on where deadline arithmetic occurs.
Fix And Qualification
setTime()ownership too: retaining a caller-owned mutable object can expose the same alias in the opposite direction.Immediate application-level mitigation: derive deadlines from
now()->toImmutable(). This does not replace the engine fix.No public API or private infrastructure change is requested.