Merge pull request #2248 from pikasTech/fix/1217-terminal-trace-hydration

fix: hydrate terminal workbench traces
This commit is contained in:
Lyon
2026-06-28 18:16:41 +08:00
committed by GitHub
2 changed files with 51 additions and 5 deletions
@@ -691,8 +691,8 @@ async function createWorkbenchLaunch(request: IncomingMessage): Promise<JsonReco
const now = new Date().toISOString();
const sessionId = typeof body.sessionId === "string" && body.sessionId.trim() ? body.sessionId : `ses_project_launch_${Date.now().toString(36)}`;
const conversationId = typeof body.conversationId === "string" && body.conversationId.trim() ? body.conversationId : `cnv_project_launch_${Date.now().toString(36)}`;
const authoritative = projectManagementLaunchContextForTask(typeof body.taskRef === "string" ? body.taskRef : null)?.launchContext;
const launchContext = authoritative ?? (body.launchContext && typeof body.launchContext === "object" ? body.launchContext as JsonRecord : {});
const authoritative = projectManagementLaunchContextForTask(typeof body.taskRef === "string" ? body.taskRef : null)?.launchContext as JsonRecord | undefined;
const launchContext: JsonRecord = authoritative ?? (body.launchContext && typeof body.launchContext === "object" ? body.launchContext as JsonRecord : {} as JsonRecord);
const session: SessionRecord = {
sessionId,
conversationId,
+49 -3
View File
@@ -80,6 +80,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
const traceHydrationInFlight = new Set<string>();
const traceHydrationQueued = new Set<string>();
const traceHydrationQueue: ChatMessage[] = [];
const forcedTraceHydrationRetryTimers = new Map<string, number>();
let traceHydrationPumpActive = false;
const terminalRealtimeRefreshInFlight = new Set<string>();
const realtimeSessionMessagesInFlight = new Set<string>();
@@ -614,7 +615,9 @@ export const useWorkbenchStore = defineStore("workbench", () => {
);
if (!response.ok || !response.data) return;
const pageMessages = Array.isArray(response.data.messages) ? response.data.messages.map((message) => normalizeChatMessage(message as ChatMessage)) : [];
rememberSessionMessages(id, mergeMessageProjectionPage(id, pageMessages));
const merged = mergeMessageProjectionPage(id, pageMessages);
rememberSessionMessages(id, merged);
hydrateTerminalTraceGaps(merged, "session-message-page");
}
async function refreshRealtimeSessionMessages(sessionId: string | null | undefined, reason: string, options: { force?: boolean } = {}): Promise<void> {
@@ -647,7 +650,9 @@ export const useWorkbenchStore = defineStore("workbench", () => {
return;
}
const pageMessages = Array.isArray(response.data.messages) ? response.data.messages.map((message) => normalizeChatMessage(message as ChatMessage)) : [];
rememberSessionMessages(id, mergeMessageProjectionPage(id, pageMessages));
const merged = mergeMessageProjectionPage(id, pageMessages);
rememberSessionMessages(id, merged);
hydrateTerminalTraceGaps(merged, `trace-message-page:${traceId}`);
}
function mergeMessageProjectionPage(sessionId: string, pageMessages: ChatMessage[]): ChatMessage[] {
@@ -914,7 +919,10 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project
async function hydrateTraceEventsForMessage(message: ChatMessage, options: { force?: boolean } = {}): Promise<void> {
const traceId = message.traceId ?? message.runnerTrace?.traceId;
if (!traceId) return;
if (traceHydrationInFlight.has(traceId)) return;
if (traceHydrationInFlight.has(traceId)) {
if (options.force) scheduleForcedTraceHydrationRetry(message);
return;
}
traceHydrationInFlight.add(traceId);
try {
let afterProjectedSeq = traceHydrationProjectedSeq(message.runnerTrace);
@@ -951,6 +959,31 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project
return new Promise((resolve) => window.setTimeout(resolve, ms));
}
function hydrateTerminalTraceGaps(source: ChatMessage[], reason: string): void {
for (const message of terminalTraceHydrationGaps(source)) {
const traceId = firstNonEmptyString(message.traceId, message.runnerTrace?.traceId);
if (traceId && activeSessionId.value) recordActivity(`${reason}:${traceId}`);
void hydrateTraceEventsForMessage(message, { force: true });
}
}
function terminalTraceHydrationGaps(source: ChatMessage[]): ChatMessage[] {
return source.filter(messageNeedsTerminalTraceGapHydration).slice(-TRACE_HYDRATION_AUTO_QUEUE_LIMIT).reverse();
}
function scheduleForcedTraceHydrationRetry(message: ChatMessage): void {
const traceId = firstNonEmptyString(message.traceId, message.runnerTrace?.traceId);
if (!traceId || typeof window === "undefined" || forcedTraceHydrationRetryTimers.has(traceId)) return;
const timer = window.setTimeout(() => {
forcedTraceHydrationRetryTimers.delete(traceId);
const ownerSessionId = traceOwnerSessionId(traceId, messageSessionAuthority(message));
const ownerMessages = ownerSessionId ? serverState.value.messagesBySessionId[ownerSessionId] ?? [] : messages.value;
const latest = latestMessageForTrace(traceId, ownerMessages) ?? message;
void hydrateTraceEventsForMessage(latest, { force: true });
}, TRACE_HYDRATION_RETRY_DELAY_MS);
forcedTraceHydrationRetryTimers.set(traceId, timer);
}
async function hydrateTraceEvents(source: ChatMessage[] = messages.value): Promise<void> {
for (const message of traceHydrationCandidates(source)) queueTraceHydration(message);
void pumpTraceHydrationQueue();
@@ -2453,6 +2486,19 @@ function messageNeedsTraceHydration(message: ChatMessage): boolean {
return events.length === 0 || trace?.eventsCompacted === true || trace?.fullTraceLoaded !== true;
}
function messageNeedsTerminalTraceGapHydration(message: ChatMessage): boolean {
if (!messageHasCompletedFinalResponse(message)) return false;
const traceId = firstNonEmptyString(message.traceId, message.runnerTrace?.traceId);
if (!traceId) return false;
const trace = message.runnerTrace;
if (!trace) return true;
const events = Array.isArray(trace.events) ? trace.events : [];
const eventCount = firstFiniteNumber(trace.eventCount) ?? events.length;
if (trace.eventsCompacted === true || trace.fullTraceLoaded !== true) return true;
if (events.length === 0 && eventCount > 0) return true;
return isTraceActiveStatus(trace.status) || isTraceActiveStatus(trace.traceStatus);
}
function traceHasCompletedFinalResponse(traceId: string | null | undefined, source: ChatMessage[]): boolean {
const id = firstNonEmptyString(traceId);
if (!id) return false;