Skip to content

update_media_buy dispatched via projectSync, not routeIfHandoff, async task not framework-scoped (unpollable via tasks/get) #5974

Description

@tej656

Summary

In createAdcpServerFromPlatform, the updateMediaBuy handler is wrapped in projectSync only, whereas getProducts, createMediaBuy, syncCreatives, and getSignals are wrapped in routeIfHandoff. Because of this, an adopter's update_media_buy handler can't return a ctx.handoffToTask(...) marker to go async. The framework never creates or owns the task, so it isn't registered with a framework-computed owner_scope, which means it can't be polled via tasks/get / get_task_status.

This is inconsistent with the spec. update_media_buy is a valid async task type (it's in enums/task-type.json and in SPEC_WEBHOOK_TASK_TYPES in runtime/protocol-for-tool.mjs), so async is spec-legal for it, but the runtime doesn't wire it up.

Version

@adcp/sdk@12.0.4 (spec 3.1.2). Present throughout the 12.x line.

Where (dist paths / symbols)

dist/lib/server/decisioning/runtime/from-platform.mjs:

  • routeIfHandoff is used by getProducts (~1858, 1936), createMediaBuy (~2007), syncCreatives (~2095, 2295), and getSignals (~2464).
  • The updateMediaBuy handler (~2033–2081) returns projectSync(...) (~2054) with no routeIfHandoff branch.
  • routeIfHandoff (defined ~1224) gates on isTaskHandoff(result); projectSync (~1173) has no handoff detection at all.

Impact

Adopters whose update_media_buy is genuinely slow (for example, an ad-server activation sync that runs past the buyer's transport timeout) have no framework path to return a Submitted task. Working around it means manually writing to the taskRegistry and hand-computing owner_scope to match what tasks/get re-derives (taskOwnerScopeForsession:${ctx.sessionKey} when a session key is present). If the manual owner_scope doesn't match exactly, taskBelongsToCaller fails and every poll returns REFERENCE_NOT_FOUND even though the task completed. It's a silent, easy-to-hit footgun, and it forces adopters to duplicate internal SDK logic they shouldn't need to know about.

Steps to reproduce

  1. Adopter update_media_buy handler returns ctx.handoffToTask(fn).
  2. The marker is ignored or mis-validated (it goes through projectSync, not routeIfHandoff).
  3. Work around it with a manual taskRegistry.create({...}) that omits ownerScope, so the row falls back to account:<id>.
  4. Poll tasks/get with the returned task_id and get REFERENCE_NOT_FOUND, because the poll expects session:<id>.

Expected

updateMediaBuy should be wrapped in routeIfHandoff like the other async-capable tools, so that a handler returning ctx.handoffToTask(...) is framework-owned. Task creation, owner_scope (taskOwnerScopeFor), polling, and webhook emission would all be handled by the runtime, with no adopter-side owner_scope duplication.

Proposed fix

Wrap the updateMediaBuy dispatch result in routeIfHandoff, mirroring the createMediaBuy closure (~2007):

return routeIfHandoff(
  taskRegistry,
  {
    tool: "update_media_buy",
    accountId,
    ownerScope: taskOwnerScopeFor(ctx, reqCtx.account.id),
    // …
  },
  result,
  project
);

update_media_buy is already in SPEC_WEBHOOK_TASK_TYPES, so sync-completion and async webhooks will emit consistently.

Workaround (for other adopters hitting this)

Register the task manually on the same taskRegistry the server was constructed with, and set owner_scope to session:${ctx.sessionKey} (that is, whatever your configured resolveSessionKey returns) so tasks/get scope-matches. Note that sessionKey isn't exposed on the handler's reqCtx, so you'll need to derive it the same way your resolveSessionKey does.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingclaude-triagedIssue has been triaged by the Claude Code triage routine. Remove to re-triage.media-buyIssue concerns the media-buy protocol domainpriority:P1High priority: important bug or follow-up, not immediate P0

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions