From c2629c5ebfca7734787d136f6e9953c4c2b5b7a6 Mon Sep 17 00:00:00 2001 From: lyon Date: Sat, 20 Jun 2026 00:04:23 +0800 Subject: [PATCH] fix: improve code agent trace visibility --- internal/cloud/otel-trace.ts | 11 +++++++++-- tools/src/hwlab-cli/trace-renderer.ts | 13 +++++++++++++ web/hwlab-cloud-web/src/stores/workbench.ts | 4 ++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/internal/cloud/otel-trace.ts b/internal/cloud/otel-trace.ts index 0652bfec..413c482f 100644 --- a/internal/cloud/otel-trace.ts +++ b/internal/cloud/otel-trace.ts @@ -1,6 +1,7 @@ import { createHash, randomBytes } from "node:crypto"; const OTLP_TIMEOUT_MS = 1500; +const MIN_VALID_EPOCH_MS = Date.UTC(2020, 0, 1); const ZERO_TRACE_ID = "00000000000000000000000000000000"; const ZERO_SPAN_ID = "0000000000000000"; @@ -36,8 +37,9 @@ export async function emitCodeAgentOtelSpan(name, traceId, env = process.env, op const endpoint = resolveOtlpTracesEndpoint(env); if (!endpoint || typeof fetch !== "function") return { ok: false, skipped: true, reason: "otlp-endpoint-missing", valuesPrinted: false }; const context = codeAgentOtelTraceContext(traceId); - const startedAtMs = Number.isFinite(Number(options.startTimeMs)) ? Number(options.startTimeMs) : Date.now(); - const endedAtMs = Number.isFinite(Number(options.endTimeMs)) ? Number(options.endTimeMs) : Date.now(); + const now = Date.now(); + const startedAtMs = epochUnixMs(options.startTimeMs, now); + const endedAtMs = epochUnixMs(options.endTimeMs, now); const spanId = nonZeroHex(String(options.spanId ?? randomBytes(8).toString("hex")).slice(0, 16), ZERO_SPAN_ID); const parentSpanId = typeof options.parentSpanId === "string" && /^[0-9a-f]{16}$/u.test(options.parentSpanId) ? options.parentSpanId : context.parentSpanId; const statusCode = options.status === "error" || options.error ? 2 : 1; @@ -120,6 +122,11 @@ function unixNano(ms) { return String(Math.trunc(ms * 1_000_000)); } +function epochUnixMs(value, fallback) { + const numeric = Number(value); + return Number.isFinite(numeric) && numeric >= MIN_VALID_EPOCH_MS ? numeric : fallback; +} + function firstNonEmpty(...values) { for (const value of values) { const text = String(value ?? "").trim(); diff --git a/tools/src/hwlab-cli/trace-renderer.ts b/tools/src/hwlab-cli/trace-renderer.ts index c19e8966..ae32e9f3 100644 --- a/tools/src/hwlab-cli/trace-renderer.ts +++ b/tools/src/hwlab-cli/trace-renderer.ts @@ -68,6 +68,8 @@ export function traceDisplayRows(trace: Record = {}, events: Tr if (lastAssistantRow) rows[lastAssistantRowIndex] = markAssistantRowTerminal(effectiveTrace, lastAssistantRow, completionEvent); else rows.push(traceCompletionSummaryRow(effectiveTrace, completionEvent)); } + const progressRow = traceBackendProgressSummaryRow(effectiveTrace, rows, events); + if (progressRow) rows.push(progressRow); if (rows.length > 0) return rows; if (events.length === 0) return []; return events.filter((event) => !isSuppressedTraceEvent(event)).map((event) => traceDisplayRow(effectiveTrace, event)); @@ -373,6 +375,17 @@ function traceNoiseSummaryRow(trace: Record, events: TraceEvent }; } +function traceBackendProgressSummaryRow(trace: Record, rows: TraceEventRow[], events: TraceEvent[]): TraceEventRow | null { + if (!Array.isArray(events) || events.length === 0) return null; + if (rows.some((row) => row.rowId.startsWith("tool:") || /助手|轮次完成/u.test(row.header))) return null; + const lastRenderedSeq = Math.max(0, ...rows.map((row) => row.seq ?? 0).filter(Number.isFinite)); + const hiddenProgress = events.filter((event) => { + const seq = numberOrNull(event.seq) ?? 0; + return seq > lastRenderedSeq && (isRequestTraceEvent(event) || isSetupTraceEvent(event) || isNoisyTraceEvent(event)); + }); + return hiddenProgress.length > 0 ? traceNoiseSummaryRow(trace, hiddenProgress) : null; +} + function traceDisplayRow(trace: Record, event: TraceEvent): TraceEventRow { const label = readableTraceLabel(event); return { diff --git a/web/hwlab-cloud-web/src/stores/workbench.ts b/web/hwlab-cloud-web/src/stores/workbench.ts index 0aa2f98a..2b41ca8d 100644 --- a/web/hwlab-cloud-web/src/stores/workbench.ts +++ b/web/hwlab-cloud-web/src/stores/workbench.ts @@ -366,6 +366,7 @@ export const useWorkbenchStore = defineStore("workbench", () => { function applyTurnStatusSnapshot(traceId: string, result: AgentChatResultResponse | TraceSnapshot): void { if (!shouldApplyActiveTraceAuthority(traceId, traceResultSessionId(result))) return; + recordActivity(`turn:${firstNonEmptyString(result.lastEventLabel, result.status, result.waitingFor, "status") ?? "status"}`); rememberTurnStatus(traceId, result); syncTurnStatusToMessage(traceId, result); } @@ -572,6 +573,8 @@ export const useWorkbenchStore = defineStore("workbench", () => { const authoritySessionId = traceResultSessionId(result); if (!shouldApplyActiveTraceAuthority(traceId, authoritySessionId)) return; const events = Array.isArray(result.events) ? result.events : Array.isArray(result.traceEvents) ? result.traceEvents : []; + const activityLabel = firstNonEmptyString(result.lastEventLabel, events.at(-1)?.label, events.at(-1)?.type, result.status); + if (events.length > 0 || result.terminal === true || isTerminalMessageStatus(result.status)) recordActivity(`trace:${activityLabel ?? "hydrated"}`); markWorkbenchTraceEventsReceived({ traceId, events, transport: "rest_gap" }); updateActiveMessages((source) => source.map((message) => { if (!shouldApplyTraceToMessage(message, traceId, authoritySessionId)) return message; @@ -912,6 +915,7 @@ export const useWorkbenchStore = defineStore("workbench", () => { function completeTrace(traceId: string, result: AgentChatResultResponse): void { const authoritySessionId = traceResultSessionId(result); if (!shouldApplyActiveTraceAuthority(traceId, authoritySessionId)) return; + recordActivity(`trace:terminal:${firstNonEmptyString(result.lastEventLabel, result.status, "completed") ?? "completed"}`); const terminalStatus = result.status === "completed" ? "completed" : statusFromResult(result.status); const finalText = finalResponseText(result.finalResponse); const errorText = agentErrorDisplayText(result.error);