fix: harden web probe performance sampling
This commit is contained in:
+387
-13
@@ -225,6 +225,9 @@ export async function runProbe(args) {
|
||||
let page;
|
||||
let result;
|
||||
let traceSamples = [];
|
||||
let sessionSummary = null;
|
||||
let traceSummary = null;
|
||||
let performanceSummary = null;
|
||||
if (args.credentialError) {
|
||||
result = {
|
||||
ok: false,
|
||||
@@ -271,6 +274,9 @@ export async function runProbe(args) {
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||
const dom = await collectDomEvidence(page, args);
|
||||
const promptValidation = validatePromptFinalResponse(dom, args);
|
||||
sessionSummary = summarizeSessionRun(actions, dom);
|
||||
traceSummary = summarizeTraceRun(traceSamples, dom, sessionSummary);
|
||||
performanceSummary = summarizeProbePerformance(actions, startedAt, traceSamples, traceSummary);
|
||||
result = {
|
||||
ok: true,
|
||||
status: dom.requiredSelectors.workspace && dom.requiredSelectors.commandInput && dom.requiredSelectors.conversationList && promptValidation.ok ? "pass" : "blocked",
|
||||
@@ -284,9 +290,13 @@ export async function runProbe(args) {
|
||||
actions,
|
||||
credentials: args.credentials,
|
||||
bootstrapRoutes: summarizeBootstrapRoutes(bootstrapRoutes),
|
||||
session: sessionSummary,
|
||||
trace: traceSummary,
|
||||
performance: performanceSummary,
|
||||
traceSamples,
|
||||
dom,
|
||||
promptValidation,
|
||||
degradedReason: runProbeDegradedReason({ traceSummary, sessionSummary, promptValidation }),
|
||||
artifacts: { reportPath, screenshotPath },
|
||||
safety: {
|
||||
repoOwnedLauncher: true,
|
||||
@@ -311,6 +321,7 @@ export async function runProbe(args) {
|
||||
actions,
|
||||
credentials: args.credentials,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
degradedReason: classifyRunProbeError(error),
|
||||
finalUrl: page ? page.url() : null,
|
||||
failureDom: page ? await collectFailureDomEvidence(page).catch((domError) => ({ error: domError instanceof Error ? domError.message : String(domError) })) : null,
|
||||
artifacts: { reportPath, screenshotPath }
|
||||
@@ -605,10 +616,10 @@ async function createFreshSession(page, args, actions) {
|
||||
const locator = page.locator(selector).first();
|
||||
if (!(await locator.isVisible({ timeout: 3000 }).catch(() => false))) {
|
||||
actions.push({ action: "fresh-session", selector, skipped: "not-visible" });
|
||||
return false;
|
||||
throw new Error("fresh session create button is not visible");
|
||||
}
|
||||
const shellReady = await waitForSessionShellReady(page, args, actions);
|
||||
const before = await collectSessionState(page);
|
||||
const before = await collectSessionState(page, args);
|
||||
await locator.click();
|
||||
const timeoutMs = Math.min(args.timeoutMs, 30000);
|
||||
const settled = await page.waitForFunction((initial) => {
|
||||
@@ -622,34 +633,136 @@ async function createFreshSession(page, args, actions) {
|
||||
const input = document.querySelector("#command-input");
|
||||
return statusReady && (activeChanged || statusChanged) && messageCount === 0 && input && !input.disabled;
|
||||
}, before, { timeout: timeoutMs }).catch(() => null);
|
||||
const after = await collectSessionState(page);
|
||||
actions.push({ action: "fresh-session", selector, settled: Boolean(settled), timeoutMs, shellReady, before, after });
|
||||
if (!settled) throw new Error(`fresh session did not settle: ${JSON.stringify({ before, after, timeoutMs })}`);
|
||||
const alignment = await waitForFreshSessionAlignment(page, args, before, timeoutMs);
|
||||
const after = alignment.after;
|
||||
actions.push({ action: "fresh-session", selector, settled: Boolean(settled), aligned: alignment.ok, timeoutMs, shellReady, before, after, alignment });
|
||||
if (!settled || !alignment.ok) throw new Error(`fresh session did not settle: ${JSON.stringify({ settled: Boolean(settled), alignment, timeoutMs })}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function waitForFreshSessionAlignment(page, args, before, timeoutMs) {
|
||||
const startedAt = Date.now();
|
||||
const attempts = [];
|
||||
let after = await collectSessionState(page, args);
|
||||
let bestCandidate = null;
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
const check = freshSessionAlignment(before, after);
|
||||
bestCandidate = bestCandidate ?? freshSessionCandidate(before, after);
|
||||
attempts.push({ elapsedMs: Date.now() - startedAt, ok: check.ok, reason: check.reason, conversationId: check.conversationId, routeConversationId: check.routeConversationId, selectedConversationId: check.selectedConversationId, apiConversationPresent: check.apiConversationPresent });
|
||||
if (check.ok) return { ok: true, reason: null, attempts, after, check };
|
||||
await page.waitForTimeout(500);
|
||||
after = await collectSessionState(page, args);
|
||||
}
|
||||
const check = freshSessionAlignment(before, after);
|
||||
bestCandidate = bestCandidate ?? freshSessionCandidate(before, after);
|
||||
attempts.push({ elapsedMs: Date.now() - startedAt, ok: check.ok, reason: check.reason, conversationId: check.conversationId, routeConversationId: check.routeConversationId, selectedConversationId: check.selectedConversationId, apiConversationPresent: check.apiConversationPresent });
|
||||
if (!check.ok && bestCandidate?.conversationId) {
|
||||
const repair = await realignFreshSession(page, args, bestCandidate.conversationId);
|
||||
after = await collectSessionState(page, args);
|
||||
const repaired = freshSessionAlignment(before, after);
|
||||
return { ok: repaired.ok, reason: repaired.reason, attempts, after, check: repaired, repair };
|
||||
}
|
||||
return { ok: false, reason: check.reason, attempts, after, check };
|
||||
}
|
||||
|
||||
function freshSessionCandidate(before, after) {
|
||||
const previousConversationId = firstNonEmpty(before?.routeConversationId, before?.api?.workspace?.selectedConversationId, before?.activeTab?.conversationId);
|
||||
const items = Array.isArray(after?.api?.conversations?.items) ? after.api.conversations.items : [];
|
||||
return items.find((item) => item.conversationId && item.conversationId !== previousConversationId && Number(item.messagesCount ?? 0) === 0) ?? null;
|
||||
}
|
||||
|
||||
async function realignFreshSession(page, args, conversationId) {
|
||||
const selected = await selectConversationViaApi(page, args, conversationId, "web-live-dom-probe-fresh-realign");
|
||||
const target = new URL(`/workbench/sessions/${encodeURIComponent(conversationId)}`, args.url).toString();
|
||||
await page.goto(target, { waitUntil: "domcontentloaded", timeout: args.timeoutMs });
|
||||
await page.locator("#workspace").waitFor({ state: "visible", timeout: args.timeoutMs });
|
||||
await page.locator("#command-input").waitFor({ state: "visible", timeout: args.timeoutMs });
|
||||
return { action: "fresh-session-realign", conversationId, target, selected };
|
||||
}
|
||||
|
||||
async function selectConversationViaApi(page, args, conversationId, updatedByClient) {
|
||||
return page.evaluate(async ({ projectId, conversationId, updatedByClient }) => {
|
||||
const workspaceResponse = await fetch(`/v1/workbench/workspace?projectId=${encodeURIComponent(projectId)}`, { credentials: "same-origin" });
|
||||
const workspaceBody = await workspaceResponse.json().catch(() => null);
|
||||
const workspaceId = workspaceBody?.workspace?.workspaceId;
|
||||
if (!workspaceId) return { ok: false, phase: "workspace", status: workspaceResponse.status, workspaceId: null, error: workspaceBody?.error ?? null };
|
||||
const selectResponse = await fetch(`/v1/workbench/workspace/${encodeURIComponent(workspaceId)}/select-conversation`, {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ projectId, conversationId, updatedByClient })
|
||||
});
|
||||
const selectBody = await selectResponse.json().catch(() => null);
|
||||
return {
|
||||
ok: selectResponse.ok,
|
||||
phase: "select-conversation",
|
||||
status: selectResponse.status,
|
||||
workspaceId,
|
||||
conversationId: selectBody?.workspace?.selectedConversationId ?? conversationId,
|
||||
revision: selectBody?.workspace?.revision ?? null,
|
||||
error: selectBody?.error ?? null
|
||||
};
|
||||
}, { projectId: args.projectId, conversationId, updatedByClient });
|
||||
}
|
||||
|
||||
function freshSessionAlignment(before, after) {
|
||||
const previousConversationId = firstNonEmpty(before?.routeConversationId, before?.api?.workspace?.selectedConversationId, before?.activeTab?.conversationId);
|
||||
const conversationId = firstNonEmpty(after?.routeConversationId, after?.api?.workspace?.selectedConversationId, after?.activeTab?.conversationId);
|
||||
const selectedConversationId = after?.api?.workspace?.selectedConversationId ?? null;
|
||||
const routeConversationId = after?.routeConversationId ?? null;
|
||||
const apiConversation = conversationId ? after?.api?.conversations?.items?.find((item) => item.conversationId === conversationId) ?? null : null;
|
||||
const routeMatches = Boolean(routeConversationId && conversationId && routeConversationId === conversationId);
|
||||
const workspaceMatches = Boolean(selectedConversationId && conversationId && selectedConversationId === conversationId);
|
||||
const apiConversationPresent = Boolean(apiConversation);
|
||||
const activeTabMatches = !after?.activeTab?.conversationId || after.activeTab.conversationId === conversationId;
|
||||
const changed = Boolean(conversationId && conversationId !== previousConversationId);
|
||||
const commandReady = after?.command?.inputDisabled === false;
|
||||
const empty = after?.messageCount === 0;
|
||||
const ok = changed && routeMatches && workspaceMatches && apiConversationPresent && activeTabMatches && commandReady && empty;
|
||||
const reason = ok ? null
|
||||
: !changed ? "fresh-session-conversation-not-changed"
|
||||
: !routeMatches ? "fresh-session-route-not-aligned"
|
||||
: !workspaceMatches ? "fresh-session-workspace-not-aligned"
|
||||
: !apiConversationPresent ? "fresh-session-conversation-api-missing"
|
||||
: !activeTabMatches ? "fresh-session-active-tab-not-aligned"
|
||||
: !commandReady ? "fresh-session-command-not-ready"
|
||||
: "fresh-session-not-empty";
|
||||
return { ok, reason, previousConversationId, conversationId, routeConversationId, selectedConversationId, apiConversationPresent, activeTabConversationId: after?.activeTab?.conversationId ?? null, commandReady, command: after?.command ?? null, messageCount: after?.messageCount ?? null };
|
||||
}
|
||||
|
||||
async function waitForSessionShellReady(page, args, actions) {
|
||||
const timeoutMs = Math.min(args.timeoutMs, 30000);
|
||||
const before = await collectSessionState(page);
|
||||
const before = await collectSessionState(page, args);
|
||||
const ready = await page.waitForFunction(() => {
|
||||
const statusText = document.querySelector("#session-status")?.textContent?.trim() ?? "";
|
||||
const workspace = document.querySelector("#workspace");
|
||||
const create = document.querySelector("#session-create");
|
||||
return Boolean(workspace && create && !create.disabled && statusText && !/加载中|等待 workspace/u.test(statusText));
|
||||
}, null, { timeout: timeoutMs }).catch(() => null);
|
||||
const after = await collectSessionState(page);
|
||||
const after = await collectSessionState(page, args);
|
||||
const result = { action: "session-shell-ready", ready: Boolean(ready), timeoutMs, before, after };
|
||||
actions.push(result);
|
||||
if (!ready) throw new Error(`session shell did not finish initial loading: ${JSON.stringify(result)}`);
|
||||
return after;
|
||||
}
|
||||
|
||||
async function collectSessionState(page) {
|
||||
return page.evaluate(() => {
|
||||
async function collectSessionState(page, args = null) {
|
||||
const dom = await page.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);
|
||||
return {
|
||||
finalUrl: window.location.href,
|
||||
locationPath: window.location.pathname,
|
||||
routeConversationId: routeMatch ? decodeURIComponent(routeMatch[1]) : null,
|
||||
statusText: document.querySelector("#session-status")?.textContent?.trim() ?? null,
|
||||
activeTabTitle: activeTab?.querySelector("strong")?.textContent?.trim() ?? activeTab?.getAttribute("title") ?? null,
|
||||
activeTab: activeTab ? {
|
||||
title: activeTab.querySelector("strong")?.textContent?.trim() ?? activeTab.getAttribute("title") ?? null,
|
||||
conversationId: activeTab.getAttribute("data-conversation-id") ?? null,
|
||||
sessionId: activeTab.getAttribute("data-session-id") ?? null,
|
||||
status: activeTab.getAttribute("data-status") ?? null,
|
||||
running: activeTab.getAttribute("data-running") ?? null
|
||||
} : null,
|
||||
tabCount: document.querySelectorAll(".session-tab").length,
|
||||
messageCount: document.querySelectorAll(".message-card").length,
|
||||
emptyVisible: Boolean(document.querySelector("[data-conversation-empty]")),
|
||||
@@ -661,6 +774,108 @@ async function collectSessionState(page) {
|
||||
}
|
||||
};
|
||||
});
|
||||
if (!args) return dom;
|
||||
const workspace = await fetchJsonFromPage(page, `/v1/workbench/workspace?projectId=${encodeURIComponent(args.projectId)}`, { timeoutMs: 8000 });
|
||||
const conversations = await fetchJsonFromPage(page, `/v1/agent/conversations?projectId=${encodeURIComponent(args.projectId)}`, { timeoutMs: 8000 });
|
||||
return {
|
||||
...dom,
|
||||
api: {
|
||||
workspace: summarizeWorkspaceFetch(workspace),
|
||||
conversations: summarizeConversationsFetch(conversations)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchJsonFromPage(page, requestPath, options = {}) {
|
||||
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 8000;
|
||||
return page.evaluate(async ({ requestPath: path, timeoutMs: fetchTimeoutMs }) => {
|
||||
const startedAt = Date.now();
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), fetchTimeoutMs);
|
||||
try {
|
||||
const response = await fetch(path, { credentials: "same-origin", headers: { accept: "application/json" }, signal: controller.signal });
|
||||
const text = await response.text();
|
||||
let json = null;
|
||||
let parseError = null;
|
||||
try {
|
||||
json = text ? JSON.parse(text) : null;
|
||||
} catch (error) {
|
||||
parseError = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
return {
|
||||
ok: response.ok && parseError === null,
|
||||
path,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
contentType: response.headers.get("content-type"),
|
||||
bodyPreview: text.slice(0, 500),
|
||||
json,
|
||||
parseError,
|
||||
error: parseError
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
path,
|
||||
status: null,
|
||||
statusText: null,
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
contentType: null,
|
||||
bodyPreview: null,
|
||||
json: null,
|
||||
parseError: null,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
errorName: error instanceof Error ? error.name : null
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}, { requestPath, timeoutMs });
|
||||
}
|
||||
|
||||
function summarizeWorkspaceFetch(fetchResult) {
|
||||
const workspace = fetchResult?.json?.workspace ?? fetchResult?.json?.data?.workspace ?? null;
|
||||
const selected = workspace?.selectedConversation ?? null;
|
||||
return {
|
||||
ok: fetchResult?.ok === true,
|
||||
path: fetchResult?.path ?? null,
|
||||
status: fetchResult?.status ?? null,
|
||||
error: fetchResult?.error ?? null,
|
||||
bodyPreview: fetchResult?.ok === true ? null : fetchResult?.bodyPreview ?? null,
|
||||
workspaceId: workspace?.workspaceId ?? null,
|
||||
revision: workspace?.revision ?? null,
|
||||
selectedConversationId: workspace?.selectedConversationId ?? selected?.conversationId ?? null,
|
||||
selectedAgentSessionId: workspace?.selectedAgentSessionId ?? selected?.sessionId ?? null,
|
||||
activeTraceId: workspace?.activeTraceId ?? selected?.lastTraceId ?? null,
|
||||
selectedConversationStatus: selected?.status ?? workspace?.sessionStatus ?? null,
|
||||
selectedMessagesCount: Array.isArray(selected?.messages) ? selected.messages.length : selected?.messagesCount ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeConversationsFetch(fetchResult) {
|
||||
const conversations = Array.isArray(fetchResult?.json?.conversations)
|
||||
? fetchResult.json.conversations
|
||||
: Array.isArray(fetchResult?.json?.data?.conversations)
|
||||
? fetchResult.json.data.conversations
|
||||
: [];
|
||||
return {
|
||||
ok: fetchResult?.ok === true,
|
||||
path: fetchResult?.path ?? null,
|
||||
status: fetchResult?.status ?? null,
|
||||
error: fetchResult?.error ?? null,
|
||||
bodyPreview: fetchResult?.ok === true ? null : fetchResult?.bodyPreview ?? null,
|
||||
count: conversations.length,
|
||||
items: conversations.slice(0, 16).map((item) => ({
|
||||
conversationId: item?.conversationId ?? null,
|
||||
sessionId: item?.sessionId ?? null,
|
||||
threadId: item?.threadId ?? null,
|
||||
status: item?.status ?? null,
|
||||
messagesCount: Array.isArray(item?.messages) ? item.messages.length : item?.messagesCount ?? null,
|
||||
lastTraceId: item?.lastTraceId ?? null,
|
||||
updatedAt: item?.updatedAt ?? null
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
async function submitPrompt(page, args, actions) {
|
||||
@@ -672,9 +887,10 @@ async function submitPrompt(page, args, actions) {
|
||||
return input?.value === expected && send && !send.disabled;
|
||||
}, args.message, { timeout: Math.min(args.timeoutMs, 5000) });
|
||||
const afterFill = await collectCommandState(page);
|
||||
const submittedAt = new Date().toISOString();
|
||||
await page.locator("#command-send").click();
|
||||
const afterClick = await collectCommandState(page);
|
||||
actions.push({ action: "submit-prompt", chars: args.message.length, readyBefore, afterFill, afterClick });
|
||||
actions.push({ action: "submit-prompt", chars: args.message.length, submittedAt, readyBefore, afterFill, afterClick });
|
||||
await page.waitForTimeout(args.waitAfterSubmitMs);
|
||||
}
|
||||
|
||||
@@ -682,14 +898,14 @@ async function sampleTraceTimeline(page, args, actions) {
|
||||
const samples = [];
|
||||
for (let index = 0; index < args.traceSampleCount; index += 1) {
|
||||
if (index > 0 && args.traceSampleIntervalMs > 0) await page.waitForTimeout(args.traceSampleIntervalMs);
|
||||
samples.push(await collectTraceTimelineSample(page, index));
|
||||
samples.push(await collectTraceTimelineSample(page, args, index));
|
||||
}
|
||||
actions.push({ action: "trace-interval-samples", count: samples.length, intervalMs: args.traceSampleIntervalMs, samples });
|
||||
return samples;
|
||||
}
|
||||
|
||||
async function collectTraceTimelineSample(page, index) {
|
||||
return page.evaluate((sampleIndex) => {
|
||||
async function collectTraceTimelineSample(page, args, index) {
|
||||
const dom = await page.evaluate((sampleIndex) => {
|
||||
const cardRole = (card) => card.getAttribute("data-role") ?? card.getAttribute("data-message-role") ?? [...card.classList].find((item) => item === "message-agent" || item === "message-user" || item === "message-system")?.replace(/^message-/, "") ?? null;
|
||||
const cardStatus = (card) => card.getAttribute("data-status") ?? card.getAttribute("data-message-status") ?? null;
|
||||
const cards = [...document.querySelectorAll(".message-card")];
|
||||
@@ -714,6 +930,155 @@ async function collectTraceTimelineSample(page, index) {
|
||||
finalTextChars: finalResponse?.textContent?.trim().length ?? 0
|
||||
};
|
||||
}, index);
|
||||
const session = await collectSessionState(page, args);
|
||||
const conversationId = firstNonEmpty(session.routeConversationId, session.api?.workspace?.selectedConversationId, session.activeTab?.conversationId);
|
||||
const conversation = conversationId ? session.api?.conversations?.items?.find((item) => item.conversationId === conversationId) ?? null : null;
|
||||
const traceId = firstNonEmpty(conversation?.lastTraceId, session.api?.workspace?.activeTraceId);
|
||||
const traceFetch = traceId
|
||||
? await fetchTraceSummaryWithRetry(page, args, traceId)
|
||||
: { ok: false, traceId: null, degradedReason: "trace-id-missing", attempts: [] };
|
||||
return { ...dom, conversationId, sessionId: conversation?.sessionId ?? session.activeTab?.sessionId ?? null, traceId, traceFetch };
|
||||
}
|
||||
|
||||
async function fetchTraceSummaryWithRetry(page, args, traceId) {
|
||||
const path = `/v1/agent/traces/${encodeURIComponent(traceId)}?projectId=${encodeURIComponent(args.projectId)}&sinceSeq=0&limit=20`;
|
||||
const attempts = [];
|
||||
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
||||
const fetched = await fetchJsonFromPage(page, path, { timeoutMs: 8000 });
|
||||
const summary = summarizeTraceFetch(fetched, traceId, attempt);
|
||||
attempts.push(summary);
|
||||
if (summary.ok) return { ...summary, attempts };
|
||||
if (attempt < 3) await page.waitForTimeout(500 * attempt);
|
||||
}
|
||||
const last = attempts.at(-1) ?? { ok: false, traceId, degradedReason: "trace-fetch-failed" };
|
||||
return { ...last, attempts, degradedReason: last.degradedReason ?? "trace-fetch-failed" };
|
||||
}
|
||||
|
||||
function summarizeTraceFetch(fetchResult, expectedTraceId, attempt) {
|
||||
const body = fetchResult?.json?.data ?? fetchResult?.json ?? null;
|
||||
const trace = body?.trace ?? body?.runnerTrace ?? body;
|
||||
const events = Array.isArray(trace?.events) ? trace.events : Array.isArray(body?.events) ? body.events : [];
|
||||
const latest = events.at(-1) ?? null;
|
||||
const ok = fetchResult?.ok === true;
|
||||
return {
|
||||
ok,
|
||||
attempt,
|
||||
path: fetchResult?.path ?? null,
|
||||
httpStatus: fetchResult?.status ?? null,
|
||||
elapsedMs: fetchResult?.elapsedMs ?? null,
|
||||
traceId: trace?.traceId ?? body?.traceId ?? expectedTraceId,
|
||||
traceStatus: trace?.status ?? body?.status ?? null,
|
||||
running: trace?.running ?? body?.running ?? null,
|
||||
terminal: trace?.terminal ?? body?.terminal ?? null,
|
||||
eventCount: events.length,
|
||||
firstEventAt: events[0]?.createdAt ?? events[0]?.at ?? null,
|
||||
latestEventAt: latest?.createdAt ?? latest?.at ?? null,
|
||||
latestEventPreview: latest ? JSON.stringify(latest).replace(/\s+/gu, " ").slice(0, 300) : null,
|
||||
error: fetchResult?.error ?? null,
|
||||
bodyPreview: ok ? null : fetchResult?.bodyPreview ?? null,
|
||||
degradedReason: ok ? null : fetchResult?.status ? "trace-fetch-http-status" : "trace-fetch-failed"
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeSessionRun(actions, dom) {
|
||||
const fresh = [...actions].reverse().find((item) => item.action === "fresh-session") ?? null;
|
||||
const alignment = fresh?.alignment?.check ?? null;
|
||||
const sessionRail = dom?.sessionRail ?? null;
|
||||
const active = sessionRail?.active ?? null;
|
||||
return {
|
||||
freshSessionRequested: Boolean(fresh),
|
||||
freshSessionAligned: fresh ? fresh.aligned === true : null,
|
||||
conversationId: firstNonEmpty(alignment?.conversationId, active?.conversationId, dom?.routeConversationId),
|
||||
routeConversationId: alignment?.routeConversationId ?? dom?.routeConversationId ?? null,
|
||||
selectedConversationId: alignment?.selectedConversationId ?? null,
|
||||
sessionId: active?.sessionId ?? null,
|
||||
revision: fresh?.after?.api?.workspace?.revision ?? null,
|
||||
messageCount: dom?.messageCount ?? null,
|
||||
degradedReason: fresh && fresh.aligned !== true ? fresh.alignment?.reason ?? "fresh-session-not-aligned" : null,
|
||||
freshSession: fresh ? {
|
||||
settled: fresh.settled === true,
|
||||
aligned: fresh.aligned === true,
|
||||
reason: fresh.alignment?.reason ?? null,
|
||||
before: compactSessionState(fresh.before),
|
||||
after: compactSessionState(fresh.after)
|
||||
} : null
|
||||
};
|
||||
}
|
||||
|
||||
function compactSessionState(state) {
|
||||
if (!state) return null;
|
||||
return {
|
||||
finalUrl: state.finalUrl ?? null,
|
||||
routeConversationId: state.routeConversationId ?? null,
|
||||
selectedConversationId: state.api?.workspace?.selectedConversationId ?? null,
|
||||
revision: state.api?.workspace?.revision ?? null,
|
||||
activeTab: state.activeTab ?? null,
|
||||
messageCount: state.messageCount ?? null,
|
||||
command: state.command ?? null,
|
||||
conversationsCount: state.api?.conversations?.count ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeTraceRun(samples, dom, sessionSummary) {
|
||||
const firstDomRow = samples.find((sample) => Number(sample.rowCount ?? 0) > 0) ?? null;
|
||||
const firstRestEvent = samples.find((sample) => Number(sample.traceFetch?.eventCount ?? 0) > 0) ?? null;
|
||||
const last = samples.at(-1) ?? null;
|
||||
const failedFetches = samples.filter((sample) => sample.traceFetch && sample.traceFetch.ok !== true);
|
||||
return {
|
||||
requested: samples.length > 0,
|
||||
sampleCount: samples.length,
|
||||
conversationId: firstNonEmpty(last?.conversationId, sessionSummary?.conversationId),
|
||||
sessionId: firstNonEmpty(last?.sessionId, sessionSummary?.sessionId),
|
||||
traceId: firstNonEmpty(last?.traceId, firstRestEvent?.traceId, dom?.sessionRail?.traceIds?.[0]),
|
||||
finalAgentStatus: last?.agentStatus ?? null,
|
||||
finalTraceStatus: last?.traceStatus ?? null,
|
||||
finalDomRowCount: last?.rowCount ?? null,
|
||||
firstDomRowVisibleAt: firstDomRow?.sampledAt ?? null,
|
||||
firstDomRowPreview: firstDomRow?.latestRowPreview ?? null,
|
||||
firstRestEventVisibleAt: firstRestEvent?.sampledAt ?? null,
|
||||
firstRestEventCount: firstRestEvent?.traceFetch?.eventCount ?? null,
|
||||
restTraceOk: samples.some((sample) => sample.traceFetch?.ok === true),
|
||||
failedFetchCount: failedFetches.length,
|
||||
latestFetch: last?.traceFetch ?? null,
|
||||
degradedReason: samples.length === 0 ? null : failedFetches.length === samples.length ? failedFetches.at(-1)?.traceFetch?.degradedReason ?? "trace-fetch-failed" : null
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeProbePerformance(actions, startedAt, samples, traceSummary) {
|
||||
const submit = [...actions].reverse().find((item) => item.action === "submit-prompt") ?? null;
|
||||
const submittedAt = submit?.submittedAt ?? null;
|
||||
return {
|
||||
startedAt,
|
||||
submittedAt,
|
||||
firstDomTraceRowAt: traceSummary?.firstDomRowVisibleAt ?? null,
|
||||
firstRestTraceEventAt: traceSummary?.firstRestEventVisibleAt ?? null,
|
||||
submitToFirstDomTraceRowMs: submittedAt && traceSummary?.firstDomRowVisibleAt ? Math.max(0, new Date(traceSummary.firstDomRowVisibleAt).getTime() - new Date(submittedAt).getTime()) : null,
|
||||
submitToFirstRestTraceEventMs: submittedAt && traceSummary?.firstRestEventVisibleAt ? Math.max(0, new Date(traceSummary.firstRestEventVisibleAt).getTime() - new Date(submittedAt).getTime()) : null,
|
||||
totalSampleWindowMs: samples.length >= 2 ? Math.max(0, new Date(samples.at(-1).sampledAt).getTime() - new Date(samples[0].sampledAt).getTime()) : 0
|
||||
};
|
||||
}
|
||||
|
||||
function runProbeDegradedReason({ traceSummary, sessionSummary, promptValidation }) {
|
||||
if (sessionSummary?.degradedReason) return sessionSummary.degradedReason;
|
||||
if (traceSummary?.degradedReason) return traceSummary.degradedReason;
|
||||
if (promptValidation?.ok === false) return "prompt-validation-failed";
|
||||
return null;
|
||||
}
|
||||
|
||||
function classifyRunProbeError(error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (/fresh session/u.test(message)) return "fresh-session-not-aligned";
|
||||
if (/login|auth/u.test(message)) return "auth-login-failed";
|
||||
if (/Timeout|timed out|timeout/u.test(message)) return "browser-timeout";
|
||||
if (/fetch/u.test(message)) return "trace-fetch-failed";
|
||||
return "web-probe-failed";
|
||||
}
|
||||
|
||||
function firstNonEmpty(...values) {
|
||||
for (const value of values) {
|
||||
if (typeof value === "string" && value.trim()) return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function waitForCommandReady(page, args, actions) {
|
||||
@@ -868,6 +1233,7 @@ async function collectDomEvidence(page, args) {
|
||||
const conversations = Array.isArray(conversationBody?.conversations) ? conversationBody.conversations : [];
|
||||
const sessionTabs = [...document.querySelectorAll("#session-tabs .session-tab")];
|
||||
const activeSessionTab = sessionTabs.find((item) => item.getAttribute("data-active") === "true") ?? null;
|
||||
const routeMatch = window.location.pathname.match(/\/workbench\/sessions\/([^/]+)/u) ?? window.location.pathname.match(/\/workspace\/sessions\/([^/]+)/u);
|
||||
const sessionIds = conversations.map((item) => item?.sessionId).filter((value) => typeof value === "string" && value).slice(0, 8);
|
||||
const traceIds = conversations.map((item) => item?.lastTraceId).filter((value) => typeof value === "string" && value).slice(0, 8);
|
||||
const scroll = workspace ? {
|
||||
@@ -880,6 +1246,7 @@ async function collectDomEvidence(page, args) {
|
||||
return {
|
||||
title: document.title,
|
||||
authState: document.body.getAttribute("data-auth-state"),
|
||||
routeConversationId: routeMatch ? decodeURIComponent(routeMatch[1]) : null,
|
||||
requiredSelectors: {
|
||||
workspace: Boolean(workspace),
|
||||
commandInput: Boolean(document.querySelector("#command-input")),
|
||||
@@ -890,6 +1257,13 @@ async function collectDomEvidence(page, args) {
|
||||
sessionRail: {
|
||||
tabCount: sessionTabs.length,
|
||||
activeTitle: activeSessionTab?.querySelector("strong")?.textContent?.trim() ?? null,
|
||||
active: activeSessionTab ? {
|
||||
title: activeSessionTab.querySelector("strong")?.textContent?.trim() ?? activeSessionTab.getAttribute("title") ?? null,
|
||||
conversationId: activeSessionTab.getAttribute("data-conversation-id") ?? null,
|
||||
sessionId: activeSessionTab.getAttribute("data-session-id") ?? null,
|
||||
status: activeSessionTab.getAttribute("data-status") ?? null,
|
||||
running: activeSessionTab.getAttribute("data-running") ?? null
|
||||
} : null,
|
||||
objectTitleCount: sessionTabs.map((item) => item.querySelector("strong")?.textContent?.trim() ?? "").filter((text) => text.includes("[object Object]")).length,
|
||||
titles: sessionTabs.map((item) => item.querySelector("strong")?.textContent?.trim() ?? "").slice(0, 8),
|
||||
sessionIds,
|
||||
|
||||
Reference in New Issue
Block a user