From c9da0ec416f7520444b0924499eccf198e409899 Mon Sep 17 00:00:00 2001 From: UniDesk Codex Date: Fri, 3 Jul 2026 04:03:09 +0800 Subject: [PATCH] fix(workbench): coalesce realtime turn snapshots by trace --- .../src/api/workbench-events.test.ts | 18 ++++++++++++++++++ .../src/api/workbench-events.ts | 7 ++++--- 2 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 web/hwlab-cloud-web/src/api/workbench-events.test.ts diff --git a/web/hwlab-cloud-web/src/api/workbench-events.test.ts b/web/hwlab-cloud-web/src/api/workbench-events.test.ts new file mode 100644 index 00000000..33273739 --- /dev/null +++ b/web/hwlab-cloud-web/src/api/workbench-events.test.ts @@ -0,0 +1,18 @@ +import assert from "node:assert/strict"; +import { test } from "bun:test"; + +import { realtimeCoalesceKey, type WorkbenchRealtimeEvent } from "./workbench-events"; + +test("turn snapshot coalescing keys by trace instead of per sequence", () => { + const first: WorkbenchRealtimeEvent = { type: "turn.snapshot", turn: { sessionId: "ses_queue", traceId: "trc_queue", status: "running" }, cursor: { traceSeq: 10 } }; + const next: WorkbenchRealtimeEvent = { type: "turn.snapshot", turn: { sessionId: "ses_queue", traceId: "trc_queue", status: "running" }, cursor: { traceSeq: 11 } }; + + assert.equal(realtimeCoalesceKey(first, "workbench.turn.snapshot"), realtimeCoalesceKey(next, "workbench.turn.snapshot")); +}); + +test("trace events keep sequence-specific coalescing keys", () => { + const first: WorkbenchRealtimeEvent = { type: "trace.event", traceId: "trc_queue", event: { traceId: "trc_queue", projectedSeq: 10 } }; + const next: WorkbenchRealtimeEvent = { type: "trace.event", traceId: "trc_queue", event: { traceId: "trc_queue", projectedSeq: 11 } }; + + assert.notEqual(realtimeCoalesceKey(first, "workbench.trace.event"), realtimeCoalesceKey(next, "workbench.trace.event")); +}); diff --git a/web/hwlab-cloud-web/src/api/workbench-events.ts b/web/hwlab-cloud-web/src/api/workbench-events.ts index bf0d2063..f7075854 100644 --- a/web/hwlab-cloud-web/src/api/workbench-events.ts +++ b/web/hwlab-cloud-web/src/api/workbench-events.ts @@ -176,11 +176,12 @@ function scheduleRealtimeFlushYield(flush: () => void, yieldMs: number | null | return () => clearTimeout(id); } -function realtimeCoalesceKey(event: WorkbenchRealtimeEvent, eventName: string): string | null { - const sessionId = firstScopePart(event.sessionId, event.message?.sessionId, event.snapshot?.sessionId, event.event?.sessionId); - const traceId = firstScopePart(event.traceId, event.message?.traceId, event.snapshot?.traceId, event.event?.traceId); +export function realtimeCoalesceKey(event: WorkbenchRealtimeEvent, eventName: string): string | null { + const sessionId = firstScopePart(event.sessionId, event.message?.sessionId, event.snapshot?.sessionId, event.event?.sessionId, event.turn?.sessionId); + const traceId = firstScopePart(event.traceId, event.message?.traceId, event.snapshot?.traceId, event.event?.traceId, event.turn?.traceId); const outboxSeq = numericCursor(event.cursor?.outboxSeq ?? event.outboxSeq); const traceSeq = numericCursor(event.cursor?.traceSeq ?? event.traceSeq ?? event.event?.seq ?? event.event?.projectedSeq); + if (eventName === "workbench.turn.snapshot" && traceId) return composeWorkbenchScopedKey("workbench.sse.turn-snapshot", sessionId, traceId); if (eventName === "workbench.trace.event" || eventName === "message") { const eventSeq = outboxSeq ?? traceSeq; return eventSeq === null ? null : composeWorkbenchScopedKey("workbench.sse.event", sessionId, traceId, eventName, eventSeq);