Skip to content

/sales/mcp: exact replay with one idempotency key returns two media-buy IDs (September follow-up) #7899

Closed

Description

@jaanijuk

Summary

This is a route-specific follow-up to #2346 and its fix in #2367. The earlier detail is in the first and second follow-up comments on #2346. #2346 itself remains closed for the original /mcp route.

On 2 and 21 September 2026, repeated byte-identical create_media_buy requests to https://test-agent.adcontextprotocol.org/sales/mcp returned two distinct media_buy_id values for one idempotency key. Each ID was later returned with replayed: true.

This does not claim the /mcp fix in #2367 was ineffective. It asks whether /sales/mcp is intended to share the same idempotency scope and backing state.

The primary retained evidence is the 21 September run. The 2 September result is included as previously reported corroboration; its raw responses are not retained in this evidence set.

These are historical observations. I have not re-tested the route under stable AdCP 3.2 and am not describing its current behavior.

Version and capability context

21 September run:

  • Request declared adcp_version: "3.1" and adcp_major_version: 3. Served version: 3.1.
  • get_adcp_capabilities declared adcp.idempotency.supported: true and adcp.idempotency.replay_ttl_seconds: 86400. No build_version was present.

2 September run: no adcp_version was declared. Served version: 3.2-beta.10.

Method

For the 21 September shared-key run:

  • The create_media_buy request body was serialized once and sent ten times, sequentially.
  • The JSON-RPC request ID, payload, bearer credential, account and idempotency key were unchanged.
  • Positive control, same run: ten requests with ten distinct idempotency keys returned ten distinct media-buy IDs.

The recorded request-body SHA-256 was identical across all ten shared-key calls:
a68636d3a72d1d96bc0475539198e0f421708c88fb248e52b55496958d865b3a

The 2 September run's method, the same in substance, is described in the first follow-up comment.

Observed results

Run Returned IDs Responses per ID
2 September mb_4e89b550, mb_5d77a8c0 7 / 3
21 September mb_a3729437, mb_f0045bfd 5 / 5

The pattern was the same in both runs:

  • Request 1 returned the first ID and was not marked as a replay.
  • Requests 2 and 3 returned that ID with replayed: true.
  • Request 4 returned a second ID and was not marked as a replay.
  • Requests 5–10 returned one or other of the two IDs, each with replayed: true.

On 21 September the two unmarked responses carried no replayed field at all.

Both IDs were subsequently returned with replayed: true, but the identical request did not converge on a single media-buy ID.

Expected behavior

#2367 describes the replay cache as scoped by (auth principal, account scope, idempotency_key). The client held the bearer credential, requested account, idempotency key and tool constant across all ten requests. The server-resolved principal and account scope were not independently observed. Within the advertised replay window, requests resolving to the same scope would be expected to return one committed operation and its cached response.

Scope and limits

  • The deployment topology was not established. Separate replay stores would be one possible explanation, not an observed cause.
  • These runs did not test expiry, purge, failover, concurrency, changed-payload conflicts or cross-principal access.
  • These runs reused the same JSON-RPC ID; a separate same-key test with fresh JSON-RPC IDs was not performed.
  • I have made no new writes to the public agent for this follow-up.
  • Related compliance storyboard: static/compliance/source/universal/idempotency.yaml. It was not run against this route.

Clarification requested

  1. Is /sales/mcp intended to use the idempotency middleware and backing state addressed by fix(training-agent): enforce idempotency replay/conflict/expired semantics (#2346) #2367?
  2. An automated triage reply on Training agent declares idempotency capability but does not enforce replay/conflict semantics #2346 suggested a separate issue. Could a maintainer confirm this is the appropriate tracker for the September observations?
  3. Is an isolated sandbox available for authorized verification against the current implementation?

Activity

  1. added
    claude-triagingTriage routine is actively working on this issue (1-3 min)
    on Oct 3, 2026
  2. added this to the P0 Bugs milestone on Oct 3, 2026
  3. added
    bugSomething isn't working
    media-buyIssue concerns the media-buy protocol domain
    priority:P0Immediate priority: user/revenue/security/reliability bug to work now
    on Oct 3, 2026
  4. bokelley commented on Oct 3, 2026

    @bokelley
    Contributor

    Thanks for the detailed reproduction, the positive control, and the cross-release follow-up, @jaanijuk — picking this up now.


    Generated by Claude Code

  5. bokelley commented on Oct 3, 2026

    @bokelley
    Contributor

    Triage

    Classification: Bug — conformance failure (data integrity)
    Bucket(s): compliance suite, training / certification
    Status: Flag for human review
    Milestone: P0 Bugs

    What experts said:

    Protocol expert: Clear spec violation. A server advertising adcp.idempotency.supported: true with replay_ttl_seconds: 86400 MUST return the same canonical response — including the same media_buy_id — for byte-identical replays within the window. The idempotency storyboard (static/compliance/source/universal/idempotency.yaml, narrative items 1–2) states this as an explicit MUST. Two distinct IDs for ten byte-identical requests is a direct violation. The pattern reproduces on stable 3.1 (September 21) and 3.2-beta.10 (September 2), meets P0 data-integrity criteria, and belongs in P0 Bugs. DR-0021 is adjacent but addresses a different failure mode (COMMITTED_RESOURCE_PURGED). The storyboard coverage gap (idempotency.yaml never run against /sales/mcp) warrants separate tracking.

    Code reviewer (updated 2026-10-05): The lazy-init-to-in-memory-fallback hypothesis from the initial analysis does not hold against the actual boot sequence. In server/src/http.ts, initializeDatabase(dbConfig) (line 10197) runs before the boot probe getIdempotencyStore().probe?.() (line 10218), and both run before listen() (line 10297). Because isDatabaseInitialized() returns pool !== null, the pool exists at the moment getIdempotencyStore() is first called, so every replica selects fencedPgBackend() on the standard boot path — there is no silent fallback to memoryBackend(). The tenant-level getSdkIdempotencyStore() call inside buildDefaultServerOptions() (registry.ts line 204) is lazy (first request triggers it), but by that point the process-wide storeInstance is already PG-backed. @jaanijuk's read of the code is correct. Root cause is unconfirmed. The non-alternating pattern (A A A B B A B B B A on 21 Sep; A A A B A A A A B B on 2 Sep) is inconsistent with a simple two-instance round-robin and a different split mechanism must explain it.

    My take:

    The spec violation and P0 classification stand. The specific mechanism previously identified (lazy-init race locking replicas into in-memory) is not supported by the boot sequence. Why byte-identical create_media_buy calls to what should be a shared PG-backed store returned two distinct IDs across replicas remains unexplained. An on-host investigation is needed before a fix can be written and validated.

    Updated blocking for @bokelley:

    1. Can you confirm all production Fly.io replicas for /sales/mcp are PG-backed at time of first request? Specifically: does isDatabaseInitialized() return true when getIdempotencyStore() is called at boot (http.ts line 10218) on every machine? A Fly log showing backend selection per replica would clarify.
    2. Is there any code path under /sales/mcp → handleCreateMediaBuy that bypasses the singleton in idempotency.ts — i.e., creates a second store or uses a distinct pool? The non-alternating sequence cannot be explained by 2-instance round-robin and may indicate 3+ replicas or a path-level isolation.
    3. Should the storyboard coverage gap (idempotency.yaml scope excludes /sales/mcp) be tracked here or in a separate issue?

    Generated by Claude Code

  6. added
    claude-triagedIssue has been triaged by the Claude Code triage routine. Remove to re-triage.
    and removed
    claude-triagingTriage routine is actively working on this issue (1-3 min)
    on Oct 3, 2026
  7. jaanijuk commented on Oct 5, 2026

    @jaanijuk
    Author

    Thanks for picking this up. One note on the hypothesized cause, from reading source only. I have not run anything. The commits are main at 8f7c110 and main as of the 21 September run at e3693a4; the code below is the same at both.

    • isDatabaseInitialized() returns pool !== null (server/src/db/client.ts). It checks that the pool object exists, not that it is ready.
    • In HTTPServer.start() (server/src/http.ts), initializeDatabase(dbConfig) runs synchronously before the boot probe getIdempotencyStore().probe?.(), and both run before listen(). On that path, the first getIdempotencyStore() call would select fencedPgBackend().
    • The sales tenant's getSdkIdempotencyStore() wiring in tenants/registry.ts is built in a registry holder created on first request, so it comes after start().

    On the production entry path (dist/index.js), I can't see how a replica would lock into memoryBackend() through the ordering described. I don't know which build was deployed in September, or whether another path applies.

    Separately, the observed orders don't alternate. On 21 September they were A A A B B A B B B A; on 2 September, A A A B A A A A B B.

    I'm not proposing a cause. I'm noting this so a fix can be checked against the mechanism that actually produced the split.

  8. added
    claude-triagingTriage routine is actively working on this issue (1-3 min)
    and removed
    claude-triagingTriage routine is actively working on this issue (1-3 min)
    on Oct 5, 2026
  9. bokelley commented on Oct 16, 2026

    @bokelley
    Contributor

    Verified against the deployed route on 2026-10-16 with the public test token from the quickstart (sandbox account test_account):

    • One idempotency key, byte-identical create_media_buy payload, 10 sequential requests then 5 concurrent: 15/15 returned the same media_buy_id (mb_d3203011), and all 14 repeats carried replayed: true.

    The fix is #7969, which makes production always use the durable Postgres replay store (resolved lazily so a cold replica can't fall back to in-memory), so /sales/mcp replays are scoped the same way #2367 describes for /mcp: by auth principal, account scope and idempotency key.

    On the questions here: yes, /sales/mcp uses the same idempotency middleware and backing state; and the public test token plus test_account is the sandbox for this kind of verification (anything created there is sandbox data). Running universal/idempotency.yaml against this route is still worth adding to the storyboard matrix; reopen or file separately if you see a split again.

  10. jaanijuk commented on Oct 16, 2026

    @jaanijuk
    Author

    Thanks — independently re-checked on the deployed /sales/mcp route this morning (2026-10-16T23:53Z) with the quickstart test token and test_account, client adcp_version 3.2:

    • One key, byte-identical create_media_buy, 10 sequential + 5 concurrent: 15/15 same media_buy_id, 14 replayed: true.
    • Fresh key, 5 concurrent first requests, then 3 sequential: one media_buy_id across all 7 accepted responses, 1 fresh. One of the five concurrent requests received IDEMPOTENCY_IN_FLIGHT (recovery: transient, retry_after: 30); the others blocked (~11 s) and then replayed.
    • Control: 5 distinct keys → 5 distinct ids.

    No split observed. Bounded run, so this doesn't prove the fix complete. I'll reopen or file separately if I see one again.

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 workingclaude-triagedIssue has been triaged by the Claude Code triage routine. Remove to re-triage.community-reportedmedia-buyIssue concerns the media-buy protocol domainpriority:P0Immediate priority: user/revenue/security/reliability bug to work now

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions