fix: recover observer shell during workbench quick verify
This commit is contained in:
@@ -423,26 +423,89 @@ async function syncObserverPageToControlSession(reason, explicitSessionId = null
|
||||
const targetUrl = new URL(target, baseUrl).toString();
|
||||
const beforeUrl = pageUrl(observerPage);
|
||||
const beforeSessionId = routeSessionIdFromUrl(beforeUrl);
|
||||
if (sessionId && beforeSessionId === sessionId && !forceRefresh) return { ok: true, reason, changed: false, observerRoundTrip: false, sessionId, beforeUrl, afterUrl: beforeUrl, pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch };
|
||||
observerPageEpoch += 1;
|
||||
let status = null;
|
||||
let statusText = null;
|
||||
const response = await observerPage.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 45000 }).catch((error) => ({ observerGotoError: errorSummary(error) }));
|
||||
if (response?.observerGotoError) return { ok: false, reason, changed: false, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, error: response.observerGotoError, valuesRedacted: true };
|
||||
status = typeof response?.status === "function" ? response.status() : null;
|
||||
statusText = typeof response?.statusText === "function" ? response.statusText() : null;
|
||||
const readiness = await waitForTargetPageReady(observerPage, targetUrl, { timeoutMs: 15000 });
|
||||
if (!readiness.ok) {
|
||||
lastObserverRefreshAtMs = Date.now();
|
||||
return { ok: false, reason, changed: true, observerRoundTrip: forceRefresh, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, httpStatus: status, statusText, readiness, hydration: null, failureKind: readiness.reason || "observer-target-not-ready", valuesRedacted: true };
|
||||
const attempts = [];
|
||||
if (sessionId && beforeSessionId === sessionId && !forceRefresh) {
|
||||
const current = await observerSessionReadiness(targetUrl, sessionId, { readinessTimeoutMs: 1000, hydrationTimeoutMs: 1000 });
|
||||
if (current.ok) return { ok: true, reason, changed: false, observerRoundTrip: false, sessionId, beforeUrl, afterUrl: beforeUrl, pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, readiness: current.readiness, hydration: current.hydration, valuesRedacted: true };
|
||||
attempts.push({ attempt: 0, ok: false, shortCircuitRejected: true, failureKind: current.failureKind, readiness: current.readiness, hydration: current.hydration, beforeUrl, afterUrl: pageUrl(observerPage), valuesRedacted: true });
|
||||
}
|
||||
if (!isWorkbenchPathname(safeUrlPath(targetUrl) || "")) {
|
||||
const maxAttempts = Number.isFinite(Number(options?.maxAttempts)) ? Math.max(1, Number(options.maxAttempts)) : 2;
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||
const attemptBeforeUrl = pageUrl(observerPage);
|
||||
observerPageEpoch += 1;
|
||||
let status = null;
|
||||
let statusText = null;
|
||||
const response = await observerPage.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 45000 }).catch((error) => ({ observerGotoError: errorSummary(error) }));
|
||||
if (response?.observerGotoError) {
|
||||
attempts.push({ attempt, ok: false, failureKind: navigationFailureKind(response.observerGotoError?.message || response.observerGotoError?.name || "observer-navigation-error"), beforeUrl: attemptBeforeUrl, afterUrl: pageUrl(observerPage), error: response.observerGotoError, valuesRedacted: true });
|
||||
if (attempt < maxAttempts && isRetryableNavigationError(response.observerGotoError?.message || response.observerGotoError?.name || "")) {
|
||||
await recreateObserverPageForNavigation("observer-goto-error", attempt).catch((resetError) => appendJsonl(files.errors, eventRecord("observer-page-reset-error", { commandId: activeCommandId, attempt, error: errorSummary(resetError) })));
|
||||
continue;
|
||||
}
|
||||
lastObserverRefreshAtMs = Date.now();
|
||||
return { ok: false, reason, changed: false, observerRoundTrip: forceRefresh, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, error: response.observerGotoError, attempts, valuesRedacted: true };
|
||||
}
|
||||
status = typeof response?.status === "function" ? response.status() : null;
|
||||
statusText = typeof response?.statusText === "function" ? response.statusText() : null;
|
||||
const readiness = await waitForTargetPageReady(observerPage, targetUrl, { timeoutMs: 15000 });
|
||||
if (!readiness.ok) {
|
||||
attempts.push({ attempt, ok: false, failureKind: readiness.reason || "observer-target-not-ready", beforeUrl: attemptBeforeUrl, afterUrl: pageUrl(observerPage), httpStatus: status, statusText, readiness, valuesRedacted: true });
|
||||
if (attempt < maxAttempts && observerReadinessRetryable(readiness)) {
|
||||
await recreateObserverPageForNavigation(readiness.reason || "observer-target-not-ready", attempt).catch((resetError) => appendJsonl(files.errors, eventRecord("observer-page-reset-error", { commandId: activeCommandId, attempt, error: errorSummary(resetError) })));
|
||||
continue;
|
||||
}
|
||||
lastObserverRefreshAtMs = Date.now();
|
||||
return { ok: false, reason, changed: true, observerRoundTrip: forceRefresh, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, httpStatus: status, statusText, readiness, hydration: null, attempts, failureKind: readiness.reason || "observer-target-not-ready", valuesRedacted: true };
|
||||
}
|
||||
if (!isWorkbenchPathname(safeUrlPath(targetUrl) || "")) {
|
||||
lastObserverRefreshAtMs = Date.now();
|
||||
attempts.push({ attempt, ok: true, beforeUrl: attemptBeforeUrl, afterUrl: pageUrl(observerPage), httpStatus: status, statusText, readiness, valuesRedacted: true });
|
||||
return { ok: true, reason, changed: true, observerRoundTrip: forceRefresh, sessionId: null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, httpStatus: status, statusText, readiness, hydration: null, attempts, valuesRedacted: true };
|
||||
}
|
||||
const hydration = await waitForWorkbenchSessionHydrated(observerPage, sessionId, { timeoutMs: 15000 });
|
||||
attempts.push({ attempt, ok: hydration.ok === true, failureKind: hydration.ok === true ? null : hydration.reason || "observer-session-hydration-failed", beforeUrl: attemptBeforeUrl, afterUrl: pageUrl(observerPage), httpStatus: status, statusText, readiness, hydration, valuesRedacted: true });
|
||||
if (hydration.ok === true) {
|
||||
lastObserverRefreshAtMs = Date.now();
|
||||
return { ok: true, reason, changed: true, observerRoundTrip: forceRefresh, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, httpStatus: status, statusText, readiness, hydration, attempts, failureKind: null, valuesRedacted: true };
|
||||
}
|
||||
if (attempt < maxAttempts && observerHydrationRetryable(hydration)) {
|
||||
await recreateObserverPageForNavigation(hydration.reason || "observer-session-hydration-failed", attempt).catch((resetError) => appendJsonl(files.errors, eventRecord("observer-page-reset-error", { commandId: activeCommandId, attempt, error: errorSummary(resetError) })));
|
||||
continue;
|
||||
}
|
||||
lastObserverRefreshAtMs = Date.now();
|
||||
return { ok: true, reason, changed: true, observerRoundTrip: forceRefresh, sessionId: null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, httpStatus: status, statusText, readiness, hydration: null, valuesRedacted: true };
|
||||
return { ok: false, reason, changed: true, observerRoundTrip: forceRefresh, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, httpStatus: status, statusText, readiness, hydration, attempts, failureKind: hydration.reason || "observer-session-hydration-failed", valuesRedacted: true };
|
||||
}
|
||||
const hydration = await waitForWorkbenchSessionHydrated(observerPage, sessionId, { timeoutMs: 15000 });
|
||||
lastObserverRefreshAtMs = Date.now();
|
||||
return { ok: hydration.ok === true, reason, changed: true, observerRoundTrip: forceRefresh, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, httpStatus: status, statusText, readiness, hydration, failureKind: hydration.ok === true ? null : hydration.reason || "observer-session-hydration-failed", valuesRedacted: true };
|
||||
return { ok: false, reason, changed: true, observerRoundTrip: forceRefresh, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, attempts, failureKind: "observer-sync-retry-exhausted", valuesRedacted: true };
|
||||
}
|
||||
|
||||
async function observerSessionReadiness(targetUrl, sessionId, options = {}) {
|
||||
const readiness = await waitForTargetPageReady(observerPage, targetUrl, { timeoutMs: options.readinessTimeoutMs ?? 1000 });
|
||||
if (!readiness.ok) return { ok: false, failureKind: readiness.reason || "observer-target-not-ready", readiness, hydration: null, valuesRedacted: true };
|
||||
if (!isWorkbenchPathname(safeUrlPath(targetUrl) || "")) return { ok: true, readiness, hydration: null, valuesRedacted: true };
|
||||
const hydration = await waitForWorkbenchSessionHydrated(observerPage, sessionId, { timeoutMs: options.hydrationTimeoutMs ?? 1000 });
|
||||
return { ok: hydration.ok === true, failureKind: hydration.ok === true ? null : hydration.reason || "observer-session-hydration-failed", readiness, hydration, valuesRedacted: true };
|
||||
}
|
||||
|
||||
function observerReadinessRetryable(readiness) {
|
||||
const reason = String(readiness?.reason || "");
|
||||
const snapshot = readiness?.snapshot || {};
|
||||
return /workbench-app-not-ready|observer-target-not-ready/iu.test(reason)
|
||||
|| snapshot.workbenchShellVisible === false
|
||||
|| snapshot.sessionRailPresent === false && snapshot.commandInputPresent === false;
|
||||
}
|
||||
|
||||
function observerHydrationRetryable(hydration) {
|
||||
const reason = String(hydration?.reason || "");
|
||||
return /observer-session-hydration-timeout|observer-session-hydration-failed|active-session-not-hydrated|route-session-not-hydrated/iu.test(reason);
|
||||
}
|
||||
|
||||
async function recreateObserverPageForNavigation(reason, attempt) {
|
||||
const before = pageUrl(observerPage);
|
||||
if (observerPage && !observerPage.isClosed()) await observerPage.close().catch(() => {});
|
||||
observerPage = await context.newPage();
|
||||
attachPassiveListeners(observerPage, "observer", observerPageId);
|
||||
await appendJsonl(files.control, eventRecord("observer-page-recreated", { reason, attempt, beforeUrl: before, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, valuesRedacted: true }));
|
||||
}
|
||||
|
||||
async function waitForWorkbenchSessionHydrated(targetPage, sessionId, options = {}) {
|
||||
|
||||
Reference in New Issue
Block a user