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
-
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>"
}
}
-
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).
-
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.
-
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.
-
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.
-
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).
-
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)
Summary
An MCP
context_buildercall that requests a typed response (response_typeofplan,question, orreview) 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 byinstructions.The root cause is a wiring gap:
instructionsis forwarded only to the discovery agent (asinstructionsOverride) 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;planandreviewreach the same gate by source inspection.Steps to reproduce
Prerequisites
dcc6a511, or any revision where the referenced code remains present).Steps
With the prerequisites above satisfied, issue an MCP
context_buildertool call that requests a typed response and passesinstructions:{ "name": "context_builder", "arguments": { "response_type": "question", "instructions": "<task>Explain how the MCP context_builder tool resolves its target tab</task>" } }Let the discovery run complete normally.
Observed: 2/2 live failures with
response_type=question;planandreviewreach 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. wheninstructionsis not the sole source of the request.Expected
With
response_typeset andinstructionssupplied, Context Builder should useinstructions(or the discovery-derived prompt) as the basis for typed-response generation and return the requested plan/question/review. Supplyinginstructionsto a typed run should be sufficient; an empty prompt box should not block generation.Actual
The call returns a JSON-RPC internal error (
-32603):(
questionis replaced byplan/reviewfor thoseresponse_typevalues.)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
lastAgentOutputexisted):Both stem from the same cause below.
Root cause
References are in
Sources/RepoPrompt/Infrastructure/MCP/WindowTools/MCPContextBuilderToolProvider.swiftandSources/RepoPrompt/Features/ContextBuilder/ViewModels/ContextBuilderAgentViewModel.swift(see the pinned permalinks under References).instructionsis discovery-only.executeContextBuilder(args:connectionID:dependencies:)readsinstructions = args["instructions"]and forwards it only asinstructionsOverride:intocontextBuilderVM.runContextBuilderForMCP(tabID:instructionsOverride:…)(instructionsOverride: instructions.isEmpty ? nil : instructions). That override drives the discovery agent's task. The provider never writesinstructionsto the tab's prompt text.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 producesContextBuilderTabContextCommitResult), not in the UI helper. When the committed tab'spromptTextis empty andrecord.session.lastAgentOutputis non-empty, the commit closure writestab.promptText = promptFallback(the agent output), stores the tab viaupdateComposeTabStoredOnly, and setsrecord.session.usedAgentOutputAsPrompt = true. Note for maintainers: the UI helpercopyAgentOutputToPromptIfEmptyis not the path here —finalizeContextBuilderRunskips it for MCP runs viaif !record.origin.isMCP { … }, so this is the fallback that actually fires.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 reproeffectivePromptis non-empty — but it holds the discovery agent's output written in step 2, not a user- orinstructions-supplied prompt. That non-emptiness is not what saves the run; the flag from step 2 is what condemns it.The response gate refuses on the flag.
responseDisposition(responseType:terminalDisposition:usedAgentOutputAsPrompt:effectivePrompt:)handles the.completedcase withwantsResponse == trueand 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 +instructionsscenario, because step 2 set the flag; andguard !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 (usedAgentOutputAsPromptfalse and the effective prompt still empty).Failure is surfaced as
-32603. The caller maps.failed(message)fromresponseDispositiontothrow MCPError.internalError(message).Under the stated preconditions (typed
response_type, empty tab prompt, request carried byinstructions, discovery completes and commits the agent-output fallback), control flow deterministically reaches the firstresponseDispositionguard. There are two candidate fix seams: (a) seed the prompt used for typed generation frominstructionswhen 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 treatingusedAgentOutputAsPromptas 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
mainat commitdcc6a511: theinstructionsOverrideforwarding andeffectivePromptread-back inMCPContextBuilderToolProvider.swift, bothresponseDispositionguards, thefinalizeContextBuilderRunMCP skip, and thecommitTabContextForAgentempty-prompt fallback are all present.response_type: "question"against an empty prompt box.Notes
MCPContextBuilderToolProvider.swift(that file is not among Fix Oracle selection packaging and mutation ingress stalls #520's changed files) and does not address this typed-response gate; theinstructions→prompt gap and theresponseDispositionguards are unchanged at thedcc6a511merge. This is a distinct, unaddressed defect.instructions— so it is a separate fix.References (permalinks pinned to
dcc6a511)instructionsforwarded asinstructionsOverride(discovery-only): https://github.com/repoprompt/repoprompt-ce/blob/dcc6a511/Sources/RepoPrompt/Infrastructure/MCP/WindowTools/MCPContextBuilderToolProvider.swift#L344effectivePromptread-back: https://github.com/repoprompt/repoprompt-ce/blob/dcc6a511/Sources/RepoPrompt/Infrastructure/MCP/WindowTools/MCPContextBuilderToolProvider.swift#L415responseDispositionguards (typed-response guard at L665, empty-prompt guard at L670): https://github.com/repoprompt/repoprompt-ce/blob/dcc6a511/Sources/RepoPrompt/Infrastructure/MCP/WindowTools/MCPContextBuilderToolProvider.swift#L651-L672finalizeContextBuilderRunskips the UI copy for MCP runs (!record.origin.isMCP): https://github.com/repoprompt/repoprompt-ce/blob/dcc6a511/Sources/RepoPrompt/Features/ContextBuilder/ViewModels/ContextBuilderAgentViewModel.swift#L2013-L2016commitTabContextForAgentempty-prompt fallback that setsusedAgentOutputAsPrompt: https://github.com/repoprompt/repoprompt-ce/blob/dcc6a511/Sources/RepoPrompt/Features/ContextBuilder/ViewModels/ContextBuilderAgentViewModel.swift#L2805-L2824