fix: recover control page when composer disappears
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user