Skip to content

Commit 256a198

Browse files
committed
fix(web): sync daemon run completion with authoritative status\n\nGenerated-By: looper 0.0.0-dev (runner=worker, agent=codex)
1 parent dbc7066 commit 256a198

2 files changed

Lines changed: 423 additions & 34 deletions

File tree

apps/web/src/components/ProjectView.tsx

Lines changed: 209 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -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`. */
90579180
type 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

90939269
function isProgrammaticBrandExtractionStatusMessage(

0 commit comments

Comments
 (0)