fix: surface workbench projection diagnostics

This commit is contained in:
lyon
2026-06-19 11:04:27 +08:00
parent 3eb022522d
commit 88112c415f
19 changed files with 769 additions and 36 deletions
@@ -111,13 +111,14 @@ async function handleRequest(request: IncomingMessage, response: ServerResponse)
const traceId = decodeURIComponent(turnMatch[1] ?? "");
if (state.scenarioId === "completed-replay-detail-404" && traceId === "trc_completed") return json(response, 404, { ok: false, status: 404, error: { code: "turn_replay_unavailable" } });
if (traceId === state.staleTraceId) return json(response, 502, { ok: false, status: 502, error: { code: "upstream_unavailable", message: "stale trace is unavailable" } });
return json(response, 200, { ok: true, status: "ok", contractVersion: "workbench-read-model-v1", turn: turnPayload(traceId) });
return json(response, 200, workbenchTurnPayload(traceId));
}
const traceMatch = path.match(/^\/v1\/workbench\/traces\/([^/]+)\/events$/u);
if (traceMatch && method === "GET") {
const traceId = decodeURIComponent(traceMatch[1] ?? "");
if (state.scenarioId === "completed-replay-detail-404" && traceId === "trc_completed") return json(response, 404, { ok: false, status: 404, error: { code: "trace_replay_unavailable" } });
if (state.scenarioId === "trace-hydration-timeout-diagnostic" && traceId === "trc_trace_hydration_timeout") return json(response, 504, { ok: false, status: 504, error: { code: "trace_hydration_timeout", message: "Trace 更新超时,运行记录暂不可见。" } });
if (traceId === state.staleTraceId) return json(response, 502, { ok: false, status: 502, error: { code: "upstream_unavailable", message: "stale trace is unavailable" } });
return json(response, 200, workbenchTracePayload(traceId, url));
}
@@ -283,6 +284,18 @@ function createScenarioState(scenarioId: string): ScenarioState {
if (id === "progress-only-final-response") markRunningProgressOnly(sessions, traces);
if (id === "terminal-completed-no-final-response") markRunningNoFinalResponse(sessions, traces);
if (id === "tool-completed-projection-running") markToolCompletedProjectionRunning(sessions, traces);
if (id === "projection-degraded-diagnostics") {
sessions.unshift(projectionDegradedSession());
traces.trc_projection_degraded = projectionDegradedTrace();
}
if (id === "projection-sse-error") {
sessions.unshift(projectionSseErrorSession());
traces.trc_projection_sse_error = projectionSseErrorTrace();
}
if (id === "trace-hydration-timeout-diagnostic") {
sessions.unshift(traceHydrationTimeoutSession());
traces.trc_trace_hydration_timeout = traceHydrationTimeoutTrace();
}
if (id === "scroll-follow-long-trace") {
const session = scrollFollowSession();
sessions.unshift(session);
@@ -307,6 +320,12 @@ function createScenarioState(scenarioId: string): ScenarioState {
? "ses_terminal_empty"
: id === "progress-only-final-response" || id === "terminal-completed-no-final-response" || id === "tool-completed-projection-running"
? "ses_running"
: id === "projection-degraded-diagnostics"
? "ses_projection_degraded"
: id === "projection-sse-error"
? "ses_projection_sse_error"
: id === "trace-hydration-timeout-diagnostic"
? "ses_trace_hydration_timeout"
: id === "create-while-route-loading"
? "ses_running"
: base.selectedSessionId;
@@ -413,6 +432,95 @@ function toolCompletedProjectionRunningTrace(): JsonRecord {
};
}
function projectionDiagnostic(input: { code: string; message: string; health?: string; category?: string; layer?: string; traceId: string; runId?: string; commandId?: string; staleMs?: number }): JsonRecord {
return {
projectionStatus: "projecting",
projectionHealth: input.health ?? "degraded",
lastProjectedSeq: 137,
sourceRunId: input.runId ?? "run_projection_diag",
sourceCommandId: input.commandId ?? "cmd_projection_diag",
staleMs: input.staleMs ?? 12_345,
blocker: {
code: input.code,
layer: input.layer ?? "agentrun",
category: input.category ?? "upstream-timeout",
message: input.message,
retryable: true,
timeoutMs: 2500,
runId: input.runId ?? "run_projection_diag",
commandId: input.commandId ?? "cmd_projection_diag",
valuesPrinted: false
},
valuesRedacted: true
};
}
function projectionDegradedTrace(): JsonRecord {
const projection = projectionDiagnostic({ traceId: "trc_projection_degraded", code: "agentrun_result_timeout", message: "状态更新超时,最近投影 seq=137,AgentRun 结果暂不可见。" });
return { traceId: "trc_projection_degraded", status: "running", sessionId: "ses_projection_degraded", threadId: "thr_projection_degraded", turnId: "turn_projection_degraded", events: [], eventCount: 0, fullTraceLoaded: true, hasMore: false, projection, ...projection };
}
function projectionDegradedSession(): SessionRecord {
const now = new Date().toISOString();
const projection = projectionDiagnostic({ traceId: "trc_projection_degraded", code: "agentrun_result_timeout", message: "状态更新超时,最近投影 seq=137,AgentRun 结果暂不可见。" });
return {
sessionId: "ses_projection_degraded",
threadId: "thr_projection_degraded",
status: "running",
lastTraceId: "trc_projection_degraded",
updatedAt: now,
messageCount: 2,
firstUserMessagePreview: "projection degraded visibility",
turnSummary: { traceId: "trc_projection_degraded", status: "running", running: true, terminal: false, ...projection },
messages: [
{ id: "msg_projection_degraded_user", messageId: "msg_projection_degraded_user", role: "user", title: "用户", text: "projection degraded visibility", status: "sent", createdAt: now, sessionId: "ses_projection_degraded", threadId: "thr_projection_degraded", turnId: "turn_projection_degraded" },
{ id: "msg_projection_degraded_agent", messageId: "msg_projection_degraded_agent", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: now, sessionId: "ses_projection_degraded", threadId: "thr_projection_degraded", traceId: "trc_projection_degraded", turnId: "turn_projection_degraded", projection, runnerTrace: projectionDegradedTrace() }
]
};
}
function projectionSseErrorTrace(): JsonRecord {
return { traceId: "trc_projection_sse_error", status: "running", sessionId: "ses_projection_sse_error", threadId: "thr_projection_sse_error", turnId: "turn_projection_sse_error", events: [], eventCount: 0, fullTraceLoaded: false, hasMore: false };
}
function projectionSseErrorSession(): SessionRecord {
const now = new Date().toISOString();
return {
sessionId: "ses_projection_sse_error",
threadId: "thr_projection_sse_error",
status: "running",
lastTraceId: "trc_projection_sse_error",
updatedAt: now,
messageCount: 2,
firstUserMessagePreview: "projection SSE error visibility",
messages: [
{ id: "msg_projection_sse_error_user", messageId: "msg_projection_sse_error_user", role: "user", title: "用户", text: "projection SSE error visibility", status: "sent", createdAt: now, sessionId: "ses_projection_sse_error", threadId: "thr_projection_sse_error", turnId: "turn_projection_sse_error" },
{ id: "msg_projection_sse_error_agent", messageId: "msg_projection_sse_error_agent", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: now, sessionId: "ses_projection_sse_error", threadId: "thr_projection_sse_error", traceId: "trc_projection_sse_error", turnId: "turn_projection_sse_error", runnerTrace: projectionSseErrorTrace() }
]
};
}
function traceHydrationTimeoutTrace(): JsonRecord {
return { traceId: "trc_trace_hydration_timeout", status: "running", sessionId: "ses_trace_hydration_timeout", threadId: "thr_trace_hydration_timeout", turnId: "turn_trace_hydration_timeout", events: [], eventCount: 0, fullTraceLoaded: false, hasMore: true };
}
function traceHydrationTimeoutSession(): SessionRecord {
const now = new Date().toISOString();
return {
sessionId: "ses_trace_hydration_timeout",
threadId: "thr_trace_hydration_timeout",
status: "running",
lastTraceId: "trc_trace_hydration_timeout",
updatedAt: now,
messageCount: 2,
firstUserMessagePreview: "trace hydration timeout visibility",
messages: [
{ id: "msg_trace_hydration_timeout_user", messageId: "msg_trace_hydration_timeout_user", role: "user", title: "用户", text: "trace hydration timeout visibility", status: "sent", createdAt: now, sessionId: "ses_trace_hydration_timeout", threadId: "thr_trace_hydration_timeout", turnId: "turn_trace_hydration_timeout" },
{ id: "msg_trace_hydration_timeout_agent", messageId: "msg_trace_hydration_timeout_agent", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: now, sessionId: "ses_trace_hydration_timeout", threadId: "thr_trace_hydration_timeout", traceId: "trc_trace_hydration_timeout", turnId: "turn_trace_hydration_timeout", runnerTrace: traceHydrationTimeoutTrace() }
]
};
}
function noFinalRunningTrace(): JsonRecord {
const createdAt = new Date().toISOString();
return {
@@ -921,6 +1029,11 @@ function turnPayload(traceId: string): JsonRecord {
return { ...trace, traceId, status, running: ["running", "pending", "accepted"].includes(status), terminal: ["completed", "failed", "blocked", "timeout", "canceled"].includes(status) };
}
function workbenchTurnPayload(traceId: string): JsonRecord {
const turn = turnPayload(traceId);
return { ok: true, status: "ok", contractVersion: "workbench-read-model-v1", turn, ...projectionEnvelope(turn) };
}
function tracePayload(traceId: string, url: URL): JsonRecord {
const turn = turnPayload(traceId);
const events = Array.isArray(turn.events) ? turn.events as JsonRecord[] : [];
@@ -939,7 +1052,22 @@ function workbenchTracePayload(traceId: string, url: URL): JsonRecord {
if (state.scenarioId === "tool-completed-projection-running" && traceId === "trc_running") {
return { ok: true, status: "ok", contractVersion: "workbench-read-model-v1", traceId, sessionId: payload.sessionId ?? null, threadId: payload.threadId ?? null, traceStatus: "completed", events: payload.events, eventCount: payload.eventCount, hasMore: payload.hasMore, nextSeq: payload.nextSinceSeq, range: payload.range, fullTraceLoaded: payload.fullTraceLoaded, projectionStatus: "projecting", terminalEvidence: null, finalResponse: null, traceSummary: payload.traceSummary, retention: payload.retention };
}
return { ok: true, status: "ok", contractVersion: "workbench-read-model-v1", traceId, sessionId: payload.sessionId ?? null, threadId: payload.threadId ?? null, traceStatus: payload.status, events: payload.events, eventCount: payload.eventCount, hasMore: payload.hasMore, nextSeq: payload.nextSinceSeq, range: payload.range, fullTraceLoaded: payload.fullTraceLoaded, terminalEvidence: payload.terminalEvidence, finalResponse: payload.finalResponse, traceSummary: payload.traceSummary, retention: payload.retention };
return { ok: true, status: "ok", contractVersion: "workbench-read-model-v1", traceId, sessionId: payload.sessionId ?? null, threadId: payload.threadId ?? null, traceStatus: payload.status, events: payload.events, eventCount: payload.eventCount, hasMore: payload.hasMore, nextSeq: payload.nextSinceSeq, range: payload.range, fullTraceLoaded: payload.fullTraceLoaded, terminalEvidence: payload.terminalEvidence, finalResponse: payload.finalResponse, traceSummary: payload.traceSummary, retention: payload.retention, ...projectionEnvelope(payload) };
}
function projectionEnvelope(payload: JsonRecord): JsonRecord {
const projection = payload.projection && typeof payload.projection === "object" ? payload.projection as JsonRecord : null;
if (!projection) return {};
return {
projection,
projectionStatus: projection.projectionStatus,
projectionHealth: projection.projectionHealth,
lastProjectedSeq: projection.lastProjectedSeq,
sourceRunId: projection.sourceRunId,
sourceCommandId: projection.sourceCommandId,
staleMs: projection.staleMs,
blocker: projection.blocker
};
}
function sse(request: IncomingMessage, response: ServerResponse, url: URL): void {
@@ -947,6 +1075,14 @@ function sse(request: IncomingMessage, response: ServerResponse, url: URL): void
sseClients.add(response);
request.on("close", () => sseClients.delete(response));
writeSse(response, "workbench.connected", { type: "connected", sessionId: url.searchParams.get("sessionId") });
if (state.scenarioId === "projection-sse-error") {
const scenarioId = state.scenarioId;
setTimeout(() => {
if (state.scenarioId !== scenarioId) return;
const projection = projectionDiagnostic({ traceId: "trc_projection_sse_error", code: "workbench_sse_projection_error", message: "SSE 投影事件异常,实时状态暂不可见。", category: "realtime-error", layer: "workbench-sse", runId: "run_projection_sse_error", commandId: "cmd_projection_sse_error" });
writeSse(response, "workbench.error", { type: "error", sessionId: "ses_projection_sse_error", threadId: "thr_projection_sse_error", traceId: "trc_projection_sse_error", projection, ...projectionEnvelope({ projection }), error: projection.blocker });
}, 350);
}
if (state.scenarioId === "cross-session-late-events") {
const scenarioId = state.scenarioId;
setTimeout(() => {