From aab43cdaff9756dc18305adb5c75bf6b3bddc723 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Fri, 12 Jun 2026 08:12:57 -0400 Subject: [PATCH] fix(approval): keep approval polling alive across busy dips (#4041) --- static/messages.js | 12 ++++- .../test_issue4041_approval_poll_busy_dip.py | 46 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 tests/test_issue4041_approval_poll_busy_dip.py diff --git a/static/messages.js b/static/messages.js index c9973fb8..998b1641 100644 --- a/static/messages.js +++ b/static/messages.js @@ -3994,6 +3994,10 @@ function _hideApprovalCardIfOwner(sid, force=false) { if (!sid || _approvalSessionId === sid) hideApprovalCard(force); } +function _approvalPollingSessionMissingOrMismatched(sid) { + return !sid || !S.session || S.session.session_id !== sid; +} + function _renderPendingApprovalForActiveSession() { const sid = _promptActiveSessionId(); if (!sid) return; @@ -4160,7 +4164,7 @@ function _startApprovalFallbackPoll(sid) { // shows its card instantly (the removed SSE 'initial' event used to do this); // then poll on the 1500ms cadence. (#3913 SHOULD-FIX) const _tick = async () => { - if (!S.busy || !S.session || S.session.session_id !== sid) { + if (_approvalPollingSessionMissingOrMismatched(sid)) { stopApprovalPolling(); _hideApprovalCardIfOwner(sid, true); return; } if (_approvalFallbackPollInFlight) return; @@ -4168,7 +4172,11 @@ function _startApprovalFallbackPoll(sid) { try { const data = await api("/api/approval/pending?session_id=" + encodeURIComponent(sid),{timeoutToast:false}); if (data.pending) { showApprovalForSession(sid, data.pending, data.pending_count||1); } - else { _clearApprovalPendingForSession(sid); _hideApprovalCardIfOwner(sid); } + else if (!_approvalPollingSessionMissingOrMismatched(sid) && !S.busy) { + _clearApprovalPendingForSession(sid); + _hideApprovalCardIfOwner(sid); + stopApprovalPollingForSession(sid); + } } catch(e) { /* ignore poll errors */ } finally { _approvalFallbackPollInFlight = false; } }; diff --git a/tests/test_issue4041_approval_poll_busy_dip.py b/tests/test_issue4041_approval_poll_busy_dip.py new file mode 100644 index 00000000..5b6353b5 --- /dev/null +++ b/tests/test_issue4041_approval_poll_busy_dip.py @@ -0,0 +1,46 @@ +from pathlib import Path + +REPO_ROOT = Path(__file__).parent.parent +MESSAGES_JS = (REPO_ROOT / "static" / "messages.js").read_text(encoding="utf-8") + + +def _body_from_brace(src: str, brace: int, label: str) -> str: + assert brace >= 0, f"body opening brace not found for: {label}" + depth = 1 + i = brace + 1 + while i < len(src) and depth: + ch = src[i] + if ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + i += 1 + assert depth == 0, f"body did not close for: {label}" + return src[brace + 1 : i - 1] + + +def _function_body(name: str) -> str: + marker = f"function {name}(" + start = MESSAGES_JS.find(marker) + assert start >= 0, f"function not found: {name}" + signature_end = MESSAGES_JS.find(")", start) + assert signature_end >= 0, f"function signature not found: {name}" + brace = MESSAGES_JS.find("{", signature_end) + return _body_from_brace(MESSAGES_JS, brace, name) + + +def test_busy_dips_no_longer_force_stop_approval_polling(): + body = _function_body("_startApprovalFallbackPoll") + + assert "_approvalPollingSessionMissingOrMismatched(sid)" in body + assert "!S.busy || !S.session || S.session.session_id !== sid" not in body + + +def test_empty_pending_only_stops_after_confirmed_idle(): + fallback_body = _function_body("_startApprovalFallbackPoll") + + assert "else if (!_approvalPollingSessionMissingOrMismatched(sid) && !S.busy)" in fallback_body + assert "_clearApprovalPendingForSession(sid);" in fallback_body + assert "_hideApprovalCardIfOwner(sid);" in fallback_body + assert "stopApprovalPollingForSession(sid);" in fallback_body + assert "stopApprovalPolling(); _hideApprovalCardIfOwner(sid, true); return;" in fallback_body