6332216384
When a gateway approval wait ends without anyone answering — the parent's delegate_task finishing and tearing the child down, a /stop, or the turn's notifier being unregistered at turn end — the tool result said "BLOCKED: Command denied by user" (outcome="denied", user_summary "You denied this command"). The user never saw or answered the prompt, so the parent agent went on reasoning about a refusal that never happened (#112026, #22992). The action stays fail-closed (the command does not run, the model still gets the NOT-consented stop text), but the attribution is now truthful: - tools/approval_gateway_wait.py: `_cancel_cause()` reads the existing per-thread interrupt-cause channel (`get_interrupt_reason()`, a trusted fixed category — no string matching) for the interrupted state and marks a notifier-unregister wake (event set, result None) as "the turn ended before the prompt was answered". Both the direct and the coalesced-follower wait return `cancelled=<cause>`; the post_approval_response hook fires choice="cancelled" instead of "deny"/"timeout". - tools/approval.py: a cancelled decision renders "BLOCKED: Command approval was withdrawn before the user answered (<cause>)." with outcome="cancelled" and its own user_summary; an explicit /deny is untouched. - tools/delegate_tool_child_run.py: `_signal_child_stop` publishes a fixed tool_reason ("parent delegation ended"; the late-child mirror forwards the parent's own category) so a child's pending approval can tell teardown from a user /stop — previously it rode the default "explicit stop requested". - tools/file_tools_write_guards.py / tools/approval_prompt.py: the protected instruction-file gate and MCP elicitation consume the same key instead of reporting "denied by the user" / "decline". Co-authored-by: zccyman <16263913+zccyman@users.noreply.github.com> Co-authored-by: KoNit-K <124019182+KoNit-K@users.noreply.github.com>