Skip to content

[Bug]: MCP thread tools replace the orchestrator's rejection reason with "The operation could not be completed." #15586

Description

@angelospk

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. In a thread, ask the agent to settle its own thread as the last step of its turn.
  2. The agent calls t3_thread_organize with action: "settle" and no threadId.

Expected behavior

The tool fails with the reason the orchestrator gave ("Thread … has active or blocked work and cannot be settled."), so the agent can tell the user why and what to do instead.

Actual behavior

The tool returns {"_tag":"OrchestratorMcpFailure","code":"orchestration_error","message":"The operation could not be completed."}. The agent can't tell why, retries, and then tells the user that settle failed "because of a T3 error". The real reason only shows up in server.trace.ndjson.

t3_thread_organize (apps/server/src/mcp/toolkits/thread/handlers.ts:300) and the shared dispatch helper (:48) both do threads.dispatch(command).pipe(Effect.mapError(unavailable)). unavailable() (apps/server/src/mcp/threadAccess.ts:15) always returns the same generic message, so the decider's OrchestratorDispatchError cause is dropped. Any decider rejection for a command dispatched through those handlers (organize, queue cancel/reorder/promote and so on) loses its reason the same way, not just settle.

ThreadMetadataMcpService.ts already passes error.message through (threadLookupFailure, :38-43), so surfacing it has precedent.

A possible fix: map OrchestratorDispatchError to a failure that carries its cause, maybe under its own code such as command_rejected, and keep the generic message for unexpected internal errors. If the generic message is there on purpose to avoid leaking internals, a small set of user-facing rejection reasons would still help agents.

Impact

Minor bug or occasional failure

Version or commit

0.0.46-nightly.20261004.2644. The same code is on main at eac52f00.

Environment

Linux x86-64, desktop app, Claude provider.

Logs or stack traces

OrchestratorDispatchError: Failed to dispatch orchestration command thread.settle (mcp:ac287a89-...).
  [cause]: Error: Thread dafebdb4-... has active or blocked work and cannot be settled.

Workaround

Settle the thread by hand from the sidebar after the turn finishes.

Related: #13630 (settle a running thread once it goes idle), #15566 (typed MCP failures returned with isError: false).

Activity

  1. juliusmarminge commented on Oct 4, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Triage

    Thanks so much for the precise report and the code pointers, @angelospk. I confirmed this on current main (eac52f00), and it's separate from #15566 and #13630.

    What I found

    Refusing to settle a thread that still has work is correct. The problem is that the sentence the agent needs gets thrown away before the tool result is built.

    • t3_thread_organize dispatches thread.settle and maps every failure through unavailable() (apps/server/src/mcp/toolkits/thread/handlers.ts:300). The shared dispatch helper does the same (:48), so queue edit, cancel, reorder, and promote lose their reasons too. t3_pending_request_respond (:219) follows the same pattern.
    • unavailable() (apps/server/src/mcp/threadAccess.ts:15-19) always returns orchestration_error / The operation could not be completed. and ignores the error it's given.
    • The settle refusal is an OrchestratorDispatchError whose cause is the string Thread … has active or blocked work and cannot be settled. (apps/server/src/orchestration-v2/Orchestrator.ts:2511-2515). Its own message is only the Failed to dispatch orchestration command thread.settle (…) wrapper (:130-132). That matches your trace.
    • Other decider refusals have the same shape, for example snooze (has a pending approval…, has a queued run…) and queue cancel (Run … is not queued.).

    The generic text is deliberate for unexpected failures. apps/server/src/mcp/toolkits/core.test.ts (returns a bounded public failure without serializing storage causes) checks that a projection error with a private storage path as its cause still comes back as the generic message. ThreadMetadataMcpService.threadLookupFailure passes error.message through, but for this error that would only be the dispatch wrapper, not the cause. Forwarding every cause blindly could also leak internals, since transcript hydration wraps arbitrary failures in OrchestratorDispatchError (ensureCommandTranscripts in ThreadManagementService.ts).

    Likely fix area

    • One option is that, on the dispatch paths above, when the error is an OrchestratorDispatchError with a string cause, that string becomes the OrchestratorMcpFailure message.
    • unavailable() could stay as-is for lookup, projection, and non-string causes, so storage paths and internal defects remain redacted.
    • The string could ride on the existing orchestration_error code. A new code like command_rejected would be a contract change to OrchestratorMcpFailure, so that part is optional.

    A maintainer will decide on the fix direction.

  2. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 4, 2026
  3. wcass77 commented on Oct 5, 2026

    @wcass77

    I am also having the same issue. Although I understand that this is, in a sense, expected behavior since the thread is still running, it would be useful in many cases to have a thread that can settle itself when done. Perhaps having that as an explicit, separate option for the thread settle tool would make sense.

    Thank you for all your work on T3 code!

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

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions