Hmbown/CodeWhale · error
cancelled tool result is always model-visible
Error message
cancelled tool result is always model-visible
What it means
Panic while assembling the terminal outcome for a cancelled tool call. The cancellation design requires a synthesized `ToolResult` (a receipt explaining the cancellation) so the model sees why there is no output; `cancelled_before_completion == true` combined with `legacy_result == None` violates that contract: the cancel flag was set, but no cancellation receipt was ever produced for this call.
Source
Thrown at crates/tui/src/core/engine/turn_loop.rs:3366
let content_blocks = result
.as_ref()
.map(|result| result.content_blocks.clone())
.unwrap_or_default();
let legacy_result = result.map(RichToolResult::into_result);
let _ = self
.tx_event
.send(Event::ToolCallComplete {
id: tool_id.clone(),
name: tool_name.clone(),
result: legacy_result.clone(),
})
.await;
let terminal = if cancelled_before_completion {
ToolExecutionOutcome::cancelled(
legacy_result
.expect("cancelled tool result is always model-visible"),
)
} else {
ToolExecutionOutcome::from_legacy(legacy_result)
};
outcomes[plan.index] = Some(ToolExecOutcome {
index: plan.index,
id: tool_id,
name: tool_name,
input: tool_input,
started_at,
terminal,
content_blocks,
});
}
}
}
// #dogfood 0.8.67: if the model mutates the goal mid-turn viaView on GitHub (pinned to 0c42157ee5)
Solutions
- Replace the expect with `unwrap_or_else` synthesizing a cancellation receipt in the same format the normal cancellation path uses.
- Fix the race at the source: create the synthesized `ToolResult` at the moment `cancelled_before_completion` is set, not at outcome assembly.
- Add a test that cancels between approval and execution.
- Audit every site that sets `cancelled_before_completion` for a matching synthesized result.
Example fix
// before
let terminal = if cancelled_before_completion {
ToolExecutionOutcome::cancelled(
legacy_result.expect("cancelled tool result is always model-visible"),
)
} else {
ToolExecutionOutcome::from_legacy(legacy_result)
};
// after: synthesize the missing cancellation receipt
let terminal = if cancelled_before_completion {
let result = legacy_result
.unwrap_or_else(|| ToolResult::text("[tool run was cancelled before completion]"));
ToolExecutionOutcome::cancelled(result)
} else {
ToolExecutionOutcome::from_legacy(legacy_result)
}; Defensive patterns
Strategy: fallback
Try / catch
let outcome = std::panic::catch_unwind(|| assemble_outcome(cancelled, legacy_result));
let outcome = outcome.unwrap_or_else(|_| cancelled_outcome_with_receipt("[tool run was cancelled]")); Prevention
- Synthesize the cancellation ToolResult at the site where the cancel flag is set, not at outcome assembly.
- Test the cancel-between-approval-and-execution race explicitly.
- Treat `cancelled_before_completion && legacy_result.is_none()` as a source bug to fix, never a state to mask.
When it happens
Trigger: Cancellation racing tool startup: the flag flips after planning but before the execution path creates the placeholder or real result; a new early-return cancellation path that sets `cancelled_before_completion` without synthesizing a result; refactors that move where `legacy_result` is populated relative to the cancel check.
Common situations: Users pressing the cancel key during a tool batch; ESC/cancel handling added to a new tool execution stage; batch cancellation of multiple calls where one path forgets to write the receipt.
Related errors
- validated sandbox permission
- sandbox escalation was validated while planning
- registered shell tool context
- runtime event JSON is serializable
- validated Fleet tool authority envelope must serialize
AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20).
Data as JSON: /api/errors/7b8a40841a09faaa.
Report an issue: GitHub.