Skip to content

feat(media-buy): add atomic total budget updates - #6271

Open
bokelley wants to merge 6 commits into
mainfrom
codex/atomic-total-budget
Open

feat(media-buy): add atomic total budget updates#6271
bokelley wants to merge 6 commits into
mainfrom
codex/atomic-total-budget

Conversation

@bokelley

@bokelley bokelley commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Closes #4241.

Summary

  • makes total_budget mutually exclusive with packages and new_packages
  • defines deterministic, atomic proportional redistribution for fixed-allocation buys while preserving shared-pool semantics for seller-optimized buys
  • returns full post-update package state for every redistributed package
  • implements the behavior in the training agent with positive and negative tests
  • adds the required total_budget_redistribution compliance scenario

Validation

  • focused training-agent tests: 2 passed
  • TypeScript compile: passed
  • compliance build and storyboard lints: passed
  • JSON schema parse and git diff --check: passed

Review note

#6088 already introduced the optional field and seller-optimized shared-pool behavior. This PR supplies the remaining accepted #4241 contract without treating seller-optimized package caps as fixed allocations.

@bokelley bokelley changed the title [codex] feat(media-buy): add atomic total budget updates feat(media-buy): add atomic total budget updates Aug 8, 2026
@bokelley
bokelley marked this pull request as ready for review August 8, 2026 04:42
@@ -0,0 +1,5 @@
---
"adcontextprotocol": minor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

MUST FIX: minor under-classifies a breaking wire change. Pre-PR, fixed-mode update_media_buy accepted total_budget alongside packages/new_packages when the amount equaled the resulting sum (schema: "In fixed mode the amount MUST equal the sum of package budgets after applying this update"). This PR rejects that exact combination with VALIDATION_ERROR (server handleUpdateMediaBuy, schema not.allOf), and redefines fixed-mode total_budget from a checked assertion into a proportional-redistribution trigger. A previously-valid request shape now fails, and the field's semantics change — Breaking-class per the constitution (semantic/default change), not Normative. The redistribution itself is additive; the mutual-exclusivity rejection is the break. Ship major and route for human ratification, or preserve the additive path (accept a consistent total_budget+packages request).

Comment thread server/src/training-agent/task-handlers.ts Outdated

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ladon verdict: Request changes

Request changes — 1 blocking finding.

Blocking findings

  • .changeset/add-atomic-total-budget-update.md:2 — Minor changeset under-classifies a breaking wire change. This PR now rejects a previously-valid total_budget+packages combination and redefines fixed-mode total_budget semantics — a Breaking-class change to the published protocol surface (static/schemas/source/**). Per repo policy, a breaking wire change requires a major changeset; a minor changeset shipping it is high.

Medium findings

  • server/src/training-agent/task-handlers.ts:1808 — Docs assert a per-package zero-budget rejection that the reference implementation does not enforce (schema/docs↔impl coherence gap).

Note: This change is also Breaking-class and touches gated paths (static/schemas/source/**) with review_decision: REVIEW_REQUIRED, so even absent the changeset blocker it would require human/CODEOWNERS ratification and cannot auto-approve. Row 1 (high finding) governs the outcome here. Resolve the changeset classification (bump to major) and align the reference impl with the documented zero-budget rejection, then re-request review.

Blocking findings

  • .changeset/add-atomic-total-budget-update.md:2 — Minor changeset under-classifies a breaking wire change; breaking wire changes require a major changeset

Medium findings

  • server/src/training-agent/task-handlers.ts:1808 — Docs assert a zero-budget rejection the reference impl does not enforce

@bokelley

bokelley commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

WG decision memo — wire compatibility

The implementation bug is closed in bfd98cc: an active zero-budget package now makes proportional redistribution fail atomically. One decision remains.

Option A — preserve the 3.2 minor contract (recommended). Keep the existing combined-request assertion path: when total_budget appears with packages or new_packages, apply the explicit package mutations and require total_budget.amount to equal the resulting fixed-mode package sum; reject only an inconsistent total. A request containing total_budget alone invokes the new proportional redistribution. This preserves every previously valid request and makes redistribution additive.

Option B — ratify a breaking contract. Keep this PR’s mutual exclusion and redefine total_budget solely as a redistribution trigger. That requires an explicit Breaking-class vote and a major changeset; it should not ship under the current minor changeset.

Requested outcome: choose A or B. I recommend A for 3.2; it delivers the atomic shortcut without invalidating the assertion form already published.

@bokelley

bokelley commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Decision: Option A. The binding constraint is the changeset class. This PR carries minor, and the mutual-exclusion rule converts a previously valid request shape into VALIDATION_ERROR. #6088 already published the assertion path — total_budget + packages where the total must equal the resulting package sum — so removing it is a semantic breaking change and cannot ship under minor. Option B explicitly requires a Breaking-class vote and major; it should not land here.

Resulting wire semantics under Option A:

total_budget alone → proportional redistribution (this PR's new behavior):

{
  "media_buy_id": "mb_001",
  "total_budget": { "amount": 50000, "currency": "USD" }
}

total_budget + packages → assertion path (preserved from #6088):

{
  "media_buy_id": "mb_001",
  "total_budget": { "amount": 50000, "currency": "USD" },
  "packages": [
    { "package_id": "pkg_001", "budget": 30000 },
    { "package_id": "pkg_002", "budget": 20000 }
  ]
}

Seller rejects only when total_budget.amount ≠ resulting package sum.

Changes needed to implement Option A in this PR:

  1. Remove the not: { allOf: [...] } block from update-media-buy-request.json
  2. Remove the mutual-exclusion early return in task-handlers.ts (~line 7052)
  3. Gate fixedRedistribution on req.packages === undefined && req.new_packages === undefined; the existing projectedPackageBudgetTotal assertion handles the combined case unchanged
  4. Update the total_budget field description in schema and docs to document both code paths
  5. Convert the "rejects total_budget combined with packages" test and the reject_ambiguous_update compliance scenario step to test assertion mismatch (total ≠ sum) rather than mutual exclusion

Generated by Claude Code

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ladon verdict: Escalate to human review

Escalate — gated-path schema change without required approval.

Escalation reasons

  • This PR modifies published protocol source under static/schemas/source/** (update-media-buy-request.json, update-media-buy-response.json), which is under a hard, deterministic approval gate (gated_paths: true). review_decision is CHANGES_REQUESTED, not APPROVED, so the gate is not satisfied. Human/CODEOWNERS approval is required before merge.

Review context

  • No current blocking (critical/high) or medium findings were surfaced this pass. The delta since the prior review is a single guard change (pkg.budget < 0<= 0) in task-handlers.ts plus an added test, which resolves the prior medium (docs asserted a zero-budget rejection the impl did not enforce). The rest of the setup delta is merge noise.
  • The prior high finding (changeset under-classifying a breaking wire change) was on .changeset/add-atomic-total-budget-update.md, which was not modified this pass and is not re-raised by the reviewer.

Because the diff is clean, this is not request-changes; but row 2 (gated_paths true AND review_decision ≠ APPROVED) forces escalate. The gate lifts once a human/CODEOWNERS approval lands.

Why human review

  • Modifies gated protocol source static/schemas/source/media-buy/update-media-buy-request.json and update-media-buy-response.json — hard approval gate (gated_paths: true) with review_decision=CHANGES_REQUESTED, not APPROVED. Human/CODEOWNERS approval required before merge.
  • This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/media-buy/update-media-buy-request.json (modified) matches static/schemas/source/**; static/schemas/source/media-buy/update-media-buy-response.json (modified) matches static/schemas/source/**) and the current GitHub review decision is 'CHANGES_REQUESTED', not APPROVED. This is a hard gate enforced in code — Ladon cannot auto-approve until a human/CODEOWNERS approval is recorded, regardless of how clean the diff is.

@aao-secretariat aao-secretariat Bot added the ladon/needs-human-review Ladon has escalated this PR for human review. label Aug 8, 2026
…_budget + packages

Per the decision in #6271 comments: total_budget alone invokes proportional
redistribution; total_budget + packages/new_packages invokes the #6088
assertion path (total must equal resulting package sum).

- Remove not/allOf mutual-exclusion block from update-media-buy-request.json
- Remove mutual-exclusion early return in task-handlers.ts
- Gate fixedRedistribution on req.packages === undefined && req.new_packages === undefined
- Update total_budget field description in schema and docs to document both paths
- Convert rejection test and compliance scenario to test assertion mismatch
  (total ≠ sum) rather than mutual exclusion
@bokelley

bokelley commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Option A applied in ef4f688. Five changes from the decision comment:

  1. Removed the not: { allOf: [...] } mutual-exclusion block from update-media-buy-request.jsontotal_budget + packages is now schema-valid.
  2. Removed the mutual-exclusion early return in task-handlers.ts — combined requests now reach the assertion check.
  3. Gated fixedRedistribution on req.packages === undefined && req.new_packages === undefined — redistribution path only fires when no explicit package patches are present.
  4. Updated total_budget description in schema and docs to document both paths (redistribution when alone; assertion when combined with packages).
  5. Updated the reject_total_with_packages test and the reject_ambiguous_update compliance step to test assertion mismatch (total_budget: 40000 / packages[0].budget: 99999) rather than mutual exclusion.

Generated by Claude Code

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ladon verdict: Escalate to human review

Escalate — gated protocol source modified with review_decision=CHANGES_REQUESTED (not APPROVED).

This PR modifies gated protocol schema source under static/schemas/source/**:

  • static/schemas/source/media-buy/update-media-buy-request.json
  • static/schemas/source/media-buy/update-media-buy-response.json

gated_paths: true and review_decision is CHANGES_REQUESTED, not APPROVED, so the hard approval gate (decision-table row 2) is not satisfied. Human/CODEOWNERS approval must be recorded before this can merge, regardless of how clean the diff is.

Blocking findings: none — the reviewer found no critical/high/medium issues.

Review notes: The delta restores a coherent two-path model for total_budget (proportional redistribution when alone in fixed mode; assertion/VALIDATION_ERROR otherwise). Handler guards partition cleanly, and the schema description, docs prose, and compliance scenario are mutually consistent. Changeset is a correctly-scoped minor for an additive capability. This is a net-clean pass with no new findings.

Escalation reason: Modifies gated protocol source update-media-buy-request.json and update-media-buy-response.json (hard approval gate) while the GitHub review decision is CHANGES_REQUESTED. Ladon cannot auto-approve until a human/CODEOWNERS approval is recorded. This is consistent with the prior escalate decision; the gate remains unsatisfied.

Why human review

  • Modifies gated protocol source static/schemas/source/media-buy/update-media-buy-request.json and update-media-buy-response.json — hard approval gate (gated_paths: true) with review_decision=CHANGES_REQUESTED, not APPROVED. Human/CODEOWNERS approval required before merge.
  • This PR touches a path under a hard, non-overridable approval gate (static/schemas/source/media-buy/update-media-buy-request.json (modified) matches static/schemas/source/**; static/schemas/source/media-buy/update-media-buy-response.json (modified) matches static/schemas/source/**) and the current GitHub review decision is 'CHANGES_REQUESTED', not APPROVED. This is a hard gate enforced in code — Ladon cannot auto-approve until a human/CODEOWNERS approval is recorded, regardless of how clean the diff is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ladon/needs-human-review Ladon has escalated this PR for human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RFC: media-buy-level total budget on UpdateMediaBuyRequest

2 participants