Context
Follow-up to #466 ("feat: implement durable legacy purchase continuation
coordinator for AdCP 3.2"). The core coordinator API, durable-store
interface, and a well-tested in-memory reference backend
(adcp/v3/legacypurchase) are implemented in a companion PR against #466 —
see that PR for the coordinator itself (Store.RegisterContinuation,
Store.ContinueLegacyPurchase, atomic single-use claim proven under
-race, deterministic replay, fail-closed crash reconciliation via
AmbiguousClaimError, and the products-only-brief-compatibility vectors
from the AdCP 3.2 schema bundle run end to end).
Two pieces of #466's scope were deliberately deferred out of that PR rather
than forced into one oversized change, and are tracked here:
1. Persistent (e.g. Postgres) legacypurchase.Backend implementation
adcp/v3/legacypurchase ships the pluggable Backend interface and
MemoryBackend (in-process) only. MemoryBackend is process-local — a
restart loses in-flight and recently-completed continuation state, which is
unacceptable for a production coordinator meant to prevent double-purchase
across a real deployment (potentially multiple instances behind a load
balancer, matching the same durability motivation as #105 /
adcp/v3/signing/pgreplay's PostgresReplayStore).
Scope for this half:
- A
PostgresBackend implementing legacypurchase.Backend's three-state
claim/commit/fail FSM atomically (unique constraint on token +
conditional UPDATE ... WHERE token = $1 AND state = 'offered' for
ClaimPending, mirroring adcp/v3/idempotency's PgBackend and
adcp/v3/signing/pgreplay's atomic-insert pattern).
- A real concurrency proof against a live Postgres instance (e.g.
testcontainers-go, matching this repo's existing
registry/redisstore / registry/glidestore /
adcp/v3/signing/pgreplay integration-test convention): many concurrent
distinct-key claims against one continuation token, exactly one winner,
under -race.
- A migration/schema file and
SweepExpiredReplays-equivalent retention
sweep for terminal and expired-offered records (never sweeping
StatePending — see the core PR's MemoryBackend for why).
2. Reverse compact-seller → legacy-buyer server-side facade
The spec's "Established buyers against a compact-backed seller" section
(specs/legacy-compact-lifecycle-compatibility.md) and
static/compliance/source/test-vectors/products-only-brief-compatibility/vectors.json's
reverse_compatibility_cases describe the opposite direction from the
coordinator in the core PR: a 3.2 seller that implements refine_proposals
etc. natively but must still preserve its deprecated get_products /
create_media_buy facades for established buyers — using "a private atomic
coordinator" internally rather than a buy-then-sync compensating saga.
This is seller-side adapter work — a materially different scope from the
buyer-side coordinator in #466 / the core PR (different failure surface: the
seller owns both the compact and legacy code paths and must keep them
transactionally consistent, rather than redeeming an opaque token issued by
someone else). It needs its own design pass (at minimum: how a seller
built with this SDK's adcp/v3 package would wire a legacy facade through
its native compact handlers) before implementation.
Acceptance criteria
Context
Follow-up to #466 ("feat: implement durable legacy purchase continuation
coordinator for AdCP 3.2"). The core coordinator API, durable-store
interface, and a well-tested in-memory reference backend
(
adcp/v3/legacypurchase) are implemented in a companion PR against #466 —see that PR for the coordinator itself (
Store.RegisterContinuation,Store.ContinueLegacyPurchase, atomic single-use claim proven under-race, deterministic replay, fail-closed crash reconciliation viaAmbiguousClaimError, and theproducts-only-brief-compatibilityvectorsfrom the AdCP 3.2 schema bundle run end to end).
Two pieces of #466's scope were deliberately deferred out of that PR rather
than forced into one oversized change, and are tracked here:
1. Persistent (e.g. Postgres)
legacypurchase.Backendimplementationadcp/v3/legacypurchaseships the pluggableBackendinterface andMemoryBackend(in-process) only.MemoryBackendis process-local — arestart loses in-flight and recently-completed continuation state, which is
unacceptable for a production coordinator meant to prevent double-purchase
across a real deployment (potentially multiple instances behind a load
balancer, matching the same durability motivation as #105 /
adcp/v3/signing/pgreplay'sPostgresReplayStore).Scope for this half:
PostgresBackendimplementinglegacypurchase.Backend's three-stateclaim/commit/fail FSM atomically (unique constraint on
token+conditional
UPDATE ... WHERE token = $1 AND state = 'offered'forClaimPending, mirroringadcp/v3/idempotency'sPgBackendandadcp/v3/signing/pgreplay's atomic-insert pattern).testcontainers-go, matching this repo's existingregistry/redisstore/registry/glidestore/adcp/v3/signing/pgreplayintegration-test convention): many concurrentdistinct-key claims against one continuation token, exactly one winner,
under
-race.SweepExpiredReplays-equivalent retentionsweep for terminal and expired-offered records (never sweeping
StatePending— see the core PR'sMemoryBackendfor why).2. Reverse compact-seller → legacy-buyer server-side facade
The spec's "Established buyers against a compact-backed seller" section
(
specs/legacy-compact-lifecycle-compatibility.md) andstatic/compliance/source/test-vectors/products-only-brief-compatibility/vectors.json'sreverse_compatibility_casesdescribe the opposite direction from thecoordinator in the core PR: a 3.2 seller that implements
refine_proposalsetc. natively but must still preserve its deprecated
get_products/create_media_buyfacades for established buyers — using "a private atomiccoordinator" internally rather than a buy-then-sync compensating saga.
This is seller-side adapter work — a materially different scope from the
buyer-side coordinator in #466 / the core PR (different failure surface: the
seller owns both the compact and legacy code paths and must keep them
transactionally consistent, rather than redeeming an opaque token issued by
someone else). It needs its own design pass (at minimum: how a seller
built with this SDK's
adcp/v3package would wire a legacy facade throughits native compact handlers) before implementation.
Acceptance criteria
legacypurchase.PostgresBackend(or similarly named) implementsBackendwith atomic claim/commit/fail transitions.-race.reverse_compatibility_casesvector.that turns out cleaner once scoped in detail — this issue exists to
track that they are real, disclosed, not-yet-done work rather than to
mandate exactly one combined PR.