@@ -208,16 +208,41 @@ export class RunPresenter {
208208
209209 //we need the start offset for each item, and the total duration of the entire tree
210210 const treeRootStartTimeMs = tree ? tree ?. data . startTime . getTime ( ) : 0 ;
211+
212+ const postgresRunDuration =
213+ runData . isFinished && run . completedAt
214+ ? millisecondsToNanoseconds (
215+ run . completedAt . getTime ( ) -
216+ ( run . rootTaskRun ?. createdAt ?? run . createdAt ) . getTime ( )
217+ )
218+ : 0 ;
219+
211220 let totalDuration = tree ?. data . duration ?? 0 ;
212221 const events = tree
213- ? flattenTree ( tree ) . map ( ( n ) => {
214- const offset = millisecondsToNanoseconds (
215- n . data . startTime . getTime ( ) - treeRootStartTimeMs
216- ) ;
222+ ? flattenTree ( tree ) . map ( ( n , index ) => {
223+ const isRoot = index === 0 ;
224+ const offset = millisecondsToNanoseconds ( n . data . startTime . getTime ( ) - treeRootStartTimeMs ) ;
225+
226+ let nIsPartial = n . data . isPartial ;
227+ let nDuration = n . data . duration ;
228+ let nIsError = n . data . isError ;
229+
230+ // NOTE: Clickhouse trace ingestion is eventually consistent.
231+ // When a run is marked finished in Postgres, we reconcile the
232+ // root span to reflect completion even if telemetry is still partial.
233+ // This is a deliberate UI-layer tradeoff to prevent stale or "stuck"
234+ // run states in the dashboard.
235+ if ( isRoot && runData . isFinished && nIsPartial ) {
236+ nIsPartial = false ;
237+ nDuration = Math . max ( nDuration ?? 0 , postgresRunDuration ) ;
238+ nIsError = isFailedRunStatus ( runData . status ) ;
239+ }
240+
217241 //only let non-debug events extend the total duration
218242 if ( ! n . data . isDebug ) {
219- totalDuration = Math . max ( totalDuration , offset + n . data . duration ) ;
243+ totalDuration = Math . max ( totalDuration , offset + ( nIsPartial ? 0 : nDuration ) ) ;
220244 }
245+
221246 return {
222247 ...n ,
223248 data : {
@@ -228,23 +253,24 @@ export class RunPresenter {
228253 treeRootStartTimeMs
229254 ) ,
230255 //set partial nodes to null duration
231- duration : n . data . isPartial ? null : n . data . duration ,
256+ duration : nIsPartial ? null : nDuration ,
257+ isPartial : nIsPartial ,
258+ isError : nIsError ,
232259 offset,
233- isRoot : n . id === traceSummary . rootSpan . id ,
260+ isRoot,
234261 } ,
235262 } ;
236263 } )
237264 : [ ] ;
238265
266+ if ( runData . isFinished ) {
267+ totalDuration = Math . max ( totalDuration , postgresRunDuration ) ;
268+ }
269+
239270 //total duration should be a minimum of 1ms
240271 totalDuration = Math . max ( totalDuration , millisecondsToNanoseconds ( 1 ) ) ;
241272
242- const reconciled = reconcileTraceWithRunLifecycle (
243- runData ,
244- traceSummary . rootSpan . id ,
245- events ,
246- totalDuration
247- ) ;
273+ const reconciled = reconcileTraceWithRunLifecycle ( runData , traceSummary . rootSpan . id , events , totalDuration ) ;
248274
249275 return {
250276 run : runData ,
@@ -285,14 +311,17 @@ export function reconcileTraceWithRunLifecycle(
285311 totalDuration : number ;
286312 rootSpanStatus : "executing" | "completed" | "failed" ;
287313} {
288- const rootEvent = events . find ( ( e ) => e . id === rootSpanId ) ;
289- const currentStatus : "executing" | "completed" | "failed" = rootEvent
290- ? rootEvent . data . isError
291- ? "failed"
292- : ! rootEvent . data . isPartial
293- ? "completed"
294- : "executing"
295- : "executing" ;
314+ const rootEvent = events [ 0 ] ;
315+ const isActualRoot = rootEvent ?. id === rootSpanId ;
316+
317+ const currentStatus : "executing" | "completed" | "failed" =
318+ isActualRoot && rootEvent
319+ ? rootEvent . data . isError
320+ ? "failed"
321+ : ! rootEvent . data . isPartial
322+ ? "completed"
323+ : "executing"
324+ : "executing" ;
296325
297326 if ( ! runData . isFinished ) {
298327 return { events, totalDuration, rootSpanStatus : currentStatus } ;
@@ -307,23 +336,28 @@ export function reconcileTraceWithRunLifecycle(
307336
308337 const updatedTotalDuration = Math . max ( totalDuration , postgresRunDuration ) ;
309338
310- const updatedEvents = events . map ( ( e ) => {
311- if ( e . id === rootSpanId && e . data . isPartial ) {
312- return {
313- ...e ,
314- data : {
315- ...e . data ,
316- isPartial : false ,
317- duration : Math . max ( e . data . duration ?? 0 , postgresRunDuration ) ,
318- isError : isFailedRunStatus ( runData . status ) ,
319- } ,
320- } ;
321- }
322- return e ;
323- } ) ;
339+ // We only need to potentially update the root event (the first one) if it matches our ID
340+ if ( isActualRoot && rootEvent && rootEvent . data . isPartial ) {
341+ const updatedEvents = [ ...events ] ;
342+ updatedEvents [ 0 ] = {
343+ ...rootEvent ,
344+ data : {
345+ ...rootEvent . data ,
346+ isPartial : false ,
347+ duration : Math . max ( rootEvent . data . duration ?? 0 , postgresRunDuration ) ,
348+ isError : isFailedRunStatus ( runData . status ) ,
349+ } ,
350+ } ;
351+
352+ return {
353+ events : updatedEvents ,
354+ totalDuration : updatedTotalDuration ,
355+ rootSpanStatus : isFailedRunStatus ( runData . status ) ? "failed" : "completed" ,
356+ } ;
357+ }
324358
325359 return {
326- events : updatedEvents ,
360+ events,
327361 totalDuration : updatedTotalDuration ,
328362 rootSpanStatus : isFailedRunStatus ( runData . status ) ? "failed" : "completed" ,
329363 } ;
0 commit comments