Repository navigation
chore: merge main into next through 9.6.1 - #1863
Conversation
…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
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>
There was a problem hiding this comment.
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.
Forward-ports
mainthrough SDK 9.6.1 intonextwith 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:
.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 chore: merge main into next through 9.3.0 #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 10.0.0b1: an SDK seller's default tools/list exceeds the SDK client's 100,000-member bound, so call_tool fails #1692 entry keeps next's text (it also describes
the client member bound, which only next has).
plus next's channel test with its storage-tag gate assertion.
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 feat(adagents): attribute and verify publisher property claims #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.
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.
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.
#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
pyproject.tomlkeepsnext's10.0.0b12. At chore: merge main into next through 9.3.0 #1671nextstill tracked main's version line. Nownext's release PRs write it, and taking9.6.1would putadcpbelow the storage package'sadcp>=10.0.0b4floor.next: there, every ledger hasoffering_id.reporting_changes.sql/reporting_row_storage.sql, which no migration includes;create_schema()applies them on every start when those options are on.reporting_changes.sql, for a 0.1.0b5 → 0.2.0b0–b2 → this release path. It is covered bytest_reporting_change_feed_horizon_upgrade_pg.py.test_adagents_property_resolution.py(and its type-check fixture) target main's Attribute adagents.json properties to a publisher domain, gate permissive resolution, and verify cross-domain claims #1697 API;nexthas its own design (feat(adagents)!: attribute properties to a publisher domain, gate permissive resolution, verify cross-domain claims #1711) and tests (test_adagents_publisher_domain.py).test_reporting_storage_contract2_refusal_pg.pyis 0.1.x-only.Unblocks #1856.
🤖 Generated with Claude Code