diff --git a/internal/cloud/kafka-event-bridge.test.ts b/internal/cloud/kafka-event-bridge.test.ts
index 722079ae..0fbc516f 100644
--- a/internal/cloud/kafka-event-bridge.test.ts
+++ b/internal/cloud/kafka-event-bridge.test.ts
@@ -216,6 +216,35 @@ test("projects AgentRun semantic infrastructure retry without reclassification",
assert.equal(projected.event.runnerJobId, "rjob_retry");
});
+test("projects AgentRun Git mirror fetch as running preparation", () => {
+ const projected = projectAgentRunKafkaEventToHwlabEvent({
+ schema: "agentrun.event.v1",
+ eventType: "agentrun.run.event",
+ producedAt: "2026-07-20T10:00:01.000Z",
+ run: { runId: "run_fetch", sessionId: "ses_fetch", status: "running" },
+ command: { commandId: "cmd_fetch", state: "pending" },
+ event: {
+ id: "evt_fetch",
+ seq: 5,
+ type: "backend_status",
+ createdAt: "2026-07-20T10:00:01.000Z",
+ payload: {
+ phase: "runner-source-fetch-started",
+ retryPhase: "initialAttempt",
+ failureDomain: "infrastructure",
+ component: "git-mirror",
+ code: "git-mirror-fetch-in-progress",
+ summary: "正在从 Git mirror 获取 Runner 源码",
+ attempt: 1,
+ maxAttempts: 3
+ }
+ }
+ }, { source: "hwlab-test" });
+ assert.equal(projected.event.status, "running");
+ assert.equal(projected.event.code, "git-mirror-fetch-in-progress");
+ assert.equal(projected.event.failureComponent, "git-mirror");
+});
+
test("projects AgentRun assistant_message Kafka event into HWLAB trace event", () => {
const projected = projectAgentRunKafkaEventToHwlabEvent({
schema: "agentrun.event.v1",
diff --git a/internal/cloud/kafka-event-bridge.ts b/internal/cloud/kafka-event-bridge.ts
index 9876913c..f78df3f6 100644
--- a/internal/cloud/kafka-event-bridge.ts
+++ b/internal/cloud/kafka-event-bridge.ts
@@ -1679,7 +1679,9 @@ function semanticFailureRetryProjection(payload) {
? "failed"
: normalized.includes("recovered")
? "running"
- : "retrying";
+ : normalized.includes("scheduled") || (normalized.includes("retry") && normalized.includes("started"))
+ ? "retrying"
+ : "running";
return {
status,
fields: {
diff --git a/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.test.ts b/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.test.ts
index 6baaaca3..b42ed397 100644
--- a/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.test.ts
+++ b/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.test.ts
@@ -6,6 +6,49 @@ import { describe, expect, it } from "vitest";
import WorkbenchMessageCard from "./WorkbenchMessageCard.vue";
describe("WorkbenchMessageCard semantic retry visibility", () => {
+ it("shows Git mirror clone progress as runtime preparation", () => {
+ const wrapper = mount(WorkbenchMessageCard, {
+ props: {
+ detailEnabled: false,
+ message: {
+ id: "msg_fetch_agent",
+ messageId: "msg_fetch_agent",
+ role: "agent",
+ title: "Code Agent",
+ text: "",
+ status: "running",
+ traceId: "trc_fetch_banner",
+ sessionId: "ses_fetch_banner",
+ createdAt: "2026-07-20T10:00:00.000Z",
+ runnerTrace: {
+ traceId: "trc_fetch_banner",
+ eventSource: "hwlab-kafka-sse",
+ status: "running",
+ eventCount: 1,
+ events: [{
+ sourceEventId: "evt_fetch_banner",
+ sourceSeq: 5,
+ type: "backend",
+ status: "running",
+ failureDomain: "infrastructure",
+ failureCode: "git-mirror-fetch-in-progress",
+ summary: "正在从 Git mirror 获取 Runner 源码",
+ retryPhase: "initialAttempt",
+ retryAttempt: 1,
+ retryMaxAttempts: 3,
+ createdAt: new Date().toISOString()
+ }]
+ }
+ }
+ },
+ global: { stubs: { TraceTimeline: true, LoadingState: true, MessageMarkdown: true, ApiErrorDiagnostic: true, StatusBadge: true } }
+ });
+ expect(wrapper.text()).toContain("运行环境准备:正在从 Git mirror 获取 Runner 源码");
+ expect(wrapper.text()).toContain("第 1/3 次源码获取进行中");
+ expect(wrapper.text()).not.toContain("基础设施故障");
+ wrapper.unmount();
+ });
+
it("shows an infrastructure retry banner without opening Trace details", () => {
const wrapper = mount(WorkbenchMessageCard, {
props: {
diff --git a/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.vue b/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.vue
index 0d858d7f..1bb82dea 100644
--- a/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.vue
+++ b/web/hwlab-cloud-web/src/components/workbench/WorkbenchMessageCard.vue
@@ -36,7 +36,7 @@ const diagnostic = computed(() => messageDiagnosticView(props.message));
const visibleText = computed(() => visibleMessageText(props.message));
const durationMeta = computed(() => messageDurationMeta(props.message));
const activityMeta = computed(() => messageActivityMeta(props.message));
-const semanticFailure = computed(() => semanticFailureView(props.message, nowMs.value));
+const semanticRuntimeStatus = computed(() => semanticRuntimeStatusView(props.message, nowMs.value));
const autoExpanded = computed(() => props.traceAutoExpanded ?? traceLifecycleExpanded(props.message, traceTimelinePolicy.value));
function isRunningMessage(message: ChatMessage): boolean {
@@ -123,7 +123,7 @@ function traceStorageKey(message: ChatMessage): string {
return `${props.storageKeyPrefix}.${message.traceId ?? message.runnerTrace?.traceId ?? message.id}`;
}
-function semanticFailureView(message: ChatMessage, currentTimeMs: number): { title: string; detail: string; tone: string; code: string | null } | null {
+function semanticRuntimeStatusView(message: ChatMessage, currentTimeMs: number): { title: string; detail: string; tone: string; code: string | null } | null {
const events = Array.isArray(message.runnerTrace?.events) ? message.runnerTrace.events : [];
const event = [...events].reverse().find((item) => typeof item.failureDomain === "string" && typeof item.retryPhase === "string");
if (!event) return null;
@@ -133,6 +133,13 @@ function semanticFailureView(message: ChatMessage, currentTimeMs: number): { tit
const attempt = finiteInteger(event.retryAttempt ?? event.attempt);
const maxAttempts = finiteInteger(event.retryMaxAttempts ?? event.maxAttempts);
const progress = attempt !== null && maxAttempts !== null ? `第 ${attempt}/${maxAttempts} 次` : "本次";
+ const code = firstText(event.failureCode, event.code);
+ if (code === "git-mirror-fetch-in-progress") {
+ return { title: `运行环境准备:${summary}`, detail: `${progress}源码获取进行中`, tone: "info", code };
+ }
+ if (code === "git-mirror-fetch-completed") {
+ return { title: `运行环境准备:${summary}`, detail: "源码获取完成,正在启动 Runner", tone: "recovered", code };
+ }
let detail = "正在判定有限重试";
let tone = "warning";
if (phase.includes("scheduled")) {
@@ -152,7 +159,7 @@ function semanticFailureView(message: ChatMessage, currentTimeMs: number): { tit
title: `${domain}:${summary}`,
detail,
tone,
- code: firstText(event.failureCode, event.code)
+ code
};
}
@@ -191,10 +198,10 @@ function finiteInteger(value: unknown): number | null {
-
- {{ semanticFailure.title }}
- {{ semanticFailure.detail }}
- {{ semanticFailure.code }}
+
+ {{ semanticRuntimeStatus.title }}
+ {{ semanticRuntimeStatus.detail }}
+ {{ semanticRuntimeStatus.code }}
@@ -221,6 +228,12 @@ function finiteInteger(value: unknown): number | null {
color: #166534;
}
+.semantic-failure-banner[data-tone="info"] {
+ border-left-color: #2563eb;
+ background: #eff6ff;
+ color: #1e40af;
+}
+
.semantic-failure-banner[data-tone="failed"] {
border-left-color: #b91c1c;
background: #fef2f2;