diff --git a/deploy/artifact-catalog.dev.json b/deploy/artifact-catalog.dev.json index 34c45857..97119b63 100644 --- a/deploy/artifact-catalog.dev.json +++ b/deploy/artifact-catalog.dev.json @@ -5,12 +5,12 @@ "profile": "dev", "namespace": "hwlab-dev", "endpoint": "http://74.48.78.17:16667", - "commitId": "01de135", + "commitId": "2be9f52", "artifactState": "published", "publish": { "ciPublished": true, "registryVerified": true, - "provenance": "g14-ci-artifact:hwlab-g14-ci-poll-01de135ecd9a-collect", + "provenance": "g14-ci-artifact:hwlab-g14-ci-poll-2be9f524eb9f-collect", "note": "Digest fields were copied from a successful DEV artifact publish report for this source commit." }, "healthContract": { @@ -66,11 +66,11 @@ "services": [ { "serviceId": "hwlab-cloud-api", - "commitId": "b59d5b0", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:b59d5b0", - "imageTag": "b59d5b0", - "digest": "sha256:f6ac338f9826917d348a3218034c08e8102ac59f79054fcf2c7a4dd0ebdb1c37", - "publishState": "reused", + "commitId": "2be9f52", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:2be9f52", + "imageTag": "2be9f52", + "digest": "sha256:d54e052b24f066a7be82143e084be9804bb11991be8006e8ba5872c2300cbbe8", + "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -79,33 +79,31 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-26T17:27:38.302Z", + "buildCreatedAt": "2026-05-27T00:02:55.991Z", "buildSource": null, - "componentCommitId": "b59d5b055224a31f4263d6939a8c6cdbbe4c2f52", - "componentInputHash": "7c94d028be9c1bbe090eee4c3d065467c532766365c10814c5f66f21ebc26c44", + "componentCommitId": "a452c14983aa957edde155d65bd17e1990782a86", + "componentInputHash": "80ebdf7bc543b37d937efc1b7e8765de9c6a1e1f3d2f3cbe1ee65052fc32d1fe", "dockerfileHash": "3f56efe5dd980bf2081e640a5dcbed1c9f814d58b5fcd8bcb70de29f57373800", "baseImageReference": "127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim", "baseImageDigest": null, "buildArgsHash": "31de87c01b0c617b550b82b7a65780fac56d03a3b8abb9301bf0096766f87237", - "ciAffected": false, + "ciAffected": true, "ciReason": [ - "docs-only-no-image-build" + "component-path-changed" ], "reuse": { - "status": "ready", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:b59d5b0", - "digest": "sha256:f6ac338f9826917d348a3218034c08e8102ac59f79054fcf2c7a4dd0ebdb1c37", - "reusedFrom": "7c94d028be9c1bbe090eee4c3d065467c532766365c10814c5f66f21ebc26c44" + "status": "not-reused", + "reason": "component-affected" }, "reusedFrom": "7c94d028be9c1bbe090eee4c3d065467c532766365c10814c5f66f21ebc26c44" }, { "serviceId": "hwlab-cloud-web", - "commitId": "b59d5b0", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:b59d5b0", - "imageTag": "b59d5b0", - "digest": "sha256:6e13ec598dea1ed0fc909b385b328d002b95870d84b6c5bf5872b2c879bf5233", - "publishState": "reused", + "commitId": "2be9f52", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:2be9f52", + "imageTag": "2be9f52", + "digest": "sha256:a5a2a65d05e83121a673afe809449961ddcf9d05cb64b0b98d199e1fd32a3630", + "publishState": "published", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -114,23 +112,21 @@ "artifactRequired": true, "artifactScope": "required", "notPublishedReason": null, - "buildCreatedAt": "2026-05-26T17:27:38.552Z", + "buildCreatedAt": "2026-05-27T00:02:58.045Z", "buildSource": null, - "componentCommitId": "b59d5b055224a31f4263d6939a8c6cdbbe4c2f52", - "componentInputHash": "6e466c55638cd947836723061be34745dc803db50d92365b3bb359f92adfaf34", + "componentCommitId": "a452c14983aa957edde155d65bd17e1990782a86", + "componentInputHash": "298b4e28acf400c768f481883d777f7f6fe10a5542ff426c37f9f676698831c9", "dockerfileHash": "3f56efe5dd980bf2081e640a5dcbed1c9f814d58b5fcd8bcb70de29f57373800", "baseImageReference": "127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim", "baseImageDigest": null, "buildArgsHash": "39effb18af665e5e286cca9f84ef2a30f577ad11c9acca098589b7ee37f6950c", - "ciAffected": false, + "ciAffected": true, "ciReason": [ - "docs-only-no-image-build" + "component-path-changed" ], "reuse": { - "status": "ready", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:b59d5b0", - "digest": "sha256:6e13ec598dea1ed0fc909b385b328d002b95870d84b6c5bf5872b2c879bf5233", - "reusedFrom": "6e466c55638cd947836723061be34745dc803db50d92365b3bb359f92adfaf34" + "status": "not-reused", + "reason": "component-affected" }, "reusedFrom": "6e466c55638cd947836723061be34745dc803db50d92365b3bb359f92adfaf34" }, @@ -159,7 +155,7 @@ "buildArgsHash": "804a2e2f48cae4f739c3e88bb0d961359fca0a8e1b8cac43517fb065989df72b", "ciAffected": false, "ciReason": [ - "docs-only-no-image-build" + "component-inputs-unchanged" ], "reuse": { "status": "ready", @@ -194,7 +190,7 @@ "buildArgsHash": "d5ea384583ca6916b9fdf4469a8c591e33e6e1a0ba9a59b9901fbaa797824cab", "ciAffected": false, "ciReason": [ - "docs-only-no-image-build" + "component-inputs-unchanged" ], "reuse": { "status": "ready", @@ -229,7 +225,7 @@ "buildArgsHash": "083cc565bccb315303df802dfc1f7fb87738e763afb9662fed7bc7c12719b070", "ciAffected": false, "ciReason": [ - "docs-only-no-image-build" + "component-inputs-unchanged" ], "reuse": { "status": "ready", @@ -264,7 +260,7 @@ "buildArgsHash": "79c757588262d875c61a2542e37c9a849000d54d8bb58e8bf19c597114956f5f", "ciAffected": false, "ciReason": [ - "docs-only-no-image-build" + "component-inputs-unchanged" ], "reuse": { "status": "ready", @@ -299,7 +295,7 @@ "buildArgsHash": "2b321eec447decb8ca22b30a42dffa467405af983aa06e487b66a45c6ecbd0e9", "ciAffected": false, "ciReason": [ - "docs-only-no-image-build" + "component-inputs-unchanged" ], "reuse": { "status": "ready", @@ -334,7 +330,7 @@ "buildArgsHash": "bea7ca0cf1e6f84b11a7848e3dff6dfe4aa613f1df2c51e9ab4807fffe718128", "ciAffected": false, "ciReason": [ - "docs-only-no-image-build" + "component-inputs-unchanged" ], "reuse": { "status": "ready", @@ -369,7 +365,7 @@ "buildArgsHash": "31f0f27ef0e4375ac141f0d54c2c9449bd5dce14df0bb6469704245d1cf86b09", "ciAffected": false, "ciReason": [ - "docs-only-no-image-build" + "component-inputs-unchanged" ], "reuse": { "status": "ready", @@ -404,7 +400,7 @@ "buildArgsHash": "c49a07574a1d4fe74642170efcbb231c2c59436e1d3d2434c285349d23a35fcb", "ciAffected": false, "ciReason": [ - "docs-only-no-image-build" + "component-inputs-unchanged" ], "reuse": { "status": "ready", @@ -439,7 +435,7 @@ "buildArgsHash": "68746696603227763e5768f8daf45d31331f4c8425e6d756b21482ad104e6183", "ciAffected": false, "ciReason": [ - "docs-only-no-image-build" + "component-inputs-unchanged" ], "reuse": { "status": "ready", @@ -474,7 +470,7 @@ "buildArgsHash": "bd9fa0f62fb873c87cf8ec47790d26c5068956c14be61eed222f78dc9f7f5c1a", "ciAffected": false, "ciReason": [ - "docs-only-no-image-build" + "component-inputs-unchanged" ], "reuse": { "status": "ready", @@ -509,7 +505,7 @@ "buildArgsHash": "d5e50a18e70c6a8b4896e527fa07b5dd2c85458ed9ef512c1d46775ef4e40a10", "ciAffected": false, "ciReason": [ - "docs-only-no-image-build" + "component-inputs-unchanged" ], "reuse": { "status": "ready", diff --git a/deploy/deploy.json b/deploy/deploy.json index 5905d976..b86228fd 100644 --- a/deploy/deploy.json +++ b/deploy/deploy.json @@ -1,7 +1,7 @@ { "manifestVersion": "v1", "environment": "dev", - "commitId": "01de135", + "commitId": "2be9f52", "namespace": "hwlab-dev", "endpoint": "http://74.48.78.17:16667", "health": { @@ -181,7 +181,7 @@ "services": [ { "serviceId": "hwlab-cloud-api", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:b59d5b0", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:2be9f52", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -190,9 +190,9 @@ "HWLAB_ENVIRONMENT": "dev", "HWLAB_PUBLIC_ENDPOINT": "http://74.48.78.17:16667", "HWLAB_CLOUD_API_PORT": "6667", - "HWLAB_COMMIT_ID": "b59d5b0", - "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-api:b59d5b0", - "HWLAB_IMAGE_TAG": "b59d5b0", + "HWLAB_COMMIT_ID": "2be9f52", + "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-api:2be9f52", + "HWLAB_IMAGE_TAG": "2be9f52", "HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN": "unidesk-backend,provider-gateway,microservice-proxy", "HWLAB_CLOUD_DB_URL": "secretRef:hwlab-cloud-api-dev-db/database-url", "HWLAB_CLOUD_DB_SSL_MODE": "disable", @@ -230,7 +230,7 @@ }, { "serviceId": "hwlab-cloud-web", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:b59d5b0", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:2be9f52", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -239,9 +239,9 @@ "HWLAB_ENVIRONMENT": "dev", "HWLAB_API_BASE_URL": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667", "HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS": "1260000", - "HWLAB_COMMIT_ID": "b59d5b0", - "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-web:b59d5b0", - "HWLAB_IMAGE_TAG": "b59d5b0" + "HWLAB_COMMIT_ID": "2be9f52", + "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-web:2be9f52", + "HWLAB_IMAGE_TAG": "2be9f52" } }, { diff --git a/deploy/k8s/base/workloads.yaml b/deploy/k8s/base/workloads.yaml index 02e0677d..03fc7ed8 100644 --- a/deploy/k8s/base/workloads.yaml +++ b/deploy/k8s/base/workloads.yaml @@ -31,7 +31,7 @@ "containers": [ { "name": "hwlab-cloud-api", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:b59d5b0", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:2be9f52", "ports": [ { "name": "http", @@ -53,15 +53,15 @@ }, { "name": "HWLAB_COMMIT_ID", - "value": "b59d5b0" + "value": "2be9f52" }, { "name": "HWLAB_IMAGE", - "value": "127.0.0.1:5000/hwlab/hwlab-cloud-api:b59d5b0" + "value": "127.0.0.1:5000/hwlab/hwlab-cloud-api:2be9f52" }, { "name": "HWLAB_IMAGE_TAG", - "value": "b59d5b0" + "value": "2be9f52" }, { "name": "HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN", @@ -245,7 +245,7 @@ }, { "name": "hwlab-codex-api-forwarder", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:b59d5b0", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:2be9f52", "command": [ "node", "/app/cmd/hwlab-codex-api-responses-forwarder/main.mjs" @@ -367,7 +367,7 @@ "containers": [ { "name": "hwlab-cloud-web", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:b59d5b0", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:2be9f52", "ports": [ { "name": "http", @@ -385,15 +385,15 @@ }, { "name": "HWLAB_COMMIT_ID", - "value": "b59d5b0" + "value": "2be9f52" }, { "name": "HWLAB_IMAGE", - "value": "127.0.0.1:5000/hwlab/hwlab-cloud-web:b59d5b0" + "value": "127.0.0.1:5000/hwlab/hwlab-cloud-web:2be9f52" }, { "name": "HWLAB_IMAGE_TAG", - "value": "b59d5b0" + "value": "2be9f52" } ], "readinessProbe": { diff --git a/internal/cloud/code-agent-session-registry.test.mjs b/internal/cloud/code-agent-session-registry.test.mjs index dbeb05dd..0d4f07a9 100644 --- a/internal/cloud/code-agent-session-registry.test.mjs +++ b/internal/cloud/code-agent-session-registry.test.mjs @@ -1057,7 +1057,7 @@ function codeAgentBlockedPayloadFromAcquire(acquireResult, { conversationId, tra }; } -function createTimedFakeAppServerClient({ calls, text = "timed stdio reply", eventEveryMs = 20, eventCount = 0, completeAfterMs = null } = {}) { +function createTimedFakeAppServerClient({ calls, text = "timed stdio reply", eventEveryMs = 20, eventCount = 0, completeAfterMs = null, commandExecution = null } = {}) { let notificationHandler = null; let turn = 0; const timers = new Set(); @@ -1099,6 +1099,19 @@ function createTimedFakeAppServerClient({ calls, text = "timed stdio reply", eve }); }); } + if (commandExecution) { + const commandItem = { + id: commandExecution.id ?? "cmd_stdio_timed_1", + type: "commandExecution", + ...commandExecution + }; + later(Math.max(1, Math.floor(eventEveryMs / 2)), () => { + notificationHandler?.({ method: "item/started", params: { item: { ...commandItem, status: "running" } } }); + }); + later(eventEveryMs, () => { + notificationHandler?.({ method: "item/completed", params: { item: { ...commandItem, status: commandExecution.status ?? "completed" } } }); + }); + } if (Number.isInteger(completeAfterMs)) { later(completeAfterMs, () => { notificationHandler?.({ method: "item/agentMessage/delta", params: { itemId: `item_${turn}`, delta: text } }); @@ -1552,7 +1565,14 @@ test("Codex app-server activity timeout does not fail an active turn", async () text: "active turn completed", eventEveryMs: 20, eventCount: 5, - completeAfterMs: 130 + completeAfterMs: 130, + commandExecution: { + id: "cmd_stdio_trace_summary", + command: ["tran_cmd", "gws_DESKTOP:/f/work/constart", "dir"], + exitCode: 0, + durationMs: 456, + aggregatedOutput: "FREQ_Controller_FW.uvprojx\nkeil\n" + } }) }); const env = { @@ -1590,6 +1610,13 @@ test("Codex app-server activity timeout does not fail an active turn", async () assert.equal(payload.reply.content, "active turn completed"); assert.ok(Date.now() - started >= 100); assert.ok(payload.runnerTrace.events.some((event) => event.label === "item/commandExecution/outputDelta")); + const commandEvent = payload.runnerTrace.events.find((event) => event.label === "item/commandExecution:completed"); + assert.ok(commandEvent, JSON.stringify(payload.runnerTrace.events)); + assert.equal(commandEvent.toolName, "commandExecution"); + assert.equal(commandEvent.command, "tran_cmd gws_DESKTOP:/f/work/constart dir"); + assert.equal(commandEvent.exitCode, 0); + assert.equal(commandEvent.durationMs, 456); + assert.match(commandEvent.stdoutSummary, /FREQ_Controller_FW\.uvprojx/); assert.equal(payload.runnerTrace.events.some((event) => event.label === "timeout:no_activity"), false); assert.equal(payload.error, undefined); } finally { diff --git a/internal/cloud/code-agent-trace-store.mjs b/internal/cloud/code-agent-trace-store.mjs index 5cf04f1b..1819454c 100644 --- a/internal/cloud/code-agent-trace-store.mjs +++ b/internal/cloud/code-agent-trace-store.mjs @@ -322,8 +322,15 @@ function normalizeTraceEvent(event, { traceId, seq, now, fallbackRunnerKind } = sessionReused: typeof event.sessionReused === "boolean" ? event.sessionReused : undefined, turn: typeof event.turn === "number" ? event.turn : undefined, toolName, + itemId: safeText(event.itemId, 180), + command: safeText(event.command, 900), + exitCode: Number.isInteger(event.exitCode) ? event.exitCode : undefined, + durationMs: typeof event.durationMs === "number" ? Math.max(0, Math.trunc(event.durationMs)) : undefined, + outputBytes: typeof event.outputBytes === "number" ? Math.max(0, Math.trunc(event.outputBytes)) : undefined, promptSummary: safeText(event.promptSummary, 240), outputSummary: safeText(event.outputSummary, 400), + stdoutSummary: safeText(event.stdoutSummary, 600), + stderrSummary: safeText(event.stderrSummary, 600), chunk: safeText(event.chunk, 400), message: safeText(event.message, 500), errorCode: safeText(event.errorCode, 120), diff --git a/internal/cloud/codex-stdio-session.mjs b/internal/cloud/codex-stdio-session.mjs index a9e89e98..36ab072e 100644 --- a/internal/cloud/codex-stdio-session.mjs +++ b/internal/cloud/codex-stdio-session.mjs @@ -1731,6 +1731,20 @@ function createAppServerTurnState({ traceRecorder, session } = {}) { }); return; } + if (method === "item/started" && item?.type === "commandExecution") { + appendCommandExecutionTrace(item, { + status: "started", + label: "item/commandExecution:started" + }); + return; + } + if (method === "item/completed" && item?.type === "commandExecution") { + appendCommandExecutionTrace(item, { + status: appServerCommandStatus(item), + label: "item/commandExecution:completed" + }); + return; + } if (method === "item/commandExecution/outputDelta" || method === "item/reasoning/summaryTextDelta" || method === "item/reasoning/textDelta") { appendTrace({ type: method.startsWith("item/reasoning/") ? "reasoning" : "tool_call", @@ -1790,6 +1804,30 @@ function createAppServerTurnState({ traceRecorder, session } = {}) { } } + function appendCommandExecutionTrace(item, { status, label }) { + const output = commandExecutionOutputText(item); + appendTrace({ + type: "tool_call", + stage: "tool_call", + status, + label, + itemId: optionalId(item.id), + toolName: "commandExecution", + command: redactText(commandExecutionCommandText(item)), + exitCode: Number.isInteger(item.exitCode) ? item.exitCode : undefined, + durationMs: typeof item.durationMs === "number" ? item.durationMs : undefined, + outputBytes: output.length, + stdoutSummary: output ? tailText(redactText(output), 600) : undefined, + stderrSummary: commandExecutionErrorText(item), + sessionId: session?.sessionId, + sessionStatus: session?.status, + turn: session?.turn, + threadId, + turnId, + waitingFor: status === "completed" ? "turn/completed" : "commandExecution/completed" + }); + } + async function wait(timeoutMs, closedPromise, { hardTimeoutMs = null } = {}) { let activityTimer; let hardTimer; @@ -2017,6 +2055,58 @@ function appServerTerminalStatus(value) { return raw ? "failed" : null; } +function appServerCommandStatus(item) { + const raw = String(item?.status ?? "").trim().toLowerCase(); + if (raw === "completed" || raw === "complete" || raw === "success" || raw === "succeeded") return "completed"; + if (raw === "failed" || raw === "error") return "failed"; + if (Number.isInteger(item?.exitCode)) return item.exitCode === 0 ? "completed" : "failed"; + return raw || "completed"; +} + +function commandExecutionCommandText(item) { + return firstCommandExecutionText( + item?.command, + item?.cmd, + item?.argv, + item?.input, + item?.metadata?.command + ); +} + +function commandExecutionOutputText(item) { + return firstCommandExecutionText( + item?.aggregatedOutput, + item?.output, + item?.stdout, + item?.result, + item?.metadata?.output + ); +} + +function commandExecutionErrorText(item) { + const error = item?.error; + const message = typeof error === "string" ? error : typeof error?.message === "string" ? error.message : ""; + const text = firstCommandExecutionText(item?.stderr, message, item?.metadata?.stderr); + return text ? tailText(redactText(text), 600) : undefined; +} + +function firstCommandExecutionText(...values) { + for (const value of values) { + const text = commandExecutionTextValue(value); + if (text) return text; + } + return ""; +} + +function commandExecutionTextValue(value) { + if (typeof value === "string") return value.trim(); + if (Array.isArray(value)) return value.map((item) => commandExecutionTextValue(item)).filter(Boolean).join(" ").trim(); + if (value && typeof value === "object") { + return firstCommandExecutionText(value.text, value.content, value.message, value.stdout, value.stderr, value.command, value.cmd); + } + return ""; +} + export function longLivedSessionGate({ provider, runnerKind, diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index c2036964..7ff3db29 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -5058,6 +5058,7 @@ function compactTraceTextTail(text, maxLength) { function traceDisplayRow(trace, event, options = {}) { if (!options.includeNoise && isNoisyTraceEvent(event)) return null; + if (isCommandExecutionTraceEvent(event)) return traceCommandExecutionRow(trace, event); const clock = traceClock(event.createdAt); const total = formatTraceDuration(traceRelativeMs(trace, event)); const status = traceStatusToken(event); @@ -5078,6 +5079,43 @@ function traceDisplayRow(trace, event, options = {}) { }; } +function traceCommandExecutionRow(trace, event) { + const clock = traceClock(event.createdAt); + const total = formatTraceDuration(traceRelativeMs(trace, event)); + const status = traceStatusToken(event); + const command = cleanTraceOutputText(event.command ?? ""); + const meta = [ + status, + "tool cmd", + command ? `:: ${compactTraceTextTail(command, 220)}` : null, + Number.isInteger(event.exitCode) ? `exit=${event.exitCode}` : null, + typeof event.durationMs === "number" ? `s=${(event.durationMs / 1000).toFixed(1)}` : null, + typeof event.outputBytes === "number" ? `out=${formatTraceBytes(event.outputBytes)}` : null, + event.itemId ? `item=${event.itemId}` : null + ].filter(Boolean).join(" "); + return { + seq: event.seq ?? null, + tone: traceEventTone(event), + header: `${clock} total=${total} ${meta}`, + body: traceCommandExecutionBody(event) + }; +} + +function traceCommandExecutionBody(event) { + const lines = [ + event.command ? `command=${compactTraceTextTail(cleanTraceOutputText(event.command), 900)}` : null, + event.stdoutSummary ? `stdout:\n${compactTraceTextTail(cleanTraceOutputText(event.stdoutSummary), 1200)}` : null, + event.stderrSummary ? `stderr:\n${compactTraceTextTail(cleanTraceOutputText(event.stderrSummary), 800)}` : null, + event.waitingFor && event.status !== "completed" ? `waiting=${event.waitingFor}` : null + ].filter(Boolean); + return lines.join("\n"); +} + +function isCommandExecutionTraceEvent(event) { + return event?.type === "tool_call" && event?.toolName === "commandExecution" && + (event?.label === "item/commandExecution:started" || event?.label === "item/commandExecution:completed"); +} + function isNoisyTraceEvent(event) { const label = String(event?.label ?? ""); if (isToolOutputChunkTraceEvent(event)) return false; @@ -5106,6 +5144,8 @@ function readableTraceLabel(event) { if (label === "request:accepted") return "request accepted"; if (label.startsWith("tool:codex-app-server.thread/start+turn/start")) return "codex turn start"; if (label.startsWith("tool:codex-app-server.thread/resume+turn/start")) return "codex turn resume"; + if (label === "item/commandExecution:started") return "cmd started"; + if (label === "item/commandExecution:completed") return "cmd completed"; if (label.startsWith("item/commandExecution/outputDelta")) return "cmd output"; if (label === "turn:waiting:first_assistant_token") return "waiting first assistant token"; if (label.startsWith("turn:completed")) return "turn completed"; @@ -5276,6 +5316,13 @@ function compactTraceSize(value) { return String(length); } +function formatTraceBytes(value) { + const bytes = Math.max(0, Number(value) || 0); + if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)}MiB`; + if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)}KiB`; + return `${bytes}B`; +} + function cleanTraceText(value) { const text = String(value ?? "").trim(); if (!text) return ""; diff --git a/web/hwlab-cloud-web/scripts/trace-scroll.test.mjs b/web/hwlab-cloud-web/scripts/trace-scroll.test.mjs index ad253a0f..b1ec9eb5 100644 --- a/web/hwlab-cloud-web/scripts/trace-scroll.test.mjs +++ b/web/hwlab-cloud-web/scripts/trace-scroll.test.mjs @@ -160,6 +160,18 @@ test("trace display full means complete readable timeline, not compacted result status: "started", message: "accepted" }, + { + label: "item/commandExecution:completed", + type: "tool_call", + status: "completed", + toolName: "commandExecution", + command: "node /app/tools/hwlab-gateway-shell.mjs --json --timeout-ms 120000", + exitCode: 0, + durationMs: 1234, + outputBytes: 42, + stdoutSummary: "gateway stdout ok", + stderrSummary: "" + }, { label: "item/commandExecution/outputDelta", type: "tool_call", @@ -183,6 +195,9 @@ test("trace display full means complete readable timeline, not compacted result const replaying = await tracePanelText(page); assert.match(replaying.count, /完整 trace 回放中/); assert.doesNotMatch(replaying.count, /压缩窗口|显示全部/); + assert.ok(replaying.rows.some((row) => row.includes("tool cmd :: node /app/tools/hwlab-gateway-shell.mjs --json") && row.includes("exit=0") && row.includes("s=1.2") && row.includes("out=42B")), replaying.rows.join("\n")); + assert.doesNotMatch(replaying.rows.join("\n"), /node \/ app \/ tools/); + assert.match(replaying.bodies.join("\n"), /stdout:\s*gateway stdout ok/); assert.equal(replaying.rows.filter((row) => row.includes("tool gateway.shell")).length, 2, replaying.rows.join("\n---\n")); assert.ok(replaying.rows.some((row) => row.includes("op=op_one")), replaying.rows.join("\n")); assert.ok(replaying.rows.some((row) => row.includes("op=op_two")), replaying.rows.join("\n"));