@@ -8,6 +8,7 @@ import { SpanSummary } from "~/v3/eventRepository/eventRepository.types";
88import { getTaskEventStoreTableForRun } from "~/v3/taskEventStore.server" ;
99import { isFailedRunStatus , isFinalRunStatus } from "~/v3/taskStatus" ;
1010import { env } from "~/env.server" ;
11+ import { reconcileTraceWithRunLifecycle } from "./reconcileTrace.server" ;
1112
1213type Result = Awaited < ReturnType < RunPresenter [ "call" ] > > ;
1314export type Run = Result [ "run" ] ;
@@ -124,6 +125,7 @@ export class RunPresenter {
124125 isFinished : isFinalRunStatus ( run . status ) ,
125126 startedAt : run . startedAt ,
126127 completedAt : run . completedAt ,
128+ createdAt : run . createdAt ,
127129 logsDeletedAt : showDeletedLogs ? null : run . logsDeletedAt ,
128130 rootTaskRun : run . rootTaskRun ,
129131 parentTaskRun : run . parentTaskRun ,
@@ -210,14 +212,30 @@ export class RunPresenter {
210212 const treeRootStartTimeMs = tree ? tree ?. data . startTime . getTime ( ) : 0 ;
211213 let totalDuration = tree ?. data . duration ?? 0 ;
212214 const events = tree
213- ? flattenTree ( tree ) . map ( ( n ) => {
214- const offset = millisecondsToNanoseconds (
215- n . data . startTime . getTime ( ) - treeRootStartTimeMs
216- ) ;
215+ ? flattenTree ( tree ) . map ( ( n , index ) => {
216+ const isRoot = index === 0 ;
217+ const offset = millisecondsToNanoseconds ( n . data . startTime . getTime ( ) - treeRootStartTimeMs ) ;
218+
219+ let nIsPartial = n . data . isPartial ;
220+ let nDuration = n . data . duration ;
221+ let nIsError = n . data . isError ;
222+
223+ // NOTE: Clickhouse trace ingestion is eventually consistent.
224+ // When a run is marked finished in Postgres, we reconcile the
225+ // root span to reflect completion even if telemetry is still partial.
226+ // This is a deliberate UI-layer tradeoff to prevent stale or "stuck"
227+ // run states in the dashboard.
228+ if ( isRoot && runData . isFinished && nIsPartial ) {
229+ nIsPartial = false ;
230+ nDuration = Math . max ( nDuration ?? 0 , postgresRunDuration ) ;
231+ nIsError = isFailedRunStatus ( runData . status ) ;
232+ }
233+
217234 //only let non-debug events extend the total duration
218235 if ( ! n . data . isDebug ) {
219- totalDuration = Math . max ( totalDuration , offset + n . data . duration ) ;
236+ totalDuration = Math . max ( totalDuration , offset + ( nIsPartial ? 0 : nDuration ) ) ;
220237 }
238+
221239 return {
222240 ...n ,
223241 data : {
@@ -228,9 +246,11 @@ export class RunPresenter {
228246 treeRootStartTimeMs
229247 ) ,
230248 //set partial nodes to null duration
231- duration : n . data . isPartial ? null : n . data . duration ,
249+ duration : nIsPartial ? null : nDuration ,
250+ isPartial : nIsPartial ,
251+ isError : nIsError ,
232252 offset,
233- isRoot : n . id === traceSummary . rootSpan . id ,
253+ isRoot,
234254 } ,
235255 } ;
236256 } )
@@ -264,67 +284,3 @@ export class RunPresenter {
264284 }
265285}
266286
267- // NOTE: Clickhouse trace ingestion is eventually consistent.
268- // When a run is marked finished in Postgres, we reconcile the
269- // root span to reflect completion even if telemetry is still partial.
270- // This is a deliberate UI-layer tradeoff to prevent stale or "stuck"
271- // run states in the dashboard.
272- export function reconcileTraceWithRunLifecycle (
273- runData : {
274- isFinished : boolean ;
275- status : Run [ "status" ] ;
276- createdAt : Date ;
277- completedAt : Date | null ;
278- rootTaskRun : { createdAt : Date } | null ;
279- } ,
280- rootSpanId : string ,
281- events : RunEvent [ ] ,
282- totalDuration : number
283- ) : {
284- events : RunEvent [ ] ;
285- totalDuration : number ;
286- rootSpanStatus : "executing" | "completed" | "failed" ;
287- } {
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" ;
296-
297- if ( ! runData . isFinished ) {
298- return { events, totalDuration, rootSpanStatus : currentStatus } ;
299- }
300-
301- const postgresRunDuration = runData . completedAt
302- ? millisecondsToNanoseconds (
303- runData . completedAt . getTime ( ) -
304- ( runData . rootTaskRun ?. createdAt ?? runData . createdAt ) . getTime ( )
305- )
306- : 0 ;
307-
308- const updatedTotalDuration = Math . max ( totalDuration , postgresRunDuration ) ;
309-
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- } ) ;
324-
325- return {
326- events : updatedEvents ,
327- totalDuration : updatedTotalDuration ,
328- rootSpanStatus : isFailedRunStatus ( runData . status ) ? "failed" : "completed" ,
329- } ;
330- }
0 commit comments