diff --git a/web/hwlab-cloud-web/src/components/conversation/MessageTracePanel.test.tsx b/web/hwlab-cloud-web/src/components/conversation/MessageTracePanel.test.tsx
index 8768e33e..5f6cc3f8 100644
--- a/web/hwlab-cloud-web/src/components/conversation/MessageTracePanel.test.tsx
+++ b/web/hwlab-cloud-web/src/components/conversation/MessageTracePanel.test.tsx
@@ -83,3 +83,23 @@ test("message trace panel renders persisted fallback for expired historical trac
assert.match(html, /sourceEventCount=31/u);
assert.doesNotMatch(html, /等待后端事件/u);
});
+
+test("message trace panel does not label completed traces with pending replay as expired", () => {
+ const trace: RunnerTrace = {
+ traceId: "trc_issue955_completed_replay",
+ status: "completed",
+ traceStatus: "completed",
+ events: [],
+ eventCount: 106,
+ traceSummary: { sourceEventCount: 106, terminalStatus: "completed", source: "code-agent-result" },
+ finalResponse: { text: "这里看到了 gateway sessions。" }
+ };
+
+ const html = renderToStaticMarkup();
+
+ assert.match(html, /完整 trace 回放中/u);
+ assert.match(html, /traceStatus=completed/u);
+ assert.match(html, /sourceEventCount=106/u);
+ assert.match(html, /这里看到了 gateway sessions/u);
+ assert.doesNotMatch(html, /历史 trace 已过期/u);
+});
diff --git a/web/hwlab-cloud-web/src/components/conversation/MessageTracePanel.tsx b/web/hwlab-cloud-web/src/components/conversation/MessageTracePanel.tsx
index 9f3da41f..671c9287 100644
--- a/web/hwlab-cloud-web/src/components/conversation/MessageTracePanel.tsx
+++ b/web/hwlab-cloud-web/src/components/conversation/MessageTracePanel.tsx
@@ -127,16 +127,29 @@ function persistentTraceFallbackRows(trace: RunnerTrace): TraceEventRow[] {
const sourceCount = positiveInteger(summary?.sourceEventCount ?? summary?.eventCount ?? trace.eventCount);
if (!text && fallback?.available !== true && sourceCount === 0) return [];
const status = firstNonEmptyTraceText(trace.traceStatus, trace.status, summary?.terminalStatus, objectOrNull(trace.agentRun)?.terminalStatus) ?? "expired";
- const header = `${trace.traceId ?? "trace"} 历史 trace 已过期,显示持久化摘要`;
+ const expired = isExpiredTraceStatus(trace.traceStatus, trace.status) || isExpiredTraceFallback(fallback);
+ const header = expired
+ ? `${trace.traceId ?? "trace"} 历史 trace 已过期,显示持久化摘要`
+ : `${trace.traceId ?? "trace"} 完整 trace 回放中,先显示持久化摘要`;
const meta = [
`traceStatus=${status}`,
sourceCount > 0 ? `sourceEventCount=${sourceCount}` : null,
firstNonEmptyTraceText(fallback?.source, summary?.source) ? `source=${firstNonEmptyTraceText(fallback?.source, summary?.source)}` : null
].filter(Boolean).join("\n");
- const body = [meta, text].filter(Boolean).join("\n\n") || "后端没有保留原始 event,但已返回持久化 trace 摘要。";
+ const body = [meta, text].filter(Boolean).join("\n\n") || (expired ? "后端没有保留原始 event,但已返回持久化 trace 摘要。" : "后端声明存在原始 event,正在通过 trace 回放入口补齐完整事件列表。");
return [{ rowId: `fallback:${trace.traceId ?? "trace"}`, seq: null, tone: "source", header, body, terminal: true, bodyFormat: text ? "markdown" : "text" }];
}
+function isExpiredTraceStatus(...values: unknown[]): boolean {
+ return values.some((value) => ["expired", "missing"].includes(String(value ?? "").trim().toLowerCase()));
+}
+
+function isExpiredTraceFallback(fallback: Record | null): boolean {
+ const store = String(fallback?.liveTraceStore ?? "").trim().toLowerCase();
+ const source = String(fallback?.source ?? "").trim().toLowerCase();
+ return store.includes("expired") || source.includes("expired");
+}
+
function objectOrNull(value: unknown): Record | null {
return value && typeof value === "object" ? value as Record : null;
}