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) {
|
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();
|
||||||
|
|||||||
Reference in New Issue
Block a user