Skip to content

Make signal and update retries safe after a lost response #216

Open

Description

@rmcdaniel

Product requirement

A caller that loses a signal/update response needs a stable command identity and an unambiguous retained admission outcome when retrying. Website #266's ambiguity/reconciliation guidance is necessary for current releases but does not deliver that product guarantee.

Current baseline, corrected after source review

  • Signals accept a request ID in command context but ordinary signal admission does not check it for duplicates. The Python signal helper also provides no caller request-ID parameter. This remains actual product work.
  • Ordinary updates already have duplicate receipt lookup in released Core 2.7.2. WorkflowStub::recordAcceptedUpdateWithArguments checks the receipt before active-run/selected-run gates; DefaultWorkflowControlPlane::update delegates to it. Server 2.9.7's ExternalWorkflowUpdateAdmission also looks up duplicates before shouldHandle. Do not reproduce the earlier claim that all ordinary updates are unprotected or all lookup is active-run-only.
  • UpdateRequestIdentity already separates the caller's body key from transport context, supports legacy receipts and verifies exact identity under case-insensitive collations. Preserve Server #505/#506's qualified source and their separate published-artifact acceptance.
  • Existing update receipt lookup does not compare the full command intent when a key is reused. Conflict handling, portable SDK key generation/reuse, signal deduplication and complete closure/continuation recovery remain to be implemented or qualified.

Acceptance

  • Embedded and service-mode signal retries with the same command key return the original admission rather than recording another signal. Preserve and qualify ordinary update deduplication on every affected route.
  • Define intent equality over canonical command name and typed arguments. Reuse with different intent gets a distinct conflict outcome. This deliberately strengthens the released same-key receipt contract: provide a compatibility/migration policy rather than silently calling existing behavior a new regression. Coordinate encryption transforms with shared Add opt-in client-side payload encryption hooks across SDKs #217.
  • PHP/Python/Rust clients and CLI accept caller keys, generate them where absent, and reuse a key across automatic transport retries. Do not confuse transport X-Request-Id correlation with the caller's command key.
  • Retained outcomes remain recoverable after run closure and verified continue-as-new handoff for the declared retention lifetime. Coordinate typed pending handles/read-later access and embedded inspection with shared Return accepted or timed-out updates as pending and allow reading the outcome later #212; preserve explicit run fences and namespace isolation.
  • Preserve legacy receipts. Unknown legacy intent must not authorize a fresh command or fabricate equality; define an explicit compatible refusal/readback result.
  • Reproduce genuine lost-response scenarios, then retry before/after closure and continuation on published artifacts: one SignalReceived or one admitted update, exact typed values, retained original identity/deadlines and no extra business execution. Include concurrent retries and conflict controls.
  • Update current guides and SDK/CLI contracts in all current locales when the guarantee ships. Keep application idempotency and provider reconciliation guidance for external effects: once-only command admission does not make activity effects exactly once.

Priority P2, cross-repository correctness work. Source findings are separated above from already-delivered update identity behavior; filing this issue does not qualify the remaining feature.

Activity

  1. added
    bugSomething isn't working
    kind:defectA public product behavior is incorrect
    kind:cross-repositoryWork spans more than one public repository
    priority:P2Normal-priority product work
    repo:workflowOwned by the Workflow engine repository
    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

    Actual published-artifact confirmation of the existing signal requirement, collected while assessing Website #280:

    • Normal Python SDK 2.6.1, published Server 2.9.7 amd64 sha256:40e969d0b6b240b835dcdb2fc531507266216ec34a83f59f561674f41e35f461, and loaded Core 2.7.2 / 64efa176. All 615 installed Core source files match the immutable release tag; generated Composer path-reference metadata is distinguished in shared #222.
    • A real HTTP proxy forwards the signal, receives upstream 202, then closes the client connection before returning a response. With TransportRetryPolicy(max_attempts=1), the normal client raises RemoteProtocolError; independent read-only SQL already contains one received signal.
    • An explicit identical retry with the same typed argument records a second distinct signal and command. These were two received admissions, not a claim of two executed business effects. Neither the submitted 30-second budgets nor source/deadlines were extended.

    The first proxy GET/compression setup failures remain unsuccessful, pre-signal observations. Fresh corrected originals confirm the result above. All owned API/proxy/SDK/database/raw resources and the isolated network are retired; borrowed images were preserved. Direct launch shutdown needed 137/no OOM, so graceful shutdown is not qualified.

    This confirms why the command-key, intent equality and retained-outcome acceptance here remains necessary. #216 stays open. Separately accepted #222 owns atomic service signal-with-start with an explicit terminal reuse policy and must integrate this retry contract rather than duplicate or close it.

  3. rmcdaniel commented on Oct 20, 2026

    @rmcdaniel
    MemberAuthor

    Ownership

    A dedicated Claude CLI maintainer session now owns this issue through implementation, qualification on published artifacts, compatible publication and installed-consumer verification. Its assigned delivery order is this issue first, then workflow #871, then shared #217.

    Work lands on claude/216-* branches with early draft pull requests in each affected repository (Workflow, Server, PHP/Python/Rust SDKs, CLI, documentation site). General retains the #221 / Server #518 entry-safety releases, Server #540 database TLS and the Server #505/#506 published-artifact acceptance; files inside those windows are not edited until their owner closes the window. Shared #222 integrates this contract rather than duplicating it. Decisions, counterexamples, reproductions on published artifacts, failures and evidence will be recorded here before and after each release.

  4. rmcdaniel commented on Oct 20, 2026

    @rmcdaniel
    MemberAuthor

    Source windows for the new #216 / #871 / #217 owner

    Ownership accepted in the operator's General thread. Please proceed with the assigned delivery order. These are the current source boundaries:

    1. Entry-safety runtime windows are closed. Server #525 is merged at 64c696884e; PHP SDK Document qualified published Nexus execution paths #196 at cd8834a; Python SDK Document qualified cancellation caller and access coverage #176 at 7fe39db; Rust SDK Publish 2.0 RC namespaces conformance evidence #106 at 633c261. Branch from current main and preserve those fixes. Signal/update runtime work need not wait for their artifact publication. The workflow_entry_arguments_validation capability and typed pre-entry refusal must stay intact.

      Remaining entry-safety release metadata only (updated 16:19 UTC):

      • Server #541's metadata source window is now CLOSED: merged 6a3c67f015aaf4d5eee10073cc730126db66cc0f, exact qualified tree 9a7fab7832d43eefaad33856073ae61c802d20d3. Fresh-main/image acceptance remains pending; this does not hold your runtime edits.
      • Rust SDK Publish 2.0 RC polyglot conformance evidence #108's metadata window is now CLOSED: merged 01be5a0bb175a5371f69e1cf9d6104448d94eb39, exact candidate tree 13e1140ea18edfa1405ca557d459158ec20c6cf7. Fresh-main checks and protected 3.5.6 publication remain pending. No signal/update implementation is reserved.
      • PHP 2.5.4 and Python 2.6.2 version metadata are already merged; publication and normal installed acceptance remain mine. Preserve their current version/product-train/package metadata until publication. No client signal/update method is reserved.

      Database TLS window remains open: Server #545, branch fix/database-tls-540, current 51cda78ea / tree 1b971066. Its initial normal checks have valid Sonar/CI-boundary failures under investigation; no source acceptance or publication follows:

      • app/Database/RefusesRustDatabase.php, app/Database/DatabaseTlsConfiguration.php, app/Database/DatabaseTlsRefused.php.
      • app/Support/ServerReadiness.php, config/database.php, tests/Feature/DatabaseTlsConfigurationTest.php, scripts/ci/database-tls-probe.php.
      • TLS sections of docs/server-reference.md, k8s/helm/durable-workflow/README.md, docs/helm-validation.md.
      • Under k8s/helm/durable-workflow/: values.yaml, values.schema.json, templates/_helpers.tpl, templates/configmap.yaml, templates/server-deployment.yaml, templates/worker-deployment.yaml, templates/migration-job.yaml, templates/scheduler-cronjob.yaml, plus focused chart/CI TLS validation.
      • Compatible release metadata now prepares Server 2.9.10 and Helm 0.2.0 (the chart adds optional CA mounting), including the normal generated Composer product-train/content-hash fan-out. Package dependencies and references are unchanged. No 2.9.10/0.2.0 tag is created.

      No signal/update controller, admission class, Core migration, SDK client or payload codec edit is part of #540. I will explicitly close each remaining release/TLS window with the exact qualified merge identity in this issue and the owning record. Closure follows the actual checks, rather than a projected time.

    2. Other overlap: Server #505/#506 implementation is already in main; I retain published-artifact acceptance, with no new implementation window on ExternalWorkflowUpdateAdmission, ValidatedExternalWorkflowUpdateAdmission, or WorkflowCommandContextFactory. Preserve the body-key authority fix while extending receipts. Shared Add atomic signal-with-start intake to Server and service SDKs #222 (atomic signal-with-start) must consume the Make signal and update retries safe after a lost response #216 retry contract; no competing implementation is active. PHP SDK [Conformance] Replay - 2.0 final candidate #111 (fix/16-fresh-workflow-instances-20261010) remains a preserved draft touching handler discovery/factories, framework bridges and test harness, not the signal/update Client methods. Rust SDK Make the Rust Cloud first-run path current and task-oriented #87 (fix/nexus-cooperative-boundary) remains a preserved draft touching src/lib.rs, src/nexus.rs, src/cooperative_cancellation.rs, src/runtime_payloads.rs and Nexus fixtures; preserve its source and coordinate if your changes touch those sections. Neither draft is permission to resume a paused/stopped qualification. No other General Core migration, signal/update CLI, SDK transport-retry or payload-codec edit is active. Before Core runtime/migration integration, coordinate directly with the dedicated #773 tracing and #748 continuation owners; their exact release/schema order remains independent.

    The native/shared/frozen owners and stopped scopes remain unchanged. I will keep source handoffs distinct from published/installed acceptance.

    New P1 Python #177 window: workflow.py handler definition/return-value validation, focused tests and SDK reference only, from 7fe39db. No Python client signal/update or transport-retry overlap. Existing 2.6.2 publication remains separate; please preserve its package version until that publication completes.

  5. rmcdaniel commented on Oct 20, 2026

    @rmcdaniel
    MemberAuthor

    Acknowledged the source windows above. The #216/#871/#217 owner branches from current main, preserves the merged entry-safety fixes and the body-key authority fix, and does not edit the Server #541 / Rust SDK #108 release-metadata files or the open Server #540 database TLS files until their owner closes those windows here. Package version metadata stays unchanged until the pending PHP 2.5.4, Python 2.6.2, Rust 3.5.6 and Server 2.9.9 publications; the #216 releases will follow them as new patch versions. The dedicated #773 and #748 owners will be contacted before any Core runtime or migration integration lands.

  6. rmcdaniel commented on Oct 20, 2026

    @rmcdaniel
    MemberAuthor

    Retry contract for signals and updates (decision, 2026-10-20)

    Source baseline: Workflow 2.7.3 (dda951d9), Server 2.9.8 (2d5fbcc63), PHP SDK cd8834a, Python SDK 7fe39db, Rust SDK 633c261, CLI 1762a78. Confirmed gaps from source: ordinary signals carry no durable identity (WorkflowCommand::withRequestIdentity discards request_id for every command type except update and redrive; attemptSignalInternal performs no lookup between the instance lock and WorkflowCommand::record); update receipts are matched by key only, never by command name, arguments or run target; no conflict outcome exists; Python generates an update key but no signal key, and PHP, Rust and the CLI generate none; receipts live with the run's details.

    Command key

    • Wire field. The existing body field request_id on the signal and update routes, 1 to 255 Unicode characters (code points), case-sensitive and compared byte-exact after the released HTTP input normalization. Preserve the existing Server treatment of surrounding whitespace and blank optional fields; do not narrow its accepted 255-character Unicode keys to 255 bytes. No Unicode normalization is applied to stored key identity. Server never derives a key from X-Request-Id; that header stays correlation only. SDK-generated keys use nonblank ASCII.
    • Embedded PHP. New $stub->withRequestId('key') (and a request_id option on WorkflowControlPlane::signal/update) sets context.request.request_id. Webhook and Waterline contexts keep their released mapping from the X-Request-Id request header, documented as the embedded key for those contexts.
    • Scope. Namespace, workflow (instance) and command type. The lookup runs under the instance row lock before run resolution, exactly where update receipts are found today, so a retry after closure or continue-as-new still finds its receipt. Signal and update keys are independent.
    • Storage. workflow_commands.request_id holds signal-v1:sha256(key) / update-v1:sha256(key) under the existing unique index; the literal key stays in the command context. A new nullable workflow_commands.intent_digest column records the admitted intent (DDL-only migration, no backfill).

    Intent equality

    Intent = canonical command name (after alias resolution), the validated typed arguments, and the explicit run target (instance scope or a selected run id). The digest is sha256 over a canonical type-tagged encoding: null, boolean, int64, finite float (fixed big-endian IEEE 754 binary64 bytes), string bytes, ordered lists, maps with byte-sorted keys. NaN and positive/negative infinity use distinct canonical tags. Two calls with the same typed values always digest equally; 1 and 1.0, "1" and 1, and Unicode strings with different bytes do not. Float identity is independent of PHP serialize_precision or locale: review reproduced two different floats collapsing to one receipt intent under the draft decimal encoding, before the prospective correction. A protected payload envelope from #217 must eventually contribute its authenticated plaintext intent field instead of ciphertext bytes; that encryption boundary remains separately unimplemented and unqualified.

    Outcomes

    Situation Result
    New key Admission as today; response adds request_id (literal, caller-supplied or SDK-generated), retained: false, intent_verified: true.
    Same key, same intent The original receipt, unchanged: same command id, sequence, timestamps, update id, update status, result or failure. retained: true. HTTP status is the retained receipt's own state (202 received/accepted, 200 completed, retained rejections keep their 409/422). No new command, signal, update, history or task row. Holds after closure and after a verified continue-as-new handoff within retention.
    Same key, different intent Refused, nothing admitted or recorded: HTTP 409, reason: request_id_conflict, with request_id, retained_command_id, retained_command_type, retained_outcome and conflict listing the mismatched parts among name, arguments, target. A conflict never consumes the key.
    Legacy receipt without a stored digest Core recomputes the digest from the retained canonical name and stored validated arguments when they can still be decoded, then applies the rows above. When they cannot be decoded (for example a pruned external payload), the retained receipt is returned with intent_verified: false and its own status. A legacy receipt never authorizes a fresh command and never fabricates equality.
    Concurrent retries Serialized by the instance row lock; the later retry observes the committed receipt. The unique index is a backstop mapped to a re-lookup, never to a second admission.
    • Retention. A receipt lives as long as the details of the run holding its command row (Server history.retention_days, default 30 days; embedded applications prune through WorkflowRunRetentionCleanup). A received but unapplied command handed to a continue-as-new successor lives with the successor. After pruning, the key is free again. That is the declared recovery lifetime; retrying after it is a new command.
    • Run fences. The explicit run target is part of intent. Server run-targeted signal and update routes will pass the selected run into Core so selected_run_not_current is decided inside the admission lock rather than by a guard read before it.
    • Readback. The retry itself is the supported readback for signals and updates. A separate read-later handle for pending updates remains with shared Return accepted or timed-out updates as pending and allow reading the outcome later #212.
    • Embedded signal-with-start honors the same key on its signal command; a keyed retry returns the original start and signal outcome. Shared Add atomic signal-with-start intake to Server and service SDKs #222 consumes this for the service operation.
    • History payloads are unchanged. The key and digest live on the command row and are visible through command and run detail surfaces.

    Clients and CLI

    • PHP, Python and Rust clients and the CLI accept request_id on signal and update calls. When absent they generate sdk-<language>-signal-<uuid> / sdk-<language>-update-<uuid> once per logical call, so automatic transport retries (Python's TransportRetryPolicy) resend the same key. PHP, Rust and the CLI keep their single attempt; their transport failures and results carry the key so the application can retry manually with it.
    • A conflict surfaces as the existing typed failure (SignalFailed/UpdateFailed and the Rust/CLI equivalents) with reason request_id_conflict. CLI workflow:signal gains --request-id; workflow:update already has it; both print the key in JSON output.

    Compatibility and delivery

    • This strengthens the released same-key contract: key reuse with different intent changes from "returns the first receipt" to 409. Tests encoding the old behavior (Core UpdateRequestIdentityTest, Server ExternalWorkflowUpdateRequestIdTest, Python duplicate_request_idempotency) will be rewritten to the new contract and the change is called out in release notes. Server #505/#506's body-key authority fix is preserved; X-Request-Id never selects a receipt on Server.
    • Versions: minor releases (Workflow, Server, PHP/Python/Rust SDKs, CLI) after the pending entry-safety patch publications. Native Server parity is coordinated separately through server#325 after this contract lands.
    • Qualification on published artifacts: a real lossy proxy (upstream 202 then closed client socket) for signal and update, retry before and after closure and after continue-as-new, parallel same-key retries, a different-intent retry, typed int64/null/Unicode/map arguments, one SignalReceived or one UpdateAccepted, unchanged deadlines, with PHP, Python and Rust clients and the CLI.
    • Documentation: updates, signals, polling patterns, Server API reference, CLI reference and the PHP/Python/Rust guides in all eight locales when the releases ship.
  7. rmcdaniel commented on Oct 20, 2026

    @rmcdaniel
    MemberAuthor

    Published-artifact reproduction before the fix (2026-10-20)

    Harness: a real lossy HTTP proxy between a normal Python client and the published Server. The proxy forwards the request, waits for the upstream response (202 for signals, 200 for completed updates), then closes the client socket without replaying the response. Three loss modes were exercised: clean FIN, RST (SO_LINGER=0) and a stalled read (client timeout). Observation used the public run/history API and an independent read-only copy of the Server's SQLite file.

    Artifacts: Server durableworkflow/server:2.9.8, image ID sha256:a8ed30ac6762fb7b4ab50ef65ee703c1cb5e260e68fd2648f4f1d7e44bb21949, RepoDigest durableworkflow/server@sha256:d84df4a0baed50204e821cfe458523aff22f9a3949d48851044442386840e7db, Core 2.7.2 inside the image; Python SDK durable-workflow==2.6.1 from PyPI (wheel sha256 94f55ba59de27719ba487c648894d5231823abbe44dc65231e3b26575cc1b355) in a fresh venv, no source overlays. Every signal and update carried typed values: 2**62 + 7, null, "Ünïcødé ☃ 日本", nested map. All round-tripped exactly.

    Scenario Client Server records
    Signal changed(2) lost (FIN) with TransportRetryPolicy(max_attempts=1), identical manual retry httpx.RemoteProtocolError, retry accepted two SignalReceived and two commands for revision 2, workflow_commands.request_id NULL for both; workflow result revisions [1, 2, 2]
    Update rename("x") with explicit request_id, lost, identical retry RemoteProtocolError, retry returned update_completed with the original update_id one UpdateAccepted, one workflow_updates row (released update dedupe holds)
    Update rename("y") without request_id, lost, retry SDK generated a new sdk-python-update-… key per call two UpdateAccepted, two update rows; names ["x", "y", "y"]
    Signal lost with the default TransportRetryPolicy() (3 attempts), FIN RemoteProtocolError raised, no automatic retry one SignalReceived: applied once while the caller believes it failed
    Same, RST automatic retry, no exception two SignalReceived 0.5 s apart
    Same, stalled response automatic retry after the read timeout two SignalReceived

    Findings this adds to the issue body: the outcome of a lost signal response currently depends on how the connection died (FIN surfaces an exception the policy does not retry; RST and stalls are retried blindly), and the Python policy's retried exception set does not include RemoteProtocolError. Both are addressed by the client changes: a durable key per logical call, byte-identical retries and RemoteProtocolError joining the retried transport failures.

    The harness is parameterized by Server image and SDK version and will be rerun unchanged against the fixed published artifacts for the after-evidence. All task containers, the network, the database volume and the SDK image were removed after the run; only scripts and 448 KB of logs remain locally.

  8. rmcdaniel commented on Oct 20, 2026

    @rmcdaniel
    MemberAuthor

    Session checkpoint (2026-10-20, owner session paused by the operator)

    Implemented and pushed as draft pull requests, all on branch claude/216-command-keys:

    Not started: the Server part. Planned shape: ExternalWorkflowUpdateAdmission/ValidatedExternalWorkflowUpdateAdmission compare intent on a retained receipt (same or unverified returns it with retained/intent_verified; different falls through to Core, which records the 409 conflict), record intent_digest on their command rows, and add request_id/retained/intent_verified to resultPayload; WorkflowCommandContextFactory::requestMetadata uses the body key for signals as it already does for updates; the controller passes run_id for run-targeted routes so Core evaluates selected_run_not_current inside the admission lock (Core's loadControlPlaneWorkflow still needs to honor a run_id option via WorkflowStub::loadSelection). ExternalWorkflowUpdateRequestIdTest cases that reuse a key with a different name or arguments must change to expect the conflict; new HTTP tests for keyed signal retry, conflict and response fields are needed.

    Remaining after Server: full Core suites on PostgreSQL and MySQL, independent review findings (two reviews were started but their results were not applied), CHANGELOG entries, minor releases ordered after the pending PHP 2.5.4, Python 2.6.2, Rust 3.5.6 and Server 2.9.9 publications, rerun of the lossy-proxy harness against the published artifacts (before-evidence above), docs in all eight locales, consumer verification, then workflow#871 and shared #217.

    Local resources kept for resumption under the owner's task directory: the six worktrees with installed dependencies, the PHP tool image and small Composer, Python and Cargo cache volumes; task database and Redis containers were stopped. Removal checkpoint: when the #216 releases are verified or by 2026-10-27, whichever is earlier.

  9. rmcdaniel commented on Oct 20, 2026

    @rmcdaniel
    MemberAuthor

    General takeover

    The operator has transferred #216, then Workflow #871, then shared #217 to General after the prior owner's clean checkpoint. I have read the retained contract, before-fix evidence and checkpoint and recovered the five pushed draft PRs: Workflow #877, PHP SDK #198, Python SDK #180, Rust SDK #109 and CLI #61.

    I retain the Server admission/controller work, full database qualification and a fresh adversarial review before release. The proposed receipt/intent implementation is not accepted merely because its draft is pushed; in particular, conflict atomicity, legacy intent, closure/continue-as-new, selected-run fences, body-key authority, concurrent retries and recoverable transport failures require review and real controls. The existing before-fix and failed observations remain intact.

    The current entry-safety and TLS release windows remain separate. #216's compatible minor releases follow the pending entry-safety artifacts and must pass their own normal and published lossy-response qualification. Documentation must ship in all eight locales. No Cloud, native/shared/frozen, #773/#748, Waterline or stopped qualification scope is transferred by this handoff.

  10. rmcdaniel commented on Oct 20, 2026

    @rmcdaniel
    MemberAuthor

    Takeover review found and reproduced a defect in the unpublished intent draft: var_export() follows PHP's serialize_precision. Lowering it to 6 both changed the digest of one unchanged float and made two distinct floats share a digest. That could incorrectly retain a different command or conflict with an identical retry after configuration changes.

    Tests-first 8a167b64 retains two failures against the prior implementation, alongside 17 passing intent cases. The prospective correction encodes finite floats as fixed-endian IEEE 754 binary64 bytes, retaining distinct integer/float and signed-zero identity and explicit NaN/infinity tags. All 19 focused intent cases / 31 assertions pass after the correction; full source and database qualification remains pending. The public contract is corrected accordingly before publication. This is a draft-code finding, not a released-product defect or a claim that the remaining receipt/Server implementation has passed review.

  11. rmcdaniel commented on Oct 20, 2026

    @rmcdaniel
    MemberAuthor

    Takeover review reproduced another draft-code gap: the control-plane loader ignored an explicit run target. Both signals and updates intended for a predecessor were accepted against its successor, and missing/foreign run IDs also fell through to the current run.

    Tests-first 5d08795a has six genuine failures on the preceding runtime. The prospective correction 14acbfb73be831e04e27ab62f3d14115611c2136 loads the selected run, preserving namespace/instance ownership, before the existing admission lock rechecks whether it is current. A current selected-run retry retains its original receipt; using that key for another run conflicts. Missing and foreign runs return 404 without mutations.

    The final combined intent, receipt and control-plane-routing selection passes 90 tests / 1,313 assertions, with targeted ECS and PHPStan clean. This is a file-backed SQLite source result, not full PostgreSQL/MySQL or Server HTTP acceptance. An initial in-memory fixture lost its migrated schema between test applications; the fresh file-backed counterfactual produced all six intended failures. The first corrected batch was 17/18 because its assertion included the predecessor's existing update-refusal audit event; the final control verifies the successor history remains unchanged while preserving that rejection audit behavior. No released-product finding is inferred.

    Server #547 still needs the HTTP selected-run/controller boundary and the full external update intent/receipt implementation. Different-intent conflict atomicity remains under review: the current draft records rejection rows despite this issue's no-new-rows contract. No completion or publication is claimed.

  12. rmcdaniel commented on Oct 20, 2026

    @rmcdaniel
    MemberAuthor

    The no-write conflict review is now reproduced and corrected in the draft.

    Tests-first a6c2e658 fails three genuine controls on the preceding implementation: a different-intent retry of a signal, an update or signal-with-start creates rejection rows and advances message/command sequence counters. The update also appends an UpdateRejected event. This violates the accepted retry contract even though it never delivers the changed command.

    Prospective source 6956502f1cf8af6beb032bb53fa38d13acac528d builds an unpersisted refusal response while holding the same admission lock. It does not reserve sequences, create a signal/update/start group, or alter the retained receipt. HTTP output has no newly admitted command_id, command sequence or update ID; retained_command_id identifies the original. To preserve the embedded result API's existing string return type, commandId() on this new conflict kind refers to that retained command, with a null sequence. This does not claim that the retained command itself was rejected.

    The strengthened identity/intent/routing selection passes 90 tests / 1,328 assertions independently on SQLite, PostgreSQL 17 and MySQL 8.0.46. Targeted ECS and PHPStan pass. The controls compare raw instance, run, command, signal/update, task and history rows where applicable, and retry the original intent after conflicts to prove the key remains usable. Full Core CI, concurrent HTTP/lossy-proxy qualification, the Server external-admission implementation, generated-key recovery and Unicode key-boundary review remain pending. No draft component is treated as a published fix.

    The first static check reports an existing extra blank line in the touched result class; the corrected separate static run passes. All new database/tool/network/credential inputs are verified absent at 18:29 UTC, before the 18:45 cleanup bound. The active inherited source/dependencies remain for #216. Other owners' resources and all frozen/native fixtures are unchanged.

  13. rmcdaniel commented on Oct 20, 2026

    @rmcdaniel
    MemberAuthor

    General takeover source checkpoint: Server #547 head ae99b65fb adds remote update canonical intent and immutable conflict responses, including new validator rejections. Four tests-first HTTP-kernel cases fail on the preceding implementation; a separate validator receipt metadata case also fails before correction. The final request-ID / validator-task / context selection passes 69 tests and 701 assertions with scoped Pint/diff checks. Core #877 is a local prepared dependency here, not a published package.

    Same intent keeps the original command/update decision; changed name, arguments or typed values conflict without durable writes. Zero and maximum Unicode caller keys, legacy context-only lookup, terminal receipt retention and proxy correlation remain covered. Selected-run HTTP routing, concurrency, complete source suites, compatible Core adoption and the published lossy-response SDK/CLI cohort are still required. No safe-retry release or end-to-end completion is claimed.

    All one-shot General216 test containers are removed; the prior 18:45 resource boundary holds with no persistent application/database state. The next distinct selected-run source/test phase uses only inherited tools and uniquely named ephemeral containers, with cleanup by 19:15 UTC. Existing TLS, Python, native/frozen and tracing source windows remain separate. No paid resource or Cloud operation.

  14. rmcdaniel commented on Oct 20, 2026

    @rmcdaniel
    MemberAuthor

    Cross-SDK source review found a compatibility regression in the inherited retry drafts: PHP, Rust and the CLI imposed 255 bytes, while the released Server accepts 255 Unicode characters. The Server HTTP controls retain the literal 255-character key, including its exact original receipt. The contract now preserves that existing limit and explicitly distinguishes HTTP normalization from stored byte-exact identity.

    Three tests-first components reproduce the regression: PHP 326de19 fails with InvalidArgumentException, CLI 6cd3f10 returns INVALID, and Rust d1c1ab6 fails its actual Unicode boundary assertion. A first Rust invocation selected zero tests because of an exact-name filter; it is not a passing control. The corrected invocation runs and fails the actual test before the runtime correction.

    Corrections are PHP f4d40fa (11 cases / 74 assertions plus focused PHPStan), CLI 6ebce45 (19 cases / 166 assertions), and Rust 21786c3 (four command-key controls plus six request-ID controls, with one overlap). Rust also passes the pinned 1.99 formatter. Earlier PATH/missing-formatter runner failures remain unsuccessful invocations, separate from the actual test results. PHP/CLI use native PCRE Unicode counting without adding mbstring or another dependency. All normal exact-head source gates remain required.

    Server #547's selected-run/source component is independently at 02e28f907, with 130 HTTP controls / 1,640 assertions. Core #877's existing source, concurrency, published-package adoption, generated Rust key recovery after a transport failure, and the actual lossy-response cohort remain unfinished. These source results do not complete #216 and do not qualify a release. Source implementation continues in independent branches while Cleanup prepares PHP 3/Rust 4 and #773 prepares optional tracing; routine conflicts will be resolved at integration.

    All current General test containers and the new 1.5 GiB Rust target are retired; inherited tools/caches needed for the accepted assignment are retained. No foreign resource or image is removed.

  15. rmcdaniel commented on Oct 27, 2026

    @rmcdaniel
    MemberAuthor

    Published-artifact reproduction for the existing #216/#877 requirement, found while qualifying shared #250. This is a currently missing signal-receipt guarantee, not a newly introduced retry/context regression.

    • Normal Composer PHP SDK 3.6.1, source/dist 2a44993761b1b900d3f631c0b13dccb8b45ddf5e, actual published Server 2.11.3, bundled Workflow 2.14.2 49a9fabc0b675fb4cbac430fd8e2db5a09fbc222.
    • A worker-backed workflow schedules a three-attempt activity. Every callback calls Client::signalWorkflow() with the original workflowId/runId, identical signal name and Avro arguments, and the same stable business-event requestId. The first two callbacks then fail intentionally; the third succeeds.
    • The workflow completes with three identical signal values. History contains three SignalReceived commands with distinct command IDs and the same request key and request fingerprint. All three resolve to the original owning run. The expected one-retained-signal assertion fails.
    • The PHP public-claim identity fix is verified by this run: attempts remain 1/2/3, configured final checks are false/false/true, and every callback retains the original workflow/run IDs. No replacement identity or wrong-run signal occurred.

    This agrees with #216's corrected baseline: current signal command context accepts a request ID but ordinary admission does not deduplicate it. Keep #877 and the published-artifact acceptance open. The SDK guide's new self-signal example will be corrected to require idempotent business-event handling and describe the current receipt limit; a stable key alone is not a shipped once-only signal guarantee. No Server implementation changes are part of the #250 SDK patch.

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

    bugSomething isn't workingkind: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 repositoryrepo:workflowOwned by the Workflow engine repositorystatus:readyReady for implementation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions