fix: surface workbench projection diagnostics
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user