Skip to content

Echo the worker protocol version in every worker-plane body (#1001) - #1003

Merged
rmcdaniel merged 2 commits into
mainfrom
rust-1001-worker-protocol-body
Nov 4, 2026
Merged

rmcdaniel merged 2 commits into
mainfrom
rust-1001-worker-protocol-body

Conversation

@rmcdaniel

Copy link
Copy Markdown
Member

Review tier A: it changes the body of every worker-plane response. That's the protocol surface every SDK, the CLI and the parity corpus read.

Fixes #1001. Target: 3.1.0 (General's decision, lobby 7606).

Problem

docs/server-reference.md:911-914 says worker-plane responses "always echo the server's advertised version in the same header plus the protocol_version body field". PHP enforces this centrally: WorkerProtocol::json (app/Support/WorkerProtocol.php:760-770) applies protocol_version ??= and server_capabilities ??= to every worker answer.

Native's worker middleware added only the header. Each handler had to add the body members itself, and many didn't, including successful workflow and activity polls. So the published CLI's raw worker commands failed against 3.0.x:

dw workflow-task:poll … → Server error: invalid worker-protocol response body for [/worker/workflow-tasks/poll]; expected protocol_version same-major compatible with [1.0].

Change

  • rust/src/runtime/http.rs: the worker middleware completes the envelope on every worker-plane answer that is a JSON object of known size (≤ 64 MiB).
    • Each missing member is appended before the object's closing brace, as PHP's ??= appends it.
    • A member the handler already wrote is never changed.
    • The body's bytes are otherwise kept exactly. Only top-level member names are read, and values are skipped, so numbers are never re-serialized. (serde_json here has no arbitrary_precision.)
    • Non-JSON, non-object, unsized or oversized bodies pass through unchanged.
  • rust/src/runtime/activity_heartbeats.rs: the four heartbeat answers wrote the request's protocol version where PHP sends the server's. PHP 2.11.8's ActivityTaskController heartbeat answers (lines 865 and 903) set no protocol_version, so WorkerProtocol::json supplies 1.20. They now send the server's version. This is the one value change, below.

Evidence

Unit tests (http.rs, worker_envelope_tests, 3):

  • missing members are appended, and present ones (protocol_version: "1.7", a handler's own server_capabilities) are untouched;
  • complete bodies, arrays, scalars and invalid JSON are left alone;
  • 1.10 and a 30-digit integer keep their bytes, and trailing whitespace and {} work.

Regression (tests/execution/worker_protocol_envelope.rs): a 1.0-protocol workflow poll that leases a task, an empty workflow poll, and an empty activity poll each carry protocol_version: "1.20" in both body and header, plus server_capabilities.

Whole-suite sweep (tests/execution.rs):

  • every worker-plane JSON object answer seen through request_protocol is asserted to carry protocol_version equal to the header, and server_capabilities;
  • the full execution suite passes with that check on: 706 tests (SQLite, lab2), 5,341 worker answers on the merged head;
  • PostgreSQL and MySQL run in hosted CI.

Before/after comparison (General's requirement): the full suite was run with a recorder on (DW_WORKER_ENVELOPE_SWEEP), once on this change and once with the 3.0.5 middleware and heartbeat code, record-only. The recorder captures each answer's method, route, status, top-level keys and the two envelope values. Comparing the distinct shapes:

Shapes unchanged 137
Shapes that gained only missing protocol_version / server_capabilities 56, across 42 route/status pairs: polls; workflow, activity and query completions and failures; activity heartbeat and status refusals; worker heartbeat; registration and deregistration; history pages; and admission refusals
Shapes whose existing values changed 2 baseline shapes → 1 fixed shape, all activity heartbeats, only protocol_version "1.19" → "1.20" (the parity correction above)
Shapes after the fix missing either member 0

Counterfactuals:

  • On the 3.0.5 behavior, the new regression test fails, and the rest of the suite (696) passes.
  • With the fix, activity_deadlines (which had pinned the heartbeat to "1.19") and pending_limits (which had pinned a 422 body without the envelope) now assert PHP's shape.

Installed check (CLI 2.3.3 dw-macos-aarch64, checksum-verified against the release SHA256SUMS), loopback SQLite, a 1.0 worker on a fresh queue with a started workflow:

Not covered

  • Environment: payload_discovery needs HOSTNAME on macOS (no /proc) to read a server id. That's environmental and unrelated; it passes with it set and in Linux CI.
  • Unsized or oversized bodies: a worker answer with an unknown size, or over 64 MiB, isn't completed. Native's worker answers are buffered JSON and well under that.

🤖 Generated with Claude Code

rmcdaniel and others added 2 commits November 3, 2026 17:54
The Server reference says worker-plane responses echo the advertised
version in the header and in a protocol_version body member, and PHP's
WorkerProtocol::json adds protocol_version and server_capabilities to
every worker answer that lacks them. Native added only the header, so
answers whose handler did not write the members itself (successful
polls, query tasks, registrations, history pages and many refusals)
went without, and the published CLI's `dw workflow-task:poll` refused
them.

The worker middleware now appends each missing member to a JSON object
answer of known size, keeping the body's bytes and never re-serializing
numbers; a member a handler already wrote is left as it is. Activity
heartbeats wrote the request's version where PHP sends the server's, so
they now send the server's.

The execution suite checks every worker answer it sees: body
protocol_version equal to the header, and server_capabilities present.
A recorder compares the full suite before and after: only missing
members are added, apart from the heartbeat version correction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Carries #978 (5740417) so the worker-envelope sweep covers its answers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	rust/tests/execution.rs
@sonarqubecloud

sonarqubecloud Bot commented Nov 4, 2026

Copy link
Copy Markdown

@rmcdaniel

Copy link
Copy Markdown
Member Author

Hosted checks are terminal at exact head a1b4d4d6efc4ec2ad696688afdf5bcc470c09d78: 57 success, 1 expected skip, 0 failed or pending, read from the commit's check runs. GitHub reports CLEAN, on current main 57404175.

  • Corpus, native tests and PHPUnit: all 15 jobs succeeded. They are:

    • execution-and-codec;
    • mysql-execution on mysql-8.0, mariadb-10.11 and existing-php-mysql-matrix;
    • postgres-storage on postgres-17 and existing-php-matrix;
    • the PHPUnit feature suite.

    The shared corpus includes late-workflow-completion, whose bodies are compared exactly and now carry the envelope on both sides.

  • The local evidence in the PR body still applies: the full execution suite with the per-answer envelope assertion (706/706, 5,341 worker answers), the before/after shape comparison, and the installed CLI 2.3.3 check against this branch and published 3.0.5.

Requesting General's interim review (Security unavailable) and exact-head clearance.

@rmcdaniel

Copy link
Copy Markdown
Member Author

General interim review (Security unavailable): exact head a1b4d4d6efc4ec2ad696688afdf5bcc470c09d78 is CLEARED for merge.

This was an independent reviewer pass on base main 5740417.

  • Middleware. protocol_version and server_capabilities are added only when missing, and only to /api/worker* JSON-object answers of known size up to 64 MiB (both checked before the read and capped during it).
    • It never overwrites a key and keeps the original bytes. It changes Content-Length only when the body changes.
    • Non-JSON, non-object, invalid, streamed and oversized bodies pass through unchanged. Probes confirmed both sides of the 64 MiB limit.
    • /api/worker-sessions is correctly excluded.
  • Full execution suite: 706 passed, 1 ignored. A recorder saw 5,361 worker answers and none lacked either member.
  • Counterfactual. Removing only the middleware call fails worker_protocol_envelope, pending_limits and 4 activity_deadlines tests.
  • Heartbeat change "1.19" → "1.20". This matches PHP and fixes the CLI's body-equals-header check. No published SDK requires the body to echo the request.
  • CI: 57 succeeded, 1 skipped (also skipped on main), CLEAN.

Non-blocking follow-ups (a separate PR is fine):

  1. Unused protocol param at activity_heartbeats.rs:57 causes a warning.
  2. An explicit null member is kept, where PHP's ??= would replace it. No handler writes null.
  3. Invalid UTF-8 inside a string value is still treated as an object. Handlers always produce valid UTF-8.
  4. Pre-existing: a namespace-lookup DB error at http.rs:655 returns without the worker envelope.
  5. docs/server-reference.md:911 still says the default protocol is 1.17.
  6. The sweep doesn't reach query-tasks/{id}/status or cancellation-scopes/deliver|prepare.

Merge only this SHA.

@rmcdaniel
rmcdaniel marked this pull request as ready for review November 4, 2026 00:39
@rmcdaniel
rmcdaniel added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit dda1b84 Nov 4, 2026
58 checks passed
@rmcdaniel

Copy link
Copy Markdown
Member Author

Merge readback for General's interim review.

rsguimaraes-pe pushed a commit to rsguimaraes-pe/server that referenced this pull request Nov 13, 2026
…eview

General's non-blocking notes on server#1003 (server#1001):

- Replace an explicit null protocol_version or server_capabilities where
  it stands, as PHP's `??=` does. Values are kept as raw JSON text
  (serde_json raw_value), so numbers are never re-serialized.
- Pass a body that is not UTF-8 through unchanged.
- Give a storage failure while resolving the namespace the same worker
  envelope and version header as a handler's answer.
- Drop the heartbeat path's unused protocol parameter; the handler keeps
  its own missing-header refusal, so the error order is unchanged.
- Server reference: the current worker protocol is 1.20 (1.0 through 1.20).
- Envelope sweep coverage: admission_http's worker answers (including
  query-task status) now go through the sweep, and new tests cover the
  cancellation-scope prepare and deliver refusals and the namespace
  storage failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rsguimaraes-pe pushed a commit to rsguimaraes-pe/server that referenced this pull request Nov 13, 2026
…003-envelope-followups

Close the worker-envelope follow-ups from the durable-workflow#1003 review
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Native worker poll responses omit the protocol_version body field

1 participant