Skip to content

chore: merge main into next through 9.6.1 - #1863

Merged
bokelley merged 29 commits into
nextfrom
claude/issue-1856-merge-main-into-next
Nov 8, 2026
Merged

bokelley merged 29 commits into
nextfrom
claude/issue-1856-merge-main-into-next

Conversation

@bokelley

@bokelley bokelley commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Forward-ports main through SDK 9.6.1 into next with a true merge, as #1671 and #1578 did. Merge with "Create a merge commit", never squash or rebase.

Forward-ports main through v9.6.1 (8da31cc) and #1858 into next with a
true merge, as #1578 and #1671 did. Most of main was already ported to next
separately; where the same fix exists, next's version is kept:
#1702 (cb2ba54 #1701), #1704 (ab39d63 #1713), #1710 (612f97a #1708),
#1718 (b014066 #1706), #1752 (#1748), #1757/#1758 (d005f95 #1759),
#1790 (#1788), #1795 (#1794), #1798 (#1797), #1810 (#1804),
#1813, #1841 (a541585) and #1847 (#1849). New on next from main: the
reporting-storage changes #1827, #1828, #1835 and #1836, #1819 (PR title
policy), the 9.3.1-9.6.1 release files and #1858 (.traycer).

Conflicts and resolutions:

#1857 does not take effect on next. It makes storage 0.1.x refuse a ledger
whose reporting_configurations has offering_id, and every 0.2.x (storage
contract 2) ledger has it. pg.py, outbox/status_pg.py and
reporting_ledger_account_generations.sql are next's (no 42P10 mapping, no
refusal branch, 0002's DDL unchanged); ReportingSchemaAheadError,
schema_ahead_error and their export are dropped from migration.py and
init.py; tests/conformance/reporting/
test_reporting_storage_contract2_refusal_pg.py (a 0.1.x test) is dropped;
the regenerated public API snapshot does not list it.

Reporting storage (#1827, #1828, #1835, #1836) in next's migration
framework: no new numbered migration. Every DDL change is in
reporting_changes.sql (change_feed=True) or reporting_row_storage.sql
(row_storage), which no numbered migration includes and which
create_schema() re-applies idempotently on every start with those options,
as on main. So (a) a fresh install gets the new DDL from create_schema;
(b) a ledger at 0006 via python -m adcp migrate has no change-feed or row
tables until create_schema runs with the option, and then gets the current
DDL; (c) a create_schema boot upgrades in place (xid horizon columns,
named consumer and binding constraints, the (account_id, xid, seq) index,
the orphan collector fence table). A 0007 that included those files would
give every migrated ledger tables it never enabled and could not reach
store= boots anyway (apply_pending stops at 0004). migration.py gains
#1835's fence table in _STARTUP_ROWS.

Account horizon: 0.2.0b0-b2 prune without maintaining
pruned_through_xid, so on a ledger 0.1.0b5 wrote, a 0.1.0b5 xid cursor could
pass the stale check and skip what 0.2.x pruned. reporting_changes.sql now
raises each account horizon, once, to the deployment horizon (kept in
(xid, seq) by every release) or just below the account's oldest retained
change when lower, so those cursors fail closed and a fresh reader still
gets every retained change. It runs only when the xid columns predate this
boot and 0001 was recorded in an earlier transaction (a 0.2 release
upgraded the ledger); a comment on adcp_reporting_change_account_horizon
records that it ran. A direct 0.1.0b5 upgrade and a pre-(xid, seq) install
are unchanged. Covered by
tests/conformance/reporting/test_reporting_change_feed_horizon_upgrade_pg.py,
documented in docs/reporting-ledger-migration.md and the storage README.

Reviewer notes

Unblocks #1856.

🤖 Generated with Claude Code

bokelley and others added 29 commits October 29, 2026 07:50
…can call it (#1702)

A version-pinned MCP seller (an adcp_version pin, which every decisioning
PlatformHandler carries) advertised the exact bundled wire schemas in
tools/list, ignoring schema_mode. get_products's response alone resolves
278 definitions, and MCP cannot share definitions between tools, so a plain
eleven-tool seller listed 106,250 JSON members (2.5 MB). An SDK 10 buyer
bounds a response's JSON members and its MCP client lists tools before
every call, so every call to such a seller failed with
ADCPResponseTooComplexError before the handler ran (#1692). This line's
client has no member bound; the fix is to the listing a 9.x seller serves.

The default schema_mode="compact" now compacts the bundled schema by the
rules the unpinned path already applied to Pydantic output (every root
field with its description and rules, every required nested field, every
scalar with its constraints, each dropped optional nested field re-admitted
as true), after dropping nested cross-field rules (if/then/else, not),
nested x- metadata, nested $schema and definition descriptions, and
advertising oneOf as anyOf so a loosened arm can never make a valid
response fail the client's output-schema check. Local $ref pointers are
JSON-Pointer-unescaped (the bundled definitions are named after paths,
"external:core~1product.json"). The compact shape is memoized with the
loader state. schema_mode="defs" and "inline" keep the exact bundled
schema. The startup size line reports JSON members as well as bytes and
warns past half of either SDK 10 buyer bound.

The issue's seller lists 17,181 members (467 KB). Every tool the SDK can
serve, pinned to the packaged release, lists 66,028 members and 1.8 MB,
under half of both bounds; a guard test pins that, and an end-to-end test
has an SDK client at defaults call get_products on an SDK seller at
defaults over MCP.

Fixes #1692

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: aao-ipr-bot[bot] <279263549+aao-ipr-bot[bot]@users.noreply.github.com>
…t 560 MiB RSS (#1704)

Importing adcp.client, adcp.server, adcp.decisioning or adcp.compat.versions
realizes the type surface through adcp.types._eager, and on 9.3.0 that peaked
at ~495 MiB RSS (~560 MiB for adcp.client) against ~130 MiB on 6.6. Two
things built pydantic-core validators at import for models a process may
never use:

- The import-time patchers (_ergonomic, _forward_compat, canonical_creative,
  aliases) edited model_fields and then called model_rebuild(force=True) on
  ~35 request/response roots. Pydantic does not share nested defer_build
  schemas between parents, so each forced rebuild walked the model's whole
  subtree into a fresh validator and serializer: ~300 MiB and ~7 s of the
  import. They now call adcp.types.base.defer_model_rebuild(), which puts the
  model back into the defer_build placeholder state so the patched fields
  take effect on first use. Same outcome, later moment.

- A generated `class X(RootModel[A | B])` built the union's validator at the
  RootModel[...] expression, before X or any config it declares existed;
  adagents.AdcpAgentsAuthorization and core.format.CanonicalParameters alone
  cost ~60 MiB. Generated composing roots now parametrize
  adcp.types.base.AdCPRootModel, which carries defer_build=True, via the new
  post-generate fixer defer_rootmodel_builds.

Peak RSS of `python -I -c "import <module>"` (Linux, CPython 3.12):

  adcp.types._eager            495 -> 114 MiB  (import 14.5 s -> 4.1 s)
  adcp.client                  561 -> 175 MiB
  adcp.server                  565 -> 183 MiB
  adcp.server.test_controller  562 -> 183 MiB
  adcp.decisioning             573 -> 187 MiB
  adcp.compat.versions         498 -> 118 MiB

The cost moves to first use: the first validation of a deferred root builds
its schema (90-330 ms for the request models, ~2.4 s for McpWebhookPayload's
JSON schema), and a process that has used every one of them sits at ~215 MiB
instead of ~520 MiB. build_all_models() still realizes everything eagerly for
freezegun callers.

Deferring more models exposed a pydantic limitation that already applied to
every defer_build model: pydantic-core reaches a serializer by reading
instance.__pydantic_serializer__ when it dumps by inference (an Any-typed
field, serialize_as_any, a plain-pydantic parent), and a placeholder there
raised TypeError instead of building. Every deferred SDK model now carries
placeholders that are also descriptors (_DeferredValSer), installed by
AdCPBaseModel/AdCPRootModel.__pydantic_init_subclass__ and by
defer_model_rebuild, so that read builds the model and returns the real
serializer. This also fixes the pre-existing TypeError for an unvalidated or
parent-built instance of any generated model dumped through such a parent.

tests/test_import_memory.py imports each entry module in a subprocess and
fails when its peak RSS exceeds 320 MiB; tests/test_lazy_rebuild.py grades
the deferral contract, including that a canonical subclass may forward-
reference a model defined later in its module (supersedes #1690).

Fixes #1694

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
generate_signing_keypair, pem_to_adcp_jwk and adcp-keygen --purpose took a
closed Literal["request-signing", "webhook-signing"] and refused every other
value, so the SDK could not mint or publish a key for response-signing (the
designated-task response JWS profile, core/response-payload-jws-envelope.json),
governance-signing (which governance.py already verifies), or a surface the
vocabulary does not name yet. The helpers they delegate to already took a str.

purpose is now validated by syntax instead of membership: a lowercase
hyphenated token of at most 64 characters. Malformed values (empty, upper
case, underscores, whitespace, leading/trailing/double hyphens, non-str)
still raise ValueError, and the CLI exits 2 before writing a file. An open
vocabulary is safe because purpose matching is fail-closed: a verifier
accepts a key only when adcp_use equals the purpose of the surface it
verifies, so a key under any other purpose verifies nothing there. A test
pins that against the request and webhook verifiers.

adcp.signing exports ADCP_USE_REQUEST, ADCP_USE_WEBHOOK (marked deprecated
upstream), and the new ADCP_USE_RESPONSE and ADCP_USE_GOVERNANCE, so callers
need not retype literals. The docstrings no
longer say request- and webhook-signing keys must be distinct for every
verifier: 3.3 webhook verifiers accept request-signing keys, and new webhook
signers publish request-signing.

Backport of #1708 to 9.x. Closes #1677 on main.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Backport of #1706 to SDK 9.x (AdCP 3.2), whose get_products / get_signals
wholesale fields, if_wholesale_feed_version / if_pricing_version rules and
unchanged: true response are the same as 3.3's.

adcp.server gains wholesale_feed_version and pricing_version (RFC 8785
canonical, SHA-256, order-independent across items, path-addressed freshness
fields ignored, keyed on cache_scope), check_feed_preconditions (the mode and
pairing rules, INVALID_REQUEST with field) and feed_unchanged.
products_response / signals_response no longer write unchanged: false, and
refuse unchanged=True with items or without wholesale_feed_version, both of
which the schema already rejects.

Differences from #1706: the extra= docs no longer name the 3.3-only
collections / signal_pricing_group_rules, and one test compares a model with
its own wire form, since SDK 9 dumps unset schema defaults.

Refs #1676

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(adagents): attribute and verify publisher property claims

* fix(adagents): explain invalid publisher claim exclusions
#1752)

The advisory, critical, covers every version up to 1.14.2, after
revocation of a CA in the Sigstore chain used for Trusted Publishing and
PyPI attestations. Same change as #1748 on next.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: aao-ipr-bot[bot] <279263549+aao-ipr-bot[bot]@users.noreply.github.com>
Backport of #1767 (c1c6d6e) from `next`. Since 2026-11-01 every
`main`-based CI run fails
`test_the_producer_stages_object_uploads_outside_its_publication_transaction`
with `STATE_UNAVAILABLE: no live worker lease covers this row upload`
(#1792): the settling harness pins its clock at 2026-11-01T02:10Z, the
producer stamps `lease_expires_at` from that clock, and `row_write.py`
checks the lease against the database's `clock_timestamp()`. Once real
time passed the frozen instant plus the lease, no upload was covered.

Run the producer's turn at the wall clock so the stamped lease lies in
the database's real future. The lease check keeps its production
semantics; only the test's clock moves.

Refs #1792

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…ncyStore.wrap (#1795)

A method that may hand off to a workflow could not use the SDK's
idempotency: IdempotencyStore.wrap coerced every completed result to a dict
for the replay cache, and a TaskHandoff or WorkflowHandoff is a control
marker for the dispatcher, not a response, so the call raised
"Cannot coerce WorkflowHandoff to dict for idempotency caching" (#1781).
Adopters copied the private wrapper to work around it.

wrap now returns a handoff uncached, after the key, conflict, lock and
cancellation handling, so the dispatcher allocates the task as usual. It
also accepts cache_response=, a predicate a seller can use to keep its own
control results out of the cache: @store.wrap(cache_response=...) caches a
result only when the predicate returns True, and a handoff is never cached.
Both forms register the wrapper for validate_idempotency_wiring.

An uncached result stores no replay entry, so a retry with the same key runs
the method again; the docs say to deduplicate the enqueued work on the key.
Replaying the task id the dispatcher allocated needs dispatcher support and
is left for a follow-up.

Closes #1781

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…orts first in any order (#1798)

Backport of the SDK 10 fix for #1784 to the 9.x line, where the cycle
reproduces identically: ``from adcp.types.domains.formats import Slot`` raised
ImportError when it was the first adcp import in a process, because the domain
roots imported every schema module eagerly and ``formats/canonical/_base.py``
re-entered ``formats/canonical`` through ``core/__init__`` while half built.

Each domain root is now a PEP 562 module: ``__all__`` and a TYPE_CHECKING
import block keep the static surface, ``_DECLARED_IN`` names each name's
schema module, and ``__getattr__`` imports the declaring module on first read
and caches the binding. Nothing is renamed and every name still resolves.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
… error (#1780) (#1810)

A seller that validates a whole request could only raise one AdcpError,
so the payload errors[] carried one entry even when the caller listed
every problem in details["errors"].

AdcpError takes errors=[...] (AdcpError, generated Error or wire mapping
entries) and AdcpError.from_errors([...]) builds the raise from the list,
the first entry filling adcp_error. build_mcp_error_result, the A2A
failed-task builder and translate_error write the whole list to errors[];
adcp_error is unchanged. A details["errors"] list whose entries carry a
string code and message is lifted into errors[] as well. An entry
without recovery takes the raised error's; entry details are sanitized
like to_wire().

A single-error raise still mirrors adcp_error into errors[] (#1558).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(canonical-formats): read version constraints from typed legacy formats

* test(a2a): isolate context ID handler registration
…#1758)

* fix(canonical-formats): check narrowing on seller-asserted references

* test(a2a): isolate context ID handler registration
…ks with, on main (#1790)

* ci(release): gate publishes on a companion the built distribution works with

scripts/check_companion_releases.py refused an adcp release only when no
published adcp-reporting-storage *range* accepted it. adcp 10.0.0b4 changed
the ledger column lists while the published storage (adcp>=9.0.0,<11) still
accepted it, so the pair installed cleanly and failed every reporting turn
(#1763). Range compatibility is not contract compatibility.

The gate now proves the pair works. With --dist, it installs the wheel just
built together with the published companion into a private environment and
runs scripts/companion_smoke.py there, which compares the storage contract
each side declares (adcp.reporting.ledger.storage_contract versus
adcp_reporting_storage.contract; releases predating the declaration are
classified by their column lists) and, with ADCP_PG_TEST_URL, creates the
ledger schema and round-trips a configuration, obligation and revision
through the installed stores on PostgreSQL, the calls that failed on the
incompatible pair. Storage publishes are gated too, in the other direction:
the built storage wheel is smoked with the oldest and the newest published
adcp its own requirement accepts, so a floor that admits an adcp of another
contract is refused.

release-publish.yml builds first, runs the gate for both packages against a
PostgreSQL service, then uploads. Verified locally against PyPI and
PostgreSQL 16: the adcp wheel built from next is refused against the
published 0.2.0b0 (contract 2 versus 1), the storage wheel from #1787
(floor 10.0.0b4) passes with 10.0.0b4, and the published 0.1.0b4 passes with
adcp 9.0.0 and 9.3.1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 7b40f34)

* test(release): assert the gate smokes both packages and classify stubbed contracts

The release configuration test expected the companion gate to run only for
adcp publishes; it now runs for both packages after the build, with a
PostgreSQL service. The smoke classification test stubs both declaration
modules instead of assuming the workspace pair agrees, which it does not
until the storage declaration (#1787) lands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 876faa5)

* test(release): classify the smoke's undeclared adcp from patched column lists

The test asserted this checkout's column lists carry offering_id, which is
true on next and false on main; both classifications are now driven by
patched lists so the test is branch-agnostic.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: aao-ipr-bot[bot] <279263549+aao-ipr-bot[bot]@users.noreply.github.com>
…#1819)

The conventional-commit policy stripped a trailing "(#N)" from every
message, including PR titles. #1810's title ended in "(#1780)", passed as
if that were the PR number, and integrated as "... (#1780) (#1810)",
which the main check rejects. Main CI failed at the 9.5.0 release commit,
so Release publish refused to publish 9.5.0.

PR titles are now checked as written, since the squash appends the PR
number only at integration. 272fced keeps its subject through an
exact-commit-and-subject exception, like the historical breaking-footer
ones. Every main commit since the policy floor validates.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: aao-ipr-bot[bot] <279263549+aao-ipr-bot[bot]@users.noreply.github.com>
…dential refs (#1828)

* feat(reporting-storage): accept 255 character row binding IDs and credential refs

row_binding_id and a binding's credential_ref were capped at 128 characters,
tighter than every comparable AdCP identifier. Both now take 1 to 255
characters of A-Za-z0-9_.:-, in the configuration check, the provider client
maps and the row bindings CHECK. The CHECK is replaced in place and
idempotently by the guarded DDL, under the name the JavaScript SDK uses, and
the catalog manifest is regenerated. The configuration check uses a full match,
so a trailing newline that the old pattern let through is refused, and a long
rejected ID is truncated in the message.

Mirrors adcp-client#3595. Closes #1498.

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

* fix(reporting-storage): refuse a non-string binding ID without a type error

The invalid binding ID message passed the key to json.dumps, which raised a
TypeError for a key such as bytes instead of the intended INVALID_INPUT.
Stringify the key before truncating and quoting it.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…#1827)

Account feeds now order by (xid, seq) behind xid < pg_snapshot_xmin, as the
deployment feed does, replacing the page truncation from #1483. A lower seq
that commits late is returned on a later page, never skipped, and no feed
depends on the account lock. Account cursors carry xid; a seq-only cursor is
mapped to a position that skips nothing and still expires exactly through
pruned_through_seq. Consumer holds, change pruning and period retirement
compare (xid, seq), prune victims are taken in that order, and the migration
adds the horizon xid columns, the consumer check constraint and the
account_id, xid, seq index idempotently.

Mirrors adcp-client#3289. Closes #1825.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
… a clone fence (#1835)

* feat(reporting-storage): collect orphaned row objects by listing with a clone fence

Opt-in collector for row objects the write-intent sweep cannot see, mirroring
the JavaScript SDK. Providers gain an optional list, implemented for the
filesystem, GCS, S3 and Azure providers and checked by the conformance suite.
collect_orphaned_row_objects lists each configured object binding under its
owned prefix and deletes an object only when it is a well-formed row key, old
enough, named by no record or upload at any version under any binding, and
unreferenced under the per-object lock of every binding. A real pass first
claims the namespace in a create-only epoch chain in the bucket, recorded in
adcp_reporting_orphan_collector_fences, re-checks it every 25 deletes and
refuses when another database holds a newer claim, so a cloned or restored
ledger cannot delete the live database's objects. take_over_orphan_collector_fence
is the deliberate step after a restore. The store option orphan_collection
schedules it in the production worker, which reports fenced and unsupported
bindings.

Mirrors adcp-client#3291 and adcp-client#3593. Closes #1829.

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

* test(reporting-storage): assert the cancelled claim outcome instead of awaiting under raises

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

* test(reporting-storage): record the orphan collector names in the public API snapshot

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

* test(reporting-storage): skip the store option test where psycopg is not installed

The base test matrix has no pg extra, and constructing the store needs it.

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

* test(reporting-storage): skip the migration upgrade test where psycopg is not installed

The test imported psycopg errors before the database skip, so the base test
matrix failed with ModuleNotFoundError on main.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…ing pairs on it (#1836)

Mirrors the cross-SDK contract from adcp-client#3606. Retirement, the intent
sweep and a commit that adopts an object its own intent recorded take no object
lock and hold no transaction or pooled connection across the provider delete.
The lock stays in the two places that pair with each other: the orphan
collector delete, which locks every binding key for the object, and a commit
that adopts an object its intent did not record. A commit closes its intent
with RETURNING created_objects to know which adopted objects it recorded. The
lock key text is unchanged, so a Python deleter and a JavaScript adopter agree
in one database.

Closes #1492.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Honor explicit lenient processing for legacy creative format projection: dispatch supported siblings and return ordered per-creative failures for unprojectable formats. Preserve strict atomic rejection, original idempotency identity, rejected-assignment safety, and SDK-managed task completion results.

Materialize SDK-owned failures before mutation and preserve accepted typed rows, including schema-valid assignment error keys. Unsafe destructive/filter/external-async combinations fail before dispatch and are documented.

Validated with 18,863 passing repository tests and 92.16% coverage, 110 focused tests, all GitHub checks, installed-wheel probes, and independent protocol and Ladon approvals.

Fixes #1839.
Co-authored-by: aao-ipr-bot[bot] <279263549+aao-ipr-bot[bot]@users.noreply.github.com>
…1847)

* fix(reporting): allocate a unique handler name for every installation

Give each newly installed reporting handler its own UUID-backed class name.
Service addresses can be reused, and one service can install sellers with
separate advertised tool sets. Preserve same-platform reinstall identity and
strict conflicting handler registration checks.

Fixes #1846

* test(reporting): isolate the installation fixture from global handler names
Co-authored-by: aao-ipr-bot[bot] <279263549+aao-ipr-bot[bot]@users.noreply.github.com>
Adds .traycer/environment.json so new Traycer worktrees run the repo setup
automatically: copy gitignored .env files from the main checkout, uv sync
with all extras, and make bootstrap.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…2 with the real remedy (#1857)

* fix(reporting-storage): refuse a ledger upgraded to storage contract 2 with the real remedy

adcp-reporting-storage 0.2.0b1+ upgrades a 0.1.x ledger in place, widening
the configuration, obligation-period and status-checkpoint keys with
offering_id. A 0.1.x process that keeps running through a rolling deploy,
or restarts after it, used to fail in ways that hid the cause:

- create_schema() raised "Reporting caller ownership requires a
  stopped-worker maintenance migration", with a hint to run
  migrate_legacy_reporting. That migration doesn't apply here and would
  move reporting state into quarantine.
- commit_obligation raised LedgerConflictError LEDGER_WRITE_FAILED
  ("integrity rule"), and put_configuration and the status checkpoint
  write raised a raw InvalidColumnReference (42P10): their ON CONFLICT
  targets no longer match a unique index.

Refuse an upgraded ledger (reporting_configurations has offering_id)
before any DDL with ReportingSchemaAheadError, an operator-action error
that startup re-raises unwrapped. Map 42P10 on those writes to the same
remedy: obligation writes raise LedgerConflictError code
REPORTING_SCHEMA_AHEAD, and configuration and checkpoint writes raise
ReportingSchemaAheadError. The remedy is to run adcp>=10.0.0b7 with
adcp-reporting-storage>=0.2.0b1 everywhere; downgrading means restoring
the pre-upgrade backup; do not run migrate_legacy_reporting. The packaged
SQL's own check now raises the same message, for operators who apply the
DDL themselves.

Refs #1831

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

* test(public-api): record ReportingSchemaAheadError in the public API snapshot

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

* test(reporting-storage): skip the contract-2 DDL test where psycopg is absent

Import psycopg only after isolated_reporting_pool has checked it is
installed, as the native suite runs without it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Forward-ports main through v9.6.1 (8da31cc) and #1858 into next with a
true merge, as #1578 and #1671 did. Most of main was already ported to next
separately; where the same fix exists, next's version is kept:
#1702 (cb2ba54 #1701), #1704 (ab39d63 #1713), #1710 (612f97a #1708),
#1718 (b014066 #1706), #1752 (#1748), #1757/#1758 (d005f95 #1759),
#1790 (#1788), #1795 (#1794), #1798 (#1797), #1810 (#1804),
#1813, #1841 (a541585) and #1847 (#1849). New on next from main: the
reporting-storage changes #1827, #1828, #1835 and #1836, #1819 (PR title
policy), the 9.3.1-9.6.1 release files and #1858 (.traycer).

Conflicts and resolutions:
- Release identity: next's release-please-config.next.json,
  .release-please-manifest.next.json and CHANGELOG.next.md are untouched;
  main's manifest and CHANGELOG.md come along. pyproject.toml keeps next's
  10.0.0b12: since #1671 next's release PRs write that line, and 9.6.1 there
  would put adcp below the workspace storage package's adcp>=10.0.0b4 floor.
  CHANGELOG.md's Unreleased #1692 entry keeps next's text (it also describes
  the client member bound, which only next has).
- tests/test_release_configuration.py: main's separate companion-gate test,
  plus next's channel test with its storage-tag gate assertion.
- adagents (#1719 vs next's #1711, two designs for #1697): next's breaking
  SDK 10 API stays (include_domainless, publisher_domain required for
  permissive mode, CrossDomainVerification). adagents.py, src/adcp/__init__.py
  and docs/adagents-*.md are next's; main's tests for #1719's API
  (tests/test_adagents_property_resolution.py,
  tests/type_checks/adagents_property_resolution.py) are dropped; main's
  AGENTS.md paragraph is rewritten for next's API.
- server/{a2a_server,mcp_tools,serve,translate,feed_version}.py,
  signing/keygen.py, decisioning/types.py, types/{_forward_compat,base,
  canonical_creative}.py, docs/handler-authoring.md,
  docs/wholesale-feed-versions.md, tests/test_feed_version.py,
  tests/test_discovery_payload_size.py,
  tests/test_mcp_client_seller_defaults_e2e.py: next's ports. The textual
  merge had duplicated main's ports (AdCPRootModel and
  __pydantic_init_subclass__ in base.py, _check_purpose in keygen.py,
  _simplify_wire_schema in mcp_tools.py), so these files are next's
  verbatim. main's scripts/post_generate_fixes.py defer_rootmodel_builds is
  dropped: next's point_rootmodels_at_adcp_root does the same. RELEASING.md
  had main's companion paragraph twice; next's is kept.
- Generated types (src/adcp/types/domains/**): next's side, then regenerated
  from next's schema cache with the merged generator, which equals next's,
  so no generated file changes and there is no renumber churn;
  make validate-generated passes.
- tests/test_reporting_production_maintenance.py: both tests kept.

#1857 does not take effect on next. It makes storage 0.1.x refuse a ledger
whose reporting_configurations has offering_id, and every 0.2.x (storage
contract 2) ledger has it. pg.py, outbox/status_pg.py and
reporting_ledger_account_generations.sql are next's (no 42P10 mapping, no
refusal branch, 0002's DDL unchanged); ReportingSchemaAheadError,
schema_ahead_error and their export are dropped from migration.py and
__init__.py; tests/conformance/reporting/
test_reporting_storage_contract2_refusal_pg.py (a 0.1.x test) is dropped;
the regenerated public API snapshot does not list it.

Reporting storage (#1827, #1828, #1835, #1836) in next's migration
framework: no new numbered migration. Every DDL change is in
reporting_changes.sql (change_feed=True) or reporting_row_storage.sql
(row_storage), which no numbered migration includes and which
create_schema() re-applies idempotently on every start with those options,
as on main. So (a) a fresh install gets the new DDL from create_schema;
(b) a ledger at 0006 via python -m adcp migrate has no change-feed or row
tables until create_schema runs with the option, and then gets the current
DDL; (c) a create_schema boot upgrades in place (xid horizon columns,
named consumer and binding constraints, the (account_id, xid, seq) index,
the orphan collector fence table). A 0007 that included those files would
give every migrated ledger tables it never enabled and could not reach
store= boots anyway (apply_pending stops at 0004). migration.py gains
#1835's fence table in _STARTUP_ROWS.

Account horizon: 0.2.0b0-b2 prune without maintaining
pruned_through_xid, so on a ledger 0.1.0b5 wrote, a 0.1.0b5 xid cursor could
pass the stale check and skip what 0.2.x pruned. reporting_changes.sql now
raises each account horizon, once, to the deployment horizon (kept in
(xid, seq) by every release) or just below the account's oldest retained
change when lower, so those cursors fail closed and a fresh reader still
gets every retained change. It runs only when the xid columns predate this
boot and 0001 was recorded in an earlier transaction (a 0.2 release
upgraded the ledger); a comment on adcp_reporting_change_account_horizon
records that it ran. A direct 0.1.0b5 upgrade and a pre-(xid, seq) install
are unchanged. Covered by
tests/conformance/reporting/test_reporting_change_feed_horizon_upgrade_pg.py,
documented in docs/reporting-ledger-migration.md and the storage README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bokelley
bokelley requested a review from rachitm022 as a code owner November 8, 2026 01:13

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ladon verdict: Approve

Approve — clean forward-port merge of main (9.3.1–9.6.1) into next.

No blocking or medium findings. The bulk of the diff is reporting-storage code already reviewed and shipped on main (orphan collector/fence, row-binding bounds, account change-feed (xid,seq) ordering); conflict resolutions are consistent with no duplication. The one genuinely new-on-next behavior — the one-time account-horizon reconcile in reporting_changes.sql — is fail-closed, idempotent, bounded to run once, correctly caps the raised horizon below the oldest retained change, and is covered by test_reporting_change_feed_horizon_upgrade_pg.py. Public API additions are additive, within the storage package namespace, and snapshotted; AGENTS.md is updated for next's adagents helpers. This is a chore merge into the pre-release next beta line with no stable-contract break.

Decision table: no critical/high findings (row 1 n/a); gated_paths false (row 2 n/a); high_risk false, no deletions/modifications triggers (rows 3–5 n/a); no prior decision (row 6 n/a); no no-auto-approve team match (row 7 n/a); zero medium findings (row 8 n/a). Falls through to row 9 → approve.

@bokelley
bokelley merged commit 902ea7a into next Nov 8, 2026
41 checks passed
@bokelley
bokelley deleted the claude/issue-1856-merge-main-into-next branch November 8, 2026 02:07
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.

1 participant