diff --git a/internal/cloud/server-workbench-http.test.ts b/internal/cloud/server-workbench-http.test.ts index 25a1ec33..d591dfd0 100644 --- a/internal/cloud/server-workbench-http.test.ts +++ b/internal/cloud/server-workbench-http.test.ts @@ -58,6 +58,36 @@ test("workbench projection diagnostics keeps projecting health distinct from cau assert.equal(diagnostics.projectionHealth, "projecting"); }); +test("workbench turn projection keeps lastEventAt on the latest event instead of stale admission timing (#1889)", () => { + const traceId = "trc_turn_timing_latest_event"; + const result = { + traceId, + status: "running", + startedAt: "2026-06-23T21:43:48.740Z", + lastEventAt: "2026-06-23T21:43:48.740Z", + timing: { + startedAt: "2026-06-23T21:43:48.740Z", + lastEventAt: "2026-06-23T21:43:48.740Z", + durationMs: null + } + }; + const trace = { + traceId, + status: "running", + updatedAt: "2026-06-23T21:44:33.486Z", + lastEventAt: "2026-06-23T21:44:31.000Z", + events: [ + { seq: 1, type: "admitted", createdAt: "2026-06-23T21:43:48.740Z" }, + { seq: 2, type: "commandExecution", status: "completed", updatedAt: "2026-06-23T21:44:31.000Z" } + ], + eventCount: 2 + }; + const projection = createWorkbenchTurnProjection({ traceId, result, trace }); + assert.equal(projection.startedAt, "2026-06-23T21:43:48.740Z"); + assert.equal(projection.lastEventAt, "2026-06-23T21:44:33.486Z"); + assert.equal(projection.timing.lastEventAt, "2026-06-23T21:44:33.486Z"); +}); + test("workbench turn projection treats retryable provider stream disconnect as active", () => { const traceId = "trc_retryable_provider_stream_disconnect"; const result = { diff --git a/internal/cloud/workbench-turn-projection.ts b/internal/cloud/workbench-turn-projection.ts index 968cedf1..1b89e9f0 100644 --- a/internal/cloud/workbench-turn-projection.ts +++ b/internal/cloud/workbench-turn-projection.ts @@ -67,7 +67,7 @@ export function createWorkbenchTurnTimingProjection({ result = null, session = n firstEvent?.createdAt, firstEvent?.occurredAt ); - const lastEventAt = firstTimestamp( + const lastEventAt = latestTimestamp( result?.lastEventAt, directTiming?.lastEventAt, trace?.lastEventAt, @@ -382,6 +382,16 @@ function firstTimestamp(...values) { return null; } +function latestTimestamp(...values) { + let latest = null; + for (const value of values) { + const ms = Date.parse(String(value ?? "")); + if (!Number.isFinite(ms)) continue; + latest = latest === null ? ms : Math.max(latest, ms); + } + return latest === null ? null : new Date(latest).toISOString(); +} + function durationValue(...values) { let max = null; for (const value of values) {