fix(chat): heal task bubble pending after completed run

This commit is contained in:
weichen
2026-09-26 04:30:38 +00:00
parent a5ca6fdcfd
commit ccce44dfc0
37 changed files with 1164 additions and 202 deletions
+83 -72
View File
@@ -573,6 +573,82 @@ export function useTaskRunAttempts(input: TaskRunAttemptsInput) {
}
};
const settleFromRemoteIfTerminal = async (runId: string): Promise<boolean> => {
if (settled || controller.signal.aborted) return settled;
const remote = await getTaskRun(runId).catch(() => null);
if (!remote) return false;
const view = readRemoteTaskRun(remote);
if (view.status === 'quality_failed') {
settled = true;
patchAsst({
status: 'quality_failed',
livePhaseLabel: undefined,
taskWorkspaceId: view.workspaceId,
taskRunId: runId,
});
taskRunStream.setThinking(sessionId, false);
return true;
}
if (isBlockedGovernanceStatus(view.status)) {
settled = true;
patchAsst({
status: 'blocked_pending_governance',
content: GOVERNANCE_BLOCKED_HINT,
livePhaseLabel: undefined,
taskWorkspaceId: view.workspaceId,
taskRunId: runId,
});
taskRunStream.setThinking(sessionId, false);
return true;
}
if (view.status === 'completed' || view.status === 'pending_author_confirmation') {
settled = true;
const mapped = mapTaskStatusToMessageStatus(view.status, 'assistant') || 'completed';
patchAsst({
status: mapped,
livePhaseLabel: undefined,
taskWorkspaceId: view.workspaceId,
taskRunId: runId,
});
taskRunStream.setThinking(sessionId, false);
await syncControlledDocsAfterTask(task.taskKey || 'intake');
await chatWindow.refresh().catch(() => undefined);
return true;
}
if (view.status === 'failed' || view.status === 'finalizing_failed') {
failOrRetry(buildFailedTaskErrorInfo(view.errorMessage));
return true;
}
if (view.status === 'interrupted') {
settled = true;
patchAsst({
status: 'failed',
livePhaseLabel: undefined,
taskWorkspaceId: view.workspaceId,
taskRunId: runId,
errorInfo: buildInterruptedTaskErrorInfo(view.errorMessage, view.workspaceId),
});
taskRunStream.setThinking(sessionId, false);
return true;
}
if (view.status === 'cancelled') {
failOrRetry(buildCancelledTaskErrorInfo());
return true;
}
return false;
};
const subscribeWithGapHeal = async (runId: string) => {
await subscribeTaskRunEvents(
runId,
{
onEvent,
onGap: async () => settleFromRemoteIfTerminal(runId),
},
controller.signal,
);
};
const startObserver = async () => {
if (mode === 'resume') {
const runId = opts?.reconnectRunId?.trim();
@@ -624,7 +700,7 @@ export function useTaskRunAttempts(input: TaskRunAttemptsInput) {
taskWorkspaceId: resumed.workspace_id || workspaceId,
livePhaseLabel: '正在继续任务…',
});
await subscribeTaskRunEvents(retainedRunId, { onEvent }, controller.signal);
await subscribeWithGapHeal(retainedRunId);
return;
}
@@ -643,7 +719,7 @@ export function useTaskRunAttempts(input: TaskRunAttemptsInput) {
retainedRunId = existingRunId;
taskRunStream.setRunId(sessionId, existingRunId);
patchAsst({ taskRunId: existingRunId, livePhaseLabel: '正在重连任务…' });
await subscribeTaskRunEvents(existingRunId, { onEvent }, controller.signal);
await subscribeWithGapHeal(existingRunId);
return;
}
if (remote && isRemoteTaskTerminal(view.status)) {
@@ -700,7 +776,7 @@ export function useTaskRunAttempts(input: TaskRunAttemptsInput) {
await chatWindow.refresh().catch(() => undefined);
return;
}
await subscribeTaskRunEvents(created.run_id, { onEvent }, controller.signal);
await subscribeWithGapHeal(created.run_id);
return;
}
@@ -717,82 +793,17 @@ export function useTaskRunAttempts(input: TaskRunAttemptsInput) {
const runId = retainedRunId || taskRunStream.activeTaskRunId || undefined;
if (runId) {
try {
if (await settleFromRemoteIfTerminal(runId)) {
return;
}
const remote = await getTaskRun(runId);
const view = readRemoteTaskRun(remote);
if (view.status === 'quality_failed') {
settled = true;
patchAsst({
status: 'quality_failed',
livePhaseLabel: undefined,
taskWorkspaceId: view.workspaceId,
taskRunId: runId,
});
taskRunStream.setThinking(sessionId, false);
return;
}
if (isBlockedGovernanceStatus(view.status)) {
settled = true;
patchAsst({
status: 'blocked_pending_governance',
content: GOVERNANCE_BLOCKED_HINT,
livePhaseLabel: undefined,
taskWorkspaceId: view.workspaceId,
taskRunId: runId,
});
taskRunStream.setThinking(sessionId, false);
return;
}
if (view.status === 'completed') {
settled = true;
patchAsst({
status: 'completed',
livePhaseLabel: undefined,
taskWorkspaceId: view.workspaceId,
taskRunId: runId,
});
taskRunStream.setThinking(sessionId, false);
await syncControlledDocsAfterTask(task.taskKey || 'intake');
return;
}
if (view.status === 'pending_author_confirmation') {
settled = true;
patchAsst({
status: 'pending_author_confirmation',
livePhaseLabel: undefined,
taskWorkspaceId: view.workspaceId,
taskRunId: runId,
});
taskRunStream.setThinking(sessionId, false);
await syncControlledDocsAfterTask(task.taskKey || 'intake');
await chatWindow.refresh().catch(() => undefined);
return;
}
if (shouldSubscribeExistingRun(view.status)) {
// 原 run 仍在跑:重订阅,不 POST 新 run
settled = false;
retainedRunId = runId;
patchAsst({ livePhaseLabel: '正在重连任务…', taskRunId: runId });
await subscribeTaskRunEvents(runId, { onEvent }, controller.signal);
return;
}
if (view.status === 'failed' || view.status === 'finalizing_failed') {
failOrRetry(buildFailedTaskErrorInfo(view.errorMessage));
return;
}
if (view.status === 'interrupted') {
settled = true;
patchAsst({
status: 'failed',
livePhaseLabel: undefined,
taskWorkspaceId: view.workspaceId,
taskRunId: runId,
errorInfo: buildInterruptedTaskErrorInfo(view.errorMessage, view.workspaceId),
});
taskRunStream.setThinking(sessionId, false);
return;
}
if (view.status === 'cancelled') {
failOrRetry(buildCancelledTaskErrorInfo());
await subscribeWithGapHeal(runId);
return;
}
} catch {