Repository navigation
fix(decisioning): project ADCPTaskError from platform methods to its own wire error - #1632
Conversation
…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>
40606d0 to
042c21a
Compare
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean, correctly-scoped fix (#1622) with no blocking or Medium findings.
What I checked:
_invoke_platform_methodand the handoff_runfn now catchADCPTaskErrorahead of the genericINTERNAL_ERRORwrap and project it via the same_extract_structured_fieldsthatserve()uses — no duplicated translation.- Catch ordering is correct: decisioning
AdcpErroris a distinct class fromadcp.exceptions.ADCPTaskError(no shadowing), andADCPTaskErrorprecedesTypeError/Exception. IdempotencyConflictError.error_codesdefaults to["IDEMPOTENCY_CONFLICT"]→_error_code_for_exceptionyields the right code →_recovery_for_code→correctable.on_failurereceives the projected error, matching theAdcpError/TypeErrorpaths.- Scrubbed message + sanitized/empty
detailskeepcaused_byoff 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.
There was a problem hiding this comment.
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.
Fixes #1622
What changed
_invoke_platform_method(src/adcp/decisioning/dispatch.py) now catchesADCPTaskErrorbefore the genericexcept Exceptionbranch. It projects the error to anAdcpErrorthat keeps the original code, message, recovery, field, suggestion and sanitized details. It does this withadcp.server.translate._extract_structured_fields, the same field extraction theserve()path uses (build_mcp_error_resultand the A2A error builder). The translation is not duplicated.So
IdempotencyConflictError, raised by@store.wrapon a decisioning platform method, now reaches the buyer asIDEMPOTENCY_CONFLICT/correctableinstead ofINTERNAL_ERROR/ terminal. OtherADCPTaskErrorsubclasses (IdempotencyExpiredError, account errors) also keep their own codes.on_failurehooks receive the projected error. Handoff (TaskHandoff) background functions get the same projection, so a task fails with the real code rather thanINTERNAL_ERROR.Adopters such as ESA can delete their
translate_idempotency_conflictwrapper.Tests
tests/test_decisioning_idempotency_conflict.py: a decisioningcreate_media_buywrapped by@store.wrap, called twice with the same key and different payloads over MCP and over A2A. The second call returnsIDEMPOTENCY_CONFLICT/correctableand the handler runs once. This test fails onmainwithINTERNAL_ERROR.tests/test_decisioning_dispatch.py: unit coverage for the sync seam, includingon_failure, and for the handoff path.make lint,make typecheck-allandmake testpass 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 cleanorigin/main.🤖 Generated with Claude Code