3305cfd2bb
A tool wedged inside _ConcurrentToolAuthorizationGate hung the whole turn forever (#79719): excluded_seconds() measured residency in gate.run() — arbitrary code — so an open window grew 1:1 with wall clock and the batch deadline's remaining was constant (remaining = deadline - window_started; now cancels out). A hanging pre_tool_call plugin or an approval round-trip to a dead client defeated the deadline entirely. The serialization lock was also an unbounded acquire, so every other worker needing authorization parked behind the wedged holder forever. Fix, in two halves: - tools/approval.py grows per-session human-wait accounting (human_wait_window / human_wait_seconds). The two places that are verifiably blocked on a HUMAN — the CLI approval prompt and the gateway approval poll loop — mark their own windows. Both are intrinsically bounded by approvals.timeout; the open-window read is additionally clamped to that timeout plus a margin as belt-and-braces. - _ConcurrentToolAuthorizationGate keeps only serialization, with a bounded acquire (approvals.timeout + 60s; on expiry the prompt runs unserialized — the same degradation the start-order gate accepted in #79705). excluded_seconds() becomes a baseline-delta read of the session's human-wait total. A wedged plugin now contributes nothing to the exclusion, so the batch times out at the normal deadline with correctly labeled results, while a genuine approval wait — which can legitimately exceed any fixed bound — still extends the deadline in full. E2E (real AIAgent, worktree imports): wedged-plugin batch on main never ends (>30s observed, 3s deadline); with the fix it ends at 3.0s. A 4s simulated approval over a 2s deadline completes without a timeout label. Closes #79719