Skip to content

[Bug]: agents can't recover from t3_background_wait refusals #37006

Open

Description

@yaacovcorcos

Before submitting

Area

apps/server

Summary

t3_background_wait refuses calls in ways the calling agent can't repair: it is listed to providers that can never serve it, a task that finished before registration looks like an invented ID, and a miss doesn't say which IDs are running. The refusals themselves are right (nothing is registered wrongly); the agent just gets nothing to retry with. We have been running the fix below in a fork and are happy to send a PR.

What happens

  • Same-response registration. Claude often sends Bash run_in_background and
    t3_background_wait in one assistant message. The task ID exists only in the Bash result, so the
    wait carries an ID the model made up. It is refused as unmatched, and the model usually retries a
    few seconds later with the real ID. The instruction text ("call t3_background_wait with its task
    ID before you end your turn") does not say when the ID exists. One model also sent
    {"taskId": "…"} (singular).
  • Finished before registration. A short command can end, with its task_notification handled
    and the roster entry gone, before the model's next response registers it. That is refused exactly
    like an invented ID.
  • Unsupported providers. BackgroundWaitTool has no McpSchema.EnabledWhen, so Codex, Pi, ACP
    agents and others list a tool that always fails with "This provider cannot mark background work as
    awaited." The handler maps that to invalid_request, so it reads as the agent's mistake.

Where

  • apps/server/src/mcp/toolkits/orchestrator/tools.ts: BackgroundWaitTool (description, no
    EnabledWhen).
  • apps/server/src/orchestration-v2/ThreadManagementService.ts: awaitBackgroundTasks;
    ThreadManagementBackgroundWaitError unmatched message.
  • packages/provider-core/src/server/ProviderAdapter.ts: awaitBackgroundTasks returns only
    { matched }.
  • apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts: matches the pending roster only;
    CLAUDE_BACKGROUND_WAIT_INSTRUCTIONS.
  • packages/provider-opencode/src/server/v2/adapter.ts: matches running shells only; the shell
    instruction text.

Proposed fix

  1. List the tool only where it works. Add an adapter-level capability (we used
    backgroundWait?: Effect.Effect<boolean> on the ProviderAdapterV2 service, so the composite
    OpenCode driver can answer for the protocol version a session would use). Stamp it on the MCP
    thread credential when it is minted, next to subagent, and annotate BackgroundWaitTool with an
    EnabledWhen that reads it. Keep the unsupported branch as a defence.
  2. Report finished tasks as settled. Each adapter remembers task IDs that finished on a thread
    (bounded; we keep 64 per session). awaitBackgroundTasks returns
    { matched, settled?, running? }. A registration whose IDs are only settled succeeds with
    settled: [...] ("already finished: read their output now") and holds nothing. A reused ID that
    is running again counts as running.
  3. Name the running tasks on a miss. When nothing matched and nothing settled, return up to 5
    running, not-yet-awaited tasks with a bounded description (the command or task description). The
    unmatched message appends: "Running and not yet awaited here: <id> (). Call again
    with one of these IDs." The agent started those commands itself, so nothing new is disclosed.
  4. Say when the ID exists. Tool description and both instruction texts: "In a later response
    than the start, pass the ID its result shows: taskIds: [id]." (fix(server): name the taskIds key in t3_background_wait guidance #31914 already fixes the
    taskIds key wording; this adds the "later response" part.)

Not proposed: waiting server-side for an unknown ID to appear (an invented ID never appears),
treating every unmatched ID as success, or accepting taskId as an alias.

Tests we added

  • An invented ID while two tasks run returns matched: [] and both as running; an awaited task is
    no longer listed; after its task_notification, registering it returns settled: [id] (Claude
    adapter, driven by SDK frames).
  • The OpenCode shell tests that registered a finished shell now expect settled: [shellId].
  • The thread service passes settled through as success and appends the running list to the
    unmatched error.
  • EnabledWhen lists the tool for a credential with the capability and hides it otherwise.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions