fix(approval): enforce explicit timeout on smart-approval guardian call and log its outcome - #84125
Closed
yflmq001 wants to merge 1 commit into
Closed
fix(approval): enforce explicit timeout on smart-approval guardian call and log its outcome#84125yflmq001 wants to merge 1 commit into
yflmq001 wants to merge 1 commit into
Conversation
…ll and log its outcome The smart-approval guardian (`_smart_approve`) gates every flagged terminal command with a synchronous auxiliary LLM call, but it never passes `timeout=` and logs nothing on the normal path. In production a stalled provider response silently froze the agent turn for 62 minutes with zero log output; the gateway kill-switch eventually fired, and only an unrelated error surfaced afterwards (NousResearch#82846; watchdog-style fix in NousResearch#72500). The call was invisible by design — nothing logs at the hang point. Changes in tools/approval.py: - Resolve the same configured timeout the client would use internally (`auxiliary.approval.timeout` via `_get_task_timeout("approval")`) and pass it explicitly to `call_llm`, so the deadline cannot be lost if the internal default resolution changes or is misconfigured. - Log the assessment call and its duration (DEBUG), and promote the failure branch from DEBUG to WARNING with elapsed time + exception class, so a wedged guardian call is visible in the logs instead of silent. - Failure still returns "escalate" (fail open to the human/pattern gate) — behavior unchanged, observability only. Complements NousResearch#72500 (watchdog hard ceiling) rather than duplicating it: explicit timeout is the root-cause hardening, logging closes the silence gap; the watchdog remains the safety net if the SDK-level timeout itself is defeated. Tests: explicit timeout forwarded to call_llm (revert-fails), failure logs WARNING + escalates. 49 approval-adjacent tests pass; one unrelated test_approval.py failure is pre-existing (fails on clean main too).
yflmq001
force-pushed
the
fix/82846-smart-approve-timeout-log
branch
from
August 12, 2026 01:07
aacdba1 to
2cae8e8
Compare
Contributor
fix(approval): enforce explicit timeout on smart-approval guardian call and log its outcome
|
Collaborator
|
Salvaged into #93809 with your commit cherry-picked, authorship preserved (#85125 Phase 2c). Your belt-and-suspenders shape was exactly right — explicit timeout from the auxiliary resolution + WARNING-with-elapsed on failure. Verified the enforced value flows through _effective_aux_timeout's explicit-wins path so config and bound stay in lockstep. Closing in favor of #93809 — thanks! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Complements #72500 (watchdog hard ceiling) — root-cause hardening + observability for the smart-approval guardian call, both gaps the watchdog fix leaves open.
Bug Description
_smart_approve(tools/approval.py:3054) gates every flagged terminal command with a synchronous auxiliary LLM call that (a) never passes an explicittimeout=and (b) logs nothing on the success path and onlylogger.debugon failure. When a provider response stalls, the agent turn freezes silently — our production hit froze for 62 minutes with zero log output; the gateway kill-switch eventually fired and only an unrelated lifecycle-guard error surfaced afterwards (2026-08-11, Feishu gateway, approvals smart mode, DeepSeek latency spike). See #82846 for the full symptom.Root Cause
call_llmresolves its own timeout from config (auxiliary.approval.timeout, default 30s), but_smart_approverelies on that default silently — no explicit deadline at the call site, and nothing visible in the logs. #72500 addresses the hang with an outer watchdog thread (45s ceiling), but:timeout=— the root cause (internal deadline not enforced at the call boundary) stays,Fix
tools/approval.py— resolve the same configured value the client would use (_get_task_timeout("approval"), honoringauxiliary.approval.timeout) and pass it explicitly tocall_llm; log the assessment call + duration (DEBUG); promote failure logging from DEBUG to WARNING with elapsed time and exception class."escalate"(fail open to the human/pattern gate).How to Verify
PYTHONPATH=. python -m pytest tests/tools/test_smart_approval_policy.py -q -k "explicit_timeout or logs_warning"test_smart_approve_passes_explicit_timeout— assertscall_llmreceivestimeout=42whenauxiliary.approval.timeoutresolves to 42. Verified as a genuine regression: removing thetimeout=kwarg makes the test fail.test_smart_approve_failure_logs_warning_and_escalates— asserts aTimeoutErrorfromcall_llmyields"escalate"and a WARNING containing the failure prefix + exception class.Test Plan
tests/tools/test_smart_approval_policy.py— 7 passedtest_smart_approval_injection.py,test_approval_interrupt.py,test_execute_code_approval_cluster.py,test_denial_circuit_breaker.py— 42 more passed (49 total)test_approval.py::test_nonrecursive_verification_artifact_cleanup_is_not_dangerousis pre-existing (fails on clean main, unrelated to this change)Risk Assessment
Low. No new dependencies, no thread/context changes (unlike the watchdog approach — this stays on the calling thread). The timeout value is the same one
call_llmwould resolve internally; passing it explicitly only removes the reliance on internal defaulting. Logging level change (DEBUG→WARNING) affects only observability.