From 266768c14f93ee26d4f6bf857d738d240a2173a4 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 26 Jun 2026 14:31:56 +0000 Subject: [PATCH] fix: retry new session after request failure --- .../hwlab-node-web-observe-runner-source.ts | 133 ++++++++++++++---- 1 file changed, 103 insertions(+), 30 deletions(-) diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index bc9bd5ee..7c2f44f7 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -1189,41 +1189,113 @@ function isProjectManagementPathname(value) { return projectManagement.targetPaths.some((target) => pathname === target || pathname.startsWith(target + "/")); } +function isAgentSessionCreateRequest(requestOrUrl) { + const method = typeof requestOrUrl?.method === "function" ? requestOrUrl.method().toUpperCase() : ""; + if (method && method !== "POST") return false; + const url = typeof requestOrUrl === "string" ? requestOrUrl : typeof requestOrUrl?.url === "function" ? requestOrUrl.url() : ""; + try { + return new URL(url).pathname === "/v1/agent/sessions"; + } catch { + return false; + } +} + +function requestFailureSummary(request) { + let failure = null; + try { + failure = request.failure(); + } catch {} + let urlPath = null; + try { + urlPath = new URL(request.url()).pathname; + } catch {} + return { + method: typeof request.method === "function" ? request.method().toUpperCase() : null, + urlPath, + failureText: failure?.errorText || null, + valuesRedacted: true + }; +} + +async function clickAndWaitForAgentSessionCreate(create) { + let removeRequestFailedListener = () => {}; + const requestFailedPromise = new Promise((resolve) => { + let timeout = null; + const handler = (request) => { + if (!isAgentSessionCreateRequest(request)) return; + removeRequestFailedListener(); + resolve({ kind: "requestfailed", requestFailure: requestFailureSummary(request) }); + }; + removeRequestFailedListener = () => { + page.off("requestfailed", handler); + if (timeout !== null) clearTimeout(timeout); + timeout = null; + }; + page.on("requestfailed", handler); + timeout = setTimeout(() => { + removeRequestFailedListener(); + resolve(null); + }, 45000); + }); + const createResponsePromise = page.waitForResponse((response) => { + const request = response.request(); + return isAgentSessionCreateRequest(request) || isAgentSessionCreateRequest(response.url()); + }, { timeout: 45000 }).then((response) => ({ kind: "response", response })).catch((error) => ({ kind: "wait-error", waitError: errorSummary(error) })); + await create.click(); + const outcome = await Promise.race([createResponsePromise, requestFailedPromise]); + removeRequestFailedListener(); + return outcome ?? await createResponsePromise; +} + async function createSessionFromUi() { const beforeUrl = currentPageUrl(); const before = await workbenchSessionSnapshot(); - const readinessBeforeClick = await workbenchReadinessSnapshot(page); - const create = page.locator("#session-create").first(); - await create.waitFor({ state: "visible", timeout: 15000 }); - const createButtonState = await create.evaluate((element) => { - const rect = element.getBoundingClientRect(); - const style = window.getComputedStyle(element); - return { - tag: element.tagName.toLowerCase(), - id: element.id || null, - disabled: Boolean(element.disabled), - ariaDisabled: element.getAttribute("aria-disabled") || null, - visible: rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none", - rect: { width: Math.round(rect.width), height: Math.round(rect.height) }, - valuesRedacted: true - }; - }).catch((error) => ({ error: errorSummary(error), valuesRedacted: true })); - const createResponsePromise = page.waitForResponse((response) => { - const request = response.request(); - if (request.method().toUpperCase() !== "POST") return false; - try { - return new URL(response.url()).pathname === "/v1/agent/sessions"; - } catch { - return false; + const attempts = []; + let createResponse = null; + for (let attempt = 1; attempt <= 2; attempt += 1) { + const readinessBeforeClick = await workbenchReadinessSnapshot(page); + const create = page.locator("#session-create").first(); + await create.waitFor({ state: "visible", timeout: 15000 }); + const createButtonState = await create.evaluate((element) => { + const rect = element.getBoundingClientRect(); + const style = window.getComputedStyle(element); + return { + tag: element.tagName.toLowerCase(), + id: element.id || null, + disabled: Boolean(element.disabled), + ariaDisabled: element.getAttribute("aria-disabled") || null, + visible: rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none", + rect: { width: Math.round(rect.width), height: Math.round(rect.height) }, + valuesRedacted: true + }; + }).catch((error) => ({ error: errorSummary(error), valuesRedacted: true })); + const outcome = await clickAndWaitForAgentSessionCreate(create); + if (outcome?.kind === "response") { + createResponse = outcome.response; + attempts.push({ attempt, outcome: "response", readinessBeforeClick, createButtonState, valuesRedacted: true }); + break; } - }, { timeout: 45000 }).catch((error) => ({ waitError: errorSummary(error) })); - await create.click(); - const createResponse = await createResponsePromise; - if (createResponse?.waitError) { - const error = new Error("newSession did not observe POST /v1/agent/sessions response after click: " + (createResponse.waitError.message || createResponse.waitError.name || "timeout")); - error.details = { beforeUrl, afterUrl: currentPageUrl(), before, readinessBeforeClick, createButtonState, waitError: createResponse.waitError, pageId, valuesRedacted: true }; + const afterAttempt = await workbenchSessionSnapshot(); + attempts.push({ + attempt, + outcome: outcome?.kind || "unknown", + readinessBeforeClick, + createButtonState, + waitError: outcome?.waitError || null, + requestFailure: outcome?.requestFailure || null, + after: afterAttempt, + valuesRedacted: true + }); + if (attempt < 2 && outcome?.kind === "requestfailed") { + await page.waitForTimeout(1500); + continue; + } + const waitError = outcome?.waitError || outcome?.requestFailure || { name: outcome?.kind || "unknown", valuesRedacted: true }; + const error = new Error("newSession did not observe POST /v1/agent/sessions response after click: " + (waitError.message || waitError.failureText || waitError.name || "timeout")); + error.details = { beforeUrl, afterUrl: currentPageUrl(), before, attempts, pageId, valuesRedacted: true }; throw error; } + if (createResponse === null) throw new Error("newSession did not produce an authoritative session create response"); const createStatus = createResponse.status(); let createPayload = null; let createPayloadError = null; @@ -1252,7 +1324,7 @@ async function createSessionFromUi() { const ok = Boolean(afterSessionId === createdSessionId && after?.routeSessionId === createdSessionId && after?.composerReady); if (!ok) { const error = new Error("newSession did not select the authoritative newly created workbench session"); - error.details = { beforeUrl, afterUrl: currentPageUrl(), before, after, createdSessionId, pageId, valuesRedacted: true }; + error.details = { beforeUrl, afterUrl: currentPageUrl(), before, after, createdSessionId, attempts, pageId, valuesRedacted: true }; throw error; } return { @@ -1261,6 +1333,7 @@ async function createSessionFromUi() { ok, before, after, + attempts, sessionId: createdSessionId, createSession: { status: createStatus, statusText: createResponse.statusText(), responseParsed: createPayload !== null, responseParseError: createPayloadError, createdSessionId, valuesRedacted: true }, pageId