fix(web-probe): scope turn timing by page epoch (#803)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user