Repository navigation
feat!: per-account reconcile report and fleet reconcile for reporting subscriptions - #1802
Conversation
… subscriptions ReconcileReport.accounts now holds one ReconcileAccount per account, the shape the TS SDK reports: account_id, subscription_ids, a status of reconciled, unauthorized or failed, unauthorized_at and withheld_period_count for a lost account, and an error naming the task, code and message of the read that failed. A principal or roster failure is the entry "*". report.account_ids is the old tuple of ids. The report also carries agent_id and since. ADCPMultiAgentClient.reporting.reconcile fans reconcile out across sellers with bounded concurrency and returns a FleetReconcileReport: by_seller, issues, definitive and the summed summary, matching TS multiClient.reporting.reconcile. A seller whose principal could not be read is a fleet issue with its agent_id. BREAKING CHANGE: ReconcileReport.accounts is a tuple of ReconcileAccount instead of account-id strings, and to_dict serves objects under accounts. Read report.account_ids for the ids. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cile Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Ladon verdict: Approve
Approve.
Clean PR with no Critical/High/Medium findings. The breaking ReconcileReport.accounts type change correctly carries the semver signal (feat! with BREAKING CHANGE footer and a report.account_ids migration note), averting the minor-cut risk where release-please would otherwise ship a break under a non-breaking prefix. Consumers of .accounts (events.py:247,605) both switched to the new account_ids property — a load-bearing switch, since the "*" roster/principal entry now lives in accounts and would otherwise break _merge_partial's "everything" path. The reconcile.py entry-append gate (L480) and _account_entry precedence (L505-521) check out, with a test added in test_reporting_subscription_reconcile.py.
One non-blocking follow-up noted by the reviewer: reconcile_fleet (reconcile.py:574) fans out via asyncio.gather + semaphore with no per-seller wait_for timeout — a hung seller holds a concurrency slot. This inherits single-reconcile timeout behavior so it is not a regression; a bounded per-seller timeout would be a nice improvement but is not required here.
Decision table: no critical/high findings (row 1 no), gated_paths false (row 2 no), high_risk false (rows 3/5 no), no medium findings (rows 4/8 no), no prior escalation (row 6 no), no team gate (row 7 no) → row 9 approve.
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean subsequent review, no findings.
This PR adds reporting subscription reconciliation support (facade, events, reconcile logic, docs, tests). The delta since the prior approve is a single guard at reconcile.py:980: with subscription_id= set, an account covered only by other subscriptions is no longer listed in report.accounts, plus a test asserting accounts == () and periods == () for a non-matching subscription.
Verification notes from the reviewer, confirmed:
- Lines for an account are only produced after
subscription_ids.append(sub_id), so no account with periods is dropped. - Unauthorized accounts covered by the requested subscription clear the loop via
accounts_unauthorizedand keep their sub_id. - The
subscription_id is Nonepath remains unconditional, matching prior behavior.
Gate posture: gated_paths: false, review_decision: APPROVED, high_risk: false, no no-auto-approve team match. No critical/high/medium findings. None of decision-table rows 1–8 fire, so the outcome falls through to row 9 — approve.
Summary
TS parity for the buyer
reconcile()report (decisions 1 and 2 of the reporting-subscriptions DX follow-ups).Per-account report.
ReconcileReport.accountsnow holds oneReconcileAccountper account, the shape of TSReconcileAccount(adcp-client#3576, #3581):failedrecords the first read that failed for the account:get_media_buys,get_reporting_status, orcoverage(aCoverageInputError, with its code). Paging that does not finish isLIMIT_EXCEEDED.account_id="*", as in TS.to_dict()serves the objects underaccounts. The report also carriesagent_idandsince, as TS does.report.account_idsis the tuple of idsaccountsused to be. It leaves out"*". The events module now reads it.Fleet fan-out.
ADCPMultiAgentClient.reportingis aFleetReportingClient:multi.agent(id).reporting, so the per-seller cache of last-read facts carries across runs.issues, as in TS's fleet report after #3581.SUBSCRIPTION_PRINCIPAL_UNAVAILABLEfleet issue with itsagent_id. It is left out ofby_sellerand the summary. In TS, that read throws inside the fleet. Python's single-sellerreconcile()turns it into a report issue instead, so the fleet lifts it.agent_idsentry is refused before dispatch (ReportingSubscriptionError,INVALID_REQUEST,dispatched=False). TS filters it out silently. So isconcurrency < 1, as in TS.Outcome names (decision 3). No change needed: Python's
accounts.changeoutcomes already usekind="separate_account"andkind="matched_existing"(AccountSeparate,AccountMatched), the names TS #3587 adopted.Docs: the buyer guide gains the account statuses and an Across sellers section. The errors guide covers failed entries and fleet issues, and the README shows the fleet call.
Test plan
tests/test_reporting_subscription_reconcile.py:to_dict;"*"on roster and principal failures;failedwithget_reporting_status;unauthorizedwithunauthorized_at,withheld_period_countandsubscription_ids;agent_ids, refusals, andmulti.reportingdelegating to each agent'sreporting.ruff check,ruff format --check,mypyon the changed modules.BREAKING CHANGE:
ReconcileReport.accountsis a tuple ofReconcileAccountinstead of account-id strings, andto_dict()serves objects underaccounts. Readreport.account_idsfor the ids.🤖 Generated with Claude Code