From 10591d8d099502e5b1721feb8c945d4ca9838aaf Mon Sep 17 00:00:00 2001 From: root Date: Thu, 23 Jul 2026 03:56:49 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=80=8F=E4=BC=A0=20Workbench=20?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E5=88=9B=E5=BB=BA=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server-workbench-facts-created-at.test.ts | 22 +++++++++++++++++++ internal/cloud/server-workbench-facts.ts | 5 +++++ internal/workbenchruntime/service.go | 2 +- internal/workbenchruntime/service_test.go | 22 +++++++++++++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 internal/cloud/server-workbench-facts-created-at.test.ts diff --git a/internal/cloud/server-workbench-facts-created-at.test.ts b/internal/cloud/server-workbench-facts-created-at.test.ts new file mode 100644 index 00000000..892db6b2 --- /dev/null +++ b/internal/cloud/server-workbench-facts-created-at.test.ts @@ -0,0 +1,22 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { factSessionSummary } from "./server-workbench-facts.ts"; + +test("session summary preserves createdAt without using updatedAt as fallback", () => { + const createdAt = "2026-07-23T07:30:00.000Z"; + const summary = factSessionSummary({ + sessionId: "ses_created_at", + status: "idle", + createdAt, + updatedAt: "2026-07-23T08:30:00.000Z", + }); + assert.equal(summary?.createdAt, createdAt); + + const withoutCreatedAt = factSessionSummary({ + sessionId: "ses_without_created_at", + status: "idle", + updatedAt: "2026-07-23T08:30:00.000Z", + }); + assert.equal(withoutCreatedAt?.createdAt, null); +}); diff --git a/internal/cloud/server-workbench-facts.ts b/internal/cloud/server-workbench-facts.ts index a3600a63..f94076c4 100644 --- a/internal/cloud/server-workbench-facts.ts +++ b/internal/cloud/server-workbench-facts.ts @@ -106,6 +106,7 @@ export function factSessionSummary(session, facts = {}) { launchContext, messageCount: messages.length, firstUserMessagePreview: firstUserPreview(messages), + createdAt: factCreatedAt(session), updatedAt: factUpdatedAt(session), turnSummary: turn ? { turnId: factTurnId(turn, traceId), @@ -710,6 +711,10 @@ export function factUpdatedAt(record) { return textValue(record?.updatedAt ?? record?.occurredAt ?? record?.createdAt) || null; } +export function factCreatedAt(record) { + return textValue(record?.createdAt) || null; +} + export function factSeq(record) { for (const value of [record?.projectedSeq, record?.sourceSeq, record?.seq]) { const parsed = Number(value); diff --git a/internal/workbenchruntime/service.go b/internal/workbenchruntime/service.go index 9b6ad3ea..7a84541c 100644 --- a/internal/workbenchruntime/service.go +++ b/internal/workbenchruntime/service.go @@ -2174,7 +2174,7 @@ func sessionSummary(session map[string]any, facts map[string][]any) map[string]a turnSummary = map[string]any{"turnId": first(text(turn["turnId"]), traceID), "traceId": traceID, "status": status, "running": isRunning(status), "terminal": isTerminal(status), "eventCount": projection["lastProjectedSeq"], "projection": projection, "projectionStatus": projection["projectionStatus"], "projectionHealth": projection["projectionHealth"], "staleMs": projection["staleMs"], "blocker": projection["blocker"], "timing": timing, "startedAt": timing["startedAt"], "lastEventAt": timing["lastEventAt"], "finishedAt": timing["finishedAt"], "durationMs": timing["durationMs"], "updatedAt": updatedAt(turn)} } title := sessionDisplayTitle(session, firstUserMessagePreview) - return map[string]any{"sessionId": sid, "threadId": nullableText(session["threadId"]), "agentId": first(text(session["agentId"]), "hwlab-code-agent"), "title": title, "name": title, "status": status, "running": isRunning(status), "terminal": isTerminal(status), "lastTraceId": nullableText(traceID), "projection": projection, "projectionStatus": projection["projectionStatus"], "projectionHealth": projection["projectionHealth"], "staleMs": projection["staleMs"], "blocker": projection["blocker"], "providerProfile": nullableText(first(text(session["providerProfile"]), text(objectMap(session["sessionJson"])["providerProfile"]))), "messageCount": messageCount, "firstUserMessagePreview": firstUserMessagePreview, "updatedAt": updatedAt(session), "turnSummary": turnSummary, "valuesRedacted": true} + return map[string]any{"sessionId": sid, "threadId": nullableText(session["threadId"]), "agentId": first(text(session["agentId"]), "hwlab-code-agent"), "title": title, "name": title, "status": status, "running": isRunning(status), "terminal": isTerminal(status), "lastTraceId": nullableText(traceID), "projection": projection, "projectionStatus": projection["projectionStatus"], "projectionHealth": projection["projectionHealth"], "staleMs": projection["staleMs"], "blocker": projection["blocker"], "providerProfile": nullableText(first(text(session["providerProfile"]), text(objectMap(session["sessionJson"])["providerProfile"]))), "messageCount": messageCount, "firstUserMessagePreview": firstUserMessagePreview, "createdAt": nullableText(session["createdAt"]), "updatedAt": updatedAt(session), "turnSummary": turnSummary, "valuesRedacted": true} } type traceCandidate struct { diff --git a/internal/workbenchruntime/service_test.go b/internal/workbenchruntime/service_test.go index a6fba8f2..0f894428 100644 --- a/internal/workbenchruntime/service_test.go +++ b/internal/workbenchruntime/service_test.go @@ -56,3 +56,25 @@ func TestSessionSummaryUsesNewestTurnFactsOverStaleSessionLastTrace(t *testing.T t.Fatalf("turnSummary.status=%q, want running; turnSummary=%#v", got, turnSummary) } } + +func TestSessionSummaryPreservesCreatedAtWithoutUpdatedAtFallback(t *testing.T) { + createdAt := "2026-07-23T07:30:00Z" + summary := sessionSummary(map[string]any{ + "sessionId": "ses_created_at", + "status": "idle", + "createdAt": createdAt, + "updatedAt": "2026-07-23T08:30:00Z", + }, emptyFacts()) + if got := text(summary["createdAt"]); got != createdAt { + t.Fatalf("createdAt=%q, want %q", got, createdAt) + } + + withoutCreatedAt := sessionSummary(map[string]any{ + "sessionId": "ses_without_created_at", + "status": "idle", + "updatedAt": "2026-07-23T08:30:00Z", + }, emptyFacts()) + if withoutCreatedAt["createdAt"] != nil { + t.Fatalf("createdAt=%#v, want nil without source creation fact", withoutCreatedAt["createdAt"]) + } +}