Skip to content

fix(server): stamp envelope status at sync dispatch chokepoint#1898

Merged
bokelley merged 1 commit into
mainfrom
bokelley/envelope-status-sweep-1897
May 21, 2026
Merged

fix(server): stamp envelope status at sync dispatch chokepoint#1898
bokelley merged 1 commit into
mainfrom
bokelley/envelope-status-sweep-1897

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

Summary

Closes #1897 (the SDK-wide gap the #1895 audit missed).

  • Adds injectEnvelopeStatusIntoResponse at the finalize() chokepoint in createAdcpServer's tool dispatcher. One seam — every framework-registered tool gets envelope status: \"completed\" without per-helper edits, whether it dispatches through productsResponse, mediaBuyResponse, genericResponse, or the test-controller bridge's wrap(merged) rewrites.
  • Chokepoint placement preserves the MediaBuyStatus / TaskStatus collision at the same top-level key — only stamps when structuredContent.status is missing. So CreateMediaBuySuccess.status: 'active' (valid MediaBuyStatus, not a valid TaskStatus) ships verbatim; the spec-level ambiguity is filed for adcontextprotocol/adcp separately.
  • Error envelopes (isError: true) are skipped — their status semantics (failed / rejected) belong to the adcp_error path. Submitted envelopes keep their handler-set status: 'submitted'. Auto-registered get_adcp_capabilities continues to stamp via capabilitiesResponse directly (bypasses finalize()); both layers reinforce.

Why the chokepoint instead of fixing each helper

19 of 21 *Response helpers had the gap, and the ~30 wrap: null tools in TOOL_META fell through to genericResponse with the same gap. Touching every helper would be near-mechanical churn and would still miss future helpers added without remembering the envelope. The finalize() chokepoint already runs sanitizeAdcpErrorEnvelope / enrichErrorTwoLayer / injectContextIntoResponse / injectVersionIntoResponse — envelope status belongs in the same row.

Test plan

  • New: 4 targeted tests in test/server-create-adcp-server.test.js covering productsResponse path, getSignalsResponse path, genericResponse fallback (list_property_lists), and the MediaBuyStatus collision case (create_media_buy returning status: 'active' ships unchanged)
  • NODE_ENV=test node --test test/server-create-adcp-server.test.js — 97/97
  • NODE_ENV=test node --test test/server-responses.test.js test/server-decisioning-from-platform.test.js test/server-idempotency.test.js — 312/312
  • NODE_ENV=test node --test test/lib/storyboard-validations.test.js test/lib/idempotency.test.js test/lib/capabilities-tools-drift.test.js test/lib/governance-e2e.test.js test/server-decisioning-capability-projections.test.js — 96/96
  • npm test full suite: 9409/9417 pass; 5 unrelated failures (all v1-canonical-mapping.json not found in 3.1.0-beta schema cache — confirmed pre-existing on origin/main, not introduced by this change)
  • npm run format:check clean

Follow-up

Spec-level question for adcontextprotocol/adcp: envelope-level status (TaskStatus) collides with payload-level status (MediaBuyStatus) at the same top-level key on mediaBuyResponse / updateMediaBuyResponse / cancelMediaBuyResponse. Either nest payload under payload (matches the normative envelope example in protocol-envelope.json) or rename the payload field. Tracking issue inbound.

🤖 Generated with Claude Code

Adds injectEnvelopeStatusIntoResponse to the finalize() chokepoint in
createAdcpServer's tool dispatcher. Stamps `status: "completed"` on
structuredContent (and the L2 text fallback) when the handler-projected
payload doesn't already carry a top-level status field.

Closes the SDK-wide envelope-status gap that #1895's narrow
get_adcp_capabilities fix didn't reach: projectSync returns the wire
payload verbatim and 19 of the 21 per-tool wrap helpers plus the
genericResponse fallback (~30 governance / SI / brand tools) built
structuredContent with no envelope status. The storyboard step
v3_envelope_integrity/no_legacy_status_fields is scoped to
get_adcp_capabilities today; @kapoost's one-tool failure masked the
SDK-wide gap.

Chokepoint placement preserves the MediaBuyStatus / TaskStatus collision
at the same top-level key for the 3 affected helpers (mediaBuyResponse,
updateMediaBuyResponse, cancelMediaBuyResponse) — the spec-level
ambiguity is filed separately. Error envelopes (isError: true) are
skipped; submitted envelopes keep their handler-set `status: 'submitted'`.

Tracks adcp-client#1897.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@aao-ipr-bot aao-ipr-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Chokepoint at finalize() is the right seam — one place, every framework-registered tool inherits envelope conformance, no per-helper churn. Follow-ups below.

Things I checked

  • Chokepoint placement at create-adcp-server.ts:3459 — ordered after sanitizeAdcpErrorEnvelope / enrichErrorTwoLayer, before context/version injection. Same row as the other envelope-shape stampers. Correct.
  • Guard logic at create-adcp-server.ts:2682: if ('status' in sc) return preserves CreateMediaBuySuccess.status: 'active' (test at test/server-create-adcp-server.test.js:476-497 locks it in), cancelMediaBuyResponse's hard-coded 'canceled', and the submitted-envelope 'submitted'. 'status' in sc is the same shape the sibling injectors use — consistency wins.
  • isError: true skip is correct for this injector's scope. The broader error-envelope conformance gap (see Follow-ups) lives on the adcpError() builder, not here.
  • L2/L3 dual write mirrors injectContextIntoResponse and injectVersionIntoResponse byte-for-byte. Drift on JSON.parse failure is the same drift those two already accept — handlers don't emit non-JSON L2 text alongside L3 structuredContent in practice.
  • Changeset present, patch level — restoring spec-required envelope behavior, no new exported API. Right call.
  • Test coverage: four targeted paths (productsResponse / getSignalsResponse / genericResponse fallback / MediaBuyStatus collision). The collision test is the load-bearing one — proves payload semantics survive the stamp.
  • ad-tech-protocol-expert verdict: sound-with-caveats. Witness invariant not violated — envelope fields (status, context_id, task_id, replayed, adcp_version, errors) are framework-owned per ProtocolResponseParser.ts:38-51's own allowlist. Stamping is the framework's job.

Follow-ups (non-blocking — file as issues)

  1. Error envelopes still don't carry status: 'failed' / 'rejected'. adcpError() at src/lib/server/errors.ts:160-164 builds { isError: true, structuredContent: { adcp_error: filtered } } with no top-level status. The PR comment frames this as 'wired separately by the adcp_error path' — today that path doesn't exist. AdCP #4876 makes envelope status required on every task response, and failed/rejected are valid TaskStatus values. Either extend the chokepoint to derive status from adcp_error.recovery / code, or land a sibling injector on the error path. Tighten the changeset prose either way — today it overstates the coverage.
  2. Stamp runs unconditionally across the wire-version compat matrix. injectVersionIntoResponse gates on if (!servedVersion) return;. injectEnvelopeStatusIntoResponse does not. COMPATIBLE_ADCP_VERSIONS in src/lib/version.ts:28-58 still covers v2.5/v2.6/3.0.0-beta.* responses. Pre-3.0 schemas don't define envelope status at the top level — additive but ambiguous to a v2.5 buyer. Mirror the version gate (bundleSupportsEnvelopeStatus(adcpVersion)) so we don't ship an envelope field on bundles whose contract didn't include it.
  3. 'status' in sc treats status: undefined as handler-owned. Edge case — a buggy handler that explicitly sets status: undefined blocks the stamp because in matches present-but-undefined. sc.status != null would be tighter without losing the documented intent. Cheap to harden.

Minor nits (non-blocking)

  1. Test reparenting. test/server-create-adcp-server.test.js:435 closes response builder wiring early; :437 opens the new envelope status describe; :560 closes it. That sweeps 'uses generic wrapper for tools without dedicated builders' (:499), 'passes through adcpError responses' (:512), and 'detects build_creative single vs multi-format' (:532) into a describe block whose theme they don't share. Tests pass, but the CI output and a future reader will both lie. Move the closing }); from :560 up to immediately after the MediaBuyStatus collision test at :497.
  2. Orphaned JSDoc at create-adcp-server.ts:2645-2655. That block described injectVersionIntoResponse. Inserting the new function between them moved the JSDoc onto the wrong target — TypeScript binds the new function instead. Lift the 2645-2655 block down next to injectVersionIntoResponse at :2700, leaving only the new envelope-status JSDoc above the new function.
  3. Notable that #1895 framed itself as the fix and this PR is the third commit in a row cleaning up the same envelope gap. The audit-vs-sweep split is fine for changelog clarity; an npm run review:fork-matrix-style assertion that every *Response helper round-trips an envelope status would close this category for good.

Safe to merge. Land the error-path stamp + version gate in the next sweep — preferably before the storyboard generalises no_legacy_status_fields past get_adcp_capabilities.

@bokelley bokelley merged commit ef59a23 into main May 21, 2026
11 checks passed
@bokelley bokelley deleted the bokelley/envelope-status-sweep-1897 branch May 21, 2026 13:35
bokelley added a commit to adcontextprotocol/adcp that referenced this pull request May 21, 2026
…pdate success responses (#4895)

Under MCP flat-on-the-wire serialization, the envelope task-status (`status`,
TaskStatus) and the body-level media-buy lifecycle status (`status`,
MediaBuyStatus) share the same root key on `CreateMediaBuySuccess` and
`UpdateMediaBuySuccess`. Enums overlap on completed | canceled | rejected and
diverge elsewhere — a MediaBuyStatus is silently destroyed when the envelope
stamps a TaskStatus at the same path. No validator catches it.

WG-recommended Option E (additive-deprecate, 3.1 minor → 4.0 removal).
**Strictly additive — no schema is renamed and no `required[]` constraint
changes.**

- create-media-buy-response.json `CreateMediaBuySuccess`: adds
  `media_buy_status: $ref media-buy-status.json` alongside the existing
  `status`. The legacy `status` is marked `deprecated: true` and slated for
  removal in 4.0. Neither field is in `required[]` (both optional in 3.1).
  `CreateMediaBuySubmitted` branch unchanged — its `status: { const: "submitted" }`
  is the TaskStatus discriminator.
- update-media-buy-response.json `UpdateMediaBuySuccess`: symmetric.

Out of scope (deliberate): `get-media-buys-response.json` `media_buys[].status`,
`get-media-buy-delivery-response.json` `media_buy_deliveries[].status`, and
`core/media-buy.json` `status` are NOT renamed. These fields live nested at
depth ≥ 1 inside arrays, so the envelope `status` at the response root does
not collide with them on the wire. Renaming them would require either a
breaking `required[]` swap or a double-field transition for no wire-collision
payoff. The nested-vocabulary inconsistency in 3.1 (one buyer call returns
`media_buy_status` at root, the next returns `status` inside an array) is the
price of keeping this strictly additive. Resolve in 4.0 alongside the
legacy-`status` removal, when a clean cascade rename is on the table.

`cancel_media_buy` is performed via update_media_buy with cancel intent —
there is no dedicated cancel tool. Inherits the rename from
UpdateMediaBuySuccess; no separate handling.

Storyboards swept:

- protocols/media-buy/state-machine.yaml — three field_present path:"status"
  assertions on update-media-buy-response.json → path:"media_buy_status".
- protocols/media-buy/scenarios/pending_creatives_to_start.yaml — two
  field_value assertions checking MediaBuyStatus values on
  create-media-buy-response.json / update-media-buy-response.json →
  path:"media_buy_status".
- protocols/media-buy/scenarios/create_media_buy_async.yaml — left as
  path:"status" (this checks the submitted-arm TaskStatus discriminator).

Docs:

- task-reference/update_media_buy.mdx — cancellation success example shows
  the canonical media_buy_status form.
- reference/whats-new-in-3-1.mdx — migration note in Final-spec
  clarifications batch.

Adopter impact:

- Sellers (3.1+) SHOULD emit media_buy_status on create/update success;
  MAY continue emitting the deprecated legacy `status` during the window.
- Buyers (3.1+) MUST prefer media_buy_status when present; MAY fall back to
  legacy `status` for compatibility.
- 3.0 sellers and buyers continue to work unchanged. No required-field swap,
  no rename, no breakage.
- 4.0: deprecated top-level `status` removed from create/update success
  branches. The nested `status` fields on get-media-buys-response items,
  get-media-buy-delivery-response items, and core/media-buy.json should be
  addressed in the same 4.0 release as a coherent cascade.
- SDK regen required for @adcp/client, adcp-go, Python.

Related:
- #4876 — envelope status REQUIRED (beta.2)
- #4897 — companion governance schema rename (separate PR #4902)
- adcontextprotocol/adcp-client#1898 — SDK-side audit + transport precedence

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit to adcontextprotocol/adcp that referenced this pull request May 21, 2026
…pdate success responses (#4895)

Under MCP flat-on-the-wire serialization, the envelope task-status (`status`,
TaskStatus) and the body-level media-buy lifecycle status (`status`,
MediaBuyStatus) share the same root key on `CreateMediaBuySuccess` and
`UpdateMediaBuySuccess`. Enums overlap on completed | canceled | rejected and
diverge elsewhere — a MediaBuyStatus is silently destroyed when the envelope
stamps a TaskStatus at the same path. No validator catches it.

WG-recommended Option E (additive-deprecate, 3.1 minor → 4.0 removal).
**Strictly additive — no schema is renamed and no `required[]` constraint
changes.**

- create-media-buy-response.json `CreateMediaBuySuccess`: adds
  `media_buy_status: $ref media-buy-status.json` alongside the existing
  `status`. The legacy `status` is marked `deprecated: true` and slated for
  removal in 4.0. Neither field is in `required[]` (both optional in 3.1).
  `CreateMediaBuySubmitted` branch unchanged — its `status: { const: "submitted" }`
  is the TaskStatus discriminator.
- update-media-buy-response.json `UpdateMediaBuySuccess`: symmetric.

Out of scope (deliberate): `get-media-buys-response.json` `media_buys[].status`,
`get-media-buy-delivery-response.json` `media_buy_deliveries[].status`, and
`core/media-buy.json` `status` are NOT renamed. These fields live nested at
depth ≥ 1 inside arrays, so the envelope `status` at the response root does
not collide with them on the wire. Renaming them would require either a
breaking `required[]` swap or a double-field transition for no wire-collision
payoff. The nested-vocabulary inconsistency in 3.1 (one buyer call returns
`media_buy_status` at root, the next returns `status` inside an array) is the
price of keeping this strictly additive. Resolve in 4.0 alongside the
legacy-`status` removal, when a clean cascade rename is on the table.

`cancel_media_buy` is performed via update_media_buy with cancel intent —
there is no dedicated cancel tool. Inherits the rename from
UpdateMediaBuySuccess; no separate handling.

Storyboards swept:

- protocols/media-buy/state-machine.yaml — three field_present path:"status"
  assertions on update-media-buy-response.json → path:"media_buy_status".
- protocols/media-buy/scenarios/pending_creatives_to_start.yaml — two
  field_value assertions checking MediaBuyStatus values on
  create-media-buy-response.json / update-media-buy-response.json →
  path:"media_buy_status".
- protocols/media-buy/scenarios/create_media_buy_async.yaml — left as
  path:"status" (this checks the submitted-arm TaskStatus discriminator).

Docs:

- task-reference/update_media_buy.mdx — cancellation success example shows
  the canonical media_buy_status form.
- reference/whats-new-in-3-1.mdx — migration note in Final-spec
  clarifications batch.

Adopter impact:

- Sellers (3.1+) SHOULD emit media_buy_status on create/update success;
  MAY continue emitting the deprecated legacy `status` during the window.
- Buyers (3.1+) MUST prefer media_buy_status when present; MAY fall back to
  legacy `status` for compatibility.
- 3.0 sellers and buyers continue to work unchanged. No required-field swap,
  no rename, no breakage.
- 4.0: deprecated top-level `status` removed from create/update success
  branches. The nested `status` fields on get-media-buys-response items,
  get-media-buy-delivery-response items, and core/media-buy.json should be
  addressed in the same 4.0 release as a coherent cascade.
- SDK regen required for @adcp/client, adcp-go, Python.

Related:
- #4876 — envelope status REQUIRED (beta.2)
- #4897 — companion governance schema rename (separate PR #4902)
- adcontextprotocol/adcp-client#1898 — SDK-side audit + transport precedence

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit to adcontextprotocol/adcp that referenced this pull request May 21, 2026
…pdate success responses (#4895)

Under MCP flat-on-the-wire serialization, the envelope task-status (`status`,
TaskStatus) and the body-level media-buy lifecycle status (`status`,
MediaBuyStatus) share the same root key on `CreateMediaBuySuccess` and
`UpdateMediaBuySuccess`. Enums overlap on completed | canceled | rejected and
diverge elsewhere — a MediaBuyStatus is silently destroyed when the envelope
stamps a TaskStatus at the same path. No validator catches it.

WG-recommended Option E (additive-deprecate, 3.1 minor → 4.0 removal).
**Strictly additive — no schema is renamed and no `required[]` constraint
changes.**

- create-media-buy-response.json `CreateMediaBuySuccess`: adds
  `media_buy_status: $ref media-buy-status.json` alongside the existing
  `status`. The legacy `status` is marked `deprecated: true` and slated for
  removal in 4.0. Neither field is in `required[]` (both optional in 3.1).
  `CreateMediaBuySubmitted` branch unchanged — its `status: { const: "submitted" }`
  is the TaskStatus discriminator.
- update-media-buy-response.json `UpdateMediaBuySuccess`: symmetric.

Out of scope (deliberate): `get-media-buys-response.json` `media_buys[].status`,
`get-media-buy-delivery-response.json` `media_buy_deliveries[].status`, and
`core/media-buy.json` `status` are NOT renamed. These fields live nested at
depth ≥ 1 inside arrays, so the envelope `status` at the response root does
not collide with them on the wire. Renaming them would require either a
breaking `required[]` swap or a double-field transition for no wire-collision
payoff. The nested-vocabulary inconsistency in 3.1 (one buyer call returns
`media_buy_status` at root, the next returns `status` inside an array) is the
price of keeping this strictly additive. Resolve in 4.0 alongside the
legacy-`status` removal, when a clean cascade rename is on the table.

`cancel_media_buy` is performed via update_media_buy with cancel intent —
there is no dedicated cancel tool. Inherits the rename from
UpdateMediaBuySuccess; no separate handling.

Storyboards swept:

- protocols/media-buy/state-machine.yaml — three field_present path:"status"
  assertions on update-media-buy-response.json → path:"media_buy_status".
- protocols/media-buy/scenarios/pending_creatives_to_start.yaml — two
  field_value assertions checking MediaBuyStatus values on
  create-media-buy-response.json / update-media-buy-response.json →
  path:"media_buy_status".
- protocols/media-buy/scenarios/create_media_buy_async.yaml — left as
  path:"status" (this checks the submitted-arm TaskStatus discriminator).

Docs:

- task-reference/update_media_buy.mdx — cancellation success example shows
  the canonical media_buy_status form.
- reference/whats-new-in-3-1.mdx — migration note in Final-spec
  clarifications batch.

Adopter impact:

- Sellers (3.1+) SHOULD emit media_buy_status on create/update success;
  MAY continue emitting the deprecated legacy `status` during the window.
- Buyers (3.1+) MUST prefer media_buy_status when present; MAY fall back to
  legacy `status` for compatibility.
- 3.0 sellers and buyers continue to work unchanged. No required-field swap,
  no rename, no breakage.
- 4.0: deprecated top-level `status` removed from create/update success
  branches. The nested `status` fields on get-media-buys-response items,
  get-media-buy-delivery-response items, and core/media-buy.json should be
  addressed in the same 4.0 release as a coherent cascade.
- SDK regen required for @adcp/client, adcp-go, Python.

Related:
- #4876 — envelope status REQUIRED (beta.2)
- #4897 — companion governance schema rename (separate PR #4902)
- adcontextprotocol/adcp-client#1898 — SDK-side audit + transport precedence

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit to adcontextprotocol/adcp that referenced this pull request May 21, 2026
…pdate success responses (#4895)

Under MCP flat-on-the-wire serialization, the envelope task-status (`status`,
TaskStatus) and the body-level media-buy lifecycle status (`status`,
MediaBuyStatus) share the same root key on `CreateMediaBuySuccess` and
`UpdateMediaBuySuccess`. Enums overlap on completed | canceled | rejected and
diverge elsewhere — a MediaBuyStatus is silently destroyed when the envelope
stamps a TaskStatus at the same path. No validator catches it.

WG-recommended Option E (additive-deprecate, 3.1 minor → 4.0 removal).
**Strictly additive — no schema is renamed and no `required[]` constraint
changes.**

- create-media-buy-response.json `CreateMediaBuySuccess`: adds
  `media_buy_status: $ref media-buy-status.json` alongside the existing
  `status`. The legacy `status` is marked `deprecated: true` and slated for
  removal in 4.0. Neither field is in `required[]` (both optional in 3.1).
  `CreateMediaBuySubmitted` branch unchanged — its `status: { const: "submitted" }`
  is the TaskStatus discriminator.
- update-media-buy-response.json `UpdateMediaBuySuccess`: symmetric.

Out of scope (deliberate): `get-media-buys-response.json` `media_buys[].status`,
`get-media-buy-delivery-response.json` `media_buy_deliveries[].status`, and
`core/media-buy.json` `status` are NOT renamed. These fields live nested at
depth ≥ 1 inside arrays, so the envelope `status` at the response root does
not collide with them on the wire. Renaming them would require either a
breaking `required[]` swap or a double-field transition for no wire-collision
payoff. The nested-vocabulary inconsistency in 3.1 (one buyer call returns
`media_buy_status` at root, the next returns `status` inside an array) is the
price of keeping this strictly additive. Resolve in 4.0 alongside the
legacy-`status` removal, when a clean cascade rename is on the table.

`cancel_media_buy` is performed via update_media_buy with cancel intent —
there is no dedicated cancel tool. Inherits the rename from
UpdateMediaBuySuccess; no separate handling.

Storyboards swept:

- protocols/media-buy/state-machine.yaml — three field_present path:"status"
  assertions on update-media-buy-response.json → path:"media_buy_status".
- protocols/media-buy/scenarios/pending_creatives_to_start.yaml — two
  field_value assertions checking MediaBuyStatus values on
  create-media-buy-response.json / update-media-buy-response.json →
  path:"media_buy_status".
- protocols/media-buy/scenarios/create_media_buy_async.yaml — left as
  path:"status" (this checks the submitted-arm TaskStatus discriminator).

Docs:

- task-reference/update_media_buy.mdx — cancellation success example shows
  the canonical media_buy_status form.
- reference/whats-new-in-3-1.mdx — migration note in Final-spec
  clarifications batch.

Adopter impact:

- Sellers (3.1+) SHOULD emit media_buy_status on create/update success;
  MAY continue emitting the deprecated legacy `status` during the window.
- Buyers (3.1+) MUST prefer media_buy_status when present; MAY fall back to
  legacy `status` for compatibility.
- 3.0 sellers and buyers continue to work unchanged. No required-field swap,
  no rename, no breakage.
- 4.0: deprecated top-level `status` removed from create/update success
  branches. The nested `status` fields on get-media-buys-response items,
  get-media-buy-delivery-response items, and core/media-buy.json should be
  addressed in the same 4.0 release as a coherent cascade.
- SDK regen required for @adcp/client, adcp-go, Python.

Related:
- #4876 — envelope status REQUIRED (beta.2)
- #4897 — companion governance schema rename (separate PR #4902)
- adcontextprotocol/adcp-client#1898 — SDK-side audit + transport precedence

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit to adcontextprotocol/adcp that referenced this pull request May 21, 2026
…pdate success responses (#4895)

Under MCP flat-on-the-wire serialization, the envelope task-status (`status`,
TaskStatus) and the body-level media-buy lifecycle status (`status`,
MediaBuyStatus) share the same root key on `CreateMediaBuySuccess` and
`UpdateMediaBuySuccess`. Enums overlap on completed | canceled | rejected and
diverge elsewhere — a MediaBuyStatus is silently destroyed when the envelope
stamps a TaskStatus at the same path. No validator catches it.

WG-recommended Option E (additive-deprecate, 3.1 minor → 4.0 removal).
**Strictly additive — no schema is renamed and no `required[]` constraint
changes.**

- create-media-buy-response.json `CreateMediaBuySuccess`: adds
  `media_buy_status: $ref media-buy-status.json` alongside the existing
  `status`. The legacy `status` is marked `deprecated: true` and slated for
  removal in 4.0. Neither field is in `required[]` (both optional in 3.1).
  `CreateMediaBuySubmitted` branch unchanged — its `status: { const: "submitted" }`
  is the TaskStatus discriminator.
- update-media-buy-response.json `UpdateMediaBuySuccess`: symmetric.

Out of scope (deliberate): `get-media-buys-response.json` `media_buys[].status`,
`get-media-buy-delivery-response.json` `media_buy_deliveries[].status`, and
`core/media-buy.json` `status` are NOT renamed. These fields live nested at
depth ≥ 1 inside arrays, so the envelope `status` at the response root does
not collide with them on the wire. Renaming them would require either a
breaking `required[]` swap or a double-field transition for no wire-collision
payoff. The nested-vocabulary inconsistency in 3.1 (one buyer call returns
`media_buy_status` at root, the next returns `status` inside an array) is the
price of keeping this strictly additive. Resolve in 4.0 alongside the
legacy-`status` removal, when a clean cascade rename is on the table.

`cancel_media_buy` is performed via update_media_buy with cancel intent —
there is no dedicated cancel tool. Inherits the rename from
UpdateMediaBuySuccess; no separate handling.

Storyboards swept:

- protocols/media-buy/state-machine.yaml — three field_present path:"status"
  assertions on update-media-buy-response.json → path:"media_buy_status".
- protocols/media-buy/scenarios/pending_creatives_to_start.yaml — two
  field_value assertions checking MediaBuyStatus values on
  create-media-buy-response.json / update-media-buy-response.json →
  path:"media_buy_status".
- protocols/media-buy/scenarios/create_media_buy_async.yaml — left as
  path:"status" (this checks the submitted-arm TaskStatus discriminator).

Docs:

- task-reference/update_media_buy.mdx — cancellation success example shows
  the canonical media_buy_status form.
- reference/whats-new-in-3-1.mdx — migration note in Final-spec
  clarifications batch.

Adopter impact:

- Sellers (3.1+) SHOULD emit media_buy_status on create/update success;
  MAY continue emitting the deprecated legacy `status` during the window.
- Buyers (3.1+) MUST prefer media_buy_status when present; MAY fall back to
  legacy `status` for compatibility.
- 3.0 sellers and buyers continue to work unchanged. No required-field swap,
  no rename, no breakage.
- 4.0: deprecated top-level `status` removed from create/update success
  branches. The nested `status` fields on get-media-buys-response items,
  get-media-buy-delivery-response items, and core/media-buy.json should be
  addressed in the same 4.0 release as a coherent cascade.
- SDK regen required for @adcp/client, adcp-go, Python.

Related:
- #4876 — envelope status REQUIRED (beta.2)
- #4897 — companion governance schema rename (separate PR #4902)
- adcontextprotocol/adcp-client#1898 — SDK-side audit + transport precedence

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit to adcontextprotocol/adcp that referenced this pull request May 21, 2026
…ditive-deprecate, #4895) (#4904)

* spec(media-buy): additive-deprecate body-level `status` on create / update success responses (#4895)

Under MCP flat-on-the-wire serialization, the envelope task-status (`status`,
TaskStatus) and the body-level media-buy lifecycle status (`status`,
MediaBuyStatus) share the same root key on `CreateMediaBuySuccess` and
`UpdateMediaBuySuccess`. Enums overlap on completed | canceled | rejected and
diverge elsewhere — a MediaBuyStatus is silently destroyed when the envelope
stamps a TaskStatus at the same path. No validator catches it.

WG-recommended Option E (additive-deprecate, 3.1 minor → 4.0 removal).
**Strictly additive — no schema is renamed and no `required[]` constraint
changes.**

- create-media-buy-response.json `CreateMediaBuySuccess`: adds
  `media_buy_status: $ref media-buy-status.json` alongside the existing
  `status`. The legacy `status` is marked `deprecated: true` and slated for
  removal in 4.0. Neither field is in `required[]` (both optional in 3.1).
  `CreateMediaBuySubmitted` branch unchanged — its `status: { const: "submitted" }`
  is the TaskStatus discriminator.
- update-media-buy-response.json `UpdateMediaBuySuccess`: symmetric.

Out of scope (deliberate): `get-media-buys-response.json` `media_buys[].status`,
`get-media-buy-delivery-response.json` `media_buy_deliveries[].status`, and
`core/media-buy.json` `status` are NOT renamed. These fields live nested at
depth ≥ 1 inside arrays, so the envelope `status` at the response root does
not collide with them on the wire. Renaming them would require either a
breaking `required[]` swap or a double-field transition for no wire-collision
payoff. The nested-vocabulary inconsistency in 3.1 (one buyer call returns
`media_buy_status` at root, the next returns `status` inside an array) is the
price of keeping this strictly additive. Resolve in 4.0 alongside the
legacy-`status` removal, when a clean cascade rename is on the table.

`cancel_media_buy` is performed via update_media_buy with cancel intent —
there is no dedicated cancel tool. Inherits the rename from
UpdateMediaBuySuccess; no separate handling.

Storyboards swept:

- protocols/media-buy/state-machine.yaml — three field_present path:"status"
  assertions on update-media-buy-response.json → path:"media_buy_status".
- protocols/media-buy/scenarios/pending_creatives_to_start.yaml — two
  field_value assertions checking MediaBuyStatus values on
  create-media-buy-response.json / update-media-buy-response.json →
  path:"media_buy_status".
- protocols/media-buy/scenarios/create_media_buy_async.yaml — left as
  path:"status" (this checks the submitted-arm TaskStatus discriminator).

Docs:

- task-reference/update_media_buy.mdx — cancellation success example shows
  the canonical media_buy_status form.
- reference/whats-new-in-3-1.mdx — migration note in Final-spec
  clarifications batch.

Adopter impact:

- Sellers (3.1+) SHOULD emit media_buy_status on create/update success;
  MAY continue emitting the deprecated legacy `status` during the window.
- Buyers (3.1+) MUST prefer media_buy_status when present; MAY fall back to
  legacy `status` for compatibility.
- 3.0 sellers and buyers continue to work unchanged. No required-field swap,
  no rename, no breakage.
- 4.0: deprecated top-level `status` removed from create/update success
  branches. The nested `status` fields on get-media-buys-response items,
  get-media-buy-delivery-response items, and core/media-buy.json should be
  addressed in the same 4.0 release as a coherent cascade.
- SDK regen required for @adcp/client, adcp-go, Python.

Related:
- #4876 — envelope status REQUIRED (beta.2)
- #4897 — companion governance schema rename (separate PR #4902)
- adcontextprotocol/adcp-client#1898 — SDK-side audit + transport precedence

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* spec(media-buy): fold #4908 — MUST-equality clause + storyboard assertions for deprecation window

During the 3.1 deprecation window both `status` (deprecated) and `media_buy_status` (canonical)
coexist on CreateMediaBuySuccess / UpdateMediaBuySuccess. The prior schema was silent on whether
sellers emitting both fields must carry the same value; JSON Schema validation passed even for
divergent values (e.g., status: active, media_buy_status: paused).

- Schema descriptions: add MUST-identical-values clause to `status` and `media_buy_status` on
  both create-media-buy-response.json and update-media-buy-response.json.
- Storyboard (pending_creatives_to_start.yaml): add `field_value_or_absent` assertions on
  the deprecated `status` path at the two steps that already check `media_buy_status` —
  passes when status is absent (conformant 3.1 sellers), fails only when status is present
  with a diverging value.
- Migration doc: document the equality requirement and explain why if/then JSON Schema
  constraint was deferred (short window, codegen compat uncertainty, storyboard is sufficient).

Resolves #4908. Refs #4895.

* spec(media-buy): align schema description with 3.2 removal timeline (#4904 follow-up)

The schema descriptions on CreateMediaBuySuccess / UpdateMediaBuySuccess for
the deprecated top-level `status` and the new `media_buy_status` both still
said "removed in 4.0" while every other surface (migration page, changeset
adopter section, whats-new bullet, task-reference tables) was updated to
"removed in 3.2 (#4906)" when the cascade was split. aao-release-bot caught
the drift.

Three replacements per file ("removed in 4.0" → "removed in 3.2 (#4906)") on:
- create-media-buy-response.json: media_buy_status description, deprecated
  status description
- update-media-buy-response.json: same

Schema↔doc parity restored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Envelope status missing on every sync wire response (not just get_adcp_capabilities)

1 participant