fix: detect workbench recovery authority fanout

This commit is contained in:
Codex
2026-07-08 19:32:56 +02:00
parent bd4722c9a8
commit 2b0778f21f
9 changed files with 509 additions and 13 deletions
@@ -152,6 +152,33 @@ export function nodeWebObserveAnalyzerFindingsSource(): string {
rootCauseConfidence: "high",
valuesRedacted: true
});
const recoveryAuthority = workbenchInPlaceProjectionLag.recoveryAuthority || {};
const recoveryAuthoritySummary = recoveryAuthority.summary || {};
const forbiddenRecoveryCount = Number(recoveryAuthoritySummary.forbiddenAutomaticRecoveryCount ?? 0);
const legacyFanoutWindowCount = Number(recoveryAuthoritySummary.legacyFanoutWindowCount ?? 0);
if (forbiddenRecoveryCount > 0 || legacyFanoutWindowCount > 0) findings.push({
id: "workbench-automatic-recovery-fanout-authority",
severity: "red",
summary: "Workbench automatic recovery used direct trace/session/turn fan-out instead of sync replay authority",
count: forbiddenRecoveryCount + legacyFanoutWindowCount,
forbiddenAutomaticRecoveryCount: forbiddenRecoveryCount,
legacyFanoutWindowCount,
syncReplayEvidenceCount: Number(recoveryAuthoritySummary.syncReplayEvidenceCount ?? 0),
syncReplayAppliedCount: Number(recoveryAuthoritySummary.syncReplayAppliedCount ?? 0),
recoveryAuthority: {
summary: recoveryAuthoritySummary,
forbiddenAutomaticRecovery: Array.isArray(recoveryAuthority.forbiddenAutomaticRecovery) ? recoveryAuthority.forbiddenAutomaticRecovery.slice(0, 12) : [],
legacyFanoutWindows: Array.isArray(recoveryAuthority.legacyFanoutWindows) ? recoveryAuthority.legacyFanoutWindows.slice(0, 12) : [],
syncEvidence: Array.isArray(recoveryAuthority.syncEvidence) ? recoveryAuthority.syncEvidence.slice(-12) : [],
observabilityGaps: Array.isArray(recoveryAuthority.observabilityGaps) ? recoveryAuthority.observabilityGaps.slice(0, 8) : [],
valuesRedacted: true
},
sourceOfTruth: "Workbench sync replay / entity version / terminal seal authority; explicit detail/history reads are allowed but automatic recovery must not hydrate direct REST fan-out",
rootCause: "workbench_automatic_recovery_legacy_fanout",
rootCauseStatus: "confirmed-from-web-performance-runtime-diagnostics-and-network-artifacts",
rootCauseConfidence: "high",
valuesRedacted: true
});
const turnStateTriad = sampleMetrics?.workbenchTurnStateTriad || {};
const turnStateTriadSummary = turnStateTriad.summary || {};
const turnStateTriadRows = [
@@ -701,7 +701,7 @@ function extractWebPerformanceRuntimeDiagnostics(network, promptTimes) {
for (const event of events) {
if (!event || typeof event !== "object") continue;
const diagnosticCode = limitText(event.diagnosticCode || event.code || "", 120);
const reason = limitText(event.reason || "", 120);
const reason = limitText(event.rootCause || event.reason || "", 120);
const eventType = limitText(event.eventType || event.type || event.kind || "", 120);
if (!diagnosticCode && !reason && !eventType) continue;
rows.push({
@@ -721,6 +721,17 @@ function extractWebPerformanceRuntimeDiagnostics(network, promptTimes) {
diagnosticCode,
reason,
module: limitText(event.module || "", 120),
recoveryAction: limitText(event.recoveryAction || "", 160),
contractVersion: limitText(event.contractVersion || "", 80),
realtimeAuthority: limitText(event.realtimeAuthority || "", 120),
sinceOutboxSeq: numberOrNull(event.sinceOutboxSeq),
syncCursorOutboxSeq: numberOrNull(event.syncCursorOutboxSeq),
entityFamilyCount: numberOrNull(event.entityFamilyCount),
entityFamilies: limitText(event.entityFamilies || "", 160),
maxEntityVersion: numberOrNull(event.maxEntityVersion),
projectionRevision: limitText(event.projectionRevision || "", 120),
terminalSeal: typeof event.terminalSeal === "boolean" ? event.terminalSeal : null,
detailProjection: typeof event.detailProjection === "boolean" ? event.detailProjection : null,
traceId: event.traceId ?? null,
sessionIdHash: event.sessionIdHash ?? null,
eventIdHash: event.eventIdHash ?? null,
@@ -823,6 +834,17 @@ function groupWebPerformanceRuntimeDiagnostics(events) {
ts: item.ts || null,
traceId: item.traceId || null,
eventCount: item.eventCount ?? null,
recoveryAction: item.recoveryAction ?? null,
sinceOutboxSeq: item.sinceOutboxSeq ?? null,
syncCursorOutboxSeq: item.syncCursorOutboxSeq ?? null,
contractVersion: item.contractVersion ?? null,
realtimeAuthority: item.realtimeAuthority ?? null,
entityFamilyCount: item.entityFamilyCount ?? null,
entityFamilies: item.entityFamilies ?? null,
maxEntityVersion: item.maxEntityVersion ?? null,
projectionRevision: item.projectionRevision ?? null,
terminalSeal: item.terminalSeal ?? null,
detailProjection: item.detailProjection ?? null,
deliveredCount: item.deliveredCount ?? null,
chunkCount: item.chunkCount ?? null,
flushDurationMs: item.flushDurationMs ?? null,
@@ -469,6 +469,7 @@ function prioritizeFindings(findings) {
if (id.startsWith("frontend-long") || id.startsWith("frontend-event-loop-gap") || id === "frontend-cpu-profile-hotspots") return 0;
if (id === "page-performance-slow-same-origin-api") return 0;
if (id === "session-rail-title-fallback-majority") return 0.5;
if (id === "workbench-automatic-recovery-fanout-authority") return 0.52;
if (id === "workbench-turn-state-triad-inconsistent") return 0.55;
if (id.startsWith("workbench-terminal-")) return 0.6;
if (id.startsWith("code-agent-card-")) return 0.8;
@@ -6,9 +6,11 @@ export function nodeWebObserveAnalyzerWorkbenchTriadSource(): string {
const canarySessionIds = sessionInvariantCanarySessionIds(control);
const terminalTraceMissing = detectWorkbenchTerminalTraceMissing(samples, canarySessionIds);
const terminalApiDomLag = detectWorkbenchTerminalApiDomLag(samples, network, canarySessionIds);
const recoveryAuthority = detectWorkbenchRecoveryAuthority(network, control);
return {
terminalTraceMissing,
terminalApiDomLag,
recoveryAuthority,
valuesRedacted: true
};
}
@@ -260,6 +262,268 @@ function isWorkbenchPathSample(sample) {
return /\/workbench(?:\/|$)/u.test(String(sample?.path || sample?.url || ""));
}
function detectWorkbenchRecoveryAuthority(network, control = []) {
void control;
const diagnostics = workbenchRecoveryDiagnosticsFromNetwork(network);
const endpointReads = workbenchRecoveryEndpointReads(network);
const syncEvidence = workbenchSyncEvidenceFromNetwork(network, diagnostics);
const forbiddenAutomaticRecovery = diagnostics
.filter(isForbiddenWorkbenchRecoveryDiagnostic)
.map(compactWorkbenchRecoveryDiagnostic);
const legacyFanoutWindows = workbenchLegacyRecoveryFanoutWindows(endpointReads, diagnostics, syncEvidence);
const observabilityGaps = [];
if (diagnostics.length > 0 && syncEvidence.length === 0) {
observabilityGaps.push({
id: "workbench-sync-replay-evidence-missing",
severity: "amber",
summary: "Workbench recovery diagnostics were captured but no sync replay diagnostic or /v1/workbench/sync network row was present in the artifact window",
diagnosticCount: diagnostics.length,
valuesRedacted: true
});
}
return {
summary: {
diagnosticCount: diagnostics.length,
forbiddenAutomaticRecoveryCount: forbiddenAutomaticRecovery.length,
legacyFanoutWindowCount: legacyFanoutWindows.length,
syncReplayEvidenceCount: syncEvidence.length,
syncReplayAppliedCount: syncEvidence.filter((item) => item.code === "workbench_sync_replay_applied").length,
syncNetworkReadCount: endpointReads.filter((item) => item.kind === "workbench-sync-replay").length,
oldEndpointReadCount: endpointReads.filter((item) => item.legacyRead === true).length,
observabilityGapCount: observabilityGaps.length,
valuesRedacted: true
},
forbiddenAutomaticRecovery: forbiddenAutomaticRecovery.slice(0, 20),
legacyFanoutWindows: legacyFanoutWindows.slice(0, 20),
syncEvidence: syncEvidence.slice(-20),
diagnostics: diagnostics.slice(-20).map(compactWorkbenchRecoveryDiagnostic),
observabilityGaps,
valuesRedacted: true
};
}
function workbenchRecoveryDiagnosticsFromNetwork(network) {
const rows = [];
for (const item of Array.isArray(network) ? network : []) {
const payload = item?.webPerformancePayload && typeof item.webPerformancePayload === "object" ? item.webPerformancePayload : null;
if (!payload || payload.parseStatus !== "parsed" || payload.schemaVersion !== "hwlab-web-performance-v2") continue;
const events = Array.isArray(payload.events) ? payload.events : [];
for (const event of events) {
if (!event || typeof event !== "object") continue;
const eventType = firstString(event.eventType, event.type, event.kind);
const diagnosticCode = firstString(event.diagnosticCode, event.code, event.errorName);
const module = firstString(event.module);
const recoveryAction = firstString(event.recoveryAction);
const rootCause = firstString(event.rootCause);
const source = firstString(event.source);
const looksRelevant = eventType === "runtime_diagnostic"
|| Boolean(diagnosticCode && /workbench|terminal|recovery|sync|hydrate|fanout/iu.test(diagnosticCode))
|| Boolean(module && /workbench/iu.test(module))
|| Boolean(recoveryAction);
if (!looksRelevant) continue;
rows.push({
ts: item.ts ?? event.ts ?? null,
tsMs: Date.parse(item.ts ?? event.ts ?? ""),
pageRole: item.pageRole ?? null,
pageId: item.pageId ?? null,
urlPath: urlPath(item.url),
eventType,
module,
diagnosticCode,
reason: rootCause ?? firstString(event.reason),
recoveryAction,
source,
contractVersion: firstString(event.contractVersion),
realtimeAuthority: firstString(event.realtimeAuthority),
eventCount: numberOrNull(event.eventCount),
sinceOutboxSeq: numberOrNull(event.sinceOutboxSeq),
syncCursorOutboxSeq: numberOrNull(event.syncCursorOutboxSeq),
entityFamilyCount: numberOrNull(event.entityFamilyCount),
entityFamilies: firstString(event.entityFamilies),
maxEntityVersion: numberOrNull(event.maxEntityVersion),
projectionRevision: firstString(event.projectionRevision),
terminalSeal: typeof event.terminalSeal === "boolean" ? event.terminalSeal : null,
detailProjection: typeof event.detailProjection === "boolean" ? event.detailProjection : null,
valuesRedacted: true
});
}
}
return rows.sort((left, right) => String(left.ts || "").localeCompare(String(right.ts || "")));
}
function workbenchSyncEvidenceFromNetwork(network, diagnostics) {
const rows = [];
for (const item of Array.isArray(network) ? network : []) {
if (item?.observerInitiated === true) continue;
const route = workbenchRecoveryNetworkRoute(item);
const summary = objectValue(item?.bodySummary);
if (route.kind !== "workbench-sync-replay" && String(summary?.pathKind || "") !== "workbench-sync-replay") continue;
rows.push({
ts: item.ts ?? null,
tsMs: Date.parse(item.ts || ""),
source: "network",
method: String(item.method || "GET").toUpperCase(),
status: item.status ?? null,
path: route.path,
code: "workbench_sync_network_read",
sinceOutboxSeq: numberOrNull(route.since ?? summary?.sinceOutboxSeq),
syncCursorOutboxSeq: numberOrNull(summary?.cursorOutboxSeq ?? summary?.syncCursorOutboxSeq),
eventCount: numberOrNull(summary?.eventCount ?? summary?.traceEventLikeCount),
contractVersion: firstString(summary?.contractVersion),
realtimeAuthority: firstString(summary?.realtimeAuthority),
entityFamilyCount: numberOrNull(summary?.entityFamilyCount),
maxEntityVersion: numberOrNull(summary?.maxEntityVersion),
terminalSeal: typeof summary?.terminalSeal === "boolean" ? summary.terminalSeal : null,
valuesRedacted: true
});
}
for (const item of Array.isArray(diagnostics) ? diagnostics : []) {
if (item.diagnosticCode !== "workbench_sync_replay_applied" && item.module !== "workbench-sync-replay") continue;
rows.push({
ts: item.ts ?? null,
tsMs: item.tsMs,
source: "web-performance-runtime-diagnostic",
code: item.diagnosticCode || "workbench_sync_replay_applied",
reason: item.reason ?? null,
eventCount: item.eventCount,
sinceOutboxSeq: item.sinceOutboxSeq,
syncCursorOutboxSeq: item.syncCursorOutboxSeq,
contractVersion: item.contractVersion,
realtimeAuthority: item.realtimeAuthority,
entityFamilyCount: item.entityFamilyCount,
entityFamilies: item.entityFamilies,
maxEntityVersion: item.maxEntityVersion,
projectionRevision: item.projectionRevision,
terminalSeal: item.terminalSeal,
detailProjection: item.detailProjection,
valuesRedacted: true
});
}
return rows.sort((left, right) => String(left.ts || "").localeCompare(String(right.ts || "")));
}
function workbenchRecoveryEndpointReads(network) {
const rows = [];
for (const item of Array.isArray(network) ? network : []) {
if (item?.observerInitiated === true) continue;
if (item?.type !== "request" && item?.type !== "response" && item?.type !== "requestfailed") continue;
const route = workbenchRecoveryNetworkRoute(item);
if (!route.kind) continue;
rows.push({
ts: item.ts ?? null,
tsMs: Date.parse(item.ts || ""),
type: item.type ?? null,
method: String(item.method || "GET").toUpperCase(),
status: item.status ?? null,
kind: route.kind,
legacyRead: route.legacyRead,
path: route.path,
traceId: route.traceId,
sessionIdPrefix: route.sessionId ? String(route.sessionId).slice(0, 12) : null,
valuesRedacted: true
});
}
return rows.sort((left, right) => Number(left.tsMs || 0) - Number(right.tsMs || 0));
}
function workbenchRecoveryNetworkRoute(item) {
const rawUrl = item?.url || item?.requestUrl || "";
const parsed = parseApiDomLagUrl(rawUrl);
const rawPath = String(parsed.rawPath || parsed.path || "");
const path = String(parsed.path || rawPath);
const sessionId = workbenchRecoveryQueryParam(rawUrl, "sessionId") || workbenchRecoveryQueryParam(rawUrl, "includeSessionId") || parsed.sessionId || null;
const traceId = workbenchRecoveryQueryParam(rawUrl, "traceId") || parsed.traceId || null;
const since = workbenchRecoveryQueryParam(rawUrl, "since");
if (rawPath === "/v1/workbench/sync" || path === "/v1/workbench/sync") return { kind: "workbench-sync-replay", legacyRead: false, path: "/v1/workbench/sync", traceId, sessionId, since, valuesRedacted: true };
let match = rawPath.match(/^\/v1\/workbench\/turns\/([^/]+)$/u);
if (match) return { kind: "workbench-turn", legacyRead: true, path: "/v1/workbench/turns/:id", traceId: match[1], sessionId, since, valuesRedacted: true };
match = rawPath.match(/^\/v1\/workbench\/sessions\/([^/]+)\/messages$/u);
if (match) return { kind: "workbench-session-messages", legacyRead: true, path: "/v1/workbench/sessions/:id/messages", traceId, sessionId: match[1], since, valuesRedacted: true };
match = rawPath.match(/^\/v1\/workbench\/traces\/([^/]+)\/events$/u);
if (match) return { kind: "workbench-trace-events", legacyRead: true, path: "/v1/workbench/traces/:id/events", traceId: match[1], sessionId, since, valuesRedacted: true };
return { kind: null, legacyRead: false, path, traceId: null, sessionId: null, since, valuesRedacted: true };
}
function workbenchRecoveryQueryParam(value, key) {
try {
const parsed = new URL(String(value || "http://invalid.local/"), "http://invalid.local/");
return parsed.searchParams.get(key) || null;
} catch {
return null;
}
}
function workbenchLegacyRecoveryFanoutWindows(endpointReads, diagnostics, syncEvidence) {
const rows = [];
const recoveryDiagnostics = (Array.isArray(diagnostics) ? diagnostics : []).filter(workbenchRecoveryDiagnosticLooksAutomatic);
for (const diagnostic of recoveryDiagnostics) {
const tsMs = Number(diagnostic.tsMs);
if (!Number.isFinite(tsMs)) continue;
const nearby = (Array.isArray(endpointReads) ? endpointReads : [])
.filter((item) => item.legacyRead === true && Number.isFinite(Number(item.tsMs)) && Math.abs(Number(item.tsMs) - tsMs) <= 15_000);
const kinds = uniqueSorted(nearby.map((item) => item.kind).filter(Boolean));
if (kinds.length < 2) continue;
const syncNearby = (Array.isArray(syncEvidence) ? syncEvidence : []).some((item) => Number.isFinite(Number(item.tsMs)) && Math.abs(Number(item.tsMs) - tsMs) <= 20_000);
rows.push({
ts: diagnostic.ts,
diagnostic: compactWorkbenchRecoveryDiagnostic(diagnostic),
oldEndpointKinds: kinds,
oldEndpointReadCount: nearby.length,
syncEvidenceNearby: syncNearby,
endpointTail: nearby.slice(-8).map((item) => ({
ts: item.ts,
type: item.type,
kind: item.kind,
method: item.method,
status: item.status,
path: item.path,
valuesRedacted: true
})),
valuesRedacted: true
});
}
return rows;
}
function isForbiddenWorkbenchRecoveryDiagnostic(item) {
const action = [item?.recoveryAction, item?.diagnosticCode, item?.source].filter(Boolean).join(" ").toLowerCase();
if (!action) return false;
if (/hydrate[-_]trace[-_]events|refresh[-_]session[-_]messages|refresh[-_]turn[-_]status/u.test(action)) return true;
if (item?.recoveryAction && /active[-_]rest[-_]gap|trace[-_]hydration|terminal[-_]rest|session[-_]detail/u.test(action)) return true;
return /legacy[-_]?fanout|automatic[-_]?recovery[-_]?fanout|direct[-_]?hydrate/u.test(action);
}
function workbenchRecoveryDiagnosticLooksAutomatic(item) {
const text = [item?.module, item?.diagnosticCode, item?.reason, item?.recoveryAction, item?.source].filter(Boolean).join(" ").toLowerCase();
return /recovery|sync[-_]?replay|stream[-_]?transport|sse[-_]?gap|hydrate[-_]?trace|active[-_]?rest[-_]?gap|refresh[-_]?session/u.test(text);
}
function compactWorkbenchRecoveryDiagnostic(item) {
if (!item) return null;
return {
ts: item.ts ?? null,
pageRole: item.pageRole ?? null,
pageId: item.pageId ?? null,
module: item.module ?? null,
diagnosticCode: item.diagnosticCode ?? null,
reason: item.reason ?? null,
recoveryAction: item.recoveryAction ?? null,
source: item.source ?? null,
eventCount: item.eventCount ?? null,
sinceOutboxSeq: item.sinceOutboxSeq ?? null,
syncCursorOutboxSeq: item.syncCursorOutboxSeq ?? null,
contractVersion: item.contractVersion ?? null,
realtimeAuthority: item.realtimeAuthority ?? null,
entityFamilyCount: item.entityFamilyCount ?? null,
entityFamilies: item.entityFamilies ?? null,
maxEntityVersion: item.maxEntityVersion ?? null,
projectionRevision: item.projectionRevision ?? null,
terminalSeal: item.terminalSeal ?? null,
detailProjection: item.detailProjection ?? null,
valuesRedacted: true
};
}
function workbenchTerminalTraceIdsFromDom(sample) {
const ids = new Set();
for (const groupName of ["turns", "messages"]) {
+32 -2
View File
@@ -585,6 +585,31 @@ function compactFinding(item){
rootCause:item.rootCause||null,
rootCauseStatus:item.rootCauseStatus||null,
rootCauseConfidence:item.rootCauseConfidence||null,
sourceOfTruth:item.sourceOfTruth?short(item.sourceOfTruth,180):null,
valuesRedacted:true
};
}
function recoveryAuthorityFromFindings(){
const finding=findingById('workbench-automatic-recovery-fanout-authority');
const authority=finding?.recoveryAuthority&&typeof finding.recoveryAuthority==='object'?finding.recoveryAuthority:null;
const summary=authority?.summary&&typeof authority.summary==='object'?authority.summary:null;
const syncEvidence=Array.isArray(authority?.syncEvidence)?authority.syncEvidence.slice(-6):[];
const forbidden=Array.isArray(authority?.forbiddenAutomaticRecovery)?authority.forbiddenAutomaticRecovery.slice(0,6):[];
const fanout=Array.isArray(authority?.legacyFanoutWindows)?authority.legacyFanoutWindows.slice(0,4):[];
const gaps=Array.isArray(authority?.observabilityGaps)?authority.observabilityGaps.slice(0,4):[];
return {
findingPresent:Boolean(finding),
forbiddenAutomaticRecoveryCount:Number(summary?.forbiddenAutomaticRecoveryCount??0),
legacyFanoutWindowCount:Number(summary?.legacyFanoutWindowCount??0),
syncReplayEvidenceCount:Number(summary?.syncReplayEvidenceCount??0),
syncReplayAppliedCount:Number(summary?.syncReplayAppliedCount??0),
syncNetworkReadCount:Number(summary?.syncNetworkReadCount??0),
oldEndpointReadCount:Number(summary?.oldEndpointReadCount??0),
observabilityGapCount:Number(summary?.observabilityGapCount??0),
latestSyncEvidence:syncEvidence.slice(-1)[0]||null,
forbiddenAutomaticRecovery:forbidden,
legacyFanoutWindows:fanout,
observabilityGaps:gaps,
valuesRedacted:true
};
}
@@ -646,6 +671,7 @@ function workbenchTriadView(rows){
const traceId=requestedTraceId||requestedRow?.traceId||rows.slice().reverse().find((row)=>row.traceId)?.traceId||traceIdsFromSamples(samples).slice(-1)[0]||null;
const row=rows.find((item)=>item.traceId===traceId)||requestedRow||rows.slice(-1)[0]||null;
const findings=[
compactFinding(findingById('workbench-automatic-recovery-fanout-authority')),
compactFinding(findingById('workbench-terminal-api-dom-not-refreshed-in-place')),
compactFinding(findingById('workbench-terminal-trace-not-hydrated-in-place')),
compactFinding(findingById('workbench-turn-state-triad-inconsistent')),
@@ -656,6 +682,7 @@ function workbenchTriadView(rows){
const networkEvidence=networkEvidenceForTrace(traceId);
const dom=traceDomSnapshot(traceId);
const freeze=browserFreezeEvidence();
const recoveryAuthority=recoveryAuthorityFromFindings();
const statusGap={
traceId,
turnSummaryStatus:row?.status??null,
@@ -669,7 +696,7 @@ function workbenchTriadView(rows){
valuesRedacted:true
};
const compactRow=row?{round:row.round??null,commandId:row.commandId??null,sessionId:row.sessionId??null,traceId:row.traceId??null,status:row.status??null,elapsedSeconds:row.elapsedSeconds??null,recentUpdateSeconds:row.recentUpdateSeconds??null,marks:row.marks??null,firstSeq:row.firstSeq??null,lastSeq:row.lastSeq??null,lastTs:row.lastTs??null,finalResponse:row.finalResponse??null,valuesRedacted:true}:null;
return {traceId,row:compactRow,dom,networkEvidence,freeze,findings, statusGap,renderedText:renderWorkbenchTriad({traceId,row:compactRow,dom,networkEvidence,freeze,findings,statusGap}),valuesRedacted:true};
return {traceId,row:compactRow,dom,networkEvidence,freeze,recoveryAuthority,findings, statusGap,renderedText:renderWorkbenchTriad({traceId,row:compactRow,dom,networkEvidence,freeze,recoveryAuthority,findings,statusGap}),valuesRedacted:true};
}
function renderWorkbenchTriad(input){
const lines=['Workbench triad offline diagnosis '+(manifest.jobId||'-'),'======================================================='];
@@ -680,6 +707,9 @@ function renderWorkbenchTriad(input){
lines.push('api evidence responses='+String(input.networkEvidence.responseCount)+' terminalResponses='+String(input.networkEvidence.terminalResponseCount)+' failed='+String(input.networkEvidence.failedCount)+' turnTerminal='+String(turnSummary.terminalEvidenceCount??'-')+' turnFinalBytes='+String(turnSummary.finalTextByteCount??'-')+' traceEvents='+String(traceSummary.traceEventLikeCount??'-')+' traceTerminal='+String(traceSummary.terminalEvidenceCount??'-'));
if(input.networkEvidence.latestTurnResponse) lines.push('latest turn API '+String(input.networkEvidence.latestTurnResponse.ts||'-')+' status='+String(input.networkEvidence.latestTurnResponse.status??'-')+' sample='+String(input.networkEvidence.latestTurnResponse.sampleSeq??'-')+' path='+String(input.networkEvidence.latestTurnResponse.urlPath||'-'));
if(input.networkEvidence.latestTraceEventsResponse) lines.push('latest trace events API '+String(input.networkEvidence.latestTraceEventsResponse.ts||'-')+' status='+String(input.networkEvidence.latestTraceEventsResponse.status??'-')+' sample='+String(input.networkEvidence.latestTraceEventsResponse.sampleSeq??'-')+' path='+String(input.networkEvidence.latestTraceEventsResponse.urlPath||'-'));
const recovery=input.recoveryAuthority||{};
lines.push('recovery authority forbidden='+String(recovery.forbiddenAutomaticRecoveryCount??0)+' legacyFanout='+String(recovery.legacyFanoutWindowCount??0)+' syncEvidence='+String(recovery.syncReplayEvidenceCount??0)+' syncApplied='+String(recovery.syncReplayAppliedCount??0)+' syncNetwork='+String(recovery.syncNetworkReadCount??0)+' oldReads='+String(recovery.oldEndpointReadCount??0)+' gaps='+String(recovery.observabilityGapCount??0));
if(recovery.latestSyncEvidence) lines.push('latest sync replay '+String(recovery.latestSyncEvidence.ts||'-')+' source='+String(recovery.latestSyncEvidence.source||'-')+' events='+String(recovery.latestSyncEvidence.eventCount??'-')+' since='+String(recovery.latestSyncEvidence.sinceOutboxSeq??'-')+' cursor='+String(recovery.latestSyncEvidence.syncCursorOutboxSeq??'-')+' authority='+String(recovery.latestSyncEvidence.realtimeAuthority||'-')+' terminalSeal='+String(recovery.latestSyncEvidence.terminalSeal??'-'));
if(input.freeze.latestBlocker) lines.push('browser freeze '+String(input.freeze.latestBlocker.ts||'-')+' sample='+String(input.freeze.latestBlocker.sampleSeq??'-')+' rootCause='+String(input.freeze.latestBlocker.rootCause||'-')+' status='+String(input.freeze.latestBlocker.rootCauseStatus||'-')+' fallbackAllowed='+String(input.freeze.latestBlocker.fallbackAllowed===true));
lines.push('','Findings');
if(input.findings.length===0) lines.push('-');
@@ -983,7 +1013,7 @@ if(view==='timeline'){
}
if(view==='workbench-triad'){
const triad=workbenchTriadView(rows);
console.log(JSON.stringify({ok:true,command:'web-probe-observe collect',view,stateDir:dir,artifactFileCount:files.length,skippedFileCount:skippedFiles.length,skippedFiles:skippedFiles.slice(0,8),traceId:triad.traceId,dom:triad.dom,networkEvidence:triad.networkEvidence,freeze:triad.freeze,findings:triad.findings,statusGap:triad.statusGap,renderedText:triad.renderedText,sourceFiles:['samples.jsonl','network.jsonl','browser-process.jsonl','analysis/report.json'],valuesRedacted:true}));
console.log(JSON.stringify({ok:true,command:'web-probe-observe collect',view,stateDir:dir,artifactFileCount:files.length,skippedFileCount:skippedFiles.length,skippedFiles:skippedFiles.slice(0,8),traceId:triad.traceId,dom:triad.dom,networkEvidence:triad.networkEvidence,freeze:triad.freeze,recoveryAuthority:triad.recoveryAuthority,findings:triad.findings,statusGap:triad.statusGap,renderedText:triad.renderedText,sourceFiles:['samples.jsonl','network.jsonl','browser-process.jsonl','analysis/report.json'],valuesRedacted:true}));
process.exit(0);
}
const frame=renderTraceFrame(selectSample(rows),rows);
@@ -65,6 +65,27 @@ const browserFreezePolicy = {
},
};
async function runObserveAnalyzer(files: Record<string, string>): Promise<Record<string, any>> {
const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-analyzer-"));
const analyzerPath = join(stateDir, "analyze.mjs");
await writeFile(analyzerPath, nodeWebObserveAnalyzerSource(), { mode: 0o700 });
for (const [name, content] of Object.entries(files)) {
await writeFile(join(stateDir, name), content.endsWith("\n") ? content : content + "\n");
}
const result = spawnSync("bun", [analyzerPath, stateDir], {
cwd: join(import.meta.dir, "../../.."),
env: {
...process.env,
UNIDESK_WEB_OBSERVE_ANALYZE_TAIL_SAMPLES: "0",
UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON: JSON.stringify(alertThresholds),
UNIDESK_WEB_OBSERVE_BROWSER_FREEZE_POLICY_JSON: JSON.stringify(browserFreezePolicy),
},
encoding: "utf8",
});
assert.equal(result.status, 0, result.stderr || result.stdout);
return JSON.parse(await readFile(join(stateDir, "analysis", "report.json"), "utf8"));
}
test("observe analyzer classifies stale completed session rail when a newer turn is running", async () => {
const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-analyzer-"));
const analyzerPath = join(stateDir, "analyze.mjs");
@@ -163,3 +184,105 @@ test("observe analyzer classifies stale completed session rail when a newer turn
assert.equal(report.sampleMetrics.workbenchTurnStateTriad.summary.invalidRowCount, 1);
assert.equal(report.sampleMetrics.workbenchTurnStateTriad.summary.cardFinalResponseMismatchCount, 0);
}, 20_000);
test("observe analyzer flags automatic recovery legacy fanout authority", async () => {
const samples = [
JSON.stringify({
seq: 1,
ts: "2026-07-01T15:21:25.000Z",
path: "/workbench/sessions/ses_auto",
url: "https://hwlab.example.test/workbench/sessions/ses_auto",
pageRole: "control",
pageId: "control-test",
routeSessionId: "ses_auto",
activeSessionId: "ses_auto",
turns: []
})
].join("\n") + "\n";
const network = [
JSON.stringify({
ts: "2026-07-01T15:21:26.000Z",
type: "response",
method: "POST",
status: 202,
url: "https://hwlab.example.test/v1/web-performance",
webPerformancePayload: {
parseStatus: "parsed",
schemaVersion: "hwlab-web-performance-v2",
events: [{
kind: "workbench_ui_event",
eventType: "runtime_diagnostic",
module: "workbench-stream-transport",
diagnosticCode: "workbench_recovery_action",
recoveryAction: "hydrate-trace-events",
rootCause: "sse-gap",
eventCount: 0
}]
}
}),
JSON.stringify({ ts: "2026-07-01T15:21:27.000Z", type: "response", method: "GET", status: 200, url: "https://hwlab.example.test/v1/workbench/turns/trc_auto", bodySummary: { pathKind: "workbench-turn", terminalEvidenceCount: 1, traceIds: ["trc_auto"] } }),
JSON.stringify({ ts: "2026-07-01T15:21:28.000Z", type: "response", method: "GET", status: 200, url: "https://hwlab.example.test/v1/workbench/sessions/ses_auto/messages", bodySummary: { pathKind: "workbench-session-messages", terminalEvidenceCount: 1, sessionIds: ["ses_auto"] } }),
JSON.stringify({ ts: "2026-07-01T15:21:29.000Z", type: "response", method: "GET", status: 200, url: "https://hwlab.example.test/v1/workbench/traces/trc_auto/events", bodySummary: { pathKind: "workbench-trace-events", traceEventLikeCount: 3, traceIds: ["trc_auto"] } }),
JSON.stringify({ ts: "2026-07-01T15:21:30.000Z", type: "response", method: "GET", status: 200, url: "https://hwlab.example.test/v1/workbench/sync?sessionId=ses_auto&traceId=trc_auto&since=7", bodySummary: { pathKind: "workbench-sync-replay", eventCount: 1, cursorOutboxSeq: 12, realtimeAuthority: "workbench-realtime-authority-v2", terminalSeal: true } })
].join("\n") + "\n";
const report = await runObserveAnalyzer({ "samples.jsonl": samples, "network.jsonl": network });
const finding = report.findings.find((item: Record<string, unknown>) => item.id === "workbench-automatic-recovery-fanout-authority");
assert.equal(finding?.rootCause, "workbench_automatic_recovery_legacy_fanout");
assert.equal(finding?.recoveryAuthority.summary.forbiddenAutomaticRecoveryCount, 1);
assert.equal(finding?.recoveryAuthority.summary.legacyFanoutWindowCount, 1);
assert.equal(finding?.recoveryAuthority.summary.syncReplayEvidenceCount, 1);
assert.equal(report.runtimeAlerts.webPerformanceRuntimeDiagnostics[0].recoveryAction, "hydrate-trace-events");
}, 20_000);
test("observe analyzer allows explicit detail trace reads when sync authority evidence exists", async () => {
const samples = JSON.stringify({
seq: 1,
ts: "2026-07-01T15:31:25.000Z",
path: "/workbench/sessions/ses_detail",
url: "https://hwlab.example.test/workbench/sessions/ses_detail",
pageRole: "control",
pageId: "control-test",
routeSessionId: "ses_detail",
activeSessionId: "ses_detail",
turns: []
}) + "\n";
const network = [
JSON.stringify({ ts: "2026-07-01T15:31:26.000Z", type: "response", method: "GET", status: 200, url: "https://hwlab.example.test/v1/workbench/traces/trc_detail/events", bodySummary: { pathKind: "workbench-trace-events", traceEventLikeCount: 2, traceIds: ["trc_detail"], authority: "trace-detail-only", detailProjection: true } }),
JSON.stringify({
ts: "2026-07-01T15:31:27.000Z",
type: "response",
method: "POST",
status: 202,
url: "https://hwlab.example.test/v1/web-performance",
webPerformancePayload: {
parseStatus: "parsed",
schemaVersion: "hwlab-web-performance-v2",
events: [{
kind: "workbench_ui_event",
eventType: "runtime_diagnostic",
module: "workbench-sync-replay",
diagnosticCode: "workbench_sync_replay_applied",
rootCause: "sse-gap",
eventCount: 1,
sinceOutboxSeq: 7,
syncCursorOutboxSeq: 12,
realtimeAuthority: "workbench-realtime-authority-v2",
entityFamilyCount: 1,
entityFamilies: "turn",
maxEntityVersion: 3,
projectionRevision: "rev_detail",
terminalSeal: true,
detailProjection: false
}]
}
})
].join("\n") + "\n";
const report = await runObserveAnalyzer({ "samples.jsonl": samples, "network.jsonl": network });
const finding = report.findings.find((item: Record<string, unknown>) => item.id === "workbench-automatic-recovery-fanout-authority");
assert.equal(finding, undefined);
assert.equal(report.runtimeAlerts.webPerformanceRuntimeDiagnostics[0].syncCursorOutboxSeq, 12);
}, 20_000);
@@ -253,7 +253,7 @@ export function compactSpanList(value: unknown, limit: number): unknown[] {
return {
name: span.name ?? null,
service: span.service ?? null,
attributes: compactRecord(attrs, ["failureKind", "terminalStatus", "status", "eventType", "idleMs", "waitingFor", "lastEventLabel", "http.route", "http.status_code", "http.response.status_code", "workbench.session_id", "workbench.trace_id", "workbench.turn_id", "workbench.read_model.route", "workbench.read_model.count", "workbench.read_model.family", "workbench.read_model.status", "workbench.read_model.reason", "hwlab.http.stage", "hwlab.http.phase", "hwlab.http.phase.outcome", "hwlab.live_builds.service_id", "hwlab.live_builds.service_kind", "hwlab.live_builds.external", "hwlab.live_builds.deploy_manifest_status", "hwlab.live_builds.artifact_catalog_status", "providerProfile", "model", "modelId", "providerModel", "code_agent.stage", "agent.chat.session_id", "agent.chat.provider_profile", "agent.chat.provider_profile_source", "agent.chat.model", "agent.chat.model_id", "agent.chat.provider_model", "defaultProviderProfile", "adapter", "adapterEnabled", "agentRunManagerHost", "agentRunRunnerNamespace", "agentRunSourceCommitPresent"]),
attributes: compactRecord(attrs, ["failureKind", "terminalStatus", "status", "eventType", "idleMs", "waitingFor", "lastEventLabel", "http.route", "http.status_code", "http.response.status_code", "workbench.session_id", "workbench.trace_id", "workbench.turn_id", "workbench.read_model.route", "workbench.read_model.count", "workbench.read_model.family", "workbench.read_model.status", "workbench.read_model.reason", "workbench.sync.contract_version", "workbench.sync.since_outbox_seq", "workbench.sync.cursor_outbox_seq", "workbench.sync.event_count", "workbench.sync.entity_family_count", "workbench.sync.entity_families", "workbench.sync.max_entity_version", "workbench.realtime.authority", "workbench.projection.revision", "workbench.terminal.seal", "workbench.detail.projection", "hwlab.http.stage", "hwlab.http.phase", "hwlab.http.phase.outcome", "hwlab.live_builds.service_id", "hwlab.live_builds.service_kind", "hwlab.live_builds.external", "hwlab.live_builds.deploy_manifest_status", "hwlab.live_builds.artifact_catalog_status", "providerProfile", "model", "modelId", "providerModel", "code_agent.stage", "agent.chat.session_id", "agent.chat.provider_profile", "agent.chat.provider_profile_source", "agent.chat.model", "agent.chat.model_id", "agent.chat.provider_model", "defaultProviderProfile", "adapter", "adapterEnabled", "agentRunManagerHost", "agentRunRunnerNamespace", "agentRunSourceCommitPresent"]),
};
});
}
@@ -120,6 +120,17 @@ export function spanDetail(span: Record<string, unknown>, maxLength: number): st
attrPart(attrs, "workbench.ui.resource_response_transfer_ms"),
attrPart(attrs, "workbench.ui.resource_next_hop_protocol"),
attrPart(attrs, "workbench.ui.resource_server_timing"),
attrPart(attrs, "workbench.sync.contract_version"),
attrPart(attrs, "workbench.sync.since_outbox_seq"),
attrPart(attrs, "workbench.sync.cursor_outbox_seq"),
attrPart(attrs, "workbench.sync.event_count"),
attrPart(attrs, "workbench.sync.entity_family_count"),
attrPart(attrs, "workbench.sync.entity_families"),
attrPart(attrs, "workbench.sync.max_entity_version"),
attrPart(attrs, "workbench.realtime.authority"),
attrPart(attrs, "workbench.projection.revision"),
attrPart(attrs, "workbench.terminal.seal"),
attrPart(attrs, "workbench.detail.projection"),
attrPart(attrs, "hwlab.http.stage"),
attrPart(attrs, "hwlab.http.phase"),
attrPart(attrs, "hwlab.http.phase.outcome"),
@@ -233,6 +233,17 @@ const knownTraceAttributeKeys = new Set([
"opencode.provider.sse.done_lines",
"opencode.provider.sse.json_errors",
"opencode.provider.sse.reasoning_only_choices_dropped",
"workbench.sync.contract_version",
"workbench.sync.since_outbox_seq",
"workbench.sync.cursor_outbox_seq",
"workbench.sync.event_count",
"workbench.sync.entity_family_count",
"workbench.sync.entity_families",
"workbench.sync.max_entity_version",
"workbench.realtime.authority",
"workbench.projection.revision",
"workbench.terminal.seal",
"workbench.detail.projection",
]);
function isKnownTraceAttributeKey(value: string): boolean {
@@ -432,7 +443,7 @@ export function renderTraceTable(input: {
formatTable(["NAME", "TRACE", "SESSION", "TURN", "STATUS", "EVENT", "PROJECTED", "SOURCE", "MESSAGE"], projectionRows.length > 0 ? projectionRows : [["-", "-", "-", "-", "-", "-", "-", "-", "-"]]),
"",
"Projection sync cursors:",
formatTable(["SERVICE", "TRACE", "SESSION", "AFTER", "END", "EVENTS", "RAW", "MAX", "LAST", "TERM"], projectionSyncRows.length > 0 ? projectionSyncRows : [["-", "-", "-", "-", "-", "-", "-", "-", "-", "-"]]),
formatTable(["SERVICE", "TRACE", "SESSION", "AFTER", "END", "EVENTS", "RAW", "MAX", "LAST", "TERM", "AUTH"], projectionSyncRows.length > 0 ? projectionSyncRows : [["-", "-", "-", "-", "-", "-", "-", "-", "-", "-", "-"]]),
"",
"Span name counts:",
formatTable(["NAME", "COUNT"], countRows.length > 0 ? countRows : [["-", "-"]]),
@@ -599,19 +610,26 @@ function traceProjectionRows(spans: Record<string, unknown>[]): string[][] {
}
function traceProjectionSyncRows(spans: Record<string, unknown>[]): string[][] {
const rows = spans.filter((span) => textValue(span.name) === "projection_sync").map((span) => {
const rows = spans.filter((span) => {
const name = textValue(span.name);
const attrs = asPlainRecord(span.attributes);
return name === "projection_sync"
|| name === "workbench.sync.replay"
|| spanColumnAttr(attrs, ["workbench.sync.contract_version", "workbench.sync.cursor_outbox_seq", "workbench.sync.event_count", "workbench.realtime.authority"]) !== "-";
}).map((span) => {
const attrs = asPlainRecord(span.attributes);
return [
shortenEnd(textValue(span.service), 18),
shortenMiddle(spanColumnAttr(attrs, ["traceId"]), 18),
shortenMiddle(spanColumnAttr(attrs, ["sessionId"]), 18),
shortenEnd(spanColumnAttr(attrs, ["afterSeq"]), 8),
shortenEnd(spanColumnAttr(attrs, ["endSeq"]), 8),
shortenEnd(spanColumnAttr(attrs, ["eventCount"]), 8),
shortenMiddle(spanColumnAttr(attrs, ["traceId", "workbench.trace_id"]), 18),
shortenMiddle(spanColumnAttr(attrs, ["sessionId", "workbench.session_id"]), 18),
shortenEnd(spanColumnAttr(attrs, ["afterSeq", "workbench.sync.since_outbox_seq"]), 8),
shortenEnd(spanColumnAttr(attrs, ["endSeq", "workbench.sync.cursor_outbox_seq"]), 8),
shortenEnd(spanColumnAttr(attrs, ["eventCount", "workbench.sync.event_count"]), 8),
shortenEnd(spanColumnAttr(attrs, ["rawEventCount"]), 8),
shortenEnd(spanColumnAttr(attrs, ["maxSeq"]), 8),
shortenEnd(spanColumnAttr(attrs, ["maxSeq", "workbench.sync.max_entity_version"]), 8),
shortenEnd(spanColumnAttr(attrs, ["traceLastSeq"]), 8),
shortenEnd(spanColumnAttr(attrs, ["terminalFromEvents", "terminalFromRawEvents", "terminalStatus"]), 12),
shortenEnd(spanColumnAttr(attrs, ["terminalFromEvents", "terminalFromRawEvents", "terminalStatus", "workbench.terminal.seal"]), 12),
shortenEnd(spanColumnAttr(attrs, ["workbench.realtime.authority", "workbench.sync.contract_version", "workbench.detail.projection"]), 18),
];
});
return dedupeRows(rows).slice(0, 16);