From ad70a508a879b826cc546562e16c4f43df7282a3 Mon Sep 17 00:00:00 2001 From: lyon Date: Fri, 19 Jun 2026 12:43:16 +0800 Subject: [PATCH] fix: cache terminal durable workbench trace reads --- internal/cloud/server-workbench-http.test.ts | 3 ++ internal/cloud/workbench-facts-store.ts | 34 +++++++++++++++++--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/internal/cloud/server-workbench-http.test.ts b/internal/cloud/server-workbench-http.test.ts index c771cd1c..3588037d 100644 --- a/internal/cloud/server-workbench-http.test.ts +++ b/internal/cloud/server-workbench-http.test.ts @@ -183,8 +183,10 @@ test("workbench read model recovers trace events from durable projection without async ensureBootstrap() {}, async authenticate() { return { ok: true, actor: ACTOR, session: { id: "uss_workbench_reader" } }; } }; + let traceQueryCount = 0; const runtimeStore = { async queryAgentTraceEvents(params = {}) { + traceQueryCount += 1; assert.equal(params.traceId, traceId); return { events: durableEvents, count: durableEvents.length }; } @@ -210,6 +212,7 @@ test("workbench read model recovers trace events from durable projection without assert.equal(trace.body.events.length, 2); assert.equal(trace.body.traceStatus, "completed"); assert.equal(trace.body.hasMore, false); + assert.equal(traceQueryCount, 1); } finally { await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); } diff --git a/internal/cloud/workbench-facts-store.ts b/internal/cloud/workbench-facts-store.ts index 27451569..9f04a07a 100644 --- a/internal/cloud/workbench-facts-store.ts +++ b/internal/cloud/workbench-facts-store.ts @@ -6,6 +6,9 @@ import { defaultCodeAgentTraceStore } from "./code-agent-trace-store.ts"; import { safeConversationId, safeSessionId, safeTraceId } from "./server-http-utils.ts"; import { durableTraceStatus, normalizeWorkbenchStatus, TERMINAL_STATUSES, traceTerminalEvidence } from "./workbench-turn-projection.ts"; +const TERMINAL_DURABLE_TRACE_CACHE_MAX = 256; +const terminalDurableTraceCache = new Map(); + export function createWorkbenchFactsStore(options = {}, actor = null) { const accessStore = options.accessController?.store ?? options.accessController ?? null; const traceStore = options.traceStore ?? defaultCodeAgentTraceStore; @@ -101,21 +104,29 @@ export function createWorkbenchFactsStore(options = {}, actor = null) { async function durableTraceSnapshot(runtimeStore, traceId) { if (!runtimeStore || typeof runtimeStore.queryAgentTraceEvents !== "function") return null; + const safeId = safeTraceId(traceId); + if (!safeId) return null; + const cached = terminalDurableTraceCache.get(safeId); + if (cached) { + terminalDurableTraceCache.delete(safeId); + terminalDurableTraceCache.set(safeId, cached); + return cached; + } let result; try { - result = await runtimeStore.queryAgentTraceEvents({ traceId }); + result = await runtimeStore.queryAgentTraceEvents({ traceId: safeId }); } catch (error) { - return projectionStoreUnavailableTrace(traceId, error); + return projectionStoreUnavailableTrace(safeId, error); } const events = Array.isArray(result?.events) ? result.events : []; if (events.length === 0) return null; - const normalizedEvents = events.map((event, index) => ({ ...event, traceId, seq: eventSeq(event, index) })); + const normalizedEvents = events.map((event, index) => ({ ...event, traceId: safeId, seq: eventSeq(event, index) })); const firstEvent = normalizedEvents[0] ?? null; const lastEvent = normalizedEvents.at(-1) ?? null; const status = durableTraceStatus(normalizedEvents); const terminalEvidence = traceTerminalEvidence({ events: normalizedEvents, status }); - return { - traceId, + const snapshot = { + traceId: safeId, status, createdAt: firstEvent?.createdAt ?? null, updatedAt: lastEvent?.createdAt ?? firstEvent?.createdAt ?? null, @@ -130,6 +141,19 @@ async function durableTraceSnapshot(runtimeStore, traceId) { outputTruncated: normalizedEvents.some((event) => event.outputTruncated === true), valuesPrinted: false }; + rememberTerminalDurableTrace(snapshot); + return snapshot; +} + +function rememberTerminalDurableTrace(snapshot) { + const traceId = safeTraceId(snapshot?.traceId); + if (!traceId || !TERMINAL_STATUSES.has(normalizeStatus(snapshot?.status))) return; + terminalDurableTraceCache.delete(traceId); + terminalDurableTraceCache.set(traceId, snapshot); + while (terminalDurableTraceCache.size > TERMINAL_DURABLE_TRACE_CACHE_MAX) { + const oldest = terminalDurableTraceCache.keys().next().value; + terminalDurableTraceCache.delete(oldest); + } } function projectionStoreUnavailableTrace(traceId, error) {