diff --git a/packages/core/src/contracts/app.ts b/packages/core/src/contracts/app.ts index e190f811..0fa8afa8 100644 --- a/packages/core/src/contracts/app.ts +++ b/packages/core/src/contracts/app.ts @@ -2280,7 +2280,7 @@ export type AgentAnalysisSubagentRow = { export type AgentAnalysisTraceRunKind = "agent" | "llm" | "route" | "subagent" | "tool"; -export type AgentAnalysisTraceRunStatus = "error" | "success"; +export type AgentAnalysisTraceRunStatus = "error" | "partial" | "success"; export type AgentAnalysisTracePayloadPreview = { kind: "empty" | "json" | "text"; diff --git a/packages/core/src/observability/request-log-store.ts b/packages/core/src/observability/request-log-store.ts index 02605f6d..f5313972 100644 --- a/packages/core/src/observability/request-log-store.ts +++ b/packages/core/src/observability/request-log-store.ts @@ -2579,7 +2579,11 @@ function buildAgentTrace(requests: AnalyzedAgentRequest[]): AgentAnalysisTrace { outputTokens: totals.outputTokens, sessionId, startedAt: isoFromMs(startMs), - status: totals.errorCount > 0 ? "error" : "success", + status: totals.errorCount === 0 + ? "success" + : totals.errorCount === totals.requestCount + ? "error" + : "partial", totalTokens: totals.totalTokens } ]; diff --git a/packages/core/test/integration/observability/request-log-store.test.mjs b/packages/core/test/integration/observability/request-log-store.test.mjs index 9850cb85..5c7d4d82 100644 --- a/packages/core/test/integration/observability/request-log-store.test.mjs +++ b/packages/core/test/integration/observability/request-log-store.test.mjs @@ -987,6 +987,10 @@ test("RequestLogStore analyzes agent sessions and exposes trace payloads", async assert.equal(selected.selectedSession?.trace.toolRunCount, 1); assert.equal(selected.selectedSession?.trace.llmRunCount, 1); assert.equal(selected.selectedSession?.trace.runs.some((run) => run.toolName === "read_file"), true); + assert.equal( + selected.selectedSession?.trace.runs.find((run) => run.id === selected.selectedSession?.trace.rootRunId)?.status, + "success" + ); const inputPayload = await store.getTracePayload({ callId: "call-read", @@ -1010,6 +1014,88 @@ test("RequestLogStore analyzes agent sessions and exposes trace payloads", async } }); +test("RequestLogStore distinguishes partial session failures from failed sessions", async () => { + const dir = mkdtempSync(path.join(tmpdir(), "ccr-request-log-agent-status-test-")); + try { + const store = new RequestLogStore(path.join(dir, "request-logs.sqlite")); + const baseTime = Date.now() - 5000; + + async function recordAgentRequest({ offsetMs, requestId, sessionId, statusCode }) { + const startedAtMs = baseTime + offsetMs; + await store.record({ + completedAt: new Date(startedAtMs + 100).toISOString(), + durationMs: 100, + ...(statusCode >= 400 ? { error: "upstream request failed" } : {}), + method: "POST", + path: "/v1/chat/completions", + providerName: "test-provider", + providerProtocol: "openai_chat_completions", + requestBody: Buffer.from(JSON.stringify({ + messages: [{ content: "continue task", role: "user" }], + model: "gpt-test", + session_id: sessionId + }), "utf8"), + requestHeaders: { + "content-type": "application/json", + "user-agent": "openai-codex test", + "x-codex-session-id": sessionId + }, + requestId, + responseBodyText: JSON.stringify({ model: "gpt-test" }), + responseHeaders: { "content-type": "application/json" }, + startedAt: new Date(startedAtMs).toISOString(), + statusCode, + url: "http://127.0.0.1:3456/v1/chat/completions" + }); + } + + await recordAgentRequest({ + offsetMs: 0, + requestId: "mixed-failed", + sessionId: "session-mixed", + statusCode: 502 + }); + await recordAgentRequest({ + offsetMs: 1000, + requestId: "mixed-recovered", + sessionId: "session-mixed", + statusCode: 200 + }); + await recordAgentRequest({ + offsetMs: 2000, + requestId: "failed-only", + sessionId: "session-failed", + statusCode: 502 + }); + + const mixed = await store.analyze({ + range: "30d", + sessionAgent: "codex", + sessionId: "session-mixed" + }); + const mixedTrace = mixed.selectedSession?.trace; + assert.equal( + mixedTrace?.runs.find((run) => run.id === mixedTrace.rootRunId)?.status, + "partial" + ); + assert.equal(mixedTrace?.runs.some((run) => run.kind === "llm" && run.status === "error"), true); + assert.equal(mixedTrace?.runs.some((run) => run.kind === "llm" && run.status === "success"), true); + + const failed = await store.analyze({ + range: "30d", + sessionAgent: "codex", + sessionId: "session-failed" + }); + const failedTrace = failed.selectedSession?.trace; + assert.equal( + failedTrace?.runs.find((run) => run.id === failedTrace.rootRunId)?.status, + "error" + ); + } finally { + rmSync(dir, { force: true, recursive: true }); + } +}); + test("RequestLogStore agent analysis cache ratio denominator includes cache tokens when total tokens omit cache", async () => { const dir = mkdtempSync(path.join(tmpdir(), "ccr-request-log-cache-ratio-test-")); try { diff --git a/packages/ui/src/pages/home/components/dashboard.tsx b/packages/ui/src/pages/home/components/dashboard.tsx index 39ed82a5..4d293566 100644 --- a/packages/ui/src/pages/home/components/dashboard.tsx +++ b/packages/ui/src/pages/home/components/dashboard.tsx @@ -3557,19 +3557,22 @@ function AgentSessionDetailCard({ const agentListSurfaceClassName = "rounded-md border border-border/70 bg-card/70 shadow-[0_1px_2px_rgba(15,23,42,0.04)]"; const agentListFrameClassName = cn("overflow-auto", agentListSurfaceClassName); const agentListTableClassName = "w-full border-collapse text-left text-[11px]"; -const agentListHeadClassName = "sticky top-0 z-10 border-b border-border/70 bg-muted/80 text-muted-foreground backdrop-blur"; +const agentListHeadClassName = "sticky top-0 z-10 border-b border-border/70 bg-muted/80 text-muted-foreground backdrop-blur [&_th]:min-w-[64px] [&_th]:whitespace-nowrap"; const agentListBodyClassName = "divide-y divide-border/50"; function agentListRowClassName({ danger, - selected + selected, + warning }: { danger?: boolean; selected?: boolean; + warning?: boolean; } = {}) { return cn( "bg-card/40 transition-colors hover:bg-muted/30", danger && "bg-rose-500/5 hover:bg-rose-500/10", + warning && "bg-amber-500/5 hover:bg-amber-500/10", selected && "bg-teal-500/10 shadow-[inset_2px_0_0_rgba(20,184,166,0.7)] hover:bg-teal-500/15" ); } @@ -3613,7 +3616,10 @@ function AgentTracePanel({ trace }: { trace: AgentTraceDetail }) {
{trace.runs.map((run) => ( -| {t("Session")} | @@ -4054,6 +4074,8 @@ function AgentSessionsCard({{t("Tools")} | {t("Subagents")} | {t("Errors")} | +{t("Cache rate")} | +{t("Cost")} | {t("Models")} | {t("Providers")} | {t("UA")} | @@ -4077,6 +4099,8 @@ function AgentSessionsCard({{formatCompactNumber(session.toolCallCount)} | {formatCompactNumber(session.subagentCallCount)} | {formatCompactNumber(session.errorCount)} | +{formatPercent(session.cacheRatio)} | +{formatUsdCost(session.costUsd)} | {session.models.join(", ") || "-"} | {session.providers.join(", ") || "-"} | {compactUserAgent(session.userAgent)} | diff --git a/packages/ui/src/pages/home/shared/i18n.tsx b/packages/ui/src/pages/home/shared/i18n.tsx index 9ccb8e25..ab08f431 100644 --- a/packages/ui/src/pages/home/shared/i18n.tsx +++ b/packages/ui/src/pages/home/shared/i18n.tsx @@ -1045,6 +1045,7 @@ export const appCopy: Record
|---|