From 3cb5322aeb39c9ccac11afa5978d7b3de57be54f Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 28 Jun 2026 09:44:19 +0000 Subject: [PATCH] fix: retry web probe prompt fill --- .../hwlab-node-web-observe-runner-source.ts | 74 +++++++++++++++++-- 1 file changed, 69 insertions(+), 5 deletions(-) diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index 9c58c7bc..6be7ef87 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -1877,7 +1877,7 @@ async function sendPrompt(text, options = {}) { error.details = { beforeUrl, afterUrl: currentPageUrl(), controlRecovery, composerRecovery, snapshot, editorWaitError: errorSummary(editorWaitError), pageId, pageEpoch: controlPageEpoch, valuesRedacted: true }; throw error; } - await fillComposerEditor(editor, text); + editor = await fillComposerEditorWithRetry(editor, text, { beforeUrl, controlRecovery }); const primarySubmitSelector = '#command-send, #command-submit, [data-testid="command-submit"], [data-testid="composer-submit"], [data-testid="send-command"]'; const primarySubmit = page.locator(primarySubmitSelector).last(); const submit = await primarySubmit.isVisible().catch(() => false) @@ -2030,19 +2030,83 @@ async function sendPrompt(text, options = {}) { }; } -async function fillComposerEditor(editor, text) { +async function fillComposerEditorWithRetry(initialEditor, text, context = {}) { + let editor = initialEditor; + let lastError = null; + let recovery = null; + for (let attempt = 1; attempt <= 2; attempt += 1) { + try { + if (attempt > 1 || !editor) editor = await resolveComposerEditor(8000); + await fillComposerEditor(editor, text, { timeoutMs: 12000 }); + const retained = await composerEditorRetainsText(editor, text); + if (retained) return editor; + throw new Error("sendPrompt composer editor did not retain filled text"); + } catch (error) { + lastError = error; + await appendJsonl(files.control, eventRecord("sendPrompt-composer-fill-retry", { + attempt, + willRetry: attempt < 2, + error: errorSummary(error), + beforeUrl: context.beforeUrl || null, + afterUrl: currentPageUrl(), + controlRecovery: context.controlRecovery || null, + pageId, + pageEpoch: controlPageEpoch, + valuesRedacted: true + })); + if (attempt >= 2) break; + recovery = await forceRecoverControlPageForCommand("sendPrompt-composer-fill-failed"); + if (recovery.ok !== true) await page.waitForTimeout(250); + editor = null; + } + } + const snapshot = await controlPageLivenessSnapshot("sendPrompt-composer-fill-failed-final", 3000); + const error = new Error("sendPrompt composer editor fill failed"); + error.details = { + beforeUrl: context.beforeUrl || null, + afterUrl: currentPageUrl(), + controlRecovery: context.controlRecovery || null, + recovery, + snapshot, + fillError: errorSummary(lastError), + pageId, + pageEpoch: controlPageEpoch, + valuesRedacted: true + }; + throw error; +} + +async function resolveComposerEditor(timeoutMs = 8000) { + const primaryEditor = page.locator("#command-input").last(); + const candidate = await primaryEditor.isVisible().catch(() => false) + ? primaryEditor + : page.locator('textarea, [role="textbox"], [contenteditable="true"], input[type="text"]').last(); + await candidate.waitFor({ state: "visible", timeout: timeoutMs }); + return candidate; +} + +async function fillComposerEditor(editor, text, options = {}) { + const timeoutMs = Number.isFinite(Number(options.timeoutMs)) ? Math.max(1000, Math.trunc(Number(options.timeoutMs))) : 30000; const tag = await editor.evaluate((element) => element.tagName.toLowerCase()).catch(() => ""); const editable = await editor.evaluate((element) => element.getAttribute("contenteditable") === "true").catch(() => false); - if (tag === "textarea" || tag === "input") await editor.fill(text); + if (tag === "textarea" || tag === "input") await editor.fill(text, { timeout: timeoutMs }); else if (editable) { - await editor.click(); + await editor.click({ timeout: timeoutMs }); await page.keyboard.insertText(text); } else { - await editor.click(); + await editor.click({ timeout: timeoutMs }); await page.keyboard.insertText(text); } } +async function composerEditorRetainsText(editor, expectedText) { + const expected = String(expectedText || ""); + return editor.evaluate((element, value) => { + if (element instanceof HTMLTextAreaElement || element instanceof HTMLInputElement) return element.value === value; + return (element.textContent || "") === value; + }, expected).catch(() => false); +} + async function clearComposerEditor(editor) { const tag = await editor.evaluate((element) => element.tagName.toLowerCase()).catch(() => ""); const editable = await editor.evaluate((element) => element.getAttribute("contenteditable") === "true").catch(() => false);