{"record":{"id":"7b8a40841a09faaa","repo":"Hmbown/CodeWhale","slug":"cancelled-tool-result-is-always-model-visible","errorCode":null,"errorMessage":"cancelled tool result is always model-visible","messagePattern":"cancelled tool result is always model-visible","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/core/engine/turn_loop.rs","lineNumber":3366,"sourceCode":"\n                        let content_blocks = result\n                            .as_ref()\n                            .map(|result| result.content_blocks.clone())\n                            .unwrap_or_default();\n                        let legacy_result = result.map(RichToolResult::into_result);\n                        let _ = self\n                            .tx_event\n                            .send(Event::ToolCallComplete {\n                                id: tool_id.clone(),\n                                name: tool_name.clone(),\n                                result: legacy_result.clone(),\n                            })\n                            .await;\n\n                        let terminal = if cancelled_before_completion {\n                            ToolExecutionOutcome::cancelled(\n                                legacy_result\n                                    .expect(\"cancelled tool result is always model-visible\"),\n                            )\n                        } else {\n                            ToolExecutionOutcome::from_legacy(legacy_result)\n                        };\n                        outcomes[plan.index] = Some(ToolExecOutcome {\n                            index: plan.index,\n                            id: tool_id,\n                            name: tool_name,\n                            input: tool_input,\n                            started_at,\n                            terminal,\n                            content_blocks,\n                        });\n                    }\n                }\n            }\n\n            // #dogfood 0.8.67: if the model mutates the goal mid-turn via","sourceCodeStart":3348,"sourceCodeEnd":3384,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/core/engine/turn_loop.rs#L3348-L3384","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nlet terminal = if cancelled_before_completion {\n    ToolExecutionOutcome::cancelled(\n        legacy_result.expect(\"cancelled tool result is always model-visible\"),\n    )\n} else {\n    ToolExecutionOutcome::from_legacy(legacy_result)\n};\n\n// after: synthesize the missing cancellation receipt\nlet terminal = if cancelled_before_completion {\n    let result = legacy_result\n        .unwrap_or_else(|| ToolResult::text(\"[tool run was cancelled before completion]\"));\n    ToolExecutionOutcome::cancelled(result)\n} else {\n    ToolExecutionOutcome::from_legacy(legacy_result)\n};","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"let outcome = std::panic::catch_unwind(|| assemble_outcome(cancelled, legacy_result));\nlet outcome = outcome.unwrap_or_else(|_| cancelled_outcome_with_receipt(\"[tool run was cancelled]\"));","preventionTips":["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."],"tags":["rust","cancellation","tool-execution","race-condition","panic","expect"],"backgroundTag":"option-unwrapped-none","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}