Skip to content

context_builder typed responses (plan/question/review) fail when the target window's prompt box is empty — instructions is not used as the prompt for typed generation #527

Description

@mplibunao

Summary

An MCP context_builder call that requests a typed response (response_type of plan, question, or review) returns an error instead of a typed response when the target window's prompt box (the tab's prompt text) is empty and the request is carried solely by instructions.

The root cause is a wiring gap: instructions is forwarded only to the discovery agent (as instructionsOverride) and is not used as the prompt for typed-response generation. On a completed MCP run with an empty prompt box, the MCP tab-context commit copies the discovery agent's own output into the prompt as a fallback and flags that it did so; the response gate then treats that flag as disqualifying and refuses to generate, returning a JSON-RPC internal error.

Under those preconditions this is a deterministic control-flow outcome, not a race. Live evidence is 2/2 with response_type=question; plan and review reach the same gate by source inspection.

Steps to reproduce

Prerequisites

  • A build containing the affected code (commit dcc6a511, or any revision where the referenced code remains present).
  • A discovery agent and a response/plan model configured and available (typed runs require a planning model).
  • An active workspace loaded in the target window.
  • The target tab's prompt box is empty (no user-entered prompt text).

Steps

  1. With the prerequisites above satisfied, issue an MCP context_builder tool call that requests a typed response and passes instructions:

    {
      "name": "context_builder",
      "arguments": {
        "response_type": "question",
        "instructions": "<task>Explain how the MCP context_builder tool resolves its target tab</task>"
      }
    }
  2. Let the discovery run complete normally.

Observed: 2/2 live failures with response_type=question; plan and review reach the same gate by source inspection. The same call was observed to succeed when the tab's prompt box already contained prompt text before the call — i.e. when instructions is not the sole source of the request.

Expected

With response_type set and instructions supplied, Context Builder should use instructions (or the discovery-derived prompt) as the basis for typed-response generation and return the requested plan/question/review. Supplying instructions to a typed run should be sufficient; an empty prompt box should not block generation.

Actual

The call returns a JSON-RPC internal error (-32603):

[-32603] Internal error: Context Builder completed without a typed direct response for the requested question

(question is replaced by plan / review for those response_type values.)

A sibling failure string is emitted by the same gate when no non-empty fallback was committed (the tab prompt was empty and no usable lastAgentOutput existed):

[-32603] Internal error: Context Builder completed without a prompt for the requested <type>

Both stem from the same cause below.

Root cause

References are in Sources/RepoPrompt/Infrastructure/MCP/WindowTools/MCPContextBuilderToolProvider.swift and Sources/RepoPrompt/Features/ContextBuilder/ViewModels/ContextBuilderAgentViewModel.swift (see the pinned permalinks under References).

  1. instructions is discovery-only. executeContextBuilder(args:connectionID:dependencies:) reads instructions = args["instructions"] and forwards it only as instructionsOverride: into contextBuilderVM.runContextBuilderForMCP(tabID:instructionsOverride:…) (instructionsOverride: instructions.isEmpty ? nil : instructions). That override drives the discovery agent's task. The provider never writes instructions to the tab's prompt text.

  2. The empty-prompt fallback copies agent output into the prompt and sets the flag. For an MCP run, this happens in commitTabContextForAgent (the function that produces ContextBuilderTabContextCommitResult), not in the UI helper. When the committed tab's promptText is empty and record.session.lastAgentOutput is non-empty, the commit closure writes tab.promptText = promptFallback (the agent output), stores the tab via updateComposeTabStoredOnly, and sets record.session.usedAgentOutputAsPrompt = true. Note for maintainers: the UI helper copyAgentOutputToPromptIfEmpty is not the path here — finalizeContextBuilderRun skips it for MCP runs via if !record.origin.isMCP { … }, so this is the fallback that actually fires.

  3. The prompt read back is the agent's own output. After the run, the provider computes effectivePrompt = overrides.useOverridePrompt ? overrides.overridePromptText : resultTab.promptText. In the repro effectivePrompt is non-empty — but it holds the discovery agent's output written in step 2, not a user- or instructions-supplied prompt. That non-emptiness is not what saves the run; the flag from step 2 is what condemns it.

  4. The response gate refuses on the flag. responseDisposition(responseType:terminalDisposition:usedAgentOutputAsPrompt:effectivePrompt:) handles the .completed case with wantsResponse == true and hits, in order:

    • guard !usedAgentOutputAsPrompt → returns .failed("Context Builder completed without a typed direct response for the requested <type>")this is the guard that fires in the empty-prompt-box + instructions scenario, because step 2 set the flag; and
    • guard !effectivePrompt.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty → returns .failed("Context Builder completed without a prompt for the requested <type>") — the sibling guard, reached when no non-empty fallback was committed (usedAgentOutputAsPrompt false and the effective prompt still empty).
  5. Failure is surfaced as -32603. The caller maps .failed(message) from responseDisposition to throw MCPError.internalError(message).

Under the stated preconditions (typed response_type, empty tab prompt, request carried by instructions, discovery completes and commits the agent-output fallback), control flow deterministically reaches the first responseDisposition guard. There are two candidate fix seams: (a) seed the prompt used for typed generation from instructions when the tab prompt is empty, so a real prompt exists before the gate; or (b) let the gate accept an agent-derived prompt for typed generation instead of treating usedAgentOutputAsPrompt as disqualifying.

Observation (possibly related, unverified)

During live reproduction we observed headless discovery children (claude -p) accumulating from 7 to 10 across two failed typed calls, not reaped within the observation window. We have not established code-path causality: run teardown (scheduleRunTeardown) runs on every terminal outcome, and the 7→10 figure is an OS-level process count, not tied to this gate. This may warrant separate investigation and should not be read as an established effect of this bug.

Environment

  • A local debug build, packaged 2026-07-12, on macOS.
  • Verified present on main at commit dcc6a511: the instructionsOverride forwarding and effectivePrompt read-back in MCPContextBuilderToolProvider.swift, both responseDisposition guards, the finalizeContextBuilderRun MCP skip, and the commitTabContextForAgent empty-prompt fallback are all present.
  • Live-reproduced 2/2 on 2026-07-12 with response_type: "question" against an empty prompt box.

Notes

References (permalinks pinned to dcc6a511)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions