Skip to content

feat(idempotency): inject 'replayed: true' on cache hit (#714)#717

Merged
bokelley merged 3 commits into
mainfrom
claude/issue-714-idempotency-replayed-flag
May 13, 2026
Merged

feat(idempotency): inject 'replayed: true' on cache hit (#714)#717
bokelley merged 3 commits into
mainfrom
claude/issue-714-idempotency-replayed-flag

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

Closes #714.

Summary

  • IdempotencyStore.wrap now injects replayed: true at the envelope level on every cache hit per AdCP L1/security idempotency rule 4.
  • The flag is set on the cloned response, not the cached entry — multiple replays of the same key all carry exactly one replayed: true and never compound.
  • CachedResponse docstring updated: the prior text said sellers were responsible for the flag, but the matching store-level docstring (already in place) described the library doing it. Library is now the authoritative owner; the two docstrings agree.

Test plan

  • New test_replay_flag_does_not_compound_across_retries — verifies the flag stays on the cloned response and a caller mutating the replay envelope can't bleed back into the cached entry.
  • Updated test_cache_hit_replays_without_handler_call — first call carries no replayed, replay carries replayed: true, all other fields identical.
  • All 60 existing tests in tests/test_server_idempotency.py pass after migrating to _without_replay_flag() for content-equivalence assertions.
  • All 92 tests in adjacent idempotency files (test_idempotency.py, test_validate_idempotency_wiring.py, test_idempotency_storyboard.py) pass.
  • ruff check + mypy clean.

Migration

Adopters who built replayed: true injection themselves (e.g. salesagent's _ReplayMarkingStore) see the flag set twice in a row — idempotent — and can delete their custom wrap on the next bump.

🤖 Generated with Claude Code

bokelley and others added 2 commits May 13, 2026 05:39
…ity rule 4)

Closes #714.

``IdempotencyStore.wrap`` now injects ``replayed: true`` at the
envelope level on every cache hit, per AdCP L1/security idempotency
rule 4. Buyer agents that key on the flag to suppress side effects
(notifications, webhook dispatch, memory writes) get the signal
natively instead of every seller subclassing the store to re-implement
the wrap path just for this one boolean.

The flag is applied to the cloned response (returned to the caller),
not the cached entry — multiple replays of the same key all carry
exactly one ``replayed: true`` without compounding through cache
poisoning. The ``isinstance(dict)`` guard preserves the path under
future widenings of ``CachedResponse.response`` (today typed as
``dict[str, Any]``, but adopters have been caught caching Pydantic
envelopes; the guard keeps the contract safe).

Also resolves contradictory docs: the store-level docstring already
described this behavior (aspirationally); the ``CachedResponse``
docstring claimed sellers inject the flag (matching the prior
implementation reality). Updated ``CachedResponse`` to point at the
store as the owner.

Migration: adopters who built ``replayed: true`` injection themselves
(e.g. salesagent's ``_ReplayMarkingStore``) see the flag set twice in
a row — idempotent — and can delete their custom wrap on the next bump.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drop the ``isinstance(replay, dict)`` guard. ``CachedResponse.response``
  is typed ``dict[str, Any]``; the guard was documenting an off-spec
  adopter pattern (caching Pydantic envelopes) rather than fixing or
  rejecting it. Per the project's no-fallbacks rule, trust the contract
  and let a type violation surface loudly via TypeError if it ever does
  occur in production.

- Rewrite ``test_replay_flag_does_not_compound_across_retries`` →
  ``test_replay_flag_does_not_poison_cached_entry``. The original test
  passed for the wrong reason — ``_clone_response`` deep-copies on
  every read, so the mutation on ``r2`` was always a no-op regardless
  of where the injection landed. The new test peeks at the backend
  directly and asserts ``"replayed" not in cached.response`` both
  before and after a replay, which actually exercises "inject on the
  clone, not the cached entry."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley changed the title fix(idempotency): inject 'replayed: true' on cache hit (#714) feat(idempotency): inject 'replayed: true' on cache hit (#714) May 13, 2026
… flag (#717)

CI on PR #717 failed because two test sites outside the immediate
``tests/test_server_idempotency.py`` module assert ``r1 == r2`` on
idempotent replay — both now need the same ``replayed: true``-tolerant
comparison:

- ``tests/test_server_caller_identity.py::TestEndToEndIdempotencyViaTransport::test_a2a_transport_identity_enables_middleware_dedup``
- ``tests/conformance/decisioning/test_pg_idempotency_backend.py::test_idempotency_store_replays_via_pg_backend``

Both now assert the replay envelope carries ``replayed: true`` and the
rest of the response is identical to the first call. Caught locally
in the full test sweep after the CI red surfaced these two.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit b511e01 into main May 13, 2026
16 checks passed
@bokelley bokelley deleted the claude/issue-714-idempotency-replayed-flag branch May 13, 2026 10:05
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.

IdempotencyStore.wrap should inject 'replayed: true' on cache hit (AdCP L1/security rule 4)

1 participant