Skip to content

fix(decisioning): project ADCPTaskError from platform methods to its own wire error - #1632

Merged
bokelley merged 1 commit into
mainfrom
claude/issue-1622-dispatch-task-errors
Oct 26, 2026
Merged

bokelley merged 1 commit into
mainfrom
claude/issue-1622-dispatch-task-errors

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Fixes #1622

What changed

_invoke_platform_method (src/adcp/decisioning/dispatch.py) now catches ADCPTaskError before the generic except Exception branch. It projects the error to an AdcpError that keeps the original code, message, recovery, field, suggestion and sanitized details. It does this with adcp.server.translate._extract_structured_fields, the same field extraction the serve() path uses (build_mcp_error_result and the A2A error builder). The translation is not duplicated.

So IdempotencyConflictError, raised by @store.wrap on a decisioning platform method, now reaches the buyer as IDEMPOTENCY_CONFLICT / correctable instead of INTERNAL_ERROR / terminal. Other ADCPTaskError subclasses (IdempotencyExpiredError, account errors) also keep their own codes. on_failure hooks receive the projected error. Handoff (TaskHandoff) background functions get the same projection, so a task fails with the real code rather than INTERNAL_ERROR.

Adopters such as ESA can delete their translate_idempotency_conflict wrapper.

Tests

  • tests/test_decisioning_idempotency_conflict.py: a decisioning create_media_buy wrapped by @store.wrap, called twice with the same key and different payloads over MCP and over A2A. The second call returns IDEMPOTENCY_CONFLICT / correctable and the handler runs once. This test fails on main with INTERNAL_ERROR.
  • tests/test_decisioning_dispatch.py: unit coverage for the sync seam, including on_failure, and for the handoff path.
  • make lint, make typecheck-all and make test pass locally except one pre-existing failure, tests/integration/test_a2a_context_id.py::test_two_calls_share_server_assigned_context_id. It fails only inside the full serial suite, and it fails the same way on clean origin/main.

🤖 Generated with Claude Code

…own wire error

_invoke_platform_method caught only AdcpError and pydantic ValidationError,
so IdempotencyConflictError raised by IdempotencyStore.wrap reached the buyer
as INTERNAL_ERROR / terminal. ADCPTaskError subclasses now keep their code and
recovery through the same field extraction serve() uses, on the sync path and
in handoff tasks.

Fixes #1622

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bokelley
bokelley force-pushed the claude/issue-1622-dispatch-task-errors branch from 40606d0 to 042c21a Compare October 26, 2026 01:58

@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, correctly-scoped fix (#1622) with no blocking or Medium findings.

What I checked:

  • _invoke_platform_method and the handoff _run fn now catch ADCPTaskError ahead of the generic INTERNAL_ERROR wrap and project it via the same _extract_structured_fields that serve() uses — no duplicated translation.
  • Catch ordering is correct: decisioning AdcpError is a distinct class from adcp.exceptions.ADCPTaskError (no shadowing), and ADCPTaskError precedes TypeError/Exception.
  • IdempotencyConflictError.error_codes defaults to ["IDEMPOTENCY_CONFLICT"] → _error_code_for_exception yields the right code → _recovery_for_code → correctable.
  • on_failure receives the projected error, matching the AdcpError/TypeError paths.
  • Scrubbed message + sanitized/empty details keep caused_by off the wire — no credential/context leakage concern.
  • Tests cover the sync seam (incl. on_failure), the handoff path, and a real MCP+A2A round-trip.

High-risk flag: true only because src/adcp/decisioning/dispatch.py is (modified) under src/adcp/decisioning/**. Per the decision rules a modification with no medium-or-higher finding is presumed safe — does not escalate on the flag alone. gated_paths is false, so row 2 does not apply despite review_decision: REVIEW_REQUIRED.

Non-blocking follow-up (Low, not in this diff): the async handoff on_complete hook block (dispatch.py:2337-2355) still collapses ADCPTaskError to INTERNAL_ERROR — same class of bug, one of three async sites. Low likelihood (completion hooks are framework-internal); worth a glance if commit/consume hooks can surface account errors.

No rows 1–8 fired; falls through to row 9.

@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 pass with no blocking findings. This fix(decisioning): change projects ADCPTaskError (e.g. IdempotencyConflictError from IdempotencyStore.wrap) to its own wire code/recovery instead of collapsing to INTERNAL_ERROR, on both the sync _invoke_platform_method seam and the handoff path. ADCPTaskError and the decisioning AdcpError are distinct hierarchies, so the new except-clause ordering is sound; _task_error_to_adcp_error reuses serve()'s _extract_structured_fields, avoiding duplicated translation. Commit type is correct — a wire-behavior fix, not a public-API break — so no semver escalation applies. Tests cover the sync seam, handoff, and a full MCP+A2A round-trip.

Decision trace: no critical/high/medium findings (rows 1, 4–8 N/A). gated_paths is false (row 2 N/A). high_risk is true via src/adcp/decisioning/dispatch.py (modified), but with no medium-or-higher finding on that modified file, rows 3 and 5 do not fire — the modification is presumed safe per the high-risk guidance. No no-auto-approve team match (row 7 N/A). Prior decision was approve, so sticky escalation (row 6) N/A. Falls through to row 9 → approve.

@bokelley
bokelley merged commit 7b95b26 into main Oct 26, 2026
47 checks passed
@bokelley
bokelley deleted the claude/issue-1622-dispatch-task-errors branch October 26, 2026 03:12
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.

The decisioning dispatcher turns IdempotencyConflictError into INTERNAL_ERROR

1 participant