diff --git a/internal/cloud/server-workbench-http.ts b/internal/cloud/server-workbench-http.ts index 0f3eae12..62f935da 100644 --- a/internal/cloud/server-workbench-http.ts +++ b/internal/cloud/server-workbench-http.ts @@ -144,34 +144,45 @@ export async function handleWorkbenchRealtimeHttp(request, response, url, option emitWorkbenchRealtimeAcceptedOtelSpan(request, options.env); if (activeTraceId) { await (perf ? perf.measure("workbench_initial_trace", () => writeTraceRealtimeSnapshotSafe({ writeEvent, options, actor: auth.actor, traceId: activeTraceId, reason: "initial" })) : writeTraceRealtimeSnapshotSafe({ writeEvent, options, actor: auth.actor, traceId: activeTraceId, reason: "initial" })); - const runtimeStore = options.runtimeStore ?? null; - const outboxPollMs = parsePositiveInteger(options.env?.HWLAB_WORKBENCH_SSE_OUTBOX_POLL_MS, 2000); - let outboxCursor = 0; - if (typeof runtimeStore?.readWorkbenchProjectionOutbox === "function") { + } + const runtimeStore = options.runtimeStore ?? null; + const outboxPollMs = parsePositiveInteger(options.env?.HWLAB_WORKBENCH_SSE_OUTBOX_POLL_MS, 2000); + let outboxCursor = 0; + const outboxTraceSnapshots = new Set(activeTraceId ? [activeTraceId] : []); + if (typeof runtimeStore?.readWorkbenchProjectionOutbox === "function" && (streamSessionId || activeTraceId)) { const pollOutbox = async () => { if (closed || response.destroyed) return; try { - const rows = await runtimeStore.readWorkbenchProjectionOutbox({ afterSeq: outboxCursor, limit: 50, traceId: activeTraceId }); + const query = { afterSeq: outboxCursor, limit: 50 }; + if (streamSessionId) query.sessionId = streamSessionId; + else query.traceId = activeTraceId; + const rows = await runtimeStore.readWorkbenchProjectionOutbox(query); for (const row of rows) { if (closed || response.destroyed) break; outboxCursor = row.outboxSeq; + const rowTraceId = safeTraceId(row.traceId) ?? activeTraceId; + const rowSessionId = safeSessionId(row.sessionId ?? streamSessionId) ?? streamSessionId; + if (rowTraceId && !outboxTraceSnapshots.has(rowTraceId)) { + outboxTraceSnapshots.add(rowTraceId); + await writeTraceRealtimeSnapshotSafe({ writeEvent, options, actor: auth.actor, traceId: rowTraceId, reason: "outbox-session" }); + } if (row.commitType === "terminal") { writeEvent("workbench.trace.snapshot", { type: "trace.snapshot", - sessionId: row.sessionId ?? streamSessionId, + sessionId: rowSessionId, threadId: streamThreadId, - traceId: row.traceId ?? activeTraceId, + traceId: rowTraceId, reason: "outbox-terminal", cursor: { traceSeq: row.projectedSeq, outboxSeq: row.outboxSeq }, projectionStatus: { terminal: row.terminal, sealed: row.sealed } }); - void writeTraceRealtimeSnapshotSafe({ writeEvent, options, actor: auth.actor, traceId: activeTraceId, reason: "terminal" }); + if (rowTraceId) void writeTraceRealtimeSnapshotSafe({ writeEvent, options, actor: auth.actor, traceId: rowTraceId, reason: "terminal" }); } else { writeEvent("workbench.trace.event", { type: "trace.event", - sessionId: row.sessionId ?? streamSessionId, + sessionId: rowSessionId, threadId: streamThreadId, - traceId: row.traceId ?? activeTraceId, + traceId: rowTraceId, cursor: { traceSeq: row.projectedSeq, outboxSeq: row.outboxSeq } }); } @@ -186,7 +197,7 @@ export async function handleWorkbenchRealtimeHttp(request, response, url, option await pollOutbox(); const outboxTimer = setInterval(() => { void pollOutbox(); }, Math.max(500, outboxPollMs)); cleanup.push(() => clearInterval(outboxTimer)); - } else { + } else if (activeTraceId) { const unsubscribe = traceStore.subscribe(activeTraceId, (event, snapshot) => { if (event) { writeEvent("workbench.trace.event", {