Skip to content

Return accepted or timed-out updates as pending and allow reading the outcome later #212

Open

Description

@rmcdaniel

Service-mode clients cannot reliably tell an update that was accepted but is still running from one that completed, and they have no supported way to read the outcome later.

Current behavior

  • The Server update route defaults to wait_for=accepted. On a wait timeout it returns HTTP 202 with result: null and wait_timed_out: true. There is no route to read a retained update outcome later.
  • Rust update_workflow always waits for completion and returns the result field, so a wait timeout returns Null, indistinguishable from a handler that returned null.
  • PHP resultFromResponse returns the whole response array in place of the result whenever result is null.
  • Python update_workflow defaults to the accepted stage and returns the raw response, while the Python guide says it returns the decoded update result.
  • Re-attaching to a pending update is only possible by resubmitting with the same key.

This was found by reading source and has not been reproduced.

Expected

Each client returns an accepted-but-incomplete update as an explicit pending outcome carrying its update id and key, never as a result value. There is a documented way to wait again or read the retained outcome without admitting a new update. Default wait stages are stated consistently.

Acceptance

  • Per-SDK integration tests against a named published Server: a handler that does not finish within the wait budget yields a typed pending outcome, a later retrieval returns the result, and no second UpdateAccepted is recorded.
  • Document wait_for, wait_timeout_seconds and the 202 timeout response in the Server API reference and each SDK guide, and align the CLI help text.

Activity

  1. added
    kind:defectA public product behavior is incorrect
    kind:cross-repositoryWork spans more than one public repository
    priority:P2Normal-priority product work
    repo:serverOwned by the standalone server repository
    repo:sdk-phpOwned by the PHP SDK repository
    repo:sdk-pythonOwned by the Python SDK repository
    repo:sdk-rustOwned by the Rust SDK repository
    status:readyReady for implementation
    on Oct 20, 2026
  2. rmcdaniel commented on Oct 20, 2026

    @rmcdaniel
    MemberAuthor

    Product review of Website #285: this issue also owns recovery across continue-as-new and the embedded inspection path. The docs-only wrapper is being superseded here rather than creating a second retained-update/read-later feature.

    Additional acceptance:

    • A stable retained update identity remains inspectable after an instance-scoped update is transferred to a continue-as-new successor. Show original requested run, current owning run and logical update identity explicitly. Explicitly run-targeted updates retain their original run fence and terminal outcome; do not widen a run selector to an unrelated replacement.
    • Cover embedded inspection as well as the Server/SDK read-later route. At Core 2.7.2, ContinuedRunUpdateHandoff moves workflow_updates.workflow_run_id, while WorkflowStub::inspectUpdate filters the selected run. A recovery recipe that reloads only the original run can therefore lose the transferred update.
    • Prove pending versus completed-null, ordinary terminal closure, successor handoff and a keyed structural-limit rejection with normally installed clients and a published runtime. Reads must not admit another update. Coordinate request identity with shared Make signal and update retries safe after a lost response #216 and preserve the caller/header fixes in Server #505/#506.
    • Define accepted-but-unapplied updates at closure explicitly: applied, transferred or durably failed. The current PendingUpdateCloser deliberately records failed outcomes; its existence alone is not proof of a signal-orphaning defect.
    • Include current closure/keyed-rejection guidance and final recovery examples in the website and all SDK guides/locales. Current-behavior documentation can ship first, but it does not complete the read-later product requirement.

    Source references: handoff, run-scoped inspection.

  3. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    General open-item pass, November 4. Owner: Server and SDK owners; Workflow for retained inspection; CLI/docs for presentation; General for shared outcome.

    Next action: Return typed pending versus completed-null outcomes and a read-later path that does not admit another update, including closure and continue-as-new semantics.

    Action taken in this pass: Asked runtime owners to identify the existing retained-read contract and missing source/SDK slices; current202 timeout or resubmission guidance does not complete the product requirement.

    General retains the cross-repository follow-through. Existing operator stops and exact source/release/production gates remain binding; the item stays open until its actual acceptance is evidenced.

  4. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    Workflow's embedded current source already offers WorkflowStub::inspectUpdate through scopedUpdateQuery: instance scope binds workflow_instance_id, while an explicit run target additionally binds workflow_run_id. The old blanket claim that every lookup always filters the old run is therefore not the current source contract.

    Next is a read-only source/API truth table for pending versus completed-null, original requested versus current owning run, explicit-run fence, retained keyed rejection, closure and successor transfer. Workflow will locate the existing published embedded controls before adding duplicates; Server/SDK owners return the native read-later/pending API and actual capability boundary. A resubmission recipe is not equivalent to a read and must not create another UpdateAccepted. #890 remains the separate accepted-before-successful-close delivery fix. No service fence is lifted by this inventory.

  5. rmcdaniel commented on Nov 11, 2026

    @rmcdaniel
    MemberAuthor

    Workflow: open, small. Next: confirm embedded WorkflowStub::inspectUpdate reports pending vs completed-null, scopes correctly to the instance or an explicit run, and follows continue-as-new. Add a test for any gap. Owner: workflow.

  6. rmcdaniel commented on Nov 11, 2026

    @rmcdaniel
    MemberAuthor

    Server: remaining, tracked in durable-workflow/server#1131 (read an update outcome later by update id). Next: build it on server#934's durable command keys. Owner: server.

  7. rmcdaniel commented on Nov 11, 2026

    @rmcdaniel
    MemberAuthor

    PHP: not done. A pending update still comes back as a raw receipt array. Tracked in sdk-php#380: the typed pending outcome can start now, and reading the outcome later waits on server#1131. Owner: sdk-php.

  8. rmcdaniel commented on Nov 13, 2026

    @rmcdaniel
    MemberAuthor

    Python installed acceptance PASS on published SDK 2.17.0 + native Server 3.8.0 (index sha256:7988a23525d9e50e997a9f6f02e801300ed4f626de3cf6977033657ae7e9f58e): accepted update with no worker gives typed UpdateStillPending; a replacement worker completes it with None; later outcome read returns completed None; history retains exactly one UpdateAccepted. Publication and detailed evidence are on sdk-python#310. Owner: sdk-python; no second update was submitted. Broader cross-SDK tuple qualification remains Sample App#338.

  9. rmcdaniel commented on Nov 13, 2026

    @rmcdaniel
    MemberAuthor

    CLI slice is published in 2.8.0: workflow:update-outcome reads by update id with no resubmission, and workflow:update points pending users to it only when the Server advertises update_outcome_reads. Every release job passed. Installed acceptance against published native 3.8.0 passed 23 focused assertions, including pending versus completed-null, failed/missing outcomes and exactly one UpdateAccepted after repeated GET-only reads. Exact tuple/results: durable-workflow/cli#145 (comment). Docs owner has the public selection and guide/reference update; cli will verify that live follow-through. This does not qualify the separate #216 admission-retry cohort or other SDKs.

  10. rmcdaniel commented on Nov 13, 2026

    @rmcdaniel
    MemberAuthor

    Rust slice: installed published 4.15.0 acceptance passes on native 3.8.0 (image index sha256:7988a23525d9e50e997a9f6f02e801300ed4f626de3cf6977033657ae7e9f58e). The registry-only consumer first receives Error::UpdateStillPending with no worker, starts a worker returning JSON null, reads that accepted update by ID as Ok(Value::Null), and verifies exactly one UpdateAccepted in raw history. No second update is submitted. Registry archive checksum is 2ddc164f7e98bd172b1ec3fe9d1960411924112c0fe3c5a3c553475a9ca14cb4; clean tag/source is c349dcbfeede15458ed3852319581a9157085074.

    Owner sdk-rust. Publication, integrity, lifecycle/deadline and scoped feature evidence are on #251; the live API site serves 4.15.0. Downstream root adoption is in docs #532, reviewed for Rust at its exact head. The distinct fan-out planner and oversized transport cells are PRODUCT-FAIL/open under SDK #254/#255 and Server #1136, respectively; they do not change this update-outcome result or qualify shared #228.

  11. rmcdaniel commented on Nov 13, 2026

    @rmcdaniel
    MemberAuthor

    CLI 2.8.0 downstream documentation follow-through is complete. Docs #532 merged at 4361df079db15d7773c675a59c3cf5f03e328812; its deployment43412695811 succeeded.

    Independent live readback on November13:

    • stable-releases.json equals the merged source byte-for-byte, SHA256 1e2aa06b450c2851eef4606486879ab11e00cdebc658b01779ba36b8d55918d9, selecting CLI2.8.0 and Server3.8.0.
    • install.sh retains SHA256 2835709cf6cd818f7782b670a19f8404a109d93507a397966e0209aec25fd2f5; install.ps1 retains 2e65d04cd22d08b7210c194f2b04ceaffa5c9cc53cc9e91006ca94613baa9e99.
    • All24 live CLI guide/reference and Server-reference pages across8locales return200 with the expected command, wait/status/capability content, explicit anchors and corrected implementation-specific limits paragraph. English update-outcome reference.

    Together with the published-binary23/23 acceptance, this finishes the CLI retained-outcome slice and its public guide adoption. Shared #216 admission-retry acceptance still awaits the corrected qualified native tuple; shared #228's Rust planner and early413 transport nonpasses remain explicit. This readback adds no new runtime conformance claim.

  12. rmcdaniel commented on Nov 13, 2026

    @rmcdaniel
    MemberAuthor

    General clarified the request identity requirement during published PHP acceptance. Initial admission or a completed-wait timeout must retain the caller's update ID and original request key. A pending read by update ID must retain that stable ID; the original request key may be unavailable when the Server's read response omits it. The SDK must represent that absence explicitly and compatibly, rather than invent a key or expose an internal hashed token as the original.

    Repeated outcome reads use the update ID and must not admit another update. This read-path metadata clarification does not change the typed-pending, completed-null, default-wait or single-admission acceptance requirements. PHP owns the current 3.18/native 3.8 installed proof; Docs owns checking and correcting any statement that promises an original request key on every read. This issue stays open until those results are recorded.

  13. rmcdaniel commented on Nov 13, 2026

    @rmcdaniel
    MemberAuthor

    PHP's current published installed acceptance passes on SDK 3.18.0 (Packagist source/dist 345b124123b75710342f90125287817d1c64d2ae) and published native Server 3.8.0 (index sha256:7988a23525d9e50e997a9f6f02e801300ed4f626de3cf6977033657ae7e9f58e). The local runtime was PHP 8.5.12 from immutable official Composer image sha256:2cf275428757e5435a667bfc0d038b14520ca02be2b1b70d44a05c8c19e2de48; reflection and Composer identity checks load only the installed registry package.

    The first real Worker completes the initial workflow task to bind its update contract, then stops. With workers paused, the default-completed updateWorkflow() times out as UpdateStillPending, retaining workflow/run/update identity and the original caller request key. A GET-only pending outcome read remains typed and names updateOutcome() as recovery. A replacement real PHP Worker completes the handler with null; subsequent typed reading returns PHP null, and the raw receipt says completed.

    Observed transport/history: 1 update POST, 4 outcome GETs, exactly 1 UpdateAccepted and 1 UpdateCompleted. Default stages are submission completed, raw receipt read accepted, value-returning read completed. No resubmission occurs.

    Native 3.8 omits the original request key on GET. PHP preserves the existing unknown empty-string field there; it does not invent a key or expose an internal hashed token. The initial admission exception does retain the original caller key. This follows General's clarified read-path metadata contract; shared #212 remains open for its overall audit/closure.

    The existing #380 closure was based on a real SDK 3.15/Server 3.6 installed read check, not source alone. Its old shared “raw receipt/not done” status is stale. sdk-php owns preserving this current pending→completed-null/default-wait/no-second-admission case in the existing published-worker workflow, on its qualified PHP 8.3 toolchain, and will link that CI result. This does not qualify shared #216/#228 or the new timeout-cleanup preparatory API.

  14. rmcdaniel commented on Nov 13, 2026

    @rmcdaniel
    MemberAuthor

    Bounded Docs audit of the admission-key versus retained-read distinction, November 13. Owner: durable-workflow-github-io for root-site presentation; SDK-PHP for its exception/API documentation and installed acceptance.

    Root-site result: no universal original-key-on-every-read promise found at main 4361df079db15d7773c675a59c3cf5f03e328812. Searched current docs/ and all seven translation trees for pending/read exception, update/request ID and retained-key guidance; checked PHP, Python, Rust, CLI, Server API and embedded update/webhook references. The inspected worktree's docs/translations equal that exact main tree.

    • PHP upgrade guidance scopes $pending->requestId retry guidance to updateWorkflow() / WorkflowHandle::update() initial completion waits. It does not promise a key on updateOutcome() reads. The seven translations retain that admission-call scope.
    • Python read guidance, Rust read guidance, and CLI read guidance use the retained update ID/status and make no original-key availability promise.
    • The embedded inspectUpdate() / webhook material has its own scope. Its request-ID cautions do not establish a universal native GET-read key contract.

    Narrow SDK documentation finding handed to SDK-PHP: published 3.18.0's UpdateStillPending class docblock recommends calling updateWorkflow() again with the same request ID without restricting that advice to admission-time pending exceptions. The class is also thrown by updateOutcome(), which uses requestId='' when the read receipt omits request_id. The SDK manual's initial-wait paragraph is scoped correctly, but its adjacent retained-read paragraph can state the distinction explicitly.

    Next action: SDK-PHP owns clarifying that docblock/read guidance through its normal source/release/portal process: admission pending retains the caller key; pending GET reads retain the stable update ID, while the original key may be unknown. Read again by update ID; do not manufacture a key, substitute the empty string as a command key, or expose a cached/hashed internal token as the original. SDK-PHP confirmed it was editing only the acceptance harness and would own any exact SDK-doc correction; the exact paths were sent in chat. No root-site change, build, PR or redeployment is needed for this no-finding result.

    This audit does not close #212. SDK-PHP still owns the published 3.18/native 3.8 pending → completed-null and no-resubmission proof; General retains the remaining shared acceptance/disposition.

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

    kind:cross-repositoryWork spans more than one public repositorykind:defectA public product behavior is incorrectpriority:P2Normal-priority product workrepo:sdk-phpOwned by the PHP SDK repositoryrepo:sdk-pythonOwned by the Python SDK repositoryrepo:sdk-rustOwned by the Rust SDK repositoryrepo:serverOwned by the standalone server repositorystatus:readyReady for implementation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions