Skip to content

Explain activity argument and result compatibility for runs in progress #363

Closed

Description

@rmcdaniel

Delivery verified

The published Workflow 2.11.3 follow-through is delivered in PR #379, merge 8510665af4fdc9aa2aff8ca7df27ca6708cd7440.

Final exact-head all-locale qualification, artifact deployment and actual Pages publication passed. All eight live versioning guides return HTTP 200 with the new embedded-activity-argument-safeguard section, minimum 2.11.3, typed refusal, affected 2.11.2 warning and unchanged historical SDK baselines. All eight table-of-contents links, canonicals/alternate-language entries and exact installed-evidence links passed. English, Japanese and Simplified Chinese 390px light/dark views were inspected with legible fonts and no document overflow.

Live sections: English, Ukrainian, Spanish, Portuguese, Chinese, Japanese, French, German.

The guide links the normal installed cold-process receipt: five PHP processes / 52 assertions, including refusal before the body and recovery of the same original input under two authored attempts. The exact published PHP/Laravel profile is terminal SUCCESS, all 17 jobs including 16 upgrades. No Core-profile wait remains.

Refusals consume the normal recorded activity attempts/backoff/deadline policy. There is no promise of indefinite waiting, terminal reopening, restored attempts, extended deadlines, missing-result-field repair or implicit Server/native/SDK tuple adoption. Older failure observations and the original independent SDK checks remain preserved. Sample #275 consumer/main/image qualification stays separate.

The first browser verifier used a misspelled pre-existing input anchor. Its failed observation was retained separately; the corrected verifier checks the actual additive-activity-input anchor and all eight pages pass. No product run was repaired or reclassified by that verifier correction.

Original report and acceptance

Problem

The versioning guide explains how a run's recorded start arguments bind to new workflow code. The signals guide has a recipe for evolving a signal payload. Nothing covers activity arguments and results, which change more often than either.

  • Each activity attempt passes the arguments recorded when the activity was scheduled to whatever activity code is deployed at that moment (RunActivityTask.php). A retry, or an attempt that starts after a deploy, runs new code with old arguments.
  • Replay checks the step shape and activity type, not argument values. It then hands the recorded result to the current workflow code (WorkflowExecutor.php). Changing an activity's input or result shape raises no history mismatch. New code just receives old data.

Users can't tell which activity changes are safe for runs already in progress, or what fails when a change isn't.

Acceptance criteria

  • The versioning guide has an activity section. It states that attempts receive their recorded arguments and that replay passes recorded results to current workflow code, for embedded PHP and the PHP, Python and Rust SDKs.
  • It shows one additive change per direction: a new optional activity argument with a default, and a new result field that workflow code reads with a fallback. It also says when to use a new activity type or getVersion() instead.
  • It states what happens when recorded arguments no longer bind to the current activity signature, and when workflow code can't read an old result. Both are verified against named published releases.
  • It links to the signal payload recipe and the workflow start-argument rules so all three are found together.

Activity

  1. added
    documentationImprovements or additions to documentation
    priority:P2Normal-priority product work
    status:readyReady for implementation
    completion:evidence-requiredClose only after all explicit acceptance and operational evidence is public
    on Oct 23, 2026
  2. rmcdaniel commented on Oct 23, 2026

    @rmcdaniel
    MemberAuthor

    Accepted as P2 documentation work after product review. Recorded activity
    arguments and completed results are immutable application contracts; deployment
    does not migrate them. The guide needs safe additive examples and a verified
    explanation of incompatible activity binding and result use. A new activity
    type or version branch is appropriate when that contract changes.

    The text must describe the code selected by the worker/build routing policy,
    not imply every attempt always runs the newest deployment. Workflow entry
    safeguards delivered in Workflow #866 and shared #221 do not by themselves
    establish an activity-input or old-result safeguard. If the required published
    reproductions expose lost work, corruption or an unrecoverable deployment
    failure, that requires a linked product fix rather than a workaround presented
    as completion of this issue.

    Next action: qualify the argument/default and result/fallback examples against
    named normally installed embedded and three-SDK releases, then update the
    versioning guide, its cross-links and all eight offered languages with matching
    source hashes. General owns this after the active P1 Workflow #908 delivery.

  3. rmcdaniel commented on Oct 23, 2026

    @rmcdaniel
    MemberAuthor

    Published-package qualification now covers five fresh embedded originals on
    normally installed Workflow 2.11.2 (a730773a), PHP 8.5.12 / Laravel 13.38.0 /
    SQLite, across separate PHP invocations with the original database retained:

    Change at the cold boundary Observed original outcome
    Add optional trailing string $currency = 'USD' to an activity Completes with original amount 7 and default USD; one attempt, zero repairs
    New workflow reads an older result with currency ?? 'USD' Completes without rerunning the completed activity; one attempt, zero repairs
    Add required activity argument ArgumentCountError, ActivityFailed, then the uncaught failure closes the workflow
    New workflow requires missing old result field ErrorException, WorkflowFailed; completed activity remains completed
    Change activity scalar argument to an injected DTO with a default amount Incorrect successful completion with amount 0 instead of recorded 7

    The last result is a confirmed product defect, now owned in
    Workflow #910.
    It is not a documentation workaround or inferred SDK behavior. The DTO
    assertion fails; its original run, immutable argument hash, one attempt and
    six success events are recorded there. No altered business payload or
    history repair was used. The first fixture used the unsupported sync queue
    and refused before activity admission; a fresh original with the supported
    async configuration is separate. Each subsequent PHP phase retained its
    30-second outer bound.

    This issue remains open for the compatibility guide and all-locale delivery,
    including independently verified published PHP/Python/Rust SDK outcomes. The
    safe additive examples are qualified for embedded PHP. The DTO safeguard and
    its recovery semantics must be implemented and published through #910 before
    we claim protection for it. Core's workflow-entry guard does not protect
    activity entry methods.

  4. rmcdaniel commented on Oct 23, 2026

    @rmcdaniel
    MemberAuthor

    Taking the versioning guide and translated compatibility guidance, including independent normally installed PHP/Python/Rust SDK checks. The embedded qualification above remains the baseline, and Workflow #910 owns the confirmed activity DTO defect. This documentation change will distinguish safe additive changes, ordinary application failures and any published safeguards; it will not treat a workaround as delivery of #910.

    Next: qualify recorded activity argument binding and old-result consumption, open a draft PR, and build all eight languages before deployment.

  5. rmcdaniel commented on Oct 23, 2026

    @rmcdaniel
    MemberAuthor

    Published SDK qualification is complete for the four activity-compatibility cases in each SDK. Product review remains unchanged: stored arguments/results are application contracts; Workflow #910 owns the embedded DTO defect, and no unpublished safeguard is claimed. Draft guide and all seven translations are in #366.

    Normally installed tuple: PHP SDK 3.2.0 (dfaae75a94b1d855379863799ce2c9a53f900f97), Python SDK 2.9.0 (wheel SHA-256 1e7547e895485dc8f670bd1b9cc34184b135f249db8da98b16b250bfc6002f73), Rust crate 4.2.2 (registry checksum 566d75ceb2cf82fade296cdb6df931b7066473d781457253dd528de436a8f962), published PHP Server 2.11.0 at immutable image index sha256:b29157eb2cf844916debbbb3c01b3257672e0dece778a9c6fa539043a3038f02. Each SDK came from its normal registry, without a source overlay. This is a functional amd64/SQLite qualification, not an ARM, native Server, Cloud or performance claim. Runner sources are attached below.

    Each original was started with amount 7, original run/execution bounds of 30 seconds, and one recorded activity attempt (max_attempts = 1). Separate seed and resume processes preserve the database. For input cases, the old workflow schedules before the cold boundary; for result cases, the old activity completes before the cold workflow replay. Worker-registration/fingerprint source remains stable while the fixture selects its deployment phase. No input/history edits, repairs, deadline extension, replacement of an original, or activity replay were used.

    Case PHP 3.2.0 Python 2.9.0 Rust 4.2.2
    Optional argument/default Completes with 7/USD Completes with 7/USD Added Serde-default field completes with 7/USD
    Read old result with fallback Completes with 7/USD Completes with 7/USD Serde-default result reader completes with 7/USD
    Require missing activity input Activity ArgumentCountError; uncaught activity failure closes run Activity TypeError; uncaught activity failure closes run Typed input decode refuses before callback; uncaught activity failure closes run
    Require missing old result field Application reader TypeError; workflow fails Reader KeyError; workflow fails Typed result decode error; workflow fails

    These failure outcomes follow the authored one-attempt policy; they do not imply that every deployment mismatch always fails a run immediately. The completed activity remains completed when the new reader fails. PHP worker failure metadata preserves the reported exception type even where the transport wrapper class is RuntimeException.

    After all writers stopped: API exited 0, no OOM. A SELECT-only audit confirms 12 originals, 6 completed runs, 6 expected failed controls, 36 first-attempt tasks, 12 activity attempts, zero repairs and exactly one activity schedule/start per original. Each stored activity argument frame matches its original ActivityScheduled record. Every selected original closed within its original deadline. The database SHA-256 was unchanged by the audit. PHP/Python snapshots additionally assert unchanged run identity, deadlines and history prefix; Rust seed/resume asserts the same original identity, and the database audit checks its lifecycle and stored input.

    The initial fixture attempts are retained separately and are not passes: an unreachable host-to-Docker route, an incorrect Python method name, missing PHP bounded-transport/bootstrap support, and an observation window too short for the initial fixture. Fresh originals use the corrected fixture; no failed original was repaired or replaced and called passing. Those setup errors establish no product finding.

    SDK/case Original run Outcome
    py/optional 01m5ktr4e1bnp6vzq1dsy30a0j completed
    py/result-fallback 01m5ktrn9bgy86mxnvwgxpmy4n completed
    py/result-required 01m5kts93m10zhsjqmd91e6twk failed
    py/required 01m5ktssgsea7cfn523fev7trb failed
    php/optional 01m5ktv9gt3h51n9g4dnekwb65 completed
    php/result-fallback 01m5ktvs9s7bx3a9v7ccfys9hz completed
    php/result-required 01m5ktw5eaepz9czac4b3h6jwm failed
    php/required 01m5ktwgx76fhspdfnb2rtah3s failed
    rust/optional 01m5ktva2f1mj66pwq4p6e18v8 completed
    rust/result-fallback 01m5ktvqqanntqtt42qzkrbm7p completed
    rust/result-required 01m5ktw2k0fen42h6efeejyrjc failed
    rust/required 01m5ktwf5e2tmjmzzjggrxx55z failed
    Standalone qualification sources and invocation

    These are bounded, synthetic fixtures, not production configuration. Start the pinned Server in an isolated Docker network with alias api, bootstrap its own SQLite database, and place these files at the root of a task mount. Install Python durable-workflow==2.9.0 into venv, Composer durable-workflow/sdk:3.2.0 into php, and build the Rust manifest under rust. Run each of optional, result-fallback, result-required, required with a new unique workflow ID, first seed, then immediately resume in a fresh process. Do not reuse an expired original. The SDK worker processes must join the same Docker network.

    python python_probe.py CASE seed UNIQUE_ID
    python python_probe.py CASE resume UNIQUE_ID
    php php_probe.php CASE seed UNIQUE_ID
    php php_probe.php CASE resume UNIQUE_ID
    rust/target/debug/website363-activity-compatibility CASE seed UNIQUE_ID
    rust/target/debug/website363-activity-compatibility CASE resume UNIQUE_ID
    

    Python needs PROBE_URL=http://api:8080. Container mount root is /task; the Rust fixture writes its two receipts there. The PHP fixture delegates bounded HTTP calls to the SDK transport and recreates that same local endpoint for independent lease maintenance. No lease response or business admission is fabricated.

    python_probe.py (SHA-256 e80af0f4ff21319bc92748893d1377563e70934d84ea7a8ce52f39df374df7de)

    import asyncio
    import importlib.metadata
    import json
    import os
    import sys
    import time
    from pathlib import Path
    import httpx
    from durable_workflow import Client, Worker, workflow, activity
    
    CASE, PHASE, ID = sys.argv[1:]
    URL = os.environ['PROBE_URL']
    ROOT = Path(__file__).parent
    CLAIMS = 0
    
    @workflow.defn(name='website363.python')
    class PriceWorkflow:
        def run(self, ctx, amount):
            result = yield ctx.schedule_activity('website363.price', [amount], retry_policy={'max_attempts': 1})
            currency = result['currency'] if CASE == 'result-required' and PHASE == 'resume' else result.get('currency', 'USD')
            return {'amount': result['amount'], 'currency': currency}
    
    if CASE == 'required' and PHASE == 'resume':
        @activity.defn(name='website363.price')
        async def price(amount: int, currency: str):
            return {'amount': amount, 'currency': currency}
    elif CASE == 'optional' and PHASE == 'resume':
        @activity.defn(name='website363.price')
        async def price(amount: int, currency: str = 'USD'):
            return {'amount': amount, 'currency': currency}
    else:
        @activity.defn(name='website363.price')
        async def price(amount: int):
            return {'amount': amount}
    
    class ObservedClient(Client):
        async def poll_workflow_task(self, **kwargs):
            global CLAIMS
            if PHASE == 'seed' and CLAIMS:
                await asyncio.sleep(.02)
                return None
            task = await super().poll_workflow_task(**kwargs)
            if task:
                CLAIMS += 1
            return task
    
        async def poll_activity_task(self, **kwargs):
            if PHASE == 'seed' and not CASE.startswith('result-'):
                await asyncio.sleep(.02)
                return None
            return await super().poll_activity_task(**kwargs)
    
    async def main():
        assert importlib.metadata.version('durable-workflow') == '2.9.0'
        headers = {'X-Namespace': 'default', 'X-Durable-Workflow-Control-Plane-Version': '2'}
        async with ObservedClient(URL) as client, httpx.AsyncClient(base_url=URL, headers=headers, timeout=5) as http:
            worker = Worker(client, task_queue=ID, workflows=[PriceWorkflow], activities=[price], worker_id=ID+'-'+PHASE, poll_timeout=1, heartbeat_interval=1)
            running = asyncio.create_task(worker.run())
            try:
                if PHASE == 'seed':
                    await asyncio.sleep(.3)
                    handle = await client.start_workflow(workflow_type='website363.python', task_queue=ID, workflow_id=ID, input=[7], run_timeout_seconds=30, execution_timeout_seconds=30)
                deadline = time.monotonic()+20
                while time.monotonic() < deadline:
                    if running.done():
                        await running
                        raise AssertionError('worker exited before observation')
                    response = await http.get('/api/workflows/'+ID)
                    response.raise_for_status()
                    state = response.json()
                    hist = await http.get('/api/workflows/'+ID+'/runs/'+state['run_id']+'/history')
                    hist.raise_for_status()
                    history = hist.json()
                    events = [e['event_type'] for e in history['events']]
                    target = 'ActivityCompleted' if CASE.startswith('result-') else 'ActivityScheduled'
                    if (PHASE == 'seed' and target in events) or (PHASE == 'resume' and state['is_terminal']):
                        break
                    await asyncio.sleep(.05)
                else:
                    raise AssertionError(('observation timeout', state, events))
            finally:
                await worker.stop()
                await asyncio.wait_for(running, 8)
            snapshot = {'sdk': '2.9.0', 'case': CASE, 'phase': PHASE, 'state': state, 'history': history}
            path = ROOT/(ID+'-'+PHASE+'.json')
            path.write_text(json.dumps(snapshot, ensure_ascii=False, indent=2))
            if PHASE == 'resume':
                before = json.loads((ROOT/(ID+'-seed.json')).read_text())
                assert state['run_id'] == before['state']['run_id']
                for key in ['run_deadline_at', 'execution_deadline_at']:
                    assert state[key] == before['state'][key]
                assert history['events'][:len(before['history']['events'])] == before['history']['events']
                assert events.count('ActivityScheduled') == 1
                assert events.count('ActivityCompleted') == (0 if CASE == 'required' else 1)
                assert state['status'] == ('failed' if CASE in ['required', 'result-required'] else 'completed'), state
                if state['status'] == 'completed':
                    assert (await client.get_workflow_handle(ID).result(timeout=2)) == {'amount': 7, 'currency': 'USD'}
            print(json.dumps({'sdk':'2.9.0', 'case':CASE, 'phase':PHASE, 'run_id':state['run_id'], 'status':state['status'], 'events':events}))
    
    if __name__ == '__main__':
        asyncio.run(main())

    php_probe.php (SHA-256 cf1404d91a06d28114148b4ab5cefb5101053a1a831027d12f559e66ee01e07c)

    <?php
    declare(strict_types=1);
    require __DIR__.'/php/vendor/autoload.php';
    use DurableWorkflow\Client;
    use DurableWorkflow\Worker;
    use DurableWorkflow\Worker\WorkflowContext;
    use DurableWorkflow\Worker\ActivityContext;
    use DurableWorkflow\Transport\Transport;
    use DurableWorkflow\Transport\Psr18Transport;
    [$script,$case,$phase,$id]=$argv;
    function requiredCurrency(string $value): string { return strtoupper($value); }
    final class ObservedTransport implements DurableWorkflow\Transport\BoundedTransport {
        public ?Worker $worker=null;
        public int $workflowClaims=0;
        private Psr18Transport $inner;
        private float $started;
        public function __construct() { $this->inner=new Psr18Transport(); $this->started=microtime(true); }
        public function supportsBoundedRequests(): bool { return true; }
        public function sendBounded(string $method,string $uri,array $headers,?array $body,int $timeoutSeconds): ?array {
            return $this->inner->sendBounded($method,$uri,$headers,$body,$timeoutSeconds);
        }
        public function send(string $method,string $uri,array $headers,?array $body=null): ?array {
            global $case,$phase,$id;
            if (microtime(true)-$this->started>20) {throw new RuntimeException('fixture observation bound');}
            if (str_ends_with($uri,'/query-tasks/poll')) {
                $state=getJson('/api/workflows/'.$id);
                $history=getJson('/api/workflows/'.$id.'/runs/'.$state['run_id'].'/history');
                $events=array_column($history['events'],'event_type');
                $target=str_starts_with($case,'result-')?'ActivityCompleted':'ActivityScheduled';
                if (($phase==='seed' && in_array($target,$events,true)) || ($phase==='resume' && $state['is_terminal'])) {$this->worker->requestShutdown();}
                return null;
            }
            if ($phase==='seed' && str_ends_with($uri,'/workflow-tasks/poll') && $this->workflowClaims>0) {return null;}
            if ($phase==='seed' && !str_starts_with($case,'result-') && str_ends_with($uri,'/activity-tasks/poll')) {return null;}
            $result=$this->inner->send($method,$uri,$headers,$body);
            if (str_ends_with($uri,'/workflow-tasks/poll') && isset($result['task'])) {++$this->workflowClaims;}
            file_put_contents(__DIR__.'/'.$id.'-'.$phase.'.jsonl',json_encode(compact('method','uri','body','result'),JSON_THROW_ON_ERROR)."\n",FILE_APPEND);
            return $result;
        }
    }
    function getJson(string $path): array {
        return json_decode(file_get_contents('http://api:8080'.$path,false,stream_context_create(['http'=>['timeout'=>3,'header'=>"X-Namespace: default\r\nX-Durable-Workflow-Control-Plane-Version: 2\r\n"]])),true,flags:JSON_THROW_ON_ERROR);
    }
    assert(Composer\InstalledVersions::getPrettyVersion('durable-workflow/sdk')==='3.2.0');
    $transport=new ObservedTransport();
    $client=(new Client('http://api:8080',transport:$transport))->withActivityLeaseBootstrap(__DIR__.'/php_lease.php');
    $worker=new Worker($client,$id,workerId:$id.'-'.$phase,heartbeatIntervalSeconds:1);
    $transport->worker=$worker;
    $worker->registerWorkflow('website363.php',static function(WorkflowContext $ctx,int $amount) use($case,$phase): array {
        $result=$ctx->activity('website363.price',[$amount],['retry_policy'=>['max_attempts'=>1]]);
        $currency=$case==='result-required' && $phase==='resume'?requiredCurrency($result['currency']):($result['currency']??'USD');
        return ['amount'=>$result['amount'],'currency'=>$currency];
    });
    if ($case==='required' && $phase==='resume') {
        $worker->registerActivity('website363.price',static fn(ActivityContext $ctx,int $amount,string $currency): array => ['amount'=>$amount,'currency'=>$currency]);
    } elseif ($case==='optional' && $phase==='resume') {
        $worker->registerActivity('website363.price',static fn(ActivityContext $ctx,int $amount,string $currency='USD'): array => ['amount'=>$amount,'currency'=>$currency]);
    } else {
        $worker->registerActivity('website363.price',static fn(ActivityContext $ctx,int $amount): array => ['amount'=>$amount]);
    }
    if ($phase==='seed') {$client->startWorkflow('website363.php',$id,$id,[7],executionTimeoutSeconds:30,runTimeoutSeconds:30);}
    $worker->run(1);
    $state=getJson('/api/workflows/'.$id);
    $history=getJson('/api/workflows/'.$id.'/runs/'.$state['run_id'].'/history');
    $events=array_column($history['events'],'event_type');
    file_put_contents(__DIR__.'/'.$id.'-'.$phase.'.json',json_encode(['sdk'=>'3.2.0','case'=>$case,'phase'=>$phase,'state'=>$state,'history'=>$history],JSON_THROW_ON_ERROR|JSON_PRETTY_PRINT));
    if ($phase==='resume') {
        $before=json_decode(file_get_contents(__DIR__.'/'.$id.'-seed.json'),true,flags:JSON_THROW_ON_ERROR);
        foreach(['run_id','run_deadline_at','execution_deadline_at'] as $key) {if ($state[$key]!==$before['state'][$key]) {throw new RuntimeException('changed '.$key);}}
        if (array_slice($history['events'],0,count($before['history']['events']))!==$before['history']['events']) {throw new RuntimeException('changed prefix');}
        $expected=in_array($case,['required','result-required'],true)?'failed':'completed';
        if ($state['status']!==$expected) {throw new RuntimeException(json_encode($state));}
        if ($expected==='completed' && $client->workflowHandle($id)->result(timeoutSeconds:2)!==['amount'=>7,'currency'=>'USD']) {throw new RuntimeException('output mismatch');}
        if (count(array_filter($events,fn($e)=>$e==='ActivityScheduled'))!==1) {throw new RuntimeException('activity rescheduled');}
    }
    echo json_encode(['sdk'=>'3.2.0','case'=>$case,'phase'=>$phase,'run_id'=>$state['run_id'],'status'=>$state['status'],'events'=>$events],JSON_THROW_ON_ERROR),"\n";

    php_lease.php (SHA-256 c2e230d8bc5ef4374e4ad2a5e97ebb2baa455d6b2eefe41811c61ac91f261ced)

    <?php
    require_once __DIR__.'/php/vendor/autoload.php';
    return new DurableWorkflow\Client('http://api:8080');

    rust/Cargo.toml (SHA-256 45c31fda00b44b68af0c5e1588cf97290c047e9749449b0d2c28c9bf183de039)

    [package]
    name = "website363-activity-compatibility"
    version = "0.1.0"
    edition = "2021"
    [dependencies]
    durable-workflow = "=4.2.2"
    serde = { version = "1", features = ["derive"] }
    serde_json = "1"
    tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

    rust/src/main.rs (SHA-256 7eb1f98c4eb47e72cfc629a471d4c0a4ca533bed1905eebad26e6ba7f0ebb183)

    use durable_workflow::{ActivityOptions, ActivityRetryPolicy, Client, Worker, WorkflowStartOptions};
    use serde::{Deserialize, Serialize};
    use serde_json::{json, Value};
    use std::{fs, time::Duration};
    
    fn usd() -> String { "USD".into() }
    #[derive(Serialize, Deserialize)]
    struct OldRequest { amount: i64 }
    #[derive(Deserialize)]
    struct OptionalRequest { amount: i64, #[serde(default="usd")] currency: String }
    #[derive(Deserialize)]
    struct RequiredRequest { amount: i64, currency: String }
    #[derive(Serialize, Deserialize)]
    struct CompatibleResult { amount: i64, #[serde(default="usd")] currency: String }
    #[derive(Serialize, Deserialize)]
    struct RequiredResult { amount: i64, currency: String }
    
    #[tokio::main]
    async fn main() -> Result<(),Box<dyn std::error::Error>> {
        let args:Vec<_>=std::env::args().collect();
        let (case,phase,id)=(&args[1],&args[2],&args[3]);
        let activity_queue=format!("{id}-activity");
        let client=Client::new("http://api:8080")?;
        let mut wf=Worker::new(client.clone(),id).worker_id(format!("{id}-wf-{phase}")).poll_timeout(Duration::from_secs(1));
        let result_required=case=="result-required" && phase=="resume";
        let aq=activity_queue.clone();
        wf.register_typed_workflow("website363.rust",move |ctx,amount:i64| {
            let aq=aq.clone();
            async move {
                let options=ActivityOptions::new().task_queue(aq).retry_policy(ActivityRetryPolicy::new(1));
                if result_required {
                    let result:RequiredResult=ctx.activity_typed_with_options("website363.price",options,OldRequest{amount}).await?;
                    Ok(json!({"amount":result.amount,"currency":result.currency}))
                } else {
                    let result:CompatibleResult=ctx.activity_typed_with_options("website363.price",options,OldRequest{amount}).await?;
                    Ok(json!({"amount":result.amount,"currency":result.currency}))
                }
            }
        });
        wf.set_workflow_definition_sources("website363.rust", &[include_str!("main.rs")])?;
        let mut act=Worker::new(client.clone(),&activity_queue).worker_id(format!("{id}-act-{phase}")).poll_timeout(Duration::from_secs(1));
        if case=="required" && phase=="resume" {
            act.register_typed_activity("website363.price",|_ctx,input:RequiredRequest|async move{Ok(json!({"amount":input.amount,"currency":input.currency}))});
        } else if case=="optional" && phase=="resume" {
            act.register_typed_activity("website363.price",|_ctx,input:OptionalRequest|async move{Ok(json!({"amount":input.amount,"currency":input.currency}))});
        } else {
            act.register_typed_activity("website363.price",|_ctx,input:OldRequest|async move{Ok(json!({"amount":input.amount}))});
        }
        wf.register().await?;
        act.register().await?;
        if phase=="seed" {
            client.start_workflow_with_options("website363.rust",id,id,WorkflowStartOptions::new().run_timeout_seconds(30).execution_timeout_seconds(30),json!([7])).await?;
            wf.run_once().await?;
            if case.starts_with("result-") {act.run_once().await?;}
        } else {
            if !case.starts_with("result-") {act.run_once().await?;}
            wf.run_once().await?;
        }
        client.deregister_worker_registration(&format!("{id}-wf-{phase}")).await?;
        client.deregister_worker_registration(&format!("{id}-act-{phase}")).await?;
        let state=client.describe_workflow(id).await?;
        let snapshot=json!({"sdk":"4.2.2","case":case,"phase":phase,"run_id":state.run_id,"status":state.status,"output":state.output});
        fs::write(format!("/task/{id}-{phase}.json"),serde_json::to_string_pretty(&snapshot)?)?;
        if phase=="resume" {
            let before:Value=serde_json::from_str(&fs::read_to_string(format!("/task/{id}-seed.json"))?)?;
            assert_eq!(snapshot["run_id"],before["run_id"]);
            let expected=if case=="required" || case=="result-required" {"failed"} else {"completed"};
            assert_eq!(state.status.as_deref(),Some(expected));
            if expected=="completed" {assert_eq!(state.output,Some(json!({"amount":7,"currency":"USD"})));}
        }
        println!("{snapshot}");
        Ok(())
    }

    Next: finish the documentation build, inspect rendered pages, run required PR checks, deploy all eight locales together and verify the live routes. #910 remains separate product work.

  6. rmcdaniel commented on Oct 23, 2026

    @rmcdaniel
    MemberAuthor

    Delivered in PR #366, merge 4b59cff29e39182208cdbe5a542d205aed379024.

    • Exact-head qualification built all eight languages, verified reviewed source hashes and matching code examples, and generated the LLM references. CodeQL, Sonar (zero new issues/hotspots), protocol and boundary checks passed.
    • Artifact deployment and the separate GitHub Pages publication both passed. The initial old-page observation happened before Pages finished and was not counted as delivery.
    • All eight canonical public versioning routes now return HTTP 200 with all five activity compatibility anchors, the four named package baselines, correct canonicals, eight language alternates plus x-default, and localized signal-recipe links.
    • Chromium exercised the signal recipe and back navigation from every language. English, German and Japanese passed 390px light/dark checks; their wide result tables scroll within the page and the document stays 390px wide.

    Start at the new activity compatibility section. The independently qualified SDK and embedded behavior is recorded above; the guide also links the workflow-start and signal evolution rules. The merged remote branch is deleted and its absence verified.

    This closes the documentation requirements. It does not fix the affected embedded 2.11.2 DTO behavior: Workflow #910 remains separate product work, and no unpublished safeguard or reopening of terminal originals is claimed.

  7. added
    status:doneDerived from the authoritative closed issue state
    completion:evidence-verifiedAcceptance, fixed version, and required operational evidence are publicly verified
    and removed
    status:readyReady for implementation
    completion:evidence-requiredClose only after all explicit acceptance and operational evidence is public
    on Oct 23, 2026
  8. rmcdaniel commented on Oct 23, 2026

    @rmcdaniel
    MemberAuthor

    The live CJK visual qualification has been repeated with actual Japanese and Chinese fonts installed in the browser environment. Both /ja/ and /zh-Hans/ versioning guides render legible glyphs, retain their canonical/alternate links, navigate to the localized signal recipe, and pass 390px light/dark reflow with the release table scrolling inside its own region. Initial missing-font screenshots establish no visual-readability claim; these font-qualified checks supersede that limitation. The website font assets and content did not change.

  9. rmcdaniel commented on Oct 23, 2026

    @rmcdaniel
    MemberAuthor

    Reopening for the published embedded-guard follow-through requested after the initial guide delivery. Workflow 2.11.3 is now published at source 71213ae1e0338cda5967e0840782a981530ed854; General's normal installed cold-process receipt passes five fresh PHP processes / 52 assertions, including one-attempt refusal and authored two-attempt recovery of the same original input. The exact 2.11.2 counterfactuals remain failures.

    The all-eight-locale update will name 2.11.3 as the minimum for this embedded activity guard, retain the older-version warning and historical SDK baselines, and explain normal attempts/backoff/deadlines and terminal-state limits. No broader Laravel/PHP profile pass, Server/native adoption, tuple promotion or additional-attempt promise follows. General retains product/consumer delivery. This issue will close again only after the updated guides are actually published and checked.

  10. added
    status:readyReady for implementation
    completion:evidence-requiredClose only after all explicit acceptance and operational evidence is public
    and removed
    status:doneDerived from the authoritative closed issue state
    completion:evidence-verifiedAcceptance, fixed version, and required operational evidence are publicly verified
    on Oct 23, 2026
  11. rmcdaniel commented on Oct 23, 2026

    @rmcdaniel
    MemberAuthor

    The published Workflow 2.11.3 follow-through is delivered in PR #379, merge 8510665af4fdc9aa2aff8ca7df27ca6708cd7440.

    Final exact-head all-locale qualification, artifact deployment and actual Pages publication passed. All eight live versioning guides return HTTP 200 with the new embedded-activity-argument-safeguard section, minimum 2.11.3, typed refusal, affected 2.11.2 warning and unchanged historical SDK baselines. All eight table-of-contents links, canonicals/alternate-language entries and exact installed-evidence links passed. English, Japanese and Simplified Chinese 390px light/dark views were inspected with legible fonts and no document overflow.

    Live sections: English, Ukrainian, Spanish, Portuguese, Chinese, Japanese, French, German.

    The guide links the normal installed cold-process receipt: five PHP processes / 52 assertions, including refusal before the body and recovery of the same original input under two authored attempts. The exact published PHP/Laravel profile is terminal SUCCESS, all 17 jobs including 16 upgrades. No Core-profile wait remains.

    Refusals consume the normal recorded activity attempts/backoff/deadline policy. There is no promise of indefinite waiting, terminal reopening, restored attempts, extended deadlines, missing-result-field repair or implicit Server/native/SDK tuple adoption. Older failure observations and the original independent SDK checks remain preserved. Sample #275 consumer/main/image qualification stays separate.

    The first browser verifier used a misspelled pre-existing input anchor. Its failed observation was retained separately; the corrected verifier checks the actual additive-activity-input anchor and all eight pages pass. No product run was repaired or reclassified by that verifier correction.

  12. added
    status:doneDerived from the authoritative closed issue state
    completion:evidence-verifiedAcceptance, fixed version, and required operational evidence are publicly verified
    and removed
    status:readyReady for implementation
    completion:evidence-requiredClose only after all explicit acceptance and operational evidence is public
    on Oct 23, 2026
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-verifiedAcceptance, fixed version, and required operational evidence are publicly verifieddocumentationImprovements or additions to documentationpriority:P2Normal-priority product workstatus:doneDerived from the authoritative closed issue state

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions