@@ -3929,6 +3929,35 @@ export function ProjectView({
39293929 let replayedContent = needsFullReplay ? '' : message . content ;
39303930 let replayedEvents : AgentEvent [ ] = needsFullReplay ? [ ] : [ ...( message . events ?? [ ] ) ] ;
39313931 let latestReattachRunStatus : ChatMessage [ 'runStatus' ] = status . status ;
3932+ const updateReattachConversationLatestRun = (
3933+ nextStatus : NonNullable < ChatMessage [ 'runStatus' ] > ,
3934+ endedAt ?: number ,
3935+ ) => {
3936+ setConversations ( ( curr ) =>
3937+ curr . map ( ( conversation ) => {
3938+ if ( conversation . id !== reattachConversationId ) return conversation ;
3939+ const startedAt =
3940+ conversation . latestRun ?. startedAt
3941+ ?? status . createdAt
3942+ ?? message . startedAt
3943+ ?? message . createdAt ;
3944+ return {
3945+ ...conversation ,
3946+ updatedAt : endedAt ?? conversation . updatedAt ,
3947+ latestRun : {
3948+ status : nextStatus ,
3949+ startedAt,
3950+ ...( endedAt === undefined
3951+ ? { }
3952+ : {
3953+ endedAt,
3954+ durationMs : Math . max ( 0 , endedAt - startedAt ) ,
3955+ } ) ,
3956+ } ,
3957+ } ;
3958+ } ) ,
3959+ ) ;
3960+ } ;
39323961 const applyContentDelta = ( delta : string ) => {
39333962 for ( const ev of parser . feed ( delta ) ) {
39343963 if ( ev . type === 'artifact:start' ) {
@@ -4074,6 +4103,10 @@ export function ProjectView({
40744103 ? { telemetryFinalized : true }
40754104 : undefined ,
40764105 ) ;
4106+ updateReattachConversationLatestRun (
4107+ latestReattachRunStatus === 'canceled' ? 'canceled' : 'succeeded' ,
4108+ endedAt ,
4109+ ) ;
40774110 if ( latestReattachRunStatus === 'canceled' ) return ;
40784111 void ( async ( ) => {
40794112 const preTurn = message . preTurnFileNames ;
@@ -4388,6 +4421,7 @@ export function ProjectView({
43884421 true ,
43894422 ) ;
43904423 latestReattachRunStatus = runStatus ;
4424+ updateReattachConversationLatestRun ( runStatus ) ;
43914425 if ( runStatus === 'canceled' ) {
43924426 textBuffer . cancel ( ) ;
43934427 unregisterTextBuffer ( ) ;
@@ -4975,6 +5009,8 @@ export function ProjectView({
49755009 // that just failed in the current session (the daemon status fetch is only
49765010 // needed on reload, not for runs that are already known to have failed).
49775011 let currentRunId : string | undefined = undefined ;
5012+ let latestLiveDaemonRunStatus : ChatMessage [ 'runStatus' ] =
5013+ config . mode === 'daemon' ? 'running' : undefined ;
49785014 const updateConversationLatestRun = (
49795015 status : NonNullable < ChatMessage [ 'runStatus' ] > ,
49805016 endedAt ?: number ,
@@ -5159,6 +5195,23 @@ export function ProjectView({
51595195 }
51605196 persistMessageById ( assistantId , { keepalive : true } ) ;
51615197 } ;
5198+ const resolveLiveDaemonTerminalRun = async (
5199+ fallbackStatus ?: TerminalRunStatus | null ,
5200+ options ?: { allowFallbackOnActiveProbe ?: boolean } ,
5201+ ) : Promise < TerminalRunResolution | null > => {
5202+ if ( ! currentRunId ) {
5203+ if ( ! fallbackStatus ) return null ;
5204+ return {
5205+ status : fallbackStatus ,
5206+ endedAt : Date . now ( ) ,
5207+ authoritative : false ,
5208+ } ;
5209+ }
5210+ return resolveDaemonTerminalRunCompletion ( currentRunId , {
5211+ fallbackStatus,
5212+ allowFallbackOnActiveProbe : options ?. allowFallbackOnActiveProbe ,
5213+ } ) ;
5214+ } ;
51625215 const pushEvent = ( ev : AgentEvent ) => {
51635216 textBuffer . flush ( ) ;
51645217 updateAssistant ( ( prev ) => ( { ...prev , events : [ ...( prev . events ?? [ ] ) , ev ] } ) ) ;
@@ -5330,7 +5383,7 @@ export function ProjectView({
53305383 } ,
53315384 } ) ) ;
53325385 } ,
5333- onDone : ( fullText = '' ) => {
5386+ onDone : async ( fullText = '' ) => {
53345387 // The daemon delivers onDone even for a canceled run, so a run
53355388 // superseded by a "send now" interrupt can still land here and must
53365389 // not apply its completion side effects over the replacement. A run
@@ -5398,25 +5451,65 @@ export function ProjectView({
53985451 clearTraceTouchedFilePaths ( ) ;
53995452 return ;
54005453 }
5401- const endedAt = Date . now ( ) ;
5454+ let endedAt = Date . now ( ) ;
54025455 let finalRunStatus : ChatMessage [ 'runStatus' ] = 'succeeded' ;
5403- updateAssistant ( ( prev ) => {
5404- finalRunStatus = resolveSucceededRunStatus ( prev . runStatus ) ;
5405- return {
5456+ if ( config . mode === 'daemon' ) {
5457+ const terminalFallbackStatus =
5458+ asTerminalRunStatus ( latestLiveDaemonRunStatus )
5459+ ?? asTerminalRunStatus ( resolveSucceededRunStatus ( latestLiveDaemonRunStatus ) ) ;
5460+ const terminalRun = await resolveLiveDaemonTerminalRun (
5461+ terminalFallbackStatus ,
5462+ {
5463+ allowFallbackOnActiveProbe : asTerminalRunStatus ( latestLiveDaemonRunStatus ) !== null ,
5464+ } ,
5465+ ) ;
5466+ if ( ! terminalRun ) {
5467+ const ownsCurrentRun = clearCurrentRunStreamingMarker (
5468+ runConversationId ,
5469+ controller ,
5470+ cancelController ,
5471+ ) ;
5472+ if ( ownsCurrentRun ) setRecoveryTick ( ( t ) => t + 1 ) ;
5473+ scheduleConversationMessageRefresh ( runConversationId ) ;
5474+ clearTraceTouchedFilePaths ( ) ;
5475+ return ;
5476+ }
5477+ endedAt = terminalRun . endedAt ;
5478+ finalRunStatus = terminalRun . status ;
5479+ latestLiveDaemonRunStatus = finalRunStatus ;
5480+ if ( finalRunStatus === 'canceled' ) setError ( null ) ;
5481+ updateAssistant ( ( prev ) => ( {
54065482 ...prev ,
54075483 endedAt,
54085484 runStatus : finalRunStatus ,
5409- } ;
5410- } ) ;
5411- if ( runCommentAttachments . length > 0 ) {
5412- void patchAttachedStatuses ( runCommentAttachments , 'needs_review' ) ;
5485+ ...( terminalRun . resumable !== undefined
5486+ ? { resumable : terminalRun . resumable }
5487+ : { } ) ,
5488+ } ) ) ;
5489+ } else {
5490+ updateAssistant ( ( prev ) => {
5491+ finalRunStatus = resolveSucceededRunStatus ( prev . runStatus ) ;
5492+ return {
5493+ ...prev ,
5494+ endedAt,
5495+ runStatus : finalRunStatus ,
5496+ } ;
5497+ } ) ;
54135498 }
54145499 const ownsCurrentRun = clearCurrentRunStreamingMarker (
54155500 runConversationId ,
54165501 controller ,
54175502 cancelController ,
54185503 ) ;
54195504 if ( ownsCurrentRun ) updateConversationLatestRun ( finalRunStatus ?? 'succeeded' , endedAt ) ;
5505+ if ( finalRunStatus !== 'succeeded' ) {
5506+ scheduleConversationMessageRefresh ( runConversationId ) ;
5507+ clearTraceTouchedFilePaths ( ) ;
5508+ return ;
5509+ }
5510+ if ( runCommentAttachments . length > 0 ) {
5511+ void patchAttachedStatuses ( runCommentAttachments , 'needs_review' ) ;
5512+ }
54205513 // Refetch the file list directly (rather than just bumping the
54215514 // refresh signal) so we can diff against the pre-turn snapshot
54225515 // and attach the new files to the assistant message as download
@@ -5815,32 +5908,62 @@ export function ProjectView({
58155908 } ;
58165909 latestAssistantMsg = pinnedAssistant ;
58175910 currentRunId = runId ;
5911+ latestLiveDaemonRunStatus = 'queued' ;
58185912 // The view may already be on a different project/conversation;
58195913 // pin the daemon run to the original row so returning can reattach.
58205914 void saveMessage ( project . id , runConversationId , pinnedAssistant ) ;
58215915 updateMessageById ( assistantId , ( prev ) => ( { ...prev , runId, runStatus : 'queued' } ) ) ;
58225916 } ,
58235917 onRunStatus : ( runStatus ) => {
5824- const endedAt = isTerminalRunStatus ( runStatus ) ? Date . now ( ) : undefined ;
58255918 const runMayFinalize =
58265919 ! supersededRunsRef . current . has ( controller ) ;
5827- updateMessageById (
5828- assistantId ,
5829- ( prev ) => ( {
5830- ...prev ,
5831- runStatus,
5832- endedAt : endedAt === undefined ? prev . endedAt : prev . endedAt ?? endedAt ,
5833- } ) ,
5834- true ,
5835- runStatus === 'canceled' ? { telemetryFinalized : true } : undefined ,
5836- ) ;
5837- if ( ! runMayFinalize ) return ;
5838- updateConversationLatestRun ( runStatus , endedAt ) ;
5839- if ( isTerminalRunStatus ( runStatus ) ) {
5920+ latestLiveDaemonRunStatus = runStatus ;
5921+ if ( ! isTerminalRunStatus ( runStatus ) ) {
5922+ updateMessageById (
5923+ assistantId ,
5924+ ( prev ) => ( {
5925+ ...prev ,
5926+ runStatus,
5927+ endedAt : prev . endedAt ,
5928+ } ) ,
5929+ true ,
5930+ ) ;
5931+ if ( ! runMayFinalize ) return ;
5932+ updateConversationLatestRun ( runStatus ) ;
5933+ return ;
5934+ }
5935+ void ( async ( ) => {
5936+ const fallbackStatus = asTerminalRunStatus ( runStatus ) ?? 'succeeded' ;
5937+ const terminalRun = await resolveLiveDaemonTerminalRun ( fallbackStatus , {
5938+ allowFallbackOnActiveProbe : true ,
5939+ } ) ?? {
5940+ status : fallbackStatus ,
5941+ endedAt : Date . now ( ) ,
5942+ authoritative : false ,
5943+ } ;
5944+ latestLiveDaemonRunStatus = terminalRun . status ;
5945+ if ( terminalRun . status === 'canceled' ) setError ( null ) ;
5946+ updateMessageById (
5947+ assistantId ,
5948+ ( prev ) => ( {
5949+ ...prev ,
5950+ runStatus : terminalRun . status ,
5951+ endedAt : terminalRun . endedAt ,
5952+ ...( terminalRun . resumable !== undefined
5953+ ? { resumable : terminalRun . resumable }
5954+ : { } ) ,
5955+ } ) ,
5956+ true ,
5957+ terminalRun . status === 'canceled'
5958+ ? { telemetryFinalized : true }
5959+ : undefined ,
5960+ ) ;
5961+ if ( ! runMayFinalize ) return ;
5962+ updateConversationLatestRun ( terminalRun . status , terminalRun . endedAt ) ;
58405963 clearCurrentRunStreamingMarker ( runConversationId , controller , cancelController ) ;
58415964 scheduleConversationMessageRefresh ( runConversationId ) ;
5842- if ( runStatus !== 'succeeded' ) clearTraceTouchedFilePaths ( ) ;
5843- }
5965+ if ( terminalRun . status !== 'succeeded' ) clearTraceTouchedFilePaths ( ) ;
5966+ } ) ( ) ;
58445967 } ,
58455968 onRunEventId : ( lastRunEventId ) => {
58465969 updateMessageById ( assistantId , ( prev ) => ( { ...prev , lastRunEventId } ) ) ;
@@ -9055,6 +9178,65 @@ function isActiveRunStatus(status: ChatMessage['runStatus']): boolean {
90559178
90569179/** A daemon run-status snapshot, as returned by `fetchChatRunStatus`/`listActiveChatRuns`. */
90579180type RunStatusSnapshot = Awaited < ReturnType < typeof fetchChatRunStatus > > ;
9181+ type TerminalRunStatus = Extract < NonNullable < ChatMessage [ 'runStatus' ] > , 'succeeded' | 'failed' | 'canceled' > ;
9182+
9183+ type TerminalRunResolution = {
9184+ status : TerminalRunStatus ;
9185+ endedAt : number ;
9186+ authoritative : boolean ;
9187+ resumable ?: boolean ;
9188+ } ;
9189+
9190+ function asTerminalRunStatus ( status : ChatMessage [ 'runStatus' ] ) : TerminalRunStatus | null {
9191+ if ( status === 'succeeded' || status === 'failed' || status === 'canceled' ) {
9192+ return status ;
9193+ }
9194+ return null ;
9195+ }
9196+
9197+ async function resolveDaemonTerminalRunCompletion (
9198+ runId : string ,
9199+ options : {
9200+ candidate ?: RunStatusSnapshot | null ;
9201+ fallbackStatus ?: TerminalRunStatus | null ;
9202+ allowFallbackOnActiveProbe ?: boolean ;
9203+ } = { } ,
9204+ ) : Promise < TerminalRunResolution | null > {
9205+ const { candidate, fallbackStatus, allowFallbackOnActiveProbe = false } = options ;
9206+ const candidateStatus = candidate ? asTerminalRunStatus ( candidate . status ) : null ;
9207+ if ( candidate && candidateStatus ) {
9208+ return {
9209+ status : candidateStatus ,
9210+ endedAt : candidate . updatedAt ,
9211+ authoritative : true ,
9212+ ...( candidate . resumable !== undefined ? { resumable : candidate . resumable } : { } ) ,
9213+ } ;
9214+ }
9215+ let probed : RunStatusSnapshot | null = null ;
9216+ try {
9217+ probed = await fetchChatRunStatus ( runId ) ;
9218+ } catch {
9219+ probed = null ;
9220+ }
9221+ const probedStatus = probed ? asTerminalRunStatus ( probed . status ) : null ;
9222+ if ( probed && probedStatus ) {
9223+ return {
9224+ status : probedStatus ,
9225+ endedAt : probed . updatedAt ,
9226+ authoritative : true ,
9227+ ...( probed . resumable !== undefined ? { resumable : probed . resumable } : { } ) ,
9228+ } ;
9229+ }
9230+ if ( ! fallbackStatus ) return null ;
9231+ if ( ! probed || allowFallbackOnActiveProbe ) {
9232+ return {
9233+ status : fallbackStatus ,
9234+ endedAt : Date . now ( ) ,
9235+ authoritative : false ,
9236+ } ;
9237+ }
9238+ return null ;
9239+ }
90589240
90599241/**
90609242 * Resolves the authoritative `endedAt` for a terminal-recovery branch.
@@ -9080,14 +9262,8 @@ async function resolveTerminalEndedAt(
90809262 runId : string ,
90819263 candidate : RunStatusSnapshot | null | undefined ,
90829264) : Promise < number > {
9083- if ( candidate && ! isActiveRunStatus ( candidate . status ) ) {
9084- return candidate . updatedAt ;
9085- }
9086- const probed = await fetchChatRunStatus ( runId ) . catch ( ( ) => null ) ;
9087- if ( probed && ! isActiveRunStatus ( probed . status ) ) {
9088- return probed . updatedAt ;
9089- }
9090- return Date . now ( ) ;
9265+ const resolved = await resolveDaemonTerminalRunCompletion ( runId , { candidate } ) ;
9266+ return resolved ?. endedAt ?? Date . now ( ) ;
90919267}
90929268
90939269function isProgrammaticBrandExtractionStatusMessage (
0 commit comments