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) { async function recoverControlPageToTarget(reason, beforeUrl, target, liveness = null) {
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
}));
let navigation = null; let navigation = null;
let hydration = null; let hydration = null;
let afterLiveness = null; let afterLiveness = null;
const attempts = []; const attempts = [];
let ok = false; let ok = false;
for (let attempt = 1; attempt <= 2; attempt += 1) { for (let attempt = 1; attempt <= 2; attempt += 1) {
await recreateControlPageForNavigation(reason + "-control-page-unresponsive", attempt); await recreateControlPageForNavigation(reason + "-control-page-recovery", attempt);
try { try {
navigation = await gotoTarget(target.targetPath); navigation = await gotoTarget(target.targetPath);
} catch (error) { } catch (error) {
@@ -1694,7 +1680,7 @@ async function ensureControlPageResponsiveForCommand(reason) {
attempts.push({ attempt, ok, navigation, hydration, afterLiveness, pageId, pageEpoch: controlPageEpoch, valuesRedacted: true }); attempts.push({ attempt, ok, navigation, hydration, afterLiveness, pageId, pageEpoch: controlPageEpoch, valuesRedacted: true });
if (ok) break; if (ok) break;
} }
const recovery = { return {
ok, ok,
recovered: ok, recovered: ok,
reason, reason,
@@ -1711,8 +1697,26 @@ async function ensureControlPageResponsiveForCommand(reason) {
pageEpoch: controlPageEpoch, pageEpoch: controlPageEpoch,
valuesRedacted: true 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); const error = new Error("control page recovery failed before " + reason);
error.details = recovery; error.details = recovery;
throw error; throw error;
@@ -1720,17 +1724,56 @@ async function ensureControlPageResponsiveForCommand(reason) {
return recovery; 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 = {}) { async function sendPrompt(text, options = {}) {
if (text.trim().length === 0) throw new Error("sendPrompt requires non-empty text"); if (text.trim().length === 0) throw new Error("sendPrompt requires non-empty text");
const responsePath = options.responsePath || "/v1/agent/chat"; const responsePath = options.responsePath || "/v1/agent/chat";
const controlRecovery = await ensureControlPageResponsiveForCommand("sendPrompt"); const controlRecovery = await ensureControlPageResponsiveForCommand("sendPrompt");
const beforeUrl = currentPageUrl(); const beforeUrl = currentPageUrl();
const beforeEvidence = await promptSideEffectSnapshot(); const beforeEvidence = await promptSideEffectSnapshot();
const primaryEditor = page.locator("#command-input").last(); let editor = null;
const editor = await primaryEditor.isVisible().catch(() => false) let composerRecovery = null;
? primaryEditor let editorWaitError = null;
: page.locator('textarea, [role="textbox"], [contenteditable="true"], input[type="text"]').last(); for (let attempt = 1; attempt <= 2; attempt += 1) {
await withHardTimeout(editor.waitFor({ state: "visible", timeout: 15000 }), 20000, "sendPrompt composer editor did not become visible within 20s"); 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); await fillComposerEditor(editor, text);
const primarySubmitSelector = '#command-send, #command-submit, [data-testid="command-submit"], [data-testid="composer-submit"], [data-testid="send-command"]'; 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 primarySubmit = page.locator(primarySubmitSelector).last();