Skip to content

feat(reporting): serve reporting subscriptions from ReliableReportingService - #1725

Merged
bokelley merged 4 commits into
nextfrom
claude/issue-1695-subscription-service
Oct 31, 2026
Merged

bokelley merged 4 commits into
nextfrom
claude/issue-1695-subscription-service

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Part of #1695, the second of the remaining stacked PRs: service wiring. It stacks on #1723, so it contains that PR's commits. Review only feat(reporting): serve reporting subscriptions from ReliableReportingService. It follows the design of adcontextprotocol/adcp-client#3518.

What ships

ReliableReportingService(reporting_subscriptions=ReportingSubscriptionServiceOptions(engine=..., list_principals=..., resolve_principal=..., definitions=...)). Without the option, capabilities, responses and worker turns are unchanged; there is a test for this.

Planning in each worker turn

  • Each turn first plans every listed principal's frozen owed set (SubscriptionObligationPlanner), then executes it with the account configurations. New obligations therefore run in the same turn.
  • Trusted path: each (version, offering) generation goes through configure()'s path. account_context receives the generation with configuration.subscription set and returns the source route, currency and advertised offering. The service validates them exactly as for an account configuration: offering contract, required finality and the registered public offering. The only owed-set exception is the account-timezone equality check, because an owed set never derives periods from its schedule; each obligation carries its own period and source timezone.
  • Horizon: obligations are written only for periods ending by min(engine through, turn clock). Each source slice carries exactly that period's frozen media buys (tested, including a period with two buys).
  • Isolation: one subscription's failure, or one principal's listing failure, never stops another subscription or an account configuration. It is recorded in turn.subscription_errors and reported to planning_error_handler, or to the worker error handler if none is set.
  • Startup: stored subscription generations are recovered with the others.

Capabilities, at the served release

  • The service adds:
    • configuration_tasks and the deprecated configuration_task;
    • the subscriptions block;
    • standard_dimensions on every offering;
    • the media_buy.reporting_subscriptions feature;
    • the principal section.
  • Callers served below 3.3 see the pre-3.3 block. The adcp#8585 offering fields reach only 3.3.0-beta.2 callers.
  • The decisioning handler now passes resolved_adcp_version to inject_capabilities.
  • validate() refuses:
    • an advertised offering without a subscription dimension mapping (adcp#8585 requires standard_dimensions on every offering);
    • a managed or receipt-reconciled offering;
    • an engine whose status_retention_days differs from the service's.

Status

  • get_reporting_status summary and periods views gain subscription_coverage[] for callers served 3.3 or later.
  • Ledger reads and sync_reporting_status stay scoped to the caller's consumer_id, and a subscription generation's consumer_id is its principal's subject, so visibility is per principal:
    • two principals with the same subscription_id each see only their own periods;
    • a principal without a subscription sees nothing.
  • resolve_principal must agree with the caller resolver; otherwise the read is refused.

Deviations from the TypeScript service

  • Planning failures are recorded on the turn, not raised. TS rejects runCycle with an AggregateError when there is no onPlanningError. The Python service already isolates configuration and extension errors on ReliableReportingTurn and reports them to worker_error_handler, so subscription failures follow that convention.
  • No separate resolveSubscriptionConfiguration hook. Python's account_context resolver already receives the ReportingConfiguration, so it returns the subscription generation's source settings through the existing trusted hook. definitions supplies each offering's content-addressed report definition, which includes monetary units that are not on the wire offering.
  • The engine is not reconciled by the service. As in TS, the host keeps it reconciled (principal.changed, authorization changes).

Not yet served

  • production options (refused at construction).
  • Managed or receipt-reconciled subscription offerings: destinations, render_path_template, per-account destinations. These are the managed-delivery PR.
  • PostgreSQL owed sets: the storage PR. The engine-state PostgreSQL store follows feat(reporting): persist reporting-subscription engine state through a CAS store #1722 separately.
  • Receipts and statuses after a loss of authority.
  • The conformance scenario.

This does not adopt beta.2.

Tests

tests/test_reporting_subscription_service.py covers:

  • end to end: two principals, one account, the per-slice media buys, visibility, coverage, idempotency;
  • the horizon cap;
  • failure isolation alongside an account configuration;
  • capabilities at 3.2, 3.3.0-beta.1 and 3.3.0-beta.2;
  • coverage gated to 3.3;
  • the principal agreement check;
  • construction refusals;
  • option off.

All reporting suites pass locally. Locally, the wheel-building packaging tests could not run because the sandbox disk is full; CI runs them.

🤖 Generated with Claude Code

bokelley and others added 4 commits October 30, 2026 03:20
…ng ledger

A principal's reporting subscription is now executable by the ledger. The
engine's frozen owed sets become ordinary obligations of one configuration
generation per account, owning principal, subscription_id, version and
resolved offering_id, aligned with the TypeScript SDK.

- ReportingConfigurationGenerationKey gains an optional offering_id, set
  only for subscription generations. ledger_asdict omits it when unset, so
  every digest, work key and persisted scope of an account generation is
  byte-identical to before; a guard test forbids bare asdict in reporting.
- SubscriptionObligationPlanner writes sparse, immutable obligations for
  periods ending by the engine's horizon, then advances the owed set's
  completeness horizon. Stores opt in through ReportingSubscriptionLedger;
  the in-memory store implements it, and the planner refuses other stores.
- The producer never grid-fills an owed set and reads an owed obligation
  only once its period has ended. Owed sets are not leased; the service
  runs them through run_configuration.
- get_reporting_status reports offering_id per period, keeps one wire
  generation per id, version and purpose, and leaves scope_closed false
  while the scope ends after the owed set's horizon.
- sync_reporting_status resolves a statement through the caller's owed
  obligation for its period and report definition; a period the owed set
  does not hold fails with INVALID_STATUS_PERIOD.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…pshot

The closed-payload allowlist in reporting_reconciliation_validate now names
offering_id, so the required-schema manifest carries the function's new
fingerprint, computed from a clean migration chain. The public API snapshot
records ReportingSubscriptionGeneration and ReportingSubscriptionLedger.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The engine now persists its state through a compare-and-swap store, so
frozen_owed_sets reads one snapshot of the principal's state, as
owed_periods and subscription_coverage do, instead of the removed
process-local lock.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…Service

ReliableReportingService(reporting_subscriptions=...) now owes and serves a
principal's reporting subscriptions. Without the option, capabilities,
responses and worker turns are unchanged.

- Each worker turn plans every listed principal's frozen owed set, then
  executes it with the account configurations. A subscription generation is
  installed through the same trusted configure path: account_context
  receives it with configuration.subscription set, and the service validates
  the source route, currency and advertised offering as for an account
  configuration. The horizon is the earlier of the engine's and the turn
  clock.
- A subscription that cannot be planned is isolated, recorded in
  turn.subscription_errors and reported to planning_error_handler or the
  worker error handler; it never stops another subscription or an account
  configuration.
- Capabilities gain configuration_tasks, the subscriptions block,
  standard_dimensions, the feature id and the principal section at the
  served release; callers below 3.3 see the pre-3.3 block, and adcp#8585
  offering fields reach only 3.3.0-beta.2 callers.
- get_reporting_status summary and periods views gain
  subscription_coverage. Ledger reads stay scoped to the caller, and
  resolve_principal is checked against the caller resolver.
- The planner takes an install hook and resolves currency per generation;
  the engine exposes its offerings and status_retention_days.

Production options, managed or receipt-reconciled subscription offerings
and PostgreSQL owed sets are refused or not yet served.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@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, additive, heavily tested reporting-ledger / subscriptions change.

What I checked:

  • No critical/high findings (row 1 N/A). gated_paths is false (row 2 N/A). No deletions in high-risk reasons (row 3 N/A) — the only high-risk match is src/adcp/decisioning/handler.py (modified), but the reviewer surfaced no medium-or-higher finding on it, so the modification is presumed safe (row 5 N/A). No findings at all, so rows 4/8 N/A. No prior decision (row 6 N/A). No no-auto-approve team match (row 7 N/A). Falls through to row 9 → approve.
  • Byte-stability: ledger_asdict special-cases the 5-field generation key dropping offering_id None so pre-existing digests/scopes stay byte-identical; reconciliation allowlist+fingerprint repinned and guard test forbids bare asdict. New ReportingObligationRecord.offering_id destabilizes no persisted digest.
  • Isolation fail-closes when resolve_principal disagrees; pg refuses subscription lookups so the planner rejects it.
  • Semver: new names additive; keyword-only adcp_version added; snapshot updated; feat: is correct.

Non-blocking note (not a finding): planning adds host I/O at turn start with no per-call timeout and stopping() is checked only between items, so a hung adopter hook could block the turn. This mirrors the existing cooperative config path and the reviewer did not raise it to Medium — worth a follow-up but not a gate.

The high_risk flag is a heuristic LOOK signal only; the modified decisioning file carries no actionable concern. Approving per row 9.

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