agent-ecosystem/src/main/services/team/TeamTaskActivityIntervalService.ts
777genius f79ea145d7 perf: batch per-member task-interval resume into one locked pass
During launch the live-status loop resumes every alive member every audit cycle.
resumeActiveIntervalsForMember runs a synchronous file-lock + full read of every
task file, so for an N-member team with M task files it did N locked passes x M
readFileSync per cycle (e.g. 6 members x 20 task files), blocking the main event
loop. Profiling a 6-member mixed launch showed mutateTeamTasks/withFileLockSync as
a top main-thread cost (~14%).

Add resumeActiveIntervalsForMembers that applies the identical per-member resume
logic against a member set in a single locked pass, and use it in the live-status
loop. Same mutations, but one lock + task read per cycle instead of one per member.
Adds a test covering multi-member resume in one pass.
2026-05-30 10:02:01 +03:00

571 lines
19 KiB
TypeScript

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 {
PersistedTeamLaunchMemberState,
PersistedTeamLaunchSnapshot,
TaskReviewInterval,
TeamTask,
} from '@shared/types';
interface ActivityIntervalResult {
changedTasks: number;
failed?: boolean;
}
type MutableTeamTask = TeamTask & {
reviewIntervals?: TaskReviewInterval[];
};
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() : '';
}
function parseIsoMs(value: string | null | undefined): number {
if (!value) return 0;
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : 0;
}
function toIso(ms: number): string {
return new Date(ms).toISOString();
}
function isClosedInterval(interval: { completedAt?: unknown } | null | undefined): boolean {
return typeof interval?.completedAt === 'string' && parseIsoMs(interval.completedAt) > 0;
}
function hasValidStartedAt(interval: { startedAt?: unknown } | null | undefined): boolean {
return typeof interval?.startedAt === 'string' && parseIsoMs(interval.startedAt) > 0;
}
function ensureCloseIso(startedAt: string, at: string): string {
const startedAtMs = parseIsoMs(startedAt);
const atMs = parseIsoMs(at);
if (startedAtMs <= 0) return at;
if (atMs <= startedAtMs) return toIso(startedAtMs);
return toIso(atMs);
}
function resumeStartIso(activeStartedAt: string | null | undefined, at: string): string {
const activeStartedAtMs = parseIsoMs(activeStartedAt ?? undefined);
const atMs = parseIsoMs(at);
if (activeStartedAtMs > 0 && activeStartedAtMs > atMs) {
return toIso(activeStartedAtMs);
}
return atMs > 0 ? toIso(atMs) : toIso(Date.now());
}
function getStartedAtString(interval: { startedAt?: unknown } | null | undefined): string {
return typeof interval?.startedAt === 'string' ? interval.startedAt : '';
}
function hasUsableCompletedAt(interval: { completedAt?: unknown } | null | undefined): boolean {
return interval?.completedAt === undefined || isClosedInterval(interval);
}
function pauseCloseIso(
interval: { startedAt?: unknown; completedAt?: unknown } | null | undefined,
at: string
): string {
const startedAt = getStartedAtString(interval);
const closeAt = interval?.completedAt === undefined ? at : startedAt || at;
return ensureCloseIso(startedAt, closeAt);
}
function crashRepairCloseIso(startedAt: string, member?: PersistedTeamLaunchMemberState): string {
const startedAtMs = parseIsoMs(startedAt);
const safeStartedAtMs = startedAtMs > 0 ? startedAtMs : Date.now();
const evidenceMs = Math.max(
parseIsoMs(member?.lastHeartbeatAt),
parseIsoMs(member?.runtimeLastSeenAt),
parseIsoMs(member?.lastRuntimeAliveAt)
);
const closeMs =
evidenceMs > 0
? Math.max(safeStartedAtMs, evidenceMs + CRASH_REPAIR_GRACE_MS)
: safeStartedAtMs + CRASH_REPAIR_GRACE_MS;
const boundedCloseMs = Math.max(safeStartedAtMs, Math.min(Date.now(), closeMs));
return toIso(boundedCloseMs);
}
function crashRepairIntervalCloseIso(
interval: { startedAt?: unknown; completedAt?: unknown } | null | undefined,
member?: PersistedTeamLaunchMemberState
): string {
const startedAt = getStartedAtString(interval);
if (interval?.completedAt === undefined) {
return crashRepairCloseIso(startedAt, member);
}
return ensureCloseIso(startedAt, startedAt || crashRepairCloseIso(startedAt, member));
}
function hasOpenWorkInterval(task: MutableTeamTask): boolean {
return (
Array.isArray(task.workIntervals) &&
task.workIntervals.some(
(interval) => hasValidStartedAt(interval) && interval.completedAt === undefined
)
);
}
function hasOpenReviewInterval(task: MutableTeamTask, reviewer: string): boolean {
const reviewerKey = normalizeMemberName(reviewer);
return (
Array.isArray(task.reviewIntervals) &&
task.reviewIntervals.some(
(interval) =>
hasValidStartedAt(interval) &&
interval.completedAt === undefined &&
normalizeMemberName(interval.reviewer) === reviewerKey
)
);
}
function closeOpenWorkIntervals(task: MutableTeamTask, at: string, owner?: string): boolean {
if (!Array.isArray(task.workIntervals)) return false;
if (owner && normalizeMemberName(task.owner) !== normalizeMemberName(owner)) return false;
let changed = false;
task.workIntervals = task.workIntervals.map((interval) => {
if (isClosedInterval(interval)) return interval;
changed = true;
return { ...interval, completedAt: pauseCloseIso(interval, at) };
});
return changed;
}
function closeOpenReviewIntervals(task: MutableTeamTask, at: string, reviewer?: string): boolean {
if (!Array.isArray(task.reviewIntervals)) return false;
const reviewerKey = normalizeMemberName(reviewer);
let changed = false;
task.reviewIntervals = task.reviewIntervals.map((interval) => {
if (isClosedInterval(interval)) return interval;
if (reviewerKey && normalizeMemberName(interval.reviewer) !== reviewerKey) return interval;
changed = true;
return { ...interval, completedAt: pauseCloseIso(interval, at) };
});
return changed;
}
function getActiveWorkStartedAt(task: MutableTeamTask): string | null {
const events = Array.isArray(task.historyEvents) ? task.historyEvents : [];
for (let index = events.length - 1; index >= 0; index -= 1) {
const event = events[index];
if (event.type === 'status_changed') {
if (event.to === 'in_progress') {
return parseIsoMs(event.timestamp) > 0 ? event.timestamp : null;
}
return null;
}
if (event.type === 'task_created') {
return event.status === 'in_progress' && parseIsoMs(event.timestamp) > 0
? event.timestamp
: null;
}
}
return null;
}
function getActiveReviewStart(
task: MutableTeamTask
): { reviewer: string; startedAt: string } | null {
const events = Array.isArray(task.historyEvents) ? task.historyEvents : [];
for (let index = events.length - 1; index >= 0; index -= 1) {
const event = events[index];
if (event.type === 'review_started') {
const reviewer =
typeof event.actor === 'string' && event.actor.trim() ? event.actor.trim() : '';
return reviewer && parseIsoMs(event.timestamp) > 0
? { reviewer, startedAt: event.timestamp }
: null;
}
if (
event.type === 'review_approved' ||
event.type === 'review_changes_requested' ||
(event.type === 'status_changed' &&
(event.to === 'in_progress' || event.to === 'pending' || event.to === 'deleted')) ||
event.type === 'task_created'
) {
return null;
}
}
return null;
}
function hasWorkIntervalForStart(task: MutableTeamTask, startedAt: string): boolean {
const startedAtMs = parseIsoMs(startedAt);
return (
startedAtMs > 0 &&
Array.isArray(task.workIntervals) &&
task.workIntervals.some((interval) => parseIsoMs(interval.startedAt) === startedAtMs)
);
}
function hasPersistedWorkIntervalAtOrAfter(task: MutableTeamTask, startedAt: string): boolean {
const startedAtMs = parseIsoMs(startedAt);
return (
startedAtMs > 0 &&
Array.isArray(task.workIntervals) &&
task.workIntervals.some(
(interval) =>
hasValidStartedAt(interval) &&
hasUsableCompletedAt(interval) &&
parseIsoMs(interval.startedAt) >= startedAtMs
)
);
}
function hasReviewIntervalForStart(
task: MutableTeamTask,
reviewer: string,
startedAt: string
): boolean {
const reviewerKey = normalizeMemberName(reviewer);
const startedAtMs = parseIsoMs(startedAt);
return (
reviewerKey.length > 0 &&
startedAtMs > 0 &&
Array.isArray(task.reviewIntervals) &&
task.reviewIntervals.some(
(interval) =>
normalizeMemberName(interval.reviewer) === reviewerKey &&
parseIsoMs(interval.startedAt) === startedAtMs
)
);
}
function hasPersistedReviewIntervalAtOrAfter(task: MutableTeamTask, startedAt: string): boolean {
const startedAtMs = parseIsoMs(startedAt);
return (
startedAtMs > 0 &&
Array.isArray(task.reviewIntervals) &&
task.reviewIntervals.some(
(interval) =>
normalizeMemberName(interval?.reviewer).length > 0 &&
hasValidStartedAt(interval) &&
hasUsableCompletedAt(interval) &&
parseIsoMs(interval.startedAt) >= startedAtMs
)
);
}
function materializePausedWorkInterval(task: MutableTeamTask, at: string, owner?: string): boolean {
if (task.status !== 'in_progress') return false;
if (owner && normalizeMemberName(task.owner) !== normalizeMemberName(owner)) return false;
const startedAt = getActiveWorkStartedAt(task);
if (
!startedAt ||
hasPersistedWorkIntervalAtOrAfter(task, startedAt) ||
hasWorkIntervalForStart(task, startedAt)
) {
return false;
}
task.workIntervals = [
...(Array.isArray(task.workIntervals) ? task.workIntervals : []),
{ startedAt, completedAt: ensureCloseIso(startedAt, at) },
];
return true;
}
function materializePausedReviewInterval(
task: MutableTeamTask,
at: string,
reviewer?: string
): boolean {
if (task.status !== 'completed') return false;
const activeReview = getActiveReviewStart(task);
if (!activeReview) return false;
if (reviewer && normalizeMemberName(activeReview.reviewer) !== normalizeMemberName(reviewer)) {
return false;
}
if (
hasPersistedReviewIntervalAtOrAfter(task, activeReview.startedAt) ||
hasReviewIntervalForStart(task, activeReview.reviewer, activeReview.startedAt)
) {
return false;
}
task.reviewIntervals = [
...(Array.isArray(task.reviewIntervals) ? task.reviewIntervals : []),
{
reviewer: activeReview.reviewer,
startedAt: activeReview.startedAt,
completedAt: ensureCloseIso(activeReview.startedAt, at),
},
];
return true;
}
function readTaskFile(filePath: string): MutableTeamTask | null {
try {
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown;
return parsed && typeof parsed === 'object' ? (parsed as MutableTeamTask) : null;
} catch {
return null;
}
}
function writeTaskFile(filePath: string, task: MutableTeamTask): void {
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
fs.writeFileSync(tempPath, JSON.stringify(task, null, 2));
fs.renameSync(tempPath, filePath);
}
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, failed: true };
}
}
private mutateTeamTasksUnlocked(
teamName: string,
mutate: (task: MutableTeamTask) => boolean
): ActivityIntervalResult {
const tasksDir = path.join(getTasksBasePath(), teamName);
let entries: string[];
try {
entries = fs.readdirSync(tasksDir);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return { changedTasks: 0 };
}
throw error;
}
let changedTasks = 0;
for (const fileName of entries) {
if (!fileName.endsWith('.json') || fileName.startsWith('.')) continue;
const filePath = path.join(tasksDir, fileName);
const task = readTaskFile(filePath);
if (!task) continue;
if (!mutate(task)) continue;
writeTaskFile(filePath, task);
changedTasks += 1;
}
if (changedTasks > 0) {
TeamTaskReader.invalidateAllTasksCache();
}
return { changedTasks };
}
pauseActiveIntervalsForTeam(
teamName: string,
at = new Date().toISOString()
): ActivityIntervalResult {
return this.mutateTeamTasks(teamName, (task) => {
const changedWork = closeOpenWorkIntervals(task, at);
const changedReview = closeOpenReviewIntervals(task, at);
const materializedWork = materializePausedWorkInterval(task, at);
const materializedReview = materializePausedReviewInterval(task, at);
return changedWork || changedReview || materializedWork || materializedReview;
});
}
pauseActiveIntervalsForMember(
teamName: string,
memberName: string,
at = new Date().toISOString()
): ActivityIntervalResult {
return this.mutateTeamTasks(teamName, (task) => {
const changedWork = closeOpenWorkIntervals(task, at, memberName);
const changedReview = closeOpenReviewIntervals(task, at, memberName);
const materializedWork = materializePausedWorkInterval(task, at, memberName);
const materializedReview = materializePausedReviewInterval(task, at, memberName);
return changedWork || changedReview || materializedWork || materializedReview;
});
}
resumeActiveIntervalsForMember(
teamName: string,
memberName: string,
at = new Date().toISOString()
): ActivityIntervalResult {
const memberKey = normalizeMemberName(memberName);
if (!memberKey) return { changedTasks: 0 };
return this.mutateTeamTasks(teamName, (task) => {
let changed = false;
if (
task.status === 'in_progress' &&
normalizeMemberName(task.owner) === memberKey &&
!hasOpenWorkInterval(task)
) {
const activeStartedAt = getActiveWorkStartedAt(task);
task.workIntervals = [
...(Array.isArray(task.workIntervals) ? task.workIntervals : []),
{ startedAt: resumeStartIso(activeStartedAt, at) },
];
changed = true;
}
const activeReview = getActiveReviewStart(task);
if (
task.status === 'completed' &&
activeReview &&
normalizeMemberName(activeReview.reviewer) === memberKey &&
!hasOpenReviewInterval(task, activeReview.reviewer)
) {
task.reviewIntervals = [
...(Array.isArray(task.reviewIntervals) ? task.reviewIntervals : []),
{
reviewer: activeReview.reviewer,
startedAt: resumeStartIso(activeReview.startedAt, at),
},
];
changed = true;
}
return changed;
});
}
/**
* Batched equivalent of resumeActiveIntervalsForMember for several members in a
* single task-file pass. During launch the live-status loop resumes every alive
* member every audit cycle; doing that per member meant one synchronous
* file-lock + read of every task file PER member PER cycle. This applies the
* identical per-member resume logic against a member set in one locked pass, so
* the mutations are exactly the same but the lock + reads happen once per cycle
* instead of once per member.
*/
resumeActiveIntervalsForMembers(
teamName: string,
memberNames: readonly string[],
at = new Date().toISOString()
): ActivityIntervalResult {
const memberKeys = new Set(
memberNames.map((name) => normalizeMemberName(name)).filter((key): key is string => !!key)
);
if (memberKeys.size === 0) return { changedTasks: 0 };
return this.mutateTeamTasks(teamName, (task) => {
let changed = false;
if (
task.status === 'in_progress' &&
memberKeys.has(normalizeMemberName(task.owner)) &&
!hasOpenWorkInterval(task)
) {
const activeStartedAt = getActiveWorkStartedAt(task);
task.workIntervals = [
...(Array.isArray(task.workIntervals) ? task.workIntervals : []),
{ startedAt: resumeStartIso(activeStartedAt, at) },
];
changed = true;
}
const activeReview = getActiveReviewStart(task);
if (
task.status === 'completed' &&
activeReview &&
memberKeys.has(normalizeMemberName(activeReview.reviewer)) &&
!hasOpenReviewInterval(task, activeReview.reviewer)
) {
task.reviewIntervals = [
...(Array.isArray(task.reviewIntervals) ? task.reviewIntervals : []),
{
reviewer: activeReview.reviewer,
startedAt: resumeStartIso(activeReview.startedAt, at),
},
];
changed = true;
}
return changed;
});
}
repairStaleIntervalsAfterCrash(
teamName: string,
launchSnapshot?: PersistedTeamLaunchSnapshot | null
): ActivityIntervalResult {
const memberByName = new Map<string, PersistedTeamLaunchMemberState>();
for (const member of Object.values(launchSnapshot?.members ?? {})) {
memberByName.set(normalizeMemberName(member.name), member);
}
return this.mutateTeamTasks(teamName, (task) => {
let changed = false;
if (Array.isArray(task.workIntervals)) {
const ownerMember = memberByName.get(normalizeMemberName(task.owner));
task.workIntervals = task.workIntervals.map((interval) => {
if (isClosedInterval(interval)) return interval;
changed = true;
return { ...interval, completedAt: crashRepairIntervalCloseIso(interval, ownerMember) };
});
}
if (task.status === 'in_progress') {
const ownerMember = memberByName.get(normalizeMemberName(task.owner));
const startedAt = getActiveWorkStartedAt(task);
if (
startedAt &&
!hasPersistedWorkIntervalAtOrAfter(task, startedAt) &&
!hasWorkIntervalForStart(task, startedAt)
) {
task.workIntervals = [
...(Array.isArray(task.workIntervals) ? task.workIntervals : []),
{ startedAt, completedAt: crashRepairCloseIso(startedAt, ownerMember) },
];
changed = true;
}
}
if (Array.isArray(task.reviewIntervals)) {
task.reviewIntervals = task.reviewIntervals.map((interval) => {
if (isClosedInterval(interval)) return interval;
const reviewerMember = memberByName.get(normalizeMemberName(interval.reviewer));
changed = true;
return {
...interval,
completedAt: crashRepairIntervalCloseIso(interval, reviewerMember),
};
});
}
if (task.status === 'completed') {
const activeReview = getActiveReviewStart(task);
if (
activeReview &&
!hasPersistedReviewIntervalAtOrAfter(task, activeReview.startedAt) &&
!hasReviewIntervalForStart(task, activeReview.reviewer, activeReview.startedAt)
) {
const reviewerMember = memberByName.get(normalizeMemberName(activeReview.reviewer));
task.reviewIntervals = [
...(Array.isArray(task.reviewIntervals) ? task.reviewIntervals : []),
{
reviewer: activeReview.reviewer,
startedAt: activeReview.startedAt,
completedAt: crashRepairCloseIso(activeReview.startedAt, reviewerMember),
},
];
changed = true;
}
}
return changed;
});
}
}