From 88b3ea2358b3976aaafd378bb38f722301ac539f Mon Sep 17 00:00:00 2001 From: 777genius Date: Fri, 8 May 2026 21:49:29 +0300 Subject: [PATCH] chore(team): checkpoint follow-up frontend edits --- agent-teams-controller/src/internal/review.js | 33 ++++++++++++++----- .../team/TeamTaskActivityIntervalService.ts | 23 ++++++++++++- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/agent-teams-controller/src/internal/review.js b/agent-teams-controller/src/internal/review.js index 4ee960fd..cf0a069c 100644 --- a/agent-teams-controller/src/internal/review.js +++ b/agent-teams-controller/src/internal/review.js @@ -66,19 +66,36 @@ function normalizeActorKey(value) { return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : ''; } +function closeTimestampForInterval(interval, timestamp) { + const startedAtMs = Date.parse(interval.startedAt); + const timestampMs = Date.parse(timestamp); + if (Number.isFinite(startedAtMs) && Number.isFinite(timestampMs) && timestampMs < startedAtMs) { + return interval.startedAt; + } + return timestamp; +} + function openReviewInterval(task, reviewer, timestamp = new Date().toISOString()) { const reviewerName = typeof reviewer === 'string' && reviewer.trim() ? reviewer.trim() : ''; if (!reviewerName) return false; const reviewerKey = normalizeActorKey(reviewerName); const intervals = Array.isArray(task.reviewIntervals) ? [...task.reviewIntervals] : []; - const hasOpenForReviewer = intervals.some( - (interval) => !interval.completedAt && normalizeActorKey(interval.reviewer) === reviewerKey - ); + let changed = false; + let hasOpenForReviewer = false; + const nextIntervals = intervals.map((interval) => { + if (interval.completedAt) return interval; + if (normalizeActorKey(interval.reviewer) === reviewerKey) { + hasOpenForReviewer = true; + return interval; + } + changed = true; + return { ...interval, completedAt: closeTimestampForInterval(interval, timestamp) }; + }); if (hasOpenForReviewer) { - task.reviewIntervals = intervals; - return false; + task.reviewIntervals = nextIntervals; + return changed; } - task.reviewIntervals = [...intervals, { reviewer: reviewerName, startedAt: timestamp }]; + task.reviewIntervals = [...nextIntervals, { reviewer: reviewerName, startedAt: timestamp }]; return true; } @@ -88,7 +105,7 @@ function closeReviewIntervals(task, timestamp = new Date().toISOString()) { task.reviewIntervals = task.reviewIntervals.map((interval) => { if (interval.completedAt) return interval; changed = true; - return { ...interval, completedAt: timestamp }; + return { ...interval, completedAt: closeTimestampForInterval(interval, timestamp) }; }); return changed; } @@ -315,7 +332,7 @@ function startReview(context, taskId, flags = {}) { }); } else { tasks.updateTask(context, task.id, (t) => { - openReviewInterval(t, existingActor); + openReviewInterval(t, existingActor, latestReviewEvent.timestamp); return t; }); } diff --git a/src/main/services/team/TeamTaskActivityIntervalService.ts b/src/main/services/team/TeamTaskActivityIntervalService.ts index 70f937ad..0d1d1c90 100644 --- a/src/main/services/team/TeamTaskActivityIntervalService.ts +++ b/src/main/services/team/TeamTaskActivityIntervalService.ts @@ -1,7 +1,9 @@ -import { getTasksBasePath } from '@main/utils/pathDecoder'; +import { getTasksBasePath, getTeamsBasePath } from '@main/utils/pathDecoder'; +import { createLogger } from '@shared/utils/logger'; import * as fs from 'fs'; import * as path from 'path'; +import { withFileLockSync } from './fileLock'; import { TeamTaskReader } from './TeamTaskReader'; import type { @@ -20,6 +22,7 @@ type MutableTeamTask = TeamTask & { }; const CRASH_REPAIR_GRACE_MS = 5_000; +const logger = createLogger('Service:TeamTaskActivityIntervalService'); function normalizeMemberName(value: string | null | undefined): string { return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : ''; @@ -142,6 +145,23 @@ export class TeamTaskActivityIntervalService { private mutateTeamTasks( teamName: string, mutate: (task: MutableTeamTask) => boolean + ): ActivityIntervalResult { + const lockScope = path.join(getTeamsBasePath(), teamName, 'board-state'); + try { + return withFileLockSync(lockScope, () => this.mutateTeamTasksUnlocked(teamName, mutate)); + } catch (error) { + logger.warn( + `[${teamName}] Failed to update task activity intervals: ${ + error instanceof Error ? error.message : String(error) + }` + ); + return { changedTasks: 0 }; + } + } + + private mutateTeamTasksUnlocked( + teamName: string, + mutate: (task: MutableTeamTask) => boolean ): ActivityIntervalResult { const tasksDir = path.join(getTasksBasePath(), teamName); let entries: string[]; @@ -216,6 +236,7 @@ export class TeamTaskActivityIntervalService { const activeReviewer = getActiveReviewActor(task); if ( + task.status === 'completed' && activeReviewer && normalizeMemberName(activeReviewer) === memberKey && !hasOpenReviewInterval(task, activeReviewer)