fix: recover control page when composer disappears

This commit is contained in:
Codex
2026-06-28 02:34:00 +00:00
parent 645709bb62
commit d495b873a5
@@ -1653,28 +1653,14 @@ async function controlPageLivenessSnapshot(reason, timeoutMs = 1500) {
}));
}
async function ensureControlPageResponsiveForCommand(reason) {
const beforeUrl = currentPageUrl();
const liveness = await controlPageLivenessSnapshot(reason + "-preflight", 3000);
if (liveness.ok) return { ok: true, recovered: false, reason, beforeUrl, afterUrl: currentPageUrl(), liveness, pageRole: "control", pageId, pageEpoch: controlPageEpoch, valuesRedacted: true };
const target = controlPageRecoveryTarget(liveness.snapshot, beforeUrl);
await appendJsonl(files.control, eventRecord("control-page-unresponsive-before-command", {
reason,
beforeUrl,
target,
liveness,
pageRole: "control",
pageId,
pageEpoch: controlPageEpoch,
valuesRedacted: true
}));
async function recoverControlPageToTarget(reason, beforeUrl, target, liveness = null) {
let navigation = null;
let hydration = null;
let afterLiveness = null;
const attempts = [];
let ok = false;
for (let attempt = 1; attempt <= 2; attempt += 1) {
await recreateControlPageForNavigation(reason + "-control-page-unresponsive", attempt);
await recreateControlPageForNavigation(reason + "-control-page-recovery", attempt);
try {
navigation = await gotoTarget(target.targetPath);
} catch (error) {
@@ -1694,7 +1680,7 @@ async function ensureControlPageResponsiveForCommand(reason) {
attempts.push({ attempt, ok, navigation, hydration, afterLiveness, pageId, pageEpoch: controlPageEpoch, valuesRedacted: true });
if (ok) break;
}
const recovery = {
return {
ok,
recovered: ok,
reason,
@@ -1711,8 +1697,26 @@ async function ensureControlPageResponsiveForCommand(reason) {
pageEpoch: controlPageEpoch,
valuesRedacted: true
};
await appendJsonl(files.control, eventRecord(ok ? "control-page-recovered-before-command" : "control-page-recovery-failed-before-command", recovery));
if (!ok) {
}
async function ensureControlPageResponsiveForCommand(reason) {
const beforeUrl = currentPageUrl();
const liveness = await controlPageLivenessSnapshot(reason + "-preflight", 3000);
if (liveness.ok) return { ok: true, recovered: false, reason, beforeUrl, afterUrl: currentPageUrl(), liveness, pageRole: "control", pageId, pageEpoch: controlPageEpoch, valuesRedacted: true };
const target = controlPageRecoveryTarget(liveness.snapshot, beforeUrl);
await appendJsonl(files.control, eventRecord("control-page-unresponsive-before-command", {
reason,
beforeUrl,
target,
liveness,
pageRole: "control",
pageId,
pageEpoch: controlPageEpoch,
valuesRedacted: true
}));
const recovery = await recoverControlPageToTarget(reason, beforeUrl, target, liveness);
await appendJsonl(files.control, eventRecord(recovery.ok ? "control-page-recovered-before-command" : "control-page-recovery-failed-before-command", recovery));
if (!recovery.ok) {
const error = new Error("control page recovery failed before " + reason);
error.details = recovery;
throw error;
@@ -1720,17 +1724,56 @@ async function ensureControlPageResponsiveForCommand(reason) {
return recovery;
}
async function forceRecoverControlPageForCommand(reason) {
const beforeUrl = currentPageUrl();
const liveness = await controlPageLivenessSnapshot(reason + "-snapshot", 3000);
const target = controlPageRecoveryTarget(liveness.snapshot, beforeUrl);
await appendJsonl(files.control, eventRecord("control-page-forced-recovery-before-command", {
reason,
beforeUrl,
target,
liveness,
pageRole: "control",
pageId,
pageEpoch: controlPageEpoch,
valuesRedacted: true
}));
const recovery = await recoverControlPageToTarget(reason, beforeUrl, target, liveness);
await appendJsonl(files.control, eventRecord(recovery.ok ? "control-page-forced-recovered-before-command" : "control-page-forced-recovery-failed-before-command", recovery));
return recovery;
}
async function sendPrompt(text, options = {}) {
if (text.trim().length === 0) throw new Error("sendPrompt requires non-empty text");
const responsePath = options.responsePath || "/v1/agent/chat";
const controlRecovery = await ensureControlPageResponsiveForCommand("sendPrompt");
const beforeUrl = currentPageUrl();
const beforeEvidence = await promptSideEffectSnapshot();
const primaryEditor = page.locator("#command-input").last();
const editor = await primaryEditor.isVisible().catch(() => false)
? primaryEditor
: page.locator('textarea, [role="textbox"], [contenteditable="true"], input[type="text"]').last();
await withHardTimeout(editor.waitFor({ state: "visible", timeout: 15000 }), 20000, "sendPrompt composer editor did not become visible within 20s");
let editor = null;
let composerRecovery = null;
let editorWaitError = null;
for (let attempt = 1; attempt <= 2; attempt += 1) {
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();
try {
await withHardTimeout(candidate.waitFor({ state: "visible", timeout: 15000 }), 20000, "sendPrompt composer editor did not become visible within 20s");
editor = candidate;
break;
} catch (error) {
editorWaitError = error;
if (attempt >= 2) break;
composerRecovery = await forceRecoverControlPageForCommand("sendPrompt-composer-editor-missing");
if (composerRecovery.ok !== true) break;
}
}
if (!editor) {
const snapshot = await controlPageLivenessSnapshot("sendPrompt-composer-editor-missing-final", 3000);
const error = new Error("sendPrompt composer editor did not become visible");
error.details = { beforeUrl, afterUrl: currentPageUrl(), controlRecovery, composerRecovery, snapshot, editorWaitError: errorSummary(editorWaitError), pageId, pageEpoch: controlPageEpoch, valuesRedacted: true };
throw error;
}
await fillComposerEditor(editor, text);
const primarySubmitSelector = '#command-send, #command-submit, [data-testid="command-submit"], [data-testid="composer-submit"], [data-testid="send-command"]';
const primarySubmit = page.locator(primarySubmitSelector).last();