fix(console): re-arm profile-paused approval/clarify pollers on focus/visibility

This commit is contained in:
carlotestor committed 2026-09-24 14:55:27 +00:00
1 parent 96bdeeb9cf
commit 6ebd12c251
2 files changed
+103 -1

No files matched your search

+24 -1
View File
@@ -8137,8 +8137,11 @@ function startApprovalPolling(sid) {
let _approvalEventSource = null;
let _approvalSSEHealthTimer = null;
let _approvalPollingSessionId = null;
// Session whose poller stopped on a profile-mismatch 409; re-armed on focus/visibility.
let _approvalProfilePausedSessionId = null;
function _startApprovalFallbackPoll(sid) {
_approvalProfilePausedSessionId = null;
// Run one tick immediately so a session already blocked on a pending approval
// shows its card instantly (the removed SSE 'initial' event used to do this);
// then poll on the 1500ms cadence. (#3913 SHOULD-FIX)
@@ -8170,6 +8173,7 @@ function _startApprovalFallbackPoll(sid) {
if (typeof _sessionProfileMismatchFromError === 'function' && _sessionProfileMismatchFromError(e)
&& _approvalPollTimer === pollTimer) {
stopApprovalPolling();
_approvalProfilePausedSessionId = sid;
}
}
finally { if (_approvalPollTimer === pollTimer) _approvalFallbackPollInFlight = false; }
@@ -9300,6 +9304,7 @@ var _clarifyFallbackTimer = null;
var _clarifyHealthTimer = null;
let _clarifyFallbackPollInFlight = false;
let _clarifyPollingSessionId = null;
let _clarifyProfilePausedSessionId = null;
function startClarifyPolling(sid) {
stopClarifyPolling();
@@ -9322,6 +9327,7 @@ function startClarifyPolling(sid) {
function _startClarifyFallbackPoll(sid) {
_clarifyPollingSessionId = sid || null;
_clarifyProfilePausedSessionId = null;
// Run one tick immediately so a session already blocked on a pending clarify
// shows its card instantly (the removed SSE 'initial' event used to do this);
// then poll on the 3000ms cadence. (#3913 SHOULD-FIX)
@@ -9356,7 +9362,10 @@ function _startClarifyFallbackPoll(sid) {
// cookie, so polling can never succeed. Stop quietly; the live card stays standing.
// Only this poller may stop itself: a late 409 from a replaced poller must not kill its successor.
if (typeof _sessionProfileMismatchFromError === "function" && _sessionProfileMismatchFromError(e)) {
if (_clarifyFallbackTimer === pollTimer) stopClarifyPolling();
if (_clarifyFallbackTimer === pollTimer) {
stopClarifyPolling();
_clarifyProfilePausedSessionId = sid;
}
return;
}
// A 404 from the active session domain is a STALE-SESSION signal — e.g.
@@ -9427,6 +9436,20 @@ function stopClarifyPolling() {
_clarifyPollingSessionId = null;
}
// Another tab may have switched the shared profile cookie back: re-arm pollers that a
// profile-mismatch 409 paused while their session is still open. A still-mismatched
// profile just 409s once and pauses them again.
function _resumeProfilePausedPromptPollers() {
if (typeof document !== 'undefined' && document.hidden) return;
const current = (S.session && S.session.session_id) || null;
const approvalSid = _approvalProfilePausedSessionId;
const clarifySid = _clarifyProfilePausedSessionId;
if (approvalSid && approvalSid === current && !_approvalPollTimer) startApprovalPolling(approvalSid);
if (clarifySid && clarifySid === current && !_clarifyFallbackTimer) startClarifyPolling(clarifySid);
}
document.addEventListener('visibilitychange', _resumeProfilePausedPromptPollers);
window.addEventListener('focus', _resumeProfilePausedPromptPollers);
// ── Notifications and Sound ──────────────────────────────────────────────────
function _completionNotificationPreviewText(lastAssistantMessage, options){
@@ -123,6 +123,9 @@ var _approvalFallbackPollInFlight = false, _approvalPollingSessionId = 'sid1';
var _clarifyEventSource = null, _clarifyFallbackTimer = null, _clarifyHealthTimer = null;
var _clarifyFallbackPollInFlight = false, _clarifyPollingSessionId = null, _clarifyMissingEndpointWarned = false;
var _approvalPendingBySession = new Map();
var _approvalProfilePausedSessionId = null, _clarifyProfilePausedSessionId = null;
function _approvalPromptGeneration(){ return 0; }
function _clarifyPromptGeneration(){ return 0; }
function _approvalPollingSessionMissingOrMismatched(sid){ return !sid || !S.session || S.session.session_id !== sid; }
function _hideApprovalCardIfOwner(){ calls.hide++; }
function _hideClarifyCardIfOwner(){ calls.hide++; }
@@ -256,3 +259,79 @@ def test_stale_profile_mismatch_does_not_stop_replacement_poller(start_fn, stop_
assert r["polling"] == "sid1", "a late 409 from a replaced poller must not stop its successor"
assert r["timerAlive"] is True
assert r["inFlight"] is True, "the successor's in-flight guard must not be cleared by the stale request"
def _run_profile_round_trip(kind):
"""Mismatch 409 pauses the poller; the cookie comes back, focus returns, and
the pending prompt for the still-open session must be fetched and shown."""
node = shutil.which("node")
if not node:
pytest.skip("node not installed")
approval = kind == "approval"
fns = [
_extract_fn(SESSIONS_JS, "_sessionProfileMismatchFromError"),
_extract_fn(MESSAGES_JS, "startApprovalPolling"),
_extract_fn(MESSAGES_JS, "_startApprovalFallbackPoll"),
_extract_fn(MESSAGES_JS, "stopApprovalPollingForSession"),
_extract_fn(MESSAGES_JS, "stopApprovalPolling"),
_extract_fn(MESSAGES_JS, "startClarifyPolling"),
_extract_fn(MESSAGES_JS, "_startClarifyFallbackPoll"),
_extract_fn(MESSAGES_JS, "stopClarifyPollingForSession"),
_extract_fn(MESSAGES_JS, "stopClarifyPolling"),
_extract_fn(MESSAGES_JS, "_resumeProfilePausedPromptPollers"),
]
harness = _HARNESS.replace("STATUS", "409").replace("BODY", json.dumps(_MISMATCH))
harness = harness.replace("async function api(){", "var mismatch = true;\nasync function api(){\n if (!mismatch) { calls.api++; return {pending: {id: 'p1'}}; }")
harness = harness.replace("function showApprovalForSession(){}", "function showApprovalForSession(){ calls.shown = (calls.shown||0) + 1; }")
harness = harness.replace("function showClarifyForSession(){}", "function showClarifyForSession(){ calls.shown = (calls.shown||0) + 1; }")
start = "startApprovalPolling" if approval else "startClarifyPolling"
timer = "_approvalPollTimer" if approval else "_clarifyFallbackTimer"
script = harness + "\n".join(fns) + f"""
const tick = () => new Promise(r => setTimeout(r, 0));
(async () => {{
{start}('sid1');
await tick();
const stoppedOnMismatch = {timer} === null;
_resumeProfilePausedPromptPollers(); // focus while still mismatched
await tick();
const stoppedAgain = {timer} === null;
const apiWhileMismatched = calls.api;
mismatch = false; // other tab switched the cookie back
_resumeProfilePausedPromptPollers(); // focus/visibility returns
await tick();
process.stdout.write(JSON.stringify({{stoppedOnMismatch, stoppedAgain, apiWhileMismatched,
api: calls.api, shown: calls.shown || 0, running: {timer} !== null}}));
}})();
"""
out = subprocess.run([node, "-e", script], capture_output=True, text=True, timeout=30)
assert out.returncode == 0, out.stderr
return json.loads(out.stdout)
@pytest.mark.parametrize("kind", ["approval", "clarify"])
def test_profile_round_trip_rearms_paused_poller_and_shows_pending(kind):
r = _run_profile_round_trip(kind)
assert r["stoppedOnMismatch"] is True
assert r["stoppedAgain"] is True, "a still-mismatched profile must stop the re-armed poller again"
assert r["apiWhileMismatched"] == 2
assert r["api"] == 3, "the poller must fetch again once the profile is back"
assert r["shown"] == 1, "the pending card must be shown after the profile round-trip"
assert r["running"] is True
def test_resume_does_not_rearm_for_a_different_open_session():
node = shutil.which("node")
if not node:
pytest.skip("node not installed")
script = _HARNESS.replace("STATUS", "409").replace("BODY", "{}") + _extract_fn(
MESSAGES_JS, "_resumeProfilePausedPromptPollers") + """
var started = 0;
function startApprovalPolling(){ started++; }
function startClarifyPolling(){ started++; }
_approvalProfilePausedSessionId = 'old'; _clarifyProfilePausedSessionId = 'old';
_resumeProfilePausedPromptPollers();
process.stdout.write(String(started));
"""
out = subprocess.run([node, "-e", script], capture_output=True, text=True, timeout=30)
assert out.returncode == 0, out.stderr
assert out.stdout == "0"