From c4ff24f60ad60e352a9cd0569045a917541f0a69 Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Thu, 4 Jun 2026 21:49:57 +0800 Subject: [PATCH] fix(v02): dedupe replayed trace tool rows (#860) Co-authored-by: Codex Agent --- tools/src/hwlab-cli/trace-renderer.ts | 21 ++++++++++++++ .../src/state/trace-renderer.test.ts | 29 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/tools/src/hwlab-cli/trace-renderer.ts b/tools/src/hwlab-cli/trace-renderer.ts index 1ffc363d..1b9df954 100644 --- a/tools/src/hwlab-cli/trace-renderer.ts +++ b/tools/src/hwlab-cli/trace-renderer.ts @@ -12,15 +12,27 @@ type TraceEvent = Record; export function traceDisplayRows(trace: Record = {}, events: TraceEvent[] = []): TraceEventRow[] { const rows: TraceEventRow[] = []; + const renderedSourceEvents = new Set(); + const renderedToolIdentities = new Set(); let requestRendered = false; let setupRendered = false; let lastAssistantRowIndex = -1; let completionEvent: TraceEvent | null = null; for (let index = 0; index < events.length; index += 1) { const event = events[index]; + const sourceEventKey = traceSourceEventKey(event); + if (sourceEventKey) { + if (renderedSourceEvents.has(sourceEventKey)) continue; + renderedSourceEvents.add(sourceEventKey); + } if (!event || isNoisyTraceEvent(event)) continue; if (isToolTraceEvent(event)) { if (isSupersededToolStart(event, events, index)) continue; + const identity = toolIdentity(event); + if (identity) { + if (renderedToolIdentities.has(identity)) continue; + renderedToolIdentities.add(identity); + } rows.push(traceToolCallRow(trace, event)); continue; } @@ -222,6 +234,14 @@ function toolIdentity(event: TraceEvent): string | null { return nonEmptyString(event.itemId ?? event.id ?? event.command); } +function traceSourceEventKey(event: TraceEvent | null | undefined): string | null { + if (!event || typeof event !== "object") return null; + const source = nonEmptyString(event.source); + const sourceSeq = nonEmptyString(event.sourceSeq); + if (!source || !sourceSeq) return null; + return `${source}:${sourceSeq}`; +} + function isSupersededToolStart(event: TraceEvent, events: TraceEvent[], index: number): boolean { if (!isToolStartTraceEvent(event)) return false; const identity = toolIdentity(event); @@ -233,6 +253,7 @@ function isNoisyTraceEvent(event: TraceEvent): boolean { const label = String(event.label ?? ""); if (isRequestTraceEvent(event) || isSetupTraceEvent(event) || isCompletionTraceEvent(event) || isTerminalAssistantTraceEvent(event) || isAssistantTraceEvent(event)) return false; if (/token_count|outputDelta:chunk/iu.test(label)) return true; + if (/^agentrun:output:(stdout|stderr)$/u.test(label)) return true; if (/^agentrun:backend:(run-created|command-created|runner-job-created|thread\/status\/changed|thread\/tokenUsage\/updated|account\/rateLimits\/updated|remoteControl\/status\/changed|configWarning|codex-app-server-closed|codex-app-server-notifications-suppressed|session-updated|command-terminal|backend-turn-finished|item\/agentMessage:(started|completed)|thread\/goal\/cleared)$/u.test(label)) return true; return event.type === "event" && !event.outputSummary && !event.message && !event.errorCode; } diff --git a/web/hwlab-cloud-web/src/state/trace-renderer.test.ts b/web/hwlab-cloud-web/src/state/trace-renderer.test.ts index 1d431c09..24e89e0e 100644 --- a/web/hwlab-cloud-web/src/state/trace-renderer.test.ts +++ b/web/hwlab-cloud-web/src/state/trace-renderer.test.ts @@ -65,3 +65,32 @@ test("web trace rows collapse tool start/completed and clean shell command escap assert.match(body, /exitCode=0/u); assert.doesNotMatch(text, /agentrun:tool:item\/started/u); }); + +test("web trace rows dedupe AgentRun replayed source events and keep one tool row", () => { + const events: Record[] = [ + { seq: 1, label: "agentrun:request:accepted", status: "accepted", createdAt: "2026-06-04T13:40:28.612Z" }, + { seq: 9, label: "agentrun:backend:run-claimed", type: "backend", status: "running", source: "agentrun", sourceSeq: 4, message: "run-claimed", createdAt: "2026-06-04T13:40:34.601Z" }, + { seq: 22, label: "agentrun:assistant:message", type: "assistant", status: "running", source: "agentrun", sourceSeq: 17, message: "我先确认 `hwpod` 可用。", createdAt: "2026-06-04T13:40:43.286Z" }, + { seq: 23, label: "item/commandExecution:started", type: "tool_call", toolName: "commandExecution", status: "started", source: "agentrun", sourceSeq: 18, itemId: "call_function_gsuuqbm1yp0s_1", command: "/bin/sh -lc 'command -v hwpod && hwpod profile list'", createdAt: "2026-06-04T13:40:43.458Z" }, + { seq: 24, label: "agentrun:output:stdout", type: "output", status: "running", source: "agentrun", sourceSeq: 19, message: "raw stdout should stay inside the completed tool row", createdAt: "2026-06-04T13:40:44.852Z" }, + { seq: 25, label: "item/commandExecution:completed", type: "tool_call", toolName: "commandExecution", status: "completed", source: "agentrun", sourceSeq: 20, itemId: "call_function_gsuuqbm1yp0s_1", command: "/bin/sh -lc 'command -v hwpod && hwpod profile list'", stdoutSummary: '{"ok":true,"action":"profile.list"}', exitCode: 0, createdAt: "2026-06-04T13:40:44.972Z" }, + { seq: 29, label: "agentrun:backend:run-claimed", type: "backend", status: "running", source: "agentrun", sourceSeq: 4, message: "run-claimed", createdAt: "2026-06-04T13:40:34.601Z" }, + { seq: 42, label: "agentrun:assistant:message", type: "assistant", status: "running", source: "agentrun", sourceSeq: 17, message: "我先确认 `hwpod` 可用。", createdAt: "2026-06-04T13:40:43.286Z" }, + { seq: 43, label: "item/commandExecution:started", type: "tool_call", toolName: "commandExecution", status: "started", source: "agentrun", sourceSeq: 18, itemId: "call_function_gsuuqbm1yp0s_1", command: "/bin/sh -lc 'command -v hwpod && hwpod profile list'", createdAt: "2026-06-04T13:40:43.458Z" }, + { seq: 44, label: "agentrun:output:stdout", type: "output", status: "running", source: "agentrun", sourceSeq: 19, message: "raw stdout should stay inside the completed tool row", createdAt: "2026-06-04T13:40:44.852Z" }, + { seq: 45, label: "item/commandExecution:completed", type: "tool_call", toolName: "commandExecution", status: "completed", source: "agentrun", sourceSeq: 20, itemId: "call_function_gsuuqbm1yp0s_1", command: "/bin/sh -lc 'command -v hwpod && hwpod profile list'", stdoutSummary: '{"ok":true,"action":"profile.list"}', exitCode: 0, createdAt: "2026-06-04T13:40:44.972Z" } + ]; + + const rows = traceDisplayRows({ startedAt: "2026-06-04T13:40:28.612Z" }, events); + const text = JSON.stringify(rows); + const toolRows = rows.filter((row) => row.rowId === "tool:call_function_gsuuqbm1yp0s_1"); + const assistantRows = rows.filter((row) => row.bodyFormat === "markdown"); + + assert.equal(toolRows.length, 1); + assert.equal(assistantRows.length, 1); + assert.equal(rows.filter((row) => /run-claimed/u.test(row.body ?? "")).length, 1); + assert.match(toolRows[0]?.body ?? "", /command -v hwpod && hwpod profile list/u); + assert.match(toolRows[0]?.body ?? "", /profile\.list/u); + assert.doesNotMatch(toolRows[0]?.body ?? "", /\/bin\/sh -lc/u); + assert.doesNotMatch(text, /agentrun:output:stdout|raw stdout should stay inside/u); +});