@@ -10,8 +10,18 @@ import { maybeRemoveImageBlocks } from "../../api/transform/image-cleaning"
1010import { findLast } from "../../shared/array"
1111import { supportPrompt } from "../../shared/support-prompt"
1212import { RooIgnoreController } from "../ignore/RooIgnoreController"
13+ import { MissingToolResultError } from "../task/validateToolResultIds"
1314import { generateFoldedFileContext } from "./foldedFileContext"
1415
16+ export const SYNTHETIC_TOOL_RESULT_REASONS = {
17+ condense : "Context condensation triggered. Tool execution deferred." ,
18+ historyShaping :
19+ "Tool result was filtered from history before this request (truncation/condensation). Continuing without it." ,
20+ } as const
21+
22+ export type SyntheticToolResultReason =
23+ ( typeof SYNTHETIC_TOOL_RESULT_REASONS ) [ keyof typeof SYNTHETIC_TOOL_RESULT_REASONS ]
24+
1525export type { FoldedFileContextResult , FoldedFileContextOptions } from "./foldedFileContext"
1626
1727/**
@@ -124,14 +134,28 @@ The goal is for work to continue seamlessly after condensation - as if it never
124134
125135/**
126136 * Injects synthetic tool_results for orphan tool_calls that don't have matching results.
127- * This is necessary because OpenAI's Responses API rejects conversations with orphan tool_calls.
128- * This can happen when the user triggers condense after receiving a tool_call (like attempt_completion)
129- * but before responding to it.
137+ * This is necessary because OpenAI's Responses API rejects conversations with orphan tool_calls,
138+ * and Anthropic's Messages API rejects requests with unpaired tool_use blocks.
139+ *
140+ * This can happen when:
141+ * - The user triggers condense after receiving a tool_call but before responding to it
142+ * (original use case; pass reason "condense").
143+ * - History shaping (truncation/condensation filters in `getEffectiveApiHistory`) drops the
144+ * user message that carried a tool_result while leaving the assistant tool_use behind
145+ * (issue #190; pass reason "historyShaping").
146+ *
147+ * Emits MissingToolResultError telemetry on each fired injection so we can confirm in
148+ * production whether this guard is doing work and which source dominates.
130149 *
131150 * @param messages - The conversation messages to process
151+ * @param reason - The synthetic tool_result body used to pair orphans. Defaults to the
152+ * condense reason to preserve historical behavior.
132153 * @returns The messages with synthetic tool_results appended if needed
133154 */
134- export function injectSyntheticToolResults ( messages : ApiMessage [ ] ) : ApiMessage [ ] {
155+ export function injectSyntheticToolResults (
156+ messages : ApiMessage [ ] ,
157+ reason : SyntheticToolResultReason = SYNTHETIC_TOOL_RESULT_REASONS . condense ,
158+ ) : ApiMessage [ ] {
135159 // Find all tool_call IDs in assistant messages
136160 const toolCallIds = new Set < string > ( )
137161 // Find all tool_result IDs in user messages
@@ -161,11 +185,32 @@ export function injectSyntheticToolResults(messages: ApiMessage[]): ApiMessage[]
161185 return messages
162186 }
163187
188+ // Mirror the validateToolResultIds.ts telemetry shape so PostHog dashboards keyed off
189+ // MissingToolResultError already aggregate this. The `reason` tag lets us split sources
190+ // once data is in.
191+ if ( TelemetryService . hasInstance ( ) ) {
192+ TelemetryService . instance . captureException (
193+ new MissingToolResultError (
194+ `injectSyntheticToolResults paired ${ orphanIds . length } orphan tool_use block(s). reason=${ reason } ` ,
195+ orphanIds ,
196+ [ ...toolResultIds ] ,
197+ ) ,
198+ {
199+ reason,
200+ missingToolUseIds : orphanIds ,
201+ existingToolResultIds : [ ...toolResultIds ] ,
202+ toolUseCount : toolCallIds . size ,
203+ toolResultCount : toolResultIds . size ,
204+ source : "injectSyntheticToolResults" ,
205+ } ,
206+ )
207+ }
208+
164209 // Inject synthetic tool_results as a new user message
165210 const syntheticResults : Anthropic . Messages . ToolResultBlockParam [ ] = orphanIds . map ( ( id ) => ( {
166211 type : "tool_result" as const ,
167212 tool_use_id : id ,
168- content : "Context condensation triggered. Tool execution deferred." ,
213+ content : reason ,
169214 } ) )
170215
171216 const syntheticMessage : ApiMessage = {
0 commit comments