fix(web-probe): scope turn timing by page epoch (#803)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-24 07:43:07 +08:00
committed by GitHub
parent 089f764427
commit b9b237747d
2 changed files with 50 additions and 17 deletions
@@ -62,6 +62,8 @@ let terminalStatus = "starting";
let lastScreenshotAtMs = 0;
let auth = null;
let pageLoadSeq = 0;
let controlPageEpoch = 0;
let observerPageEpoch = 0;
let currentPageProvenance = null;
const jsonlRotation = { stamp: compactFileTimestamp(startedAt), files: [] };
@@ -361,21 +363,22 @@ async function syncObserverPageToControlSession(reason, explicitSessionId = null
const targetUrl = new URL(target, baseUrl).toString();
const beforeUrl = pageUrl(observerPage);
const beforeSessionId = routeSessionIdFromUrl(beforeUrl);
if (sessionId && beforeSessionId === sessionId && !forceRefresh) return { ok: true, reason, changed: false, observerRoundTrip: false, sessionId, beforeUrl, afterUrl: beforeUrl, pageRole: "observer", pageId: observerPageId };
if (sessionId && beforeSessionId === sessionId && !forceRefresh) return { ok: true, reason, changed: false, observerRoundTrip: false, sessionId, beforeUrl, afterUrl: beforeUrl, pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch };
observerPageEpoch += 1;
let status = null;
let statusText = null;
const response = await observerPage.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 45000 }).catch((error) => ({ observerGotoError: errorSummary(error) }));
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 };
if (response?.observerGotoError) return { ok: false, reason, changed: false, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, error: response.observerGotoError, valuesRedacted: true };
status = typeof response?.status === "function" ? response.status() : null;
statusText = typeof response?.statusText === "function" ? response.statusText() : null;
const readiness = await waitForTargetPageReady(observerPage, targetUrl, { timeoutMs: 15000 });
if (!readiness.ok) {
lastObserverRefreshAtMs = Date.now();
return { ok: false, reason, changed: true, observerRoundTrip: forceRefresh, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, httpStatus: status, statusText, readiness, hydration: null, failureKind: readiness.reason || "observer-target-not-ready", valuesRedacted: true };
return { ok: false, reason, changed: true, observerRoundTrip: forceRefresh, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, httpStatus: status, statusText, readiness, hydration: null, failureKind: readiness.reason || "observer-target-not-ready", valuesRedacted: true };
}
const hydration = await waitForWorkbenchSessionHydrated(observerPage, sessionId, { timeoutMs: 15000 });
lastObserverRefreshAtMs = Date.now();
return { ok: hydration.ok === true, reason, changed: true, observerRoundTrip: forceRefresh, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, httpStatus: status, statusText, readiness, hydration, failureKind: hydration.ok === true ? null : hydration.reason || "observer-session-hydration-failed", valuesRedacted: true };
return { ok: hydration.ok === true, reason, changed: true, observerRoundTrip: forceRefresh, sessionId: sessionId ?? null, targetPath: target, beforeUrl, afterUrl: pageUrl(observerPage), pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, httpStatus: status, statusText, readiness, hydration, failureKind: hydration.ok === true ? null : hydration.reason || "observer-session-hydration-failed", valuesRedacted: true };
}
async function waitForWorkbenchSessionHydrated(targetPage, sessionId, options = {}) {
@@ -1332,9 +1335,9 @@ async function preflightSummary() {
async function samplePage(reason, options = {}) {
if (options?.refreshObserver !== false) await maybeRefreshObserverPage(reason);
const groupSeq = sampleSeq + 1;
if (page && !page.isClosed()) await sampleOnePage(page, { reason, groupSeq, pageRole: "control", targetPageId: pageId });
if (page && !page.isClosed()) await sampleOnePage(page, { reason, groupSeq, pageRole: "control", targetPageId: pageId, pageEpoch: controlPageEpoch });
if (observerPage && !observerPage.isClosed()) {
await sampleOnePage(observerPage, { reason, groupSeq, pageRole: "observer", targetPageId: observerPageId }).catch((error) => appendJsonl(files.errors, eventRecord("observer-sample-error", { pageRole: "observer", pageId: observerPageId, error: errorSummary(error) })));
await sampleOnePage(observerPage, { reason, groupSeq, pageRole: "observer", targetPageId: observerPageId, pageEpoch: observerPageEpoch }).catch((error) => appendJsonl(files.errors, eventRecord("observer-sample-error", { pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, error: errorSummary(error) })));
}
if (options?.screenshot !== false && screenshotIntervalMs > 0 && Date.now() - lastScreenshotAtMs >= screenshotIntervalMs) {
await captureScreenshot("checkpoint", "jpeg").catch((error) => appendJsonl(files.errors, eventRecord("screenshot-error", { pageRole: "control", pageId, error: errorSummary(error) })));
@@ -1342,7 +1345,7 @@ async function samplePage(reason, options = {}) {
await writeHeartbeat({ status: terminalStatus });
}
async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPageId }) {
async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPageId, pageEpoch }) {
sampleSeq += 1;
const dom = await targetPage.evaluate(() => {
const trim = (value, limit = 500) => String(value || "").replace(/\s+/g, " ").trim().slice(0, limit);
@@ -1804,6 +1807,7 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
reason,
pageRole,
pageId: targetPageId,
pageEpoch: Number.isFinite(Number(pageEpoch)) ? Number(pageEpoch) : 0,
commandId: activeCommandId,
observerInitiated: false,
...digestDom(dom, pageRole),
@@ -2198,7 +2202,7 @@ console.log(JSON.stringify({
sampleMetrics: {
...recentWindow.sampleMetrics.summary,
rounds: recentWindow.sampleMetrics.rounds.slice(-8).map((item) => ({ promptIndex: item.promptIndex, promptTextHash: item.promptTextHash, sampleCount: item.sampleCount, firstSeq: item.firstSeq, lastSeq: item.lastSeq, lastTotalElapsedSeconds: item.lastTotalElapsedSeconds, lastRecentUpdateSeconds: item.lastRecentUpdateSeconds, loadingSamples: item.loadingSamples, maxLoadingCount: item.maxLoadingCount, loadingOwnerCount: item.loadingOwnerCount, diagnosticSamples: item.diagnosticSamples, terminalSamples: item.terminalSamples, finalTextSamples: item.finalTextSamples, turnTimingTotalElapsedZeroResetCount: item.turnTimingTotalElapsedZeroResetCount, turnTimingTotalElapsedForwardJumpCount: item.turnTimingTotalElapsedForwardJumpCount, turnTimingTotalElapsedForwardJumpMaxSeconds: item.turnTimingTotalElapsedForwardJumpMaxSeconds, turnTimingRecentUpdateJumpCount: item.turnTimingRecentUpdateJumpCount, turnTimingRecentUpdateMaxIncreaseSeconds: item.turnTimingRecentUpdateMaxIncreaseSeconds })),
turnColumns: recentWindow.sampleMetrics.turnColumns.slice(-12).map((item) => ({ label: item.label, source: item.source, pageRole: item.pageRole ?? null, pageId: item.pageId ?? null, promptIndex: item.promptIndex, lastPromptIndex: item.lastPromptIndex, firstSeq: item.firstSeq, lastSeq: item.lastSeq, traceId: item.traceId, messageId: item.messageId })),
turnColumns: recentWindow.sampleMetrics.turnColumns.slice(-12).map((item) => ({ label: item.label, source: item.source, pageRole: item.pageRole ?? null, pageId: item.pageId ?? null, pageEpoch: item.pageEpoch ?? null, promptIndex: item.promptIndex, lastPromptIndex: item.lastPromptIndex, firstSeq: item.firstSeq, lastSeq: item.lastSeq, traceId: item.traceId, messageId: item.messageId })),
loading: compactLoadingMetricsForOutput(recentWindow.sampleMetrics.loading),
sessionRailTitles: compactSessionRailTitleMetricsForOutput(recentWindow.sampleMetrics.sessionRailTitles),
},
@@ -2271,6 +2275,9 @@ console.log(JSON.stringify({
})),
turnTimingRecentUpdateJumps: recentWindow.sampleMetrics.turnTimingRecentUpdateSawtoothJumps.slice(0, 8).map((item) => ({
columnLabel: item.columnLabel ?? item.columnId ?? null,
pageRole: item.pageRole ?? null,
pageId: item.pageId ?? null,
pageEpoch: item.pageEpoch ?? null,
promptIndex: item.promptIndex ?? null,
traceId: item.traceId ?? null,
fromSeq: item.fromSeq ?? null,
@@ -2286,6 +2293,9 @@ console.log(JSON.stringify({
})),
turnTimingElapsedZeroResets: recentWindow.sampleMetrics.turnTimingElapsedZeroResets.slice(0, 8).map((item) => ({
columnLabel: item.columnLabel ?? item.columnId ?? null,
pageRole: item.pageRole ?? null,
pageId: item.pageId ?? null,
pageEpoch: item.pageEpoch ?? null,
promptIndex: item.promptIndex ?? null,
traceId: item.traceId ?? null,
fromSeq: item.fromSeq ?? null,
@@ -2299,6 +2309,9 @@ console.log(JSON.stringify({
})),
turnTimingTotalElapsedForwardJumps: recentWindow.sampleMetrics.turnTimingTotalElapsedForwardJumps.slice(0, 8).map((item) => ({
columnLabel: item.columnLabel ?? item.columnId ?? null,
pageRole: item.pageRole ?? null,
pageId: item.pageId ?? null,
pageEpoch: item.pageEpoch ?? null,
promptIndex: item.promptIndex ?? null,
traceId: item.traceId ?? null,
fromSeq: item.fromSeq ?? null,
@@ -4587,14 +4600,15 @@ function buildTurnTimingTable(samples, timeline) {
const domIndexes = rawMetrics.map((item) => Number(item.domIndex)).filter(Number.isFinite);
const maxDomIndex = domIndexes.length > 0 ? Math.max(...domIndexes) : null;
for (const rawMetric of rawMetrics) {
const scopedKey = turnTimingScopedMetricKey(rawMetric, sample);
const samplePromptIndex = Number(timelineItem.promptIndex ?? 0);
const evidencePromptIndex = inferTurnMetricPromptIndex(rawMetric, samplePromptIndex, maxDomIndex);
if (evidencePromptIndex !== null) {
const existingPromptIndex = promptAssignmentByKey.get(rawMetric.key);
if (existingPromptIndex === undefined || evidencePromptIndex > existingPromptIndex) promptAssignmentByKey.set(rawMetric.key, evidencePromptIndex);
const existingPromptIndex = promptAssignmentByKey.get(scopedKey);
if (existingPromptIndex === undefined || evidencePromptIndex > existingPromptIndex) promptAssignmentByKey.set(scopedKey, evidencePromptIndex);
}
const assignedPromptIndex = promptAssignmentByKey.get(rawMetric.key) ?? null;
const metric = { ...rawMetric, promptIndex: assignedPromptIndex, samplePromptIndex };
const assignedPromptIndex = promptAssignmentByKey.get(scopedKey) ?? null;
const metric = { ...rawMetric, key: scopedKey, baseKey: rawMetric.key, promptIndex: assignedPromptIndex, samplePromptIndex, pageEpoch: Number(sample?.pageEpoch ?? rawMetric.pageEpoch ?? 0) || 0 };
let column = registry.get(metric.key);
if (!column) {
column = {
@@ -4612,7 +4626,8 @@ function buildTurnTimingTable(samples, timeline) {
messageId: metric.messageId ?? null,
domIndex: metric.domIndex ?? null,
pageRole: metric.pageRole ?? sample.pageRole ?? null,
pageId: metric.pageId ?? sample.pageId ?? null
pageId: metric.pageId ?? sample.pageId ?? null,
pageEpoch: metric.pageEpoch ?? null
};
registry.set(metric.key, column);
columns.push(column);
@@ -4633,6 +4648,7 @@ function buildTurnTimingTable(samples, timeline) {
source: metric.source,
pageRole: metric.pageRole ?? sample.pageRole ?? null,
pageId: metric.pageId ?? sample.pageId ?? null,
pageEpoch: metric.pageEpoch ?? null,
sampleGroupSeq: sample.sampleGroupSeq ?? null,
traceId: metric.traceId ?? null,
messageId: metric.messageId ?? null,
@@ -4646,6 +4662,7 @@ function buildTurnTimingTable(samples, timeline) {
sampleGroupSeq: sample.sampleGroupSeq ?? null,
pageRole: sample.pageRole ?? null,
pageId: sample.pageId ?? null,
pageEpoch: Number(sample?.pageEpoch ?? 0) || 0,
promptIndex: timelineItem.promptIndex ?? 0,
routeSessionId: sample.routeSessionId ?? null,
activeSessionId: sample.activeSessionId ?? null,
@@ -4665,6 +4682,15 @@ function buildTurnTimingTable(samples, timeline) {
};
}
function turnTimingScopedMetricKey(metric, sample) {
return [
metric?.key ?? "unknown",
metric?.pageRole ?? sample?.pageRole ?? "unknown-role",
metric?.pageId ?? sample?.pageId ?? "unknown-page",
Number(sample?.pageEpoch ?? metric?.pageEpoch ?? 0) || 0
].join("|page:");
}
function inferTurnMetricPromptIndex(metric, samplePromptIndex, maxDomIndex) {
const promptIndex = Number(samplePromptIndex);
if (!Number.isFinite(promptIndex) || promptIndex <= 0) return null;
@@ -4715,6 +4741,7 @@ function detectTurnTimingNonMonotonic(columns, rows) {
source: cell.source ?? column.source ?? null,
pageRole: cell.pageRole ?? column.pageRole ?? null,
pageId: cell.pageId ?? column.pageId ?? null,
pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null,
valuesRedacted: true
};
anomalies.push(event);
@@ -4751,6 +4778,7 @@ function detectTurnTimingNonMonotonic(columns, rows) {
source: cell.source ?? column.source ?? null,
pageRole: cell.pageRole ?? column.pageRole ?? null,
pageId: cell.pageId ?? column.pageId ?? null,
pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null,
valuesRedacted: true
});
}
@@ -4779,6 +4807,7 @@ function detectTurnTimingNonMonotonic(columns, rows) {
source: cell.source ?? column.source ?? null,
pageRole: cell.pageRole ?? column.pageRole ?? null,
pageId: cell.pageId ?? column.pageId ?? null,
pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null,
valuesRedacted: true
});
}
@@ -4815,6 +4844,7 @@ function detectTurnTimingNonMonotonic(columns, rows) {
source: cell.source ?? column.source ?? null,
pageRole: cell.pageRole ?? column.pageRole ?? null,
pageId: cell.pageId ?? column.pageId ?? null,
pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null,
valuesRedacted: true
});
if (increase < 0) {
@@ -4838,6 +4868,7 @@ function detectTurnTimingNonMonotonic(columns, rows) {
source: cell.source ?? column.source ?? null,
pageRole: cell.pageRole ?? column.pageRole ?? null,
pageId: cell.pageId ?? column.pageId ?? null,
pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null,
valuesRedacted: true
});
}
@@ -4865,6 +4896,7 @@ function detectTurnTimingNonMonotonic(columns, rows) {
source: cell.source ?? column.source ?? null,
pageRole: cell.pageRole ?? column.pageRole ?? null,
pageId: cell.pageId ?? column.pageId ?? null,
pageEpoch: cell.pageEpoch ?? row.pageEpoch ?? column.pageEpoch ?? null,
valuesRedacted: true
});
}