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; }