fix: dedupe task timeline milestones

This commit is contained in:
2026-05-26 11:59:49 +08:00
parent a1164dc49a
commit 2e4f8541ee
2 changed files with 67 additions and 3 deletions

View File

@ -221,4 +221,52 @@ describe('buildTaskTimelineCards', () => {
expect(cards.map((card) => card.id)).toEqual(['task-1:created', 'evt-invalid-date']);
});
it('dedupes synthetic result and acceptance milestones when lifecycle events exist', () => {
const task = makeTask({
is_open: false,
status: 'closed',
updated_at: '2026-05-26T10:04:00.000Z',
closed_at: '2026-05-26T10:04:00.000Z',
feedback: [
{
acceptance_type: 'accept',
comment: '可以',
created_at: '2026-05-26T10:05:00.000Z',
run_id: 'run-main',
},
],
});
const processEvents: ProcessEvent[] = [
{
event_id: 'evt-result-ready',
run_id: 'run-main',
parent_run_id: null,
kind: 'task_result_ready',
actor_type: 'agent',
actor_id: 'main-agent',
actor_name: 'Main Agent',
text: 'Result is ready.',
created_at: '2026-05-26T10:04:00.000Z',
},
{
event_id: 'evt-acceptance-recorded',
run_id: 'run-main',
parent_run_id: null,
kind: 'task_acceptance_recorded',
actor_type: 'system',
actor_id: 'task-system',
actor_name: 'Task System',
text: '可以',
created_at: '2026-05-26T10:05:00.000Z',
},
];
const cards = buildTaskTimelineCards({ task, processEvents });
expect(cards.filter((card) => card.type === 'result')).toHaveLength(1);
expect(cards.filter((card) => card.type === 'acceptance')).toHaveLength(1);
expect(cards.map((card) => card.id)).toContain('evt-result-ready');
expect(cards.map((card) => card.id)).toContain('evt-acceptance-recorded');
});
});

View File

@ -209,6 +209,10 @@ function compareCardsByCreatedAt(
return aTime - bTime || a.index - b.index;
}
function acceptanceKey(runId: string | null | undefined, createdAt: string): string {
return `${runId ?? ''}:${createdAt}`;
}
export function buildTaskTimelineCards(input: BuildTaskTimelineCardsInput): TaskTimelineCard[] {
const { task } = input;
const processRuns = input.processRuns ?? task.process_runs ?? [];
@ -216,6 +220,8 @@ export function buildTaskTimelineCards(input: BuildTaskTimelineCardsInput): Task
const processArtifacts = input.processArtifacts ?? task.process_artifacts ?? [];
const runsById = buildRunMap(processRuns);
const runsWithProgressEvents = new Set<string>();
const acceptanceEventKeys = new Set<string>();
let hasResultEventCard = false;
const cards: TaskTimelineCard[] = [
{
id: `${task.task_id}:created`,
@ -236,6 +242,12 @@ export function buildTaskTimelineCards(input: BuildTaskTimelineCardsInput): Task
if (type === 'agent_progress') {
runsWithProgressEvents.add(event.run_id);
}
if (type === 'result') {
hasResultEventCard = true;
}
if (type === 'acceptance') {
acceptanceEventKeys.add(acceptanceKey(event.run_id, event.created_at));
}
cards.push({
id: event.event_id,
@ -292,7 +304,7 @@ export function buildTaskTimelineCards(input: BuildTaskTimelineCardsInput): Task
});
}
if (RESULT_STATUSES.has(task.status)) {
if (RESULT_STATUSES.has(task.status) && !hasResultEventCard) {
cards.push({
id: `${task.task_id}:result`,
taskId: task.task_id,
@ -308,15 +320,19 @@ export function buildTaskTimelineCards(input: BuildTaskTimelineCardsInput): Task
for (let index = 0; index < task.feedback.length; index += 1) {
const feedback = task.feedback[index];
const runId = firstString(feedback.run_id) ?? null;
const createdAt = feedbackCreatedAt(feedback, task);
if (acceptanceEventKeys.has(acceptanceKey(runId, createdAt))) continue;
cards.push({
id: `${task.task_id}:acceptance:${index}`,
taskId: task.task_id,
runId: firstString(feedback.run_id) ?? null,
runId,
type: 'acceptance',
title: titleForCard('acceptance'),
summary: feedbackSummary(feedback),
status: firstString(feedback.acceptance_type),
createdAt: feedbackCreatedAt(feedback, task),
createdAt,
details: feedback,
});
}