fix(console): re-arm profile-paused approval/clarify pollers on focus/visibility
This commit is contained in:
1 parent
96bdeeb9cf
commit
6ebd12c251
2 files changed
+103
-1
No files matched your search
+24
-1
@@ -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"
|
||||
Reference in new issue
Block a user