fix(web-probe): wait observer refresh hydration (#680)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-22 20:50:50 +08:00
committed by GitHub
parent 24885272ab
commit d30d5a2722
2 changed files with 29 additions and 5 deletions
+1 -1
View File
@@ -86,7 +86,7 @@ export function hwlabNodeWebProbeHelp(): Record<string, unknown> {
actions: {
run: "Run the repo-owned scripts/web-live-dom-probe.mjs helper.",
script: "Run caller-provided Playwright JS after CLI-managed /auth/login; the script receives authenticated browser/context/page plus gotoStable/reloadStable/gotoCurrentStable/safeReload/fetchJson/safeFetchJson/fetchApiMatrix/recordStep/collectText/safeEvaluate/waitWorkbenchReady/screenshotOnError/summarizeWorkspace/summarizeConversation helpers and must not handle secrets itself.",
observe: "Start, inspect, control, stop, collect, and analyze a pure-client long-running Workbench observer on the target host. The observer has one Playwright page authority, receives commands through stateDir/commands files, writes JSONL artifacts, and does not expose any inbound service API.",
observe: "Start, inspect, control, stop, collect, and analyze a pure-client long-running Workbench observer on the target host. The observer runs a control page plus a passive observer page in a shared-auth browser context, receives commands through stateDir/commands files, writes JSONL artifacts, and does not expose any inbound service API.",
},
notes: [
"Prefer --script-file for reusable probes; stdin heredocs remain supported for one-off probes.",
@@ -278,9 +278,25 @@ async function syncObserverPageToControlSession(reason, explicitSessionId = null
if (response?.observerGotoError) return { ok: false, reason, changed: false, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, error: response.observerGotoError, valuesRedacted: true };
status = typeof response?.status === "function" ? response.status() : null;
statusText = typeof response?.statusText === "function" ? response.statusText() : null;
await observerPage.waitForTimeout(1000);
const hydration = await waitForWorkbenchSessionHydrated(observerPage, sessionId, { timeoutMs: 15000 });
lastObserverRefreshAtMs = Date.now();
return { ok: true, reason, changed: true, observerRoundTrip: forceRefresh, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, httpStatus: status, statusText, valuesRedacted: true };
return { ok: true, reason, changed: true, observerRoundTrip: forceRefresh, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, httpStatus: status, statusText, hydration, valuesRedacted: true };
}
async function waitForWorkbenchSessionHydrated(targetPage, sessionId, options = {}) {
const timeoutMs = Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 15000;
const started = Date.now();
const deadline = started + Math.max(1, timeoutMs);
let last = null;
while (Date.now() <= deadline) {
last = await workbenchSessionSnapshot(targetPage);
const expected = String(sessionId || "").trim();
const routeOk = !expected || last?.routeSessionId === expected;
const activeOk = !expected || last?.activeSessionId === expected;
if (routeOk && activeOk) return { ok: true, durationMs: Date.now() - started, snapshot: last, valuesRedacted: true };
await targetPage.waitForTimeout(250).catch(() => {});
}
return { ok: false, durationMs: Date.now() - started, snapshot: last, reason: "observer-session-hydration-timeout", expectedSessionId: sessionId || null, valuesRedacted: true };
}
async function maybeRefreshObserverPage(reason) {
@@ -696,12 +712,17 @@ async function createSessionFromUi() {
};
}
async function workbenchSessionSnapshot() {
return page.evaluate(() => {
async function workbenchSessionSnapshot(targetPage = page) {
return targetPage.evaluate(() => {
const activeTab = document.querySelector(".session-tab[data-active='true'], .session-tab[aria-selected='true']");
const routeMatch = window.location.pathname.match(/\/workbench\/sessions\/([^/]+)/u) || window.location.pathname.match(/\/workspace\/sessions\/([^/]+)/u);
const input = document.querySelector("#command-input");
const warning = document.querySelector(".composer-warning")?.textContent?.trim() || null;
const visible = (element) => {
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
};
return {
url: window.location.href,
routeSessionId: routeMatch ? decodeURIComponent(routeMatch[1] || "") : null,
@@ -709,6 +730,9 @@ async function workbenchSessionSnapshot() {
activeConversationId: activeTab?.getAttribute("data-conversation-id") || null,
activeStatus: activeTab?.getAttribute("data-status") || null,
tabCount: document.querySelectorAll(".session-tab").length,
messageCount: Array.from(document.querySelectorAll('[data-testid*="message" i], [class*="message" i], article, [role="article"]')).filter(visible).length,
traceRowCount: Array.from(document.querySelectorAll('[data-testid*="trace" i], [class*="trace" i], [data-trace-id], [data-testid*="event" i]')).filter(visible).length,
loadingCount: Array.from(document.querySelectorAll('[aria-busy="true"], [data-loading="true"], [class*="loading" i], [data-testid*="loading" i]')).filter(visible).length,
composerReady: Boolean(activeTab && input && !input.disabled && !warning),
warning
};