diff --git a/README.md b/README.md index aa277f24..87f35db2 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ all three runtimes and the artifact versions used for the run. The qualified tuple in [`polyglot/qualified-artifact-tuple.json`](polyglot/qualified-artifact-tuple.json) pins every published artifact the journeys use: native Durable Workflow Server -3.0.5 (by image digest), Workflow 2.20.1, Waterline 2.13.2, PHP SDK 3.9.1, +3.0.5 (by image digest), Workflow 2.20.1, Waterline 2.13.2, PHP SDK 3.9.4, Python SDK 2.13.0, Rust SDK 4.10.3 and CLI 2.3.3. `artifacts.workflow` selects the embedded application's Composer package; the Composer lock must match it. Native Server is one process that creates its schema and `default` namespace diff --git a/composer.json b/composer.json index 5318585d..afc399b0 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "require": { "php": "^8.4", "durable-workflow/ai": "dev-main", - "durable-workflow/sdk": "^3.9.1", + "durable-workflow/sdk": "^3.9.4", "durable-workflow/waterline": "^2.0", "durable-workflow/workflow": "^2.20.1", "laravel/ai": "^0.6.0", diff --git a/composer.lock b/composer.lock index d04cc0ea..db1bca8d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f0814b9eafcdfbcc1945c50df8098196", + "content-hash": "a9b8086ae4083a8a66acd5685d4d44f9", "packages": [ { "name": "apache/avro", @@ -720,16 +720,16 @@ }, { "name": "durable-workflow/sdk", - "version": "3.9.1", + "version": "3.9.4", "source": { "type": "git", "url": "https://github.com/durable-workflow/sdk-php.git", - "reference": "e4d77aade4ddc02eb0ba40f827709b8b4817725e" + "reference": "11085f0c624674a4b70d3a6fb3e83eaff3869fcb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/durable-workflow/sdk-php/zipball/e4d77aade4ddc02eb0ba40f827709b8b4817725e", - "reference": "e4d77aade4ddc02eb0ba40f827709b8b4817725e", + "url": "https://api.github.com/repos/durable-workflow/sdk-php/zipball/11085f0c624674a4b70d3a6fb3e83eaff3869fcb", + "reference": "11085f0c624674a4b70d3a6fb3e83eaff3869fcb", "shasum": "" }, "require": { @@ -781,7 +781,7 @@ "laravel": "^10.50.4|^11.56.1|^12.69.0|^13.30.0", "symfony": "^6.4|^7.0|^8.0" }, - "product-train": "3.9.1", + "product-train": "3.9.4", "payload-codecs": [ "avro" ], @@ -851,8 +851,7 @@ "issues": "https://github.com/durable-workflow/sdk-php/issues", "source": "https://github.com/durable-workflow/sdk-php" }, - "time": "2026-11-04T02:37:04+00:00", - "published-time": "2026-11-04T02:37:27+00:00" + "time": "2026-11-04T05:28:03+00:00" }, { "name": "durable-workflow/waterline", @@ -934,8 +933,7 @@ "issues": "https://github.com/durable-workflow/waterline/issues", "source": "https://github.com/durable-workflow/waterline/tree/2.13.2" }, - "time": "2026-11-02T23:25:52+00:00", - "published-time": "2026-11-02T23:54:37+00:00" + "time": "2026-11-02T23:25:52+00:00" }, { "name": "durable-workflow/workflow", @@ -1018,8 +1016,7 @@ "issues": "https://github.com/durable-workflow/workflow/issues", "source": "https://github.com/durable-workflow/workflow/tree/2.20.1" }, - "time": "2026-11-04T02:23:19+00:00", - "published-time": "2026-11-04T02:23:42+00:00" + "time": "2026-11-04T02:23:19+00:00" }, { "name": "egulias/email-validator", @@ -3392,8 +3389,7 @@ "issues": "https://github.com/pear/HTTP_Request2/issues", "source": "https://github.com/pear/HTTP_Request2" }, - "time": "2026-09-04T10:44:42+00:00", - "published-time": "2026-09-04T10:45:33+00:00" + "time": "2026-09-04T10:44:42+00:00" }, { "name": "pear/net_url2", @@ -3461,8 +3457,7 @@ "issues": "https://pear.php.net/bugs/search.php?cmd=display&package_name[]=Net_URL2", "source": "https://github.com/pear/Net_URL2" }, - "time": "2025-03-24T08:03:00+00:00", - "published-time": "2025-03-26T10:43:16+00:00" + "time": "2025-03-24T08:03:00+00:00" }, { "name": "pear/pear_exception", @@ -3521,8 +3516,7 @@ "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR_Exception", "source": "https://github.com/pear/PEAR_Exception" }, - "time": "2021-03-21T15:43:46+00:00", - "published-time": "2021-03-21T15:44:58+00:00" + "time": "2021-03-21T15:43:46+00:00" }, { "name": "phpoption/phpoption", @@ -9604,5 +9598,5 @@ "php": "^8.4" }, "platform-dev": {}, - "plugin-api-version": "2.11.0" + "plugin-api-version": "2.9.0" } diff --git a/microservice/composer.json b/microservice/composer.json index 41be7b9d..31a06b0d 100644 --- a/microservice/composer.json +++ b/microservice/composer.json @@ -10,7 +10,7 @@ "license": "MIT", "require": { "php": "^8.4", - "durable-workflow/sdk": "^3.9.1", + "durable-workflow/sdk": "^3.9.4", "durable-workflow/workflow": "^2.20.1", "durable-workflow/waterline": "^2.0", "laravel/framework": "^12.69", diff --git a/microservice/composer.lock b/microservice/composer.lock index 35c528ac..348bd056 100644 --- a/microservice/composer.lock +++ b/microservice/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e8f09bfe6ba2d5b90cfbdcebbe644d23", + "content-hash": "640a15ace917897880755cb8e0293ba2", "packages": [ { "name": "apache/avro", @@ -505,16 +505,16 @@ }, { "name": "durable-workflow/sdk", - "version": "3.9.1", + "version": "3.9.4", "source": { "type": "git", "url": "https://github.com/durable-workflow/sdk-php.git", - "reference": "e4d77aade4ddc02eb0ba40f827709b8b4817725e" + "reference": "11085f0c624674a4b70d3a6fb3e83eaff3869fcb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/durable-workflow/sdk-php/zipball/e4d77aade4ddc02eb0ba40f827709b8b4817725e", - "reference": "e4d77aade4ddc02eb0ba40f827709b8b4817725e", + "url": "https://api.github.com/repos/durable-workflow/sdk-php/zipball/11085f0c624674a4b70d3a6fb3e83eaff3869fcb", + "reference": "11085f0c624674a4b70d3a6fb3e83eaff3869fcb", "shasum": "" }, "require": { @@ -566,7 +566,7 @@ "laravel": "^10.50.4|^11.56.1|^12.69.0|^13.30.0", "symfony": "^6.4|^7.0|^8.0" }, - "product-train": "3.9.1", + "product-train": "3.9.4", "payload-codecs": [ "avro" ], @@ -636,8 +636,7 @@ "issues": "https://github.com/durable-workflow/sdk-php/issues", "source": "https://github.com/durable-workflow/sdk-php" }, - "time": "2026-11-04T02:37:04+00:00", - "published-time": "2026-11-04T02:37:27+00:00" + "time": "2026-11-04T05:28:03+00:00" }, { "name": "durable-workflow/waterline", @@ -719,8 +718,7 @@ "issues": "https://github.com/durable-workflow/waterline/issues", "source": "https://github.com/durable-workflow/waterline/tree/2.13.2" }, - "time": "2026-11-02T23:25:52+00:00", - "published-time": "2026-11-02T23:54:37+00:00" + "time": "2026-11-02T23:25:52+00:00" }, { "name": "durable-workflow/workflow", @@ -803,8 +801,7 @@ "issues": "https://github.com/durable-workflow/workflow/issues", "source": "https://github.com/durable-workflow/workflow/tree/2.20.1" }, - "time": "2026-11-04T02:23:19+00:00", - "published-time": "2026-11-04T02:23:42+00:00" + "time": "2026-11-04T02:23:19+00:00" }, { "name": "egulias/email-validator", @@ -2958,8 +2955,7 @@ "issues": "https://github.com/pear/HTTP_Request2/issues", "source": "https://github.com/pear/HTTP_Request2" }, - "time": "2026-09-04T10:44:42+00:00", - "published-time": "2026-09-04T10:45:33+00:00" + "time": "2026-09-04T10:44:42+00:00" }, { "name": "pear/net_url2", @@ -3027,8 +3023,7 @@ "issues": "https://pear.php.net/bugs/search.php?cmd=display&package_name[]=Net_URL2", "source": "https://github.com/pear/Net_URL2" }, - "time": "2025-03-24T08:03:00+00:00", - "published-time": "2025-03-26T10:43:16+00:00" + "time": "2025-03-24T08:03:00+00:00" }, { "name": "pear/pear_exception", @@ -3087,8 +3082,7 @@ "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR_Exception", "source": "https://github.com/pear/PEAR_Exception" }, - "time": "2021-03-21T15:43:46+00:00", - "published-time": "2021-03-21T15:44:58+00:00" + "time": "2021-03-21T15:43:46+00:00" }, { "name": "phpoption/phpoption", @@ -9011,5 +9005,5 @@ "php": "^8.4" }, "platform-dev": {}, - "plugin-api-version": "2.11.0" + "plugin-api-version": "2.9.0" } diff --git a/playground/php-runtime/composer.json b/playground/php-runtime/composer.json index c2a61722..92c730cc 100644 --- a/playground/php-runtime/composer.json +++ b/playground/php-runtime/composer.json @@ -6,7 +6,7 @@ "license": "MIT", "require": { "php": "^8.4", - "durable-workflow/sdk": "^3.9.1" + "durable-workflow/sdk": "^3.9.4" }, "config": { "allow-plugins": false, diff --git a/playground/php-runtime/composer.lock b/playground/php-runtime/composer.lock index ed0497f3..bda81999 100644 --- a/playground/php-runtime/composer.lock +++ b/playground/php-runtime/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0bbb9bdb7acc5fd5f16bf5bae61c8e22", + "content-hash": "53d58b4b6598a1f6311305b40312b9d9", "packages": [ { "name": "apache/avro", @@ -140,21 +140,20 @@ "type": "tidelift" } ], - "time": "2026-09-06T14:11:38+00:00", - "published-time": "2026-09-06T14:15:00+00:00" + "time": "2026-09-06T14:11:38+00:00" }, { "name": "durable-workflow/sdk", - "version": "3.9.1", + "version": "3.9.4", "source": { "type": "git", "url": "https://github.com/durable-workflow/sdk-php.git", - "reference": "e4d77aade4ddc02eb0ba40f827709b8b4817725e" + "reference": "11085f0c624674a4b70d3a6fb3e83eaff3869fcb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/durable-workflow/sdk-php/zipball/e4d77aade4ddc02eb0ba40f827709b8b4817725e", - "reference": "e4d77aade4ddc02eb0ba40f827709b8b4817725e", + "url": "https://api.github.com/repos/durable-workflow/sdk-php/zipball/11085f0c624674a4b70d3a6fb3e83eaff3869fcb", + "reference": "11085f0c624674a4b70d3a6fb3e83eaff3869fcb", "shasum": "" }, "require": { @@ -206,7 +205,7 @@ "laravel": "^10.50.4|^11.56.1|^12.69.0|^13.30.0", "symfony": "^6.4|^7.0|^8.0" }, - "product-train": "3.9.1", + "product-train": "3.9.4", "payload-codecs": [ "avro" ], @@ -276,8 +275,7 @@ "issues": "https://github.com/durable-workflow/sdk-php/issues", "source": "https://github.com/durable-workflow/sdk-php" }, - "time": "2026-11-04T02:37:04+00:00", - "published-time": "2026-11-04T02:37:27+00:00" + "time": "2026-11-04T05:28:03+00:00" }, { "name": "guzzlehttp/guzzle", @@ -711,8 +709,7 @@ "type": "tidelift" } ], - "time": "2026-10-25T14:32:48+00:00", - "published-time": "2026-10-25T14:41:05+00:00" + "time": "2026-10-25T14:32:48+00:00" }, { "name": "pear/http_request2", @@ -772,8 +769,7 @@ "issues": "https://github.com/pear/HTTP_Request2/issues", "source": "https://github.com/pear/HTTP_Request2" }, - "time": "2026-09-04T10:44:42+00:00", - "published-time": "2026-09-04T10:45:33+00:00" + "time": "2026-09-04T10:44:42+00:00" }, { "name": "pear/net_url2", @@ -841,8 +837,7 @@ "issues": "https://pear.php.net/bugs/search.php?cmd=display&package_name[]=Net_URL2", "source": "https://github.com/pear/Net_URL2" }, - "time": "2025-03-24T08:03:00+00:00", - "published-time": "2025-03-26T10:43:16+00:00" + "time": "2025-03-24T08:03:00+00:00" }, { "name": "pear/pear_exception", @@ -901,8 +896,7 @@ "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR_Exception", "source": "https://github.com/pear/PEAR_Exception" }, - "time": "2021-03-21T15:43:46+00:00", - "published-time": "2021-03-21T15:44:58+00:00" + "time": "2021-03-21T15:43:46+00:00" }, { "name": "psr/clock", @@ -950,8 +944,7 @@ "issues": "https://github.com/php-fig/clock/issues", "source": "https://github.com/php-fig/clock/tree/1.0.0" }, - "time": "2022-11-25T14:36:26+00:00", - "published-time": "2022-11-25T14:59:22+00:00" + "time": "2022-11-25T14:36:26+00:00" }, { "name": "psr/container", @@ -1291,8 +1284,7 @@ "type": "tidelift" } ], - "time": "2026-10-12T14:32:04+00:00", - "published-time": "2026-10-14T06:48:32+00:00" + "time": "2026-10-12T14:32:04+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -1377,8 +1369,7 @@ "type": "tidelift" } ], - "time": "2026-10-28T09:35:49+00:00", - "published-time": "2026-10-28T10:07:45+00:00" + "time": "2026-10-28T09:35:49+00:00" }, { "name": "symfony/polyfill-php80", @@ -1625,8 +1616,7 @@ "type": "tidelift" } ], - "time": "2026-08-07T13:08:13+00:00", - "published-time": "2026-08-24T11:45:15+00:00" + "time": "2026-08-07T13:08:13+00:00" }, { "name": "symfony/translation", @@ -1719,8 +1709,7 @@ "type": "tidelift" } ], - "time": "2026-10-25T14:40:25+00:00", - "published-time": "2026-10-28T18:24:27+00:00" + "time": "2026-10-25T14:40:25+00:00" }, { "name": "symfony/translation-contracts", @@ -1802,8 +1791,7 @@ "type": "tidelift" } ], - "time": "2026-06-05T06:23:12+00:00", - "published-time": "2026-06-27T21:46:40+00:00" + "time": "2026-06-05T06:23:12+00:00" } ], "packages-dev": [], @@ -1816,5 +1804,5 @@ "php": "^8.4" }, "platform-dev": {}, - "plugin-api-version": "2.11.0" + "plugin-api-version": "2.9.0" } diff --git a/polyglot/qualified-artifact-tuple.json b/polyglot/qualified-artifact-tuple.json index 06175dab..4929fa92 100644 --- a/polyglot/qualified-artifact-tuple.json +++ b/polyglot/qualified-artifact-tuple.json @@ -5,7 +5,7 @@ "serverImage": "durableworkflow/server:3.0.5@sha256:3cb6e5bf8ec766ac2abd933369b40a77af62794b6d7b921ce21d3fb263e88b72", "artifacts": { "cli": "2.3.3", - "sdk-php": "3.9.1", + "sdk-php": "3.9.4", "sdk-python": "2.13.0", "sdk-rust": "4.10.3", "server": "3.0.5", diff --git a/tests/Feature/Workflows/ServiceMode/ConversationWorkflowTest.php b/tests/Feature/Workflows/ServiceMode/ConversationWorkflowTest.php index 1558b13e..820d2fc5 100644 --- a/tests/Feature/Workflows/ServiceMode/ConversationWorkflowTest.php +++ b/tests/Feature/Workflows/ServiceMode/ConversationWorkflowTest.php @@ -9,6 +9,7 @@ use App\Workflows\ServiceMode\Conversation\ConversationActivities; use App\Workflows\ServiceMode\Conversation\ConversationWorkflow; use App\Workflows\ServiceMode\WelcomeWorkflow; +use DateTimeImmutable; use DurableWorkflow\Bridge\Laravel\WorkerFactory; use DurableWorkflow\Client; use DurableWorkflow\Codec\AvroPayloadCodec; @@ -50,6 +51,8 @@ final class ConversationWorkflowTest extends TestCase private int $nextSequence = 1; + private DateTimeImmutable $clock; + /** @var array{type: string, value: mixed}|null */ private ?array $closed = null; @@ -163,6 +166,33 @@ public function test_undecided_proposal_changes_nothing_after_its_timeout(): voi $this->assertSame(0, ConversationRecord::query()->where('kind', 'action')->count()); } + public function test_a_wrong_decision_keeps_the_five_minute_approval_deadline(): void + { + $this->start([]); + $this->signal('message', 'Please refund my order'); + $this->assertSame(300, $this->lastEvent('ConditionWaitOpened')['timeout_seconds']); + + // Any signal wakes the open wait. The predicate is still false, so the + // worker reopens the wait with the time left, not another five minutes. + $this->advance(120); + $this->signal('decision', 'not-this-proposal', true, 'Stale'); + $reopened = $this->lastEvent('ConditionWaitOpened'); + $this->assertSame('decision-1', $reopened['condition_key']); + $this->assertSame(180, $reopened['timeout_seconds']); + + $this->advance(150); + $this->signal('decision', 'still-not-this-proposal', true, 'Stale'); + $this->assertSame(30, $this->lastEvent('ConditionWaitOpened')['timeout_seconds']); + + $this->advance(30); + $this->timeOutOpenWait(); + + $this->assertSame( + ['Turn 1: I can refund the most recent order once a reviewer approves. No decision arrived in time, so nothing was changed.'], + $this->replies(), + ); + } + public function test_conversation_ends_after_the_inactivity_timeout(): void { $this->start([]); @@ -227,6 +257,7 @@ private function start(array $input): void { $this->history = []; $this->nextSequence = 1; + $this->clock = new DateTimeImmutable('2026-11-04T02:36:05.295+00:00'); $this->closed = null; $this->input = $input; $this->process(); @@ -235,7 +266,12 @@ private function start(array $input): void private function signal(string $name, mixed ...$arguments): void { $this->record('SignalReceived', ['signal_name' => $name, 'value' => $this->codec->envelope($arguments)]); - $this->process(); + $this->process(woken: true); + } + + private function advance(int $seconds): void + { + $this->clock = $this->clock->modify("+{$seconds} seconds"); } private function timeOutOpenWait(): void @@ -248,8 +284,12 @@ private function timeOutOpenWait(): void /** * Runs workflow tasks until the workflow waits or closes. + * + * After a signal, a worker whose predicate is still false sends the open + * wait again. Native Server then records the old wait satisfied and opens + * a new one with the timeout the worker sent. */ - private function process(): void + private function process(bool $woken = false): void { for ($task = 0; $task < 50 && $this->closed === null; $task++) { $commands = $this->harness->runWorkflow( @@ -266,6 +306,11 @@ private function process(): void && $commands[0]['condition_key'] === $wait['condition_key']; if ($stillWaiting) { + if ($woken) { + $this->record('ConditionWaitSatisfied', ['sequence' => $wait['sequence'], 'condition_wait_id' => $wait['condition_wait_id']]); + $this->apply($commands[0]); + } + return; } @@ -333,7 +378,11 @@ private function apply(array $command): void */ private function record(string $type, array $payload): void { - $this->history[] = ['event_type' => $type, 'payload' => $payload]; + $this->history[] = [ + 'event_type' => $type, + 'timestamp' => $this->clock->format('Y-m-d\\TH:i:s.uP'), + 'payload' => $payload, + ]; } /**